From 73e1621c482bd49f0c34c18a06cd2c062be9961e Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 26 Aug 2019 11:18:10 -0500
Subject: [PATCH 001/174] Update for amphp/socket 1.0

---
 composer.json              |  9 ++-------
 src/Connection.php         | 18 ++++--------------
 src/ConnectionConfig.php   | 29 +++++++++++------------------
 src/Internal/Processor.php | 14 +++++---------
 src/TimeoutConnector.php   | 13 ++++++++-----
 5 files changed, 30 insertions(+), 53 deletions(-)

diff --git a/composer.json b/composer.json
index cf31cef..fc1c1a2 100644
--- a/composer.json
+++ b/composer.json
@@ -2,10 +2,10 @@
     "name": "amphp/mysql",
     "description": "Asynchronous MySQL client for PHP based on Amp.",
     "require": {
-        "php": "^7.0",
+        "php": "^7.1",
         "amphp/amp": "^2",
         "amphp/file": "^0.3.5",
-        "amphp/socket": "^0.10.9",
+        "amphp/socket": "^1",
         "amphp/sql": "^1",
         "amphp/sql-common": "^1"
     },
@@ -40,11 +40,6 @@
             "Amp\\Mysql\\Bench\\": "benchmarks"
         }
     },
-    "config": {
-        "platform": {
-            "php": "7.0.0"
-        }
-    },
     "scripts": {
         "check": [
             "@cs",
diff --git a/src/Connection.php b/src/Connection.php
index a13f8c8..3219a48 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -4,7 +4,6 @@
 
 use Amp\CancellationToken;
 use Amp\Deferred;
-use Amp\NullCancellationToken;
 use Amp\Promise;
 use Amp\Socket;
 use Amp\Sql\FailureException;
@@ -34,22 +33,13 @@ final class Connection implements Link
     /**
      * @param ConnectionConfig $config
      * @param CancellationToken|null $token
-     * @param Socket\ClientConnectContext|null $context Note that TCP No Delay will be set to on automatically.
      *
      * @return Promise
      */
-    public static function connect(
-        ConnectionConfig $config,
-        CancellationToken $token = null,
-        Socket\ClientConnectContext $context = null
-    ): Promise {
-        $token = $token ?? new NullCancellationToken;
-
-        $context = $context ?? new Socket\ClientConnectContext;
-
-        return call(function () use ($config, $token, $context) {
-            $context = $context->withTcpNoDelay();
-            $socket = yield Socket\connect($config->getConnectionString(), $context, $token);
+    public static function connect(ConnectionConfig $config, ?CancellationToken $token = null): Promise
+    {
+        return call(function () use ($config, $token) {
+            $socket = yield Socket\connect($config->getConnectionString(), $config->getConnectContext(), $token);
 
             $processor = new Internal\Processor($socket, $config);
             yield $processor->connect();
diff --git a/src/ConnectionConfig.php b/src/ConnectionConfig.php
index 4874d22..4c517dc 100644
--- a/src/ConnectionConfig.php
+++ b/src/ConnectionConfig.php
@@ -2,7 +2,7 @@
 
 namespace Amp\Mysql;
 
-use Amp\Socket\ClientTlsContext;
+use Amp\Socket\ConnectContext;
 use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
 
 final class ConnectionConfig extends SqlConnectionConfig
@@ -26,8 +26,8 @@ final class ConnectionConfig extends SqlConnectionConfig
     private $useCompression = false;
     /** @var bool */
     private $useLocalInfile = false;
-    /** @var ClientTlsContext|null Null for no ssl   */
-    private $ssl;
+    /** @var ConnectContext */
+    private $context;
     /** @var string */
     private $charset = "utf8mb4";
     /** @var string  */
@@ -37,7 +37,7 @@ final class ConnectionConfig extends SqlConnectionConfig
     /** @var string|null */
     private $string;
 
-    public static function fromString(string $connectionString, ClientTlsContext $tlsContext = null): self
+    public static function fromString(string $connectionString, ConnectContext $context = null): self
     {
         $parts = self::parseConnectionString($connectionString);
 
@@ -51,7 +51,7 @@ public static function fromString(string $connectionString, ClientTlsContext $tl
             $parts["user"] ?? null,
             $parts["password"] ?? null,
             $parts["db"] ?? null,
-            $tlsContext,
+            $context,
             $parts['charset'] ?? self::DEFAULT_CHARSET,
             self::DEFAULT_COLLATE,
             $parts['compress'] ?? false,
@@ -65,7 +65,7 @@ public function __construct(
         string $user = null,
         string $password = null,
         string $database = null,
-        ClientTlsContext $tlsContext = null,
+        ConnectContext $context = null,
         string $charset = self::DEFAULT_CHARSET,
         string $collate = self::DEFAULT_COLLATE,
         bool $useCompression = false,
@@ -74,7 +74,7 @@ public function __construct(
     ) {
         parent::__construct($host, $port, $user, $password, $database);
 
-        $this->ssl = $tlsContext;
+        $this->context = $context ?? (new ConnectContext);
         $this->charset = $charset;
         $this->collate = $collate;
         $this->useCompression = $useCompression;
@@ -148,22 +148,15 @@ public function withoutLocalInfile(): self
         return $new;
     }
 
-    public function getTlsContext()
+    public function getConnectContext()
     {
-        return $this->ssl;
+        return $this->context;
     }
 
-    public function withTlsContext(ClientTlsContext $context): self
+    public function withConnectContext(ConnectContext $context): self
     {
         $new = clone $this;
-        $new->ssl = $context;
-        return $new;
-    }
-
-    public function withoutTlsContext(): self
-    {
-        $new = clone $this;
-        $new->ssl = null;
+        $new->context = $context;
         return $new;
     }
 
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 5fd1384..fa99a0b 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -11,8 +11,7 @@
 use Amp\Mysql\DataTypes;
 use Amp\Mysql\InitializationException;
 use Amp\Promise;
-use Amp\Socket\ClientTlsContext;
-use Amp\Socket\Socket;
+use Amp\Socket\EncryptableSocket;
 use Amp\Sql\ConnectionException;
 use Amp\Sql\FailureException;
 use Amp\Sql\QueryError;
@@ -66,7 +65,7 @@ class Processor implements TransientResource
     private $seqId = -1;
     private $compressionId = -1;
 
-    /** @var \Amp\Socket\ClientSocket */
+    /** @var EncryptableSocket */
     private $socket;
 
     private $authPluginDataLen;
@@ -138,7 +137,7 @@ class Processor implements TransientResource
     const CLOSING = 3;
     const CLOSED = 4;
 
-    public function __construct(Socket $socket, ConnectionConfig $config)
+    public function __construct(EncryptableSocket $socket, ConnectionConfig $config)
     {
         $this->socket = $socket;
         $this->connInfo = new ConnectionState;
@@ -1431,7 +1430,7 @@ private function sendHandshake($inSSL = false)
             $this->capabilities |= self::CLIENT_CONNECT_WITH_DB;
         }
 
-        if ($this->config->getTlsContext() !== null) {
+        if ($this->config->getConnectContext()->getTlsContext() !== null) {
             $this->capabilities |= self::CLIENT_SSL;
         }
 
@@ -1448,10 +1447,7 @@ private function sendHandshake($inSSL = false)
                 try {
                     yield $this->write($payload);
 
-                    $context = $this->config->getTlsContext() ?: new ClientTlsContext;
-                    $context = $context->withPeerName($this->config->getHost());
-
-                    yield $this->socket->enableCrypto($context);
+                    yield $this->socket->setupTls();
 
                     $this->sendHandshake(true);
                 } catch (\Throwable $e) {
diff --git a/src/TimeoutConnector.php b/src/TimeoutConnector.php
index 74bc38d..1623ca4 100644
--- a/src/TimeoutConnector.php
+++ b/src/TimeoutConnector.php
@@ -3,7 +3,6 @@
 namespace Amp\Mysql;
 
 use Amp\Promise;
-use Amp\Socket\ClientConnectContext;
 use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
 use Amp\Sql\Connector;
 
@@ -11,15 +10,15 @@ final class TimeoutConnector implements Connector
 {
     const DEFAULT_TIMEOUT = 5000;
 
-    /** @var ClientConnectContext */
-    private $context;
+    /** @var int */
+    private $timeout;
 
     /**
      * @param int $timeout Milliseconds until connections attempts are cancelled.
      */
     public function __construct(int $timeout = self::DEFAULT_TIMEOUT)
     {
-        $this->context = (new ClientConnectContext())->withConnectTimeout($timeout);
+        $this->timeout = $timeout;
     }
 
     /**
@@ -33,6 +32,10 @@ public function connect(SqlConnectionConfig $config): Promise
             throw new \TypeError(\sprintf("Must provide an instance of %s to MySQL connectors", ConnectionConfig::class));
         }
 
-        return Connection::connect($config, null, $this->context);
+        $config = $config->withConnectContext(
+            $config->getConnectContext()->withConnectTimeout($this->timeout)
+        );
+
+        return Connection::connect($config);
     }
 }

From 27c6de6a141e089e7823b567df1177a8ec19264b Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 26 Aug 2019 13:48:47 -0500
Subject: [PATCH 002/174] Update with PHP 7.1 features

---
 src/ConnectionConfig.php      | 20 ++++++++++----------
 src/ConnectionStatement.php   |  8 ++++----
 src/ConnectionTransaction.php |  2 +-
 src/DataTypes.php             | 28 ++++++++++++++--------------
 src/Internal/Processor.php    | 34 +++++++++++++++++-----------------
 src/PooledStatement.php       |  2 +-
 src/Statement.php             |  2 +-
 src/StatementPool.php         |  2 +-
 src/functions.php             |  3 +--
 9 files changed, 50 insertions(+), 51 deletions(-)

diff --git a/src/ConnectionConfig.php b/src/ConnectionConfig.php
index 4c517dc..27e4022 100644
--- a/src/ConnectionConfig.php
+++ b/src/ConnectionConfig.php
@@ -41,21 +41,21 @@ public static function fromString(string $connectionString, ConnectContext $cont
     {
         $parts = self::parseConnectionString($connectionString);
 
-        if (!isset($parts["host"])) {
-            throw new \Error("Host must be provided in connection string");
+        if (!isset($parts['host'])) {
+            throw new \Error('Host must be provided in connection string');
         }
 
         return new self(
-            $parts["host"],
-            $parts["port"] ?? self::DEFAULT_PORT,
-            $parts["user"] ?? null,
-            $parts["password"] ?? null,
-            $parts["db"] ?? null,
+            $parts['host'],
+            $parts['port'] ?? self::DEFAULT_PORT,
+            $parts['user'] ?? null,
+            $parts['password'] ?? null,
+            $parts['db'] ?? null,
             $context,
             $parts['charset'] ?? self::DEFAULT_CHARSET,
-            self::DEFAULT_COLLATE,
-            $parts['compress'] ?? false,
-            $parts['local_infile'] ?? false
+            $parts['collate'] ?? self::DEFAULT_COLLATE,
+            ($parts['compression'] ?? '') === 'on',
+            ($parts['local-infile'] ?? '') === 'on'
         );
     }
 
diff --git a/src/ConnectionStatement.php b/src/ConnectionStatement.php
index 9cb0f5b..5ba6cb0 100644
--- a/src/ConnectionStatement.php
+++ b/src/ConnectionStatement.php
@@ -70,7 +70,7 @@ public function isAlive(): bool
     }
 
     /** {@inheritdoc} */
-    public function bind($paramId, $data)
+    public function bind($paramId, $data): void
     {
         if (\is_int($paramId)) {
             if ($paramId >= $this->numParamCount) {
@@ -103,9 +103,9 @@ public function bind($paramId, $data)
         } while (isset($array) && $i = \next($array));
 
         if (isset($this->prebound[$paramId])) {
-            $this->prebound[$paramId] .= $data;
+            $this->prebound[$paramId] .= (string) $data;
         } else {
-            $this->prebound[$paramId] = $data;
+            $this->prebound[$paramId] = (string) $data;
         }
     }
 
@@ -167,7 +167,7 @@ public function getQuery(): string
         return $this->query;
     }
 
-    private function close()
+    private function close(): void
     {
         if ($this->processor === null) {
             return;
diff --git a/src/ConnectionTransaction.php b/src/ConnectionTransaction.php
index a43afca..88f69a4 100644
--- a/src/ConnectionTransaction.php
+++ b/src/ConnectionTransaction.php
@@ -74,7 +74,7 @@ public function getLastUsedAt(): int
      *
      * Closes and commits all changes in the transaction.
      */
-    public function close()
+    public function close(): void
     {
         if ($this->processor) {
             $this->commit(); // Invokes $this->release callback.
diff --git a/src/DataTypes.php b/src/DataTypes.php
index 8c23cc0..8b13881 100644
--- a/src/DataTypes.php
+++ b/src/DataTypes.php
@@ -81,7 +81,7 @@ public static function encodeBinary($param): array
     }
 
     /** @see 14.7.3 Binary Protocol Value */
-    public static function decodeBinary(int $type, string $str, /* ?int */ &$len = 0)
+    public static function decodeBinary(int $type, string $str, ?int &$len = null) /* : mixed */
     {
         $unsigned = $type & 0x80;
         switch ($type) {
@@ -189,12 +189,12 @@ public static function decodeBinary(int $type, string $str, /* ?int */ &$len = 0
         }
     }
 
-    public static function decodeNullString(string $str, /* ?int */ &$len = 0): string
+    public static function decodeNullString(string $str, ?int &$len = null): string
     {
         return \substr($str, 0, $len = \strpos($str, "\0"));
     }
 
-    public static function decodeStringOff(int $type, string $str, int &$off)
+    public static function decodeStringOff(int $type, string $str, int &$off) /* : mixed */
     {
         $len = self::decodeUnsignedOff($str, $off);
         $off += $len;
@@ -256,13 +256,13 @@ public static function decodeUnsignedOff(string $str, int &$off): int
         throw new FailureException("$int is not in ranges [0x00, 0xfa] or [0xfc, 0xfe]");
     }
 
-    public static function decodeString(string $str, /* ?int */ &$intlen = 0, /* ?int */ &$len = 0): string
+    public static function decodeString(string $str, ?int &$intlen = null, ?int &$len = null): string
     {
         $len = self::decodeUnsigned($str, $intlen);
         return \substr($str, $intlen, $len);
     }
 
-    public static function decodeUnsigned(string $str, /* ?int */ &$len = 0)
+    public static function decodeUnsigned(string $str, ?int &$len = null)
     {
         $int = \ord($str);
         if ($int < 0xfb) {
@@ -298,7 +298,7 @@ public static function decodeIntByLen(string $str, int $len): int
         return $int;
     }
 
-    public static function decodeInt8(string $str)
+    public static function decodeInt8(string $str): int
     {
         $int = \ord($str);
         if ($int < (1 << 7)) {
@@ -308,12 +308,12 @@ public static function decodeInt8(string $str)
         return $int << $shift >> $shift;
     }
 
-    public static function decodeUnsigned8(string $str)
+    public static function decodeUnsigned8(string $str): int
     {
         return \ord($str);
     }
 
-    public static function decodeInt16(string $str)
+    public static function decodeInt16(string $str): int
     {
         $int = \unpack("v", $str)[1];
         if ($int < (1 << 15)) {
@@ -328,7 +328,7 @@ public static function decodeUnsigned16(string $str)
         return \unpack("v", $str)[1];
     }
 
-    public static function decodeInt24(string $str)
+    public static function decodeInt24(string $str): int
     {
         $int = \unpack("V", \substr($str, 0, 3) . "\x00")[1];
         if ($int < (1 << 23)) {
@@ -338,12 +338,12 @@ public static function decodeInt24(string $str)
         return $int << $shift >> $shift;
     }
 
-    public static function decodeUnsigned24(string $str)
+    public static function decodeUnsigned24(string $str): int
     {
         return \unpack("V", \substr($str, 0, 3) . "\x00")[1];
     }
 
-    public static function decodeInt32($str)
+    public static function decodeInt32($str): int
     {
         if (\PHP_INT_SIZE > 4) {
             $int = \unpack("V", $str)[1];
@@ -356,7 +356,7 @@ public static function decodeInt32($str)
         return \unpack("V", $str)[1];
     }
 
-    public static function decodeUnsigned32(string $str)
+    public static function decodeUnsigned32(string $str) /* : int|string */
     {
         if (\PHP_INT_SIZE > 4) {
             return \unpack("V", $str)[1];
@@ -366,7 +366,7 @@ public static function decodeUnsigned32(string $str)
         return \gmp_strval(\gmp_import(\substr($str, 0, 4), 1, \GMP_LSW_FIRST));
     }
 
-    public static function decodeInt64(string $str)
+    public static function decodeInt64(string $str) /* : int|string */
     {
         if (\PHP_INT_SIZE > 4) {
             return \unpack("P", $str)[1];
@@ -376,7 +376,7 @@ public static function decodeInt64(string $str)
         return \gmp_strval(\gmp_import(\substr($str, 0, 8), 1, \GMP_LSW_FIRST));
     }
 
-    public static function decodeUnsigned64(string $str)
+    public static function decodeUnsigned64(string $str) /* : int|string */
     {
         \assert(\extension_loaded("gmp"), "The GMP extension is required for UNSIGNED BIGINT fields");
         return \gmp_strval(\gmp_import(\substr($str, 0, 8), 1, \GMP_LSW_FIRST));
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index fa99a0b..49ffa6e 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -155,7 +155,7 @@ public function isReady(): bool
         return $this->connectionState === self::READY;
     }
 
-    public function unreference()
+    public function unreference(): void
     {
         if (!--$this->refcount) {
             $this->appendTask(function () {
@@ -164,7 +164,7 @@ public function unreference()
         }
     }
 
-    private function ready()
+    private function ready(): void
     {
         if (!empty($this->deferreds)) {
             return;
@@ -187,7 +187,7 @@ private function ready()
         }
     }
 
-    private function addDeferred(Deferred $deferred)
+    private function addDeferred(Deferred $deferred): void
     {
         \assert($this->socket, "The connection has been closed");
         $this->deferreds[] = $deferred;
@@ -263,7 +263,7 @@ private function read(): \Generator
         }
     }
 
-    private function processData(string $data)
+    private function processData(string $data): void
     {
         foreach ($this->processors as $processor) {
             if (empty($data = $processor->send($data))) {
@@ -283,7 +283,7 @@ private function getDeferred(): Deferred
         return \array_shift($this->deferreds);
     }
 
-    private function appendTask(callable $callback)
+    private function appendTask(callable $callback): void
     {
         if ($this->packetCallback || $this->parseCallback || !empty($this->onReady) || !empty($this->deferreds) || $this->connectionState !== self::READY) {
             $this->onReady[] = $callback;
@@ -419,7 +419,7 @@ public function resetConnection(): Promise
     }
 
     /** @see 14.7.5 COM_STMT_SEND_LONG_DATA */
-    public function bindParam(int $stmtId, int $paramId, string $data)
+    public function bindParam(int $stmtId, int $paramId, string $data): void
     {
         $payload = "\x18";
         $payload .= DataTypes::encodeInt32($stmtId);
@@ -488,7 +488,7 @@ public function execute(int $stmtId, string $query, array &$params, array $prebo
     }
 
     /** @see 14.7.7 COM_STMT_CLOSE */
-    public function closeStmt(int $stmtId)
+    public function closeStmt(int $stmtId): void
     {
         $payload = "\x19" . DataTypes::encodeInt32($stmtId);
         $this->appendTask(function () use ($payload) {
@@ -533,7 +533,7 @@ public function listAllFields(string $table, string $like = "%"): Promise
     }
 
     /** @see 14.6.6 COM_CREATE_DB */
-    public function createDatabase($db)
+    public function createDatabase(string $db): Promise
     {
         return $this->startCommand(function () use ($db) {
             $this->sendPacket("\x05$db");
@@ -586,7 +586,7 @@ public function processInfo(): Promise
     }
 
     /** @see 14.6.13 COM_PROCESS_KILL */
-    public function killProcess($process): Promise
+    public function killProcess(int $process): Promise
     {
         return $this->startCommand(function () use ($process) {
             $this->sendPacket("\x0c" . DataTypes::encodeInt32($process));
@@ -1141,7 +1141,7 @@ private function handlePrepare($packet)
         }
     }
 
-    private function readStatistics($packet)
+    private function readStatistics($packet): void
     {
         $this->getDeferred()->resolve($packet);
         $this->ready();
@@ -1157,7 +1157,7 @@ public function sendClose(): Promise
         });
     }
 
-    public function close()
+    public function close(): void
     {
         if ($this->connectionState === self::CLOSING && $this->deferreds) {
             \array_pop($this->deferreds)->resolve();
@@ -1199,7 +1199,7 @@ private function write(string $packet): Promise
         return $this->pendingWrite = $this->socket->write($packet);
     }
 
-    private function resetIds()
+    private function resetIds(): void
     {
         if ($this->pendingWrite) {
             $this->pendingWrite->onResolve(function () {
@@ -1325,7 +1325,7 @@ private function parseMysql(): \Generator
         }
     }
 
-    private function parsePayload($packet)
+    private function parsePayload($packet): void
     {
         if ($this->connectionState === self::UNCONNECTED) {
             $this->established();
@@ -1387,19 +1387,19 @@ private function parsePayload($packet)
         }
     }
 
-    private function secureAuth($pass, $scramble)
+    private function secureAuth(string $pass, string $scramble): string
     {
         $hash = \sha1($pass, 1);
         return $hash ^ \sha1(\substr($scramble, 0, 20) . \sha1($hash, 1), 1);
     }
 
-    private function sha256Auth($pass, $scramble, $key)
+    private function sha256Auth(string $pass, string $scramble, string $key): string
     {
         \openssl_public_encrypt($pass ^ \str_repeat($scramble, \ceil(\strlen($pass) / \strlen($scramble))), $auth, $key, OPENSSL_PKCS1_OAEP_PADDING);
         return $auth;
     }
 
-    private function authSwitchRequest($packet)
+    private function authSwitchRequest($packet): void
     {
         $this->parseCallback = null;
         switch (\ord($packet)) {
@@ -1424,7 +1424,7 @@ private function authSwitchRequest($packet)
      * @see 14.2.5 Connection Phase Packets
      * @see 14.3 Authentication Method
      */
-    private function sendHandshake($inSSL = false)
+    private function sendHandshake($inSSL = false): void
     {
         if ($this->config->getDatabase() !== null) {
             $this->capabilities |= self::CLIENT_CONNECT_WITH_DB;
diff --git a/src/PooledStatement.php b/src/PooledStatement.php
index f2c2217..35d2f86 100644
--- a/src/PooledStatement.php
+++ b/src/PooledStatement.php
@@ -22,7 +22,7 @@ protected function createResultSet(SqlResultSet $resultSet, callable $release):
         return new PooledResultSet($resultSet, $release);
     }
 
-    public function bind($paramId, $data)
+    public function bind($paramId, $data): void
     {
         $this->statement->bind($paramId, $data);
     }
diff --git a/src/Statement.php b/src/Statement.php
index 7665079..c4e37ef 100644
--- a/src/Statement.php
+++ b/src/Statement.php
@@ -13,7 +13,7 @@ interface Statement extends SqlStatement
      *
      * @throws \Error If $paramId is not an int or string, or the position does not exist.
      */
-    public function bind($paramId, $data);
+    public function bind($paramId, $data): void;
 
     /**
      * @return Promise<array>
diff --git a/src/StatementPool.php b/src/StatementPool.php
index 0fbe10b..3345015 100644
--- a/src/StatementPool.php
+++ b/src/StatementPool.php
@@ -43,7 +43,7 @@ protected function createResultSet(SqlResultSet $resultSet, callable $release):
         return new PooledResultSet($resultSet, $release);
     }
 
-    public function bind($paramId, $data)
+    public function bind($paramId, $data): void
     {
         if (!\is_int($paramId) && !\is_string($paramId)) {
             throw new \TypeError("Invalid parameter ID type");
diff --git a/src/functions.php b/src/functions.php
index 8bb0c77..836edb2 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -10,7 +10,7 @@
 
 const LOOP_CONNECTOR_IDENTIFIER = Connector::class . "\\Mysql";
 
-function connector(Connector $connector = null): Connector
+function connector(?Connector $connector = null): Connector
 {
     if ($connector === null) {
         $connector = Loop::getState(LOOP_CONNECTOR_IDENTIFIER);
@@ -29,7 +29,6 @@ function connector(Connector $connector = null): Connector
  * Create a connection using the global Connector instance.
  *
  * @param SqlConnectionConfig $config
- * @param \Amp\Socket\ClientTlsContext $sslOptions
  *
  * @return Promise<Connection>
  *

From 0dc66d21e053c9123e0b7095a7a8492096e5caaf Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 27 Aug 2019 09:33:19 -0500
Subject: [PATCH 003/174] Swap TimeoutConnector for CancellableConnector

Timeout can be set ConnectContext, so the timeout connector seems confusing.
---
 benchmarks/QueryBench.php    | 12 +++--------
 src/CancellableConnector.php | 20 ++++++++++++++++++
 src/TimeoutConnector.php     | 41 ------------------------------------
 src/functions.php            |  2 +-
 4 files changed, 24 insertions(+), 51 deletions(-)
 create mode 100644 src/CancellableConnector.php
 delete mode 100644 src/TimeoutConnector.php

diff --git a/benchmarks/QueryBench.php b/benchmarks/QueryBench.php
index d38388a..a394bb8 100644
--- a/benchmarks/QueryBench.php
+++ b/benchmarks/QueryBench.php
@@ -2,17 +2,11 @@
 
 namespace Amp\Mysql\Bench;
 
+use Amp\Mysql\CancellableConnector;
 use Amp\Mysql\Connection;
 use Amp\Mysql\ConnectionConfig;
 use Amp\Mysql\Pool as ConnectionPool;
 use Amp\Mysql\ResultSet;
-use Amp\Mysql\TimeoutConnector;
-use PhpBench\Benchmark\Metadata\Annotations\AfterMethods;
-use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods;
-use PhpBench\Benchmark\Metadata\Annotations\Iterations;
-use PhpBench\Benchmark\Metadata\Annotations\OutputTimeUnit;
-use PhpBench\Benchmark\Metadata\Annotations\Revs;
-use PhpBench\Benchmark\Metadata\Annotations\Warmup;
 use function Amp\call;
 use function Amp\Promise\wait;
 
@@ -44,8 +38,8 @@ class QueryBench extends AbstractBench
     public function init()
     {
         $config = ConnectionConfig::fromString("host=$this->host;user=$this->user;pass=$this->pass");
-        $connector = new TimeoutConnector;
-        $this->connectionPool = new ConnectionPool($config, $this->poolLimit, $connector);
+        $connector = new CancellableConnector;
+        $this->connectionPool = new ConnectionPool($config, $this->poolLimit, 10, $connector);
         $connectionPromise = $connector->connect($config);
         $this->connection = wait($connectionPromise);
         $this->pdoConnection = new \PDO("mysql:host=$this->host;port=3306", $this->user, $this->pass);
diff --git a/src/CancellableConnector.php b/src/CancellableConnector.php
new file mode 100644
index 0000000..5ade176
--- /dev/null
+++ b/src/CancellableConnector.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Amp\Mysql;
+
+use Amp\CancellationToken;
+use Amp\Promise;
+use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
+use Amp\Sql\Connector;
+
+final class CancellableConnector implements Connector
+{
+    public function connect(SqlConnectionConfig $config, ?CancellationToken $token = null): Promise
+    {
+        if (!$config instanceof ConnectionConfig) {
+            throw new \TypeError(\sprintf("Must provide an instance of %s to MySQL connectors", ConnectionConfig::class));
+        }
+
+        return Connection::connect($config, $token);
+    }
+}
diff --git a/src/TimeoutConnector.php b/src/TimeoutConnector.php
deleted file mode 100644
index 1623ca4..0000000
--- a/src/TimeoutConnector.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-use Amp\Promise;
-use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
-use Amp\Sql\Connector;
-
-final class TimeoutConnector implements Connector
-{
-    const DEFAULT_TIMEOUT = 5000;
-
-    /** @var int */
-    private $timeout;
-
-    /**
-     * @param int $timeout Milliseconds until connections attempts are cancelled.
-     */
-    public function __construct(int $timeout = self::DEFAULT_TIMEOUT)
-    {
-        $this->timeout = $timeout;
-    }
-
-    /**
-     * {@inheritdoc}
-     *
-     * @throws \Amp\Sql\FailureException If connecting fails.
-     */
-    public function connect(SqlConnectionConfig $config): Promise
-    {
-        if (!$config instanceof ConnectionConfig) {
-            throw new \TypeError(\sprintf("Must provide an instance of %s to MySQL connectors", ConnectionConfig::class));
-        }
-
-        $config = $config->withConnectContext(
-            $config->getConnectContext()->withConnectTimeout($this->timeout)
-        );
-
-        return Connection::connect($config);
-    }
-}
diff --git a/src/functions.php b/src/functions.php
index 836edb2..ccdd456 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -18,7 +18,7 @@ function connector(?Connector $connector = null): Connector
             return $connector;
         }
 
-        $connector = new TimeoutConnector;
+        $connector = new CancellableConnector;
     }
 
     Loop::setState(LOOP_CONNECTOR_IDENTIFIER, $connector);

From ba8435ea17a75540bb74b4259687fa9c60c221e1 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 27 Aug 2019 09:33:28 -0500
Subject: [PATCH 004/174] Update tests

---
 test/ConnectionTest.php |  5 ++--
 test/LinkTest.php       | 59 ++++++++++++++++++++++-------------------
 2 files changed, 34 insertions(+), 30 deletions(-)

diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php
index efa87bb..95c449e 100644
--- a/test/ConnectionTest.php
+++ b/test/ConnectionTest.php
@@ -3,6 +3,7 @@
 namespace Amp\Mysql\Test;
 
 use Amp\Loop;
+use Amp\Mysql\Connection;
 use Amp\Mysql\ConnectionConfig;
 use Amp\Mysql\TimeoutConnector;
 use Amp\Promise;
@@ -19,7 +20,7 @@ public function testConnect()
     {
         $complete = false;
         Loop::run(function () use (&$complete) {
-            /** @var \Amp\Mysql\Connection $db */
+            /** @var Connection $db */
             $db = yield connect(ConnectionConfig::fromString("host=".DB_HOST." user=".DB_USER." pass=".DB_PASS." db=test"));
 
             /* use an alternative charset... Default is utf8mb4_general_ci */
@@ -43,7 +44,7 @@ public function testInvalidConnectionString()
     public function testDoubleClose()
     {
         Loop::run(function () {
-            /** @var \Amp\Mysql\Connection $db */
+            /** @var Connection $db */
             $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
             $db->close();
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 5219639..f48f285 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -6,7 +6,10 @@
 use Amp\Mysql\CommandResult;
 use Amp\Mysql\DataTypes;
 use Amp\Mysql\ResultSet;
+use Amp\Mysql\Statement;
 use Amp\Promise;
+use Amp\Sql\Link;
+use Amp\Sql\Transaction;
 use PHPUnit\Framework\TestCase;
 
 abstract class LinkTest extends TestCase
@@ -14,17 +17,17 @@ abstract class LinkTest extends TestCase
     /**
      * Returns the Link class to be tested.
      *
-     * @return \Amp\Mysql\Link
+     * @return Promise<Link>
      */
     abstract protected function getLink(string $connectionString): Promise;
 
     public function testQuery()
     {
         Loop::run(function () {
-            /** @var \Amp\Mysql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-            /** @var \Amp\Mysql\ResultSet $resultset */
+            /** @var ResultSet $resultset */
             $resultset = yield $db->execute("SELECT ? AS a", [M_PI]);
             $this->assertInstanceOf(ResultSet::class, $resultset);
 
@@ -39,10 +42,10 @@ public function testQuery()
     public function testQueryFetchRow()
     {
         Loop::run(function () {
-            /** @var \Amp\Mysql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-            /** @var \Amp\Mysql\ResultSet $resultset */
+            /** @var ResultSet $resultset */
             $resultset = yield $db->query('SELECT a FROM main WHERE a < 4');
             $this->assertInstanceOf(ResultSet::class, $resultset);
 
@@ -62,7 +65,7 @@ public function testQueryFetchRow()
     public function testQueryWithInvalidQuery()
     {
         Loop::run(function () {
-            /** @var \Amp\Sql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
             yield $db->query("SELECT & FROM main WHERE a = 1");
@@ -72,10 +75,10 @@ public function testQueryWithInvalidQuery()
     public function testMultiStmt()
     {
         Loop::run(function () {
-            /** @var \Amp\Mysql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
 
-            /** @var \Amp\Mysql\ResultSet $resultset */
+            /** @var ResultSet $resultset */
             $resultset = yield $db->query("SELECT a FROM main; SELECT b FROM main WHERE a = 5; SELECT b AS d, a + 1 AS c FROM main WHERE b > 4");
             $this->assertInstanceOf(ResultSet::class, $resultset);
 
@@ -115,12 +118,12 @@ public function testMultiStmt()
     public function testPrepared()
     {
         Loop::run(function () {
-            /** @var \Amp\Mysql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
 
             /**
-             * @var \Amp\Mysql\Statement           $stmt
-             * @var \Amp\Mysql\ResultSet $result
+             * @var Statement $stmt
+             * @var ResultSet $result
              */
             $stmt = yield $db->prepare("SELECT * FROM main WHERE a = ? OR b = :num");
             $base = [
@@ -193,10 +196,10 @@ public function testPrepareWithInvalidQuery()
     public function testBindWithInvalidParamId()
     {
         Loop::run(function () {
-            /** @var \Amp\Mysql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
 
-            /** @var \Amp\Mysql\Statement $statement */
+            /** @var Statement $statement */
             $statement = yield $db->prepare("SELECT * FROM main WHERE a = ?");
 
             $statement->bind(1, 1);
@@ -212,10 +215,10 @@ public function testBindWithInvalidParamId()
     public function testBindWithInvalidParamName()
     {
         Loop::run(function () {
-            /** @var \Amp\Mysql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
 
-            /** @var \Amp\Mysql\Statement $statement */
+            /** @var Statement $statement */
             $statement = yield $db->prepare("SELECT * FROM main WHERE a = :a");
 
             $statement->bind("b", 1);
@@ -231,10 +234,10 @@ public function testBindWithInvalidParamName()
     public function testBindWithInvalidParamType()
     {
         Loop::run(function () {
-            /** @var \Amp\Mysql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
 
-            /** @var \Amp\Mysql\Statement $statement */
+            /** @var Statement $statement */
             $statement = yield $db->prepare("SELECT * FROM main WHERE a = :a");
 
             $statement->bind(3.14, 1);
@@ -250,10 +253,10 @@ public function testBindWithInvalidParamType()
     public function testStatementExecuteWithTooFewParams()
     {
         Loop::run(function () {
-            /** @var \Amp\Mysql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-            /** @var \Amp\Mysql\Statement $stmt */
+            /** @var Statement $stmt */
             $stmt = yield $db->prepare("SELECT * FROM main WHERE a = ? AND b = ?");
             yield $stmt->execute([1]);
         });
@@ -262,10 +265,10 @@ public function testStatementExecuteWithTooFewParams()
     public function testExecute()
     {
         Loop::run(function () {
-            /** @var \Amp\Sql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-            /** @var \Amp\Mysql\ResultSet $result */
+            /** @var ResultSet $result */
             $result = yield $db->execute("SELECT * FROM main WHERE a = ? OR b = ?", [2, 5]);
             $this->assertInstanceOf(ResultSet::class, $result);
             $got = [];
@@ -290,7 +293,7 @@ public function testExecute()
     public function testExecuteWithInvalidQuery()
     {
         Loop::run(function () {
-            /** @var \Amp\Sql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
             yield $db->execute("SELECT & FROM main WHERE a = ?", [1]);
@@ -304,7 +307,7 @@ public function testExecuteWithInvalidQuery()
     public function testExecuteWithTooFewParams()
     {
         Loop::run(function () {
-            /** @var \Amp\Sql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
             yield $db->execute("SELECT * FROM main WHERE a = ? AND b = ?", [1]);
@@ -314,7 +317,7 @@ public function testExecuteWithTooFewParams()
     public function testPreparedWithNegativeValue()
     {
         Loop::run(function () {
-            /** @var \Amp\Sql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
             yield $db->query("DROP TABLE IF EXISTS tmp");
@@ -335,18 +338,18 @@ public function testPreparedWithNegativeValue()
     public function testTransaction()
     {
         Loop::run(function () {
-            /** @var \Amp\Sql\Link $db */
+            /** @var Link $db */
             $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-            /** @var \Amp\Sql\Transaction $transaction */
+            /** @var Transaction $transaction */
             $transaction = yield $db->beginTransaction();
 
-            /** @var \Amp\Sql\Statement $statement */
+            /** @var Statement $statement */
             $statement = yield $transaction->prepare("INSERT INTO main VALUES (?, ?)");
             $result = yield $statement->execute([6, 7]);
             $this->assertInstanceOf(CommandResult::class, $result);
 
-            /** @var \Amp\Mysql\ResultSet $result */
+            /** @var ResultSet $result */
             $result = yield $transaction->execute("SELECT * FROM main WHERE a = ?", [6]);
 
             $got = [];

From db407b0c635306e016827d0fff25f2d50bc53458 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 27 Aug 2019 09:34:57 -0500
Subject: [PATCH 005/174] Update PHP CS config

---
 .php_cs => .php_cs.dist | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
 rename .php_cs => .php_cs.dist (63%)

diff --git a/.php_cs b/.php_cs.dist
similarity index 63%
rename from .php_cs
rename to .php_cs.dist
index 233c4e9..12f4c88 100644
--- a/.php_cs
+++ b/.php_cs.dist
@@ -1,7 +1,10 @@
 <?php
 
 $config = new Amp\CodeStyle\Config();
-$config->getFinder()->in(__DIR__);
+$config->getFinder()
+    ->in(__DIR__ . '/benchmarks')
+    ->in(__DIR__ . '/src')
+    ->in(__DIR__ . '/test');
 
 $cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__;
 

From 5f974878318757f976e647f9f2e6c90dff7b57ad Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 26 Sep 2019 23:34:10 -0500
Subject: [PATCH 006/174] Update tests

---
 .gitignore              |   1 +
 .travis.yml             |  56 +++--
 composer.json           |  20 +-
 phpunit.xml.dist        |   3 -
 test/ConnectionTest.php |  46 ++--
 test/LinkTest.php       | 502 +++++++++++++++++++---------------------
 test/PoolTest.php       | 126 ++++------
 test/StatementTest.php  |   8 +-
 8 files changed, 342 insertions(+), 420 deletions(-)

diff --git a/.gitignore b/.gitignore
index 8fc7c97..7f5f4d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 .php_cs.cache
+.phpunit.result.cache
 build
 composer.lock
 phpunit.xml
diff --git a/.travis.yml b/.travis.yml
index adaa30b..676102e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,53 +9,59 @@ php:
   - 7.0
   - 7.1
   - 7.2
+  - 7.3
+  - 7.4snapshot
   - nightly
 
 jobs:
   include:
     - stage: test
       addons:
-         mariadb: 10.1
-      php: '7.0'
+        mariadb: '10.1'
+      php: 7.0
     - addons:
-         mariadb: 10.2
-      php: '7.0'
+        mariadb: '10.2'
+      php: 7.0
     - addons:
-         mariadb: 10.3
-      php: '7.0'
+        mariadb: '10.3'
+      php: 7.0
     - addons:
-         mariadb: 10.1
-      php: '7.1'
+        mariadb: '10.1'
+      php: 7.1
     - addons:
-         mariadb: 10.2
-      php: '7.1'
+        mariadb: '10.2'
+      php: 7.1
     - addons:
-         mariadb: 10.3
-      php: '7.1'
+        mariadb: '10.3'
+      php: 7.1
     - addons:
-         mariadb: 10.1
-      php: '7.2'
+        mariadb: '10.1'
+      php: 7.2
     - addons:
-         mariadb: 10.2
-      php: '7.2'
+         mariadb: '10.2'
+      php: 7.2
     - addons:
-         mariadb: 10.3
-      php: '7.2'
+        mariadb: '10.3'
+      php: 7.2
     - addons:
-         mariadb: 10.3
+        mariadb: '10.3'
+      php: 7.3
+    - addons:
+        mariadb: '10.3'
+      php: 7.4snapshot
+    - addons:
+         mariadb: '10.3'
       php: 'nightly'
     - stage: benchmark
-      php: '7.2'
+      php: 7.3
       env:
         - AMP_DEBUG=false
       script:
         - PATH=$PATH:$(pwd)/benchmarks/bin vendor/bin/phpbench run --report=aggregate
   allow_failures:
-    - php: 'nightly'
-    - addons:
-         mariadb: 10.3
-      php: 'nightly'
-  fast_finish: true
+      - php: 7.4snapshot
+      - php: nightly
+    fast_finish: true
 
 env:
   - AMP_DEBUG=true
diff --git a/composer.json b/composer.json
index fc1c1a2..4e96cfc 100644
--- a/composer.json
+++ b/composer.json
@@ -1,14 +1,6 @@
 {
     "name": "amphp/mysql",
     "description": "Asynchronous MySQL client for PHP based on Amp.",
-    "require": {
-        "php": "^7.1",
-        "amphp/amp": "^2",
-        "amphp/file": "^0.3.5",
-        "amphp/socket": "^1",
-        "amphp/sql": "^1",
-        "amphp/sql-common": "^1"
-    },
     "license": "MIT",
     "authors": [
         {
@@ -20,9 +12,17 @@
             "email": "aaron@trowski.com"
         }
     ],
+    "require": {
+        "php": "^7.1",
+        "amphp/amp": "^2",
+        "amphp/file": "^1 || ^0.3.5",
+        "amphp/socket": "^1",
+        "amphp/sql": "^1",
+        "amphp/sql-common": "^1"
+    },
     "require-dev": {
-        "phpunit/phpunit": "^6",
-        "amphp/phpunit-util": "^1",
+        "phpunit/phpunit": "^8 || ^7",
+        "amphp/phpunit-util": "^1.1.2",
         "amphp/php-cs-fixer-config": "dev-master",
         "phpbench/phpbench": "^0.13.0"
     },
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index c31ecac..58130d5 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -22,7 +22,4 @@
             <directory suffix=".php">src</directory>
         </whitelist>
     </filter>
-    <listeners>
-        <listener class="Amp\PHPUnit\LoopReset"/>
-    </listeners>
 </phpunit>
diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php
index 95c449e..be76090 100644
--- a/test/ConnectionTest.php
+++ b/test/ConnectionTest.php
@@ -2,56 +2,40 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Loop;
+use Amp\Mysql\CancellableConnector;
 use Amp\Mysql\Connection;
 use Amp\Mysql\ConnectionConfig;
-use Amp\Mysql\TimeoutConnector;
 use Amp\Promise;
-use function Amp\Mysql\connect;
 
 class ConnectionTest extends LinkTest
 {
     protected function getLink(string $connectionString): Promise
     {
-        return (new TimeoutConnector)->connect(ConnectionConfig::fromString($connectionString));
+        return (new CancellableConnector())->connect(ConnectionConfig::fromString($connectionString));
     }
 
     public function testConnect()
     {
-        $complete = false;
-        Loop::run(function () use (&$complete) {
-            /** @var Connection $db */
-            $db = yield connect(ConnectionConfig::fromString("host=".DB_HOST." user=".DB_USER." pass=".DB_PASS." db=test"));
-
-            /* use an alternative charset... Default is utf8mb4_general_ci */
-            yield $db->setCharset("latin1_general_ci");
-
-            $db->close();
-            $complete = true;
-        });
-        $this->assertTrue($complete, "Database commands did not complete.");
-    }
+        /** @var Connection $db */
+        $db = yield Connection::connect(ConnectionConfig::fromString("host=".DB_HOST." user=".DB_USER." pass=".DB_PASS." db=test"));
 
-    /**
-     * @expectedException \Error
-     * @expectedExceptionMessage Host must be provided in connection string
-     */
-    public function testInvalidConnectionString()
-    {
-        $promise = connect(ConnectionConfig::fromString("username=".DB_USER));
+        $this->assertInstanceOf(Connection::class, $db);
+
+        /* use an alternative charset... Default is utf8mb4_general_ci */
+        yield $db->setCharset("latin1_general_ci");
+
+        $db->close();
     }
 
     public function testDoubleClose()
     {
-        Loop::run(function () {
-            /** @var Connection $db */
-            $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        /** @var Connection $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-            $db->close();
+        $db->close();
 
-            $this->assertFalse($db->isAlive());
+        $this->assertFalse($db->isAlive());
 
-            $db->close(); // Should not throw an exception.
-        });
+        $db->close(); // Should not throw an exception.
     }
 }
diff --git a/test/LinkTest.php b/test/LinkTest.php
index f48f285..87a5827 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -2,17 +2,17 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Loop;
 use Amp\Mysql\CommandResult;
 use Amp\Mysql\DataTypes;
 use Amp\Mysql\ResultSet;
 use Amp\Mysql\Statement;
+use Amp\PHPUnit\AsyncTestCase;
 use Amp\Promise;
 use Amp\Sql\Link;
+use Amp\Sql\QueryError;
 use Amp\Sql\Transaction;
-use PHPUnit\Framework\TestCase;
 
-abstract class LinkTest extends TestCase
+abstract class LinkTest extends AsyncTestCase
 {
     /**
      * Returns the Link class to be tested.
@@ -23,351 +23,313 @@ abstract protected function getLink(string $connectionString): Promise;
 
     public function testQuery()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        /** @var Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-            /** @var ResultSet $resultset */
-            $resultset = yield $db->execute("SELECT ? AS a", [M_PI]);
-            $this->assertInstanceOf(ResultSet::class, $resultset);
+        /** @var ResultSet $resultset */
+        $resultset = yield $db->execute("SELECT ? AS a", [M_PI]);
+        $this->assertInstanceOf(ResultSet::class, $resultset);
 
-            for ($i = 0; yield $resultset->advance(); ++$i) {
-                $this->assertSame(["a" => M_PI], $resultset->getCurrent());
-            }
+        for ($i = 0; yield $resultset->advance(); ++$i) {
+            $this->assertSame(["a" => M_PI], $resultset->getCurrent());
+        }
 
-            $this->assertSame(1, $i);
-        });
+        $this->assertSame(1, $i);
     }
 
     public function testQueryFetchRow()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        /** @var Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-            /** @var ResultSet $resultset */
-            $resultset = yield $db->query('SELECT a FROM main WHERE a < 4');
-            $this->assertInstanceOf(ResultSet::class, $resultset);
+        /** @var ResultSet $resultset */
+        $resultset = yield $db->query('SELECT a FROM main WHERE a < 4');
+        $this->assertInstanceOf(ResultSet::class, $resultset);
 
-            $got = [];
-            while (yield $resultset->advance()) {
-                $got[] = \array_values($resultset->getCurrent());
-            }
+        $got = [];
+        while (yield $resultset->advance()) {
+            $got[] = \array_values($resultset->getCurrent());
+        }
 
-            $this->assertSame($got, [[1], [2], [3]]);
-        });
+        $this->assertSame($got, [[1], [2], [3]]);
     }
 
-    /**
-     * @expectedException \Amp\Sql\QueryError
-     * @expectedExceptionMessage You have an error in your SQL syntax
-     */
     public function testQueryWithInvalidQuery()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $this->expectException(QueryError::class);
+        $this->expectExceptionMessage('You have an error in your SQL syntax');
 
-            yield $db->query("SELECT & FROM main WHERE a = 1");
-        });
+        /** @var Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+
+        yield $db->query("SELECT & FROM main WHERE a = 1");
     }
 
     public function testMultiStmt()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
-
-            /** @var ResultSet $resultset */
-            $resultset = yield $db->query("SELECT a FROM main; SELECT b FROM main WHERE a = 5; SELECT b AS d, a + 1 AS c FROM main WHERE b > 4");
-            $this->assertInstanceOf(ResultSet::class, $resultset);
-
-            $got = [];
-            while (yield $resultset->advance()) {
-                $got[] = \array_values($resultset->getCurrent());
-            }
-            $this->assertSame([[1], [2], [3], [4], [5]], $got);
-            $this->assertTrue(yield $resultset->nextResultSet());
-
-            $got = [];
-            while (yield $resultset->advance()) {
-                $got[] = \array_values($resultset->getCurrent());
-            }
-            $this->assertSame([[6]], $got);
-            $this->assertTrue(yield $resultset->nextResultSet());
-
-            $fields = yield $resultset->getFields();
-
-            $got = [];
-            while (yield $resultset->advance()) {
-                $got[] = $resultset->getCurrent();
-            }
-            $this->assertSame([["d" => 5, "c" => 5], ["d" => 6, "c" => 6]], $got);
-
-            $this->assertCount(2, $fields);
-            $this->assertSame($fields[0]["original_name"], "b");
-            $this->assertSame($fields[0]["name"], "d");
-            $this->assertSame($fields[0]["type"], DataTypes::MYSQL_TYPE_LONG);
-            $this->assertSame($fields[1]["name"], "c");
-            $this->assertSame($fields[1]["type"], DataTypes::MYSQL_TYPE_LONGLONG);
-
-            $this->assertFalse(yield $resultset->nextResultSet());
-        });
+        /** @var Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
+
+        /** @var ResultSet $resultset */
+        $resultset = yield $db->query("SELECT a FROM main; SELECT b FROM main WHERE a = 5; SELECT b AS d, a + 1 AS c FROM main WHERE b > 4");
+        $this->assertInstanceOf(ResultSet::class, $resultset);
+
+        $got = [];
+        while (yield $resultset->advance()) {
+            $got[] = \array_values($resultset->getCurrent());
+        }
+        $this->assertSame([[1], [2], [3], [4], [5]], $got);
+        $this->assertTrue(yield $resultset->nextResultSet());
+
+        $got = [];
+        while (yield $resultset->advance()) {
+            $got[] = \array_values($resultset->getCurrent());
+        }
+        $this->assertSame([[6]], $got);
+        $this->assertTrue(yield $resultset->nextResultSet());
+
+        $fields = yield $resultset->getFields();
+
+        $got = [];
+        while (yield $resultset->advance()) {
+            $got[] = $resultset->getCurrent();
+        }
+        $this->assertSame([["d" => 5, "c" => 5], ["d" => 6, "c" => 6]], $got);
+
+        $this->assertCount(2, $fields);
+        $this->assertSame($fields[0]["original_name"], "b");
+        $this->assertSame($fields[0]["name"], "d");
+        $this->assertSame($fields[0]["type"], DataTypes::MYSQL_TYPE_LONG);
+        $this->assertSame($fields[1]["name"], "c");
+        $this->assertSame($fields[1]["type"], DataTypes::MYSQL_TYPE_LONGLONG);
+
+        $this->assertFalse(yield $resultset->nextResultSet());
     }
 
     public function testPrepared()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
-
-            /**
-             * @var Statement $stmt
-             * @var ResultSet $result
-             */
-            $stmt = yield $db->prepare("SELECT * FROM main WHERE a = ? OR b = :num");
-            $base = [
-                "catalog" => "def",
-                "schema" => "test",
-                "table" => "main",
-                "original_table" => "main",
-                "charset" => 63,
-                "columnlen" => 11,
-                "type" => 3,
-                "flags" => 0,
-                "decimals" => 0,
-            ];
-            $this->assertEquals(yield $stmt->getFields(), [$base + ["name" => "a", "original_name" => "a"], $base + ["name" => "b", "original_name" => "b"]]);
-            $stmt->bind("num", 5);
-            $result = yield $stmt->execute([2]);
-            $this->assertInstanceOf(ResultSet::class, $result);
-            $got = [];
-            while (yield $result->advance()) {
-                $got[] = \array_values($result->getCurrent());
-            }
-            $this->assertCount(2, $got);
-
-            $stmt = yield $db->prepare("SELECT * FROM main WHERE a = ? OR b = ?");
-            $result = yield $stmt->execute([1, 8]);
-            $this->assertInstanceOf(ResultSet::class, $result);
-            $got = [];
-            while (yield $result->advance()) {
-                $got[] = \array_values($result->getCurrent());
-            }
-            $this->assertCount(1, $got);
-
-            $stmt = yield $db->prepare("SELECT * FROM main WHERE a = :a OR b = ?");
-            $result = yield $stmt->execute(["a" => 2, 5]);
-            $this->assertInstanceOf(ResultSet::class, $result);
-            $got = [];
-            while (yield $result->advance()) {
-                $got[] = \array_values($result->getCurrent());
-            }
-            $this->assertCount(2, $got);
-
-            $stmt = yield $db->prepare("INSERT INTO main VALUES (:a, :b)");
-            $result = yield $stmt->execute(["a" => 10, "b" => 11]);
-            $this->assertInstanceOf(CommandResult::class, $result);
-
-            $stmt = yield $db->prepare("DELETE FROM main WHERE a = :a");
-            $result = yield $stmt->execute(["a" => 10]);
-            $this->assertInstanceOf(CommandResult::class, $result);
-        });
+        /** @var Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
+
+        /**
+         * @var Statement $stmt
+         * @var ResultSet $result
+         */
+        $stmt = yield $db->prepare("SELECT * FROM main WHERE a = ? OR b = :num");
+        $base = [
+            "catalog" => "def",
+            "schema" => "test",
+            "table" => "main",
+            "original_table" => "main",
+            "charset" => 63,
+            "columnlen" => 11,
+            "type" => 3,
+            "flags" => 0,
+            "decimals" => 0,
+        ];
+        $this->assertEquals(yield $stmt->getFields(), [$base + ["name" => "a", "original_name" => "a"], $base + ["name" => "b", "original_name" => "b"]]);
+        $stmt->bind("num", 5);
+        $result = yield $stmt->execute([2]);
+        $this->assertInstanceOf(ResultSet::class, $result);
+        $got = [];
+        while (yield $result->advance()) {
+            $got[] = \array_values($result->getCurrent());
+        }
+        $this->assertCount(2, $got);
+
+        $stmt = yield $db->prepare("SELECT * FROM main WHERE a = ? OR b = ?");
+        $result = yield $stmt->execute([1, 8]);
+        $this->assertInstanceOf(ResultSet::class, $result);
+        $got = [];
+        while (yield $result->advance()) {
+            $got[] = \array_values($result->getCurrent());
+        }
+        $this->assertCount(1, $got);
+
+        $stmt = yield $db->prepare("SELECT * FROM main WHERE a = :a OR b = ?");
+        $result = yield $stmt->execute(["a" => 2, 5]);
+        $this->assertInstanceOf(ResultSet::class, $result);
+        $got = [];
+        while (yield $result->advance()) {
+            $got[] = \array_values($result->getCurrent());
+        }
+        $this->assertCount(2, $got);
+
+        $stmt = yield $db->prepare("INSERT INTO main VALUES (:a, :b)");
+        $result = yield $stmt->execute(["a" => 10, "b" => 11]);
+        $this->assertInstanceOf(CommandResult::class, $result);
+
+        $stmt = yield $db->prepare("DELETE FROM main WHERE a = :a");
+        $result = yield $stmt->execute(["a" => 10]);
+        $this->assertInstanceOf(CommandResult::class, $result);
     }
 
-    /**
-     * @expectedException \Amp\Sql\QueryError
-     * @expectedExceptionMessage You have an error in your SQL syntax
-     */
     public function testPrepareWithInvalidQuery()
     {
-        Loop::run(function () {
-            /** @var \Amp\Sql\Link $db */
-            $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $this->expectException(QueryError::class);
+        $this->expectExceptionMessage('You have an error in your SQL syntax');
+
+        /** @var \Amp\Sql\Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-            yield $db->prepare("SELECT & FROM main WHERE a = ?");
-        });
+        yield $db->prepare("SELECT & FROM main WHERE a = ?");
     }
 
-    /**
-     * @expectedException \Error
-     * @expectedExceptionMessage Parameter id 1 is not defined for this prepared statement
-     */
     public function testBindWithInvalidParamId()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
+        $this->expectException(\Error::class);
+        $this->expectExceptionMessage('Parameter id 1 is not defined for this prepared statement');
 
-            /** @var Statement $statement */
-            $statement = yield $db->prepare("SELECT * FROM main WHERE a = ?");
+        /** @var Link $db */
+        $db = yield $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
 
-            $statement->bind(1, 1);
+        /** @var Statement $statement */
+        $statement = yield $db->prepare("SELECT * FROM main WHERE a = ?");
 
-            yield $statement->execute(); // Some implementations do not throw until execute() is called.
-        });
+        $statement->bind(1, 1);
+
+        yield $statement->execute(); // Some implementations do not throw until execute() is called.
     }
 
-    /**
-     * @expectedException \Error
-     * @expectedExceptionMessage Parameter :b is not defined for this prepared statement
-     */
     public function testBindWithInvalidParamName()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
+        $this->expectException(\Error::class);
+        $this->expectExceptionMessage('Parameter :b is not defined for this prepared statement');
+
+        /** @var Link $db */
+        $db = yield $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
 
-            /** @var Statement $statement */
-            $statement = yield $db->prepare("SELECT * FROM main WHERE a = :a");
+        /** @var Statement $statement */
+        $statement = yield $db->prepare("SELECT * FROM main WHERE a = :a");
 
-            $statement->bind("b", 1);
+        $statement->bind("b", 1);
 
-            yield $statement->execute(); // Some implementations do not throw until execute() is called.
-        });
+        yield $statement->execute(); // Some implementations do not throw until execute() is called.
     }
 
-    /**
-     * @expectedException \Error
-     * @expectedExceptionMessage Invalid parameter ID type
-     */
     public function testBindWithInvalidParamType()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
+        $this->expectException(\Error::class);
+        $this->expectExceptionMessage('Invalid parameter ID type');
 
-            /** @var Statement $statement */
-            $statement = yield $db->prepare("SELECT * FROM main WHERE a = :a");
+        /** @var Link $db */
+        $db = yield $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
 
-            $statement->bind(3.14, 1);
+        /** @var Statement $statement */
+        $statement = yield $db->prepare("SELECT * FROM main WHERE a = :a");
 
-            yield $statement->execute(); // Some implementations do not throw until execute() is called.
-        });
+        $statement->bind(3.14, 1);
+
+        yield $statement->execute(); // Some implementations do not throw until execute() is called.
     }
 
-    /**
-     * @expectedException \Error
-     * @expectedExceptionMessage Parameter 1 for prepared statement missing
-     */
     public function testStatementExecuteWithTooFewParams()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
-
-            /** @var Statement $stmt */
-            $stmt = yield $db->prepare("SELECT * FROM main WHERE a = ? AND b = ?");
-            yield $stmt->execute([1]);
-        });
+        $this->expectException(\Error::class);
+        $this->expectExceptionMessage('Parameter 1 for prepared statement missing');
+
+        /** @var Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+
+        /** @var Statement $stmt */
+        $stmt = yield $db->prepare("SELECT * FROM main WHERE a = ? AND b = ?");
+        yield $stmt->execute([1]);
     }
 
     public function testExecute()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
-
-            /** @var ResultSet $result */
-            $result = yield $db->execute("SELECT * FROM main WHERE a = ? OR b = ?", [2, 5]);
-            $this->assertInstanceOf(ResultSet::class, $result);
-            $got = [];
-            while (yield $result->advance()) {
-                $got[] = \array_values($result->getCurrent());
-            }
-            $this->assertCount(2, $got);
-            $this->assertSame([[2, 3], [4, 5]], $got);
-
-            $result = yield $db->execute("INSERT INTO main VALUES (:a, :b)", ["a" => 10, "b" => 11]);
-            $this->assertInstanceOf(CommandResult::class, $result);
-
-            $result = yield $db->execute("DELETE FROM main WHERE a = :a", ["a" => 10]);
-            $this->assertInstanceOf(CommandResult::class, $result);
-        });
+        /** @var Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+
+        /** @var ResultSet $result */
+        $result = yield $db->execute("SELECT * FROM main WHERE a = ? OR b = ?", [2, 5]);
+        $this->assertInstanceOf(ResultSet::class, $result);
+        $got = [];
+        while (yield $result->advance()) {
+            $got[] = \array_values($result->getCurrent());
+        }
+        $this->assertCount(2, $got);
+        $this->assertSame([[2, 3], [4, 5]], $got);
+
+        $result = yield $db->execute("INSERT INTO main VALUES (:a, :b)", ["a" => 10, "b" => 11]);
+        $this->assertInstanceOf(CommandResult::class, $result);
+
+        $result = yield $db->execute("DELETE FROM main WHERE a = :a", ["a" => 10]);
+        $this->assertInstanceOf(CommandResult::class, $result);
     }
 
-    /**
-     * @expectedException \Amp\Sql\QueryError
-     * @expectedExceptionMessage You have an error in your SQL syntax
-     */
     public function testExecuteWithInvalidQuery()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $this->expectException(QueryError::class);
+        $this->expectExceptionMessage('You have an error in your SQL syntax');
+
+        /** @var Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-            yield $db->execute("SELECT & FROM main WHERE a = ?", [1]);
-        });
+        yield $db->execute("SELECT & FROM main WHERE a = ?", [1]);
     }
 
-    /**
-     * @expectedException \Error
-     * @expectedExceptionMessage Parameter 1 for prepared statement missing
-     */
     public function testExecuteWithTooFewParams()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $this->expectException(\Error::class);
+        $this->expectExceptionMessage('Parameter 1 for prepared statement missing');
 
-            yield $db->execute("SELECT * FROM main WHERE a = ? AND b = ?", [1]);
-        });
+        /** @var Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+
+        yield $db->execute("SELECT * FROM main WHERE a = ? AND b = ?", [1]);
     }
 
     public function testPreparedWithNegativeValue()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        /** @var Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-            yield $db->query("DROP TABLE IF EXISTS tmp");
+        yield $db->query("DROP TABLE IF EXISTS tmp");
 
-            /** @var \Amp\Sql\Statement $stmt */
-            $stmt = yield $db->prepare("CREATE TABLE tmp SELECT ? AS a");
-            yield $stmt->execute([-1]);
+        /** @var \Amp\Sql\Statement $stmt */
+        $stmt = yield $db->prepare("CREATE TABLE tmp SELECT ? AS a");
+        yield $stmt->execute([-1]);
 
-            /** @var \Amp\Mysql\ResultSet $result */
-            $stmt = yield $db->prepare("SELECT a FROM tmp");
-            $result = yield $stmt->execute();
-            yield $result->advance();
+        /** @var \Amp\Mysql\ResultSet $result */
+        $stmt = yield $db->prepare("SELECT a FROM tmp");
+        $result = yield $stmt->execute();
+        yield $result->advance();
 
-            $this->assertEquals(\array_values($result->getCurrent()), [-1]);
-        });
+        $this->assertEquals(\array_values($result->getCurrent()), [-1]);
     }
 
     public function testTransaction()
     {
-        Loop::run(function () {
-            /** @var Link $db */
-            $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
-
-            /** @var Transaction $transaction */
-            $transaction = yield $db->beginTransaction();
-
-            /** @var Statement $statement */
-            $statement = yield $transaction->prepare("INSERT INTO main VALUES (?, ?)");
-            $result = yield $statement->execute([6, 7]);
-            $this->assertInstanceOf(CommandResult::class, $result);
-
-            /** @var ResultSet $result */
-            $result = yield $transaction->execute("SELECT * FROM main WHERE a = ?", [6]);
-
-            $got = [];
-            while (yield $result->advance()) {
-                $got[] = \array_values($result->getCurrent());
-            }
-            $this->assertCount(1, $got);
-            yield $result->nextResultSet();
-
-            yield $transaction->rollback();
-
-            $result = yield $db->execute("SELECT * FROM main WHERE a = ?", [6]);
-
-            $got = [];
-            while (yield $result->advance()) {
-                $got[] = \array_values($result->getCurrent());
-            }
-            $this->assertCount(0, $got);
-        });
+        /** @var Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+
+        /** @var Transaction $transaction */
+        $transaction = yield $db->beginTransaction();
+
+        /** @var Statement $statement */
+        $statement = yield $transaction->prepare("INSERT INTO main VALUES (?, ?)");
+        $result = yield $statement->execute([6, 7]);
+        $this->assertInstanceOf(CommandResult::class, $result);
+
+        /** @var ResultSet $result */
+        $result = yield $transaction->execute("SELECT * FROM main WHERE a = ?", [6]);
+
+        $got = [];
+        while (yield $result->advance()) {
+            $got[] = \array_values($result->getCurrent());
+        }
+        $this->assertCount(1, $got);
+        yield $result->nextResultSet();
+
+        yield $transaction->rollback();
+
+        $result = yield $db->execute("SELECT * FROM main WHERE a = ?", [6]);
+
+        $got = [];
+        while (yield $result->advance()) {
+            $got[] = \array_values($result->getCurrent());
+        }
+        $this->assertCount(0, $got);
     }
 }
diff --git a/test/PoolTest.php b/test/PoolTest.php
index fd2b4e6..a68c3d0 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -3,7 +3,6 @@
 namespace Amp\Mysql\Test;
 
 use Amp\Delayed;
-use Amp\Loop;
 use Amp\Mysql\CommandResult;
 use Amp\Mysql\Connection;
 use Amp\Mysql\ConnectionConfig;
@@ -15,7 +14,6 @@
 use Amp\Sql\Transaction as SqlTransaction;
 use Amp\Success;
 use function Amp\call;
-use function Amp\Mysql\pool;
 
 interface StatementOperation extends Statement
 {
@@ -94,10 +92,8 @@ public function testSingleQuery(int $count)
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
-        Loop::run(function () use ($pool, $result) {
-            $return = yield $pool->prepare('SQL Query');
-            $this->assertInstanceOf(Statement::class, $return);
-        });
+        $return = yield $pool->prepare('SQL Query');
+        $this->assertInstanceOf(Statement::class, $return);
     }
 
     /**
@@ -120,19 +116,17 @@ public function testConsecutiveQueries(int $count)
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
-        Loop::run(function () use ($count, $rounds, $pool) {
-            $promises = [];
+        $promises = [];
 
-            for ($i = 0; $i < $count; ++$i) {
-                $promises[] = $pool->prepare('SQL Query');
-            }
+        for ($i = 0; $i < $count; ++$i) {
+            $promises[] = $pool->prepare('SQL Query');
+        }
 
-            $results = yield $promises;
+        $results = yield $promises;
 
-            foreach ($results as $result) {
-                $this->assertInstanceOf(Statement::class, $result);
-            }
-        });
+        foreach ($results as $result) {
+            $this->assertInstanceOf(Statement::class, $result);
+        }
     }
 
     /**
@@ -153,11 +147,9 @@ public function testMutlipleTransactions(int $count)
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
-        Loop::run(function () use ($pool, $result) {
-            $return = yield $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED);
-            $this->assertInstanceOf(SqlTransaction::class, $return);
-            yield $return->rollback();
-        });
+        $return = yield $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED);
+        $this->assertInstanceOf(SqlTransaction::class, $return);
+        yield $return->rollback();
     }
 
     /**
@@ -181,24 +173,22 @@ public function testConsecutiveTransactions(int $count)
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
-        Loop::run(function () use ($count, $rounds, $pool) {
-            $promises = [];
-            for ($i = 0; $i < $count; ++$i) {
-                $promises[] = $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED);
-            }
-
-            $results = yield \array_map(function (Promise $promise): Promise {
-                return call(function () use ($promise) {
-                    $transaction = yield $promise;
-                    $this->assertInstanceOf(SqlTransaction::class, $transaction);
-                    return yield $transaction->rollback();
-                });
-            }, $promises);
-
-            foreach ($results as $result) {
-                $this->assertInstanceof(CommandResult::class, $result);
-            }
-        });
+        $promises = [];
+        for ($i = 0; $i < $count; ++$i) {
+            $promises[] = $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED);
+        }
+
+        $results = yield \array_map(function (Promise $promise): Promise {
+            return call(function () use ($promise) {
+                $transaction = yield $promise;
+                $this->assertInstanceOf(SqlTransaction::class, $transaction);
+                return yield $transaction->rollback();
+            });
+        }, $promises);
+
+        foreach ($results as $result) {
+            $this->assertInstanceof(CommandResult::class, $result);
+        }
     }
 
     /**
@@ -219,19 +209,17 @@ public function testExtractConnection(int $count)
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
-        Loop::run(function () use ($pool, $query, $count) {
-            $promises = [];
-            for ($i = 0; $i < $count; ++$i) {
-                $promises[] = $pool->extractConnection();
-            }
+        $promises = [];
+        for ($i = 0; $i < $count; ++$i) {
+            $promises[] = $pool->extractConnection();
+        }
 
-            $results = yield $promises;
+        $results = yield $promises;
 
-            foreach ($results as $result) {
-                $this->assertInstanceof(Connection::class, $result);
-                $result->query($query);
-            }
-        });
+        foreach ($results as $result) {
+            $this->assertInstanceof(Connection::class, $result);
+            $result->query($query);
+        }
     }
 
     /**
@@ -265,34 +253,16 @@ public function testConnectionClosedInPool(int $count)
         $pool = $this->createPool($this->makeConnectionSet($processors));
         $this->assertSame($count + 1, $pool->getConnectionLimit());
 
-        Loop::run(function () use ($pool, $query, $count) {
-            $promises = [];
-            for ($i = 0; $i < $count + 1; ++$i) {
-                $promises[] = $pool->query($query);
-            }
-            yield $promises;
-
-            $promises = [];
-            for ($i = 0; $i < $count; ++$i) {
-                $promises[] = $pool->query($query);
-            }
-            yield $promises;
-        });
-    }
-
-    /**
-     * @expectedException \Amp\Mysql\InitializationException
-     * @expectedExceptionMessage Access denied for user
-     */
-    public function testWrongPassword()
-    {
-        Loop::run(function () {
-            $db = pool(ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=the_wrong_password;db=test"));
-
-            $promise = $db->query("CREATE TABLE tmp SELECT 1 AS a, 2 AS b");
+        $promises = [];
+        for ($i = 0; $i < $count + 1; ++$i) {
+            $promises[] = $pool->query($query);
+        }
+        yield $promises;
 
-            /* Try a query */
-            $result = yield $promise;
-        });
+        $promises = [];
+        for ($i = 0; $i < $count; ++$i) {
+            $promises[] = $pool->query($query);
+        }
+        yield $promises;
     }
 }
diff --git a/test/StatementTest.php b/test/StatementTest.php
index 3e41c80..bced299 100644
--- a/test/StatementTest.php
+++ b/test/StatementTest.php
@@ -4,9 +4,9 @@
 
 use Amp\Mysql\ConnectionStatement;
 use Amp\Mysql\Internal\ResultProxy;
-use PHPUnit\Framework\TestCase;
+use Amp\PHPUnit\AsyncTestCase;
 
-class StatementTest extends TestCase
+class StatementTest extends AsyncTestCase
 {
     /** @var \Prophecy\Prophecy\ObjectProphecy */
     protected $processor;
@@ -14,8 +14,10 @@ class StatementTest extends TestCase
     /** @var \Amp\Mysql\Internal\ResultProxy */
     protected $resultProxy;
 
-    public function setUp()
+    public function setUp(): void
     {
+        parent::setUp();
+
         $this->processor = $this->prophesize('Amp\Mysql\Internal\Processor');
         $this->resultProxy = new ResultProxy;
     }

From 5bc6d7e99ef5c085b78ed636ba4803db0527300d Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 28 Sep 2019 09:00:21 -0500
Subject: [PATCH 007/174] Port parsed out by amphp/sql

---
 src/ConnectionConfig.php      | 29 ++---------------------------
 test/ConnectionConfigTest.php | 24 ++++++++++++++++++++++++
 2 files changed, 26 insertions(+), 27 deletions(-)
 create mode 100644 test/ConnectionConfigTest.php

diff --git a/src/ConnectionConfig.php b/src/ConnectionConfig.php
index 27e4022..04e9917 100644
--- a/src/ConnectionConfig.php
+++ b/src/ConnectionConfig.php
@@ -34,8 +34,6 @@ final class ConnectionConfig extends SqlConnectionConfig
     private $collate = "utf8mb4_general_ci";
     /* @var string private key to use for sha256_password auth method */
     private $key;
-    /** @var string|null */
-    private $string;
 
     public static function fromString(string $connectionString, ConnectContext $context = null): self
     {
@@ -47,7 +45,7 @@ public static function fromString(string $connectionString, ConnectContext $cont
 
         return new self(
             $parts['host'],
-            $parts['port'] ?? self::DEFAULT_PORT,
+            (int) ($parts['port'] ?? self::DEFAULT_PORT),
             $parts['user'] ?? null,
             $parts['password'] ?? null,
             $parts['db'] ?? null,
@@ -82,32 +80,9 @@ public function __construct(
         $this->useLocalInfile = $useLocalInfile;
     }
 
-    public function __clone()
-    {
-        $this->string = null;
-    }
-
     public function getConnectionString(): string
     {
-        if ($this->string !== null) {
-            return $this->string;
-        }
-
-        $host = $this->getHost();
-
-        $index = \strpos($host, ':');
-
-        if ($index === false) {
-            return $this->string = "tcp://$host:3306";
-        }
-
-        if ($index === 0) {
-            return $this->string = "tcp://localhost:" . (int) \substr($host, 1);
-        }
-
-        list($host, $port) = \explode(':', $host, 2);
-
-        return $this->string = "tcp://$host:" . (int) $port;
+        return 'tcp://' . $this->getHost() . ':' . $this->getPort();
     }
 
     public function isCompressionEnabled(): bool
diff --git a/test/ConnectionConfigTest.php b/test/ConnectionConfigTest.php
new file mode 100644
index 0000000..57737bb
--- /dev/null
+++ b/test/ConnectionConfigTest.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Amp\Mysql\Test;
+
+use Amp\Mysql\ConnectionConfig;
+use Amp\PHPUnit\AsyncTestCase;
+
+class ConnectionConfigTest extends AsyncTestCase
+{
+    public function testInvalidConnectionString()
+    {
+        $this->expectException(\Error::class);
+        $this->expectExceptionMessage('Host must be provided in connection string');
+
+        ConnectionConfig::fromString("username=" . DB_USER);
+    }
+
+    public function testGetConnectionString()
+    {
+        $config = ConnectionConfig::fromString('host=host;port=1234;user=user;password=password');
+
+        $this->assertSame('tcp://host:1234', $config->getConnectionString());
+    }
+}

From e7c04bb93579d003e6ea32e46793c636e7802c2b Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 28 Sep 2019 09:01:33 -0500
Subject: [PATCH 008/174] Allow connector injection

---
 src/CancellableConnector.php | 11 ++++++++++-
 src/Connection.php           | 20 ++++++++++++++------
 src/Internal/Processor.php   | 17 +++++++++++++----
 3 files changed, 37 insertions(+), 11 deletions(-)

diff --git a/src/CancellableConnector.php b/src/CancellableConnector.php
index 5ade176..33a39cd 100644
--- a/src/CancellableConnector.php
+++ b/src/CancellableConnector.php
@@ -4,17 +4,26 @@
 
 use Amp\CancellationToken;
 use Amp\Promise;
+use Amp\Socket;
 use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
 use Amp\Sql\Connector;
 
 final class CancellableConnector implements Connector
 {
+    /** @var Socket\Connector */
+    private $connector;
+
+    public function __construct(?Socket\Connector $connector = null)
+    {
+        $this->connector = $connector ?? Socket\connector();
+    }
+
     public function connect(SqlConnectionConfig $config, ?CancellationToken $token = null): Promise
     {
         if (!$config instanceof ConnectionConfig) {
             throw new \TypeError(\sprintf("Must provide an instance of %s to MySQL connectors", ConnectionConfig::class));
         }
 
-        return Connection::connect($config, $token);
+        return Connection::connect($config, $token, $this->connector);
     }
 }
diff --git a/src/Connection.php b/src/Connection.php
index 3219a48..c8be013 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -4,6 +4,7 @@
 
 use Amp\CancellationToken;
 use Amp\Deferred;
+use Amp\NullCancellationToken;
 use Amp\Promise;
 use Amp\Socket;
 use Amp\Sql\FailureException;
@@ -33,16 +34,23 @@ final class Connection implements Link
     /**
      * @param ConnectionConfig $config
      * @param CancellationToken|null $token
+     * @param Socket\Connector|null $connector
      *
-     * @return Promise
+     * @return Promise<self>
      */
-    public static function connect(ConnectionConfig $config, ?CancellationToken $token = null): Promise
-    {
-        return call(function () use ($config, $token) {
-            $socket = yield Socket\connect($config->getConnectionString(), $config->getConnectContext(), $token);
+    public static function connect(
+        ConnectionConfig $config,
+        ?CancellationToken $token = null,
+        ?Socket\Connector $connector = null
+    ): Promise {
+        $token = $token ?? new NullCancellationToken;
+
+        return call(function () use ($config, $token, $connector) {
+            $socket = yield ($connector ?? Socket\connector())
+                ->connect($config->getConnectionString(), $config->getConnectContext(), $token);
 
             $processor = new Internal\Processor($socket, $config);
-            yield $processor->connect();
+            yield $processor->connect($token);
             return new self($processor);
         });
     }
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 49ffa6e..2b53f83 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -2,6 +2,7 @@
 
 namespace Amp\Mysql\Internal;
 
+use Amp\CancellationToken;
 use Amp\Coroutine;
 use Amp\Deferred;
 use Amp\Loop;
@@ -194,7 +195,7 @@ private function addDeferred(Deferred $deferred): void
         $this->socket->reference();
     }
 
-    public function connect(): Promise
+    public function connect(CancellationToken $token): Promise
     {
         \assert(!$this->processors, self::class."::connect() must not be called twice");
 
@@ -202,7 +203,11 @@ public function connect(): Promise
 
         $this->processors = [$this->parseMysql()];
 
-        (new Coroutine($this->read()))->onResolve(function () {
+        $id = $token->subscribe(function (): void {
+            $this->close();
+        });
+
+        (new Coroutine($this->read()))->onResolve(function (): void {
             $this->close();
 
             foreach ($this->deferreds as $deferred) {
@@ -216,8 +221,12 @@ public function connect(): Promise
 
         $promise = $deferred->promise();
 
+        $promise->onResolve(static function () use ($id, $token): void {
+            $token->unsubscribe($id);
+        });
+
         if ($this->config->getCharset() !== ConnectionConfig::DEFAULT_CHARSET || $this->config->getCollation() !== ConnectionConfig::DEFAULT_COLLATE) {
-            $promise->onResolve(function ($exception) {
+            $promise->onResolve(function (?\Throwable $exception): void {
                 if ($exception) {
                     return;
                 }
@@ -668,7 +677,7 @@ private function handleError($packet)
         } elseif ($this->connectionState < self::READY) {
             // connection failure
             $this->close();
-            $this->getDeferred()->fail(new InitializationException("Could not connect to {$this->config->connectionString()}: {$this->connInfo->errorState} {$this->connInfo->errorMsg}"));
+            $this->getDeferred()->fail(new InitializationException("Could not connect to {$this->config->getConnectionString()}: {$this->connInfo->errorState} {$this->connInfo->errorMsg}"));
         }
     }
 

From 93bbe185ac00e1a9bd6f6c4dd6559160d2bf64d2 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 30 Sep 2019 17:52:33 -0500
Subject: [PATCH 009/174] Update readme

---
 README.md | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index 7091777..bf54c74 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ composer require amphp/mysql
 
 ## Requirements
 
-* PHP 7.0+
+* PHP 7.1+
 * [Amp framework](https://github.com/amphp/amp) (installed via composer)
 
 ## Documentation & Examples
@@ -47,17 +47,19 @@ composer require amphp/mysql
 More extensive code examples reside in the [`examples`](examples) directory.
 
 ```php
-\Amp\Loop::run(function() {
-    $config = Amp\Mysql\ConnectionConfig::fromString("host=127.0.0.1 user=username password=password db=test");
+Amp\Loop::run(function() {
+    $config = Amp\Mysql\ConnectionConfig::fromString(
+        "host=127.0.0.1 user=username password=password db=test"
+    );
     
     /** @var \Amp\Mysql\Pool $pool */
     $pool = Amp\Mysql\pool($config);
     
     /** @var \Amp\Mysql\Statement $statement */
-    $statement = yield $pool->prepare("SELECT * FROM table_name WHERE id=?");
+    $statement = yield $pool->prepare("SELECT * FROM table_name WHERE id = :id");
     
     /** @var \Amp\Mysql\ResultSet $result */
-    $result = yield $statement->execute([1337]);
+    $result = yield $statement->execute(['id' => 1337]);
     while (yield $result->advance()) {
         $row = $result->getCurrent();
         // $row is an associative array of column values. e.g.: $row['column_name']

From c02e97ed82e92037ff1cef0bee5cbb8cb6b596ab Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Wed, 9 Oct 2019 18:36:28 -0500
Subject: [PATCH 010/174] Update travis build

---
 .travis.yml | 10 ----------
 1 file changed, 10 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 676102e..2083539 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,7 +6,6 @@ services:
   - mysql
 
 php:
-  - 7.0
   - 7.1
   - 7.2
   - 7.3
@@ -18,15 +17,6 @@ jobs:
     - stage: test
       addons:
         mariadb: '10.1'
-      php: 7.0
-    - addons:
-        mariadb: '10.2'
-      php: 7.0
-    - addons:
-        mariadb: '10.3'
-      php: 7.0
-    - addons:
-        mariadb: '10.1'
       php: 7.1
     - addons:
         mariadb: '10.2'

From b9b56aa530cccb569465dfc2654f15f02b8dd9a0 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 10 Oct 2019 16:35:59 -0500
Subject: [PATCH 011/174] Fix travis by using trusty dist

---
 .travis.yml | 49 ++++++++++++++++++++++++++++++-------------------
 1 file changed, 30 insertions(+), 19 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 2083539..1fef6d5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,6 +2,8 @@ sudo: false
 
 language: php
 
+dist: trusty
+
 services:
   - mysql
 
@@ -16,62 +18,71 @@ jobs:
   include:
     - stage: test
       addons:
-        mariadb: '10.1'
+        mariadb: 10.1
       php: 7.1
     - addons:
-        mariadb: '10.2'
+        mariadb: 10.2
       php: 7.1
     - addons:
-        mariadb: '10.3'
+        mariadb: 10.3
       php: 7.1
     - addons:
-        mariadb: '10.1'
+        mariadb: 10.1
       php: 7.2
     - addons:
-         mariadb: '10.2'
+        mariadb: 10.2
       php: 7.2
     - addons:
-        mariadb: '10.3'
+        mariadb: 10.3
       php: 7.2
     - addons:
-        mariadb: '10.3'
+        mariadb: 10.1
+      php: 7.3
+    - addons:
+        mariadb: 10.2
       php: 7.3
     - addons:
-        mariadb: '10.3'
+        mariadb: 10.3
+      php: 7.3
+    - addons:
+        mariadb: 10.3
       php: 7.4snapshot
     - addons:
-         mariadb: '10.3'
-      php: 'nightly'
+        mariadb: 10.3
+      php: nightly
     - stage: benchmark
       php: 7.3
       env:
         - AMP_DEBUG=false
       script:
         - PATH=$PATH:$(pwd)/benchmarks/bin vendor/bin/phpbench run --report=aggregate
+
+matrix:
   allow_failures:
-      - php: 7.4snapshot
-      - php: nightly
-    fast_finish: true
+    - php: 7.4snapshot
+    - php: nightly
+  fast_finish: true
 
 env:
   - AMP_DEBUG=true
 
-install:
-  - composer update -n --prefer-dist
-  - wget https://github.com/php-coveralls/php-coveralls/releases/download/v1.0.2/coveralls.phar
-  - chmod +x coveralls.phar
+before_install:
   - mysqld --version
+  - mysql -u root -e "SELECT VERSION(); CREATE DATABASE test; CREATE TABLE test.main (a INT(11), b INT(11)); INSERT INTO test.main VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6);"
+
+install:
+  - travis_retry composer update -n --prefer-dist
 
 before_script:
   - echo '<?php $autoloader = require(__DIR__."/../vendor/autoload.php"); const DB_HOST = "localhost"; const DB_USER = "root"; const DB_PASS = "";' > test/bootstrap.php
-  - mysql -u root -e "SELECT VERSION(); CREATE DATABASE test; CREATE TABLE test.main (a INT(11), b INT(11)); INSERT INTO test.main VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6);"
 
 script:
   - vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml
   - PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer --diff --dry-run -v fix
 
 after_script:
-  - ./coveralls.phar
+  - travis_retry composer require php-coveralls/php-coveralls
+  - travis_retry vendor/bin/php-coveralls -v
 
 cache:
   directories:

From 59086f7acc3151eea92e062ece6f21516aff28a5 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 10 Oct 2019 16:39:55 -0500
Subject: [PATCH 012/174] Test with MariaDB 10.4

---
 .travis.yml | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/.travis.yml b/.travis.yml
index 1fef6d5..eb98a11 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,6 +26,9 @@ jobs:
     - addons:
         mariadb: 10.3
       php: 7.1
+    - addons:
+        mariadb: 10.4
+      php: 7.1
     - addons:
         mariadb: 10.1
       php: 7.2
@@ -35,6 +38,9 @@ jobs:
     - addons:
         mariadb: 10.3
       php: 7.2
+    - addons:
+        mariadb: 10.4
+      php: 7.2
     - addons:
         mariadb: 10.1
       php: 7.3
@@ -45,10 +51,22 @@ jobs:
         mariadb: 10.3
       php: 7.3
     - addons:
-        mariadb: 10.3
+        mariadb: 10.4
+      php: 7.3
+    - addons:
+        mariadb: 10.1
+      php: 7.4snapshot
+    - addons:
+        mariadb: 10.2
       php: 7.4snapshot
     - addons:
         mariadb: 10.3
+      php: 7.4snapshot
+    - addons:
+        mariadb: 10.4
+      php: 7.4snapshot
+    - addons:
+        mariadb: 10.4
       php: nightly
     - stage: benchmark
       php: 7.3

From 67949059ceab4cd0e3cd4c14f581f566c01f8214 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 15 Oct 2019 14:57:48 -0500
Subject: [PATCH 013/174] Decode JSON

Fixes #88.
---
 src/DataTypes.php | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/src/DataTypes.php b/src/DataTypes.php
index 8b13881..a2e6993 100644
--- a/src/DataTypes.php
+++ b/src/DataTypes.php
@@ -39,6 +39,8 @@ final class DataTypes
     const MYSQL_TYPE_STRING = 0xfe;
     const MYSQL_TYPE_GEOMETRY = 0xff;
 
+    private const ENCODED_JSON_PREFIX = "base64:type251:";
+
     /** @see 14.7.3 Binary Value */
     public static function encodeBinary($param): array
     {
@@ -96,13 +98,17 @@ public static function decodeBinary(int $type, string $str, ?int &$len = null) /
             case self::MYSQL_TYPE_TINY_BLOB:
             case self::MYSQL_TYPE_GEOMETRY:
             case self::MYSQL_TYPE_BIT:
-            case self::MYSQL_TYPE_JSON:
             case self::MYSQL_TYPE_DECIMAL:
             case self::MYSQL_TYPE_NEWDECIMAL:
                 $ret = self::decodeString($str, $intlen, $len);
                 $len += $intlen;
                 return $ret;
 
+            case self::MYSQL_TYPE_JSON:
+                $ret = self::decodeString($str, $intlen, $len);
+                $len += $intlen;
+                return self::decodeJson($ret);
+
             case self::MYSQL_TYPE_LONGLONG:
             case self::MYSQL_TYPE_LONGLONG | 0x80:
                 $len = 8;
@@ -224,11 +230,24 @@ public static function decodeStringOff(int $type, string $str, int &$off) /* : m
             case self::MYSQL_TYPE_FLOAT:
                 return (float) $data;
 
+            case self::MYSQL_TYPE_JSON:
+                return self::decodeJson($data);
+
             default:
                 return $data;
         }
     }
 
+    private static function decodeJson(string $data): string
+    {
+        if (!\strncmp(self::ENCODED_JSON_PREFIX, $data, \strlen(self::ENCODED_JSON_PREFIX))) {
+            return $data; // Data was not base-64 encoded.
+        }
+
+        $data = \substr($data, \strlen(self::ENCODED_JSON_PREFIX));
+        return \base64_decode($data);
+    }
+
     public static function decodeUnsignedOff(string $str, int &$off): int
     {
         $int = \ord($str[$off]);

From dea6c3b64ce7c5e9415e4d8806fae305be9fe902 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Wed, 30 Oct 2019 10:22:52 -0500
Subject: [PATCH 014/174] More PHP 7.1 Goodness

---
 composer.json                |   3 +-
 src/Connection.php           |  16 +--
 src/ConnectionConfig.php     |   8 +-
 src/ConnectionResultSet.php  |   2 +-
 src/DataTypes.php            |  62 +++++-----
 src/Internal/Processor.php   | 228 +++++++++++++++++++----------------
 src/Internal/ResultProxy.php |  26 ++--
 7 files changed, 181 insertions(+), 164 deletions(-)

diff --git a/composer.json b/composer.json
index 4e96cfc..efe023d 100644
--- a/composer.json
+++ b/composer.json
@@ -13,7 +13,7 @@
         }
     ],
     "require": {
-        "php": "^7.1",
+        "php": ">=7.1",
         "amphp/amp": "^2",
         "amphp/file": "^1 || ^0.3.5",
         "amphp/socket": "^1",
@@ -21,6 +21,7 @@
         "amphp/sql-common": "^1"
     },
     "require-dev": {
+        "ext-openssl": "*",
         "phpunit/phpunit": "^8 || ^7",
         "amphp/phpunit-util": "^1.1.2",
         "amphp/php-cs-fixer-config": "dev-master",
diff --git a/src/Connection.php b/src/Connection.php
index c8be013..7523d6f 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -13,14 +13,14 @@
 
 final class Connection implements Link
 {
-    const REFRESH_GRANT = 0x01;
-    const REFRESH_LOG = 0x02;
-    const REFRESH_TABLES = 0x04;
-    const REFRESH_HOSTS = 0x08;
-    const REFRESH_STATUS = 0x10;
-    const REFRESH_THREADS = 0x20;
-    const REFRESH_SLAVE = 0x40;
-    const REFRESH_MASTER = 0x80;
+    public const REFRESH_GRANT = 0x01;
+    public const REFRESH_LOG = 0x02;
+    public const REFRESH_TABLES = 0x04;
+    public const REFRESH_HOSTS = 0x08;
+    public const REFRESH_STATUS = 0x10;
+    public const REFRESH_THREADS = 0x20;
+    public const REFRESH_SLAVE = 0x40;
+    public const REFRESH_MASTER = 0x80;
 
     /** @var Internal\Processor */
     private $processor;
diff --git a/src/ConnectionConfig.php b/src/ConnectionConfig.php
index 04e9917..6fddeee 100644
--- a/src/ConnectionConfig.php
+++ b/src/ConnectionConfig.php
@@ -7,8 +7,8 @@
 
 final class ConnectionConfig extends SqlConnectionConfig
 {
-    const DEFAULT_PORT = 3306;
-    const BIN_CHARSET = 45; // utf8mb4_general_ci
+    public const DEFAULT_PORT = 3306;
+    public const BIN_CHARSET = 45; // utf8mb4_general_ci
 
     const KEY_MAP = [
         'username' => 'user',
@@ -19,8 +19,8 @@ final class ConnectionConfig extends SqlConnectionConfig
         'charset' => 'charset',
     ];
 
-    const DEFAULT_CHARSET = "utf8mb4";
-    const DEFAULT_COLLATE = "utf8mb4_general_ci";
+    public const DEFAULT_CHARSET = "utf8mb4";
+    public const DEFAULT_COLLATE = "utf8mb4_general_ci";
 
     /** @var bool */
     private $useCompression = false;
diff --git a/src/ConnectionResultSet.php b/src/ConnectionResultSet.php
index 43fa972..35a7e3e 100644
--- a/src/ConnectionResultSet.php
+++ b/src/ConnectionResultSet.php
@@ -108,7 +108,7 @@ private static function fetchRow(Internal\ResultProxy $result): Promise
             return $row;
         };
 
-        $result->deferreds[Internal\ResultProxy::SINGLE_ROW_FETCH][] = [$deferred, null, $incRow];
+        $result->deferreds[Internal\ResultProxy::UNFETCHED][] = [$deferred, null, $incRow];
         return $deferred->promise();
     }
 
diff --git a/src/DataTypes.php b/src/DataTypes.php
index a2e6993..b488be1 100644
--- a/src/DataTypes.php
+++ b/src/DataTypes.php
@@ -7,37 +7,37 @@
 /** @see 14.6.4.1.1.1 Column Type */
 final class DataTypes
 {
-    const MYSQL_TYPE_DECIMAL = 0x00;
-    const MYSQL_TYPE_TINY = 0x01;
-    const MYSQL_TYPE_SHORT = 0x02;
-    const MYSQL_TYPE_LONG = 0x03;
-    const MYSQL_TYPE_FLOAT = 0x04;
-    const MYSQL_TYPE_DOUBLE = 0x05;
-    const MYSQL_TYPE_NULL = 0x06;
-    const MYSQL_TYPE_TIMESTAMP = 0x07;
-    const MYSQL_TYPE_LONGLONG = 0x08;
-    const MYSQL_TYPE_INT24 = 0x09;
-    const MYSQL_TYPE_DATE = 0x0a;
-    const MYSQL_TYPE_TIME = 0x0b;
-    const MYSQL_TYPE_DATETIME = 0x0c;
-    const MYSQL_TYPE_YEAR = 0x0d;
-    const MYSQL_TYPE_NEWDATE = 0x0e;
-    const MYSQL_TYPE_VARCHAR = 0x0f;
-    const MYSQL_TYPE_BIT = 0x10;
-    const MYSQL_TYPE_TIMESTAMP2 = 0x11;
-    const MYSQL_TYPE_DATETIME2 = 0x12;
-    const MYSQL_TYPE_TIME2 = 0x13;
-    const MYSQL_TYPE_JSON = 0xf5;
-    const MYSQL_TYPE_NEWDECIMAL = 0xf6;
-    const MYSQL_TYPE_ENUM = 0xf7;
-    const MYSQL_TYPE_SET = 0xf8;
-    const MYSQL_TYPE_TINY_BLOB = 0xf9;
-    const MYSQL_TYPE_MEDIUM_BLOB = 0xfa;
-    const MYSQL_TYPE_LONG_BLOB = 0xfb;
-    const MYSQL_TYPE_BLOB = 0xfc;
-    const MYSQL_TYPE_VAR_STRING = 0xfd;
-    const MYSQL_TYPE_STRING = 0xfe;
-    const MYSQL_TYPE_GEOMETRY = 0xff;
+    public const MYSQL_TYPE_DECIMAL = 0x00;
+    public const MYSQL_TYPE_TINY = 0x01;
+    public const MYSQL_TYPE_SHORT = 0x02;
+    public const MYSQL_TYPE_LONG = 0x03;
+    public const MYSQL_TYPE_FLOAT = 0x04;
+    public const MYSQL_TYPE_DOUBLE = 0x05;
+    public const MYSQL_TYPE_NULL = 0x06;
+    public const MYSQL_TYPE_TIMESTAMP = 0x07;
+    public const MYSQL_TYPE_LONGLONG = 0x08;
+    public const MYSQL_TYPE_INT24 = 0x09;
+    public const MYSQL_TYPE_DATE = 0x0a;
+    public const MYSQL_TYPE_TIME = 0x0b;
+    public const MYSQL_TYPE_DATETIME = 0x0c;
+    public const MYSQL_TYPE_YEAR = 0x0d;
+    public const MYSQL_TYPE_NEWDATE = 0x0e;
+    public const MYSQL_TYPE_VARCHAR = 0x0f;
+    public const MYSQL_TYPE_BIT = 0x10;
+    public const MYSQL_TYPE_TIMESTAMP2 = 0x11;
+    public const MYSQL_TYPE_DATETIME2 = 0x12;
+    public const MYSQL_TYPE_TIME2 = 0x13;
+    public const MYSQL_TYPE_JSON = 0xf5;
+    public const MYSQL_TYPE_NEWDECIMAL = 0xf6;
+    public const MYSQL_TYPE_ENUM = 0xf7;
+    public const MYSQL_TYPE_SET = 0xf8;
+    public const MYSQL_TYPE_TINY_BLOB = 0xf9;
+    public const MYSQL_TYPE_MEDIUM_BLOB = 0xfa;
+    public const MYSQL_TYPE_LONG_BLOB = 0xfb;
+    public const MYSQL_TYPE_BLOB = 0xfc;
+    public const MYSQL_TYPE_VAR_STRING = 0xfd;
+    public const MYSQL_TYPE_STRING = 0xfe;
+    public const MYSQL_TYPE_GEOMETRY = 0xff;
 
     private const ENCODED_JSON_PREFIX = "base64:type251:";
 
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 3e56745..c126b9c 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -24,30 +24,30 @@
  */
 
 /** @see 14.1.3.4 Status Flags */
-class StatusFlags
+final class StatusFlags
 {
-    const SERVER_STATUS_IN_TRANS = 0x0001; // a transaction is active
-    const SERVER_STATUS_AUTOCOMMIT = 0x0002; // auto-commit is enabled
-    const SERVER_MORE_RESULTS_EXISTS = 0x0008;
-    const SERVER_STATUS_NO_GOOD_INDEX_USED = 0x0010;
-    const SERVER_STATUS_NO_INDEX_USED = 0x0020;
-    const SERVER_STATUS_CURSOR_EXISTS = 0x0040; // Used by Binary Protocol Resultset to signal that COM_STMT_FETCH has to be used to fetch the row-data.
-    const SERVER_STATUS_LAST_ROW_SENT = 0x0080;
-    const SERVER_STATUS_DB_DROPPED = 0x0100;
-    const SERVER_STATUS_NO_BACKSLASH_ESCAPES = 0x0200;
-    const SERVER_STATUS_METADATA_CHANGED = 0x0400;
-    const SERVER_QUERY_WAS_SLOW = 0x0800;
-    const SERVER_PS_OUT_PARAMS = 0x1000;
-    const SERVER_STATUS_IN_TRANS_READONLY = 0x2000; // in a read-only transaction
-    const SERVER_SESSION_STATE_CHANGED = 0x4000; // connection state information has changed
+    public const SERVER_STATUS_IN_TRANS = 0x0001; // a transaction is active
+    public const SERVER_STATUS_AUTOCOMMIT = 0x0002; // auto-commit is enabled
+    public const SERVER_MORE_RESULTS_EXISTS = 0x0008;
+    public const SERVER_STATUS_NO_GOOD_INDEX_USED = 0x0010;
+    public const SERVER_STATUS_NO_INDEX_USED = 0x0020;
+    public const SERVER_STATUS_CURSOR_EXISTS = 0x0040; // Used by Binary Protocol Resultset to signal that COM_STMT_FETCH has to be used to fetch the row-data.
+    public const SERVER_STATUS_LAST_ROW_SENT = 0x0080;
+    public const SERVER_STATUS_DB_DROPPED = 0x0100;
+    public const SERVER_STATUS_NO_BACKSLASH_ESCAPES = 0x0200;
+    public const SERVER_STATUS_METADATA_CHANGED = 0x0400;
+    public const SERVER_QUERY_WAS_SLOW = 0x0800;
+    public const SERVER_PS_OUT_PARAMS = 0x1000;
+    public const SERVER_STATUS_IN_TRANS_READONLY = 0x2000; // in a read-only transaction
+    public const SERVER_SESSION_STATE_CHANGED = 0x4000; // connection state information has changed
 }
 
 /** @see 13.1.3.1.1 Session State Information */
-class SessionStateTypes
+final class SessionStateTypes
 {
-    const SESSION_TRACK_SYSTEM_VARIABLES = 0x00;
-    const SESSION_TRACK_SCHEMA = 0x01;
-    const SESSION_TRACK_STATE_CHANGE = 0x02;
+    public const SESSION_TRACK_SYSTEM_VARIABLES = 0x00;
+    public const SESSION_TRACK_SCHEMA = 0x01;
+    public const SESSION_TRACK_STATE_CHANGE = 0x02;
 }
 
 /** @internal */
@@ -106,37 +106,37 @@ class Processor implements TransientResource
 
     private $connectionState = self::UNCONNECTED;
 
-    const MAX_PACKET_SIZE = 0xffffff;
-    const MAX_UNCOMPRESSED_BUFLEN = 0xfffffb;
-
-    const CLIENT_LONG_FLAG = 0x00000004;
-    const CLIENT_CONNECT_WITH_DB = 0x00000008;
-    const CLIENT_COMPRESS = 0x00000020;
-    const CLIENT_LOCAL_INFILE = 0x00000080;
-    const CLIENT_PROTOCOL_41 = 0x00000200;
-    const CLIENT_SSL = 0x00000800;
-    const CLIENT_TRANSACTIONS = 0x00002000;
-    const CLIENT_SECURE_CONNECTION = 0x00008000;
-    const CLIENT_MULTI_STATEMENTS = 0x00010000;
-    const CLIENT_MULTI_RESULTS = 0x00020000;
-    const CLIENT_PS_MULTI_RESULTS = 0x00040000;
-    const CLIENT_PLUGIN_AUTH = 0x00080000;
-    const CLIENT_CONNECT_ATTRS = 0x00100000;
-    const CLIENT_SESSION_TRACK = 0x00800000;
-    const CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 0x00200000;
-    const CLIENT_DEPRECATE_EOF = 0x01000000;
-
-    const OK_PACKET = 0x00;
-    const EXTRA_AUTH_PACKET = 0x01;
-    const LOCAL_INFILE_REQUEST = 0xfb;
-    const EOF_PACKET = 0xfe;
-    const ERR_PACKET = 0xff;
-
-    const UNCONNECTED = 0;
-    const ESTABLISHED = 1;
-    const READY = 2;
-    const CLOSING = 3;
-    const CLOSED = 4;
+    private const MAX_PACKET_SIZE = 0xffffff;
+    private const MAX_UNCOMPRESSED_BUFLEN = 0xfffffb;
+
+    private const CLIENT_LONG_FLAG = 0x00000004;
+    private const CLIENT_CONNECT_WITH_DB = 0x00000008;
+    private const CLIENT_COMPRESS = 0x00000020;
+    private const CLIENT_LOCAL_INFILE = 0x00000080;
+    private const CLIENT_PROTOCOL_41 = 0x00000200;
+    private const CLIENT_SSL = 0x00000800;
+    private const CLIENT_TRANSACTIONS = 0x00002000;
+    private const CLIENT_SECURE_CONNECTION = 0x00008000;
+    private const CLIENT_MULTI_STATEMENTS = 0x00010000;
+    private const CLIENT_MULTI_RESULTS = 0x00020000;
+    private const CLIENT_PS_MULTI_RESULTS = 0x00040000;
+    private const CLIENT_PLUGIN_AUTH = 0x00080000;
+    private const CLIENT_CONNECT_ATTRS = 0x00100000;
+    private const CLIENT_SESSION_TRACK = 0x00800000;
+    private const CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 0x00200000;
+    private const CLIENT_DEPRECATE_EOF = 0x01000000;
+
+    private const OK_PACKET = 0x00;
+    private const EXTRA_AUTH_PACKET = 0x01;
+    private const LOCAL_INFILE_REQUEST = 0xfb;
+    private const EOF_PACKET = 0xfe;
+    private const ERR_PACKET = 0xff;
+
+    private const UNCONNECTED = 0;
+    private const ESTABLISHED = 1;
+    private const READY = 2;
+    private const CLOSING = 3;
+    private const CLOSED = 4;
 
     public function __construct(EncryptableSocket $socket, ConnectionConfig $config)
     {
@@ -294,7 +294,12 @@ private function getDeferred(): Deferred
 
     private function appendTask(callable $callback): void
     {
-        if ($this->packetCallback || $this->parseCallback || !empty($this->onReady) || !empty($this->deferreds) || $this->connectionState !== self::READY) {
+        if ($this->packetCallback
+            || $this->parseCallback
+            || !empty($this->onReady)
+            || !empty($this->deferreds)
+            || $this->connectionState !== self::READY
+        ) {
             $this->onReady[] = $callback;
         } else {
             $callback();
@@ -636,10 +641,17 @@ public function fetchStmt(int $stmtId): Promise
         return $deferred->promise();
     }
 
-    private function established()
+    private function established(): void
     {
         // @TODO flags to use?
-        $this->capabilities |= self::CLIENT_SESSION_TRACK | self::CLIENT_TRANSACTIONS | self::CLIENT_PROTOCOL_41 | self::CLIENT_SECURE_CONNECTION | self::CLIENT_MULTI_RESULTS | self::CLIENT_PS_MULTI_RESULTS | self::CLIENT_MULTI_STATEMENTS | self::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA;
+        $this->capabilities |= self::CLIENT_SESSION_TRACK
+            | self::CLIENT_TRANSACTIONS
+            | self::CLIENT_PROTOCOL_41
+            | self::CLIENT_SECURE_CONNECTION
+            | self::CLIENT_MULTI_RESULTS
+            | self::CLIENT_PS_MULTI_RESULTS
+            | self::CLIENT_MULTI_STATEMENTS
+            | self::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA;
 
         if (\extension_loaded("zlib") && $this->config->isCompressionEnabled()) {
             $this->capabilities |= self::CLIENT_COMPRESS;
@@ -651,7 +663,7 @@ private function established()
     }
 
     /** @see 14.1.3.2 ERR-Packet */
-    private function handleError($packet)
+    private function handleError(string $packet): void
     {
         $off = 1;
 
@@ -690,7 +702,7 @@ private function handleError($packet)
     }
 
     /** @see 14.1.3.1 OK-Packet */
-    private function parseOk($packet)
+    private function parseOk(string $packet): void
     {
         $off = 1;
 
@@ -745,7 +757,7 @@ private function parseOk($packet)
         }
     }
 
-    private function handleOk($packet)
+    private function handleOk(string $packet): void
     {
         $this->parseOk($packet);
         $this->getDeferred()->resolve(new CommandResult($this->connInfo->affectedRows, $this->connInfo->insertId));
@@ -753,7 +765,7 @@ private function handleOk($packet)
     }
 
     /** @see 14.1.3.3 EOF-Packet */
-    private function parseEof($packet)
+    private function parseEof(string $packet): void
     {
         if ($this->capabilities & self::CLIENT_PROTOCOL_41) {
             $this->connInfo->warnings = DataTypes::decodeUnsigned16(\substr($packet, 1));
@@ -762,7 +774,7 @@ private function parseEof($packet)
         }
     }
 
-    private function handleEof($packet)
+    private function handleEof(string $packet): void
     {
         $this->parseEof($packet);
         $exception = new FailureException($this->connInfo->errorMsg, $this->connInfo->errorCode);
@@ -771,7 +783,7 @@ private function handleEof($packet)
     }
 
     /** @see 14.2.5 Connection Phase Packets */
-    private function handleHandshake($packet)
+    private function handleHandshake(string $packet): void
     {
         $off = 1;
 
@@ -824,7 +836,7 @@ private function handleHandshake($packet)
     }
 
     /** @see 14.6.4.1.2 LOCAL INFILE Request */
-    private function handleLocalInfileRequest($packet)
+    private function handleLocalInfileRequest(string $packet): void
     {
         \Amp\asyncCall(function () use ($packet) {
             try {
@@ -842,7 +854,7 @@ private function handleLocalInfileRequest($packet)
     }
 
     /** @see 14.6.4.1.1 Text Resultset */
-    private function handleQuery($packet)
+    private function handleQuery(string $packet): void
     {
         switch (\ord($packet)) {
             case self::OK_PACKET:
@@ -878,7 +890,7 @@ private function handleQuery($packet)
     }
 
     /** @see 14.7.1 Binary Protocol Resultset */
-    private function handleExecute($packet)
+    private function handleExecute(string $packet): void
     {
         $this->parseCallback = [$this, "handleBinaryColumnDefinition"];
         $this->getDeferred()->resolve($result = new ResultProxy);
@@ -887,7 +899,7 @@ private function handleExecute($packet)
         $result->setColumns(\ord($packet));
     }
 
-    private function handleFieldList($packet)
+    private function handleFieldList(string $packet): void
     {
         if (\ord($packet) === self::ERR_PACKET) {
             $this->parseCallback = null;
@@ -903,17 +915,17 @@ private function handleFieldList($packet)
         }
     }
 
-    private function handleTextColumnDefinition($packet)
+    private function handleTextColumnDefinition(string $packet): void
     {
-        $this->handleColumnDefinition($packet, "handleTextResultsetRow");
+        $this->handleColumnDefinition($packet, "handleTextResultSetRow");
     }
 
-    private function handleBinaryColumnDefinition($packet)
+    private function handleBinaryColumnDefinition(string $packet): void
     {
-        $this->handleColumnDefinition($packet, "handleBinaryResultsetRow");
+        $this->handleColumnDefinition($packet, "handleBinaryResultSetRow");
     }
 
-    private function handleColumnDefinition($packet, $cbMethod)
+    private function handleColumnDefinition(string $packet, string $cbMethod): void
     {
         if (!$this->result->columnsToFetch--) {
             $this->result->updateState(ResultProxy::COLUMNS_FETCHED);
@@ -935,7 +947,7 @@ private function handleColumnDefinition($packet, $cbMethod)
         $this->result->columns[] = $this->parseColumnDefinition($packet);
     }
 
-    private function prepareParams($packet)
+    private function prepareParams(string $packet): void
     {
         if (!$this->result->columnsToFetch--) {
             $this->result->columnsToFetch = $this->result->columnCount;
@@ -950,7 +962,7 @@ private function prepareParams($packet)
         $this->result->params[] = $this->parseColumnDefinition($packet);
     }
 
-    private function prepareFields($packet)
+    private function prepareFields(string $packet): void
     {
         if (!$this->result->columnsToFetch--) {
             $this->parseCallback = null;
@@ -967,7 +979,7 @@ private function prepareFields($packet)
     }
 
     /** @see 14.6.4.1.1.2 Column Defintion */
-    private function parseColumnDefinition($packet)
+    private function parseColumnDefinition(string $packet): array
     {
         $off = 0;
 
@@ -1029,7 +1041,7 @@ private function parseColumnDefinition($packet)
         return $column;
     }
 
-    private function successfulResultsetFetch()
+    private function successfulResultsetFetch(): void
     {
         $result = $this->result;
         $deferred = &$result->next;
@@ -1050,7 +1062,7 @@ private function successfulResultsetFetch()
     }
 
     /** @see 14.6.4.1.1.3 Resultset Row */
-    private function handleTextResultsetRow($packet)
+    private function handleTextResultSetRow(string $packet): void
     {
         $packettype = \ord($packet);
         if ($packettype === self::EOF_PACKET) {
@@ -1082,7 +1094,7 @@ private function handleTextResultsetRow($packet)
     }
 
     /** @see 14.7.2 Binary Protocol Resultset Row */
-    private function handleBinaryResultsetRow($packet)
+    private function handleBinaryResultSetRow(string $packet): void
     {
         $packettype = \ord($packet);
         if ($packettype === self::EOF_PACKET) {
@@ -1119,7 +1131,7 @@ private function handleBinaryResultsetRow($packet)
     }
 
     /** @see 14.7.4.1 COM_STMT_PREPARE Response */
-    private function handlePrepare($packet)
+    private function handlePrepare(string $packet): void
     {
         switch (\ord($packet)) {
             case self::OK_PACKET:
@@ -1158,7 +1170,7 @@ private function handlePrepare($packet)
         }
     }
 
-    private function readStatistics($packet): void
+    private function readStatistics(string $packet): void
     {
         $this->getDeferred()->resolve($packet);
         $this->ready();
@@ -1342,13 +1354,16 @@ private function parseMysql(): \Generator
         }
     }
 
-    private function parsePayload($packet): void
+    private function parsePayload(string $packet): void
     {
         if ($this->connectionState === self::UNCONNECTED) {
             $this->established();
             $this->connectionState = self::ESTABLISHED;
             $this->handleHandshake($packet);
-        } elseif ($this->connectionState === self::ESTABLISHED) {
+            return;
+        }
+
+        if ($this->connectionState === self::ESTABLISHED) {
             switch (\ord($packet)) {
                 case self::OK_PACKET:
                     if ($this->capabilities & self::CLIENT_COMPRESS) {
@@ -1373,34 +1388,35 @@ private function parsePayload($packet): void
                     }
                     break;
             }
-        } else {
-            if ($this->parseCallback) {
-                ($this->parseCallback)($packet);
-                return;
-            }
+            return;
+        }
 
-            $cb = $this->packetCallback;
-            $this->packetCallback = null;
-            switch (\ord($packet)) {
-                case self::OK_PACKET:
-                    $this->handleOk($packet);
-                    break;
-                case self::ERR_PACKET:
-                    $this->handleError($packet);
+        if ($this->parseCallback) {
+            ($this->parseCallback)($packet);
+            return;
+        }
+
+        $cb = $this->packetCallback;
+        $this->packetCallback = null;
+        switch (\ord($packet)) {
+            case self::OK_PACKET:
+                $this->handleOk($packet);
+                break;
+            case self::ERR_PACKET:
+                $this->handleError($packet);
+                break;
+            case self::EOF_PACKET:
+                if (\strlen($packet) < 6) {
+                    $this->handleEof($packet);
                     break;
-                case self::EOF_PACKET:
-                    if (\strlen($packet) < 6) {
-                        $this->handleEof($packet);
-                        break;
-                    }
-                    // no break
-                default:
-                    if ($cb) {
-                        $cb($packet);
-                    } else {
-                        throw new ConnectionException("Unexpected packet type: " . \ord($packet));
-                    }
-            }
+                }
+                // no break
+            default:
+                if (!$cb) {
+                    throw new ConnectionException("Unexpected packet type: " . \ord($packet));
+                }
+
+                $cb($packet);
         }
     }
 
@@ -1416,7 +1432,7 @@ private function sha256Auth(string $pass, string $scramble, string $key): string
         return $auth;
     }
 
-    private function authSwitchRequest($packet): void
+    private function authSwitchRequest(string $packet): void
     {
         $this->parseCallback = null;
         switch (\ord($packet)) {
@@ -1441,7 +1457,7 @@ private function authSwitchRequest($packet): void
      * @see 14.2.5 Connection Phase Packets
      * @see 14.3 Authentication Method
      */
-    private function sendHandshake($inSSL = false): void
+    private function sendHandshake(bool $inSSL = false): void
     {
         if ($this->config->getDatabase() !== null) {
             $this->capabilities |= self::CLIENT_CONNECT_WITH_DB;
diff --git a/src/Internal/ResultProxy.php b/src/Internal/ResultProxy.php
index 20e1c39..99c7fc9 100644
--- a/src/Internal/ResultProxy.php
+++ b/src/Internal/ResultProxy.php
@@ -17,7 +17,7 @@ final class ResultProxy
     public $fetchedRows = 0;
     public $userFetched = 0;
 
-    public $deferreds = [self::SINGLE_ROW_FETCH => [], self::COLUMNS_FETCHED => [], self::ROWS_FETCHED => []];
+    public $deferreds = [self::UNFETCHED => [], self::COLUMNS_FETCHED => [], self::ROWS_FETCHED => []];
 
     /** @var int */
     public $state = self::UNFETCHED;
@@ -25,18 +25,18 @@ final class ResultProxy
     /** @var \Amp\Deferred|null */
     public $next;
 
-    const UNFETCHED = 0;
-    const COLUMNS_FETCHED = 1;
-    const ROWS_FETCHED = 2;
+    public const UNFETCHED = 0;
+    public const COLUMNS_FETCHED = 1;
+    public const ROWS_FETCHED = 2;
 
-    const SINGLE_ROW_FETCH = 255;
+    public const SINGLE_ROW_FETCH = 255;
 
-    public function setColumns(int $columns)
+    public function setColumns(int $columns): void
     {
         $this->columnCount = $this->columnsToFetch = $columns;
     }
 
-    public function updateState(int $state)
+    public function updateState(int $state): void
     {
         $this->state = $state;
         if ($state === self::ROWS_FETCHED) {
@@ -45,28 +45,28 @@ public function updateState(int $state)
         if (empty($this->deferreds[$state])) {
             return;
         }
-        foreach ($this->deferreds[$state] as list($deferred, $rows, $cb)) {
+        foreach ($this->deferreds[$state] as [$deferred, $rows, $cb]) {
             $deferred->resolve($cb ? $cb($rows) : $rows);
         }
         $this->deferreds[$state] = [];
     }
 
-    public function rowFetched($row)
+    public function rowFetched(?array $row): void
     {
         if ($row !== null) {
             $this->rows[$this->fetchedRows++] = $row;
         }
-        list($entry, , $cb) = \current($this->deferreds[self::SINGLE_ROW_FETCH]);
+        [$entry, , $cb] = \current($this->deferreds[self::UNFETCHED]);
         if ($entry !== null) {
-            unset($this->deferreds[self::SINGLE_ROW_FETCH][\key($this->deferreds[self::SINGLE_ROW_FETCH])]);
+            unset($this->deferreds[self::UNFETCHED][\key($this->deferreds[self::UNFETCHED])]);
             $entry->resolve($cb && $row ? $cb($row) : $row);
         }
     }
 
-    public function fail(FailureException $e)
+    public function fail(FailureException $e): void
     {
         foreach ($this->deferreds as $state) {
-            foreach ($this->deferreds[$state] as list($deferred)) {
+            foreach ($this->deferreds[$state] as [$deferred]) {
                 $deferred->fail($e);
             }
             $this->deferreds[$state] = [];

From 931f00877cde386c7593f66391193e17026b8bd8 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Wed, 30 Oct 2019 10:25:23 -0500
Subject: [PATCH 015/174] Remove circular reference in destructor

---
 src/ConnectionResultSet.php | 27 +++++++++++++--------------
 1 file changed, 13 insertions(+), 14 deletions(-)

diff --git a/src/ConnectionResultSet.php b/src/ConnectionResultSet.php
index 43fa972..a813c88 100644
--- a/src/ConnectionResultSet.php
+++ b/src/ConnectionResultSet.php
@@ -2,12 +2,13 @@
 
 namespace Amp\Mysql;
 
-use Amp\Coroutine;
 use Amp\Deferred;
 use Amp\Iterator;
 use Amp\Producer;
 use Amp\Promise;
 use Amp\Success;
+use function Amp\asyncCall;
+use function Amp\call;
 
 final class ConnectionResultSet implements ResultSet
 {
@@ -43,20 +44,18 @@ private static function makeIterator(Internal\ResultProxy $result): Iterator
 
     public function __destruct()
     {
-        if ($this->result) { // All results were not necessarily consumed.
-            Promise\rethrow(new Coroutine($this->dispose()));
+        if (!$this->result) {
+            return;
         }
-    }
 
-    private function dispose(): \Generator
-    {
-        try {
-            do {
-                while (yield $this->advance()); // Discard unused result rows.
-            } while (yield $this->nextResultSet());
-        } catch (\Throwable $exception) {
-            // Ignore failure while discarding results.
-        }
+        $producer = $this->producer;
+        asyncCall(static function () use ($producer) {
+            try {
+                while (yield $producer->advance());
+            } catch (\Throwable $exception) {
+                // Ignore iterator failure when destroying.
+            }
+        });
     }
 
     /**
@@ -122,7 +121,7 @@ public function nextResultSet(): Promise
             return new Success(false);
         }
 
-        return \Amp\call(function () {
+        return call(function () {
             while (yield $this->advance()); // Consume any values left in the current result.
 
             $this->columnNames = null;

From 361d93d2c2a6c4253ad258bc5f242043aacf4a8c Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Wed, 30 Oct 2019 10:26:33 -0500
Subject: [PATCH 016/174] Fix ConnectionConfig key map

The key map was not being used prior.
---
 src/ConnectionConfig.php | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/ConnectionConfig.php b/src/ConnectionConfig.php
index 4874d22..886cc9b 100644
--- a/src/ConnectionConfig.php
+++ b/src/ConnectionConfig.php
@@ -11,12 +11,15 @@ final class ConnectionConfig extends SqlConnectionConfig
     const BIN_CHARSET = 45; // utf8mb4_general_ci
 
     const KEY_MAP = [
+        'hostname' => 'host',
         'username' => 'user',
-        'password' => 'pass',
+        'pass' => 'password',
         'database' => 'db',
         'dbname' => 'db',
-        'compress' => 'useCompression',
-        'charset' => 'charset',
+        'compress' => 'compression',
+        'useCompression' => 'compression',
+        'cs' => 'charset',
+        'localInfile' => 'local-infile',
     ];
 
     const DEFAULT_CHARSET = "utf8mb4";
@@ -39,7 +42,7 @@ final class ConnectionConfig extends SqlConnectionConfig
 
     public static function fromString(string $connectionString, ClientTlsContext $tlsContext = null): self
     {
-        $parts = self::parseConnectionString($connectionString);
+        $parts = self::parseConnectionString($connectionString, self::KEY_MAP);
 
         if (!isset($parts["host"])) {
             throw new \Error("Host must be provided in connection string");

From b0b3eed14c5ab76807e7e4b66136f6cffa21b5c1 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Wed, 30 Oct 2019 10:36:13 -0500
Subject: [PATCH 017/174] Use one value for repeated parameter name

Fixes #90.

This mirrors the behavior of amphp/postgres and PDO.
---
 src/ConnectionStatement.php | 19 +++++--------------
 1 file changed, 5 insertions(+), 14 deletions(-)

diff --git a/src/ConnectionStatement.php b/src/ConnectionStatement.php
index 5ba6cb0..e4d9268 100644
--- a/src/ConnectionStatement.php
+++ b/src/ConnectionStatement.php
@@ -118,26 +118,17 @@ public function execute(array $params = []): Promise
         for ($unnamed = $i = 0; $i < $this->paramCount; $i++) {
             if (isset($this->named[$i])) {
                 $name = $this->named[$i];
-                if (\array_key_exists($name, $params) && $params[$name] !== []) {
-                    if (\is_array($params[$name])) {
-                        $args[$i] = \reset($params[$name]);
-                        unset($params[$name][\key($params[$name])]);
-                    } else {
-                        $args[$i] = $params[$name];
-                        unset($params[$name]);
-                    }
-                } elseif (!isset($this->prebound[$name])) {
-                    if ($params[$name] === []) {
-                        throw new \Error("Named parameter $name is not providing enough elements");
-                    }
-                    throw new \Error("Named parameter $name missing for executing prepared statement");
+                if (\array_key_exists($name, $params)) {
+                    $args[$i] = $params[$name];
+                } elseif (!\array_key_exists($name, $this->prebound)) {
+                    throw new \Error("Named parameter '$name' missing for executing prepared statement");
                 } else {
                     $prebound[$i] = $this->prebound[$name];
                 }
             } elseif (\array_key_exists($unnamed, $params)) {
                 $args[$i] = $params[$unnamed];
                 $unnamed++;
-            } elseif (!isset($this->prebound[$unnamed])) {
+            } elseif (!\array_key_exists($unnamed, $this->prebound)) {
                 throw new \Error("Parameter $unnamed for prepared statement missing");
             } else {
                 $prebound[$i] = $this->prebound[$unnamed++];

From 000e0b41560ad81c5ec680162f603cec6e908651 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Wed, 30 Oct 2019 10:50:14 -0500
Subject: [PATCH 018/174] =?UTF-8?q?Add=20test=20for=20INSERT=20=E2=80=A6?=
 =?UTF-8?q?=20SELECT?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Test for #89.
---
 test/LinkTest.php | 32 ++++++++++++++++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/test/LinkTest.php b/test/LinkTest.php
index 87a5827..2d2485e 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -2,6 +2,7 @@
 
 namespace Amp\Mysql\Test;
 
+use Amp\Iterator;
 use Amp\Mysql\CommandResult;
 use Amp\Mysql\DataTypes;
 use Amp\Mysql\ResultSet;
@@ -332,4 +333,35 @@ public function testTransaction()
         }
         $this->assertCount(0, $got);
     }
+
+    /**
+     * @depends testTransaction
+     */
+    public function testInsertSelect()
+    {
+        /** @var Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+
+        $a = 1;
+
+        /** @var Transaction $transaction */
+        $transaction = yield $db->beginTransaction();
+
+        try {
+            /** @var Statement $statement */
+            $statement = yield $transaction->prepare("SELECT a, b FROM main WHERE a >= ?");
+
+            $count = \count(yield Iterator\toArray(yield $statement->execute([$a])));
+
+            /** @var Statement $statement */
+            $statement = yield $transaction->prepare("INSERT INTO main (a, b) SELECT a, b FROM main WHERE a >= ?");
+
+            /** @var CommandResult $result */
+            $result = yield $statement->execute([$a]);
+
+            $this->assertSame($count, $result->getAffectedRowCount());
+        } finally {
+            yield $transaction->rollback();
+        }
+    }
 }

From 1f9e5ef10608ef69191a9fd66bdc92ed2a3991da Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 14 Nov 2019 09:49:41 -0600
Subject: [PATCH 019/174] Fix processor state after error

Fixes #91.
---
 src/Internal/Processor.php | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index c126b9c..c289cc5 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -695,9 +695,11 @@ private function handleError(string $packet): void
         $deferred = $this->result ?? $this->getDeferred();
 
         // normal error
-        $deferred->fail(new QueryError("MySQL error ({$this->connInfo->errorCode}): {$this->connInfo->errorState} {$this->connInfo->errorMsg}", $this->query));
+        $exception = new QueryError("MySQL error ({$this->connInfo->errorCode}): {$this->connInfo->errorState} {$this->connInfo->errorMsg}", $this->query);
         $this->result = null;
         $this->query = null;
+        $deferred->fail($exception);
+
         $this->ready();
     }
 

From 47be8eb2f8468fd7c3058f14f9c162dba4fc6157 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Fri, 15 Nov 2019 17:59:23 -0600
Subject: [PATCH 020/174] Attach exception thrown while reading to exception
 failing op

Closes #92.
---
 src/Internal/Processor.php | 80 ++++++++++++++++++++------------------
 1 file changed, 42 insertions(+), 38 deletions(-)

diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index c289cc5..adf8835 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -199,7 +199,7 @@ public function connect(CancellationToken $token): Promise
     {
         \assert(!$this->processors, self::class."::connect() must not be called twice");
 
-        $this->deferreds[] = $deferred = new Deferred; // Will be resolved below or in sendHandshake().
+        $this->deferreds[] = $deferred = new Deferred; // Will be resolved in sendHandshake().
 
         $this->processors = [$this->parseMysql()];
 
@@ -207,17 +207,7 @@ public function connect(CancellationToken $token): Promise
             $this->close();
         });
 
-        (new Coroutine($this->read()))->onResolve(function (): void {
-            $this->close();
-
-            foreach ($this->deferreds as $deferred) {
-                if ($this->query === "") {
-                    $deferred->fail(new InitializationException("Connection went away"));
-                } else {
-                    $deferred->fail(new ConnectionException("Connection went away... unable to fulfil this deferred ... It's unknown whether the query was executed..."));
-                }
-            }
-        });
+        Promise\rethrow(new Coroutine($this->read()));
 
         $promise = $deferred->promise();
 
@@ -243,31 +233,45 @@ public function connect(CancellationToken $token): Promise
 
     private function read(): \Generator
     {
-        while (($bytes = yield $this->socket->read()) !== null) {
-            // @codeCoverageIgnoreStart
-            \assert((function () use ($bytes) {
-                if (\defined("MYSQL_DEBUG")) {
-                    \fwrite(STDERR, "in: ");
-                    for ($i = 0; $i < \min(\strlen($bytes), 200); $i++) {
-                        \fwrite(STDERR, \dechex(\ord($bytes[$i])) . " ");
+        try {
+            while (($bytes = yield $this->socket->read()) !== null) {
+                // @codeCoverageIgnoreStart
+                \assert((function () use ($bytes) {
+                    if (\defined("MYSQL_DEBUG")) {
+                        \fwrite(STDERR, "in: ");
+                        for ($i = 0; $i < \min(\strlen($bytes), 200); $i++) {
+                            \fwrite(STDERR, \dechex(\ord($bytes[$i])) . " ");
+                        }
+                        $r = \range("\0", "\x1f");
+                        unset($r[10], $r[9]);
+                        \fwrite(STDERR, "len: " . \strlen($bytes) . "\n");
+                        \fwrite(STDERR, \str_replace($r, ".", \substr($bytes, 0, 200)) . "\n");
                     }
-                    $r = \range("\0", "\x1f");
-                    unset($r[10], $r[9]);
-                    \fwrite(STDERR, "len: ".\strlen($bytes)."\n");
-                    \fwrite(STDERR, \str_replace($r, ".", \substr($bytes, 0, 200))."\n");
-                }
 
-                return true;
-            })());
-            // @codeCoverageIgnoreEnd
+                    return true;
+                })());
+                // @codeCoverageIgnoreEnd
 
-            $this->lastUsedAt = \time();
+                $this->lastUsedAt = \time();
 
-            $this->processData($bytes);
-            $bytes = null; // Free last data read.
+                $this->processData($bytes);
+                $bytes = null; // Free last data read.
 
-            if (!$this->socket) { // Connection closed.
-                break;
+                if (!$this->socket) { // Connection closed.
+                    break;
+                }
+            }
+        } catch (\Throwable $exception) {
+            // $exception used as previous exception below.
+        } finally {
+            $this->close();
+        }
+
+        if (!empty($this->deferreds)) {
+            $exception = new ConnectionException("Connection closed unexpectedly", 0, $exception ?? null);
+
+            foreach ($this->deferreds as $deferred) {
+                $deferred->fail($exception);
             }
         }
     }
@@ -1047,17 +1051,17 @@ private function successfulResultsetFetch(): void
     {
         $result = $this->result;
         $deferred = &$result->next;
+        if (!$deferred) {
+            $deferred = new Deferred;
+        }
         if ($this->connInfo->statusFlags & StatusFlags::SERVER_MORE_RESULTS_EXISTS) {
             $this->parseCallback = [$this, "handleQuery"];
-            $this->addDeferred($deferred ?: $deferred = new Deferred);
+            $this->addDeferred($deferred);
         } else {
-            if (!$deferred) {
-                $deferred = new Deferred;
-            }
-            $deferred->resolve();
             $this->parseCallback = null;
             $this->query = null;
             $this->result = null;
+            $deferred->resolve();
             $this->ready();
         }
         $result->updateState(ResultProxy::ROWS_FETCHED);
@@ -1175,8 +1179,8 @@ private function handlePrepare(string $packet): void
     private function readStatistics(string $packet): void
     {
         $this->getDeferred()->resolve($packet);
-        $this->ready();
         $this->parseCallback = null;
+        $this->ready();
     }
 
     /** @see 14.6.2 COM_QUIT */

From cbef7dccaf076218470267a2739aa0f3b49511f1 Mon Sep 17 00:00:00 2001
From: Bob Weinand <bobwei9@hotmail.com>
Date: Tue, 10 Dec 2019 12:02:12 +0100
Subject: [PATCH 021/174] Interpret a filesystem path host as an unix domain
 socket

---
 src/ConnectionConfig.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/ConnectionConfig.php b/src/ConnectionConfig.php
index 125ff7c..181bd4f 100644
--- a/src/ConnectionConfig.php
+++ b/src/ConnectionConfig.php
@@ -85,7 +85,7 @@ public function __construct(
 
     public function getConnectionString(): string
     {
-        return 'tcp://' . $this->getHost() . ':' . $this->getPort();
+        return $this->getHost()[0] == "/" ? 'unix://' . $this->getHost() : 'tcp://' . $this->getHost() . ':' . $this->getPort();
     }
 
     public function isCompressionEnabled(): bool

From 935d0031c1ffa6f7a38e00ce9cb0b5491800f814 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 29 Oct 2020 12:08:13 -0500
Subject: [PATCH 022/174] Update test bootstrap

---
 composer.json      |   1 +
 test/bootstrap.php | 123 +++++++++++++++++++++++++--------------------
 2 files changed, 69 insertions(+), 55 deletions(-)

diff --git a/composer.json b/composer.json
index efe023d..93c7f05 100644
--- a/composer.json
+++ b/composer.json
@@ -22,6 +22,7 @@
     },
     "require-dev": {
         "ext-openssl": "*",
+        "amphp/process": "^1",
         "phpunit/phpunit": "^8 || ^7",
         "amphp/phpunit-util": "^1.1.2",
         "amphp/php-cs-fixer-config": "dev-master",
diff --git a/test/bootstrap.php b/test/bootstrap.php
index 1bc4dde..36b203b 100644
--- a/test/bootstrap.php
+++ b/test/bootstrap.php
@@ -1,5 +1,12 @@
 <?php
 
+namespace Amp\Mysql\Test;
+
+use Amp\ByteStream;
+use Amp\Loop;
+use Amp\Process\Process;
+use function Amp\delay;
+
 require __DIR__.'/../vendor/autoload.php';
 
 const DB_HOST = 'localhost:10101';
@@ -7,69 +14,75 @@
 const DB_PASS = '';
 const DB_NAME = 'test';
 
-$pipes = [];
-$buf = "";
-
-/* cleanup in case it wasn't terminated properly... */
-$pidfile = __DIR__."/mysql.pid";
-if (\file_exists($pidfile)) {
-    if (\stripos(PHP_OS, "win") === 0) {
-        \shell_exec("Taskkill /PID ".\file_get_contents($pidfile)." /F");
-    } else {
-        \shell_exec("kill -9 `cat '$pidfile'` 2>/dev/null");
+(function (): void {
+    /* cleanup in case it wasn't terminated properly... */
+    $pidfile = __DIR__ . "/mysql.pid";
+    if (\file_exists($pidfile)) {
+        if (\stripos(PHP_OS, "win") === 0) {
+            \shell_exec("Taskkill /PID " . \file_get_contents($pidfile) . " /F");
+        } else {
+            \shell_exec("kill -9 `cat '$pidfile'` 2>/dev/null");
+        }
+        \sleep(1);
     }
-    \sleep(1);
-}
-if (\file_exists(__DIR__."/mysql_db")) {
-    $rm_all = function ($dir) use (&$rm_all) {
-        $files = \glob("$dir/*");
-        if (\is_array($files)) {
-            foreach ($files as $file) {
-                if (\is_dir($file)) {
-                    $rm_all($file);
-                    \rmdir($file);
-                } else {
-                    \unlink($file);
+
+    if (\file_exists(__DIR__ . "/mysql_db")) {
+        $rm_all = function ($dir) use (&$rm_all) {
+            $files = \glob("$dir/*");
+            if (\is_array($files)) {
+                foreach ($files as $file) {
+                    if (\is_dir($file)) {
+                        $rm_all($file);
+                        \rmdir($file);
+                    } else {
+                        \unlink($file);
+                    }
                 }
             }
-        }
-    };
-    $rm_all(__DIR__ . "/mysql_db");
-} else {
-    @\mkdir(__DIR__ . "/mysql_db");
-}
-
-$proc = \proc_open("mysqld --defaults-file=my.cnf --initialize-insecure", [2 => ["pipe", "w"]], $pipes, __DIR__);
-$stderr = $pipes[2];
-do {
-    if (!($row = \fgets($stderr)) || \preg_match("# \[ERROR\] #", $row)) {
-        print "\nERROR: Aborting, couldn't start mysql successfully\n$buf$row";
-        exit(127);
+        };
+        $rm_all(__DIR__ . "/mysql_db");
+    } else {
+        @\mkdir(__DIR__ . "/mysql_db");
     }
-    $buf .= $row;
-} while (!\preg_match("(root@localhost is created with an empty password)", $row));
-\sleep(3); // :-(
-\proc_terminate($proc, 9);
-@\proc_terminate($proc, 9);
+})();
 
-$proc = \proc_open("mysqld --defaults-file=my.cnf --user=root", [2 => ["pipe", "w"]], $pipes, __DIR__);
+Loop::run(function () {
+    print "\rCreating mysql server... ";
 
-\register_shutdown_function(function () use ($proc) {
-    \proc_terminate($proc, 9);
-});
+    $dir = __DIR__;
+
+    $process = new Process("mysqld --defaults-file={$dir}/my.cnf --initialize-insecure", __DIR__);
 
-$stderr = $pipes[2];
-do {
-    if (!($row = \fgets($stderr)) || \preg_match("# \[ERROR\] #", $row)) {
-        print "\nERROR: Aborting, couldn't start mysql successfully\n$buf$row";
+    yield $process->start();
+
+    $stderr = yield ByteStream\buffer($process->getStderr());
+
+    if (\preg_match("# \[ERROR\] #", $stderr)) {
+        print "\nERROR: Aborting, couldn't start mysql successfully\n{$stderr}";
         exit(127);
     }
-    $buf .= $row;
-} while (!\preg_match("(^Version: '[0-9.a-zA-Z]+')", $row));
 
+    yield $process->join();
+
+    $process = new Process("mysqld --defaults-file={$dir}/my.cnf --user=root", __DIR__);
+
+    yield $process->start();
+
+    print "\rStarting mysqld...       ";
 
-$db = new mysqli(DB_HOST, DB_USER, DB_PASS);
-$db->query("CREATE DATABASE test");
-$db->query("CREATE TABLE test.main (a INT(11), b INT(11))");
-$db->query("INSERT INTO test.main VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)");
-$db->close();
+    yield delay(1000); // Give mysqld time to start.
+
+    print "\rCreating test database...";
+
+    $db = new \mysqli(DB_HOST, DB_USER, DB_PASS);
+    $db->query("CREATE DATABASE test");
+    $db->query("CREATE TABLE test.main (a INT(11), b INT(11))");
+    $db->query("INSERT INTO test.main VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)");
+    $db->close();
+
+    print "\r";
+
+    \register_shutdown_function(function () use ($process): void {
+        $process->signal(\SIGTERM);
+    });
+});

From 101c6cfd93ef16053b21a826084ff511cb0cba7b Mon Sep 17 00:00:00 2001
From: Bob Weinand <bobwei9@hotmail.com>
Date: Mon, 9 Nov 2020 23:53:18 +0100
Subject: [PATCH 023/174] Add plugin auth (in particular caching_sha2_password)

---
 src/Internal/Processor.php | 32 +++++++++++++++++++++++++++++---
 1 file changed, 29 insertions(+), 3 deletions(-)

diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index adf8835..85189ce 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -655,6 +655,7 @@ private function established(): void
             | self::CLIENT_MULTI_RESULTS
             | self::CLIENT_PS_MULTI_RESULTS
             | self::CLIENT_MULTI_STATEMENTS
+            | self::CLIENT_PLUGIN_AUTH
             | self::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA;
 
         if (\extension_loaded("zlib") && $this->config->isCompressionEnabled()) {
@@ -1389,6 +1390,23 @@ private function parsePayload(string $packet): void
                             $this->config = $this->config->withKey($key);
                             $this->sendHandshake();
                             break;
+                        case "caching_sha2_password":
+                            switch (\ord(substr($packet, 1, 1))) {
+                                case 3: // success
+                                    return; // expecting OK afterwards
+                                case 4: // fast auth failure
+                                    if ($this->capabilities & self::CLIENT_SSL || $this->config->getHost()[0] === "/" /* unix domain socket, information not trivially available from $this->socket */) {
+                                        $this->write($this->config->getPassword() . "\0");
+                                    } else {
+                                        $this->write("\x02");
+                                    }
+                                    break;
+                                case 0x2d: // certificate
+                                    $pubkey = substr($packet, 1);
+                                    $this->write($this->sha256Auth($this->config->getPassword(), $this->authPluginData, $pubkey));
+                                    break;
+                            }
+                            break;
                         default:
                             throw new ConnectionException("Unexpected EXTRA_AUTH_PACKET in authentication phase for method {$this->authPluginName}");
                     }
@@ -1434,10 +1452,16 @@ private function secureAuth(string $pass, string $scramble): string
 
     private function sha256Auth(string $pass, string $scramble, string $key): string
     {
-        \openssl_public_encrypt($pass ^ \str_repeat($scramble, \ceil(\strlen($pass) / \strlen($scramble))), $auth, $key, OPENSSL_PKCS1_OAEP_PADDING);
+        \openssl_public_encrypt("$pass\0" ^ \str_repeat($scramble, \ceil(\strlen($pass) / \strlen($scramble))), $auth, $key, OPENSSL_PKCS1_OAEP_PADDING);
         return $auth;
     }
 
+    private function sha2Auth(string $pass, string $scramble): string
+    {
+        $hash = \hash("sha256", $pass, true);
+        return $hash ^ \hash("sha256", \substr($scramble, 0, 20) . \hash("sha256", $hash, true), true);
+    }
+
     private function authSwitchRequest(string $packet): void
     {
         $this->parseCallback = null;
@@ -1520,6 +1544,9 @@ private function sendHandshake(bool $inSSL = false): void
                             $auth = "\x01";
                         }
                     }
+		    break;
+                case "caching_sha2_password":
+                    $auth = $this->sha2Auth($this->config->getPassword(), $this->authPluginData);
                     break;
                 case "mysql_old_password":
                     throw new ConnectionException("mysql_old_password is outdated and insecure. Intentionally not implemented!");
@@ -1542,8 +1569,7 @@ private function sendHandshake(bool $inSSL = false): void
             $payload .= "{$this->config->getDatabase()}\0";
         }
         if ($this->capabilities & self::CLIENT_PLUGIN_AUTH) {
-            $payload .= "\0"; // @TODO AUTH
-//            $payload .= "mysql_native_password\0";
+            $payload .= "{$this->authPluginName}\0";
         }
         if ($this->capabilities & self::CLIENT_CONNECT_ATTRS) {
             // connection attributes?! 5.6.6+ only!

From 01d4b6c46172dc6e6a262da91d7ff2256806b88c Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 9 Nov 2020 21:10:46 -0600
Subject: [PATCH 024/174] Add session state types

---
 src/Internal/Processor.php | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 85189ce..2b16b51 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -48,6 +48,9 @@ final class SessionStateTypes
     public const SESSION_TRACK_SYSTEM_VARIABLES = 0x00;
     public const SESSION_TRACK_SCHEMA = 0x01;
     public const SESSION_TRACK_STATE_CHANGE = 0x02;
+    public const SESSION_TRACK_GTIDS = 0x03;
+    public const SESSION_TRACK_TRANSACTION_CHARACTERISTICS = 0x04;
+    public const SESSION_TRACK_TRANSACTION_STATE = 0x05;
 }
 
 /** @internal */
@@ -741,13 +744,14 @@ private function parseOk(string $packet): void
                         switch ($type = DataTypes::decodeUnsigned8(\substr($sessionState, $len))) {
                             case SessionStateTypes::SESSION_TRACK_SYSTEM_VARIABLES:
                                 $var = DataTypes::decodeString($data, $varintlen, $strlen);
-                                $this->connInfo->sessionState[SessionStateTypes::SESSION_TRACK_SYSTEM_VARIABLES][$var] = DataTypes::decodeString(\substr($data, $varintlen + $strlen));
+                                $this->connInfo->sessionState[$type][$var] = DataTypes::decodeString(\substr($data, $varintlen + $strlen));
                                 break;
                             case SessionStateTypes::SESSION_TRACK_SCHEMA:
-                                $this->connInfo->sessionState[SessionStateTypes::SESSION_TRACK_SCHEMA] = DataTypes::decodeString($data);
-                                break;
                             case SessionStateTypes::SESSION_TRACK_STATE_CHANGE:
-                                $this->connInfo->sessionState[SessionStateTypes::SESSION_TRACK_STATE_CHANGE] = DataTypes::decodeString($data);
+                            case SessionStateTypes::SESSION_TRACK_GTIDS:
+                            case SessionStateTypes::SESSION_TRACK_TRANSACTION_CHARACTERISTICS:
+                            case SessionStateTypes::SESSION_TRACK_TRANSACTION_STATE:
+                                $this->connInfo->sessionState[$type] = DataTypes::decodeString($data);
                                 break;
                             default:
                                 throw new \Error("$type is not a valid mysql session state type");

From b6e9c25365dd34a269bd1038f44ddd53a952c2e1 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 9 Nov 2020 21:11:08 -0600
Subject: [PATCH 025/174] Remove prophecy from test

Deprecated in PHPUnit 9.
---
 composer.json          |  2 +-
 test/StatementTest.php | 29 +++++++++++++++++++----------
 2 files changed, 20 insertions(+), 11 deletions(-)

diff --git a/composer.json b/composer.json
index 93c7f05..c7c50bf 100644
--- a/composer.json
+++ b/composer.json
@@ -23,7 +23,7 @@
     "require-dev": {
         "ext-openssl": "*",
         "amphp/process": "^1",
-        "phpunit/phpunit": "^8 || ^7",
+        "phpunit/phpunit": "^9 || ^8 || ^7",
         "amphp/phpunit-util": "^1.1.2",
         "amphp/php-cs-fixer-config": "dev-master",
         "phpbench/phpbench": "^0.13.0"
diff --git a/test/StatementTest.php b/test/StatementTest.php
index bced299..593cff2 100644
--- a/test/StatementTest.php
+++ b/test/StatementTest.php
@@ -3,22 +3,24 @@
 namespace Amp\Mysql\Test;
 
 use Amp\Mysql\ConnectionStatement;
+use Amp\Mysql\Internal\Processor;
 use Amp\Mysql\Internal\ResultProxy;
 use Amp\PHPUnit\AsyncTestCase;
+use PHPUnit\Framework\MockObject\MockObject;
 
 class StatementTest extends AsyncTestCase
 {
-    /** @var \Prophecy\Prophecy\ObjectProphecy */
+    /** @var MockObject&Processor */
     protected $processor;
 
-    /** @var \Amp\Mysql\Internal\ResultProxy */
+    /** @var ResultProxy */
     protected $resultProxy;
 
     public function setUp(): void
     {
         parent::setUp();
 
-        $this->processor = $this->prophesize('Amp\Mysql\Internal\Processor');
+        $this->processor = $this->createMock(Processor::class);
         $this->resultProxy = new ResultProxy;
     }
 
@@ -33,19 +35,26 @@ public function testBindDataTypes($data, $expectedException)
         $paramId = 0;
         $named = [];
 
-        $this->processor->isAlive()->willReturn(true);
-        $this->processor->unreference()->shouldBeCalled();
-        $this->processor->closeStmt(\Prophecy\Argument::any())->shouldBeCalled();
+        $this->processor->method('isAlive')
+            ->willReturn(true);
+        $this->processor->expects($this->once())
+            ->method('unreference');
+        $this->processor->expects($this->once())
+            ->method('closeStmt');
+
         $this->resultProxy->columnsToFetch = 1;
-        $stmt = new ConnectionStatement($this->processor->reveal(), $query, $stmtId, $named, $this->resultProxy);
+        $stmt = new ConnectionStatement($this->processor, $query, $stmtId, $named, $this->resultProxy);
 
         // assert
         if ($expectedException) {
             $this->expectException($expectedException);
-            $this->processor->bindParam($stmtId, \Prophecy\Argument::any(), $data)->shouldNotBeCalled();
+            $this->processor->expects($this->never())
+                ->method('bindParam')
+                ->with($stmtId, $this->anything(), $data);
         } else {
-            $this->addToAssertionCount(1);
-            $this->processor->bindParam($stmtId, \Prophecy\Argument::any(), $data)->shouldBeCalled();
+            $this->processor->expects($this->once())
+                ->method('bindParam')
+                ->with($stmtId, $this->anything(), $data);
         }
 
         $this->assertSame($query, $stmt->getQuery());

From 87294955fe30fe56c6e89a063f2f15e9912ad20a Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 9 Nov 2020 21:16:09 -0600
Subject: [PATCH 026/174] Make amphp/file optional

Added support for amphp/file 2.x.
---
 composer.json              |  2 +-
 src/Internal/Processor.php | 14 ++++++++++++--
 2 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/composer.json b/composer.json
index c7c50bf..89ede75 100644
--- a/composer.json
+++ b/composer.json
@@ -15,7 +15,6 @@
     "require": {
         "php": ">=7.1",
         "amphp/amp": "^2",
-        "amphp/file": "^1 || ^0.3.5",
         "amphp/socket": "^1",
         "amphp/sql": "^1",
         "amphp/sql-common": "^1"
@@ -23,6 +22,7 @@
     "require-dev": {
         "ext-openssl": "*",
         "amphp/process": "^1",
+        "amphp/file": "^2 || ^1 || ^0.3.5",
         "phpunit/phpunit": "^9 || ^8 || ^7",
         "amphp/phpunit-util": "^1.1.2",
         "amphp/php-cs-fixer-config": "dev-master",
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 2b16b51..b637c6f 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -5,6 +5,7 @@
 use Amp\CancellationToken;
 use Amp\Coroutine;
 use Amp\Deferred;
+use Amp\File;
 use Amp\Loop;
 use Amp\Mysql\CommandResult;
 use Amp\Mysql\ConnectionConfig;
@@ -852,8 +853,17 @@ private function handleLocalInfileRequest(string $packet): void
         \Amp\asyncCall(function () use ($packet) {
             try {
                 $filePath = \substr($packet, 1);
-                /** @var \Amp\File\Handle $fileHandle */
-                $fileHandle = yield \Amp\File\open($filePath, 'r');
+                /** @var \Amp\File\File $fileHandle */
+                if (\function_exists("Amp\\File\\openFile")) {
+                    // amphp/file 2.x
+                    $fileHandle = yield File\openFile($filePath, 'r');
+                } elseif (\function_exists("Amp\\File\\open")) {
+                    // amphp/file 1.x or 0.3.x
+                    $fileHandle = yield File\open($filePath, 'r');
+                } else {
+                    throw new \Error("amphp/file must be installed for LOCAL INFILE queries");
+                }
+
                 while ("" != ($chunk = yield $fileHandle->read())) {
                     $this->sendPacket($chunk);
                 }

From 03c69546c07cacf4cb09790e77f0777e9d3ae7f7 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 9 Nov 2020 23:46:55 -0600
Subject: [PATCH 027/174] Fix code style

---
 src/Internal/Processor.php | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index b637c6f..ee3b2aa 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -1405,7 +1405,7 @@ private function parsePayload(string $packet): void
                             $this->sendHandshake();
                             break;
                         case "caching_sha2_password":
-                            switch (\ord(substr($packet, 1, 1))) {
+                            switch (\ord(\substr($packet, 1, 1))) {
                                 case 3: // success
                                     return; // expecting OK afterwards
                                 case 4: // fast auth failure
@@ -1416,7 +1416,7 @@ private function parsePayload(string $packet): void
                                     }
                                     break;
                                 case 0x2d: // certificate
-                                    $pubkey = substr($packet, 1);
+                                    $pubkey = \substr($packet, 1);
                                     $this->write($this->sha256Auth($this->config->getPassword(), $this->authPluginData, $pubkey));
                                     break;
                             }
@@ -1558,7 +1558,7 @@ private function sendHandshake(bool $inSSL = false): void
                             $auth = "\x01";
                         }
                     }
-		    break;
+            break;
                 case "caching_sha2_password":
                     $auth = $this->sha2Auth($this->config->getPassword(), $this->authPluginData);
                     break;

From d6275483d19178a8b801eba38b53620c3d8cafa8 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 28 May 2020 12:03:03 -0500
Subject: [PATCH 028/174] Refactor to new Stream API

---
 composer.json                     |   6 +-
 examples/2-simple-query.php       |   5 +-
 examples/3-generic-with-yield.php |   6 +-
 examples/4-multi-rows.php         |  12 +--
 examples/5-multi-stmts.php        |   8 +-
 examples/6-transaction.php        |  10 +-
 examples/test.php                 |  23 +++++
 src/CommandResult.php             |  36 -------
 src/Connection.php                |   6 +-
 src/ConnectionResultSet.php       | 160 ------------------------------
 src/ConnectionStatement.php       |   4 +-
 src/ConnectionTransaction.php     |  27 ++---
 src/Internal/CommandResult.php    |  56 +++++++++++
 src/Internal/ConnectionResult.php | 142 ++++++++++++++++++++++++++
 src/Internal/Processor.php        |   6 +-
 src/Internal/ResultProxy.php      |   8 ++
 src/Pool.php                      |  10 +-
 src/PooledResult.php              |  42 ++++++++
 src/PooledResultSet.php           |  88 ----------------
 src/PooledStatement.php           |  10 +-
 src/PooledTransaction.php         |  14 ++-
 src/Result.php                    |  23 +++++
 src/ResultSet.php                 |  22 ----
 src/StatementPool.php             |  14 +--
 test/LinkTest.php                 | 105 ++++++++++----------
 test/PoolTest.php                 |   2 +-
 26 files changed, 417 insertions(+), 428 deletions(-)
 create mode 100644 examples/test.php
 delete mode 100644 src/CommandResult.php
 delete mode 100644 src/ConnectionResultSet.php
 create mode 100644 src/Internal/CommandResult.php
 create mode 100644 src/Internal/ConnectionResult.php
 create mode 100644 src/PooledResult.php
 delete mode 100644 src/PooledResultSet.php
 create mode 100644 src/Result.php
 delete mode 100644 src/ResultSet.php

diff --git a/composer.json b/composer.json
index 89ede75..b241085 100644
--- a/composer.json
+++ b/composer.json
@@ -14,10 +14,10 @@
     ],
     "require": {
         "php": ">=7.1",
-        "amphp/amp": "^2",
+        "amphp/amp": "dev-streams as 2.6",
         "amphp/socket": "^1",
-        "amphp/sql": "^1",
-        "amphp/sql-common": "^1"
+        "amphp/sql": "dev-streams as 2",
+        "amphp/sql-common": "dev-streams as 2"
     },
     "require-dev": {
         "ext-openssl": "*",
diff --git a/examples/2-simple-query.php b/examples/2-simple-query.php
index ed59500..a536685 100644
--- a/examples/2-simple-query.php
+++ b/examples/2-simple-query.php
@@ -7,11 +7,10 @@
 Amp\Loop::run(function () {
     $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
-    /** @var Mysql\ResultSet $result */
+    /** @var Mysql\Result $result */
     $result = yield $db->query("SELECT 1 AS value");
 
-    while (yield $result->advance()) {
-        $row = $result->getCurrent();
+    while ($row = yield $result->continue()) {
         \var_dump($row['value']);
     }
 
diff --git a/examples/3-generic-with-yield.php b/examples/3-generic-with-yield.php
index 0b0aa04..c87050e 100644
--- a/examples/3-generic-with-yield.php
+++ b/examples/3-generic-with-yield.php
@@ -26,11 +26,11 @@
 
     print "Insertion successful (if it wasn't, an exception would have been thrown by now)" . PHP_EOL;
 
-    /** @var Mysql\ResultSet $result */
+    /** @var Mysql\Result $result */
     $result = yield $db->query("SELECT a, b FROM tmp");
 
-    while (yield $result->advance()) {
-        \var_dump($result->getCurrent());
+    while ($row = yield $result->continue()) {
+        \var_dump($row);
     }
 
     yield $db->query("DROP TABLE tmp");
diff --git a/examples/4-multi-rows.php b/examples/4-multi-rows.php
index 9307112..974a665 100644
--- a/examples/4-multi-rows.php
+++ b/examples/4-multi-rows.php
@@ -17,19 +17,19 @@
     $promises[] = $db->execute("SELECT POW(a, ?) AS power FROM tmp", [2]);
 
     /**
-     * @var Mysql\ResultSet $result1
-     * @var Mysql\ResultSet $result2
+     * @var Mysql\Result $result1
+     * @var Mysql\Result $result2
      */
     list($result1, $result2) = yield $promises; // Both queries execute simultaneously. Wait for both to finish here.
 
     print "Query 1 Results:" . PHP_EOL;
-    while (yield $result1->advance()) {
-        \var_dump($result1->getCurrent());
+    while ($row = yield $result1->continue()) {
+        \var_dump($row);
     }
 
     print  PHP_EOL . "Query 2 Results:" . PHP_EOL;
-    while (yield $result2->advance()) {
-        \var_dump($result2->getCurrent());
+    while ($row = yield $result2->continue()) {
+        \var_dump($row);
     }
 
     yield $db->query("DROP TABLE tmp");
diff --git a/examples/5-multi-stmts.php b/examples/5-multi-stmts.php
index 5670ba7..fc64804 100644
--- a/examples/5-multi-stmts.php
+++ b/examples/5-multi-stmts.php
@@ -15,13 +15,13 @@
     $result = yield $db->query("SELECT a + b FROM tmp; SELECT a - b FROM tmp;");
 
     $i = 0;
-    /** @var Mysql\ResultSet $result */
+    /** @var Mysql\Result $result */
     do {
         print PHP_EOL . "Query " . ++$i . " Results:" . PHP_EOL;
-        while (yield $result->advance()) {
-            \var_dump($result->getCurrent());
+        while ($row = yield $result->continue()) {
+            \var_dump($row);
         }
-    } while (yield $result->nextResultSet()); // Advances to the next result set.
+    } while ($result = yield $result->getNextResult()); // Advances to the next result set.
 
     yield $db->query("DROP TABLE tmp");
 
diff --git a/examples/6-transaction.php b/examples/6-transaction.php
index f6ca92c..d5a0d21 100644
--- a/examples/6-transaction.php
+++ b/examples/6-transaction.php
@@ -16,11 +16,11 @@
 
     yield $transaction->execute("INSERT INTO tmp VALUES (?, ? * 2)", [6, 6]);
 
-    /** @var Mysql\ResultSet $result */
+    /** @var Mysql\Result $result */
     $result = yield $transaction->execute("SELECT * FROM tmp WHERE a >= ?", [5]); // Two rows should be returned.
 
-    while (yield $result->advance()) {
-        \var_dump($result->getCurrent());
+    while ($row = yield $result->continue()) {
+        \var_dump($row);
     }
 
     yield $transaction->rollback();
@@ -28,8 +28,8 @@
     // Run same query again, should only return a single row since the other was rolled back.
     $result = yield $db->execute("SELECT * FROM tmp WHERE a >= ?", [5]);
 
-    while (yield $result->advance()) {
-        \var_dump($result->getCurrent());
+    while ($row = yield $result->continue()) {
+        \var_dump($row);
     }
 
     $db->close();
diff --git a/examples/test.php b/examples/test.php
new file mode 100644
index 0000000..990dbed
--- /dev/null
+++ b/examples/test.php
@@ -0,0 +1,23 @@
+<?php
+
+require 'support/bootstrap.php';
+require 'support/generic-table.php';
+
+use Amp\Mysql;
+
+Amp\Loop::run(function () {
+    $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+
+    yield $db->query("DROP TABLE IF EXISTS amp_json");
+
+    yield $db->query("CREATE TABLE IF NOT EXISTS amp_json (a TEXT, b JSON)");
+
+    /** @var \Amp\Mysql\Statement $statement */
+    $statement = yield $db->prepare("INSERT INTO amp_json (a, b) VALUES (?, cast(? as json))");
+    $json = '{"id": "evt_1FOZLq2dR4q4lysGBlnAxReO", "data": {"object": {"id": "po_1FODXN2dR4q4lysGUftc642s", "type": "bank_account", "amount": 75417, "method": "standard", "object": "payout", "status": "paid", "created": 1569806333, "currency": "usd", "livemode": true, "metadata": [], "automatic": true, "description": "STRIPE PAYOUT", "destination": "ba_1ATw1g2dR4q4lysGqe7POQQB", "source_type": "card", "arrival_date": 1569888000, "failure_code": null, "failure_message": null, "balance_transaction": "txn_1FODXN2dR4q4lysGxJg5ir6s", "statement_descriptor": null, "failure_balance_transaction": null}}, "type": "payout.paid", "object": "event", "created": 1569890186, "request": {"id": null, "idempotency_key": null}, "livemode": true, "api_version": "2019-08-14", "pending_webhooks": 1}';
+    yield $statement->execute([$json, $json]);
+
+    print_r(yield \Amp\Iterator\toArray(yield $db->query('select * from amp_json')));
+
+    $db->close();
+});
diff --git a/src/CommandResult.php b/src/CommandResult.php
deleted file mode 100644
index 5d1786f..0000000
--- a/src/CommandResult.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-use Amp\Sql\CommandResult as SqlCommandResult;
-
-final class CommandResult implements SqlCommandResult
-{
-    /** @var int */
-    private $affectedRows;
-
-    /** @var int */
-    private $lastInsertId;
-
-    public function __construct(int $affectedRows, int $lastInsertId)
-    {
-        $this->affectedRows = $affectedRows;
-        $this->lastInsertId = $lastInsertId;
-    }
-
-    /**
-     * @return int Number of rows affected by the modification query.
-     */
-    public function getAffectedRowCount(): int
-    {
-        return $this->affectedRows;
-    }
-
-    /**
-     * @return int Insert ID of the last auto increment row.
-     */
-    public function getLastInsertId(): int
-    {
-        return $this->lastInsertId;
-    }
-}
diff --git a/src/Connection.php b/src/Connection.php
index 7523d6f..b79fa75 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -95,7 +95,7 @@ public function setCharset(string $charset, string $collate = ""): Promise
         return $this->processor->setCharset($charset, $collate);
     }
 
-    public function close()
+    public function close(): void
     {
         $processor = $this->processor;
         // Send close command if connection is not already in a closed or closing state
@@ -131,10 +131,10 @@ public function query(string $query): Promise
             $result = yield $this->processor->query($query);
 
             if ($result instanceof Internal\ResultProxy) {
-                return new ConnectionResultSet($result);
+                return new Internal\ConnectionResult($result);
             }
 
-            if ($result instanceof CommandResult) {
+            if ($result instanceof Internal\CommandResult) {
                 return $result;
             }
 
diff --git a/src/ConnectionResultSet.php b/src/ConnectionResultSet.php
deleted file mode 100644
index 5618c60..0000000
--- a/src/ConnectionResultSet.php
+++ /dev/null
@@ -1,160 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-use Amp\Deferred;
-use Amp\Iterator;
-use Amp\Producer;
-use Amp\Promise;
-use Amp\Success;
-use function Amp\asyncCall;
-use function Amp\call;
-
-final class ConnectionResultSet implements ResultSet
-{
-    /** @var Internal\ResultProxy|null */
-    private $result;
-
-    /** @var \Amp\Producer */
-    private $producer;
-
-    /** @var array|object Last emitted row. */
-    private $currentRow;
-
-    /** @var string[]|null */
-    private $columnNames;
-
-    public function __construct(Internal\ResultProxy $result)
-    {
-        $this->result = $result;
-        $this->producer = self::makeIterator($result);
-    }
-
-    private static function makeIterator(Internal\ResultProxy $result): Iterator
-    {
-        return new Producer(static function (callable $emit) use ($result) {
-            $row = yield self::fetchRow($result);
-            while ($row !== null) {
-                $next = self::fetchRow($result); // Fetch next row while emitting last row.
-                yield $emit($row);
-                $row = yield $next;
-            }
-        });
-    }
-
-    public function __destruct()
-    {
-        if (!$this->result) {
-            return;
-        }
-
-        $producer = $this->producer;
-        asyncCall(static function () use ($producer) {
-            try {
-                while (yield $producer->advance());
-            } catch (\Throwable $exception) {
-                // Ignore iterator failure when destroying.
-            }
-        });
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function advance(): Promise
-    {
-        $this->currentRow = null;
-
-        return $this->producer->advance();
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function getCurrent(): array
-    {
-        if ($this->currentRow !== null) {
-            return $this->currentRow;
-        }
-
-        if (!$this->columnNames) {
-            $this->columnNames = \array_column($this->result->columns, "name");
-        }
-
-        return $this->currentRow = \array_combine($this->columnNames, $this->producer->getCurrent());
-    }
-
-    private static function fetchRow(Internal\ResultProxy $result): Promise
-    {
-        if ($result->userFetched < $result->fetchedRows) {
-            $row = $result->rows[$result->userFetched];
-            unset($result->rows[$result->userFetched]);
-            $result->userFetched++;
-            return new Success($row);
-        }
-
-        if ($result->state === Internal\ResultProxy::ROWS_FETCHED) {
-            return new Success;
-        }
-
-        $deferred = new Deferred;
-
-        /* We need to increment the internal counter, else the next time fetch is called,
-         * it'll simply return the row we fetch here instead of fetching a new row
-         * since callback order on promises isn't defined, we can't do this via onResolve() */
-        $incRow = function ($row) use ($result) {
-            unset($result->rows[$result->userFetched++]);
-            return $row;
-        };
-
-        $result->deferreds[Internal\ResultProxy::UNFETCHED][] = [$deferred, null, $incRow];
-        return $deferred->promise();
-    }
-
-    /**
-     * @return Promise<bool> Resolves with true if another result set exists, false if all result sets have
-     *     been consumed.
-     */
-    public function nextResultSet(): Promise
-    {
-        if (!$this->result) {
-            return new Success(false);
-        }
-
-        return call(function () {
-            while (yield $this->advance()); // Consume any values left in the current result.
-
-            $this->columnNames = null;
-
-            $deferred = $this->result->next ?: $this->result->next = new Deferred;
-            $this->result = yield $deferred->promise();
-
-            if ($this->result) {
-                $this->producer = self::makeIterator($this->result);
-                return true;
-            }
-
-            return false;
-        });
-    }
-
-    /**
-     * @return Promise<mixed[][]>
-     *
-     * @throws \Error If nextResultSet() has been invoked and no further result sets were available.
-     */
-    public function getFields(): Promise
-    {
-        if ($this->result === null) {
-            throw new \Error("The current result set is empty; call this method before invoking ResultSet::nextResultSet()");
-        }
-
-        if ($this->result->state >= Internal\ResultProxy::COLUMNS_FETCHED) {
-            return new Success($this->result->columns);
-        }
-
-        $deferred = new Deferred;
-        $this->result->deferreds[Internal\ResultProxy::COLUMNS_FETCHED][] = [$deferred, &$this->result->columns, null];
-        return $deferred->promise();
-    }
-}
diff --git a/src/ConnectionStatement.php b/src/ConnectionStatement.php
index e4d9268..ae37ec3 100644
--- a/src/ConnectionStatement.php
+++ b/src/ConnectionStatement.php
@@ -3,6 +3,8 @@
 namespace Amp\Mysql;
 
 use Amp\Deferred;
+use Amp\Mysql\Internal\CommandResult;
+use Amp\Mysql\Internal\ConnectionResult;
 use Amp\Promise;
 use Amp\Sql\ConnectionException;
 use Amp\Sql\FailureException;
@@ -141,7 +143,7 @@ public function execute(array $params = []): Promise
             $result = yield $promise;
 
             if ($result instanceof Internal\ResultProxy) {
-                $result = new ConnectionResultSet($result);
+                $result = new ConnectionResult($result);
                 return $result;
             }
 
diff --git a/src/ConnectionTransaction.php b/src/ConnectionTransaction.php
index 88f69a4..e79eb03 100644
--- a/src/ConnectionTransaction.php
+++ b/src/ConnectionTransaction.php
@@ -118,13 +118,8 @@ public function query(string $sql): Promise
 
         return call(function () use ($sql) {
             $result = yield $this->processor->query($sql);
-
-            if ($result instanceof ResultSet) {
-                ++$this->refCount;
-                return new PooledResultSet($result, $this->release);
-            }
-
-            return $result;
+            ++$this->refCount;
+            return new PooledResult($result, $this->release);
         });
     }
 
@@ -161,19 +156,15 @@ public function execute(string $sql, array $params = []): Promise
             \assert($statement instanceof Statement);
             $result = yield $statement->execute($params);
 
-            if ($result instanceof ResultSet) {
-                ++$this->refCount;
-                return new PooledResultSet($result, $this->release);
-            }
-
-            return $result;
+            ++$this->refCount;
+            return new PooledResult($result, $this->release);
         });
     }
 
     /**
      * Commits the transaction and makes it inactive.
      *
-     * @return Promise<\Amp\Sql\CommandResult>
+     * @return Promise<Result>
      *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
@@ -193,7 +184,7 @@ public function commit(): Promise
     /**
      * Rolls back the transaction and makes it inactive.
      *
-     * @return Promise<\Amp\Sql\CommandResult>
+     * @return Promise<Result>
      *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
@@ -215,7 +206,7 @@ public function rollback(): Promise
      *
      * @param string $identifier Savepoint identifier.
      *
-     * @return Promise<\Amp\Sql\CommandResult>
+     * @return Promise<Result>
      *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
@@ -229,7 +220,7 @@ public function createSavepoint(string $identifier): Promise
      *
      * @param string $identifier Savepoint identifier.
      *
-     * @return Promise<\Amp\Sql\CommandResult>
+     * @return Promise<Result>
      *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
@@ -243,7 +234,7 @@ public function rollbackTo(string $identifier): Promise
      *
      * @param string $identifier Savepoint identifier.
      *
-     * @return Promise<\Amp\Sql\CommandResult>
+     * @return Promise<Result>
      *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
diff --git a/src/Internal/CommandResult.php b/src/Internal/CommandResult.php
new file mode 100644
index 0000000..1387e6f
--- /dev/null
+++ b/src/Internal/CommandResult.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Amp\Mysql\Internal;
+
+use Amp\Mysql\Result;
+use Amp\Sql\Common\CommandResult as SqlCommandResult;
+use Amp\Promise;
+use Amp\Success;
+
+final class CommandResult implements Result
+{
+    /** @var int */
+    private $lastInsertId;
+
+    /** @var SqlCommandResult */
+    private $delegate;
+
+    public function __construct(int $affectedRows, int $lastInsertId)
+    {
+        $this->delegate = new SqlCommandResult($affectedRows, new Success);
+        $this->lastInsertId = $lastInsertId ?: null; // Convert 0 to null
+    }
+
+    public function continue(): Promise
+    {
+        return $this->delegate->continue();
+    }
+
+    public function dispose()
+    {
+        $this->delegate->dispose();
+    }
+
+    public function getRowCount(): int
+    {
+        return $this->delegate->getRowCount();
+    }
+
+    /**
+     * @return int Insert ID of the last auto increment row.
+     */
+    public function getLastInsertId(): ?int
+    {
+        return $this->lastInsertId;
+    }
+
+    public function getNextResult(): Promise
+    {
+        return $this->delegate->getNextResult();
+    }
+
+    public function getFields(): Promise
+    {
+        return new Success; // Command results do not have a field list.
+    }
+}
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
new file mode 100644
index 0000000..97d18b7
--- /dev/null
+++ b/src/Internal/ConnectionResult.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace Amp\Mysql\Internal;
+
+use Amp\AsyncGenerator;
+use Amp\Deferred;
+use Amp\DisposedException;
+use Amp\Mysql\Internal;
+use Amp\Mysql\Result;
+use Amp\Promise;
+use Amp\Stream;
+use Amp\Success;
+use function Amp\call;
+
+final class ConnectionResult implements Result
+{
+    /** @var Internal\ResultProxy */
+    private $result;
+
+    /** @var AsyncGenerator */
+    private $generator;
+
+    /** @var Promise|null */
+    private $nextResult;
+
+    public function __construct(Internal\ResultProxy $result)
+    {
+        $this->result = $result;
+        $this->generator = self::makeStream($result);
+    }
+
+    private static function makeStream(Internal\ResultProxy $result): Stream
+    {
+        return new AsyncGenerator(static function (callable $emit) use ($result): \Generator {
+            $next = self::fetchRow($result);
+            try {
+                while ($row = yield $next) {
+                    if (!isset($columnNames)) {
+                        $columnNames = \array_column($result->columns, 'name');
+                    }
+                    $next = self::fetchRow($result);
+                    yield $emit(\array_combine($columnNames, $row));
+                }
+            } catch (DisposedException $exception) {
+                // Discard remaining results if disposed.
+                while ($row = yield $next) {
+                    $next = self::fetchRow($result);
+                }
+            }
+        });
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function continue(): Promise
+    {
+        return $this->generator->continue();
+    }
+
+    public function dispose(): void
+    {
+        $this->generator->dispose();
+    }
+
+    private static function fetchRow(Internal\ResultProxy $result): Promise
+    {
+        if ($result->userFetched < $result->fetchedRows) {
+            $row = $result->rows[$result->userFetched];
+            unset($result->rows[$result->userFetched]);
+            $result->userFetched++;
+            return new Success($row);
+        }
+
+        if ($result->state === Internal\ResultProxy::ROWS_FETCHED) {
+            return new Success;
+        }
+
+        $deferred = new Deferred;
+
+        /* We need to increment the internal counter, else the next time fetch is called,
+         * it'll simply return the row we fetch here instead of fetching a new row
+         * since callback order on promises isn't defined, we can't do this via onResolve() */
+        $incRow = function ($row) use ($result) {
+            unset($result->rows[$result->userFetched++]);
+            return $row;
+        };
+
+        $result->deferreds[Internal\ResultProxy::UNFETCHED][] = [$deferred, null, $incRow];
+        return $deferred->promise();
+    }
+
+    /**
+     * @return Promise<bool> Resolves with true if another result set exists, false if all result sets have
+     *     been consumed.
+     */
+    public function getNextResult(): Promise
+    {
+        if ($this->nextResult) {
+            return $this->nextResult;
+        }
+
+        return $this->nextResult = call(function (): \Generator {
+            $deferred = $this->result->next ?: $this->result->next = new Deferred;
+            $result = yield $deferred->promise();
+
+            if ($result instanceof Internal\ResultProxy) {
+                return new self($result);
+            }
+
+            return $result; // Instance of CommandResult or null.
+        });
+    }
+
+    public function getRowCount(): ?int
+    {
+        return $this->result->affectedRows;
+    }
+
+    public function getLastInsertId(): ?int
+    {
+        return $this->result->insertId;
+    }
+
+    /**
+     * @return Promise<mixed[][]>
+     */
+    public function getFields(): Promise
+    {
+        if ($this->result === null) {
+            throw new \Error("The current result set is empty; call this method before invoking ResultSet::nextResultSet()");
+        }
+
+        if ($this->result->state >= Internal\ResultProxy::COLUMNS_FETCHED) {
+            return new Success($this->result->columns);
+        }
+
+        $deferred = new Deferred;
+        $this->result->deferreds[Internal\ResultProxy::COLUMNS_FETCHED][] = [$deferred, &$this->result->columns, null];
+        return $deferred->promise();
+    }
+}
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index ee3b2aa..a99c634 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -7,7 +7,6 @@
 use Amp\Deferred;
 use Amp\File;
 use Amp\Loop;
-use Amp\Mysql\CommandResult;
 use Amp\Mysql\ConnectionConfig;
 use Amp\Mysql\ConnectionStatement;
 use Amp\Mysql\DataTypes;
@@ -881,7 +880,10 @@ private function handleQuery(string $packet): void
             case self::OK_PACKET:
                 $this->parseOk($packet);
                 if ($this->connInfo->statusFlags & StatusFlags::SERVER_MORE_RESULTS_EXISTS) {
-                    $this->getDeferred()->resolve($result = new ResultProxy);
+                    $result = new ResultProxy;
+                    $result->affectedRows = $this->connInfo->affectedRows;
+                    $result->insertId = $this->connInfo->insertId;
+                    $this->getDeferred()->resolve($result);
                     $this->result = $result;
                     $result->updateState(ResultProxy::COLUMNS_FETCHED);
                     $this->successfulResultsetFetch();
diff --git a/src/Internal/ResultProxy.php b/src/Internal/ResultProxy.php
index 99c7fc9..d30a336 100644
--- a/src/Internal/ResultProxy.php
+++ b/src/Internal/ResultProxy.php
@@ -2,6 +2,7 @@
 
 namespace Amp\Mysql\Internal;
 
+use Amp\Deferred;
 use Amp\Sql\FailureException;
 use Amp\Struct;
 
@@ -17,6 +18,12 @@ final class ResultProxy
     public $fetchedRows = 0;
     public $userFetched = 0;
 
+    /** @var int|null */
+    public $insertId;
+
+    /** @var int|null */
+    public $affectedRows;
+
     public $deferreds = [self::UNFETCHED => [], self::COLUMNS_FETCHED => [], self::ROWS_FETCHED => []];
 
     /** @var int */
@@ -46,6 +53,7 @@ public function updateState(int $state): void
             return;
         }
         foreach ($this->deferreds[$state] as [$deferred, $rows, $cb]) {
+            \assert($deferred instanceof Deferred);
             $deferred->resolve($cb ? $cb($rows) : $rows);
         }
         $this->deferreds[$state] = [];
diff --git a/src/Pool.php b/src/Pool.php
index f671aa9..91f5be8 100644
--- a/src/Pool.php
+++ b/src/Pool.php
@@ -6,7 +6,7 @@
 use Amp\Sql\Common\StatementPool as SqlStatementPool;
 use Amp\Sql\Connector;
 use Amp\Sql\Pool as SqlPool;
-use Amp\Sql\ResultSet as SqlResultSet;
+use Amp\Sql\Result as SqlResult;
 use Amp\Sql\Statement as SqlStatement;
 use Amp\Sql\Transaction as SqlTransaction;
 
@@ -17,10 +17,12 @@ protected function createDefaultConnector(): Connector
         return connector();
     }
 
-    protected function createResultSet(SqlResultSet $resultSet, callable $release): SqlResultSet
+    protected function createResult(SqlResult $result, callable $release): SqlResult
     {
-        \assert($resultSet instanceof ResultSet);
-        return new PooledResultSet($resultSet, $release);
+        if (!$result instanceof Result) {
+            throw new \TypeError('Result object must be an instance of ' . Result::class);
+        }
+        return new PooledResult($result, $release);
     }
 
     protected function createStatement(SqlStatement $statement, callable $release): SqlStatement
diff --git a/src/PooledResult.php b/src/PooledResult.php
new file mode 100644
index 0000000..4bb7acb
--- /dev/null
+++ b/src/PooledResult.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Amp\Mysql;
+
+use Amp\Promise;
+use Amp\Sql\Common\PooledResult as SqlPooledResult;
+use Amp\Sql\Result as SqlResult;
+
+final class PooledResult extends SqlPooledResult implements Result
+{
+    /** @var Result */
+    private $result;
+
+    /**
+     * @param Result $result
+     * @param callable  $release
+     */
+    public function __construct(Result $result, callable $release)
+    {
+        parent::__construct($result, $release);
+        $this->result = $result;
+    }
+
+    protected function newInstanceFrom(SqlResult $result, callable $release): SqlPooledResult
+    {
+        if (!$result instanceof Result) {
+            throw new \TypeError('Result object must be an instance of ' . Result::class);
+        }
+
+        return new self($result, $release);
+    }
+
+    public function getLastInsertId(): ?int
+    {
+        return $this->result->getRowCount();
+    }
+
+    public function getFields(): Promise
+    {
+        return $this->result->getFields();
+    }
+}
diff --git a/src/PooledResultSet.php b/src/PooledResultSet.php
deleted file mode 100644
index b420f9b..0000000
--- a/src/PooledResultSet.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-use Amp\Promise;
-use Amp\Sql\Common\PooledResultSet as SqlPooledResultSet;
-
-final class PooledResultSet extends SqlPooledResultSet implements ResultSet
-{
-    /** @var ConnectionResultSet */
-    private $result;
-
-    /** @var callable */
-    private $release;
-
-    /** @var Promise|null a*/
-    private $nextResultPromise;
-
-    /**
-     * @param ResultSet $result
-     * @param callable  $release
-     */
-    public function __construct(ResultSet $result, callable $release)
-    {
-        $this->result = $result;
-        $this->release = &$release;
-
-        parent::__construct($this->result, static function () use (&$release) {
-            if ($release !== null) {
-                $release();
-            }
-        });
-    }
-
-    public function advance(): Promise
-    {
-        $promise = $this->result->advance();
-
-        $promise->onResolve(function (\Throwable $exception = null, bool $moreResults = null) {
-            if ($moreResults || $this->release === null) {
-                return;
-            }
-
-            $this->nextResultPromise = $this->result->nextResultSet();
-            $this->nextResultPromise->onResolve(function (\Throwable $exception = null, bool $moreResults = null) {
-                $this->nextResultPromise = null;
-
-                if ($moreResults || $this->release === null) {
-                    return;
-                }
-
-                $release = $this->release;
-                $this->release = null;
-                $release();
-            });
-        });
-
-        return $promise;
-    }
-
-    public function nextResultSet(): Promise
-    {
-        if ($this->nextResultPromise !== null) {
-            $nextResultPromise = $this->nextResultPromise;
-            $this->nextResultPromise = null;
-            return $nextResultPromise;
-        }
-
-        $promise = $this->result->nextResultSet();
-
-        $promise->onResolve(function (\Throwable $exception = null, bool $moreResults = null) {
-            if ($moreResults || $this->release === null) {
-                return;
-            }
-
-            $release = $this->release;
-            $this->release = null;
-            $release();
-        });
-
-        return $promise;
-    }
-
-    public function getFields(): Promise
-    {
-        return $this->result->getFields();
-    }
-}
diff --git a/src/PooledStatement.php b/src/PooledStatement.php
index 35d2f86..74d0b37 100644
--- a/src/PooledStatement.php
+++ b/src/PooledStatement.php
@@ -4,7 +4,7 @@
 
 use Amp\Promise;
 use Amp\Sql\Common\PooledStatement as SqlPooledStatement;
-use Amp\Sql\ResultSet as SqlResultSet;
+use Amp\Sql\Result as SqlResult;
 
 final class PooledStatement extends SqlPooledStatement implements Statement
 {
@@ -16,10 +16,12 @@ public function __construct(Statement $statement, callable $release)
         $this->statement = $statement;
     }
 
-    protected function createResultSet(SqlResultSet $resultSet, callable $release): SqlResultSet
+    protected function createResult(SqlResult $result, callable $release): SqlResult
     {
-        \assert($resultSet instanceof ResultSet);
-        return new PooledResultSet($resultSet, $release);
+        if (!$result instanceof Result) {
+            throw new \TypeError('Result object must be an instance of ' . Result::class);
+        }
+        return new PooledResult($result, $release);
     }
 
     public function bind($paramId, $data): void
diff --git a/src/PooledTransaction.php b/src/PooledTransaction.php
index 04def2a..7890f7c 100644
--- a/src/PooledTransaction.php
+++ b/src/PooledTransaction.php
@@ -3,20 +3,24 @@
 namespace Amp\Mysql;
 
 use Amp\Sql\Common\PooledTransaction as SqlPooledTransaction;
-use Amp\Sql\ResultSet as SqlResultSet;
+use Amp\Sql\Result as SqlResult;
 use Amp\Sql\Statement as SqlStatement;
 
 final class PooledTransaction extends SqlPooledTransaction
 {
     protected function createStatement(SqlStatement $statement, callable $release): SqlStatement
     {
-        \assert($statement instanceof Statement);
+        if (!$statement instanceof Statement) {
+            throw new \TypeError('Statement object must be an instance of ' . Statement::class);
+        }
         return new PooledStatement($statement, $release);
     }
 
-    protected function createResultSet(SqlResultSet $resultSet, callable $release): SqlResultSet
+    protected function createResult(SqlResult $result, callable $release): SqlResult
     {
-        \assert($resultSet instanceof ResultSet);
-        return new PooledResultSet($resultSet, $release);
+        if (!$result instanceof Result) {
+            throw new \TypeError('Result object must be an instance of ' . Result::class);
+        }
+        return new PooledResult($result, $release);
     }
 }
diff --git a/src/Result.php b/src/Result.php
new file mode 100644
index 0000000..a195c92
--- /dev/null
+++ b/src/Result.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Amp\Mysql;
+
+use Amp\Promise;
+use Amp\Sql\Result as SqlResult;
+
+interface Result extends SqlResult
+{
+    /**
+     * @return int|null Insert ID of the last auto increment row if applicable to the result or null if no ID
+     *                  is available.
+     */
+    public function getLastInsertId(): ?int;
+
+    /**
+     * @return Promise<array|null>
+     *
+     * @psalm-return Promise<array<int, array<int, string>>|null>
+     */
+    public function getFields(): Promise;
+
+}
diff --git a/src/ResultSet.php b/src/ResultSet.php
deleted file mode 100644
index 1b97ace..0000000
--- a/src/ResultSet.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-use Amp\Promise;
-use Amp\Sql\ResultSet as SqlResultSet;
-
-interface ResultSet extends SqlResultSet
-{
-    /**
-     * @return Promise<bool> Resolves with true if another result set exists, false if all result sets have
-     *     been consumed.
-     */
-    public function nextResultSet(): Promise;
-
-    /**
-     * @return Promise<mixed[][]>
-     *
-     * @throws \Error If nextResultSet() has been invoked and no further result sets were available.
-     */
-    public function getFields(): Promise;
-}
diff --git a/src/StatementPool.php b/src/StatementPool.php
index 3345015..bedd5ce 100644
--- a/src/StatementPool.php
+++ b/src/StatementPool.php
@@ -5,7 +5,7 @@
 use Amp\Promise;
 use Amp\Sql\Common\StatementPool as SqlStatementPool;
 use Amp\Sql\Pool;
-use Amp\Sql\ResultSet as SqlResultSet;
+use Amp\Sql\Result as SqlResult;
 use Amp\Sql\Statement as SqlStatement;
 use Amp\Success;
 use function Amp\call;
@@ -34,13 +34,13 @@ protected function prepare(SqlStatement $statement): Promise
         });
     }
 
-    protected function createResultSet(SqlResultSet $resultSet, callable $release): SqlResultSet
+    protected function createResult(SqlResult $result, callable $release): SqlResult
     {
-        \assert(
-            $resultSet instanceof ConnectionResultSet
-            || $resultSet instanceof PooledResultSet
-        );
-        return new PooledResultSet($resultSet, $release);
+        if (!$result instanceof Result) {
+            throw new \TypeError('Result object must be an instance of ' . Result::class);
+        }
+
+        return new PooledResult($result, $release);
     }
 
     public function bind($paramId, $data): void
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 2d2485e..feb9613 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -2,10 +2,9 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Iterator;
-use Amp\Mysql\CommandResult;
+use Amp\Stream;
 use Amp\Mysql\DataTypes;
-use Amp\Mysql\ResultSet;
+use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
 use Amp\PHPUnit\AsyncTestCase;
 use Amp\Promise;
@@ -27,12 +26,12 @@ public function testQuery()
         /** @var Link $db */
         $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-        /** @var ResultSet $resultset */
+        /** @var Result $resultset */
         $resultset = yield $db->execute("SELECT ? AS a", [M_PI]);
-        $this->assertInstanceOf(ResultSet::class, $resultset);
+        $this->assertInstanceOf(Result::class, $resultset);
 
-        for ($i = 0; yield $resultset->advance(); ++$i) {
-            $this->assertSame(["a" => M_PI], $resultset->getCurrent());
+        for ($i = 0; $row = yield $resultset->continue(); ++$i) {
+            $this->assertSame(["a" => M_PI], $row);
         }
 
         $this->assertSame(1, $i);
@@ -43,13 +42,13 @@ public function testQueryFetchRow()
         /** @var Link $db */
         $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-        /** @var ResultSet $resultset */
+        /** @var Result $resultset */
         $resultset = yield $db->query('SELECT a FROM main WHERE a < 4');
-        $this->assertInstanceOf(ResultSet::class, $resultset);
+        $this->assertInstanceOf(Result::class, $resultset);
 
         $got = [];
-        while (yield $resultset->advance()) {
-            $got[] = \array_values($resultset->getCurrent());
+        while ($row = yield $resultset->continue()) {
+            $got[] = \array_values($row);
         }
 
         $this->assertSame($got, [[1], [2], [3]]);
@@ -71,29 +70,29 @@ public function testMultiStmt()
         /** @var Link $db */
         $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
 
-        /** @var ResultSet $resultset */
+        /** @var Result $resultset */
         $resultset = yield $db->query("SELECT a FROM main; SELECT b FROM main WHERE a = 5; SELECT b AS d, a + 1 AS c FROM main WHERE b > 4");
-        $this->assertInstanceOf(ResultSet::class, $resultset);
+        $this->assertInstanceOf(Result::class, $resultset);
 
         $got = [];
-        while (yield $resultset->advance()) {
-            $got[] = \array_values($resultset->getCurrent());
+        while ($row = yield $resultset->continue()) {
+            $got[] = \array_values($row);
         }
         $this->assertSame([[1], [2], [3], [4], [5]], $got);
-        $this->assertTrue(yield $resultset->nextResultSet());
+        $this->assertInstanceOf(Result::class, $resultset = yield $resultset->getNextResult());
 
         $got = [];
-        while (yield $resultset->advance()) {
-            $got[] = \array_values($resultset->getCurrent());
+        while ($row = yield $resultset->continue()) {
+            $got[] = \array_values($row);
         }
         $this->assertSame([[6]], $got);
-        $this->assertTrue(yield $resultset->nextResultSet());
+        $this->assertInstanceOf(Result::class, $resultset = yield $resultset->getNextResult());
 
         $fields = yield $resultset->getFields();
 
         $got = [];
-        while (yield $resultset->advance()) {
-            $got[] = $resultset->getCurrent();
+        while ($row = yield $resultset->continue()) {
+            $got[] = $row;
         }
         $this->assertSame([["d" => 5, "c" => 5], ["d" => 6, "c" => 6]], $got);
 
@@ -104,7 +103,7 @@ public function testMultiStmt()
         $this->assertSame($fields[1]["name"], "c");
         $this->assertSame($fields[1]["type"], DataTypes::MYSQL_TYPE_LONGLONG);
 
-        $this->assertFalse(yield $resultset->nextResultSet());
+        $this->assertNull(yield $resultset->getNextResult());
     }
 
     public function testPrepared()
@@ -114,7 +113,7 @@ public function testPrepared()
 
         /**
          * @var Statement $stmt
-         * @var ResultSet $result
+         * @var Result $result
          */
         $stmt = yield $db->prepare("SELECT * FROM main WHERE a = ? OR b = :num");
         $base = [
@@ -131,38 +130,38 @@ public function testPrepared()
         $this->assertEquals(yield $stmt->getFields(), [$base + ["name" => "a", "original_name" => "a"], $base + ["name" => "b", "original_name" => "b"]]);
         $stmt->bind("num", 5);
         $result = yield $stmt->execute([2]);
-        $this->assertInstanceOf(ResultSet::class, $result);
+        $this->assertInstanceOf(Result::class, $result);
         $got = [];
-        while (yield $result->advance()) {
-            $got[] = \array_values($result->getCurrent());
+        while ($row = yield $result->continue()) {
+            $got[] = \array_values($row);
         }
         $this->assertCount(2, $got);
 
         $stmt = yield $db->prepare("SELECT * FROM main WHERE a = ? OR b = ?");
         $result = yield $stmt->execute([1, 8]);
-        $this->assertInstanceOf(ResultSet::class, $result);
+        $this->assertInstanceOf(Result::class, $result);
         $got = [];
-        while (yield $result->advance()) {
-            $got[] = \array_values($result->getCurrent());
+        while ($row = yield $result->continue()) {
+            $got[] = \array_values($row);
         }
         $this->assertCount(1, $got);
 
         $stmt = yield $db->prepare("SELECT * FROM main WHERE a = :a OR b = ?");
         $result = yield $stmt->execute(["a" => 2, 5]);
-        $this->assertInstanceOf(ResultSet::class, $result);
+        $this->assertInstanceOf(Result::class, $result);
         $got = [];
-        while (yield $result->advance()) {
-            $got[] = \array_values($result->getCurrent());
+        while ($row = yield $result->continue()) {
+            $got[] = \array_values($row);
         }
         $this->assertCount(2, $got);
 
         $stmt = yield $db->prepare("INSERT INTO main VALUES (:a, :b)");
         $result = yield $stmt->execute(["a" => 10, "b" => 11]);
-        $this->assertInstanceOf(CommandResult::class, $result);
+        $this->assertInstanceOf(Result::class, $result);
 
         $stmt = yield $db->prepare("DELETE FROM main WHERE a = :a");
         $result = yield $stmt->execute(["a" => 10]);
-        $this->assertInstanceOf(CommandResult::class, $result);
+        $this->assertInstanceOf(Result::class, $result);
     }
 
     public function testPrepareWithInvalidQuery()
@@ -242,21 +241,21 @@ public function testExecute()
         /** @var Link $db */
         $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-        /** @var ResultSet $result */
+        /** @var Result $result */
         $result = yield $db->execute("SELECT * FROM main WHERE a = ? OR b = ?", [2, 5]);
-        $this->assertInstanceOf(ResultSet::class, $result);
+        $this->assertInstanceOf(Result::class, $result);
         $got = [];
-        while (yield $result->advance()) {
-            $got[] = \array_values($result->getCurrent());
+        while ($row = yield $result->continue()) {
+            $got[] = \array_values($row);
         }
         $this->assertCount(2, $got);
         $this->assertSame([[2, 3], [4, 5]], $got);
 
         $result = yield $db->execute("INSERT INTO main VALUES (:a, :b)", ["a" => 10, "b" => 11]);
-        $this->assertInstanceOf(CommandResult::class, $result);
+        $this->assertInstanceOf(Result::class, $result);
 
         $result = yield $db->execute("DELETE FROM main WHERE a = :a", ["a" => 10]);
-        $this->assertInstanceOf(CommandResult::class, $result);
+        $this->assertInstanceOf(Result::class, $result);
     }
 
     public function testExecuteWithInvalidQuery()
@@ -292,12 +291,12 @@ public function testPreparedWithNegativeValue()
         $stmt = yield $db->prepare("CREATE TABLE tmp SELECT ? AS a");
         yield $stmt->execute([-1]);
 
-        /** @var \Amp\Mysql\ResultSet $result */
+        /** @var \Amp\Mysql\Result $result */
         $stmt = yield $db->prepare("SELECT a FROM tmp");
         $result = yield $stmt->execute();
-        yield $result->advance();
+        $row = yield $result->continue();
 
-        $this->assertEquals(\array_values($result->getCurrent()), [-1]);
+        $this->assertEquals(\array_values($row), [-1]);
     }
 
     public function testTransaction()
@@ -311,25 +310,25 @@ public function testTransaction()
         /** @var Statement $statement */
         $statement = yield $transaction->prepare("INSERT INTO main VALUES (?, ?)");
         $result = yield $statement->execute([6, 7]);
-        $this->assertInstanceOf(CommandResult::class, $result);
+        $this->assertInstanceOf(Result::class, $result);
 
-        /** @var ResultSet $result */
+        /** @var Result $result */
         $result = yield $transaction->execute("SELECT * FROM main WHERE a = ?", [6]);
 
         $got = [];
-        while (yield $result->advance()) {
-            $got[] = \array_values($result->getCurrent());
+        while ($row = yield $result->continue()) {
+            $got[] = \array_values($row);
         }
         $this->assertCount(1, $got);
-        yield $result->nextResultSet();
+        $result = yield $result->getNextResult();
 
         yield $transaction->rollback();
 
         $result = yield $db->execute("SELECT * FROM main WHERE a = ?", [6]);
 
         $got = [];
-        while (yield $result->advance()) {
-            $got[] = \array_values($result->getCurrent());
+        while ($row = yield $result->continue()) {
+            $got[] = \array_values($row);
         }
         $this->assertCount(0, $got);
     }
@@ -351,15 +350,15 @@ public function testInsertSelect()
             /** @var Statement $statement */
             $statement = yield $transaction->prepare("SELECT a, b FROM main WHERE a >= ?");
 
-            $count = \count(yield Iterator\toArray(yield $statement->execute([$a])));
+            $count = \count(yield Stream\toArray(yield $statement->execute([$a])));
 
             /** @var Statement $statement */
             $statement = yield $transaction->prepare("INSERT INTO main (a, b) SELECT a, b FROM main WHERE a >= ?");
 
-            /** @var CommandResult $result */
+            /** @var Result $result */
             $result = yield $statement->execute([$a]);
 
-            $this->assertSame($count, $result->getAffectedRowCount());
+            $this->assertSame($count, $result->getRowCount());
         } finally {
             yield $transaction->rollback();
         }
diff --git a/test/PoolTest.php b/test/PoolTest.php
index a68c3d0..e6a64a1 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -3,7 +3,7 @@
 namespace Amp\Mysql\Test;
 
 use Amp\Delayed;
-use Amp\Mysql\CommandResult;
+use Amp\Mysql\Internal\CommandResult;
 use Amp\Mysql\Connection;
 use Amp\Mysql\ConnectionConfig;
 use Amp\Mysql\Internal\Processor;

From 825f5da0ecc5c49b02792e1905cf700d44a84f29 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 28 May 2020 13:08:45 -0500
Subject: [PATCH 029/174] Fix styles

---
 src/Internal/CommandResult.php | 2 +-
 src/Result.php                 | 1 -
 test/LinkTest.php              | 2 +-
 test/PoolTest.php              | 4 ++--
 4 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/Internal/CommandResult.php b/src/Internal/CommandResult.php
index 1387e6f..b713135 100644
--- a/src/Internal/CommandResult.php
+++ b/src/Internal/CommandResult.php
@@ -3,8 +3,8 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\Mysql\Result;
-use Amp\Sql\Common\CommandResult as SqlCommandResult;
 use Amp\Promise;
+use Amp\Sql\Common\CommandResult as SqlCommandResult;
 use Amp\Success;
 
 final class CommandResult implements Result
diff --git a/src/Result.php b/src/Result.php
index a195c92..220abf7 100644
--- a/src/Result.php
+++ b/src/Result.php
@@ -19,5 +19,4 @@ public function getLastInsertId(): ?int;
      * @psalm-return Promise<array<int, array<int, string>>|null>
      */
     public function getFields(): Promise;
-
 }
diff --git a/test/LinkTest.php b/test/LinkTest.php
index feb9613..a08a6d5 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -2,7 +2,6 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Stream;
 use Amp\Mysql\DataTypes;
 use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
@@ -11,6 +10,7 @@
 use Amp\Sql\Link;
 use Amp\Sql\QueryError;
 use Amp\Sql\Transaction;
+use Amp\Stream;
 
 abstract class LinkTest extends AsyncTestCase
 {
diff --git a/test/PoolTest.php b/test/PoolTest.php
index e6a64a1..f9cdc4a 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -3,9 +3,9 @@
 namespace Amp\Mysql\Test;
 
 use Amp\Delayed;
-use Amp\Mysql\Internal\CommandResult;
 use Amp\Mysql\Connection;
 use Amp\Mysql\ConnectionConfig;
+use Amp\Mysql\Internal\CommandResult;
 use Amp\Mysql\Internal\Processor;
 use Amp\Mysql\Pool;
 use Amp\Mysql\Statement;
@@ -242,7 +242,7 @@ public function testConnectionClosedInPool(int $count)
 
         $processor = $this->createMock(Processor::class);
         $processor->method('isAlive')
-            ->willReturnOnConsecutiveCalls(true, false);
+            ->willReturnOnConsecutiveCalls(true, false, false);
         $processor->expects($this->once())
             ->method('query')
             ->with($query)

From 7083adff9f71af071f62422dd37c6801cb71f4cd Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 28 May 2020 13:30:30 -0500
Subject: [PATCH 030/174] Wrap ResultProxy into ConnectionResult in Processor

Moved ConnectionStatement and ConnectionTransaction to internal namespace since they depend on internal classes.
---
 src/Connection.php                           | 30 ++++++---------
 src/Internal/ConnectionResult.php            | 19 +++++-----
 src/{ => Internal}/ConnectionStatement.php   | 40 ++++++--------------
 src/{ => Internal}/ConnectionTransaction.php | 12 ++++--
 src/Internal/Processor.php                   |  7 ++--
 test/PoolTest.php                            |  5 ++-
 6 files changed, 46 insertions(+), 67 deletions(-)
 rename src/{ => Internal}/ConnectionStatement.php (77%)
 rename src/{ => Internal}/ConnectionTransaction.php (95%)

diff --git a/src/Connection.php b/src/Connection.php
index b79fa75..06cc6cd 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -7,8 +7,8 @@
 use Amp\NullCancellationToken;
 use Amp\Promise;
 use Amp\Socket;
-use Amp\Sql\FailureException;
 use Amp\Sql\Link;
+use Amp\Sql\Transaction;
 use function Amp\call;
 
 final class Connection implements Link
@@ -130,35 +130,29 @@ public function query(string $query): Promise
 
             $result = yield $this->processor->query($query);
 
-            if ($result instanceof Internal\ResultProxy) {
-                return new Internal\ConnectionResult($result);
-            }
-
-            if ($result instanceof Internal\CommandResult) {
-                return $result;
-            }
+            \assert($result instanceof Result);
 
-            throw new FailureException("Unrecognized result type");
+            return $result;
         });
     }
 
-    public function beginTransaction(int $isolation = ConnectionTransaction::ISOLATION_COMMITTED): Promise
+    public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITTED): Promise
     {
         return call(function () use ($isolation) {
             switch ($isolation) {
-                case ConnectionTransaction::ISOLATION_UNCOMMITTED:
+                case Transaction::ISOLATION_UNCOMMITTED:
                     yield $this->query("SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
                     break;
 
-                case ConnectionTransaction::ISOLATION_COMMITTED:
+                case Transaction::ISOLATION_COMMITTED:
                     yield $this->query("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
                     break;
 
-                case ConnectionTransaction::ISOLATION_REPEATABLE:
+                case Transaction::ISOLATION_REPEATABLE:
                     yield $this->query("SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ");
                     break;
 
-                case ConnectionTransaction::ISOLATION_SERIALIZABLE:
+                case Transaction::ISOLATION_SERIALIZABLE:
                     yield $this->query("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE");
                     break;
 
@@ -170,7 +164,7 @@ public function beginTransaction(int $isolation = ConnectionTransaction::ISOLATI
 
             $this->busy = new Deferred;
 
-            return new ConnectionTransaction($this->processor, $this->release, $isolation);
+            return new Internal\ConnectionTransaction($this->processor, $this->release, $isolation);
         });
     }
 
@@ -196,9 +190,9 @@ public function prepare(string $query): Promise
     public function execute(string $sql, array $params = []): Promise
     {
         return call(function () use ($sql, $params) {
-            /** @var Statement $statment */
-            $statment = yield $this->prepare($sql);
-            return yield $statment->execute($params);
+            $statement = yield $this->prepare($sql);
+            \assert($statement instanceof Statement);
+            return yield $statement->execute($params);
         });
     }
 
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index 97d18b7..8fd31c6 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -5,7 +5,6 @@
 use Amp\AsyncGenerator;
 use Amp\Deferred;
 use Amp\DisposedException;
-use Amp\Mysql\Internal;
 use Amp\Mysql\Result;
 use Amp\Promise;
 use Amp\Stream;
@@ -14,7 +13,7 @@
 
 final class ConnectionResult implements Result
 {
-    /** @var Internal\ResultProxy */
+    /** @var ResultProxy */
     private $result;
 
     /** @var AsyncGenerator */
@@ -23,13 +22,13 @@ final class ConnectionResult implements Result
     /** @var Promise|null */
     private $nextResult;
 
-    public function __construct(Internal\ResultProxy $result)
+    public function __construct(ResultProxy $result)
     {
         $this->result = $result;
         $this->generator = self::makeStream($result);
     }
 
-    private static function makeStream(Internal\ResultProxy $result): Stream
+    private static function makeStream(ResultProxy $result): Stream
     {
         return new AsyncGenerator(static function (callable $emit) use ($result): \Generator {
             $next = self::fetchRow($result);
@@ -63,7 +62,7 @@ public function dispose(): void
         $this->generator->dispose();
     }
 
-    private static function fetchRow(Internal\ResultProxy $result): Promise
+    private static function fetchRow(ResultProxy $result): Promise
     {
         if ($result->userFetched < $result->fetchedRows) {
             $row = $result->rows[$result->userFetched];
@@ -72,7 +71,7 @@ private static function fetchRow(Internal\ResultProxy $result): Promise
             return new Success($row);
         }
 
-        if ($result->state === Internal\ResultProxy::ROWS_FETCHED) {
+        if ($result->state === ResultProxy::ROWS_FETCHED) {
             return new Success;
         }
 
@@ -86,7 +85,7 @@ private static function fetchRow(Internal\ResultProxy $result): Promise
             return $row;
         };
 
-        $result->deferreds[Internal\ResultProxy::UNFETCHED][] = [$deferred, null, $incRow];
+        $result->deferreds[ResultProxy::UNFETCHED][] = [$deferred, null, $incRow];
         return $deferred->promise();
     }
 
@@ -104,7 +103,7 @@ public function getNextResult(): Promise
             $deferred = $this->result->next ?: $this->result->next = new Deferred;
             $result = yield $deferred->promise();
 
-            if ($result instanceof Internal\ResultProxy) {
+            if ($result instanceof ResultProxy) {
                 return new self($result);
             }
 
@@ -131,12 +130,12 @@ public function getFields(): Promise
             throw new \Error("The current result set is empty; call this method before invoking ResultSet::nextResultSet()");
         }
 
-        if ($this->result->state >= Internal\ResultProxy::COLUMNS_FETCHED) {
+        if ($this->result->state >= ResultProxy::COLUMNS_FETCHED) {
             return new Success($this->result->columns);
         }
 
         $deferred = new Deferred;
-        $this->result->deferreds[Internal\ResultProxy::COLUMNS_FETCHED][] = [$deferred, &$this->result->columns, null];
+        $this->result->deferreds[ResultProxy::COLUMNS_FETCHED][] = [$deferred, &$this->result->columns, null];
         return $deferred->promise();
     }
 }
diff --git a/src/ConnectionStatement.php b/src/Internal/ConnectionStatement.php
similarity index 77%
rename from src/ConnectionStatement.php
rename to src/Internal/ConnectionStatement.php
index ae37ec3..e86b0ea 100644
--- a/src/ConnectionStatement.php
+++ b/src/Internal/ConnectionStatement.php
@@ -1,15 +1,12 @@
 <?php
 
-namespace Amp\Mysql;
+namespace Amp\Mysql\Internal;
 
 use Amp\Deferred;
-use Amp\Mysql\Internal\CommandResult;
-use Amp\Mysql\Internal\ConnectionResult;
+use Amp\Mysql\Statement;
 use Amp\Promise;
 use Amp\Sql\ConnectionException;
-use Amp\Sql\FailureException;
 use Amp\Success;
-use function Amp\call;
 
 final class ConnectionStatement implements Statement
 {
@@ -21,16 +18,16 @@ final class ConnectionStatement implements Statement
     private $stmtId;
     private $prebound = [];
 
-    /** @var Internal\Processor|null */
+    /** @var Processor|null */
     private $processor;
 
-    /** @var Internal\ResultProxy */
+    /** @var ResultProxy */
     private $result;
 
     /** @var int */
     private $lastUsedAt;
 
-    public function __construct(Internal\Processor $processor, string $query, int $stmtId, array $named, Internal\ResultProxy $result)
+    public function __construct(Processor $processor, string $query, int $stmtId, array $named, ResultProxy $result)
     {
         $this->processor = $processor;
         $this->query = $query;
@@ -49,7 +46,7 @@ public function __construct(Internal\Processor $processor, string $query, int $s
         $this->lastUsedAt = \time();
     }
 
-    private function getProcessor(): Internal\Processor
+    private function getProcessor(): Processor
     {
         if ($this->processor === null) {
             throw new \Error("The statement has been closed");
@@ -137,22 +134,7 @@ public function execute(array $params = []): Promise
             }
         }
 
-        $promise = $this->getProcessor()->execute($this->stmtId, $this->query, $this->result->params, $prebound, $args);
-
-        return call(function () use ($promise) {
-            $result = yield $promise;
-
-            if ($result instanceof Internal\ResultProxy) {
-                $result = new ConnectionResult($result);
-                return $result;
-            }
-
-            if ($result instanceof CommandResult) {
-                return $result;
-            }
-
-            throw new FailureException("Unrecognized result type");
-        });
+        return $this->getProcessor()->execute($this->stmtId, $this->query, $this->result->params, $prebound, $args);
     }
 
     public function getQuery(): string
@@ -178,16 +160,16 @@ public function reset(): Promise
 
     public function getFields(): Promise
     {
-        if ($this->result->state >= Internal\ResultProxy::COLUMNS_FETCHED) {
+        if ($this->result->state >= ResultProxy::COLUMNS_FETCHED) {
             return new Success($this->result->columns);
         }
 
-        if (isset($this->result->deferreds[Internal\ResultProxy::COLUMNS_FETCHED][0])) {
-            return $this->result->deferreds[Internal\ResultProxy::COLUMNS_FETCHED][0][0]->promise();
+        if (isset($this->result->deferreds[ResultProxy::COLUMNS_FETCHED][0])) {
+            return $this->result->deferreds[ResultProxy::COLUMNS_FETCHED][0][0]->promise();
         }
 
         $deferred = new Deferred;
-        $this->result->deferreds[Internal\ResultProxy::COLUMNS_FETCHED][0] = [$deferred, &$this->result->columns, null];
+        $this->result->deferreds[ResultProxy::COLUMNS_FETCHED][0] = [$deferred, &$this->result->columns, null];
         return $deferred->promise();
     }
 
diff --git a/src/ConnectionTransaction.php b/src/Internal/ConnectionTransaction.php
similarity index 95%
rename from src/ConnectionTransaction.php
rename to src/Internal/ConnectionTransaction.php
index e79eb03..a611448 100644
--- a/src/ConnectionTransaction.php
+++ b/src/Internal/ConnectionTransaction.php
@@ -1,7 +1,11 @@
 <?php
 
-namespace Amp\Mysql;
+namespace Amp\Mysql\Internal;
 
+use Amp\Mysql\PooledResult;
+use Amp\Mysql\PooledStatement;
+use Amp\Mysql\Result;
+use Amp\Mysql\Statement;
 use Amp\Promise;
 use Amp\Sql\Transaction as SqlTransaction;
 use Amp\Sql\TransactionError;
@@ -11,7 +15,7 @@ final class ConnectionTransaction implements SqlTransaction
 {
     const SAVEPOINT_PREFIX = "amp_";
 
-    /** @var Internal\Processor|null */
+    /** @var Processor|null */
     private $processor;
 
     /** @var int */
@@ -24,13 +28,13 @@ final class ConnectionTransaction implements SqlTransaction
     private $refCount = 1;
 
     /**
-     * @param Internal\Processor $processor
+     * @param Processor $processor
      * @param callable $release
      * @param int $isolation
      *
      * @throws \Error If the isolation level is invalid.
      */
-    public function __construct(Internal\Processor $processor, callable $release, int $isolation = SqlTransaction::ISOLATION_COMMITTED)
+    public function __construct(Processor $processor, callable $release, int $isolation = SqlTransaction::ISOLATION_COMMITTED)
     {
         switch ($isolation) {
             case SqlTransaction::ISOLATION_UNCOMMITTED:
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index a99c634..fba461b 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -8,7 +8,6 @@
 use Amp\File;
 use Amp\Loop;
 use Amp\Mysql\ConnectionConfig;
-use Amp\Mysql\ConnectionStatement;
 use Amp\Mysql\DataTypes;
 use Amp\Mysql\InitializationException;
 use Amp\Promise;
@@ -883,7 +882,7 @@ private function handleQuery(string $packet): void
                     $result = new ResultProxy;
                     $result->affectedRows = $this->connInfo->affectedRows;
                     $result->insertId = $this->connInfo->insertId;
-                    $this->getDeferred()->resolve($result);
+                    $this->getDeferred()->resolve(new ConnectionResult($result));
                     $this->result = $result;
                     $result->updateState(ResultProxy::COLUMNS_FETCHED);
                     $this->successfulResultsetFetch();
@@ -906,7 +905,7 @@ private function handleQuery(string $packet): void
         }
 
         $this->parseCallback = [$this, "handleTextColumnDefinition"];
-        $this->getDeferred()->resolve($result = new ResultProxy);
+        $this->getDeferred()->resolve(new ConnectionResult($result = new ResultProxy));
         /* we need to resolve before assigning vars, so that a onResolve() handler won't have a partial result available */
         $this->result = $result;
         $result->setColumns(DataTypes::decodeUnsigned($packet));
@@ -916,7 +915,7 @@ private function handleQuery(string $packet): void
     private function handleExecute(string $packet): void
     {
         $this->parseCallback = [$this, "handleBinaryColumnDefinition"];
-        $this->getDeferred()->resolve($result = new ResultProxy);
+        $this->getDeferred()->resolve(new ConnectionResult($result = new ResultProxy));
         /* we need to resolve before assigning vars, so that a onResolve() handler won't have a partial result available */
         $this->result = $result;
         $result->setColumns(\ord($packet));
diff --git a/test/PoolTest.php b/test/PoolTest.php
index f9cdc4a..4598394 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -8,6 +8,7 @@
 use Amp\Mysql\Internal\CommandResult;
 use Amp\Mysql\Internal\Processor;
 use Amp\Mysql\Pool;
+use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
 use Amp\Promise;
 use Amp\Sql\Connector;
@@ -231,7 +232,7 @@ public function testConnectionClosedInPool(int $count)
     {
         $processors = $this->makeProcessorSet($count);
         $query = "SELECT * FROM test";
-        $result = new CommandResult(0, 0);
+        $result = $this->createMock(Result::class);
 
         foreach ($processors as $processor) {
             $processor->expects($this->exactly(2))
@@ -242,7 +243,7 @@ public function testConnectionClosedInPool(int $count)
 
         $processor = $this->createMock(Processor::class);
         $processor->method('isAlive')
-            ->willReturnOnConsecutiveCalls(true, false, false);
+            ->willReturnOnConsecutiveCalls(true, false);
         $processor->expects($this->once())
             ->method('query')
             ->with($query)

From 291988ab2318691b44994e26f4e8fa04f77a0b05 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 28 May 2020 13:36:00 -0500
Subject: [PATCH 031/174] Update travis build

---
 .travis.yml | 19 ++++++++-----------
 1 file changed, 8 insertions(+), 11 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index eb98a11..f505ff8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,8 @@
-sudo: false
+os: linux
 
 language: php
 
-dist: trusty
+dist: xenial
 
 services:
   - mysql
@@ -11,7 +11,7 @@ php:
   - 7.1
   - 7.2
   - 7.3
-  - 7.4snapshot
+  - 7.4
   - nightly
 
 jobs:
@@ -55,29 +55,26 @@ jobs:
       php: 7.3
     - addons:
         mariadb: 10.1
-      php: 7.4snapshot
+      php: 7.4
     - addons:
         mariadb: 10.2
-      php: 7.4snapshot
+      php: 7.4
     - addons:
         mariadb: 10.3
-      php: 7.4snapshot
+      php: 7.4
     - addons:
         mariadb: 10.4
-      php: 7.4snapshot
+      php: 7.4
     - addons:
         mariadb: 10.4
       php: nightly
     - stage: benchmark
-      php: 7.3
+      php: 7.4
       env:
         - AMP_DEBUG=false
       script:
         - PATH=$PATH:$(pwd)/benchmarks/bin vendor/bin/phpbench run --report=aggregate
-
-matrix:
   allow_failures:
-    - php: 7.4snapshot
     - php: nightly
   fast_finish: true
 

From d7e9a197efe48654bfb0ecb1238bd2969cdefe7d Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 29 Oct 2020 12:00:43 -0500
Subject: [PATCH 032/174] Fix naming

---
 src/Internal/ConnectionResult.php | 6 +++---
 test/LinkTest.php                 | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index 8fd31c6..86c2868 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -6,8 +6,8 @@
 use Amp\Deferred;
 use Amp\DisposedException;
 use Amp\Mysql\Result;
+use Amp\Pipeline;
 use Amp\Promise;
-use Amp\Stream;
 use Amp\Success;
 use function Amp\call;
 
@@ -25,10 +25,10 @@ final class ConnectionResult implements Result
     public function __construct(ResultProxy $result)
     {
         $this->result = $result;
-        $this->generator = self::makeStream($result);
+        $this->generator = self::makePipeline($result);
     }
 
-    private static function makeStream(ResultProxy $result): Stream
+    private static function makePipeline(ResultProxy $result): Pipeline
     {
         return new AsyncGenerator(static function (callable $emit) use ($result): \Generator {
             $next = self::fetchRow($result);
diff --git a/test/LinkTest.php b/test/LinkTest.php
index a08a6d5..f5bafa5 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -6,11 +6,11 @@
 use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
 use Amp\PHPUnit\AsyncTestCase;
+use Amp\Pipeline;
 use Amp\Promise;
 use Amp\Sql\Link;
 use Amp\Sql\QueryError;
 use Amp\Sql\Transaction;
-use Amp\Stream;
 
 abstract class LinkTest extends AsyncTestCase
 {
@@ -350,7 +350,7 @@ public function testInsertSelect()
             /** @var Statement $statement */
             $statement = yield $transaction->prepare("SELECT a, b FROM main WHERE a >= ?");
 
-            $count = \count(yield Stream\toArray(yield $statement->execute([$a])));
+            $count = \count(yield Pipeline\toArray(yield $statement->execute([$a])));
 
             /** @var Statement $statement */
             $statement = yield $transaction->prepare("INSERT INTO main (a, b) SELECT a, b FROM main WHERE a >= ?");

From 601b907a41cadb7d8bb82950be067893f755e4cd Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 10 Nov 2020 11:37:50 -0600
Subject: [PATCH 033/174] Refactor for Amp v3

---
 composer.json                          |  20 ++-
 examples/1-connect.php                 |  25 ++-
 examples/2-simple-query.php            |  15 +-
 examples/3-generic-with-yield.php      |  44 +++--
 examples/4-multi-rows.php              |  42 +++--
 examples/5-multi-stmts.php             |  31 ++--
 examples/6-transaction.php             |  36 ++--
 examples/support/generic-table.php     |  16 +-
 examples/test.php                      |  23 ---
 src/CancellableConnector.php           |   6 +-
 src/Connection.php                     | 124 ++++++-------
 src/DataTypes.php                      |   2 +-
 src/Internal/CommandResult.php         |  17 +-
 src/Internal/ConnectionResult.php      |  52 +++---
 src/Internal/ConnectionState.php       |  25 ++-
 src/Internal/ConnectionStatement.php   |  49 +++---
 src/Internal/ConnectionTransaction.php |  75 +++-----
 src/Internal/Processor.php             | 162 ++++++++---------
 src/Internal/ResultProxy.php           |  28 ++-
 src/Pool.php                           |  14 +-
 src/PooledResult.php                   |  13 +-
 src/PooledStatement.php                |  17 +-
 src/PooledTransaction.php              |  12 +-
 src/RefreshTypes.php                   |  16 +-
 src/Result.php                         |   7 +-
 src/Statement.php                      |  12 +-
 src/StatementPool.php                  |  48 ++---
 src/TransactionError.php               |   7 -
 src/functions.php                      |   5 +-
 test/ConnectionTest.php                |  14 +-
 test/LinkTest.php                      | 234 +++++++++++--------------
 test/PoolTest.php                      | 133 +++++++-------
 test/StatementTest.php                 |   7 +-
 test/bootstrap.php                     |  55 +++---
 34 files changed, 600 insertions(+), 786 deletions(-)
 delete mode 100644 examples/test.php
 delete mode 100644 src/TransactionError.php

diff --git a/composer.json b/composer.json
index b241085..280f786 100644
--- a/composer.json
+++ b/composer.json
@@ -13,21 +13,23 @@
         }
     ],
     "require": {
-        "php": ">=7.1",
-        "amphp/amp": "dev-streams as 2.6",
-        "amphp/socket": "^1",
-        "amphp/sql": "dev-streams as 2",
-        "amphp/sql-common": "dev-streams as 2"
+        "php": ">=8",
+        "amphp/amp": "^3-dev",
+        "amphp/socket": "^2-dev",
+        "amphp/sql": "^2-dev",
+        "amphp/sql-common": "^2-dev"
     },
     "require-dev": {
         "ext-openssl": "*",
-        "amphp/process": "^1",
-        "amphp/file": "^2 || ^1 || ^0.3.5",
-        "phpunit/phpunit": "^9 || ^8 || ^7",
-        "amphp/phpunit-util": "^1.1.2",
+        "amphp/process": "^2-dev",
+        "amphp/file": "^3-dev",
+        "phpunit/phpunit": "^9",
+        "amphp/phpunit-util": "^2-dev",
         "amphp/php-cs-fixer-config": "dev-master",
         "phpbench/phpbench": "^0.13.0"
     },
+    "minimum-stability": "dev",
+    "prefer-stable": true,
     "autoload": {
         "psr-4": {
             "Amp\\Mysql\\": "src"
diff --git a/examples/1-connect.php b/examples/1-connect.php
index 4f9ae5b..d97abe9 100644
--- a/examples/1-connect.php
+++ b/examples/1-connect.php
@@ -8,16 +8,15 @@
 
 use Amp\Mysql;
 
-Amp\Loop::run(function () {
-    /* If you want ssl, pass as second argument an array with ssl options (an empty options array is valid too); if null is passed, ssl is not enabled either */
-    $config = Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME);
-    /* use an alternative charset... Default is utf8mb4_general_ci */
-    $config = $config->withCharset("latin1_general_ci");
-    /** @var Mysql\Connection $db */
-    $db = yield Mysql\connect($config);
-
-    echo "Character set changed\n";
-
-    /* optional, as connection will automatically close when destructed. */
-    $db->close();
-});
+/* If you want ssl, pass as second argument an array with ssl options (an empty options array is valid too); if null is passed, ssl is not enabled either */
+$config = Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME);
+
+/* use an alternative charset... Default is utf8mb4_general_ci */
+$config = $config->withCharset("latin1_general_ci");
+
+$db = Mysql\connect($config);
+
+echo "Character set changed\n";
+
+/* optional, as connection will automatically close when destructed. */
+$db->close();
diff --git a/examples/2-simple-query.php b/examples/2-simple-query.php
index a536685..5d66288 100644
--- a/examples/2-simple-query.php
+++ b/examples/2-simple-query.php
@@ -4,15 +4,12 @@
 
 use Amp\Mysql;
 
-Amp\Loop::run(function () {
-    $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
-    /** @var Mysql\Result $result */
-    $result = yield $db->query("SELECT 1 AS value");
+$result = $db->query("SELECT 1 AS value");
 
-    while ($row = yield $result->continue()) {
-        \var_dump($row['value']);
-    }
+while ($row = $result->continue()) {
+    \var_dump($row['value']);
+}
 
-    $db->close();
-});
+$db->close();
diff --git a/examples/3-generic-with-yield.php b/examples/3-generic-with-yield.php
index c87050e..7387d34 100644
--- a/examples/3-generic-with-yield.php
+++ b/examples/3-generic-with-yield.php
@@ -3,37 +3,35 @@
 require 'support/bootstrap.php';
 
 use Amp\Mysql;
+use function Amp\async;
+use function Amp\await;
 
-Amp\Loop::run(function () {
-    $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
-    /* Create table and insert a few rows */
-    /* we need to wait until table is finished, so that we can insert. */
-    yield $db->query("CREATE TABLE IF NOT EXISTS tmp (a INT(10), b INT(10))");
+/* Create table and insert a few rows */
+/* we need to wait until table is finished, so that we can insert. */
+$db->query("CREATE TABLE IF NOT EXISTS tmp (a INT(10), b INT(10))");
 
-    print "Table successfully created." . PHP_EOL;
+print "Table successfully created." . PHP_EOL;
 
-    /** @var Mysql\Statement $statement */
-    $statement = yield $db->prepare("INSERT INTO tmp (a, b) VALUES (?, ? * 2)");
+$statement = $db->prepare("INSERT INTO tmp (a, b) VALUES (?, ? * 2)");
 
-    $promises = [];
-    foreach (\range(1, 5) as $num) {
-        $promises[] = $statement->execute([$num, $num]);
-    }
+$promises = [];
+foreach (\range(1, 5) as $num) {
+    $promises[] = async(fn() => $statement->execute([$num, $num]));
+}
 
-    /* wait until everything is inserted */
-    yield $promises;
+/* wait until everything is inserted */
+await($promises);
 
-    print "Insertion successful (if it wasn't, an exception would have been thrown by now)" . PHP_EOL;
+print "Insertion successful (if it wasn't, an exception would have been thrown by now)" . PHP_EOL;
 
-    /** @var Mysql\Result $result */
-    $result = yield $db->query("SELECT a, b FROM tmp");
+$result = $db->query("SELECT a, b FROM tmp");
 
-    while ($row = yield $result->continue()) {
-        \var_dump($row);
-    }
+while ($row = $result->continue()) {
+    \var_dump($row);
+}
 
-    yield $db->query("DROP TABLE tmp");
+$db->query("DROP TABLE tmp");
 
-    $db->close();
-});
+$db->close();
diff --git a/examples/4-multi-rows.php b/examples/4-multi-rows.php
index 974a665..c062a05 100644
--- a/examples/4-multi-rows.php
+++ b/examples/4-multi-rows.php
@@ -4,35 +4,39 @@
 require 'support/generic-table.php';
 
 use Amp\Mysql;
+use function Amp\async;
+use function Amp\await;
 
-Amp\Loop::run(function () {
-    $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
-    /* create same table than in 3-generic-with-yield.php */
-    yield from createGenericTable($db);
+/* create same table than in 3-generic-with-yield.php */
+createGenericTable($db);
 
-    $promises = [];
+$promises = [];
 
-    $promises[] = $db->query("SELECT a * b FROM tmp");
-    $promises[] = $db->execute("SELECT POW(a, ?) AS power FROM tmp", [2]);
+$promises[] = async(fn() => $db->query("SELECT a * b FROM tmp"));
+$promises[] = async(fn() => $db->execute("SELECT POW(a, ?) AS power FROM tmp", [2]));
 
+try {
     /**
      * @var Mysql\Result $result1
      * @var Mysql\Result $result2
      */
-    list($result1, $result2) = yield $promises; // Both queries execute simultaneously. Wait for both to finish here.
+    [$result1, $result2] = await($promises); // Both queries execute simultaneously. Wait for both to finish here.
+} catch (\Throwable $e) {
+    var_dump($e);
+}
 
-    print "Query 1 Results:" . PHP_EOL;
-    while ($row = yield $result1->continue()) {
-        \var_dump($row);
-    }
+print "Query 1 Results:" . PHP_EOL;
+while ($row = $result1->continue()) {
+    \var_dump($row);
+}
 
-    print  PHP_EOL . "Query 2 Results:" . PHP_EOL;
-    while ($row = yield $result2->continue()) {
-        \var_dump($row);
-    }
+print  PHP_EOL . "Query 2 Results:" . PHP_EOL;
+while ($row = $result2->continue()) {
+    \var_dump($row);
+}
 
-    yield $db->query("DROP TABLE tmp");
+$db->query("DROP TABLE tmp");
 
-    $db->close();
-});
+$db->close();
diff --git a/examples/5-multi-stmts.php b/examples/5-multi-stmts.php
index fc64804..a85a25e 100644
--- a/examples/5-multi-stmts.php
+++ b/examples/5-multi-stmts.php
@@ -5,25 +5,22 @@
 
 use Amp\Mysql;
 
-\Amp\Loop::run(function () {
-    $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
-    /* create same table than in 3-generic-with-yield.php */
-    yield from createGenericTable($db);
+/* create same table than in 3-generic-with-yield.php */
+createGenericTable($db);
 
-    /* multi statements are enabled by default, but generally stored procedures also might return multiple resultsets anyway */
-    $result = yield $db->query("SELECT a + b FROM tmp; SELECT a - b FROM tmp;");
+/* multi statements are enabled by default, but generally stored procedures also might return multiple resultsets anyway */
+$result = $db->query("SELECT a + b FROM tmp; SELECT a - b FROM tmp;");
 
-    $i = 0;
-    /** @var Mysql\Result $result */
-    do {
-        print PHP_EOL . "Query " . ++$i . " Results:" . PHP_EOL;
-        while ($row = yield $result->continue()) {
-            \var_dump($row);
-        }
-    } while ($result = yield $result->getNextResult()); // Advances to the next result set.
+$i = 0;
+do {
+    print PHP_EOL . "Query " . ++$i . " Results:" . PHP_EOL;
+    while ($row = $result->continue()) {
+        \var_dump($row);
+    }
+} while ($result = $result->getNextResult()); // Advances to the next result set.
 
-    yield $db->query("DROP TABLE tmp");
+$db->query("DROP TABLE tmp");
 
-    $db->close();
-});
+$db->close();
diff --git a/examples/6-transaction.php b/examples/6-transaction.php
index d5a0d21..e5edd0f 100644
--- a/examples/6-transaction.php
+++ b/examples/6-transaction.php
@@ -5,32 +5,28 @@
 
 use Amp\Mysql;
 
-Amp\Loop::run(function () {
-    $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
-    /* create same table than in 3-generic-with-yield.php */
-    yield from createGenericTable($db);
+/* create same table than in 3-generic-with-yield.php */
+createGenericTable($db);
 
-    /** @var \Amp\Sql\Transaction $transaction */
-    $transaction = yield $db->beginTransaction();
+$transaction = $db->beginTransaction();
 
-    yield $transaction->execute("INSERT INTO tmp VALUES (?, ? * 2)", [6, 6]);
+$transaction->execute("INSERT INTO tmp VALUES (?, ? * 2)", [6, 6]);
 
-    /** @var Mysql\Result $result */
-    $result = yield $transaction->execute("SELECT * FROM tmp WHERE a >= ?", [5]); // Two rows should be returned.
+$result = $transaction->execute("SELECT * FROM tmp WHERE a >= ?", [5]); // Two rows should be returned.
 
-    while ($row = yield $result->continue()) {
-        \var_dump($row);
-    }
+while ($row = $result->continue()) {
+    \var_dump($row);
+}
 
-    yield $transaction->rollback();
+$transaction->rollback();
 
-    // Run same query again, should only return a single row since the other was rolled back.
-    $result = yield $db->execute("SELECT * FROM tmp WHERE a >= ?", [5]);
+// Run same query again, should only return a single row since the other was rolled back.
+$result = $db->execute("SELECT * FROM tmp WHERE a >= ?", [5]);
 
-    while ($row = yield $result->continue()) {
-        \var_dump($row);
-    }
+while ($row = $result->continue()) {
+    \var_dump($row);
+}
 
-    $db->close();
-});
+$db->close();
diff --git a/examples/support/generic-table.php b/examples/support/generic-table.php
index f69509b..2a31307 100644
--- a/examples/support/generic-table.php
+++ b/examples/support/generic-table.php
@@ -1,18 +1,22 @@
 <?php
 
+use Amp\Sql\Link;
+use function Amp\async;
+use function Amp\await;
+
 /* Create table and fill in a few rows for examples; for comments see 3-generic-with-yield.php */
-function createGenericTable(\Amp\Sql\Link $db): Generator
+function createGenericTable(Link $db): void
 {
-    yield $db->query("DROP TABLE IF EXISTS tmp");
+    $db->query("DROP TABLE IF EXISTS tmp");
 
-    yield $db->query("CREATE TABLE tmp (a INT(10), b INT(10))");
+    $db->query("CREATE TABLE tmp (a INT(10), b INT(10))");
 
-    $statement = yield $db->prepare("INSERT INTO tmp (a, b) VALUES (?, ? * 2)");
+    $statement = $db->prepare("INSERT INTO tmp (a, b) VALUES (?, ? * 2)");
 
     $promises = [];
     foreach (\range(1, 5) as $num) {
-        $promises[] = $statement->execute([$num, $num]);
+        $promises[] = async(fn() => $statement->execute([$num, $num]));
     }
 
-    return yield $promises;
+    await($promises);
 }
diff --git a/examples/test.php b/examples/test.php
deleted file mode 100644
index 990dbed..0000000
--- a/examples/test.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-require 'support/bootstrap.php';
-require 'support/generic-table.php';
-
-use Amp\Mysql;
-
-Amp\Loop::run(function () {
-    $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
-
-    yield $db->query("DROP TABLE IF EXISTS amp_json");
-
-    yield $db->query("CREATE TABLE IF NOT EXISTS amp_json (a TEXT, b JSON)");
-
-    /** @var \Amp\Mysql\Statement $statement */
-    $statement = yield $db->prepare("INSERT INTO amp_json (a, b) VALUES (?, cast(? as json))");
-    $json = '{"id": "evt_1FOZLq2dR4q4lysGBlnAxReO", "data": {"object": {"id": "po_1FODXN2dR4q4lysGUftc642s", "type": "bank_account", "amount": 75417, "method": "standard", "object": "payout", "status": "paid", "created": 1569806333, "currency": "usd", "livemode": true, "metadata": [], "automatic": true, "description": "STRIPE PAYOUT", "destination": "ba_1ATw1g2dR4q4lysGqe7POQQB", "source_type": "card", "arrival_date": 1569888000, "failure_code": null, "failure_message": null, "balance_transaction": "txn_1FODXN2dR4q4lysGxJg5ir6s", "statement_descriptor": null, "failure_balance_transaction": null}}, "type": "payout.paid", "object": "event", "created": 1569890186, "request": {"id": null, "idempotency_key": null}, "livemode": true, "api_version": "2019-08-14", "pending_webhooks": 1}';
-    yield $statement->execute([$json, $json]);
-
-    print_r(yield \Amp\Iterator\toArray(yield $db->query('select * from amp_json')));
-
-    $db->close();
-});
diff --git a/src/CancellableConnector.php b/src/CancellableConnector.php
index 33a39cd..be14511 100644
--- a/src/CancellableConnector.php
+++ b/src/CancellableConnector.php
@@ -3,22 +3,20 @@
 namespace Amp\Mysql;
 
 use Amp\CancellationToken;
-use Amp\Promise;
 use Amp\Socket;
 use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
 use Amp\Sql\Connector;
 
 final class CancellableConnector implements Connector
 {
-    /** @var Socket\Connector */
-    private $connector;
+    private Socket\Connector $connector;
 
     public function __construct(?Socket\Connector $connector = null)
     {
         $this->connector = $connector ?? Socket\connector();
     }
 
-    public function connect(SqlConnectionConfig $config, ?CancellationToken $token = null): Promise
+    public function connect(SqlConnectionConfig $config, ?CancellationToken $token = null): Connection
     {
         if (!$config instanceof ConnectionConfig) {
             throw new \TypeError(\sprintf("Must provide an instance of %s to MySQL connectors", ConnectionConfig::class));
diff --git a/src/Connection.php b/src/Connection.php
index 06cc6cd..89742ee 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -5,11 +5,10 @@
 use Amp\CancellationToken;
 use Amp\Deferred;
 use Amp\NullCancellationToken;
-use Amp\Promise;
 use Amp\Socket;
 use Amp\Sql\Link;
 use Amp\Sql\Transaction;
-use function Amp\call;
+use function Amp\await;
 
 final class Connection implements Link
 {
@@ -22,11 +21,9 @@ final class Connection implements Link
     public const REFRESH_SLAVE = 0x40;
     public const REFRESH_MASTER = 0x80;
 
-    /** @var Internal\Processor */
-    private $processor;
+    private Internal\Processor $processor;
 
-    /** @var Deferred|null */
-    private $busy;
+    private ?Deferred $busy = null;
 
     /** @var callable Function used to release connection after a transaction has completed. */
     private $release;
@@ -36,23 +33,21 @@ final class Connection implements Link
      * @param CancellationToken|null $token
      * @param Socket\Connector|null $connector
      *
-     * @return Promise<self>
+     * @return self
      */
     public static function connect(
         ConnectionConfig $config,
         ?CancellationToken $token = null,
         ?Socket\Connector $connector = null
-    ): Promise {
+    ): self {
         $token = $token ?? new NullCancellationToken;
 
-        return call(function () use ($config, $token, $connector) {
-            $socket = yield ($connector ?? Socket\connector())
-                ->connect($config->getConnectionString(), $config->getConnectContext(), $token);
+        $socket = ($connector ?? Socket\connector())
+            ->connect($config->getConnectionString(), $config->getConnectContext(), $token);
 
-            $processor = new Internal\Processor($socket, $config);
-            yield $processor->connect($token);
-            return new self($processor);
-        });
+        $processor = new Internal\Processor($socket, $config);
+        $processor->connect($token);
+        return new self($processor);
     }
 
     /**
@@ -61,7 +56,7 @@ public static function connect(
     private function __construct(Internal\Processor $processor)
     {
         $this->processor = $processor;
-        $this->release = function () {
+        $this->release = function (): void {
             \assert($this->busy instanceof Deferred);
             $deferred = $this->busy;
             $this->busy = null;
@@ -90,9 +85,9 @@ public function isReady(): bool
         return $this->processor->isReady();
     }
 
-    public function setCharset(string $charset, string $collate = ""): Promise
+    public function setCharset(string $charset, string $collate = ""): void
     {
-        return $this->processor->setCharset($charset, $collate);
+        await($this->processor->setCharset($charset, $collate));
     }
 
     public function close(): void
@@ -106,94 +101,79 @@ public function close(): void
         }
     }
 
-    public function useDb(string $db): Promise
+    public function useDb(string $db): void
     {
-        return $this->processor->useDb($db);
+        await($this->processor->useDb($db));
     }
 
     /**
      * @param int $subcommand int one of the self::REFRESH_* constants
-     *
-     * @return Promise
      */
-    public function refresh(int $subcommand): Promise
+    public function refresh(int $subcommand): void
     {
-        return $this->processor->refresh($subcommand);
+        $this->processor->refresh($subcommand);
     }
 
-    public function query(string $query): Promise
+    public function query(string $query): Result
     {
-        return call(function () use ($query) {
-            while ($this->busy) {
-                yield $this->busy->promise();
-            }
-
-            $result = yield $this->processor->query($query);
-
-            \assert($result instanceof Result);
+        while ($this->busy) {
+            await($this->busy->promise());
+        }
 
-            return $result;
-        });
+        return await($this->processor->query($query));
     }
 
-    public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITTED): Promise
+    public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITTED): Transaction
     {
-        return call(function () use ($isolation) {
-            switch ($isolation) {
-                case Transaction::ISOLATION_UNCOMMITTED:
-                    yield $this->query("SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
-                    break;
+        switch ($isolation) {
+            case Transaction::ISOLATION_UNCOMMITTED:
+                $this->query("SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
+                break;
 
-                case Transaction::ISOLATION_COMMITTED:
-                    yield $this->query("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
-                    break;
+            case Transaction::ISOLATION_COMMITTED:
+                $this->query("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
+                break;
 
-                case Transaction::ISOLATION_REPEATABLE:
-                    yield $this->query("SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ");
-                    break;
+            case Transaction::ISOLATION_REPEATABLE:
+                $this->query("SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ");
+                break;
 
-                case Transaction::ISOLATION_SERIALIZABLE:
-                    yield $this->query("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE");
-                    break;
+            case Transaction::ISOLATION_SERIALIZABLE:
+                $this->query("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE");
+                break;
 
-                default:
-                    throw new \Error("Invalid transaction type");
-            }
+            default:
+                throw new \Error("Invalid transaction type");
+        }
 
-            yield $this->query("START TRANSACTION");
+        $this->query("START TRANSACTION");
 
-            $this->busy = new Deferred;
+        $this->busy = new Deferred;
 
-            return new Internal\ConnectionTransaction($this->processor, $this->release, $isolation);
-        });
+        return new Internal\ConnectionTransaction($this->processor, $this->release, $isolation);
     }
 
-    public function ping(): Promise
+    public function ping(): void
     {
-        return $this->processor->ping();
+        await($this->processor->ping());
     }
 
-    public function prepare(string $query): Promise
+    public function prepare(string $query): Statement
     {
-        return call(function () use ($query) {
-            while ($this->busy) {
-                yield $this->busy->promise();
-            }
+        while ($this->busy) {
+            await($this->busy->promise());
+        }
 
-            return $this->processor->prepare($query);
-        });
+        return await($this->processor->prepare($query));
     }
 
     /**
      * {@inheritdoc}
      */
-    public function execute(string $sql, array $params = []): Promise
+    public function execute(string $sql, array $params = []): Result
     {
-        return call(function () use ($sql, $params) {
-            $statement = yield $this->prepare($sql);
-            \assert($statement instanceof Statement);
-            return yield $statement->execute($params);
-        });
+        $statement = $this->prepare($sql);
+        return $statement->execute($params);
     }
 
     public function __destruct()
diff --git a/src/DataTypes.php b/src/DataTypes.php
index b488be1..a698ea5 100644
--- a/src/DataTypes.php
+++ b/src/DataTypes.php
@@ -42,7 +42,7 @@ final class DataTypes
     private const ENCODED_JSON_PREFIX = "base64:type251:";
 
     /** @see 14.7.3 Binary Value */
-    public static function encodeBinary($param): array
+    public static function encodeBinary(mixed $param): array
     {
         $unsigned = 0;
 
diff --git a/src/Internal/CommandResult.php b/src/Internal/CommandResult.php
index b713135..73d1974 100644
--- a/src/Internal/CommandResult.php
+++ b/src/Internal/CommandResult.php
@@ -3,17 +3,14 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\Mysql\Result;
-use Amp\Promise;
 use Amp\Sql\Common\CommandResult as SqlCommandResult;
 use Amp\Success;
 
 final class CommandResult implements Result
 {
-    /** @var int */
-    private $lastInsertId;
+    private ?int $lastInsertId;
 
-    /** @var SqlCommandResult */
-    private $delegate;
+    private SqlCommandResult $delegate;
 
     public function __construct(int $affectedRows, int $lastInsertId)
     {
@@ -21,12 +18,12 @@ public function __construct(int $affectedRows, int $lastInsertId)
         $this->lastInsertId = $lastInsertId ?: null; // Convert 0 to null
     }
 
-    public function continue(): Promise
+    public function continue(): ?array
     {
         return $this->delegate->continue();
     }
 
-    public function dispose()
+    public function dispose(): void
     {
         $this->delegate->dispose();
     }
@@ -44,13 +41,13 @@ public function getLastInsertId(): ?int
         return $this->lastInsertId;
     }
 
-    public function getNextResult(): Promise
+    public function getNextResult(): ?Result
     {
         return $this->delegate->getNextResult();
     }
 
-    public function getFields(): Promise
+    public function getFields(): ?array
     {
-        return new Success; // Command results do not have a field list.
+        return null; // Command results do not have a field list.
     }
 }
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index 86c2868..c063349 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -4,45 +4,36 @@
 
 use Amp\AsyncGenerator;
 use Amp\Deferred;
-use Amp\DisposedException;
 use Amp\Mysql\Result;
-use Amp\Pipeline;
 use Amp\Promise;
 use Amp\Success;
-use function Amp\call;
+use function Amp\async;
+use function Amp\await;
 
 final class ConnectionResult implements Result
 {
-    /** @var ResultProxy */
-    private $result;
+    private ResultProxy $result;
 
-    /** @var AsyncGenerator */
-    private $generator;
+    private AsyncGenerator $generator;
 
-    /** @var Promise|null */
-    private $nextResult;
+    private ?Promise $nextResult = null;
 
     public function __construct(ResultProxy $result)
     {
         $this->result = $result;
-        $this->generator = self::makePipeline($result);
-    }
-
-    private static function makePipeline(ResultProxy $result): Pipeline
-    {
-        return new AsyncGenerator(static function (callable $emit) use ($result): \Generator {
+        $this->generator = new AsyncGenerator(static function () use ($result): \Generator {
             $next = self::fetchRow($result);
             try {
-                while ($row = yield $next) {
+                while ($row = await($next)) {
                     if (!isset($columnNames)) {
                         $columnNames = \array_column($result->columns, 'name');
                     }
                     $next = self::fetchRow($result);
-                    yield $emit(\array_combine($columnNames, $row));
+                    yield \array_combine($columnNames, $row);
                 }
-            } catch (DisposedException $exception) {
+            } finally {
                 // Discard remaining results if disposed.
-                while ($row = yield $next) {
+                while ($row = await($next)) {
                     $next = self::fetchRow($result);
                 }
             }
@@ -52,7 +43,7 @@ private static function makePipeline(ResultProxy $result): Pipeline
     /**
      * @inheritDoc
      */
-    public function continue(): Promise
+    public function continue(): ?array
     {
         return $this->generator->continue();
     }
@@ -90,18 +81,17 @@ private static function fetchRow(ResultProxy $result): Promise
     }
 
     /**
-     * @return Promise<bool> Resolves with true if another result set exists, false if all result sets have
-     *     been consumed.
+     * @inheritDoc
      */
-    public function getNextResult(): Promise
+    public function getNextResult(): ?Result
     {
         if ($this->nextResult) {
-            return $this->nextResult;
+            return await($this->nextResult);
         }
 
-        return $this->nextResult = call(function (): \Generator {
+        $this->nextResult = async(function (): ?Result {
             $deferred = $this->result->next ?: $this->result->next = new Deferred;
-            $result = yield $deferred->promise();
+            $result = await($deferred->promise());
 
             if ($result instanceof ResultProxy) {
                 return new self($result);
@@ -109,6 +99,8 @@ public function getNextResult(): Promise
 
             return $result; // Instance of CommandResult or null.
         });
+
+        return await($this->nextResult);
     }
 
     public function getRowCount(): ?int
@@ -122,20 +114,20 @@ public function getLastInsertId(): ?int
     }
 
     /**
-     * @return Promise<mixed[][]>
+     * @inheritDoc
      */
-    public function getFields(): Promise
+    public function getFields(): ?array
     {
         if ($this->result === null) {
             throw new \Error("The current result set is empty; call this method before invoking ResultSet::nextResultSet()");
         }
 
         if ($this->result->state >= ResultProxy::COLUMNS_FETCHED) {
-            return new Success($this->result->columns);
+            return $this->result->columns;
         }
 
         $deferred = new Deferred;
         $this->result->deferreds[ResultProxy::COLUMNS_FETCHED][] = [$deferred, &$this->result->columns, null];
-        return $deferred->promise();
+        return await($deferred->promise());
     }
 }
diff --git a/src/Internal/ConnectionState.php b/src/Internal/ConnectionState.php
index 28eef3d..a18fe57 100644
--- a/src/Internal/ConnectionState.php
+++ b/src/Internal/ConnectionState.php
@@ -8,20 +8,17 @@ final class ConnectionState
 {
     use Struct;
 
-    /** @var int|null */
-    public $affectedRows;
-    public $insertId;
-    public $statusFlags;
-    public $warnings;
-    public $statusInfo;
-    public $sessionState = [];
-    public $errorMsg;
-    public $errorCode;
-    public $errorState; // begins with "#"
+    public ?int $affectedRows = null;
+    public ?int $insertId = null;
+    public ?int $statusFlags = null;
+    public ?int $warnings = null;
+    public ?string $statusInfo = null;
+    public array $sessionState = [];
+    public ?string $errorMsg = null;
+    public ?int $errorCode = null;
+    public ?string $errorState = null; // begins with "#"
 
-    /** @var string */
-    public $serverVersion;
+    public string $serverVersion;
 
-    /** @var string */
-    public $charset;
+    public int $charset;
 }
diff --git a/src/Internal/ConnectionStatement.php b/src/Internal/ConnectionStatement.php
index e86b0ea..76c289c 100644
--- a/src/Internal/ConnectionStatement.php
+++ b/src/Internal/ConnectionStatement.php
@@ -3,29 +3,26 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\Deferred;
+use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
-use Amp\Promise;
 use Amp\Sql\ConnectionException;
-use Amp\Success;
+use function Amp\await;
 
 final class ConnectionStatement implements Statement
 {
-    private $paramCount;
-    private $numParamCount;
-    private $named = [];
-    private $byNamed;
-    private $query;
-    private $stmtId;
-    private $prebound = [];
+    private int $paramCount;
+    private int $numParamCount;
+    private array $named = [];
+    private array $byNamed;
+    private string $query;
+    private int $stmtId;
+    private array $prebound = [];
 
-    /** @var Processor|null */
-    private $processor;
+    private ?Processor $processor;
 
-    /** @var ResultProxy */
-    private $result;
+    private ResultProxy $result;
 
-    /** @var int */
-    private $lastUsedAt;
+    private int $lastUsedAt;
 
     public function __construct(Processor $processor, string $query, int $stmtId, array $named, ResultProxy $result)
     {
@@ -69,21 +66,19 @@ public function isAlive(): bool
     }
 
     /** {@inheritdoc} */
-    public function bind($paramId, $data): void
+    public function bind(int|string $paramId, mixed $data): void
     {
         if (\is_int($paramId)) {
             if ($paramId >= $this->numParamCount) {
-                throw new \Error("Parameter id $paramId is not defined for this prepared statement");
+                throw new \Error("Parameter $paramId is not defined for this prepared statement");
             }
             $i = $paramId;
-        } elseif (\is_string($paramId)) {
+        } else {
             if (!isset($this->byNamed[$paramId])) {
                 throw new \Error("Parameter :$paramId is not defined for this prepared statement");
             }
             $array = $this->byNamed[$paramId];
             $i = \reset($array);
-        } else {
-            throw new \TypeError("Invalid parameter ID type");
         }
 
         if (!\is_scalar($data) && !(\is_object($data) && \method_exists($data, '__toString'))) {
@@ -109,7 +104,7 @@ public function bind($paramId, $data): void
     }
 
     /** {@inheritdoc} */
-    public function execute(array $params = []): Promise
+    public function execute(array $params = []): Result
     {
         $this->lastUsedAt = \time();
 
@@ -134,7 +129,7 @@ public function execute(array $params = []): Promise
             }
         }
 
-        return $this->getProcessor()->execute($this->stmtId, $this->query, $this->result->params, $prebound, $args);
+        return await($this->getProcessor()->execute($this->stmtId, $this->query, $this->result->params, $prebound, $args));
     }
 
     public function getQuery(): string
@@ -153,15 +148,15 @@ private function close(): void
         $this->processor = null;
     }
 
-    public function reset(): Promise
+    public function reset(): void
     {
-        return $this->getProcessor()->resetStmt($this->stmtId);
+        await($this->getProcessor()->resetStmt($this->stmtId));
     }
 
-    public function getFields(): Promise
+    public function getFields(): ?array
     {
         if ($this->result->state >= ResultProxy::COLUMNS_FETCHED) {
-            return new Success($this->result->columns);
+            return $this->result->columns;
         }
 
         if (isset($this->result->deferreds[ResultProxy::COLUMNS_FETCHED][0])) {
@@ -170,7 +165,7 @@ public function getFields(): Promise
 
         $deferred = new Deferred;
         $this->result->deferreds[ResultProxy::COLUMNS_FETCHED][0] = [$deferred, &$this->result->columns, null];
-        return $deferred->promise();
+        return await($deferred->promise());
     }
 
     /** {@inheritdoc} */
diff --git a/src/Internal/ConnectionTransaction.php b/src/Internal/ConnectionTransaction.php
index a611448..6cf0e39 100644
--- a/src/Internal/ConnectionTransaction.php
+++ b/src/Internal/ConnectionTransaction.php
@@ -6,26 +6,22 @@
 use Amp\Mysql\PooledStatement;
 use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
-use Amp\Promise;
 use Amp\Sql\Transaction as SqlTransaction;
 use Amp\Sql\TransactionError;
-use function Amp\call;
+use function Amp\await;
 
 final class ConnectionTransaction implements SqlTransaction
 {
     const SAVEPOINT_PREFIX = "amp_";
 
-    /** @var Processor|null */
-    private $processor;
+    private ?Processor $processor;
 
-    /** @var int */
-    private $isolation;
+    private int $isolation;
 
     /** @var callable */
     private $release;
 
-    /** @var int */
-    private $refCount = 1;
+    private int $refCount = 1;
 
     /**
      * @param Processor $processor
@@ -51,7 +47,7 @@ public function __construct(Processor $processor, callable $release, int $isolat
         $this->processor = $processor;
 
         $refCount =& $this->refCount;
-        $this->release = static function () use (&$refCount, $release) {
+        $this->release = static function () use (&$refCount, $release): void {
             if (--$refCount === 0) {
                 $release();
             }
@@ -114,17 +110,15 @@ public function getIsolationLevel(): int
      *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
-    public function query(string $sql): Promise
+    public function query(string $sql): Result
     {
         if ($this->processor === null) {
             throw new TransactionError("The transaction has been committed or rolled back");
         }
 
-        return call(function () use ($sql) {
-            $result = yield $this->processor->query($sql);
-            ++$this->refCount;
-            return new PooledResult($result, $this->release);
-        });
+        $result = await($this->processor->query($sql));
+        ++$this->refCount;
+        return new PooledResult($result, $this->release);
     }
 
     /**
@@ -132,16 +126,14 @@ public function query(string $sql): Promise
      *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
-    public function prepare(string $sql): Promise
+    public function prepare(string $sql): Statement
     {
         if ($this->processor === null) {
             throw new TransactionError("The transaction has been committed or rolled back");
         }
 
-        return call(function () use ($sql) {
-            $statement = yield $this->processor->prepare($sql);
-            return new PooledStatement($statement, $this->release);
-        });
+        $statement = await($this->processor->prepare($sql));
+        return new PooledStatement($statement, $this->release);
     }
 
     /**
@@ -149,30 +141,25 @@ public function prepare(string $sql): Promise
      *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
-    public function execute(string $sql, array $params = []): Promise
+    public function execute(string $sql, array $params = []): Result
     {
         if ($this->processor === null) {
             throw new TransactionError("The transaction has been committed or rolled back");
         }
 
-        return call(function () use ($sql, $params) {
-            $statement = yield $this->processor->prepare($sql);
-            \assert($statement instanceof Statement);
-            $result = yield $statement->execute($params);
+        $statement = await($this->processor->prepare($sql));
+        $result = $statement->execute($params);
 
-            ++$this->refCount;
-            return new PooledResult($result, $this->release);
-        });
+        ++$this->refCount;
+        return new PooledResult($result, $this->release);
     }
 
     /**
      * Commits the transaction and makes it inactive.
      *
-     * @return Promise<Result>
-     *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
-    public function commit(): Promise
+    public function commit(): void
     {
         if ($this->processor === null) {
             throw new TransactionError("The transaction has been committed or rolled back");
@@ -182,17 +169,15 @@ public function commit(): Promise
         $this->processor = null;
         $promise->onResolve($this->release);
 
-        return $promise;
+        await($promise);
     }
 
     /**
      * Rolls back the transaction and makes it inactive.
      *
-     * @return Promise<Result>
-     *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
-    public function rollback(): Promise
+    public function rollback(): void
     {
         if ($this->processor === null) {
             throw new TransactionError("The transaction has been committed or rolled back");
@@ -202,7 +187,7 @@ public function rollback(): Promise
         $this->processor = null;
         $promise->onResolve($this->release);
 
-        return $promise;
+        await($promise);
     }
 
     /**
@@ -210,13 +195,11 @@ public function rollback(): Promise
      *
      * @param string $identifier Savepoint identifier.
      *
-     * @return Promise<Result>
-     *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
-    public function createSavepoint(string $identifier): Promise
+    public function createSavepoint(string $identifier): void
     {
-        return $this->query(\sprintf("SAVEPOINT `%s%s`", self::SAVEPOINT_PREFIX, \sha1($identifier)));
+        $this->query(\sprintf("SAVEPOINT `%s%s`", self::SAVEPOINT_PREFIX, \sha1($identifier)));
     }
 
     /**
@@ -224,13 +207,11 @@ public function createSavepoint(string $identifier): Promise
      *
      * @param string $identifier Savepoint identifier.
      *
-     * @return Promise<Result>
-     *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
-    public function rollbackTo(string $identifier): Promise
+    public function rollbackTo(string $identifier): void
     {
-        return $this->query(\sprintf("ROLLBACK TO `%s%s`", self::SAVEPOINT_PREFIX, \sha1($identifier)));
+        $this->query(\sprintf("ROLLBACK TO `%s%s`", self::SAVEPOINT_PREFIX, \sha1($identifier)));
     }
 
     /**
@@ -238,12 +219,10 @@ public function rollbackTo(string $identifier): Promise
      *
      * @param string $identifier Savepoint identifier.
      *
-     * @return Promise<Result>
-     *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
-    public function releaseSavepoint(string $identifier): Promise
+    public function releaseSavepoint(string $identifier): void
     {
-        return $this->query(\sprintf("RELEASE SAVEPOINT `%s%s`", self::SAVEPOINT_PREFIX, \sha1($identifier)));
+        $this->query(\sprintf("RELEASE SAVEPOINT `%s%s`", self::SAVEPOINT_PREFIX, \sha1($identifier)));
     }
 }
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index fba461b..effa6e9 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -3,7 +3,6 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\CancellationToken;
-use Amp\Coroutine;
 use Amp\Deferred;
 use Amp\File;
 use Amp\Loop;
@@ -16,6 +15,8 @@
 use Amp\Sql\FailureException;
 use Amp\Sql\QueryError;
 use Amp\Sql\TransientResource;
+use function Amp\await;
+use function Amp\defer;
 
 /* @TODO
  * 14.2.3 Auth switch request??
@@ -60,53 +61,49 @@ class Processor implements TransientResource
 REGEX;
 
     /** @var \Generator[] */
-    private $processors = [];
+    private array $processors = [];
 
     /** @var int */
-    private $protocol;
+    private int $protocol;
 
-    private $seqId = -1;
-    private $compressionId = -1;
+    private int $seqId = -1;
+    private int $compressionId = -1;
 
-    /** @var EncryptableSocket */
-    private $socket;
+    private ?EncryptableSocket $socket;
 
-    private $authPluginDataLen;
-    private $query;
-    public $named = [];
+    private int $authPluginDataLen;
+    private ?string $query = null;
+    public array $named = [];
 
     /** @var callable|null */
     private $parseCallback = null;
     /** @var callable|null */
     private $packetCallback = null;
 
-    /** @var \Amp\Promise|null */
-    private $pendingWrite;
+    private ?Promise $pendingWrite = null;
 
-    /** @var \Amp\Sql\ConnectionConfig */
-    public $config;
+    public ConnectionConfig $config;
 
-    /** @var \Amp\Deferred[] */
-    private $deferreds = [];
+    /** @var Deferred[] */
+    private array $deferreds = [];
 
     /** @var callable[] */
-    private $onReady = [];
+    private array $onReady = [];
 
-    /** @var ResultProxy|null */
-    private $result;
+    private ?ResultProxy $result = null;
 
     /** @var int */
-    private $lastUsedAt;
+    private int $lastUsedAt;
 
-    private $connectionId;
-    private $authPluginData;
-    private $capabilities = 0;
-    private $serverCapabilities = 0;
-    private $authPluginName;
-    private $connInfo;
-    private $refcount = 1;
+    private int $connectionId;
+    private string $authPluginData;
+    private int $capabilities = 0;
+    private int $serverCapabilities = 0;
+    private string $authPluginName;
+    private ConnectionState $connInfo;
+    private int $refcount = 1;
 
-    private $connectionState = self::UNCONNECTED;
+    private int $connectionState = self::UNCONNECTED;
 
     private const MAX_PACKET_SIZE = 0xffffff;
     private const MAX_UNCOMPRESSED_BUFLEN = 0xfffffb;
@@ -197,7 +194,7 @@ private function addDeferred(Deferred $deferred): void
         $this->socket->reference();
     }
 
-    public function connect(CancellationToken $token): Promise
+    public function connect(CancellationToken $token): void
     {
         \assert(!$this->processors, self::class."::connect() must not be called twice");
 
@@ -205,17 +202,13 @@ public function connect(CancellationToken $token): Promise
 
         $this->processors = [$this->parseMysql()];
 
-        $id = $token->subscribe(function (): void {
-            $this->close();
-        });
+        $id = $token->subscribe(fn() => $this->close());
 
-        Promise\rethrow(new Coroutine($this->read()));
+        defer(fn() => $this->read());
 
         $promise = $deferred->promise();
 
-        $promise->onResolve(static function () use ($id, $token): void {
-            $token->unsubscribe($id);
-        });
+        $promise->onResolve(static fn() => $token->unsubscribe($id));
 
         if ($this->config->getCharset() !== ConnectionConfig::DEFAULT_CHARSET || $this->config->getCollation() !== ConnectionConfig::DEFAULT_COLLATE) {
             $promise->onResolve(function (?\Throwable $exception): void {
@@ -230,13 +223,13 @@ public function connect(CancellationToken $token): Promise
             });
         }
 
-        return $promise;
+        await($promise);
     }
 
-    private function read(): \Generator
+    private function read(): void
     {
         try {
-            while (($bytes = yield $this->socket->read()) !== null) {
+            while (($bytes = $this->socket->read()) !== null) {
                 // @codeCoverageIgnoreStart
                 \assert((function () use ($bytes) {
                     if (\defined("MYSQL_DEBUG")) {
@@ -344,26 +337,24 @@ protected function startCommand(callable $callback): Promise
 
     public function setCharset(string $charset, string $collate = ""): Promise
     {
-        return \Amp\call(function () use ($charset, $collate) {
-            if ($collate === "" && false !== $off = \strpos($charset, "_")) {
-                $collate = $charset;
-                $charset = \substr($collate, 0, $off);
-            }
+        if ($collate === "" && false !== $off = \strpos($charset, "_")) {
+            $collate = $charset;
+            $charset = \substr($collate, 0, $off);
+        }
 
-            $query = "SET NAMES '$charset'" . ($collate === "" ? "" : " COLLATE '$collate'");
-            $result = yield $this->query($query);
+        $query = "SET NAMES '$charset'" . ($collate === "" ? "" : " COLLATE '$collate'");
+        $promise = $this->query($query);
 
-            $this->config = $this->config->withCharset($charset, $collate);
+        $this->config = $this->config->withCharset($charset, $collate);
 
-            return $result;
-        });
+        return $promise;
     }
 
 
     /** @see 14.6.3 COM_INIT_DB */
     public function useDb(string $db): Promise
     {
-        return $this->startCommand(function () use ($db) {
+        return $this->startCommand(function () use ($db): void {
             $this->config = $this->config->withDatabase($db);
             $this->sendPacket("\x02$db");
         });
@@ -372,7 +363,7 @@ public function useDb(string $db): Promise
     /** @see 14.6.4 COM_QUERY */
     public function query(string $query): Promise
     {
-        return $this->startCommand(function () use ($query) {
+        return $this->startCommand(function () use ($query): void {
             $this->query = $query;
             $this->parseCallback = [$this, "handleQuery"];
             $this->sendPacket("\x03$query");
@@ -382,11 +373,11 @@ public function query(string $query): Promise
     /** @see 14.7.4 COM_STMT_PREPARE */
     public function prepare(string $query): Promise
     {
-        return $this->startCommand(function () use ($query) {
+        return $this->startCommand(function () use ($query): void {
             $this->query = $query;
             $this->parseCallback = [$this, "handlePrepare"];
 
-            $query = \preg_replace_callback(self::STATEMENT_PARAM_REGEX, function ($m) {
+            $query = \preg_replace_callback(self::STATEMENT_PARAM_REGEX, function ($m): string {
                 static $index = 0;
                 if ($m[2] !== "?") {
                     $this->named[$m[3]][] = $index;
@@ -425,7 +416,7 @@ public function changeUser($user, $pass, $db = null) {
     /** @see 14.6.15 COM_PING */
     public function ping(): Promise
     {
-        return $this->startCommand(function () {
+        return $this->startCommand(function (): void {
             $this->sendPacket("\x0e");
         });
     }
@@ -433,7 +424,7 @@ public function ping(): Promise
     /** @see 14.6.19 COM_RESET_CONNECTION */
     public function resetConnection(): Promise
     {
-        return $this->startCommand(function () {
+        return $this->startCommand(function (): void {
             $this->sendPacket("\x1f");
         });
     }
@@ -458,7 +449,7 @@ public function bindParam(int $stmtId, int $paramId, string $data): void
     public function execute(int $stmtId, string $query, array &$params, array $prebound, array $data = []): Promise
     {
         $deferred = new Deferred;
-        $this->appendTask(function () use ($stmtId, $query, &$params, $prebound, $data, $deferred) {
+        $this->appendTask(function () use ($stmtId, $query, &$params, $prebound, $data, $deferred): void {
             $payload = "\x17";
             $payload .= DataTypes::encodeInt32($stmtId);
             $payload .= \chr(0); // cursor flag // @TODO cursor types?!
@@ -511,7 +502,7 @@ public function execute(int $stmtId, string $query, array &$params, array $prebo
     public function closeStmt(int $stmtId): void
     {
         $payload = "\x19" . DataTypes::encodeInt32($stmtId);
-        $this->appendTask(function () use ($payload) {
+        $this->appendTask(function () use ($payload): void {
             if ($this->connectionState === self::READY) {
                 $this->resetIds();
                 $this->sendPacket($payload);
@@ -524,7 +515,7 @@ public function closeStmt(int $stmtId): void
     /** @see 14.6.5 COM_FIELD_LIST */
     public function listFields(string $table, string $like = "%"): Promise
     {
-        return $this->startCommand(static function () use ($table, $like) {
+        return $this->startCommand(static function () use ($table, $like): void {
             $this->sendPacket("\x04$table\0$like");
             $this->parseCallback = [$this, "handleFieldlist"];
         });
@@ -555,7 +546,7 @@ public function listAllFields(string $table, string $like = "%"): Promise
     /** @see 14.6.6 COM_CREATE_DB */
     public function createDatabase(string $db): Promise
     {
-        return $this->startCommand(function () use ($db) {
+        return $this->startCommand(function () use ($db): void {
             $this->sendPacket("\x05$db");
         });
     }
@@ -563,7 +554,7 @@ public function createDatabase(string $db): Promise
     /** @see 14.6.7 COM_DROP_DB */
     public function dropDatabase(string $db): Promise
     {
-        return $this->startCommand(function () use ($db) {
+        return $this->startCommand(function () use ($db): void {
             $this->sendPacket("\x06$db");
         });
     }
@@ -574,7 +565,7 @@ public function dropDatabase(string $db): Promise
      */
     public function refresh(int $subcommand): Promise
     {
-        return $this->startCommand(function () use ($subcommand) {
+        return $this->startCommand(function () use ($subcommand): void {
             $this->sendPacket("\x07" . \chr($subcommand));
         });
     }
@@ -582,7 +573,7 @@ public function refresh(int $subcommand): Promise
     /** @see 14.6.9 COM_SHUTDOWN */
     public function shutdown(): Promise
     {
-        return $this->startCommand(function () {
+        return $this->startCommand(function (): void {
             $this->sendPacket("\x08\x00"); /* SHUTDOWN_DEFAULT / SHUTDOWN_WAIT_ALL_BUFFERS, only one in use */
         });
     }
@@ -590,7 +581,7 @@ public function shutdown(): Promise
     /** @see 14.6.10 COM_STATISTICS */
     public function statistics(): Promise
     {
-        return $this->startCommand(function () {
+        return $this->startCommand(function (): void {
             $this->sendPacket("\x09");
             $this->parseCallback = [$this, "readStatistics"];
         });
@@ -599,7 +590,7 @@ public function statistics(): Promise
     /** @see 14.6.11 COM_PROCESS_INFO */
     public function processInfo(): Promise
     {
-        return $this->startCommand(function () {
+        return $this->startCommand(function (): void {
             $this->sendPacket("\x0a");
             $this->query("SHOW PROCESSLIST");
         });
@@ -608,7 +599,7 @@ public function processInfo(): Promise
     /** @see 14.6.13 COM_PROCESS_KILL */
     public function killProcess(int $process): Promise
     {
-        return $this->startCommand(function () use ($process) {
+        return $this->startCommand(function () use ($process): void {
             $this->sendPacket("\x0c" . DataTypes::encodeInt32($process));
         });
     }
@@ -616,7 +607,7 @@ public function killProcess(int $process): Promise
     /** @see 14.6.14 COM_DEBUG */
     public function debugStdout(): Promise
     {
-        return $this->startCommand(function () {
+        return $this->startCommand(function (): void {
             $this->sendPacket("\x0d");
         });
     }
@@ -626,7 +617,7 @@ public function resetStmt(int $stmtId): Promise
     {
         $payload = "\x1a" . DataTypes::encodeInt32($stmtId);
         $deferred = new Deferred;
-        $this->appendTask(function () use ($payload, $deferred) {
+        $this->appendTask(function () use ($payload, $deferred): void {
             $this->resetIds();
             $this->addDeferred($deferred);
             $this->sendPacket($payload);
@@ -639,7 +630,7 @@ public function fetchStmt(int $stmtId): Promise
     {
         $payload = "\x1c" . DataTypes::encodeInt32($stmtId) . DataTypes::encodeInt32(1);
         $deferred = new Deferred;
-        $this->appendTask(function () use ($payload, $deferred) {
+        $this->appendTask(function () use ($payload, $deferred): void {
             $this->resetIds();
             $this->addDeferred($deferred);
             $this->sendPacket($payload);
@@ -848,21 +839,17 @@ private function handleHandshake(string $packet): void
     /** @see 14.6.4.1.2 LOCAL INFILE Request */
     private function handleLocalInfileRequest(string $packet): void
     {
-        \Amp\asyncCall(function () use ($packet) {
+        defer(function () use ($packet): void {
             try {
                 $filePath = \substr($packet, 1);
                 /** @var \Amp\File\File $fileHandle */
-                if (\function_exists("Amp\\File\\openFile")) {
-                    // amphp/file 2.x
-                    $fileHandle = yield File\openFile($filePath, 'r');
-                } elseif (\function_exists("Amp\\File\\open")) {
-                    // amphp/file 1.x or 0.3.x
-                    $fileHandle = yield File\open($filePath, 'r');
-                } else {
+                if (!\function_exists("Amp\\File\\openFile")) {
                     throw new \Error("amphp/file must be installed for LOCAL INFILE queries");
                 }
 
-                while ("" != ($chunk = yield $fileHandle->read())) {
+                $fileHandle = File\openFile($filePath, 'r');
+
+                while ("" != ($chunk = $fileHandle->read())) {
                     $this->sendPacket($chunk);
                 }
                 $this->sendPacket("");
@@ -1222,7 +1209,7 @@ public function close(): void
         }
     }
 
-    private function write(string $packet): Promise
+    private function write(string $packet): void
     {
         $packet = $this->compilePacket($packet);
 
@@ -1247,18 +1234,11 @@ private function write(string $packet): Promise
             $packet = $this->compressPacket($packet);
         }
 
-        return $this->pendingWrite = $this->socket->write($packet);
+        $this->socket->write($packet);
     }
 
     private function resetIds(): void
     {
-        if ($this->pendingWrite) {
-            $this->pendingWrite->onResolve(function () {
-                $this->seqId = $this->compressionId = -1;
-            });
-            return;
-        }
-
         $this->seqId = $this->compressionId = -1;
     }
 
@@ -1521,11 +1501,11 @@ private function sendHandshake(bool $inSSL = false): void
         $payload .= \str_repeat("\0", 23); // reserved
 
         if (!$inSSL && ($this->capabilities & self::CLIENT_SSL)) {
-            \Amp\asyncCall(function () use ($payload) {
+            defer(function () use ($payload): void {
                 try {
-                    yield $this->write($payload);
+                    $this->write($payload);
 
-                    yield $this->socket->setupTls();
+                    $this->socket->setupTls();
 
                     $this->sendHandshake(true);
                 } catch (\Throwable $e) {
@@ -1593,12 +1573,12 @@ private function sendHandshake(bool $inSSL = false): void
     }
 
     /** @see 14.1.2 MySQL Packet */
-    protected function sendPacket(string $payload): Promise
+    protected function sendPacket(string $payload): void
     {
         if ($this->connectionState !== self::READY) {
             throw new \Error("Connection not ready, cannot send any packets");
         }
 
-        return $this->write($payload);
+        $this->write($payload);
     }
 }
diff --git a/src/Internal/ResultProxy.php b/src/Internal/ResultProxy.php
index d30a336..dbef6d5 100644
--- a/src/Internal/ResultProxy.php
+++ b/src/Internal/ResultProxy.php
@@ -10,27 +10,23 @@ final class ResultProxy
 {
     use Struct;
 
-    public $columnCount;
-    public $columns = [];
-    public $params = [];
-    public $columnsToFetch;
-    public $rows = [];
-    public $fetchedRows = 0;
-    public $userFetched = 0;
+    public int $columnCount;
+    public array $columns = [];
+    public array $params = [];
+    public int $columnsToFetch;
+    public array $rows = [];
+    public int $fetchedRows = 0;
+    public int $userFetched = 0;
 
-    /** @var int|null */
-    public $insertId;
+    public ?int $insertId = null;
 
-    /** @var int|null */
-    public $affectedRows;
+    public ?int $affectedRows = null;
 
-    public $deferreds = [self::UNFETCHED => [], self::COLUMNS_FETCHED => [], self::ROWS_FETCHED => []];
+    public array $deferreds = [self::UNFETCHED => [], self::COLUMNS_FETCHED => [], self::ROWS_FETCHED => []];
 
-    /** @var int */
-    public $state = self::UNFETCHED;
+    public int $state = self::UNFETCHED;
 
-    /** @var \Amp\Deferred|null */
-    public $next;
+    public ?Deferred $next = null;
 
     public const UNFETCHED = 0;
     public const COLUMNS_FETCHED = 1;
diff --git a/src/Pool.php b/src/Pool.php
index 91f5be8..a21d6b0 100644
--- a/src/Pool.php
+++ b/src/Pool.php
@@ -3,7 +3,6 @@
 namespace Amp\Mysql;
 
 use Amp\Sql\Common\ConnectionPool;
-use Amp\Sql\Common\StatementPool as SqlStatementPool;
 use Amp\Sql\Connector;
 use Amp\Sql\Pool as SqlPool;
 use Amp\Sql\Result as SqlResult;
@@ -17,24 +16,21 @@ protected function createDefaultConnector(): Connector
         return connector();
     }
 
-    protected function createResult(SqlResult $result, callable $release): SqlResult
+    protected function createResult(SqlResult $result, callable $release): Result
     {
-        if (!$result instanceof Result) {
-            throw new \TypeError('Result object must be an instance of ' . Result::class);
-        }
+        \assert($result instanceof Result);
         return new PooledResult($result, $release);
     }
 
-    protected function createStatement(SqlStatement $statement, callable $release): SqlStatement
+    protected function createStatement(SqlStatement $statement, callable $release): Statement
     {
         \assert($statement instanceof Statement);
         return new PooledStatement($statement, $release);
     }
 
-    protected function createStatementPool(SqlPool $pool, SqlStatement $statement, callable $prepare): SqlStatementPool
+    protected function createStatementPool(SqlPool $pool, string $sql, callable $prepare): StatementPool
     {
-        \assert($statement instanceof Statement);
-        return new StatementPool($pool, $statement, $prepare);
+        return new StatementPool($pool, $sql, $prepare);
     }
 
     protected function createTransaction(SqlTransaction $transaction, callable $release): SqlTransaction
diff --git a/src/PooledResult.php b/src/PooledResult.php
index 4bb7acb..c914e66 100644
--- a/src/PooledResult.php
+++ b/src/PooledResult.php
@@ -2,14 +2,12 @@
 
 namespace Amp\Mysql;
 
-use Amp\Promise;
 use Amp\Sql\Common\PooledResult as SqlPooledResult;
 use Amp\Sql\Result as SqlResult;
 
 final class PooledResult extends SqlPooledResult implements Result
 {
-    /** @var Result */
-    private $result;
+    private Result $result;
 
     /**
      * @param Result $result
@@ -21,12 +19,9 @@ public function __construct(Result $result, callable $release)
         $this->result = $result;
     }
 
-    protected function newInstanceFrom(SqlResult $result, callable $release): SqlPooledResult
+    protected function newInstanceFrom(SqlResult $result, callable $release): PooledResult
     {
-        if (!$result instanceof Result) {
-            throw new \TypeError('Result object must be an instance of ' . Result::class);
-        }
-
+        \assert($result instanceof Result);
         return new self($result, $release);
     }
 
@@ -35,7 +30,7 @@ public function getLastInsertId(): ?int
         return $this->result->getRowCount();
     }
 
-    public function getFields(): Promise
+    public function getFields(): ?array
     {
         return $this->result->getFields();
     }
diff --git a/src/PooledStatement.php b/src/PooledStatement.php
index 74d0b37..7dc5c1c 100644
--- a/src/PooledStatement.php
+++ b/src/PooledStatement.php
@@ -2,13 +2,12 @@
 
 namespace Amp\Mysql;
 
-use Amp\Promise;
 use Amp\Sql\Common\PooledStatement as SqlPooledStatement;
 use Amp\Sql\Result as SqlResult;
 
 final class PooledStatement extends SqlPooledStatement implements Statement
 {
-    private $statement;
+    private Statement $statement;
 
     public function __construct(Statement $statement, callable $release)
     {
@@ -16,26 +15,24 @@ public function __construct(Statement $statement, callable $release)
         $this->statement = $statement;
     }
 
-    protected function createResult(SqlResult $result, callable $release): SqlResult
+    protected function createResult(SqlResult $result, callable $release): Result
     {
-        if (!$result instanceof Result) {
-            throw new \TypeError('Result object must be an instance of ' . Result::class);
-        }
+        \assert($result instanceof Result);
         return new PooledResult($result, $release);
     }
 
-    public function bind($paramId, $data): void
+    public function bind(int|string $paramId, mixed $data): void
     {
         $this->statement->bind($paramId, $data);
     }
 
-    public function getFields(): Promise
+    public function getFields(): ?array
     {
         return $this->statement->getFields();
     }
 
-    public function reset(): Promise
+    public function reset(): void
     {
-        return $this->statement->reset();
+        $this->statement->reset();
     }
 }
diff --git a/src/PooledTransaction.php b/src/PooledTransaction.php
index 7890f7c..7e83f2e 100644
--- a/src/PooledTransaction.php
+++ b/src/PooledTransaction.php
@@ -8,19 +8,15 @@
 
 final class PooledTransaction extends SqlPooledTransaction
 {
-    protected function createStatement(SqlStatement $statement, callable $release): SqlStatement
+    protected function createStatement(SqlStatement $statement, callable $release): Statement
     {
-        if (!$statement instanceof Statement) {
-            throw new \TypeError('Statement object must be an instance of ' . Statement::class);
-        }
+        \assert($statement instanceof Statement);
         return new PooledStatement($statement, $release);
     }
 
-    protected function createResult(SqlResult $result, callable $release): SqlResult
+    protected function createResult(SqlResult $result, callable $release): Result
     {
-        if (!$result instanceof Result) {
-            throw new \TypeError('Result object must be an instance of ' . Result::class);
-        }
+        \assert($result instanceof Result);
         return new PooledResult($result, $release);
     }
 }
diff --git a/src/RefreshTypes.php b/src/RefreshTypes.php
index 3735fa7..399284e 100644
--- a/src/RefreshTypes.php
+++ b/src/RefreshTypes.php
@@ -4,12 +4,12 @@
 
 final class RefreshTypes
 {
-    const REFRESH_GRANT = 0x01;
-    const REFRESH_LOG = 0x02;
-    const REFRESH_TABLES = 0x04;
-    const REFRESH_HOSTS = 0x08;
-    const REFRESH_STATUS = 0x10;
-    const REFRESH_THREADS = 0x20;
-    const REFRESH_SLAVE = 0x40;
-    const REFRESH_MASTER = 0x80;
+    public const REFRESH_GRANT = 0x01;
+    public const REFRESH_LOG = 0x02;
+    public const REFRESH_TABLES = 0x04;
+    public const REFRESH_HOSTS = 0x08;
+    public const REFRESH_STATUS = 0x10;
+    public const REFRESH_THREADS = 0x20;
+    public const REFRESH_SLAVE = 0x40;
+    public const REFRESH_MASTER = 0x80;
 }
diff --git a/src/Result.php b/src/Result.php
index 220abf7..619c441 100644
--- a/src/Result.php
+++ b/src/Result.php
@@ -2,7 +2,6 @@
 
 namespace Amp\Mysql;
 
-use Amp\Promise;
 use Amp\Sql\Result as SqlResult;
 
 interface Result extends SqlResult
@@ -14,9 +13,9 @@ interface Result extends SqlResult
     public function getLastInsertId(): ?int;
 
     /**
-     * @return Promise<array|null>
+     * @return array|null
      *
-     * @psalm-return Promise<array<int, array<int, string>>|null>
+     * @psalm-return array<int, array<int, string>>|null
      */
-    public function getFields(): Promise;
+    public function getFields(): ?array;
 }
diff --git a/src/Statement.php b/src/Statement.php
index c4e37ef..63c8aff 100644
--- a/src/Statement.php
+++ b/src/Statement.php
@@ -2,7 +2,6 @@
 
 namespace Amp\Mysql;
 
-use Amp\Promise;
 use Amp\Sql\Statement as SqlStatement;
 
 interface Statement extends SqlStatement
@@ -13,17 +12,12 @@ interface Statement extends SqlStatement
      *
      * @throws \Error If $paramId is not an int or string, or the position does not exist.
      */
-    public function bind($paramId, $data): void;
+    public function bind(int|string $paramId, mixed $data): void;
 
-    /**
-     * @return Promise<array>
-     */
-    public function getFields(): Promise;
+    public function getFields(): ?array;
 
     /**
      * Reset statement to state just after preparing.
-     *
-     * @return Promise<null>
      */
-    public function reset(): Promise;
+    public function reset(): void;
 }
diff --git a/src/StatementPool.php b/src/StatementPool.php
index bedd5ce..a96c2d3 100644
--- a/src/StatementPool.php
+++ b/src/StatementPool.php
@@ -2,39 +2,28 @@
 
 namespace Amp\Mysql;
 
-use Amp\Promise;
 use Amp\Sql\Common\StatementPool as SqlStatementPool;
-use Amp\Sql\Pool;
 use Amp\Sql\Result as SqlResult;
 use Amp\Sql\Statement as SqlStatement;
-use Amp\Success;
-use function Amp\call;
 
 final class StatementPool extends SqlStatementPool implements Statement
 {
-    private $params = [];
+    private array $params = [];
 
-    public function __construct(Pool $pool, Statement $statement, callable $prepare)
-    {
-        parent::__construct($pool, $statement, $prepare);
-    }
-
-    protected function prepare(SqlStatement $statement): Promise
+    protected function prepare(SqlStatement $statement): Statement
     {
         \assert($statement instanceof Statement);
 
-        return call(function () use ($statement) {
-            yield $statement->reset();
+        $statement->reset();
 
-            foreach ($this->params as $paramId => $data) {
-                $statement->bind($paramId, $data);
-            }
+        foreach ($this->params as $paramId => $data) {
+            $statement->bind($paramId, $data);
+        }
 
-            return $statement;
-        });
+        return $statement;
     }
 
-    protected function createResult(SqlResult $result, callable $release): SqlResult
+    protected function createResult(SqlResult $result, callable $release): Result
     {
         if (!$result instanceof Result) {
             throw new \TypeError('Result object must be an instance of ' . Result::class);
@@ -43,7 +32,7 @@ protected function createResult(SqlResult $result, callable $release): SqlResult
         return new PooledResult($result, $release);
     }
 
-    public function bind($paramId, $data): void
+    public function bind(int|string $paramId, mixed $data): void
     {
         if (!\is_int($paramId) && !\is_string($paramId)) {
             throw new \TypeError("Invalid parameter ID type");
@@ -52,23 +41,16 @@ public function bind($paramId, $data): void
         $this->params[$paramId] = $data;
     }
 
-    public function reset(): Promise
+    public function reset(): void
     {
         $this->params = [];
-        return new Success;
     }
 
-    /**
-     * @return Promise<array>
-     */
-    public function getFields(): Promise
+    public function getFields(): ?array
     {
-        return call(function () {
-            $statement = yield from $this->pop();
-            \assert($statement instanceof Statement);
-            $fields = yield $statement->getFields();
-            $this->push($statement);
-            return $fields;
-        });
+        $statement = $this->pop();
+        $fields = $statement->getFields();
+        $this->push($statement);
+        return $fields;
     }
 }
diff --git a/src/TransactionError.php b/src/TransactionError.php
deleted file mode 100644
index 9ce8cda..0000000
--- a/src/TransactionError.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-class TransactionError extends \Amp\Sql\TransactionError
-{
-}
diff --git a/src/functions.php b/src/functions.php
index ccdd456..c11c414 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -3,7 +3,6 @@
 namespace Amp\Mysql;
 
 use Amp\Loop;
-use Amp\Promise;
 use Amp\Sql\Common\ConnectionPool;
 use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
 use Amp\Sql\Connector;
@@ -30,12 +29,12 @@ function connector(?Connector $connector = null): Connector
  *
  * @param SqlConnectionConfig $config
  *
- * @return Promise<Connection>
+ * @return Connection
  *
  * @throws \Amp\Sql\FailureException If connecting fails.
  * @throws \Error If the connection string does not contain a host, user, and password.
  */
-function connect(SqlConnectionConfig $config): Promise
+function connect(SqlConnectionConfig $config): Connection
 {
     return connector()->connect($config);
 }
diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php
index be76090..3685a34 100644
--- a/test/ConnectionTest.php
+++ b/test/ConnectionTest.php
@@ -5,32 +5,30 @@
 use Amp\Mysql\CancellableConnector;
 use Amp\Mysql\Connection;
 use Amp\Mysql\ConnectionConfig;
-use Amp\Promise;
+use Amp\Sql\Link;
 
 class ConnectionTest extends LinkTest
 {
-    protected function getLink(string $connectionString): Promise
+    protected function getLink(string $connectionString): Link
     {
-        return (new CancellableConnector())->connect(ConnectionConfig::fromString($connectionString));
+        return (new CancellableConnector)->connect(ConnectionConfig::fromString($connectionString));
     }
 
     public function testConnect()
     {
-        /** @var Connection $db */
-        $db = yield Connection::connect(ConnectionConfig::fromString("host=".DB_HOST." user=".DB_USER." pass=".DB_PASS." db=test"));
+        $db = Connection::connect(ConnectionConfig::fromString("host=".DB_HOST." user=".DB_USER." pass=".DB_PASS." db=test"));
 
         $this->assertInstanceOf(Connection::class, $db);
 
         /* use an alternative charset... Default is utf8mb4_general_ci */
-        yield $db->setCharset("latin1_general_ci");
+        $db->setCharset("latin1_general_ci");
 
         $db->close();
     }
 
     public function testDoubleClose()
     {
-        /** @var Connection $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
         $db->close();
 
diff --git a/test/LinkTest.php b/test/LinkTest.php
index f5bafa5..2b96834 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -4,33 +4,34 @@
 
 use Amp\Mysql\DataTypes;
 use Amp\Mysql\Result;
-use Amp\Mysql\Statement;
 use Amp\PHPUnit\AsyncTestCase;
 use Amp\Pipeline;
-use Amp\Promise;
 use Amp\Sql\Link;
 use Amp\Sql\QueryError;
-use Amp\Sql\Transaction;
 
 abstract class LinkTest extends AsyncTestCase
 {
     /**
      * Returns the Link class to be tested.
      *
-     * @return Promise<Link>
+     * @return Link
      */
-    abstract protected function getLink(string $connectionString): Promise;
+    abstract protected function getLink(string $connectionString): Link;
+
+    public function setUp(): void
+    {
+        parent::setUp();
+        $this->ignoreLoopWatchers();
+    }
 
     public function testQuery()
     {
-        /** @var Link $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-        /** @var Result $resultset */
-        $resultset = yield $db->execute("SELECT ? AS a", [M_PI]);
+        $resultset = $db->execute("SELECT ? AS a", [M_PI]);
         $this->assertInstanceOf(Result::class, $resultset);
 
-        for ($i = 0; $row = yield $resultset->continue(); ++$i) {
+        for ($i = 0; $row = $resultset->continue(); ++$i) {
             $this->assertSame(["a" => M_PI], $row);
         }
 
@@ -39,15 +40,13 @@ public function testQuery()
 
     public function testQueryFetchRow()
     {
-        /** @var Link $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-        /** @var Result $resultset */
-        $resultset = yield $db->query('SELECT a FROM main WHERE a < 4');
+        $resultset = $db->query('SELECT a FROM main WHERE a < 4');
         $this->assertInstanceOf(Result::class, $resultset);
 
         $got = [];
-        while ($row = yield $resultset->continue()) {
+        while ($row = $resultset->continue()) {
             $got[] = \array_values($row);
         }
 
@@ -59,39 +58,36 @@ public function testQueryWithInvalidQuery()
         $this->expectException(QueryError::class);
         $this->expectExceptionMessage('You have an error in your SQL syntax');
 
-        /** @var Link $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-        yield $db->query("SELECT & FROM main WHERE a = 1");
+        $db->query("SELECT & FROM main WHERE a = 1");
     }
 
     public function testMultiStmt()
     {
-        /** @var Link $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
 
-        /** @var Result $resultset */
-        $resultset = yield $db->query("SELECT a FROM main; SELECT b FROM main WHERE a = 5; SELECT b AS d, a + 1 AS c FROM main WHERE b > 4");
+        $resultset = $db->query("SELECT a FROM main; SELECT b FROM main WHERE a = 5; SELECT b AS d, a + 1 AS c FROM main WHERE b > 4");
         $this->assertInstanceOf(Result::class, $resultset);
 
         $got = [];
-        while ($row = yield $resultset->continue()) {
+        while ($row = $resultset->continue()) {
             $got[] = \array_values($row);
         }
         $this->assertSame([[1], [2], [3], [4], [5]], $got);
-        $this->assertInstanceOf(Result::class, $resultset = yield $resultset->getNextResult());
+        $this->assertInstanceOf(Result::class, $resultset = $resultset->getNextResult());
 
         $got = [];
-        while ($row = yield $resultset->continue()) {
+        while ($row = $resultset->continue()) {
             $got[] = \array_values($row);
         }
         $this->assertSame([[6]], $got);
-        $this->assertInstanceOf(Result::class, $resultset = yield $resultset->getNextResult());
+        $this->assertInstanceOf(Result::class, $resultset = $resultset->getNextResult());
 
-        $fields = yield $resultset->getFields();
+        $fields = $resultset->getFields();
 
         $got = [];
-        while ($row = yield $resultset->continue()) {
+        while ($row = $resultset->continue()) {
             $got[] = $row;
         }
         $this->assertSame([["d" => 5, "c" => 5], ["d" => 6, "c" => 6]], $got);
@@ -103,19 +99,14 @@ public function testMultiStmt()
         $this->assertSame($fields[1]["name"], "c");
         $this->assertSame($fields[1]["type"], DataTypes::MYSQL_TYPE_LONGLONG);
 
-        $this->assertNull(yield $resultset->getNextResult());
+        $this->assertNull($resultset->getNextResult());
     }
 
     public function testPrepared()
     {
-        /** @var Link $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
-
-        /**
-         * @var Statement $stmt
-         * @var Result $result
-         */
-        $stmt = yield $db->prepare("SELECT * FROM main WHERE a = ? OR b = :num");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
+
+        $stmt = $db->prepare("SELECT * FROM main WHERE a = ? OR b = :num");
         $base = [
             "catalog" => "def",
             "schema" => "test",
@@ -127,40 +118,40 @@ public function testPrepared()
             "flags" => 0,
             "decimals" => 0,
         ];
-        $this->assertEquals(yield $stmt->getFields(), [$base + ["name" => "a", "original_name" => "a"], $base + ["name" => "b", "original_name" => "b"]]);
+        $this->assertEquals($stmt->getFields(), [$base + ["name" => "a", "original_name" => "a"], $base + ["name" => "b", "original_name" => "b"]]);
         $stmt->bind("num", 5);
-        $result = yield $stmt->execute([2]);
+        $result = $stmt->execute([2]);
         $this->assertInstanceOf(Result::class, $result);
         $got = [];
-        while ($row = yield $result->continue()) {
+        while ($row = $result->continue()) {
             $got[] = \array_values($row);
         }
         $this->assertCount(2, $got);
 
-        $stmt = yield $db->prepare("SELECT * FROM main WHERE a = ? OR b = ?");
-        $result = yield $stmt->execute([1, 8]);
+        $stmt = $db->prepare("SELECT * FROM main WHERE a = ? OR b = ?");
+        $result = $stmt->execute([1, 8]);
         $this->assertInstanceOf(Result::class, $result);
         $got = [];
-        while ($row = yield $result->continue()) {
+        while ($row = $result->continue()) {
             $got[] = \array_values($row);
         }
         $this->assertCount(1, $got);
 
-        $stmt = yield $db->prepare("SELECT * FROM main WHERE a = :a OR b = ?");
-        $result = yield $stmt->execute(["a" => 2, 5]);
+        $stmt = $db->prepare("SELECT * FROM main WHERE a = :a OR b = ?");
+        $result = $stmt->execute(["a" => 2, 5]);
         $this->assertInstanceOf(Result::class, $result);
         $got = [];
-        while ($row = yield $result->continue()) {
+        while ($row = $result->continue()) {
             $got[] = \array_values($row);
         }
         $this->assertCount(2, $got);
 
-        $stmt = yield $db->prepare("INSERT INTO main VALUES (:a, :b)");
-        $result = yield $stmt->execute(["a" => 10, "b" => 11]);
+        $stmt = $db->prepare("INSERT INTO main VALUES (:a, :b)");
+        $result = $stmt->execute(["a" => 10, "b" => 11]);
         $this->assertInstanceOf(Result::class, $result);
 
-        $stmt = yield $db->prepare("DELETE FROM main WHERE a = :a");
-        $result = yield $stmt->execute(["a" => 10]);
+        $stmt = $db->prepare("DELETE FROM main WHERE a = :a");
+        $result = $stmt->execute(["a" => 10]);
         $this->assertInstanceOf(Result::class, $result);
     }
 
@@ -169,26 +160,25 @@ public function testPrepareWithInvalidQuery()
         $this->expectException(QueryError::class);
         $this->expectExceptionMessage('You have an error in your SQL syntax');
 
-        /** @var \Amp\Sql\Link $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+
+        $statement = $db->prepare("SELECT & FROM main WHERE a = ?");
 
-        yield $db->prepare("SELECT & FROM main WHERE a = ?");
+        $statement->execute(); // Some implementations do not throw until execute() is called.
     }
 
     public function testBindWithInvalidParamId()
     {
         $this->expectException(\Error::class);
-        $this->expectExceptionMessage('Parameter id 1 is not defined for this prepared statement');
+        $this->expectExceptionMessage('Parameter 1 is not defined for this prepared statement');
 
-        /** @var Link $db */
-        $db = yield $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
+        $db = $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
 
-        /** @var Statement $statement */
-        $statement = yield $db->prepare("SELECT * FROM main WHERE a = ?");
+        $statement = $db->prepare("SELECT * FROM main WHERE a = ?");
 
         $statement->bind(1, 1);
 
-        yield $statement->execute(); // Some implementations do not throw until execute() is called.
+        $statement->execute(); // Some implementations do not throw until execute() is called.
     }
 
     public function testBindWithInvalidParamName()
@@ -196,31 +186,13 @@ public function testBindWithInvalidParamName()
         $this->expectException(\Error::class);
         $this->expectExceptionMessage('Parameter :b is not defined for this prepared statement');
 
-        /** @var Link $db */
-        $db = yield $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
+        $db = $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
 
-        /** @var Statement $statement */
-        $statement = yield $db->prepare("SELECT * FROM main WHERE a = :a");
+        $statement = $db->prepare("SELECT * FROM main WHERE a = :a");
 
         $statement->bind("b", 1);
 
-        yield $statement->execute(); // Some implementations do not throw until execute() is called.
-    }
-
-    public function testBindWithInvalidParamType()
-    {
-        $this->expectException(\Error::class);
-        $this->expectExceptionMessage('Invalid parameter ID type');
-
-        /** @var Link $db */
-        $db = yield $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
-
-        /** @var Statement $statement */
-        $statement = yield $db->prepare("SELECT * FROM main WHERE a = :a");
-
-        $statement->bind(3.14, 1);
-
-        yield $statement->execute(); // Some implementations do not throw until execute() is called.
+        $statement->execute(); // Some implementations do not throw until execute() is called.
     }
 
     public function testStatementExecuteWithTooFewParams()
@@ -228,33 +200,29 @@ public function testStatementExecuteWithTooFewParams()
         $this->expectException(\Error::class);
         $this->expectExceptionMessage('Parameter 1 for prepared statement missing');
 
-        /** @var Link $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-        /** @var Statement $stmt */
-        $stmt = yield $db->prepare("SELECT * FROM main WHERE a = ? AND b = ?");
-        yield $stmt->execute([1]);
+        $stmt = $db->prepare("SELECT * FROM main WHERE a = ? AND b = ?");
+        $stmt->execute([1]);
     }
 
     public function testExecute()
     {
-        /** @var Link $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-        /** @var Result $result */
-        $result = yield $db->execute("SELECT * FROM main WHERE a = ? OR b = ?", [2, 5]);
+        $result = $db->execute("SELECT * FROM main WHERE a = ? OR b = ?", [2, 5]);
         $this->assertInstanceOf(Result::class, $result);
         $got = [];
-        while ($row = yield $result->continue()) {
+        while ($row = $result->continue()) {
             $got[] = \array_values($row);
         }
         $this->assertCount(2, $got);
         $this->assertSame([[2, 3], [4, 5]], $got);
 
-        $result = yield $db->execute("INSERT INTO main VALUES (:a, :b)", ["a" => 10, "b" => 11]);
+        $result = $db->execute("INSERT INTO main VALUES (:a, :b)", ["a" => 10, "b" => 11]);
         $this->assertInstanceOf(Result::class, $result);
 
-        $result = yield $db->execute("DELETE FROM main WHERE a = :a", ["a" => 10]);
+        $result = $db->execute("DELETE FROM main WHERE a = :a", ["a" => 10]);
         $this->assertInstanceOf(Result::class, $result);
     }
 
@@ -263,10 +231,11 @@ public function testExecuteWithInvalidQuery()
         $this->expectException(QueryError::class);
         $this->expectExceptionMessage('You have an error in your SQL syntax');
 
-        /** @var Link $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-        yield $db->execute("SELECT & FROM main WHERE a = ?", [1]);
+        $db->execute("SELECT & FROM main WHERE a = ?", [1]);
+
+        $db->close();
     }
 
     public function testExecuteWithTooFewParams()
@@ -274,63 +243,63 @@ public function testExecuteWithTooFewParams()
         $this->expectException(\Error::class);
         $this->expectExceptionMessage('Parameter 1 for prepared statement missing');
 
-        /** @var Link $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+
+        $db->execute("SELECT * FROM main WHERE a = ? AND b = ?", [1]);
 
-        yield $db->execute("SELECT * FROM main WHERE a = ? AND b = ?", [1]);
+        $db->close();
     }
 
     public function testPreparedWithNegativeValue()
     {
-        /** @var Link $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-        yield $db->query("DROP TABLE IF EXISTS tmp");
+        $db->query("DROP TABLE IF EXISTS tmp");
 
-        /** @var \Amp\Sql\Statement $stmt */
-        $stmt = yield $db->prepare("CREATE TABLE tmp SELECT ? AS a");
-        yield $stmt->execute([-1]);
+        $stmt = $db->prepare("CREATE TABLE tmp SELECT ? AS a");
+        $stmt->execute([-1]);
 
-        /** @var \Amp\Mysql\Result $result */
-        $stmt = yield $db->prepare("SELECT a FROM tmp");
-        $result = yield $stmt->execute();
-        $row = yield $result->continue();
+        $stmt = $db->prepare("SELECT a FROM tmp");
+        $result = $stmt->execute();
+        $row = $result->continue();
 
         $this->assertEquals(\array_values($row), [-1]);
+
+        while ($result->continue()); // Consume remaining results.
+
+        $db->close();
     }
 
     public function testTransaction()
     {
-        /** @var Link $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-        /** @var Transaction $transaction */
-        $transaction = yield $db->beginTransaction();
+        $transaction = $db->beginTransaction();
 
-        /** @var Statement $statement */
-        $statement = yield $transaction->prepare("INSERT INTO main VALUES (?, ?)");
-        $result = yield $statement->execute([6, 7]);
+        $statement = $transaction->prepare("INSERT INTO main VALUES (?, ?)");
+        $result = $statement->execute([6, 7]);
         $this->assertInstanceOf(Result::class, $result);
 
-        /** @var Result $result */
-        $result = yield $transaction->execute("SELECT * FROM main WHERE a = ?", [6]);
+        $result = $transaction->execute("SELECT * FROM main WHERE a = ?", [6]);
 
         $got = [];
-        while ($row = yield $result->continue()) {
+        while ($row = $result->continue()) {
             $got[] = \array_values($row);
         }
         $this->assertCount(1, $got);
-        $result = yield $result->getNextResult();
+        $result = $result->getNextResult();
 
-        yield $transaction->rollback();
+        $transaction->rollback();
 
-        $result = yield $db->execute("SELECT * FROM main WHERE a = ?", [6]);
+        $result = $db->execute("SELECT * FROM main WHERE a = ?", [6]);
 
         $got = [];
-        while ($row = yield $result->continue()) {
+        while ($row = $result->continue()) {
             $got[] = \array_values($row);
         }
         $this->assertCount(0, $got);
+
+        $db->close();
     }
 
     /**
@@ -338,29 +307,26 @@ public function testTransaction()
      */
     public function testInsertSelect()
     {
-        /** @var Link $db */
-        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
         $a = 1;
 
-        /** @var Transaction $transaction */
-        $transaction = yield $db->beginTransaction();
+        $transaction = $db->beginTransaction();
 
         try {
-            /** @var Statement $statement */
-            $statement = yield $transaction->prepare("SELECT a, b FROM main WHERE a >= ?");
+            $statement = $transaction->prepare("SELECT a, b FROM main WHERE a >= ?");
 
-            $count = \count(yield Pipeline\toArray(yield $statement->execute([$a])));
+            $count = \count(Pipeline\toArray($statement->execute([$a])));
 
-            /** @var Statement $statement */
-            $statement = yield $transaction->prepare("INSERT INTO main (a, b) SELECT a, b FROM main WHERE a >= ?");
+            $statement = $transaction->prepare("INSERT INTO main (a, b) SELECT a, b FROM main WHERE a >= ?");
 
-            /** @var Result $result */
-            $result = yield $statement->execute([$a]);
+            $result = $statement->execute([$a]);
 
             $this->assertSame($count, $result->getRowCount());
         } finally {
-            yield $transaction->rollback();
+            $transaction->rollback();
         }
+
+        $db->close();
     }
 }
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 4598394..5ac6245 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -12,9 +12,12 @@
 use Amp\Mysql\Statement;
 use Amp\Promise;
 use Amp\Sql\Connector;
+use Amp\Sql\Link;
 use Amp\Sql\Transaction as SqlTransaction;
 use Amp\Success;
-use function Amp\call;
+use function Amp\async;
+use function Amp\await;
+use function Amp\delay;
 
 interface StatementOperation extends Statement
 {
@@ -22,18 +25,18 @@ interface StatementOperation extends Statement
 
 class PoolTest extends LinkTest
 {
-    protected function getLink(string $connectionString): Promise
+    protected function getLink(string $connectionString): Link
     {
-        return new Success(new Pool(ConnectionConfig::fromString($connectionString)));
+        return new Pool(ConnectionConfig::fromString($connectionString));
     }
 
     protected function createPool(array $connections): Pool
     {
         $connector = $this->createMock(Connector::class);
         $connector->method('connect')
-            ->will($this->returnCallback(function () use ($connections): Promise {
+            ->will($this->returnCallback(function () use ($connections): Connection {
                 static $count = 0;
-                return new Success($connections[$count++ % \count($connections)]);
+                return $connections[$count++ % \count($connections)];
             }));
 
         $config = ConnectionConfig::fromString('host=host;user=user;password=password');
@@ -46,7 +49,7 @@ protected function createPool(array $connections): Pool
      *
      * @return \Amp\Mysql\Internal\Processor[]|\PHPUnit\Framework\MockObject\MockObject[]
      */
-    private function makeProcessorSet(int $count)
+    private function makeProcessorSet(int $count): array
     {
         $processors = [];
 
@@ -59,7 +62,7 @@ private function makeProcessorSet(int $count)
         return $processors;
     }
 
-    private function makeConnectionSet(array $processors)
+    private function makeConnectionSet(array $processors): array
     {
         return \array_map((function (Processor $processor): Connection {
             return new self($processor);
@@ -81,20 +84,22 @@ public function getConnectionCounts(): array
      */
     public function testSingleQuery(int $count)
     {
-        $result = $this->createMock(StatementOperation::class);
+        $result = $this->createMock(Result::class);
 
         $processors = $this->makeProcessorSet($count);
 
         $connection = $processors[0];
         $connection->expects($this->once())
-            ->method('prepare')
+            ->method('query')
             ->with('SQL Query')
             ->will($this->returnValue(new Delayed(10, $result)));
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
-        $return = yield $pool->prepare('SQL Query');
-        $this->assertInstanceOf(Statement::class, $return);
+        $return = $pool->query('SQL Query');
+        $this->assertInstanceOf(Result::class, $return);
+
+        $pool->close();
     }
 
     /**
@@ -105,28 +110,32 @@ public function testSingleQuery(int $count)
     public function testConsecutiveQueries(int $count)
     {
         $rounds = 3;
-        $result = $this->createMock(StatementOperation::class);
+        $result = $this->createMock(Result::class);
 
         $processors = $this->makeProcessorSet($count);
 
         foreach ($processors as $connection) {
-            $connection->method('prepare')
+            $connection->method('query')
                 ->with('SQL Query')
                 ->will($this->returnValue(new Delayed(10, $result)));
         }
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
-        $promises = [];
+        try {
+            $promises = [];
 
-        for ($i = 0; $i < $count; ++$i) {
-            $promises[] = $pool->prepare('SQL Query');
-        }
+            for ($i = 0; $i < $count; ++$i) {
+                $promises[] = async(fn() => $pool->query('SQL Query'));
+            }
 
-        $results = yield $promises;
+            $results = await($promises);
 
-        foreach ($results as $result) {
-            $this->assertInstanceOf(Statement::class, $result);
+            foreach ($results as $result) {
+                $this->assertInstanceOf(Result::class, $result);
+            }
+        } finally {
+            $pool->close();
         }
     }
 
@@ -135,7 +144,7 @@ public function testConsecutiveQueries(int $count)
      *
      * @param int $count
      */
-    public function testMutlipleTransactions(int $count)
+    public function testMultipleTransactions(int $count)
     {
         $processors = $this->makeProcessorSet($count);
 
@@ -148,9 +157,13 @@ public function testMutlipleTransactions(int $count)
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
-        $return = yield $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED);
-        $this->assertInstanceOf(SqlTransaction::class, $return);
-        yield $return->rollback();
+        try {
+            $return = $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED);
+            $this->assertInstanceOf(SqlTransaction::class, $return);
+            $return->rollback();
+        } finally {
+            $pool->close();
+        }
     }
 
     /**
@@ -167,28 +180,24 @@ public function testConsecutiveTransactions(int $count)
 
         foreach ($processors as $connection) {
             $connection->method('query')
-                ->will($this->returnCallback(function () use ($result) {
-                    return new Delayed(10, $result);
-                }));
+                ->will($this->returnCallback(fn() => new Delayed(10, $result)));
         }
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
         $promises = [];
         for ($i = 0; $i < $count; ++$i) {
-            $promises[] = $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED);
+            $promises[] = async(fn() => $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED));
         }
 
-        $results = yield \array_map(function (Promise $promise): Promise {
-            return call(function () use ($promise) {
-                $transaction = yield $promise;
+        try {
+            \array_map(function (Promise $promise) {
+                $transaction = await($promise);
                 $this->assertInstanceOf(SqlTransaction::class, $transaction);
-                return yield $transaction->rollback();
-            });
-        }, $promises);
-
-        foreach ($results as $result) {
-            $this->assertInstanceof(CommandResult::class, $result);
+                $transaction->rollback();
+            }, $promises);
+        } finally {
+            $pool->close();
         }
     }
 
@@ -205,21 +214,24 @@ public function testExtractConnection(int $count)
         foreach ($processors as $connection) {
             $connection->expects($this->once())
                 ->method('query')
-                ->with($query);
+                ->with($query)
+                ->willReturn(new Success($this->createMock(Result::class)));
         }
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
-        $promises = [];
-        for ($i = 0; $i < $count; ++$i) {
-            $promises[] = $pool->extractConnection();
-        }
-
-        $results = yield $promises;
-
-        foreach ($results as $result) {
-            $this->assertInstanceof(Connection::class, $result);
-            $result->query($query);
+        try {
+            $promises = [];
+            for ($i = 0; $i < $count; ++$i) {
+                $promises[] = async(fn() => $pool->extractConnection());
+            }
+            $results = await($promises);
+            foreach ($results as $result) {
+                $this->assertInstanceof(Connection::class, $result);
+                $result->query($query);
+            }
+        } finally {
+            $pool->close();
         }
     }
 
@@ -252,18 +264,21 @@ public function testConnectionClosedInPool(int $count)
         \array_unshift($processors, $processor);
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
-        $this->assertSame($count + 1, $pool->getConnectionLimit());
 
-        $promises = [];
-        for ($i = 0; $i < $count + 1; ++$i) {
-            $promises[] = $pool->query($query);
-        }
-        yield $promises;
-
-        $promises = [];
-        for ($i = 0; $i < $count; ++$i) {
-            $promises[] = $pool->query($query);
+        try {
+            $this->assertSame($count + 1, $pool->getConnectionLimit());
+            $promises = [];
+            for ($i = 0; $i < $count + 1; ++$i) {
+                $promises[] = async(fn() => $pool->query($query));
+            }
+            await($promises);
+            $promises = [];
+            for ($i = 0; $i < $count; ++$i) {
+                $promises[] = async(fn() => $pool->query($query));
+            }
+            await($promises);
+        } finally {
+            $pool->close();
         }
-        yield $promises;
     }
 }
diff --git a/test/StatementTest.php b/test/StatementTest.php
index 593cff2..e8f2c07 100644
--- a/test/StatementTest.php
+++ b/test/StatementTest.php
@@ -2,7 +2,7 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Mysql\ConnectionStatement;
+use Amp\Mysql\Internal\ConnectionStatement;
 use Amp\Mysql\Internal\Processor;
 use Amp\Mysql\Internal\ResultProxy;
 use Amp\PHPUnit\AsyncTestCase;
@@ -11,10 +11,9 @@
 class StatementTest extends AsyncTestCase
 {
     /** @var MockObject&Processor */
-    protected $processor;
+    protected Processor $processor;
 
-    /** @var ResultProxy */
-    protected $resultProxy;
+    protected ResultProxy $resultProxy;
 
     public function setUp(): void
     {
diff --git a/test/bootstrap.php b/test/bootstrap.php
index 36b203b..024b26e 100644
--- a/test/bootstrap.php
+++ b/test/bootstrap.php
@@ -3,7 +3,6 @@
 namespace Amp\Mysql\Test;
 
 use Amp\ByteStream;
-use Amp\Loop;
 use Amp\Process\Process;
 use function Amp\delay;
 
@@ -14,6 +13,8 @@
 const DB_PASS = '';
 const DB_NAME = 'test';
 
+print "\rCleaning up test directory...";
+
 (function (): void {
     /* cleanup in case it wasn't terminated properly... */
     $pidfile = __DIR__ . "/mysql.pid";
@@ -23,11 +24,11 @@
         } else {
             \shell_exec("kill -9 `cat '$pidfile'` 2>/dev/null");
         }
-        \sleep(1);
+        delay(1000);
     }
 
     if (\file_exists(__DIR__ . "/mysql_db")) {
-        $rm_all = function ($dir) use (&$rm_all) {
+        $rm_all = function ($dir) use (&$rm_all): void {
             $files = \glob("$dir/*");
             if (\is_array($files)) {
                 foreach ($files as $file) {
@@ -46,43 +47,39 @@
     }
 })();
 
-Loop::run(function () {
-    print "\rCreating mysql server... ";
+print "\rCreating mysql server...     ";
 
-    $dir = __DIR__;
+$dir = __DIR__;
 
-    $process = new Process("mysqld --defaults-file={$dir}/my.cnf --initialize-insecure", __DIR__);
+$process = new Process("mysqld --defaults-file={$dir}/my.cnf --initialize-insecure", __DIR__);
 
-    yield $process->start();
+$process->start();
 
-    $stderr = yield ByteStream\buffer($process->getStderr());
+$stderr = ByteStream\buffer($process->getStderr());
 
-    if (\preg_match("# \[ERROR\] #", $stderr)) {
-        print "\nERROR: Aborting, couldn't start mysql successfully\n{$stderr}";
-        exit(127);
-    }
+if (\preg_match("# \[ERROR\] #", $stderr)) {
+    print "\nERROR: Aborting, couldn't start mysql successfully\n{$stderr}";
+    exit(127);
+}
 
-    yield $process->join();
+$process->join();
 
-    $process = new Process("mysqld --defaults-file={$dir}/my.cnf --user=root", __DIR__);
+$process = new Process("mysqld --defaults-file={$dir}/my.cnf --user=root", __DIR__);
 
-    yield $process->start();
+$process->start();
 
-    print "\rStarting mysqld...       ";
+print "\rStarting mysqld...           ";
 
-    yield delay(1000); // Give mysqld time to start.
+delay(2000); // Give mysqld time to start.
 
-    print "\rCreating test database...";
+print "\rCreating test database...    ";
 
-    $db = new \mysqli(DB_HOST, DB_USER, DB_PASS);
-    $db->query("CREATE DATABASE test");
-    $db->query("CREATE TABLE test.main (a INT(11), b INT(11))");
-    $db->query("INSERT INTO test.main VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)");
-    $db->close();
+$db = new \mysqli(DB_HOST, DB_USER, DB_PASS);
+$db->query("CREATE DATABASE test");
+$db->query("CREATE TABLE test.main (a INT(11), b INT(11))");
+$db->query("INSERT INTO test.main VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)");
+$db->close();
 
-    print "\r";
+print "\r";
 
-    \register_shutdown_function(function () use ($process): void {
-        $process->signal(\SIGTERM);
-    });
-});
+\register_shutdown_function(fn() => $process->signal(\SIGTERM));

From 708328b29fc2de427e8ecb6524f7a166cc20e773 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 10 Nov 2020 12:47:03 -0600
Subject: [PATCH 034/174] Implement IteratorAggregate in Results

---
 src/Internal/CommandResult.php    | 7 ++++++-
 src/Internal/ConnectionResult.php | 7 ++++++-
 2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/Internal/CommandResult.php b/src/Internal/CommandResult.php
index 73d1974..778a4f7 100644
--- a/src/Internal/CommandResult.php
+++ b/src/Internal/CommandResult.php
@@ -6,7 +6,7 @@
 use Amp\Sql\Common\CommandResult as SqlCommandResult;
 use Amp\Success;
 
-final class CommandResult implements Result
+final class CommandResult implements Result, \IteratorAggregate
 {
     private ?int $lastInsertId;
 
@@ -28,6 +28,11 @@ public function dispose(): void
         $this->delegate->dispose();
     }
 
+    public function getIterator(): \Iterator
+    {
+        return $this->delegate->getIterator();
+    }
+
     public function getRowCount(): int
     {
         return $this->delegate->getRowCount();
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index c063349..fff1b93 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -10,7 +10,7 @@
 use function Amp\async;
 use function Amp\await;
 
-final class ConnectionResult implements Result
+final class ConnectionResult implements Result, \IteratorAggregate
 {
     private ResultProxy $result;
 
@@ -53,6 +53,11 @@ public function dispose(): void
         $this->generator->dispose();
     }
 
+    public function getIterator(): \Iterator
+    {
+        return $this->generator->getIterator();
+    }
+
     private static function fetchRow(ResultProxy $result): Promise
     {
         if ($result->userFetched < $result->fetchedRows) {

From 3590910dfc3f2042de36bf1b64b235f08ea16dee Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 10 Nov 2020 22:09:08 -0600
Subject: [PATCH 035/174] Update examples to use foreach

---
 examples/2-simple-query.php       | 2 +-
 examples/3-generic-with-yield.php | 2 +-
 examples/4-multi-rows.php         | 4 ++--
 examples/5-multi-stmts.php        | 2 +-
 examples/6-transaction.php        | 4 ++--
 5 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/examples/2-simple-query.php b/examples/2-simple-query.php
index 5d66288..5907ecd 100644
--- a/examples/2-simple-query.php
+++ b/examples/2-simple-query.php
@@ -8,7 +8,7 @@
 
 $result = $db->query("SELECT 1 AS value");
 
-while ($row = $result->continue()) {
+foreach ($result as $row) {
     \var_dump($row['value']);
 }
 
diff --git a/examples/3-generic-with-yield.php b/examples/3-generic-with-yield.php
index 7387d34..ed4c106 100644
--- a/examples/3-generic-with-yield.php
+++ b/examples/3-generic-with-yield.php
@@ -28,7 +28,7 @@
 
 $result = $db->query("SELECT a, b FROM tmp");
 
-while ($row = $result->continue()) {
+foreach ($result as $row) {
     \var_dump($row);
 }
 
diff --git a/examples/4-multi-rows.php b/examples/4-multi-rows.php
index c062a05..bb03286 100644
--- a/examples/4-multi-rows.php
+++ b/examples/4-multi-rows.php
@@ -28,12 +28,12 @@
 }
 
 print "Query 1 Results:" . PHP_EOL;
-while ($row = $result1->continue()) {
+foreach ($result1 as $row) {
     \var_dump($row);
 }
 
 print  PHP_EOL . "Query 2 Results:" . PHP_EOL;
-while ($row = $result2->continue()) {
+foreach ($result2 as $row) {
     \var_dump($row);
 }
 
diff --git a/examples/5-multi-stmts.php b/examples/5-multi-stmts.php
index a85a25e..8f9ed70 100644
--- a/examples/5-multi-stmts.php
+++ b/examples/5-multi-stmts.php
@@ -16,7 +16,7 @@
 $i = 0;
 do {
     print PHP_EOL . "Query " . ++$i . " Results:" . PHP_EOL;
-    while ($row = $result->continue()) {
+    foreach ($result as $row) {
         \var_dump($row);
     }
 } while ($result = $result->getNextResult()); // Advances to the next result set.
diff --git a/examples/6-transaction.php b/examples/6-transaction.php
index e5edd0f..fc70ea8 100644
--- a/examples/6-transaction.php
+++ b/examples/6-transaction.php
@@ -16,7 +16,7 @@
 
 $result = $transaction->execute("SELECT * FROM tmp WHERE a >= ?", [5]); // Two rows should be returned.
 
-while ($row = $result->continue()) {
+foreach ($result as $row) {
     \var_dump($row);
 }
 
@@ -25,7 +25,7 @@
 // Run same query again, should only return a single row since the other was rolled back.
 $result = $db->execute("SELECT * FROM tmp WHERE a >= ?", [5]);
 
-while ($row = $result->continue()) {
+foreach ($result as $row) {
     \var_dump($row);
 }
 

From 0c7d1ea50b0259c1f133a78caf2a52dc6ea500e6 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 10 Nov 2020 22:09:17 -0600
Subject: [PATCH 036/174] Use Traversable as return type

---
 src/Internal/CommandResult.php    | 2 +-
 src/Internal/ConnectionResult.php | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Internal/CommandResult.php b/src/Internal/CommandResult.php
index 778a4f7..a4f4256 100644
--- a/src/Internal/CommandResult.php
+++ b/src/Internal/CommandResult.php
@@ -28,7 +28,7 @@ public function dispose(): void
         $this->delegate->dispose();
     }
 
-    public function getIterator(): \Iterator
+    public function getIterator(): \Traversable
     {
         return $this->delegate->getIterator();
     }
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index fff1b93..12d75bb 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -53,7 +53,7 @@ public function dispose(): void
         $this->generator->dispose();
     }
 
-    public function getIterator(): \Iterator
+    public function getIterator(): \Traversable
     {
         return $this->generator->getIterator();
     }

From 4ca7cef7528b59102bab1a7c65082fee6a4a1a21 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 14 Nov 2020 19:50:36 -0600
Subject: [PATCH 037/174] Use covariant return types

Better for static analysis.
---
 src/Connection.php                     |  2 --
 src/ConnectionConfig.php               | 22 ++++++-------
 src/Executor.php                       | 29 +++++++++++++++++
 src/Internal/ConnectionTransaction.php | 14 ++++----
 src/Link.php                           | 15 +++++++++
 src/Pool.php                           | 44 ++++++++++++++++++++++++--
 src/PooledResult.php                   |  2 +-
 src/PooledStatement.php                | 10 ++++++
 src/PooledTransaction.php              | 32 ++++++++++++++++++-
 src/Statement.php                      |  2 ++
 src/StatementPool.php                  |  5 +++
 src/Transaction.php                    |  9 ++++++
 12 files changed, 162 insertions(+), 24 deletions(-)
 create mode 100644 src/Executor.php
 create mode 100644 src/Link.php
 create mode 100644 src/Transaction.php

diff --git a/src/Connection.php b/src/Connection.php
index 89742ee..11f8eac 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -6,8 +6,6 @@
 use Amp\Deferred;
 use Amp\NullCancellationToken;
 use Amp\Socket;
-use Amp\Sql\Link;
-use Amp\Sql\Transaction;
 use function Amp\await;
 
 final class Connection implements Link
diff --git a/src/ConnectionConfig.php b/src/ConnectionConfig.php
index 181bd4f..b8b9596 100644
--- a/src/ConnectionConfig.php
+++ b/src/ConnectionConfig.php
@@ -25,18 +25,18 @@ final class ConnectionConfig extends SqlConnectionConfig
     public const DEFAULT_CHARSET = "utf8mb4";
     public const DEFAULT_COLLATE = "utf8mb4_general_ci";
 
-    /** @var bool */
-    private $useCompression = false;
-    /** @var bool */
-    private $useLocalInfile = false;
-    /** @var ConnectContext */
-    private $context;
-    /** @var string */
-    private $charset = "utf8mb4";
-    /** @var string  */
-    private $collate = "utf8mb4_general_ci";
+    private bool $useCompression;
+
+    private bool $useLocalInfile;
+
+    private ConnectContext $context;
+
+    private string $charset;
+
+    private string $collate;
+
     /* @var string private key to use for sha256_password auth method */
-    private $key;
+    private string $key;
 
     public static function fromString(string $connectionString, ConnectContext $context = null): self
     {
diff --git a/src/Executor.php b/src/Executor.php
new file mode 100644
index 0000000..eb3881e
--- /dev/null
+++ b/src/Executor.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Amp\Mysql;
+
+use Amp\Sql\Executor as SqlExecutor;
+
+interface Executor extends SqlExecutor
+{
+    /**
+     * @inheritDoc
+     *
+     * @return Result Result object specific to this library.
+     */
+    public function query(string $sql): Result;
+
+    /**
+     * @inheritDoc
+     *
+     * @return Result Result object specific to this library.
+     */
+    public function prepare(string $sql): Statement;
+
+    /**
+     * @inheritDoc
+     *
+     * @return Result Result object specific to this library.
+     */
+    public function execute(string $sql, array $params = []): Result;
+}
diff --git a/src/Internal/ConnectionTransaction.php b/src/Internal/ConnectionTransaction.php
index 6cf0e39..4cfc44f 100644
--- a/src/Internal/ConnectionTransaction.php
+++ b/src/Internal/ConnectionTransaction.php
@@ -6,11 +6,11 @@
 use Amp\Mysql\PooledStatement;
 use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
-use Amp\Sql\Transaction as SqlTransaction;
+use Amp\Mysql\Transaction;
 use Amp\Sql\TransactionError;
 use function Amp\await;
 
-final class ConnectionTransaction implements SqlTransaction
+final class ConnectionTransaction implements Transaction
 {
     const SAVEPOINT_PREFIX = "amp_";
 
@@ -30,13 +30,13 @@ final class ConnectionTransaction implements SqlTransaction
      *
      * @throws \Error If the isolation level is invalid.
      */
-    public function __construct(Processor $processor, callable $release, int $isolation = SqlTransaction::ISOLATION_COMMITTED)
+    public function __construct(Processor $processor, callable $release, int $isolation = Transaction::ISOLATION_COMMITTED)
     {
         switch ($isolation) {
-            case SqlTransaction::ISOLATION_UNCOMMITTED:
-            case SqlTransaction::ISOLATION_COMMITTED:
-            case SqlTransaction::ISOLATION_REPEATABLE:
-            case SqlTransaction::ISOLATION_SERIALIZABLE:
+            case Transaction::ISOLATION_UNCOMMITTED:
+            case Transaction::ISOLATION_COMMITTED:
+            case Transaction::ISOLATION_REPEATABLE:
+            case Transaction::ISOLATION_SERIALIZABLE:
                 $this->isolation = $isolation;
                 break;
 
diff --git a/src/Link.php b/src/Link.php
new file mode 100644
index 0000000..02d0d33
--- /dev/null
+++ b/src/Link.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Amp\Mysql;
+
+use Amp\Sql\Link as SqlLink;
+
+interface Link extends Executor, SqlLink
+{
+    /**
+     * @inheritDoc
+     *
+     * @return Transaction Transaction object specific to this library.
+     */
+    public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITTED): Transaction;
+}
diff --git a/src/Pool.php b/src/Pool.php
index a21d6b0..5280185 100644
--- a/src/Pool.php
+++ b/src/Pool.php
@@ -9,7 +9,7 @@
 use Amp\Sql\Statement as SqlStatement;
 use Amp\Sql\Transaction as SqlTransaction;
 
-final class Pool extends ConnectionPool
+final class Pool extends ConnectionPool implements Link
 {
     protected function createDefaultConnector(): Connector
     {
@@ -33,8 +33,48 @@ protected function createStatementPool(SqlPool $pool, string $sql, callable $pre
         return new StatementPool($pool, $sql, $prepare);
     }
 
-    protected function createTransaction(SqlTransaction $transaction, callable $release): SqlTransaction
+    protected function createTransaction(SqlTransaction $transaction, callable $release): Transaction
     {
         return new PooledTransaction($transaction, $release);
     }
+
+    /**
+     * Changes return type to this library's Result type.
+     *
+     * @inheritDoc
+     */
+    public function query(string $sql): Result
+    {
+        return parent::query($sql);
+    }
+
+    /**
+     * Changes return type to this library's Statement type.
+     *
+     * @inheritDoc
+     */
+    public function prepare(string $sql): Statement
+    {
+        return parent::prepare($sql);
+    }
+
+    /**
+     * Changes return type to this library's Result type.
+     *
+     * @inheritDoc
+     */
+    public function execute(string $sql, array $params = []): Result
+    {
+        return parent::execute($sql, $params);
+    }
+
+    /**
+     * Changes return type to this library's Transaction type.
+     *
+     * @inheritDoc
+     */
+    public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITTED): Transaction
+    {
+        return parent::beginTransaction($isolation);
+    }
 }
diff --git a/src/PooledResult.php b/src/PooledResult.php
index c914e66..cf7bf6b 100644
--- a/src/PooledResult.php
+++ b/src/PooledResult.php
@@ -19,7 +19,7 @@ public function __construct(Result $result, callable $release)
         $this->result = $result;
     }
 
-    protected function newInstanceFrom(SqlResult $result, callable $release): PooledResult
+    protected function newInstanceFrom(SqlResult $result, callable $release): self
     {
         \assert($result instanceof Result);
         return new self($result, $release);
diff --git a/src/PooledStatement.php b/src/PooledStatement.php
index 7dc5c1c..eb27e24 100644
--- a/src/PooledStatement.php
+++ b/src/PooledStatement.php
@@ -21,6 +21,16 @@ protected function createResult(SqlResult $result, callable $release): Result
         return new PooledResult($result, $release);
     }
 
+    /**
+     * Changes return type to this library's Result type.
+     *
+     * @inheritDoc
+     */
+    public function execute(array $params = []): Result
+    {
+        return $this->statement->execute($params);
+    }
+
     public function bind(int|string $paramId, mixed $data): void
     {
         $this->statement->bind($paramId, $data);
diff --git a/src/PooledTransaction.php b/src/PooledTransaction.php
index 7e83f2e..e6789fa 100644
--- a/src/PooledTransaction.php
+++ b/src/PooledTransaction.php
@@ -6,7 +6,7 @@
 use Amp\Sql\Result as SqlResult;
 use Amp\Sql\Statement as SqlStatement;
 
-final class PooledTransaction extends SqlPooledTransaction
+final class PooledTransaction extends SqlPooledTransaction implements Transaction
 {
     protected function createStatement(SqlStatement $statement, callable $release): Statement
     {
@@ -19,4 +19,34 @@ protected function createResult(SqlResult $result, callable $release): Result
         \assert($result instanceof Result);
         return new PooledResult($result, $release);
     }
+
+    /**
+     * Changes return type to this library's Result type.
+     *
+     * @inheritDoc
+     */
+    public function query(string $sql): Result
+    {
+        return parent::query($sql);
+    }
+
+    /**
+     * Changes return type to this library's Statement type.
+     *
+     * @inheritDoc
+     */
+    public function prepare(string $sql): Statement
+    {
+        return parent::prepare($sql);
+    }
+
+    /**
+     * Changes return type to this library's Result type.
+     *
+     * @inheritDoc
+     */
+    public function execute(string $sql, array $params = []): Result
+    {
+        return parent::execute($sql, $params);
+    }
 }
diff --git a/src/Statement.php b/src/Statement.php
index 63c8aff..b839f04 100644
--- a/src/Statement.php
+++ b/src/Statement.php
@@ -6,6 +6,8 @@
 
 interface Statement extends SqlStatement
 {
+    public function execute(array $params = []): Result;
+
     /**
      * @param int|string $paramId Parameter ID or name.
      * @param mixed $data Data to bind to parameter.
diff --git a/src/StatementPool.php b/src/StatementPool.php
index a96c2d3..91c0261 100644
--- a/src/StatementPool.php
+++ b/src/StatementPool.php
@@ -32,6 +32,11 @@ protected function createResult(SqlResult $result, callable $release): Result
         return new PooledResult($result, $release);
     }
 
+    public function execute(array $params = []): Result
+    {
+        return parent::execute($params);
+    }
+
     public function bind(int|string $paramId, mixed $data): void
     {
         if (!\is_int($paramId) && !\is_string($paramId)) {
diff --git a/src/Transaction.php b/src/Transaction.php
new file mode 100644
index 0000000..ad935eb
--- /dev/null
+++ b/src/Transaction.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Amp\Mysql;
+
+use Amp\Sql\Transaction as SqlTransaction;
+
+interface Transaction extends Executor, SqlTransaction
+{
+}

From bd4423e4793fca780153671eae6fdd49a3bed5a7 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 14 Nov 2020 20:35:10 -0600
Subject: [PATCH 038/174] Fix simultaneous call to beginTransaction()

---
 src/Connection.php | 43 +++++++++++++++++++++++++++----------------
 1 file changed, 27 insertions(+), 16 deletions(-)

diff --git a/src/Connection.php b/src/Connection.php
index 11f8eac..4011f95 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -23,8 +23,8 @@ final class Connection implements Link
 
     private ?Deferred $busy = null;
 
-    /** @var callable Function used to release connection after a transaction has completed. */
-    private $release;
+    /** @var \Closure Function used to release connection after a transaction has completed. */
+    private \Closure $release;
 
     /**
      * @param ConnectionConfig $config
@@ -54,10 +54,12 @@ public static function connect(
     private function __construct(Internal\Processor $processor)
     {
         $this->processor = $processor;
-        $this->release = function (): void {
-            \assert($this->busy instanceof Deferred);
-            $deferred = $this->busy;
-            $this->busy = null;
+
+        $busy = &$this->busy;
+        $this->release = static function () use (&$busy): void {
+            \assert($busy instanceof Deferred);
+            $deferred = $busy;
+            $busy = null;
             $deferred->resolve();
         };
     }
@@ -93,9 +95,7 @@ public function close(): void
         $processor = $this->processor;
         // Send close command if connection is not already in a closed or closing state
         if ($processor->isAlive()) {
-            $processor->sendClose()->onResolve(static function () use ($processor) {
-                $processor->close();
-            });
+            $processor->sendClose()->onResolve(static fn() => $processor->close());
         }
     }
 
@@ -123,30 +123,41 @@ public function query(string $query): Result
 
     public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITTED): Transaction
     {
+        while ($this->busy) {
+            await($this->busy->promise());
+        }
+
+        $this->busy = $deferred = new Deferred;
+
         switch ($isolation) {
             case Transaction::ISOLATION_UNCOMMITTED:
-                $this->query("SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
+                $promise = $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
                 break;
 
             case Transaction::ISOLATION_COMMITTED:
-                $this->query("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
+                $promise = $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
                 break;
 
             case Transaction::ISOLATION_REPEATABLE:
-                $this->query("SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ");
+                $promise = $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ");
                 break;
 
             case Transaction::ISOLATION_SERIALIZABLE:
-                $this->query("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE");
+                $promise = $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE");
                 break;
 
             default:
                 throw new \Error("Invalid transaction type");
         }
 
-        $this->query("START TRANSACTION");
-
-        $this->busy = new Deferred;
+        try {
+            await($promise);
+            await($this->processor->query("START TRANSACTION"));
+        } catch (\Throwable $exception) {
+            $this->busy = null;
+            $deferred->resolve();
+            throw $exception;
+        }
 
         return new Internal\ConnectionTransaction($this->processor, $this->release, $isolation);
     }

From 8d57ecc87145f4685020bb352ed8a44f47e85259 Mon Sep 17 00:00:00 2001
From: Bob Weinand <bobwei9@hotmail.com>
Date: Thu, 24 Dec 2020 19:08:48 +0100
Subject: [PATCH 039/174] Add error handling for very first packet from server
 (Fixes #101)

---
 src/Internal/Processor.php | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index ee3b2aa..11ddb34 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -1378,6 +1378,10 @@ private function parseMysql(): \Generator
     private function parsePayload(string $packet): void
     {
         if ($this->connectionState === self::UNCONNECTED) {
+            if (\ord($packet) == self::ERR_PACKET) {
+                $this->handleError($packet);
+                return;
+            }
             $this->established();
             $this->connectionState = self::ESTABLISHED;
             $this->handleHandshake($packet);

From cf4b981c7e50af2859840fb005d1d283eee2bde8 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 24 Dec 2020 13:15:06 -0600
Subject: [PATCH 040/174] Make test less restrictive

Test depended on ordering of connections in the pool, which is not guarenteed.
---
 test/PoolTest.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/PoolTest.php b/test/PoolTest.php
index a68c3d0..d6b743a 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -234,7 +234,7 @@ public function testConnectionClosedInPool(int $count)
         $result = new CommandResult(0, 0);
 
         foreach ($processors as $processor) {
-            $processor->expects($this->exactly(2))
+            $processor->expects($this->atLeastOnce())
                 ->method('query')
                 ->with($query)
                 ->willReturn(new Delayed(10, $result));

From c72ee2d298a05112feace8b4dc694cf835707710 Mon Sep 17 00:00:00 2001
From: dickson-michael <dickson.michael@gmail.com>
Date: Wed, 6 Jan 2021 03:19:43 -0500
Subject: [PATCH 041/174] Update Processor.php

Add support to Protocol::AuthSwitchResponse
---
 src/Internal/Processor.php | 71 ++++++++++++++++++++++++++------------
 1 file changed, 48 insertions(+), 23 deletions(-)

diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 11ddb34..d2ecdbe 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -133,6 +133,7 @@ class Processor implements TransientResource
     private const OK_PACKET = 0x00;
     private const EXTRA_AUTH_PACKET = 0x01;
     private const LOCAL_INFILE_REQUEST = 0xfb;
+    private const AUTH_SWITCH_PACKET = 0xfe;
     private const EOF_PACKET = 0xfe;
     private const ERR_PACKET = 0xff;
 
@@ -847,6 +848,24 @@ private function handleHandshake(string $packet): void
         $this->sendHandshake();
     }
 
+    /** @see 14.2.5 Connection Phase Packets */
+    private function handleAuthSwitch($packet)
+    {
+        $off = 1;
+
+        $this->authPluginName = DataTypes::decodeNullString(\substr($packet, $off));
+        $off += \strlen($this->authPluginName) + 1;
+
+        $this->authPluginData = \substr($packet, $off);
+
+        $this->sendAuthSwitchResponse();
+    }
+
+    private function sendAuthSwitchResponse()
+    {
+        $this->write($this->getAuthData());
+    }
+
     /** @see 14.6.4.1.2 LOCAL INFILE Request */
     private function handleLocalInfileRequest(string $packet): void
     {
@@ -1378,10 +1397,6 @@ private function parseMysql(): \Generator
     private function parsePayload(string $packet): void
     {
         if ($this->connectionState === self::UNCONNECTED) {
-            if (\ord($packet) == self::ERR_PACKET) {
-                $this->handleError($packet);
-                return;
-            }
             $this->established();
             $this->connectionState = self::ESTABLISHED;
             $this->handleHandshake($packet);
@@ -1400,6 +1415,9 @@ private function parsePayload(string $packet): void
                 case self::ERR_PACKET:
                     $this->handleError($packet);
                     break;
+                case self::AUTH_SWITCH_PACKET:
+                    $this->handleAuthSwitch($packet);
+                    break;
                 case self::EXTRA_AUTH_PACKET:
                     /** @see 14.2.5 Connection Phase Packets (AuthMoreData) */
                     switch ($this->authPluginName) {
@@ -1541,6 +1559,31 @@ private function sendHandshake(bool $inSSL = false): void
         }
 
         $payload .= $this->config->getUser()."\0";
+
+        $auth = $this->getAuthData();
+        if ($this->capabilities & self::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) {
+            $payload .= DataTypes::encodeInt(\strlen($auth));
+            $payload .= $auth;
+        } elseif ($this->capabilities & self::CLIENT_SECURE_CONNECTION) {
+            $payload .= \chr(\strlen($auth));
+            $payload .= $auth;
+        } else {
+            $payload .= "$auth\0";
+        }
+        if ($this->capabilities & self::CLIENT_CONNECT_WITH_DB) {
+            $payload .= "{$this->config->getDatabase()}\0";
+        }
+        if ($this->capabilities & self::CLIENT_PLUGIN_AUTH) {
+            $payload .= "{$this->authPluginName}\0";
+        }
+        if ($this->capabilities & self::CLIENT_CONNECT_ATTRS) {
+            // connection attributes?! 5.6.6+ only!
+        }
+        $this->write($payload);
+    }
+
+    private function getAuthData()
+    {
         if ($this->config->getPassword() == "") {
             $auth = "";
         } elseif ($this->capabilities & self::CLIENT_PLUGIN_AUTH) {
@@ -1574,25 +1617,7 @@ private function sendHandshake(bool $inSSL = false): void
         } else {
             $auth = $this->secureAuth($this->config->getPassword(), $this->authPluginData);
         }
-        if ($this->capabilities & self::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) {
-            $payload .= DataTypes::encodeInt(\strlen($auth));
-            $payload .= $auth;
-        } elseif ($this->capabilities & self::CLIENT_SECURE_CONNECTION) {
-            $payload .= \chr(\strlen($auth));
-            $payload .= $auth;
-        } else {
-            $payload .= "$auth\0";
-        }
-        if ($this->capabilities & self::CLIENT_CONNECT_WITH_DB) {
-            $payload .= "{$this->config->getDatabase()}\0";
-        }
-        if ($this->capabilities & self::CLIENT_PLUGIN_AUTH) {
-            $payload .= "{$this->authPluginName}\0";
-        }
-        if ($this->capabilities & self::CLIENT_CONNECT_ATTRS) {
-            // connection attributes?! 5.6.6+ only!
-        }
-        $this->write($payload);
+        return $auth;
     }
 
     /** @see 14.1.2 MySQL Packet */

From ad45180c779ff2f6993ef834080421d009238f69 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 1 Mar 2021 10:25:56 -0600
Subject: [PATCH 042/174] Defer result consumption in finally

---
 src/Internal/ConnectionResult.php | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index 12d75bb..a0afa12 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -9,6 +9,7 @@
 use Amp\Success;
 use function Amp\async;
 use function Amp\await;
+use function Amp\defer;
 
 final class ConnectionResult implements Result, \IteratorAggregate
 {
@@ -32,10 +33,20 @@ public function __construct(ResultProxy $result)
                     yield \array_combine($columnNames, $row);
                 }
             } finally {
-                // Discard remaining results if disposed.
-                while ($row = await($next)) {
-                    $next = self::fetchRow($result);
+                if ($row === null) {
+                    return; // Result fully consumed.
                 }
+
+                defer(static function () use ($next, $result): void {
+                    try {
+                        // Discard remaining results if disposed.
+                        while ($row = await($next)) {
+                            $next = self::fetchRow($result);
+                        }
+                    } catch (\Throwable $exception) {
+                        // Ignore errors while discarding result.
+                    }
+                });
             }
         });
     }

From 361635a1fd3c9f17ed5cdb29b6f02538d8ec3334 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 8 Jul 2021 18:07:38 -0500
Subject: [PATCH 043/174] Fix PooledResult getLastInsertId

Added tests for auto-increment fields. Fixed type returned by LinkTest::getLink() for better typing.
---
 src/PooledResult.php    |  2 +-
 test/ConnectionTest.php |  2 +-
 test/LinkTest.php       | 22 +++++++++++++++-------
 test/PoolTest.php       |  3 +--
 test/bootstrap.php      |  4 ++--
 5 files changed, 20 insertions(+), 13 deletions(-)

diff --git a/src/PooledResult.php b/src/PooledResult.php
index cf7bf6b..e7dedb6 100644
--- a/src/PooledResult.php
+++ b/src/PooledResult.php
@@ -27,7 +27,7 @@ protected function newInstanceFrom(SqlResult $result, callable $release): self
 
     public function getLastInsertId(): ?int
     {
-        return $this->result->getRowCount();
+        return $this->result->getLastInsertId();
     }
 
     public function getFields(): ?array
diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php
index 3685a34..fb0bacb 100644
--- a/test/ConnectionTest.php
+++ b/test/ConnectionTest.php
@@ -5,7 +5,7 @@
 use Amp\Mysql\CancellableConnector;
 use Amp\Mysql\Connection;
 use Amp\Mysql\ConnectionConfig;
-use Amp\Sql\Link;
+use Amp\Mysql\Link;
 
 class ConnectionTest extends LinkTest
 {
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 2b96834..5f2cf2e 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -6,7 +6,7 @@
 use Amp\Mysql\Result;
 use Amp\PHPUnit\AsyncTestCase;
 use Amp\Pipeline;
-use Amp\Sql\Link;
+use Amp\Mysql\Link;
 use Amp\Sql\QueryError;
 
 abstract class LinkTest extends AsyncTestCase
@@ -118,7 +118,11 @@ public function testPrepared()
             "flags" => 0,
             "decimals" => 0,
         ];
-        $this->assertEquals($stmt->getFields(), [$base + ["name" => "a", "original_name" => "a"], $base + ["name" => "b", "original_name" => "b"]]);
+        $this->assertEquals($stmt->getFields(), [
+            \array_merge($base, ["name" => "id", "original_name" => "id", "flags" => 16899]),
+            \array_merge($base, ["name" => "a", "original_name" => "a"]),
+            \array_merge($base + ["name" => "b", "original_name" => "b"]),
+        ]);
         $stmt->bind("num", 5);
         $result = $stmt->execute([2]);
         $this->assertInstanceOf(Result::class, $result);
@@ -146,9 +150,10 @@ public function testPrepared()
         }
         $this->assertCount(2, $got);
 
-        $stmt = $db->prepare("INSERT INTO main VALUES (:a, :b)");
+        $stmt = $db->prepare("INSERT INTO main (a, b) VALUES (:a, :b)");
         $result = $stmt->execute(["a" => 10, "b" => 11]);
         $this->assertInstanceOf(Result::class, $result);
+        $this->assertGreaterThan(5, $result->getLastInsertId());
 
         $stmt = $db->prepare("DELETE FROM main WHERE a = :a");
         $result = $stmt->execute(["a" => 10]);
@@ -210,17 +215,18 @@ public function testExecute()
     {
         $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
-        $result = $db->execute("SELECT * FROM main WHERE a = ? OR b = ?", [2, 5]);
+        $result = $db->execute("SELECT * FROM test.main WHERE a = ? OR b = ?", [2, 5]);
         $this->assertInstanceOf(Result::class, $result);
         $got = [];
         while ($row = $result->continue()) {
             $got[] = \array_values($row);
         }
         $this->assertCount(2, $got);
-        $this->assertSame([[2, 3], [4, 5]], $got);
+        $this->assertSame([[2, 2, 3], [4, 4, 5]], $got);
 
-        $result = $db->execute("INSERT INTO main VALUES (:a, :b)", ["a" => 10, "b" => 11]);
+        $result = $db->execute("INSERT INTO main (a, b) VALUES (:a, :b)", ["a" => 10, "b" => 11]);
         $this->assertInstanceOf(Result::class, $result);
+        $this->assertGreaterThan(5, $result->getLastInsertId());
 
         $result = $db->execute("DELETE FROM main WHERE a = :a", ["a" => 10]);
         $this->assertInstanceOf(Result::class, $result);
@@ -276,9 +282,10 @@ public function testTransaction()
 
         $transaction = $db->beginTransaction();
 
-        $statement = $transaction->prepare("INSERT INTO main VALUES (?, ?)");
+        $statement = $transaction->prepare("INSERT INTO main (a, b) VALUES (?, ?)");
         $result = $statement->execute([6, 7]);
         $this->assertInstanceOf(Result::class, $result);
+        $this->assertGreaterThan(5, $result->getLastInsertId());
 
         $result = $transaction->execute("SELECT * FROM main WHERE a = ?", [6]);
 
@@ -323,6 +330,7 @@ public function testInsertSelect()
             $result = $statement->execute([$a]);
 
             $this->assertSame($count, $result->getRowCount());
+            $this->assertGreaterThan(5, $result->getLastInsertId());
         } finally {
             $transaction->rollback();
         }
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 0e8f14c..e473127 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -7,17 +7,16 @@
 use Amp\Mysql\ConnectionConfig;
 use Amp\Mysql\Internal\CommandResult;
 use Amp\Mysql\Internal\Processor;
+use Amp\Mysql\Link;
 use Amp\Mysql\Pool;
 use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
 use Amp\Promise;
 use Amp\Sql\Connector;
-use Amp\Sql\Link;
 use Amp\Sql\Transaction as SqlTransaction;
 use Amp\Success;
 use function Amp\async;
 use function Amp\await;
-use function Amp\delay;
 
 interface StatementOperation extends Statement
 {
diff --git a/test/bootstrap.php b/test/bootstrap.php
index 024b26e..b947a38 100644
--- a/test/bootstrap.php
+++ b/test/bootstrap.php
@@ -76,8 +76,8 @@
 
 $db = new \mysqli(DB_HOST, DB_USER, DB_PASS);
 $db->query("CREATE DATABASE test");
-$db->query("CREATE TABLE test.main (a INT(11), b INT(11))");
-$db->query("INSERT INTO test.main VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)");
+$db->query("CREATE TABLE test.main (id INT NOT NULL AUTO_INCREMENT, a INT, b INT, PRIMARY KEY (id))");
+$db->query("INSERT INTO test.main (a, b) VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)");
 $db->close();
 
 print "\r";

From 509e426d5446c286e7607e0452f41543467ddb34 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 30 Oct 2021 13:14:16 -0500
Subject: [PATCH 044/174] Use Revolt

---
 composer.json                          |   2 +-
 examples/3-generic-with-yield.php      |  12 +-
 examples/4-multi-rows.php              |  12 +-
 examples/support/generic-table.php     |   9 +-
 src/Connection.php                     |  38 +++---
 src/Internal/CommandResult.php         |  16 +--
 src/Internal/ConnectionResult.php      |  99 ++++++--------
 src/Internal/ConnectionState.php       |   4 -
 src/Internal/ConnectionStatement.php   |  35 ++---
 src/Internal/ConnectionTransaction.php |  42 +++---
 src/Internal/Processor.php             | 182 +++++++++++--------------
 src/Internal/ResultProxy.php           |  11 +-
 src/functions.php                      |  18 +--
 test/LinkTest.php                      |  39 +++---
 test/PoolTest.php                      |  78 +++++++----
 test/bootstrap.php                     |   4 +-
 16 files changed, 284 insertions(+), 317 deletions(-)

diff --git a/composer.json b/composer.json
index 280f786..bb3e99b 100644
--- a/composer.json
+++ b/composer.json
@@ -26,7 +26,7 @@
         "phpunit/phpunit": "^9",
         "amphp/phpunit-util": "^2-dev",
         "amphp/php-cs-fixer-config": "dev-master",
-        "phpbench/phpbench": "^0.13.0"
+        "phpbench/phpbench": "^1.1"
     },
     "minimum-stability": "dev",
     "prefer-stable": true,
diff --git a/examples/3-generic-with-yield.php b/examples/3-generic-with-yield.php
index ed4c106..2e82f4f 100644
--- a/examples/3-generic-with-yield.php
+++ b/examples/3-generic-with-yield.php
@@ -2,12 +2,14 @@
 
 require 'support/bootstrap.php';
 
+use Amp\Future;
 use Amp\Mysql;
-use function Amp\async;
-use function Amp\await;
+use function Amp\coroutine;
 
 $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
+$db->query("DROP TABLE IF EXISTS tmp");
+
 /* Create table and insert a few rows */
 /* we need to wait until table is finished, so that we can insert. */
 $db->query("CREATE TABLE IF NOT EXISTS tmp (a INT(10), b INT(10))");
@@ -16,13 +18,13 @@
 
 $statement = $db->prepare("INSERT INTO tmp (a, b) VALUES (?, ? * 2)");
 
-$promises = [];
+$future = [];
 foreach (\range(1, 5) as $num) {
-    $promises[] = async(fn() => $statement->execute([$num, $num]));
+    $future[] = coroutine(fn() => $statement->execute([$num, $num]));
 }
 
 /* wait until everything is inserted */
-await($promises);
+Future\all($future);
 
 print "Insertion successful (if it wasn't, an exception would have been thrown by now)" . PHP_EOL;
 
diff --git a/examples/4-multi-rows.php b/examples/4-multi-rows.php
index bb03286..5cc50de 100644
--- a/examples/4-multi-rows.php
+++ b/examples/4-multi-rows.php
@@ -3,26 +3,26 @@
 require 'support/bootstrap.php';
 require 'support/generic-table.php';
 
+use Amp\Future;
 use Amp\Mysql;
-use function Amp\async;
-use function Amp\await;
+use function Amp\coroutine;
 
 $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
 /* create same table than in 3-generic-with-yield.php */
 createGenericTable($db);
 
-$promises = [];
+$future = [];
 
-$promises[] = async(fn() => $db->query("SELECT a * b FROM tmp"));
-$promises[] = async(fn() => $db->execute("SELECT POW(a, ?) AS power FROM tmp", [2]));
+$future[] = coroutine(fn() => $db->query("SELECT a * b FROM tmp"));
+$future[] = coroutine(fn() => $db->execute("SELECT POW(a, ?) AS power FROM tmp", [2]));
 
 try {
     /**
      * @var Mysql\Result $result1
      * @var Mysql\Result $result2
      */
-    [$result1, $result2] = await($promises); // Both queries execute simultaneously. Wait for both to finish here.
+    [$result1, $result2] = Future\all($future); // Both queries execute simultaneously. Wait for both to finish here.
 } catch (\Throwable $e) {
     var_dump($e);
 }
diff --git a/examples/support/generic-table.php b/examples/support/generic-table.php
index 2a31307..12b4ba3 100644
--- a/examples/support/generic-table.php
+++ b/examples/support/generic-table.php
@@ -1,8 +1,7 @@
 <?php
 
+use Amp\Future;
 use Amp\Sql\Link;
-use function Amp\async;
-use function Amp\await;
 
 /* Create table and fill in a few rows for examples; for comments see 3-generic-with-yield.php */
 function createGenericTable(Link $db): void
@@ -13,10 +12,10 @@ function createGenericTable(Link $db): void
 
     $statement = $db->prepare("INSERT INTO tmp (a, b) VALUES (?, ? * 2)");
 
-    $promises = [];
+    $futures = [];
     foreach (\range(1, 5) as $num) {
-        $promises[] = async(fn() => $statement->execute([$num, $num]));
+        $futures[] = \Amp\coroutine(fn() => $statement->execute([$num, $num]));
     }
 
-    await($promises);
+    Future\all($futures);
 }
diff --git a/src/Connection.php b/src/Connection.php
index 4011f95..c372884 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -6,7 +6,7 @@
 use Amp\Deferred;
 use Amp\NullCancellationToken;
 use Amp\Socket;
-use function Amp\await;
+use Revolt\EventLoop;
 
 final class Connection implements Link
 {
@@ -58,9 +58,8 @@ private function __construct(Internal\Processor $processor)
         $busy = &$this->busy;
         $this->release = static function () use (&$busy): void {
             \assert($busy instanceof Deferred);
-            $deferred = $busy;
+            $busy->complete(null);
             $busy = null;
-            $deferred->resolve();
         };
     }
 
@@ -87,7 +86,7 @@ public function isReady(): bool
 
     public function setCharset(string $charset, string $collate = ""): void
     {
-        await($this->processor->setCharset($charset, $collate));
+        $this->processor->setCharset($charset, $collate)->await();
     }
 
     public function close(): void
@@ -95,13 +94,13 @@ public function close(): void
         $processor = $this->processor;
         // Send close command if connection is not already in a closed or closing state
         if ($processor->isAlive()) {
-            $processor->sendClose()->onResolve(static fn() => $processor->close());
+            $processor->sendClose()->finally(static fn() => $processor->close())->ignore();
         }
     }
 
     public function useDb(string $db): void
     {
-        await($this->processor->useDb($db));
+        $this->processor->useDb($db)->await();
     }
 
     /**
@@ -109,22 +108,22 @@ public function useDb(string $db): void
      */
     public function refresh(int $subcommand): void
     {
-        $this->processor->refresh($subcommand);
+        $this->processor->refresh($subcommand)->await();
     }
 
-    public function query(string $query): Result
+    public function query(string $sql): Result
     {
         while ($this->busy) {
-            await($this->busy->promise());
+            $this->busy->getFuture()->await();
         }
 
-        return await($this->processor->query($query));
+        return $this->processor->query($sql)->await();
     }
 
     public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITTED): Transaction
     {
         while ($this->busy) {
-            await($this->busy->promise());
+            $this->busy->getFuture()->await();
         }
 
         $this->busy = $deferred = new Deferred;
@@ -151,11 +150,11 @@ public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITT
         }
 
         try {
-            await($promise);
-            await($this->processor->query("START TRANSACTION"));
+            $promise->await();
+            $this->processor->query("START TRANSACTION")->await();
         } catch (\Throwable $exception) {
             $this->busy = null;
-            $deferred->resolve();
+            $deferred->complete(null);
             throw $exception;
         }
 
@@ -164,16 +163,16 @@ public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITT
 
     public function ping(): void
     {
-        await($this->processor->ping());
+        $this->processor->ping()->await();
     }
 
-    public function prepare(string $query): Statement
+    public function prepare(string $sql): Statement
     {
         while ($this->busy) {
-            await($this->busy->promise());
+            $this->busy->getFuture()->await();
         }
 
-        return await($this->processor->prepare($query));
+        return $this->processor->prepare($sql)->await();
     }
 
     /**
@@ -187,6 +186,7 @@ public function execute(string $sql, array $params = []): Result
 
     public function __destruct()
     {
-        $this->processor->unreference();
+        $processor = $this->processor;
+        EventLoop::queue(static fn () => $processor->unreference());
     }
 }
diff --git a/src/Internal/CommandResult.php b/src/Internal/CommandResult.php
index a4f4256..db17173 100644
--- a/src/Internal/CommandResult.php
+++ b/src/Internal/CommandResult.php
@@ -2,9 +2,9 @@
 
 namespace Amp\Mysql\Internal;
 
+use Amp\Future;
 use Amp\Mysql\Result;
 use Amp\Sql\Common\CommandResult as SqlCommandResult;
-use Amp\Success;
 
 final class CommandResult implements Result, \IteratorAggregate
 {
@@ -14,20 +14,10 @@ final class CommandResult implements Result, \IteratorAggregate
 
     public function __construct(int $affectedRows, int $lastInsertId)
     {
-        $this->delegate = new SqlCommandResult($affectedRows, new Success);
+        $this->delegate = new SqlCommandResult($affectedRows, Future::complete(null));
         $this->lastInsertId = $lastInsertId ?: null; // Convert 0 to null
     }
 
-    public function continue(): ?array
-    {
-        return $this->delegate->continue();
-    }
-
-    public function dispose(): void
-    {
-        $this->delegate->dispose();
-    }
-
     public function getIterator(): \Traversable
     {
         return $this->delegate->getIterator();
@@ -39,7 +29,7 @@ public function getRowCount(): int
     }
 
     /**
-     * @return int Insert ID of the last auto increment row.
+     * @return int|null Insert ID of the last auto increment row.
      */
     public function getLastInsertId(): ?int
     {
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index a0afa12..1822198 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -2,84 +2,73 @@
 
 namespace Amp\Mysql\Internal;
 
-use Amp\AsyncGenerator;
 use Amp\Deferred;
+use Amp\Future;
 use Amp\Mysql\Result;
-use Amp\Promise;
-use Amp\Success;
-use function Amp\async;
-use function Amp\await;
-use function Amp\defer;
+use Revolt\EventLoop;
+use function Amp\coroutine;
 
 final class ConnectionResult implements Result, \IteratorAggregate
 {
     private ResultProxy $result;
 
-    private AsyncGenerator $generator;
+    private \Generator $generator;
 
-    private ?Promise $nextResult = null;
+    private ?Future $nextResult = null;
 
     public function __construct(ResultProxy $result)
     {
         $this->result = $result;
-        $this->generator = new AsyncGenerator(static function () use ($result): \Generator {
-            $next = self::fetchRow($result);
-            try {
-                while ($row = await($next)) {
-                    if (!isset($columnNames)) {
-                        $columnNames = \array_column($result->columns, 'name');
-                    }
-                    $next = self::fetchRow($result);
-                    yield \array_combine($columnNames, $row);
-                }
-            } finally {
-                if ($row === null) {
-                    return; // Result fully consumed.
-                }
-
-                defer(static function () use ($next, $result): void {
-                    try {
-                        // Discard remaining results if disposed.
-                        while ($row = await($next)) {
-                            $next = self::fetchRow($result);
-                        }
-                    } catch (\Throwable $exception) {
-                        // Ignore errors while discarding result.
-                    }
-                });
-            }
-        });
+        $this->generator = self::iterate($result);
     }
 
-    /**
-     * @inheritDoc
-     */
-    public function continue(): ?array
+    public function getIterator(): \Traversable
     {
-        return $this->generator->continue();
+        return $this->generator;
     }
 
-    public function dispose(): void
+    private static function iterate(ResultProxy $result): \Generator
     {
-        $this->generator->dispose();
-    }
+        $next = self::fetchRow($result);
+
+        try {
+            while ($row = $next->await()) {
+                if (!isset($columnNames)) {
+                    $columnNames = \array_column($result->columns, 'name');
+                }
+                $next = self::fetchRow($result);
+                yield \array_combine($columnNames, $row);
+            }
+        } finally {
+            if (($row ?? null) === null) {
+                return; // Result fully consumed.
+            }
+
+            EventLoop::queue(static function () use ($next, $result): void {
+                try {
+                    // Discard remaining results if disposed.
+                    while ($next->await()) {
+                        $next = self::fetchRow($result);
+                    }
+                } catch (\Throwable) {
+                    // Ignore errors while discarding result.
+                }
+            });
+        }
 
-    public function getIterator(): \Traversable
-    {
-        return $this->generator->getIterator();
     }
 
-    private static function fetchRow(ResultProxy $result): Promise
+    private static function fetchRow(ResultProxy $result): Future
     {
         if ($result->userFetched < $result->fetchedRows) {
             $row = $result->rows[$result->userFetched];
             unset($result->rows[$result->userFetched]);
             $result->userFetched++;
-            return new Success($row);
+            return Future::complete($row);
         }
 
         if ($result->state === ResultProxy::ROWS_FETCHED) {
-            return new Success;
+            return Future::complete(null);
         }
 
         $deferred = new Deferred;
@@ -93,7 +82,7 @@ private static function fetchRow(ResultProxy $result): Promise
         };
 
         $result->deferreds[ResultProxy::UNFETCHED][] = [$deferred, null, $incRow];
-        return $deferred->promise();
+        return $deferred->getFuture();
     }
 
     /**
@@ -102,12 +91,12 @@ private static function fetchRow(ResultProxy $result): Promise
     public function getNextResult(): ?Result
     {
         if ($this->nextResult) {
-            return await($this->nextResult);
+            return $this->nextResult->await();
         }
 
-        $this->nextResult = async(function (): ?Result {
+        $this->nextResult = coroutine(function (): ?Result {
             $deferred = $this->result->next ?: $this->result->next = new Deferred;
-            $result = await($deferred->promise());
+            $result = $deferred->getFuture()->await();
 
             if ($result instanceof ResultProxy) {
                 return new self($result);
@@ -116,7 +105,7 @@ public function getNextResult(): ?Result
             return $result; // Instance of CommandResult or null.
         });
 
-        return await($this->nextResult);
+        return $this->nextResult->await();
     }
 
     public function getRowCount(): ?int
@@ -144,6 +133,6 @@ public function getFields(): ?array
 
         $deferred = new Deferred;
         $this->result->deferreds[ResultProxy::COLUMNS_FETCHED][] = [$deferred, &$this->result->columns, null];
-        return await($deferred->promise());
+        return $deferred->getFuture()->await();
     }
 }
diff --git a/src/Internal/ConnectionState.php b/src/Internal/ConnectionState.php
index a18fe57..6a9c67c 100644
--- a/src/Internal/ConnectionState.php
+++ b/src/Internal/ConnectionState.php
@@ -2,12 +2,8 @@
 
 namespace Amp\Mysql\Internal;
 
-use Amp\Struct;
-
 final class ConnectionState
 {
-    use Struct;
-
     public ?int $affectedRows = null;
     public ?int $insertId = null;
     public ?int $statusFlags = null;
diff --git a/src/Internal/ConnectionStatement.php b/src/Internal/ConnectionStatement.php
index 76c289c..53418ac 100644
--- a/src/Internal/ConnectionStatement.php
+++ b/src/Internal/ConnectionStatement.php
@@ -6,7 +6,7 @@
 use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
 use Amp\Sql\ConnectionException;
-use function Amp\await;
+use Revolt\EventLoop;
 
 final class ConnectionStatement implements Statement
 {
@@ -129,7 +129,9 @@ public function execute(array $params = []): Result
             }
         }
 
-        return await($this->getProcessor()->execute($this->stmtId, $this->query, $this->result->params, $prebound, $args));
+        return $this->getProcessor()
+            ->execute($this->stmtId, $this->query, $this->result->params, $prebound, $args)
+            ->await();
     }
 
     public function getQuery(): string
@@ -137,20 +139,11 @@ public function getQuery(): string
         return $this->query;
     }
 
-    private function close(): void
-    {
-        if ($this->processor === null) {
-            return;
-        }
-
-        $this->processor->closeStmt($this->stmtId);
-        $this->processor->unreference();
-        $this->processor = null;
-    }
-
     public function reset(): void
     {
-        await($this->getProcessor()->resetStmt($this->stmtId));
+        $this->getProcessor()
+            ->resetStmt($this->stmtId)
+            ->await();
     }
 
     public function getFields(): ?array
@@ -165,7 +158,7 @@ public function getFields(): ?array
 
         $deferred = new Deferred;
         $this->result->deferreds[ResultProxy::COLUMNS_FETCHED][0] = [$deferred, &$this->result->columns, null];
-        return await($deferred->promise());
+        return $deferred->getFuture()->await();
     }
 
     /** {@inheritdoc} */
@@ -176,6 +169,16 @@ public function getLastUsedAt(): int
 
     public function __destruct()
     {
-        $this->close();
+        if ($this->processor) {
+            $processor = $this->processor;
+            $stmtId = $this->stmtId;
+            EventLoop::queue(static fn () => self::close($processor, $stmtId));
+        }
+    }
+
+    private static function close(Processor $processor, int $stmtId): void
+    {
+        $processor->closeStmt($stmtId);
+        $processor->unreference();
     }
 }
diff --git a/src/Internal/ConnectionTransaction.php b/src/Internal/ConnectionTransaction.php
index 4cfc44f..614f4c4 100644
--- a/src/Internal/ConnectionTransaction.php
+++ b/src/Internal/ConnectionTransaction.php
@@ -8,7 +8,7 @@
 use Amp\Mysql\Statement;
 use Amp\Mysql\Transaction;
 use Amp\Sql\TransactionError;
-use function Amp\await;
+use Revolt\EventLoop;
 
 final class ConnectionTransaction implements Transaction
 {
@@ -32,17 +32,14 @@ final class ConnectionTransaction implements Transaction
      */
     public function __construct(Processor $processor, callable $release, int $isolation = Transaction::ISOLATION_COMMITTED)
     {
-        switch ($isolation) {
-            case Transaction::ISOLATION_UNCOMMITTED:
-            case Transaction::ISOLATION_COMMITTED:
-            case Transaction::ISOLATION_REPEATABLE:
-            case Transaction::ISOLATION_SERIALIZABLE:
-                $this->isolation = $isolation;
-                break;
-
-            default:
-                throw new \Error("Isolation must be a valid transaction isolation level");
-        }
+        $this->isolation = match ($isolation) {
+            Transaction::ISOLATION_UNCOMMITTED,
+            Transaction::ISOLATION_COMMITTED,
+            Transaction::ISOLATION_REPEATABLE,
+            Transaction::ISOLATION_SERIALIZABLE,
+                => $isolation,
+            default => throw new \Error("Isolation must be a valid transaction isolation level"),
+        };
 
         $this->processor = $processor;
 
@@ -57,7 +54,12 @@ public function __construct(Processor $processor, callable $release, int $isolat
     public function __destruct()
     {
         if ($this->processor && $this->processor->isAlive()) {
-            $this->rollback(); // Invokes $this->release callback.
+            $processor = $this->processor;
+            $release = $this->release;
+            EventLoop::queue(static function () use ($processor, $release): void {
+                $processor->query("ROLLBACK");
+                $release();
+            });
         }
     }
 
@@ -116,7 +118,7 @@ public function query(string $sql): Result
             throw new TransactionError("The transaction has been committed or rolled back");
         }
 
-        $result = await($this->processor->query($sql));
+        $result = $this->processor->query($sql)->await();
         ++$this->refCount;
         return new PooledResult($result, $this->release);
     }
@@ -132,7 +134,7 @@ public function prepare(string $sql): Statement
             throw new TransactionError("The transaction has been committed or rolled back");
         }
 
-        $statement = await($this->processor->prepare($sql));
+        $statement = $this->processor->prepare($sql)->await();
         return new PooledStatement($statement, $this->release);
     }
 
@@ -147,7 +149,7 @@ public function execute(string $sql, array $params = []): Result
             throw new TransactionError("The transaction has been committed or rolled back");
         }
 
-        $statement = await($this->processor->prepare($sql));
+        $statement = $this->processor->prepare($sql)->await();
         $result = $statement->execute($params);
 
         ++$this->refCount;
@@ -167,9 +169,7 @@ public function commit(): void
 
         $promise = $this->processor->query("COMMIT");
         $this->processor = null;
-        $promise->onResolve($this->release);
-
-        await($promise);
+        $promise->finally($this->release)->await();
     }
 
     /**
@@ -185,9 +185,7 @@ public function rollback(): void
 
         $promise = $this->processor->query("ROLLBACK");
         $this->processor = null;
-        $promise->onResolve($this->release);
-
-        await($promise);
+        $promise->finally($this->release)->await();
     }
 
     /**
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 56159e1..42a7361 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -5,18 +5,16 @@
 use Amp\CancellationToken;
 use Amp\Deferred;
 use Amp\File;
-use Amp\Loop;
+use Amp\Future;
 use Amp\Mysql\ConnectionConfig;
 use Amp\Mysql\DataTypes;
 use Amp\Mysql\InitializationException;
-use Amp\Promise;
 use Amp\Socket\EncryptableSocket;
 use Amp\Sql\ConnectionException;
 use Amp\Sql\FailureException;
 use Amp\Sql\QueryError;
 use Amp\Sql\TransientResource;
-use function Amp\await;
-use function Amp\defer;
+use Revolt\EventLoop;
 
 /* @TODO
  * 14.2.3 Auth switch request??
@@ -80,7 +78,7 @@ class Processor implements TransientResource
     /** @var callable|null */
     private $packetCallback = null;
 
-    private ?Promise $pendingWrite = null;
+    private ?Future $pendingWrite = null;
 
     public ConnectionConfig $config;
 
@@ -181,7 +179,7 @@ private function ready(): void
         if ($this->socket) {
             try {
                 $this->socket->unreference();
-            } catch (Loop\InvalidWatcherError $exception) {
+            } catch (EventLoop\InvalidCallbackError) {
                 // Undefined destruct order can cause unref of an invalid watcher if the loop is swapped.
                 // Generally this will only happen during tests.
             }
@@ -205,18 +203,13 @@ public function connect(CancellationToken $token): void
 
         $id = $token->subscribe(fn() => $this->close());
 
-        defer(fn() => $this->read());
+        EventLoop::queue(fn() => $this->read());
 
-        $promise = $deferred->promise();
-
-        $promise->onResolve(static fn() => $token->unsubscribe($id));
+        $future = $deferred->getFuture();
+        $future = $future->finally(static fn() => $token->unsubscribe($id));
 
         if ($this->config->getCharset() !== ConnectionConfig::DEFAULT_CHARSET || $this->config->getCollation() !== ConnectionConfig::DEFAULT_COLLATE) {
-            $promise->onResolve(function (?\Throwable $exception): void {
-                if ($exception) {
-                    return;
-                }
-
+            $future = $future->apply(function () {
                 $charset = $this->config->getCharset();
                 $collate = $this->config->getCollation();
 
@@ -224,7 +217,7 @@ public function connect(CancellationToken $token): void
             });
         }
 
-        await($promise);
+        $future->await();
     }
 
     private function read(): void
@@ -267,8 +260,10 @@ private function read(): void
             $exception = new ConnectionException("Connection closed unexpectedly", 0, $exception ?? null);
 
             foreach ($this->deferreds as $deferred) {
-                $deferred->fail($exception);
+                $deferred->error($exception);
             }
+
+            $this->deferreds = [];
         }
     }
 
@@ -321,7 +316,7 @@ public function getLastUsedAt(): int
         return $this->lastUsedAt;
     }
 
-    protected function startCommand(callable $callback): Promise
+    protected function startCommand(callable $callback): Future
     {
         if ($this->connectionState > self::READY) {
             throw new \Error("The connection has been closed");
@@ -333,10 +328,10 @@ protected function startCommand(callable $callback): Promise
             $this->addDeferred($deferred);
             $callback();
         });
-        return $deferred->promise();
+        return $deferred->getFuture();
     }
 
-    public function setCharset(string $charset, string $collate = ""): Promise
+    public function setCharset(string $charset, string $collate = ""): Future
     {
         if ($collate === "" && false !== $off = \strpos($charset, "_")) {
             $collate = $charset;
@@ -344,16 +339,16 @@ public function setCharset(string $charset, string $collate = ""): Promise
         }
 
         $query = "SET NAMES '$charset'" . ($collate === "" ? "" : " COLLATE '$collate'");
-        $promise = $this->query($query);
+        $future = $this->query($query);
 
         $this->config = $this->config->withCharset($charset, $collate);
 
-        return $promise;
+        return $future;
     }
 
 
     /** @see 14.6.3 COM_INIT_DB */
-    public function useDb(string $db): Promise
+    public function useDb(string $db): Future
     {
         return $this->startCommand(function () use ($db): void {
             $this->config = $this->config->withDatabase($db);
@@ -362,7 +357,7 @@ public function useDb(string $db): Promise
     }
 
     /** @see 14.6.4 COM_QUERY */
-    public function query(string $query): Promise
+    public function query(string $query): Future
     {
         return $this->startCommand(function () use ($query): void {
             $this->query = $query;
@@ -372,7 +367,7 @@ public function query(string $query): Promise
     }
 
     /** @see 14.7.4 COM_STMT_PREPARE */
-    public function prepare(string $query): Promise
+    public function prepare(string $query): Future
     {
         return $this->startCommand(function () use ($query): void {
             $this->query = $query;
@@ -415,7 +410,7 @@ public function changeUser($user, $pass, $db = null) {
     */
 
     /** @see 14.6.15 COM_PING */
-    public function ping(): Promise
+    public function ping(): Future
     {
         return $this->startCommand(function (): void {
             $this->sendPacket("\x0e");
@@ -423,7 +418,7 @@ public function ping(): Promise
     }
 
     /** @see 14.6.19 COM_RESET_CONNECTION */
-    public function resetConnection(): Promise
+    public function resetConnection(): Future
     {
         return $this->startCommand(function (): void {
             $this->sendPacket("\x1f");
@@ -447,7 +442,7 @@ public function bindParam(int $stmtId, int $paramId, string $data): void
     /** @see 14.7.6 COM_STMT_EXECUTE */
     // prebound params: null-bit set, type MYSQL_TYPE_LONG_BLOB, no value
     // $params is by-ref, because the actual result object might not yet have been filled completely with data upon call of this method ...
-    public function execute(int $stmtId, string $query, array &$params, array $prebound, array $data = []): Promise
+    public function execute(int $stmtId, string $query, array &$params, array $prebound, array $data = []): Future
     {
         $deferred = new Deferred;
         $this->appendTask(function () use ($stmtId, $query, &$params, $prebound, $data, $deferred): void {
@@ -496,7 +491,7 @@ public function execute(int $stmtId, string $query, array &$params, array $prebo
             // apparently LOAD DATA LOCAL INFILE requests are not supported via prepared statements
             $this->packetCallback = [$this, "handleExecute"];
         });
-        return $deferred->promise(); // do not use $this->startCommand(), that might unexpectedly reset the seqId!
+        return $deferred->getFuture(); // do not use $this->startCommand(), that might unexpectedly reset the seqId!
     }
 
     /** @see 14.7.7 COM_STMT_CLOSE */
@@ -514,7 +509,7 @@ public function closeStmt(int $stmtId): void
     }
 
     /** @see 14.6.5 COM_FIELD_LIST */
-    public function listFields(string $table, string $like = "%"): Promise
+    public function listFields(string $table, string $like = "%"): Future
     {
         return $this->startCommand(static function () use ($table, $like): void {
             $this->sendPacket("\x04$table\0$like");
@@ -522,30 +517,24 @@ public function listFields(string $table, string $like = "%"): Promise
         });
     }
 
-    public function listAllFields(string $table, string $like = "%"): Promise
+    public function listAllFields(string $table, string $like = "%"): Future
     {
-        $deferred = new Deferred;
+        $apply = function (?array $array) use (&$apply): array {
+            static $columns = [];
 
-        $columns = [];
-        $onResolve = function ($error, $array) use (&$columns, &$onResolve, $deferred) {
-            if ($error) {
-                $deferred->fail($error);
-                return;
-            }
             if ($array === null) {
-                $deferred->resolve($columns);
-                return;
+                return $columns;
             }
-            list($columns[], $promise) = $array;
-            $promise->onResolve($onResolve);
+
+            [$columns[], $future] = $array;
+            return $future->apply($apply)->await();
         };
-        $this->listFields($table, $like)->onResolve($onResolve);
 
-        return $deferred->promise();
+        return $this->listFields($table, $like)->apply($apply);
     }
 
     /** @see 14.6.6 COM_CREATE_DB */
-    public function createDatabase(string $db): Promise
+    public function createDatabase(string $db): Future
     {
         return $this->startCommand(function () use ($db): void {
             $this->sendPacket("\x05$db");
@@ -553,7 +542,7 @@ public function createDatabase(string $db): Promise
     }
 
     /** @see 14.6.7 COM_DROP_DB */
-    public function dropDatabase(string $db): Promise
+    public function dropDatabase(string $db): Future
     {
         return $this->startCommand(function () use ($db): void {
             $this->sendPacket("\x06$db");
@@ -564,7 +553,7 @@ public function dropDatabase(string $db): Promise
      * @param $subcommand int one of the self::REFRESH_* constants
      * @see 14.6.8 COM_REFRESH
      */
-    public function refresh(int $subcommand): Promise
+    public function refresh(int $subcommand): Future
     {
         return $this->startCommand(function () use ($subcommand): void {
             $this->sendPacket("\x07" . \chr($subcommand));
@@ -572,7 +561,7 @@ public function refresh(int $subcommand): Promise
     }
 
     /** @see 14.6.9 COM_SHUTDOWN */
-    public function shutdown(): Promise
+    public function shutdown(): Future
     {
         return $this->startCommand(function (): void {
             $this->sendPacket("\x08\x00"); /* SHUTDOWN_DEFAULT / SHUTDOWN_WAIT_ALL_BUFFERS, only one in use */
@@ -580,7 +569,7 @@ public function shutdown(): Promise
     }
 
     /** @see 14.6.10 COM_STATISTICS */
-    public function statistics(): Promise
+    public function statistics(): Future
     {
         return $this->startCommand(function (): void {
             $this->sendPacket("\x09");
@@ -589,7 +578,7 @@ public function statistics(): Promise
     }
 
     /** @see 14.6.11 COM_PROCESS_INFO */
-    public function processInfo(): Promise
+    public function processInfo(): Future
     {
         return $this->startCommand(function (): void {
             $this->sendPacket("\x0a");
@@ -598,7 +587,7 @@ public function processInfo(): Promise
     }
 
     /** @see 14.6.13 COM_PROCESS_KILL */
-    public function killProcess(int $process): Promise
+    public function killProcess(int $process): Future
     {
         return $this->startCommand(function () use ($process): void {
             $this->sendPacket("\x0c" . DataTypes::encodeInt32($process));
@@ -606,7 +595,7 @@ public function killProcess(int $process): Promise
     }
 
     /** @see 14.6.14 COM_DEBUG */
-    public function debugStdout(): Promise
+    public function debugStdout(): Future
     {
         return $this->startCommand(function (): void {
             $this->sendPacket("\x0d");
@@ -614,7 +603,7 @@ public function debugStdout(): Promise
     }
 
     /** @see 14.7.8 COM_STMT_RESET */
-    public function resetStmt(int $stmtId): Promise
+    public function resetStmt(int $stmtId): Future
     {
         $payload = "\x1a" . DataTypes::encodeInt32($stmtId);
         $deferred = new Deferred;
@@ -623,11 +612,11 @@ public function resetStmt(int $stmtId): Promise
             $this->addDeferred($deferred);
             $this->sendPacket($payload);
         });
-        return $deferred->promise();
+        return $deferred->getFuture();
     }
 
     /** @see 14.8.4 COM_STMT_FETCH */
-    public function fetchStmt(int $stmtId): Promise
+    public function fetchStmt(int $stmtId): Future
     {
         $payload = "\x1c" . DataTypes::encodeInt32($stmtId) . DataTypes::encodeInt32(1);
         $deferred = new Deferred;
@@ -636,7 +625,7 @@ public function fetchStmt(int $stmtId): Promise
             $this->addDeferred($deferred);
             $this->sendPacket($payload);
         });
-        return $deferred->promise();
+        return $deferred->getFuture();
     }
 
     private function established(): void
@@ -681,7 +670,7 @@ private function handleError(string $packet): void
         if ($this->connectionState < self::READY) {
             // connection failure
             $this->close();
-            $this->getDeferred()->fail(new InitializationException("Could not connect to {$this->config->getConnectionString()}: {$this->connInfo->errorState} {$this->connInfo->errorMsg}"));
+            $this->getDeferred()->error(new InitializationException("Could not connect to {$this->config->getConnectionString()}: {$this->connInfo->errorState} {$this->connInfo->errorMsg}"));
             return;
         }
 
@@ -697,7 +686,7 @@ private function handleError(string $packet): void
         $exception = new QueryError("MySQL error ({$this->connInfo->errorCode}): {$this->connInfo->errorState} {$this->connInfo->errorMsg}", $this->query);
         $this->result = null;
         $this->query = null;
-        $deferred->fail($exception);
+        $deferred->error($exception);
 
         $this->ready();
     }
@@ -762,7 +751,7 @@ private function parseOk(string $packet): void
     private function handleOk(string $packet): void
     {
         $this->parseOk($packet);
-        $this->getDeferred()->resolve(new CommandResult($this->connInfo->affectedRows, $this->connInfo->insertId));
+        $this->getDeferred()->complete(new CommandResult($this->connInfo->affectedRows, $this->connInfo->insertId));
         $this->ready();
     }
 
@@ -780,7 +769,7 @@ private function handleEof(string $packet): void
     {
         $this->parseEof($packet);
         $exception = new FailureException($this->connInfo->errorMsg, $this->connInfo->errorCode);
-        $this->getDeferred()->fail($exception);
+        $this->getDeferred()->error($exception);
         $this->ready();
     }
 
@@ -858,7 +847,7 @@ private function sendAuthSwitchResponse()
     /** @see 14.6.4.1.2 LOCAL INFILE Request */
     private function handleLocalInfileRequest(string $packet): void
     {
-        defer(function () use ($packet): void {
+        EventLoop::queue(function () use ($packet): void {
             try {
                 $filePath = \substr($packet, 1);
                 /** @var \Amp\File\File $fileHandle */
@@ -873,7 +862,7 @@ private function handleLocalInfileRequest(string $packet): void
                 }
                 $this->sendPacket("");
             } catch (\Throwable $e) {
-                $this->getDeferred()->fail(new ConnectionException("Failed to transfer a file to the server", 0, $e));
+                $this->getDeferred()->error(new ConnectionException("Failed to transfer a file to the server", 0, $e));
             }
         });
     }
@@ -888,13 +877,16 @@ private function handleQuery(string $packet): void
                     $result = new ResultProxy;
                     $result->affectedRows = $this->connInfo->affectedRows;
                     $result->insertId = $this->connInfo->insertId;
-                    $this->getDeferred()->resolve(new ConnectionResult($result));
+                    $this->getDeferred()->complete(new ConnectionResult($result));
                     $this->result = $result;
                     $result->updateState(ResultProxy::COLUMNS_FETCHED);
                     $this->successfulResultsetFetch();
                 } else {
                     $this->parseCallback = null;
-                    $this->getDeferred()->resolve(new CommandResult($this->connInfo->affectedRows, $this->connInfo->insertId));
+                    $this->getDeferred()->complete(new CommandResult(
+                        $this->connInfo->affectedRows,
+                        $this->connInfo->insertId
+                    ));
                     $this->ready();
                 }
                 return;
@@ -902,7 +894,7 @@ private function handleQuery(string $packet): void
                 if ($this->config->isLocalInfileEnabled()) {
                     $this->handleLocalInfileRequest($packet);
                 } else {
-                    $this->getDeferred()->fail(new ConnectionException("Unexpected LOCAL_INFILE_REQUEST packet"));
+                    $this->getDeferred()->error(new ConnectionException("Unexpected LOCAL_INFILE_REQUEST packet"));
                 }
                 return;
             case self::ERR_PACKET:
@@ -911,7 +903,7 @@ private function handleQuery(string $packet): void
         }
 
         $this->parseCallback = [$this, "handleTextColumnDefinition"];
-        $this->getDeferred()->resolve(new ConnectionResult($result = new ResultProxy));
+        $this->getDeferred()->complete(new ConnectionResult($result = new ResultProxy));
         /* we need to resolve before assigning vars, so that a onResolve() handler won't have a partial result available */
         $this->result = $result;
         $result->setColumns(DataTypes::decodeUnsigned($packet));
@@ -921,7 +913,7 @@ private function handleQuery(string $packet): void
     private function handleExecute(string $packet): void
     {
         $this->parseCallback = [$this, "handleBinaryColumnDefinition"];
-        $this->getDeferred()->resolve(new ConnectionResult($result = new ResultProxy));
+        $this->getDeferred()->complete(new ConnectionResult($result = new ResultProxy));
         /* we need to resolve before assigning vars, so that a onResolve() handler won't have a partial result available */
         $this->result = $result;
         $result->setColumns(\ord($packet));
@@ -935,11 +927,11 @@ private function handleFieldList(string $packet): void
         } elseif (\ord($packet) === self::EOF_PACKET) {
             $this->parseCallback = null;
             $this->parseEof($packet);
-            $this->getDeferred()->resolve(null);
+            $this->getDeferred()->complete(null);
             $this->ready();
         } else {
             $this->addDeferred($deferred = new Deferred);
-            $this->getDeferred()->resolve([$this->parseColumnDefinition($packet), $deferred]);
+            $this->getDeferred()->complete([$this->parseColumnDefinition($packet), $deferred]);
         }
     }
 
@@ -1083,7 +1075,7 @@ private function successfulResultsetFetch(): void
             $this->parseCallback = null;
             $this->query = null;
             $this->result = null;
-            $deferred->resolve();
+            $deferred->complete(null);
             $this->ready();
         }
         $result->updateState(ResultProxy::ROWS_FETCHED);
@@ -1189,7 +1181,7 @@ private function handlePrepare(string $packet): void
         $this->result->columnsToFetch = $params;
         $this->result->columnCount = $columns;
         $this->refcount++;
-        $this->getDeferred()->resolve(new ConnectionStatement($this, $this->query, $stmtId, $this->named, $this->result));
+        $this->getDeferred()->complete(new ConnectionStatement($this, $this->query, $stmtId, $this->named, $this->result));
         $this->named = [];
         if ($params) {
             $this->parseCallback = [$this, "prepareParams"];
@@ -1200,13 +1192,13 @@ private function handlePrepare(string $packet): void
 
     private function readStatistics(string $packet): void
     {
-        $this->getDeferred()->resolve($packet);
+        $this->getDeferred()->complete($packet);
         $this->parseCallback = null;
         $this->ready();
     }
 
     /** @see 14.6.2 COM_QUIT */
-    public function sendClose(): Promise
+    public function sendClose(): Future
     {
         return $this->startCommand(function () {
             $this->sendPacket("\x01");
@@ -1217,7 +1209,7 @@ public function sendClose(): Promise
     public function close(): void
     {
         if ($this->connectionState === self::CLOSING && $this->deferreds) {
-            \array_pop($this->deferreds)->resolve();
+            \array_pop($this->deferreds)->complete(null);
         }
 
         $this->connectionState = self::CLOSED;
@@ -1253,7 +1245,7 @@ private function write(string $packet): void
             $packet = $this->compressPacket($packet);
         }
 
-        $this->socket->write($packet);
+        $this->socket->write($packet)->await();
     }
 
     private function resetIds(): void
@@ -1523,7 +1515,7 @@ private function sendHandshake(bool $inSSL = false): void
         $payload .= \str_repeat("\0", 23); // reserved
 
         if (!$inSSL && ($this->capabilities & self::CLIENT_SSL)) {
-            defer(function () use ($payload): void {
+            EventLoop::queue(function () use ($payload): void {
                 try {
                     $this->write($payload);
 
@@ -1532,7 +1524,7 @@ private function sendHandshake(bool $inSSL = false): void
                     $this->sendHandshake(true);
                 } catch (\Throwable $e) {
                     $this->close();
-                    $this->getDeferred()->fail($e);
+                    $this->getDeferred()->error($e);
                 }
             });
 
@@ -1563,42 +1555,34 @@ private function sendHandshake(bool $inSSL = false): void
         $this->write($payload);
     }
 
-    private function getAuthData()
+    private function getAuthData(): ?string
     {
         if ($this->config->getPassword() == "") {
-            $auth = "";
-        } elseif ($this->capabilities & self::CLIENT_PLUGIN_AUTH) {
+            return "";
+        }
+
+        if ($this->capabilities & self::CLIENT_PLUGIN_AUTH) {
             switch ($this->authPluginName) {
                 case "mysql_native_password":
-                    $auth = $this->secureAuth($this->config->getPassword(), $this->authPluginData);
-                    break;
+                    return $this->secureAuth($this->config->getPassword(), $this->authPluginData);
                 case "mysql_clear_password":
-                    $auth = $this->config->getPassword();
-                    break;
+                    return $this->config->getPassword();
                 case "sha256_password":
-                    if ($this->config->getPassword() === "") {
-                        $auth = "";
-                    } else {
-                        $key = $this->config->getKey();
-                        if ($key !== null) {
-                            $auth = $this->sha256Auth($this->config->getPassword(), $this->authPluginData, $key);
-                        } else {
-                            $auth = "\x01";
-                        }
+                    $key = $this->config->getKey();
+                    if ($key !== null) {
+                        return $this->sha256Auth($this->config->getPassword(), $this->authPluginData, $key);
                     }
-            break;
+                    return "\x01";
                 case "caching_sha2_password":
-                    $auth = $this->sha2Auth($this->config->getPassword(), $this->authPluginData);
-                    break;
+                    return $this->sha2Auth($this->config->getPassword(), $this->authPluginData);
                 case "mysql_old_password":
                     throw new ConnectionException("mysql_old_password is outdated and insecure. Intentionally not implemented!");
                 default:
                     throw new ConnectionException("Invalid (or unimplemented?) auth method requested by server: {$this->authPluginName}");
             }
-        } else {
-            $auth = $this->secureAuth($this->config->getPassword(), $this->authPluginData);
         }
-        return $auth;
+
+        return $this->secureAuth($this->config->getPassword(), $this->authPluginData);
     }
 
     /** @see 14.1.2 MySQL Packet */
diff --git a/src/Internal/ResultProxy.php b/src/Internal/ResultProxy.php
index dbef6d5..0799a8f 100644
--- a/src/Internal/ResultProxy.php
+++ b/src/Internal/ResultProxy.php
@@ -4,12 +4,9 @@
 
 use Amp\Deferred;
 use Amp\Sql\FailureException;
-use Amp\Struct;
 
 final class ResultProxy
 {
-    use Struct;
-
     public int $columnCount;
     public array $columns = [];
     public array $params = [];
@@ -50,7 +47,7 @@ public function updateState(int $state): void
         }
         foreach ($this->deferreds[$state] as [$deferred, $rows, $cb]) {
             \assert($deferred instanceof Deferred);
-            $deferred->resolve($cb ? $cb($rows) : $rows);
+            $deferred->complete($cb ? $cb($rows) : $rows);
         }
         $this->deferreds[$state] = [];
     }
@@ -63,7 +60,8 @@ public function rowFetched(?array $row): void
         [$entry, , $cb] = \current($this->deferreds[self::UNFETCHED]);
         if ($entry !== null) {
             unset($this->deferreds[self::UNFETCHED][\key($this->deferreds[self::UNFETCHED])]);
-            $entry->resolve($cb && $row ? $cb($row) : $row);
+            \assert($entry instanceof Deferred);
+            $entry->complete($cb && $row ? $cb($row) : $row);
         }
     }
 
@@ -71,7 +69,8 @@ public function fail(FailureException $e): void
     {
         foreach ($this->deferreds as $state) {
             foreach ($this->deferreds[$state] as [$deferred]) {
-                $deferred->fail($e);
+                \assert($deferred instanceof Deferred);
+                $deferred->error($e);
             }
             $this->deferreds[$state] = [];
         }
diff --git a/src/functions.php b/src/functions.php
index c11c414..b068ab6 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -2,26 +2,22 @@
 
 namespace Amp\Mysql;
 
-use Amp\Loop;
 use Amp\Sql\Common\ConnectionPool;
 use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
 use Amp\Sql\Connector;
-
-const LOOP_CONNECTOR_IDENTIFIER = Connector::class . "\\Mysql";
+use Revolt\EventLoop;
 
 function connector(?Connector $connector = null): Connector
 {
-    if ($connector === null) {
-        $connector = Loop::getState(LOOP_CONNECTOR_IDENTIFIER);
-        if ($connector) {
-            return $connector;
-        }
+    static $map;
+    $map ??= new \WeakMap();
+    $driver = EventLoop::getDriver();
 
-        $connector = new CancellableConnector;
+    if ($connector) {
+        return $map[$driver] = $connector;
     }
 
-    Loop::setState(LOOP_CONNECTOR_IDENTIFIER, $connector);
-    return $connector;
+    return $map[$driver] ??= new CancellableConnector;
 }
 
 /**
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 5f2cf2e..97d8912 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -5,7 +5,6 @@
 use Amp\Mysql\DataTypes;
 use Amp\Mysql\Result;
 use Amp\PHPUnit\AsyncTestCase;
-use Amp\Pipeline;
 use Amp\Mysql\Link;
 use Amp\Sql\QueryError;
 
@@ -18,12 +17,6 @@ abstract class LinkTest extends AsyncTestCase
      */
     abstract protected function getLink(string $connectionString): Link;
 
-    public function setUp(): void
-    {
-        parent::setUp();
-        $this->ignoreLoopWatchers();
-    }
-
     public function testQuery()
     {
         $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
@@ -31,8 +24,10 @@ public function testQuery()
         $resultset = $db->execute("SELECT ? AS a", [M_PI]);
         $this->assertInstanceOf(Result::class, $resultset);
 
-        for ($i = 0; $row = $resultset->continue(); ++$i) {
+        $i = 0;
+        foreach ($resultset as $row) {
             $this->assertSame(["a" => M_PI], $row);
+            ++$i;
         }
 
         $this->assertSame(1, $i);
@@ -46,7 +41,7 @@ public function testQueryFetchRow()
         $this->assertInstanceOf(Result::class, $resultset);
 
         $got = [];
-        while ($row = $resultset->continue()) {
+        foreach ($resultset as $row) {
             $got[] = \array_values($row);
         }
 
@@ -71,14 +66,14 @@ public function testMultiStmt()
         $this->assertInstanceOf(Result::class, $resultset);
 
         $got = [];
-        while ($row = $resultset->continue()) {
+        foreach ($resultset as $row) {
             $got[] = \array_values($row);
         }
         $this->assertSame([[1], [2], [3], [4], [5]], $got);
         $this->assertInstanceOf(Result::class, $resultset = $resultset->getNextResult());
 
         $got = [];
-        while ($row = $resultset->continue()) {
+        foreach ($resultset as $row) {
             $got[] = \array_values($row);
         }
         $this->assertSame([[6]], $got);
@@ -87,7 +82,7 @@ public function testMultiStmt()
         $fields = $resultset->getFields();
 
         $got = [];
-        while ($row = $resultset->continue()) {
+        foreach ($resultset as $row) {
             $got[] = $row;
         }
         $this->assertSame([["d" => 5, "c" => 5], ["d" => 6, "c" => 6]], $got);
@@ -127,7 +122,7 @@ public function testPrepared()
         $result = $stmt->execute([2]);
         $this->assertInstanceOf(Result::class, $result);
         $got = [];
-        while ($row = $result->continue()) {
+        foreach ($result as $row) {
             $got[] = \array_values($row);
         }
         $this->assertCount(2, $got);
@@ -136,7 +131,7 @@ public function testPrepared()
         $result = $stmt->execute([1, 8]);
         $this->assertInstanceOf(Result::class, $result);
         $got = [];
-        while ($row = $result->continue()) {
+        foreach ($result as $row) {
             $got[] = \array_values($row);
         }
         $this->assertCount(1, $got);
@@ -145,7 +140,7 @@ public function testPrepared()
         $result = $stmt->execute(["a" => 2, 5]);
         $this->assertInstanceOf(Result::class, $result);
         $got = [];
-        while ($row = $result->continue()) {
+        foreach ($result as $row) {
             $got[] = \array_values($row);
         }
         $this->assertCount(2, $got);
@@ -218,7 +213,7 @@ public function testExecute()
         $result = $db->execute("SELECT * FROM test.main WHERE a = ? OR b = ?", [2, 5]);
         $this->assertInstanceOf(Result::class, $result);
         $got = [];
-        while ($row = $result->continue()) {
+        foreach ($result as $row) {
             $got[] = \array_values($row);
         }
         $this->assertCount(2, $got);
@@ -267,11 +262,9 @@ public function testPreparedWithNegativeValue()
 
         $stmt = $db->prepare("SELECT a FROM tmp");
         $result = $stmt->execute();
-        $row = $result->continue();
-
-        $this->assertEquals(\array_values($row), [-1]);
+        $result = \iterator_to_array($result);
 
-        while ($result->continue()); // Consume remaining results.
+        $this->assertEquals(\array_values(\array_shift($result)), [-1]);
 
         $db->close();
     }
@@ -290,7 +283,7 @@ public function testTransaction()
         $result = $transaction->execute("SELECT * FROM main WHERE a = ?", [6]);
 
         $got = [];
-        while ($row = $result->continue()) {
+        foreach ($result as $row) {
             $got[] = \array_values($row);
         }
         $this->assertCount(1, $got);
@@ -301,7 +294,7 @@ public function testTransaction()
         $result = $db->execute("SELECT * FROM main WHERE a = ?", [6]);
 
         $got = [];
-        while ($row = $result->continue()) {
+        foreach ($result as $row) {
             $got[] = \array_values($row);
         }
         $this->assertCount(0, $got);
@@ -323,7 +316,7 @@ public function testInsertSelect()
         try {
             $statement = $transaction->prepare("SELECT a, b FROM main WHERE a >= ?");
 
-            $count = \count(Pipeline\toArray($statement->execute([$a])));
+            $count = \count(\iterator_to_array($statement->execute([$a])));
 
             $statement = $transaction->prepare("INSERT INTO main (a, b) SELECT a, b FROM main WHERE a >= ?");
 
diff --git a/test/PoolTest.php b/test/PoolTest.php
index e473127..d1321a1 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -2,7 +2,7 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Delayed;
+use Amp\Future;
 use Amp\Mysql\Connection;
 use Amp\Mysql\ConnectionConfig;
 use Amp\Mysql\Internal\CommandResult;
@@ -11,12 +11,11 @@
 use Amp\Mysql\Pool;
 use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
-use Amp\Promise;
 use Amp\Sql\Connector;
 use Amp\Sql\Transaction as SqlTransaction;
-use Amp\Success;
-use function Amp\async;
-use function Amp\await;
+use PHPUnit\Framework\MockObject\MockObject;
+use function Amp\coroutine;
+use function Amp\delay;
 
 interface StatementOperation extends Statement
 {
@@ -46,7 +45,7 @@ protected function createPool(array $connections): Pool
     /**
      * @param int $count
      *
-     * @return \Amp\Mysql\Internal\Processor[]|\PHPUnit\Framework\MockObject\MockObject[]
+     * @return array<int, Processor&MockObject>
      */
     private function makeProcessorSet(int $count): array
     {
@@ -55,6 +54,7 @@ private function makeProcessorSet(int $count): array
         for ($i = 0; $i < $count; ++$i) {
             $processor = $this->createMock(Processor::class);
             $processor->method('isAlive')->willReturn(true);
+            $processor->method('sendClose')->willReturn(Future::complete(null));
             $processors[] = $processor;
         }
 
@@ -91,7 +91,10 @@ public function testSingleQuery(int $count)
         $connection->expects($this->once())
             ->method('query')
             ->with('SQL Query')
-            ->will($this->returnValue(new Delayed(10, $result)));
+            ->willReturn(coroutine(function () use ($result): Result {
+                delay(0.01);
+                return $result;
+            }));
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
@@ -116,19 +119,22 @@ public function testConsecutiveQueries(int $count)
         foreach ($processors as $connection) {
             $connection->method('query')
                 ->with('SQL Query')
-                ->will($this->returnValue(new Delayed(10, $result)));
+                ->willReturn(coroutine(function () use ($result): Result {
+                    delay(0.01);
+                    return $result;
+                }));
         }
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
         try {
-            $promises = [];
+            $futures = [];
 
             for ($i = 0; $i < $count; ++$i) {
-                $promises[] = async(fn() => $pool->query('SQL Query'));
+                $futures[] = coroutine(fn() => $pool->query('SQL Query'));
             }
 
-            $results = await($promises);
+            $results = Future\all($futures);
 
             foreach ($results as $result) {
                 $this->assertInstanceOf(Result::class, $result);
@@ -152,7 +158,10 @@ public function testMultipleTransactions(int $count)
 
         $connection->expects($this->exactly(3))
             ->method('query')
-            ->will($this->returnValue(new Delayed(10, $result)));
+            ->willReturn(coroutine(function () use ($result): Result {
+                delay(0.01);
+                return $result;
+            }));
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
@@ -179,22 +188,25 @@ public function testConsecutiveTransactions(int $count)
 
         foreach ($processors as $connection) {
             $connection->method('query')
-                ->will($this->returnCallback(fn() => new Delayed(10, $result)));
+                ->willReturnCallback(fn () => coroutine(function () use ($result): Result {
+                    delay(0.01);
+                    return $result;
+                }));
         }
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
-        $promises = [];
+        $futures = [];
         for ($i = 0; $i < $count; ++$i) {
-            $promises[] = async(fn() => $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED));
+            $futures[] = coroutine(fn() => $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED));
         }
 
         try {
-            \array_map(function (Promise $promise) {
-                $transaction = await($promise);
+            \array_map(function (Future $future) {
+                $transaction = $future->await();
                 $this->assertInstanceOf(SqlTransaction::class, $transaction);
                 $transaction->rollback();
-            }, $promises);
+            }, $futures);
         } finally {
             $pool->close();
         }
@@ -214,17 +226,17 @@ public function testExtractConnection(int $count)
             $connection->expects($this->once())
                 ->method('query')
                 ->with($query)
-                ->willReturn(new Success($this->createMock(Result::class)));
+                ->willReturn(Future::complete($this->createMock(Result::class)));
         }
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
         try {
-            $promises = [];
+            $futures = [];
             for ($i = 0; $i < $count; ++$i) {
-                $promises[] = async(fn() => $pool->extractConnection());
+                $futures[] = coroutine(fn() => $pool->extractConnection());
             }
-            $results = await($promises);
+            $results = Future\all($futures);
             foreach ($results as $result) {
                 $this->assertInstanceof(Connection::class, $result);
                 $result->query($query);
@@ -249,7 +261,10 @@ public function testConnectionClosedInPool(int $count)
             $processor->expects($this->atLeastOnce())
                 ->method('query')
                 ->with($query)
-                ->willReturn(new Delayed(10, $result));
+                ->willReturn(coroutine(function () use ($result): Result {
+                    delay(0.01);
+                    return $result;
+                }));
         }
 
         $processor = $this->createMock(Processor::class);
@@ -258,7 +273,10 @@ public function testConnectionClosedInPool(int $count)
         $processor->expects($this->once())
             ->method('query')
             ->with($query)
-            ->willReturn(new Delayed(10, $result));
+            ->willReturn(coroutine(function () use ($result): Result {
+                delay(0.01);
+                return $result;
+            }));
 
         \array_unshift($processors, $processor);
 
@@ -266,16 +284,16 @@ public function testConnectionClosedInPool(int $count)
 
         try {
             $this->assertSame($count + 1, $pool->getConnectionLimit());
-            $promises = [];
+            $futures = [];
             for ($i = 0; $i < $count + 1; ++$i) {
-                $promises[] = async(fn() => $pool->query($query));
+                $futures[] = coroutine(fn() => $pool->query($query));
             }
-            await($promises);
-            $promises = [];
+            Future\all($futures);
+            $futures = [];
             for ($i = 0; $i < $count; ++$i) {
-                $promises[] = async(fn() => $pool->query($query));
+                $futures[] = coroutine(fn() => $pool->query($query));
             }
-            await($promises);
+            Future\all($futures);
         } finally {
             $pool->close();
         }
diff --git a/test/bootstrap.php b/test/bootstrap.php
index b947a38..6416264 100644
--- a/test/bootstrap.php
+++ b/test/bootstrap.php
@@ -24,7 +24,7 @@
         } else {
             \shell_exec("kill -9 `cat '$pidfile'` 2>/dev/null");
         }
-        delay(1000);
+        delay(1);
     }
 
     if (\file_exists(__DIR__ . "/mysql_db")) {
@@ -70,7 +70,7 @@
 
 print "\rStarting mysqld...           ";
 
-delay(2000); // Give mysqld time to start.
+delay(2); // Give mysqld time to start.
 
 print "\rCreating test database...    ";
 

From f5e6cc88a13d409c58a9f6eadb4e672b3cfe52dc Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 31 Oct 2021 09:26:28 -0500
Subject: [PATCH 045/174] Add Result::getColumnCount()

---
 src/Internal/CommandResult.php    | 5 +++++
 src/Internal/ConnectionResult.php | 5 +++++
 test/LinkTest.php                 | 7 +++++++
 3 files changed, 17 insertions(+)

diff --git a/src/Internal/CommandResult.php b/src/Internal/CommandResult.php
index db17173..92c95cf 100644
--- a/src/Internal/CommandResult.php
+++ b/src/Internal/CommandResult.php
@@ -28,6 +28,11 @@ public function getRowCount(): int
         return $this->delegate->getRowCount();
     }
 
+    public function getColumnCount(): ?int
+    {
+        return $this->delegate->getColumnCount();
+    }
+
     /**
      * @return int|null Insert ID of the last auto increment row.
      */
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index 1822198..f0ca730 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -113,6 +113,11 @@ public function getRowCount(): ?int
         return $this->result->affectedRows;
     }
 
+    public function getColumnCount(): ?int
+    {
+        return $this->result->columnCount;
+    }
+
     public function getLastInsertId(): ?int
     {
         return $this->result->insertId;
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 97d8912..b648bee 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -40,6 +40,8 @@ public function testQueryFetchRow()
         $resultset = $db->query('SELECT a FROM main WHERE a < 4');
         $this->assertInstanceOf(Result::class, $resultset);
 
+        $this->assertSame(1, $resultset->getColumnCount());
+
         $got = [];
         foreach ($resultset as $row) {
             $got[] = \array_values($row);
@@ -121,6 +123,7 @@ public function testPrepared()
         $stmt->bind("num", 5);
         $result = $stmt->execute([2]);
         $this->assertInstanceOf(Result::class, $result);
+        $this->assertSame(3, $result->getColumnCount());
         $got = [];
         foreach ($result as $row) {
             $got[] = \array_values($row);
@@ -130,6 +133,7 @@ public function testPrepared()
         $stmt = $db->prepare("SELECT * FROM main WHERE a = ? OR b = ?");
         $result = $stmt->execute([1, 8]);
         $this->assertInstanceOf(Result::class, $result);
+        $this->assertSame(3, $result->getColumnCount());
         $got = [];
         foreach ($result as $row) {
             $got[] = \array_values($row);
@@ -139,6 +143,7 @@ public function testPrepared()
         $stmt = $db->prepare("SELECT * FROM main WHERE a = :a OR b = ?");
         $result = $stmt->execute(["a" => 2, 5]);
         $this->assertInstanceOf(Result::class, $result);
+        $this->assertSame(3, $result->getColumnCount());
         $got = [];
         foreach ($result as $row) {
             $got[] = \array_values($row);
@@ -147,11 +152,13 @@ public function testPrepared()
 
         $stmt = $db->prepare("INSERT INTO main (a, b) VALUES (:a, :b)");
         $result = $stmt->execute(["a" => 10, "b" => 11]);
+        $this->assertNull($result->getColumnCount());
         $this->assertInstanceOf(Result::class, $result);
         $this->assertGreaterThan(5, $result->getLastInsertId());
 
         $stmt = $db->prepare("DELETE FROM main WHERE a = :a");
         $result = $stmt->execute(["a" => 10]);
+        $this->assertNull($result->getColumnCount());
         $this->assertInstanceOf(Result::class, $result);
     }
 

From d26a4f078d84f7d0861f1e663964b80cc1bd8325 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 31 Oct 2021 09:59:20 -0500
Subject: [PATCH 046/174] Revert to using AsyncGenerator

Ensures the result is consumed before destruction.
---
 composer.json                     |  1 +
 src/Internal/ConnectionResult.php | 33 ++++++++++++++++++++-----------
 src/Internal/ResultProxy.php      |  9 ++++++---
 3 files changed, 29 insertions(+), 14 deletions(-)

diff --git a/composer.json b/composer.json
index bb3e99b..de8f12f 100644
--- a/composer.json
+++ b/composer.json
@@ -15,6 +15,7 @@
     "require": {
         "php": ">=8",
         "amphp/amp": "^3-dev",
+        "amphp/pipeline": "^1-dev",
         "amphp/socket": "^2-dev",
         "amphp/sql": "^2-dev",
         "amphp/sql-common": "^2-dev"
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index f0ca730..cc35061 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -5,6 +5,7 @@
 use Amp\Deferred;
 use Amp\Future;
 use Amp\Mysql\Result;
+use Amp\Pipeline\AsyncGenerator;
 use Revolt\EventLoop;
 use function Amp\coroutine;
 
@@ -12,14 +13,14 @@ final class ConnectionResult implements Result, \IteratorAggregate
 {
     private ResultProxy $result;
 
-    private \Generator $generator;
+    private AsyncGenerator $generator;
 
     private ?Future $nextResult = null;
 
     public function __construct(ResultProxy $result)
     {
         $this->result = $result;
-        $this->generator = self::iterate($result);
+        $this->generator = new AsyncGenerator(static fn () => self::iterate($result));
     }
 
     public function getIterator(): \Traversable
@@ -32,15 +33,19 @@ private static function iterate(ResultProxy $result): \Generator
         $next = self::fetchRow($result);
 
         try {
-            while ($row = $next->await()) {
-                if (!isset($columnNames)) {
-                    $columnNames = \array_column($result->columns, 'name');
-                }
+            if (!($row = $next->await())) {
+                return;
+            }
+
+            // Column names are only available once a result row has been fetched.
+            $columnNames = \array_column($result->columns, 'name');
+
+            do {
                 $next = self::fetchRow($result);
                 yield \array_combine($columnNames, $row);
-            }
+            } while ($row = $next->await());
         } finally {
-            if (($row ?? null) === null) {
+            if (!isset($row)) {
                 return; // Result fully consumed.
             }
 
@@ -55,9 +60,12 @@ private static function iterate(ResultProxy $result): \Generator
                 }
             });
         }
-
     }
 
+    /**
+     * @param ResultProxy $result
+     * @return Future<array|null>
+     */
     private static function fetchRow(ResultProxy $result): Future
     {
         if ($result->userFetched < $result->fetchedRows) {
@@ -76,7 +84,7 @@ private static function fetchRow(ResultProxy $result): Future
         /* We need to increment the internal counter, else the next time fetch is called,
          * it'll simply return the row we fetch here instead of fetching a new row
          * since callback order on promises isn't defined, we can't do this via onResolve() */
-        $incRow = function ($row) use ($result) {
+        $incRow = static function ($row) use ($result) {
             unset($result->rows[$result->userFetched++]);
             return $row;
         };
@@ -129,7 +137,10 @@ public function getLastInsertId(): ?int
     public function getFields(): ?array
     {
         if ($this->result === null) {
-            throw new \Error("The current result set is empty; call this method before invoking ResultSet::nextResultSet()");
+            throw new \Error(\sprintf(
+                "The current result set is empty; call this method before invoking %s::getNextResult()",
+                Result::class,
+            ));
         }
 
         if ($this->result->state >= ResultProxy::COLUMNS_FETCHED) {
diff --git a/src/Internal/ResultProxy.php b/src/Internal/ResultProxy.php
index 0799a8f..52c0774 100644
--- a/src/Internal/ResultProxy.php
+++ b/src/Internal/ResultProxy.php
@@ -3,7 +3,6 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\Deferred;
-use Amp\Sql\FailureException;
 
 final class ResultProxy
 {
@@ -19,7 +18,11 @@ final class ResultProxy
 
     public ?int $affectedRows = null;
 
-    public array $deferreds = [self::UNFETCHED => [], self::COLUMNS_FETCHED => [], self::ROWS_FETCHED => []];
+    public array $deferreds = [
+        self::UNFETCHED => [],
+        self::COLUMNS_FETCHED => [],
+        self::ROWS_FETCHED => []
+    ];
 
     public int $state = self::UNFETCHED;
 
@@ -65,7 +68,7 @@ public function rowFetched(?array $row): void
         }
     }
 
-    public function fail(FailureException $e): void
+    public function error(\Throwable $e): void
     {
         foreach ($this->deferreds as $state) {
             foreach ($this->deferreds[$state] as [$deferred]) {

From 42930602e3022b856a5fca256546c8b98ad76323 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 6 Nov 2021 12:15:38 -0500
Subject: [PATCH 047/174] Cleanup auth data handling

---
 src/Internal/Processor.php | 40 ++++++++++++++++----------------------
 1 file changed, 17 insertions(+), 23 deletions(-)

diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index d2ecdbe..4776018 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -1582,42 +1582,36 @@ private function sendHandshake(bool $inSSL = false): void
         $this->write($payload);
     }
 
-    private function getAuthData()
+    private function getAuthData(): string
     {
-        if ($this->config->getPassword() == "") {
-            $auth = "";
-        } elseif ($this->capabilities & self::CLIENT_PLUGIN_AUTH) {
+        $password = (string) $this->config->getPassword();
+
+        if ($password === "") {
+            return "";
+        }
+
+        if ($this->capabilities & self::CLIENT_PLUGIN_AUTH) {
             switch ($this->authPluginName) {
                 case "mysql_native_password":
-                    $auth = $this->secureAuth($this->config->getPassword(), $this->authPluginData);
-                    break;
+                    return $this->secureAuth($password, $this->authPluginData);
                 case "mysql_clear_password":
-                    $auth = $this->config->getPassword();
-                    break;
+                    return $password;
                 case "sha256_password":
-                    if ($this->config->getPassword() === "") {
-                        $auth = "";
-                    } else {
-                        $key = $this->config->getKey();
-                        if ($key !== null) {
-                            $auth = $this->sha256Auth($this->config->getPassword(), $this->authPluginData, $key);
-                        } else {
-                            $auth = "\x01";
-                        }
+                    $key = $this->config->getKey();
+                    if ($key !== "") {
+                        return $this->sha256Auth($password, $this->authPluginData, $key);
                     }
-            break;
+                    return "\x01";
                 case "caching_sha2_password":
-                    $auth = $this->sha2Auth($this->config->getPassword(), $this->authPluginData);
-                    break;
+                    return $this->sha2Auth($password, $this->authPluginData);
                 case "mysql_old_password":
                     throw new ConnectionException("mysql_old_password is outdated and insecure. Intentionally not implemented!");
                 default:
                     throw new ConnectionException("Invalid (or unimplemented?) auth method requested by server: {$this->authPluginName}");
             }
-        } else {
-            $auth = $this->secureAuth($this->config->getPassword(), $this->authPluginData);
         }
-        return $auth;
+
+        return $this->secureAuth($password, $this->authPluginData);
     }
 
     /** @see 14.1.2 MySQL Packet */

From 6c07a68191ad08599ffc11625327d10b1e2a83a1 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 22 Nov 2021 23:10:42 -0600
Subject: [PATCH 048/174] Update for rename of coroutine to launch

---
 examples/3-generic-with-yield.php  |  4 ++--
 examples/4-multi-rows.php          |  6 +++---
 examples/support/generic-table.php |  2 +-
 src/Internal/ConnectionResult.php  |  4 ++--
 test/PoolTest.php                  | 24 ++++++++++++------------
 5 files changed, 20 insertions(+), 20 deletions(-)

diff --git a/examples/3-generic-with-yield.php b/examples/3-generic-with-yield.php
index 2e82f4f..e49c357 100644
--- a/examples/3-generic-with-yield.php
+++ b/examples/3-generic-with-yield.php
@@ -4,7 +4,7 @@
 
 use Amp\Future;
 use Amp\Mysql;
-use function Amp\coroutine;
+use function Amp\launch;
 
 $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
@@ -20,7 +20,7 @@
 
 $future = [];
 foreach (\range(1, 5) as $num) {
-    $future[] = coroutine(fn() => $statement->execute([$num, $num]));
+    $future[] = launch(fn() => $statement->execute([$num, $num]));
 }
 
 /* wait until everything is inserted */
diff --git a/examples/4-multi-rows.php b/examples/4-multi-rows.php
index 5cc50de..2a08267 100644
--- a/examples/4-multi-rows.php
+++ b/examples/4-multi-rows.php
@@ -5,7 +5,7 @@
 
 use Amp\Future;
 use Amp\Mysql;
-use function Amp\coroutine;
+use function Amp\launch;
 
 $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
@@ -14,8 +14,8 @@
 
 $future = [];
 
-$future[] = coroutine(fn() => $db->query("SELECT a * b FROM tmp"));
-$future[] = coroutine(fn() => $db->execute("SELECT POW(a, ?) AS power FROM tmp", [2]));
+$future[] = launch(fn() => $db->query("SELECT a * b FROM tmp"));
+$future[] = launch(fn() => $db->execute("SELECT POW(a, ?) AS power FROM tmp", [2]));
 
 try {
     /**
diff --git a/examples/support/generic-table.php b/examples/support/generic-table.php
index 12b4ba3..c172640 100644
--- a/examples/support/generic-table.php
+++ b/examples/support/generic-table.php
@@ -14,7 +14,7 @@ function createGenericTable(Link $db): void
 
     $futures = [];
     foreach (\range(1, 5) as $num) {
-        $futures[] = \Amp\coroutine(fn() => $statement->execute([$num, $num]));
+        $futures[] = \Amp\launch(fn() => $statement->execute([$num, $num]));
     }
 
     Future\all($futures);
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index cc35061..e744768 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -7,7 +7,7 @@
 use Amp\Mysql\Result;
 use Amp\Pipeline\AsyncGenerator;
 use Revolt\EventLoop;
-use function Amp\coroutine;
+use function Amp\launch;
 
 final class ConnectionResult implements Result, \IteratorAggregate
 {
@@ -102,7 +102,7 @@ public function getNextResult(): ?Result
             return $this->nextResult->await();
         }
 
-        $this->nextResult = coroutine(function (): ?Result {
+        $this->nextResult = launch(function (): ?Result {
             $deferred = $this->result->next ?: $this->result->next = new Deferred;
             $result = $deferred->getFuture()->await();
 
diff --git a/test/PoolTest.php b/test/PoolTest.php
index d1321a1..5cba125 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -14,7 +14,7 @@
 use Amp\Sql\Connector;
 use Amp\Sql\Transaction as SqlTransaction;
 use PHPUnit\Framework\MockObject\MockObject;
-use function Amp\coroutine;
+use function Amp\launch;
 use function Amp\delay;
 
 interface StatementOperation extends Statement
@@ -91,7 +91,7 @@ public function testSingleQuery(int $count)
         $connection->expects($this->once())
             ->method('query')
             ->with('SQL Query')
-            ->willReturn(coroutine(function () use ($result): Result {
+            ->willReturn(launch(function () use ($result): Result {
                 delay(0.01);
                 return $result;
             }));
@@ -119,7 +119,7 @@ public function testConsecutiveQueries(int $count)
         foreach ($processors as $connection) {
             $connection->method('query')
                 ->with('SQL Query')
-                ->willReturn(coroutine(function () use ($result): Result {
+                ->willReturn(launch(function () use ($result): Result {
                     delay(0.01);
                     return $result;
                 }));
@@ -131,7 +131,7 @@ public function testConsecutiveQueries(int $count)
             $futures = [];
 
             for ($i = 0; $i < $count; ++$i) {
-                $futures[] = coroutine(fn() => $pool->query('SQL Query'));
+                $futures[] = launch(fn() => $pool->query('SQL Query'));
             }
 
             $results = Future\all($futures);
@@ -158,7 +158,7 @@ public function testMultipleTransactions(int $count)
 
         $connection->expects($this->exactly(3))
             ->method('query')
-            ->willReturn(coroutine(function () use ($result): Result {
+            ->willReturn(launch(function () use ($result): Result {
                 delay(0.01);
                 return $result;
             }));
@@ -188,7 +188,7 @@ public function testConsecutiveTransactions(int $count)
 
         foreach ($processors as $connection) {
             $connection->method('query')
-                ->willReturnCallback(fn () => coroutine(function () use ($result): Result {
+                ->willReturnCallback(fn () => launch(function () use ($result): Result {
                     delay(0.01);
                     return $result;
                 }));
@@ -198,7 +198,7 @@ public function testConsecutiveTransactions(int $count)
 
         $futures = [];
         for ($i = 0; $i < $count; ++$i) {
-            $futures[] = coroutine(fn() => $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED));
+            $futures[] = launch(fn() => $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED));
         }
 
         try {
@@ -234,7 +234,7 @@ public function testExtractConnection(int $count)
         try {
             $futures = [];
             for ($i = 0; $i < $count; ++$i) {
-                $futures[] = coroutine(fn() => $pool->extractConnection());
+                $futures[] = launch(fn() => $pool->extractConnection());
             }
             $results = Future\all($futures);
             foreach ($results as $result) {
@@ -261,7 +261,7 @@ public function testConnectionClosedInPool(int $count)
             $processor->expects($this->atLeastOnce())
                 ->method('query')
                 ->with($query)
-                ->willReturn(coroutine(function () use ($result): Result {
+                ->willReturn(launch(function () use ($result): Result {
                     delay(0.01);
                     return $result;
                 }));
@@ -273,7 +273,7 @@ public function testConnectionClosedInPool(int $count)
         $processor->expects($this->once())
             ->method('query')
             ->with($query)
-            ->willReturn(coroutine(function () use ($result): Result {
+            ->willReturn(launch(function () use ($result): Result {
                 delay(0.01);
                 return $result;
             }));
@@ -286,12 +286,12 @@ public function testConnectionClosedInPool(int $count)
             $this->assertSame($count + 1, $pool->getConnectionLimit());
             $futures = [];
             for ($i = 0; $i < $count + 1; ++$i) {
-                $futures[] = coroutine(fn() => $pool->query($query));
+                $futures[] = launch(fn() => $pool->query($query));
             }
             Future\all($futures);
             $futures = [];
             for ($i = 0; $i < $count; ++$i) {
-                $futures[] = coroutine(fn() => $pool->query($query));
+                $futures[] = launch(fn() => $pool->query($query));
             }
             Future\all($futures);
         } finally {

From d0b9a618b45ad3a10452a3860056db76a6930a75 Mon Sep 17 00:00:00 2001
From: Paul Kofmann <crtlib@gmail.com>
Date: Wed, 1 Dec 2021 18:38:48 +0100
Subject: [PATCH 049/174] Fix bug in decodeJson() function (#113)

---
 src/DataTypes.php  |  2 +-
 test/LinkTest.php  | 13 +++++++++++++
 test/bootstrap.php |  2 ++
 3 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/src/DataTypes.php b/src/DataTypes.php
index b488be1..cdc6c48 100644
--- a/src/DataTypes.php
+++ b/src/DataTypes.php
@@ -240,7 +240,7 @@ public static function decodeStringOff(int $type, string $str, int &$off) /* : m
 
     private static function decodeJson(string $data): string
     {
-        if (!\strncmp(self::ENCODED_JSON_PREFIX, $data, \strlen(self::ENCODED_JSON_PREFIX))) {
+        if (\strncmp(self::ENCODED_JSON_PREFIX, $data, \strlen(self::ENCODED_JSON_PREFIX)) !== 0) {
             return $data; // Data was not base-64 encoded.
         }
 
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 2d2485e..4bce488 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -364,4 +364,17 @@ public function testInsertSelect()
             yield $transaction->rollback();
         }
     }
+
+    public function testJsonDecoding()
+    {
+        /** @var Link $db */
+        $db = yield $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+
+        /** @var ResultSet $resultset */
+        $resultset = yield $db->execute("SELECT a FROM test.json");
+        $this->assertInstanceOf(ResultSet::class, $resultset);
+
+        yield $resultset->advance();
+        $this->assertSame(["a" => '{"key": "value"}'], $resultset->getCurrent());
+    }
 }
diff --git a/test/bootstrap.php b/test/bootstrap.php
index 36b203b..293a1af 100644
--- a/test/bootstrap.php
+++ b/test/bootstrap.php
@@ -78,6 +78,8 @@
     $db->query("CREATE DATABASE test");
     $db->query("CREATE TABLE test.main (a INT(11), b INT(11))");
     $db->query("INSERT INTO test.main VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)");
+    $db->query("CREATE TABLE test.json (a JSON)");
+    $db->query("INSERT INTO test.json VALUES ('{\"key\": \"value\"}')");
     $db->close();
 
     print "\r";

From f03908ef25f6895811b286167664c168bbd48808 Mon Sep 17 00:00:00 2001
From: Bob Weinand <bobwei9@hotmail.com>
Date: Sat, 4 Dec 2021 13:56:09 +0100
Subject: [PATCH 050/174] Update for recent renames

---
 examples/3-generic-with-yield.php    |  4 ++--
 examples/4-multi-rows.php            |  6 ++---
 examples/support/generic-table.php   |  2 +-
 src/CancellableConnector.php         |  4 ++--
 src/Connection.php                   | 18 +++++++-------
 src/Internal/ConnectionResult.php    | 12 +++++-----
 src/Internal/ConnectionStatement.php |  4 ++--
 src/Internal/Processor.php           | 36 ++++++++++++++--------------
 src/Internal/ResultProxy.php         | 10 ++++----
 test/PoolTest.php                    | 24 +++++++++----------
 10 files changed, 60 insertions(+), 60 deletions(-)

diff --git a/examples/3-generic-with-yield.php b/examples/3-generic-with-yield.php
index e49c357..42da744 100644
--- a/examples/3-generic-with-yield.php
+++ b/examples/3-generic-with-yield.php
@@ -4,7 +4,7 @@
 
 use Amp\Future;
 use Amp\Mysql;
-use function Amp\launch;
+use function Amp\async;
 
 $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
@@ -20,7 +20,7 @@
 
 $future = [];
 foreach (\range(1, 5) as $num) {
-    $future[] = launch(fn() => $statement->execute([$num, $num]));
+    $future[] = async(fn() => $statement->execute([$num, $num]));
 }
 
 /* wait until everything is inserted */
diff --git a/examples/4-multi-rows.php b/examples/4-multi-rows.php
index 2a08267..c55a070 100644
--- a/examples/4-multi-rows.php
+++ b/examples/4-multi-rows.php
@@ -5,7 +5,7 @@
 
 use Amp\Future;
 use Amp\Mysql;
-use function Amp\launch;
+use function Amp\async;
 
 $db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
@@ -14,8 +14,8 @@
 
 $future = [];
 
-$future[] = launch(fn() => $db->query("SELECT a * b FROM tmp"));
-$future[] = launch(fn() => $db->execute("SELECT POW(a, ?) AS power FROM tmp", [2]));
+$future[] = async(fn() => $db->query("SELECT a * b FROM tmp"));
+$future[] = async(fn() => $db->execute("SELECT POW(a, ?) AS power FROM tmp", [2]));
 
 try {
     /**
diff --git a/examples/support/generic-table.php b/examples/support/generic-table.php
index c172640..a618d56 100644
--- a/examples/support/generic-table.php
+++ b/examples/support/generic-table.php
@@ -14,7 +14,7 @@ function createGenericTable(Link $db): void
 
     $futures = [];
     foreach (\range(1, 5) as $num) {
-        $futures[] = \Amp\launch(fn() => $statement->execute([$num, $num]));
+        $futures[] = \Amp\async(fn() => $statement->execute([$num, $num]));
     }
 
     Future\all($futures);
diff --git a/src/CancellableConnector.php b/src/CancellableConnector.php
index be14511..aad3b3e 100644
--- a/src/CancellableConnector.php
+++ b/src/CancellableConnector.php
@@ -2,7 +2,7 @@
 
 namespace Amp\Mysql;
 
-use Amp\CancellationToken;
+use Amp\Cancellation;
 use Amp\Socket;
 use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
 use Amp\Sql\Connector;
@@ -16,7 +16,7 @@ public function __construct(?Socket\Connector $connector = null)
         $this->connector = $connector ?? Socket\connector();
     }
 
-    public function connect(SqlConnectionConfig $config, ?CancellationToken $token = null): Connection
+    public function connect(SqlConnectionConfig $config, ?Cancellation $token = null): Connection
     {
         if (!$config instanceof ConnectionConfig) {
             throw new \TypeError(\sprintf("Must provide an instance of %s to MySQL connectors", ConnectionConfig::class));
diff --git a/src/Connection.php b/src/Connection.php
index c372884..3d37e8e 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -2,9 +2,9 @@
 
 namespace Amp\Mysql;
 
-use Amp\CancellationToken;
-use Amp\Deferred;
-use Amp\NullCancellationToken;
+use Amp\Cancellation;
+use Amp\DeferredFuture;
+use Amp\NullCancellation;
 use Amp\Socket;
 use Revolt\EventLoop;
 
@@ -21,24 +21,24 @@ final class Connection implements Link
 
     private Internal\Processor $processor;
 
-    private ?Deferred $busy = null;
+    private ?DeferredFuture $busy = null;
 
     /** @var \Closure Function used to release connection after a transaction has completed. */
     private \Closure $release;
 
     /**
      * @param ConnectionConfig $config
-     * @param CancellationToken|null $token
+     * @param Cancellation|null $token
      * @param Socket\Connector|null $connector
      *
      * @return self
      */
     public static function connect(
         ConnectionConfig $config,
-        ?CancellationToken $token = null,
+        ?Cancellation $token = null,
         ?Socket\Connector $connector = null
     ): self {
-        $token = $token ?? new NullCancellationToken;
+        $token = $token ?? new NullCancellation;
 
         $socket = ($connector ?? Socket\connector())
             ->connect($config->getConnectionString(), $config->getConnectContext(), $token);
@@ -57,7 +57,7 @@ private function __construct(Internal\Processor $processor)
 
         $busy = &$this->busy;
         $this->release = static function () use (&$busy): void {
-            \assert($busy instanceof Deferred);
+            \assert($busy instanceof DeferredFuture);
             $busy->complete(null);
             $busy = null;
         };
@@ -126,7 +126,7 @@ public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITT
             $this->busy->getFuture()->await();
         }
 
-        $this->busy = $deferred = new Deferred;
+        $this->busy = $deferred = new DeferredFuture;
 
         switch ($isolation) {
             case Transaction::ISOLATION_UNCOMMITTED:
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index e744768..a409c48 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -2,12 +2,12 @@
 
 namespace Amp\Mysql\Internal;
 
-use Amp\Deferred;
+use Amp\DeferredFuture;
 use Amp\Future;
 use Amp\Mysql\Result;
 use Amp\Pipeline\AsyncGenerator;
 use Revolt\EventLoop;
-use function Amp\launch;
+use function Amp\async;
 
 final class ConnectionResult implements Result, \IteratorAggregate
 {
@@ -79,7 +79,7 @@ private static function fetchRow(ResultProxy $result): Future
             return Future::complete(null);
         }
 
-        $deferred = new Deferred;
+        $deferred = new DeferredFuture;
 
         /* We need to increment the internal counter, else the next time fetch is called,
          * it'll simply return the row we fetch here instead of fetching a new row
@@ -102,8 +102,8 @@ public function getNextResult(): ?Result
             return $this->nextResult->await();
         }
 
-        $this->nextResult = launch(function (): ?Result {
-            $deferred = $this->result->next ?: $this->result->next = new Deferred;
+        $this->nextResult = async(function (): ?Result {
+            $deferred = $this->result->next ?: $this->result->next = new DeferredFuture;
             $result = $deferred->getFuture()->await();
 
             if ($result instanceof ResultProxy) {
@@ -147,7 +147,7 @@ public function getFields(): ?array
             return $this->result->columns;
         }
 
-        $deferred = new Deferred;
+        $deferred = new DeferredFuture;
         $this->result->deferreds[ResultProxy::COLUMNS_FETCHED][] = [$deferred, &$this->result->columns, null];
         return $deferred->getFuture()->await();
     }
diff --git a/src/Internal/ConnectionStatement.php b/src/Internal/ConnectionStatement.php
index 53418ac..52122d9 100644
--- a/src/Internal/ConnectionStatement.php
+++ b/src/Internal/ConnectionStatement.php
@@ -2,7 +2,7 @@
 
 namespace Amp\Mysql\Internal;
 
-use Amp\Deferred;
+use Amp\DeferredFuture;
 use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
 use Amp\Sql\ConnectionException;
@@ -156,7 +156,7 @@ public function getFields(): ?array
             return $this->result->deferreds[ResultProxy::COLUMNS_FETCHED][0][0]->promise();
         }
 
-        $deferred = new Deferred;
+        $deferred = new DeferredFuture;
         $this->result->deferreds[ResultProxy::COLUMNS_FETCHED][0] = [$deferred, &$this->result->columns, null];
         return $deferred->getFuture()->await();
     }
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 42a7361..048de29 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -2,8 +2,8 @@
 
 namespace Amp\Mysql\Internal;
 
-use Amp\CancellationToken;
-use Amp\Deferred;
+use Amp\Cancellation;
+use Amp\DeferredFuture;
 use Amp\File;
 use Amp\Future;
 use Amp\Mysql\ConnectionConfig;
@@ -82,7 +82,7 @@ class Processor implements TransientResource
 
     public ConnectionConfig $config;
 
-    /** @var Deferred[] */
+    /** @var DeferredFuture[] */
     private array $deferreds = [];
 
     /** @var callable[] */
@@ -186,18 +186,18 @@ private function ready(): void
         }
     }
 
-    private function addDeferred(Deferred $deferred): void
+    private function addDeferred(DeferredFuture $deferred): void
     {
         \assert($this->socket, "The connection has been closed");
         $this->deferreds[] = $deferred;
         $this->socket->reference();
     }
 
-    public function connect(CancellationToken $token): void
+    public function connect(Cancellation $token): void
     {
         \assert(!$this->processors, self::class."::connect() must not be called twice");
 
-        $this->deferreds[] = $deferred = new Deferred; // Will be resolved in sendHandshake().
+        $this->deferreds[] = $deferred = new DeferredFuture; // Will be resolved in sendHandshake().
 
         $this->processors = [$this->parseMysql()];
 
@@ -209,7 +209,7 @@ public function connect(CancellationToken $token): void
         $future = $future->finally(static fn() => $token->unsubscribe($id));
 
         if ($this->config->getCharset() !== ConnectionConfig::DEFAULT_CHARSET || $this->config->getCollation() !== ConnectionConfig::DEFAULT_COLLATE) {
-            $future = $future->apply(function () {
+            $future = $future->map(function () {
                 $charset = $this->config->getCharset();
                 $collate = $this->config->getCollation();
 
@@ -282,7 +282,7 @@ private function processData(string $data): void
         }
     }
 
-    private function getDeferred(): Deferred
+    private function getDeferred(): DeferredFuture
     {
         return \array_shift($this->deferreds);
     }
@@ -322,7 +322,7 @@ protected function startCommand(callable $callback): Future
             throw new \Error("The connection has been closed");
         }
 
-        $deferred = new Deferred;
+        $deferred = new DeferredFuture;
         $this->appendTask(function () use ($callback, $deferred) {
             $this->seqId = $this->compressionId = -1;
             $this->addDeferred($deferred);
@@ -444,7 +444,7 @@ public function bindParam(int $stmtId, int $paramId, string $data): void
     // $params is by-ref, because the actual result object might not yet have been filled completely with data upon call of this method ...
     public function execute(int $stmtId, string $query, array &$params, array $prebound, array $data = []): Future
     {
-        $deferred = new Deferred;
+        $deferred = new DeferredFuture;
         $this->appendTask(function () use ($stmtId, $query, &$params, $prebound, $data, $deferred): void {
             $payload = "\x17";
             $payload .= DataTypes::encodeInt32($stmtId);
@@ -511,7 +511,7 @@ public function closeStmt(int $stmtId): void
     /** @see 14.6.5 COM_FIELD_LIST */
     public function listFields(string $table, string $like = "%"): Future
     {
-        return $this->startCommand(static function () use ($table, $like): void {
+        return $this->startCommand(function () use ($table, $like): void {
             $this->sendPacket("\x04$table\0$like");
             $this->parseCallback = [$this, "handleFieldlist"];
         });
@@ -519,7 +519,7 @@ public function listFields(string $table, string $like = "%"): Future
 
     public function listAllFields(string $table, string $like = "%"): Future
     {
-        $apply = function (?array $array) use (&$apply): array {
+        $map = function (?array $array) use (&$map): array {
             static $columns = [];
 
             if ($array === null) {
@@ -527,10 +527,10 @@ public function listAllFields(string $table, string $like = "%"): Future
             }
 
             [$columns[], $future] = $array;
-            return $future->apply($apply)->await();
+            return $future->map($map)->await();
         };
 
-        return $this->listFields($table, $like)->apply($apply);
+        return $this->listFields($table, $like)->map($map);
     }
 
     /** @see 14.6.6 COM_CREATE_DB */
@@ -606,7 +606,7 @@ public function debugStdout(): Future
     public function resetStmt(int $stmtId): Future
     {
         $payload = "\x1a" . DataTypes::encodeInt32($stmtId);
-        $deferred = new Deferred;
+        $deferred = new DeferredFuture;
         $this->appendTask(function () use ($payload, $deferred): void {
             $this->resetIds();
             $this->addDeferred($deferred);
@@ -619,7 +619,7 @@ public function resetStmt(int $stmtId): Future
     public function fetchStmt(int $stmtId): Future
     {
         $payload = "\x1c" . DataTypes::encodeInt32($stmtId) . DataTypes::encodeInt32(1);
-        $deferred = new Deferred;
+        $deferred = new DeferredFuture;
         $this->appendTask(function () use ($payload, $deferred): void {
             $this->resetIds();
             $this->addDeferred($deferred);
@@ -930,7 +930,7 @@ private function handleFieldList(string $packet): void
             $this->getDeferred()->complete(null);
             $this->ready();
         } else {
-            $this->addDeferred($deferred = new Deferred);
+            $this->addDeferred($deferred = new DeferredFuture);
             $this->getDeferred()->complete([$this->parseColumnDefinition($packet), $deferred]);
         }
     }
@@ -1066,7 +1066,7 @@ private function successfulResultsetFetch(): void
         $result = $this->result;
         $deferred = &$result->next;
         if (!$deferred) {
-            $deferred = new Deferred;
+            $deferred = new DeferredFuture;
         }
         if ($this->connInfo->statusFlags & StatusFlags::SERVER_MORE_RESULTS_EXISTS) {
             $this->parseCallback = [$this, "handleQuery"];
diff --git a/src/Internal/ResultProxy.php b/src/Internal/ResultProxy.php
index 52c0774..6d6a1b2 100644
--- a/src/Internal/ResultProxy.php
+++ b/src/Internal/ResultProxy.php
@@ -2,7 +2,7 @@
 
 namespace Amp\Mysql\Internal;
 
-use Amp\Deferred;
+use Amp\DeferredFuture;
 
 final class ResultProxy
 {
@@ -26,7 +26,7 @@ final class ResultProxy
 
     public int $state = self::UNFETCHED;
 
-    public ?Deferred $next = null;
+    public ?DeferredFuture $next = null;
 
     public const UNFETCHED = 0;
     public const COLUMNS_FETCHED = 1;
@@ -49,7 +49,7 @@ public function updateState(int $state): void
             return;
         }
         foreach ($this->deferreds[$state] as [$deferred, $rows, $cb]) {
-            \assert($deferred instanceof Deferred);
+            \assert($deferred instanceof DeferredFuture);
             $deferred->complete($cb ? $cb($rows) : $rows);
         }
         $this->deferreds[$state] = [];
@@ -63,7 +63,7 @@ public function rowFetched(?array $row): void
         [$entry, , $cb] = \current($this->deferreds[self::UNFETCHED]);
         if ($entry !== null) {
             unset($this->deferreds[self::UNFETCHED][\key($this->deferreds[self::UNFETCHED])]);
-            \assert($entry instanceof Deferred);
+            \assert($entry instanceof DeferredFuture);
             $entry->complete($cb && $row ? $cb($row) : $row);
         }
     }
@@ -72,7 +72,7 @@ public function error(\Throwable $e): void
     {
         foreach ($this->deferreds as $state) {
             foreach ($this->deferreds[$state] as [$deferred]) {
-                \assert($deferred instanceof Deferred);
+                \assert($deferred instanceof DeferredFuture);
                 $deferred->error($e);
             }
             $this->deferreds[$state] = [];
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 5cba125..503d6b6 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -14,7 +14,7 @@
 use Amp\Sql\Connector;
 use Amp\Sql\Transaction as SqlTransaction;
 use PHPUnit\Framework\MockObject\MockObject;
-use function Amp\launch;
+use function Amp\async;
 use function Amp\delay;
 
 interface StatementOperation extends Statement
@@ -91,7 +91,7 @@ public function testSingleQuery(int $count)
         $connection->expects($this->once())
             ->method('query')
             ->with('SQL Query')
-            ->willReturn(launch(function () use ($result): Result {
+            ->willReturn(async(function () use ($result): Result {
                 delay(0.01);
                 return $result;
             }));
@@ -119,7 +119,7 @@ public function testConsecutiveQueries(int $count)
         foreach ($processors as $connection) {
             $connection->method('query')
                 ->with('SQL Query')
-                ->willReturn(launch(function () use ($result): Result {
+                ->willReturn(async(function () use ($result): Result {
                     delay(0.01);
                     return $result;
                 }));
@@ -131,7 +131,7 @@ public function testConsecutiveQueries(int $count)
             $futures = [];
 
             for ($i = 0; $i < $count; ++$i) {
-                $futures[] = launch(fn() => $pool->query('SQL Query'));
+                $futures[] = async(fn() => $pool->query('SQL Query'));
             }
 
             $results = Future\all($futures);
@@ -158,7 +158,7 @@ public function testMultipleTransactions(int $count)
 
         $connection->expects($this->exactly(3))
             ->method('query')
-            ->willReturn(launch(function () use ($result): Result {
+            ->willReturn(async(function () use ($result): Result {
                 delay(0.01);
                 return $result;
             }));
@@ -188,7 +188,7 @@ public function testConsecutiveTransactions(int $count)
 
         foreach ($processors as $connection) {
             $connection->method('query')
-                ->willReturnCallback(fn () => launch(function () use ($result): Result {
+                ->willReturnCallback(fn () => async(function () use ($result): Result {
                     delay(0.01);
                     return $result;
                 }));
@@ -198,7 +198,7 @@ public function testConsecutiveTransactions(int $count)
 
         $futures = [];
         for ($i = 0; $i < $count; ++$i) {
-            $futures[] = launch(fn() => $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED));
+            $futures[] = async(fn() => $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED));
         }
 
         try {
@@ -234,7 +234,7 @@ public function testExtractConnection(int $count)
         try {
             $futures = [];
             for ($i = 0; $i < $count; ++$i) {
-                $futures[] = launch(fn() => $pool->extractConnection());
+                $futures[] = async(fn() => $pool->extractConnection());
             }
             $results = Future\all($futures);
             foreach ($results as $result) {
@@ -261,7 +261,7 @@ public function testConnectionClosedInPool(int $count)
             $processor->expects($this->atLeastOnce())
                 ->method('query')
                 ->with($query)
-                ->willReturn(launch(function () use ($result): Result {
+                ->willReturn(async(function () use ($result): Result {
                     delay(0.01);
                     return $result;
                 }));
@@ -273,7 +273,7 @@ public function testConnectionClosedInPool(int $count)
         $processor->expects($this->once())
             ->method('query')
             ->with($query)
-            ->willReturn(launch(function () use ($result): Result {
+            ->willReturn(async(function () use ($result): Result {
                 delay(0.01);
                 return $result;
             }));
@@ -286,12 +286,12 @@ public function testConnectionClosedInPool(int $count)
             $this->assertSame($count + 1, $pool->getConnectionLimit());
             $futures = [];
             for ($i = 0; $i < $count + 1; ++$i) {
-                $futures[] = launch(fn() => $pool->query($query));
+                $futures[] = async(fn() => $pool->query($query));
             }
             Future\all($futures);
             $futures = [];
             for ($i = 0; $i < $count; ++$i) {
-                $futures[] = launch(fn() => $pool->query($query));
+                $futures[] = async(fn() => $pool->query($query));
             }
             Future\all($futures);
         } finally {

From 930cd49809818264d977e7365c70dbd5ea1fcfc2 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 24 Feb 2022 19:08:45 -0600
Subject: [PATCH 051/174] Update to PHP 8.1 and amphp/sql + sql-common changes

---
 composer.json                          | 19 ++++-----
 examples/3-generic-with-yield.php      |  2 +-
 examples/4-multi-rows.php              |  2 +-
 src/CancellableConnector.php           |  8 +---
 src/Connection.php                     | 45 +++++++--------------
 src/ConnectionConfig.php               |  2 +-
 src/Connector.php                      | 11 +++++
 src/Internal/CommandResult.php         |  4 +-
 src/Internal/ConnectionResult.php      | 18 +++------
 src/Internal/ConnectionStatement.php   |  2 +-
 src/Internal/ConnectionTransaction.php | 34 ++++++----------
 src/Internal/Processor.php             | 56 ++++++++++++--------------
 src/Link.php                           |  3 +-
 src/Pool.php                           | 11 ++---
 src/PooledResult.php                   |  8 ++--
 src/PooledStatement.php                |  6 +--
 src/PooledTransaction.php              |  4 +-
 src/StatementPool.php                  |  2 +-
 src/functions.php                      |  1 -
 test/PoolTest.php                      |  4 +-
 test/bootstrap.php                     |  8 +---
 21 files changed, 109 insertions(+), 141 deletions(-)
 create mode 100644 src/Connector.php

diff --git a/composer.json b/composer.json
index de8f12f..8c51196 100644
--- a/composer.json
+++ b/composer.json
@@ -13,19 +13,20 @@
         }
     ],
     "require": {
-        "php": ">=8",
-        "amphp/amp": "^3-dev",
-        "amphp/pipeline": "^1-dev",
-        "amphp/socket": "^2-dev",
-        "amphp/sql": "^2-dev",
-        "amphp/sql-common": "^2-dev"
+        "php": ">=8.1",
+        "amphp/amp": "^3",
+        "amphp/pipeline": "^1",
+        "amphp/socket": "^2",
+        "amphp/sql": "^2",
+        "amphp/sql-common": "^2",
+        "revolt/event-loop": "dev-main as 0.2.3"
     },
     "require-dev": {
         "ext-openssl": "*",
-        "amphp/process": "^2-dev",
-        "amphp/file": "^3-dev",
+        "amphp/process": "^2",
+        "amphp/file": "^3",
         "phpunit/phpunit": "^9",
-        "amphp/phpunit-util": "^2-dev",
+        "amphp/phpunit-util": "^3",
         "amphp/php-cs-fixer-config": "dev-master",
         "phpbench/phpbench": "^1.1"
     },
diff --git a/examples/3-generic-with-yield.php b/examples/3-generic-with-yield.php
index 42da744..771f512 100644
--- a/examples/3-generic-with-yield.php
+++ b/examples/3-generic-with-yield.php
@@ -24,7 +24,7 @@
 }
 
 /* wait until everything is inserted */
-Future\all($future);
+$results = Future\await($future);
 
 print "Insertion successful (if it wasn't, an exception would have been thrown by now)" . PHP_EOL;
 
diff --git a/examples/4-multi-rows.php b/examples/4-multi-rows.php
index c55a070..da1db2b 100644
--- a/examples/4-multi-rows.php
+++ b/examples/4-multi-rows.php
@@ -22,7 +22,7 @@
      * @var Mysql\Result $result1
      * @var Mysql\Result $result2
      */
-    [$result1, $result2] = Future\all($future); // Both queries execute simultaneously. Wait for both to finish here.
+    [$result1, $result2] = Future\await($future); // Both queries execute simultaneously. Wait for both to finish here.
 } catch (\Throwable $e) {
     var_dump($e);
 }
diff --git a/src/CancellableConnector.php b/src/CancellableConnector.php
index aad3b3e..78a923c 100644
--- a/src/CancellableConnector.php
+++ b/src/CancellableConnector.php
@@ -5,15 +5,11 @@
 use Amp\Cancellation;
 use Amp\Socket;
 use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
-use Amp\Sql\Connector;
 
 final class CancellableConnector implements Connector
 {
-    private Socket\Connector $connector;
-
-    public function __construct(?Socket\Connector $connector = null)
+    public function __construct(private readonly ?Socket\SocketConnector $connector = null)
     {
-        $this->connector = $connector ?? Socket\connector();
     }
 
     public function connect(SqlConnectionConfig $config, ?Cancellation $token = null): Connection
@@ -22,6 +18,6 @@ public function connect(SqlConnectionConfig $config, ?Cancellation $token = null
             throw new \TypeError(\sprintf("Must provide an instance of %s to MySQL connectors", ConnectionConfig::class));
         }
 
-        return Connection::connect($config, $token, $this->connector);
+        return Connection::connect($config, $token, $this->connector ?? Socket\socketConnector());
     }
 }
diff --git a/src/Connection.php b/src/Connection.php
index 3d37e8e..05f6bdf 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -6,6 +6,7 @@
 use Amp\DeferredFuture;
 use Amp\NullCancellation;
 use Amp\Socket;
+use Amp\Sql\TransactionIsolation;
 use Revolt\EventLoop;
 
 final class Connection implements Link
@@ -29,18 +30,18 @@ final class Connection implements Link
     /**
      * @param ConnectionConfig $config
      * @param Cancellation|null $token
-     * @param Socket\Connector|null $connector
+     * @param Socket\SocketConnector|null $connector
      *
      * @return self
      */
     public static function connect(
         ConnectionConfig $config,
         ?Cancellation $token = null,
-        ?Socket\Connector $connector = null
+        ?Socket\SocketConnector $connector = null
     ): self {
         $token = $token ?? new NullCancellation;
 
-        $socket = ($connector ?? Socket\connector())
+        $socket = ($connector ?? Socket\socketConnector())
             ->connect($config->getConnectionString(), $config->getConnectContext(), $token);
 
         $processor = new Internal\Processor($socket, $config);
@@ -91,10 +92,9 @@ public function setCharset(string $charset, string $collate = ""): void
 
     public function close(): void
     {
-        $processor = $this->processor;
         // Send close command if connection is not already in a closed or closing state
-        if ($processor->isAlive()) {
-            $processor->sendClose()->finally(static fn() => $processor->close())->ignore();
+        if ($this->processor->isAlive()) {
+            $this->processor->sendClose()->await();
         }
     }
 
@@ -120,7 +120,7 @@ public function query(string $sql): Result
         return $this->processor->query($sql)->await();
     }
 
-    public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITTED): Transaction
+    public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::COMMITTED): Transaction
     {
         while ($this->busy) {
             $this->busy->getFuture()->await();
@@ -128,33 +128,18 @@ public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITT
 
         $this->busy = $deferred = new DeferredFuture;
 
-        switch ($isolation) {
-            case Transaction::ISOLATION_UNCOMMITTED:
-                $promise = $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
-                break;
-
-            case Transaction::ISOLATION_COMMITTED:
-                $promise = $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
-                break;
-
-            case Transaction::ISOLATION_REPEATABLE:
-                $promise = $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ");
-                break;
-
-            case Transaction::ISOLATION_SERIALIZABLE:
-                $promise = $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE");
-                break;
-
-            default:
-                throw new \Error("Invalid transaction type");
-        }
-
         try {
-            $promise->await();
+            $this->processor->query(match ($isolation) {
+                TransactionIsolation::UNCOMMITTED => "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED",
+                TransactionIsolation::COMMITTED => "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
+                TransactionIsolation::REPEATABLE => "SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ",
+                TransactionIsolation::SERIALIZABLE => "SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE",
+            })->await();
+
             $this->processor->query("START TRANSACTION")->await();
         } catch (\Throwable $exception) {
             $this->busy = null;
-            $deferred->complete(null);
+            $deferred->complete();
             throw $exception;
         }
 
diff --git a/src/ConnectionConfig.php b/src/ConnectionConfig.php
index b8b9596..dd16bbb 100644
--- a/src/ConnectionConfig.php
+++ b/src/ConnectionConfig.php
@@ -126,7 +126,7 @@ public function withoutLocalInfile(): self
         return $new;
     }
 
-    public function getConnectContext()
+    public function getConnectContext(): ConnectContext
     {
         return $this->context;
     }
diff --git a/src/Connector.php b/src/Connector.php
new file mode 100644
index 0000000..4aaf725
--- /dev/null
+++ b/src/Connector.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Amp\Mysql;
+
+use Amp\Sql\ConnectionConfig;
+use Amp\Sql\Connector as SqlConnector;
+
+interface Connector extends SqlConnector
+{
+    public function connect(ConnectionConfig $config): Connection;
+}
diff --git a/src/Internal/CommandResult.php b/src/Internal/CommandResult.php
index 92c95cf..611dc38 100644
--- a/src/Internal/CommandResult.php
+++ b/src/Internal/CommandResult.php
@@ -10,11 +10,11 @@ final class CommandResult implements Result, \IteratorAggregate
 {
     private ?int $lastInsertId;
 
-    private SqlCommandResult $delegate;
+    private readonly SqlCommandResult $delegate;
 
     public function __construct(int $affectedRows, int $lastInsertId)
     {
-        $this->delegate = new SqlCommandResult($affectedRows, Future::complete(null));
+        $this->delegate = new SqlCommandResult($affectedRows, Future::complete());
         $this->lastInsertId = $lastInsertId ?: null; // Convert 0 to null
     }
 
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index a409c48..ba6e052 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -5,22 +5,23 @@
 use Amp\DeferredFuture;
 use Amp\Future;
 use Amp\Mysql\Result;
-use Amp\Pipeline\AsyncGenerator;
+use Amp\Pipeline\ConcurrentIterableIterator;
+use Amp\Pipeline\ConcurrentIterator;
 use Revolt\EventLoop;
 use function Amp\async;
 
 final class ConnectionResult implements Result, \IteratorAggregate
 {
-    private ResultProxy $result;
+    private readonly ResultProxy $result;
 
-    private AsyncGenerator $generator;
+    private readonly ConcurrentIterator $generator;
 
     private ?Future $nextResult = null;
 
     public function __construct(ResultProxy $result)
     {
         $this->result = $result;
-        $this->generator = new AsyncGenerator(static fn () => self::iterate($result));
+        $this->generator = new ConcurrentIterableIterator(self::iterate($result));
     }
 
     public function getIterator(): \Traversable
@@ -76,7 +77,7 @@ private static function fetchRow(ResultProxy $result): Future
         }
 
         if ($result->state === ResultProxy::ROWS_FETCHED) {
-            return Future::complete(null);
+            return Future::complete();
         }
 
         $deferred = new DeferredFuture;
@@ -136,13 +137,6 @@ public function getLastInsertId(): ?int
      */
     public function getFields(): ?array
     {
-        if ($this->result === null) {
-            throw new \Error(\sprintf(
-                "The current result set is empty; call this method before invoking %s::getNextResult()",
-                Result::class,
-            ));
-        }
-
         if ($this->result->state >= ResultProxy::COLUMNS_FETCHED) {
             return $this->result->columns;
         }
diff --git a/src/Internal/ConnectionStatement.php b/src/Internal/ConnectionStatement.php
index 52122d9..d99c411 100644
--- a/src/Internal/ConnectionStatement.php
+++ b/src/Internal/ConnectionStatement.php
@@ -20,7 +20,7 @@ final class ConnectionStatement implements Statement
 
     private ?Processor $processor;
 
-    private ResultProxy $result;
+    private readonly ResultProxy $result;
 
     private int $lastUsedAt;
 
diff --git a/src/Internal/ConnectionTransaction.php b/src/Internal/ConnectionTransaction.php
index 614f4c4..931796a 100644
--- a/src/Internal/ConnectionTransaction.php
+++ b/src/Internal/ConnectionTransaction.php
@@ -8,6 +8,7 @@
 use Amp\Mysql\Statement;
 use Amp\Mysql\Transaction;
 use Amp\Sql\TransactionError;
+use Amp\Sql\TransactionIsolation;
 use Revolt\EventLoop;
 
 final class ConnectionTransaction implements Transaction
@@ -16,32 +17,24 @@ final class ConnectionTransaction implements Transaction
 
     private ?Processor $processor;
 
-    private int $isolation;
+    private readonly TransactionIsolation $isolation;
 
-    /** @var callable */
-    private $release;
+    private readonly \Closure $release;
 
     private int $refCount = 1;
 
     /**
-     * @param Processor $processor
-     * @param callable $release
-     * @param int $isolation
+     * @param \Closure():void $release
      *
      * @throws \Error If the isolation level is invalid.
      */
-    public function __construct(Processor $processor, callable $release, int $isolation = Transaction::ISOLATION_COMMITTED)
-    {
-        $this->isolation = match ($isolation) {
-            Transaction::ISOLATION_UNCOMMITTED,
-            Transaction::ISOLATION_COMMITTED,
-            Transaction::ISOLATION_REPEATABLE,
-            Transaction::ISOLATION_SERIALIZABLE,
-                => $isolation,
-            default => throw new \Error("Isolation must be a valid transaction isolation level"),
-        };
-
+    public function __construct(
+        Processor $processor,
+        \Closure $release,
+        TransactionIsolation $isolation = TransactionIsolation::COMMITTED
+    ) {
         $this->processor = $processor;
+        $this->isolation = $isolation;
 
         $refCount =& $this->refCount;
         $this->release = static function () use (&$refCount, $release): void {
@@ -79,7 +72,7 @@ public function getLastUsedAt(): int
     public function close(): void
     {
         if ($this->processor) {
-            $this->commit(); // Invokes $this->release callback.
+            $this->rollback(); // Invokes $this->release callback.
         }
     }
 
@@ -99,10 +92,7 @@ public function isActive(): bool
         return $this->processor !== null;
     }
 
-    /**
-     * @return int
-     */
-    public function getIsolationLevel(): int
+    public function getIsolationLevel(): TransactionIsolation
     {
         return $this->isolation;
     }
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 048de29..4b886a2 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -61,24 +61,16 @@ class Processor implements TransientResource
     /** @var \Generator[] */
     private array $processors = [];
 
-    /** @var int */
-    private int $protocol;
-
     private int $seqId = -1;
     private int $compressionId = -1;
 
     private ?EncryptableSocket $socket;
 
-    private int $authPluginDataLen;
     private ?string $query = null;
     public array $named = [];
 
-    /** @var callable|null */
-    private $parseCallback = null;
-    /** @var callable|null */
-    private $packetCallback = null;
-
-    private ?Future $pendingWrite = null;
+    private ?\Closure $parseCallback = null;
+    private ?\Closure $packetCallback = null;
 
     public ConnectionConfig $config;
 
@@ -287,7 +279,7 @@ private function getDeferred(): DeferredFuture
         return \array_shift($this->deferreds);
     }
 
-    private function appendTask(callable $callback): void
+    private function appendTask(\Closure $callback): void
     {
         if ($this->packetCallback
             || $this->parseCallback
@@ -316,7 +308,7 @@ public function getLastUsedAt(): int
         return $this->lastUsedAt;
     }
 
-    protected function startCommand(callable $callback): Future
+    protected function startCommand(\Closure $callback): Future
     {
         if ($this->connectionState > self::READY) {
             throw new \Error("The connection has been closed");
@@ -361,7 +353,7 @@ public function query(string $query): Future
     {
         return $this->startCommand(function () use ($query): void {
             $this->query = $query;
-            $this->parseCallback = [$this, "handleQuery"];
+            $this->parseCallback = $this->handleQuery(...);
             $this->sendPacket("\x03$query");
         });
     }
@@ -371,7 +363,7 @@ public function prepare(string $query): Future
     {
         return $this->startCommand(function () use ($query): void {
             $this->query = $query;
-            $this->parseCallback = [$this, "handlePrepare"];
+            $this->parseCallback = $this->handlePrepare(...);
 
             $query = \preg_replace_callback(self::STATEMENT_PARAM_REGEX, function ($m): string {
                 static $index = 0;
@@ -489,7 +481,7 @@ public function execute(int $stmtId, string $query, array &$params, array $prebo
             $this->addDeferred($deferred);
             $this->sendPacket($payload);
             // apparently LOAD DATA LOCAL INFILE requests are not supported via prepared statements
-            $this->packetCallback = [$this, "handleExecute"];
+            $this->packetCallback = $this->handleExecute(...);
         });
         return $deferred->getFuture(); // do not use $this->startCommand(), that might unexpectedly reset the seqId!
     }
@@ -513,7 +505,7 @@ public function listFields(string $table, string $like = "%"): Future
     {
         return $this->startCommand(function () use ($table, $like): void {
             $this->sendPacket("\x04$table\0$like");
-            $this->parseCallback = [$this, "handleFieldlist"];
+            $this->parseCallback = $this->handleFieldList(...);
         });
     }
 
@@ -573,7 +565,7 @@ public function statistics(): Future
     {
         return $this->startCommand(function (): void {
             $this->sendPacket("\x09");
-            $this->parseCallback = [$this, "readStatistics"];
+            $this->parseCallback = $this->readStatistics(...);
         });
     }
 
@@ -778,8 +770,8 @@ private function handleHandshake(string $packet): void
     {
         $off = 1;
 
-        $this->protocol = \ord($packet);
-        if ($this->protocol !== 0x0a) {
+        $protocol = \ord($packet);
+        if ($protocol !== 0x0a) {
             throw new ConnectionException("Unsupported protocol version ".\ord($packet)." (Expected: 10)");
         }
 
@@ -807,13 +799,13 @@ private function handleHandshake(string $packet): void
             $this->serverCapabilities += DataTypes::decodeUnsigned16(\substr($packet, $off)) << 16;
             $off += 2;
 
-            $this->authPluginDataLen = $this->serverCapabilities & self::CLIENT_PLUGIN_AUTH ? \ord(\substr($packet, $off)) : 0;
+            $authPluginDataLen = $this->serverCapabilities & self::CLIENT_PLUGIN_AUTH ? \ord(\substr($packet, $off)) : 0;
             $off += 1;
 
             if ($this->serverCapabilities & self::CLIENT_SECURE_CONNECTION) {
                 $off += 10;
 
-                $strlen = \max(13, $this->authPluginDataLen - 8);
+                $strlen = \max(13, $authPluginDataLen - 8);
                 $this->authPluginData .= \substr($packet, $off, $strlen);
                 $off += $strlen;
 
@@ -902,7 +894,7 @@ private function handleQuery(string $packet): void
                 return;
         }
 
-        $this->parseCallback = [$this, "handleTextColumnDefinition"];
+        $this->parseCallback = $this->handleTextColumnDefinition(...);
         $this->getDeferred()->complete(new ConnectionResult($result = new ResultProxy));
         /* we need to resolve before assigning vars, so that a onResolve() handler won't have a partial result available */
         $this->result = $result;
@@ -912,7 +904,7 @@ private function handleQuery(string $packet): void
     /** @see 14.7.1 Binary Protocol Resultset */
     private function handleExecute(string $packet): void
     {
-        $this->parseCallback = [$this, "handleBinaryColumnDefinition"];
+        $this->parseCallback = $this->handleBinaryColumnDefinition(...);
         $this->getDeferred()->complete(new ConnectionResult($result = new ResultProxy));
         /* we need to resolve before assigning vars, so that a onResolve() handler won't have a partial result available */
         $this->result = $result;
@@ -953,7 +945,7 @@ private function handleColumnDefinition(string $packet, string $cbMethod): void
                 $this->parseCallback = null;
                 $this->handleError($packet);
             } else {
-                $cb = $this->parseCallback = [$this, $cbMethod];
+                $cb = $this->parseCallback = $this->{$cbMethod}(...);
                 if ($this->capabilities & self::CLIENT_DEPRECATE_EOF) {
                     $cb($packet);
                 } else {
@@ -974,7 +966,7 @@ private function prepareParams(string $packet): void
             if (!$this->result->columnsToFetch) {
                 $this->prepareFields($packet);
             } else {
-                $this->parseCallback = [$this, "prepareFields"];
+                $this->parseCallback = $this->prepareFields(...);
             }
             return;
         }
@@ -1069,13 +1061,13 @@ private function successfulResultsetFetch(): void
             $deferred = new DeferredFuture;
         }
         if ($this->connInfo->statusFlags & StatusFlags::SERVER_MORE_RESULTS_EXISTS) {
-            $this->parseCallback = [$this, "handleQuery"];
+            $this->parseCallback = $this->handleQuery(...);
             $this->addDeferred($deferred);
         } else {
             $this->parseCallback = null;
             $this->query = null;
             $this->result = null;
-            $deferred->complete(null);
+            $deferred->complete();
             $this->ready();
         }
         $result->updateState(ResultProxy::ROWS_FETCHED);
@@ -1184,7 +1176,7 @@ private function handlePrepare(string $packet): void
         $this->getDeferred()->complete(new ConnectionStatement($this, $this->query, $stmtId, $this->named, $this->result));
         $this->named = [];
         if ($params) {
-            $this->parseCallback = [$this, "prepareParams"];
+            $this->parseCallback = $this->prepareParams(...);
         } else {
             $this->prepareParams($packet);
         }
@@ -1203,13 +1195,13 @@ public function sendClose(): Future
         return $this->startCommand(function () {
             $this->sendPacket("\x01");
             $this->connectionState = self::CLOSING;
-        });
+        })->finally(fn () => $this->close());
     }
 
     public function close(): void
     {
         if ($this->connectionState === self::CLOSING && $this->deferreds) {
-            \array_pop($this->deferreds)->complete(null);
+            \array_pop($this->deferreds)->complete();
         }
 
         $this->connectionState = self::CLOSED;
@@ -1218,6 +1210,8 @@ public function close(): void
             $this->socket->close();
             $this->socket = null;
         }
+
+        $this->processors = [];
     }
 
     private function write(string $packet): void
@@ -1245,7 +1239,7 @@ private function write(string $packet): void
             $packet = $this->compressPacket($packet);
         }
 
-        $this->socket->write($packet)->await();
+        $this->socket->write($packet);
     }
 
     private function resetIds(): void
diff --git a/src/Link.php b/src/Link.php
index 02d0d33..705697e 100644
--- a/src/Link.php
+++ b/src/Link.php
@@ -3,6 +3,7 @@
 namespace Amp\Mysql;
 
 use Amp\Sql\Link as SqlLink;
+use Amp\Sql\TransactionIsolation;
 
 interface Link extends Executor, SqlLink
 {
@@ -11,5 +12,5 @@ interface Link extends Executor, SqlLink
      *
      * @return Transaction Transaction object specific to this library.
      */
-    public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITTED): Transaction;
+    public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::COMMITTED): Transaction;
 }
diff --git a/src/Pool.php b/src/Pool.php
index 5280185..17670ad 100644
--- a/src/Pool.php
+++ b/src/Pool.php
@@ -8,6 +8,7 @@
 use Amp\Sql\Result as SqlResult;
 use Amp\Sql\Statement as SqlStatement;
 use Amp\Sql\Transaction as SqlTransaction;
+use Amp\Sql\TransactionIsolation;
 
 final class Pool extends ConnectionPool implements Link
 {
@@ -16,24 +17,24 @@ protected function createDefaultConnector(): Connector
         return connector();
     }
 
-    protected function createResult(SqlResult $result, callable $release): Result
+    protected function createResult(SqlResult $result, \Closure $release): Result
     {
         \assert($result instanceof Result);
         return new PooledResult($result, $release);
     }
 
-    protected function createStatement(SqlStatement $statement, callable $release): Statement
+    protected function createStatement(SqlStatement $statement, \Closure $release): Statement
     {
         \assert($statement instanceof Statement);
         return new PooledStatement($statement, $release);
     }
 
-    protected function createStatementPool(SqlPool $pool, string $sql, callable $prepare): StatementPool
+    protected function createStatementPool(SqlPool $pool, string $sql, \Closure $prepare): StatementPool
     {
         return new StatementPool($pool, $sql, $prepare);
     }
 
-    protected function createTransaction(SqlTransaction $transaction, callable $release): Transaction
+    protected function createTransaction(SqlTransaction $transaction, \Closure $release): Transaction
     {
         return new PooledTransaction($transaction, $release);
     }
@@ -73,7 +74,7 @@ public function execute(string $sql, array $params = []): Result
      *
      * @inheritDoc
      */
-    public function beginTransaction(int $isolation = Transaction::ISOLATION_COMMITTED): Transaction
+    public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::COMMITTED): Transaction
     {
         return parent::beginTransaction($isolation);
     }
diff --git a/src/PooledResult.php b/src/PooledResult.php
index e7dedb6..bcec625 100644
--- a/src/PooledResult.php
+++ b/src/PooledResult.php
@@ -7,19 +7,19 @@
 
 final class PooledResult extends SqlPooledResult implements Result
 {
-    private Result $result;
+    private readonly Result $result;
 
     /**
      * @param Result $result
-     * @param callable  $release
+     * @param \Closure():void $release
      */
-    public function __construct(Result $result, callable $release)
+    public function __construct(Result $result, \Closure $release)
     {
         parent::__construct($result, $release);
         $this->result = $result;
     }
 
-    protected function newInstanceFrom(SqlResult $result, callable $release): self
+    protected function newInstanceFrom(SqlResult $result, \Closure $release): self
     {
         \assert($result instanceof Result);
         return new self($result, $release);
diff --git a/src/PooledStatement.php b/src/PooledStatement.php
index eb27e24..1d890ed 100644
--- a/src/PooledStatement.php
+++ b/src/PooledStatement.php
@@ -7,15 +7,15 @@
 
 final class PooledStatement extends SqlPooledStatement implements Statement
 {
-    private Statement $statement;
+    private readonly Statement $statement;
 
-    public function __construct(Statement $statement, callable $release)
+    public function __construct(Statement $statement, \Closure $release)
     {
         parent::__construct($statement, $release);
         $this->statement = $statement;
     }
 
-    protected function createResult(SqlResult $result, callable $release): Result
+    protected function createResult(SqlResult $result, \Closure $release): Result
     {
         \assert($result instanceof Result);
         return new PooledResult($result, $release);
diff --git a/src/PooledTransaction.php b/src/PooledTransaction.php
index e6789fa..7a0e787 100644
--- a/src/PooledTransaction.php
+++ b/src/PooledTransaction.php
@@ -8,13 +8,13 @@
 
 final class PooledTransaction extends SqlPooledTransaction implements Transaction
 {
-    protected function createStatement(SqlStatement $statement, callable $release): Statement
+    protected function createStatement(SqlStatement $statement, \Closure $release): Statement
     {
         \assert($statement instanceof Statement);
         return new PooledStatement($statement, $release);
     }
 
-    protected function createResult(SqlResult $result, callable $release): Result
+    protected function createResult(SqlResult $result, \Closure $release): Result
     {
         \assert($result instanceof Result);
         return new PooledResult($result, $release);
diff --git a/src/StatementPool.php b/src/StatementPool.php
index 91c0261..1011b80 100644
--- a/src/StatementPool.php
+++ b/src/StatementPool.php
@@ -23,7 +23,7 @@ protected function prepare(SqlStatement $statement): Statement
         return $statement;
     }
 
-    protected function createResult(SqlResult $result, callable $release): Result
+    protected function createResult(SqlResult $result, \Closure $release): Result
     {
         if (!$result instanceof Result) {
             throw new \TypeError('Result object must be an instance of ' . Result::class);
diff --git a/src/functions.php b/src/functions.php
index b068ab6..43ab07e 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -4,7 +4,6 @@
 
 use Amp\Sql\Common\ConnectionPool;
 use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
-use Amp\Sql\Connector;
 use Revolt\EventLoop;
 
 function connector(?Connector $connector = null): Connector
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 503d6b6..8d37877 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -166,7 +166,7 @@ public function testMultipleTransactions(int $count)
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
         try {
-            $return = $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED);
+            $return = $pool->beginTransaction();
             $this->assertInstanceOf(SqlTransaction::class, $return);
             $return->rollback();
         } finally {
@@ -198,7 +198,7 @@ public function testConsecutiveTransactions(int $count)
 
         $futures = [];
         for ($i = 0; $i < $count; ++$i) {
-            $futures[] = async(fn() => $pool->beginTransaction(SqlTransaction::ISOLATION_COMMITTED));
+            $futures[] = async(fn() => $pool->beginTransaction());
         }
 
         try {
diff --git a/test/bootstrap.php b/test/bootstrap.php
index 6416264..a6221b7 100644
--- a/test/bootstrap.php
+++ b/test/bootstrap.php
@@ -51,9 +51,7 @@
 
 $dir = __DIR__;
 
-$process = new Process("mysqld --defaults-file={$dir}/my.cnf --initialize-insecure", __DIR__);
-
-$process->start();
+$process = Process::start("mysqld --defaults-file={$dir}/my.cnf --initialize-insecure", __DIR__);
 
 $stderr = ByteStream\buffer($process->getStderr());
 
@@ -64,9 +62,7 @@
 
 $process->join();
 
-$process = new Process("mysqld --defaults-file={$dir}/my.cnf --user=root", __DIR__);
-
-$process->start();
+$process = Process::start("mysqld --defaults-file={$dir}/my.cnf --user=root", __DIR__);
 
 print "\rStarting mysqld...           ";
 

From 61fe1f3230da58d64007af90fa9e2f440d48acbd Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 24 Feb 2022 19:21:59 -0600
Subject: [PATCH 052/174] Better line clearing in test runner

---
 test/bootstrap.php | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/test/bootstrap.php b/test/bootstrap.php
index a6221b7..d9dc715 100644
--- a/test/bootstrap.php
+++ b/test/bootstrap.php
@@ -13,7 +13,9 @@
 const DB_PASS = '';
 const DB_NAME = 'test';
 
-print "\rCleaning up test directory...";
+const CLEAR = "\r\033[K";
+
+echo CLEAR, "Cleaning up test directory...";
 
 (function (): void {
     /* cleanup in case it wasn't terminated properly... */
@@ -47,7 +49,7 @@
     }
 })();
 
-print "\rCreating mysql server...     ";
+echo CLEAR, "Creating mysql server...";
 
 $dir = __DIR__;
 
@@ -64,11 +66,11 @@
 
 $process = Process::start("mysqld --defaults-file={$dir}/my.cnf --user=root", __DIR__);
 
-print "\rStarting mysqld...           ";
+echo CLEAR, "Starting mysqld...";
 
 delay(2); // Give mysqld time to start.
 
-print "\rCreating test database...    ";
+echo CLEAR, "Creating test database...";
 
 $db = new \mysqli(DB_HOST, DB_USER, DB_PASS);
 $db->query("CREATE DATABASE test");
@@ -76,6 +78,6 @@
 $db->query("INSERT INTO test.main (a, b) VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)");
 $db->close();
 
-print "\r";
+echo CLEAR;
 
 \register_shutdown_function(fn() => $process->signal(\SIGTERM));

From cdf56c650521c74a08c785f94fdaa1ff19af52e6 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 27 Feb 2022 01:00:19 -0600
Subject: [PATCH 053/174] Update for TransactionIsolation enum cases

---
 src/Connection.php                     | 12 ++++++------
 src/Internal/ConnectionTransaction.php |  2 +-
 src/Link.php                           |  2 +-
 src/Pool.php                           |  2 +-
 4 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/src/Connection.php b/src/Connection.php
index 05f6bdf..9c7707e 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -120,7 +120,7 @@ public function query(string $sql): Result
         return $this->processor->query($sql)->await();
     }
 
-    public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::COMMITTED): Transaction
+    public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::Committed): Transaction
     {
         while ($this->busy) {
             $this->busy->getFuture()->await();
@@ -129,11 +129,11 @@ public function beginTransaction(TransactionIsolation $isolation = TransactionIs
         $this->busy = $deferred = new DeferredFuture;
 
         try {
-            $this->processor->query(match ($isolation) {
-                TransactionIsolation::UNCOMMITTED => "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED",
-                TransactionIsolation::COMMITTED => "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED",
-                TransactionIsolation::REPEATABLE => "SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ",
-                TransactionIsolation::SERIALIZABLE => "SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE",
+            $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL " . match ($isolation) {
+                TransactionIsolation::Uncommitted => "READ UNCOMMITTED",
+                TransactionIsolation::Committed => "READ COMMITTED",
+                TransactionIsolation::Repeatable => "REPEATABLE READ",
+                TransactionIsolation::Serializable => "SERIALIZABLE",
             })->await();
 
             $this->processor->query("START TRANSACTION")->await();
diff --git a/src/Internal/ConnectionTransaction.php b/src/Internal/ConnectionTransaction.php
index 931796a..9b792b0 100644
--- a/src/Internal/ConnectionTransaction.php
+++ b/src/Internal/ConnectionTransaction.php
@@ -31,7 +31,7 @@ final class ConnectionTransaction implements Transaction
     public function __construct(
         Processor $processor,
         \Closure $release,
-        TransactionIsolation $isolation = TransactionIsolation::COMMITTED
+        TransactionIsolation $isolation
     ) {
         $this->processor = $processor;
         $this->isolation = $isolation;
diff --git a/src/Link.php b/src/Link.php
index 705697e..6c73070 100644
--- a/src/Link.php
+++ b/src/Link.php
@@ -12,5 +12,5 @@ interface Link extends Executor, SqlLink
      *
      * @return Transaction Transaction object specific to this library.
      */
-    public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::COMMITTED): Transaction;
+    public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::Committed): Transaction;
 }
diff --git a/src/Pool.php b/src/Pool.php
index 17670ad..f52379f 100644
--- a/src/Pool.php
+++ b/src/Pool.php
@@ -74,7 +74,7 @@ public function execute(string $sql, array $params = []): Result
      *
      * @inheritDoc
      */
-    public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::COMMITTED): Transaction
+    public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::Committed): Transaction
     {
         return parent::beginTransaction($isolation);
     }

From 6e583f06ace12b6c7d312759e434d795e33bdeaa Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 27 Feb 2022 09:44:14 -0600
Subject: [PATCH 054/174] Add Psalm

---
 composer.json     |  3 ++-
 psalm.xml         | 15 +++++++++++++++
 src/DataTypes.php |  9 ++++++---
 3 files changed, 23 insertions(+), 4 deletions(-)
 create mode 100644 psalm.xml

diff --git a/composer.json b/composer.json
index 8c51196..4d24b59 100644
--- a/composer.json
+++ b/composer.json
@@ -28,7 +28,8 @@
         "phpunit/phpunit": "^9",
         "amphp/phpunit-util": "^3",
         "amphp/php-cs-fixer-config": "dev-master",
-        "phpbench/phpbench": "^1.1"
+        "phpbench/phpbench": "^1.1",
+        "psalm/phar": "^4.22"
     },
     "minimum-stability": "dev",
     "prefer-stable": true,
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..7c0333d
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<psalm
+    errorLevel="7"
+    resolveFromConfigFile="true"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns="https://getpsalm.org/schema/config"
+    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
+>
+    <projectFiles>
+        <directory name="src" />
+        <ignoreFiles>
+            <directory name="vendor" />
+        </ignoreFiles>
+    </projectFiles>
+</psalm>
diff --git a/src/DataTypes.php b/src/DataTypes.php
index a698ea5..195a22d 100644
--- a/src/DataTypes.php
+++ b/src/DataTypes.php
@@ -375,29 +375,32 @@ public static function decodeInt32($str): int
         return \unpack("V", $str)[1];
     }
 
-    public static function decodeUnsigned32(string $str) /* : int|string */
+    public static function decodeUnsigned32(string $str): int|string
     {
         if (\PHP_INT_SIZE > 4) {
             return \unpack("V", $str)[1];
         }
 
         \assert(\extension_loaded("gmp"), "The GMP extension is required for UNSIGNED INT fields on 32-bit systems");
+        /** @psalm-suppress UndefinedConstant */
         return \gmp_strval(\gmp_import(\substr($str, 0, 4), 1, \GMP_LSW_FIRST));
     }
 
-    public static function decodeInt64(string $str) /* : int|string */
+    public static function decodeInt64(string $str): int|string
     {
         if (\PHP_INT_SIZE > 4) {
             return \unpack("P", $str)[1];
         }
 
         \assert(\extension_loaded("gmp"), "The GMP extension is required for BIGINT fields on 32-bit systems");
+        /** @psalm-suppress UndefinedConstant */
         return \gmp_strval(\gmp_import(\substr($str, 0, 8), 1, \GMP_LSW_FIRST));
     }
 
-    public static function decodeUnsigned64(string $str) /* : int|string */
+    public static function decodeUnsigned64(string $str): int|string
     {
         \assert(\extension_loaded("gmp"), "The GMP extension is required for UNSIGNED BIGINT fields");
+        /** @psalm-suppress UndefinedConstant */
         return \gmp_strval(\gmp_import(\substr($str, 0, 8), 1, \GMP_LSW_FIRST));
     }
 

From 55b3b34becdd3e9fb78b592c8e0cae52958686b4 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 27 Feb 2022 09:49:20 -0600
Subject: [PATCH 055/174] Update CS fixer and use new rules

---
 .php-cs-fixer.dist.php                 | 10 ++++++++++
 composer.json                          |  2 +-
 src/Connection.php                     | 13 -------------
 src/Executor.php                       |  6 ------
 src/Internal/ConnectionResult.php      |  7 -------
 src/Internal/ConnectionStatement.php   |  3 ---
 src/Internal/ConnectionTransaction.php | 16 +--------------
 src/Internal/Processor.php             |  8 +++-----
 src/Internal/ResultProxy.php           |  2 --
 src/Link.php                           |  2 --
 src/Pool.php                           |  8 --------
 src/PooledResult.php                   |  1 -
 src/PooledStatement.php                |  2 --
 src/PooledTransaction.php              |  6 ------
 src/Result.php                         |  2 --
 src/functions.php                      | 13 ++-----------
 test/LinkTest.php                      |  4 +---
 test/PoolTest.php                      | 27 +++++---------------------
 test/bootstrap.php                     |  2 +-
 19 files changed, 24 insertions(+), 110 deletions(-)
 create mode 100644 .php-cs-fixer.dist.php

diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..d146dba
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,10 @@
+<?php
+
+$config = new Amp\CodeStyle\Config;
+$config->getFinder()
+    ->in(__DIR__ . '/src')
+    ->in(__DIR__ . '/test');
+
+$config->setCacheFile(__DIR__ . '/.php_cs.cache');
+
+return $config;
diff --git a/composer.json b/composer.json
index 4d24b59..a9f680b 100644
--- a/composer.json
+++ b/composer.json
@@ -27,7 +27,7 @@
         "amphp/file": "^3",
         "phpunit/phpunit": "^9",
         "amphp/phpunit-util": "^3",
-        "amphp/php-cs-fixer-config": "dev-master",
+        "amphp/php-cs-fixer-config": "^2-dev",
         "phpbench/phpbench": "^1.1",
         "psalm/phar": "^4.22"
     },
diff --git a/src/Connection.php b/src/Connection.php
index 9c7707e..57d06b0 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -27,13 +27,6 @@ final class Connection implements Link
     /** @var \Closure Function used to release connection after a transaction has completed. */
     private \Closure $release;
 
-    /**
-     * @param ConnectionConfig $config
-     * @param Cancellation|null $token
-     * @param Socket\SocketConnector|null $connector
-     *
-     * @return self
-     */
     public static function connect(
         ConnectionConfig $config,
         ?Cancellation $token = null,
@@ -49,9 +42,6 @@ public static function connect(
         return new self($processor);
     }
 
-    /**
-     * @param Internal\Processor $processor
-     */
     private function __construct(Internal\Processor $processor)
     {
         $this->processor = $processor;
@@ -160,9 +150,6 @@ public function prepare(string $sql): Statement
         return $this->processor->prepare($sql)->await();
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function execute(string $sql, array $params = []): Result
     {
         $statement = $this->prepare($sql);
diff --git a/src/Executor.php b/src/Executor.php
index eb3881e..dc3479f 100644
--- a/src/Executor.php
+++ b/src/Executor.php
@@ -7,22 +7,16 @@
 interface Executor extends SqlExecutor
 {
     /**
-     * @inheritDoc
-     *
      * @return Result Result object specific to this library.
      */
     public function query(string $sql): Result;
 
     /**
-     * @inheritDoc
-     *
      * @return Result Result object specific to this library.
      */
     public function prepare(string $sql): Statement;
 
     /**
-     * @inheritDoc
-     *
      * @return Result Result object specific to this library.
      */
     public function execute(string $sql, array $params = []): Result;
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index ba6e052..ef997c1 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -64,7 +64,6 @@ private static function iterate(ResultProxy $result): \Generator
     }
 
     /**
-     * @param ResultProxy $result
      * @return Future<array|null>
      */
     private static function fetchRow(ResultProxy $result): Future
@@ -94,9 +93,6 @@ private static function fetchRow(ResultProxy $result): Future
         return $deferred->getFuture();
     }
 
-    /**
-     * @inheritDoc
-     */
     public function getNextResult(): ?Result
     {
         if ($this->nextResult) {
@@ -132,9 +128,6 @@ public function getLastInsertId(): ?int
         return $this->result->insertId;
     }
 
-    /**
-     * @inheritDoc
-     */
     public function getFields(): ?array
     {
         if ($this->result->state >= ResultProxy::COLUMNS_FETCHED) {
diff --git a/src/Internal/ConnectionStatement.php b/src/Internal/ConnectionStatement.php
index d99c411..8e10250 100644
--- a/src/Internal/ConnectionStatement.php
+++ b/src/Internal/ConnectionStatement.php
@@ -65,7 +65,6 @@ public function isAlive(): bool
         return $this->processor->isAlive();
     }
 
-    /** {@inheritdoc} */
     public function bind(int|string $paramId, mixed $data): void
     {
         if (\is_int($paramId)) {
@@ -103,7 +102,6 @@ public function bind(int|string $paramId, mixed $data): void
         }
     }
 
-    /** {@inheritdoc} */
     public function execute(array $params = []): Result
     {
         $this->lastUsedAt = \time();
@@ -161,7 +159,6 @@ public function getFields(): ?array
         return $deferred->getFuture()->await();
     }
 
-    /** {@inheritdoc} */
     public function getLastUsedAt(): int
     {
         return $this->lastUsedAt;
diff --git a/src/Internal/ConnectionTransaction.php b/src/Internal/ConnectionTransaction.php
index 9b792b0..1e45da4 100644
--- a/src/Internal/ConnectionTransaction.php
+++ b/src/Internal/ConnectionTransaction.php
@@ -56,18 +56,13 @@ public function __destruct()
         }
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function getLastUsedAt(): int
     {
         return $this->processor->getLastUsedAt();
     }
 
     /**
-     * {@inheritdoc}
-     *
-     * Closes and commits all changes in the transaction.
+     * Closes and rolls back all changes in the transaction.
      */
     public function close(): void
     {
@@ -76,9 +71,6 @@ public function close(): void
         }
     }
 
-    /**
-     * {@inheritdoc}
-     */
     public function isAlive(): bool
     {
         return $this->processor && $this->processor->isAlive();
@@ -98,8 +90,6 @@ public function getIsolationLevel(): TransactionIsolation
     }
 
     /**
-     * {@inheritdoc}
-     *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
     public function query(string $sql): Result
@@ -114,8 +104,6 @@ public function query(string $sql): Result
     }
 
     /**
-     * {@inheritdoc}
-     *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
     public function prepare(string $sql): Statement
@@ -129,8 +117,6 @@ public function prepare(string $sql): Statement
     }
 
     /**
-     * {@inheritdoc}
-     *
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
     public function execute(string $sql, array $params = []): Result
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 4b886a2..d6edfae 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -82,7 +82,6 @@ class Processor implements TransientResource
 
     private ?ResultProxy $result = null;
 
-    /** @var int */
     private int $lastUsedAt;
 
     private int $connectionId;
@@ -193,12 +192,12 @@ public function connect(Cancellation $token): void
 
         $this->processors = [$this->parseMysql()];
 
-        $id = $token->subscribe(fn() => $this->close());
+        $id = $token->subscribe(fn () => $this->close());
 
-        EventLoop::queue(fn() => $this->read());
+        EventLoop::queue(fn () => $this->read());
 
         $future = $deferred->getFuture();
-        $future = $future->finally(static fn() => $token->unsubscribe($id));
+        $future = $future->finally(static fn () => $token->unsubscribe($id));
 
         if ($this->config->getCharset() !== ConnectionConfig::DEFAULT_CHARSET || $this->config->getCollation() !== ConnectionConfig::DEFAULT_COLLATE) {
             $future = $future->map(function () {
@@ -338,7 +337,6 @@ public function setCharset(string $charset, string $collate = ""): Future
         return $future;
     }
 
-
     /** @see 14.6.3 COM_INIT_DB */
     public function useDb(string $db): Future
     {
diff --git a/src/Internal/ResultProxy.php b/src/Internal/ResultProxy.php
index 6d6a1b2..03d1079 100644
--- a/src/Internal/ResultProxy.php
+++ b/src/Internal/ResultProxy.php
@@ -80,8 +80,6 @@ public function error(\Throwable $e): void
     }
 
     /**
-     * @return array
-     *
      * @codeCoverageIgnore
      */
     public function __debugInfo(): array
diff --git a/src/Link.php b/src/Link.php
index 6c73070..5b8dbb3 100644
--- a/src/Link.php
+++ b/src/Link.php
@@ -8,8 +8,6 @@
 interface Link extends Executor, SqlLink
 {
     /**
-     * @inheritDoc
-     *
      * @return Transaction Transaction object specific to this library.
      */
     public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::Committed): Transaction;
diff --git a/src/Pool.php b/src/Pool.php
index f52379f..bb2ca0e 100644
--- a/src/Pool.php
+++ b/src/Pool.php
@@ -41,8 +41,6 @@ protected function createTransaction(SqlTransaction $transaction, \Closure $rele
 
     /**
      * Changes return type to this library's Result type.
-     *
-     * @inheritDoc
      */
     public function query(string $sql): Result
     {
@@ -51,8 +49,6 @@ public function query(string $sql): Result
 
     /**
      * Changes return type to this library's Statement type.
-     *
-     * @inheritDoc
      */
     public function prepare(string $sql): Statement
     {
@@ -61,8 +57,6 @@ public function prepare(string $sql): Statement
 
     /**
      * Changes return type to this library's Result type.
-     *
-     * @inheritDoc
      */
     public function execute(string $sql, array $params = []): Result
     {
@@ -71,8 +65,6 @@ public function execute(string $sql, array $params = []): Result
 
     /**
      * Changes return type to this library's Transaction type.
-     *
-     * @inheritDoc
      */
     public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::Committed): Transaction
     {
diff --git a/src/PooledResult.php b/src/PooledResult.php
index bcec625..74d9298 100644
--- a/src/PooledResult.php
+++ b/src/PooledResult.php
@@ -10,7 +10,6 @@ final class PooledResult extends SqlPooledResult implements Result
     private readonly Result $result;
 
     /**
-     * @param Result $result
      * @param \Closure():void $release
      */
     public function __construct(Result $result, \Closure $release)
diff --git a/src/PooledStatement.php b/src/PooledStatement.php
index 1d890ed..42e6121 100644
--- a/src/PooledStatement.php
+++ b/src/PooledStatement.php
@@ -23,8 +23,6 @@ protected function createResult(SqlResult $result, \Closure $release): Result
 
     /**
      * Changes return type to this library's Result type.
-     *
-     * @inheritDoc
      */
     public function execute(array $params = []): Result
     {
diff --git a/src/PooledTransaction.php b/src/PooledTransaction.php
index 7a0e787..7ebaefc 100644
--- a/src/PooledTransaction.php
+++ b/src/PooledTransaction.php
@@ -22,8 +22,6 @@ protected function createResult(SqlResult $result, \Closure $release): Result
 
     /**
      * Changes return type to this library's Result type.
-     *
-     * @inheritDoc
      */
     public function query(string $sql): Result
     {
@@ -32,8 +30,6 @@ public function query(string $sql): Result
 
     /**
      * Changes return type to this library's Statement type.
-     *
-     * @inheritDoc
      */
     public function prepare(string $sql): Statement
     {
@@ -42,8 +38,6 @@ public function prepare(string $sql): Statement
 
     /**
      * Changes return type to this library's Result type.
-     *
-     * @inheritDoc
      */
     public function execute(string $sql, array $params = []): Result
     {
diff --git a/src/Result.php b/src/Result.php
index 619c441..09428ae 100644
--- a/src/Result.php
+++ b/src/Result.php
@@ -13,8 +13,6 @@ interface Result extends SqlResult
     public function getLastInsertId(): ?int;
 
     /**
-     * @return array|null
-     *
      * @psalm-return array<int, array<int, string>>|null
      */
     public function getFields(): ?array;
diff --git a/src/functions.php b/src/functions.php
index 43ab07e..1bf2f65 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -4,6 +4,7 @@
 
 use Amp\Sql\Common\ConnectionPool;
 use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
+use Amp\Sql\FailureException;
 use Revolt\EventLoop;
 
 function connector(?Connector $connector = null): Connector
@@ -22,11 +23,7 @@ function connector(?Connector $connector = null): Connector
 /**
  * Create a connection using the global Connector instance.
  *
- * @param SqlConnectionConfig $config
- *
- * @return Connection
- *
- * @throws \Amp\Sql\FailureException If connecting fails.
+ * @throws FailureException If connecting fails.
  * @throws \Error If the connection string does not contain a host, user, and password.
  */
 function connect(SqlConnectionConfig $config): Connection
@@ -37,12 +34,6 @@ function connect(SqlConnectionConfig $config): Connection
 /**
  * Create a pool using the global Connector instance.
  *
- * @param SqlConnectionConfig $config
- * @param int $maxConnections
- * @param int $idleTimeout
- *
- * @return Pool
- *
  * @throws \Error If the connection string does not contain a host, user, and password.
  */
 function pool(
diff --git a/test/LinkTest.php b/test/LinkTest.php
index b648bee..1ebe27d 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -3,17 +3,15 @@
 namespace Amp\Mysql\Test;
 
 use Amp\Mysql\DataTypes;
+use Amp\Mysql\Link;
 use Amp\Mysql\Result;
 use Amp\PHPUnit\AsyncTestCase;
-use Amp\Mysql\Link;
 use Amp\Sql\QueryError;
 
 abstract class LinkTest extends AsyncTestCase
 {
     /**
      * Returns the Link class to be tested.
-     *
-     * @return Link
      */
     abstract protected function getLink(string $connectionString): Link;
 
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 8d37877..a99f75c 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -43,8 +43,6 @@ protected function createPool(array $connections): Pool
     }
 
     /**
-     * @param int $count
-     *
      * @return array<int, Processor&MockObject>
      */
     private function makeProcessorSet(int $count): array
@@ -68,9 +66,6 @@ private function makeConnectionSet(array $processors): array
         })->bindTo(null, Connection::class), $processors);
     }
 
-    /**
-     * @return array
-     */
     public function getConnectionCounts(): array
     {
         return \array_map(function (int $count): array { return [$count]; }, \range(2, 10, 2));
@@ -78,8 +73,6 @@ public function getConnectionCounts(): array
 
     /**
      * @dataProvider getConnectionCounts
-     *
-     * @param int $count
      */
     public function testSingleQuery(int $count)
     {
@@ -106,8 +99,6 @@ public function testSingleQuery(int $count)
 
     /**
      * @dataProvider getConnectionCounts
-     *
-     * @param int $count
      */
     public function testConsecutiveQueries(int $count)
     {
@@ -131,7 +122,7 @@ public function testConsecutiveQueries(int $count)
             $futures = [];
 
             for ($i = 0; $i < $count; ++$i) {
-                $futures[] = async(fn() => $pool->query('SQL Query'));
+                $futures[] = async(fn () => $pool->query('SQL Query'));
             }
 
             $results = Future\all($futures);
@@ -146,8 +137,6 @@ public function testConsecutiveQueries(int $count)
 
     /**
      * @dataProvider getConnectionCounts
-     *
-     * @param int $count
      */
     public function testMultipleTransactions(int $count)
     {
@@ -176,8 +165,6 @@ public function testMultipleTransactions(int $count)
 
     /**
      * @dataProvider getConnectionCounts
-     *
-     * @param int $count
      */
     public function testConsecutiveTransactions(int $count)
     {
@@ -198,7 +185,7 @@ public function testConsecutiveTransactions(int $count)
 
         $futures = [];
         for ($i = 0; $i < $count; ++$i) {
-            $futures[] = async(fn() => $pool->beginTransaction());
+            $futures[] = async(fn () => $pool->beginTransaction());
         }
 
         try {
@@ -214,8 +201,6 @@ public function testConsecutiveTransactions(int $count)
 
     /**
      * @dataProvider getConnectionCounts
-     *
-     * @param int $count
      */
     public function testExtractConnection(int $count)
     {
@@ -234,7 +219,7 @@ public function testExtractConnection(int $count)
         try {
             $futures = [];
             for ($i = 0; $i < $count; ++$i) {
-                $futures[] = async(fn() => $pool->extractConnection());
+                $futures[] = async(fn () => $pool->extractConnection());
             }
             $results = Future\all($futures);
             foreach ($results as $result) {
@@ -248,8 +233,6 @@ public function testExtractConnection(int $count)
 
     /**
      * @dataProvider getConnectionCounts
-     *
-     * @param int $count
      */
     public function testConnectionClosedInPool(int $count)
     {
@@ -286,12 +269,12 @@ public function testConnectionClosedInPool(int $count)
             $this->assertSame($count + 1, $pool->getConnectionLimit());
             $futures = [];
             for ($i = 0; $i < $count + 1; ++$i) {
-                $futures[] = async(fn() => $pool->query($query));
+                $futures[] = async(fn () => $pool->query($query));
             }
             Future\all($futures);
             $futures = [];
             for ($i = 0; $i < $count; ++$i) {
-                $futures[] = async(fn() => $pool->query($query));
+                $futures[] = async(fn () => $pool->query($query));
             }
             Future\all($futures);
         } finally {
diff --git a/test/bootstrap.php b/test/bootstrap.php
index d9dc715..d9797da 100644
--- a/test/bootstrap.php
+++ b/test/bootstrap.php
@@ -80,4 +80,4 @@
 
 echo CLEAR;
 
-\register_shutdown_function(fn() => $process->signal(\SIGTERM));
+\register_shutdown_function(fn () => $process->signal(\SIGTERM));

From 78072e93e01c8a4466df615c8f49d127c24ac08a Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 27 Feb 2022 10:29:10 -0600
Subject: [PATCH 056/174] Add github actions build

---
 .github/workflows/bootstrap.php |  9 ++++
 .github/workflows/ci.yml        | 77 +++++++++++++++++++++++++++++++++
 composer.json                   |  6 ++-
 test/bootstrap.php              |  7 +--
 test/initialize.php             | 11 +++++
 5 files changed, 103 insertions(+), 7 deletions(-)
 create mode 100644 .github/workflows/bootstrap.php
 create mode 100644 .github/workflows/ci.yml
 create mode 100644 test/initialize.php

diff --git a/.github/workflows/bootstrap.php b/.github/workflows/bootstrap.php
new file mode 100644
index 0000000..e759777
--- /dev/null
+++ b/.github/workflows/bootstrap.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Amp\Mysql\Test;
+
+const DB_HOST = 'localhost:3306';
+const DB_USER = 'root';
+const DB_PASS = 'root';
+
+initialize(new \mysqli(DB_HOST, DB_USER, DB_PASS));
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..954d06e
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,77 @@
+name: Continuous Integration
+
+on:
+  - push
+  - pull_request
+
+jobs:
+  tests:
+    strategy:
+      matrix:
+        include:
+          - operating-system: 'ubuntu-latest'
+            php-version: '8.1'
+
+    name: PHP ${{ matrix.php-version }} ${{ matrix.job-description }}
+
+    runs-on: ${{ matrix.operating-system }}
+
+    env:
+      MYSQL_DATABASE: test
+      MYSQL_USER: root
+      MYSQL_PASSWORD: root
+
+    steps:
+      - name: Setup MySQL
+        run: |
+          sudo systemctl start mysql
+
+      - name: Set git to use LF
+        run: |
+          git config --global core.autocrlf false
+          git config --global core.eol lf
+
+      - name: Checkout code
+        uses: actions/checkout@v2
+
+      - name: Setup PHP
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: ${{ matrix.php-version }}
+          extensions: pq
+
+      - name: Get Composer cache directory
+        id: composer-cache
+        run: echo "::set-output name=dir::$(composer config cache-dir)"
+
+      - name: Cache dependencies
+        uses: actions/cache@v2
+        with:
+          path: ${{ steps.composer-cache.outputs.dir }}
+          key: composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}-${{ matrix.composer-flags }}
+          restore-keys: |
+            composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}-
+            composer-${{ runner.os }}-${{ matrix.php-version }}-
+            composer-${{ runner.os }}-
+            composer-
+
+      - name: Install dependencies
+        uses: nick-invision/retry@v2
+        with:
+          timeout_minutes: 5
+          max_attempts: 5
+          retry_wait_seconds: 30
+          command: |
+            composer update --optimize-autoloader --no-interaction --no-progress ${{ matrix.composer-flags }}
+            composer info -D
+
+      - name: Run tests
+        run: vendor/bin/phpunit --bootstrap .github/workflows/bootstrap.php ${{ matrix.phpunit-flags }}
+
+      - name: Run static analysis
+        run: vendor/bin/psalm.phar
+
+      - name: Run style fixer
+        env:
+          PHP_CS_FIXER_IGNORE_ENV: 1
+        run: vendor/bin/php-cs-fixer --diff --dry-run -v fix
diff --git a/composer.json b/composer.json
index a9f680b..bf30b57 100644
--- a/composer.json
+++ b/composer.json
@@ -22,6 +22,7 @@
         "revolt/event-loop": "dev-main as 0.2.3"
     },
     "require-dev": {
+        "ext-mysqli": "*",
         "ext-openssl": "*",
         "amphp/process": "^2",
         "amphp/file": "^3",
@@ -45,7 +46,10 @@
         "psr-4": {
             "Amp\\Mysql\\Test\\": "test",
             "Amp\\Mysql\\Bench\\": "benchmarks"
-        }
+        },
+        "files": [
+            "test/initialize.php"
+        ]
     },
     "scripts": {
         "check": [
diff --git a/test/bootstrap.php b/test/bootstrap.php
index d9797da..3d59b7d 100644
--- a/test/bootstrap.php
+++ b/test/bootstrap.php
@@ -11,7 +11,6 @@
 const DB_HOST = 'localhost:10101';
 const DB_USER = 'root';
 const DB_PASS = '';
-const DB_NAME = 'test';
 
 const CLEAR = "\r\033[K";
 
@@ -72,11 +71,7 @@
 
 echo CLEAR, "Creating test database...";
 
-$db = new \mysqli(DB_HOST, DB_USER, DB_PASS);
-$db->query("CREATE DATABASE test");
-$db->query("CREATE TABLE test.main (id INT NOT NULL AUTO_INCREMENT, a INT, b INT, PRIMARY KEY (id))");
-$db->query("INSERT INTO test.main (a, b) VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)");
-$db->close();
+initialize(new \mysqli(DB_HOST, DB_USER, DB_PASS));
 
 echo CLEAR;
 
diff --git a/test/initialize.php b/test/initialize.php
new file mode 100644
index 0000000..5553f4b
--- /dev/null
+++ b/test/initialize.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Amp\Mysql\Test;
+
+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, PRIMARY KEY (id))");
+    $db->query("INSERT INTO test.main (a, b) VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)");
+    $db->close();
+}

From fa24ce84795cbc6e68862b521fdad4671e998b6e Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 27 Feb 2022 10:48:56 -0600
Subject: [PATCH 057/174] Properly wrap internal object on transaction query

Closes #114.
---
 src/ConnectionTransaction.php |  4 ++--
 test/LinkTest.php             | 12 +++++++++++-
 2 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/src/ConnectionTransaction.php b/src/ConnectionTransaction.php
index 88f69a4..2f29b6a 100644
--- a/src/ConnectionTransaction.php
+++ b/src/ConnectionTransaction.php
@@ -119,9 +119,9 @@ public function query(string $sql): Promise
         return call(function () use ($sql) {
             $result = yield $this->processor->query($sql);
 
-            if ($result instanceof ResultSet) {
+            if ($result instanceof Internal\ResultProxy) {
                 ++$this->refCount;
-                return new PooledResultSet($result, $this->release);
+                return new PooledResultSet(new ConnectionResultSet($result), $this->release);
             }
 
             return $result;
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 4bce488..8cfae49 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -313,6 +313,16 @@ public function testTransaction()
         $result = yield $statement->execute([6, 7]);
         $this->assertInstanceOf(CommandResult::class, $result);
 
+        /** @var ResultSet $result */
+        $result = yield $transaction->query("SELECT * FROM main WHERE a = 6");
+
+        $got = [];
+        while (yield $result->advance()) {
+            $got[] = \array_values($result->getCurrent());
+        }
+        $this->assertCount(1, $got);
+        $this->assertFalse(yield $result->nextResultSet());
+
         /** @var ResultSet $result */
         $result = yield $transaction->execute("SELECT * FROM main WHERE a = ?", [6]);
 
@@ -321,7 +331,7 @@ public function testTransaction()
             $got[] = \array_values($result->getCurrent());
         }
         $this->assertCount(1, $got);
-        yield $result->nextResultSet();
+        $this->assertFalse(yield $result->nextResultSet());
 
         yield $transaction->rollback();
 

From f48964ec76d6c8d7b8ce746e2c8e24660202999e Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 1 Mar 2022 08:00:43 -0600
Subject: [PATCH 058/174] Revert pin to revolt/event-loop:main

---
 composer.json | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/composer.json b/composer.json
index bf30b57..282a90a 100644
--- a/composer.json
+++ b/composer.json
@@ -18,8 +18,7 @@
         "amphp/pipeline": "^1",
         "amphp/socket": "^2",
         "amphp/sql": "^2",
-        "amphp/sql-common": "^2",
-        "revolt/event-loop": "dev-main as 0.2.3"
+        "amphp/sql-common": "^2"
     },
     "require-dev": {
         "ext-mysqli": "*",

From df1a3b93fe26fee744d2b0834a1da7bb4b69ab09 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Wed, 13 Apr 2022 23:27:41 -0500
Subject: [PATCH 059/174] Refactors and renames based on upstream changes

---
 README.md                                     |  2 +-
 benchmarks/QueryBench.php                     |  8 +++---
 examples/1-connect.php                        |  2 +-
 examples/2-simple-query.php                   |  2 +-
 examples/3-generic-with-yield.php             |  6 ++---
 examples/4-multi-rows.php                     |  6 ++---
 examples/5-multi-stmts.php                    |  2 +-
 examples/6-transaction.php                    |  2 +-
 src/CancellableConnector.php                  | 23 ----------------
 src/Connection.php                            | 26 +++++--------------
 src/Connector.php                             | 11 --------
 src/DataTypes.php                             | 16 ++++++------
 src/DefaultMysqlConnector.php                 | 26 +++++++++++++++++++
 src/InitializationException.php               |  4 +--
 src/Internal/ConnectionTransaction.php        |  1 +
 src/Internal/Processor.php                    | 22 +++++++++-------
 src/{ConnectionConfig.php => MysqlConfig.php} |  4 +--
 src/MysqlConnector.php                        | 16 ++++++++++++
 src/Pool.php                                  | 11 +++++---
 src/functions.php                             | 15 +++++------
 test/ConnectionTest.php                       | 10 ++++---
 ...tionConfigTest.php => MysqlConfigTest.php} |  8 +++---
 test/PoolTest.php                             | 10 +++----
 23 files changed, 118 insertions(+), 115 deletions(-)
 delete mode 100644 src/CancellableConnector.php
 delete mode 100644 src/Connector.php
 create mode 100644 src/DefaultMysqlConnector.php
 rename src/{ConnectionConfig.php => MysqlConfig.php} (97%)
 create mode 100644 src/MysqlConnector.php
 rename test/{ConnectionConfigTest.php => MysqlConfigTest.php} (62%)

diff --git a/README.md b/README.md
index bf54c74..2c5fca0 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@ More extensive code examples reside in the [`examples`](examples) directory.
 
 ```php
 Amp\Loop::run(function() {
-    $config = Amp\Mysql\ConnectionConfig::fromString(
+    $config = Amp\Mysql\MysqlConfig::fromString(
         "host=127.0.0.1 user=username password=password db=test"
     );
     
diff --git a/benchmarks/QueryBench.php b/benchmarks/QueryBench.php
index a394bb8..219a686 100644
--- a/benchmarks/QueryBench.php
+++ b/benchmarks/QueryBench.php
@@ -2,9 +2,9 @@
 
 namespace Amp\Mysql\Bench;
 
-use Amp\Mysql\CancellableConnector;
+use Amp\Mysql\DefaultMysqlConnector;
 use Amp\Mysql\Connection;
-use Amp\Mysql\ConnectionConfig;
+use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\Pool as ConnectionPool;
 use Amp\Mysql\ResultSet;
 use function Amp\call;
@@ -37,8 +37,8 @@ class QueryBench extends AbstractBench
 
     public function init()
     {
-        $config = ConnectionConfig::fromString("host=$this->host;user=$this->user;pass=$this->pass");
-        $connector = new CancellableConnector;
+        $config = MysqlConfig::fromString("host=$this->host;user=$this->user;pass=$this->pass");
+        $connector = new DefaultMysqlConnector;
         $this->connectionPool = new ConnectionPool($config, $this->poolLimit, 10, $connector);
         $connectionPromise = $connector->connect($config);
         $this->connection = wait($connectionPromise);
diff --git a/examples/1-connect.php b/examples/1-connect.php
index d97abe9..2fdbde9 100644
--- a/examples/1-connect.php
+++ b/examples/1-connect.php
@@ -9,7 +9,7 @@
 use Amp\Mysql;
 
 /* If you want ssl, pass as second argument an array with ssl options (an empty options array is valid too); if null is passed, ssl is not enabled either */
-$config = Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME);
+$config = Mysql\MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME);
 
 /* use an alternative charset... Default is utf8mb4_general_ci */
 $config = $config->withCharset("latin1_general_ci");
diff --git a/examples/2-simple-query.php b/examples/2-simple-query.php
index 5907ecd..eba0540 100644
--- a/examples/2-simple-query.php
+++ b/examples/2-simple-query.php
@@ -4,7 +4,7 @@
 
 use Amp\Mysql;
 
-$db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = Mysql\pool(Mysql\MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
 $result = $db->query("SELECT 1 AS value");
 
diff --git a/examples/3-generic-with-yield.php b/examples/3-generic-with-yield.php
index 771f512..20b359b 100644
--- a/examples/3-generic-with-yield.php
+++ b/examples/3-generic-with-yield.php
@@ -6,7 +6,7 @@
 use Amp\Mysql;
 use function Amp\async;
 
-$db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = Mysql\pool(Mysql\MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
 $db->query("DROP TABLE IF EXISTS tmp");
 
@@ -20,7 +20,7 @@
 
 $future = [];
 foreach (\range(1, 5) as $num) {
-    $future[] = async(fn() => $statement->execute([$num, $num]));
+    $future[] = async(fn () => $statement->execute([$num, $num]));
 }
 
 /* wait until everything is inserted */
@@ -31,7 +31,7 @@
 $result = $db->query("SELECT a, b FROM tmp");
 
 foreach ($result as $row) {
-    \var_dump($row);
+    var_dump($row);
 }
 
 $db->query("DROP TABLE tmp");
diff --git a/examples/4-multi-rows.php b/examples/4-multi-rows.php
index da1db2b..4b2cac2 100644
--- a/examples/4-multi-rows.php
+++ b/examples/4-multi-rows.php
@@ -7,15 +7,15 @@
 use Amp\Mysql;
 use function Amp\async;
 
-$db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = Mysql\pool(Mysql\MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
 /* create same table than in 3-generic-with-yield.php */
 createGenericTable($db);
 
 $future = [];
 
-$future[] = async(fn() => $db->query("SELECT a * b FROM tmp"));
-$future[] = async(fn() => $db->execute("SELECT POW(a, ?) AS power FROM tmp", [2]));
+$future[] = async(fn () => $db->query("SELECT a * b FROM tmp"));
+$future[] = async(fn () => $db->execute("SELECT POW(a, ?) AS power FROM tmp", [2]));
 
 try {
     /**
diff --git a/examples/5-multi-stmts.php b/examples/5-multi-stmts.php
index 8f9ed70..5c248ad 100644
--- a/examples/5-multi-stmts.php
+++ b/examples/5-multi-stmts.php
@@ -5,7 +5,7 @@
 
 use Amp\Mysql;
 
-$db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = Mysql\pool(Mysql\MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
 /* create same table than in 3-generic-with-yield.php */
 createGenericTable($db);
diff --git a/examples/6-transaction.php b/examples/6-transaction.php
index fc70ea8..f9e0bbc 100644
--- a/examples/6-transaction.php
+++ b/examples/6-transaction.php
@@ -5,7 +5,7 @@
 
 use Amp\Mysql;
 
-$db = Mysql\pool(Mysql\ConnectionConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = Mysql\pool(Mysql\MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
 /* create same table than in 3-generic-with-yield.php */
 createGenericTable($db);
diff --git a/src/CancellableConnector.php b/src/CancellableConnector.php
deleted file mode 100644
index 78a923c..0000000
--- a/src/CancellableConnector.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-use Amp\Cancellation;
-use Amp\Socket;
-use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
-
-final class CancellableConnector implements Connector
-{
-    public function __construct(private readonly ?Socket\SocketConnector $connector = null)
-    {
-    }
-
-    public function connect(SqlConnectionConfig $config, ?Cancellation $token = null): Connection
-    {
-        if (!$config instanceof ConnectionConfig) {
-            throw new \TypeError(\sprintf("Must provide an instance of %s to MySQL connectors", ConnectionConfig::class));
-        }
-
-        return Connection::connect($config, $token, $this->connector ?? Socket\socketConnector());
-    }
-}
diff --git a/src/Connection.php b/src/Connection.php
index 57d06b0..da1c425 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -4,8 +4,7 @@
 
 use Amp\Cancellation;
 use Amp\DeferredFuture;
-use Amp\NullCancellation;
-use Amp\Socket;
+use Amp\Socket\Socket;
 use Amp\Sql\TransactionIsolation;
 use Revolt\EventLoop;
 
@@ -27,18 +26,13 @@ final class Connection implements Link
     /** @var \Closure Function used to release connection after a transaction has completed. */
     private \Closure $release;
 
-    public static function connect(
-        ConnectionConfig $config,
-        ?Cancellation $token = null,
-        ?Socket\SocketConnector $connector = null
+    public static function initialize(
+        Socket $socket,
+        MysqlConfig $config,
+        ?Cancellation $cancellation = null,
     ): self {
-        $token = $token ?? new NullCancellation;
-
-        $socket = ($connector ?? Socket\socketConnector())
-            ->connect($config->getConnectionString(), $config->getConnectContext(), $token);
-
         $processor = new Internal\Processor($socket, $config);
-        $processor->connect($token);
+        $processor->connect($cancellation);
         return new self($processor);
     }
 
@@ -119,13 +113,7 @@ public function beginTransaction(TransactionIsolation $isolation = TransactionIs
         $this->busy = $deferred = new DeferredFuture;
 
         try {
-            $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL " . match ($isolation) {
-                TransactionIsolation::Uncommitted => "READ UNCOMMITTED",
-                TransactionIsolation::Committed => "READ COMMITTED",
-                TransactionIsolation::Repeatable => "REPEATABLE READ",
-                TransactionIsolation::Serializable => "SERIALIZABLE",
-            })->await();
-
+            $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL " . $isolation->toSql())->await();
             $this->processor->query("START TRANSACTION")->await();
         } catch (\Throwable $exception) {
             $this->busy = null;
diff --git a/src/Connector.php b/src/Connector.php
deleted file mode 100644
index 4aaf725..0000000
--- a/src/Connector.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-use Amp\Sql\ConnectionConfig;
-use Amp\Sql\Connector as SqlConnector;
-
-interface Connector extends SqlConnector
-{
-    public function connect(ConnectionConfig $config): Connection;
-}
diff --git a/src/DataTypes.php b/src/DataTypes.php
index 6e688b2..e10c5fe 100644
--- a/src/DataTypes.php
+++ b/src/DataTypes.php
@@ -2,7 +2,7 @@
 
 namespace Amp\Mysql;
 
-use Amp\Sql\FailureException;
+use Amp\Sql\SqlException;
 
 /** @see 14.6.4.1.1.1 Column Type */
 final class DataTypes
@@ -76,7 +76,7 @@ public static function encodeBinary(mixed $param): array
                 $value = "";
                 break;
             default:
-                throw new FailureException("Unexpected type for binding parameter: " . \gettype($param));
+                throw new SqlException("Unexpected type for binding parameter: " . \gettype($param));
         }
 
         return [$unsigned, $type, $value];
@@ -161,7 +161,7 @@ public static function decodeBinary(int $type, string $str, ?int &$len = null) /
                         break;
 
                     default:
-                        throw new FailureException("Unexpected string length for date in binary protocol: " . ($len - 1));
+                        throw new SqlException("Unexpected string length for date in binary protocol: " . ($len - 1));
                 }
                 return \str_pad($year, 2, "0", STR_PAD_LEFT) . "-" . \str_pad($month, 2, "0", STR_PAD_LEFT) . "-" . \str_pad($day, 2, "0", STR_PAD_LEFT) . " " . \str_pad($hour, 2, "0", STR_PAD_LEFT) . ":" . \str_pad($minute, 2, "0", STR_PAD_LEFT) . ":" . \str_pad($second, 2, "0", STR_PAD_LEFT) . "." . \str_pad($microsecond, 5, "0", STR_PAD_LEFT);
 
@@ -182,7 +182,7 @@ public static function decodeBinary(int $type, string $str, ?int &$len = null) /
                         break;
 
                     default:
-                        throw new FailureException("Unexpected string length for time in binary protocol: " . ($len - 1));
+                        throw new SqlException("Unexpected string length for time in binary protocol: " . ($len - 1));
                 }
                 return ($negative ? "" : "-") . \str_pad($day, 2, "0", STR_PAD_LEFT) . "d " . \str_pad($hour, 2, "0", STR_PAD_LEFT) . ":" . \str_pad($minute, 2, "0", STR_PAD_LEFT) . ":" . \str_pad($second, 2, "0", STR_PAD_LEFT) . "." . \str_pad($microsecond, 5, "0", STR_PAD_LEFT);
 
@@ -191,7 +191,7 @@ public static function decodeBinary(int $type, string $str, ?int &$len = null) /
                 return null;
 
             default:
-                throw new FailureException("Invalid type for Binary Protocol: 0x" . \dechex($type));
+                throw new SqlException("Invalid type for Binary Protocol: 0x" . \dechex($type));
         }
     }
 
@@ -272,7 +272,7 @@ public static function decodeUnsignedOff(string $str, int &$off): int
         }
 
         // If that happens connection is borked...
-        throw new FailureException("$int is not in ranges [0x00, 0xfa] or [0xfc, 0xfe]");
+        throw new SqlException("$int is not in ranges [0x00, 0xfa] or [0xfc, 0xfe]");
     }
 
     public static function decodeString(string $str, ?int &$intlen = null, ?int &$len = null): string
@@ -305,7 +305,7 @@ public static function decodeUnsigned(string $str, ?int &$len = null)
         }
 
         // If that happens connection is borked...
-        throw new FailureException("$int is not in ranges [0x00, 0xfa] or [0xfc, 0xfe]");
+        throw new SqlException("$int is not in ranges [0x00, 0xfa] or [0xfc, 0xfe]");
     }
 
     public static function decodeIntByLen(string $str, int $len): int
@@ -422,7 +422,7 @@ public static function encodeInt(int $int): string
             return "\xfe" . self::encodeInt64($int);
         }
 
-        throw new FailureException("encodeInt doesn't allow integers bigger than 2^64 - 1 (current: $int)");
+        throw new SqlException("encodeInt doesn't allow integers bigger than 2^64 - 1 (current: $int)");
     }
 
     public static function encodeInt16(int $int): string
diff --git a/src/DefaultMysqlConnector.php b/src/DefaultMysqlConnector.php
new file mode 100644
index 0000000..f219a2e
--- /dev/null
+++ b/src/DefaultMysqlConnector.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Amp\Mysql;
+
+use Amp\Cancellation;
+use Amp\Socket;
+use Amp\Sql\SqlConfig;
+
+final class DefaultMysqlConnector implements MysqlConnector
+{
+    public function __construct(private readonly ?Socket\SocketConnector $connector = null)
+    {
+    }
+
+    public function connect(SqlConfig $config, ?Cancellation $cancellation = null): Connection
+    {
+        if (!$config instanceof MysqlConfig) {
+            throw new \TypeError(\sprintf("Must provide an instance of %s to MySQL connectors", MysqlConfig::class));
+        }
+
+        $socket = ($this->connector ?? Socket\socketConnector())
+            ->connect($config->getConnectionString(), $config->getConnectContext(), $cancellation);
+
+        return Connection::initialize($socket, $config, $cancellation);
+    }
+}
diff --git a/src/InitializationException.php b/src/InitializationException.php
index 055b4e9..6b74e26 100644
--- a/src/InitializationException.php
+++ b/src/InitializationException.php
@@ -2,8 +2,8 @@
 
 namespace Amp\Mysql;
 
-use Amp\Sql\FailureException;
+use Amp\Sql\SqlException;
 
-class InitializationException extends FailureException
+class InitializationException extends SqlException
 {
 }
diff --git a/src/Internal/ConnectionTransaction.php b/src/Internal/ConnectionTransaction.php
index 1e45da4..d128054 100644
--- a/src/Internal/ConnectionTransaction.php
+++ b/src/Internal/ConnectionTransaction.php
@@ -19,6 +19,7 @@ final class ConnectionTransaction implements Transaction
 
     private readonly TransactionIsolation $isolation;
 
+    /** @var \Closure():void */
     private readonly \Closure $release;
 
     private int $refCount = 1;
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index d6edfae..d9bd75b 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -6,13 +6,13 @@
 use Amp\DeferredFuture;
 use Amp\File;
 use Amp\Future;
-use Amp\Mysql\ConnectionConfig;
 use Amp\Mysql\DataTypes;
 use Amp\Mysql\InitializationException;
+use Amp\Mysql\MysqlConfig;
 use Amp\Socket\EncryptableSocket;
 use Amp\Sql\ConnectionException;
-use Amp\Sql\FailureException;
 use Amp\Sql\QueryError;
+use Amp\Sql\SqlException;
 use Amp\Sql\TransientResource;
 use Revolt\EventLoop;
 
@@ -72,7 +72,7 @@ class Processor implements TransientResource
     private ?\Closure $parseCallback = null;
     private ?\Closure $packetCallback = null;
 
-    public ConnectionConfig $config;
+    public MysqlConfig $config;
 
     /** @var DeferredFuture[] */
     private array $deferreds = [];
@@ -127,7 +127,7 @@ class Processor implements TransientResource
     private const CLOSING = 3;
     private const CLOSED = 4;
 
-    public function __construct(EncryptableSocket $socket, ConnectionConfig $config)
+    public function __construct(EncryptableSocket $socket, MysqlConfig $config)
     {
         $this->socket = $socket;
         $this->connInfo = new ConnectionState;
@@ -184,7 +184,7 @@ private function addDeferred(DeferredFuture $deferred): void
         $this->socket->reference();
     }
 
-    public function connect(Cancellation $token): void
+    public function connect(?Cancellation $cancellation = null): void
     {
         \assert(!$this->processors, self::class."::connect() must not be called twice");
 
@@ -192,14 +192,16 @@ public function connect(Cancellation $token): void
 
         $this->processors = [$this->parseMysql()];
 
-        $id = $token->subscribe(fn () => $this->close());
+        $id = $cancellation?->subscribe(fn () => $this->close());
 
         EventLoop::queue(fn () => $this->read());
 
         $future = $deferred->getFuture();
-        $future = $future->finally(static fn () => $token->unsubscribe($id));
+        if ($id !== null) {
+            $future = $future->finally(static fn () => $cancellation?->unsubscribe($id));
+        }
 
-        if ($this->config->getCharset() !== ConnectionConfig::DEFAULT_CHARSET || $this->config->getCollation() !== ConnectionConfig::DEFAULT_COLLATE) {
+        if ($this->config->getCharset() !== MysqlConfig::DEFAULT_CHARSET || $this->config->getCollation() !== MysqlConfig::DEFAULT_COLLATE) {
             $future = $future->map(function () {
                 $charset = $this->config->getCharset();
                 $collate = $this->config->getCollation();
@@ -758,7 +760,7 @@ private function parseEof(string $packet): void
     private function handleEof(string $packet): void
     {
         $this->parseEof($packet);
-        $exception = new FailureException($this->connInfo->errorMsg, $this->connInfo->errorCode);
+        $exception = new SqlException($this->connInfo->errorMsg, $this->connInfo->errorCode);
         $this->getDeferred()->error($exception);
         $this->ready();
     }
@@ -1503,7 +1505,7 @@ private function sendHandshake(bool $inSSL = false): void
         $payload = "";
         $payload .= \pack("V", $this->capabilities);
         $payload .= \pack("V", 1 << 24 - 1); // max-packet size
-        $payload .= \chr(ConnectionConfig::BIN_CHARSET);
+        $payload .= \chr(MysqlConfig::BIN_CHARSET);
         $payload .= \str_repeat("\0", 23); // reserved
 
         if (!$inSSL && ($this->capabilities & self::CLIENT_SSL)) {
diff --git a/src/ConnectionConfig.php b/src/MysqlConfig.php
similarity index 97%
rename from src/ConnectionConfig.php
rename to src/MysqlConfig.php
index dd16bbb..f72b899 100644
--- a/src/ConnectionConfig.php
+++ b/src/MysqlConfig.php
@@ -3,9 +3,9 @@
 namespace Amp\Mysql;
 
 use Amp\Socket\ConnectContext;
-use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
+use Amp\Sql\SqlConfig;
 
-final class ConnectionConfig extends SqlConnectionConfig
+final class MysqlConfig extends SqlConfig
 {
     public const DEFAULT_PORT = 3306;
     public const BIN_CHARSET = 45; // utf8mb4_general_ci
diff --git a/src/MysqlConnector.php b/src/MysqlConnector.php
new file mode 100644
index 0000000..7249383
--- /dev/null
+++ b/src/MysqlConnector.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Amp\Mysql;
+
+use Amp\Cancellation;
+use Amp\Sql\SqlConfig;
+use Amp\Sql\SqlConnector;
+use Amp\Sql\SqlException;
+
+interface MysqlConnector extends SqlConnector
+{
+    /**
+     * @throws SqlException
+     */
+    public function connect(SqlConfig $config, ?Cancellation $cancellation = null): Connection;
+}
diff --git a/src/Pool.php b/src/Pool.php
index bb2ca0e..a4dd0d7 100644
--- a/src/Pool.php
+++ b/src/Pool.php
@@ -3,7 +3,6 @@
 namespace Amp\Mysql;
 
 use Amp\Sql\Common\ConnectionPool;
-use Amp\Sql\Connector;
 use Amp\Sql\Pool as SqlPool;
 use Amp\Sql\Result as SqlResult;
 use Amp\Sql\Statement as SqlStatement;
@@ -12,9 +11,13 @@
 
 final class Pool extends ConnectionPool implements Link
 {
-    protected function createDefaultConnector(): Connector
-    {
-        return connector();
+    public function __construct(
+        MysqlConfig $config,
+        int $maxConnections = self::DEFAULT_MAX_CONNECTIONS,
+        int $idleTimeout = self::DEFAULT_IDLE_TIMEOUT,
+        ?MysqlConnector $connector = null,
+    ) {
+        parent::__construct($config, $connector ?? connector(), $maxConnections, $idleTimeout);
     }
 
     protected function createResult(SqlResult $result, \Closure $release): Result
diff --git a/src/functions.php b/src/functions.php
index 1bf2f65..a3d7c81 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -3,11 +3,10 @@
 namespace Amp\Mysql;
 
 use Amp\Sql\Common\ConnectionPool;
-use Amp\Sql\ConnectionConfig as SqlConnectionConfig;
-use Amp\Sql\FailureException;
+use Amp\Sql\SqlException;
 use Revolt\EventLoop;
 
-function connector(?Connector $connector = null): Connector
+function connector(?MysqlConnector $connector = null): MysqlConnector
 {
     static $map;
     $map ??= new \WeakMap();
@@ -17,16 +16,16 @@ function connector(?Connector $connector = null): Connector
         return $map[$driver] = $connector;
     }
 
-    return $map[$driver] ??= new CancellableConnector;
+    return $map[$driver] ??= new DefaultMysqlConnector;
 }
 
 /**
  * Create a connection using the global Connector instance.
  *
- * @throws FailureException If connecting fails.
+ * @throws SqlException If connecting fails.
  * @throws \Error If the connection string does not contain a host, user, and password.
  */
-function connect(SqlConnectionConfig $config): Connection
+function connect(MysqlConfig $config): Connection
 {
     return connector()->connect($config);
 }
@@ -37,9 +36,9 @@ function connect(SqlConnectionConfig $config): Connection
  * @throws \Error If the connection string does not contain a host, user, and password.
  */
 function pool(
-    SqlConnectionConfig $config,
+    MysqlConfig $config,
     int $maxConnections = ConnectionPool::DEFAULT_MAX_CONNECTIONS,
-    int $idleTimeout = ConnectionPool::DEFAULT_IDLE_TIMEOUT
+    int $idleTimeout = ConnectionPool::DEFAULT_IDLE_TIMEOUT,
 ): Pool {
     return new Pool($config, $maxConnections, $idleTimeout, connector());
 }
diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php
index fb0bacb..59eda27 100644
--- a/test/ConnectionTest.php
+++ b/test/ConnectionTest.php
@@ -2,21 +2,23 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Mysql\CancellableConnector;
 use Amp\Mysql\Connection;
-use Amp\Mysql\ConnectionConfig;
+use Amp\Mysql\DefaultMysqlConnector;
 use Amp\Mysql\Link;
+use Amp\Mysql\MysqlConfig;
 
 class ConnectionTest extends LinkTest
 {
     protected function getLink(string $connectionString): Link
     {
-        return (new CancellableConnector)->connect(ConnectionConfig::fromString($connectionString));
+        return (new DefaultMysqlConnector)->connect(MysqlConfig::fromString($connectionString));
     }
 
     public function testConnect()
     {
-        $db = Connection::connect(ConnectionConfig::fromString("host=".DB_HOST." user=".DB_USER." pass=".DB_PASS." db=test"));
+        $connector = new DefaultMysqlConnector();
+
+        $db = $connector->connect(MysqlConfig::fromString("host=".DB_HOST." user=".DB_USER." pass=".DB_PASS." db=test"));
 
         $this->assertInstanceOf(Connection::class, $db);
 
diff --git a/test/ConnectionConfigTest.php b/test/MysqlConfigTest.php
similarity index 62%
rename from test/ConnectionConfigTest.php
rename to test/MysqlConfigTest.php
index 57737bb..1a95611 100644
--- a/test/ConnectionConfigTest.php
+++ b/test/MysqlConfigTest.php
@@ -2,22 +2,22 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Mysql\ConnectionConfig;
+use Amp\Mysql\MysqlConfig;
 use Amp\PHPUnit\AsyncTestCase;
 
-class ConnectionConfigTest extends AsyncTestCase
+class MysqlConfigTest extends AsyncTestCase
 {
     public function testInvalidConnectionString()
     {
         $this->expectException(\Error::class);
         $this->expectExceptionMessage('Host must be provided in connection string');
 
-        ConnectionConfig::fromString("username=" . DB_USER);
+        MysqlConfig::fromString("username=" . DB_USER);
     }
 
     public function testGetConnectionString()
     {
-        $config = ConnectionConfig::fromString('host=host;port=1234;user=user;password=password');
+        $config = MysqlConfig::fromString('host=host;port=1234;user=user;password=password');
 
         $this->assertSame('tcp://host:1234', $config->getConnectionString());
     }
diff --git a/test/PoolTest.php b/test/PoolTest.php
index a99f75c..710fe8c 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -4,14 +4,14 @@
 
 use Amp\Future;
 use Amp\Mysql\Connection;
-use Amp\Mysql\ConnectionConfig;
 use Amp\Mysql\Internal\CommandResult;
 use Amp\Mysql\Internal\Processor;
 use Amp\Mysql\Link;
+use Amp\Mysql\MysqlConfig;
+use Amp\Mysql\MysqlConnector;
 use Amp\Mysql\Pool;
 use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
-use Amp\Sql\Connector;
 use Amp\Sql\Transaction as SqlTransaction;
 use PHPUnit\Framework\MockObject\MockObject;
 use function Amp\async;
@@ -25,19 +25,19 @@ class PoolTest extends LinkTest
 {
     protected function getLink(string $connectionString): Link
     {
-        return new Pool(ConnectionConfig::fromString($connectionString));
+        return new Pool(MysqlConfig::fromString($connectionString));
     }
 
     protected function createPool(array $connections): Pool
     {
-        $connector = $this->createMock(Connector::class);
+        $connector = $this->createMock(MysqlConnector::class);
         $connector->method('connect')
             ->will($this->returnCallback(function () use ($connections): Connection {
                 static $count = 0;
                 return $connections[$count++ % \count($connections)];
             }));
 
-        $config = ConnectionConfig::fromString('host=host;user=user;password=password');
+        $config = MysqlConfig::fromString('host=host;user=user;password=password');
 
         return new Pool($config, \count($connections), Pool::DEFAULT_IDLE_TIMEOUT, $connector);
     }

From 9c98967ea616b960704e4f7dc0c59f25c791994d Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 16 Apr 2022 22:07:25 -0500
Subject: [PATCH 060/174] Add missing types and cleanup formatting

---
 src/DataTypes.php | 24 ++++++++++++++++++------
 1 file changed, 18 insertions(+), 6 deletions(-)

diff --git a/src/DataTypes.php b/src/DataTypes.php
index e10c5fe..ac5d397 100644
--- a/src/DataTypes.php
+++ b/src/DataTypes.php
@@ -83,7 +83,7 @@ public static function encodeBinary(mixed $param): array
     }
 
     /** @see 14.7.3 Binary Protocol Value */
-    public static function decodeBinary(int $type, string $str, ?int &$len = null) /* : mixed */
+    public static function decodeBinary(int $type, string $str, ?int &$len = null): mixed
     {
         $unsigned = $type & 0x80;
         switch ($type) {
@@ -163,7 +163,14 @@ public static function decodeBinary(int $type, string $str, ?int &$len = null) /
                     default:
                         throw new SqlException("Unexpected string length for date in binary protocol: " . ($len - 1));
                 }
-                return \str_pad($year, 2, "0", STR_PAD_LEFT) . "-" . \str_pad($month, 2, "0", STR_PAD_LEFT) . "-" . \str_pad($day, 2, "0", STR_PAD_LEFT) . " " . \str_pad($hour, 2, "0", STR_PAD_LEFT) . ":" . \str_pad($minute, 2, "0", STR_PAD_LEFT) . ":" . \str_pad($second, 2, "0", STR_PAD_LEFT) . "." . \str_pad($microsecond, 5, "0", STR_PAD_LEFT);
+
+                return \str_pad($year, 2, "0", STR_PAD_LEFT)
+                    . "-" . \str_pad($month, 2, "0", STR_PAD_LEFT)
+                    . "-" . \str_pad($day, 2, "0", STR_PAD_LEFT)
+                    . " " . \str_pad($hour, 2, "0", STR_PAD_LEFT)
+                    . ":" . \str_pad($minute, 2, "0", STR_PAD_LEFT)
+                    . ":" . \str_pad($second, 2, "0", STR_PAD_LEFT)
+                    . "." . \str_pad($microsecond, 5, "0", STR_PAD_LEFT);
 
             case self::MYSQL_TYPE_TIME:
                 $negative = $day = $hour = $minute = $second = $microsecond = 0;
@@ -184,7 +191,12 @@ public static function decodeBinary(int $type, string $str, ?int &$len = null) /
                     default:
                         throw new SqlException("Unexpected string length for time in binary protocol: " . ($len - 1));
                 }
-                return ($negative ? "" : "-") . \str_pad($day, 2, "0", STR_PAD_LEFT) . "d " . \str_pad($hour, 2, "0", STR_PAD_LEFT) . ":" . \str_pad($minute, 2, "0", STR_PAD_LEFT) . ":" . \str_pad($second, 2, "0", STR_PAD_LEFT) . "." . \str_pad($microsecond, 5, "0", STR_PAD_LEFT);
+
+                return ($negative ? "" : "-") . \str_pad($day, 2, "0", STR_PAD_LEFT)
+                    . "d " . \str_pad($hour, 2, "0", STR_PAD_LEFT)
+                    . ":" . \str_pad($minute, 2, "0", STR_PAD_LEFT)
+                    . ":" . \str_pad($second, 2, "0", STR_PAD_LEFT)
+                    . "." . \str_pad($microsecond, 5, "0", STR_PAD_LEFT);
 
             case self::MYSQL_TYPE_NULL:
                 $len = 0;
@@ -200,7 +212,7 @@ public static function decodeNullString(string $str, ?int &$len = null): string
         return \substr($str, 0, $len = \strpos($str, "\0"));
     }
 
-    public static function decodeStringOff(int $type, string $str, int &$off) /* : mixed */
+    public static function decodeStringOff(int $type, string $str, int &$off): mixed
     {
         $len = self::decodeUnsignedOff($str, $off);
         $off += $len;
@@ -281,7 +293,7 @@ public static function decodeString(string $str, ?int &$intlen = null, ?int &$le
         return \substr($str, $intlen, $len);
     }
 
-    public static function decodeUnsigned(string $str, ?int &$len = null)
+    public static function decodeUnsigned(string $str, ?int &$len = null): int|string
     {
         $int = \ord($str);
         if ($int < 0xfb) {
@@ -342,7 +354,7 @@ public static function decodeInt16(string $str): int
         return $int << $shift >> $shift;
     }
 
-    public static function decodeUnsigned16(string $str)
+    public static function decodeUnsigned16(string $str): int
     {
         return \unpack("v", $str)[1];
     }

From fa5172da58ce7c69dad8761fd7deb3d385ea2a01 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 17 Apr 2022 08:40:49 -0500
Subject: [PATCH 061/174] Refactor DataType

---
 composer.json                     |   2 +-
 src/DataType.php                  | 443 ++++++++++++++++++++++++++
 src/DataTypes.php                 | 459 ---------------------------
 src/Internal/ConnectionState.php  |   6 +-
 src/Internal/Processor.php        | 507 +++++++++++++++---------------
 src/Internal/SessionStateType.php |  14 +
 src/Internal/StatusFlag.php       |  27 ++
 src/RefreshTypes.php              |  15 -
 test/LinkTest.php                 |   6 +-
 9 files changed, 738 insertions(+), 741 deletions(-)
 create mode 100644 src/DataType.php
 delete mode 100644 src/DataTypes.php
 create mode 100644 src/Internal/SessionStateType.php
 create mode 100644 src/Internal/StatusFlag.php
 delete mode 100644 src/RefreshTypes.php

diff --git a/composer.json b/composer.json
index 282a90a..ccb4978 100644
--- a/composer.json
+++ b/composer.json
@@ -27,7 +27,7 @@
         "amphp/file": "^3",
         "phpunit/phpunit": "^9",
         "amphp/phpunit-util": "^3",
-        "amphp/php-cs-fixer-config": "^2-dev",
+        "amphp/php-cs-fixer-config": "^2",
         "phpbench/phpbench": "^1.1",
         "psalm/phar": "^4.22"
     },
diff --git a/src/DataType.php b/src/DataType.php
new file mode 100644
index 0000000..f098edb
--- /dev/null
+++ b/src/DataType.php
@@ -0,0 +1,443 @@
+<?php
+
+namespace Amp\Mysql;
+
+use Amp\Sql\SqlException;
+
+/** @see 14.6.4.1.1.1 Column Type */
+enum DataType: int
+{
+    case Decimal = 0x00;
+    case Tiny = 0x01;
+    case Short = 0x02;
+    case Long = 0x03;
+    case Float = 0x04;
+    case Double = 0x05;
+    case Null = 0x06;
+    case Timestamp = 0x07;
+    case LongLong = 0x08;
+    case Int24 = 0x09;
+    case Date = 0x0a;
+    case Time = 0x0b;
+    case Datetime = 0x0c;
+    case Year = 0x0d;
+    case NewDate = 0x0e;
+    case Varchar = 0x0f;
+    case Bit = 0x10;
+    case Timestamp2 = 0x11;
+    case Datetime2 = 0x12;
+    case Time2 = 0x13;
+    case Json = 0xf5;
+    case NewDecimal = 0xf6;
+    case Enum = 0xf7;
+    case Set = 0xf8;
+    case TinyBlob = 0xf9;
+    case MediumBlob = 0xfa;
+    case LongBlob = 0xfb;
+    case Blob = 0xfc;
+    case VarString = 0xfd;
+    case String = 0xfe;
+    case Geometry = 0xff;
+
+    private const ENCODED_JSON_PREFIX = "base64:type251:";
+
+    /**
+     * @return array{DataType, string}
+     *
+     * @see 14.7.3 Binary Value
+     */
+    public static function encodeBinary(mixed $param): array
+    {
+        switch (\get_debug_type($param)) {
+            case "bool":
+                return [self::Tiny, $param ? "\x01" : "\0"];
+
+            case "int":
+                if ($param >= -(1 << 15) && $param < (1 << 15)) {
+                    return [self::Short, self::encodeInt16($param)];
+                }
+
+                if ($param >= -(1 << 31) && $param < (1 << 31)) {
+                    return [self::Long, self::encodeInt32($param)];
+                }
+
+                return [self::LongLong, self::encodeInt64($param)];
+
+            case "float":
+                return [self::Double, \pack("e", $param)];
+
+            case "string":
+                return [self::LongBlob, self::encodeInt(\strlen($param)) . $param];
+
+            case "null":
+                return [self::Null, ""];
+
+            default:
+                throw new SqlException("Unexpected type for binding parameter: " . \get_debug_type($param));
+        }
+    }
+
+    /** @see 14.7.3 Binary Protocol Value */
+    public function decodeBinary(string $string, int &$offset = 0, int $flags = 0): int|float|string|null
+    {
+        $unsigned = $flags & 0x20;
+
+        switch ($this) {
+            case self::String:
+            case self::Varchar:
+            case self::VarString:
+            case self::Enum:
+            case self::Set:
+            case self::LongBlob:
+            case self::MediumBlob:
+            case self::Blob:
+            case self::TinyBlob:
+            case self::Geometry:
+            case self::Bit:
+            case self::Decimal:
+            case self::NewDecimal:
+                return $this->decodeText($string, $offset);
+
+            case self::Json:
+                $data = $this->decodeText($string, $offset);
+                return self::decodeJson($data);
+
+            case self::LongLong:
+                return $unsigned
+                    ? self::decodeUnsigned64($string, $offset)
+                    : self::decodeInt64($string, $offset);
+
+            case self::Long:
+                return $unsigned
+                    ? self::decodeUnsigned32($string, $offset)
+                    : self::decodeInt32($string, $offset);
+
+            case self::Int24:
+                return $unsigned
+                    ? self::decodeUnsigned24($string, $offset)
+                    : self::decodeInt24($string, $offset);
+
+            case self::Short:
+                return $unsigned
+                    ? self::decodeUnsigned16($string, $offset)
+                    : self::decodeInt16($string, $offset);
+
+            case self::Tiny:
+                return $unsigned
+                    ? self::decodeUnsigned8($string, $offset)
+                    : self::decodeInt8($string, $offset);
+
+            case self::Double:
+                $string = \substr($string, $offset, 8);
+                $offset += 8;
+                return \unpack("e", $string)[1];
+
+            case self::Float:
+                $string = \substr($string, $offset, 4);
+                $offset += 4;
+                return \unpack("g", $string)[1];
+
+            case self::Date:
+            case self::Datetime:
+            case self::Timestamp:
+                $string = \substr($string, $offset, 12);
+                $year = $month = $day = $hour = $minute = $second = $microsecond = 0;
+                switch ($length = \ord($string) + 1) {
+                    case 12:
+                        $microsecond = self::decodeUnsigned32(\substr($string, 8));
+                    // no break
+                    case 8:
+                        $second = \ord($string[7]);
+                        $minute = \ord($string[6]);
+                        $hour = \ord($string[5]);
+                    // no break
+                    case 5:
+                        $day = \ord($string[4]);
+                        $month = \ord($string[3]);
+                        $year = self::decodeUnsigned16(\substr($string, 1));
+                    // no break
+                    case 1:
+                        break;
+
+                    default:
+                        throw new SqlException("Unexpected string length for date in binary protocol: " . ($length - 1));
+                }
+
+                return \str_pad($year, 2, "0", \STR_PAD_LEFT)
+                    . "-" . \str_pad($month, 2, "0", \STR_PAD_LEFT)
+                    . "-" . \str_pad($day, 2, "0", \STR_PAD_LEFT)
+                    . " " . \str_pad($hour, 2, "0", \STR_PAD_LEFT)
+                    . ":" . \str_pad($minute, 2, "0", \STR_PAD_LEFT)
+                    . ":" . \str_pad($second, 2, "0", \STR_PAD_LEFT)
+                    . "." . \str_pad($microsecond, 5, "0", \STR_PAD_LEFT);
+
+            case self::Time:
+                $string = \substr($string, $offset, 13);
+                $negative = $day = $hour = $minute = $second = $microsecond = 0;
+                switch ($length = \ord($string) + 1) {
+                    case 13:
+                        $microsecond = self::decodeUnsigned32(\substr($string, 9));
+                    // no break
+                    case 9:
+                        $second = \ord($string[8]);
+                        $minute = \ord($string[7]);
+                        $hour = \ord($string[6]);
+                        $day = self::decodeUnsigned32(\substr($string, 2));
+                        $negative = \ord($string[1]);
+                    // no break
+                    case 1:
+                        break;
+
+                    default:
+                        throw new SqlException("Unexpected string length for time in binary protocol: " . ($length - 1));
+                }
+
+                return ($negative ? "" : "-") . \str_pad($day, 2, "0", \STR_PAD_LEFT)
+                    . "d " . \str_pad($hour, 2, "0", \STR_PAD_LEFT)
+                    . ":" . \str_pad($minute, 2, "0", \STR_PAD_LEFT)
+                    . ":" . \str_pad($second, 2, "0", \STR_PAD_LEFT)
+                    . "." . \str_pad($microsecond, 5, "0", \STR_PAD_LEFT);
+
+            case self::Null:
+                return null;
+
+            default:
+                throw new SqlException("Invalid type for Binary Protocol: 0x" . \dechex($this->value));
+        }
+    }
+
+    public function decodeText(string $string, int &$offset = 0, int $flags = 0): int|float|string
+    {
+        $length = self::decodeUnsigned($string, $offset);
+        $offset += $length;
+        $data = \substr($string, $offset - $length, $length);
+
+        switch ($this) {
+            case self::LongLong:
+                if ($flags & 0x20) {
+                    return $data; // Return UNSIGNED BIGINT as a string.
+                }
+                // no break
+
+            case self::Long:
+                if (\PHP_INT_SIZE < 8) {
+                    return $data; // Return BIGINT and UNSIGNED INT as string on 32-bit.
+                }
+                // no break
+
+            case self::Int24:
+            case self::Short:
+            case self::Tiny:
+                return (int) $data;
+
+            case self::Double:
+            case self::Float:
+                return (float) $data;
+
+            case self::Json:
+                return self::decodeJson($data);
+
+            default:
+                return $data;
+        }
+    }
+
+    private static function decodeJson(string $data): string
+    {
+        if (\strncmp(self::ENCODED_JSON_PREFIX, $data, \strlen(self::ENCODED_JSON_PREFIX)) !== 0) {
+            return $data; // Data was not base-64 encoded.
+        }
+
+        $data = \substr($data, \strlen(self::ENCODED_JSON_PREFIX));
+        return \base64_decode($data);
+    }
+
+    public static function decodeNullTerminatedString(string $string, int &$offset = 0): string
+    {
+        $length = \strpos($string, "\0", $offset) - $offset;
+        $result = \substr($string, $offset, $length);
+        $offset += $length + 1;
+        return $result;
+    }
+
+    public static function decodeUnsigned(string $string, int &$offset = 0): int|string
+    {
+        $int = \ord($string[$offset]);
+        if ($int < 0xfb) {
+            $offset += 1;
+            return $int;
+        }
+
+        if ($int === 0xfc) {
+            $offset += 3;
+            return self::decodeUnsigned16(\substr($string, $offset - 2, 2));
+        }
+
+        if ($int === 0xfd) {
+            $offset += 4;
+            return self::decodeUnsigned24(\substr($string, $offset - 3, 3));
+        }
+
+        if ($int === 0xfe) {
+            $offset += 9;
+            return self::decodeUnsigned64(\substr($string, $offset - 8, 8));
+        }
+
+        // If that happens connection is borked...
+        throw new SqlException("$int is not in ranges [0x00, 0xfa] or [0xfc, 0xfe]");
+    }
+
+    public static function decodeIntByLength(string $string, int $length, int &$offset = 0): int
+    {
+        $int = 0;
+        while ($length--) {
+            $int = ($int << 8) + \ord($string[$length + $offset]);
+        }
+
+        $offset += $length;
+
+        return $int;
+    }
+
+    public static function decodeInt8(string $string, int &$offset = 0): int
+    {
+        $int = \ord($string[$offset++]);
+        if ($int < (1 << 7)) {
+            return $int;
+        }
+        $shift = \PHP_INT_SIZE * 8 - 8;
+        return $int << $shift >> $shift;
+    }
+
+    public static function decodeUnsigned8(string $string, int &$offset = 0): int
+    {
+        return \ord($string[$offset++]);
+    }
+
+    public static function decodeInt16(string $string, int &$offset = 0): int
+    {
+        $int = \unpack("v", \substr($string, $offset, 2))[1];
+        $offset += 2;
+        if ($int < (1 << 15)) {
+            return $int;
+        }
+        $shift = \PHP_INT_SIZE * 8 - 16;
+        return $int << $shift >> $shift;
+    }
+
+    public static function decodeUnsigned16(string $string, int &$offset = 0): int
+    {
+        $result = \unpack("v", \substr($string, $offset, 2))[1];
+        $offset += 2;
+        return $result;
+    }
+
+    public static function decodeInt24(string $string, int &$offset = 0): int
+    {
+        $int = \unpack("V", \substr($string, $offset, 3) . "\x00")[1];
+        $offset += 3;
+        if ($int < (1 << 23)) {
+            return $int;
+        }
+        $shift = \PHP_INT_SIZE * 8 - 24;
+        return $int << $shift >> $shift;
+    }
+
+    public static function decodeUnsigned24(string $string, int &$offset = 0): int
+    {
+        $result = \unpack("V", \substr($string, $offset, 3) . "\x00")[1];
+        $offset += 3;
+        return $result;
+    }
+
+    public static function decodeInt32(string $string, int &$offset = 0): int
+    {
+        $string = \substr($string, $offset, 4);
+        $offset += 4;
+
+        if (\PHP_INT_SIZE > 4) {
+            $int = \unpack("V", $string)[1];
+            if ($int < (1 << 31)) {
+                return $int;
+            }
+            return $int << 32 >> 32;
+        }
+
+        return \unpack("V", $string)[1];
+    }
+
+    public static function decodeUnsigned32(string $string, int &$offset = 0): int|string
+    {
+        $string = \substr($string, $offset, 4);
+        $offset += 4;
+
+        if (\PHP_INT_SIZE > 4) {
+            return \unpack("V", $string)[1];
+        }
+
+        \assert(\extension_loaded("gmp"), "The GMP extension is required for UNSIGNED INT fields on 32-bit systems");
+        /** @psalm-suppress UndefinedConstant */
+        return \gmp_strval(\gmp_import(\substr($string, 0, 4), 1, \GMP_LSW_FIRST));
+    }
+
+    public static function decodeInt64(string $string, int &$offset = 0): int|string
+    {
+        $string = \substr($string, $offset, 8);
+        $offset += 8;
+
+        if (\PHP_INT_SIZE > 4) {
+            return \unpack("P", $string)[1];
+        }
+
+        \assert(\extension_loaded("gmp"), "The GMP extension is required for BIGINT fields on 32-bit systems");
+        /** @psalm-suppress UndefinedConstant */
+        return \gmp_strval(\gmp_import(\substr($string, 0, 8), 1, \GMP_LSW_FIRST));
+    }
+
+    public static function decodeUnsigned64(string $string, int &$offset = 0): int|string
+    {
+        $string = \substr($string, $offset, 8);
+        $offset += 8;
+
+        \assert(\extension_loaded("gmp"), "The GMP extension is required for UNSIGNED BIGINT fields");
+        /** @psalm-suppress UndefinedConstant */
+        return \gmp_strval(\gmp_import(\substr($string, 0, 8), 1, \GMP_LSW_FIRST));
+    }
+
+    public static function encodeInt(int $int): string
+    {
+        if ($int < 0xfb) {
+            return \chr($int);
+        }
+
+        if ($int < (1 << 16)) {
+            return "\xfc" . self::encodeInt16($int);
+        }
+
+        if ($int < (1 << 24)) {
+            return "\xfd" . self::encodeInt24($int);
+        }
+
+        return "\xfe" . self::encodeInt64($int);
+    }
+
+    public static function encodeInt16(int $int): string
+    {
+        return \pack("v", $int);
+    }
+
+    public static function encodeInt24(int $int): string
+    {
+        return \substr(\pack("V", $int), 0, 3);
+    }
+
+    public static function encodeInt32(int $int): string
+    {
+        return \pack("V", $int);
+    }
+
+    public static function encodeInt64(int $int): string
+    {
+        return \pack("VV", $int & 0xffffffff, $int >> 32);
+    }
+}
diff --git a/src/DataTypes.php b/src/DataTypes.php
deleted file mode 100644
index ac5d397..0000000
--- a/src/DataTypes.php
+++ /dev/null
@@ -1,459 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-use Amp\Sql\SqlException;
-
-/** @see 14.6.4.1.1.1 Column Type */
-final class DataTypes
-{
-    public const MYSQL_TYPE_DECIMAL = 0x00;
-    public const MYSQL_TYPE_TINY = 0x01;
-    public const MYSQL_TYPE_SHORT = 0x02;
-    public const MYSQL_TYPE_LONG = 0x03;
-    public const MYSQL_TYPE_FLOAT = 0x04;
-    public const MYSQL_TYPE_DOUBLE = 0x05;
-    public const MYSQL_TYPE_NULL = 0x06;
-    public const MYSQL_TYPE_TIMESTAMP = 0x07;
-    public const MYSQL_TYPE_LONGLONG = 0x08;
-    public const MYSQL_TYPE_INT24 = 0x09;
-    public const MYSQL_TYPE_DATE = 0x0a;
-    public const MYSQL_TYPE_TIME = 0x0b;
-    public const MYSQL_TYPE_DATETIME = 0x0c;
-    public const MYSQL_TYPE_YEAR = 0x0d;
-    public const MYSQL_TYPE_NEWDATE = 0x0e;
-    public const MYSQL_TYPE_VARCHAR = 0x0f;
-    public const MYSQL_TYPE_BIT = 0x10;
-    public const MYSQL_TYPE_TIMESTAMP2 = 0x11;
-    public const MYSQL_TYPE_DATETIME2 = 0x12;
-    public const MYSQL_TYPE_TIME2 = 0x13;
-    public const MYSQL_TYPE_JSON = 0xf5;
-    public const MYSQL_TYPE_NEWDECIMAL = 0xf6;
-    public const MYSQL_TYPE_ENUM = 0xf7;
-    public const MYSQL_TYPE_SET = 0xf8;
-    public const MYSQL_TYPE_TINY_BLOB = 0xf9;
-    public const MYSQL_TYPE_MEDIUM_BLOB = 0xfa;
-    public const MYSQL_TYPE_LONG_BLOB = 0xfb;
-    public const MYSQL_TYPE_BLOB = 0xfc;
-    public const MYSQL_TYPE_VAR_STRING = 0xfd;
-    public const MYSQL_TYPE_STRING = 0xfe;
-    public const MYSQL_TYPE_GEOMETRY = 0xff;
-
-    private const ENCODED_JSON_PREFIX = "base64:type251:";
-
-    /** @see 14.7.3 Binary Value */
-    public static function encodeBinary(mixed $param): array
-    {
-        $unsigned = 0;
-
-        switch (\gettype($param)) {
-            case "boolean":
-                $type = self::MYSQL_TYPE_TINY;
-                $value = $param ? "\x01" : "\0";
-                break;
-            case "integer":
-                if ($param >= 0) {
-                    $unsigned = 1;
-                }
-                if ($param >= 0 && $param < (1 << 15)) {
-                    $value = self::encodeInt16($param);
-                    $type = self::MYSQL_TYPE_SHORT;
-                } else {
-                    $value = self::encodeInt64($param);
-                    $type = self::MYSQL_TYPE_LONGLONG;
-                }
-                break;
-            case "double":
-                $value = \pack("e", $param);
-                $type = self::MYSQL_TYPE_DOUBLE;
-                break;
-            case "string":
-                $type = self::MYSQL_TYPE_LONG_BLOB;
-                $value = self::encodeInt(\strlen($param)) . $param;
-                break;
-            case "NULL":
-                $type = self::MYSQL_TYPE_NULL;
-                $value = "";
-                break;
-            default:
-                throw new SqlException("Unexpected type for binding parameter: " . \gettype($param));
-        }
-
-        return [$unsigned, $type, $value];
-    }
-
-    /** @see 14.7.3 Binary Protocol Value */
-    public static function decodeBinary(int $type, string $str, ?int &$len = null): mixed
-    {
-        $unsigned = $type & 0x80;
-        switch ($type) {
-            case self::MYSQL_TYPE_STRING:
-            case self::MYSQL_TYPE_VARCHAR:
-            case self::MYSQL_TYPE_VAR_STRING:
-            case self::MYSQL_TYPE_ENUM:
-            case self::MYSQL_TYPE_SET:
-            case self::MYSQL_TYPE_LONG_BLOB:
-            case self::MYSQL_TYPE_MEDIUM_BLOB:
-            case self::MYSQL_TYPE_BLOB:
-            case self::MYSQL_TYPE_TINY_BLOB:
-            case self::MYSQL_TYPE_GEOMETRY:
-            case self::MYSQL_TYPE_BIT:
-            case self::MYSQL_TYPE_DECIMAL:
-            case self::MYSQL_TYPE_NEWDECIMAL:
-                $ret = self::decodeString($str, $intlen, $len);
-                $len += $intlen;
-                return $ret;
-
-            case self::MYSQL_TYPE_JSON:
-                $ret = self::decodeString($str, $intlen, $len);
-                $len += $intlen;
-                return self::decodeJson($ret);
-
-            case self::MYSQL_TYPE_LONGLONG:
-            case self::MYSQL_TYPE_LONGLONG | 0x80:
-                $len = 8;
-                return $unsigned ? self::decodeUnsigned64($str) : self::decodeInt64($str);
-
-            case self::MYSQL_TYPE_LONG:
-            case self::MYSQL_TYPE_LONG | 0x80:
-            case self::MYSQL_TYPE_INT24:
-            case self::MYSQL_TYPE_INT24 | 0x80:
-                $len = 4;
-                return $unsigned ? self::decodeUnsigned32($str) : self::decodeInt32($str);
-
-            case self::MYSQL_TYPE_SHORT:
-            case self::MYSQL_TYPE_SHORT | 0x80:
-                $len = 2;
-                return $unsigned ? self::decodeUnsigned16($str) : self::decodeInt16($str);
-
-            case self::MYSQL_TYPE_TINY:
-            case self::MYSQL_TYPE_TINY | 0x80:
-                $len = 1;
-                return $unsigned ? \ord($str) : self::decodeInt8($str);
-
-            case self::MYSQL_TYPE_DOUBLE:
-                $len = 8;
-                return \unpack("e", $str)[1];
-
-            case self::MYSQL_TYPE_FLOAT:
-                $len = 4;
-                return \unpack("g", $str)[1];
-
-            case self::MYSQL_TYPE_DATE:
-            case self::MYSQL_TYPE_DATETIME:
-            case self::MYSQL_TYPE_TIMESTAMP:
-                $year = $month = $day = $hour = $minute = $second = $microsecond = 0;
-                switch ($len = \ord($str) + 1) {
-                    case 12:
-                        $microsecond = self::decodeUnsigned32(\substr($str, 8));
-                        // no break
-                    case 8:
-                        $second = \ord($str[7]);
-                        $minute = \ord($str[6]);
-                        $hour = \ord($str[5]);
-                        // no break
-                    case 5:
-                        $day = \ord($str[4]);
-                        $month = \ord($str[3]);
-                        $year = self::decodeUnsigned16(\substr($str, 1));
-                        // no break
-                    case 1:
-                        break;
-
-                    default:
-                        throw new SqlException("Unexpected string length for date in binary protocol: " . ($len - 1));
-                }
-
-                return \str_pad($year, 2, "0", STR_PAD_LEFT)
-                    . "-" . \str_pad($month, 2, "0", STR_PAD_LEFT)
-                    . "-" . \str_pad($day, 2, "0", STR_PAD_LEFT)
-                    . " " . \str_pad($hour, 2, "0", STR_PAD_LEFT)
-                    . ":" . \str_pad($minute, 2, "0", STR_PAD_LEFT)
-                    . ":" . \str_pad($second, 2, "0", STR_PAD_LEFT)
-                    . "." . \str_pad($microsecond, 5, "0", STR_PAD_LEFT);
-
-            case self::MYSQL_TYPE_TIME:
-                $negative = $day = $hour = $minute = $second = $microsecond = 0;
-                switch ($len = \ord($str) + 1) {
-                    case 13:
-                        $microsecond = self::decodeUnsigned32(\substr($str, 9));
-                        // no break
-                    case 9:
-                        $second = \ord($str[8]);
-                        $minute = \ord($str[7]);
-                        $hour = \ord($str[6]);
-                        $day = self::decodeUnsigned32(\substr($str, 2));
-                        $negative = \ord($str[1]);
-                        // no break
-                    case 1:
-                        break;
-
-                    default:
-                        throw new SqlException("Unexpected string length for time in binary protocol: " . ($len - 1));
-                }
-
-                return ($negative ? "" : "-") . \str_pad($day, 2, "0", STR_PAD_LEFT)
-                    . "d " . \str_pad($hour, 2, "0", STR_PAD_LEFT)
-                    . ":" . \str_pad($minute, 2, "0", STR_PAD_LEFT)
-                    . ":" . \str_pad($second, 2, "0", STR_PAD_LEFT)
-                    . "." . \str_pad($microsecond, 5, "0", STR_PAD_LEFT);
-
-            case self::MYSQL_TYPE_NULL:
-                $len = 0;
-                return null;
-
-            default:
-                throw new SqlException("Invalid type for Binary Protocol: 0x" . \dechex($type));
-        }
-    }
-
-    public static function decodeNullString(string $str, ?int &$len = null): string
-    {
-        return \substr($str, 0, $len = \strpos($str, "\0"));
-    }
-
-    public static function decodeStringOff(int $type, string $str, int &$off): mixed
-    {
-        $len = self::decodeUnsignedOff($str, $off);
-        $off += $len;
-        $data = (string) \substr($str, $off - $len, $len);
-
-        switch ($type) {
-            case self::MYSQL_TYPE_LONGLONG | 0x80:
-                return $type; // Return UNSIGNED BIGINT as a string.
-
-            case self::MYSQL_TYPE_LONGLONG:
-            case self::MYSQL_TYPE_LONG | 0x80:
-                if (\PHP_INT_SIZE < 8) {
-                    return $type; // Return BIGINT and UNSIGNED INT as string on 32-bit.
-                }
-                // no break
-
-            case self::MYSQL_TYPE_LONG:
-            case self::MYSQL_TYPE_INT24:
-            case self::MYSQL_TYPE_INT24 | 0x80:
-            case self::MYSQL_TYPE_SHORT:
-            case self::MYSQL_TYPE_SHORT | 0x80:
-            case self::MYSQL_TYPE_TINY:
-            case self::MYSQL_TYPE_TINY | 0x80:
-                return (int) $data;
-
-            case self::MYSQL_TYPE_DOUBLE:
-            case self::MYSQL_TYPE_FLOAT:
-                return (float) $data;
-
-            case self::MYSQL_TYPE_JSON:
-                return self::decodeJson($data);
-
-            default:
-                return $data;
-        }
-    }
-
-    private static function decodeJson(string $data): string
-    {
-        if (\strncmp(self::ENCODED_JSON_PREFIX, $data, \strlen(self::ENCODED_JSON_PREFIX)) !== 0) {
-            return $data; // Data was not base-64 encoded.
-        }
-
-        $data = \substr($data, \strlen(self::ENCODED_JSON_PREFIX));
-        return \base64_decode($data);
-    }
-
-    public static function decodeUnsignedOff(string $str, int &$off): int
-    {
-        $int = \ord($str[$off]);
-        if ($int < 0xfb) {
-            $off += 1;
-            return $int;
-        }
-
-        if ($int == 0xfc) {
-            $off += 3;
-            return self::decodeUnsigned16(\substr($str, $off - 2, 2));
-        }
-
-        if ($int == 0xfd) {
-            $off += 4;
-            return self::decodeUnsigned24(\substr($str, $off - 3, 3));
-        }
-
-        if ($int == 0xfe) {
-            $off += 9;
-            return self::decodeUnsigned64(\substr($str, $off - 8, 8));
-        }
-
-        // If that happens connection is borked...
-        throw new SqlException("$int is not in ranges [0x00, 0xfa] or [0xfc, 0xfe]");
-    }
-
-    public static function decodeString(string $str, ?int &$intlen = null, ?int &$len = null): string
-    {
-        $len = self::decodeUnsigned($str, $intlen);
-        return \substr($str, $intlen, $len);
-    }
-
-    public static function decodeUnsigned(string $str, ?int &$len = null): int|string
-    {
-        $int = \ord($str);
-        if ($int < 0xfb) {
-            $len = 1;
-            return $int;
-        }
-
-        if ($int == 0xfc) {
-            $len = 3;
-            return self::decodeUnsigned16(\substr($str, 1, 2));
-        }
-
-        if ($int == 0xfd) {
-            $len = 4;
-            return self::decodeUnsigned24(\substr($str, 1, 4));
-        }
-
-        if ($int == 0xfe) {
-            $len = 9;
-            return self::decodeUnsigned64(\substr($str, 1, 8));
-        }
-
-        // If that happens connection is borked...
-        throw new SqlException("$int is not in ranges [0x00, 0xfa] or [0xfc, 0xfe]");
-    }
-
-    public static function decodeIntByLen(string $str, int $len): int
-    {
-        $int = 0;
-        while ($len--) {
-            $int = ($int << 8) + \ord($str[$len]);
-        }
-        return $int;
-    }
-
-    public static function decodeInt8(string $str): int
-    {
-        $int = \ord($str);
-        if ($int < (1 << 7)) {
-            return $int;
-        }
-        $shift = \PHP_INT_SIZE * 8 - 8;
-        return $int << $shift >> $shift;
-    }
-
-    public static function decodeUnsigned8(string $str): int
-    {
-        return \ord($str);
-    }
-
-    public static function decodeInt16(string $str): int
-    {
-        $int = \unpack("v", $str)[1];
-        if ($int < (1 << 15)) {
-            return $int;
-        }
-        $shift = \PHP_INT_SIZE * 8 - 16;
-        return $int << $shift >> $shift;
-    }
-
-    public static function decodeUnsigned16(string $str): int
-    {
-        return \unpack("v", $str)[1];
-    }
-
-    public static function decodeInt24(string $str): int
-    {
-        $int = \unpack("V", \substr($str, 0, 3) . "\x00")[1];
-        if ($int < (1 << 23)) {
-            return $int;
-        }
-        $shift = \PHP_INT_SIZE * 8 - 24;
-        return $int << $shift >> $shift;
-    }
-
-    public static function decodeUnsigned24(string $str): int
-    {
-        return \unpack("V", \substr($str, 0, 3) . "\x00")[1];
-    }
-
-    public static function decodeInt32($str): int
-    {
-        if (\PHP_INT_SIZE > 4) {
-            $int = \unpack("V", $str)[1];
-            if ($int < (1 << 31)) {
-                return $int;
-            }
-            return $int << 32 >> 32;
-        }
-
-        return \unpack("V", $str)[1];
-    }
-
-    public static function decodeUnsigned32(string $str): int|string
-    {
-        if (\PHP_INT_SIZE > 4) {
-            return \unpack("V", $str)[1];
-        }
-
-        \assert(\extension_loaded("gmp"), "The GMP extension is required for UNSIGNED INT fields on 32-bit systems");
-        /** @psalm-suppress UndefinedConstant */
-        return \gmp_strval(\gmp_import(\substr($str, 0, 4), 1, \GMP_LSW_FIRST));
-    }
-
-    public static function decodeInt64(string $str): int|string
-    {
-        if (\PHP_INT_SIZE > 4) {
-            return \unpack("P", $str)[1];
-        }
-
-        \assert(\extension_loaded("gmp"), "The GMP extension is required for BIGINT fields on 32-bit systems");
-        /** @psalm-suppress UndefinedConstant */
-        return \gmp_strval(\gmp_import(\substr($str, 0, 8), 1, \GMP_LSW_FIRST));
-    }
-
-    public static function decodeUnsigned64(string $str): int|string
-    {
-        \assert(\extension_loaded("gmp"), "The GMP extension is required for UNSIGNED BIGINT fields");
-        /** @psalm-suppress UndefinedConstant */
-        return \gmp_strval(\gmp_import(\substr($str, 0, 8), 1, \GMP_LSW_FIRST));
-    }
-
-    public static function encodeInt(int $int): string
-    {
-        if ($int < 0xfb) {
-            return \chr($int);
-        }
-
-        if ($int < (1 << 16)) {
-            return "\xfc" . self::encodeInt16($int);
-        }
-
-        if ($int < (1 << 24)) {
-            return "\xfd" . self::encodeInt24($int);
-        }
-
-        if ($int < (1 << 62) * 4) {
-            return "\xfe" . self::encodeInt64($int);
-        }
-
-        throw new SqlException("encodeInt doesn't allow integers bigger than 2^64 - 1 (current: $int)");
-    }
-
-    public static function encodeInt16(int $int): string
-    {
-        return \pack("v", $int);
-    }
-
-    public static function encodeInt24(int $int): string
-    {
-        return \substr(\pack("V", $int), 0, 3);
-    }
-
-    public static function encodeInt32(int $int): string
-    {
-        return \pack("V", $int);
-    }
-
-    public static function encodeInt64(int $int): string
-    {
-        return \pack("VV", $int & 0xffffffff, $int >> 32);
-    }
-}
diff --git a/src/Internal/ConnectionState.php b/src/Internal/ConnectionState.php
index 6a9c67c..c06205c 100644
--- a/src/Internal/ConnectionState.php
+++ b/src/Internal/ConnectionState.php
@@ -4,9 +4,9 @@
 
 final class ConnectionState
 {
-    public ?int $affectedRows = null;
-    public ?int $insertId = null;
-    public ?int $statusFlags = null;
+    public int $affectedRows = 0;
+    public int $insertId = 0;
+    public int $statusFlags = 0;
     public ?int $warnings = null;
     public ?string $statusInfo = null;
     public array $sessionState = [];
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index d9bd75b..2cb98ed 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -6,7 +6,7 @@
 use Amp\DeferredFuture;
 use Amp\File;
 use Amp\Future;
-use Amp\Mysql\DataTypes;
+use Amp\Mysql\DataType;
 use Amp\Mysql\InitializationException;
 use Amp\Mysql\MysqlConfig;
 use Amp\Socket\EncryptableSocket;
@@ -21,36 +21,6 @@
  * 14.2.4 COM_CHANGE_USER
  */
 
-/** @see 14.1.3.4 Status Flags */
-final class StatusFlags
-{
-    public const SERVER_STATUS_IN_TRANS = 0x0001; // a transaction is active
-    public const SERVER_STATUS_AUTOCOMMIT = 0x0002; // auto-commit is enabled
-    public const SERVER_MORE_RESULTS_EXISTS = 0x0008;
-    public const SERVER_STATUS_NO_GOOD_INDEX_USED = 0x0010;
-    public const SERVER_STATUS_NO_INDEX_USED = 0x0020;
-    public const SERVER_STATUS_CURSOR_EXISTS = 0x0040; // Used by Binary Protocol Resultset to signal that COM_STMT_FETCH has to be used to fetch the row-data.
-    public const SERVER_STATUS_LAST_ROW_SENT = 0x0080;
-    public const SERVER_STATUS_DB_DROPPED = 0x0100;
-    public const SERVER_STATUS_NO_BACKSLASH_ESCAPES = 0x0200;
-    public const SERVER_STATUS_METADATA_CHANGED = 0x0400;
-    public const SERVER_QUERY_WAS_SLOW = 0x0800;
-    public const SERVER_PS_OUT_PARAMS = 0x1000;
-    public const SERVER_STATUS_IN_TRANS_READONLY = 0x2000; // in a read-only transaction
-    public const SERVER_SESSION_STATE_CHANGED = 0x4000; // connection state information has changed
-}
-
-/** @see 13.1.3.1.1 Session State Information */
-final class SessionStateTypes
-{
-    public const SESSION_TRACK_SYSTEM_VARIABLES = 0x00;
-    public const SESSION_TRACK_SCHEMA = 0x01;
-    public const SESSION_TRACK_STATE_CHANGE = 0x02;
-    public const SESSION_TRACK_GTIDS = 0x03;
-    public const SESSION_TRACK_TRANSACTION_CHARACTERISTICS = 0x04;
-    public const SESSION_TRACK_TRANSACTION_STATE = 0x05;
-}
-
 /** @internal */
 class Processor implements TransientResource
 {
@@ -201,7 +171,9 @@ public function connect(?Cancellation $cancellation = null): void
             $future = $future->finally(static fn () => $cancellation?->unsubscribe($id));
         }
 
-        if ($this->config->getCharset() !== MysqlConfig::DEFAULT_CHARSET || $this->config->getCollation() !== MysqlConfig::DEFAULT_COLLATE) {
+        if ($this->config->getCharset() !== MysqlConfig::DEFAULT_CHARSET
+            || $this->config->getCollation() !== MysqlConfig::DEFAULT_COLLATE
+        ) {
             $future = $future->map(function () {
                 $charset = $this->config->getCharset();
                 $collate = $this->config->getCollation();
@@ -326,9 +298,9 @@ protected function startCommand(\Closure $callback): Future
 
     public function setCharset(string $charset, string $collate = ""): Future
     {
-        if ($collate === "" && false !== $off = \strpos($charset, "_")) {
+        if ($collate === "" && false !== $offset = \strpos($charset, "_")) {
             $collate = $charset;
-            $charset = \substr($collate, 0, $off);
+            $charset = \substr($collate, 0, $offset);
         }
 
         $query = "SET NAMES '$charset'" . ($collate === "" ? "" : " COLLATE '$collate'");
@@ -421,8 +393,8 @@ public function resetConnection(): Future
     public function bindParam(int $stmtId, int $paramId, string $data): void
     {
         $payload = "\x18";
-        $payload .= DataTypes::encodeInt32($stmtId);
-        $payload .= DataTypes::encodeInt16($paramId);
+        $payload .= DataType::encodeInt32($stmtId);
+        $payload .= DataType::encodeInt16($paramId);
         $payload .= $data;
         $this->appendTask(function () use ($payload) {
             $this->resetIds();
@@ -433,15 +405,16 @@ public function bindParam(int $stmtId, int $paramId, string $data): void
 
     /** @see 14.7.6 COM_STMT_EXECUTE */
     // prebound params: null-bit set, type MYSQL_TYPE_LONG_BLOB, no value
-    // $params is by-ref, because the actual result object might not yet have been filled completely with data upon call of this method ...
+    // $params is by-ref, because the actual result object might not yet have been filled completely with data upon
+    // call of this method ...
     public function execute(int $stmtId, string $query, array &$params, array $prebound, array $data = []): Future
     {
         $deferred = new DeferredFuture;
         $this->appendTask(function () use ($stmtId, $query, &$params, $prebound, $data, $deferred): void {
             $payload = "\x17";
-            $payload .= DataTypes::encodeInt32($stmtId);
+            $payload .= DataType::encodeInt32($stmtId);
             $payload .= \chr(0); // cursor flag // @TODO cursor types?!
-            $payload .= DataTypes::encodeInt32(1);
+            $payload .= DataType::encodeInt32(1);
             $paramCount = \count($params);
             $bound = !empty($data) || !empty($prebound);
             $types = "";
@@ -454,18 +427,20 @@ public function execute(int $stmtId, string $query, array &$params, array $prebo
                 $payload .= \str_repeat("\0", ($paramCount + 7) >> 3);
                 foreach ($args as $paramId => $param) {
                     if ($param === null) {
-                        $off = $nullOff + ($paramId >> 3);
-                        $payload[$off] = $payload[$off] | \chr(1 << ($paramId % 8));
+                        $offset = $nullOff + ($paramId >> 3);
+                        $payload[$offset] = $payload[$offset] | \chr(1 << ($paramId % 8));
                     } else {
                         $bound = 1;
                     }
-                    list($unsigned, $type, $value) = DataTypes::encodeBinary($param);
+
+                    /** @var DataType $type */
+                    [$type, $value] = DataType::encodeBinary($param);
                     if (isset($prebound[$paramId])) {
-                        $types .= \chr(DataTypes::MYSQL_TYPE_LONG_BLOB);
+                        $types .= \chr(DataType::LongBlob->value);
                     } else {
-                        $types .= \chr($type);
+                        $types .= \chr($type->value);
                     }
-                    $types .= $unsigned?"\x80":"\0";
+                    $types .= "\0";
                     $values .= $value;
                 }
                 $payload .= \chr($bound);
@@ -489,7 +464,7 @@ public function execute(int $stmtId, string $query, array &$params, array $prebo
     /** @see 14.7.7 COM_STMT_CLOSE */
     public function closeStmt(int $stmtId): void
     {
-        $payload = "\x19" . DataTypes::encodeInt32($stmtId);
+        $payload = "\x19" . DataType::encodeInt32($stmtId);
         $this->appendTask(function () use ($payload): void {
             if ($this->connectionState === self::READY) {
                 $this->resetIds();
@@ -582,7 +557,7 @@ public function processInfo(): Future
     public function killProcess(int $process): Future
     {
         return $this->startCommand(function () use ($process): void {
-            $this->sendPacket("\x0c" . DataTypes::encodeInt32($process));
+            $this->sendPacket("\x0c" . DataType::encodeInt32($process));
         });
     }
 
@@ -597,7 +572,7 @@ public function debugStdout(): Future
     /** @see 14.7.8 COM_STMT_RESET */
     public function resetStmt(int $stmtId): Future
     {
-        $payload = "\x1a" . DataTypes::encodeInt32($stmtId);
+        $payload = "\x1a" . DataType::encodeInt32($stmtId);
         $deferred = new DeferredFuture;
         $this->appendTask(function () use ($payload, $deferred): void {
             $this->resetIds();
@@ -610,7 +585,7 @@ public function resetStmt(int $stmtId): Future
     /** @see 14.8.4 COM_STMT_FETCH */
     public function fetchStmt(int $stmtId): Future
     {
-        $payload = "\x1c" . DataTypes::encodeInt32($stmtId) . DataTypes::encodeInt32(1);
+        $payload = "\x1c" . DataType::encodeInt32($stmtId) . DataType::encodeInt32(1);
         $deferred = new DeferredFuture;
         $this->appendTask(function () use ($payload, $deferred): void {
             $this->resetIds();
@@ -645,24 +620,28 @@ private function established(): void
     /** @see 14.1.3.2 ERR-Packet */
     private function handleError(string $packet): void
     {
-        $off = 1;
+        $offset = 1;
 
-        $this->connInfo->errorCode = DataTypes::decodeUnsigned16(\substr($packet, $off, 2));
-        $off += 2;
+        $this->connInfo->errorCode = DataType::decodeUnsigned16($packet, $offset);
 
         if ($this->capabilities & self::CLIENT_PROTOCOL_41) {
-            $this->connInfo->errorState = \substr($packet, $off, 6);
-            $off += 6;
+            $this->connInfo->errorState = \substr($packet, $offset, 6);
+            $offset += 6;
         }
 
-        $this->connInfo->errorMsg = \substr($packet, $off);
+        $this->connInfo->errorMsg = \substr($packet, $offset);
 
         $this->parseCallback = null;
 
         if ($this->connectionState < self::READY) {
             // connection failure
             $this->close();
-            $this->getDeferred()->error(new InitializationException("Could not connect to {$this->config->getConnectionString()}: {$this->connInfo->errorState} {$this->connInfo->errorMsg}"));
+            $this->getDeferred()->error(new InitializationException(\sprintf(
+                'Could not connect to %s: %s %s',
+                $this->config->getConnectionString(),
+                $this->connInfo->errorState ?? 'Unknown state',
+                $this->connInfo->errorMsg ?? 'Unknown error',
+            )));
             return;
         }
 
@@ -675,9 +654,16 @@ private function handleError(string $packet): void
         $deferred = $this->result ?? $this->getDeferred();
 
         // normal error
-        $exception = new QueryError("MySQL error ({$this->connInfo->errorCode}): {$this->connInfo->errorState} {$this->connInfo->errorMsg}", $this->query);
+        $exception = new QueryError(\sprintf(
+            'MySQL error (%d): %s %s',
+            $this->connInfo->errorCode ?? 0,
+            $this->connInfo->errorState ?? 'Unknown state',
+            $this->connInfo->errorMsg ?? 'Unknown error',
+        ), $this->query);
+
         $this->result = null;
         $this->query = null;
+
         $deferred->error($exception);
 
         $this->ready();
@@ -686,57 +672,57 @@ private function handleError(string $packet): void
     /** @see 14.1.3.1 OK-Packet */
     private function parseOk(string $packet): void
     {
-        $off = 1;
-
-        $this->connInfo->affectedRows = DataTypes::decodeUnsigned(\substr($packet, $off), $intlen);
-        $off += $intlen;
+        $offset = 1;
 
-        $this->connInfo->insertId = DataTypes::decodeUnsigned(\substr($packet, $off), $intlen);
-        $off += $intlen;
+        $this->connInfo->affectedRows = DataType::decodeUnsigned($packet, $offset);
+        $this->connInfo->insertId = DataType::decodeUnsigned($packet, $offset);
 
         if ($this->capabilities & (self::CLIENT_PROTOCOL_41 | self::CLIENT_TRANSACTIONS)) {
-            $this->connInfo->statusFlags = DataTypes::decodeUnsigned16(\substr($packet, $off));
-            $off += 2;
-
-            $this->connInfo->warnings = DataTypes::decodeUnsigned16(\substr($packet, $off));
-            $off += 2;
-        }
-
-        if ($this->capabilities & self::CLIENT_SESSION_TRACK) {
-            // Even though it seems required according to 14.1.3.1, there is no length encoded string, i.e. no trailing NULL byte ....???
-            if (\strlen($packet) > $off) {
-                $this->connInfo->statusInfo = DataTypes::decodeStringOff(DataTypes::MYSQL_TYPE_STRING, $packet, $off);
-
-                if ($this->connInfo->statusFlags & StatusFlags::SERVER_SESSION_STATE_CHANGED) {
-                    $sessionState = DataTypes::decodeString(\substr($packet, $off), $intlen, $sessionStateLen);
-                    $len = 0;
-                    while ($len < $sessionStateLen) {
-                        $data = DataTypes::decodeString(\substr($sessionState, $len + 1), $datalen, $intlen);
-
-                        switch ($type = DataTypes::decodeUnsigned8(\substr($sessionState, $len))) {
-                            case SessionStateTypes::SESSION_TRACK_SYSTEM_VARIABLES:
-                                $var = DataTypes::decodeString($data, $varintlen, $strlen);
-                                $this->connInfo->sessionState[$type][$var] = DataTypes::decodeString(\substr($data, $varintlen + $strlen));
-                                break;
-                            case SessionStateTypes::SESSION_TRACK_SCHEMA:
-                            case SessionStateTypes::SESSION_TRACK_STATE_CHANGE:
-                            case SessionStateTypes::SESSION_TRACK_GTIDS:
-                            case SessionStateTypes::SESSION_TRACK_TRANSACTION_CHARACTERISTICS:
-                            case SessionStateTypes::SESSION_TRACK_TRANSACTION_STATE:
-                                $this->connInfo->sessionState[$type] = DataTypes::decodeString($data);
-                                break;
-                            default:
-                                throw new \Error("$type is not a valid mysql session state type");
-                        }
+            $this->connInfo->statusFlags = DataType::decodeUnsigned16($packet, $offset);
+            $this->connInfo->warnings = DataType::decodeUnsigned16($packet, $offset);
+        }
 
-                        $len += 1 + $intlen + $datalen;
-                    }
-                }
-            } else {
-                $this->connInfo->statusInfo = "";
+        if (!($this->capabilities & self::CLIENT_SESSION_TRACK)) {
+            $this->connInfo->statusInfo = \substr($packet, $offset);
+            return;
+        }
+
+        // Even though it seems required according to 14.1.3.1, there is no length encoded string,
+        // i.e. no trailing NULL byte ....???
+        if (\strlen($packet) <= $offset) {
+            $this->connInfo->statusInfo = "";
+            return;
+        }
+
+        $this->connInfo->statusInfo = DataType::String->decodeText($packet, $offset);
+
+        if (!StatusFlag::SessionStateChanged->inFlags($this->connInfo->statusFlags)) {
+            return;
+        }
+
+        $sessionState = DataType::String->decodeText($packet, $offset);
+
+        while (\strlen($packet) > $offset) {
+            $data = DataType::String->decodeText($sessionState, $offset);
+            $type = DataType::decodeUnsigned8($sessionState, $offset);
+
+            switch (SessionStateType::tryFrom($type)) {
+                case SessionStateType::SystemVariables:
+                    $var = DataType::String->decodeText($data, $offset);
+                    $this->connInfo->sessionState[$type][$var] = DataType::String->decodeText($data, $offset);
+                    break;
+
+                case SessionStateType::Schema:
+                case SessionStateType::StateChange:
+                case SessionStateType::Gtids:
+                case SessionStateType::TransactionCharacteristics:
+                case SessionStateType::TransactionState:
+                    $this->connInfo->sessionState[$type] = DataType::String->decodeText($data, $offset);
+                    break;
+
+                default:
+                    throw new \Error("$type is not a valid mysql session state type");
             }
-        } else {
-            $this->connInfo->statusInfo = \substr($packet, $off);
         }
     }
 
@@ -750,10 +736,10 @@ private function handleOk(string $packet): void
     /** @see 14.1.3.3 EOF-Packet */
     private function parseEof(string $packet): void
     {
+        $offset = 1;
         if ($this->capabilities & self::CLIENT_PROTOCOL_41) {
-            $this->connInfo->warnings = DataTypes::decodeUnsigned16(\substr($packet, 1));
-
-            $this->connInfo->statusFlags = DataTypes::decodeUnsigned16(\substr($packet, 3));
+            $this->connInfo->warnings = DataType::decodeUnsigned16($packet, $offset);
+            $this->connInfo->statusFlags = DataType::decodeUnsigned16($packet, $offset);
         }
     }
 
@@ -768,49 +754,42 @@ private function handleEof(string $packet): void
     /** @see 14.2.5 Connection Phase Packets */
     private function handleHandshake(string $packet): void
     {
-        $off = 1;
+        $offset = 1;
 
         $protocol = \ord($packet);
         if ($protocol !== 0x0a) {
             throw new ConnectionException("Unsupported protocol version ".\ord($packet)." (Expected: 10)");
         }
 
-        $this->connInfo->serverVersion = DataTypes::decodeNullString(\substr($packet, $off), $len);
-        $off += $len + 1;
-
-        $this->connectionId = DataTypes::decodeUnsigned32(\substr($packet, $off));
-        $off += 4;
-
-        $this->authPluginData = \substr($packet, $off, 8);
-        $off += 8;
+        $this->connInfo->serverVersion = DataType::decodeNullTerminatedString($packet, $offset);
 
-        $off += 1; // filler byte
+        $this->connectionId = DataType::decodeUnsigned32($packet, $offset);
 
-        $this->serverCapabilities = DataTypes::decodeUnsigned16(\substr($packet, $off));
-        $off += 2;
+        $this->authPluginData = \substr($packet, $offset, 8);
+        $offset += 8;
 
-        if (\strlen($packet) > $off) {
-            $this->connInfo->charset = \ord(\substr($packet, $off));
-            $off += 1;
+        $offset += 1; // filler byte
 
-            $this->connInfo->statusFlags = DataTypes::decodeUnsigned16(\substr($packet, $off));
-            $off += 2;
+        $this->serverCapabilities = DataType::decodeUnsigned16($packet, $offset);
 
-            $this->serverCapabilities += DataTypes::decodeUnsigned16(\substr($packet, $off)) << 16;
-            $off += 2;
+        if (\strlen($packet) > $offset) {
+            $this->connInfo->charset = DataType::decodeUnsigned8($packet, $offset);
+            $this->connInfo->statusFlags = DataType::decodeUnsigned16($packet, $offset);
+            $this->serverCapabilities += DataType::decodeUnsigned16($packet, $offset) << 16;
 
-            $authPluginDataLen = $this->serverCapabilities & self::CLIENT_PLUGIN_AUTH ? \ord(\substr($packet, $off)) : 0;
-            $off += 1;
+            $authPluginDataLen = $this->serverCapabilities & self::CLIENT_PLUGIN_AUTH
+                ? DataType::decodeUnsigned8($packet, $offset)
+                : 0;
 
             if ($this->serverCapabilities & self::CLIENT_SECURE_CONNECTION) {
-                $off += 10;
+                $offset += 10;
 
                 $strlen = \max(13, $authPluginDataLen - 8);
-                $this->authPluginData .= \substr($packet, $off, $strlen);
-                $off += $strlen;
+                $this->authPluginData .= \substr($packet, $offset, $strlen);
+                $offset += $strlen;
 
                 if ($this->serverCapabilities & self::CLIENT_PLUGIN_AUTH) {
-                    $this->authPluginName = DataTypes::decodeNullString(\substr($packet, $off));
+                    $this->authPluginName = DataType::decodeNullTerminatedString($packet, $offset);
                 }
             }
         }
@@ -821,13 +800,9 @@ private function handleHandshake(string $packet): void
     /** @see 14.2.5 Connection Phase Packets */
     private function handleAuthSwitch($packet)
     {
-        $off = 1;
-
-        $this->authPluginName = DataTypes::decodeNullString(\substr($packet, $off));
-        $off += \strlen($this->authPluginName) + 1;
-
-        $this->authPluginData = \substr($packet, $off);
-
+        $offset = 1;
+        $this->authPluginName = DataType::decodeNullTerminatedString($packet, $offset);
+        $this->authPluginData = \substr($packet, $offset);
         $this->sendAuthSwitchResponse();
     }
 
@@ -865,14 +840,15 @@ private function handleQuery(string $packet): void
         switch (\ord($packet)) {
             case self::OK_PACKET:
                 $this->parseOk($packet);
-                if ($this->connInfo->statusFlags & StatusFlags::SERVER_MORE_RESULTS_EXISTS) {
+
+                if (StatusFlag::MoreResultsExist->inFlags($this->connInfo->statusFlags)) {
                     $result = new ResultProxy;
                     $result->affectedRows = $this->connInfo->affectedRows;
                     $result->insertId = $this->connInfo->insertId;
                     $this->getDeferred()->complete(new ConnectionResult($result));
                     $this->result = $result;
                     $result->updateState(ResultProxy::COLUMNS_FETCHED);
-                    $this->successfulResultsetFetch();
+                    $this->successfulResultFetch();
                 } else {
                     $this->parseCallback = null;
                     $this->getDeferred()->complete(new CommandResult(
@@ -898,7 +874,7 @@ private function handleQuery(string $packet): void
         $this->getDeferred()->complete(new ConnectionResult($result = new ResultProxy));
         /* we need to resolve before assigning vars, so that a onResolve() handler won't have a partial result available */
         $this->result = $result;
-        $result->setColumns(DataTypes::decodeUnsigned($packet));
+        $result->setColumns(DataType::decodeUnsigned($packet));
     }
 
     /** @see 14.7.1 Binary Protocol Resultset */
@@ -929,15 +905,15 @@ private function handleFieldList(string $packet): void
 
     private function handleTextColumnDefinition(string $packet): void
     {
-        $this->handleColumnDefinition($packet, "handleTextResultSetRow");
+        $this->handleColumnDefinition($packet, $this->handleTextResultSetRow(...));
     }
 
     private function handleBinaryColumnDefinition(string $packet): void
     {
-        $this->handleColumnDefinition($packet, "handleBinaryResultSetRow");
+        $this->handleColumnDefinition($packet, $this->handleBinaryResultSetRow(...));
     }
 
-    private function handleColumnDefinition(string $packet, string $cbMethod): void
+    private function handleColumnDefinition(string $packet, \Closure $parseCallback): void
     {
         if (!$this->result->columnsToFetch--) {
             $this->result->updateState(ResultProxy::COLUMNS_FETCHED);
@@ -945,7 +921,7 @@ private function handleColumnDefinition(string $packet, string $cbMethod): void
                 $this->parseCallback = null;
                 $this->handleError($packet);
             } else {
-                $cb = $this->parseCallback = $this->{$cbMethod}(...);
+                $cb = $this->parseCallback = $parseCallback;
                 if ($this->capabilities & self::CLIENT_DEPRECATE_EOF) {
                     $cb($packet);
                 } else {
@@ -993,74 +969,67 @@ private function prepareFields(string $packet): void
     /** @see 14.6.4.1.1.2 Column Defintion */
     private function parseColumnDefinition(string $packet): array
     {
-        $off = 0;
-
+        $offset = 0;
         $column = [];
 
         if ($this->capabilities & self::CLIENT_PROTOCOL_41) {
-            $column["catalog"] = DataTypes::decodeStringOff(DataTypes::MYSQL_TYPE_STRING, $packet, $off);
-            $column["schema"] = DataTypes::decodeStringOff(DataTypes::MYSQL_TYPE_STRING, $packet, $off);
-            $column["table"] = DataTypes::decodeStringOff(DataTypes::MYSQL_TYPE_STRING, $packet, $off);
-            $column["original_table"] = DataTypes::decodeStringOff(DataTypes::MYSQL_TYPE_STRING, $packet, $off);
-            $column["name"] = DataTypes::decodeStringOff(DataTypes::MYSQL_TYPE_STRING, $packet, $off);
-            $column["original_name"] = DataTypes::decodeStringOff(DataTypes::MYSQL_TYPE_STRING, $packet, $off);
-            $fixlen = DataTypes::decodeUnsignedOff($packet, $off);
-
-            $len = 0;
-            $column["charset"] = DataTypes::decodeUnsigned16(\substr($packet, $off + $len));
-            $len += 2;
-            $column["columnlen"] = DataTypes::decodeUnsigned32(\substr($packet, $off + $len));
-            $len += 4;
-            $column["type"] = \ord($packet[$off + $len]);
-            $len += 1;
-            $column["flags"] = DataTypes::decodeUnsigned16(\substr($packet, $off + $len));
-            $len += 2;
-            $column["decimals"] = \ord($packet[$off + $len]);
-            //$len += 1;
-
-            $off += $fixlen;
+            $column["catalog"] = DataType::String->decodeText($packet, $offset);
+            $column["schema"] = DataType::String->decodeText($packet, $offset);
+            $column["table"] = DataType::String->decodeText($packet, $offset);
+            $column["original_table"] = DataType::String->decodeText($packet, $offset);
+            $column["name"] = DataType::String->decodeText($packet, $offset);
+            $column["original_name"] = DataType::String->decodeText($packet, $offset);
+            $fixLength = DataType::decodeUnsigned($packet, $offset);
+
+            $column["charset"] = DataType::decodeUnsigned16($packet, $offset);
+            $column["columnlen"] = DataType::decodeUnsigned32($packet, $offset);
+            $column["type"] = DataType::decodeUnsigned8($packet, $offset);
+            $column["flags"] = DataType::decodeUnsigned16($packet, $offset);
+            $column["decimals"] = DataType::decodeUnsigned8($packet, $offset);
+
+            $offset += $fixLength;
         } else {
-            $column["table"] = DataTypes::decodeStringOff(DataTypes::MYSQL_TYPE_STRING, $packet, $off);
-            $column["name"] = DataTypes::decodeStringOff(DataTypes::MYSQL_TYPE_STRING, $packet, $off);
+            $column["table"] = DataType::String->decodeText($packet, $offset);
+            $column["name"] = DataType::String->decodeText($packet, $offset);
 
-            $collen = DataTypes::decodeUnsignedOff($packet, $off);
-            $column["columnlen"] = DataTypes::decodeIntByLen(\substr($packet, $off), $collen);
-            $off += $collen;
+            $columnLength = DataType::decodeUnsigned($packet, $offset);
+            $column["columnlen"] = DataType::decodeIntByLength($packet, $columnLength, $offset);
 
-            $typelen = DataTypes::decodeUnsignedOff($packet, $off);
-            $column["type"] = DataTypes::decodeIntByLen(\substr($packet, $off), $typelen);
-            $off += $typelen;
+            $typeLength = DataType::decodeUnsigned($packet, $offset);
+            $column["type"] = $type = DataType::decodeIntByLength($packet, $typeLength, $offset);
+            if ($type & 0x80) {
+                throw new \Exception($column["name"]);
+            }
 
-            $len = 1;
-            $flaglen = $this->capabilities & self::CLIENT_LONG_FLAG ? DataTypes::decodeUnsigned(\substr($packet, $off, 9), $len) : \ord($packet[$off]);
-            $off += $len;
+            $flagLength = $this->capabilities & self::CLIENT_LONG_FLAG
+                ? DataType::decodeUnsigned($packet, $offset)
+                : \ord($packet[$offset]);
 
-            if ($flaglen > 2) {
-                $len = 2;
-                $column["flags"] = DataTypes::decodeUnsigned16(\substr($packet, $off, 4));
+            if ($flagLength > 2) {
+                $column["flags"] = DataType::decodeUnsigned16($packet, $offset);
             } else {
-                $len = 1;
-                $column["flags"] = \ord($packet[$off]);
+                $column["flags"] = DataType::decodeUnsigned8($packet, $offset);
             }
-            $column["decimals"] = \ord($packet[$off + $len]);
-            $off += $flaglen;
+
+            $column["decimals"] = DataType::decodeUnsigned8($packet, $offset);
         }
 
-        if ($off < \strlen($packet)) {
-            $column["defaults"] = DataTypes::decodeString(\substr($packet, $off));
+        if ($offset < \strlen($packet)) {
+            $column["defaults"] = DataType::String->decodeText($packet, $offset);
         }
 
         return $column;
     }
 
-    private function successfulResultsetFetch(): void
+    private function successfulResultFetch(): void
     {
         $result = $this->result;
         $deferred = &$result->next;
         if (!$deferred) {
             $deferred = new DeferredFuture;
         }
-        if ($this->connInfo->statusFlags & StatusFlags::SERVER_MORE_RESULTS_EXISTS) {
+
+        if (StatusFlag::MoreResultsExist->inFlags($this->connInfo->statusFlags)) {
             $this->parseCallback = $this->handleQuery(...);
             $this->addDeferred($deferred);
         } else {
@@ -1070,75 +1039,83 @@ private function successfulResultsetFetch(): void
             $deferred->complete();
             $this->ready();
         }
+
         $result->updateState(ResultProxy::ROWS_FETCHED);
     }
 
     /** @see 14.6.4.1.1.3 Resultset Row */
     private function handleTextResultSetRow(string $packet): void
     {
-        $packettype = \ord($packet);
-        if ($packettype === self::EOF_PACKET) {
+        $packetType = \ord($packet);
+        if ($packetType === self::EOF_PACKET) {
             if ($this->capabilities & self::CLIENT_DEPRECATE_EOF) {
                 $this->parseOk($packet);
             } else {
                 $this->parseEof($packet);
             }
-            $this->successfulResultsetFetch();
+            $this->successfulResultFetch();
             return;
-        } elseif ($packettype === self::ERR_PACKET) {
+        }
+
+        if ($packetType === self::ERR_PACKET) {
             $this->handleError($packet);
             return;
         }
 
-        $off = 0;
+        $offset = 0;
         $columns = $this->result->columns;
 
         $fields = [];
-        for ($i = 0; $off < \strlen($packet); ++$i) {
-            if (\ord($packet[$off]) === 0xfb) {
+        for ($i = 0; $offset < \strlen($packet); ++$i) {
+            if (\ord($packet[$offset]) === 0xfb) {
                 $fields[] = null;
-                $off += 1;
+                $offset += 1;
             } else {
-                $fields[] = DataTypes::decodeStringOff($columns[$i]["type"], $packet, $off);
+                $type = DataType::from($columns[$i]["type"]);
+                $flags = $columns[$i]["flags"];
+                $fields[] = $type->decodeText($packet, $offset, $flags);
             }
         }
+
         $this->result->rowFetched($fields);
     }
 
     /** @see 14.7.2 Binary Protocol Resultset Row */
     private function handleBinaryResultSetRow(string $packet): void
     {
-        $packettype = \ord($packet);
-        if ($packettype === self::EOF_PACKET) {
+        $packetType = \ord($packet);
+        if ($packetType === self::EOF_PACKET) {
             $this->parseEof($packet);
-            $this->successfulResultsetFetch();
+            $this->successfulResultFetch();
             return;
-        } elseif ($packettype === self::ERR_PACKET) {
+        } elseif ($packetType === self::ERR_PACKET) {
             $this->handleError($packet);
             return;
         }
 
-        $off = 1; // skip first byte
+        $offset = 1; // skip first byte
 
         $columnCount = $this->result->columnCount;
         $columns = $this->result->columns;
         $fields = [];
 
         for ($i = 0; $i < $columnCount; $i++) {
-            if (\ord($packet[$off + (($i + 2) >> 3)]) & (1 << (($i + 2) % 8))) {
+            if (\ord($packet[$offset + (($i + 2) >> 3)]) & (1 << (($i + 2) % 8))) {
                 $fields[$i] = null;
             }
         }
-        $off += ($columnCount + 9) >> 3;
+        $offset += ($columnCount + 9) >> 3;
 
-        for ($i = 0; $off < \strlen($packet); $i++) {
+        for ($i = 0; $offset < \strlen($packet); $i++) {
             while (\array_key_exists($i, $fields)) {
                 $i++;
             }
-            $fields[$i] = DataTypes::decodeBinary($columns[$i]["type"], \substr($packet, $off), $len);
-            $off += $len;
+
+            $type = DataType::from($columns[$i]["type"]);
+            $flags = $columns[$i]["flags"];
+            $fields[$i] = $type->decodeBinary($packet, $offset, $flags);
         }
-        \ksort($fields);
+
         $this->result->rowFetched($fields);
     }
 
@@ -1154,20 +1131,16 @@ private function handlePrepare(string $packet): void
             default:
                 throw new ConnectionException("Unexpected value for first byte of COM_STMT_PREPARE Response");
         }
-        $off = 1;
-
-        $stmtId = DataTypes::decodeUnsigned32(\substr($packet, $off));
-        $off += 4;
 
-        $columns = DataTypes::decodeUnsigned16(\substr($packet, $off));
-        $off += 2;
+        $offset = 1;
 
-        $params = DataTypes::decodeUnsigned16(\substr($packet, $off));
-        $off += 2;
+        $stmtId = DataType::decodeUnsigned32($packet, $offset);
+        $columns = DataType::decodeUnsigned16($packet, $offset);
+        $params = DataType::decodeUnsigned16($packet, $offset);
 
-        $off += 1; // filler
+        $offset += 1; // filler
 
-        $this->connInfo->warnings = DataTypes::decodeUnsigned16(\substr($packet, $off));
+        $this->connInfo->warnings = DataType::decodeUnsigned16($packet, $offset);
 
         $this->result = new ResultProxy;
         $this->result->columnsToFetch = $params;
@@ -1251,16 +1224,16 @@ private function compilePacket(string $pending): string
     {
         $packet = "";
         do {
-            $len = \strlen($pending);
-            if ($len >= (1 << 24) - 1) {
+            $length = \strlen($pending);
+            if ($length >= (1 << 24) - 1) {
                 $out = \substr($pending, 0, (1 << 24) - 1);
                 $pending = \substr($pending, (1 << 24) - 1);
-                $len = (1 << 24) - 1;
+                $length = (1 << 24) - 1;
             } else {
                 $out = $pending;
                 $pending = "";
             }
-            $packet .= \substr_replace(\pack("V", $len), \chr(++$this->seqId), 3, 1) . $out; // expects $len < (1 << 24) - 1
+            $packet .= \substr_replace(\pack("V", $length), \chr(++$this->seqId), 3, 1) . $out; // expects $length < (1 << 24) - 1
         } while ($pending !== "");
 
         return $packet;
@@ -1272,47 +1245,48 @@ private function compressPacket(string $packet): string
             return "";
         }
 
-        $len = \strlen($packet);
+        $length = \strlen($packet);
         $deflated = \zlib_encode($packet, ZLIB_ENCODING_DEFLATE);
 
-        if ($len < \strlen($deflated)) {
+        if ($length < \strlen($deflated)) {
             return \substr_replace(\pack("V", \strlen($packet)), \chr(++$this->compressionId), 3, 1) . "\0\0\0" . $packet;
         }
 
-        return \substr_replace(\pack("V", \strlen($deflated)), \chr(++$this->compressionId), 3, 1) . \substr(\pack("V", $len), 0, 3) . $deflated;
+        return \substr_replace(\pack("V", \strlen($deflated)), \chr(++$this->compressionId), 3, 1)
+            . \substr(\pack("V", $length), 0, 3) . $deflated;
     }
 
     /** @see 14.4 Compression */
     private function parseCompression(): \Generator
     {
         $inflated = "";
-        $buf = "";
+        $buffer = "";
 
         while (true) {
-            while (\strlen($buf) < 7) {
-                $buf .= yield $inflated;
+            while (\strlen($buffer) < 7) {
+                $buffer .= yield $inflated;
                 $inflated = "";
             }
 
-            $size = DataTypes::decodeUnsigned24($buf);
-            $this->compressionId = \ord($buf[3]);
-            $uncompressed = DataTypes::decodeUnsigned24(\substr($buf, 4, 3));
+            $size = DataType::decodeUnsigned24($buffer);
+            $this->compressionId = \ord($buffer[3]);
+            $uncompressed = DataType::decodeUnsigned24(\substr($buffer, 4, 3));
 
-            $buf = \substr($buf, 7);
+            $buffer = \substr($buffer, 7);
 
             if ($size > 0) {
-                while (\strlen($buf) < $size) {
-                    $buf .= yield $inflated;
+                while (\strlen($buffer) < $size) {
+                    $buffer .= yield $inflated;
                     $inflated = "";
                 }
 
                 if ($uncompressed === 0) {
-                    $inflated .= \substr($buf, 0, $size);
+                    $inflated .= \substr($buffer, 0, $size);
                 } else {
-                    $inflated .= \zlib_decode(\substr($buf, 0, $size), $uncompressed);
+                    $inflated .= \zlib_decode(\substr($buffer, 0, $size), $uncompressed);
                 }
 
-                $buf = \substr($buf, $size);
+                $buffer = \substr($buffer, $size);
             }
         }
     }
@@ -1323,36 +1297,36 @@ private function parseCompression(): \Generator
      */
     private function parseMysql(): \Generator
     {
-        $buf = "";
+        $buffer = "";
         $parsed = [];
 
         while (true) {
             $packet = "";
 
             do {
-                while (\strlen($buf) < 4) {
-                    $buf .= yield $parsed;
+                while (\strlen($buffer) < 4) {
+                    $buffer .= yield $parsed;
                     $parsed = [];
                 }
 
-                $len = DataTypes::decodeUnsigned24($buf);
-                $this->seqId = \ord($buf[3]);
-                $buf = \substr($buf, 4);
+                $length = DataType::decodeUnsigned24($buffer);
+                $this->seqId = \ord($buffer[3]);
+                $buffer = \substr($buffer, 4);
 
-                while (\strlen($buf) < ($len & 0xffffff)) {
-                    $buf .= yield $parsed;
+                while (\strlen($buffer) < ($length & 0xffffff)) {
+                    $buffer .= yield $parsed;
                     $parsed = [];
                 }
 
-                $lastIn = $len !== 0xffffff;
+                $lastIn = $length !== 0xffffff;
                 if ($lastIn) {
-                    $size = $len % 0xffffff;
+                    $size = $length % 0xffffff;
                 } else {
                     $size = 0xffffff;
                 }
 
-                $packet .= \substr($buf, 0, $size);
-                $buf = \substr($buf, $size);
+                $packet .= \substr($buffer, 0, $size);
+                $buffer = \substr($buffer, $size);
             } while (!$lastIn);
 
             if (\strlen($packet) > 0) {
@@ -1398,7 +1372,8 @@ private function parsePayload(string $packet): void
                                 case 3: // success
                                     return; // expecting OK afterwards
                                 case 4: // fast auth failure
-                                    if ($this->capabilities & self::CLIENT_SSL || $this->config->getHost()[0] === "/" /* unix domain socket, information not trivially available from $this->socket */) {
+                                    /* unix domain socket, information not trivially available from $this->socket */
+                                    if ($this->capabilities & self::CLIENT_SSL || $this->config->getHost()[0] === "/") {
                                         $this->write($this->config->getPassword() . "\0");
                                     } else {
                                         $this->write("\x02");
@@ -1411,7 +1386,9 @@ private function parsePayload(string $packet): void
                             }
                             break;
                         default:
-                            throw new ConnectionException("Unexpected EXTRA_AUTH_PACKET in authentication phase for method {$this->authPluginName}");
+                            throw new ConnectionException(
+                                "Unexpected EXTRA_AUTH_PACKET in authentication phase for method {$this->authPluginName}"
+                            );
                     }
                     break;
             }
@@ -1455,7 +1432,13 @@ private function secureAuth(string $pass, string $scramble): string
 
     private function sha256Auth(string $pass, string $scramble, string $key): string
     {
-        \openssl_public_encrypt("$pass\0" ^ \str_repeat($scramble, \ceil(\strlen($pass) / \strlen($scramble))), $auth, $key, OPENSSL_PKCS1_OAEP_PADDING);
+        \openssl_public_encrypt(
+            "$pass\0" ^ \str_repeat($scramble, \ceil(\strlen($pass) / \strlen($scramble))),
+            $auth,
+            $key,
+            OPENSSL_PKCS1_OAEP_PADDING,
+        );
+
         return $auth;
     }
 
@@ -1473,9 +1456,9 @@ private function authSwitchRequest(string $packet): void
                 if (\strlen($packet) === 1) {
                     break;
                 }
-                $len = \strpos($packet, "\0");
-                $pluginName = \substr($packet, 0, $len); // @TODO mysql_native_pass only now...
-                $authPluginData = \substr($packet, $len + 1);
+                $length = \strpos($packet, "\0");
+                $pluginName = \substr($packet, 0, $length); // @TODO mysql_native_pass only now...
+                $authPluginData = \substr($packet, $length + 1);
                 $this->sendPacket($this->secureAuth($this->config->getPassword(), $authPluginData));
                 break;
             case self::ERR_PACKET:
@@ -1529,7 +1512,7 @@ private function sendHandshake(bool $inSSL = false): void
 
         $auth = $this->getAuthData();
         if ($this->capabilities & self::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) {
-            $payload .= DataTypes::encodeInt(\strlen($auth));
+            $payload .= DataType::encodeInt(\strlen($auth));
             $payload .= $auth;
         } elseif ($this->capabilities & self::CLIENT_SECURE_CONNECTION) {
             $payload .= \chr(\strlen($auth));
@@ -1570,9 +1553,13 @@ private function getAuthData(): ?string
                 case "caching_sha2_password":
                     return $this->sha2Auth($this->config->getPassword(), $this->authPluginData);
                 case "mysql_old_password":
-                    throw new ConnectionException("mysql_old_password is outdated and insecure. Intentionally not implemented!");
+                    throw new ConnectionException(
+                        "mysql_old_password is outdated and insecure. Intentionally not implemented!"
+                    );
                 default:
-                    throw new ConnectionException("Invalid (or unimplemented?) auth method requested by server: {$this->authPluginName}");
+                    throw new ConnectionException(
+                        "Invalid (or unimplemented?) auth method requested by server: {$this->authPluginName}"
+                    );
             }
         }
 
diff --git a/src/Internal/SessionStateType.php b/src/Internal/SessionStateType.php
new file mode 100644
index 0000000..6252d52
--- /dev/null
+++ b/src/Internal/SessionStateType.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Amp\Mysql\Internal;
+
+/** @see 13.1.3.1.1 Session State Information */
+enum SessionStateType: int
+{
+    public const SystemVariables = 0x00;
+    public const Schema = 0x01;
+    public const StateChange = 0x02;
+    public const Gtids = 0x03;
+    public const TransactionCharacteristics = 0x04;
+    public const TransactionState = 0x05;
+}
diff --git a/src/Internal/StatusFlag.php b/src/Internal/StatusFlag.php
new file mode 100644
index 0000000..de59a6d
--- /dev/null
+++ b/src/Internal/StatusFlag.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Amp\Mysql\Internal;
+
+/** @see 14.1.3.4 Status Flags */
+enum StatusFlag: int
+{
+    case InTransaction = 0x0001; // a transaction is active
+    case AutoCommit = 0x0002; // auto-commit is enabled
+    case MoreResultsExist = 0x0008;
+    case NoGoodIndexUsed = 0x0010;
+    case NoIndexUsed = 0x0020;
+    case CursorExists = 0x0040; // Used by Binary Protocol Result to signal that COM_STMT_FETCH must be used to fetch row-data.
+    case LastRowSent = 0x0080;
+    case DbDropped = 0x0100;
+    case NoBackslashEscapes = 0x0200;
+    case MetadataChanged = 0x0400;
+    case ServerQueryWasSlow = 0x0800;
+    case PsOutParams = 0x1000;
+    case InReadonlyTransaction = 0x2000; // in a read-only transaction
+    case SessionStateChanged = 0x4000; // connection state information has changed
+
+    public function inFlags(int $flags): bool
+    {
+        return (bool) ($this->value & $flags);
+    }
+}
diff --git a/src/RefreshTypes.php b/src/RefreshTypes.php
deleted file mode 100644
index 399284e..0000000
--- a/src/RefreshTypes.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-final class RefreshTypes
-{
-    public const REFRESH_GRANT = 0x01;
-    public const REFRESH_LOG = 0x02;
-    public const REFRESH_TABLES = 0x04;
-    public const REFRESH_HOSTS = 0x08;
-    public const REFRESH_STATUS = 0x10;
-    public const REFRESH_THREADS = 0x20;
-    public const REFRESH_SLAVE = 0x40;
-    public const REFRESH_MASTER = 0x80;
-}
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 99a0e6a..4e23c76 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -2,7 +2,7 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Mysql\DataTypes;
+use Amp\Mysql\DataType;
 use Amp\Mysql\Link;
 use Amp\Mysql\Result;
 use Amp\PHPUnit\AsyncTestCase;
@@ -90,9 +90,9 @@ public function testMultiStmt()
         $this->assertCount(2, $fields);
         $this->assertSame($fields[0]["original_name"], "b");
         $this->assertSame($fields[0]["name"], "d");
-        $this->assertSame($fields[0]["type"], DataTypes::MYSQL_TYPE_LONG);
+        $this->assertSame($fields[0]["type"], DataType::Long->value);
         $this->assertSame($fields[1]["name"], "c");
-        $this->assertSame($fields[1]["type"], DataTypes::MYSQL_TYPE_LONGLONG);
+        $this->assertSame($fields[1]["type"], DataType::LongLong->value);
 
         $this->assertNull($resultset->getNextResult());
     }

From 7c970e23fb92ea58fb7e2bae47901e2ef58c905f Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 24 Apr 2022 10:48:19 -0500
Subject: [PATCH 062/174] Add ColumnDefinition

---
 src/ColumnDefinition.php             | 22 +++++++++++++++
 src/Internal/CommandResult.php       |  2 +-
 src/Internal/ConnectionResult.php    |  2 +-
 src/Internal/ConnectionStatement.php |  2 +-
 src/Internal/Processor.php           | 42 +++++++++++-----------------
 src/Internal/ResultProxy.php         |  2 ++
 src/PooledResult.php                 |  4 +--
 src/PooledStatement.php              |  4 +--
 src/Result.php                       |  4 +--
 src/Statement.php                    |  5 +++-
 src/StatementPool.php                |  4 +--
 test/LinkTest.php                    | 33 ++++++++++++----------
 12 files changed, 73 insertions(+), 53 deletions(-)
 create mode 100644 src/ColumnDefinition.php

diff --git a/src/ColumnDefinition.php b/src/ColumnDefinition.php
new file mode 100644
index 0000000..ef6c9b6
--- /dev/null
+++ b/src/ColumnDefinition.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Amp\Mysql;
+
+final class ColumnDefinition
+{
+    public function __construct(
+        public readonly string $table,
+        public readonly string $name,
+        public readonly int $length,
+        public readonly DataType $type,
+        public readonly int $flags,
+        public readonly int $decimals,
+        public readonly string $defaults = '',
+        public readonly ?string $originalTable = null,
+        public readonly ?string $originalName = null,
+        public readonly ?int $charset = null,
+        public readonly ?string $catalog = null,
+        public readonly ?string $schema = null,
+    ) {
+    }
+}
diff --git a/src/Internal/CommandResult.php b/src/Internal/CommandResult.php
index 611dc38..00122ee 100644
--- a/src/Internal/CommandResult.php
+++ b/src/Internal/CommandResult.php
@@ -46,7 +46,7 @@ public function getNextResult(): ?Result
         return $this->delegate->getNextResult();
     }
 
-    public function getFields(): ?array
+    public function getColumnDefinitions(): ?array
     {
         return null; // Command results do not have a field list.
     }
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index ef997c1..6e26340 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -128,7 +128,7 @@ public function getLastInsertId(): ?int
         return $this->result->insertId;
     }
 
-    public function getFields(): ?array
+    public function getColumnDefinitions(): ?array
     {
         if ($this->result->state >= ResultProxy::COLUMNS_FETCHED) {
             return $this->result->columns;
diff --git a/src/Internal/ConnectionStatement.php b/src/Internal/ConnectionStatement.php
index 8e10250..01ef665 100644
--- a/src/Internal/ConnectionStatement.php
+++ b/src/Internal/ConnectionStatement.php
@@ -144,7 +144,7 @@ public function reset(): void
             ->await();
     }
 
-    public function getFields(): ?array
+    public function getColumnDefinitions(): ?array
     {
         if ($this->result->state >= ResultProxy::COLUMNS_FETCHED) {
             return $this->result->columns;
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 2cb98ed..a6c70be 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -6,6 +6,7 @@
 use Amp\DeferredFuture;
 use Amp\File;
 use Amp\Future;
+use Amp\Mysql\ColumnDefinition;
 use Amp\Mysql\DataType;
 use Amp\Mysql\InitializationException;
 use Amp\Mysql\MysqlConfig;
@@ -967,7 +968,7 @@ private function prepareFields(string $packet): void
     }
 
     /** @see 14.6.4.1.1.2 Column Defintion */
-    private function parseColumnDefinition(string $packet): array
+    private function parseColumnDefinition(string $packet): ColumnDefinition
     {
         $offset = 0;
         $column = [];
@@ -976,14 +977,14 @@ private function parseColumnDefinition(string $packet): array
             $column["catalog"] = DataType::String->decodeText($packet, $offset);
             $column["schema"] = DataType::String->decodeText($packet, $offset);
             $column["table"] = DataType::String->decodeText($packet, $offset);
-            $column["original_table"] = DataType::String->decodeText($packet, $offset);
+            $column["originalTable"] = DataType::String->decodeText($packet, $offset);
             $column["name"] = DataType::String->decodeText($packet, $offset);
-            $column["original_name"] = DataType::String->decodeText($packet, $offset);
+            $column["originalName"] = DataType::String->decodeText($packet, $offset);
             $fixLength = DataType::decodeUnsigned($packet, $offset);
 
             $column["charset"] = DataType::decodeUnsigned16($packet, $offset);
-            $column["columnlen"] = DataType::decodeUnsigned32($packet, $offset);
-            $column["type"] = DataType::decodeUnsigned8($packet, $offset);
+            $column["length"] = DataType::decodeUnsigned32($packet, $offset);
+            $column["type"] = DataType::from(DataType::decodeUnsigned8($packet, $offset));
             $column["flags"] = DataType::decodeUnsigned16($packet, $offset);
             $column["decimals"] = DataType::decodeUnsigned8($packet, $offset);
 
@@ -993,17 +994,14 @@ private function parseColumnDefinition(string $packet): array
             $column["name"] = DataType::String->decodeText($packet, $offset);
 
             $columnLength = DataType::decodeUnsigned($packet, $offset);
-            $column["columnlen"] = DataType::decodeIntByLength($packet, $columnLength, $offset);
+            $column["length"] = DataType::decodeIntByLength($packet, $columnLength, $offset);
 
             $typeLength = DataType::decodeUnsigned($packet, $offset);
-            $column["type"] = $type = DataType::decodeIntByLength($packet, $typeLength, $offset);
-            if ($type & 0x80) {
-                throw new \Exception($column["name"]);
-            }
+            $column["type"] = DataType::from(DataType::decodeIntByLength($packet, $typeLength, $offset));
 
             $flagLength = $this->capabilities & self::CLIENT_LONG_FLAG
                 ? DataType::decodeUnsigned($packet, $offset)
-                : \ord($packet[$offset]);
+                : DataType::decodeUnsigned8($packet, $offset);
 
             if ($flagLength > 2) {
                 $column["flags"] = DataType::decodeUnsigned16($packet, $offset);
@@ -1018,7 +1016,7 @@ private function parseColumnDefinition(string $packet): array
             $column["defaults"] = DataType::String->decodeText($packet, $offset);
         }
 
-        return $column;
+        return new ColumnDefinition(...$column);
     }
 
     private function successfulResultFetch(): void
@@ -1063,17 +1061,14 @@ private function handleTextResultSetRow(string $packet): void
         }
 
         $offset = 0;
-        $columns = $this->result->columns;
-
         $fields = [];
         for ($i = 0; $offset < \strlen($packet); ++$i) {
             if (\ord($packet[$offset]) === 0xfb) {
                 $fields[] = null;
                 $offset += 1;
             } else {
-                $type = DataType::from($columns[$i]["type"]);
-                $flags = $columns[$i]["flags"];
-                $fields[] = $type->decodeText($packet, $offset, $flags);
+                $column = $this->result->columns[$i];
+                $fields[] = $column->type->decodeText($packet, $offset, $column->flags);
             }
         }
 
@@ -1094,26 +1089,21 @@ private function handleBinaryResultSetRow(string $packet): void
         }
 
         $offset = 1; // skip first byte
-
-        $columnCount = $this->result->columnCount;
-        $columns = $this->result->columns;
         $fields = [];
-
-        for ($i = 0; $i < $columnCount; $i++) {
+        for ($i = 0; $i < $this->result->columnCount; $i++) {
             if (\ord($packet[$offset + (($i + 2) >> 3)]) & (1 << (($i + 2) % 8))) {
                 $fields[$i] = null;
             }
         }
-        $offset += ($columnCount + 9) >> 3;
+        $offset += ($this->result->columnCount + 9) >> 3;
 
         for ($i = 0; $offset < \strlen($packet); $i++) {
             while (\array_key_exists($i, $fields)) {
                 $i++;
             }
 
-            $type = DataType::from($columns[$i]["type"]);
-            $flags = $columns[$i]["flags"];
-            $fields[$i] = $type->decodeBinary($packet, $offset, $flags);
+            $column = $this->result->columns[$i];
+            $fields[$i] = $column->type->decodeBinary($packet, $offset, $column->flags);
         }
 
         $this->result->rowFetched($fields);
diff --git a/src/Internal/ResultProxy.php b/src/Internal/ResultProxy.php
index 03d1079..ad86963 100644
--- a/src/Internal/ResultProxy.php
+++ b/src/Internal/ResultProxy.php
@@ -3,10 +3,12 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\DeferredFuture;
+use Amp\Mysql\ColumnDefinition;
 
 final class ResultProxy
 {
     public int $columnCount;
+    /** @var list<ColumnDefinition> */
     public array $columns = [];
     public array $params = [];
     public int $columnsToFetch;
diff --git a/src/PooledResult.php b/src/PooledResult.php
index 74d9298..0921d9f 100644
--- a/src/PooledResult.php
+++ b/src/PooledResult.php
@@ -29,8 +29,8 @@ public function getLastInsertId(): ?int
         return $this->result->getLastInsertId();
     }
 
-    public function getFields(): ?array
+    public function getColumnDefinitions(): ?array
     {
-        return $this->result->getFields();
+        return $this->result->getColumnDefinitions();
     }
 }
diff --git a/src/PooledStatement.php b/src/PooledStatement.php
index 42e6121..0bf96da 100644
--- a/src/PooledStatement.php
+++ b/src/PooledStatement.php
@@ -34,9 +34,9 @@ public function bind(int|string $paramId, mixed $data): void
         $this->statement->bind($paramId, $data);
     }
 
-    public function getFields(): ?array
+    public function getColumnDefinitions(): ?array
     {
-        return $this->statement->getFields();
+        return $this->statement->getColumnDefinitions();
     }
 
     public function reset(): void
diff --git a/src/Result.php b/src/Result.php
index 09428ae..55e78ad 100644
--- a/src/Result.php
+++ b/src/Result.php
@@ -13,7 +13,7 @@ interface Result extends SqlResult
     public function getLastInsertId(): ?int;
 
     /**
-     * @psalm-return array<int, array<int, string>>|null
+     * @return list<ColumnDefinition>|null
      */
-    public function getFields(): ?array;
+    public function getColumnDefinitions(): ?array;
 }
diff --git a/src/Statement.php b/src/Statement.php
index b839f04..b4a8f40 100644
--- a/src/Statement.php
+++ b/src/Statement.php
@@ -16,7 +16,10 @@ public function execute(array $params = []): Result;
      */
     public function bind(int|string $paramId, mixed $data): void;
 
-    public function getFields(): ?array;
+    /**
+     * @return list<ColumnDefinition>|null
+     */
+    public function getColumnDefinitions(): ?array;
 
     /**
      * Reset statement to state just after preparing.
diff --git a/src/StatementPool.php b/src/StatementPool.php
index 1011b80..b81460c 100644
--- a/src/StatementPool.php
+++ b/src/StatementPool.php
@@ -51,10 +51,10 @@ public function reset(): void
         $this->params = [];
     }
 
-    public function getFields(): ?array
+    public function getColumnDefinitions(): ?array
     {
         $statement = $this->pop();
-        $fields = $statement->getFields();
+        $fields = $statement->getColumnDefinitions();
         $this->push($statement);
         return $fields;
     }
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 4e23c76..2f5e82c 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -2,6 +2,7 @@
 
 namespace Amp\Mysql\Test;
 
+use Amp\Mysql\ColumnDefinition;
 use Amp\Mysql\DataType;
 use Amp\Mysql\Link;
 use Amp\Mysql\Result;
@@ -79,7 +80,7 @@ public function testMultiStmt()
         $this->assertSame([[6]], $got);
         $this->assertInstanceOf(Result::class, $resultset = $resultset->getNextResult());
 
-        $fields = $resultset->getFields();
+        $fields = $resultset->getColumnDefinitions();
 
         $got = [];
         foreach ($resultset as $row) {
@@ -88,11 +89,11 @@ public function testMultiStmt()
         $this->assertSame([["d" => 5, "c" => 5], ["d" => 6, "c" => 6]], $got);
 
         $this->assertCount(2, $fields);
-        $this->assertSame($fields[0]["original_name"], "b");
-        $this->assertSame($fields[0]["name"], "d");
-        $this->assertSame($fields[0]["type"], DataType::Long->value);
-        $this->assertSame($fields[1]["name"], "c");
-        $this->assertSame($fields[1]["type"], DataType::LongLong->value);
+        $this->assertSame($fields[0]->originalName, "b");
+        $this->assertSame($fields[0]->name, "d");
+        $this->assertSame($fields[0]->type, DataType::Long);
+        $this->assertSame($fields[1]->name, "c");
+        $this->assertSame($fields[1]->type, DataType::LongLong);
 
         $this->assertNull($resultset->getNextResult());
     }
@@ -101,23 +102,25 @@ public function testPrepared()
     {
         $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
 
-        $stmt = $db->prepare("SELECT * FROM main WHERE a = ? OR b = :num");
+        $stmt = $db->prepare("SELECT id as no, a, b FROM main as table_alias WHERE a = ? OR b = :num");
         $base = [
             "catalog" => "def",
             "schema" => "test",
-            "table" => "main",
-            "original_table" => "main",
+            "table" => "table_alias",
+            "originalTable" => "main",
             "charset" => 63,
-            "columnlen" => 11,
-            "type" => 3,
+            "length" => 11,
+            "type" => DataType::from(3),
             "flags" => 0,
             "decimals" => 0,
         ];
-        $this->assertEquals($stmt->getFields(), [
-            \array_merge($base, ["name" => "id", "original_name" => "id", "flags" => 16899]),
-            \array_merge($base, ["name" => "a", "original_name" => "a"]),
-            \array_merge($base + ["name" => "b", "original_name" => "b"]),
+
+        $this->assertEquals($stmt->getColumnDefinitions(), [
+            new ColumnDefinition(...\array_merge($base, ["name" => "no", "originalName" => "id", "flags" => 16899])),
+            new ColumnDefinition(...\array_merge($base, ["name" => "a", "originalName" => "a"])),
+            new ColumnDefinition(...\array_merge($base + ["name" => "b", "originalName" => "b"])),
         ]);
+
         $stmt->bind("num", 5);
         $result = $stmt->execute([2]);
         $this->assertInstanceOf(Result::class, $result);

From d4969798fc8690104127f81b9762a1879620ad99 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 24 Apr 2022 12:06:58 -0500
Subject: [PATCH 063/174] Psalm level 7 to 2

---
 psalm.xml                              |   2 +-
 src/Connection.php                     |   6 +-
 src/DataType.php                       |  92 +++++++++++++----
 src/Executor.php                       |   2 +-
 src/Internal/CommandResult.php         |   3 +
 src/Internal/ConnectionResult.php      |   2 +-
 src/Internal/ConnectionState.php       |   4 +-
 src/Internal/ConnectionStatement.php   |   2 +-
 src/Internal/ConnectionTransaction.php |   4 +-
 src/Internal/Processor.php             | 130 +++++++++++++++----------
 src/Internal/ResultProxy.php           |   4 +-
 src/MysqlConfig.php                    |  20 ++--
 src/Pool.php                           |  12 +++
 src/PooledStatement.php                |   3 +
 src/PooledTransaction.php              |   6 ++
 src/StatementPool.php                  |  12 +--
 src/functions.php                      |   3 +
 17 files changed, 206 insertions(+), 101 deletions(-)

diff --git a/psalm.xml b/psalm.xml
index 7c0333d..4e9226b 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0"?>
 <psalm
-    errorLevel="7"
+    errorLevel="2"
     resolveFromConfigFile="true"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns="https://getpsalm.org/schema/config"
diff --git a/src/Connection.php b/src/Connection.php
index da1c425..74bd4d2 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -4,7 +4,7 @@
 
 use Amp\Cancellation;
 use Amp\DeferredFuture;
-use Amp\Socket\Socket;
+use Amp\Socket\EncryptableSocket;
 use Amp\Sql\TransactionIsolation;
 use Revolt\EventLoop;
 
@@ -23,11 +23,11 @@ final class Connection implements Link
 
     private ?DeferredFuture $busy = null;
 
-    /** @var \Closure Function used to release connection after a transaction has completed. */
+    /** @var \Closure():void Function used to release connection after a transaction has completed. */
     private \Closure $release;
 
     public static function initialize(
-        Socket $socket,
+        EncryptableSocket $socket,
         MysqlConfig $config,
         ?Cancellation $cancellation = null,
     ): self {
diff --git a/src/DataType.php b/src/DataType.php
index f098edb..96fad9b 100644
--- a/src/DataType.php
+++ b/src/DataType.php
@@ -96,10 +96,10 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
             case self::Bit:
             case self::Decimal:
             case self::NewDecimal:
-                return $this->decodeText($string, $offset);
+                return self::decodeString($string, $offset);
 
             case self::Json:
-                $data = $this->decodeText($string, $offset);
+                $data = self::decodeString($string, $offset);
                 return self::decodeJson($data);
 
             case self::LongLong:
@@ -163,13 +163,13 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
                         throw new SqlException("Unexpected string length for date in binary protocol: " . ($length - 1));
                 }
 
-                return \str_pad($year, 2, "0", \STR_PAD_LEFT)
-                    . "-" . \str_pad($month, 2, "0", \STR_PAD_LEFT)
-                    . "-" . \str_pad($day, 2, "0", \STR_PAD_LEFT)
-                    . " " . \str_pad($hour, 2, "0", \STR_PAD_LEFT)
-                    . ":" . \str_pad($minute, 2, "0", \STR_PAD_LEFT)
-                    . ":" . \str_pad($second, 2, "0", \STR_PAD_LEFT)
-                    . "." . \str_pad($microsecond, 5, "0", \STR_PAD_LEFT);
+                return \str_pad((string) $year, 2, "0", \STR_PAD_LEFT)
+                    . "-" . \str_pad((string) $month, 2, "0", \STR_PAD_LEFT)
+                    . "-" . \str_pad((string) $day, 2, "0", \STR_PAD_LEFT)
+                    . " " . \str_pad((string) $hour, 2, "0", \STR_PAD_LEFT)
+                    . ":" . \str_pad((string) $minute, 2, "0", \STR_PAD_LEFT)
+                    . ":" . \str_pad((string) $second, 2, "0", \STR_PAD_LEFT)
+                    . "." . \str_pad((string) $microsecond, 5, "0", \STR_PAD_LEFT);
 
             case self::Time:
                 $string = \substr($string, $offset, 13);
@@ -192,11 +192,11 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
                         throw new SqlException("Unexpected string length for time in binary protocol: " . ($length - 1));
                 }
 
-                return ($negative ? "" : "-") . \str_pad($day, 2, "0", \STR_PAD_LEFT)
-                    . "d " . \str_pad($hour, 2, "0", \STR_PAD_LEFT)
-                    . ":" . \str_pad($minute, 2, "0", \STR_PAD_LEFT)
-                    . ":" . \str_pad($second, 2, "0", \STR_PAD_LEFT)
-                    . "." . \str_pad($microsecond, 5, "0", \STR_PAD_LEFT);
+                return ($negative ? "" : "-") . \str_pad((string) $day, 2, "0", \STR_PAD_LEFT)
+                    . "d " . \str_pad((string) $hour, 2, "0", \STR_PAD_LEFT)
+                    . ":" . \str_pad((string) $minute, 2, "0", \STR_PAD_LEFT)
+                    . ":" . \str_pad((string) $second, 2, "0", \STR_PAD_LEFT)
+                    . "." . \str_pad((string) $microsecond, 5, "0", \STR_PAD_LEFT);
 
             case self::Null:
                 return null;
@@ -254,13 +254,25 @@ private static function decodeJson(string $data): string
 
     public static function decodeNullTerminatedString(string $string, int &$offset = 0): string
     {
-        $length = \strpos($string, "\0", $offset) - $offset;
+        $length = \strpos($string, "\0", $offset);
+        if ($length === false) {
+            throw new \ValueError('Null not found in string');
+        }
+
+        $length -= $offset;
         $result = \substr($string, $offset, $length);
         $offset += $length + 1;
         return $result;
     }
 
-    public static function decodeUnsigned(string $string, int &$offset = 0): int|string
+    public static function decodeString(string $string, int &$offset = 0): string
+    {
+        $length = self::decodeUnsigned($string, $offset);
+        $offset += $length;
+        return \substr($string, $offset - $length, $length);
+    }
+
+    public static function decodeUnsigned(string $string, int &$offset = 0): int
     {
         $int = \ord($string[$offset]);
         if ($int < 0xfb) {
@@ -366,7 +378,20 @@ public static function decodeInt32(string $string, int &$offset = 0): int
         return \unpack("V", $string)[1];
     }
 
-    public static function decodeUnsigned32(string $string, int &$offset = 0): int|string
+    public static function decodeUnsigned32(string $string, int &$offset = 0): int
+    {
+        $string = \substr($string, $offset, 4);
+        $offset += 4;
+
+        $result = \unpack("V", $string)[1];
+        if ($result < 0) {
+            throw new \RuntimeException('Expecting a non-negative integer');
+        }
+
+        return $result;
+    }
+
+    public static function decodeUnsigned32WithGmp(string $string, int &$offset = 0): int|string
     {
         $string = \substr($string, $offset, 4);
         $offset += 4;
@@ -380,7 +405,19 @@ public static function decodeUnsigned32(string $string, int &$offset = 0): int|s
         return \gmp_strval(\gmp_import(\substr($string, 0, 4), 1, \GMP_LSW_FIRST));
     }
 
-    public static function decodeInt64(string $string, int &$offset = 0): int|string
+    public static function decodeInt64(string $string, int &$offset = 0): int
+    {
+        $string = \substr($string, $offset, 8);
+        $offset += 8;
+
+        if (\PHP_INT_SIZE > 4) {
+            return \unpack("P", $string)[1];
+        }
+
+        throw new \RuntimeException('64-bit integers are not supported by 32-bit builds of PHP');
+    }
+
+    public static function decodeInt64WithGmp(string $string, int &$offset = 0): int|string
     {
         $string = \substr($string, $offset, 8);
         $offset += 8;
@@ -394,7 +431,24 @@ public static function decodeInt64(string $string, int &$offset = 0): int|string
         return \gmp_strval(\gmp_import(\substr($string, 0, 8), 1, \GMP_LSW_FIRST));
     }
 
-    public static function decodeUnsigned64(string $string, int &$offset = 0): int|string
+    public static function decodeUnsigned64(string $string, int &$offset = 0): int
+    {
+        if (\PHP_INT_SIZE <= 4) {
+            throw new \RuntimeException('64-bit integers are not supported by 32-bit builds of PHP');
+        }
+
+        $string = \substr($string, $offset, 8);
+        $offset += 8;
+
+        $result = \unpack("P", $string)[1];
+        if ($result < 0) {
+            throw new \RuntimeException('Expecting a non-negative integer');
+        }
+
+        return $result;
+    }
+
+    public static function decodeUnsigned64WithGmp(string $string, int &$offset = 0): string
     {
         $string = \substr($string, $offset, 8);
         $offset += 8;
diff --git a/src/Executor.php b/src/Executor.php
index dc3479f..1528770 100644
--- a/src/Executor.php
+++ b/src/Executor.php
@@ -12,7 +12,7 @@ interface Executor extends SqlExecutor
     public function query(string $sql): Result;
 
     /**
-     * @return Result Result object specific to this library.
+     * @return Statement Statement object specific to this library.
      */
     public function prepare(string $sql): Statement;
 
diff --git a/src/Internal/CommandResult.php b/src/Internal/CommandResult.php
index 00122ee..8cbc481 100644
--- a/src/Internal/CommandResult.php
+++ b/src/Internal/CommandResult.php
@@ -41,6 +41,9 @@ public function getLastInsertId(): ?int
         return $this->lastInsertId;
     }
 
+    /**
+     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
+     */
     public function getNextResult(): ?Result
     {
         return $this->delegate->getNextResult();
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index 6e26340..3098d4d 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -84,7 +84,7 @@ private static function fetchRow(ResultProxy $result): Future
         /* We need to increment the internal counter, else the next time fetch is called,
          * it'll simply return the row we fetch here instead of fetching a new row
          * since callback order on promises isn't defined, we can't do this via onResolve() */
-        $incRow = static function ($row) use ($result) {
+        $incRow = static function (?array $row) use ($result): ?array {
             unset($result->rows[$result->userFetched++]);
             return $row;
         };
diff --git a/src/Internal/ConnectionState.php b/src/Internal/ConnectionState.php
index c06205c..e8ea32b 100644
--- a/src/Internal/ConnectionState.php
+++ b/src/Internal/ConnectionState.php
@@ -14,7 +14,7 @@ final class ConnectionState
     public ?int $errorCode = null;
     public ?string $errorState = null; // begins with "#"
 
-    public string $serverVersion;
+    public string $serverVersion = '';
 
-    public int $charset;
+    public int $charset = 0;
 }
diff --git a/src/Internal/ConnectionStatement.php b/src/Internal/ConnectionStatement.php
index 01ef665..19fd487 100644
--- a/src/Internal/ConnectionStatement.php
+++ b/src/Internal/ConnectionStatement.php
@@ -92,7 +92,7 @@ public function bind(int|string $paramId, mixed $data): void
                 }
             }
 
-            $this->getProcessor()->bindParam($this->stmtId, $realId, $data);
+            $this->getProcessor()->bindParam($this->stmtId, $realId, (string) $data);
         } while (isset($array) && $i = \next($array));
 
         if (isset($this->prebound[$paramId])) {
diff --git a/src/Internal/ConnectionTransaction.php b/src/Internal/ConnectionTransaction.php
index d128054..8e9ca6b 100644
--- a/src/Internal/ConnectionTransaction.php
+++ b/src/Internal/ConnectionTransaction.php
@@ -59,7 +59,7 @@ public function __destruct()
 
     public function getLastUsedAt(): int
     {
-        return $this->processor->getLastUsedAt();
+        return $this->processor?->getLastUsedAt() ?? 0;
     }
 
     /**
@@ -106,6 +106,8 @@ public function query(string $sql): Result
 
     /**
      * @throws TransactionError If the transaction has been committed or rolled back.
+     *
+     * @psalm-suppress InvalidReturnStatement, InvalidReturnType
      */
     public function prepare(string $sql): Statement
     {
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index a6c70be..d53a765 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -55,11 +55,11 @@ class Processor implements TransientResource
 
     private int $lastUsedAt;
 
-    private int $connectionId;
-    private string $authPluginData;
+    private int $connectionId = 0;
+    private string $authPluginData = '';
     private int $capabilities = 0;
     private int $serverCapabilities = 0;
-    private string $authPluginName;
+    private string $authPluginName = '';
     private ConnectionState $connInfo;
     private int $refcount = 1;
 
@@ -150,7 +150,7 @@ private function ready(): void
 
     private function addDeferred(DeferredFuture $deferred): void
     {
-        \assert($this->socket, "The connection has been closed");
+        \assert($this->socket !== null, "The connection has been closed");
         $this->deferreds[] = $deferred;
         $this->socket->reference();
     }
@@ -189,7 +189,7 @@ public function connect(?Cancellation $cancellation = null): void
     private function read(): void
     {
         try {
-            while (($bytes = $this->socket->read()) !== null) {
+            while ($this->socket !== null && ($bytes = $this->socket->read()) !== null) {
                 // @codeCoverageIgnoreStart
                 \assert((function () use ($bytes) {
                     if (\defined("MYSQL_DEBUG")) {
@@ -211,10 +211,6 @@ private function read(): void
 
                 $this->processData($bytes);
                 $bytes = null; // Free last data read.
-
-                if (!$this->socket) { // Connection closed.
-                    break;
-                }
             }
         } catch (\Throwable $exception) {
             // $exception used as previous exception below.
@@ -316,6 +312,7 @@ public function setCharset(string $charset, string $collate = ""): Future
     public function useDb(string $db): Future
     {
         return $this->startCommand(function () use ($db): void {
+            /** @psalm-suppress PropertyTypeCoercion */
             $this->config = $this->config->withDatabase($db);
             $this->sendPacket("\x02$db");
         });
@@ -404,10 +401,14 @@ public function bindParam(int $stmtId, int $paramId, string $data): void
         });
     }
 
-    /** @see 14.7.6 COM_STMT_EXECUTE */
-    // prebound params: null-bit set, type MYSQL_TYPE_LONG_BLOB, no value
-    // $params is by-ref, because the actual result object might not yet have been filled completely with data upon
-    // call of this method ...
+    /** @see 14.7.6 COM_STMT_EXECUTE
+     * prebound params: null-bit set, type MYSQL_TYPE_LONG_BLOB, no value
+     * $params is by-ref, because the actual result object might not yet have been filled completely with data upon
+     * call of this method ...
+     *
+     * @param array<int, mixed> $prebound
+     * @param array<int, mixed> $data
+     */
     public function execute(int $stmtId, string $query, array &$params, array $prebound, array $data = []): Future
     {
         $deferred = new DeferredFuture;
@@ -417,7 +418,7 @@ public function execute(int $stmtId, string $query, array &$params, array $prebo
             $payload .= \chr(0); // cursor flag // @TODO cursor types?!
             $payload .= DataType::encodeInt32(1);
             $paramCount = \count($params);
-            $bound = !empty($data) || !empty($prebound);
+            $bound = (!empty($data) || !empty($prebound)) ? 1 : 0;
             $types = "";
             $values = "";
             if ($paramCount) {
@@ -641,7 +642,7 @@ private function handleError(string $packet): void
                 'Could not connect to %s: %s %s',
                 $this->config->getConnectionString(),
                 $this->connInfo->errorState ?? 'Unknown state',
-                $this->connInfo->errorMsg ?? 'Unknown error',
+                $this->connInfo->errorMsg,
             )));
             return;
         }
@@ -657,10 +658,10 @@ private function handleError(string $packet): void
         // normal error
         $exception = new QueryError(\sprintf(
             'MySQL error (%d): %s %s',
-            $this->connInfo->errorCode ?? 0,
+            $this->connInfo->errorCode,
             $this->connInfo->errorState ?? 'Unknown state',
-            $this->connInfo->errorMsg ?? 'Unknown error',
-        ), $this->query);
+            $this->connInfo->errorMsg,
+        ), $this->query ?? '');
 
         $this->result = null;
         $this->query = null;
@@ -670,7 +671,11 @@ private function handleError(string $packet): void
         $this->ready();
     }
 
-    /** @see 14.1.3.1 OK-Packet */
+    /**
+     * @see 14.1.3.1 OK-Packet
+     *
+     * @psalm-suppress TypeDoesNotContainType Psalm seems to be having trouble with enums.
+     */
     private function parseOk(string $packet): void
     {
         $offset = 1;
@@ -695,22 +700,22 @@ private function parseOk(string $packet): void
             return;
         }
 
-        $this->connInfo->statusInfo = DataType::String->decodeText($packet, $offset);
+        $this->connInfo->statusInfo = DataType::decodeString($packet, $offset);
 
         if (!StatusFlag::SessionStateChanged->inFlags($this->connInfo->statusFlags)) {
             return;
         }
 
-        $sessionState = DataType::String->decodeText($packet, $offset);
+        $sessionState = DataType::decodeString($packet, $offset);
 
         while (\strlen($packet) > $offset) {
-            $data = DataType::String->decodeText($sessionState, $offset);
+            $data = DataType::decodeString($sessionState, $offset);
             $type = DataType::decodeUnsigned8($sessionState, $offset);
 
             switch (SessionStateType::tryFrom($type)) {
                 case SessionStateType::SystemVariables:
-                    $var = DataType::String->decodeText($data, $offset);
-                    $this->connInfo->sessionState[$type][$var] = DataType::String->decodeText($data, $offset);
+                    $var = DataType::decodeString($data, $offset);
+                    $this->connInfo->sessionState[$type][$var] = DataType::decodeString($data, $offset);
                     break;
 
                 case SessionStateType::Schema:
@@ -718,7 +723,7 @@ private function parseOk(string $packet): void
                 case SessionStateType::Gtids:
                 case SessionStateType::TransactionCharacteristics:
                 case SessionStateType::TransactionState:
-                    $this->connInfo->sessionState[$type] = DataType::String->decodeText($data, $offset);
+                    $this->connInfo->sessionState[$type] = DataType::decodeString($data, $offset);
                     break;
 
                 default:
@@ -747,7 +752,7 @@ private function parseEof(string $packet): void
     private function handleEof(string $packet): void
     {
         $this->parseEof($packet);
-        $exception = new SqlException($this->connInfo->errorMsg, $this->connInfo->errorCode);
+        $exception = new SqlException($this->connInfo->errorMsg ?? 'Unknown error', $this->connInfo->errorCode ?? 0);
         $this->getDeferred()->error($exception);
         $this->ready();
     }
@@ -799,7 +804,7 @@ private function handleHandshake(string $packet): void
     }
 
     /** @see 14.2.5 Connection Phase Packets */
-    private function handleAuthSwitch($packet)
+    private function handleAuthSwitch(string $packet): void
     {
         $offset = 1;
         $this->authPluginName = DataType::decodeNullTerminatedString($packet, $offset);
@@ -807,7 +812,7 @@ private function handleAuthSwitch($packet)
         $this->sendAuthSwitchResponse();
     }
 
-    private function sendAuthSwitchResponse()
+    private function sendAuthSwitchResponse(): void
     {
         $this->write($this->getAuthData());
     }
@@ -825,7 +830,7 @@ private function handleLocalInfileRequest(string $packet): void
 
                 $fileHandle = File\openFile($filePath, 'r');
 
-                while ("" != ($chunk = $fileHandle->read())) {
+                while (null !== ($chunk = $fileHandle->read())) {
                     $this->sendPacket($chunk);
                 }
                 $this->sendPacket("");
@@ -916,6 +921,8 @@ private function handleBinaryColumnDefinition(string $packet): void
 
     private function handleColumnDefinition(string $packet, \Closure $parseCallback): void
     {
+        \assert($this->result !== null, 'Connection result was in invalid state');
+
         if (!$this->result->columnsToFetch--) {
             $this->result->updateState(ResultProxy::COLUMNS_FETCHED);
             if (\ord($packet) === self::ERR_PACKET) {
@@ -938,6 +945,8 @@ private function handleColumnDefinition(string $packet, \Closure $parseCallback)
 
     private function prepareParams(string $packet): void
     {
+        \assert($this->result !== null, 'Connection result was in invalid state');
+
         if (!$this->result->columnsToFetch--) {
             $this->result->columnsToFetch = $this->result->columnCount;
             if (!$this->result->columnsToFetch) {
@@ -953,6 +962,8 @@ private function prepareParams(string $packet): void
 
     private function prepareFields(string $packet): void
     {
+        \assert($this->result !== null, 'Connection result was in invalid state');
+
         if (!$this->result->columnsToFetch--) {
             $this->parseCallback = null;
             $this->query = null;
@@ -974,12 +985,12 @@ private function parseColumnDefinition(string $packet): ColumnDefinition
         $column = [];
 
         if ($this->capabilities & self::CLIENT_PROTOCOL_41) {
-            $column["catalog"] = DataType::String->decodeText($packet, $offset);
-            $column["schema"] = DataType::String->decodeText($packet, $offset);
-            $column["table"] = DataType::String->decodeText($packet, $offset);
-            $column["originalTable"] = DataType::String->decodeText($packet, $offset);
-            $column["name"] = DataType::String->decodeText($packet, $offset);
-            $column["originalName"] = DataType::String->decodeText($packet, $offset);
+            $column["catalog"] = DataType::decodeString($packet, $offset);
+            $column["schema"] = DataType::decodeString($packet, $offset);
+            $column["table"] = DataType::decodeString($packet, $offset);
+            $column["originalTable"] = DataType::decodeString($packet, $offset);
+            $column["name"] = DataType::decodeString($packet, $offset);
+            $column["originalName"] = DataType::decodeString($packet, $offset);
             $fixLength = DataType::decodeUnsigned($packet, $offset);
 
             $column["charset"] = DataType::decodeUnsigned16($packet, $offset);
@@ -990,8 +1001,8 @@ private function parseColumnDefinition(string $packet): ColumnDefinition
 
             $offset += $fixLength;
         } else {
-            $column["table"] = DataType::String->decodeText($packet, $offset);
-            $column["name"] = DataType::String->decodeText($packet, $offset);
+            $column["table"] = DataType::decodeString($packet, $offset);
+            $column["name"] = DataType::decodeString($packet, $offset);
 
             $columnLength = DataType::decodeUnsigned($packet, $offset);
             $column["length"] = DataType::decodeIntByLength($packet, $columnLength, $offset);
@@ -1013,14 +1024,17 @@ private function parseColumnDefinition(string $packet): ColumnDefinition
         }
 
         if ($offset < \strlen($packet)) {
-            $column["defaults"] = DataType::String->decodeText($packet, $offset);
+            $column["defaults"] = DataType::decodeString($packet, $offset);
         }
 
+        /** @psalm-suppress InvalidScalarArgument */
         return new ColumnDefinition(...$column);
     }
 
     private function successfulResultFetch(): void
     {
+        \assert($this->result !== null, 'Connection result was in invalid state');
+
         $result = $this->result;
         $deferred = &$result->next;
         if (!$deferred) {
@@ -1060,6 +1074,8 @@ private function handleTextResultSetRow(string $packet): void
             return;
         }
 
+        \assert($this->result !== null, 'Connection result was in invalid state');
+
         $offset = 0;
         $fields = [];
         for ($i = 0; $offset < \strlen($packet); ++$i) {
@@ -1067,7 +1083,7 @@ private function handleTextResultSetRow(string $packet): void
                 $fields[] = null;
                 $offset += 1;
             } else {
-                $column = $this->result->columns[$i];
+                $column = $this->result->columns[$i] ?? throw new \RuntimeException("Definition missing for column $i");
                 $fields[] = $column->type->decodeText($packet, $offset, $column->flags);
             }
         }
@@ -1088,6 +1104,8 @@ private function handleBinaryResultSetRow(string $packet): void
             return;
         }
 
+        \assert($this->result !== null, 'Connection result was in invalid state');
+
         $offset = 1; // skip first byte
         $fields = [];
         for ($i = 0; $i < $this->result->columnCount; $i++) {
@@ -1102,7 +1120,7 @@ private function handleBinaryResultSetRow(string $packet): void
                 $i++;
             }
 
-            $column = $this->result->columns[$i];
+            $column = $this->result->columns[$i] ?? throw new \RuntimeException("Definition missing for column $i");
             $fields[$i] = $column->type->decodeBinary($packet, $offset, $column->flags);
         }
 
@@ -1136,6 +1154,7 @@ private function handlePrepare(string $packet): void
         $this->result->columnsToFetch = $params;
         $this->result->columnCount = $columns;
         $this->refcount++;
+        \assert($this->query !== null, 'Invalid value for connection query');
         $this->getDeferred()->complete(new ConnectionStatement($this, $this->query, $stmtId, $this->named, $this->result));
         $this->named = [];
         if ($params) {
@@ -1202,6 +1221,7 @@ private function write(string $packet): void
             $packet = $this->compressPacket($packet);
         }
 
+        \assert($this->socket !== null, 'The connection was closed during a call to write');
         $this->socket->write($packet);
     }
 
@@ -1371,7 +1391,7 @@ private function parsePayload(string $packet): void
                                     break;
                                 case 0x2d: // certificate
                                     $pubkey = \substr($packet, 1);
-                                    $this->write($this->sha256Auth($this->config->getPassword(), $this->authPluginData, $pubkey));
+                                    $this->write($this->sha256Auth($this->config->getPassword() ?? '', $this->authPluginData, $pubkey));
                                     break;
                             }
                             break;
@@ -1416,14 +1436,14 @@ private function parsePayload(string $packet): void
 
     private function secureAuth(string $pass, string $scramble): string
     {
-        $hash = \sha1($pass, 1);
-        return $hash ^ \sha1(\substr($scramble, 0, 20) . \sha1($hash, 1), 1);
+        $hash = \sha1($pass, true);
+        return $hash ^ \sha1(\substr($scramble, 0, 20) . \sha1($hash, true), true);
     }
 
     private function sha256Auth(string $pass, string $scramble, string $key): string
     {
         \openssl_public_encrypt(
-            "$pass\0" ^ \str_repeat($scramble, \ceil(\strlen($pass) / \strlen($scramble))),
+            "$pass\0" ^ \str_repeat($scramble, (int) \ceil(\strlen($pass) / \strlen($scramble))),
             $auth,
             $key,
             OPENSSL_PKCS1_OAEP_PADDING,
@@ -1446,10 +1466,10 @@ private function authSwitchRequest(string $packet): void
                 if (\strlen($packet) === 1) {
                     break;
                 }
-                $length = \strpos($packet, "\0");
+                $length = (int) \strpos($packet, "\0");
                 $pluginName = \substr($packet, 0, $length); // @TODO mysql_native_pass only now...
                 $authPluginData = \substr($packet, $length + 1);
-                $this->sendPacket($this->secureAuth($this->config->getPassword(), $authPluginData));
+                $this->sendPacket($this->secureAuth($this->config->getPassword() ?? '', $authPluginData));
                 break;
             case self::ERR_PACKET:
                 $this->handleError($packet);
@@ -1486,7 +1506,7 @@ private function sendHandshake(bool $inSSL = false): void
                 try {
                     $this->write($payload);
 
-                    $this->socket->setupTls();
+                    $this->socket?->setupTls();
 
                     $this->sendHandshake(true);
                 } catch (\Throwable $e) {
@@ -1522,8 +1542,10 @@ private function sendHandshake(bool $inSSL = false): void
         $this->write($payload);
     }
 
-    private function getAuthData(): ?string
+    private function getAuthData(): string
     {
+        $password = $this->config->getPassword() ?? "";
+
         if ($this->config->getPassword() == "") {
             return "";
         }
@@ -1531,17 +1553,17 @@ private function getAuthData(): ?string
         if ($this->capabilities & self::CLIENT_PLUGIN_AUTH) {
             switch ($this->authPluginName) {
                 case "mysql_native_password":
-                    return $this->secureAuth($this->config->getPassword(), $this->authPluginData);
+                    return $this->secureAuth($password, $this->authPluginData);
                 case "mysql_clear_password":
-                    return $this->config->getPassword();
+                    return $password;
                 case "sha256_password":
                     $key = $this->config->getKey();
-                    if ($key !== null) {
-                        return $this->sha256Auth($this->config->getPassword(), $this->authPluginData, $key);
+                    if ($key !== '') {
+                        return $this->sha256Auth($password, $this->authPluginData, $key);
                     }
                     return "\x01";
                 case "caching_sha2_password":
-                    return $this->sha2Auth($this->config->getPassword(), $this->authPluginData);
+                    return $this->sha2Auth($password, $this->authPluginData);
                 case "mysql_old_password":
                     throw new ConnectionException(
                         "mysql_old_password is outdated and insecure. Intentionally not implemented!"
@@ -1553,7 +1575,7 @@ private function getAuthData(): ?string
             }
         }
 
-        return $this->secureAuth($this->config->getPassword(), $this->authPluginData);
+        return $this->secureAuth($password, $this->authPluginData);
     }
 
     /** @see 14.1.2 MySQL Packet */
diff --git a/src/Internal/ResultProxy.php b/src/Internal/ResultProxy.php
index ad86963..94446c7 100644
--- a/src/Internal/ResultProxy.php
+++ b/src/Internal/ResultProxy.php
@@ -7,11 +7,11 @@
 
 final class ResultProxy
 {
-    public int $columnCount;
+    public int $columnCount = 0;
     /** @var list<ColumnDefinition> */
     public array $columns = [];
     public array $params = [];
-    public int $columnsToFetch;
+    public int $columnsToFetch = 0;
     public array $rows = [];
     public int $fetchedRows = 0;
     public int $userFetched = 0;
diff --git a/src/MysqlConfig.php b/src/MysqlConfig.php
index f72b899..eb059d3 100644
--- a/src/MysqlConfig.php
+++ b/src/MysqlConfig.php
@@ -47,16 +47,16 @@ public static function fromString(string $connectionString, ConnectContext $cont
         }
 
         return new self(
-            $parts['host'],
-            (int) ($parts['port'] ?? self::DEFAULT_PORT),
-            $parts['user'] ?? null,
-            $parts['password'] ?? null,
-            $parts['db'] ?? null,
-            $context,
-            $parts['charset'] ?? self::DEFAULT_CHARSET,
-            $parts['collate'] ?? self::DEFAULT_COLLATE,
-            ($parts['compression'] ?? '') === 'on',
-            ($parts['local-infile'] ?? '') === 'on'
+            host: $parts['host'],
+            port: (int) ($parts['port'] ?? self::DEFAULT_PORT),
+            user: $parts['user'] ?? null,
+            password: $parts['password'] ?? null,
+            database: $parts['db'] ?? null,
+            context: $context,
+            charset: $parts['charset'] ?? self::DEFAULT_CHARSET,
+            collate: $parts['collate'] ?? self::DEFAULT_COLLATE,
+            useCompression: ($parts['compression'] ?? '') === 'on',
+            useLocalInfile: ($parts['local-infile'] ?? '') === 'on'
         );
     }
 
diff --git a/src/Pool.php b/src/Pool.php
index a4dd0d7..40ee4b7 100644
--- a/src/Pool.php
+++ b/src/Pool.php
@@ -11,6 +11,10 @@
 
 final class Pool extends ConnectionPool implements Link
 {
+    /**
+     * @param positive-int $maxConnections
+     * @param positive-int $idleTimeout
+     */
     public function __construct(
         MysqlConfig $config,
         int $maxConnections = self::DEFAULT_MAX_CONNECTIONS,
@@ -44,6 +48,8 @@ protected function createTransaction(SqlTransaction $transaction, \Closure $rele
 
     /**
      * Changes return type to this library's Result type.
+     *
+     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function query(string $sql): Result
     {
@@ -52,6 +58,8 @@ public function query(string $sql): Result
 
     /**
      * Changes return type to this library's Statement type.
+     *
+     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function prepare(string $sql): Statement
     {
@@ -60,6 +68,8 @@ public function prepare(string $sql): Statement
 
     /**
      * Changes return type to this library's Result type.
+     *
+     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function execute(string $sql, array $params = []): Result
     {
@@ -68,6 +78,8 @@ public function execute(string $sql, array $params = []): Result
 
     /**
      * Changes return type to this library's Transaction type.
+     *
+     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::Committed): Transaction
     {
diff --git a/src/PooledStatement.php b/src/PooledStatement.php
index 0bf96da..f9c2497 100644
--- a/src/PooledStatement.php
+++ b/src/PooledStatement.php
@@ -9,6 +9,9 @@ final class PooledStatement extends SqlPooledStatement implements Statement
 {
     private readonly Statement $statement;
 
+    /**
+     * @param \Closure():void $release
+     */
     public function __construct(Statement $statement, \Closure $release)
     {
         parent::__construct($statement, $release);
diff --git a/src/PooledTransaction.php b/src/PooledTransaction.php
index 7ebaefc..4c12951 100644
--- a/src/PooledTransaction.php
+++ b/src/PooledTransaction.php
@@ -22,6 +22,8 @@ protected function createResult(SqlResult $result, \Closure $release): Result
 
     /**
      * Changes return type to this library's Result type.
+     *
+     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function query(string $sql): Result
     {
@@ -30,6 +32,8 @@ public function query(string $sql): Result
 
     /**
      * Changes return type to this library's Statement type.
+     *
+     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function prepare(string $sql): Statement
     {
@@ -38,6 +42,8 @@ public function prepare(string $sql): Statement
 
     /**
      * Changes return type to this library's Result type.
+     *
+     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function execute(string $sql, array $params = []): Result
     {
diff --git a/src/StatementPool.php b/src/StatementPool.php
index b81460c..7cc7981 100644
--- a/src/StatementPool.php
+++ b/src/StatementPool.php
@@ -32,6 +32,9 @@ protected function createResult(SqlResult $result, \Closure $release): Result
         return new PooledResult($result, $release);
     }
 
+    /**
+     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
+     */
     public function execute(array $params = []): Result
     {
         return parent::execute($params);
@@ -39,10 +42,6 @@ public function execute(array $params = []): Result
 
     public function bind(int|string $paramId, mixed $data): void
     {
-        if (!\is_int($paramId) && !\is_string($paramId)) {
-            throw new \TypeError("Invalid parameter ID type");
-        }
-
         $this->params[$paramId] = $data;
     }
 
@@ -54,8 +53,9 @@ public function reset(): void
     public function getColumnDefinitions(): ?array
     {
         $statement = $this->pop();
-        $fields = $statement->getColumnDefinitions();
+        \assert($statement instanceof Statement);
+        $columns = $statement->getColumnDefinitions();
         $this->push($statement);
-        return $fields;
+        return $columns;
     }
 }
diff --git a/src/functions.php b/src/functions.php
index a3d7c81..9cc8959 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -33,6 +33,9 @@ function connect(MysqlConfig $config): Connection
 /**
  * Create a pool using the global Connector instance.
  *
+ * @param positive-int $maxConnections
+ * @param positive-int $idleTimeout
+ *
  * @throws \Error If the connection string does not contain a host, user, and password.
  */
 function pool(

From 80e75f192842c9c5439a074b5ebd4c34c83d3354 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 24 Apr 2022 12:22:32 -0500
Subject: [PATCH 064/174] Improve decodeUnsigned

No need for substr here anymore either.
---
 src/DataType.php | 27 ++++++++-------------------
 1 file changed, 8 insertions(+), 19 deletions(-)

diff --git a/src/DataType.php b/src/DataType.php
index 96fad9b..f62b925 100644
--- a/src/DataType.php
+++ b/src/DataType.php
@@ -274,29 +274,18 @@ public static function decodeString(string $string, int &$offset = 0): string
 
     public static function decodeUnsigned(string $string, int &$offset = 0): int
     {
-        $int = \ord($string[$offset]);
+        $int = self::decodeUnsigned8($string, $offset);
         if ($int < 0xfb) {
-            $offset += 1;
             return $int;
         }
 
-        if ($int === 0xfc) {
-            $offset += 3;
-            return self::decodeUnsigned16(\substr($string, $offset - 2, 2));
-        }
-
-        if ($int === 0xfd) {
-            $offset += 4;
-            return self::decodeUnsigned24(\substr($string, $offset - 3, 3));
-        }
-
-        if ($int === 0xfe) {
-            $offset += 9;
-            return self::decodeUnsigned64(\substr($string, $offset - 8, 8));
-        }
-
-        // If that happens connection is borked...
-        throw new SqlException("$int is not in ranges [0x00, 0xfa] or [0xfc, 0xfe]");
+        return match ($int) {
+            0xfc => self::decodeUnsigned16($string, $offset),
+            0xfd => self::decodeUnsigned24($string, $offset),
+            0xfe => self::decodeUnsigned64($string, $offset),
+            // If this happens connection is borked...
+            default => throw new SqlException("$int is not in ranges [0x00, 0xfa] or [0xfc, 0xfe]"),
+        };
     }
 
     public static function decodeIntByLength(string $string, int $length, int &$offset = 0): int

From 344a11450845833249c4485dfca842cdd0b286a5 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 24 Apr 2022 12:51:18 -0500
Subject: [PATCH 065/174] Remove deprecated refresh command

---
 src/Connection.php         | 23 +++--------------------
 src/Internal/Processor.php |  2 +-
 2 files changed, 4 insertions(+), 21 deletions(-)

diff --git a/src/Connection.php b/src/Connection.php
index 74bd4d2..96378f8 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -10,21 +10,12 @@
 
 final class Connection implements Link
 {
-    public const REFRESH_GRANT = 0x01;
-    public const REFRESH_LOG = 0x02;
-    public const REFRESH_TABLES = 0x04;
-    public const REFRESH_HOSTS = 0x08;
-    public const REFRESH_STATUS = 0x10;
-    public const REFRESH_THREADS = 0x20;
-    public const REFRESH_SLAVE = 0x40;
-    public const REFRESH_MASTER = 0x80;
-
-    private Internal\Processor $processor;
+    private readonly Internal\Processor $processor;
 
     private ?DeferredFuture $busy = null;
 
     /** @var \Closure():void Function used to release connection after a transaction has completed. */
-    private \Closure $release;
+    private readonly \Closure $release;
 
     public static function initialize(
         EncryptableSocket $socket,
@@ -43,7 +34,7 @@ private function __construct(Internal\Processor $processor)
         $busy = &$this->busy;
         $this->release = static function () use (&$busy): void {
             \assert($busy instanceof DeferredFuture);
-            $busy->complete(null);
+            $busy->complete();
             $busy = null;
         };
     }
@@ -87,14 +78,6 @@ public function useDb(string $db): void
         $this->processor->useDb($db)->await();
     }
 
-    /**
-     * @param int $subcommand int one of the self::REFRESH_* constants
-     */
-    public function refresh(int $subcommand): void
-    {
-        $this->processor->refresh($subcommand)->await();
-    }
-
     public function query(string $sql): Result
     {
         while ($this->busy) {
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index d53a765..f28c935 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -519,7 +519,7 @@ public function dropDatabase(string $db): Future
     }
 
     /**
-     * @param $subcommand int one of the self::REFRESH_* constants
+     * @param int $subcommand
      * @see 14.6.8 COM_REFRESH
      */
     public function refresh(int $subcommand): Future

From 65463a1a806fbefe70a325bf1797d1e261db38f0 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 24 Apr 2022 12:52:24 -0500
Subject: [PATCH 066/174] Skip style fixer on CI for now

PHP-CS-Fixer currently does not support Null as an enum case name.
---
 .github/workflows/ci.yml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 954d06e..ffd4935 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -71,7 +71,7 @@ jobs:
       - name: Run static analysis
         run: vendor/bin/psalm.phar
 
-      - name: Run style fixer
-        env:
-          PHP_CS_FIXER_IGNORE_ENV: 1
-        run: vendor/bin/php-cs-fixer --diff --dry-run -v fix
+#      - name: Run style fixer
+#        env:
+#          PHP_CS_FIXER_IGNORE_ENV: 1
+#        run: vendor/bin/php-cs-fixer --diff --dry-run -v fix

From dcf73e4a3a6183554981a780165f7de4acc01853 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 25 Apr 2022 20:54:28 -0500
Subject: [PATCH 067/174] Make Processor state an enum

---
 src/Internal/ConnectionMetadata.php |  20 +++++
 src/Internal/ConnectionState.php    |  23 +++---
 src/Internal/Processor.php          | 110 +++++++++++++---------------
 src/Internal/SessionStateType.php   |   6 +-
 src/Internal/StatusFlag.php         |   6 +-
 5 files changed, 91 insertions(+), 74 deletions(-)
 create mode 100644 src/Internal/ConnectionMetadata.php

diff --git a/src/Internal/ConnectionMetadata.php b/src/Internal/ConnectionMetadata.php
new file mode 100644
index 0000000..8ba3100
--- /dev/null
+++ b/src/Internal/ConnectionMetadata.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Amp\Mysql\Internal;
+
+final class ConnectionMetadata
+{
+    public int $affectedRows = 0;
+    public int $insertId = 0;
+    public int $statusFlags = 0;
+    public ?int $warnings = null;
+    public ?string $statusInfo = null;
+    public array $sessionState = [];
+    public ?string $errorMsg = null;
+    public ?int $errorCode = null;
+    public ?string $errorState = null; // begins with "#"
+
+    public string $serverVersion = '';
+
+    public int $charset = 0;
+}
diff --git a/src/Internal/ConnectionState.php b/src/Internal/ConnectionState.php
index e8ea32b..f7586d8 100644
--- a/src/Internal/ConnectionState.php
+++ b/src/Internal/ConnectionState.php
@@ -2,19 +2,14 @@
 
 namespace Amp\Mysql\Internal;
 
-final class ConnectionState
+/**
+ * @internal
+ */
+enum ConnectionState
 {
-    public int $affectedRows = 0;
-    public int $insertId = 0;
-    public int $statusFlags = 0;
-    public ?int $warnings = null;
-    public ?string $statusInfo = null;
-    public array $sessionState = [];
-    public ?string $errorMsg = null;
-    public ?int $errorCode = null;
-    public ?string $errorState = null; // begins with "#"
-
-    public string $serverVersion = '';
-
-    public int $charset = 0;
+    case Unconnected;
+    case Established;
+    case Ready;
+    case Closing;
+    case Closed;
 }
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index f28c935..2536e52 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -60,10 +60,10 @@ class Processor implements TransientResource
     private int $capabilities = 0;
     private int $serverCapabilities = 0;
     private string $authPluginName = '';
-    private ConnectionState $connInfo;
+    private ConnectionMetadata $metadata;
     private int $refcount = 1;
 
-    private int $connectionState = self::UNCONNECTED;
+    private ConnectionState $connectionState = ConnectionState::Unconnected;
 
     private const MAX_PACKET_SIZE = 0xffffff;
     private const MAX_UNCOMPRESSED_BUFLEN = 0xfffffb;
@@ -92,28 +92,22 @@ class Processor implements TransientResource
     private const EOF_PACKET = 0xfe;
     private const ERR_PACKET = 0xff;
 
-    private const UNCONNECTED = 0;
-    private const ESTABLISHED = 1;
-    private const READY = 2;
-    private const CLOSING = 3;
-    private const CLOSED = 4;
-
     public function __construct(EncryptableSocket $socket, MysqlConfig $config)
     {
         $this->socket = $socket;
-        $this->connInfo = new ConnectionState;
+        $this->metadata = new ConnectionMetadata;
         $this->config = $config;
         $this->lastUsedAt = \time();
     }
 
     public function isAlive(): bool
     {
-        return $this->connectionState <= self::READY;
+        return $this->connectionState <= ConnectionState::Ready;
     }
 
     public function isReady(): bool
     {
-        return $this->connectionState === self::READY;
+        return $this->connectionState === ConnectionState::Ready;
     }
 
     public function unreference(): void
@@ -255,7 +249,7 @@ private function appendTask(\Closure $callback): void
             || $this->parseCallback
             || !empty($this->onReady)
             || !empty($this->deferreds)
-            || $this->connectionState !== self::READY
+            || $this->connectionState !== ConnectionState::Ready
         ) {
             $this->onReady[] = $callback;
         } else {
@@ -263,9 +257,9 @@ private function appendTask(\Closure $callback): void
         }
     }
 
-    public function getConnInfo(): ConnectionState
+    public function getMetadata(): ConnectionMetadata
     {
-        return clone $this->connInfo;
+        return clone $this->metadata;
     }
 
     public function getConnectionId(): int
@@ -280,7 +274,7 @@ public function getLastUsedAt(): int
 
     protected function startCommand(\Closure $callback): Future
     {
-        if ($this->connectionState > self::READY) {
+        if ($this->connectionState > ConnectionState::Ready) {
             throw new \Error("The connection has been closed");
         }
 
@@ -468,7 +462,7 @@ public function closeStmt(int $stmtId): void
     {
         $payload = "\x19" . DataType::encodeInt32($stmtId);
         $this->appendTask(function () use ($payload): void {
-            if ($this->connectionState === self::READY) {
+            if ($this->connectionState === ConnectionState::Ready) {
                 $this->resetIds();
                 $this->sendPacket($payload);
                 $this->resetIds(); // does not expect a reply - must be reset immediately
@@ -624,25 +618,25 @@ private function handleError(string $packet): void
     {
         $offset = 1;
 
-        $this->connInfo->errorCode = DataType::decodeUnsigned16($packet, $offset);
+        $this->metadata->errorCode = DataType::decodeUnsigned16($packet, $offset);
 
         if ($this->capabilities & self::CLIENT_PROTOCOL_41) {
-            $this->connInfo->errorState = \substr($packet, $offset, 6);
+            $this->metadata->errorState = \substr($packet, $offset, 6);
             $offset += 6;
         }
 
-        $this->connInfo->errorMsg = \substr($packet, $offset);
+        $this->metadata->errorMsg = \substr($packet, $offset);
 
         $this->parseCallback = null;
 
-        if ($this->connectionState < self::READY) {
+        if ($this->connectionState < ConnectionState::Ready) {
             // connection failure
             $this->close();
             $this->getDeferred()->error(new InitializationException(\sprintf(
                 'Could not connect to %s: %s %s',
                 $this->config->getConnectionString(),
-                $this->connInfo->errorState ?? 'Unknown state',
-                $this->connInfo->errorMsg,
+                $this->metadata->errorState ?? 'Unknown state',
+                $this->metadata->errorMsg,
             )));
             return;
         }
@@ -658,9 +652,9 @@ private function handleError(string $packet): void
         // normal error
         $exception = new QueryError(\sprintf(
             'MySQL error (%d): %s %s',
-            $this->connInfo->errorCode,
-            $this->connInfo->errorState ?? 'Unknown state',
-            $this->connInfo->errorMsg,
+            $this->metadata->errorCode,
+            $this->metadata->errorState ?? 'Unknown state',
+            $this->metadata->errorMsg,
         ), $this->query ?? '');
 
         $this->result = null;
@@ -680,29 +674,29 @@ private function parseOk(string $packet): void
     {
         $offset = 1;
 
-        $this->connInfo->affectedRows = DataType::decodeUnsigned($packet, $offset);
-        $this->connInfo->insertId = DataType::decodeUnsigned($packet, $offset);
+        $this->metadata->affectedRows = DataType::decodeUnsigned($packet, $offset);
+        $this->metadata->insertId = DataType::decodeUnsigned($packet, $offset);
 
         if ($this->capabilities & (self::CLIENT_PROTOCOL_41 | self::CLIENT_TRANSACTIONS)) {
-            $this->connInfo->statusFlags = DataType::decodeUnsigned16($packet, $offset);
-            $this->connInfo->warnings = DataType::decodeUnsigned16($packet, $offset);
+            $this->metadata->statusFlags = DataType::decodeUnsigned16($packet, $offset);
+            $this->metadata->warnings = DataType::decodeUnsigned16($packet, $offset);
         }
 
         if (!($this->capabilities & self::CLIENT_SESSION_TRACK)) {
-            $this->connInfo->statusInfo = \substr($packet, $offset);
+            $this->metadata->statusInfo = \substr($packet, $offset);
             return;
         }
 
         // Even though it seems required according to 14.1.3.1, there is no length encoded string,
         // i.e. no trailing NULL byte ....???
         if (\strlen($packet) <= $offset) {
-            $this->connInfo->statusInfo = "";
+            $this->metadata->statusInfo = "";
             return;
         }
 
-        $this->connInfo->statusInfo = DataType::decodeString($packet, $offset);
+        $this->metadata->statusInfo = DataType::decodeString($packet, $offset);
 
-        if (!StatusFlag::SessionStateChanged->inFlags($this->connInfo->statusFlags)) {
+        if (!StatusFlag::SessionStateChanged->inFlags($this->metadata->statusFlags)) {
             return;
         }
 
@@ -715,7 +709,7 @@ private function parseOk(string $packet): void
             switch (SessionStateType::tryFrom($type)) {
                 case SessionStateType::SystemVariables:
                     $var = DataType::decodeString($data, $offset);
-                    $this->connInfo->sessionState[$type][$var] = DataType::decodeString($data, $offset);
+                    $this->metadata->sessionState[$type][$var] = DataType::decodeString($data, $offset);
                     break;
 
                 case SessionStateType::Schema:
@@ -723,7 +717,7 @@ private function parseOk(string $packet): void
                 case SessionStateType::Gtids:
                 case SessionStateType::TransactionCharacteristics:
                 case SessionStateType::TransactionState:
-                    $this->connInfo->sessionState[$type] = DataType::decodeString($data, $offset);
+                    $this->metadata->sessionState[$type] = DataType::decodeString($data, $offset);
                     break;
 
                 default:
@@ -735,7 +729,7 @@ private function parseOk(string $packet): void
     private function handleOk(string $packet): void
     {
         $this->parseOk($packet);
-        $this->getDeferred()->complete(new CommandResult($this->connInfo->affectedRows, $this->connInfo->insertId));
+        $this->getDeferred()->complete(new CommandResult($this->metadata->affectedRows, $this->metadata->insertId));
         $this->ready();
     }
 
@@ -744,15 +738,15 @@ private function parseEof(string $packet): void
     {
         $offset = 1;
         if ($this->capabilities & self::CLIENT_PROTOCOL_41) {
-            $this->connInfo->warnings = DataType::decodeUnsigned16($packet, $offset);
-            $this->connInfo->statusFlags = DataType::decodeUnsigned16($packet, $offset);
+            $this->metadata->warnings = DataType::decodeUnsigned16($packet, $offset);
+            $this->metadata->statusFlags = DataType::decodeUnsigned16($packet, $offset);
         }
     }
 
     private function handleEof(string $packet): void
     {
         $this->parseEof($packet);
-        $exception = new SqlException($this->connInfo->errorMsg ?? 'Unknown error', $this->connInfo->errorCode ?? 0);
+        $exception = new SqlException($this->metadata->errorMsg ?? 'Unknown error', $this->metadata->errorCode ?? 0);
         $this->getDeferred()->error($exception);
         $this->ready();
     }
@@ -767,7 +761,7 @@ private function handleHandshake(string $packet): void
             throw new ConnectionException("Unsupported protocol version ".\ord($packet)." (Expected: 10)");
         }
 
-        $this->connInfo->serverVersion = DataType::decodeNullTerminatedString($packet, $offset);
+        $this->metadata->serverVersion = DataType::decodeNullTerminatedString($packet, $offset);
 
         $this->connectionId = DataType::decodeUnsigned32($packet, $offset);
 
@@ -779,8 +773,8 @@ private function handleHandshake(string $packet): void
         $this->serverCapabilities = DataType::decodeUnsigned16($packet, $offset);
 
         if (\strlen($packet) > $offset) {
-            $this->connInfo->charset = DataType::decodeUnsigned8($packet, $offset);
-            $this->connInfo->statusFlags = DataType::decodeUnsigned16($packet, $offset);
+            $this->metadata->charset = DataType::decodeUnsigned8($packet, $offset);
+            $this->metadata->statusFlags = DataType::decodeUnsigned16($packet, $offset);
             $this->serverCapabilities += DataType::decodeUnsigned16($packet, $offset) << 16;
 
             $authPluginDataLen = $this->serverCapabilities & self::CLIENT_PLUGIN_AUTH
@@ -847,10 +841,10 @@ private function handleQuery(string $packet): void
             case self::OK_PACKET:
                 $this->parseOk($packet);
 
-                if (StatusFlag::MoreResultsExist->inFlags($this->connInfo->statusFlags)) {
+                if (StatusFlag::MoreResultsExist->inFlags($this->metadata->statusFlags)) {
                     $result = new ResultProxy;
-                    $result->affectedRows = $this->connInfo->affectedRows;
-                    $result->insertId = $this->connInfo->insertId;
+                    $result->affectedRows = $this->metadata->affectedRows;
+                    $result->insertId = $this->metadata->insertId;
                     $this->getDeferred()->complete(new ConnectionResult($result));
                     $this->result = $result;
                     $result->updateState(ResultProxy::COLUMNS_FETCHED);
@@ -858,8 +852,8 @@ private function handleQuery(string $packet): void
                 } else {
                     $this->parseCallback = null;
                     $this->getDeferred()->complete(new CommandResult(
-                        $this->connInfo->affectedRows,
-                        $this->connInfo->insertId
+                        $this->metadata->affectedRows,
+                        $this->metadata->insertId
                     ));
                     $this->ready();
                 }
@@ -1041,7 +1035,7 @@ private function successfulResultFetch(): void
             $deferred = new DeferredFuture;
         }
 
-        if (StatusFlag::MoreResultsExist->inFlags($this->connInfo->statusFlags)) {
+        if (StatusFlag::MoreResultsExist->inFlags($this->metadata->statusFlags)) {
             $this->parseCallback = $this->handleQuery(...);
             $this->addDeferred($deferred);
         } else {
@@ -1148,7 +1142,7 @@ private function handlePrepare(string $packet): void
 
         $offset += 1; // filler
 
-        $this->connInfo->warnings = DataType::decodeUnsigned16($packet, $offset);
+        $this->metadata->warnings = DataType::decodeUnsigned16($packet, $offset);
 
         $this->result = new ResultProxy;
         $this->result->columnsToFetch = $params;
@@ -1176,17 +1170,17 @@ public function sendClose(): Future
     {
         return $this->startCommand(function () {
             $this->sendPacket("\x01");
-            $this->connectionState = self::CLOSING;
+            $this->connectionState = ConnectionState::Closing;
         })->finally(fn () => $this->close());
     }
 
     public function close(): void
     {
-        if ($this->connectionState === self::CLOSING && $this->deferreds) {
+        if ($this->connectionState === ConnectionState::Closing && $this->deferreds) {
             \array_pop($this->deferreds)->complete();
         }
 
-        $this->connectionState = self::CLOSED;
+        $this->connectionState = ConnectionState::Closed;
 
         if ($this->socket) {
             $this->socket->close();
@@ -1217,7 +1211,7 @@ private function write(string $packet): void
         })());
         // @codeCoverageIgnoreEnd
 
-        if (($this->capabilities & self::CLIENT_COMPRESS) && $this->connectionState >= self::READY) {
+        if (($this->capabilities & self::CLIENT_COMPRESS) && $this->connectionState >= ConnectionState::Ready) {
             $packet = $this->compressPacket($packet);
         }
 
@@ -1347,20 +1341,20 @@ private function parseMysql(): \Generator
 
     private function parsePayload(string $packet): void
     {
-        if ($this->connectionState === self::UNCONNECTED) {
+        if ($this->connectionState === ConnectionState::Unconnected) {
             $this->established();
-            $this->connectionState = self::ESTABLISHED;
+            $this->connectionState = ConnectionState::Established;
             $this->handleHandshake($packet);
             return;
         }
 
-        if ($this->connectionState === self::ESTABLISHED) {
+        if ($this->connectionState === ConnectionState::Established) {
             switch (\ord($packet)) {
                 case self::OK_PACKET:
                     if ($this->capabilities & self::CLIENT_COMPRESS) {
                         $this->processors = \array_merge([$this->parseCompression()], $this->processors);
                     }
-                    $this->connectionState = self::READY;
+                    $this->connectionState = ConnectionState::Ready;
                     $this->handleOk($packet);
                     break;
                 case self::ERR_PACKET:
@@ -1581,7 +1575,7 @@ private function getAuthData(): string
     /** @see 14.1.2 MySQL Packet */
     protected function sendPacket(string $payload): void
     {
-        if ($this->connectionState !== self::READY) {
+        if ($this->connectionState !== ConnectionState::Ready) {
             throw new \Error("Connection not ready, cannot send any packets");
         }
 
diff --git a/src/Internal/SessionStateType.php b/src/Internal/SessionStateType.php
index 6252d52..ec258a4 100644
--- a/src/Internal/SessionStateType.php
+++ b/src/Internal/SessionStateType.php
@@ -2,7 +2,11 @@
 
 namespace Amp\Mysql\Internal;
 
-/** @see 13.1.3.1.1 Session State Information */
+/**
+ * @internal
+ *
+ * @see 13.1.3.1.1 Session State Information
+ */
 enum SessionStateType: int
 {
     public const SystemVariables = 0x00;
diff --git a/src/Internal/StatusFlag.php b/src/Internal/StatusFlag.php
index de59a6d..06a0cb9 100644
--- a/src/Internal/StatusFlag.php
+++ b/src/Internal/StatusFlag.php
@@ -2,7 +2,11 @@
 
 namespace Amp\Mysql\Internal;
 
-/** @see 14.1.3.4 Status Flags */
+/**
+ * @internal
+ *
+ * @see 14.1.3.4 Status Flags
+ */
 enum StatusFlag: int
 {
     case InTransaction = 0x0001; // a transaction is active

From 6e5467229ff19965ea0238cb70d4968f69e511b3 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 25 Apr 2022 20:53:15 -0500
Subject: [PATCH 068/174] Make status flags constants again

On second thought, an enum doesn't seem correct for this purpose.
---
 src/Internal/Processor.php  | 22 +++++++++++++++++++---
 src/Internal/StatusFlag.php | 31 -------------------------------
 2 files changed, 19 insertions(+), 34 deletions(-)
 delete mode 100644 src/Internal/StatusFlag.php

diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 2536e52..26011ea 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -85,6 +85,22 @@ class Processor implements TransientResource
     private const CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 0x00200000;
     private const CLIENT_DEPRECATE_EOF = 0x01000000;
 
+    /* @see 14.1.3.4 Status Flags */
+    private const SERVER_STATUS_IN_TRANS = 0x0001; // a transaction is active
+    private const SERVER_STATUS_AUTOCOMMIT = 0x0002; // auto-commit is enabled
+    private const SERVER_MORE_RESULTS_EXISTS = 0x0008;
+    private const SERVER_STATUS_NO_GOOD_INDEX_USED = 0x0010;
+    private const SERVER_STATUS_NO_INDEX_USED = 0x0020;
+    private const SERVER_STATUS_CURSOR_EXISTS = 0x0040; // Used by Binary Protocol Resultset to signal that COM_STMT_FETCH has to be used to fetch the row-data.
+    private const SERVER_STATUS_LAST_ROW_SENT = 0x0080;
+    private const SERVER_STATUS_DB_DROPPED = 0x0100;
+    private const SERVER_STATUS_NO_BACKSLASH_ESCAPES = 0x0200;
+    private const SERVER_STATUS_METADATA_CHANGED = 0x0400;
+    private const SERVER_QUERY_WAS_SLOW = 0x0800;
+    private const SERVER_PS_OUT_PARAMS = 0x1000;
+    private const SERVER_STATUS_IN_TRANS_READONLY = 0x2000; // in a read-only transaction
+    private const SERVER_SESSION_STATE_CHANGED = 0x4000; // connection state information has changed
+
     private const OK_PACKET = 0x00;
     private const EXTRA_AUTH_PACKET = 0x01;
     private const LOCAL_INFILE_REQUEST = 0xfb;
@@ -696,7 +712,7 @@ private function parseOk(string $packet): void
 
         $this->metadata->statusInfo = DataType::decodeString($packet, $offset);
 
-        if (!StatusFlag::SessionStateChanged->inFlags($this->metadata->statusFlags)) {
+        if (!($this->metadata->statusFlags & self::SERVER_SESSION_STATE_CHANGED)) {
             return;
         }
 
@@ -841,7 +857,7 @@ private function handleQuery(string $packet): void
             case self::OK_PACKET:
                 $this->parseOk($packet);
 
-                if (StatusFlag::MoreResultsExist->inFlags($this->metadata->statusFlags)) {
+                if ($this->metadata->statusFlags & self::SERVER_MORE_RESULTS_EXISTS) {
                     $result = new ResultProxy;
                     $result->affectedRows = $this->metadata->affectedRows;
                     $result->insertId = $this->metadata->insertId;
@@ -1035,7 +1051,7 @@ private function successfulResultFetch(): void
             $deferred = new DeferredFuture;
         }
 
-        if (StatusFlag::MoreResultsExist->inFlags($this->metadata->statusFlags)) {
+        if ($this->metadata->statusFlags & self::SERVER_MORE_RESULTS_EXISTS) {
             $this->parseCallback = $this->handleQuery(...);
             $this->addDeferred($deferred);
         } else {
diff --git a/src/Internal/StatusFlag.php b/src/Internal/StatusFlag.php
deleted file mode 100644
index 06a0cb9..0000000
--- a/src/Internal/StatusFlag.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-namespace Amp\Mysql\Internal;
-
-/**
- * @internal
- *
- * @see 14.1.3.4 Status Flags
- */
-enum StatusFlag: int
-{
-    case InTransaction = 0x0001; // a transaction is active
-    case AutoCommit = 0x0002; // auto-commit is enabled
-    case MoreResultsExist = 0x0008;
-    case NoGoodIndexUsed = 0x0010;
-    case NoIndexUsed = 0x0020;
-    case CursorExists = 0x0040; // Used by Binary Protocol Result to signal that COM_STMT_FETCH must be used to fetch row-data.
-    case LastRowSent = 0x0080;
-    case DbDropped = 0x0100;
-    case NoBackslashEscapes = 0x0200;
-    case MetadataChanged = 0x0400;
-    case ServerQueryWasSlow = 0x0800;
-    case PsOutParams = 0x1000;
-    case InReadonlyTransaction = 0x2000; // in a read-only transaction
-    case SessionStateChanged = 0x4000; // connection state information has changed
-
-    public function inFlags(int $flags): bool
-    {
-        return (bool) ($this->value & $flags);
-    }
-}

From e7b67082ef45be4be8386385e1ec23a329458b59 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Fri, 27 May 2022 15:11:19 -0500
Subject: [PATCH 069/174] Update for TransactionIsolation change

---
 src/Connection.php         | 6 ++++--
 src/Internal/Processor.php | 1 -
 src/Link.php               | 5 ++++-
 src/Pool.php               | 6 ++++--
 4 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/src/Connection.php b/src/Connection.php
index 96378f8..a292772 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -6,6 +6,7 @@
 use Amp\DeferredFuture;
 use Amp\Socket\EncryptableSocket;
 use Amp\Sql\TransactionIsolation;
+use Amp\Sql\TransactionIsolationLevel;
 use Revolt\EventLoop;
 
 final class Connection implements Link
@@ -87,8 +88,9 @@ public function query(string $sql): Result
         return $this->processor->query($sql)->await();
     }
 
-    public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::Committed): Transaction
-    {
+    public function beginTransaction(
+        TransactionIsolation $isolation = TransactionIsolationLevel::Committed
+    ): Transaction {
         while ($this->busy) {
             $this->busy->getFuture()->await();
         }
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 26011ea..5b06bf4 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -529,7 +529,6 @@ public function dropDatabase(string $db): Future
     }
 
     /**
-     * @param int $subcommand
      * @see 14.6.8 COM_REFRESH
      */
     public function refresh(int $subcommand): Future
diff --git a/src/Link.php b/src/Link.php
index 5b8dbb3..33b387e 100644
--- a/src/Link.php
+++ b/src/Link.php
@@ -4,11 +4,14 @@
 
 use Amp\Sql\Link as SqlLink;
 use Amp\Sql\TransactionIsolation;
+use Amp\Sql\TransactionIsolationLevel;
 
 interface Link extends Executor, SqlLink
 {
     /**
      * @return Transaction Transaction object specific to this library.
      */
-    public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::Committed): Transaction;
+    public function beginTransaction(
+        TransactionIsolation $isolation = TransactionIsolationLevel::Committed
+    ): Transaction;
 }
diff --git a/src/Pool.php b/src/Pool.php
index 40ee4b7..36ec48e 100644
--- a/src/Pool.php
+++ b/src/Pool.php
@@ -8,6 +8,7 @@
 use Amp\Sql\Statement as SqlStatement;
 use Amp\Sql\Transaction as SqlTransaction;
 use Amp\Sql\TransactionIsolation;
+use Amp\Sql\TransactionIsolationLevel;
 
 final class Pool extends ConnectionPool implements Link
 {
@@ -81,8 +82,9 @@ public function execute(string $sql, array $params = []): Result
      *
      * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
-    public function beginTransaction(TransactionIsolation $isolation = TransactionIsolation::Committed): Transaction
-    {
+    public function beginTransaction(
+        TransactionIsolation $isolation = TransactionIsolationLevel::Committed
+    ): Transaction {
         return parent::beginTransaction($isolation);
     }
 }

From ea8b6af236adae39851a76492cf3e5defb693b37 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 28 May 2022 10:23:36 -0500
Subject: [PATCH 070/174] Update StatementPool for changes in sql-common

---
 src/StatementPool.php | 21 +++++++++++++++------
 1 file changed, 15 insertions(+), 6 deletions(-)

diff --git a/src/StatementPool.php b/src/StatementPool.php
index 7cc7981..1d4d168 100644
--- a/src/StatementPool.php
+++ b/src/StatementPool.php
@@ -4,20 +4,29 @@
 
 use Amp\Sql\Common\StatementPool as SqlStatementPool;
 use Amp\Sql\Result as SqlResult;
-use Amp\Sql\Statement as SqlStatement;
 
 final class StatementPool extends SqlStatementPool implements Statement
 {
     private array $params = [];
 
-    protected function prepare(SqlStatement $statement): Statement
+    /**
+     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
+     */
+    protected function pop(): Statement
     {
-        \assert($statement instanceof Statement);
+        $statement = parent::pop();
+
+        try {
+            \assert($statement instanceof Statement);
 
-        $statement->reset();
+            $statement->reset();
 
-        foreach ($this->params as $paramId => $data) {
-            $statement->bind($paramId, $data);
+            foreach ($this->params as $paramId => $data) {
+                $statement->bind($paramId, $data);
+            }
+        } catch (\Throwable $exception) {
+            $this->push($statement);
+            throw $exception;
         }
 
         return $statement;

From 14e558eafc65f6b0e14d24d4883bb43f62c85aa2 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 21 Jun 2022 17:04:33 -0500
Subject: [PATCH 071/174] Update for TransientResource extending Closable

---
 src/Connection.php                     | 11 ++++--
 src/Internal/ConnectionStatement.php   | 35 ++++++++++++-----
 src/Internal/ConnectionTransaction.php | 44 +++++++++++++++------
 src/Internal/Processor.php             | 53 ++++++++++++++------------
 src/StatementPool.php                  |  1 -
 test/ConnectionTest.php                |  2 +-
 test/PoolTest.php                      |  6 +--
 test/StatementTest.php                 |  4 +-
 8 files changed, 102 insertions(+), 54 deletions(-)

diff --git a/src/Connection.php b/src/Connection.php
index a292772..056a1b0 100644
--- a/src/Connection.php
+++ b/src/Connection.php
@@ -43,9 +43,9 @@ private function __construct(Internal\Processor $processor)
     /**
      * @return bool False if the connection has been closed.
      */
-    public function isAlive(): bool
+    public function isClosed(): bool
     {
-        return $this->processor->isAlive();
+        return $this->processor->isClosed();
     }
 
     /**
@@ -69,11 +69,16 @@ public function setCharset(string $charset, string $collate = ""): void
     public function close(): void
     {
         // Send close command if connection is not already in a closed or closing state
-        if ($this->processor->isAlive()) {
+        if (!$this->processor->isClosed()) {
             $this->processor->sendClose()->await();
         }
     }
 
+    public function onClose(\Closure $onClose): void
+    {
+        $this->processor->onClose($onClose);
+    }
+
     public function useDb(string $db): void
     {
         $this->processor->useDb($db)->await();
diff --git a/src/Internal/ConnectionStatement.php b/src/Internal/ConnectionStatement.php
index 19fd487..b7f73a2 100644
--- a/src/Internal/ConnectionStatement.php
+++ b/src/Internal/ConnectionStatement.php
@@ -24,6 +24,8 @@ final class ConnectionStatement implements Statement
 
     private int $lastUsedAt;
 
+    private readonly DeferredFuture $onClose;
+
     public function __construct(Processor $processor, string $query, int $stmtId, array $named, ResultProxy $result)
     {
         $this->processor = $processor;
@@ -32,6 +34,7 @@ public function __construct(Processor $processor, string $query, int $stmtId, ar
         $this->result = $result;
         $this->numParamCount = $this->paramCount = $this->result->columnsToFetch;
         $this->byNamed = $named;
+        $this->onClose = new DeferredFuture();
 
         foreach ($named as $name => $ids) {
             foreach ($ids as $id) {
@@ -49,20 +52,29 @@ private function getProcessor(): Processor
             throw new \Error("The statement has been closed");
         }
 
-        if (!$this->processor->isAlive()) {
+        if ($this->processor->isClosed()) {
             throw new ConnectionException("Connection went away");
         }
 
         return $this->processor;
     }
 
-    public function isAlive(): bool
+    public function isClosed(): bool
     {
-        if ($this->processor === null) {
-            return false;
+        return !$this->processor || $this->processor->isClosed();
+    }
+
+    public function close(): void
+    {
+        if ($this->processor) {
+            self::shutdown($this->processor, $this->stmtId, $this->onClose);
+            $this->processor = null;
         }
+    }
 
-        return $this->processor->isAlive();
+    public function onClose(\Closure $onClose): void
+    {
+        $this->onClose->getFuture()->finally($onClose);
     }
 
     public function bind(int|string $paramId, mixed $data): void
@@ -169,13 +181,18 @@ public function __destruct()
         if ($this->processor) {
             $processor = $this->processor;
             $stmtId = $this->stmtId;
-            EventLoop::queue(static fn () => self::close($processor, $stmtId));
+            $onClose = $this->onClose;
+            EventLoop::queue(static fn () => self::shutdown($processor, $stmtId, $onClose));
         }
     }
 
-    private static function close(Processor $processor, int $stmtId): void
+    private static function shutdown(Processor $processor, int $stmtId, DeferredFuture $onClose): void
     {
-        $processor->closeStmt($stmtId);
-        $processor->unreference();
+        try {
+            $processor->closeStmt($stmtId);
+            $processor->unreference();
+        } finally {
+            $onClose->complete();
+        }
     }
 }
diff --git a/src/Internal/ConnectionTransaction.php b/src/Internal/ConnectionTransaction.php
index 8e9ca6b..affcfd0 100644
--- a/src/Internal/ConnectionTransaction.php
+++ b/src/Internal/ConnectionTransaction.php
@@ -2,11 +2,13 @@
 
 namespace Amp\Mysql\Internal;
 
+use Amp\DeferredFuture;
 use Amp\Mysql\PooledResult;
 use Amp\Mysql\PooledStatement;
 use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
 use Amp\Mysql\Transaction;
+use Amp\Sql\SqlException;
 use Amp\Sql\TransactionError;
 use Amp\Sql\TransactionIsolation;
 use Revolt\EventLoop;
@@ -24,6 +26,8 @@ final class ConnectionTransaction implements Transaction
 
     private int $refCount = 1;
 
+    private readonly DeferredFuture $onClose;
+
     /**
      * @param \Closure():void $release
      *
@@ -43,18 +47,31 @@ public function __construct(
                 $release();
             }
         };
+
+        $this->onClose = new DeferredFuture();
+        $this->onClose($this->release);
     }
 
     public function __destruct()
     {
-        if ($this->processor && $this->processor->isAlive()) {
-            $processor = $this->processor;
-            $release = $this->release;
-            EventLoop::queue(static function () use ($processor, $release): void {
-                $processor->query("ROLLBACK");
-                $release();
-            });
+        if ($this->onClose->isComplete()) {
+            return;
+        }
+
+        $this->onClose->complete();
+
+        if (!$this->processor || $this->processor->isClosed()) {
+            return;
         }
+
+        $processor = $this->processor;
+        EventLoop::queue(static function () use ($processor): void {
+            try {
+                !$processor->isClosed() && $processor->query('ROLLBACK');
+            } catch (SqlException) {
+                // Ignore failure if connection closes during query.
+            }
+        });
     }
 
     public function getLastUsedAt(): int
@@ -72,9 +89,14 @@ public function close(): void
         }
     }
 
-    public function isAlive(): bool
+    public function isClosed(): bool
+    {
+        return !$this->processor || $this->processor->isClosed();
+    }
+
+    public function onClose(\Closure $onClose): void
     {
-        return $this->processor && $this->processor->isAlive();
+        $this->onClose->getFuture()->finally($onClose);
     }
 
     /**
@@ -148,7 +170,7 @@ public function commit(): void
 
         $promise = $this->processor->query("COMMIT");
         $this->processor = null;
-        $promise->finally($this->release)->await();
+        $promise->finally($this->onClose->complete(...))->await();
     }
 
     /**
@@ -164,7 +186,7 @@ public function rollback(): void
 
         $promise = $this->processor->query("ROLLBACK");
         $this->processor = null;
-        $promise->finally($this->release)->await();
+        $promise->finally($this->onClose->complete(...))->await();
     }
 
     /**
diff --git a/src/Internal/Processor.php b/src/Internal/Processor.php
index 5b06bf4..0c4ded3 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/Processor.php
@@ -35,7 +35,7 @@ class Processor implements TransientResource
     private int $seqId = -1;
     private int $compressionId = -1;
 
-    private ?EncryptableSocket $socket;
+    private readonly EncryptableSocket $socket;
 
     private ?string $query = null;
     public array $named = [];
@@ -48,7 +48,7 @@ class Processor implements TransientResource
     /** @var DeferredFuture[] */
     private array $deferreds = [];
 
-    /** @var callable[] */
+    /** @var array<int, \Closure():void> */
     private array $onReady = [];
 
     private ?ResultProxy $result = null;
@@ -111,14 +111,29 @@ class Processor implements TransientResource
     public function __construct(EncryptableSocket $socket, MysqlConfig $config)
     {
         $this->socket = $socket;
-        $this->metadata = new ConnectionMetadata;
+        $this->metadata = new ConnectionMetadata();
         $this->config = $config;
         $this->lastUsedAt = \time();
     }
 
-    public function isAlive(): bool
+    public function __destruct()
     {
-        return $this->connectionState <= ConnectionState::Ready;
+        if (!$this->isClosed()) {
+            $this->close();
+        }
+    }
+
+    public function isClosed(): bool
+    {
+        return match ($this->connectionState) {
+            ConnectionState::Closing, ConnectionState::Closed => true,
+            default => false,
+        };
+    }
+
+    public function onClose(\Closure $onClose): void
+    {
+        $this->socket->onClose($onClose);
     }
 
     public function isReady(): bool
@@ -147,20 +162,12 @@ private function ready(): void
         }
 
         $this->resetIds();
-
-        if ($this->socket) {
-            try {
-                $this->socket->unreference();
-            } catch (EventLoop\InvalidCallbackError) {
-                // Undefined destruct order can cause unref of an invalid watcher if the loop is swapped.
-                // Generally this will only happen during tests.
-            }
-        }
+        $this->socket->unreference();
     }
 
     private function addDeferred(DeferredFuture $deferred): void
     {
-        \assert($this->socket !== null, "The connection has been closed");
+        \assert(!$this->socket->isClosed(), "The connection has been closed");
         $this->deferreds[] = $deferred;
         $this->socket->reference();
     }
@@ -199,7 +206,7 @@ public function connect(?Cancellation $cancellation = null): void
     private function read(): void
     {
         try {
-            while ($this->socket !== null && ($bytes = $this->socket->read()) !== null) {
+            while (($bytes = $this->socket->read()) !== null) {
                 // @codeCoverageIgnoreStart
                 \assert((function () use ($bytes) {
                     if (\defined("MYSQL_DEBUG")) {
@@ -259,6 +266,9 @@ private function getDeferred(): DeferredFuture
         return \array_shift($this->deferreds);
     }
 
+    /**
+     * @param \Closure():void $callback
+     */
     private function appendTask(\Closure $callback): void
     {
         if ($this->packetCallback
@@ -1196,12 +1206,7 @@ public function close(): void
         }
 
         $this->connectionState = ConnectionState::Closed;
-
-        if ($this->socket) {
-            $this->socket->close();
-            $this->socket = null;
-        }
-
+        $this->socket->close();
         $this->processors = [];
     }
 
@@ -1230,7 +1235,7 @@ private function write(string $packet): void
             $packet = $this->compressPacket($packet);
         }
 
-        \assert($this->socket !== null, 'The connection was closed during a call to write');
+        \assert(!$this->socket->isClosed(), 'The connection was closed during a call to write');
         $this->socket->write($packet);
     }
 
@@ -1515,7 +1520,7 @@ private function sendHandshake(bool $inSSL = false): void
                 try {
                     $this->write($payload);
 
-                    $this->socket?->setupTls();
+                    $this->socket->setupTls();
 
                     $this->sendHandshake(true);
                 } catch (\Throwable $e) {
diff --git a/src/StatementPool.php b/src/StatementPool.php
index 1d4d168..63dfddb 100644
--- a/src/StatementPool.php
+++ b/src/StatementPool.php
@@ -62,7 +62,6 @@ public function reset(): void
     public function getColumnDefinitions(): ?array
     {
         $statement = $this->pop();
-        \assert($statement instanceof Statement);
         $columns = $statement->getColumnDefinitions();
         $this->push($statement);
         return $columns;
diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php
index 59eda27..17812e0 100644
--- a/test/ConnectionTest.php
+++ b/test/ConnectionTest.php
@@ -34,7 +34,7 @@ public function testDoubleClose()
 
         $db->close();
 
-        $this->assertFalse($db->isAlive());
+        $this->assertTrue($db->isClosed());
 
         $db->close(); // Should not throw an exception.
     }
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 710fe8c..108da5d 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -51,7 +51,7 @@ private function makeProcessorSet(int $count): array
 
         for ($i = 0; $i < $count; ++$i) {
             $processor = $this->createMock(Processor::class);
-            $processor->method('isAlive')->willReturn(true);
+            $processor->method('isClosed')->willReturn(false);
             $processor->method('sendClose')->willReturn(Future::complete(null));
             $processors[] = $processor;
         }
@@ -251,8 +251,8 @@ public function testConnectionClosedInPool(int $count)
         }
 
         $processor = $this->createMock(Processor::class);
-        $processor->method('isAlive')
-            ->willReturnOnConsecutiveCalls(true, false);
+        $processor->method('isClosed')
+            ->willReturnOnConsecutiveCalls(false, true);
         $processor->expects($this->once())
             ->method('query')
             ->with($query)
diff --git a/test/StatementTest.php b/test/StatementTest.php
index e8f2c07..10dac3a 100644
--- a/test/StatementTest.php
+++ b/test/StatementTest.php
@@ -34,8 +34,8 @@ public function testBindDataTypes($data, $expectedException)
         $paramId = 0;
         $named = [];
 
-        $this->processor->method('isAlive')
-            ->willReturn(true);
+        $this->processor->method('isClosed')
+            ->willReturn(false);
         $this->processor->expects($this->once())
             ->method('unreference');
         $this->processor->expects($this->once())

From 51c81b3d54f31bd143f56be1750f6c1ffea0e976 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 21 Jun 2022 17:04:50 -0500
Subject: [PATCH 072/174] Update LICENSE

---
 LICENSE | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/LICENSE b/LICENSE
index 3be5784..5961b70 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2015-2017 amphp
+Copyright (c) 2015-2022 amphp (Aaron Piotrowski, Niklas Keller, Bob Weinand, and contributors)
 
 Contributions are licensed subject to the rules in CONTRIBUTORS.md.
 
@@ -20,4 +20,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-IN THE SOFTWARE.
\ No newline at end of file
+IN THE SOFTWARE.

From 84d19f71b0e70d8a81e1520598bd45f6516b0799 Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Sun, 26 Jun 2022 23:00:23 +0200
Subject: [PATCH 073/174] Add missing @internal annotations

---
 src/Internal/CommandResult.php         | 1 +
 src/Internal/ConnectionMetadata.php    | 1 +
 src/Internal/ConnectionResult.php      | 1 +
 src/Internal/ConnectionState.php       | 4 +---
 src/Internal/ConnectionStatement.php   | 1 +
 src/Internal/ConnectionTransaction.php | 1 +
 src/Internal/ResultProxy.php           | 1 +
 7 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/Internal/CommandResult.php b/src/Internal/CommandResult.php
index 8cbc481..5b4f834 100644
--- a/src/Internal/CommandResult.php
+++ b/src/Internal/CommandResult.php
@@ -6,6 +6,7 @@
 use Amp\Mysql\Result;
 use Amp\Sql\Common\CommandResult as SqlCommandResult;
 
+/** @internal */
 final class CommandResult implements Result, \IteratorAggregate
 {
     private ?int $lastInsertId;
diff --git a/src/Internal/ConnectionMetadata.php b/src/Internal/ConnectionMetadata.php
index 8ba3100..13c6125 100644
--- a/src/Internal/ConnectionMetadata.php
+++ b/src/Internal/ConnectionMetadata.php
@@ -2,6 +2,7 @@
 
 namespace Amp\Mysql\Internal;
 
+/** @internal */
 final class ConnectionMetadata
 {
     public int $affectedRows = 0;
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/ConnectionResult.php
index 3098d4d..82a555b 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/ConnectionResult.php
@@ -10,6 +10,7 @@
 use Revolt\EventLoop;
 use function Amp\async;
 
+/** @internal */
 final class ConnectionResult implements Result, \IteratorAggregate
 {
     private readonly ResultProxy $result;
diff --git a/src/Internal/ConnectionState.php b/src/Internal/ConnectionState.php
index f7586d8..1403ed3 100644
--- a/src/Internal/ConnectionState.php
+++ b/src/Internal/ConnectionState.php
@@ -2,9 +2,7 @@
 
 namespace Amp\Mysql\Internal;
 
-/**
- * @internal
- */
+/** @internal */
 enum ConnectionState
 {
     case Unconnected;
diff --git a/src/Internal/ConnectionStatement.php b/src/Internal/ConnectionStatement.php
index b7f73a2..3973913 100644
--- a/src/Internal/ConnectionStatement.php
+++ b/src/Internal/ConnectionStatement.php
@@ -8,6 +8,7 @@
 use Amp\Sql\ConnectionException;
 use Revolt\EventLoop;
 
+/** @internal */
 final class ConnectionStatement implements Statement
 {
     private int $paramCount;
diff --git a/src/Internal/ConnectionTransaction.php b/src/Internal/ConnectionTransaction.php
index affcfd0..a2219a6 100644
--- a/src/Internal/ConnectionTransaction.php
+++ b/src/Internal/ConnectionTransaction.php
@@ -13,6 +13,7 @@
 use Amp\Sql\TransactionIsolation;
 use Revolt\EventLoop;
 
+/** @internal */
 final class ConnectionTransaction implements Transaction
 {
     const SAVEPOINT_PREFIX = "amp_";
diff --git a/src/Internal/ResultProxy.php b/src/Internal/ResultProxy.php
index 94446c7..022b184 100644
--- a/src/Internal/ResultProxy.php
+++ b/src/Internal/ResultProxy.php
@@ -5,6 +5,7 @@
 use Amp\DeferredFuture;
 use Amp\Mysql\ColumnDefinition;
 
+/** @internal */
 final class ResultProxy
 {
     public int $columnCount = 0;

From e6c61c32f245400c46995fc65f6f87bbc4d717f8 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Fri, 1 Jul 2022 23:17:39 -0500
Subject: [PATCH 074/174] Fix borked enum

---
 src/Internal/SessionStateType.php | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/Internal/SessionStateType.php b/src/Internal/SessionStateType.php
index ec258a4..281eeb8 100644
--- a/src/Internal/SessionStateType.php
+++ b/src/Internal/SessionStateType.php
@@ -9,10 +9,10 @@
  */
 enum SessionStateType: int
 {
-    public const SystemVariables = 0x00;
-    public const Schema = 0x01;
-    public const StateChange = 0x02;
-    public const Gtids = 0x03;
-    public const TransactionCharacteristics = 0x04;
-    public const TransactionState = 0x05;
+    case SystemVariables = 0x00;
+    case Schema = 0x01;
+    case StateChange = 0x02;
+    case Gtids = 0x03;
+    case TransactionCharacteristics = 0x04;
+    case TransactionState = 0x05;
 }

From 9ab30521ebcaa3cb95d90f12ab7beeef36a346b0 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 4 Jul 2022 12:46:04 -0500
Subject: [PATCH 075/174] Move some classes to be internal

---
 src/Internal/ConnectionTransaction.php   | 2 --
 src/{ => Internal}/PooledResult.php      | 4 +++-
 src/{ => Internal}/PooledStatement.php   | 5 ++++-
 src/{ => Internal}/PooledTransaction.php | 6 +++++-
 src/{ => Internal}/StatementPool.php     | 5 ++++-
 src/Pool.php                             | 9 +++++----
 6 files changed, 21 insertions(+), 10 deletions(-)
 rename src/{ => Internal}/PooledResult.php (92%)
 rename src/{ => Internal}/PooledStatement.php (92%)
 rename src/{ => Internal}/PooledTransaction.php (92%)
 rename src/{ => Internal}/StatementPool.php (94%)

diff --git a/src/Internal/ConnectionTransaction.php b/src/Internal/ConnectionTransaction.php
index a2219a6..688cffa 100644
--- a/src/Internal/ConnectionTransaction.php
+++ b/src/Internal/ConnectionTransaction.php
@@ -3,8 +3,6 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\DeferredFuture;
-use Amp\Mysql\PooledResult;
-use Amp\Mysql\PooledStatement;
 use Amp\Mysql\Result;
 use Amp\Mysql\Statement;
 use Amp\Mysql\Transaction;
diff --git a/src/PooledResult.php b/src/Internal/PooledResult.php
similarity index 92%
rename from src/PooledResult.php
rename to src/Internal/PooledResult.php
index 0921d9f..610a15c 100644
--- a/src/PooledResult.php
+++ b/src/Internal/PooledResult.php
@@ -1,10 +1,12 @@
 <?php
 
-namespace Amp\Mysql;
+namespace Amp\Mysql\Internal;
 
+use Amp\Mysql\Result;
 use Amp\Sql\Common\PooledResult as SqlPooledResult;
 use Amp\Sql\Result as SqlResult;
 
+/** @internal */
 final class PooledResult extends SqlPooledResult implements Result
 {
     private readonly Result $result;
diff --git a/src/PooledStatement.php b/src/Internal/PooledStatement.php
similarity index 92%
rename from src/PooledStatement.php
rename to src/Internal/PooledStatement.php
index f9c2497..08ff56d 100644
--- a/src/PooledStatement.php
+++ b/src/Internal/PooledStatement.php
@@ -1,10 +1,13 @@
 <?php
 
-namespace Amp\Mysql;
+namespace Amp\Mysql\Internal;
 
+use Amp\Mysql\Result;
+use Amp\Mysql\Statement;
 use Amp\Sql\Common\PooledStatement as SqlPooledStatement;
 use Amp\Sql\Result as SqlResult;
 
+/** @internal */
 final class PooledStatement extends SqlPooledStatement implements Statement
 {
     private readonly Statement $statement;
diff --git a/src/PooledTransaction.php b/src/Internal/PooledTransaction.php
similarity index 92%
rename from src/PooledTransaction.php
rename to src/Internal/PooledTransaction.php
index 4c12951..2b755d6 100644
--- a/src/PooledTransaction.php
+++ b/src/Internal/PooledTransaction.php
@@ -1,11 +1,15 @@
 <?php
 
-namespace Amp\Mysql;
+namespace Amp\Mysql\Internal;
 
+use Amp\Mysql\Result;
+use Amp\Mysql\Statement;
+use Amp\Mysql\Transaction;
 use Amp\Sql\Common\PooledTransaction as SqlPooledTransaction;
 use Amp\Sql\Result as SqlResult;
 use Amp\Sql\Statement as SqlStatement;
 
+/** @internal */
 final class PooledTransaction extends SqlPooledTransaction implements Transaction
 {
     protected function createStatement(SqlStatement $statement, \Closure $release): Statement
diff --git a/src/StatementPool.php b/src/Internal/StatementPool.php
similarity index 94%
rename from src/StatementPool.php
rename to src/Internal/StatementPool.php
index 63dfddb..f8e9799 100644
--- a/src/StatementPool.php
+++ b/src/Internal/StatementPool.php
@@ -1,10 +1,13 @@
 <?php
 
-namespace Amp\Mysql;
+namespace Amp\Mysql\Internal;
 
+use Amp\Mysql\Result;
+use Amp\Mysql\Statement;
 use Amp\Sql\Common\StatementPool as SqlStatementPool;
 use Amp\Sql\Result as SqlResult;
 
+/** @internal */
 final class StatementPool extends SqlStatementPool implements Statement
 {
     private array $params = [];
diff --git a/src/Pool.php b/src/Pool.php
index 36ec48e..e1ca952 100644
--- a/src/Pool.php
+++ b/src/Pool.php
@@ -3,6 +3,7 @@
 namespace Amp\Mysql;
 
 use Amp\Sql\Common\ConnectionPool;
+use Amp\Sql\Common\StatementPool;
 use Amp\Sql\Pool as SqlPool;
 use Amp\Sql\Result as SqlResult;
 use Amp\Sql\Statement as SqlStatement;
@@ -28,23 +29,23 @@ public function __construct(
     protected function createResult(SqlResult $result, \Closure $release): Result
     {
         \assert($result instanceof Result);
-        return new PooledResult($result, $release);
+        return new Internal\PooledResult($result, $release);
     }
 
     protected function createStatement(SqlStatement $statement, \Closure $release): Statement
     {
         \assert($statement instanceof Statement);
-        return new PooledStatement($statement, $release);
+        return new Internal\PooledStatement($statement, $release);
     }
 
     protected function createStatementPool(SqlPool $pool, string $sql, \Closure $prepare): StatementPool
     {
-        return new StatementPool($pool, $sql, $prepare);
+        return new Internal\StatementPool($pool, $sql, $prepare);
     }
 
     protected function createTransaction(SqlTransaction $transaction, \Closure $release): Transaction
     {
-        return new PooledTransaction($transaction, $release);
+        return new Internal\PooledTransaction($transaction, $release);
     }
 
     /**

From 84c804dee4ef2f21aa9970e93a7547ff906dd6e7 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 4 Jul 2022 13:06:47 -0500
Subject: [PATCH 076/174] Prefix classes and interfaces with Mysql

Provides clarity between those defined in amphp/sql and this library.
---
 benchmarks/QueryBench.php                     |   6 +-
 examples/4-multi-rows.php                     |   4 +-
 src/DefaultMysqlConnector.php                 |   4 +-
 src/Executor.php                              |  23 ---
 ...{Processor.php => ConnectionProcessor.php} | 178 +++++++++---------
 ...mmandResult.php => MysqlCommandResult.php} |   6 +-
 ...etadata.php => MysqlConnectionMetdata.php} |   2 +-
 ...onResult.php => MysqlConnectionResult.php} |  26 +--
 ...ement.php => MysqlConnectionStatement.php} |  26 +--
 ...ion.php => MysqlConnectionTransaction.php} |  24 +--
 ...PooledResult.php => MysqlPooledResult.php} |  10 +-
 ...Statement.php => MysqlPooledStatement.php} |  18 +-
 ...saction.php => MysqlPooledTransaction.php} |  26 +--
 .../{ResultProxy.php => MysqlResultProxy.php} |   6 +-
 ...atementPool.php => MysqlStatementPool.php} |  20 +-
 ...finition.php => MysqlColumnDefinition.php} |   4 +-
 src/{Connection.php => MysqlConnection.php}   |  18 +-
 src/MysqlConnector.php                        |   2 +-
 src/{DataType.php => MysqlDataType.php}       |   4 +-
 src/MysqlExecutor.php                         |  23 +++
 src/{Link.php => MysqlLink.php}               |   8 +-
 src/{Pool.php => MysqlPool.php}               |  30 +--
 src/{Result.php => MysqlResult.php}           |   6 +-
 src/{Statement.php => MysqlStatement.php}     |   8 +-
 src/MysqlTransaction.php                      |   9 +
 src/Transaction.php                           |   9 -
 src/functions.php                             |  10 +-
 test/ConnectionTest.php                       |   8 +-
 test/LinkTest.php                             |  50 ++---
 test/PoolTest.php                             |  66 +++----
 test/StatementTest.php                        |  18 +-
 31 files changed, 326 insertions(+), 326 deletions(-)
 delete mode 100644 src/Executor.php
 rename src/Internal/{Processor.php => ConnectionProcessor.php} (88%)
 rename src/Internal/{CommandResult.php => MysqlCommandResult.php} (89%)
 rename src/Internal/{ConnectionMetadata.php => MysqlConnectionMetdata.php} (93%)
 rename src/Internal/{ConnectionResult.php => MysqlConnectionResult.php} (79%)
 rename src/Internal/{ConnectionStatement.php => MysqlConnectionStatement.php} (84%)
 rename src/Internal/{ConnectionTransaction.php => MysqlConnectionTransaction.php} (90%)
 rename src/Internal/{PooledResult.php => MysqlPooledResult.php} (71%)
 rename src/Internal/{PooledStatement.php => MysqlPooledStatement.php} (65%)
 rename src/Internal/{PooledTransaction.php => MysqlPooledTransaction.php} (60%)
 rename src/Internal/{ResultProxy.php => MysqlResultProxy.php} (95%)
 rename src/Internal/{StatementPool.php => MysqlStatementPool.php} (74%)
 rename src/{ColumnDefinition.php => MysqlColumnDefinition.php} (87%)
 rename src/{Connection.php => MysqlConnection.php} (85%)
 rename src/{DataType.php => MysqlDataType.php} (99%)
 create mode 100644 src/MysqlExecutor.php
 rename src/{Link.php => MysqlLink.php} (58%)
 rename src/{Pool.php => MysqlPool.php} (69%)
 rename src/{Result.php => MysqlResult.php} (73%)
 rename src/{Statement.php => MysqlStatement.php} (73%)
 create mode 100644 src/MysqlTransaction.php
 delete mode 100644 src/Transaction.php

diff --git a/benchmarks/QueryBench.php b/benchmarks/QueryBench.php
index 219a686..8468c1b 100644
--- a/benchmarks/QueryBench.php
+++ b/benchmarks/QueryBench.php
@@ -3,9 +3,9 @@
 namespace Amp\Mysql\Bench;
 
 use Amp\Mysql\DefaultMysqlConnector;
-use Amp\Mysql\Connection;
+use Amp\Mysql\MysqlConnection;
 use Amp\Mysql\MysqlConfig;
-use Amp\Mysql\Pool as ConnectionPool;
+use Amp\Mysql\MysqlPool as ConnectionPool;
 use Amp\Mysql\ResultSet;
 use function Amp\call;
 use function Amp\Promise\wait;
@@ -23,7 +23,7 @@ class QueryBench extends AbstractBench
     /** @var  ConnectionPool */
     protected $connectionPool;
 
-    /** @var  Connection */
+    /** @var  MysqlConnection */
     protected $connection;
 
     /** @var  \PDO */
diff --git a/examples/4-multi-rows.php b/examples/4-multi-rows.php
index 4b2cac2..2786ac1 100644
--- a/examples/4-multi-rows.php
+++ b/examples/4-multi-rows.php
@@ -19,8 +19,8 @@
 
 try {
     /**
-     * @var Mysql\Result $result1
-     * @var Mysql\Result $result2
+     * @var Mysql\MysqlResult $result1
+     * @var Mysql\MysqlResult $result2
      */
     [$result1, $result2] = Future\await($future); // Both queries execute simultaneously. Wait for both to finish here.
 } catch (\Throwable $e) {
diff --git a/src/DefaultMysqlConnector.php b/src/DefaultMysqlConnector.php
index f219a2e..0223e9f 100644
--- a/src/DefaultMysqlConnector.php
+++ b/src/DefaultMysqlConnector.php
@@ -12,7 +12,7 @@ public function __construct(private readonly ?Socket\SocketConnector $connector
     {
     }
 
-    public function connect(SqlConfig $config, ?Cancellation $cancellation = null): Connection
+    public function connect(SqlConfig $config, ?Cancellation $cancellation = null): MysqlConnection
     {
         if (!$config instanceof MysqlConfig) {
             throw new \TypeError(\sprintf("Must provide an instance of %s to MySQL connectors", MysqlConfig::class));
@@ -21,6 +21,6 @@ public function connect(SqlConfig $config, ?Cancellation $cancellation = null):
         $socket = ($this->connector ?? Socket\socketConnector())
             ->connect($config->getConnectionString(), $config->getConnectContext(), $cancellation);
 
-        return Connection::initialize($socket, $config, $cancellation);
+        return MysqlConnection::initialize($socket, $config, $cancellation);
     }
 }
diff --git a/src/Executor.php b/src/Executor.php
deleted file mode 100644
index 1528770..0000000
--- a/src/Executor.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-use Amp\Sql\Executor as SqlExecutor;
-
-interface Executor extends SqlExecutor
-{
-    /**
-     * @return Result Result object specific to this library.
-     */
-    public function query(string $sql): Result;
-
-    /**
-     * @return Statement Statement object specific to this library.
-     */
-    public function prepare(string $sql): Statement;
-
-    /**
-     * @return Result Result object specific to this library.
-     */
-    public function execute(string $sql, array $params = []): Result;
-}
diff --git a/src/Internal/Processor.php b/src/Internal/ConnectionProcessor.php
similarity index 88%
rename from src/Internal/Processor.php
rename to src/Internal/ConnectionProcessor.php
index 0c4ded3..6d22e51 100644
--- a/src/Internal/Processor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -6,10 +6,10 @@
 use Amp\DeferredFuture;
 use Amp\File;
 use Amp\Future;
-use Amp\Mysql\ColumnDefinition;
-use Amp\Mysql\DataType;
 use Amp\Mysql\InitializationException;
+use Amp\Mysql\MysqlColumnDefinition;
 use Amp\Mysql\MysqlConfig;
+use Amp\Mysql\MysqlDataType;
 use Amp\Socket\EncryptableSocket;
 use Amp\Sql\ConnectionException;
 use Amp\Sql\QueryError;
@@ -23,7 +23,7 @@
  */
 
 /** @internal */
-class Processor implements TransientResource
+class ConnectionProcessor implements TransientResource
 {
     const STATEMENT_PARAM_REGEX = <<<'REGEX'
 ~(["'`])(?:\\(?:\\|\1)|(?!\1).)*+\1(*SKIP)(*FAIL)|(\?)|:([a-zA-Z_][a-zA-Z0-9_]*)~ms
@@ -51,7 +51,7 @@ class Processor implements TransientResource
     /** @var array<int, \Closure():void> */
     private array $onReady = [];
 
-    private ?ResultProxy $result = null;
+    private ?MysqlResultProxy $result = null;
 
     private int $lastUsedAt;
 
@@ -60,7 +60,7 @@ class Processor implements TransientResource
     private int $capabilities = 0;
     private int $serverCapabilities = 0;
     private string $authPluginName = '';
-    private ConnectionMetadata $metadata;
+    private MysqlConnectionMetdata $metadata;
     private int $refcount = 1;
 
     private ConnectionState $connectionState = ConnectionState::Unconnected;
@@ -111,7 +111,7 @@ class Processor implements TransientResource
     public function __construct(EncryptableSocket $socket, MysqlConfig $config)
     {
         $this->socket = $socket;
-        $this->metadata = new ConnectionMetadata();
+        $this->metadata = new MysqlConnectionMetdata();
         $this->config = $config;
         $this->lastUsedAt = \time();
     }
@@ -283,7 +283,7 @@ private function appendTask(\Closure $callback): void
         }
     }
 
-    public function getMetadata(): ConnectionMetadata
+    public function getMetadata(): MysqlConnectionMetdata
     {
         return clone $this->metadata;
     }
@@ -411,8 +411,8 @@ public function resetConnection(): Future
     public function bindParam(int $stmtId, int $paramId, string $data): void
     {
         $payload = "\x18";
-        $payload .= DataType::encodeInt32($stmtId);
-        $payload .= DataType::encodeInt16($paramId);
+        $payload .= MysqlDataType::encodeInt32($stmtId);
+        $payload .= MysqlDataType::encodeInt16($paramId);
         $payload .= $data;
         $this->appendTask(function () use ($payload) {
             $this->resetIds();
@@ -434,9 +434,9 @@ public function execute(int $stmtId, string $query, array &$params, array $prebo
         $deferred = new DeferredFuture;
         $this->appendTask(function () use ($stmtId, $query, &$params, $prebound, $data, $deferred): void {
             $payload = "\x17";
-            $payload .= DataType::encodeInt32($stmtId);
+            $payload .= MysqlDataType::encodeInt32($stmtId);
             $payload .= \chr(0); // cursor flag // @TODO cursor types?!
-            $payload .= DataType::encodeInt32(1);
+            $payload .= MysqlDataType::encodeInt32(1);
             $paramCount = \count($params);
             $bound = (!empty($data) || !empty($prebound)) ? 1 : 0;
             $types = "";
@@ -455,10 +455,10 @@ public function execute(int $stmtId, string $query, array &$params, array $prebo
                         $bound = 1;
                     }
 
-                    /** @var DataType $type */
-                    [$type, $value] = DataType::encodeBinary($param);
+                    /** @var MysqlDataType $type */
+                    [$type, $value] = MysqlDataType::encodeBinary($param);
                     if (isset($prebound[$paramId])) {
-                        $types .= \chr(DataType::LongBlob->value);
+                        $types .= \chr(MysqlDataType::LongBlob->value);
                     } else {
                         $types .= \chr($type->value);
                     }
@@ -486,7 +486,7 @@ public function execute(int $stmtId, string $query, array &$params, array $prebo
     /** @see 14.7.7 COM_STMT_CLOSE */
     public function closeStmt(int $stmtId): void
     {
-        $payload = "\x19" . DataType::encodeInt32($stmtId);
+        $payload = "\x19" . MysqlDataType::encodeInt32($stmtId);
         $this->appendTask(function () use ($payload): void {
             if ($this->connectionState === ConnectionState::Ready) {
                 $this->resetIds();
@@ -578,7 +578,7 @@ public function processInfo(): Future
     public function killProcess(int $process): Future
     {
         return $this->startCommand(function () use ($process): void {
-            $this->sendPacket("\x0c" . DataType::encodeInt32($process));
+            $this->sendPacket("\x0c" . MysqlDataType::encodeInt32($process));
         });
     }
 
@@ -593,7 +593,7 @@ public function debugStdout(): Future
     /** @see 14.7.8 COM_STMT_RESET */
     public function resetStmt(int $stmtId): Future
     {
-        $payload = "\x1a" . DataType::encodeInt32($stmtId);
+        $payload = "\x1a" . MysqlDataType::encodeInt32($stmtId);
         $deferred = new DeferredFuture;
         $this->appendTask(function () use ($payload, $deferred): void {
             $this->resetIds();
@@ -606,7 +606,7 @@ public function resetStmt(int $stmtId): Future
     /** @see 14.8.4 COM_STMT_FETCH */
     public function fetchStmt(int $stmtId): Future
     {
-        $payload = "\x1c" . DataType::encodeInt32($stmtId) . DataType::encodeInt32(1);
+        $payload = "\x1c" . MysqlDataType::encodeInt32($stmtId) . MysqlDataType::encodeInt32(1);
         $deferred = new DeferredFuture;
         $this->appendTask(function () use ($payload, $deferred): void {
             $this->resetIds();
@@ -643,7 +643,7 @@ private function handleError(string $packet): void
     {
         $offset = 1;
 
-        $this->metadata->errorCode = DataType::decodeUnsigned16($packet, $offset);
+        $this->metadata->errorCode = MysqlDataType::decodeUnsigned16($packet, $offset);
 
         if ($this->capabilities & self::CLIENT_PROTOCOL_41) {
             $this->metadata->errorState = \substr($packet, $offset, 6);
@@ -699,12 +699,12 @@ private function parseOk(string $packet): void
     {
         $offset = 1;
 
-        $this->metadata->affectedRows = DataType::decodeUnsigned($packet, $offset);
-        $this->metadata->insertId = DataType::decodeUnsigned($packet, $offset);
+        $this->metadata->affectedRows = MysqlDataType::decodeUnsigned($packet, $offset);
+        $this->metadata->insertId = MysqlDataType::decodeUnsigned($packet, $offset);
 
         if ($this->capabilities & (self::CLIENT_PROTOCOL_41 | self::CLIENT_TRANSACTIONS)) {
-            $this->metadata->statusFlags = DataType::decodeUnsigned16($packet, $offset);
-            $this->metadata->warnings = DataType::decodeUnsigned16($packet, $offset);
+            $this->metadata->statusFlags = MysqlDataType::decodeUnsigned16($packet, $offset);
+            $this->metadata->warnings = MysqlDataType::decodeUnsigned16($packet, $offset);
         }
 
         if (!($this->capabilities & self::CLIENT_SESSION_TRACK)) {
@@ -719,22 +719,22 @@ private function parseOk(string $packet): void
             return;
         }
 
-        $this->metadata->statusInfo = DataType::decodeString($packet, $offset);
+        $this->metadata->statusInfo = MysqlDataType::decodeString($packet, $offset);
 
         if (!($this->metadata->statusFlags & self::SERVER_SESSION_STATE_CHANGED)) {
             return;
         }
 
-        $sessionState = DataType::decodeString($packet, $offset);
+        $sessionState = MysqlDataType::decodeString($packet, $offset);
 
         while (\strlen($packet) > $offset) {
-            $data = DataType::decodeString($sessionState, $offset);
-            $type = DataType::decodeUnsigned8($sessionState, $offset);
+            $data = MysqlDataType::decodeString($sessionState, $offset);
+            $type = MysqlDataType::decodeUnsigned8($sessionState, $offset);
 
             switch (SessionStateType::tryFrom($type)) {
                 case SessionStateType::SystemVariables:
-                    $var = DataType::decodeString($data, $offset);
-                    $this->metadata->sessionState[$type][$var] = DataType::decodeString($data, $offset);
+                    $var = MysqlDataType::decodeString($data, $offset);
+                    $this->metadata->sessionState[$type][$var] = MysqlDataType::decodeString($data, $offset);
                     break;
 
                 case SessionStateType::Schema:
@@ -742,7 +742,7 @@ private function parseOk(string $packet): void
                 case SessionStateType::Gtids:
                 case SessionStateType::TransactionCharacteristics:
                 case SessionStateType::TransactionState:
-                    $this->metadata->sessionState[$type] = DataType::decodeString($data, $offset);
+                    $this->metadata->sessionState[$type] = MysqlDataType::decodeString($data, $offset);
                     break;
 
                 default:
@@ -754,7 +754,7 @@ private function parseOk(string $packet): void
     private function handleOk(string $packet): void
     {
         $this->parseOk($packet);
-        $this->getDeferred()->complete(new CommandResult($this->metadata->affectedRows, $this->metadata->insertId));
+        $this->getDeferred()->complete(new MysqlCommandResult($this->metadata->affectedRows, $this->metadata->insertId));
         $this->ready();
     }
 
@@ -763,8 +763,8 @@ private function parseEof(string $packet): void
     {
         $offset = 1;
         if ($this->capabilities & self::CLIENT_PROTOCOL_41) {
-            $this->metadata->warnings = DataType::decodeUnsigned16($packet, $offset);
-            $this->metadata->statusFlags = DataType::decodeUnsigned16($packet, $offset);
+            $this->metadata->warnings = MysqlDataType::decodeUnsigned16($packet, $offset);
+            $this->metadata->statusFlags = MysqlDataType::decodeUnsigned16($packet, $offset);
         }
     }
 
@@ -786,24 +786,24 @@ private function handleHandshake(string $packet): void
             throw new ConnectionException("Unsupported protocol version ".\ord($packet)." (Expected: 10)");
         }
 
-        $this->metadata->serverVersion = DataType::decodeNullTerminatedString($packet, $offset);
+        $this->metadata->serverVersion = MysqlDataType::decodeNullTerminatedString($packet, $offset);
 
-        $this->connectionId = DataType::decodeUnsigned32($packet, $offset);
+        $this->connectionId = MysqlDataType::decodeUnsigned32($packet, $offset);
 
         $this->authPluginData = \substr($packet, $offset, 8);
         $offset += 8;
 
         $offset += 1; // filler byte
 
-        $this->serverCapabilities = DataType::decodeUnsigned16($packet, $offset);
+        $this->serverCapabilities = MysqlDataType::decodeUnsigned16($packet, $offset);
 
         if (\strlen($packet) > $offset) {
-            $this->metadata->charset = DataType::decodeUnsigned8($packet, $offset);
-            $this->metadata->statusFlags = DataType::decodeUnsigned16($packet, $offset);
-            $this->serverCapabilities += DataType::decodeUnsigned16($packet, $offset) << 16;
+            $this->metadata->charset = MysqlDataType::decodeUnsigned8($packet, $offset);
+            $this->metadata->statusFlags = MysqlDataType::decodeUnsigned16($packet, $offset);
+            $this->serverCapabilities += MysqlDataType::decodeUnsigned16($packet, $offset) << 16;
 
             $authPluginDataLen = $this->serverCapabilities & self::CLIENT_PLUGIN_AUTH
-                ? DataType::decodeUnsigned8($packet, $offset)
+                ? MysqlDataType::decodeUnsigned8($packet, $offset)
                 : 0;
 
             if ($this->serverCapabilities & self::CLIENT_SECURE_CONNECTION) {
@@ -814,7 +814,7 @@ private function handleHandshake(string $packet): void
                 $offset += $strlen;
 
                 if ($this->serverCapabilities & self::CLIENT_PLUGIN_AUTH) {
-                    $this->authPluginName = DataType::decodeNullTerminatedString($packet, $offset);
+                    $this->authPluginName = MysqlDataType::decodeNullTerminatedString($packet, $offset);
                 }
             }
         }
@@ -826,7 +826,7 @@ private function handleHandshake(string $packet): void
     private function handleAuthSwitch(string $packet): void
     {
         $offset = 1;
-        $this->authPluginName = DataType::decodeNullTerminatedString($packet, $offset);
+        $this->authPluginName = MysqlDataType::decodeNullTerminatedString($packet, $offset);
         $this->authPluginData = \substr($packet, $offset);
         $this->sendAuthSwitchResponse();
     }
@@ -867,16 +867,16 @@ private function handleQuery(string $packet): void
                 $this->parseOk($packet);
 
                 if ($this->metadata->statusFlags & self::SERVER_MORE_RESULTS_EXISTS) {
-                    $result = new ResultProxy;
+                    $result = new MysqlResultProxy;
                     $result->affectedRows = $this->metadata->affectedRows;
                     $result->insertId = $this->metadata->insertId;
-                    $this->getDeferred()->complete(new ConnectionResult($result));
+                    $this->getDeferred()->complete(new MysqlConnectionResult($result));
                     $this->result = $result;
-                    $result->updateState(ResultProxy::COLUMNS_FETCHED);
+                    $result->updateState(MysqlResultProxy::COLUMNS_FETCHED);
                     $this->successfulResultFetch();
                 } else {
                     $this->parseCallback = null;
-                    $this->getDeferred()->complete(new CommandResult(
+                    $this->getDeferred()->complete(new MysqlCommandResult(
                         $this->metadata->affectedRows,
                         $this->metadata->insertId
                     ));
@@ -896,17 +896,17 @@ private function handleQuery(string $packet): void
         }
 
         $this->parseCallback = $this->handleTextColumnDefinition(...);
-        $this->getDeferred()->complete(new ConnectionResult($result = new ResultProxy));
+        $this->getDeferred()->complete(new MysqlConnectionResult($result = new MysqlResultProxy));
         /* we need to resolve before assigning vars, so that a onResolve() handler won't have a partial result available */
         $this->result = $result;
-        $result->setColumns(DataType::decodeUnsigned($packet));
+        $result->setColumns(MysqlDataType::decodeUnsigned($packet));
     }
 
     /** @see 14.7.1 Binary Protocol Resultset */
     private function handleExecute(string $packet): void
     {
         $this->parseCallback = $this->handleBinaryColumnDefinition(...);
-        $this->getDeferred()->complete(new ConnectionResult($result = new ResultProxy));
+        $this->getDeferred()->complete(new MysqlConnectionResult($result = new MysqlResultProxy));
         /* we need to resolve before assigning vars, so that a onResolve() handler won't have a partial result available */
         $this->result = $result;
         $result->setColumns(\ord($packet));
@@ -943,7 +943,7 @@ private function handleColumnDefinition(string $packet, \Closure $parseCallback)
         \assert($this->result !== null, 'Connection result was in invalid state');
 
         if (!$this->result->columnsToFetch--) {
-            $this->result->updateState(ResultProxy::COLUMNS_FETCHED);
+            $this->result->updateState(MysqlResultProxy::COLUMNS_FETCHED);
             if (\ord($packet) === self::ERR_PACKET) {
                 $this->parseCallback = null;
                 $this->handleError($packet);
@@ -989,7 +989,7 @@ private function prepareFields(string $packet): void
             $result = $this->result;
             $this->result = null;
             $this->ready();
-            $result->updateState(ResultProxy::COLUMNS_FETCHED);
+            $result->updateState(MysqlResultProxy::COLUMNS_FETCHED);
 
             return;
         }
@@ -998,56 +998,56 @@ private function prepareFields(string $packet): void
     }
 
     /** @see 14.6.4.1.1.2 Column Defintion */
-    private function parseColumnDefinition(string $packet): ColumnDefinition
+    private function parseColumnDefinition(string $packet): MysqlColumnDefinition
     {
         $offset = 0;
         $column = [];
 
         if ($this->capabilities & self::CLIENT_PROTOCOL_41) {
-            $column["catalog"] = DataType::decodeString($packet, $offset);
-            $column["schema"] = DataType::decodeString($packet, $offset);
-            $column["table"] = DataType::decodeString($packet, $offset);
-            $column["originalTable"] = DataType::decodeString($packet, $offset);
-            $column["name"] = DataType::decodeString($packet, $offset);
-            $column["originalName"] = DataType::decodeString($packet, $offset);
-            $fixLength = DataType::decodeUnsigned($packet, $offset);
-
-            $column["charset"] = DataType::decodeUnsigned16($packet, $offset);
-            $column["length"] = DataType::decodeUnsigned32($packet, $offset);
-            $column["type"] = DataType::from(DataType::decodeUnsigned8($packet, $offset));
-            $column["flags"] = DataType::decodeUnsigned16($packet, $offset);
-            $column["decimals"] = DataType::decodeUnsigned8($packet, $offset);
+            $column["catalog"] = MysqlDataType::decodeString($packet, $offset);
+            $column["schema"] = MysqlDataType::decodeString($packet, $offset);
+            $column["table"] = MysqlDataType::decodeString($packet, $offset);
+            $column["originalTable"] = MysqlDataType::decodeString($packet, $offset);
+            $column["name"] = MysqlDataType::decodeString($packet, $offset);
+            $column["originalName"] = MysqlDataType::decodeString($packet, $offset);
+            $fixLength = MysqlDataType::decodeUnsigned($packet, $offset);
+
+            $column["charset"] = MysqlDataType::decodeUnsigned16($packet, $offset);
+            $column["length"] = MysqlDataType::decodeUnsigned32($packet, $offset);
+            $column["type"] = MysqlDataType::from(MysqlDataType::decodeUnsigned8($packet, $offset));
+            $column["flags"] = MysqlDataType::decodeUnsigned16($packet, $offset);
+            $column["decimals"] = MysqlDataType::decodeUnsigned8($packet, $offset);
 
             $offset += $fixLength;
         } else {
-            $column["table"] = DataType::decodeString($packet, $offset);
-            $column["name"] = DataType::decodeString($packet, $offset);
+            $column["table"] = MysqlDataType::decodeString($packet, $offset);
+            $column["name"] = MysqlDataType::decodeString($packet, $offset);
 
-            $columnLength = DataType::decodeUnsigned($packet, $offset);
-            $column["length"] = DataType::decodeIntByLength($packet, $columnLength, $offset);
+            $columnLength = MysqlDataType::decodeUnsigned($packet, $offset);
+            $column["length"] = MysqlDataType::decodeIntByLength($packet, $columnLength, $offset);
 
-            $typeLength = DataType::decodeUnsigned($packet, $offset);
-            $column["type"] = DataType::from(DataType::decodeIntByLength($packet, $typeLength, $offset));
+            $typeLength = MysqlDataType::decodeUnsigned($packet, $offset);
+            $column["type"] = MysqlDataType::from(MysqlDataType::decodeIntByLength($packet, $typeLength, $offset));
 
             $flagLength = $this->capabilities & self::CLIENT_LONG_FLAG
-                ? DataType::decodeUnsigned($packet, $offset)
-                : DataType::decodeUnsigned8($packet, $offset);
+                ? MysqlDataType::decodeUnsigned($packet, $offset)
+                : MysqlDataType::decodeUnsigned8($packet, $offset);
 
             if ($flagLength > 2) {
-                $column["flags"] = DataType::decodeUnsigned16($packet, $offset);
+                $column["flags"] = MysqlDataType::decodeUnsigned16($packet, $offset);
             } else {
-                $column["flags"] = DataType::decodeUnsigned8($packet, $offset);
+                $column["flags"] = MysqlDataType::decodeUnsigned8($packet, $offset);
             }
 
-            $column["decimals"] = DataType::decodeUnsigned8($packet, $offset);
+            $column["decimals"] = MysqlDataType::decodeUnsigned8($packet, $offset);
         }
 
         if ($offset < \strlen($packet)) {
-            $column["defaults"] = DataType::decodeString($packet, $offset);
+            $column["defaults"] = MysqlDataType::decodeString($packet, $offset);
         }
 
         /** @psalm-suppress InvalidScalarArgument */
-        return new ColumnDefinition(...$column);
+        return new MysqlColumnDefinition(...$column);
     }
 
     private function successfulResultFetch(): void
@@ -1071,7 +1071,7 @@ private function successfulResultFetch(): void
             $this->ready();
         }
 
-        $result->updateState(ResultProxy::ROWS_FETCHED);
+        $result->updateState(MysqlResultProxy::ROWS_FETCHED);
     }
 
     /** @see 14.6.4.1.1.3 Resultset Row */
@@ -1161,20 +1161,20 @@ private function handlePrepare(string $packet): void
 
         $offset = 1;
 
-        $stmtId = DataType::decodeUnsigned32($packet, $offset);
-        $columns = DataType::decodeUnsigned16($packet, $offset);
-        $params = DataType::decodeUnsigned16($packet, $offset);
+        $stmtId = MysqlDataType::decodeUnsigned32($packet, $offset);
+        $columns = MysqlDataType::decodeUnsigned16($packet, $offset);
+        $params = MysqlDataType::decodeUnsigned16($packet, $offset);
 
         $offset += 1; // filler
 
-        $this->metadata->warnings = DataType::decodeUnsigned16($packet, $offset);
+        $this->metadata->warnings = MysqlDataType::decodeUnsigned16($packet, $offset);
 
-        $this->result = new ResultProxy;
+        $this->result = new MysqlResultProxy;
         $this->result->columnsToFetch = $params;
         $this->result->columnCount = $columns;
         $this->refcount++;
         \assert($this->query !== null, 'Invalid value for connection query');
-        $this->getDeferred()->complete(new ConnectionStatement($this, $this->query, $stmtId, $this->named, $this->result));
+        $this->getDeferred()->complete(new MysqlConnectionStatement($this, $this->query, $stmtId, $this->named, $this->result));
         $this->named = [];
         if ($params) {
             $this->parseCallback = $this->prepareParams(...);
@@ -1292,9 +1292,9 @@ private function parseCompression(): \Generator
                 $inflated = "";
             }
 
-            $size = DataType::decodeUnsigned24($buffer);
+            $size = MysqlDataType::decodeUnsigned24($buffer);
             $this->compressionId = \ord($buffer[3]);
-            $uncompressed = DataType::decodeUnsigned24(\substr($buffer, 4, 3));
+            $uncompressed = MysqlDataType::decodeUnsigned24(\substr($buffer, 4, 3));
 
             $buffer = \substr($buffer, 7);
 
@@ -1333,7 +1333,7 @@ private function parseMysql(): \Generator
                     $parsed = [];
                 }
 
-                $length = DataType::decodeUnsigned24($buffer);
+                $length = MysqlDataType::decodeUnsigned24($buffer);
                 $this->seqId = \ord($buffer[3]);
                 $buffer = \substr($buffer, 4);
 
@@ -1536,7 +1536,7 @@ private function sendHandshake(bool $inSSL = false): void
 
         $auth = $this->getAuthData();
         if ($this->capabilities & self::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) {
-            $payload .= DataType::encodeInt(\strlen($auth));
+            $payload .= MysqlDataType::encodeInt(\strlen($auth));
             $payload .= $auth;
         } elseif ($this->capabilities & self::CLIENT_SECURE_CONNECTION) {
             $payload .= \chr(\strlen($auth));
diff --git a/src/Internal/CommandResult.php b/src/Internal/MysqlCommandResult.php
similarity index 89%
rename from src/Internal/CommandResult.php
rename to src/Internal/MysqlCommandResult.php
index 5b4f834..d5a7746 100644
--- a/src/Internal/CommandResult.php
+++ b/src/Internal/MysqlCommandResult.php
@@ -3,11 +3,11 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\Future;
-use Amp\Mysql\Result;
+use Amp\Mysql\MysqlResult;
 use Amp\Sql\Common\CommandResult as SqlCommandResult;
 
 /** @internal */
-final class CommandResult implements Result, \IteratorAggregate
+final class MysqlCommandResult implements MysqlResult, \IteratorAggregate
 {
     private ?int $lastInsertId;
 
@@ -45,7 +45,7 @@ public function getLastInsertId(): ?int
     /**
      * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
-    public function getNextResult(): ?Result
+    public function getNextResult(): ?MysqlResult
     {
         return $this->delegate->getNextResult();
     }
diff --git a/src/Internal/ConnectionMetadata.php b/src/Internal/MysqlConnectionMetdata.php
similarity index 93%
rename from src/Internal/ConnectionMetadata.php
rename to src/Internal/MysqlConnectionMetdata.php
index 13c6125..bef8042 100644
--- a/src/Internal/ConnectionMetadata.php
+++ b/src/Internal/MysqlConnectionMetdata.php
@@ -3,7 +3,7 @@
 namespace Amp\Mysql\Internal;
 
 /** @internal */
-final class ConnectionMetadata
+final class MysqlConnectionMetdata
 {
     public int $affectedRows = 0;
     public int $insertId = 0;
diff --git a/src/Internal/ConnectionResult.php b/src/Internal/MysqlConnectionResult.php
similarity index 79%
rename from src/Internal/ConnectionResult.php
rename to src/Internal/MysqlConnectionResult.php
index 82a555b..62d60de 100644
--- a/src/Internal/ConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -4,22 +4,22 @@
 
 use Amp\DeferredFuture;
 use Amp\Future;
-use Amp\Mysql\Result;
+use Amp\Mysql\MysqlResult;
 use Amp\Pipeline\ConcurrentIterableIterator;
 use Amp\Pipeline\ConcurrentIterator;
 use Revolt\EventLoop;
 use function Amp\async;
 
 /** @internal */
-final class ConnectionResult implements Result, \IteratorAggregate
+final class MysqlConnectionResult implements MysqlResult, \IteratorAggregate
 {
-    private readonly ResultProxy $result;
+    private readonly MysqlResultProxy $result;
 
     private readonly ConcurrentIterator $generator;
 
     private ?Future $nextResult = null;
 
-    public function __construct(ResultProxy $result)
+    public function __construct(MysqlResultProxy $result)
     {
         $this->result = $result;
         $this->generator = new ConcurrentIterableIterator(self::iterate($result));
@@ -30,7 +30,7 @@ public function getIterator(): \Traversable
         return $this->generator;
     }
 
-    private static function iterate(ResultProxy $result): \Generator
+    private static function iterate(MysqlResultProxy $result): \Generator
     {
         $next = self::fetchRow($result);
 
@@ -67,7 +67,7 @@ private static function iterate(ResultProxy $result): \Generator
     /**
      * @return Future<array|null>
      */
-    private static function fetchRow(ResultProxy $result): Future
+    private static function fetchRow(MysqlResultProxy $result): Future
     {
         if ($result->userFetched < $result->fetchedRows) {
             $row = $result->rows[$result->userFetched];
@@ -76,7 +76,7 @@ private static function fetchRow(ResultProxy $result): Future
             return Future::complete($row);
         }
 
-        if ($result->state === ResultProxy::ROWS_FETCHED) {
+        if ($result->state === MysqlResultProxy::ROWS_FETCHED) {
             return Future::complete();
         }
 
@@ -90,21 +90,21 @@ private static function fetchRow(ResultProxy $result): Future
             return $row;
         };
 
-        $result->deferreds[ResultProxy::UNFETCHED][] = [$deferred, null, $incRow];
+        $result->deferreds[MysqlResultProxy::UNFETCHED][] = [$deferred, null, $incRow];
         return $deferred->getFuture();
     }
 
-    public function getNextResult(): ?Result
+    public function getNextResult(): ?MysqlResult
     {
         if ($this->nextResult) {
             return $this->nextResult->await();
         }
 
-        $this->nextResult = async(function (): ?Result {
+        $this->nextResult = async(function (): ?MysqlResult {
             $deferred = $this->result->next ?: $this->result->next = new DeferredFuture;
             $result = $deferred->getFuture()->await();
 
-            if ($result instanceof ResultProxy) {
+            if ($result instanceof MysqlResultProxy) {
                 return new self($result);
             }
 
@@ -131,12 +131,12 @@ public function getLastInsertId(): ?int
 
     public function getColumnDefinitions(): ?array
     {
-        if ($this->result->state >= ResultProxy::COLUMNS_FETCHED) {
+        if ($this->result->state >= MysqlResultProxy::COLUMNS_FETCHED) {
             return $this->result->columns;
         }
 
         $deferred = new DeferredFuture;
-        $this->result->deferreds[ResultProxy::COLUMNS_FETCHED][] = [$deferred, &$this->result->columns, null];
+        $this->result->deferreds[MysqlResultProxy::COLUMNS_FETCHED][] = [$deferred, &$this->result->columns, null];
         return $deferred->getFuture()->await();
     }
 }
diff --git a/src/Internal/ConnectionStatement.php b/src/Internal/MysqlConnectionStatement.php
similarity index 84%
rename from src/Internal/ConnectionStatement.php
rename to src/Internal/MysqlConnectionStatement.php
index 3973913..5434069 100644
--- a/src/Internal/ConnectionStatement.php
+++ b/src/Internal/MysqlConnectionStatement.php
@@ -3,13 +3,13 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\DeferredFuture;
-use Amp\Mysql\Result;
-use Amp\Mysql\Statement;
+use Amp\Mysql\MysqlResult;
+use Amp\Mysql\MysqlStatement;
 use Amp\Sql\ConnectionException;
 use Revolt\EventLoop;
 
 /** @internal */
-final class ConnectionStatement implements Statement
+final class MysqlConnectionStatement implements MysqlStatement
 {
     private int $paramCount;
     private int $numParamCount;
@@ -19,15 +19,15 @@ final class ConnectionStatement implements Statement
     private int $stmtId;
     private array $prebound = [];
 
-    private ?Processor $processor;
+    private ?ConnectionProcessor $processor;
 
-    private readonly ResultProxy $result;
+    private readonly MysqlResultProxy $result;
 
     private int $lastUsedAt;
 
     private readonly DeferredFuture $onClose;
 
-    public function __construct(Processor $processor, string $query, int $stmtId, array $named, ResultProxy $result)
+    public function __construct(ConnectionProcessor $processor, string $query, int $stmtId, array $named, MysqlResultProxy $result)
     {
         $this->processor = $processor;
         $this->query = $query;
@@ -47,7 +47,7 @@ public function __construct(Processor $processor, string $query, int $stmtId, ar
         $this->lastUsedAt = \time();
     }
 
-    private function getProcessor(): Processor
+    private function getProcessor(): ConnectionProcessor
     {
         if ($this->processor === null) {
             throw new \Error("The statement has been closed");
@@ -115,7 +115,7 @@ public function bind(int|string $paramId, mixed $data): void
         }
     }
 
-    public function execute(array $params = []): Result
+    public function execute(array $params = []): MysqlResult
     {
         $this->lastUsedAt = \time();
 
@@ -159,16 +159,16 @@ public function reset(): void
 
     public function getColumnDefinitions(): ?array
     {
-        if ($this->result->state >= ResultProxy::COLUMNS_FETCHED) {
+        if ($this->result->state >= MysqlResultProxy::COLUMNS_FETCHED) {
             return $this->result->columns;
         }
 
-        if (isset($this->result->deferreds[ResultProxy::COLUMNS_FETCHED][0])) {
-            return $this->result->deferreds[ResultProxy::COLUMNS_FETCHED][0][0]->promise();
+        if (isset($this->result->deferreds[MysqlResultProxy::COLUMNS_FETCHED][0])) {
+            return $this->result->deferreds[MysqlResultProxy::COLUMNS_FETCHED][0][0]->promise();
         }
 
         $deferred = new DeferredFuture;
-        $this->result->deferreds[ResultProxy::COLUMNS_FETCHED][0] = [$deferred, &$this->result->columns, null];
+        $this->result->deferreds[MysqlResultProxy::COLUMNS_FETCHED][0] = [$deferred, &$this->result->columns, null];
         return $deferred->getFuture()->await();
     }
 
@@ -187,7 +187,7 @@ public function __destruct()
         }
     }
 
-    private static function shutdown(Processor $processor, int $stmtId, DeferredFuture $onClose): void
+    private static function shutdown(ConnectionProcessor $processor, int $stmtId, DeferredFuture $onClose): void
     {
         try {
             $processor->closeStmt($stmtId);
diff --git a/src/Internal/ConnectionTransaction.php b/src/Internal/MysqlConnectionTransaction.php
similarity index 90%
rename from src/Internal/ConnectionTransaction.php
rename to src/Internal/MysqlConnectionTransaction.php
index 688cffa..a348fd3 100644
--- a/src/Internal/ConnectionTransaction.php
+++ b/src/Internal/MysqlConnectionTransaction.php
@@ -3,20 +3,20 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\DeferredFuture;
-use Amp\Mysql\Result;
-use Amp\Mysql\Statement;
-use Amp\Mysql\Transaction;
+use Amp\Mysql\MysqlResult;
+use Amp\Mysql\MysqlStatement;
+use Amp\Mysql\MysqlTransaction;
 use Amp\Sql\SqlException;
 use Amp\Sql\TransactionError;
 use Amp\Sql\TransactionIsolation;
 use Revolt\EventLoop;
 
 /** @internal */
-final class ConnectionTransaction implements Transaction
+final class MysqlConnectionTransaction implements MysqlTransaction
 {
     const SAVEPOINT_PREFIX = "amp_";
 
-    private ?Processor $processor;
+    private ?ConnectionProcessor $processor;
 
     private readonly TransactionIsolation $isolation;
 
@@ -33,7 +33,7 @@ final class ConnectionTransaction implements Transaction
      * @throws \Error If the isolation level is invalid.
      */
     public function __construct(
-        Processor $processor,
+        ConnectionProcessor $processor,
         \Closure $release,
         TransactionIsolation $isolation
     ) {
@@ -114,7 +114,7 @@ public function getIsolationLevel(): TransactionIsolation
     /**
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
-    public function query(string $sql): Result
+    public function query(string $sql): MysqlResult
     {
         if ($this->processor === null) {
             throw new TransactionError("The transaction has been committed or rolled back");
@@ -122,7 +122,7 @@ public function query(string $sql): Result
 
         $result = $this->processor->query($sql)->await();
         ++$this->refCount;
-        return new PooledResult($result, $this->release);
+        return new MysqlPooledResult($result, $this->release);
     }
 
     /**
@@ -130,20 +130,20 @@ public function query(string $sql): Result
      *
      * @psalm-suppress InvalidReturnStatement, InvalidReturnType
      */
-    public function prepare(string $sql): Statement
+    public function prepare(string $sql): MysqlStatement
     {
         if ($this->processor === null) {
             throw new TransactionError("The transaction has been committed or rolled back");
         }
 
         $statement = $this->processor->prepare($sql)->await();
-        return new PooledStatement($statement, $this->release);
+        return new MysqlPooledStatement($statement, $this->release);
     }
 
     /**
      * @throws TransactionError If the transaction has been committed or rolled back.
      */
-    public function execute(string $sql, array $params = []): Result
+    public function execute(string $sql, array $params = []): MysqlResult
     {
         if ($this->processor === null) {
             throw new TransactionError("The transaction has been committed or rolled back");
@@ -153,7 +153,7 @@ public function execute(string $sql, array $params = []): Result
         $result = $statement->execute($params);
 
         ++$this->refCount;
-        return new PooledResult($result, $this->release);
+        return new MysqlPooledResult($result, $this->release);
     }
 
     /**
diff --git a/src/Internal/PooledResult.php b/src/Internal/MysqlPooledResult.php
similarity index 71%
rename from src/Internal/PooledResult.php
rename to src/Internal/MysqlPooledResult.php
index 610a15c..4b9bf91 100644
--- a/src/Internal/PooledResult.php
+++ b/src/Internal/MysqlPooledResult.php
@@ -2,19 +2,19 @@
 
 namespace Amp\Mysql\Internal;
 
-use Amp\Mysql\Result;
+use Amp\Mysql\MysqlResult;
 use Amp\Sql\Common\PooledResult as SqlPooledResult;
 use Amp\Sql\Result as SqlResult;
 
 /** @internal */
-final class PooledResult extends SqlPooledResult implements Result
+final class MysqlPooledResult extends SqlPooledResult implements MysqlResult
 {
-    private readonly Result $result;
+    private readonly MysqlResult $result;
 
     /**
      * @param \Closure():void $release
      */
-    public function __construct(Result $result, \Closure $release)
+    public function __construct(MysqlResult $result, \Closure $release)
     {
         parent::__construct($result, $release);
         $this->result = $result;
@@ -22,7 +22,7 @@ public function __construct(Result $result, \Closure $release)
 
     protected function newInstanceFrom(SqlResult $result, \Closure $release): self
     {
-        \assert($result instanceof Result);
+        \assert($result instanceof MysqlResult);
         return new self($result, $release);
     }
 
diff --git a/src/Internal/PooledStatement.php b/src/Internal/MysqlPooledStatement.php
similarity index 65%
rename from src/Internal/PooledStatement.php
rename to src/Internal/MysqlPooledStatement.php
index 08ff56d..931aec6 100644
--- a/src/Internal/PooledStatement.php
+++ b/src/Internal/MysqlPooledStatement.php
@@ -2,35 +2,35 @@
 
 namespace Amp\Mysql\Internal;
 
-use Amp\Mysql\Result;
-use Amp\Mysql\Statement;
+use Amp\Mysql\MysqlResult;
+use Amp\Mysql\MysqlStatement;
 use Amp\Sql\Common\PooledStatement as SqlPooledStatement;
 use Amp\Sql\Result as SqlResult;
 
 /** @internal */
-final class PooledStatement extends SqlPooledStatement implements Statement
+final class MysqlPooledStatement extends SqlPooledStatement implements MysqlStatement
 {
-    private readonly Statement $statement;
+    private readonly MysqlStatement $statement;
 
     /**
      * @param \Closure():void $release
      */
-    public function __construct(Statement $statement, \Closure $release)
+    public function __construct(MysqlStatement $statement, \Closure $release)
     {
         parent::__construct($statement, $release);
         $this->statement = $statement;
     }
 
-    protected function createResult(SqlResult $result, \Closure $release): Result
+    protected function createResult(SqlResult $result, \Closure $release): MysqlResult
     {
-        \assert($result instanceof Result);
-        return new PooledResult($result, $release);
+        \assert($result instanceof MysqlResult);
+        return new MysqlPooledResult($result, $release);
     }
 
     /**
      * Changes return type to this library's Result type.
      */
-    public function execute(array $params = []): Result
+    public function execute(array $params = []): MysqlResult
     {
         return $this->statement->execute($params);
     }
diff --git a/src/Internal/PooledTransaction.php b/src/Internal/MysqlPooledTransaction.php
similarity index 60%
rename from src/Internal/PooledTransaction.php
rename to src/Internal/MysqlPooledTransaction.php
index 2b755d6..a0c7148 100644
--- a/src/Internal/PooledTransaction.php
+++ b/src/Internal/MysqlPooledTransaction.php
@@ -2,26 +2,26 @@
 
 namespace Amp\Mysql\Internal;
 
-use Amp\Mysql\Result;
-use Amp\Mysql\Statement;
-use Amp\Mysql\Transaction;
+use Amp\Mysql\MysqlResult;
+use Amp\Mysql\MysqlStatement;
+use Amp\Mysql\MysqlTransaction;
 use Amp\Sql\Common\PooledTransaction as SqlPooledTransaction;
 use Amp\Sql\Result as SqlResult;
 use Amp\Sql\Statement as SqlStatement;
 
 /** @internal */
-final class PooledTransaction extends SqlPooledTransaction implements Transaction
+final class MysqlPooledTransaction extends SqlPooledTransaction implements MysqlTransaction
 {
-    protected function createStatement(SqlStatement $statement, \Closure $release): Statement
+    protected function createStatement(SqlStatement $statement, \Closure $release): MysqlStatement
     {
-        \assert($statement instanceof Statement);
-        return new PooledStatement($statement, $release);
+        \assert($statement instanceof MysqlStatement);
+        return new MysqlPooledStatement($statement, $release);
     }
 
-    protected function createResult(SqlResult $result, \Closure $release): Result
+    protected function createResult(SqlResult $result, \Closure $release): MysqlResult
     {
-        \assert($result instanceof Result);
-        return new PooledResult($result, $release);
+        \assert($result instanceof MysqlResult);
+        return new MysqlPooledResult($result, $release);
     }
 
     /**
@@ -29,7 +29,7 @@ protected function createResult(SqlResult $result, \Closure $release): Result
      *
      * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
-    public function query(string $sql): Result
+    public function query(string $sql): MysqlResult
     {
         return parent::query($sql);
     }
@@ -39,7 +39,7 @@ public function query(string $sql): Result
      *
      * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
-    public function prepare(string $sql): Statement
+    public function prepare(string $sql): MysqlStatement
     {
         return parent::prepare($sql);
     }
@@ -49,7 +49,7 @@ public function prepare(string $sql): Statement
      *
      * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
-    public function execute(string $sql, array $params = []): Result
+    public function execute(string $sql, array $params = []): MysqlResult
     {
         return parent::execute($sql, $params);
     }
diff --git a/src/Internal/ResultProxy.php b/src/Internal/MysqlResultProxy.php
similarity index 95%
rename from src/Internal/ResultProxy.php
rename to src/Internal/MysqlResultProxy.php
index 022b184..91d2431 100644
--- a/src/Internal/ResultProxy.php
+++ b/src/Internal/MysqlResultProxy.php
@@ -3,13 +3,13 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\DeferredFuture;
-use Amp\Mysql\ColumnDefinition;
+use Amp\Mysql\MysqlColumnDefinition;
 
 /** @internal */
-final class ResultProxy
+final class MysqlResultProxy
 {
     public int $columnCount = 0;
-    /** @var list<ColumnDefinition> */
+    /** @var list<MysqlColumnDefinition> */
     public array $columns = [];
     public array $params = [];
     public int $columnsToFetch = 0;
diff --git a/src/Internal/StatementPool.php b/src/Internal/MysqlStatementPool.php
similarity index 74%
rename from src/Internal/StatementPool.php
rename to src/Internal/MysqlStatementPool.php
index f8e9799..d98c75e 100644
--- a/src/Internal/StatementPool.php
+++ b/src/Internal/MysqlStatementPool.php
@@ -2,25 +2,25 @@
 
 namespace Amp\Mysql\Internal;
 
-use Amp\Mysql\Result;
-use Amp\Mysql\Statement;
+use Amp\Mysql\MysqlResult;
+use Amp\Mysql\MysqlStatement;
 use Amp\Sql\Common\StatementPool as SqlStatementPool;
 use Amp\Sql\Result as SqlResult;
 
 /** @internal */
-final class StatementPool extends SqlStatementPool implements Statement
+final class MysqlStatementPool extends SqlStatementPool implements MysqlStatement
 {
     private array $params = [];
 
     /**
      * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
-    protected function pop(): Statement
+    protected function pop(): MysqlStatement
     {
         $statement = parent::pop();
 
         try {
-            \assert($statement instanceof Statement);
+            \assert($statement instanceof MysqlStatement);
 
             $statement->reset();
 
@@ -35,19 +35,19 @@ protected function pop(): Statement
         return $statement;
     }
 
-    protected function createResult(SqlResult $result, \Closure $release): Result
+    protected function createResult(SqlResult $result, \Closure $release): MysqlResult
     {
-        if (!$result instanceof Result) {
-            throw new \TypeError('Result object must be an instance of ' . Result::class);
+        if (!$result instanceof MysqlResult) {
+            throw new \TypeError('Result object must be an instance of ' . MysqlResult::class);
         }
 
-        return new PooledResult($result, $release);
+        return new MysqlPooledResult($result, $release);
     }
 
     /**
      * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
-    public function execute(array $params = []): Result
+    public function execute(array $params = []): MysqlResult
     {
         return parent::execute($params);
     }
diff --git a/src/ColumnDefinition.php b/src/MysqlColumnDefinition.php
similarity index 87%
rename from src/ColumnDefinition.php
rename to src/MysqlColumnDefinition.php
index ef6c9b6..5f3bec7 100644
--- a/src/ColumnDefinition.php
+++ b/src/MysqlColumnDefinition.php
@@ -2,13 +2,13 @@
 
 namespace Amp\Mysql;
 
-final class ColumnDefinition
+final class MysqlColumnDefinition
 {
     public function __construct(
         public readonly string $table,
         public readonly string $name,
         public readonly int $length,
-        public readonly DataType $type,
+        public readonly MysqlDataType $type,
         public readonly int $flags,
         public readonly int $decimals,
         public readonly string $defaults = '',
diff --git a/src/Connection.php b/src/MysqlConnection.php
similarity index 85%
rename from src/Connection.php
rename to src/MysqlConnection.php
index 056a1b0..507f203 100644
--- a/src/Connection.php
+++ b/src/MysqlConnection.php
@@ -9,9 +9,9 @@
 use Amp\Sql\TransactionIsolationLevel;
 use Revolt\EventLoop;
 
-final class Connection implements Link
+final class MysqlConnection implements MysqlLink
 {
-    private readonly Internal\Processor $processor;
+    private readonly Internal\ConnectionProcessor $processor;
 
     private ?DeferredFuture $busy = null;
 
@@ -23,12 +23,12 @@ public static function initialize(
         MysqlConfig $config,
         ?Cancellation $cancellation = null,
     ): self {
-        $processor = new Internal\Processor($socket, $config);
+        $processor = new Internal\ConnectionProcessor($socket, $config);
         $processor->connect($cancellation);
         return new self($processor);
     }
 
-    private function __construct(Internal\Processor $processor)
+    private function __construct(Internal\ConnectionProcessor $processor)
     {
         $this->processor = $processor;
 
@@ -84,7 +84,7 @@ public function useDb(string $db): void
         $this->processor->useDb($db)->await();
     }
 
-    public function query(string $sql): Result
+    public function query(string $sql): MysqlResult
     {
         while ($this->busy) {
             $this->busy->getFuture()->await();
@@ -95,7 +95,7 @@ public function query(string $sql): Result
 
     public function beginTransaction(
         TransactionIsolation $isolation = TransactionIsolationLevel::Committed
-    ): Transaction {
+    ): MysqlTransaction {
         while ($this->busy) {
             $this->busy->getFuture()->await();
         }
@@ -111,7 +111,7 @@ public function beginTransaction(
             throw $exception;
         }
 
-        return new Internal\ConnectionTransaction($this->processor, $this->release, $isolation);
+        return new Internal\MysqlConnectionTransaction($this->processor, $this->release, $isolation);
     }
 
     public function ping(): void
@@ -119,7 +119,7 @@ public function ping(): void
         $this->processor->ping()->await();
     }
 
-    public function prepare(string $sql): Statement
+    public function prepare(string $sql): MysqlStatement
     {
         while ($this->busy) {
             $this->busy->getFuture()->await();
@@ -128,7 +128,7 @@ public function prepare(string $sql): Statement
         return $this->processor->prepare($sql)->await();
     }
 
-    public function execute(string $sql, array $params = []): Result
+    public function execute(string $sql, array $params = []): MysqlResult
     {
         $statement = $this->prepare($sql);
         return $statement->execute($params);
diff --git a/src/MysqlConnector.php b/src/MysqlConnector.php
index 7249383..5b2af95 100644
--- a/src/MysqlConnector.php
+++ b/src/MysqlConnector.php
@@ -12,5 +12,5 @@ interface MysqlConnector extends SqlConnector
     /**
      * @throws SqlException
      */
-    public function connect(SqlConfig $config, ?Cancellation $cancellation = null): Connection;
+    public function connect(SqlConfig $config, ?Cancellation $cancellation = null): MysqlConnection;
 }
diff --git a/src/DataType.php b/src/MysqlDataType.php
similarity index 99%
rename from src/DataType.php
rename to src/MysqlDataType.php
index f62b925..55ef7c3 100644
--- a/src/DataType.php
+++ b/src/MysqlDataType.php
@@ -5,7 +5,7 @@
 use Amp\Sql\SqlException;
 
 /** @see 14.6.4.1.1.1 Column Type */
-enum DataType: int
+enum MysqlDataType: int
 {
     case Decimal = 0x00;
     case Tiny = 0x01;
@@ -42,7 +42,7 @@ enum DataType: int
     private const ENCODED_JSON_PREFIX = "base64:type251:";
 
     /**
-     * @return array{DataType, string}
+     * @return array{self, string}
      *
      * @see 14.7.3 Binary Value
      */
diff --git a/src/MysqlExecutor.php b/src/MysqlExecutor.php
new file mode 100644
index 0000000..302f524
--- /dev/null
+++ b/src/MysqlExecutor.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Amp\Mysql;
+
+use Amp\Sql\Executor;
+
+interface MysqlExecutor extends Executor
+{
+    /**
+     * @return MysqlResult Result object specific to this library.
+     */
+    public function query(string $sql): MysqlResult;
+
+    /**
+     * @return MysqlStatement Statement object specific to this library.
+     */
+    public function prepare(string $sql): MysqlStatement;
+
+    /**
+     * @return MysqlResult Result object specific to this library.
+     */
+    public function execute(string $sql, array $params = []): MysqlResult;
+}
diff --git a/src/Link.php b/src/MysqlLink.php
similarity index 58%
rename from src/Link.php
rename to src/MysqlLink.php
index 33b387e..554097f 100644
--- a/src/Link.php
+++ b/src/MysqlLink.php
@@ -2,16 +2,16 @@
 
 namespace Amp\Mysql;
 
-use Amp\Sql\Link as SqlLink;
+use Amp\Sql\Link;
 use Amp\Sql\TransactionIsolation;
 use Amp\Sql\TransactionIsolationLevel;
 
-interface Link extends Executor, SqlLink
+interface MysqlLink extends MysqlExecutor, Link
 {
     /**
-     * @return Transaction Transaction object specific to this library.
+     * @return MysqlTransaction Transaction object specific to this library.
      */
     public function beginTransaction(
         TransactionIsolation $isolation = TransactionIsolationLevel::Committed
-    ): Transaction;
+    ): MysqlTransaction;
 }
diff --git a/src/Pool.php b/src/MysqlPool.php
similarity index 69%
rename from src/Pool.php
rename to src/MysqlPool.php
index e1ca952..971ff93 100644
--- a/src/Pool.php
+++ b/src/MysqlPool.php
@@ -11,7 +11,7 @@
 use Amp\Sql\TransactionIsolation;
 use Amp\Sql\TransactionIsolationLevel;
 
-final class Pool extends ConnectionPool implements Link
+final class MysqlPool extends ConnectionPool implements MysqlLink
 {
     /**
      * @param positive-int $maxConnections
@@ -23,29 +23,29 @@ public function __construct(
         int $idleTimeout = self::DEFAULT_IDLE_TIMEOUT,
         ?MysqlConnector $connector = null,
     ) {
-        parent::__construct($config, $connector ?? connector(), $maxConnections, $idleTimeout);
+        parent::__construct($config, $connector ?? mysqlConnector(), $maxConnections, $idleTimeout);
     }
 
-    protected function createResult(SqlResult $result, \Closure $release): Result
+    protected function createResult(SqlResult $result, \Closure $release): MysqlResult
     {
-        \assert($result instanceof Result);
-        return new Internal\PooledResult($result, $release);
+        \assert($result instanceof MysqlResult);
+        return new Internal\MysqlPooledResult($result, $release);
     }
 
-    protected function createStatement(SqlStatement $statement, \Closure $release): Statement
+    protected function createStatement(SqlStatement $statement, \Closure $release): MysqlStatement
     {
-        \assert($statement instanceof Statement);
-        return new Internal\PooledStatement($statement, $release);
+        \assert($statement instanceof MysqlStatement);
+        return new Internal\MysqlPooledStatement($statement, $release);
     }
 
     protected function createStatementPool(SqlPool $pool, string $sql, \Closure $prepare): StatementPool
     {
-        return new Internal\StatementPool($pool, $sql, $prepare);
+        return new Internal\MysqlStatementPool($pool, $sql, $prepare);
     }
 
-    protected function createTransaction(SqlTransaction $transaction, \Closure $release): Transaction
+    protected function createTransaction(SqlTransaction $transaction, \Closure $release): MysqlTransaction
     {
-        return new Internal\PooledTransaction($transaction, $release);
+        return new Internal\MysqlPooledTransaction($transaction, $release);
     }
 
     /**
@@ -53,7 +53,7 @@ protected function createTransaction(SqlTransaction $transaction, \Closure $rele
      *
      * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
-    public function query(string $sql): Result
+    public function query(string $sql): MysqlResult
     {
         return parent::query($sql);
     }
@@ -63,7 +63,7 @@ public function query(string $sql): Result
      *
      * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
-    public function prepare(string $sql): Statement
+    public function prepare(string $sql): MysqlStatement
     {
         return parent::prepare($sql);
     }
@@ -73,7 +73,7 @@ public function prepare(string $sql): Statement
      *
      * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
-    public function execute(string $sql, array $params = []): Result
+    public function execute(string $sql, array $params = []): MysqlResult
     {
         return parent::execute($sql, $params);
     }
@@ -85,7 +85,7 @@ public function execute(string $sql, array $params = []): Result
      */
     public function beginTransaction(
         TransactionIsolation $isolation = TransactionIsolationLevel::Committed
-    ): Transaction {
+    ): MysqlTransaction {
         return parent::beginTransaction($isolation);
     }
 }
diff --git a/src/Result.php b/src/MysqlResult.php
similarity index 73%
rename from src/Result.php
rename to src/MysqlResult.php
index 55e78ad..1e0d84a 100644
--- a/src/Result.php
+++ b/src/MysqlResult.php
@@ -2,9 +2,9 @@
 
 namespace Amp\Mysql;
 
-use Amp\Sql\Result as SqlResult;
+use Amp\Sql\Result;
 
-interface Result extends SqlResult
+interface MysqlResult extends Result
 {
     /**
      * @return int|null Insert ID of the last auto increment row if applicable to the result or null if no ID
@@ -13,7 +13,7 @@ interface Result extends SqlResult
     public function getLastInsertId(): ?int;
 
     /**
-     * @return list<ColumnDefinition>|null
+     * @return list<MysqlColumnDefinition>|null
      */
     public function getColumnDefinitions(): ?array;
 }
diff --git a/src/Statement.php b/src/MysqlStatement.php
similarity index 73%
rename from src/Statement.php
rename to src/MysqlStatement.php
index b4a8f40..cea98f9 100644
--- a/src/Statement.php
+++ b/src/MysqlStatement.php
@@ -2,11 +2,11 @@
 
 namespace Amp\Mysql;
 
-use Amp\Sql\Statement as SqlStatement;
+use Amp\Sql\Statement;
 
-interface Statement extends SqlStatement
+interface MysqlStatement extends Statement
 {
-    public function execute(array $params = []): Result;
+    public function execute(array $params = []): MysqlResult;
 
     /**
      * @param int|string $paramId Parameter ID or name.
@@ -17,7 +17,7 @@ public function execute(array $params = []): Result;
     public function bind(int|string $paramId, mixed $data): void;
 
     /**
-     * @return list<ColumnDefinition>|null
+     * @return list<MysqlColumnDefinition>|null
      */
     public function getColumnDefinitions(): ?array;
 
diff --git a/src/MysqlTransaction.php b/src/MysqlTransaction.php
new file mode 100644
index 0000000..e756c75
--- /dev/null
+++ b/src/MysqlTransaction.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Amp\Mysql;
+
+use Amp\Sql\Transaction;
+
+interface MysqlTransaction extends MysqlExecutor, Transaction
+{
+}
diff --git a/src/Transaction.php b/src/Transaction.php
deleted file mode 100644
index ad935eb..0000000
--- a/src/Transaction.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-use Amp\Sql\Transaction as SqlTransaction;
-
-interface Transaction extends Executor, SqlTransaction
-{
-}
diff --git a/src/functions.php b/src/functions.php
index 9cc8959..76ded39 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -6,7 +6,7 @@
 use Amp\Sql\SqlException;
 use Revolt\EventLoop;
 
-function connector(?MysqlConnector $connector = null): MysqlConnector
+function mysqlConnector(?MysqlConnector $connector = null): MysqlConnector
 {
     static $map;
     $map ??= new \WeakMap();
@@ -25,9 +25,9 @@ function connector(?MysqlConnector $connector = null): MysqlConnector
  * @throws SqlException If connecting fails.
  * @throws \Error If the connection string does not contain a host, user, and password.
  */
-function connect(MysqlConfig $config): Connection
+function connect(MysqlConfig $config): MysqlConnection
 {
-    return connector()->connect($config);
+    return mysqlConnector()->connect($config);
 }
 
 /**
@@ -42,6 +42,6 @@ function pool(
     MysqlConfig $config,
     int $maxConnections = ConnectionPool::DEFAULT_MAX_CONNECTIONS,
     int $idleTimeout = ConnectionPool::DEFAULT_IDLE_TIMEOUT,
-): Pool {
-    return new Pool($config, $maxConnections, $idleTimeout, connector());
+): MysqlPool {
+    return new MysqlPool($config, $maxConnections, $idleTimeout, mysqlConnector());
 }
diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php
index 17812e0..f17f100 100644
--- a/test/ConnectionTest.php
+++ b/test/ConnectionTest.php
@@ -2,14 +2,14 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Mysql\Connection;
 use Amp\Mysql\DefaultMysqlConnector;
-use Amp\Mysql\Link;
 use Amp\Mysql\MysqlConfig;
+use Amp\Mysql\MysqlConnection;
+use Amp\Mysql\MysqlLink;
 
 class ConnectionTest extends LinkTest
 {
-    protected function getLink(string $connectionString): Link
+    protected function getLink(string $connectionString): MysqlLink
     {
         return (new DefaultMysqlConnector)->connect(MysqlConfig::fromString($connectionString));
     }
@@ -20,7 +20,7 @@ public function testConnect()
 
         $db = $connector->connect(MysqlConfig::fromString("host=".DB_HOST." user=".DB_USER." pass=".DB_PASS." db=test"));
 
-        $this->assertInstanceOf(Connection::class, $db);
+        $this->assertInstanceOf(MysqlConnection::class, $db);
 
         /* use an alternative charset... Default is utf8mb4_general_ci */
         $db->setCharset("latin1_general_ci");
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 2f5e82c..af3f0aa 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -2,10 +2,10 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Mysql\ColumnDefinition;
-use Amp\Mysql\DataType;
-use Amp\Mysql\Link;
-use Amp\Mysql\Result;
+use Amp\Mysql\MysqlColumnDefinition;
+use Amp\Mysql\MysqlDataType;
+use Amp\Mysql\MysqlLink;
+use Amp\Mysql\MysqlResult;
 use Amp\PHPUnit\AsyncTestCase;
 use Amp\Sql\QueryError;
 
@@ -14,14 +14,14 @@ abstract class LinkTest extends AsyncTestCase
     /**
      * Returns the Link class to be tested.
      */
-    abstract protected function getLink(string $connectionString): Link;
+    abstract protected function getLink(string $connectionString): MysqlLink;
 
     public function testQuery()
     {
         $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
         $resultset = $db->execute("SELECT ? AS a", [M_PI]);
-        $this->assertInstanceOf(Result::class, $resultset);
+        $this->assertInstanceOf(MysqlResult::class, $resultset);
 
         $i = 0;
         foreach ($resultset as $row) {
@@ -37,7 +37,7 @@ public function testQueryFetchRow()
         $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
         $resultset = $db->query('SELECT a FROM main WHERE a < 4');
-        $this->assertInstanceOf(Result::class, $resultset);
+        $this->assertInstanceOf(MysqlResult::class, $resultset);
 
         $this->assertSame(1, $resultset->getColumnCount());
 
@@ -64,21 +64,21 @@ public function testMultiStmt()
         $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
 
         $resultset = $db->query("SELECT a FROM main; SELECT b FROM main WHERE a = 5; SELECT b AS d, a + 1 AS c FROM main WHERE b > 4");
-        $this->assertInstanceOf(Result::class, $resultset);
+        $this->assertInstanceOf(MysqlResult::class, $resultset);
 
         $got = [];
         foreach ($resultset as $row) {
             $got[] = \array_values($row);
         }
         $this->assertSame([[1], [2], [3], [4], [5]], $got);
-        $this->assertInstanceOf(Result::class, $resultset = $resultset->getNextResult());
+        $this->assertInstanceOf(MysqlResult::class, $resultset = $resultset->getNextResult());
 
         $got = [];
         foreach ($resultset as $row) {
             $got[] = \array_values($row);
         }
         $this->assertSame([[6]], $got);
-        $this->assertInstanceOf(Result::class, $resultset = $resultset->getNextResult());
+        $this->assertInstanceOf(MysqlResult::class, $resultset = $resultset->getNextResult());
 
         $fields = $resultset->getColumnDefinitions();
 
@@ -91,9 +91,9 @@ public function testMultiStmt()
         $this->assertCount(2, $fields);
         $this->assertSame($fields[0]->originalName, "b");
         $this->assertSame($fields[0]->name, "d");
-        $this->assertSame($fields[0]->type, DataType::Long);
+        $this->assertSame($fields[0]->type, MysqlDataType::Long);
         $this->assertSame($fields[1]->name, "c");
-        $this->assertSame($fields[1]->type, DataType::LongLong);
+        $this->assertSame($fields[1]->type, MysqlDataType::LongLong);
 
         $this->assertNull($resultset->getNextResult());
     }
@@ -110,20 +110,20 @@ public function testPrepared()
             "originalTable" => "main",
             "charset" => 63,
             "length" => 11,
-            "type" => DataType::from(3),
+            "type" => MysqlDataType::from(3),
             "flags" => 0,
             "decimals" => 0,
         ];
 
         $this->assertEquals($stmt->getColumnDefinitions(), [
-            new ColumnDefinition(...\array_merge($base, ["name" => "no", "originalName" => "id", "flags" => 16899])),
-            new ColumnDefinition(...\array_merge($base, ["name" => "a", "originalName" => "a"])),
-            new ColumnDefinition(...\array_merge($base + ["name" => "b", "originalName" => "b"])),
+            new MysqlColumnDefinition(...\array_merge($base, ["name" => "no", "originalName" => "id", "flags" => 16899])),
+            new MysqlColumnDefinition(...\array_merge($base, ["name" => "a", "originalName" => "a"])),
+            new MysqlColumnDefinition(...\array_merge($base + ["name" => "b", "originalName" => "b"])),
         ]);
 
         $stmt->bind("num", 5);
         $result = $stmt->execute([2]);
-        $this->assertInstanceOf(Result::class, $result);
+        $this->assertInstanceOf(MysqlResult::class, $result);
         $this->assertSame(3, $result->getColumnCount());
         $got = [];
         foreach ($result as $row) {
@@ -133,7 +133,7 @@ public function testPrepared()
 
         $stmt = $db->prepare("SELECT * FROM main WHERE a = ? OR b = ?");
         $result = $stmt->execute([1, 8]);
-        $this->assertInstanceOf(Result::class, $result);
+        $this->assertInstanceOf(MysqlResult::class, $result);
         $this->assertSame(3, $result->getColumnCount());
         $got = [];
         foreach ($result as $row) {
@@ -143,7 +143,7 @@ public function testPrepared()
 
         $stmt = $db->prepare("SELECT * FROM main WHERE a = :a OR b = ?");
         $result = $stmt->execute(["a" => 2, 5]);
-        $this->assertInstanceOf(Result::class, $result);
+        $this->assertInstanceOf(MysqlResult::class, $result);
         $this->assertSame(3, $result->getColumnCount());
         $got = [];
         foreach ($result as $row) {
@@ -154,13 +154,13 @@ public function testPrepared()
         $stmt = $db->prepare("INSERT INTO main (a, b) VALUES (:a, :b)");
         $result = $stmt->execute(["a" => 10, "b" => 11]);
         $this->assertNull($result->getColumnCount());
-        $this->assertInstanceOf(Result::class, $result);
+        $this->assertInstanceOf(MysqlResult::class, $result);
         $this->assertGreaterThan(5, $result->getLastInsertId());
 
         $stmt = $db->prepare("DELETE FROM main WHERE a = :a");
         $result = $stmt->execute(["a" => 10]);
         $this->assertNull($result->getColumnCount());
-        $this->assertInstanceOf(Result::class, $result);
+        $this->assertInstanceOf(MysqlResult::class, $result);
     }
 
     public function testPrepareWithInvalidQuery()
@@ -219,7 +219,7 @@ public function testExecute()
         $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
 
         $result = $db->execute("SELECT * FROM test.main WHERE a = ? OR b = ?", [2, 5]);
-        $this->assertInstanceOf(Result::class, $result);
+        $this->assertInstanceOf(MysqlResult::class, $result);
         $got = [];
         foreach ($result as $row) {
             $got[] = \array_values($row);
@@ -228,11 +228,11 @@ public function testExecute()
         $this->assertSame([[2, 2, 3], [4, 4, 5]], $got);
 
         $result = $db->execute("INSERT INTO main (a, b) VALUES (:a, :b)", ["a" => 10, "b" => 11]);
-        $this->assertInstanceOf(Result::class, $result);
+        $this->assertInstanceOf(MysqlResult::class, $result);
         $this->assertGreaterThan(5, $result->getLastInsertId());
 
         $result = $db->execute("DELETE FROM main WHERE a = :a", ["a" => 10]);
-        $this->assertInstanceOf(Result::class, $result);
+        $this->assertInstanceOf(MysqlResult::class, $result);
     }
 
     public function testExecuteWithInvalidQuery()
@@ -285,7 +285,7 @@ public function testTransaction()
 
         $statement = $transaction->prepare("INSERT INTO main (a, b) VALUES (?, ?)");
         $result = $statement->execute([6, 7]);
-        $this->assertInstanceOf(Result::class, $result);
+        $this->assertInstanceOf(MysqlResult::class, $result);
         $this->assertGreaterThan(5, $result->getLastInsertId());
 
         $result = $transaction->query("SELECT * FROM main WHERE a = 6");
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 108da5d..8aaebca 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -3,54 +3,54 @@
 namespace Amp\Mysql\Test;
 
 use Amp\Future;
-use Amp\Mysql\Connection;
-use Amp\Mysql\Internal\CommandResult;
-use Amp\Mysql\Internal\Processor;
-use Amp\Mysql\Link;
+use Amp\Mysql\Internal\ConnectionProcessor;
+use Amp\Mysql\Internal\MysqlCommandResult;
 use Amp\Mysql\MysqlConfig;
+use Amp\Mysql\MysqlConnection;
 use Amp\Mysql\MysqlConnector;
-use Amp\Mysql\Pool;
-use Amp\Mysql\Result;
-use Amp\Mysql\Statement;
+use Amp\Mysql\MysqlLink;
+use Amp\Mysql\MysqlPool;
+use Amp\Mysql\MysqlResult;
+use Amp\Mysql\MysqlStatement;
 use Amp\Sql\Transaction as SqlTransaction;
 use PHPUnit\Framework\MockObject\MockObject;
 use function Amp\async;
 use function Amp\delay;
 
-interface StatementOperation extends Statement
+interface StatementOperation extends MysqlStatement
 {
 }
 
 class PoolTest extends LinkTest
 {
-    protected function getLink(string $connectionString): Link
+    protected function getLink(string $connectionString): MysqlLink
     {
-        return new Pool(MysqlConfig::fromString($connectionString));
+        return new MysqlPool(MysqlConfig::fromString($connectionString));
     }
 
-    protected function createPool(array $connections): Pool
+    protected function createPool(array $connections): MysqlPool
     {
         $connector = $this->createMock(MysqlConnector::class);
         $connector->method('connect')
-            ->will($this->returnCallback(function () use ($connections): Connection {
+            ->will($this->returnCallback(function () use ($connections): MysqlConnection {
                 static $count = 0;
                 return $connections[$count++ % \count($connections)];
             }));
 
         $config = MysqlConfig::fromString('host=host;user=user;password=password');
 
-        return new Pool($config, \count($connections), Pool::DEFAULT_IDLE_TIMEOUT, $connector);
+        return new MysqlPool($config, \count($connections), MysqlPool::DEFAULT_IDLE_TIMEOUT, $connector);
     }
 
     /**
-     * @return array<int, Processor&MockObject>
+     * @return array<int, ConnectionProcessor&MockObject>
      */
     private function makeProcessorSet(int $count): array
     {
         $processors = [];
 
         for ($i = 0; $i < $count; ++$i) {
-            $processor = $this->createMock(Processor::class);
+            $processor = $this->createMock(ConnectionProcessor::class);
             $processor->method('isClosed')->willReturn(false);
             $processor->method('sendClose')->willReturn(Future::complete(null));
             $processors[] = $processor;
@@ -61,9 +61,9 @@ private function makeProcessorSet(int $count): array
 
     private function makeConnectionSet(array $processors): array
     {
-        return \array_map((function (Processor $processor): Connection {
+        return \array_map((function (ConnectionProcessor $processor): MysqlConnection {
             return new self($processor);
-        })->bindTo(null, Connection::class), $processors);
+        })->bindTo(null, MysqlConnection::class), $processors);
     }
 
     public function getConnectionCounts(): array
@@ -76,7 +76,7 @@ public function getConnectionCounts(): array
      */
     public function testSingleQuery(int $count)
     {
-        $result = $this->createMock(Result::class);
+        $result = $this->createMock(MysqlResult::class);
 
         $processors = $this->makeProcessorSet($count);
 
@@ -84,7 +84,7 @@ public function testSingleQuery(int $count)
         $connection->expects($this->once())
             ->method('query')
             ->with('SQL Query')
-            ->willReturn(async(function () use ($result): Result {
+            ->willReturn(async(function () use ($result): MysqlResult {
                 delay(0.01);
                 return $result;
             }));
@@ -92,7 +92,7 @@ public function testSingleQuery(int $count)
         $pool = $this->createPool($this->makeConnectionSet($processors));
 
         $return = $pool->query('SQL Query');
-        $this->assertInstanceOf(Result::class, $return);
+        $this->assertInstanceOf(MysqlResult::class, $return);
 
         $pool->close();
     }
@@ -103,14 +103,14 @@ public function testSingleQuery(int $count)
     public function testConsecutiveQueries(int $count)
     {
         $rounds = 3;
-        $result = $this->createMock(Result::class);
+        $result = $this->createMock(MysqlResult::class);
 
         $processors = $this->makeProcessorSet($count);
 
         foreach ($processors as $connection) {
             $connection->method('query')
                 ->with('SQL Query')
-                ->willReturn(async(function () use ($result): Result {
+                ->willReturn(async(function () use ($result): MysqlResult {
                     delay(0.01);
                     return $result;
                 }));
@@ -128,7 +128,7 @@ public function testConsecutiveQueries(int $count)
             $results = Future\all($futures);
 
             foreach ($results as $result) {
-                $this->assertInstanceOf(Result::class, $result);
+                $this->assertInstanceOf(MysqlResult::class, $result);
             }
         } finally {
             $pool->close();
@@ -143,11 +143,11 @@ public function testMultipleTransactions(int $count)
         $processors = $this->makeProcessorSet($count);
 
         $connection = $processors[0];
-        $result = new CommandResult(0, 0);
+        $result = new MysqlCommandResult(0, 0);
 
         $connection->expects($this->exactly(3))
             ->method('query')
-            ->willReturn(async(function () use ($result): Result {
+            ->willReturn(async(function () use ($result): MysqlResult {
                 delay(0.01);
                 return $result;
             }));
@@ -169,13 +169,13 @@ public function testMultipleTransactions(int $count)
     public function testConsecutiveTransactions(int $count)
     {
         $rounds = 3;
-        $result = new CommandResult(0, 0);
+        $result = new MysqlCommandResult(0, 0);
 
         $processors = $this->makeProcessorSet($count);
 
         foreach ($processors as $connection) {
             $connection->method('query')
-                ->willReturnCallback(fn () => async(function () use ($result): Result {
+                ->willReturnCallback(fn () => async(function () use ($result): MysqlResult {
                     delay(0.01);
                     return $result;
                 }));
@@ -211,7 +211,7 @@ public function testExtractConnection(int $count)
             $connection->expects($this->once())
                 ->method('query')
                 ->with($query)
-                ->willReturn(Future::complete($this->createMock(Result::class)));
+                ->willReturn(Future::complete($this->createMock(MysqlResult::class)));
         }
 
         $pool = $this->createPool($this->makeConnectionSet($processors));
@@ -223,7 +223,7 @@ public function testExtractConnection(int $count)
             }
             $results = Future\all($futures);
             foreach ($results as $result) {
-                $this->assertInstanceof(Connection::class, $result);
+                $this->assertInstanceof(MysqlConnection::class, $result);
                 $result->query($query);
             }
         } finally {
@@ -238,25 +238,25 @@ public function testConnectionClosedInPool(int $count)
     {
         $processors = $this->makeProcessorSet($count);
         $query = "SELECT * FROM test";
-        $result = $this->createMock(Result::class);
+        $result = $this->createMock(MysqlResult::class);
 
         foreach ($processors as $processor) {
             $processor->expects($this->atLeastOnce())
                 ->method('query')
                 ->with($query)
-                ->willReturn(async(function () use ($result): Result {
+                ->willReturn(async(function () use ($result): MysqlResult {
                     delay(0.01);
                     return $result;
                 }));
         }
 
-        $processor = $this->createMock(Processor::class);
+        $processor = $this->createMock(ConnectionProcessor::class);
         $processor->method('isClosed')
             ->willReturnOnConsecutiveCalls(false, true);
         $processor->expects($this->once())
             ->method('query')
             ->with($query)
-            ->willReturn(async(function () use ($result): Result {
+            ->willReturn(async(function () use ($result): MysqlResult {
                 delay(0.01);
                 return $result;
             }));
diff --git a/test/StatementTest.php b/test/StatementTest.php
index 10dac3a..4ea68f1 100644
--- a/test/StatementTest.php
+++ b/test/StatementTest.php
@@ -2,25 +2,25 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Mysql\Internal\ConnectionStatement;
-use Amp\Mysql\Internal\Processor;
-use Amp\Mysql\Internal\ResultProxy;
+use Amp\Mysql\Internal\ConnectionProcessor;
+use Amp\Mysql\Internal\MysqlConnectionStatement;
+use Amp\Mysql\Internal\MysqlResultProxy;
 use Amp\PHPUnit\AsyncTestCase;
 use PHPUnit\Framework\MockObject\MockObject;
 
 class StatementTest extends AsyncTestCase
 {
-    /** @var MockObject&Processor */
-    protected Processor $processor;
+    /** @var MockObject&ConnectionProcessor */
+    protected ConnectionProcessor $processor;
 
-    protected ResultProxy $resultProxy;
+    protected MysqlResultProxy $resultProxy;
 
     public function setUp(): void
     {
         parent::setUp();
 
-        $this->processor = $this->createMock(Processor::class);
-        $this->resultProxy = new ResultProxy;
+        $this->processor = $this->createMock(ConnectionProcessor::class);
+        $this->resultProxy = new MysqlResultProxy;
     }
 
     /**
@@ -42,7 +42,7 @@ public function testBindDataTypes($data, $expectedException)
             ->method('closeStmt');
 
         $this->resultProxy->columnsToFetch = 1;
-        $stmt = new ConnectionStatement($this->processor, $query, $stmtId, $named, $this->resultProxy);
+        $stmt = new MysqlConnectionStatement($this->processor, $query, $stmtId, $named, $this->resultProxy);
 
         // assert
         if ($expectedException) {

From eaad4a32ecd659e9395083d6b65bcc8564102ad7 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 4 Jul 2022 13:24:05 -0500
Subject: [PATCH 077/174] Remove pool() function

Added
---
 examples/2-simple-query.php       |  5 +++--
 examples/3-generic-with-yield.php |  5 +++--
 examples/4-multi-rows.php         | 10 ++++++----
 examples/5-multi-stmts.php        |  5 +++--
 examples/6-transaction.php        |  5 +++--
 src/functions.php                 | 17 -----------------
 6 files changed, 18 insertions(+), 29 deletions(-)

diff --git a/examples/2-simple-query.php b/examples/2-simple-query.php
index eba0540..5150c45 100644
--- a/examples/2-simple-query.php
+++ b/examples/2-simple-query.php
@@ -2,9 +2,10 @@
 
 require 'support/bootstrap.php';
 
-use Amp\Mysql;
+use Amp\Mysql\MysqlConfig;
+use Amp\Mysql\MysqlPool;
 
-$db = Mysql\pool(Mysql\MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = new MysqlPool(MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
 $result = $db->query("SELECT 1 AS value");
 
diff --git a/examples/3-generic-with-yield.php b/examples/3-generic-with-yield.php
index 20b359b..1007be8 100644
--- a/examples/3-generic-with-yield.php
+++ b/examples/3-generic-with-yield.php
@@ -3,10 +3,11 @@
 require 'support/bootstrap.php';
 
 use Amp\Future;
-use Amp\Mysql;
+use Amp\Mysql\MysqlConfig;
+use Amp\Mysql\MysqlPool;
 use function Amp\async;
 
-$db = Mysql\pool(Mysql\MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = new MysqlPool(MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
 $db->query("DROP TABLE IF EXISTS tmp");
 
diff --git a/examples/4-multi-rows.php b/examples/4-multi-rows.php
index 2786ac1..e00f986 100644
--- a/examples/4-multi-rows.php
+++ b/examples/4-multi-rows.php
@@ -4,10 +4,12 @@
 require 'support/generic-table.php';
 
 use Amp\Future;
-use Amp\Mysql;
+use Amp\Mysql\MysqlConfig;
+use Amp\Mysql\MysqlPool;
+use Amp\Mysql\MysqlResult;
 use function Amp\async;
 
-$db = Mysql\pool(Mysql\MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = new MysqlPool(MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
 /* create same table than in 3-generic-with-yield.php */
 createGenericTable($db);
@@ -19,8 +21,8 @@
 
 try {
     /**
-     * @var Mysql\MysqlResult $result1
-     * @var Mysql\MysqlResult $result2
+     * @var MysqlResult $result1
+     * @var MysqlResult $result2
      */
     [$result1, $result2] = Future\await($future); // Both queries execute simultaneously. Wait for both to finish here.
 } catch (\Throwable $e) {
diff --git a/examples/5-multi-stmts.php b/examples/5-multi-stmts.php
index 5c248ad..7b5f528 100644
--- a/examples/5-multi-stmts.php
+++ b/examples/5-multi-stmts.php
@@ -3,9 +3,10 @@
 require 'support/bootstrap.php';
 require 'support/generic-table.php';
 
-use Amp\Mysql;
+use Amp\Mysql\MysqlConfig;
+use Amp\Mysql\MysqlPool;
 
-$db = Mysql\pool(Mysql\MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = new MysqlPool(MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
 /* create same table than in 3-generic-with-yield.php */
 createGenericTable($db);
diff --git a/examples/6-transaction.php b/examples/6-transaction.php
index f9e0bbc..9ab0ddf 100644
--- a/examples/6-transaction.php
+++ b/examples/6-transaction.php
@@ -3,9 +3,10 @@
 require 'support/bootstrap.php';
 require 'support/generic-table.php';
 
-use Amp\Mysql;
+use Amp\Mysql\MysqlConfig;
+use Amp\Mysql\MysqlPool;
 
-$db = Mysql\pool(Mysql\MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = new MysqlPool(MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
 
 /* create same table than in 3-generic-with-yield.php */
 createGenericTable($db);
diff --git a/src/functions.php b/src/functions.php
index 76ded39..43ada08 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -2,7 +2,6 @@
 
 namespace Amp\Mysql;
 
-use Amp\Sql\Common\ConnectionPool;
 use Amp\Sql\SqlException;
 use Revolt\EventLoop;
 
@@ -29,19 +28,3 @@ function connect(MysqlConfig $config): MysqlConnection
 {
     return mysqlConnector()->connect($config);
 }
-
-/**
- * Create a pool using the global Connector instance.
- *
- * @param positive-int $maxConnections
- * @param positive-int $idleTimeout
- *
- * @throws \Error If the connection string does not contain a host, user, and password.
- */
-function pool(
-    MysqlConfig $config,
-    int $maxConnections = ConnectionPool::DEFAULT_MAX_CONNECTIONS,
-    int $idleTimeout = ConnectionPool::DEFAULT_IDLE_TIMEOUT,
-): MysqlPool {
-    return new MysqlPool($config, $maxConnections, $idleTimeout, mysqlConnector());
-}

From cdff6fa81fc05760108d988f38f16e5d77e161bc Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 4 Jul 2022 14:30:23 -0500
Subject: [PATCH 078/174] Add MysqlConfig::fromAuthority()

Using the new function to cleanup examples and test case creation of a MysqlConfig object.
---
 examples/1-connect.php               |  5 ++--
 examples/2-simple-query.php          |  2 +-
 examples/3-generic-with-yield.php    |  2 +-
 examples/4-multi-rows.php            |  2 +-
 examples/5-multi-stmts.php           |  2 +-
 examples/6-transaction.php           |  2 +-
 src/Internal/ConnectionProcessor.php |  4 +--
 src/MysqlConfig.php                  | 33 ++++++++++++++++----
 test/ConnectionTest.php              |  9 +++---
 test/LinkTest.php                    | 45 +++++++++++++++++-----------
 test/PoolTest.php                    |  4 +--
 11 files changed, 70 insertions(+), 40 deletions(-)

diff --git a/examples/1-connect.php b/examples/1-connect.php
index 2fdbde9..d5d5e3b 100644
--- a/examples/1-connect.php
+++ b/examples/1-connect.php
@@ -8,11 +8,10 @@
 
 use Amp\Mysql;
 
-/* If you want ssl, pass as second argument an array with ssl options (an empty options array is valid too); if null is passed, ssl is not enabled either */
-$config = Mysql\MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME);
+$config = Mysql\MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME);
 
 /* use an alternative charset... Default is utf8mb4_general_ci */
-$config = $config->withCharset("latin1_general_ci");
+$config = $config->withCharset("ascii", "ascii_general_ci");
 
 $db = Mysql\connect($config);
 
diff --git a/examples/2-simple-query.php b/examples/2-simple-query.php
index 5150c45..9a488f0 100644
--- a/examples/2-simple-query.php
+++ b/examples/2-simple-query.php
@@ -5,7 +5,7 @@
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlPool;
 
-$db = new MysqlPool(MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = new MysqlPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
 
 $result = $db->query("SELECT 1 AS value");
 
diff --git a/examples/3-generic-with-yield.php b/examples/3-generic-with-yield.php
index 1007be8..ea967b5 100644
--- a/examples/3-generic-with-yield.php
+++ b/examples/3-generic-with-yield.php
@@ -7,7 +7,7 @@
 use Amp\Mysql\MysqlPool;
 use function Amp\async;
 
-$db = new MysqlPool(MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = new MysqlPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
 
 $db->query("DROP TABLE IF EXISTS tmp");
 
diff --git a/examples/4-multi-rows.php b/examples/4-multi-rows.php
index e00f986..3bc1693 100644
--- a/examples/4-multi-rows.php
+++ b/examples/4-multi-rows.php
@@ -9,7 +9,7 @@
 use Amp\Mysql\MysqlResult;
 use function Amp\async;
 
-$db = new MysqlPool(MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = new MysqlPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
 
 /* create same table than in 3-generic-with-yield.php */
 createGenericTable($db);
diff --git a/examples/5-multi-stmts.php b/examples/5-multi-stmts.php
index 7b5f528..4ddbf55 100644
--- a/examples/5-multi-stmts.php
+++ b/examples/5-multi-stmts.php
@@ -6,7 +6,7 @@
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlPool;
 
-$db = new MysqlPool(MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = new MysqlPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
 
 /* create same table than in 3-generic-with-yield.php */
 createGenericTable($db);
diff --git a/examples/6-transaction.php b/examples/6-transaction.php
index 9ab0ddf..a3cfbdb 100644
--- a/examples/6-transaction.php
+++ b/examples/6-transaction.php
@@ -6,7 +6,7 @@
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlPool;
 
-$db = new MysqlPool(MysqlConfig::fromString("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=".DB_NAME));
+$db = new MysqlPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
 
 /* create same table than in 3-generic-with-yield.php */
 createGenericTable($db);
diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 6d22e51..47e3450 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -192,11 +192,11 @@ public function connect(?Cancellation $cancellation = null): void
         if ($this->config->getCharset() !== MysqlConfig::DEFAULT_CHARSET
             || $this->config->getCollation() !== MysqlConfig::DEFAULT_COLLATE
         ) {
-            $future = $future->map(function () {
+            $future = $future->map(function (): void {
                 $charset = $this->config->getCharset();
                 $collate = $this->config->getCollation();
 
-                $this->query("SET NAMES '$charset'" . ($collate === "" ? "" : " COLLATE '$collate'"));
+                $this->query("SET NAMES '$charset'" . ($collate === "" ? "" : " COLLATE '$collate'"))->await();
             });
         }
 
diff --git a/src/MysqlConfig.php b/src/MysqlConfig.php
index eb059d3..5e78507 100644
--- a/src/MysqlConfig.php
+++ b/src/MysqlConfig.php
@@ -60,13 +60,32 @@ public static function fromString(string $connectionString, ConnectContext $cont
         );
     }
 
+    public static function fromAuthority(
+        string $authority,
+        string $user,
+        string $password,
+        ?string $database = null,
+        ?ConnectContext $context = null
+    ): self {
+        [$host, $port] = \explode(':', $authority, 2) + ['', (string) self::DEFAULT_PORT];
+
+        return new self(
+            host: $host,
+            port: (int) $port,
+            user: $user,
+            password: $password,
+            database: $database,
+            context: $context,
+        );
+    }
+
     public function __construct(
         string $host,
         int $port = self::DEFAULT_PORT,
-        string $user = null,
-        string $password = null,
-        string $database = null,
-        ConnectContext $context = null,
+        ?string $user = null,
+        ?string $password = null,
+        ?string $database = null,
+        ?ConnectContext $context = null,
         string $charset = self::DEFAULT_CHARSET,
         string $collate = self::DEFAULT_COLLATE,
         bool $useCompression = false,
@@ -85,7 +104,9 @@ public function __construct(
 
     public function getConnectionString(): string
     {
-        return $this->getHost()[0] == "/" ? 'unix://' . $this->getHost() : 'tcp://' . $this->getHost() . ':' . $this->getPort();
+        return $this->getHost()[0] == "/"
+            ? 'unix://' . $this->getHost()
+            : 'tcp://' . $this->getHost() . ':' . $this->getPort();
     }
 
     public function isCompressionEnabled(): bool
@@ -148,7 +169,7 @@ public function getCollation(): string
         return $this->collate;
     }
 
-    public function withCharset(string $charset = self::DEFAULT_CHARSET, string $collate = self::DEFAULT_COLLATE): self
+    public function withCharset(string $charset, string $collate): self
     {
         $new = clone $this;
         $new->charset = $charset;
diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php
index f17f100..07ae239 100644
--- a/test/ConnectionTest.php
+++ b/test/ConnectionTest.php
@@ -3,22 +3,21 @@
 namespace Amp\Mysql\Test;
 
 use Amp\Mysql\DefaultMysqlConnector;
-use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlConnection;
 use Amp\Mysql\MysqlLink;
 
 class ConnectionTest extends LinkTest
 {
-    protected function getLink(string $connectionString): MysqlLink
+    protected function getLink(bool $useCompression = false): MysqlLink
     {
-        return (new DefaultMysqlConnector)->connect(MysqlConfig::fromString($connectionString));
+        return (new DefaultMysqlConnector)->connect($this->getConfig($useCompression));
     }
 
     public function testConnect()
     {
         $connector = new DefaultMysqlConnector();
 
-        $db = $connector->connect(MysqlConfig::fromString("host=".DB_HOST." user=".DB_USER." pass=".DB_PASS." db=test"));
+        $db = $connector->connect($this->getConfig());
 
         $this->assertInstanceOf(MysqlConnection::class, $db);
 
@@ -30,7 +29,7 @@ public function testConnect()
 
     public function testDoubleClose()
     {
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink();
 
         $db->close();
 
diff --git a/test/LinkTest.php b/test/LinkTest.php
index af3f0aa..ebda979 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -3,6 +3,7 @@
 namespace Amp\Mysql\Test;
 
 use Amp\Mysql\MysqlColumnDefinition;
+use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlDataType;
 use Amp\Mysql\MysqlLink;
 use Amp\Mysql\MysqlResult;
@@ -14,11 +15,21 @@ abstract class LinkTest extends AsyncTestCase
     /**
      * Returns the Link class to be tested.
      */
-    abstract protected function getLink(string $connectionString): MysqlLink;
+    abstract protected function getLink(bool $useCompression = false): MysqlLink;
+
+    protected function getConfig(bool $useCompression = false): MysqlConfig
+    {
+        $config = MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, 'test');
+        if ($useCompression) {
+            $config = $config->withCompression();
+        }
+
+        return $config;
+    }
 
     public function testQuery()
     {
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink();
 
         $resultset = $db->execute("SELECT ? AS a", [M_PI]);
         $this->assertInstanceOf(MysqlResult::class, $resultset);
@@ -34,7 +45,7 @@ public function testQuery()
 
     public function testQueryFetchRow()
     {
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink();
 
         $resultset = $db->query('SELECT a FROM main WHERE a < 4');
         $this->assertInstanceOf(MysqlResult::class, $resultset);
@@ -54,14 +65,14 @@ public function testQueryWithInvalidQuery()
         $this->expectException(QueryError::class);
         $this->expectExceptionMessage('You have an error in your SQL syntax');
 
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink();
 
         $db->query("SELECT & FROM main WHERE a = 1");
     }
 
     public function testMultiStmt()
     {
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
+        $db = $this->getLink(true);
 
         $resultset = $db->query("SELECT a FROM main; SELECT b FROM main WHERE a = 5; SELECT b AS d, a + 1 AS c FROM main WHERE b > 4");
         $this->assertInstanceOf(MysqlResult::class, $resultset);
@@ -100,7 +111,7 @@ public function testMultiStmt()
 
     public function testPrepared()
     {
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test;useCompression=true");
+        $db = $this->getLink(true);
 
         $stmt = $db->prepare("SELECT id as no, a, b FROM main as table_alias WHERE a = ? OR b = :num");
         $base = [
@@ -168,7 +179,7 @@ public function testPrepareWithInvalidQuery()
         $this->expectException(QueryError::class);
         $this->expectExceptionMessage('You have an error in your SQL syntax');
 
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink();
 
         $statement = $db->prepare("SELECT & FROM main WHERE a = ?");
 
@@ -180,7 +191,7 @@ public function testBindWithInvalidParamId()
         $this->expectException(\Error::class);
         $this->expectExceptionMessage('Parameter 1 is not defined for this prepared statement');
 
-        $db = $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
+        $db = $this->getLink();
 
         $statement = $db->prepare("SELECT * FROM main WHERE a = ?");
 
@@ -194,7 +205,7 @@ public function testBindWithInvalidParamName()
         $this->expectException(\Error::class);
         $this->expectExceptionMessage('Parameter :b is not defined for this prepared statement');
 
-        $db = $this->getLink("host=" . DB_HOST . ";user=" . DB_USER . ";pass=" . DB_PASS . ";db=test");
+        $db = $this->getLink();
 
         $statement = $db->prepare("SELECT * FROM main WHERE a = :a");
 
@@ -208,7 +219,7 @@ public function testStatementExecuteWithTooFewParams()
         $this->expectException(\Error::class);
         $this->expectExceptionMessage('Parameter 1 for prepared statement missing');
 
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink();
 
         $stmt = $db->prepare("SELECT * FROM main WHERE a = ? AND b = ?");
         $stmt->execute([1]);
@@ -216,7 +227,7 @@ public function testStatementExecuteWithTooFewParams()
 
     public function testExecute()
     {
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink();
 
         $result = $db->execute("SELECT * FROM test.main WHERE a = ? OR b = ?", [2, 5]);
         $this->assertInstanceOf(MysqlResult::class, $result);
@@ -240,7 +251,7 @@ public function testExecuteWithInvalidQuery()
         $this->expectException(QueryError::class);
         $this->expectExceptionMessage('You have an error in your SQL syntax');
 
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink();
 
         $db->execute("SELECT & FROM main WHERE a = ?", [1]);
 
@@ -252,7 +263,7 @@ public function testExecuteWithTooFewParams()
         $this->expectException(\Error::class);
         $this->expectExceptionMessage('Parameter 1 for prepared statement missing');
 
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink();
 
         $db->execute("SELECT * FROM main WHERE a = ? AND b = ?", [1]);
 
@@ -261,7 +272,7 @@ public function testExecuteWithTooFewParams()
 
     public function testPreparedWithNegativeValue()
     {
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink();
 
         $db->query("DROP TABLE IF EXISTS tmp");
 
@@ -279,7 +290,7 @@ public function testPreparedWithNegativeValue()
 
     public function testTransaction()
     {
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink();
 
         $transaction = $db->beginTransaction();
 
@@ -324,7 +335,7 @@ public function testTransaction()
      */
     public function testInsertSelect()
     {
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink();
 
         $a = 1;
 
@@ -350,7 +361,7 @@ public function testInsertSelect()
 
     public function testJsonDecoding()
     {
-        $db = $this->getLink("host=".DB_HOST.";user=".DB_USER.";pass=".DB_PASS.";db=test");
+        $db = $this->getLink();
 
         $result = $db->execute("SELECT a FROM test.json");
 
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 8aaebca..3de9471 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -23,9 +23,9 @@ interface StatementOperation extends MysqlStatement
 
 class PoolTest extends LinkTest
 {
-    protected function getLink(string $connectionString): MysqlLink
+    protected function getLink(bool $useCompression = false): MysqlLink
     {
-        return new MysqlPool(MysqlConfig::fromString($connectionString));
+        return new MysqlPool($this->getConfig($useCompression));
     }
 
     protected function createPool(array $connections): MysqlPool

From 6d7ba86c373d4e2a0538c79937bbda17311dd6df Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Fri, 8 Jul 2022 19:51:15 -0500
Subject: [PATCH 079/174] Update using template types

Added MysqlConnection interface so connector is not coupled to a concrete class. The class that was MysqlConnection is now MysqlSocketConnection. Renamed DefaultMysqlConnector to SocketMysqlConnector.
---
 benchmarks/QueryBench.php                     |   4 +-
 src/Internal/ConnectionProcessor.php          |   2 +-
 src/Internal/MysqlCommandResult.php           |  10 +-
 src/Internal/MysqlPooledResult.php            |  18 ++-
 src/Internal/MysqlPooledStatement.php         |   9 +-
 src/Internal/MysqlPooledTransaction.php       |  23 ++-
 src/Internal/MysqlStatementPool.php           |  11 +-
 src/MysqlConnection.php                       | 136 +----------------
 src/MysqlConnector.php                        |   5 +-
 src/MysqlExecutor.php                         |   3 +
 src/MysqlLink.php                             |   3 +
 src/MysqlPool.php                             |  31 ++--
 src/MysqlResult.php                           |   5 +
 src/MysqlSocketConnection.php                 | 137 ++++++++++++++++++
 src/MysqlTransaction.php                      |   3 +
 ...Connector.php => SocketMysqlConnector.php} |   4 +-
 src/functions.php                             |  11 +-
 test/ConnectionTest.php                       |   8 +-
 test/PoolTest.php                             |   5 +-
 19 files changed, 232 insertions(+), 196 deletions(-)
 create mode 100644 src/MysqlSocketConnection.php
 rename src/{DefaultMysqlConnector.php => SocketMysqlConnector.php} (82%)

diff --git a/benchmarks/QueryBench.php b/benchmarks/QueryBench.php
index 8468c1b..9610bc5 100644
--- a/benchmarks/QueryBench.php
+++ b/benchmarks/QueryBench.php
@@ -2,7 +2,7 @@
 
 namespace Amp\Mysql\Bench;
 
-use Amp\Mysql\DefaultMysqlConnector;
+use Amp\Mysql\SocketMysqlConnector;
 use Amp\Mysql\MysqlConnection;
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlPool as ConnectionPool;
@@ -38,7 +38,7 @@ class QueryBench extends AbstractBench
     public function init()
     {
         $config = MysqlConfig::fromString("host=$this->host;user=$this->user;pass=$this->pass");
-        $connector = new DefaultMysqlConnector;
+        $connector = new SocketMysqlConnector;
         $this->connectionPool = new ConnectionPool($config, $this->poolLimit, 10, $connector);
         $connectionPromise = $connector->connect($config);
         $this->connection = wait($connectionPromise);
diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 47e3450..246dc58 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -313,7 +313,7 @@ protected function startCommand(\Closure $callback): Future
         return $deferred->getFuture();
     }
 
-    public function setCharset(string $charset, string $collate = ""): Future
+    public function setCharset(string $charset, string $collate): Future
     {
         if ($collate === "" && false !== $offset = \strpos($charset, "_")) {
             $collate = $charset;
diff --git a/src/Internal/MysqlCommandResult.php b/src/Internal/MysqlCommandResult.php
index d5a7746..10d5b77 100644
--- a/src/Internal/MysqlCommandResult.php
+++ b/src/Internal/MysqlCommandResult.php
@@ -4,18 +4,19 @@
 
 use Amp\Future;
 use Amp\Mysql\MysqlResult;
-use Amp\Sql\Common\CommandResult as SqlCommandResult;
+use Amp\Sql\Common\CommandResult;
 
 /** @internal */
 final class MysqlCommandResult implements MysqlResult, \IteratorAggregate
 {
     private ?int $lastInsertId;
 
-    private readonly SqlCommandResult $delegate;
+    /** @var CommandResult<null> */
+    private readonly CommandResult $delegate;
 
     public function __construct(int $affectedRows, int $lastInsertId)
     {
-        $this->delegate = new SqlCommandResult($affectedRows, Future::complete());
+        $this->delegate = new CommandResult($affectedRows, Future::complete());
         $this->lastInsertId = $lastInsertId ?: null; // Convert 0 to null
     }
 
@@ -42,9 +43,6 @@ public function getLastInsertId(): ?int
         return $this->lastInsertId;
     }
 
-    /**
-     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
-     */
     public function getNextResult(): ?MysqlResult
     {
         return $this->delegate->getNextResult();
diff --git a/src/Internal/MysqlPooledResult.php b/src/Internal/MysqlPooledResult.php
index 4b9bf91..b1732b6 100644
--- a/src/Internal/MysqlPooledResult.php
+++ b/src/Internal/MysqlPooledResult.php
@@ -3,11 +3,14 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\Mysql\MysqlResult;
-use Amp\Sql\Common\PooledResult as SqlPooledResult;
-use Amp\Sql\Result as SqlResult;
+use Amp\Sql\Common\PooledResult;
+use Amp\Sql\Result;
 
-/** @internal */
-final class MysqlPooledResult extends SqlPooledResult implements MysqlResult
+/**
+ * @internal
+ * @extends PooledResult<MysqlResult>
+ */
+final class MysqlPooledResult extends PooledResult implements MysqlResult
 {
     private readonly MysqlResult $result;
 
@@ -20,12 +23,17 @@ public function __construct(MysqlResult $result, \Closure $release)
         $this->result = $result;
     }
 
-    protected function newInstanceFrom(SqlResult $result, \Closure $release): self
+    protected function newInstanceFrom(Result $result, \Closure $release): self
     {
         \assert($result instanceof MysqlResult);
         return new self($result, $release);
     }
 
+    public function getNextResult(): ?MysqlResult
+    {
+        return parent::getNextResult();
+    }
+
     public function getLastInsertId(): ?int
     {
         return $this->result->getLastInsertId();
diff --git a/src/Internal/MysqlPooledStatement.php b/src/Internal/MysqlPooledStatement.php
index 931aec6..6530cdd 100644
--- a/src/Internal/MysqlPooledStatement.php
+++ b/src/Internal/MysqlPooledStatement.php
@@ -4,11 +4,14 @@
 
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
-use Amp\Sql\Common\PooledStatement as SqlPooledStatement;
+use Amp\Sql\Common\PooledStatement;
 use Amp\Sql\Result as SqlResult;
 
-/** @internal */
-final class MysqlPooledStatement extends SqlPooledStatement implements MysqlStatement
+/**
+ * @internal
+ * @extends PooledStatement<MysqlResult, MysqlStatement>
+ */
+final class MysqlPooledStatement extends PooledStatement implements MysqlStatement
 {
     private readonly MysqlStatement $statement;
 
diff --git a/src/Internal/MysqlPooledTransaction.php b/src/Internal/MysqlPooledTransaction.php
index a0c7148..dae9714 100644
--- a/src/Internal/MysqlPooledTransaction.php
+++ b/src/Internal/MysqlPooledTransaction.php
@@ -5,20 +5,23 @@
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
 use Amp\Mysql\MysqlTransaction;
-use Amp\Sql\Common\PooledTransaction as SqlPooledTransaction;
-use Amp\Sql\Result as SqlResult;
-use Amp\Sql\Statement as SqlStatement;
+use Amp\Sql\Common\PooledTransaction;
+use Amp\Sql\Result;
+use Amp\Sql\Statement;
 
-/** @internal */
-final class MysqlPooledTransaction extends SqlPooledTransaction implements MysqlTransaction
+/**
+ * @internal
+ * @extends PooledTransaction<MysqlResult, MysqlStatement, MysqlTransaction>
+ */
+final class MysqlPooledTransaction extends PooledTransaction implements MysqlTransaction
 {
-    protected function createStatement(SqlStatement $statement, \Closure $release): MysqlStatement
+    protected function createStatement(Statement $statement, \Closure $release): MysqlStatement
     {
         \assert($statement instanceof MysqlStatement);
         return new MysqlPooledStatement($statement, $release);
     }
 
-    protected function createResult(SqlResult $result, \Closure $release): MysqlResult
+    protected function createResult(Result $result, \Closure $release): MysqlResult
     {
         \assert($result instanceof MysqlResult);
         return new MysqlPooledResult($result, $release);
@@ -26,8 +29,6 @@ protected function createResult(SqlResult $result, \Closure $release): MysqlResu
 
     /**
      * Changes return type to this library's Result type.
-     *
-     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function query(string $sql): MysqlResult
     {
@@ -36,8 +37,6 @@ public function query(string $sql): MysqlResult
 
     /**
      * Changes return type to this library's Statement type.
-     *
-     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function prepare(string $sql): MysqlStatement
     {
@@ -46,8 +45,6 @@ public function prepare(string $sql): MysqlStatement
 
     /**
      * Changes return type to this library's Result type.
-     *
-     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function execute(string $sql, array $params = []): MysqlResult
     {
diff --git a/src/Internal/MysqlStatementPool.php b/src/Internal/MysqlStatementPool.php
index d98c75e..0fb6717 100644
--- a/src/Internal/MysqlStatementPool.php
+++ b/src/Internal/MysqlStatementPool.php
@@ -7,14 +7,14 @@
 use Amp\Sql\Common\StatementPool as SqlStatementPool;
 use Amp\Sql\Result as SqlResult;
 
-/** @internal */
+/**
+ * @internal
+ * @extends SqlStatementPool<MysqlResult, MysqlStatement>
+ */
 final class MysqlStatementPool extends SqlStatementPool implements MysqlStatement
 {
     private array $params = [];
 
-    /**
-     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
-     */
     protected function pop(): MysqlStatement
     {
         $statement = parent::pop();
@@ -44,9 +44,6 @@ protected function createResult(SqlResult $result, \Closure $release): MysqlResu
         return new MysqlPooledResult($result, $release);
     }
 
-    /**
-     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
-     */
     public function execute(array $params = []): MysqlResult
     {
         return parent::execute($params);
diff --git a/src/MysqlConnection.php b/src/MysqlConnection.php
index 507f203..bce1b13 100644
--- a/src/MysqlConnection.php
+++ b/src/MysqlConnection.php
@@ -2,141 +2,15 @@
 
 namespace Amp\Mysql;
 
-use Amp\Cancellation;
-use Amp\DeferredFuture;
-use Amp\Socket\EncryptableSocket;
-use Amp\Sql\TransactionIsolation;
-use Amp\Sql\TransactionIsolationLevel;
-use Revolt\EventLoop;
-
-final class MysqlConnection implements MysqlLink
+interface MysqlConnection extends MysqlLink
 {
-    private readonly Internal\ConnectionProcessor $processor;
-
-    private ?DeferredFuture $busy = null;
-
-    /** @var \Closure():void Function used to release connection after a transaction has completed. */
-    private readonly \Closure $release;
-
-    public static function initialize(
-        EncryptableSocket $socket,
-        MysqlConfig $config,
-        ?Cancellation $cancellation = null,
-    ): self {
-        $processor = new Internal\ConnectionProcessor($socket, $config);
-        $processor->connect($cancellation);
-        return new self($processor);
-    }
-
-    private function __construct(Internal\ConnectionProcessor $processor)
-    {
-        $this->processor = $processor;
-
-        $busy = &$this->busy;
-        $this->release = static function () use (&$busy): void {
-            \assert($busy instanceof DeferredFuture);
-            $busy->complete();
-            $busy = null;
-        };
-    }
-
     /**
-     * @return bool False if the connection has been closed.
+     * Change the active database on the connection.
      */
-    public function isClosed(): bool
-    {
-        return $this->processor->isClosed();
-    }
+    public function useDb(string $db): void;
 
     /**
-     * @return int Timestamp of the last time this connection was used.
+     * Change the character set used by the connection.
      */
-    public function getLastUsedAt(): int
-    {
-        return $this->processor->getLastUsedAt();
-    }
-
-    public function isReady(): bool
-    {
-        return $this->processor->isReady();
-    }
-
-    public function setCharset(string $charset, string $collate = ""): void
-    {
-        $this->processor->setCharset($charset, $collate)->await();
-    }
-
-    public function close(): void
-    {
-        // Send close command if connection is not already in a closed or closing state
-        if (!$this->processor->isClosed()) {
-            $this->processor->sendClose()->await();
-        }
-    }
-
-    public function onClose(\Closure $onClose): void
-    {
-        $this->processor->onClose($onClose);
-    }
-
-    public function useDb(string $db): void
-    {
-        $this->processor->useDb($db)->await();
-    }
-
-    public function query(string $sql): MysqlResult
-    {
-        while ($this->busy) {
-            $this->busy->getFuture()->await();
-        }
-
-        return $this->processor->query($sql)->await();
-    }
-
-    public function beginTransaction(
-        TransactionIsolation $isolation = TransactionIsolationLevel::Committed
-    ): MysqlTransaction {
-        while ($this->busy) {
-            $this->busy->getFuture()->await();
-        }
-
-        $this->busy = $deferred = new DeferredFuture;
-
-        try {
-            $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL " . $isolation->toSql())->await();
-            $this->processor->query("START TRANSACTION")->await();
-        } catch (\Throwable $exception) {
-            $this->busy = null;
-            $deferred->complete();
-            throw $exception;
-        }
-
-        return new Internal\MysqlConnectionTransaction($this->processor, $this->release, $isolation);
-    }
-
-    public function ping(): void
-    {
-        $this->processor->ping()->await();
-    }
-
-    public function prepare(string $sql): MysqlStatement
-    {
-        while ($this->busy) {
-            $this->busy->getFuture()->await();
-        }
-
-        return $this->processor->prepare($sql)->await();
-    }
-
-    public function execute(string $sql, array $params = []): MysqlResult
-    {
-        $statement = $this->prepare($sql);
-        return $statement->execute($params);
-    }
-
-    public function __destruct()
-    {
-        $processor = $this->processor;
-        EventLoop::queue(static fn () => $processor->unreference());
-    }
+    public function setCharset(string $charset, string $collate): void;
 }
diff --git a/src/MysqlConnector.php b/src/MysqlConnector.php
index 5b2af95..011e67e 100644
--- a/src/MysqlConnector.php
+++ b/src/MysqlConnector.php
@@ -7,10 +7,13 @@
 use Amp\Sql\SqlConnector;
 use Amp\Sql\SqlException;
 
+/**
+ * @extends SqlConnector<MysqlConfig, MysqlConnection>
+ */
 interface MysqlConnector extends SqlConnector
 {
     /**
      * @throws SqlException
      */
-    public function connect(SqlConfig $config, ?Cancellation $cancellation = null): MysqlConnection;
+    public function connect(SqlConfig $config, ?Cancellation $cancellation = null): MysqlLink;
 }
diff --git a/src/MysqlExecutor.php b/src/MysqlExecutor.php
index 302f524..1b9a7ca 100644
--- a/src/MysqlExecutor.php
+++ b/src/MysqlExecutor.php
@@ -4,6 +4,9 @@
 
 use Amp\Sql\Executor;
 
+/**
+ * @extends Executor<MysqlResult, MysqlStatement>
+ */
 interface MysqlExecutor extends Executor
 {
     /**
diff --git a/src/MysqlLink.php b/src/MysqlLink.php
index 554097f..4362520 100644
--- a/src/MysqlLink.php
+++ b/src/MysqlLink.php
@@ -6,6 +6,9 @@
 use Amp\Sql\TransactionIsolation;
 use Amp\Sql\TransactionIsolationLevel;
 
+/**
+ * @extends Link<MysqlResult, MysqlStatement, MysqlTransaction>
+ */
 interface MysqlLink extends MysqlExecutor, Link
 {
     /**
diff --git a/src/MysqlPool.php b/src/MysqlPool.php
index 971ff93..c35e984 100644
--- a/src/MysqlPool.php
+++ b/src/MysqlPool.php
@@ -4,54 +4,57 @@
 
 use Amp\Sql\Common\ConnectionPool;
 use Amp\Sql\Common\StatementPool;
-use Amp\Sql\Pool as SqlPool;
-use Amp\Sql\Result as SqlResult;
-use Amp\Sql\Statement as SqlStatement;
-use Amp\Sql\Transaction as SqlTransaction;
+use Amp\Sql\Pool;
+use Amp\Sql\Result;
+use Amp\Sql\SqlConnector;
+use Amp\Sql\Statement;
+use Amp\Sql\Transaction;
 use Amp\Sql\TransactionIsolation;
 use Amp\Sql\TransactionIsolationLevel;
 
+/**
+ * @extends ConnectionPool<MysqlConfig, MysqlConnection, MysqlResult, MysqlStatement, MysqlTransaction>
+ */
 final class MysqlPool extends ConnectionPool implements MysqlLink
 {
     /**
      * @param positive-int $maxConnections
      * @param positive-int $idleTimeout
+     * @param SqlConnector<MysqlConfig, MysqlConnection>|null $connector
      */
     public function __construct(
         MysqlConfig $config,
         int $maxConnections = self::DEFAULT_MAX_CONNECTIONS,
         int $idleTimeout = self::DEFAULT_IDLE_TIMEOUT,
-        ?MysqlConnector $connector = null,
+        ?SqlConnector $connector = null,
     ) {
         parent::__construct($config, $connector ?? mysqlConnector(), $maxConnections, $idleTimeout);
     }
 
-    protected function createResult(SqlResult $result, \Closure $release): MysqlResult
+    protected function createResult(Result $result, \Closure $release): MysqlResult
     {
         \assert($result instanceof MysqlResult);
         return new Internal\MysqlPooledResult($result, $release);
     }
 
-    protected function createStatement(SqlStatement $statement, \Closure $release): MysqlStatement
+    protected function createStatement(Statement $statement, \Closure $release): MysqlStatement
     {
         \assert($statement instanceof MysqlStatement);
         return new Internal\MysqlPooledStatement($statement, $release);
     }
 
-    protected function createStatementPool(SqlPool $pool, string $sql, \Closure $prepare): StatementPool
+    protected function createStatementPool(Pool $pool, string $sql, \Closure $prepare): StatementPool
     {
         return new Internal\MysqlStatementPool($pool, $sql, $prepare);
     }
 
-    protected function createTransaction(SqlTransaction $transaction, \Closure $release): MysqlTransaction
+    protected function createTransaction(Transaction $transaction, \Closure $release): MysqlTransaction
     {
         return new Internal\MysqlPooledTransaction($transaction, $release);
     }
 
     /**
      * Changes return type to this library's Result type.
-     *
-     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function query(string $sql): MysqlResult
     {
@@ -60,8 +63,6 @@ public function query(string $sql): MysqlResult
 
     /**
      * Changes return type to this library's Statement type.
-     *
-     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function prepare(string $sql): MysqlStatement
     {
@@ -70,8 +71,6 @@ public function prepare(string $sql): MysqlStatement
 
     /**
      * Changes return type to this library's Result type.
-     *
-     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function execute(string $sql, array $params = []): MysqlResult
     {
@@ -80,8 +79,6 @@ public function execute(string $sql, array $params = []): MysqlResult
 
     /**
      * Changes return type to this library's Transaction type.
-     *
-     * @psalm-suppress LessSpecificReturnStatement, MoreSpecificReturnType
      */
     public function beginTransaction(
         TransactionIsolation $isolation = TransactionIsolationLevel::Committed
diff --git a/src/MysqlResult.php b/src/MysqlResult.php
index 1e0d84a..7ed1f2a 100644
--- a/src/MysqlResult.php
+++ b/src/MysqlResult.php
@@ -6,6 +6,11 @@
 
 interface MysqlResult extends Result
 {
+    /**
+     * Changes return type to this library's Transaction type.
+     */
+    public function getNextResult(): ?self;
+
     /**
      * @return int|null Insert ID of the last auto increment row if applicable to the result or null if no ID
      *                  is available.
diff --git a/src/MysqlSocketConnection.php b/src/MysqlSocketConnection.php
new file mode 100644
index 0000000..16a994e
--- /dev/null
+++ b/src/MysqlSocketConnection.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Amp\Mysql;
+
+use Amp\Cancellation;
+use Amp\DeferredFuture;
+use Amp\Socket\EncryptableSocket;
+use Amp\Sql\TransactionIsolation;
+use Amp\Sql\TransactionIsolationLevel;
+use Revolt\EventLoop;
+
+final class MysqlSocketConnection implements MysqlConnection
+{
+    private readonly Internal\ConnectionProcessor $processor;
+
+    private ?DeferredFuture $busy = null;
+
+    /** @var \Closure():void Function used to release connection after a transaction has completed. */
+    private readonly \Closure $release;
+
+    public static function initialize(
+        EncryptableSocket $socket,
+        MysqlConfig $config,
+        ?Cancellation $cancellation = null,
+    ): self {
+        $processor = new Internal\ConnectionProcessor($socket, $config);
+        $processor->connect($cancellation);
+        return new self($processor);
+    }
+
+    private function __construct(Internal\ConnectionProcessor $processor)
+    {
+        $this->processor = $processor;
+
+        $busy = &$this->busy;
+        $this->release = static function () use (&$busy): void {
+            \assert($busy instanceof DeferredFuture);
+            $busy->complete();
+            $busy = null;
+        };
+    }
+
+    /**
+     * @return bool False if the connection has been closed.
+     */
+    public function isClosed(): bool
+    {
+        return $this->processor->isClosed();
+    }
+
+    /**
+     * @return int Timestamp of the last time this connection was used.
+     */
+    public function getLastUsedAt(): int
+    {
+        return $this->processor->getLastUsedAt();
+    }
+
+    public function setCharset(string $charset, string $collate): void
+    {
+        $this->processor->setCharset($charset, $collate)->await();
+    }
+
+    public function close(): void
+    {
+        // Send close command if connection is not already in a closed or closing state
+        if (!$this->processor->isClosed()) {
+            $this->processor->sendClose()->await();
+        }
+    }
+
+    public function onClose(\Closure $onClose): void
+    {
+        $this->processor->onClose($onClose);
+    }
+
+    public function useDb(string $db): void
+    {
+        $this->processor->useDb($db)->await();
+    }
+
+    public function query(string $sql): MysqlResult
+    {
+        while ($this->busy) {
+            $this->busy->getFuture()->await();
+        }
+
+        return $this->processor->query($sql)->await();
+    }
+
+    public function beginTransaction(
+        TransactionIsolation $isolation = TransactionIsolationLevel::Committed
+    ): MysqlTransaction {
+        while ($this->busy) {
+            $this->busy->getFuture()->await();
+        }
+
+        $this->busy = $deferred = new DeferredFuture;
+
+        try {
+            $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL " . $isolation->toSql())->await();
+            $this->processor->query("START TRANSACTION")->await();
+        } catch (\Throwable $exception) {
+            $this->busy = null;
+            $deferred->complete();
+            throw $exception;
+        }
+
+        return new Internal\MysqlConnectionTransaction($this->processor, $this->release, $isolation);
+    }
+
+    public function ping(): void
+    {
+        $this->processor->ping()->await();
+    }
+
+    public function prepare(string $sql): MysqlStatement
+    {
+        while ($this->busy) {
+            $this->busy->getFuture()->await();
+        }
+
+        return $this->processor->prepare($sql)->await();
+    }
+
+    public function execute(string $sql, array $params = []): MysqlResult
+    {
+        $statement = $this->prepare($sql);
+        return $statement->execute($params);
+    }
+
+    public function __destruct()
+    {
+        $processor = $this->processor;
+        EventLoop::queue(static fn () => $processor->unreference());
+    }
+}
diff --git a/src/MysqlTransaction.php b/src/MysqlTransaction.php
index e756c75..08ce9d6 100644
--- a/src/MysqlTransaction.php
+++ b/src/MysqlTransaction.php
@@ -4,6 +4,9 @@
 
 use Amp\Sql\Transaction;
 
+/**
+ * @extends Transaction<MysqlResult, MysqlStatement>
+ */
 interface MysqlTransaction extends MysqlExecutor, Transaction
 {
 }
diff --git a/src/DefaultMysqlConnector.php b/src/SocketMysqlConnector.php
similarity index 82%
rename from src/DefaultMysqlConnector.php
rename to src/SocketMysqlConnector.php
index 0223e9f..5355796 100644
--- a/src/DefaultMysqlConnector.php
+++ b/src/SocketMysqlConnector.php
@@ -6,7 +6,7 @@
 use Amp\Socket;
 use Amp\Sql\SqlConfig;
 
-final class DefaultMysqlConnector implements MysqlConnector
+final class SocketMysqlConnector implements MysqlConnector
 {
     public function __construct(private readonly ?Socket\SocketConnector $connector = null)
     {
@@ -21,6 +21,6 @@ public function connect(SqlConfig $config, ?Cancellation $cancellation = null):
         $socket = ($this->connector ?? Socket\socketConnector())
             ->connect($config->getConnectionString(), $config->getConnectContext(), $cancellation);
 
-        return MysqlConnection::initialize($socket, $config, $cancellation);
+        return MysqlSocketConnection::initialize($socket, $config, $cancellation);
     }
 }
diff --git a/src/functions.php b/src/functions.php
index 43ada08..30a368a 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -2,10 +2,17 @@
 
 namespace Amp\Mysql;
 
+use Amp\Sql\Common\RetrySqlConnector;
+use Amp\Sql\SqlConnector;
 use Amp\Sql\SqlException;
 use Revolt\EventLoop;
 
-function mysqlConnector(?MysqlConnector $connector = null): MysqlConnector
+/**
+ * @param SqlConnector<MysqlConfig, MysqlConnection>|null $connector
+ *
+ * @return SqlConnector<MysqlConfig, MysqlConnection>
+ */
+function mysqlConnector(?SqlConnector $connector = null): SqlConnector
 {
     static $map;
     $map ??= new \WeakMap();
@@ -15,7 +22,7 @@ function mysqlConnector(?MysqlConnector $connector = null): MysqlConnector
         return $map[$driver] = $connector;
     }
 
-    return $map[$driver] ??= new DefaultMysqlConnector;
+    return $map[$driver] ??= new RetrySqlConnector(new SocketMysqlConnector());
 }
 
 /**
diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php
index 07ae239..8f1bc29 100644
--- a/test/ConnectionTest.php
+++ b/test/ConnectionTest.php
@@ -2,7 +2,7 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Mysql\DefaultMysqlConnector;
+use Amp\Mysql\SocketMysqlConnector;
 use Amp\Mysql\MysqlConnection;
 use Amp\Mysql\MysqlLink;
 
@@ -10,19 +10,19 @@ class ConnectionTest extends LinkTest
 {
     protected function getLink(bool $useCompression = false): MysqlLink
     {
-        return (new DefaultMysqlConnector)->connect($this->getConfig($useCompression));
+        return (new SocketMysqlConnector)->connect($this->getConfig($useCompression));
     }
 
     public function testConnect()
     {
-        $connector = new DefaultMysqlConnector();
+        $connector = new SocketMysqlConnector();
 
         $db = $connector->connect($this->getConfig());
 
         $this->assertInstanceOf(MysqlConnection::class, $db);
 
         /* use an alternative charset... Default is utf8mb4_general_ci */
-        $db->setCharset("latin1_general_ci");
+        $db->setCharset("latin1", "latin1_general_ci");
 
         $db->close();
     }
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 3de9471..095b6c0 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -11,6 +11,7 @@
 use Amp\Mysql\MysqlLink;
 use Amp\Mysql\MysqlPool;
 use Amp\Mysql\MysqlResult;
+use Amp\Mysql\MysqlSocketConnection;
 use Amp\Mysql\MysqlStatement;
 use Amp\Sql\Transaction as SqlTransaction;
 use PHPUnit\Framework\MockObject\MockObject;
@@ -61,9 +62,9 @@ private function makeProcessorSet(int $count): array
 
     private function makeConnectionSet(array $processors): array
     {
-        return \array_map((function (ConnectionProcessor $processor): MysqlConnection {
+        return \array_map((function (ConnectionProcessor $processor): MysqlSocketConnection {
             return new self($processor);
-        })->bindTo(null, MysqlConnection::class), $processors);
+        })->bindTo(null, MysqlSocketConnection::class), $processors);
     }
 
     public function getConnectionCounts(): array

From cd7c4bf7f93b49742f250b8b6c7ff5679012d624 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 10 Jul 2022 12:26:54 -0500
Subject: [PATCH 080/174] Update for parameter change in amphp/sql-common

---
 src/Internal/MysqlPooledStatement.php | 2 +-
 src/MysqlPool.php                     | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/Internal/MysqlPooledStatement.php b/src/Internal/MysqlPooledStatement.php
index 6530cdd..f67d9e3 100644
--- a/src/Internal/MysqlPooledStatement.php
+++ b/src/Internal/MysqlPooledStatement.php
@@ -35,7 +35,7 @@ protected function createResult(SqlResult $result, \Closure $release): MysqlResu
      */
     public function execute(array $params = []): MysqlResult
     {
-        return $this->statement->execute($params);
+        return parent::execute($params);
     }
 
     public function bind(int|string $paramId, mixed $data): void
diff --git a/src/MysqlPool.php b/src/MysqlPool.php
index c35e984..84e96bc 100644
--- a/src/MysqlPool.php
+++ b/src/MysqlPool.php
@@ -43,9 +43,9 @@ protected function createStatement(Statement $statement, \Closure $release): Mys
         return new Internal\MysqlPooledStatement($statement, $release);
     }
 
-    protected function createStatementPool(Pool $pool, string $sql, \Closure $prepare): StatementPool
+    protected function createStatementPool(string $sql, \Closure $prepare): MysqlStatement
     {
-        return new Internal\MysqlStatementPool($pool, $sql, $prepare);
+        return new Internal\MysqlStatementPool($this, $sql, $prepare);
     }
 
     protected function createTransaction(Transaction $transaction, \Closure $release): MysqlTransaction

From 5cf6d022361e45001bd7894e6321c818ba2ca646 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 10 Jul 2022 23:30:12 -0500
Subject: [PATCH 081/174] Add Cancellation param to connect()

---
 src/MysqlPool.php       |  2 --
 src/functions.php       |  5 ++--
 test/ConnectionTest.php | 56 ++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 58 insertions(+), 5 deletions(-)

diff --git a/src/MysqlPool.php b/src/MysqlPool.php
index 84e96bc..7a7fc77 100644
--- a/src/MysqlPool.php
+++ b/src/MysqlPool.php
@@ -3,8 +3,6 @@
 namespace Amp\Mysql;
 
 use Amp\Sql\Common\ConnectionPool;
-use Amp\Sql\Common\StatementPool;
-use Amp\Sql\Pool;
 use Amp\Sql\Result;
 use Amp\Sql\SqlConnector;
 use Amp\Sql\Statement;
diff --git a/src/functions.php b/src/functions.php
index 30a368a..1766a5c 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -2,6 +2,7 @@
 
 namespace Amp\Mysql;
 
+use Amp\Cancellation;
 use Amp\Sql\Common\RetrySqlConnector;
 use Amp\Sql\SqlConnector;
 use Amp\Sql\SqlException;
@@ -31,7 +32,7 @@ function mysqlConnector(?SqlConnector $connector = null): SqlConnector
  * @throws SqlException If connecting fails.
  * @throws \Error If the connection string does not contain a host, user, and password.
  */
-function connect(MysqlConfig $config): MysqlConnection
+function connect(MysqlConfig $config, ?Cancellation $cancellation = null): MysqlConnection
 {
-    return mysqlConnector()->connect($config);
+    return mysqlConnector()->connect($config, $cancellation);
 }
diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php
index 8f1bc29..8db72ce 100644
--- a/test/ConnectionTest.php
+++ b/test/ConnectionTest.php
@@ -2,9 +2,12 @@
 
 namespace Amp\Mysql\Test;
 
-use Amp\Mysql\SocketMysqlConnector;
+use Amp\CancelledException;
+use Amp\DeferredCancellation;
 use Amp\Mysql\MysqlConnection;
 use Amp\Mysql\MysqlLink;
+use Amp\Mysql\SocketMysqlConnector;
+use function Amp\Mysql\connect;
 
 class ConnectionTest extends LinkTest
 {
@@ -27,6 +30,57 @@ public function testConnect()
         $db->close();
     }
 
+    /**
+     * @depends testConnect
+     */
+    public function testConnectCancellationBeforeConnect()
+    {
+        $this->expectException(CancelledException::class);
+
+        $connector = new SocketMysqlConnector();
+
+        $source = new DeferredCancellation;
+        $cancellation = $source->getCancellation();
+        $source->cancel();
+        $connector->connect($this->getConfig(), $cancellation);
+    }
+
+    /**
+     * @depends testConnectCancellationBeforeConnect
+     */
+    public function testConnectCancellationAfterConnect()
+    {
+        $connector = new SocketMysqlConnector();
+
+        $source = new DeferredCancellation;
+        $cancellation = $source->getCancellation();
+        $connection = $connector->connect($this->getConfig(), $cancellation);
+        $this->assertInstanceOf(MysqlConnection::class, $connection);
+        $source->cancel();
+    }
+
+    /**
+     * @depends testConnect
+     */
+    public function testConnectFunction()
+    {
+        $connection = connect($this->getConfig());
+        $this->assertInstanceOf(MysqlConnection::class, $connection);
+    }
+
+    /**
+     * @depends testConnectFunction
+     */
+    public function testCancelConnect()
+    {
+        $this->expectException(CancelledException::class);
+
+        $source = new DeferredCancellation;
+        $cancellation = $source->getCancellation();
+        $source->cancel();
+        connect($this->getConfig(), $cancellation);
+    }
+
     public function testDoubleClose()
     {
         $db = $this->getLink();

From 9b9ffeefb5fc5eb7193cf2519cd1dd8c76e734cc Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Mon, 11 Jul 2022 21:49:46 +0200
Subject: [PATCH 082/174] =?UTF-8?q?Rename=20MysqlSocketConnection=20?=
 =?UTF-8?q?=E2=86=92=20SocketMysqlConnection?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/MysqlDataType.php                                  | 10 +++++-----
 ...lSocketConnection.php => SocketMysqlConnection.php} |  2 +-
 src/SocketMysqlConnector.php                           |  2 +-
 test/PoolTest.php                                      |  6 +++---
 4 files changed, 10 insertions(+), 10 deletions(-)
 rename src/{MysqlSocketConnection.php => SocketMysqlConnection.php} (98%)

diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index 55ef7c3..a2f97ab 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -145,17 +145,17 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
                 switch ($length = \ord($string) + 1) {
                     case 12:
                         $microsecond = self::decodeUnsigned32(\substr($string, 8));
-                    // no break
+                        // no break
                     case 8:
                         $second = \ord($string[7]);
                         $minute = \ord($string[6]);
                         $hour = \ord($string[5]);
-                    // no break
+                        // no break
                     case 5:
                         $day = \ord($string[4]);
                         $month = \ord($string[3]);
                         $year = self::decodeUnsigned16(\substr($string, 1));
-                    // no break
+                        // no break
                     case 1:
                         break;
 
@@ -177,14 +177,14 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
                 switch ($length = \ord($string) + 1) {
                     case 13:
                         $microsecond = self::decodeUnsigned32(\substr($string, 9));
-                    // no break
+                        // no break
                     case 9:
                         $second = \ord($string[8]);
                         $minute = \ord($string[7]);
                         $hour = \ord($string[6]);
                         $day = self::decodeUnsigned32(\substr($string, 2));
                         $negative = \ord($string[1]);
-                    // no break
+                        // no break
                     case 1:
                         break;
 
diff --git a/src/MysqlSocketConnection.php b/src/SocketMysqlConnection.php
similarity index 98%
rename from src/MysqlSocketConnection.php
rename to src/SocketMysqlConnection.php
index 16a994e..03269d2 100644
--- a/src/MysqlSocketConnection.php
+++ b/src/SocketMysqlConnection.php
@@ -9,7 +9,7 @@
 use Amp\Sql\TransactionIsolationLevel;
 use Revolt\EventLoop;
 
-final class MysqlSocketConnection implements MysqlConnection
+final class SocketMysqlConnection implements MysqlConnection
 {
     private readonly Internal\ConnectionProcessor $processor;
 
diff --git a/src/SocketMysqlConnector.php b/src/SocketMysqlConnector.php
index 5355796..6e85236 100644
--- a/src/SocketMysqlConnector.php
+++ b/src/SocketMysqlConnector.php
@@ -21,6 +21,6 @@ public function connect(SqlConfig $config, ?Cancellation $cancellation = null):
         $socket = ($this->connector ?? Socket\socketConnector())
             ->connect($config->getConnectionString(), $config->getConnectContext(), $cancellation);
 
-        return MysqlSocketConnection::initialize($socket, $config, $cancellation);
+        return SocketMysqlConnection::initialize($socket, $config, $cancellation);
     }
 }
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 095b6c0..1163498 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -11,8 +11,8 @@
 use Amp\Mysql\MysqlLink;
 use Amp\Mysql\MysqlPool;
 use Amp\Mysql\MysqlResult;
-use Amp\Mysql\MysqlSocketConnection;
 use Amp\Mysql\MysqlStatement;
+use Amp\Mysql\SocketMysqlConnection;
 use Amp\Sql\Transaction as SqlTransaction;
 use PHPUnit\Framework\MockObject\MockObject;
 use function Amp\async;
@@ -62,9 +62,9 @@ private function makeProcessorSet(int $count): array
 
     private function makeConnectionSet(array $processors): array
     {
-        return \array_map((function (ConnectionProcessor $processor): MysqlSocketConnection {
+        return \array_map((function (ConnectionProcessor $processor): SocketMysqlConnection {
             return new self($processor);
-        })->bindTo(null, MysqlSocketConnection::class), $processors);
+        })->bindTo(null, SocketMysqlConnection::class), $processors);
     }
 
     public function getConnectionCounts(): array

From 15194a2527f8cc3444d8d25b4cf4f9409086a8a3 Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Mon, 11 Jul 2022 21:53:15 +0200
Subject: [PATCH 083/174] Extract variable to prevent php-cs-fixer no-break bug

---
 src/MysqlDataType.php | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index a2f97ab..a31d19d 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -50,7 +50,8 @@ public static function encodeBinary(mixed $param): array
     {
         switch (\get_debug_type($param)) {
             case "bool":
-                return [self::Tiny, $param ? "\x01" : "\0"];
+                $encoded = $param ? "\x01" : "\0";
+                return [self::Tiny, $encoded];
 
             case "int":
                 if ($param >= -(1 << 15) && $param < (1 << 15)) {

From e1c77168e8c27a0a6f64c36362ada893dcba8253 Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Mon, 11 Jul 2022 21:55:12 +0200
Subject: [PATCH 084/174] =?UTF-8?q?Rename=20MysqlPool=20=E2=86=92=20MysqlC?=
 =?UTF-8?q?onnectionPool?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 benchmarks/QueryBench.php                      | 2 +-
 examples/2-simple-query.php                    | 4 ++--
 examples/3-generic-with-yield.php              | 4 ++--
 examples/4-multi-rows.php                      | 4 ++--
 examples/5-multi-stmts.php                     | 4 ++--
 examples/6-transaction.php                     | 4 ++--
 src/{MysqlPool.php => MysqlConnectionPool.php} | 2 +-
 test/PoolTest.php                              | 8 ++++----
 8 files changed, 16 insertions(+), 16 deletions(-)
 rename src/{MysqlPool.php => MysqlConnectionPool.php} (97%)

diff --git a/benchmarks/QueryBench.php b/benchmarks/QueryBench.php
index 9610bc5..fdc8325 100644
--- a/benchmarks/QueryBench.php
+++ b/benchmarks/QueryBench.php
@@ -5,7 +5,7 @@
 use Amp\Mysql\SocketMysqlConnector;
 use Amp\Mysql\MysqlConnection;
 use Amp\Mysql\MysqlConfig;
-use Amp\Mysql\MysqlPool as ConnectionPool;
+use Amp\Mysql\MysqlConnectionPool as ConnectionPool;
 use Amp\Mysql\ResultSet;
 use function Amp\call;
 use function Amp\Promise\wait;
diff --git a/examples/2-simple-query.php b/examples/2-simple-query.php
index 9a488f0..16ce835 100644
--- a/examples/2-simple-query.php
+++ b/examples/2-simple-query.php
@@ -3,9 +3,9 @@
 require 'support/bootstrap.php';
 
 use Amp\Mysql\MysqlConfig;
-use Amp\Mysql\MysqlPool;
+use Amp\Mysql\MysqlConnectionPool;
 
-$db = new MysqlPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
+$db = new MysqlConnectionPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
 
 $result = $db->query("SELECT 1 AS value");
 
diff --git a/examples/3-generic-with-yield.php b/examples/3-generic-with-yield.php
index ea967b5..9d7d52d 100644
--- a/examples/3-generic-with-yield.php
+++ b/examples/3-generic-with-yield.php
@@ -4,10 +4,10 @@
 
 use Amp\Future;
 use Amp\Mysql\MysqlConfig;
-use Amp\Mysql\MysqlPool;
+use Amp\Mysql\MysqlConnectionPool;
 use function Amp\async;
 
-$db = new MysqlPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
+$db = new MysqlConnectionPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
 
 $db->query("DROP TABLE IF EXISTS tmp");
 
diff --git a/examples/4-multi-rows.php b/examples/4-multi-rows.php
index 3bc1693..0189af5 100644
--- a/examples/4-multi-rows.php
+++ b/examples/4-multi-rows.php
@@ -5,11 +5,11 @@
 
 use Amp\Future;
 use Amp\Mysql\MysqlConfig;
-use Amp\Mysql\MysqlPool;
+use Amp\Mysql\MysqlConnectionPool;
 use Amp\Mysql\MysqlResult;
 use function Amp\async;
 
-$db = new MysqlPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
+$db = new MysqlConnectionPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
 
 /* create same table than in 3-generic-with-yield.php */
 createGenericTable($db);
diff --git a/examples/5-multi-stmts.php b/examples/5-multi-stmts.php
index 4ddbf55..b4d9949 100644
--- a/examples/5-multi-stmts.php
+++ b/examples/5-multi-stmts.php
@@ -4,9 +4,9 @@
 require 'support/generic-table.php';
 
 use Amp\Mysql\MysqlConfig;
-use Amp\Mysql\MysqlPool;
+use Amp\Mysql\MysqlConnectionPool;
 
-$db = new MysqlPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
+$db = new MysqlConnectionPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
 
 /* create same table than in 3-generic-with-yield.php */
 createGenericTable($db);
diff --git a/examples/6-transaction.php b/examples/6-transaction.php
index a3cfbdb..cf05f0f 100644
--- a/examples/6-transaction.php
+++ b/examples/6-transaction.php
@@ -4,9 +4,9 @@
 require 'support/generic-table.php';
 
 use Amp\Mysql\MysqlConfig;
-use Amp\Mysql\MysqlPool;
+use Amp\Mysql\MysqlConnectionPool;
 
-$db = new MysqlPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
+$db = new MysqlConnectionPool(MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, DB_NAME));
 
 /* create same table than in 3-generic-with-yield.php */
 createGenericTable($db);
diff --git a/src/MysqlPool.php b/src/MysqlConnectionPool.php
similarity index 97%
rename from src/MysqlPool.php
rename to src/MysqlConnectionPool.php
index 7a7fc77..d43ec85 100644
--- a/src/MysqlPool.php
+++ b/src/MysqlConnectionPool.php
@@ -13,7 +13,7 @@
 /**
  * @extends ConnectionPool<MysqlConfig, MysqlConnection, MysqlResult, MysqlStatement, MysqlTransaction>
  */
-final class MysqlPool extends ConnectionPool implements MysqlLink
+final class MysqlConnectionPool extends ConnectionPool implements MysqlLink
 {
     /**
      * @param positive-int $maxConnections
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 1163498..cb2f604 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -7,9 +7,9 @@
 use Amp\Mysql\Internal\MysqlCommandResult;
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlConnection;
+use Amp\Mysql\MysqlConnectionPool;
 use Amp\Mysql\MysqlConnector;
 use Amp\Mysql\MysqlLink;
-use Amp\Mysql\MysqlPool;
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
 use Amp\Mysql\SocketMysqlConnection;
@@ -26,10 +26,10 @@ class PoolTest extends LinkTest
 {
     protected function getLink(bool $useCompression = false): MysqlLink
     {
-        return new MysqlPool($this->getConfig($useCompression));
+        return new MysqlConnectionPool($this->getConfig($useCompression));
     }
 
-    protected function createPool(array $connections): MysqlPool
+    protected function createPool(array $connections): MysqlConnectionPool
     {
         $connector = $this->createMock(MysqlConnector::class);
         $connector->method('connect')
@@ -40,7 +40,7 @@ protected function createPool(array $connections): MysqlPool
 
         $config = MysqlConfig::fromString('host=host;user=user;password=password');
 
-        return new MysqlPool($config, \count($connections), MysqlPool::DEFAULT_IDLE_TIMEOUT, $connector);
+        return new MysqlConnectionPool($config, \count($connections), MysqlConnectionPool::DEFAULT_IDLE_TIMEOUT, $connector);
     }
 
     /**

From dd7382eb389ce6fbcd677c3ad6999538385ac1b6 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 11 Jul 2022 14:55:05 -0500
Subject: [PATCH 085/174] Remove ConcurrentIterator from MysqlConnectionResult

---
 composer.json                          | 1 -
 src/Internal/MysqlConnectionResult.php | 6 ++----
 2 files changed, 2 insertions(+), 5 deletions(-)

diff --git a/composer.json b/composer.json
index ccb4978..6b51209 100644
--- a/composer.json
+++ b/composer.json
@@ -15,7 +15,6 @@
     "require": {
         "php": ">=8.1",
         "amphp/amp": "^3",
-        "amphp/pipeline": "^1",
         "amphp/socket": "^2",
         "amphp/sql": "^2",
         "amphp/sql-common": "^2"
diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index 62d60de..14d5fc9 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -5,8 +5,6 @@
 use Amp\DeferredFuture;
 use Amp\Future;
 use Amp\Mysql\MysqlResult;
-use Amp\Pipeline\ConcurrentIterableIterator;
-use Amp\Pipeline\ConcurrentIterator;
 use Revolt\EventLoop;
 use function Amp\async;
 
@@ -15,14 +13,14 @@ final class MysqlConnectionResult implements MysqlResult, \IteratorAggregate
 {
     private readonly MysqlResultProxy $result;
 
-    private readonly ConcurrentIterator $generator;
+    private readonly \Generator $generator;
 
     private ?Future $nextResult = null;
 
     public function __construct(MysqlResultProxy $result)
     {
         $this->result = $result;
-        $this->generator = new ConcurrentIterableIterator(self::iterate($result));
+        $this->generator = self::iterate($result);
     }
 
     public function getIterator(): \Traversable

From 853684f80e24fe30c585814603ad4d691722d58b Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Tue, 12 Jul 2022 22:39:38 +0200
Subject: [PATCH 086/174] Replace all with await

---
 test/PoolTest.php | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/test/PoolTest.php b/test/PoolTest.php
index cb2f604..1526aef 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -126,7 +126,7 @@ public function testConsecutiveQueries(int $count)
                 $futures[] = async(fn () => $pool->query('SQL Query'));
             }
 
-            $results = Future\all($futures);
+            $results = Future\await($futures);
 
             foreach ($results as $result) {
                 $this->assertInstanceOf(MysqlResult::class, $result);
@@ -222,7 +222,7 @@ public function testExtractConnection(int $count)
             for ($i = 0; $i < $count; ++$i) {
                 $futures[] = async(fn () => $pool->extractConnection());
             }
-            $results = Future\all($futures);
+            $results = Future\await($futures);
             foreach ($results as $result) {
                 $this->assertInstanceof(MysqlConnection::class, $result);
                 $result->query($query);
@@ -272,12 +272,12 @@ public function testConnectionClosedInPool(int $count)
             for ($i = 0; $i < $count + 1; ++$i) {
                 $futures[] = async(fn () => $pool->query($query));
             }
-            Future\all($futures);
+            Future\await($futures);
             $futures = [];
             for ($i = 0; $i < $count; ++$i) {
                 $futures[] = async(fn () => $pool->query($query));
             }
-            Future\all($futures);
+            Future\await($futures);
         } finally {
             $pool->close();
         }

From e4b0c52962bb4d78ba5a8d51226bc2c6886471be Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Tue, 12 Jul 2022 22:44:37 +0200
Subject: [PATCH 087/174] Replace all with await

---
 examples/support/generic-table.php | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/examples/support/generic-table.php b/examples/support/generic-table.php
index a618d56..ce8e4c2 100644
--- a/examples/support/generic-table.php
+++ b/examples/support/generic-table.php
@@ -2,6 +2,7 @@
 
 use Amp\Future;
 use Amp\Sql\Link;
+use function Amp\async;
 
 /* Create table and fill in a few rows for examples; for comments see 3-generic-with-yield.php */
 function createGenericTable(Link $db): void
@@ -14,8 +15,8 @@ function createGenericTable(Link $db): void
 
     $futures = [];
     foreach (\range(1, 5) as $num) {
-        $futures[] = \Amp\async(fn() => $statement->execute([$num, $num]));
+        $futures[] = async(fn () => $statement->execute([$num, $num]));
     }
 
-    Future\all($futures);
+    Future\await($futures);
 }

From caad22e7def92aacb28cce10facfa312a1c0e284 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Fri, 15 Jul 2022 16:57:42 -0500
Subject: [PATCH 088/174] Fix typo

---
 src/MysqlResult.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/MysqlResult.php b/src/MysqlResult.php
index 7ed1f2a..1fb2ca0 100644
--- a/src/MysqlResult.php
+++ b/src/MysqlResult.php
@@ -7,7 +7,7 @@
 interface MysqlResult extends Result
 {
     /**
-     * Changes return type to this library's Transaction type.
+     * Changes return type to this library's Result type.
      */
     public function getNextResult(): ?self;
 

From 287afcb57ff22924711d55046c1c0a27b4b39d5c Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Fri, 15 Jul 2022 16:58:13 -0500
Subject: [PATCH 089/174] Rename MysqlConnection methods

---
 src/Internal/ConnectionProcessor.php | 10 +++++-----
 src/MysqlConnection.php              |  4 ++--
 src/SocketMysqlConnection.php        |  8 ++++----
 test/ConnectionTest.php              |  2 +-
 4 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 246dc58..6a3066f 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -313,7 +313,7 @@ protected function startCommand(\Closure $callback): Future
         return $deferred->getFuture();
     }
 
-    public function setCharset(string $charset, string $collate): Future
+    public function useCharacterSet(string $charset, string $collate): Future
     {
         if ($collate === "" && false !== $offset = \strpos($charset, "_")) {
             $collate = $charset;
@@ -329,12 +329,12 @@ public function setCharset(string $charset, string $collate): Future
     }
 
     /** @see 14.6.3 COM_INIT_DB */
-    public function useDb(string $db): Future
+    public function useDatabase(string $database): Future
     {
-        return $this->startCommand(function () use ($db): void {
+        return $this->startCommand(function () use ($database): void {
             /** @psalm-suppress PropertyTypeCoercion */
-            $this->config = $this->config->withDatabase($db);
-            $this->sendPacket("\x02$db");
+            $this->config = $this->config->withDatabase($database);
+            $this->sendPacket("\x02$database");
         });
     }
 
diff --git a/src/MysqlConnection.php b/src/MysqlConnection.php
index bce1b13..038a1ff 100644
--- a/src/MysqlConnection.php
+++ b/src/MysqlConnection.php
@@ -7,10 +7,10 @@ interface MysqlConnection extends MysqlLink
     /**
      * Change the active database on the connection.
      */
-    public function useDb(string $db): void;
+    public function useDatabase(string $database): void;
 
     /**
      * Change the character set used by the connection.
      */
-    public function setCharset(string $charset, string $collate): void;
+    public function useCharacterSet(string $charset, string $collate): void;
 }
diff --git a/src/SocketMysqlConnection.php b/src/SocketMysqlConnection.php
index 03269d2..a08b51e 100644
--- a/src/SocketMysqlConnection.php
+++ b/src/SocketMysqlConnection.php
@@ -56,9 +56,9 @@ public function getLastUsedAt(): int
         return $this->processor->getLastUsedAt();
     }
 
-    public function setCharset(string $charset, string $collate): void
+    public function useCharacterSet(string $charset, string $collate): void
     {
-        $this->processor->setCharset($charset, $collate)->await();
+        $this->processor->useCharacterSet($charset, $collate)->await();
     }
 
     public function close(): void
@@ -74,9 +74,9 @@ public function onClose(\Closure $onClose): void
         $this->processor->onClose($onClose);
     }
 
-    public function useDb(string $db): void
+    public function useDatabase(string $database): void
     {
-        $this->processor->useDb($db)->await();
+        $this->processor->useDatabase($database)->await();
     }
 
     public function query(string $sql): MysqlResult
diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php
index 8db72ce..6a92777 100644
--- a/test/ConnectionTest.php
+++ b/test/ConnectionTest.php
@@ -25,7 +25,7 @@ public function testConnect()
         $this->assertInstanceOf(MysqlConnection::class, $db);
 
         /* use an alternative charset... Default is utf8mb4_general_ci */
-        $db->setCharset("latin1", "latin1_general_ci");
+        $db->useCharacterSet("latin1", "latin1_general_ci");
 
         $db->close();
     }

From b441a1f07ba0314afefa386edfd7482cc6355b03 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 17 Jul 2022 12:40:29 -0500
Subject: [PATCH 090/174] Add Result template type

---
 src/MysqlResult.php | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/MysqlResult.php b/src/MysqlResult.php
index 1fb2ca0..975fdf7 100644
--- a/src/MysqlResult.php
+++ b/src/MysqlResult.php
@@ -4,6 +4,9 @@
 
 use Amp\Sql\Result;
 
+/**
+ * @extends Result<bool|int|float|string|null>
+ */
 interface MysqlResult extends Result
 {
     /**

From f96012f45fb5cfe31b52aee70ad84368f0becc10 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 17 Jul 2022 13:23:59 -0500
Subject: [PATCH 091/174] Remove InitializationException

---
 src/InitializationException.php      | 9 ---------
 src/Internal/ConnectionProcessor.php | 2 +-
 2 files changed, 1 insertion(+), 10 deletions(-)
 delete mode 100644 src/InitializationException.php

diff --git a/src/InitializationException.php b/src/InitializationException.php
deleted file mode 100644
index 6b74e26..0000000
--- a/src/InitializationException.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-use Amp\Sql\SqlException;
-
-class InitializationException extends SqlException
-{
-}
diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 6a3066f..be5806e 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -657,7 +657,7 @@ private function handleError(string $packet): void
         if ($this->connectionState < ConnectionState::Ready) {
             // connection failure
             $this->close();
-            $this->getDeferred()->error(new InitializationException(\sprintf(
+            $this->getDeferred()->error(new ConnectionException(\sprintf(
                 'Could not connect to %s: %s %s',
                 $this->config->getConnectionString(),
                 $this->metadata->errorState ?? 'Unknown state',

From 3a2c329e391df63fb282a282d5eb792685d66e46 Mon Sep 17 00:00:00 2001
From: bennnjamin <10929253+bennnjamin@users.noreply.github.com>
Date: Wed, 10 Aug 2022 14:17:31 -0700
Subject: [PATCH 092/174] MySQL time value type binary decoding now properly
 increments offset (#120)

---
 src/MysqlDataType.php | 4 ++--
 test/LinkTest.php     | 8 ++++----
 test/initialize.php   | 5 +++--
 3 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index a31d19d..827caaa 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -163,7 +163,7 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
                     default:
                         throw new SqlException("Unexpected string length for date in binary protocol: " . ($length - 1));
                 }
-
+                $offset += $length;
                 return \str_pad((string) $year, 2, "0", \STR_PAD_LEFT)
                     . "-" . \str_pad((string) $month, 2, "0", \STR_PAD_LEFT)
                     . "-" . \str_pad((string) $day, 2, "0", \STR_PAD_LEFT)
@@ -192,7 +192,7 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
                     default:
                         throw new SqlException("Unexpected string length for time in binary protocol: " . ($length - 1));
                 }
-
+                $offset += $length;
                 return ($negative ? "" : "-") . \str_pad((string) $day, 2, "0", \STR_PAD_LEFT)
                     . "d " . \str_pad((string) $hour, 2, "0", \STR_PAD_LEFT)
                     . ":" . \str_pad((string) $minute, 2, "0", \STR_PAD_LEFT)
diff --git a/test/LinkTest.php b/test/LinkTest.php
index ebda979..de6b684 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -145,7 +145,7 @@ public function testPrepared()
         $stmt = $db->prepare("SELECT * FROM main WHERE a = ? OR b = ?");
         $result = $stmt->execute([1, 8]);
         $this->assertInstanceOf(MysqlResult::class, $result);
-        $this->assertSame(3, $result->getColumnCount());
+        $this->assertSame(4, $result->getColumnCount());
         $got = [];
         foreach ($result as $row) {
             $got[] = \array_values($row);
@@ -155,7 +155,7 @@ public function testPrepared()
         $stmt = $db->prepare("SELECT * FROM main WHERE a = :a OR b = ?");
         $result = $stmt->execute(["a" => 2, 5]);
         $this->assertInstanceOf(MysqlResult::class, $result);
-        $this->assertSame(3, $result->getColumnCount());
+        $this->assertSame(4, $result->getColumnCount());
         $got = [];
         foreach ($result as $row) {
             $got[] = \array_values($row);
@@ -236,9 +236,9 @@ public function testExecute()
             $got[] = \array_values($row);
         }
         $this->assertCount(2, $got);
-        $this->assertSame([[2, 2, 3], [4, 4, 5]], $got);
+        $this->assertSame([[2, 2, 3, '1970-01-01 00:00:00.00000'], [4, 4, 5, '1970-01-01 00:00:00.00000']], $got);
 
-        $result = $db->execute("INSERT INTO main (a, b) VALUES (:a, :b)", ["a" => 10, "b" => 11]);
+        $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);
         $this->assertGreaterThan(5, $result->getLastInsertId());
 
diff --git a/test/initialize.php b/test/initialize.php
index 1754e6b..c5bdab1 100644
--- a/test/initialize.php
+++ b/test/initialize.php
@@ -6,8 +6,9 @@ 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, PRIMARY KEY (id))");
-    $db->query("INSERT INTO test.main (a, b) VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)");
+    $db->query("CREATE TABLE test.main (id INT NOT NULL AUTO_INCREMENT, a INT, b INT, c DATETIME, PRIMARY KEY (id))");
+    $epoch = date('1970-01-01 00:00:00');
+    $db->query("INSERT INTO test.main (a, b, c) VALUES (1, 2, '$epoch'), (2, 3, '$epoch'), (3, 4, '$epoch'), (4, 5, '$epoch'), (5, 6, '$epoch')");
 
     $db->query("CREATE TABLE test.json (a JSON)");
     $db->query("INSERT INTO test.json VALUES ('{\"key\": \"value\"}')");

From 173e94a58be645109f63e6c58ad0fd9262a29ad7 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 26 Jul 2022 13:16:12 -0500
Subject: [PATCH 093/174] Fix example

---
 examples/4-multi-rows.php | 14 +++++---------
 1 file changed, 5 insertions(+), 9 deletions(-)

diff --git a/examples/4-multi-rows.php b/examples/4-multi-rows.php
index 0189af5..0c50e65 100644
--- a/examples/4-multi-rows.php
+++ b/examples/4-multi-rows.php
@@ -19,15 +19,11 @@
 $future[] = async(fn () => $db->query("SELECT a * b FROM tmp"));
 $future[] = async(fn () => $db->execute("SELECT POW(a, ?) AS power FROM tmp", [2]));
 
-try {
-    /**
-     * @var MysqlResult $result1
-     * @var MysqlResult $result2
-     */
-    [$result1, $result2] = Future\await($future); // Both queries execute simultaneously. Wait for both to finish here.
-} catch (\Throwable $e) {
-    var_dump($e);
-}
+/**
+ * @var MysqlResult $result1
+ * @var MysqlResult $result2
+ */
+[$result1, $result2] = Future\await($future); // Both queries execute simultaneously. Wait for both to finish here.
 
 print "Query 1 Results:" . PHP_EOL;
 foreach ($result1 as $row) {

From 37a63a1537da9fe425140083a37c659adec79fb9 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 13 Aug 2022 22:23:02 -0500
Subject: [PATCH 094/174] Refactor closing ConnectionProcessor

---
 src/Internal/ConnectionProcessor.php | 96 +++++++++++++++-------------
 src/Internal/MysqlResultProxy.php    |  7 +-
 2 files changed, 57 insertions(+), 46 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index be5806e..e70e25f 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -6,7 +6,6 @@
 use Amp\DeferredFuture;
 use Amp\File;
 use Amp\Future;
-use Amp\Mysql\InitializationException;
 use Amp\Mysql\MysqlColumnDefinition;
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlDataType;
@@ -165,7 +164,7 @@ private function ready(): void
         $this->socket->unreference();
     }
 
-    private function addDeferred(DeferredFuture $deferred): void
+    private function enqueueDeferred(DeferredFuture $deferred): void
     {
         \assert(!$this->socket->isClosed(), "The connection has been closed");
         $this->deferreds[] = $deferred;
@@ -232,17 +231,7 @@ private function read(): void
         } catch (\Throwable $exception) {
             // $exception used as previous exception below.
         } finally {
-            $this->close();
-        }
-
-        if (!empty($this->deferreds)) {
-            $exception = new ConnectionException("Connection closed unexpectedly", 0, $exception ?? null);
-
-            foreach ($this->deferreds as $deferred) {
-                $deferred->error($exception);
-            }
-
-            $this->deferreds = [];
+            $this->free($exception ?? null);
         }
     }
 
@@ -261,8 +250,9 @@ private function processData(string $data): void
         }
     }
 
-    private function getDeferred(): DeferredFuture
+    private function dequeueDeferred(): DeferredFuture
     {
+        \assert($this->deferreds, 'Pending deferred not found when shifting from pending queue');
         return \array_shift($this->deferreds);
     }
 
@@ -300,14 +290,14 @@ public function getLastUsedAt(): int
 
     protected function startCommand(\Closure $callback): Future
     {
-        if ($this->connectionState > ConnectionState::Ready) {
+        if ($this->isClosed()) {
             throw new \Error("The connection has been closed");
         }
 
         $deferred = new DeferredFuture;
         $this->appendTask(function () use ($callback, $deferred) {
             $this->seqId = $this->compressionId = -1;
-            $this->addDeferred($deferred);
+            $this->enqueueDeferred($deferred);
             $callback();
         });
         return $deferred->getFuture();
@@ -475,7 +465,7 @@ public function execute(int $stmtId, string $query, array &$params, array $prebo
             $this->query = $query;
 
             $this->resetIds();
-            $this->addDeferred($deferred);
+            $this->enqueueDeferred($deferred);
             $this->sendPacket($payload);
             // apparently LOAD DATA LOCAL INFILE requests are not supported via prepared statements
             $this->packetCallback = $this->handleExecute(...);
@@ -597,7 +587,7 @@ public function resetStmt(int $stmtId): Future
         $deferred = new DeferredFuture;
         $this->appendTask(function () use ($payload, $deferred): void {
             $this->resetIds();
-            $this->addDeferred($deferred);
+            $this->enqueueDeferred($deferred);
             $this->sendPacket($payload);
         });
         return $deferred->getFuture();
@@ -610,7 +600,7 @@ public function fetchStmt(int $stmtId): Future
         $deferred = new DeferredFuture;
         $this->appendTask(function () use ($payload, $deferred): void {
             $this->resetIds();
-            $this->addDeferred($deferred);
+            $this->enqueueDeferred($deferred);
             $this->sendPacket($payload);
         });
         return $deferred->getFuture();
@@ -656,8 +646,7 @@ private function handleError(string $packet): void
 
         if ($this->connectionState < ConnectionState::Ready) {
             // connection failure
-            $this->close();
-            $this->getDeferred()->error(new ConnectionException(\sprintf(
+            $this->free(new ConnectionException(\sprintf(
                 'Could not connect to %s: %s %s',
                 $this->config->getConnectionString(),
                 $this->metadata->errorState ?? 'Unknown state',
@@ -668,11 +657,11 @@ private function handleError(string $packet): void
 
         if ($this->result === null && empty($this->deferreds)) {
             // connection killed without pending query or active result
-            $this->close();
+            $this->free(new ConnectionException('Connection closed after receiving an unexpected error packet'));
             return;
         }
 
-        $deferred = $this->result ?? $this->getDeferred();
+        $deferred = $this->result ?? $this->dequeueDeferred();
 
         // normal error
         $exception = new QueryError(\sprintf(
@@ -754,7 +743,7 @@ private function parseOk(string $packet): void
     private function handleOk(string $packet): void
     {
         $this->parseOk($packet);
-        $this->getDeferred()->complete(new MysqlCommandResult($this->metadata->affectedRows, $this->metadata->insertId));
+        $this->dequeueDeferred()->complete(new MysqlCommandResult($this->metadata->affectedRows, $this->metadata->insertId));
         $this->ready();
     }
 
@@ -772,7 +761,7 @@ private function handleEof(string $packet): void
     {
         $this->parseEof($packet);
         $exception = new SqlException($this->metadata->errorMsg ?? 'Unknown error', $this->metadata->errorCode ?? 0);
-        $this->getDeferred()->error($exception);
+        $this->dequeueDeferred()->error($exception);
         $this->ready();
     }
 
@@ -854,7 +843,7 @@ private function handleLocalInfileRequest(string $packet): void
                 }
                 $this->sendPacket("");
             } catch (\Throwable $e) {
-                $this->getDeferred()->error(new ConnectionException("Failed to transfer a file to the server", 0, $e));
+                $this->dequeueDeferred()->error(new ConnectionException("Failed to transfer a file to the server", 0, $e));
             }
         });
     }
@@ -870,13 +859,13 @@ private function handleQuery(string $packet): void
                     $result = new MysqlResultProxy;
                     $result->affectedRows = $this->metadata->affectedRows;
                     $result->insertId = $this->metadata->insertId;
-                    $this->getDeferred()->complete(new MysqlConnectionResult($result));
+                    $this->dequeueDeferred()->complete(new MysqlConnectionResult($result));
                     $this->result = $result;
                     $result->updateState(MysqlResultProxy::COLUMNS_FETCHED);
                     $this->successfulResultFetch();
                 } else {
                     $this->parseCallback = null;
-                    $this->getDeferred()->complete(new MysqlCommandResult(
+                    $this->dequeueDeferred()->complete(new MysqlCommandResult(
                         $this->metadata->affectedRows,
                         $this->metadata->insertId
                     ));
@@ -887,7 +876,7 @@ private function handleQuery(string $packet): void
                 if ($this->config->isLocalInfileEnabled()) {
                     $this->handleLocalInfileRequest($packet);
                 } else {
-                    $this->getDeferred()->error(new ConnectionException("Unexpected LOCAL_INFILE_REQUEST packet"));
+                    $this->dequeueDeferred()->error(new ConnectionException("Unexpected LOCAL_INFILE_REQUEST packet"));
                 }
                 return;
             case self::ERR_PACKET:
@@ -896,7 +885,7 @@ private function handleQuery(string $packet): void
         }
 
         $this->parseCallback = $this->handleTextColumnDefinition(...);
-        $this->getDeferred()->complete(new MysqlConnectionResult($result = new MysqlResultProxy));
+        $this->dequeueDeferred()->complete(new MysqlConnectionResult($result = new MysqlResultProxy));
         /* we need to resolve before assigning vars, so that a onResolve() handler won't have a partial result available */
         $this->result = $result;
         $result->setColumns(MysqlDataType::decodeUnsigned($packet));
@@ -906,7 +895,7 @@ private function handleQuery(string $packet): void
     private function handleExecute(string $packet): void
     {
         $this->parseCallback = $this->handleBinaryColumnDefinition(...);
-        $this->getDeferred()->complete(new MysqlConnectionResult($result = new MysqlResultProxy));
+        $this->dequeueDeferred()->complete(new MysqlConnectionResult($result = new MysqlResultProxy));
         /* we need to resolve before assigning vars, so that a onResolve() handler won't have a partial result available */
         $this->result = $result;
         $result->setColumns(\ord($packet));
@@ -920,11 +909,11 @@ private function handleFieldList(string $packet): void
         } elseif (\ord($packet) === self::EOF_PACKET) {
             $this->parseCallback = null;
             $this->parseEof($packet);
-            $this->getDeferred()->complete(null);
+            $this->dequeueDeferred()->complete();
             $this->ready();
         } else {
-            $this->addDeferred($deferred = new DeferredFuture);
-            $this->getDeferred()->complete([$this->parseColumnDefinition($packet), $deferred]);
+            $this->enqueueDeferred($deferred = new DeferredFuture);
+            $this->dequeueDeferred()->complete([$this->parseColumnDefinition($packet), $deferred]);
         }
     }
 
@@ -1062,7 +1051,7 @@ private function successfulResultFetch(): void
 
         if ($this->metadata->statusFlags & self::SERVER_MORE_RESULTS_EXISTS) {
             $this->parseCallback = $this->handleQuery(...);
-            $this->addDeferred($deferred);
+            $this->enqueueDeferred($deferred);
         } else {
             $this->parseCallback = null;
             $this->query = null;
@@ -1102,7 +1091,10 @@ private function handleTextResultSetRow(string $packet): void
                 $fields[] = null;
                 $offset += 1;
             } else {
-                $column = $this->result->columns[$i] ?? throw new \RuntimeException("Definition missing for column $i");
+                $column = $this->result->columns[$i] ?? null;
+                if (!$column) {
+                    throw new \RuntimeException("Definition missing for column $i");
+                }
                 $fields[] = $column->type->decodeText($packet, $offset, $column->flags);
             }
         }
@@ -1174,7 +1166,7 @@ private function handlePrepare(string $packet): void
         $this->result->columnCount = $columns;
         $this->refcount++;
         \assert($this->query !== null, 'Invalid value for connection query');
-        $this->getDeferred()->complete(new MysqlConnectionStatement($this, $this->query, $stmtId, $this->named, $this->result));
+        $this->dequeueDeferred()->complete(new MysqlConnectionStatement($this, $this->query, $stmtId, $this->named, $this->result));
         $this->named = [];
         if ($params) {
             $this->parseCallback = $this->prepareParams(...);
@@ -1185,7 +1177,7 @@ private function handlePrepare(string $packet): void
 
     private function readStatistics(string $packet): void
     {
-        $this->getDeferred()->complete($packet);
+        $this->dequeueDeferred()->complete($packet);
         $this->parseCallback = null;
         $this->ready();
     }
@@ -1193,19 +1185,38 @@ private function readStatistics(string $packet): void
     /** @see 14.6.2 COM_QUIT */
     public function sendClose(): Future
     {
-        return $this->startCommand(function () {
+        return $this->startCommand(function (): void {
             $this->sendPacket("\x01");
             $this->connectionState = ConnectionState::Closing;
-        })->finally(fn () => $this->close());
+        });
     }
 
     public function close(): void
     {
-        if ($this->connectionState === ConnectionState::Closing && $this->deferreds) {
+        $this->free();
+    }
+
+    private function free(?\Throwable $exception = null): void
+    {
+        if ($this->connectionState === ConnectionState::Closing) {
+            \assert($this->deferreds, 'Closing deferred not found in array when in closing state');
             \array_pop($this->deferreds)->complete();
         }
 
         $this->connectionState = ConnectionState::Closed;
+
+        if ($this->deferreds || $this->result) {
+            $exception = new ConnectionException("Connection closed unexpectedly", 0, $exception ?? null);
+
+            foreach ($this->deferreds as $deferred) {
+                $deferred->error($exception);
+            }
+
+            $this->deferreds = [];
+
+            $this->result?->error($exception);
+        }
+
         $this->socket->close();
         $this->processors = [];
     }
@@ -1524,8 +1535,7 @@ private function sendHandshake(bool $inSSL = false): void
 
                     $this->sendHandshake(true);
                 } catch (\Throwable $e) {
-                    $this->close();
-                    $this->getDeferred()->error($e);
+                    $this->free($e);
                 }
             });
 
diff --git a/src/Internal/MysqlResultProxy.php b/src/Internal/MysqlResultProxy.php
index 91d2431..317b4bc 100644
--- a/src/Internal/MysqlResultProxy.php
+++ b/src/Internal/MysqlResultProxy.php
@@ -73,13 +73,14 @@ public function rowFetched(?array $row): void
 
     public function error(\Throwable $e): void
     {
-        foreach ($this->deferreds as $state) {
-            foreach ($this->deferreds[$state] as [$deferred]) {
+        foreach ($this->deferreds as $deferreds) {
+            foreach ($deferreds as [$deferred]) {
                 \assert($deferred instanceof DeferredFuture);
                 $deferred->error($e);
             }
-            $this->deferreds[$state] = [];
         }
+
+        $this->deferreds = [];
     }
 
     /**

From 39e9f223f75b496bf3d1a861c8f6b11f829ce734 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 14 Aug 2022 09:40:07 -0500
Subject: [PATCH 095/174] Use queues instead of arrays

---
 src/Internal/ConnectionProcessor.php | 59 ++++++++++++++++------------
 1 file changed, 33 insertions(+), 26 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index e70e25f..9ac80b4 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -28,7 +28,7 @@ class ConnectionProcessor implements TransientResource
 ~(["'`])(?:\\(?:\\|\1)|(?!\1).)*+\1(*SKIP)(*FAIL)|(\?)|:([a-zA-Z_][a-zA-Z0-9_]*)~ms
 REGEX;
 
-    /** @var \Generator[] */
+    /** @var list<\Generator> */
     private array $processors = [];
 
     private int $seqId = -1;
@@ -37,18 +37,20 @@ class ConnectionProcessor implements TransientResource
     private readonly EncryptableSocket $socket;
 
     private ?string $query = null;
-    public array $named = [];
+    private array $named = [];
 
     private ?\Closure $parseCallback = null;
     private ?\Closure $packetCallback = null;
 
-    public MysqlConfig $config;
+    private MysqlConfig $config;
 
-    /** @var DeferredFuture[] */
-    private array $deferreds = [];
+    private readonly MysqlConnectionMetdata $metadata;
 
-    /** @var array<int, \Closure():void> */
-    private array $onReady = [];
+    /** @var \SplQueue<DeferredFuture> */
+    private readonly \SplQueue $deferreds;
+
+    /** @var \SplQueue<\Closure():void> */
+    private readonly \SplQueue $onReady;
 
     private ?MysqlResultProxy $result = null;
 
@@ -59,7 +61,6 @@ class ConnectionProcessor implements TransientResource
     private int $capabilities = 0;
     private int $serverCapabilities = 0;
     private string $authPluginName = '';
-    private MysqlConnectionMetdata $metadata;
     private int $refcount = 1;
 
     private ConnectionState $connectionState = ConnectionState::Unconnected;
@@ -113,6 +114,9 @@ public function __construct(EncryptableSocket $socket, MysqlConfig $config)
         $this->metadata = new MysqlConnectionMetdata();
         $this->config = $config;
         $this->lastUsedAt = \time();
+
+        $this->deferreds = new \SplQueue();
+        $this->onReady = new \SplQueue();
     }
 
     public function __destruct()
@@ -151,12 +155,12 @@ public function unreference(): void
 
     private function ready(): void
     {
-        if (!empty($this->deferreds)) {
+        if (!$this->deferreds->isEmpty()) {
             return;
         }
 
-        if (!empty($this->onReady)) {
-            \array_shift($this->onReady)();
+        if (!$this->onReady->isEmpty()) {
+            $this->onReady->shift()();
             return;
         }
 
@@ -167,7 +171,7 @@ private function ready(): void
     private function enqueueDeferred(DeferredFuture $deferred): void
     {
         \assert(!$this->socket->isClosed(), "The connection has been closed");
-        $this->deferreds[] = $deferred;
+        $this->deferreds->push($deferred);
         $this->socket->reference();
     }
 
@@ -175,7 +179,7 @@ public function connect(?Cancellation $cancellation = null): void
     {
         \assert(!$this->processors, self::class."::connect() must not be called twice");
 
-        $this->deferreds[] = $deferred = new DeferredFuture; // Will be resolved in sendHandshake().
+        $this->enqueueDeferred($deferred = new DeferredFuture()); // Will be resolved in sendHandshake().
 
         $this->processors = [$this->parseMysql()];
 
@@ -252,8 +256,8 @@ private function processData(string $data): void
 
     private function dequeueDeferred(): DeferredFuture
     {
-        \assert($this->deferreds, 'Pending deferred not found when shifting from pending queue');
-        return \array_shift($this->deferreds);
+        \assert(!$this->deferreds->isEmpty(), 'Pending deferred not found when shifting from pending queue');
+        return $this->deferreds->shift();
     }
 
     /**
@@ -263,11 +267,11 @@ private function appendTask(\Closure $callback): void
     {
         if ($this->packetCallback
             || $this->parseCallback
-            || !empty($this->onReady)
-            || !empty($this->deferreds)
+            || !$this->onReady->isEmpty()
+            || !$this->deferreds->isEmpty()
             || $this->connectionState !== ConnectionState::Ready
         ) {
-            $this->onReady[] = $callback;
+            $this->onReady->push($callback);
         } else {
             $callback();
         }
@@ -655,7 +659,7 @@ private function handleError(string $packet): void
             return;
         }
 
-        if ($this->result === null && empty($this->deferreds)) {
+        if ($this->result === null && $this->deferreds->isEmpty()) {
             // connection killed without pending query or active result
             $this->free(new ConnectionException('Connection closed after receiving an unexpected error packet'));
             return;
@@ -673,6 +677,7 @@ private function handleError(string $packet): void
 
         $this->result = null;
         $this->query = null;
+        $this->named = [];
 
         $deferred->error($exception);
 
@@ -1199,21 +1204,19 @@ public function close(): void
     private function free(?\Throwable $exception = null): void
     {
         if ($this->connectionState === ConnectionState::Closing) {
-            \assert($this->deferreds, 'Closing deferred not found in array when in closing state');
-            \array_pop($this->deferreds)->complete();
+            \assert(!$this->deferreds->isEmpty(), 'Closing deferred not found in array when in closing state');
+            $this->deferreds->pop()->complete();
         }
 
         $this->connectionState = ConnectionState::Closed;
 
-        if ($this->deferreds || $this->result) {
+        if (!$this->deferreds->isEmpty() || $this->result) {
             $exception = new ConnectionException("Connection closed unexpectedly", 0, $exception ?? null);
 
-            foreach ($this->deferreds as $deferred) {
-                $deferred->error($exception);
+            while (!$this->deferreds->isEmpty()) {
+                $this->deferreds->shift()->error($exception);
             }
 
-            $this->deferreds = [];
-
             $this->result?->error($exception);
         }
 
@@ -1554,15 +1557,19 @@ private function sendHandshake(bool $inSSL = false): void
         } else {
             $payload .= "$auth\0";
         }
+
         if ($this->capabilities & self::CLIENT_CONNECT_WITH_DB) {
             $payload .= "{$this->config->getDatabase()}\0";
         }
+
         if ($this->capabilities & self::CLIENT_PLUGIN_AUTH) {
             $payload .= "{$this->authPluginName}\0";
         }
+
         if ($this->capabilities & self::CLIENT_CONNECT_ATTRS) {
             // connection attributes?! 5.6.6+ only!
         }
+
         $this->write($payload);
     }
 

From 7e3cadaefdb7f8dcb41455b0aac8a770cbc9f9e4 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 14 Aug 2022 09:41:19 -0500
Subject: [PATCH 096/174] Port statement regex from postgres library

---
 src/Internal/ConnectionProcessor.php | 27 ++++++++++++++++++++++-----
 1 file changed, 22 insertions(+), 5 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 9ac80b4..bf3dbfe 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -25,8 +25,24 @@
 class ConnectionProcessor implements TransientResource
 {
     const STATEMENT_PARAM_REGEX = <<<'REGEX'
-~(["'`])(?:\\(?:\\|\1)|(?!\1).)*+\1(*SKIP)(*FAIL)|(\?)|:([a-zA-Z_][a-zA-Z0-9_]*)~ms
-REGEX;
+        [
+            # Skip all quoted groups.
+            (['"])(?:\\(?:\\|\1)|(?!\1).)*+\1(*SKIP)(*FAIL)
+            |
+            # Unnamed parameters.
+            (?<unnamed>
+                # Match all question marks except those surrounded by "operator"-class characters on either side.
+                (?<!(?<operators>[-+\\*/<>~!@#%^&|`?]))
+                \?
+                (?!\g<operators>|=)
+                |
+                :\?
+            )
+            |
+            # Named parameters.
+            (?<!:):(?<named>[a-zA-Z_][a-zA-Z0-9_]*)
+        ]msxS
+        REGEX;
 
     /** @var list<\Generator> */
     private array $processors = [];
@@ -349,14 +365,15 @@ public function prepare(string $query): Future
             $this->query = $query;
             $this->parseCallback = $this->handlePrepare(...);
 
-            $query = \preg_replace_callback(self::STATEMENT_PARAM_REGEX, function ($m): string {
+            $query = \preg_replace_callback(self::STATEMENT_PARAM_REGEX, function (array $m): string {
                 static $index = 0;
-                if ($m[2] !== "?") {
-                    $this->named[$m[3]][] = $index;
+                if (isset($m['named'])) {
+                    $this->named[$m['named']][] = $index;
                 }
                 $index++;
                 return "?";
             }, $query);
+
             $this->sendPacket("\x16$query");
         });
     }

From 9412e74510b56339bccbe62f102ddf0ffb729c53 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 14 Aug 2022 10:26:34 -0500
Subject: [PATCH 097/174] Add error flag in result proxy to forward connection
 failures to result set

---
 src/Internal/ConnectionProcessor.php   | 18 ++++++------------
 src/Internal/MysqlConnectionResult.php |  6 +++++-
 src/Internal/MysqlResultProxy.php      |  4 ++++
 3 files changed, 15 insertions(+), 13 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index bf3dbfe..ae53288 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -162,11 +162,11 @@ public function isReady(): bool
 
     public function unreference(): void
     {
-        if (!--$this->refcount) {
-            $this->appendTask(function () {
-                $this->close();
-            });
+        if (--$this->refcount || $this->isClosed()) {
+            return;
         }
+
+        $this->sendClose()->ignore();
     }
 
     private function ready(): void
@@ -1066,10 +1066,7 @@ private function successfulResultFetch(): void
         \assert($this->result !== null, 'Connection result was in invalid state');
 
         $result = $this->result;
-        $deferred = &$result->next;
-        if (!$deferred) {
-            $deferred = new DeferredFuture;
-        }
+        $deferred = $result->next ??= new DeferredFuture();
 
         if ($this->metadata->statusFlags & self::SERVER_MORE_RESULTS_EXISTS) {
             $this->parseCallback = $this->handleQuery(...);
@@ -1113,10 +1110,7 @@ private function handleTextResultSetRow(string $packet): void
                 $fields[] = null;
                 $offset += 1;
             } else {
-                $column = $this->result->columns[$i] ?? null;
-                if (!$column) {
-                    throw new \RuntimeException("Definition missing for column $i");
-                }
+                $column = $this->result->columns[$i] ?? throw new \RuntimeException("Definition missing for column $i");
                 $fields[] = $column->type->decodeText($packet, $offset, $column->flags);
             }
         }
diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index 14d5fc9..2bfdccb 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -78,6 +78,10 @@ private static function fetchRow(MysqlResultProxy $result): Future
             return Future::complete();
         }
 
+        if ($result->exception) {
+            throw $result->exception;
+        }
+
         $deferred = new DeferredFuture;
 
         /* We need to increment the internal counter, else the next time fetch is called,
@@ -99,7 +103,7 @@ public function getNextResult(): ?MysqlResult
         }
 
         $this->nextResult = async(function (): ?MysqlResult {
-            $deferred = $this->result->next ?: $this->result->next = new DeferredFuture;
+            $deferred = $this->result->next ??= new DeferredFuture;
             $result = $deferred->getFuture()->await();
 
             if ($result instanceof MysqlResultProxy) {
diff --git a/src/Internal/MysqlResultProxy.php b/src/Internal/MysqlResultProxy.php
index 317b4bc..05bb977 100644
--- a/src/Internal/MysqlResultProxy.php
+++ b/src/Internal/MysqlResultProxy.php
@@ -37,6 +37,8 @@ final class MysqlResultProxy
 
     public const SINGLE_ROW_FETCH = 255;
 
+    public ?\Throwable $exception = null;
+
     public function setColumns(int $columns): void
     {
         $this->columnCount = $this->columnsToFetch = $columns;
@@ -73,6 +75,8 @@ public function rowFetched(?array $row): void
 
     public function error(\Throwable $e): void
     {
+        $this->exception = $e;
+
         foreach ($this->deferreds as $deferreds) {
             foreach ($deferreds as [$deferred]) {
                 \assert($deferred instanceof DeferredFuture);

From 8482a6875fe57bf6df3784b8e60b2bf375dd1957 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 14 Aug 2022 13:05:28 -0500
Subject: [PATCH 098/174] Refactor connection results

Using amphp/pipeline to simplify iterating result set rows.
---
 composer.json                             |   1 +
 src/Internal/ConnectionProcessor.php      |  37 +++----
 src/Internal/MysqlConnectionResult.php    |  97 +++++-------------
 src/Internal/MysqlConnectionStatement.php |  12 +--
 src/Internal/MysqlResultProxy.php         | 115 ++++++++++------------
 src/Internal/MysqlResultProxyState.php    |  10 ++
 test/LinkTest.php                         |  16 +++
 test/initialize.php                       |   3 +-
 8 files changed, 123 insertions(+), 168 deletions(-)
 create mode 100644 src/Internal/MysqlResultProxyState.php

diff --git a/composer.json b/composer.json
index 6b51209..ccb4978 100644
--- a/composer.json
+++ b/composer.json
@@ -15,6 +15,7 @@
     "require": {
         "php": ">=8.1",
         "amphp/amp": "^3",
+        "amphp/pipeline": "^1",
         "amphp/socket": "^2",
         "amphp/sql": "^2",
         "amphp/sql-common": "^2"
diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index ae53288..12517d5 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -878,12 +878,12 @@ private function handleQuery(string $packet): void
                 $this->parseOk($packet);
 
                 if ($this->metadata->statusFlags & self::SERVER_MORE_RESULTS_EXISTS) {
-                    $result = new MysqlResultProxy;
-                    $result->affectedRows = $this->metadata->affectedRows;
-                    $result->insertId = $this->metadata->insertId;
-                    $this->dequeueDeferred()->complete(new MysqlConnectionResult($result));
-                    $this->result = $result;
-                    $result->updateState(MysqlResultProxy::COLUMNS_FETCHED);
+                    $this->result = new MysqlResultProxy(
+                        affectedRows: $this->metadata->affectedRows,
+                        insertId: $this->metadata->insertId
+                    );
+                    $this->result->updateState(MysqlResultProxyState::ColumnsFetched);
+                    $this->dequeueDeferred()->complete(new MysqlConnectionResult($this->result));
                     $this->successfulResultFetch();
                 } else {
                     $this->parseCallback = null;
@@ -907,20 +907,16 @@ private function handleQuery(string $packet): void
         }
 
         $this->parseCallback = $this->handleTextColumnDefinition(...);
-        $this->dequeueDeferred()->complete(new MysqlConnectionResult($result = new MysqlResultProxy));
-        /* we need to resolve before assigning vars, so that a onResolve() handler won't have a partial result available */
-        $this->result = $result;
-        $result->setColumns(MysqlDataType::decodeUnsigned($packet));
+        $this->result = new MysqlResultProxy(MysqlDataType::decodeUnsigned($packet));
+        $this->dequeueDeferred()->complete(new MysqlConnectionResult($this->result));
     }
 
     /** @see 14.7.1 Binary Protocol Resultset */
     private function handleExecute(string $packet): void
     {
         $this->parseCallback = $this->handleBinaryColumnDefinition(...);
-        $this->dequeueDeferred()->complete(new MysqlConnectionResult($result = new MysqlResultProxy));
-        /* we need to resolve before assigning vars, so that a onResolve() handler won't have a partial result available */
-        $this->result = $result;
-        $result->setColumns(\ord($packet));
+        $this->result = new MysqlResultProxy(\ord($packet));
+        $this->dequeueDeferred()->complete(new MysqlConnectionResult($this->result));
     }
 
     private function handleFieldList(string $packet): void
@@ -954,7 +950,7 @@ private function handleColumnDefinition(string $packet, \Closure $parseCallback)
         \assert($this->result !== null, 'Connection result was in invalid state');
 
         if (!$this->result->columnsToFetch--) {
-            $this->result->updateState(MysqlResultProxy::COLUMNS_FETCHED);
+            $this->result->updateState(MysqlResultProxyState::ColumnsFetched);
             if (\ord($packet) === self::ERR_PACKET) {
                 $this->parseCallback = null;
                 $this->handleError($packet);
@@ -1000,7 +996,7 @@ private function prepareFields(string $packet): void
             $result = $this->result;
             $this->result = null;
             $this->ready();
-            $result->updateState(MysqlResultProxy::COLUMNS_FETCHED);
+            $result->updateState(MysqlResultProxyState::ColumnsFetched);
 
             return;
         }
@@ -1074,12 +1070,13 @@ private function successfulResultFetch(): void
         } else {
             $this->parseCallback = null;
             $this->query = null;
-            $this->result = null;
             $deferred->complete();
             $this->ready();
         }
 
-        $result->updateState(MysqlResultProxy::ROWS_FETCHED);
+        $this->result = null;
+
+        $result->updateState(MysqlResultProxyState::Complete);
     }
 
     /** @see 14.6.4.1.1.3 Resultset Row */
@@ -1177,9 +1174,7 @@ private function handlePrepare(string $packet): void
 
         $this->metadata->warnings = MysqlDataType::decodeUnsigned16($packet, $offset);
 
-        $this->result = new MysqlResultProxy;
-        $this->result->columnsToFetch = $params;
-        $this->result->columnCount = $columns;
+        $this->result = new MysqlResultProxy($columns, $params);
         $this->refcount++;
         \assert($this->query !== null, 'Invalid value for connection query');
         $this->dequeueDeferred()->complete(new MysqlConnectionStatement($this, $this->query, $stmtId, $this->named, $this->result));
diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index 2bfdccb..3aff048 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -13,47 +13,40 @@ final class MysqlConnectionResult implements MysqlResult, \IteratorAggregate
 {
     private readonly MysqlResultProxy $result;
 
-    private readonly \Generator $generator;
+    private readonly \Generator $iterator;
 
     private ?Future $nextResult = null;
 
     public function __construct(MysqlResultProxy $result)
     {
         $this->result = $result;
-        $this->generator = self::iterate($result);
-    }
-
-    public function getIterator(): \Traversable
-    {
-        return $this->generator;
+        $this->iterator = self::iterate($result);
     }
 
     private static function iterate(MysqlResultProxy $result): \Generator
     {
-        $next = self::fetchRow($result);
-
-        try {
-            if (!($row = $next->await())) {
-                return;
-            }
+        if (!$result->rowIterator->continue()) {
+            return;
+        }
 
-            // Column names are only available once a result row has been fetched.
-            $columnNames = \array_column($result->columns, 'name');
+        // Column names are only available once a result row has been fetched.
+        $columnNames = \array_column($result->columns, 'name');
 
-            do {
-                $next = self::fetchRow($result);
-                yield \array_combine($columnNames, $row);
-            } while ($row = $next->await());
-        } finally {
-            if (!isset($row)) {
-                return; // Result fully consumed.
-            }
+        do {
+            $row = $result->rowIterator->getValue();
+            yield \array_combine($columnNames, $row);
+        } while ($result->rowIterator->continue());
+    }
 
-            EventLoop::queue(static function () use ($next, $result): void {
+    public function __destruct()
+    {
+        if ($this->iterator->valid()) {
+            $iterator = $this->iterator;
+            EventLoop::queue(static function () use ($iterator): void {
                 try {
-                    // Discard remaining results if disposed.
-                    while ($next->await()) {
-                        $next = self::fetchRow($result);
+                    // Discard remaining rows in the result set.
+                    while ($iterator->valid()) {
+                        $iterator->next();
                     }
                 } catch (\Throwable) {
                     // Ignore errors while discarding result.
@@ -62,47 +55,15 @@ private static function iterate(MysqlResultProxy $result): \Generator
         }
     }
 
-    /**
-     * @return Future<array|null>
-     */
-    private static function fetchRow(MysqlResultProxy $result): Future
+    public function getIterator(): \Traversable
     {
-        if ($result->userFetched < $result->fetchedRows) {
-            $row = $result->rows[$result->userFetched];
-            unset($result->rows[$result->userFetched]);
-            $result->userFetched++;
-            return Future::complete($row);
-        }
-
-        if ($result->state === MysqlResultProxy::ROWS_FETCHED) {
-            return Future::complete();
-        }
-
-        if ($result->exception) {
-            throw $result->exception;
-        }
-
-        $deferred = new DeferredFuture;
-
-        /* We need to increment the internal counter, else the next time fetch is called,
-         * it'll simply return the row we fetch here instead of fetching a new row
-         * since callback order on promises isn't defined, we can't do this via onResolve() */
-        $incRow = static function (?array $row) use ($result): ?array {
-            unset($result->rows[$result->userFetched++]);
-            return $row;
-        };
-
-        $result->deferreds[MysqlResultProxy::UNFETCHED][] = [$deferred, null, $incRow];
-        return $deferred->getFuture();
+        // Using a Generator to keep a reference to $this.
+        yield from $this->iterator;
     }
 
     public function getNextResult(): ?MysqlResult
     {
-        if ($this->nextResult) {
-            return $this->nextResult->await();
-        }
-
-        $this->nextResult = async(function (): ?MysqlResult {
+        $this->nextResult ??= async(function (): ?MysqlResult {
             $deferred = $this->result->next ??= new DeferredFuture;
             $result = $deferred->getFuture()->await();
 
@@ -121,7 +82,7 @@ public function getRowCount(): ?int
         return $this->result->affectedRows;
     }
 
-    public function getColumnCount(): ?int
+    public function getColumnCount(): int
     {
         return $this->result->columnCount;
     }
@@ -133,12 +94,6 @@ public function getLastInsertId(): ?int
 
     public function getColumnDefinitions(): ?array
     {
-        if ($this->result->state >= MysqlResultProxy::COLUMNS_FETCHED) {
-            return $this->result->columns;
-        }
-
-        $deferred = new DeferredFuture;
-        $this->result->deferreds[MysqlResultProxy::COLUMNS_FETCHED][] = [$deferred, &$this->result->columns, null];
-        return $deferred->getFuture()->await();
+        return $this->result->getColumnDefinitions();
     }
 }
diff --git a/src/Internal/MysqlConnectionStatement.php b/src/Internal/MysqlConnectionStatement.php
index 5434069..c1baa4f 100644
--- a/src/Internal/MysqlConnectionStatement.php
+++ b/src/Internal/MysqlConnectionStatement.php
@@ -159,17 +159,7 @@ public function reset(): void
 
     public function getColumnDefinitions(): ?array
     {
-        if ($this->result->state >= MysqlResultProxy::COLUMNS_FETCHED) {
-            return $this->result->columns;
-        }
-
-        if (isset($this->result->deferreds[MysqlResultProxy::COLUMNS_FETCHED][0])) {
-            return $this->result->deferreds[MysqlResultProxy::COLUMNS_FETCHED][0][0]->promise();
-        }
-
-        $deferred = new DeferredFuture;
-        $this->result->deferreds[MysqlResultProxy::COLUMNS_FETCHED][0] = [$deferred, &$this->result->columns, null];
-        return $deferred->getFuture()->await();
+        return $this->result->getColumnDefinitions();
     }
 
     public function getLastUsedAt(): int
diff --git a/src/Internal/MysqlResultProxy.php b/src/Internal/MysqlResultProxy.php
index 05bb977..139ce82 100644
--- a/src/Internal/MysqlResultProxy.php
+++ b/src/Internal/MysqlResultProxy.php
@@ -4,101 +4,88 @@
 
 use Amp\DeferredFuture;
 use Amp\Mysql\MysqlColumnDefinition;
+use Amp\Pipeline\ConcurrentIterator;
+use Amp\Pipeline\Queue;
 
 /** @internal */
 final class MysqlResultProxy
 {
-    public int $columnCount = 0;
+    public int $columnsToFetch = 0;
+
     /** @var list<MysqlColumnDefinition> */
     public array $columns = [];
+
     public array $params = [];
-    public int $columnsToFetch = 0;
-    public array $rows = [];
-    public int $fetchedRows = 0;
-    public int $userFetched = 0;
 
-    public ?int $insertId = null;
+    private readonly Queue $rowQueue;
 
-    public ?int $affectedRows = null;
+    public readonly ConcurrentIterator $rowIterator;
 
-    public array $deferreds = [
-        self::UNFETCHED => [],
-        self::COLUMNS_FETCHED => [],
-        self::ROWS_FETCHED => []
-    ];
+    private ?DeferredFuture $columnDeferred = null;
 
-    public int $state = self::UNFETCHED;
+    private MysqlResultProxyState $state = MysqlResultProxyState::Initial;
 
     public ?DeferredFuture $next = null;
 
-    public const UNFETCHED = 0;
-    public const COLUMNS_FETCHED = 1;
-    public const ROWS_FETCHED = 2;
+    public function __construct(
+        public readonly int $columnCount = 0,
+        ?int $columnsToFetch = null,
+        public readonly ?int $affectedRows = null,
+        public readonly ?int $insertId = null,
+    ) {
+        $this->rowQueue = new Queue();
+        $this->rowIterator = $this->rowQueue->iterate();
 
-    public const SINGLE_ROW_FETCH = 255;
-
-    public ?\Throwable $exception = null;
-
-    public function setColumns(int $columns): void
-    {
-        $this->columnCount = $this->columnsToFetch = $columns;
+        $this->columnsToFetch = $columnsToFetch ?? $this->columnCount;
     }
 
-    public function updateState(int $state): void
+    public function getColumnDefinitions(): array
     {
-        $this->state = $state;
-        if ($state === self::ROWS_FETCHED) {
-            $this->rowFetched(null);
-        }
-        if (empty($this->deferreds[$state])) {
-            return;
+        if ($this->state === MysqlResultProxyState::Initial) {
+            $this->columnDeferred ??= new DeferredFuture();
+            $this->columnDeferred->getFuture()->await();
         }
-        foreach ($this->deferreds[$state] as [$deferred, $rows, $cb]) {
-            \assert($deferred instanceof DeferredFuture);
-            $deferred->complete($cb ? $cb($rows) : $rows);
-        }
-        $this->deferreds[$state] = [];
+
+        return $this->columns;
     }
 
-    public function rowFetched(?array $row): void
+    public function updateState(MysqlResultProxyState $state): void
     {
-        if ($row !== null) {
-            $this->rows[$this->fetchedRows++] = $row;
-        }
-        [$entry, , $cb] = \current($this->deferreds[self::UNFETCHED]);
-        if ($entry !== null) {
-            unset($this->deferreds[self::UNFETCHED][\key($this->deferreds[self::UNFETCHED])]);
-            \assert($entry instanceof DeferredFuture);
-            $entry->complete($cb && $row ? $cb($row) : $row);
+        if ($this->state === MysqlResultProxyState::Complete) {
+            throw new \RuntimeException('Result set already complete');
         }
+
+        match ($state) {
+            MysqlResultProxyState::Complete => $this->rowQueue->complete(),
+            MysqlResultProxyState::ColumnsFetched => $this->columnsFetched(),
+            MysqlResultProxyState::Initial => throw new \RuntimeException('Cannot reset to initial state'),
+        };
+
+        $this->state = $state;
     }
 
-    public function error(\Throwable $e): void
+    private function columnsFetched(): void
     {
-        $this->exception = $e;
-
-        foreach ($this->deferreds as $deferreds) {
-            foreach ($deferreds as [$deferred]) {
-                \assert($deferred instanceof DeferredFuture);
-                $deferred->error($e);
-            }
-        }
+        $this->columnDeferred?->complete();
+        $this->columnDeferred = null;
+    }
 
-        $this->deferreds = [];
+    public function rowFetched(array $row): void
+    {
+        $this->rowQueue->push($row);
     }
 
-    /**
-     * @codeCoverageIgnore
-     */
-    public function __debugInfo(): array
+    public function error(\Throwable $e): void
     {
-        $tmp = clone $this;
-        foreach ($tmp->deferreds as &$type) {
-            foreach ($type as &$entry) {
-                unset($entry[0], $entry[2]);
-            }
+        if ($this->state === MysqlResultProxyState::Complete) {
+            return;
         }
 
-        return (array) $tmp;
+        $this->state = MysqlResultProxyState::Complete;
+
+        $this->rowQueue->error($e);
+
+        $this->columnDeferred?->error($e);
+        $this->columnDeferred = null;
     }
 }
diff --git a/src/Internal/MysqlResultProxyState.php b/src/Internal/MysqlResultProxyState.php
new file mode 100644
index 0000000..84c071c
--- /dev/null
+++ b/src/Internal/MysqlResultProxyState.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Amp\Mysql\Internal;
+
+enum MysqlResultProxyState
+{
+    case Initial;
+    case ColumnsFetched;
+    case Complete;
+}
diff --git a/test/LinkTest.php b/test/LinkTest.php
index de6b684..5171fd6 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -9,6 +9,7 @@
 use Amp\Mysql\MysqlResult;
 use Amp\PHPUnit\AsyncTestCase;
 use Amp\Sql\QueryError;
+use Amp\Sql\Result;
 
 abstract class LinkTest extends AsyncTestCase
 {
@@ -109,6 +110,21 @@ public function testMultiStmt()
         $this->assertNull($resultset->getNextResult());
     }
 
+    public function testQueryWithUnconsumedTupleResult()
+    {
+        $db = $this->getLink();
+
+        $result = $db->query("SELECT * FROM main");
+
+        $this->assertInstanceOf(Result::class, $result);
+
+        unset($result); // Force destruction of result object.
+
+        $result = $db->query("SELECT * FROM main");
+
+        $this->assertInstanceOf(Result::class, $result);
+    }
+
     public function testPrepared()
     {
         $db = $this->getLink(true);
diff --git a/test/initialize.php b/test/initialize.php
index c5bdab1..6edd61e 100644
--- a/test/initialize.php
+++ b/test/initialize.php
@@ -7,7 +7,8 @@ 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, PRIMARY KEY (id))");
-    $epoch = date('1970-01-01 00:00:00');
+
+    $epoch = '1970-01-01 00:00:00';
     $db->query("INSERT INTO test.main (a, b, c) VALUES (1, 2, '$epoch'), (2, 3, '$epoch'), (3, 4, '$epoch'), (4, 5, '$epoch'), (5, 6, '$epoch')");
 
     $db->query("CREATE TABLE test.json (a JSON)");

From 56a5a9c54ec21e88a8db257aed88c8cceceb3a12 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 14 Aug 2022 22:30:32 -0500
Subject: [PATCH 099/174] Avoid calling Generator::valid() in destructor

If the generator was not started, the call could suspend.
---
 src/Internal/MysqlConnectionResult.php | 30 +++++++++++++-------------
 1 file changed, 15 insertions(+), 15 deletions(-)

diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index 3aff048..8b4d6d1 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -13,14 +13,14 @@ final class MysqlConnectionResult implements MysqlResult, \IteratorAggregate
 {
     private readonly MysqlResultProxy $result;
 
-    private readonly \Generator $iterator;
+    private readonly \Generator $generator;
 
     private ?Future $nextResult = null;
 
     public function __construct(MysqlResultProxy $result)
     {
         $this->result = $result;
-        $this->iterator = self::iterate($result);
+        $this->generator = self::iterate($result);
     }
 
     private static function iterate(MysqlResultProxy $result): \Generator
@@ -40,25 +40,25 @@ private static function iterate(MysqlResultProxy $result): \Generator
 
     public function __destruct()
     {
-        if ($this->iterator->valid()) {
-            $iterator = $this->iterator;
-            EventLoop::queue(static function () use ($iterator): void {
-                try {
-                    // Discard remaining rows in the result set.
-                    while ($iterator->valid()) {
-                        $iterator->next();
-                    }
-                } catch (\Throwable) {
-                    // Ignore errors while discarding result.
-                }
-            });
+        EventLoop::queue(self::dispose(...), $this->generator);
+    }
+
+    private static function dispose(\Generator $generator): void
+    {
+        try {
+            // Discard remaining rows in the result set.
+            while ($generator->valid()) {
+                $generator->next();
+            }
+        } catch (\Throwable) {
+            // Ignore errors while discarding result.
         }
     }
 
     public function getIterator(): \Traversable
     {
         // Using a Generator to keep a reference to $this.
-        yield from $this->iterator;
+        yield from $this->generator;
     }
 
     public function getNextResult(): ?MysqlResult

From 04baf7c6c3bed96b39cae3cf5ba141eda5dc0c8f Mon Sep 17 00:00:00 2001
From: bennnjamin <10929253+bennnjamin@users.noreply.github.com>
Date: Thu, 18 Aug 2022 13:26:18 -0700
Subject: [PATCH 100/174] Sort binary result sets correctly (#121)

---
 src/Internal/ConnectionProcessor.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 12517d5..c1f2686 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -1147,7 +1147,7 @@ private function handleBinaryResultSetRow(string $packet): void
             $column = $this->result->columns[$i] ?? throw new \RuntimeException("Definition missing for column $i");
             $fields[$i] = $column->type->decodeBinary($packet, $offset, $column->flags);
         }
-
+        \ksort($fields);
         $this->result->rowFetched($fields);
     }
 

From 74be324ff953208fc29cc510c68552ef4c2ce011 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 18 Aug 2022 21:53:57 -0500
Subject: [PATCH 101/174] Add MysqlResult::getParameterDefinitions()

---
 src/Internal/ConnectionProcessor.php      | 18 +++++++-----
 src/Internal/MysqlConnectionStatement.php |  7 ++++-
 src/Internal/MysqlPooledStatement.php     |  7 ++++-
 src/Internal/MysqlResultProxy.php         | 35 ++++++++++++++--------
 src/Internal/MysqlResultProxyState.php    |  2 +-
 src/Internal/MysqlStatementPool.php       | 12 ++++++--
 src/MysqlResult.php                       |  2 +-
 src/MysqlStatement.php                    |  9 ++++--
 test/LinkTest.php                         | 36 ++++++++++++++++++-----
 test/initialize.php                       |  2 +-
 10 files changed, 94 insertions(+), 36 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index c1f2686..7432b22 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -437,13 +437,14 @@ public function bindParam(int $stmtId, int $paramId, string $data): void
      * $params is by-ref, because the actual result object might not yet have been filled completely with data upon
      * call of this method ...
      *
+     * @param list<MysqlColumnDefinition> $params
      * @param array<int, mixed> $prebound
      * @param array<int, mixed> $data
      */
-    public function execute(int $stmtId, string $query, array &$params, array $prebound, array $data = []): Future
+    public function execute(int $stmtId, string $query, array $params, array $prebound, array $data = []): Future
     {
         $deferred = new DeferredFuture;
-        $this->appendTask(function () use ($stmtId, $query, &$params, $prebound, $data, $deferred): void {
+        $this->appendTask(function () use ($stmtId, $query, $params, $prebound, $data, $deferred): void {
             $payload = "\x17";
             $payload .= MysqlDataType::encodeInt32($stmtId);
             $payload .= \chr(0); // cursor flag // @TODO cursor types?!
@@ -882,7 +883,7 @@ private function handleQuery(string $packet): void
                         affectedRows: $this->metadata->affectedRows,
                         insertId: $this->metadata->insertId
                     );
-                    $this->result->updateState(MysqlResultProxyState::ColumnsFetched);
+                    $this->result->markDefinitionsFetched();
                     $this->dequeueDeferred()->complete(new MysqlConnectionResult($this->result));
                     $this->successfulResultFetch();
                 } else {
@@ -950,7 +951,7 @@ private function handleColumnDefinition(string $packet, \Closure $parseCallback)
         \assert($this->result !== null, 'Connection result was in invalid state');
 
         if (!$this->result->columnsToFetch--) {
-            $this->result->updateState(MysqlResultProxyState::ColumnsFetched);
+            $this->result->markDefinitionsFetched();
             if (\ord($packet) === self::ERR_PACKET) {
                 $this->parseCallback = null;
                 $this->handleError($packet);
@@ -996,7 +997,7 @@ private function prepareFields(string $packet): void
             $result = $this->result;
             $this->result = null;
             $this->ready();
-            $result->updateState(MysqlResultProxyState::ColumnsFetched);
+            $result->markDefinitionsFetched();
 
             return;
         }
@@ -1076,7 +1077,7 @@ private function successfulResultFetch(): void
 
         $this->result = null;
 
-        $result->updateState(MysqlResultProxyState::Complete);
+        $result->complete();
     }
 
     /** @see 14.6.4.1.1.3 Resultset Row */
@@ -1112,7 +1113,7 @@ private function handleTextResultSetRow(string $packet): void
             }
         }
 
-        $this->result->rowFetched($fields);
+        $this->result->pushRow($fields);
     }
 
     /** @see 14.7.2 Binary Protocol Resultset Row */
@@ -1147,8 +1148,9 @@ private function handleBinaryResultSetRow(string $packet): void
             $column = $this->result->columns[$i] ?? throw new \RuntimeException("Definition missing for column $i");
             $fields[$i] = $column->type->decodeBinary($packet, $offset, $column->flags);
         }
+
         \ksort($fields);
-        $this->result->rowFetched($fields);
+        $this->result->pushRow($fields);
     }
 
     /** @see 14.7.4.1 COM_STMT_PREPARE Response */
diff --git a/src/Internal/MysqlConnectionStatement.php b/src/Internal/MysqlConnectionStatement.php
index c1baa4f..54b89de 100644
--- a/src/Internal/MysqlConnectionStatement.php
+++ b/src/Internal/MysqlConnectionStatement.php
@@ -157,11 +157,16 @@ public function reset(): void
             ->await();
     }
 
-    public function getColumnDefinitions(): ?array
+    public function getColumnDefinitions(): array
     {
         return $this->result->getColumnDefinitions();
     }
 
+    public function getParameterDefinitions(): array
+    {
+        return $this->result->getParameterDefinitions();
+    }
+
     public function getLastUsedAt(): int
     {
         return $this->lastUsedAt;
diff --git a/src/Internal/MysqlPooledStatement.php b/src/Internal/MysqlPooledStatement.php
index f67d9e3..88b375a 100644
--- a/src/Internal/MysqlPooledStatement.php
+++ b/src/Internal/MysqlPooledStatement.php
@@ -43,11 +43,16 @@ public function bind(int|string $paramId, mixed $data): void
         $this->statement->bind($paramId, $data);
     }
 
-    public function getColumnDefinitions(): ?array
+    public function getColumnDefinitions(): array
     {
         return $this->statement->getColumnDefinitions();
     }
 
+    public function getParameterDefinitions(): array
+    {
+        return $this->statement->getParameterDefinitions();
+    }
+
     public function reset(): void
     {
         $this->statement->reset();
diff --git a/src/Internal/MysqlResultProxy.php b/src/Internal/MysqlResultProxy.php
index 139ce82..14c00d4 100644
--- a/src/Internal/MysqlResultProxy.php
+++ b/src/Internal/MysqlResultProxy.php
@@ -4,6 +4,7 @@
 
 use Amp\DeferredFuture;
 use Amp\Mysql\MysqlColumnDefinition;
+use Amp\Mysql\MysqlResult;
 use Amp\Pipeline\ConcurrentIterator;
 use Amp\Pipeline\Queue;
 
@@ -15,6 +16,7 @@ final class MysqlResultProxy
     /** @var list<MysqlColumnDefinition> */
     public array $columns = [];
 
+    /** @var list<MysqlColumnDefinition> */
     public array $params = [];
 
     private readonly Queue $rowQueue;
@@ -25,6 +27,7 @@ final class MysqlResultProxy
 
     private MysqlResultProxyState $state = MysqlResultProxyState::Initial;
 
+    /** @var DeferredFuture<MysqlResult|null>|null */
     public ?DeferredFuture $next = null;
 
     public function __construct(
@@ -49,32 +52,40 @@ public function getColumnDefinitions(): array
         return $this->columns;
     }
 
-    public function updateState(MysqlResultProxyState $state): void
+    public function getParameterDefinitions(): array
     {
-        if ($this->state === MysqlResultProxyState::Complete) {
-            throw new \RuntimeException('Result set already complete');
+        if ($this->state === MysqlResultProxyState::Initial) {
+            $this->columnDeferred ??= new DeferredFuture();
+            $this->columnDeferred->getFuture()->await();
         }
 
-        match ($state) {
-            MysqlResultProxyState::Complete => $this->rowQueue->complete(),
-            MysqlResultProxyState::ColumnsFetched => $this->columnsFetched(),
-            MysqlResultProxyState::Initial => throw new \RuntimeException('Cannot reset to initial state'),
-        };
-
-        $this->state = $state;
+        return $this->params;
     }
 
-    private function columnsFetched(): void
+    public function markDefinitionsFetched(): void
     {
+        \assert($this->state === MysqlResultProxyState::Initial, 'Result proxy in invalid state');
+
+        $this->state = MysqlResultProxyState::Fetched;
         $this->columnDeferred?->complete();
         $this->columnDeferred = null;
     }
 
-    public function rowFetched(array $row): void
+    public function pushRow(array $row): void
     {
+        \assert($this->state === MysqlResultProxyState::Fetched, 'Result proxy in invalid state');
+
         $this->rowQueue->push($row);
     }
 
+    public function complete(): void
+    {
+        \assert($this->state === MysqlResultProxyState::Fetched, 'Result proxy in invalid state');
+
+        $this->state = MysqlResultProxyState::Complete;
+        $this->rowQueue->complete();
+    }
+
     public function error(\Throwable $e): void
     {
         if ($this->state === MysqlResultProxyState::Complete) {
diff --git a/src/Internal/MysqlResultProxyState.php b/src/Internal/MysqlResultProxyState.php
index 84c071c..327f551 100644
--- a/src/Internal/MysqlResultProxyState.php
+++ b/src/Internal/MysqlResultProxyState.php
@@ -5,6 +5,6 @@
 enum MysqlResultProxyState
 {
     case Initial;
-    case ColumnsFetched;
+    case Fetched;
     case Complete;
 }
diff --git a/src/Internal/MysqlStatementPool.php b/src/Internal/MysqlStatementPool.php
index 0fb6717..c107085 100644
--- a/src/Internal/MysqlStatementPool.php
+++ b/src/Internal/MysqlStatementPool.php
@@ -59,11 +59,19 @@ public function reset(): void
         $this->params = [];
     }
 
-    public function getColumnDefinitions(): ?array
+    public function getColumnDefinitions(): array
     {
-        $statement = $this->pop();
+        $statement = parent::pop();
         $columns = $statement->getColumnDefinitions();
         $this->push($statement);
         return $columns;
     }
+
+    public function getParameterDefinitions(): array
+    {
+        $statement = parent::pop();
+        $parameters = $statement->getParameterDefinitions();
+        $this->push($statement);
+        return $parameters;
+    }
 }
diff --git a/src/MysqlResult.php b/src/MysqlResult.php
index 975fdf7..f0ea75d 100644
--- a/src/MysqlResult.php
+++ b/src/MysqlResult.php
@@ -5,7 +5,7 @@
 use Amp\Sql\Result;
 
 /**
- * @extends Result<bool|int|float|string|null>
+ * @extends Result<int|float|string|null>
  */
 interface MysqlResult extends Result
 {
diff --git a/src/MysqlStatement.php b/src/MysqlStatement.php
index cea98f9..b089d67 100644
--- a/src/MysqlStatement.php
+++ b/src/MysqlStatement.php
@@ -17,9 +17,14 @@ public function execute(array $params = []): MysqlResult;
     public function bind(int|string $paramId, mixed $data): void;
 
     /**
-     * @return list<MysqlColumnDefinition>|null
+     * @return list<MysqlColumnDefinition>
      */
-    public function getColumnDefinitions(): ?array;
+    public function getColumnDefinitions(): array;
+
+    /**
+     * @return list<MysqlColumnDefinition>
+     */
+    public function getParameterDefinitions(): array;
 
     /**
      * Reset statement to state just after preparing.
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 5171fd6..d48d5aa 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -13,6 +13,8 @@
 
 abstract class LinkTest extends AsyncTestCase
 {
+    public const EPOCH = '1970-01-01 00:00:00';
+
     /**
      * Returns the Link class to be tested.
      */
@@ -129,7 +131,8 @@ public function testPrepared()
     {
         $db = $this->getLink(true);
 
-        $stmt = $db->prepare("SELECT id as no, a, b FROM main as table_alias WHERE a = ? OR b = :num");
+        $stmt = $db->prepare("SELECT id AS no, a AS d, b, c FROM main AS table_alias WHERE b = :num AND c = ?");
+
         $base = [
             "catalog" => "def",
             "schema" => "test",
@@ -137,26 +140,45 @@ public function testPrepared()
             "originalTable" => "main",
             "charset" => 63,
             "length" => 11,
-            "type" => MysqlDataType::from(3),
+            "type" => MysqlDataType::Long,
             "flags" => 0,
             "decimals" => 0,
         ];
 
         $this->assertEquals($stmt->getColumnDefinitions(), [
             new MysqlColumnDefinition(...\array_merge($base, ["name" => "no", "originalName" => "id", "flags" => 16899])),
-            new MysqlColumnDefinition(...\array_merge($base, ["name" => "a", "originalName" => "a"])),
-            new MysqlColumnDefinition(...\array_merge($base + ["name" => "b", "originalName" => "b"])),
+            new MysqlColumnDefinition(...\array_merge($base, ["name" => "d", "originalName" => "a"])),
+            new MysqlColumnDefinition(...\array_merge($base, ["name" => "b", "originalName" => "b"])),
+            new MysqlColumnDefinition(...\array_merge($base, ["name" => "c", "originalName" => "c", "type" => MysqlDataType::Datetime, "length" => 19, "flags" => 128])),
+        ]);
+
+        $base = [
+            "name" => "?",
+            "catalog" => "def",
+            "schema" => "",
+            "table" => "",
+            "originalTable" => "",
+            "originalName" => "",
+            "charset" => 63,
+            "length" => 21,
+            "flags" => 0,
+            "decimals" => 0,
+        ];
+
+        $this->assertEquals($stmt->getParameterDefinitions(), [
+            new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::LongLong, "flags" => 128])),
+            new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::Datetime, "length" => 104, "decimals" => 6, "charset" => 45])),
         ]);
 
         $stmt->bind("num", 5);
-        $result = $stmt->execute([2]);
+        $result = $stmt->execute([self::EPOCH]);
         $this->assertInstanceOf(MysqlResult::class, $result);
-        $this->assertSame(3, $result->getColumnCount());
+        $this->assertSame(4, $result->getColumnCount());
         $got = [];
         foreach ($result as $row) {
             $got[] = \array_values($row);
         }
-        $this->assertCount(2, $got);
+        $this->assertCount(1, $got);
 
         $stmt = $db->prepare("SELECT * FROM main WHERE a = ? OR b = ?");
         $result = $stmt->execute([1, 8]);
diff --git a/test/initialize.php b/test/initialize.php
index 6edd61e..ec5dbf9 100644
--- a/test/initialize.php
+++ b/test/initialize.php
@@ -8,7 +8,7 @@ function initialize(\mysqli $db): void
 
     $db->query("CREATE TABLE test.main (id INT NOT NULL AUTO_INCREMENT, a INT, b INT, c DATETIME, PRIMARY KEY (id))");
 
-    $epoch = '1970-01-01 00:00:00';
+    $epoch = LinkTest::EPOCH;
     $db->query("INSERT INTO test.main (a, b, c) VALUES (1, 2, '$epoch'), (2, 3, '$epoch'), (3, 4, '$epoch'), (4, 5, '$epoch'), (5, 6, '$epoch')");
 
     $db->query("CREATE TABLE test.json (a JSON)");

From eb2e72cfa4618b3ca131c479023b566226c59c2c Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 18 Aug 2022 21:55:11 -0500
Subject: [PATCH 102/174] Simplify result set generator

---
 src/Internal/MysqlConnectionResult.php | 12 +++---------
 1 file changed, 3 insertions(+), 9 deletions(-)

diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index 8b4d6d1..fb24c12 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -25,17 +25,11 @@ public function __construct(MysqlResultProxy $result)
 
     private static function iterate(MysqlResultProxy $result): \Generator
     {
-        if (!$result->rowIterator->continue()) {
-            return;
-        }
-
-        // Column names are only available once a result row has been fetched.
-        $columnNames = \array_column($result->columns, 'name');
+        $columnNames = \array_column($result->getColumnDefinitions(), 'name');
 
-        do {
-            $row = $result->rowIterator->getValue();
+        foreach ($result->rowIterator as $row) {
             yield \array_combine($columnNames, $row);
-        } while ($result->rowIterator->continue());
+        }
     }
 
     public function __destruct()

From 4eca415998788e1b69eb9ee6f54d7b44662704a6 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Fri, 19 Aug 2022 22:55:45 -0500
Subject: [PATCH 103/174] Throw if current result is not consumed when
 requesting next result

---
 src/Internal/MysqlConnectionResult.php |  4 ++
 test/LinkTest.php                      | 65 +++++++++++++++++++-------
 2 files changed, 51 insertions(+), 18 deletions(-)

diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index fb24c12..028a3bd 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -57,6 +57,10 @@ public function getIterator(): \Traversable
 
     public function getNextResult(): ?MysqlResult
     {
+        if ($this->generator->valid()) {
+            throw new \Error('Consume entire current result before requesting next result');
+        }
+
         $this->nextResult ??= async(function (): ?MysqlResult {
             $deferred = $this->result->next ??= new DeferredFuture;
             $result = $deferred->getFuture()->await();
diff --git a/test/LinkTest.php b/test/LinkTest.php
index d48d5aa..05c8647 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -34,11 +34,11 @@ public function testQuery()
     {
         $db = $this->getLink();
 
-        $resultset = $db->execute("SELECT ? AS a", [M_PI]);
-        $this->assertInstanceOf(MysqlResult::class, $resultset);
+        $result = $db->execute("SELECT ? AS a", [M_PI]);
+        $this->assertInstanceOf(MysqlResult::class, $result);
 
         $i = 0;
-        foreach ($resultset as $row) {
+        foreach ($result as $row) {
             $this->assertSame(["a" => M_PI], $row);
             ++$i;
         }
@@ -50,13 +50,13 @@ public function testQueryFetchRow()
     {
         $db = $this->getLink();
 
-        $resultset = $db->query('SELECT a FROM main WHERE a < 4');
-        $this->assertInstanceOf(MysqlResult::class, $resultset);
+        $result = $db->query('SELECT a FROM main WHERE a < 4');
+        $this->assertInstanceOf(MysqlResult::class, $result);
 
-        $this->assertSame(1, $resultset->getColumnCount());
+        $this->assertSame(1, $result->getColumnCount());
 
         $got = [];
-        foreach ($resultset as $row) {
+        foreach ($result as $row) {
             $got[] = \array_values($row);
         }
 
@@ -77,27 +77,27 @@ public function testMultiStmt()
     {
         $db = $this->getLink(true);
 
-        $resultset = $db->query("SELECT a FROM main; SELECT b FROM main WHERE a = 5; SELECT b AS d, a + 1 AS c FROM main WHERE b > 4");
-        $this->assertInstanceOf(MysqlResult::class, $resultset);
+        $result = $db->query("SELECT a FROM main; SELECT b FROM main WHERE a = 5; SELECT b AS d, a + 1 AS c FROM main WHERE b > 4");
+        $this->assertInstanceOf(MysqlResult::class, $result);
 
         $got = [];
-        foreach ($resultset as $row) {
+        foreach ($result as $row) {
             $got[] = \array_values($row);
         }
         $this->assertSame([[1], [2], [3], [4], [5]], $got);
-        $this->assertInstanceOf(MysqlResult::class, $resultset = $resultset->getNextResult());
+        $this->assertInstanceOf(MysqlResult::class, $result = $result->getNextResult());
 
         $got = [];
-        foreach ($resultset as $row) {
+        foreach ($result as $row) {
             $got[] = \array_values($row);
         }
         $this->assertSame([[6]], $got);
-        $this->assertInstanceOf(MysqlResult::class, $resultset = $resultset->getNextResult());
+        $this->assertInstanceOf(MysqlResult::class, $result = $result->getNextResult());
 
-        $fields = $resultset->getColumnDefinitions();
+        $fields = $result->getColumnDefinitions();
 
         $got = [];
-        foreach ($resultset as $row) {
+        foreach ($result as $row) {
             $got[] = $row;
         }
         $this->assertSame([["d" => 5, "c" => 5], ["d" => 6, "c" => 6]], $got);
@@ -109,24 +109,53 @@ public function testMultiStmt()
         $this->assertSame($fields[1]->name, "c");
         $this->assertSame($fields[1]->type, MysqlDataType::LongLong);
 
-        $this->assertNull($resultset->getNextResult());
+        $this->assertNull($result->getNextResult());
+    }
+
+    public function testNextResultBeforeConsumption()
+    {
+        $db = $this->getLink(true);
+
+        $result = $db->query("SELECT a FROM main; SELECT b FROM main;");
+
+        $this->expectException(\Error::class);
+        $this->expectExceptionMessage('Consume entire current result before requesting next result');
+
+        $result->getNextResult();
     }
 
     public function testQueryWithUnconsumedTupleResult()
     {
         $db = $this->getLink();
 
-        $result = $db->query("SELECT * FROM main");
+        $result = $db->query("SELECT a FROM main");
 
         $this->assertInstanceOf(Result::class, $result);
 
         unset($result); // Force destruction of result object.
 
-        $result = $db->query("SELECT * FROM main");
+        $result = $db->query("SELECT b FROM main");
 
         $this->assertInstanceOf(Result::class, $result);
     }
 
+    public function testUnconsumedMultiResult()
+    {
+        $db = $this->getLink(true);
+
+        $result = $db->query("SELECT a FROM main; SELECT b FROM main");
+
+        unset($result);
+
+        $result = $db->query("SELECT a, b FROM main WHERE a = 5");
+
+        $got = [];
+        foreach ($result as $row) {
+            $got[] = $row;
+        }
+        self::assertSame([['a' => 5, 'b' => 6]], $got);
+    }
+
     public function testPrepared()
     {
         $db = $this->getLink(true);

From f37ddfcac597f22c5cd03f86862a73557399aeee Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 20 Aug 2022 08:54:45 -0500
Subject: [PATCH 104/174] Cleanup MysqlConnectionStatement

---
 src/Internal/MysqlConnectionStatement.php | 44 +++++++++++------------
 1 file changed, 21 insertions(+), 23 deletions(-)

diff --git a/src/Internal/MysqlConnectionStatement.php b/src/Internal/MysqlConnectionStatement.php
index 54b89de..7ebc2e7 100644
--- a/src/Internal/MysqlConnectionStatement.php
+++ b/src/Internal/MysqlConnectionStatement.php
@@ -11,39 +11,40 @@
 /** @internal */
 final class MysqlConnectionStatement implements MysqlStatement
 {
-    private int $paramCount;
-    private int $numParamCount;
+    private readonly int $totalParamCount;
+    private readonly int $positionalParamCount;
+
     private array $named = [];
-    private array $byNamed;
-    private string $query;
-    private int $stmtId;
     private array $prebound = [];
 
     private ?ConnectionProcessor $processor;
 
-    private readonly MysqlResultProxy $result;
-
     private int $lastUsedAt;
 
     private readonly DeferredFuture $onClose;
 
-    public function __construct(ConnectionProcessor $processor, string $query, int $stmtId, array $named, MysqlResultProxy $result)
-    {
+    public function __construct(
+        ConnectionProcessor $processor,
+        private readonly string $query,
+        private readonly int $stmtId,
+        private readonly array $byNamed,
+        private readonly MysqlResultProxy $result
+    ) {
         $this->processor = $processor;
-        $this->query = $query;
-        $this->stmtId = $stmtId;
-        $this->result = $result;
-        $this->numParamCount = $this->paramCount = $this->result->columnsToFetch;
-        $this->byNamed = $named;
+        $this->totalParamCount = $this->result->columnsToFetch;
+
         $this->onClose = new DeferredFuture();
 
-        foreach ($named as $name => $ids) {
+        $positionalParamCount = $this->totalParamCount;
+        foreach ($this->byNamed as $name => $ids) {
             foreach ($ids as $id) {
                 $this->named[$id] = $name;
-                $this->numParamCount--;
+                $positionalParamCount--;
             }
         }
 
+        $this->positionalParamCount = $positionalParamCount;
+
         $this->lastUsedAt = \time();
     }
 
@@ -81,7 +82,7 @@ public function onClose(\Closure $onClose): void
     public function bind(int|string $paramId, mixed $data): void
     {
         if (\is_int($paramId)) {
-            if ($paramId >= $this->numParamCount) {
+            if ($paramId >= $this->positionalParamCount) {
                 throw new \Error("Parameter $paramId is not defined for this prepared statement");
             }
             $i = $paramId;
@@ -94,7 +95,7 @@ public function bind(int|string $paramId, mixed $data): void
         }
 
         if (!\is_scalar($data) && !(\is_object($data) && \method_exists($data, '__toString'))) {
-            throw new \TypeError("Data must be scalar or object that implements __toString method");
+            throw new \TypeError("Data must be scalar or an object that implements __toString method");
         }
 
         do {
@@ -120,7 +121,7 @@ public function execute(array $params = []): MysqlResult
         $this->lastUsedAt = \time();
 
         $prebound = $args = [];
-        for ($unnamed = $i = 0; $i < $this->paramCount; $i++) {
+        for ($unnamed = $i = 0; $i < $this->totalParamCount; $i++) {
             if (isset($this->named[$i])) {
                 $name = $this->named[$i];
                 if (\array_key_exists($name, $params)) {
@@ -175,10 +176,7 @@ public function getLastUsedAt(): int
     public function __destruct()
     {
         if ($this->processor) {
-            $processor = $this->processor;
-            $stmtId = $this->stmtId;
-            $onClose = $this->onClose;
-            EventLoop::queue(static fn () => self::shutdown($processor, $stmtId, $onClose));
+            EventLoop::queue(self::shutdown(...), $this->processor, $this->stmtId, $this->onClose);
         }
     }
 

From 4e77d6719715e48e388b70db1d6d75f450237893 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 20 Aug 2022 09:22:20 -0500
Subject: [PATCH 105/174] Nest generator parsers

---
 src/Internal/ConnectionProcessor.php | 66 ++++++++++------------------
 1 file changed, 24 insertions(+), 42 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 7432b22..37bf123 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -44,8 +44,7 @@ class ConnectionProcessor implements TransientResource
         ]msxS
         REGEX;
 
-    /** @var list<\Generator> */
-    private array $processors = [];
+    private ?\Generator $parser = null;
 
     private int $seqId = -1;
     private int $compressionId = -1;
@@ -193,15 +192,15 @@ private function enqueueDeferred(DeferredFuture $deferred): void
 
     public function connect(?Cancellation $cancellation = null): void
     {
-        \assert(!$this->processors, self::class."::connect() must not be called twice");
+        \assert(!$this->parser, self::class."::connect() must not be called twice");
 
         $this->enqueueDeferred($deferred = new DeferredFuture()); // Will be resolved in sendHandshake().
 
-        $this->processors = [$this->parseMysql()];
+        $this->parser = $this->parseMysql();
 
-        $id = $cancellation?->subscribe(fn () => $this->close());
+        $id = $cancellation?->subscribe($this->close(...));
 
-        EventLoop::queue(fn () => $this->read());
+        EventLoop::queue($this->read(...));
 
         $future = $deferred->getFuture();
         if ($id !== null) {
@@ -245,7 +244,8 @@ private function read(): void
 
                 $this->lastUsedAt = \time();
 
-                $this->processData($bytes);
+                \assert($this->parser !== null, 'Inconsistent object state after receiving data');
+                $this->parser->send($bytes);
                 $bytes = null; // Free last data read.
             }
         } catch (\Throwable $exception) {
@@ -255,21 +255,6 @@ private function read(): void
         }
     }
 
-    private function processData(string $data): void
-    {
-        foreach ($this->processors as $processor) {
-            if (empty($data = $processor->send($data))) {
-                return;
-            }
-        }
-
-        \assert(\is_array($data), "Final processor should yield an array");
-
-        foreach ($data as $packet) {
-            $this->parsePayload($packet);
-        }
-    }
-
     private function dequeueDeferred(): DeferredFuture
     {
         \assert(!$this->deferreds->isEmpty(), 'Pending deferred not found when shifting from pending queue');
@@ -1229,7 +1214,7 @@ private function free(?\Throwable $exception = null): void
         }
 
         $this->socket->close();
-        $this->processors = [];
+        $this->parser = null;
     }
 
     private function write(string $packet): void
@@ -1303,33 +1288,32 @@ private function compressPacket(string $packet): string
     }
 
     /** @see 14.4 Compression */
-    private function parseCompression(): \Generator
+    private function parseCompression(\Generator $parser): \Generator
     {
-        $inflated = "";
         $buffer = "";
 
         while (true) {
             while (\strlen($buffer) < 7) {
-                $buffer .= yield $inflated;
-                $inflated = "";
+                $buffer .= yield;
             }
 
-            $size = MysqlDataType::decodeUnsigned24($buffer);
-            $this->compressionId = \ord($buffer[3]);
-            $uncompressed = MysqlDataType::decodeUnsigned24(\substr($buffer, 4, 3));
+            $offset = 0;
+
+            $size = MysqlDataType::decodeUnsigned24($buffer, $offset);
+            $this->compressionId = \ord($buffer[$offset++]);
+            $uncompressed = MysqlDataType::decodeUnsigned24($buffer, $offset);
 
-            $buffer = \substr($buffer, 7);
+            $buffer = \substr($buffer, $offset);
 
             if ($size > 0) {
                 while (\strlen($buffer) < $size) {
-                    $buffer .= yield $inflated;
-                    $inflated = "";
+                    $buffer .= yield;
                 }
 
                 if ($uncompressed === 0) {
-                    $inflated .= \substr($buffer, 0, $size);
+                    $parser->send(\substr($buffer, 0, $size));
                 } else {
-                    $inflated .= \zlib_decode(\substr($buffer, 0, $size), $uncompressed);
+                    $parser->send(\zlib_decode(\substr($buffer, 0, $size), $uncompressed));
                 }
 
                 $buffer = \substr($buffer, $size);
@@ -1344,15 +1328,13 @@ private function parseCompression(): \Generator
     private function parseMysql(): \Generator
     {
         $buffer = "";
-        $parsed = [];
 
         while (true) {
             $packet = "";
 
             do {
                 while (\strlen($buffer) < 4) {
-                    $buffer .= yield $parsed;
-                    $parsed = [];
+                    $buffer .= yield;
                 }
 
                 $length = MysqlDataType::decodeUnsigned24($buffer);
@@ -1360,8 +1342,7 @@ private function parseMysql(): \Generator
                 $buffer = \substr($buffer, 4);
 
                 while (\strlen($buffer) < ($length & 0xffffff)) {
-                    $buffer .= yield $parsed;
-                    $parsed = [];
+                    $buffer .= yield;
                 }
 
                 $lastIn = $length !== 0xffffff;
@@ -1376,7 +1357,7 @@ private function parseMysql(): \Generator
             } while (!$lastIn);
 
             if (\strlen($packet) > 0) {
-                $parsed[] = $packet;
+                $this->parsePayload($packet);
             }
         }
     }
@@ -1394,7 +1375,8 @@ private function parsePayload(string $packet): void
             switch (\ord($packet)) {
                 case self::OK_PACKET:
                     if ($this->capabilities & self::CLIENT_COMPRESS) {
-                        $this->processors = \array_merge([$this->parseCompression()], $this->processors);
+                        \assert($this->parser !== null, 'Inconsistent object state when parsing payload');
+                        $this->parser = $this->parseCompression($this->parser);
                     }
                     $this->connectionState = ConnectionState::Ready;
                     $this->handleOk($packet);

From f978db15aebb92babb445af2599d2ab7fb403615 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 20 Aug 2022 14:03:29 -0500
Subject: [PATCH 106/174] Use parser library for packet parsing

---
 composer.json                        |   1 +
 src/Internal/ConnectionProcessor.php | 114 +++++++++++----------------
 src/Internal/ConnectionState.php     |   1 +
 src/Internal/MysqlResultProxy.php    |   9 ++-
 4 files changed, 56 insertions(+), 69 deletions(-)

diff --git a/composer.json b/composer.json
index ccb4978..10575ac 100644
--- a/composer.json
+++ b/composer.json
@@ -15,6 +15,7 @@
     "require": {
         "php": ">=8.1",
         "amphp/amp": "^3",
+        "amphp/parser": "^1",
         "amphp/pipeline": "^1",
         "amphp/socket": "^2",
         "amphp/sql": "^2",
diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 37bf123..91a747a 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -9,6 +9,7 @@
 use Amp\Mysql\MysqlColumnDefinition;
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlDataType;
+use Amp\Parser\Parser;
 use Amp\Socket\EncryptableSocket;
 use Amp\Sql\ConnectionException;
 use Amp\Sql\QueryError;
@@ -44,7 +45,7 @@ class ConnectionProcessor implements TransientResource
         ]msxS
         REGEX;
 
-    private ?\Generator $parser = null;
+    private Parser $parser;
 
     private int $seqId = -1;
     private int $compressionId = -1;
@@ -132,13 +133,8 @@ public function __construct(EncryptableSocket $socket, MysqlConfig $config)
 
         $this->deferreds = new \SplQueue();
         $this->onReady = new \SplQueue();
-    }
 
-    public function __destruct()
-    {
-        if (!$this->isClosed()) {
-            $this->close();
-        }
+        $this->parser = new Parser($this->parseMysql());
     }
 
     public function isClosed(): bool
@@ -192,11 +188,14 @@ private function enqueueDeferred(DeferredFuture $deferred): void
 
     public function connect(?Cancellation $cancellation = null): void
     {
-        \assert(!$this->parser, self::class."::connect() must not be called twice");
+        \assert(
+            $this->connectionState === ConnectionState::Unconnected,
+            self::class . "::connect() must not be called twice",
+        );
 
-        $this->enqueueDeferred($deferred = new DeferredFuture()); // Will be resolved in sendHandshake().
+        $this->connectionState = ConnectionState::Connecting;
 
-        $this->parser = $this->parseMysql();
+        $this->enqueueDeferred($deferred = new DeferredFuture()); // Will be resolved in sendHandshake().
 
         $id = $cancellation?->subscribe($this->close(...));
 
@@ -244,8 +243,7 @@ private function read(): void
 
                 $this->lastUsedAt = \time();
 
-                \assert($this->parser !== null, 'Inconsistent object state after receiving data');
-                $this->parser->send($bytes);
+                $this->parser->push($bytes);
                 $bytes = null; // Free last data read.
             }
         } catch (\Throwable $exception) {
@@ -1047,8 +1045,7 @@ private function successfulResultFetch(): void
     {
         \assert($this->result !== null, 'Connection result was in invalid state');
 
-        $result = $this->result;
-        $deferred = $result->next ??= new DeferredFuture();
+        $deferred = $this->result->next ??= new DeferredFuture();
 
         if ($this->metadata->statusFlags & self::SERVER_MORE_RESULTS_EXISTS) {
             $this->parseCallback = $this->handleQuery(...);
@@ -1060,9 +1057,8 @@ private function successfulResultFetch(): void
             $this->ready();
         }
 
+        $this->result->complete();
         $this->result = null;
-
-        $result->complete();
     }
 
     /** @see 14.6.4.1.1.3 Resultset Row */
@@ -1203,6 +1199,9 @@ private function free(?\Throwable $exception = null): void
 
         $this->connectionState = ConnectionState::Closed;
 
+        $this->socket->close();
+        $this->parser->cancel();
+
         if (!$this->deferreds->isEmpty() || $this->result) {
             $exception = new ConnectionException("Connection closed unexpectedly", 0, $exception ?? null);
 
@@ -1211,10 +1210,8 @@ private function free(?\Throwable $exception = null): void
             }
 
             $this->result?->error($exception);
+            $this->result = null;
         }
-
-        $this->socket->close();
-        $this->parser = null;
     }
 
     private function write(string $packet): void
@@ -1287,36 +1284,32 @@ private function compressPacket(string $packet): string
             . \substr(\pack("V", $length), 0, 3) . $deflated;
     }
 
-    /** @see 14.4 Compression */
-    private function parseCompression(\Generator $parser): \Generator
+    /**
+     * @see 14.4 Compression
+     *
+     * @return \Generator<int, int, string, void>
+     */
+    private function parseCompression(Parser $parser): \Generator
     {
-        $buffer = "";
-
         while (true) {
-            while (\strlen($buffer) < 7) {
-                $buffer .= yield;
-            }
-
+            $buffer = yield 7;
             $offset = 0;
 
-            $size = MysqlDataType::decodeUnsigned24($buffer, $offset);
-            $this->compressionId = \ord($buffer[$offset++]);
+            $length = MysqlDataType::decodeUnsigned24($buffer, $offset);
+            $this->compressionId = MysqlDataType::decodeUnsigned8($buffer, $offset);
             $uncompressed = MysqlDataType::decodeUnsigned24($buffer, $offset);
 
-            $buffer = \substr($buffer, $offset);
-
-            if ($size > 0) {
-                while (\strlen($buffer) < $size) {
-                    $buffer .= yield;
-                }
-
+            if ($length > 0) {
                 if ($uncompressed === 0) {
-                    $parser->send(\substr($buffer, 0, $size));
+                    $parser->push(yield $length);
                 } else {
-                    $parser->send(\zlib_decode(\substr($buffer, 0, $size), $uncompressed));
-                }
+                    $buffer = \zlib_decode(yield $length, $uncompressed);
+                    if ($buffer === false) {
+                        throw new \RuntimeException('Decompression failed');
+                    }
 
-                $buffer = \substr($buffer, $size);
+                    $parser->push($buffer);
+                }
             }
         }
     }
@@ -1324,47 +1317,35 @@ private function parseCompression(\Generator $parser): \Generator
     /**
      * @see 14.1.2 MySQL Packet
      * @see 14.1.3 Generic Response Packets
+     *
+     * @return \Generator<int, int, string, void>
      */
     private function parseMysql(): \Generator
     {
-        $buffer = "";
-
         while (true) {
-            $packet = "";
+            $packet = [];
 
             do {
-                while (\strlen($buffer) < 4) {
-                    $buffer .= yield;
-                }
+                $buffer = yield 4;
+                $offset = 0;
 
-                $length = MysqlDataType::decodeUnsigned24($buffer);
-                $this->seqId = \ord($buffer[3]);
-                $buffer = \substr($buffer, 4);
+                $length = MysqlDataType::decodeUnsigned24($buffer, $offset);
+                $this->seqId = MysqlDataType::decodeUnsigned8($buffer, $offset);
 
-                while (\strlen($buffer) < ($length & 0xffffff)) {
-                    $buffer .= yield;
+                if ($length > 0) {
+                    $packet[] = yield $length;
                 }
+            } while ($length === 0xffffff);
 
-                $lastIn = $length !== 0xffffff;
-                if ($lastIn) {
-                    $size = $length % 0xffffff;
-                } else {
-                    $size = 0xffffff;
-                }
-
-                $packet .= \substr($buffer, 0, $size);
-                $buffer = \substr($buffer, $size);
-            } while (!$lastIn);
-
-            if (\strlen($packet) > 0) {
-                $this->parsePayload($packet);
+            if ($packet) {
+                $this->parsePayload(\implode($packet));
             }
         }
     }
 
     private function parsePayload(string $packet): void
     {
-        if ($this->connectionState === ConnectionState::Unconnected) {
+        if ($this->connectionState === ConnectionState::Connecting) {
             $this->established();
             $this->connectionState = ConnectionState::Established;
             $this->handleHandshake($packet);
@@ -1375,8 +1356,7 @@ private function parsePayload(string $packet): void
             switch (\ord($packet)) {
                 case self::OK_PACKET:
                     if ($this->capabilities & self::CLIENT_COMPRESS) {
-                        \assert($this->parser !== null, 'Inconsistent object state when parsing payload');
-                        $this->parser = $this->parseCompression($this->parser);
+                        $this->parser = new Parser($this->parseCompression($this->parser));
                     }
                     $this->connectionState = ConnectionState::Ready;
                     $this->handleOk($packet);
diff --git a/src/Internal/ConnectionState.php b/src/Internal/ConnectionState.php
index 1403ed3..d94ce96 100644
--- a/src/Internal/ConnectionState.php
+++ b/src/Internal/ConnectionState.php
@@ -6,6 +6,7 @@
 enum ConnectionState
 {
     case Unconnected;
+    case Connecting;
     case Established;
     case Ready;
     case Closing;
diff --git a/src/Internal/MysqlResultProxy.php b/src/Internal/MysqlResultProxy.php
index 14c00d4..6b1e829 100644
--- a/src/Internal/MysqlResultProxy.php
+++ b/src/Internal/MysqlResultProxy.php
@@ -83,7 +83,10 @@ public function complete(): void
         \assert($this->state === MysqlResultProxyState::Fetched, 'Result proxy in invalid state');
 
         $this->state = MysqlResultProxyState::Complete;
-        $this->rowQueue->complete();
+
+        if (!$this->rowQueue->isComplete()) {
+            $this->rowQueue->complete();
+        }
     }
 
     public function error(\Throwable $e): void
@@ -94,7 +97,9 @@ public function error(\Throwable $e): void
 
         $this->state = MysqlResultProxyState::Complete;
 
-        $this->rowQueue->error($e);
+        if (!$this->rowQueue->isComplete()) {
+            $this->rowQueue->error($e);
+        }
 
         $this->columnDeferred?->error($e);
         $this->columnDeferred = null;

From e1ba8fa3dfa2aed10d59d94e1d8c34f0fd9918ca Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 21 Aug 2022 15:28:22 -0500
Subject: [PATCH 107/174] Make parsing compression slightly cleaner

---
 src/Internal/ConnectionProcessor.php | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 91a747a..ec47311 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -1300,16 +1300,16 @@ private function parseCompression(Parser $parser): \Generator
             $uncompressed = MysqlDataType::decodeUnsigned24($buffer, $offset);
 
             if ($length > 0) {
-                if ($uncompressed === 0) {
-                    $parser->push(yield $length);
-                } else {
-                    $buffer = \zlib_decode(yield $length, $uncompressed);
+                $buffer = yield $length;
+
+                if ($uncompressed !== 0) {
+                    $buffer = \zlib_decode($buffer, $uncompressed);
                     if ($buffer === false) {
                         throw new \RuntimeException('Decompression failed');
                     }
-
-                    $parser->push($buffer);
                 }
+
+                $parser->push($buffer);
             }
         }
     }

From 8a8fa9e89b0809301a33fd60531defe84f7b4123 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 21 Aug 2022 22:18:55 -0500
Subject: [PATCH 108/174] Further reduce substr() calls

---
 src/MysqlDataType.php | 41 +++++++++++++++--------------------------
 1 file changed, 15 insertions(+), 26 deletions(-)

diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index 827caaa..64cd967 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -129,14 +129,12 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
                     : self::decodeInt8($string, $offset);
 
             case self::Double:
-                $string = \substr($string, $offset, 8);
                 $offset += 8;
-                return \unpack("e", $string)[1];
+                return \unpack("e", $string, $offset - 8)[1];
 
             case self::Float:
-                $string = \substr($string, $offset, 4);
                 $offset += 4;
-                return \unpack("g", $string)[1];
+                return \unpack("g", $string, $offset - 4)[1];
 
             case self::Date:
             case self::Datetime:
@@ -318,7 +316,7 @@ public static function decodeUnsigned8(string $string, int &$offset = 0): int
 
     public static function decodeInt16(string $string, int &$offset = 0): int
     {
-        $int = \unpack("v", \substr($string, $offset, 2))[1];
+        $int = \unpack("v", $string, $offset)[1];
         $offset += 2;
         if ($int < (1 << 15)) {
             return $int;
@@ -329,9 +327,8 @@ public static function decodeInt16(string $string, int &$offset = 0): int
 
     public static function decodeUnsigned16(string $string, int &$offset = 0): int
     {
-        $result = \unpack("v", \substr($string, $offset, 2))[1];
         $offset += 2;
-        return $result;
+        return \unpack("v", $string, $offset - 2)[1];
     }
 
     public static function decodeInt24(string $string, int &$offset = 0): int
@@ -354,26 +351,24 @@ public static function decodeUnsigned24(string $string, int &$offset = 0): int
 
     public static function decodeInt32(string $string, int &$offset = 0): int
     {
-        $string = \substr($string, $offset, 4);
         $offset += 4;
 
         if (\PHP_INT_SIZE > 4) {
-            $int = \unpack("V", $string)[1];
+            $int = \unpack("V", $string, $offset - 4)[1];
             if ($int < (1 << 31)) {
                 return $int;
             }
             return $int << 32 >> 32;
         }
 
-        return \unpack("V", $string)[1];
+        return \unpack("V", $string, $offset - 4)[1];
     }
 
     public static function decodeUnsigned32(string $string, int &$offset = 0): int
     {
-        $string = \substr($string, $offset, 4);
+        $result = \unpack("V", $string, $offset)[1];
         $offset += 4;
 
-        $result = \unpack("V", $string)[1];
         if ($result < 0) {
             throw new \RuntimeException('Expecting a non-negative integer');
         }
@@ -383,25 +378,22 @@ public static function decodeUnsigned32(string $string, int &$offset = 0): int
 
     public static function decodeUnsigned32WithGmp(string $string, int &$offset = 0): int|string
     {
-        $string = \substr($string, $offset, 4);
         $offset += 4;
 
         if (\PHP_INT_SIZE > 4) {
-            return \unpack("V", $string)[1];
+            return \unpack("V", $string, $offset - 4)[1];
         }
 
         \assert(\extension_loaded("gmp"), "The GMP extension is required for UNSIGNED INT fields on 32-bit systems");
         /** @psalm-suppress UndefinedConstant */
-        return \gmp_strval(\gmp_import(\substr($string, 0, 4), 1, \GMP_LSW_FIRST));
+        return \gmp_strval(\gmp_import(\substr($string, $offset - 4, 4), 1, \GMP_LSW_FIRST));
     }
 
     public static function decodeInt64(string $string, int &$offset = 0): int
     {
-        $string = \substr($string, $offset, 8);
-        $offset += 8;
-
         if (\PHP_INT_SIZE > 4) {
-            return \unpack("P", $string)[1];
+            $offset += 8;
+            return \unpack("P", $string, $offset - 8)[1];
         }
 
         throw new \RuntimeException('64-bit integers are not supported by 32-bit builds of PHP');
@@ -409,16 +401,15 @@ public static function decodeInt64(string $string, int &$offset = 0): int
 
     public static function decodeInt64WithGmp(string $string, int &$offset = 0): int|string
     {
-        $string = \substr($string, $offset, 8);
         $offset += 8;
 
         if (\PHP_INT_SIZE > 4) {
-            return \unpack("P", $string)[1];
+            return \unpack("P", $string, $offset - 8)[1];
         }
 
         \assert(\extension_loaded("gmp"), "The GMP extension is required for BIGINT fields on 32-bit systems");
         /** @psalm-suppress UndefinedConstant */
-        return \gmp_strval(\gmp_import(\substr($string, 0, 8), 1, \GMP_LSW_FIRST));
+        return \gmp_strval(\gmp_import(\substr($string, $offset - 8, 8), 1, \GMP_LSW_FIRST));
     }
 
     public static function decodeUnsigned64(string $string, int &$offset = 0): int
@@ -427,10 +418,9 @@ public static function decodeUnsigned64(string $string, int &$offset = 0): int
             throw new \RuntimeException('64-bit integers are not supported by 32-bit builds of PHP');
         }
 
-        $string = \substr($string, $offset, 8);
+        $result = \unpack("P", $string, $offset)[1];
         $offset += 8;
 
-        $result = \unpack("P", $string)[1];
         if ($result < 0) {
             throw new \RuntimeException('Expecting a non-negative integer');
         }
@@ -440,12 +430,11 @@ public static function decodeUnsigned64(string $string, int &$offset = 0): int
 
     public static function decodeUnsigned64WithGmp(string $string, int &$offset = 0): string
     {
-        $string = \substr($string, $offset, 8);
         $offset += 8;
 
         \assert(\extension_loaded("gmp"), "The GMP extension is required for UNSIGNED BIGINT fields");
         /** @psalm-suppress UndefinedConstant */
-        return \gmp_strval(\gmp_import(\substr($string, 0, 8), 1, \GMP_LSW_FIRST));
+        return \gmp_strval(\gmp_import(\substr($string, $offset - 8, 8), 1, \GMP_LSW_FIRST));
     }
 
     public static function encodeInt(int $int): string

From ba965d4df9d8de7f0d9f8e3deac2f7f9807ab685 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 21 Aug 2022 22:20:23 -0500
Subject: [PATCH 109/174] Refactor date and time decoding

---
 src/MysqlDataType.php | 134 +++++++++++++++++++++++++-----------------
 test/LinkTest.php     |   2 +-
 2 files changed, 80 insertions(+), 56 deletions(-)

diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index 64cd967..b44bbec 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -139,63 +139,10 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
             case self::Date:
             case self::Datetime:
             case self::Timestamp:
-                $string = \substr($string, $offset, 12);
-                $year = $month = $day = $hour = $minute = $second = $microsecond = 0;
-                switch ($length = \ord($string) + 1) {
-                    case 12:
-                        $microsecond = self::decodeUnsigned32(\substr($string, 8));
-                        // no break
-                    case 8:
-                        $second = \ord($string[7]);
-                        $minute = \ord($string[6]);
-                        $hour = \ord($string[5]);
-                        // no break
-                    case 5:
-                        $day = \ord($string[4]);
-                        $month = \ord($string[3]);
-                        $year = self::decodeUnsigned16(\substr($string, 1));
-                        // no break
-                    case 1:
-                        break;
-
-                    default:
-                        throw new SqlException("Unexpected string length for date in binary protocol: " . ($length - 1));
-                }
-                $offset += $length;
-                return \str_pad((string) $year, 2, "0", \STR_PAD_LEFT)
-                    . "-" . \str_pad((string) $month, 2, "0", \STR_PAD_LEFT)
-                    . "-" . \str_pad((string) $day, 2, "0", \STR_PAD_LEFT)
-                    . " " . \str_pad((string) $hour, 2, "0", \STR_PAD_LEFT)
-                    . ":" . \str_pad((string) $minute, 2, "0", \STR_PAD_LEFT)
-                    . ":" . \str_pad((string) $second, 2, "0", \STR_PAD_LEFT)
-                    . "." . \str_pad((string) $microsecond, 5, "0", \STR_PAD_LEFT);
+                return self::decodeDateTime($string, $offset);
 
             case self::Time:
-                $string = \substr($string, $offset, 13);
-                $negative = $day = $hour = $minute = $second = $microsecond = 0;
-                switch ($length = \ord($string) + 1) {
-                    case 13:
-                        $microsecond = self::decodeUnsigned32(\substr($string, 9));
-                        // no break
-                    case 9:
-                        $second = \ord($string[8]);
-                        $minute = \ord($string[7]);
-                        $hour = \ord($string[6]);
-                        $day = self::decodeUnsigned32(\substr($string, 2));
-                        $negative = \ord($string[1]);
-                        // no break
-                    case 1:
-                        break;
-
-                    default:
-                        throw new SqlException("Unexpected string length for time in binary protocol: " . ($length - 1));
-                }
-                $offset += $length;
-                return ($negative ? "" : "-") . \str_pad((string) $day, 2, "0", \STR_PAD_LEFT)
-                    . "d " . \str_pad((string) $hour, 2, "0", \STR_PAD_LEFT)
-                    . ":" . \str_pad((string) $minute, 2, "0", \STR_PAD_LEFT)
-                    . ":" . \str_pad((string) $second, 2, "0", \STR_PAD_LEFT)
-                    . "." . \str_pad((string) $microsecond, 5, "0", \STR_PAD_LEFT);
+                return self::decodeTime($string, $offset);
 
             case self::Null:
                 return null;
@@ -241,6 +188,83 @@ public function decodeText(string $string, int &$offset = 0, int $flags = 0): in
         }
     }
 
+    private static function decodeDateTime(string $string, int &$offset): string
+    {
+        $year = $month = $day = $hour = $minute = $second = $microsecond = 0;
+
+        switch ($length = self::decodeUnsigned8($string, $offset)) {
+            case 11:
+                $position = $offset + 7;
+                $microsecond = self::decodeUnsigned32($string, $position);
+                // no break
+
+            case 7:
+                $position = $offset + 4;
+                $hour = self::decodeUnsigned8($string, $position);
+                $minute = self::decodeUnsigned8($string, $position);
+                $second = self::decodeUnsigned8($string, $position);
+                // no break
+
+            case 4:
+                $position = $offset;
+                $year = self::decodeUnsigned16($string, $position);
+                $month = self::decodeUnsigned8($string, $position);
+                $day = self::decodeUnsigned8($string, $position);
+                // no break
+
+            case 0:
+                break;
+
+            default:
+                throw new SqlException("Unexpected string length for datetime in binary protocol: $length");
+        }
+
+        $offset += $length;
+
+        $result = \sprintf('%04d-%02d-%02d %02d:%02d:%02d', $year, $month, $day, $hour, $minute, $second);
+        if ($microsecond) {
+            $result .= sprintf('.%06d', $microsecond);
+        }
+
+        return $result;
+    }
+
+    private static function decodeTime(string $string, int &$offset): string
+    {
+        $negative = $day = $hour = $minute = $second = $microsecond = 0;
+
+        switch ($length = self::decodeUnsigned8($string, $offset)) {
+            case 12:
+                $position = $offset + 8;
+                $microsecond = self::decodeUnsigned32($string, $position);
+                // no break
+
+            case 8:
+                $position = $offset;
+                $negative = self::decodeUnsigned8($string, $position);
+                $day = self::decodeUnsigned32($string, $position);
+                $hour = self::decodeUnsigned8($string, $position);
+                $minute = self::decodeUnsigned8($string, $position);
+                $second = self::decodeUnsigned8($string, $position);
+                // no break
+
+            case 0:
+                break;
+
+            default:
+                throw new SqlException("Unexpected string length for time in binary protocol: $length");
+        }
+
+        $offset += $length;
+
+        $result = \sprintf('%s%dd %02d:%02d:%02d', ($negative ? "-" : ""), $day, $hour, $minute, $second);
+        if ($microsecond) {
+            $result .= sprintf('.%06d', $microsecond);
+        }
+
+        return $result;
+    }
+
     private static function decodeJson(string $data): string
     {
         if (\strncmp(self::ENCODED_JSON_PREFIX, $data, \strlen(self::ENCODED_JSON_PREFIX)) !== 0) {
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 05c8647..9830fcb 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -303,7 +303,7 @@ public function testExecute()
             $got[] = \array_values($row);
         }
         $this->assertCount(2, $got);
-        $this->assertSame([[2, 2, 3, '1970-01-01 00:00:00.00000'], [4, 4, 5, '1970-01-01 00:00:00.00000']], $got);
+        $this->assertSame([[2, 2, 3, self::EPOCH], [4, 4, 5, self::EPOCH]], $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);

From 8a5be61ecb35c37cc70fa78760584dbc1787cbb7 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 27 Aug 2022 13:46:04 -0500
Subject: [PATCH 110/174] Only return date for DATE and fix format for TIME

---
 src/MysqlDataType.php | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index b44bbec..c5abc37 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -137,9 +137,11 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
                 return \unpack("g", $string, $offset - 4)[1];
 
             case self::Date:
+                return self::decodeDateTime($string, true, $offset);
+
             case self::Datetime:
             case self::Timestamp:
-                return self::decodeDateTime($string, $offset);
+                return self::decodeDateTime($string, false, $offset);
 
             case self::Time:
                 return self::decodeTime($string, $offset);
@@ -221,7 +223,12 @@ private static function decodeDateTime(string $string, int &$offset): string
 
         $offset += $length;
 
-        $result = \sprintf('%04d-%02d-%02d %02d:%02d:%02d', $year, $month, $day, $hour, $minute, $second);
+        $result = \sprintf('%04d-%02d-%02d', $year, $month, $day);
+        if ($dateOnly) {
+            return $result;
+        }
+
+        $result .= \sprintf(' %02d:%02d:%02d', $hour, $minute, $second);
         if ($microsecond) {
             $result .= sprintf('.%06d', $microsecond);
         }
@@ -257,7 +264,9 @@ private static function decodeTime(string $string, int &$offset): string
 
         $offset += $length;
 
-        $result = \sprintf('%s%dd %02d:%02d:%02d', ($negative ? "-" : ""), $day, $hour, $minute, $second);
+        $hour += $day * 24;
+
+        $result = \sprintf('%s%02d:%02d:%02d', ($negative ? "-" : ""), $hour, $minute, $second);
         if ($microsecond) {
             $result .= sprintf('.%06d', $microsecond);
         }

From ab9cd08e2cf34efe65b030cd367999aec91ed774 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 27 Aug 2022 13:48:10 -0500
Subject: [PATCH 111/174] Add MysqlDataType tests

---
 test/LinkTest.php          |  36 +++++-------
 test/MysqlDataTypeTest.php | 111 +++++++++++++++++++++++++++++++++++++
 test/MysqlTestCase.php     |  19 +++++++
 test/initialize.php        |   7 +--
 4 files changed, 145 insertions(+), 28 deletions(-)
 create mode 100644 test/MysqlDataTypeTest.php
 create mode 100644 test/MysqlTestCase.php

diff --git a/test/LinkTest.php b/test/LinkTest.php
index 9830fcb..eeef93b 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -3,15 +3,14 @@
 namespace Amp\Mysql\Test;
 
 use Amp\Mysql\MysqlColumnDefinition;
-use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlDataType;
 use Amp\Mysql\MysqlLink;
 use Amp\Mysql\MysqlResult;
-use Amp\PHPUnit\AsyncTestCase;
 use Amp\Sql\QueryError;
 use Amp\Sql\Result;
+use Amp\Sql\SqlException;
 
-abstract class LinkTest extends AsyncTestCase
+abstract class LinkTest extends MysqlTestCase
 {
     public const EPOCH = '1970-01-01 00:00:00';
 
@@ -20,16 +19,6 @@ abstract class LinkTest extends AsyncTestCase
      */
     abstract protected function getLink(bool $useCompression = false): MysqlLink;
 
-    protected function getConfig(bool $useCompression = false): MysqlConfig
-    {
-        $config = MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, 'test');
-        if ($useCompression) {
-            $config = $config->withCompression();
-        }
-
-        return $config;
-    }
-
     public function testQuery()
     {
         $db = $this->getLink();
@@ -160,7 +149,7 @@ public function testPrepared()
     {
         $db = $this->getLink(true);
 
-        $stmt = $db->prepare("SELECT id AS no, a AS d, b, c FROM main AS table_alias WHERE b = :num AND c = ?");
+        $stmt = $db->prepare("SELECT id AS no, a AS d, b, c FROM main AS table_alias WHERE b = ? AND c = :date AND d = :data");
 
         $base = [
             "catalog" => "def",
@@ -174,12 +163,12 @@ public function testPrepared()
             "decimals" => 0,
         ];
 
-        $this->assertEquals($stmt->getColumnDefinitions(), [
+        $this->assertEquals([
             new MysqlColumnDefinition(...\array_merge($base, ["name" => "no", "originalName" => "id", "flags" => 16899])),
             new MysqlColumnDefinition(...\array_merge($base, ["name" => "d", "originalName" => "a"])),
             new MysqlColumnDefinition(...\array_merge($base, ["name" => "b", "originalName" => "b"])),
             new MysqlColumnDefinition(...\array_merge($base, ["name" => "c", "originalName" => "c", "type" => MysqlDataType::Datetime, "length" => 19, "flags" => 128])),
-        ]);
+        ], $stmt->getColumnDefinitions());
 
         $base = [
             "name" => "?",
@@ -194,13 +183,14 @@ public function testPrepared()
             "decimals" => 0,
         ];
 
-        $this->assertEquals($stmt->getParameterDefinitions(), [
+        $this->assertEquals([
             new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::LongLong, "flags" => 128])),
             new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::Datetime, "length" => 104, "decimals" => 6, "charset" => 45])),
-        ]);
+            new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::VarString, "length" => 65532, "decimals" => 31, "charset" => 45])),
+        ], $stmt->getParameterDefinitions());
 
-        $stmt->bind("num", 5);
-        $result = $stmt->execute([self::EPOCH]);
+        $stmt->bind("data", 'd');
+        $result = $stmt->execute([0 => 5, 'date' => self::EPOCH]);
         $this->assertInstanceOf(MysqlResult::class, $result);
         $this->assertSame(4, $result->getColumnCount());
         $got = [];
@@ -212,7 +202,7 @@ public function testPrepared()
         $stmt = $db->prepare("SELECT * FROM main WHERE a = ? OR b = ?");
         $result = $stmt->execute([1, 8]);
         $this->assertInstanceOf(MysqlResult::class, $result);
-        $this->assertSame(4, $result->getColumnCount());
+        $this->assertSame(5, $result->getColumnCount());
         $got = [];
         foreach ($result as $row) {
             $got[] = \array_values($row);
@@ -222,7 +212,7 @@ public function testPrepared()
         $stmt = $db->prepare("SELECT * FROM main WHERE a = :a OR b = ?");
         $result = $stmt->execute(["a" => 2, 5]);
         $this->assertInstanceOf(MysqlResult::class, $result);
-        $this->assertSame(4, $result->getColumnCount());
+        $this->assertSame(5, $result->getColumnCount());
         $got = [];
         foreach ($result as $row) {
             $got[] = \array_values($row);
@@ -303,7 +293,7 @@ public function testExecute()
             $got[] = \array_values($row);
         }
         $this->assertCount(2, $got);
-        $this->assertSame([[2, 2, 3, self::EPOCH], [4, 4, 5, self::EPOCH]], $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);
diff --git a/test/MysqlDataTypeTest.php b/test/MysqlDataTypeTest.php
new file mode 100644
index 0000000..3dd94dc
--- /dev/null
+++ b/test/MysqlDataTypeTest.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Amp\Mysql\Test;
+
+use Amp\Mysql\MysqlConnection;
+use Amp\Mysql\SocketMysqlConnector;
+
+class MysqlDataTypeTest extends MysqlTestCase
+{
+    private readonly MysqlConnection $connection;
+
+    private readonly \DateTimeImmutable $now;
+
+    public function setUp(): void
+    {
+        parent::setUp();
+
+        $this->connection = (new SocketMysqlConnector)->connect($this->getConfig());
+        $this->now = new \DateTimeImmutable();
+    }
+
+    protected function assertResult(int|float|string|null $expected, string $type): void
+    {
+        $result = $this->connection->execute("SELECT CAST(:expected AS $type) AS data", ['expected' => $expected]);
+
+        foreach ($result as $row) {
+            $data = $row['data'];
+
+            if (\is_float($expected)) {
+                self::assertEqualsWithDelta($expected, $data, 1E-5);
+            } else {
+                self::assertSame($expected, $data);
+            }
+        }
+    }
+
+    public function testDate(): void
+    {
+        $this->assertResult('1970-01-01', 'DATE');
+        $this->assertResult('2020-03-17', 'DATE');
+        $this->assertResult($this->now->format('Y-m-d'), 'DATE');
+    }
+
+    public function testDateTime(): void
+    {
+        $this->assertResult('1970-01-01 00:00:00', 'DATETIME(6)');
+        $this->assertResult($this->now->format('Y-m-d H:i:s.u'), 'DATETIME(6)');
+    }
+
+    public function testTime(): void
+    {
+        $this->assertResult('184:15:56.271282', 'TIME(6)');
+        $this->assertResult($this->now->format('H:i:s.u'), 'TIME(6)');
+    }
+
+    public function testFloatAndDouble(): void
+    {
+        $double = 3.1415926;
+        $this->assertResult($double, 'FLOAT');
+        $this->assertResult($double, 'DOUBLE');
+    }
+
+    public function testInteger(): void
+    {
+        $this->assertResult(-12345, 'SIGNED');
+        $this->assertResult(12345, 'UNSIGNED');
+    }
+
+    public function provideJsonData(): \Generator
+    {
+        yield 'object' => [<<<JSON
+            {
+                "object": {"key": "value"},
+                "array": [1, 2, 3],
+                "string": "value",
+                "integer": 42,
+                "float": 3.1415926,
+                "boolean": true,
+                "null": null
+            }
+            JSON, false];
+
+        yield 'array' => ['[1, 2, 3]', true];
+
+        yield 'float' => ['3.1415926', true];
+
+        yield 'integer' => ['42', true];
+
+        yield 'boolean' => ['true', true];
+
+        yield 'null' => ['null', true];
+    }
+
+    /**
+     * @dataProvider provideJsonData
+     */
+    public function testJson(mixed $json, bool $same): void
+    {
+        $result = $this->connection->execute("SELECT CAST(? AS JSON) AS data", [$json]);
+
+        foreach ($result as $row) {
+            $expected = \json_decode($json);
+            $actual = \json_decode($row['data']);
+            if ($same) {
+                self::assertSame($expected, $actual);
+            } else {
+                self::assertEquals($expected, $actual);
+            }
+        }
+    }
+}
diff --git a/test/MysqlTestCase.php b/test/MysqlTestCase.php
new file mode 100644
index 0000000..d8cc51a
--- /dev/null
+++ b/test/MysqlTestCase.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Amp\Mysql\Test;
+
+use Amp\Mysql\MysqlConfig;
+use Amp\PHPUnit\AsyncTestCase;
+
+abstract class MysqlTestCase extends AsyncTestCase
+{
+    protected function getConfig(bool $useCompression = false): MysqlConfig
+    {
+        $config = MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, 'test');
+        if ($useCompression) {
+            $config = $config->withCompression();
+        }
+
+        return $config;
+    }
+}
diff --git a/test/initialize.php b/test/initialize.php
index ec5dbf9..c20c623 100644
--- a/test/initialize.php
+++ b/test/initialize.php
@@ -6,13 +6,10 @@ 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, PRIMARY KEY (id))");
+    $db->query("CREATE TABLE test.main (id INT NOT NULL AUTO_INCREMENT, a INT, b INT, c DATETIME, d VARCHAR(255), PRIMARY KEY (id))");
 
     $epoch = LinkTest::EPOCH;
-    $db->query("INSERT INTO test.main (a, b, c) VALUES (1, 2, '$epoch'), (2, 3, '$epoch'), (3, 4, '$epoch'), (4, 5, '$epoch'), (5, 6, '$epoch')");
-
-    $db->query("CREATE TABLE test.json (a JSON)");
-    $db->query("INSERT INTO test.json VALUES ('{\"key\": \"value\"}')");
+    $db->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->close();
 }

From bb22cf8f8ab5fbf44d5fff8cc10a1c45cf1675b1 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 27 Aug 2022 13:59:36 -0500
Subject: [PATCH 112/174] Refactor packing sending

Also fixed JSON encoding/decoding in prepared statements.
---
 src/Internal/ConnectionProcessor.php      | 234 +++++++++++-----------
 src/Internal/MysqlConnectionStatement.php |  23 +--
 src/MysqlDataType.php                     |  53 +++--
 test/LinkTest.php                         |  12 +-
 4 files changed, 164 insertions(+), 158 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index ec47311..3edd50b 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -25,6 +25,9 @@
 /** @internal */
 class ConnectionProcessor implements TransientResource
 {
+    private const COMPRESSION_MINIMUM_LENGTH = 860;
+    private const MAX_PACKET_LENGTH = 0xffffff;
+
     const STATEMENT_PARAM_REGEX = <<<'REGEX'
         [
             # Skip all quoted groups.
@@ -327,7 +330,7 @@ public function useDatabase(string $database): Future
         return $this->startCommand(function () use ($database): void {
             /** @psalm-suppress PropertyTypeCoercion */
             $this->config = $this->config->withDatabase($database);
-            $this->sendPacket("\x02$database");
+            $this->write("\x02$database");
         });
     }
 
@@ -337,7 +340,7 @@ public function query(string $query): Future
         return $this->startCommand(function () use ($query): void {
             $this->query = $query;
             $this->parseCallback = $this->handleQuery(...);
-            $this->sendPacket("\x03$query");
+            $this->write("\x03$query");
         });
     }
 
@@ -357,7 +360,7 @@ public function prepare(string $query): Future
                 return "?";
             }, $query);
 
-            $this->sendPacket("\x16$query");
+            $this->write("\x16$query");
         });
     }
 
@@ -379,7 +382,7 @@ public function changeUser($user, $pass, $db = null) {
             }
             $payload .= "$db\0";
 
-            $this->sendPacket($payload);
+            $this->write($payload);
             $this->parseCallback = [$this, "authSwitchRequest"];
         });
     }
@@ -389,7 +392,7 @@ public function changeUser($user, $pass, $db = null) {
     public function ping(): Future
     {
         return $this->startCommand(function (): void {
-            $this->sendPacket("\x0e");
+            $this->write("\x0e");
         });
     }
 
@@ -397,20 +400,20 @@ public function ping(): Future
     public function resetConnection(): Future
     {
         return $this->startCommand(function (): void {
-            $this->sendPacket("\x1f");
+            $this->write("\x1f");
         });
     }
 
     /** @see 14.7.5 COM_STMT_SEND_LONG_DATA */
     public function bindParam(int $stmtId, int $paramId, string $data): void
     {
-        $payload = "\x18";
-        $payload .= MysqlDataType::encodeInt32($stmtId);
-        $payload .= MysqlDataType::encodeInt16($paramId);
-        $payload .= $data;
-        $this->appendTask(function () use ($payload) {
+        $payload = ["\x18"];
+        $payload[] = MysqlDataType::encodeInt32($stmtId);
+        $payload[] = MysqlDataType::encodeInt16($paramId);
+        $payload[] = $data;
+        $this->appendTask(function () use ($payload): void {
             $this->resetIds();
-            $this->sendPacket($payload);
+            $this->write(\implode($payload));
             $this->ready();
         });
     }
@@ -428,42 +431,55 @@ public function execute(int $stmtId, string $query, array $params, array $prebou
     {
         $deferred = new DeferredFuture;
         $this->appendTask(function () use ($stmtId, $query, $params, $prebound, $data, $deferred): void {
-            $payload = "\x17";
-            $payload .= MysqlDataType::encodeInt32($stmtId);
-            $payload .= \chr(0); // cursor flag // @TODO cursor types?!
-            $payload .= MysqlDataType::encodeInt32(1);
+            $payload = ["\x17"];
+            $payload[] = MysqlDataType::encodeInt32($stmtId);
+            $payload[] = \chr(0); // cursor flag // @TODO cursor types?!
+            $payload[] = MysqlDataType::encodeInt32(1);
+
             $paramCount = \count($params);
             $bound = (!empty($data) || !empty($prebound)) ? 1 : 0;
-            $types = "";
-            $values = "";
+            $types = [];
+            $values = [];
+
             if ($paramCount) {
                 $args = $data + \array_fill(0, $paramCount, null);
                 \ksort($args);
                 $args = \array_slice($args, 0, $paramCount);
-                $nullOff = \strlen($payload);
-                $payload .= \str_repeat("\0", ($paramCount + 7) >> 3);
+                $paramList = \str_repeat("\0", ($paramCount + 7) >> 3);
                 foreach ($args as $paramId => $param) {
                     if ($param === null) {
-                        $offset = $nullOff + ($paramId >> 3);
-                        $payload[$offset] = $payload[$offset] | \chr(1 << ($paramId % 8));
+                        $offset = ($paramId >> 3);
+                        $paramList[$offset] = $paramList[$offset] | \chr(1 << ($paramId & 0x7));
                     } else {
                         $bound = 1;
                     }
 
-                    /** @var MysqlDataType $type */
-                    [$type, $value] = MysqlDataType::encodeBinary($param);
+                    $paramType = $params[$paramId]->type;
+
                     if (isset($prebound[$paramId])) {
-                        $types .= \chr(MysqlDataType::LongBlob->value);
-                    } else {
-                        $types .= \chr($type->value);
+                        if (!$paramType->isBindable()) {
+                            throw new SqlException("Cannot use bind with columns of type " . \strtoupper($paramType->name));
+                        }
+
+                        $types[] = MysqlDataType::encodeInt16(MysqlDataType::LongBlob->value);
+                        continue;
+                    }
+
+                    [$encodedType, $value] = MysqlDataType::encodeBinary($param);
+
+                    if ($paramType === MysqlDataType::Json && $encodedType === MysqlDataType::LongBlob) {
+                        $encodedType = $paramType;
                     }
-                    $types .= "\0";
-                    $values .= $value;
+
+                    $types[] = MysqlDataType::encodeInt16($encodedType->value);
+                    $values[] = $value;
                 }
-                $payload .= \chr($bound);
+
+                $payload[] = $paramList;
+                $payload[] = \chr($bound);
                 if ($bound) {
-                    $payload .= $types;
-                    $payload .= $values;
+                    $payload[] = \implode($types);
+                    $payload[] = \implode($values);
                 }
             }
 
@@ -471,7 +487,7 @@ public function execute(int $stmtId, string $query, array $params, array $prebou
 
             $this->resetIds();
             $this->enqueueDeferred($deferred);
-            $this->sendPacket($payload);
+            $this->write(\implode($payload));
             // apparently LOAD DATA LOCAL INFILE requests are not supported via prepared statements
             $this->packetCallback = $this->handleExecute(...);
         });
@@ -485,7 +501,7 @@ public function closeStmt(int $stmtId): void
         $this->appendTask(function () use ($payload): void {
             if ($this->connectionState === ConnectionState::Ready) {
                 $this->resetIds();
-                $this->sendPacket($payload);
+                $this->write($payload);
                 $this->resetIds(); // does not expect a reply - must be reset immediately
             }
             $this->ready();
@@ -496,7 +512,7 @@ public function closeStmt(int $stmtId): void
     public function listFields(string $table, string $like = "%"): Future
     {
         return $this->startCommand(function () use ($table, $like): void {
-            $this->sendPacket("\x04$table\0$like");
+            $this->write("\x04$table\0$like");
             $this->parseCallback = $this->handleFieldList(...);
         });
     }
@@ -521,7 +537,7 @@ public function listAllFields(string $table, string $like = "%"): Future
     public function createDatabase(string $db): Future
     {
         return $this->startCommand(function () use ($db): void {
-            $this->sendPacket("\x05$db");
+            $this->write("\x05$db");
         });
     }
 
@@ -529,7 +545,7 @@ public function createDatabase(string $db): Future
     public function dropDatabase(string $db): Future
     {
         return $this->startCommand(function () use ($db): void {
-            $this->sendPacket("\x06$db");
+            $this->write("\x06$db");
         });
     }
 
@@ -539,7 +555,7 @@ public function dropDatabase(string $db): Future
     public function refresh(int $subcommand): Future
     {
         return $this->startCommand(function () use ($subcommand): void {
-            $this->sendPacket("\x07" . \chr($subcommand));
+            $this->write("\x07" . \chr($subcommand));
         });
     }
 
@@ -547,7 +563,7 @@ public function refresh(int $subcommand): Future
     public function shutdown(): Future
     {
         return $this->startCommand(function (): void {
-            $this->sendPacket("\x08\x00"); /* SHUTDOWN_DEFAULT / SHUTDOWN_WAIT_ALL_BUFFERS, only one in use */
+            $this->write("\x08\x00"); /* SHUTDOWN_DEFAULT / SHUTDOWN_WAIT_ALL_BUFFERS, only one in use */
         });
     }
 
@@ -555,7 +571,7 @@ public function shutdown(): Future
     public function statistics(): Future
     {
         return $this->startCommand(function (): void {
-            $this->sendPacket("\x09");
+            $this->write("\x09");
             $this->parseCallback = $this->readStatistics(...);
         });
     }
@@ -564,7 +580,7 @@ public function statistics(): Future
     public function processInfo(): Future
     {
         return $this->startCommand(function (): void {
-            $this->sendPacket("\x0a");
+            $this->write("\x0a");
             $this->query("SHOW PROCESSLIST");
         });
     }
@@ -573,7 +589,7 @@ public function processInfo(): Future
     public function killProcess(int $process): Future
     {
         return $this->startCommand(function () use ($process): void {
-            $this->sendPacket("\x0c" . MysqlDataType::encodeInt32($process));
+            $this->write("\x0c" . MysqlDataType::encodeInt32($process));
         });
     }
 
@@ -581,7 +597,7 @@ public function killProcess(int $process): Future
     public function debugStdout(): Future
     {
         return $this->startCommand(function (): void {
-            $this->sendPacket("\x0d");
+            $this->write("\x0d");
         });
     }
 
@@ -593,7 +609,7 @@ public function resetStmt(int $stmtId): Future
         $this->appendTask(function () use ($payload, $deferred): void {
             $this->resetIds();
             $this->enqueueDeferred($deferred);
-            $this->sendPacket($payload);
+            $this->write($payload);
         });
         return $deferred->getFuture();
     }
@@ -606,7 +622,7 @@ public function fetchStmt(int $stmtId): Future
         $this->appendTask(function () use ($payload, $deferred): void {
             $this->resetIds();
             $this->enqueueDeferred($deferred);
-            $this->sendPacket($payload);
+            $this->write($payload);
         });
         return $deferred->getFuture();
     }
@@ -845,9 +861,9 @@ private function handleLocalInfileRequest(string $packet): void
                 $fileHandle = File\openFile($filePath, 'r');
 
                 while (null !== ($chunk = $fileHandle->read())) {
-                    $this->sendPacket($chunk);
+                    $this->write($chunk);
                 }
-                $this->sendPacket("");
+                $this->write("");
             } catch (\Throwable $e) {
                 $this->dequeueDeferred()->error(new ConnectionException("Failed to transfer a file to the server", 0, $e));
             }
@@ -1180,7 +1196,7 @@ private function readStatistics(string $packet): void
     public function sendClose(): Future
     {
         return $this->startCommand(function (): void {
-            $this->sendPacket("\x01");
+            $this->write("\x01");
             $this->connectionState = ConnectionState::Closing;
         });
     }
@@ -1214,9 +1230,29 @@ private function free(?\Throwable $exception = null): void
         }
     }
 
+    private function resetIds(): void
+    {
+        $this->seqId = $this->compressionId = -1;
+    }
+
     private function write(string $packet): void
     {
-        $packet = $this->compilePacket($packet);
+        \assert(!$this->socket->isClosed(), 'The connection was closed during a call to write');
+
+        while (\strlen($packet) >= self::MAX_PACKET_LENGTH) {
+            $this->sendPacket(\substr($packet, 0, self::MAX_PACKET_LENGTH));
+            $packet = \substr($packet, self::MAX_PACKET_LENGTH);
+        }
+
+        $this->sendPacket($packet);
+    }
+
+    /**
+     * @see 14.1.2 MySQL Packets
+     */
+    private function sendPacket(string $out): void
+    {
+        $packet = MysqlDataType::encodeInt32(\strlen($out) | (++$this->seqId << 24)) . $out;
 
         // @codeCoverageIgnoreStart
         \assert((function () use ($packet) {
@@ -1239,49 +1275,31 @@ private function write(string $packet): void
             $packet = $this->compressPacket($packet);
         }
 
-        \assert(!$this->socket->isClosed(), 'The connection was closed during a call to write');
         $this->socket->write($packet);
     }
 
-    private function resetIds(): void
-    {
-        $this->seqId = $this->compressionId = -1;
-    }
-
-    private function compilePacket(string $pending): string
-    {
-        $packet = "";
-        do {
-            $length = \strlen($pending);
-            if ($length >= (1 << 24) - 1) {
-                $out = \substr($pending, 0, (1 << 24) - 1);
-                $pending = \substr($pending, (1 << 24) - 1);
-                $length = (1 << 24) - 1;
-            } else {
-                $out = $pending;
-                $pending = "";
-            }
-            $packet .= \substr_replace(\pack("V", $length), \chr(++$this->seqId), 3, 1) . $out; // expects $length < (1 << 24) - 1
-        } while ($pending !== "");
-
-        return $packet;
-    }
-
+    /**
+     * @see 14.4 Compression
+     */
     private function compressPacket(string $packet): string
     {
-        if ($packet === "") {
-            return "";
-        }
-
         $length = \strlen($packet);
-        $deflated = \zlib_encode($packet, ZLIB_ENCODING_DEFLATE);
+        if ($length < self::COMPRESSION_MINIMUM_LENGTH) {
+            return $this->makeCompressedPacket(0, $packet);
+        }
 
+        $deflated = \zlib_encode($packet, \ZLIB_ENCODING_DEFLATE);
         if ($length < \strlen($deflated)) {
-            return \substr_replace(\pack("V", \strlen($packet)), \chr(++$this->compressionId), 3, 1) . "\0\0\0" . $packet;
+            return $this->makeCompressedPacket(0, $packet);
         }
 
-        return \substr_replace(\pack("V", \strlen($deflated)), \chr(++$this->compressionId), 3, 1)
-            . \substr(\pack("V", $length), 0, 3) . $deflated;
+        return $this->makeCompressedPacket($length, $deflated);
+    }
+
+    private function makeCompressedPacket(int $uncompressed, string $packet): string
+    {
+        return MysqlDataType::encodeInt32(\strlen($packet) | (++$this->compressionId << 24))
+            . MysqlDataType::encodeInt24($uncompressed) . $packet;
     }
 
     /**
@@ -1323,7 +1341,7 @@ private function parseCompression(Parser $parser): \Generator
     private function parseMysql(): \Generator
     {
         while (true) {
-            $packet = [];
+            $packet = "";
 
             do {
                 $buffer = yield 4;
@@ -1333,12 +1351,12 @@ private function parseMysql(): \Generator
                 $this->seqId = MysqlDataType::decodeUnsigned8($buffer, $offset);
 
                 if ($length > 0) {
-                    $packet[] = yield $length;
+                    $packet .= yield $length;
                 }
-            } while ($length === 0xffffff);
+            } while ($length === self::MAX_PACKET_LENGTH);
 
-            if ($packet) {
-                $this->parsePayload(\implode($packet));
+            if (\strlen($packet)) {
+                $this->parsePayload($packet);
             }
         }
     }
@@ -1467,7 +1485,7 @@ private function authSwitchRequest(string $packet): void
                 $length = (int) \strpos($packet, "\0");
                 $pluginName = \substr($packet, 0, $length); // @TODO mysql_native_pass only now...
                 $authPluginData = \substr($packet, $length + 1);
-                $this->sendPacket($this->secureAuth($this->config->getPassword() ?? '', $authPluginData));
+                $this->write($this->secureAuth($this->config->getPassword() ?? '', $authPluginData));
                 break;
             case self::ERR_PACKET:
                 $this->handleError($packet);
@@ -1493,16 +1511,16 @@ private function sendHandshake(bool $inSSL = false): void
 
         $this->capabilities &= $this->serverCapabilities;
 
-        $payload = "";
-        $payload .= \pack("V", $this->capabilities);
-        $payload .= \pack("V", 1 << 24 - 1); // max-packet size
-        $payload .= \chr(MysqlConfig::BIN_CHARSET);
-        $payload .= \str_repeat("\0", 23); // reserved
+        $payload = [];
+        $payload[] = \pack("V", $this->capabilities);
+        $payload[] = \pack("V", self::MAX_PACKET_LENGTH);
+        $payload[] = \chr(MysqlConfig::BIN_CHARSET);
+        $payload[] = \str_repeat("\0", 23); // reserved
 
         if (!$inSSL && ($this->capabilities & self::CLIENT_SSL)) {
             EventLoop::queue(function () use ($payload): void {
                 try {
-                    $this->write($payload);
+                    $this->write(\implode($payload));
 
                     $this->socket->setupTls();
 
@@ -1515,32 +1533,32 @@ private function sendHandshake(bool $inSSL = false): void
             return;
         }
 
-        $payload .= $this->config->getUser()."\0";
+        $payload[] = $this->config->getUser()."\0";
 
         $auth = $this->getAuthData();
         if ($this->capabilities & self::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) {
-            $payload .= MysqlDataType::encodeInt(\strlen($auth));
-            $payload .= $auth;
+            $payload[] = MysqlDataType::encodeInt(\strlen($auth));
+            $payload[] = $auth;
         } elseif ($this->capabilities & self::CLIENT_SECURE_CONNECTION) {
-            $payload .= \chr(\strlen($auth));
-            $payload .= $auth;
+            $payload[] = \chr(\strlen($auth));
+            $payload[] = $auth;
         } else {
-            $payload .= "$auth\0";
+            $payload[] = "$auth\0";
         }
 
         if ($this->capabilities & self::CLIENT_CONNECT_WITH_DB) {
-            $payload .= "{$this->config->getDatabase()}\0";
+            $payload[] = "{$this->config->getDatabase()}\0";
         }
 
         if ($this->capabilities & self::CLIENT_PLUGIN_AUTH) {
-            $payload .= "{$this->authPluginName}\0";
+            $payload[] = "{$this->authPluginName}\0";
         }
 
         if ($this->capabilities & self::CLIENT_CONNECT_ATTRS) {
             // connection attributes?! 5.6.6+ only!
         }
 
-        $this->write($payload);
+        $this->write(\implode($payload));
     }
 
     private function getAuthData(): string
@@ -1578,14 +1596,4 @@ private function getAuthData(): string
 
         return $this->secureAuth($password, $this->authPluginData);
     }
-
-    /** @see 14.1.2 MySQL Packet */
-    protected function sendPacket(string $payload): void
-    {
-        if ($this->connectionState !== ConnectionState::Ready) {
-            throw new \Error("Connection not ready, cannot send any packets");
-        }
-
-        $this->write($payload);
-    }
 }
diff --git a/src/Internal/MysqlConnectionStatement.php b/src/Internal/MysqlConnectionStatement.php
index 7ebc2e7..ebea38a 100644
--- a/src/Internal/MysqlConnectionStatement.php
+++ b/src/Internal/MysqlConnectionStatement.php
@@ -26,7 +26,7 @@ final class MysqlConnectionStatement implements MysqlStatement
     public function __construct(
         ConnectionProcessor $processor,
         private readonly string $query,
-        private readonly int $stmtId,
+        private readonly int $statementId,
         private readonly array $byNamed,
         private readonly MysqlResultProxy $result
     ) {
@@ -69,7 +69,7 @@ public function isClosed(): bool
     public function close(): void
     {
         if ($this->processor) {
-            self::shutdown($this->processor, $this->stmtId, $this->onClose);
+            self::shutdown($this->processor, $this->statementId, $this->onClose);
             $this->processor = null;
         }
     }
@@ -98,22 +98,21 @@ public function bind(int|string $paramId, mixed $data): void
             throw new \TypeError("Data must be scalar or an object that implements __toString method");
         }
 
+        $data = (string) $data;
+
         do {
             $realId = -1;
             while (isset($this->named[++$realId]) || $i-- > 0) {
-                if (!\is_numeric($paramId) && isset($this->named[$realId]) && $this->named[$realId] == $paramId) {
+                if (!\is_numeric($paramId) && isset($this->named[$realId]) && $this->named[$realId] === $paramId) {
                     break;
                 }
             }
 
-            $this->getProcessor()->bindParam($this->stmtId, $realId, (string) $data);
+            $this->getProcessor()->bindParam($this->statementId, $realId, $data);
         } while (isset($array) && $i = \next($array));
 
-        if (isset($this->prebound[$paramId])) {
-            $this->prebound[$paramId] .= (string) $data;
-        } else {
-            $this->prebound[$paramId] = (string) $data;
-        }
+        $prior = $this->prebound[$paramId] ?? '';
+        $this->prebound[$paramId] = $prior . $data;
     }
 
     public function execute(array $params = []): MysqlResult
@@ -142,7 +141,7 @@ public function execute(array $params = []): MysqlResult
         }
 
         return $this->getProcessor()
-            ->execute($this->stmtId, $this->query, $this->result->params, $prebound, $args)
+            ->execute($this->statementId, $this->query, $this->result->params, $prebound, $args)
             ->await();
     }
 
@@ -154,7 +153,7 @@ public function getQuery(): string
     public function reset(): void
     {
         $this->getProcessor()
-            ->resetStmt($this->stmtId)
+            ->resetStmt($this->statementId)
             ->await();
     }
 
@@ -176,7 +175,7 @@ public function getLastUsedAt(): int
     public function __destruct()
     {
         if ($this->processor) {
-            EventLoop::queue(self::shutdown(...), $this->processor, $this->stmtId, $this->onClose);
+            EventLoop::queue(self::shutdown(...), $this->processor, $this->statementId, $this->onClose);
         }
     }
 
diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index c5abc37..461f254 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -21,12 +21,12 @@ enum MysqlDataType: int
     case Time = 0x0b;
     case Datetime = 0x0c;
     case Year = 0x0d;
-    case NewDate = 0x0e;
+    case NewDate = 0x0e; // Internal, not used in protocol, see Date
     case Varchar = 0x0f;
     case Bit = 0x10;
-    case Timestamp2 = 0x11;
-    case Datetime2 = 0x12;
-    case Time2 = 0x13;
+    case Timestamp2 = 0x11; // Internal, not used in protocol, see Timestamp
+    case Datetime2 = 0x12; // Internal, not used in protocol, see DateTime
+    case Time2 = 0x13; // Internal, not used in protocol, see Time
     case Json = 0xf5;
     case NewDecimal = 0xf6;
     case Enum = 0xf7;
@@ -39,11 +39,11 @@ enum MysqlDataType: int
     case String = 0xfe;
     case Geometry = 0xff;
 
-    private const ENCODED_JSON_PREFIX = "base64:type251:";
-
     /**
      * @return array{self, string}
      *
+     * @throws SqlException If the given value cannot be encoded.
+     *
      * @see 14.7.3 Binary Value
      */
     public static function encodeBinary(mixed $param): array
@@ -97,11 +97,8 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
             case self::Bit:
             case self::Decimal:
             case self::NewDecimal:
-                return self::decodeString($string, $offset);
-
             case self::Json:
-                $data = self::decodeString($string, $offset);
-                return self::decodeJson($data);
+                return self::decodeString($string, $offset);
 
             case self::LongLong:
                 return $unsigned
@@ -119,6 +116,7 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
                     : self::decodeInt24($string, $offset);
 
             case self::Short:
+            case self::Year:
                 return $unsigned
                     ? self::decodeUnsigned16($string, $offset)
                     : self::decodeInt16($string, $offset);
@@ -176,21 +174,27 @@ public function decodeText(string $string, int &$offset = 0, int $flags = 0): in
             case self::Int24:
             case self::Short:
             case self::Tiny:
+            case self::Year:
                 return (int) $data;
 
             case self::Double:
             case self::Float:
                 return (float) $data;
 
-            case self::Json:
-                return self::decodeJson($data);
-
             default:
                 return $data;
         }
     }
 
-    private static function decodeDateTime(string $string, int &$offset): string
+    public function isBindable(): bool
+    {
+        return match ($this) {
+            self::Json => false,
+            default => true,
+        };
+    }
+
+    private static function decodeDateTime(string $string, bool $dateOnly, int &$offset): string
     {
         $year = $month = $day = $hour = $minute = $second = $microsecond = 0;
 
@@ -230,7 +234,7 @@ private static function decodeDateTime(string $string, int &$offset): string
 
         $result .= \sprintf(' %02d:%02d:%02d', $hour, $minute, $second);
         if ($microsecond) {
-            $result .= sprintf('.%06d', $microsecond);
+            $result .= \sprintf('.%06d', $microsecond);
         }
 
         return $result;
@@ -268,22 +272,12 @@ private static function decodeTime(string $string, int &$offset): string
 
         $result = \sprintf('%s%02d:%02d:%02d', ($negative ? "-" : ""), $hour, $minute, $second);
         if ($microsecond) {
-            $result .= sprintf('.%06d', $microsecond);
+            $result .= \sprintf('.%06d', $microsecond);
         }
 
         return $result;
     }
 
-    private static function decodeJson(string $data): string
-    {
-        if (\strncmp(self::ENCODED_JSON_PREFIX, $data, \strlen(self::ENCODED_JSON_PREFIX)) !== 0) {
-            return $data; // Data was not base-64 encoded.
-        }
-
-        $data = \substr($data, \strlen(self::ENCODED_JSON_PREFIX));
-        return \base64_decode($data);
-    }
-
     public static function decodeNullTerminatedString(string $string, int &$offset = 0): string
     {
         $length = \strpos($string, "\0", $offset);
@@ -473,7 +467,7 @@ public static function decodeUnsigned64WithGmp(string $string, int &$offset = 0)
     public static function encodeInt(int $int): string
     {
         if ($int < 0xfb) {
-            return \chr($int);
+            return self::encodeInt8($int);
         }
 
         if ($int < (1 << 16)) {
@@ -487,6 +481,11 @@ public static function encodeInt(int $int): string
         return "\xfe" . self::encodeInt64($int);
     }
 
+    public static function encodeInt8(int $int): string
+    {
+        return \chr($int);
+    }
+
     public static function encodeInt16(int $int): string
     {
         return \pack("v", $int);
diff --git a/test/LinkTest.php b/test/LinkTest.php
index eeef93b..b459115 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -416,14 +416,14 @@ public function testInsertSelect()
         $db->close();
     }
 
-    public function testJsonDecoding()
+    public function testBindJson(): void
     {
-        $db = $this->getLink();
+        $statement = $this->getLink()->prepare("SELECT CAST(? AS JSON)");
+        $statement->bind(0, '{"key": "value"}');
 
-        $result = $db->execute("SELECT a FROM test.json");
+        $this->expectException(SqlException::class);
+        $this->expectExceptionMessage("Cannot use bind with columns of type JSON");
 
-        foreach ($result as $row) {
-            $this->assertSame(["a" => '{"key": "value"}'], $row);
-        }
+        $statement->execute();
     }
 }

From 86411d27df7308a7493d56c736994a2e02e23e8c Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 27 Aug 2022 14:00:54 -0500
Subject: [PATCH 113/174] Update PHPUnit config

---
 phpunit.xml.dist | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 58130d5..da7bcf4 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <phpunit
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
+        xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
         backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="test/bootstrap.php"
@@ -12,14 +12,14 @@
         processIsolation="false"
         stopOnFailure="false"
 >
-    <testsuites>
-        <testsuite name="Postgres Client">
-            <directory>test</directory>
-        </testsuite>
-    </testsuites>
-    <filter>
-        <whitelist>
-            <directory suffix=".php">src</directory>
-        </whitelist>
-    </filter>
+  <coverage>
+    <include>
+      <directory suffix=".php">src</directory>
+    </include>
+  </coverage>
+  <testsuites>
+    <testsuite name="MySQL Client">
+      <directory>test</directory>
+    </testsuite>
+  </testsuites>
 </phpunit>

From 9a65b5a62ca6495befe79192c04f4e77b1561d33 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 27 Aug 2022 14:15:27 -0500
Subject: [PATCH 114/174] Fix missed inequalities on ConnectionState

---
 src/Internal/ConnectionProcessor.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 3edd50b..b541ac3 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -665,7 +665,7 @@ private function handleError(string $packet): void
 
         $this->parseCallback = null;
 
-        if ($this->connectionState < ConnectionState::Ready) {
+        if ($this->connectionState === ConnectionState::Connecting) {
             // connection failure
             $this->free(new ConnectionException(\sprintf(
                 'Could not connect to %s: %s %s',
@@ -1271,7 +1271,7 @@ private function sendPacket(string $out): void
         })());
         // @codeCoverageIgnoreEnd
 
-        if (($this->capabilities & self::CLIENT_COMPRESS) && $this->connectionState >= ConnectionState::Ready) {
+        if (($this->capabilities & self::CLIENT_COMPRESS) && $this->connectionState === ConnectionState::Ready) {
             $packet = $this->compressPacket($packet);
         }
 

From c7e05a5cffe8fb293000d5ca8db8ffb0fb4c55c5 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 27 Aug 2022 15:37:56 -0500
Subject: [PATCH 115/174] Use data provider in MysqlDateTypeTest

---
 test/MysqlDataTypeTest.php | 111 +++++++++++++++++++++----------------
 1 file changed, 62 insertions(+), 49 deletions(-)

diff --git a/test/MysqlDataTypeTest.php b/test/MysqlDataTypeTest.php
index 3dd94dc..8e0221e 100644
--- a/test/MysqlDataTypeTest.php
+++ b/test/MysqlDataTypeTest.php
@@ -9,17 +9,65 @@ class MysqlDataTypeTest extends MysqlTestCase
 {
     private readonly MysqlConnection $connection;
 
-    private readonly \DateTimeImmutable $now;
-
     public function setUp(): void
     {
         parent::setUp();
 
         $this->connection = (new SocketMysqlConnector)->connect($this->getConfig());
-        $this->now = new \DateTimeImmutable();
     }
 
-    protected function assertResult(int|float|string|null $expected, string $type): void
+    public function provideDataAndTypes(): array
+    {
+        $now = new \DateTimeImmutable();
+
+        return [
+            // DATE
+            ['0000-00-00', 'DATE'],
+            ['1970-01-01', 'DATE'],
+            ['2020-03-17', 'DATE'],
+            [$now->format('Y-m-d'), 'DATE'],
+
+            // DATETIME
+            ['0000-00-00 00:00:00', 'DATETIME(6)'],
+            ['1970-01-01 00:00:00', 'DATETIME(6)'],
+            [$now->format('Y-m-d H:i:s.u'), 'DATETIME(6)'],
+
+            // TIME
+            ['00:00:00', 'TIME(6)'],
+            ['96:42:23', 'TIME(6)'],
+            ['184:15:56.271282', 'TIME(6)'],
+            [$now->format('H:i:s.u'), 'TIME(6)'],
+
+            // YEAR
+            [0, 'YEAR'],
+            [1901, 'YEAR'],
+            [2155, 'YEAR'],
+
+            // FLOAT
+            [0.0, 'FLOAT'],
+            [\M_PI, 'FLOAT'],
+            [-\M_E, 'FLOAT'],
+
+            // DOUBLE
+            [0.0, 'DOUBLE'],
+            [\M_PI, 'DOUBLE'],
+            [-\M_E, 'DOUBLE'],
+
+            // SIGNED
+            [0, 'SIGNED'],
+            [-12345, 'SIGNED'],
+            [12345, 'UNSIGNED'],
+
+            // UNSIGNED
+            [0, 'UNSIGNED'],
+            [12345, 'UNSIGNED'],
+        ];
+    }
+
+    /**
+     * @dataProvider provideDataAndTypes
+     */
+    public function testDateType(int|float|string|null $expected, string $type): void
     {
         $result = $this->connection->execute("SELECT CAST(:expected AS $type) AS data", ['expected' => $expected]);
 
@@ -34,41 +82,10 @@ protected function assertResult(int|float|string|null $expected, string $type):
         }
     }
 
-    public function testDate(): void
-    {
-        $this->assertResult('1970-01-01', 'DATE');
-        $this->assertResult('2020-03-17', 'DATE');
-        $this->assertResult($this->now->format('Y-m-d'), 'DATE');
-    }
-
-    public function testDateTime(): void
-    {
-        $this->assertResult('1970-01-01 00:00:00', 'DATETIME(6)');
-        $this->assertResult($this->now->format('Y-m-d H:i:s.u'), 'DATETIME(6)');
-    }
-
-    public function testTime(): void
-    {
-        $this->assertResult('184:15:56.271282', 'TIME(6)');
-        $this->assertResult($this->now->format('H:i:s.u'), 'TIME(6)');
-    }
-
-    public function testFloatAndDouble(): void
+    public function provideJsonData(): array
     {
-        $double = 3.1415926;
-        $this->assertResult($double, 'FLOAT');
-        $this->assertResult($double, 'DOUBLE');
-    }
-
-    public function testInteger(): void
-    {
-        $this->assertResult(-12345, 'SIGNED');
-        $this->assertResult(12345, 'UNSIGNED');
-    }
-
-    public function provideJsonData(): \Generator
-    {
-        yield 'object' => [<<<JSON
+        return [
+            'object' => [<<<JSON
             {
                 "object": {"key": "value"},
                 "array": [1, 2, 3],
@@ -78,17 +95,13 @@ public function provideJsonData(): \Generator
                 "boolean": true,
                 "null": null
             }
-            JSON, false];
-
-        yield 'array' => ['[1, 2, 3]', true];
-
-        yield 'float' => ['3.1415926', true];
-
-        yield 'integer' => ['42', true];
-
-        yield 'boolean' => ['true', true];
-
-        yield 'null' => ['null', true];
+            JSON, false],
+            'array' => ['[1, 2, 3]', true],
+            'float' => ['3.1415926', true],
+            'integer' => ['42', true],
+            'boolean' => ['true', true],
+            'null' => ['null', true],
+        ];
     }
 
     /**

From ecf6cdf037eb879c490a66b2b122d2c3b571accf Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 28 Aug 2022 09:40:40 -0500
Subject: [PATCH 116/174] Use $this as flag

Nicely eliminates need to separate decode cases.
---
 src/MysqlDataType.php | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index 461f254..6fd715a 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -135,11 +135,9 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
                 return \unpack("g", $string, $offset - 4)[1];
 
             case self::Date:
-                return self::decodeDateTime($string, true, $offset);
-
             case self::Datetime:
             case self::Timestamp:
-                return self::decodeDateTime($string, false, $offset);
+                return self::decodeDateTime($this, $string, $offset);
 
             case self::Time:
                 return self::decodeTime($string, $offset);
@@ -194,7 +192,7 @@ public function isBindable(): bool
         };
     }
 
-    private static function decodeDateTime(string $string, bool $dateOnly, int &$offset): string
+    private static function decodeDateTime(self $type, string $string, int &$offset): string
     {
         $year = $month = $day = $hour = $minute = $second = $microsecond = 0;
 
@@ -228,7 +226,7 @@ private static function decodeDateTime(string $string, bool $dateOnly, int &$off
         $offset += $length;
 
         $result = \sprintf('%04d-%02d-%02d', $year, $month, $day);
-        if ($dateOnly) {
+        if ($type === self::Date) {
             return $result;
         }
 

From 93d971174501f85c21a198a8c0330feb94488833 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 28 Aug 2022 10:38:18 -0500
Subject: [PATCH 117/174] Update phpbench for v3

---
 benchmarks/AbstractBench.php |   9 +--
 benchmarks/QueryBench.php    | 129 +++++++++++++++++------------------
 composer.json                |   2 +-
 phpbench.json                |   6 +-
 4 files changed, 70 insertions(+), 76 deletions(-)

diff --git a/benchmarks/AbstractBench.php b/benchmarks/AbstractBench.php
index 7efb8d2..99152a2 100644
--- a/benchmarks/AbstractBench.php
+++ b/benchmarks/AbstractBench.php
@@ -4,12 +4,9 @@
 
 class AbstractBench
 {
-    /** @var string */
-    protected $host = '127.0.0.1';
+    protected string $host = 'localhost:10101';
 
-    /** @var string */
-    protected $user = 'root';
+    protected string $user = 'root';
 
-    /** @var string */
-    protected $pass = '';
+    protected string $password = '';
 }
diff --git a/benchmarks/QueryBench.php b/benchmarks/QueryBench.php
index fdc8325..0c6f781 100644
--- a/benchmarks/QueryBench.php
+++ b/benchmarks/QueryBench.php
@@ -2,102 +2,99 @@
 
 namespace Amp\Mysql\Bench;
 
+use Amp\Future;
+use Amp\Mysql\MysqlConnectionPool;
+use Amp\Mysql\MysqlLink;
 use Amp\Mysql\SocketMysqlConnector;
 use Amp\Mysql\MysqlConnection;
 use Amp\Mysql\MysqlConfig;
-use Amp\Mysql\MysqlConnectionPool as ConnectionPool;
-use Amp\Mysql\ResultSet;
-use function Amp\call;
-use function Amp\Promise\wait;
-
-/**
- * @BeforeMethods({"init"})
- * @AfterMethods({"onAfterMethods"})
- * @Iterations(1)
- * @Revs(100)
- * @Warmup(1)
- * @OutputTimeUnit("milliseconds", precision=5)
- */
+use PhpBench\Attributes\AfterMethods;
+use PhpBench\Attributes\BeforeMethods;
+use PhpBench\Attributes\Iterations;
+use PhpBench\Attributes\Revs;
+use PhpBench\Attributes\OutputTimeUnit;
+use PhpBench\Attributes\Warmup;
+use function Amp\async;
+
+#[
+    BeforeMethods('init'),
+    AfterMethods('cleanup'),
+    Iterations(1),
+    Revs(100),
+    Warmup(1),
+    OutputTimeUnit('milliseconds', precision: 5),
+]
 class QueryBench extends AbstractBench
 {
-    /** @var  ConnectionPool */
-    protected $connectionPool;
+    protected MysqlConnectionPool $connectionPool;
 
-    /** @var  MysqlConnection */
-    protected $connection;
+    protected MysqlConnection $connection;
 
-    /** @var  \PDO */
-    protected $pdoConnection;
+    protected \PDO $pdoConnection;
 
-    /** @var int */
-    protected $maxQueries = 10;
+    protected int $maxQueries = 100;
 
     /** @var int */
-    protected $poolLimit = 10;
+    protected int $poolLimit = 10;
 
-    public function init()
+    public function init(): void
     {
-        $config = MysqlConfig::fromString("host=$this->host;user=$this->user;pass=$this->pass");
+        $config = MysqlConfig::fromAuthority($this->host, $this->user, $this->password);
         $connector = new SocketMysqlConnector;
-        $this->connectionPool = new ConnectionPool($config, $this->poolLimit, 10, $connector);
-        $connectionPromise = $connector->connect($config);
-        $this->connection = wait($connectionPromise);
-        $this->pdoConnection = new \PDO("mysql:host=$this->host;port=3306", $this->user, $this->pass);
+
+        $this->connectionPool = new MysqlConnectionPool(
+            config: $config,
+            maxConnections: $this->poolLimit,
+            connector: $connector,
+        );
+
+        $this->connection = $connector->connect($config);
+
+        $this->pdoConnection = new \PDO("mysql:host=$this->host", $this->user, $this->password);
     }
 
-    public function onAfterMethods()
+    public function cleanup(): void
     {
         $this->connectionPool->close();
         $this->connection->close();
     }
 
-    public function benchPdoQueries()
+    public function benchPdoQueries(): void
+    {
+        $statement = $this->pdoConnection->prepare("SELECT ?");
+
+        foreach (\range(1, $this->maxQueries) as $i) {
+            $statement->execute([$i]);
+            $statement->fetch(\PDO::FETCH_ASSOC);
+        }
+    }
+
+    public function benchSyncQueries(): void
     {
-        foreach (\range(1, $this->maxQueries) as $ii) {
-            $resultSet = $this->pdoConnection->query("SELECT $ii");
-            $resultSet->fetch(\PDO::FETCH_ASSOC);
+        $statement = $this->connection->prepare("SELECT ?");
+
+        foreach (\range(1, $this->maxQueries) as $i) {
+            \iterator_to_array($statement->execute([$i]));
         }
     }
 
-    public function benchSyncQueries()
+    private function runAsyncQueries(MysqlLink $link): void
     {
-        wait(call(function () {
-            $connection = $this->connection;
-            foreach (\range(1, $this->maxQueries) as $i) {
-                /** @var ResultSet $resultSet */
-                $resultSet = yield $connection->query("SELECT $i");
-                yield $resultSet->advance();
-            }
-        }));
+        $statement = $link->prepare("SELECT ?");
+
+        Future\await(\array_map(
+            fn (int $i) => async(fn () => \iterator_to_array($statement->execute([$i]))),
+            \range(1, $this->maxQueries),
+        ));
     }
 
-    public function benchAsyncQueries()
+    public function benchAsyncQueriesUsingSingleConnection(): void
     {
-        wait(call(function () {
-            $connection = $this->connection;
-            /** @var ResultSet[] $resultSets */
-            $resultSets = yield \array_map(function ($i) use ($connection) {
-                return $connection->query("SELECT $i");
-            }, \range(1, $this->maxQueries));
-            yield \array_map(function ($resultSet) {
-                /** @var ResultSet $resultSet */
-                return $resultSet->advance();
-            }, $resultSets);
-        }));
+        $this->runAsyncQueries($this->connection);
     }
 
-    public function benchAsyncQueriesUsingPool()
+    public function benchAsyncQueriesUsingConnectionPool(): void
     {
-        wait(call(function () {
-            $connection = $this->connectionPool;
-            /** @var ResultSet[] $resultSets */
-            $resultSets = yield \array_map(function ($i) use ($connection) {
-                return $connection->query("SELECT $i");
-            }, \range(1, $this->maxQueries));
-            yield \array_map(function ($resultSet) {
-                /** @var ResultSet $resultSet */
-                return $resultSet->advance();
-            }, $resultSets);
-        }));
+        $this->runAsyncQueries($this->connectionPool);
     }
 }
diff --git a/composer.json b/composer.json
index 10575ac..26b4aac 100644
--- a/composer.json
+++ b/composer.json
@@ -29,7 +29,7 @@
         "phpunit/phpunit": "^9",
         "amphp/phpunit-util": "^3",
         "amphp/php-cs-fixer-config": "^2",
-        "phpbench/phpbench": "^1.1",
+        "phpbench/phpbench": "^1.2.6",
         "psalm/phar": "^4.22"
     },
     "minimum-stability": "dev",
diff --git a/phpbench.json b/phpbench.json
index af6adfd..74cd29d 100644
--- a/phpbench.json
+++ b/phpbench.json
@@ -1,5 +1,5 @@
 {
-    "bootstrap": "vendor/autoload.php",
-    "path": "benchmarks",
-    "php_binary": "php_no_xdebug"
+    "$schema": "vendor/phpbench/phpbench/phpbench.schema.json",
+    "runner.bootstrap": "vendor/autoload.php",
+    "runner.path": "benchmarks"
 }

From df3439aab82785779c55428d8859aa393ef8fa7c Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 28 Aug 2022 10:55:59 -0500
Subject: [PATCH 118/174] Require bind data to be a string

Data was being cast to a string internally, instead have the user do that explicitly. Also fixed multiple bind calls on statement pool.
---
 src/Internal/MysqlConnectionStatement.php | 14 +---
 src/Internal/MysqlPooledStatement.php     |  2 +-
 src/Internal/MysqlStatementPool.php       |  5 +-
 src/MysqlStatement.php                    |  6 +-
 test/LinkTest.php                         | 10 +--
 test/StatementTest.php                    | 91 -----------------------
 6 files changed, 16 insertions(+), 112 deletions(-)
 delete mode 100644 test/StatementTest.php

diff --git a/src/Internal/MysqlConnectionStatement.php b/src/Internal/MysqlConnectionStatement.php
index ebea38a..414a7d7 100644
--- a/src/Internal/MysqlConnectionStatement.php
+++ b/src/Internal/MysqlConnectionStatement.php
@@ -79,27 +79,21 @@ public function onClose(\Closure $onClose): void
         $this->onClose->getFuture()->finally($onClose);
     }
 
-    public function bind(int|string $paramId, mixed $data): void
+    public function bind(int|string $paramId, string $data): void
     {
         if (\is_int($paramId)) {
-            if ($paramId >= $this->positionalParamCount) {
+            if ($paramId >= $this->positionalParamCount || $paramId < 0) {
                 throw new \Error("Parameter $paramId is not defined for this prepared statement");
             }
             $i = $paramId;
         } else {
             if (!isset($this->byNamed[$paramId])) {
-                throw new \Error("Parameter :$paramId is not defined for this prepared statement");
+                throw new \Error("Named parameter :$paramId is not defined for this prepared statement");
             }
             $array = $this->byNamed[$paramId];
             $i = \reset($array);
         }
 
-        if (!\is_scalar($data) && !(\is_object($data) && \method_exists($data, '__toString'))) {
-            throw new \TypeError("Data must be scalar or an object that implements __toString method");
-        }
-
-        $data = (string) $data;
-
         do {
             $realId = -1;
             while (isset($this->named[++$realId]) || $i-- > 0) {
@@ -134,7 +128,7 @@ public function execute(array $params = []): MysqlResult
                 $args[$i] = $params[$unnamed];
                 $unnamed++;
             } elseif (!\array_key_exists($unnamed, $this->prebound)) {
-                throw new \Error("Parameter $unnamed for prepared statement missing");
+                throw new \Error("Parameter $unnamed missing for executing prepared statement");
             } else {
                 $prebound[$i] = $this->prebound[$unnamed++];
             }
diff --git a/src/Internal/MysqlPooledStatement.php b/src/Internal/MysqlPooledStatement.php
index 88b375a..e1c78af 100644
--- a/src/Internal/MysqlPooledStatement.php
+++ b/src/Internal/MysqlPooledStatement.php
@@ -38,7 +38,7 @@ public function execute(array $params = []): MysqlResult
         return parent::execute($params);
     }
 
-    public function bind(int|string $paramId, mixed $data): void
+    public function bind(int|string $paramId, string $data): void
     {
         $this->statement->bind($paramId, $data);
     }
diff --git a/src/Internal/MysqlStatementPool.php b/src/Internal/MysqlStatementPool.php
index c107085..1f9f2d6 100644
--- a/src/Internal/MysqlStatementPool.php
+++ b/src/Internal/MysqlStatementPool.php
@@ -49,9 +49,10 @@ public function execute(array $params = []): MysqlResult
         return parent::execute($params);
     }
 
-    public function bind(int|string $paramId, mixed $data): void
+    public function bind(int|string $paramId, string $data): void
     {
-        $this->params[$paramId] = $data;
+        $prior = $this->params[$paramId] ?? '';
+        $this->params[$paramId] = $prior . $data;
     }
 
     public function reset(): void
diff --git a/src/MysqlStatement.php b/src/MysqlStatement.php
index b089d67..923f443 100644
--- a/src/MysqlStatement.php
+++ b/src/MysqlStatement.php
@@ -10,11 +10,11 @@ public function execute(array $params = []): MysqlResult;
 
     /**
      * @param int|string $paramId Parameter ID or name.
-     * @param mixed $data Data to bind to parameter.
+     * @param string $data Data to bind to parameter.
      *
-     * @throws \Error If $paramId is not an int or string, or the position does not exist.
+     * @throws \Error If $paramId does not exist.
      */
-    public function bind(int|string $paramId, mixed $data): void;
+    public function bind(int|string $paramId, string $data): void;
 
     /**
      * @return list<MysqlColumnDefinition>
diff --git a/test/LinkTest.php b/test/LinkTest.php
index b459115..9d4a562 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -252,7 +252,7 @@ public function testBindWithInvalidParamId()
 
         $statement = $db->prepare("SELECT * FROM main WHERE a = ?");
 
-        $statement->bind(1, 1);
+        $statement->bind(1, "1");
 
         $statement->execute(); // Some implementations do not throw until execute() is called.
     }
@@ -260,13 +260,13 @@ public function testBindWithInvalidParamId()
     public function testBindWithInvalidParamName()
     {
         $this->expectException(\Error::class);
-        $this->expectExceptionMessage('Parameter :b is not defined for this prepared statement');
+        $this->expectExceptionMessage('Named parameter :b is not defined for this prepared statement');
 
         $db = $this->getLink();
 
         $statement = $db->prepare("SELECT * FROM main WHERE a = :a");
 
-        $statement->bind("b", 1);
+        $statement->bind("b", "1");
 
         $statement->execute(); // Some implementations do not throw until execute() is called.
     }
@@ -274,7 +274,7 @@ public function testBindWithInvalidParamName()
     public function testStatementExecuteWithTooFewParams()
     {
         $this->expectException(\Error::class);
-        $this->expectExceptionMessage('Parameter 1 for prepared statement missing');
+        $this->expectExceptionMessage('Parameter 1 missing for executing prepared statement');
 
         $db = $this->getLink();
 
@@ -318,7 +318,7 @@ public function testExecuteWithInvalidQuery()
     public function testExecuteWithTooFewParams()
     {
         $this->expectException(\Error::class);
-        $this->expectExceptionMessage('Parameter 1 for prepared statement missing');
+        $this->expectExceptionMessage('Parameter 1 missing for executing prepared statement');
 
         $db = $this->getLink();
 
diff --git a/test/StatementTest.php b/test/StatementTest.php
deleted file mode 100644
index 4ea68f1..0000000
--- a/test/StatementTest.php
+++ /dev/null
@@ -1,91 +0,0 @@
-<?php
-
-namespace Amp\Mysql\Test;
-
-use Amp\Mysql\Internal\ConnectionProcessor;
-use Amp\Mysql\Internal\MysqlConnectionStatement;
-use Amp\Mysql\Internal\MysqlResultProxy;
-use Amp\PHPUnit\AsyncTestCase;
-use PHPUnit\Framework\MockObject\MockObject;
-
-class StatementTest extends AsyncTestCase
-{
-    /** @var MockObject&ConnectionProcessor */
-    protected ConnectionProcessor $processor;
-
-    protected MysqlResultProxy $resultProxy;
-
-    public function setUp(): void
-    {
-        parent::setUp();
-
-        $this->processor = $this->createMock(ConnectionProcessor::class);
-        $this->resultProxy = new MysqlResultProxy;
-    }
-
-    /**
-     * @dataProvider provideTestBindDataTypes
-     */
-    public function testBindDataTypes($data, $expectedException)
-    {
-        // arrange
-        $query = 'SELECT * FROM test WHERE id = ?';
-        $stmtId = 1;
-        $paramId = 0;
-        $named = [];
-
-        $this->processor->method('isClosed')
-            ->willReturn(false);
-        $this->processor->expects($this->once())
-            ->method('unreference');
-        $this->processor->expects($this->once())
-            ->method('closeStmt');
-
-        $this->resultProxy->columnsToFetch = 1;
-        $stmt = new MysqlConnectionStatement($this->processor, $query, $stmtId, $named, $this->resultProxy);
-
-        // assert
-        if ($expectedException) {
-            $this->expectException($expectedException);
-            $this->processor->expects($this->never())
-                ->method('bindParam')
-                ->with($stmtId, $this->anything(), $data);
-        } else {
-            $this->processor->expects($this->once())
-                ->method('bindParam')
-                ->with($stmtId, $this->anything(), $data);
-        }
-
-        $this->assertSame($query, $stmt->getQuery());
-
-        // act
-        $stmt->bind($paramId, $data);
-    }
-
-    public function provideTestBindDataTypes()
-    {
-        return [
-            'test scalar' => [
-                'data' => 1,
-                'expectedException' => null,
-            ],
-            'test object' => [
-                'data' => (object) [],
-                'expectedException' => 'TypeError',
-            ],
-            'test array' => [
-                'data' => [],
-                'expectedException' => 'TypeError',
-            ],
-            'test object with __toString defined' => [
-                'data' => new class {
-                    public function __toString()
-                    {
-                        return '';
-                    }
-                },
-                'expectedException' => null,
-            ],
-        ];
-    }
-}

From 3c0e7eae5563b47f39e714357d59fe2441a0b9a5 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 28 Aug 2022 11:07:38 -0500
Subject: [PATCH 119/174] Reset statement when pushing instead of pulling

---
 src/Internal/MysqlStatementPool.php | 17 +++++++++++++----
 1 file changed, 13 insertions(+), 4 deletions(-)

diff --git a/src/Internal/MysqlStatementPool.php b/src/Internal/MysqlStatementPool.php
index 1f9f2d6..2a367a1 100644
--- a/src/Internal/MysqlStatementPool.php
+++ b/src/Internal/MysqlStatementPool.php
@@ -6,6 +6,7 @@
 use Amp\Mysql\MysqlStatement;
 use Amp\Sql\Common\StatementPool as SqlStatementPool;
 use Amp\Sql\Result as SqlResult;
+use Amp\Sql\Statement;
 
 /**
  * @internal
@@ -22,8 +23,6 @@ protected function pop(): MysqlStatement
         try {
             \assert($statement instanceof MysqlStatement);
 
-            $statement->reset();
-
             foreach ($this->params as $paramId => $data) {
                 $statement->bind($paramId, $data);
             }
@@ -35,6 +34,16 @@ protected function pop(): MysqlStatement
         return $statement;
     }
 
+    protected function push(Statement $statement): void
+    {
+        if ($statement->isClosed()) {
+            return;
+        }
+
+        $statement->reset();
+        parent::push($statement);
+    }
+
     protected function createResult(SqlResult $result, \Closure $release): MysqlResult
     {
         if (!$result instanceof MysqlResult) {
@@ -64,7 +73,7 @@ public function getColumnDefinitions(): array
     {
         $statement = parent::pop();
         $columns = $statement->getColumnDefinitions();
-        $this->push($statement);
+        parent::push($statement);
         return $columns;
     }
 
@@ -72,7 +81,7 @@ public function getParameterDefinitions(): array
     {
         $statement = parent::pop();
         $parameters = $statement->getParameterDefinitions();
-        $this->push($statement);
+        parent::push($statement);
         return $parameters;
     }
 }

From 01a523b65ad9fcd4042ba1fad6414da7449191bb Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 30 Aug 2022 08:35:55 -0500
Subject: [PATCH 120/174] Convert closures to arrows

---
 src/Internal/ConnectionProcessor.php | 33 ++++++++--------------------
 1 file changed, 9 insertions(+), 24 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index b541ac3..bbc1c64 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -391,17 +391,13 @@ public function changeUser($user, $pass, $db = null) {
     /** @see 14.6.15 COM_PING */
     public function ping(): Future
     {
-        return $this->startCommand(function (): void {
-            $this->write("\x0e");
-        });
+        return $this->startCommand(fn () => $this->write("\x0e"));
     }
 
     /** @see 14.6.19 COM_RESET_CONNECTION */
     public function resetConnection(): Future
     {
-        return $this->startCommand(function (): void {
-            $this->write("\x1f");
-        });
+        return $this->startCommand(fn () => $this->write("\x1f"));
     }
 
     /** @see 14.7.5 COM_STMT_SEND_LONG_DATA */
@@ -536,17 +532,13 @@ public function listAllFields(string $table, string $like = "%"): Future
     /** @see 14.6.6 COM_CREATE_DB */
     public function createDatabase(string $db): Future
     {
-        return $this->startCommand(function () use ($db): void {
-            $this->write("\x05$db");
-        });
+        return $this->startCommand(fn () => $this->write("\x05$db"));
     }
 
     /** @see 14.6.7 COM_DROP_DB */
     public function dropDatabase(string $db): Future
     {
-        return $this->startCommand(function () use ($db): void {
-            $this->write("\x06$db");
-        });
+        return $this->startCommand(fn () => $this->write("\x06$db"));
     }
 
     /**
@@ -554,17 +546,14 @@ public function dropDatabase(string $db): Future
      */
     public function refresh(int $subcommand): Future
     {
-        return $this->startCommand(function () use ($subcommand): void {
-            $this->write("\x07" . \chr($subcommand));
-        });
+        return $this->startCommand(fn () => $this->write("\x07" . \chr($subcommand)));
     }
 
     /** @see 14.6.9 COM_SHUTDOWN */
     public function shutdown(): Future
     {
-        return $this->startCommand(function (): void {
-            $this->write("\x08\x00"); /* SHUTDOWN_DEFAULT / SHUTDOWN_WAIT_ALL_BUFFERS, only one in use */
-        });
+        /* SHUTDOWN_DEFAULT / SHUTDOWN_WAIT_ALL_BUFFERS, only one in use */
+        return $this->startCommand(fn () => $this->write("\x08\x00"));
     }
 
     /** @see 14.6.10 COM_STATISTICS */
@@ -588,17 +577,13 @@ public function processInfo(): Future
     /** @see 14.6.13 COM_PROCESS_KILL */
     public function killProcess(int $process): Future
     {
-        return $this->startCommand(function () use ($process): void {
-            $this->write("\x0c" . MysqlDataType::encodeInt32($process));
-        });
+        return $this->startCommand(fn () => $this->write("\x0c" . MysqlDataType::encodeInt32($process)));
     }
 
     /** @see 14.6.14 COM_DEBUG */
     public function debugStdout(): Future
     {
-        return $this->startCommand(function (): void {
-            $this->write("\x0d");
-        });
+        return $this->startCommand(fn () => $this->write("\x0d"));
     }
 
     /** @see 14.7.8 COM_STMT_RESET */

From 11ff0783940a7d040426e99c98689ec88a0a2968 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 30 Aug 2022 08:36:09 -0500
Subject: [PATCH 121/174] Cleanup debug output code

---
 src/Internal/ConnectionProcessor.php | 53 +++++++++++-----------------
 1 file changed, 21 insertions(+), 32 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index bbc1c64..2569b18 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -227,22 +227,7 @@ private function read(): void
     {
         try {
             while (($bytes = $this->socket->read()) !== null) {
-                // @codeCoverageIgnoreStart
-                \assert((function () use ($bytes) {
-                    if (\defined("MYSQL_DEBUG")) {
-                        \fwrite(STDERR, "in: ");
-                        for ($i = 0; $i < \min(\strlen($bytes), 200); $i++) {
-                            \fwrite(STDERR, \dechex(\ord($bytes[$i])) . " ");
-                        }
-                        $r = \range("\0", "\x1f");
-                        unset($r[10], $r[9]);
-                        \fwrite(STDERR, "len: " . \strlen($bytes) . "\n");
-                        \fwrite(STDERR, \str_replace($r, ".", \substr($bytes, 0, 200)) . "\n");
-                    }
-
-                    return true;
-                })());
-                // @codeCoverageIgnoreEnd
+                \assert($this->writeDebugData($bytes, 'in'));
 
                 $this->lastUsedAt = \time();
 
@@ -1232,6 +1217,25 @@ private function write(string $packet): void
         $this->sendPacket($packet);
     }
 
+    /**
+     * @codeCoverageIgnore
+     */
+    private function writeDebugData(string $packet, string $label): bool
+    {
+        if (\defined("MYSQL_DEBUG")) {
+            \fwrite(STDERR, "$label: ");
+            for ($i = 0; $i < \min(\strlen($packet), 200); $i++) {
+                \fwrite(STDERR, \dechex(\ord($packet[$i])) . " ");
+            }
+            $r = \range("\0", "\x1f");
+            unset($r[10], $r[9]);
+            \fwrite(STDERR, "len: ".\strlen($packet)."\n");
+            \fwrite(STDERR, \str_replace($r, ".", \substr($packet, 0, 200))."\n");
+        }
+
+        return true;
+    }
+
     /**
      * @see 14.1.2 MySQL Packets
      */
@@ -1239,22 +1243,7 @@ private function sendPacket(string $out): void
     {
         $packet = MysqlDataType::encodeInt32(\strlen($out) | (++$this->seqId << 24)) . $out;
 
-        // @codeCoverageIgnoreStart
-        \assert((function () use ($packet) {
-            if (\defined("MYSQL_DEBUG")) {
-                \fwrite(STDERR, "out: ");
-                for ($i = 0; $i < \min(\strlen($packet), 200); $i++) {
-                    \fwrite(STDERR, \dechex(\ord($packet[$i])) . " ");
-                }
-                $r = \range("\0", "\x1f");
-                unset($r[10], $r[9]);
-                \fwrite(STDERR, "len: ".\strlen($packet)."\n");
-                \fwrite(STDERR, \str_replace($r, ".", \substr($packet, 0, 200))."\n");
-            }
-
-            return true;
-        })());
-        // @codeCoverageIgnoreEnd
+        \assert($this->writeDebugData($packet, 'out'));
 
         if (($this->capabilities & self::CLIENT_COMPRESS) && $this->connectionState === ConnectionState::Ready) {
             $packet = $this->compressPacket($packet);

From c96931716cfdd845ce7505a4cc0435e57df4f072 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 1 Sep 2022 23:05:57 -0500
Subject: [PATCH 122/174] Update binary result handling

---
 src/Internal/ConnectionProcessor.php | 22 ++++++++++------------
 src/Internal/MysqlResultProxy.php    | 10 +++++++++-
 src/MysqlDataType.php                | 11 ++++++++---
 src/MysqlResult.php                  |  4 +++-
 test/PoolTest.php                    |  5 -----
 5 files changed, 30 insertions(+), 22 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 2569b18..81ac84e 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -1091,7 +1091,9 @@ private function handleBinaryResultSetRow(string $packet): void
             $this->parseEof($packet);
             $this->successfulResultFetch();
             return;
-        } elseif ($packetType === self::ERR_PACKET) {
+        }
+
+        if ($packetType === self::ERR_PACKET) {
             $this->handleError($packet);
             return;
         }
@@ -1099,24 +1101,20 @@ private function handleBinaryResultSetRow(string $packet): void
         \assert($this->result !== null, 'Connection result was in invalid state');
 
         $offset = 1; // skip first byte
-        $fields = [];
-        for ($i = 0; $i < $this->result->columnCount; $i++) {
-            if (\ord($packet[$offset + (($i + 2) >> 3)]) & (1 << (($i + 2) % 8))) {
-                $fields[$i] = null;
-            }
-        }
         $offset += ($this->result->columnCount + 9) >> 3;
+        $fields = [];
 
-        for ($i = 0; $offset < \strlen($packet); $i++) {
-            while (\array_key_exists($i, $fields)) {
-                $i++;
+        for ($i = 0; $i < $this->result->columnCount; $i++) {
+            if (\ord($packet[1 + (($i + 2) >> 3)]) & (1 << (($i + 2) % 8))) {
+                $fields[] = null;
+                continue;
             }
 
             $column = $this->result->columns[$i] ?? throw new \RuntimeException("Definition missing for column $i");
-            $fields[$i] = $column->type->decodeBinary($packet, $offset, $column->flags);
+            \assert($offset < \strlen($packet));
+            $fields[] = $column->type->decodeBinary($packet, $offset, $column->flags);
         }
 
-        \ksort($fields);
         $this->result->pushRow($fields);
     }
 
diff --git a/src/Internal/MysqlResultProxy.php b/src/Internal/MysqlResultProxy.php
index 6b1e829..e6e5a55 100644
--- a/src/Internal/MysqlResultProxy.php
+++ b/src/Internal/MysqlResultProxy.php
@@ -8,7 +8,10 @@
 use Amp\Pipeline\ConcurrentIterator;
 use Amp\Pipeline\Queue;
 
-/** @internal */
+/**
+ * @internal
+ * @psalm-import-type TRow from MysqlResult
+ */
 final class MysqlResultProxy
 {
     public int $columnsToFetch = 0;
@@ -19,8 +22,10 @@ final class MysqlResultProxy
     /** @var list<MysqlColumnDefinition> */
     public array $params = [];
 
+    /** @var Queue<list<TRow>> */
     private readonly Queue $rowQueue;
 
+    /** @var ConcurrentIterator<list<TRow>> */
     public readonly ConcurrentIterator $rowIterator;
 
     private ?DeferredFuture $columnDeferred = null;
@@ -71,6 +76,9 @@ public function markDefinitionsFetched(): void
         $this->columnDeferred = null;
     }
 
+    /**
+     * @param list<TRow> $row
+     */
     public function pushRow(array $row): void
     {
         \assert($this->state === MysqlResultProxyState::Fetched, 'Result proxy in invalid state');
diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index 6fd715a..48777ed 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -184,14 +184,19 @@ public function decodeText(string $string, int &$offset = 0, int $flags = 0): in
         }
     }
 
-    public function isBindable(): bool
+    public function isInternal(): bool
     {
         return match ($this) {
-            self::Json => false,
-            default => true,
+            self::Timestamp2, self::Datetime2, self::NewDate, self::Time2 => true,
+            default => false,
         };
     }
 
+    public function isBindable(): bool
+    {
+        return $this !== self::Json;
+    }
+
     private static function decodeDateTime(self $type, string $string, int &$offset): string
     {
         $year = $month = $day = $hour = $minute = $second = $microsecond = 0;
diff --git a/src/MysqlResult.php b/src/MysqlResult.php
index f0ea75d..b052240 100644
--- a/src/MysqlResult.php
+++ b/src/MysqlResult.php
@@ -5,7 +5,9 @@
 use Amp\Sql\Result;
 
 /**
- * @extends Result<int|float|string|null>
+ * @psalm-type TRow = int|float|string|null
+ *
+ * @extends Result<TRow>
  */
 interface MysqlResult extends Result
 {
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 1526aef..5e5bf04 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -11,17 +11,12 @@
 use Amp\Mysql\MysqlConnector;
 use Amp\Mysql\MysqlLink;
 use Amp\Mysql\MysqlResult;
-use Amp\Mysql\MysqlStatement;
 use Amp\Mysql\SocketMysqlConnection;
 use Amp\Sql\Transaction as SqlTransaction;
 use PHPUnit\Framework\MockObject\MockObject;
 use function Amp\async;
 use function Amp\delay;
 
-interface StatementOperation extends MysqlStatement
-{
-}
-
 class PoolTest extends LinkTest
 {
     protected function getLink(bool $useCompression = false): MysqlLink

From dd6583c6876db5c99e6f61b40ba30f88e5bfa2ab Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 4 Sep 2022 13:23:27 -0500
Subject: [PATCH 123/174] Make encodeBinary an instance method

---
 src/Internal/ConnectionProcessor.php |  6 +-----
 src/MysqlDataType.php                | 29 ++++++++++++++++++++++------
 2 files changed, 24 insertions(+), 11 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 81ac84e..5b851a2 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -446,11 +446,7 @@ public function execute(int $stmtId, string $query, array $params, array $prebou
                         continue;
                     }
 
-                    [$encodedType, $value] = MysqlDataType::encodeBinary($param);
-
-                    if ($paramType === MysqlDataType::Json && $encodedType === MysqlDataType::LongBlob) {
-                        $encodedType = $paramType;
-                    }
+                    [$encodedType, $value] = $paramType->encodeBinary($param);
 
                     $types[] = MysqlDataType::encodeInt16($encodedType->value);
                     $values[] = $value;
diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index 48777ed..20beb9f 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -46,7 +46,24 @@ enum MysqlDataType: int
      *
      * @see 14.7.3 Binary Value
      */
-    public static function encodeBinary(mixed $param): array
+    public function encodeBinary(mixed $param): array
+    {
+        $encodedPair = self::encodeValue($param);
+
+        if ($this === self::Json) {
+            [$encodedType, $encodedValue] = $encodedPair;
+            if ($encodedType === self::LongBlob) {
+                return [$this, $encodedValue];
+            }
+        }
+
+        return $encodedPair;
+    }
+
+    /**
+     * @return array{self, string}
+     */
+    private static function encodeValue(mixed $param): array
     {
         switch (\get_debug_type($param)) {
             case "bool":
@@ -137,10 +154,10 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
             case self::Date:
             case self::Datetime:
             case self::Timestamp:
-                return self::decodeDateTime($this, $string, $offset);
+                return $this->decodeDateTime($string, $offset);
 
             case self::Time:
-                return self::decodeTime($string, $offset);
+                return $this->decodeTime($string, $offset);
 
             case self::Null:
                 return null;
@@ -197,7 +214,7 @@ public function isBindable(): bool
         return $this !== self::Json;
     }
 
-    private static function decodeDateTime(self $type, string $string, int &$offset): string
+    private function decodeDateTime(string $string, int &$offset): string
     {
         $year = $month = $day = $hour = $minute = $second = $microsecond = 0;
 
@@ -231,7 +248,7 @@ private static function decodeDateTime(self $type, string $string, int &$offset)
         $offset += $length;
 
         $result = \sprintf('%04d-%02d-%02d', $year, $month, $day);
-        if ($type === self::Date) {
+        if ($this === self::Date) {
             return $result;
         }
 
@@ -243,7 +260,7 @@ private static function decodeDateTime(self $type, string $string, int &$offset)
         return $result;
     }
 
-    private static function decodeTime(string $string, int &$offset): string
+    private function decodeTime(string $string, int &$offset): string
     {
         $negative = $day = $hour = $minute = $second = $microsecond = 0;
 

From bc2f4649833edb26a6c09d48dd30a22b0570f1b7 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 4 Sep 2022 13:26:23 -0500
Subject: [PATCH 124/174] Allow BackedEnums and Stringable objects in query
 params

---
 src/MysqlDataType.php | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index 20beb9f..cf5c584 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -46,7 +46,7 @@ enum MysqlDataType: int
      *
      * @see 14.7.3 Binary Value
      */
-    public function encodeBinary(mixed $param): array
+    public function encodeBinary(int|float|string|bool|\BackedEnum|\Stringable|null $param): array
     {
         $encodedPair = self::encodeValue($param);
 
@@ -91,7 +91,17 @@ private static function encodeValue(mixed $param): array
                 return [self::Null, ""];
 
             default:
-                throw new SqlException("Unexpected type for binding parameter: " . \get_debug_type($param));
+                if (\is_object($param)) {
+                    if ($param instanceof \BackedEnum) {
+                        return self::encodeValue($param->value);
+                    }
+
+                    if ($param instanceof \Stringable) {
+                        return self::encodeValue((string) $param);
+                    }
+                }
+
+                throw new SqlException("Unexpected type for query parameter: " . \get_debug_type($param));
         }
     }
 

From 2677a567e7d6d34db5f75128006f265b16ce1ee1 Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Fri, 23 Sep 2022 18:29:50 +0200
Subject: [PATCH 125/174] Update README

---
 README.md | 67 +++++++++++++++++--------------------------------------
 1 file changed, 21 insertions(+), 46 deletions(-)

diff --git a/README.md b/README.md
index 2c5fca0..0995fd6 100644
--- a/README.md
+++ b/README.md
@@ -1,34 +1,19 @@
-<p align="center">
-<a href="https://amphp.org/mysql"><img src="https://raw.githubusercontent.com/amphp/logo/master/repos/mysql.png?v=12-07-2017" alt="mysql"/></a>
-</p>
+# amphp/mysql
 
-<p align="center">
-<a href="https://travis-ci.org/amphp/mysql"><img src="https://img.shields.io/travis/amphp/mysql/master.svg?style=flat-square" alt="Build Status"/></a>
-<a href="https://coveralls.io/github/amphp/mysql?branch=master"><img src="https://img.shields.io/coveralls/amphp/mysql/master.svg?style=flat-square" alt="Code Coverage"/></a>
-<a href="https://github.com/amphp/mysql/releases"><img src="https://img.shields.io/github/release/amphp/mysql.svg?style=flat-square" alt="Release"/></a>
-<a href="https://github.com/amphp/mysql/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="License"/></a>
-</p>
+AMPHP is a collection of event-driven libraries for PHP designed with fibers and concurrency in mind.
+`amphp/mysql` is an asynchronous MySQL client.
+The library allows to dynamically query a server with multiple MySQL connections concurrently.
+The client transparently distributes these queries across a scalable pool of available connections and does so using 100% userland PHP; there are *no external extension dependencies* (e.g. `ext/mysqli`, `ext/pdo`, etc).
 
-<p align="center"><strong>Async MySQL client built with <a href="https://amphp.org/">Amp</a>.</strong></p>
+## Features
 
-<hr/>
-
-`amp/mysql` is an asynchronous MySQL client built on the [Amp concurrency framework](https://amphp.org/). The library exposes a Promise-based API to dynamically query multiple MySQL connections concurrently. The client transparently distributes these queries across a scalable pool of available connections and does so using 100% userland PHP; there are *no external extension dependencies* (e.g. `ext/mysqli`, `ext/pdo`, etc).
-
-#### Features
-
- - Asynchronous API exposing full single-threaded concurrency
+ - Exposes a non-blocking API for issuing multiple MySQL queries concurrently
  - Transparent connection pooling to overcome MySQL's fundamentally synchronous connection protocol
  - MySQL transfer encoding support (gzip, TLS encryption)
- - Support for all MySQL commands<sup>†</sup>
+ - *Full* MySQL protocol support including *all*<sup>†</sup> available commands asynchronously
 
 <sup>† As documented in [official Mysql Internals Manual](https://dev.mysql.com/doc/internals/en/client-server-protocol.html)</sup>
 
-#### Project Goals
-
-* Expose a non-blocking API for issuing multiple MySQL queries in parallel
-* Support the *full* MySQL protocol and *all* available commands asynchronously
-
 ## Installation
 
 This package can be installed as a [Composer](https://getcomposer.org/) dependency.
@@ -37,35 +22,25 @@ This package can be installed as a [Composer](https://getcomposer.org/) dependen
 composer require amphp/mysql
 ```
 
-## Requirements
+This package requires PHP 8.1 or later.
 
-* PHP 7.1+
-* [Amp framework](https://github.com/amphp/amp) (installed via composer)
-
-## Documentation & Examples
+## Usage
 
 More extensive code examples reside in the [`examples`](examples) directory.
 
 ```php
-Amp\Loop::run(function() {
-    $config = Amp\Mysql\MysqlConfig::fromString(
-        "host=127.0.0.1 user=username password=password db=test"
-    );
-    
-    /** @var \Amp\Mysql\Pool $pool */
-    $pool = Amp\Mysql\pool($config);
-    
-    /** @var \Amp\Mysql\Statement $statement */
-    $statement = yield $pool->prepare("SELECT * FROM table_name WHERE id = :id");
-    
-    /** @var \Amp\Mysql\ResultSet $result */
-    $result = yield $statement->execute(['id' => 1337]);
-    while (yield $result->advance()) {
-        $row = $result->getCurrent();
-        // $row is an associative array of column values. e.g.: $row['column_name']
-    }
-});
+$config = Amp\Mysql\MysqlConfig::fromString(
+    "host=127.0.0.1 user=username password=password db=test"
+);
+
+$pool = new Amp\Mysql\MysqlConnectionPool($config);
+
+$statement = $pool->prepare("SELECT * FROM table_name WHERE id = :id");
+foreach ($statement->execute(['id' => 1337]) as $row) {
+    // $row is an associative array of column values. e.g.: $row['column_name']
+}
 ```
+
 ## Versioning
 
 `amphp/mysql` follows the [semver](http://semver.org/) semantic versioning specification like all other `amphp` packages.

From 73deb6a5d846507e353c7f5d07565beb25e6a933 Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Fri, 23 Sep 2022 18:32:34 +0200
Subject: [PATCH 126/174] Remove outdated Travis config

---
 .travis.yml | 106 ----------------------------------------------------
 1 file changed, 106 deletions(-)
 delete mode 100644 .travis.yml

diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index f505ff8..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,106 +0,0 @@
-os: linux
-
-language: php
-
-dist: xenial
-
-services:
-  - mysql
-
-php:
-  - 7.1
-  - 7.2
-  - 7.3
-  - 7.4
-  - nightly
-
-jobs:
-  include:
-    - stage: test
-      addons:
-        mariadb: 10.1
-      php: 7.1
-    - addons:
-        mariadb: 10.2
-      php: 7.1
-    - addons:
-        mariadb: 10.3
-      php: 7.1
-    - addons:
-        mariadb: 10.4
-      php: 7.1
-    - addons:
-        mariadb: 10.1
-      php: 7.2
-    - addons:
-        mariadb: 10.2
-      php: 7.2
-    - addons:
-        mariadb: 10.3
-      php: 7.2
-    - addons:
-        mariadb: 10.4
-      php: 7.2
-    - addons:
-        mariadb: 10.1
-      php: 7.3
-    - addons:
-        mariadb: 10.2
-      php: 7.3
-    - addons:
-        mariadb: 10.3
-      php: 7.3
-    - addons:
-        mariadb: 10.4
-      php: 7.3
-    - addons:
-        mariadb: 10.1
-      php: 7.4
-    - addons:
-        mariadb: 10.2
-      php: 7.4
-    - addons:
-        mariadb: 10.3
-      php: 7.4
-    - addons:
-        mariadb: 10.4
-      php: 7.4
-    - addons:
-        mariadb: 10.4
-      php: nightly
-    - stage: benchmark
-      php: 7.4
-      env:
-        - AMP_DEBUG=false
-      script:
-        - PATH=$PATH:$(pwd)/benchmarks/bin vendor/bin/phpbench run --report=aggregate
-  allow_failures:
-    - php: nightly
-  fast_finish: true
-
-env:
-  - AMP_DEBUG=true
-
-before_install:
-  - mysqld --version
-  - mysql -u root -e "SELECT VERSION(); CREATE DATABASE test; CREATE TABLE test.main (a INT(11), b INT(11)); INSERT INTO test.main VALUES (1, 2), (2, 3), (3, 4), (4, 5), (5, 6);"
-
-install:
-  - travis_retry composer update -n --prefer-dist
-
-before_script:
-  - echo '<?php $autoloader = require(__DIR__."/../vendor/autoload.php"); const DB_HOST = "localhost"; const DB_USER = "root"; const DB_PASS = "";' > test/bootstrap.php
-
-script:
-  - vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml
-  - PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer --diff --dry-run -v fix
-
-after_script:
-  - travis_retry composer require php-coveralls/php-coveralls
-  - travis_retry vendor/bin/php-coveralls -v
-
-cache:
-  directories:
-    - $HOME/.composer/cache
-    - $HOME/.php-cs-fixer
-    - $HOME/.local

From a1b9280128cae2c6adfc48371a303faed2835fee Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Sun, 25 Sep 2022 19:38:39 +0200
Subject: [PATCH 127/174] Remove php_no_xdebug

---
 benchmarks/bin/php_no_xdebug | 2 --
 1 file changed, 2 deletions(-)
 delete mode 100755 benchmarks/bin/php_no_xdebug

diff --git a/benchmarks/bin/php_no_xdebug b/benchmarks/bin/php_no_xdebug
deleted file mode 100755
index c151f80..0000000
--- a/benchmarks/bin/php_no_xdebug
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/usr/bin/env bash
-php -n -dopcache.enable_cli=1 -dzend.assertions=-1 "$@"

From 2c35c760a5a53155be170bb1c8b06770c94b711b Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Sun, 25 Sep 2022 19:42:17 +0200
Subject: [PATCH 128/174] Use simpler GeneratorParser instead of amphp/parser

We don't need string delimiters.
---
 composer.json                        |  1 -
 src/Internal/ConnectionProcessor.php | 12 +++---
 src/Internal/GeneratorParser.php     | 59 ++++++++++++++++++++++++++++
 3 files changed, 65 insertions(+), 7 deletions(-)
 create mode 100644 src/Internal/GeneratorParser.php

diff --git a/composer.json b/composer.json
index 26b4aac..12b714f 100644
--- a/composer.json
+++ b/composer.json
@@ -15,7 +15,6 @@
     "require": {
         "php": ">=8.1",
         "amphp/amp": "^3",
-        "amphp/parser": "^1",
         "amphp/pipeline": "^1",
         "amphp/socket": "^2",
         "amphp/sql": "^2",
diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 5b851a2..2f4870d 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -48,7 +48,7 @@ class ConnectionProcessor implements TransientResource
         ]msxS
         REGEX;
 
-    private Parser $parser;
+    private GeneratorParser $parser;
 
     private int $seqId = -1;
     private int $compressionId = -1;
@@ -137,7 +137,7 @@ public function __construct(EncryptableSocket $socket, MysqlConfig $config)
         $this->deferreds = new \SplQueue();
         $this->onReady = new \SplQueue();
 
-        $this->parser = new Parser($this->parseMysql());
+        $this->parser = new GeneratorParser($this->parseMysql());
     }
 
     public function isClosed(): bool
@@ -1275,7 +1275,7 @@ private function makeCompressedPacket(int $uncompressed, string $packet): string
      *
      * @return \Generator<int, int, string, void>
      */
-    private function parseCompression(Parser $parser): \Generator
+    private function parseCompression(GeneratorParser $parser): \Generator
     {
         while (true) {
             $buffer = yield 7;
@@ -1309,7 +1309,7 @@ private function parseCompression(Parser $parser): \Generator
     private function parseMysql(): \Generator
     {
         while (true) {
-            $packet = "";
+            $packet = '';
 
             do {
                 $buffer = yield 4;
@@ -1323,7 +1323,7 @@ private function parseMysql(): \Generator
                 }
             } while ($length === self::MAX_PACKET_LENGTH);
 
-            if (\strlen($packet)) {
+            if ($packet !== '') {
                 $this->parsePayload($packet);
             }
         }
@@ -1342,7 +1342,7 @@ private function parsePayload(string $packet): void
             switch (\ord($packet)) {
                 case self::OK_PACKET:
                     if ($this->capabilities & self::CLIENT_COMPRESS) {
-                        $this->parser = new Parser($this->parseCompression($this->parser));
+                        $this->parser = new GeneratorParser($this->parseCompression($this->parser));
                     }
                     $this->connectionState = ConnectionState::Ready;
                     $this->handleOk($packet);
diff --git a/src/Internal/GeneratorParser.php b/src/Internal/GeneratorParser.php
new file mode 100644
index 0000000..b2e2081
--- /dev/null
+++ b/src/Internal/GeneratorParser.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Amp\Mysql\Internal;
+
+final class GeneratorParser
+{
+    private ?\Generator $generator;
+
+    private array $buffers = [];
+
+    private int $bufferLength = 0;
+
+    private int $length;
+
+    public function __construct(\Generator $generator)
+    {
+        $this->generator = $generator;
+        $this->length = $this->generator->current();
+
+        if (!$this->generator->valid()) {
+            $this->generator = null;
+        }
+    }
+
+    final public function cancel(): void
+    {
+        $this->generator = null;
+    }
+
+    final public function isValid(): bool
+    {
+        return $this->generator !== null;
+    }
+
+    final public function push(string $data): void
+    {
+        if ($this->generator === null) {
+            throw new \Error("The parser is no longer writable");
+        }
+
+        $this->buffers[] = $data;
+        $this->bufferLength += \strlen($data);
+
+        if ($this->bufferLength >= $this->length) {
+            $offset = 0;
+            $buffer = \implode('', $this->buffers);
+
+            while ($this->bufferLength >= $this->length) {
+                $send = \substr($buffer, $offset, $this->length);
+                $offset += $this->length;
+                $this->bufferLength -= $this->length;
+
+                $this->length = $this->generator->send($send);
+            }
+
+            $this->buffers = [\substr($buffer, $offset)];
+        }
+    }
+}
\ No newline at end of file

From a047e1fe08029c8993d2335e33d376199ea48a94 Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Sun, 25 Sep 2022 19:44:04 +0200
Subject: [PATCH 129/174] Cache server public keys

---
 src/Internal/ConnectionProcessor.php |  4 ++--
 src/Internal/PublicKeyCache.php      | 20 ++++++++++++++++++++
 2 files changed, 22 insertions(+), 2 deletions(-)
 create mode 100644 src/Internal/PublicKeyCache.php

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 2f4870d..c02efc5 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -1429,8 +1429,8 @@ private function sha256Auth(string $pass, string $scramble, string $key): string
         \openssl_public_encrypt(
             "$pass\0" ^ \str_repeat($scramble, (int) \ceil(\strlen($pass) / \strlen($scramble))),
             $auth,
-            $key,
-            OPENSSL_PKCS1_OAEP_PADDING,
+            PublicKeyCache::loadKey($key),
+            \OPENSSL_PKCS1_OAEP_PADDING,
         );
 
         return $auth;
diff --git a/src/Internal/PublicKeyCache.php b/src/Internal/PublicKeyCache.php
new file mode 100644
index 0000000..96b2348
--- /dev/null
+++ b/src/Internal/PublicKeyCache.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Amp\Mysql\Internal;
+
+use Amp\Cache\AtomicCache;
+use Amp\Cache\LocalCache;
+use Amp\Sync\LocalKeyedMutex;
+
+/** @internal */
+final class PublicKeyCache
+{
+    private static AtomicCache $cache;
+
+    public static function loadKey(string $pem): \OpenSSLAsymmetricKey
+    {
+        self::$cache ??= new AtomicCache(new LocalCache(32), new LocalKeyedMutex());
+
+        return self::$cache->computeIfAbsent($pem, fn () => \openssl_pkey_get_public($pem));
+    }
+}
\ No newline at end of file

From e784274562ea4d265a6cb46c1a2ed6d702fff267 Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Sun, 25 Sep 2022 19:44:39 +0200
Subject: [PATCH 130/174] Improve benchmark code

---
 benchmarks/AbstractBench.php |  5 ++++-
 benchmarks/QueryBench.php    | 25 +++++++++++++------------
 2 files changed, 17 insertions(+), 13 deletions(-)

diff --git a/benchmarks/AbstractBench.php b/benchmarks/AbstractBench.php
index 99152a2..d59dd9b 100644
--- a/benchmarks/AbstractBench.php
+++ b/benchmarks/AbstractBench.php
@@ -2,11 +2,14 @@
 
 namespace Amp\Mysql\Bench;
 
+/**
+ * docker run --rm -ti -e MYSQL_ROOT_PASSWORD=secret -p 10101:3306 mysql:latest --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
+ */
 class AbstractBench
 {
     protected string $host = 'localhost:10101';
 
     protected string $user = 'root';
 
-    protected string $password = '';
+    protected string $password = 'secret';
 }
diff --git a/benchmarks/QueryBench.php b/benchmarks/QueryBench.php
index 0c6f781..e033561 100644
--- a/benchmarks/QueryBench.php
+++ b/benchmarks/QueryBench.php
@@ -51,6 +51,7 @@ public function init(): void
         $this->connection = $connector->connect($config);
 
         $this->pdoConnection = new \PDO("mysql:host=$this->host", $this->user, $this->password);
+        $this->pdoConnection->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
     }
 
     public function cleanup(): void
@@ -69,7 +70,7 @@ public function benchPdoQueries(): void
         }
     }
 
-    public function benchSyncQueries(): void
+    public function benchSequentialQueries(): void
     {
         $statement = $this->connection->prepare("SELECT ?");
 
@@ -78,23 +79,23 @@ public function benchSyncQueries(): void
         }
     }
 
-    private function runAsyncQueries(MysqlLink $link): void
+    public function benchConcurrentQueriesUsingSingleConnection(): void
     {
-        $statement = $link->prepare("SELECT ?");
-
-        Future\await(\array_map(
-            fn (int $i) => async(fn () => \iterator_to_array($statement->execute([$i]))),
-            \range(1, $this->maxQueries),
-        ));
+        $this->runConcurrentQueries($this->connection);
     }
 
-    public function benchAsyncQueriesUsingSingleConnection(): void
+    public function benchConcurrentQueriesUsingConnectionPool(): void
     {
-        $this->runAsyncQueries($this->connection);
+        $this->runConcurrentQueries($this->connectionPool);
     }
 
-    public function benchAsyncQueriesUsingConnectionPool(): void
+    private function runConcurrentQueries(MysqlLink $link): void
     {
-        $this->runAsyncQueries($this->connectionPool);
+        $statement = $link->prepare("SELECT ?");
+
+        Future\await(\array_map(
+            fn (int $i) => async(fn () => \iterator_to_array($statement->execute([$i]))),
+            \range(1, $this->maxQueries),
+        ));
     }
 }

From 4af50f9d4ab57edc6ea95d99e56bc13016792484 Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Sun, 25 Sep 2022 22:19:16 +0200
Subject: [PATCH 131/174] Fix fast auth in caching_sha2_password

The scramble wasn't calculated correctly. This made the client always do a full auth.
---
 src/Internal/ConnectionProcessor.php | 11 ++++++-----
 test/ScrambleTest.php                | 21 +++++++++++++++++++++
 2 files changed, 27 insertions(+), 5 deletions(-)
 create mode 100644 test/ScrambleTest.php

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index c02efc5..fd2a314 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -9,7 +9,6 @@
 use Amp\Mysql\MysqlColumnDefinition;
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlDataType;
-use Amp\Parser\Parser;
 use Amp\Socket\EncryptableSocket;
 use Amp\Sql\ConnectionException;
 use Amp\Sql\QueryError;
@@ -1436,10 +1435,12 @@ private function sha256Auth(string $pass, string $scramble, string $key): string
         return $auth;
     }
 
-    private function sha2Auth(string $pass, string $scramble): string
+    public static function sha2Auth(string $pass, string $scramble): string
     {
-        $hash = \hash("sha256", $pass, true);
-        return $hash ^ \hash("sha256", \substr($scramble, 0, 20) . \hash("sha256", $hash, true), true);
+        $digestStage1 = \hash("sha256", $pass, true);
+        $digestStage2 = \hash("sha256", $digestStage1, true);
+        $scrambleStage1 = \hash("sha256", $digestStage2 . \substr($scramble, 0, 20), true);
+        return $digestStage1 ^ $scrambleStage1;
     }
 
     private function authSwitchRequest(string $packet): void
@@ -1550,7 +1551,7 @@ private function getAuthData(): string
                     }
                     return "\x01";
                 case "caching_sha2_password":
-                    return $this->sha2Auth($password, $this->authPluginData);
+                    return self::sha2Auth($password, $this->authPluginData);
                 case "mysql_old_password":
                     throw new ConnectionException(
                         "mysql_old_password is outdated and insecure. Intentionally not implemented!"
diff --git a/test/ScrambleTest.php b/test/ScrambleTest.php
new file mode 100644
index 0000000..419da5a
--- /dev/null
+++ b/test/ScrambleTest.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Amp\Mysql\Test;
+
+use Amp\Mysql\Internal\ConnectionProcessor;
+use PHPUnit\Framework\TestCase;
+
+final class ScrambleTest extends TestCase
+{
+    public function test()
+    {
+        $password = 'Ab12#$Cd56&*';
+        $scramble = 'eF!@34gH%^78';
+
+        $expected = "\x6a\x45\x37\x96\x6b\x29\x63\x59\x24\x8d\x64\x86\x0a\xd6\xcc\x2a\x06\x47\x8c\x26\xea\xaa\x3b\x02\x69\x4c\x85\x02\xf5\x5b\xc8\xdc";
+
+        $actual = ConnectionProcessor::sha2Auth($password, $scramble);
+
+        self::assertSame($expected, $actual);
+    }
+}
\ No newline at end of file

From 572ba9d4a1025bd64bb639a416d1bcddb3c7b2f0 Mon Sep 17 00:00:00 2001
From: Niklas Keller <me@kelunik.com>
Date: Sat, 12 Nov 2022 19:39:17 +0100
Subject: [PATCH 132/174] Return proper error on too many connections (#124)

Fixes #123.
---
 src/Internal/ConnectionProcessor.php | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index fd2a314..45df86a 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -621,7 +621,10 @@ private function handleError(string $packet): void
 
         $this->metadata->errorCode = MysqlDataType::decodeUnsigned16($packet, $offset);
 
-        if ($this->capabilities & self::CLIENT_PROTOCOL_41) {
+        $connecting = $this->connectionState === ConnectionState::Connecting
+            || $this->connectionState === ConnectionState::Established;
+
+        if ($this->capabilities & self::CLIENT_PROTOCOL_41 && !$connecting) {
             $this->metadata->errorState = \substr($packet, $offset, 6);
             $offset += 6;
         }
@@ -630,12 +633,11 @@ private function handleError(string $packet): void
 
         $this->parseCallback = null;
 
-        if ($this->connectionState === ConnectionState::Connecting) {
+        if ($connecting) {
             // connection failure
             $this->free(new ConnectionException(\sprintf(
-                'Could not connect to %s: %s %s',
+                'Could not connect to %s: %s',
                 $this->config->getConnectionString(),
-                $this->metadata->errorState ?? 'Unknown state',
                 $this->metadata->errorMsg,
             )));
             return;
@@ -758,6 +760,12 @@ private function handleHandshake(string $packet): void
         $offset = 1;
 
         $protocol = \ord($packet);
+
+        if ($protocol === self::ERR_PACKET) {
+            $this->handleError($packet);
+            return;
+        }
+
         if ($protocol !== 0x0a) {
             throw new ConnectionException("Unsupported protocol version ".\ord($packet)." (Expected: 10)");
         }
@@ -1182,7 +1190,9 @@ private function free(?\Throwable $exception = null): void
         $this->parser->cancel();
 
         if (!$this->deferreds->isEmpty() || $this->result) {
-            $exception = new ConnectionException("Connection closed unexpectedly", 0, $exception ?? null);
+            if (!$exception instanceof ConnectionException) {
+                $exception = new ConnectionException("Connection closed unexpectedly", 0, $exception ?? null);
+            }
 
             while (!$this->deferreds->isEmpty()) {
                 $this->deferreds->shift()->error($exception);

From c969c91784fffa175e45c04387cc5dbc8741a74d Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 15 Nov 2022 16:18:25 -0600
Subject: [PATCH 133/174] Update for Result::fetchRow()

---
 src/Internal/MysqlCommandResult.php    |  5 +++++
 src/Internal/MysqlConnectionResult.php | 11 +++++++++++
 test/LinkTest.php                      |  2 +-
 3 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/src/Internal/MysqlCommandResult.php b/src/Internal/MysqlCommandResult.php
index 10d5b77..822ce48 100644
--- a/src/Internal/MysqlCommandResult.php
+++ b/src/Internal/MysqlCommandResult.php
@@ -25,6 +25,11 @@ public function getIterator(): \Traversable
         return $this->delegate->getIterator();
     }
 
+    public function fetchRow(): ?array
+    {
+        return $this->delegate->fetchRow();
+    }
+
     public function getRowCount(): int
     {
         return $this->delegate->getRowCount();
diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index 028a3bd..0b0aae3 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -55,6 +55,17 @@ public function getIterator(): \Traversable
         yield from $this->generator;
     }
 
+    public function fetchRow(): ?array
+    {
+        if (!$this->generator->valid()) {
+            return null;
+        }
+
+        $current = $this->generator->current();
+        $this->generator->next();
+        return $current;
+    }
+
     public function getNextResult(): ?MysqlResult
     {
         if ($this->generator->valid()) {
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 9d4a562..93922ff 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -45,7 +45,7 @@ public function testQueryFetchRow()
         $this->assertSame(1, $result->getColumnCount());
 
         $got = [];
-        foreach ($result as $row) {
+        while ($row = $result->fetchRow()) {
             $got[] = \array_values($row);
         }
 

From 6ef5cd88f916488010d65887ad190fcf4f4aaee4 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 17 Dec 2022 23:17:30 -0600
Subject: [PATCH 134/174] Remove MysqlConnector and fix Psalm issues

---
 src/Internal/MysqlCommandResult.php |  7 +++++--
 src/Internal/MysqlStatementPool.php |  3 ++-
 src/Internal/PublicKeyCache.php     |  4 ++--
 src/MysqlConnectionPool.php         |  2 +-
 src/MysqlConnector.php              | 19 -------------------
 src/MysqlStatement.php              |  3 +++
 src/SocketMysqlConnector.php        |  6 +++++-
 src/functions.php                   |  4 ++++
 test/PoolTest.php                   |  4 ++--
 9 files changed, 24 insertions(+), 28 deletions(-)
 delete mode 100644 src/MysqlConnector.php

diff --git a/src/Internal/MysqlCommandResult.php b/src/Internal/MysqlCommandResult.php
index 822ce48..bf675b1 100644
--- a/src/Internal/MysqlCommandResult.php
+++ b/src/Internal/MysqlCommandResult.php
@@ -11,12 +11,15 @@ final class MysqlCommandResult implements MysqlResult, \IteratorAggregate
 {
     private ?int $lastInsertId;
 
-    /** @var CommandResult<null> */
+    /** @var CommandResult<MysqlResult> */
     private readonly CommandResult $delegate;
 
     public function __construct(int $affectedRows, int $lastInsertId)
     {
-        $this->delegate = new CommandResult($affectedRows, Future::complete());
+        /** @var Future<MysqlResult|null> $future Explicit declaration for Psalm. */
+        $future = Future::complete();
+
+        $this->delegate = new CommandResult($affectedRows, $future);
         $this->lastInsertId = $lastInsertId ?: null; // Convert 0 to null
     }
 
diff --git a/src/Internal/MysqlStatementPool.php b/src/Internal/MysqlStatementPool.php
index 2a367a1..c945604 100644
--- a/src/Internal/MysqlStatementPool.php
+++ b/src/Internal/MysqlStatementPool.php
@@ -4,13 +4,14 @@
 
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
+use Amp\Mysql\MysqlTransaction;
 use Amp\Sql\Common\StatementPool as SqlStatementPool;
 use Amp\Sql\Result as SqlResult;
 use Amp\Sql\Statement;
 
 /**
  * @internal
- * @extends SqlStatementPool<MysqlResult, MysqlStatement>
+ * @extends SqlStatementPool<MysqlResult, MysqlStatement, MysqlTransaction>
  */
 final class MysqlStatementPool extends SqlStatementPool implements MysqlStatement
 {
diff --git a/src/Internal/PublicKeyCache.php b/src/Internal/PublicKeyCache.php
index 96b2348..a3bb0d5 100644
--- a/src/Internal/PublicKeyCache.php
+++ b/src/Internal/PublicKeyCache.php
@@ -9,7 +9,7 @@
 /** @internal */
 final class PublicKeyCache
 {
-    private static AtomicCache $cache;
+    private static ?AtomicCache $cache = null;
 
     public static function loadKey(string $pem): \OpenSSLAsymmetricKey
     {
@@ -17,4 +17,4 @@ public static function loadKey(string $pem): \OpenSSLAsymmetricKey
 
         return self::$cache->computeIfAbsent($pem, fn () => \openssl_pkey_get_public($pem));
     }
-}
\ No newline at end of file
+}
diff --git a/src/MysqlConnectionPool.php b/src/MysqlConnectionPool.php
index d43ec85..bfa906f 100644
--- a/src/MysqlConnectionPool.php
+++ b/src/MysqlConnectionPool.php
@@ -11,7 +11,7 @@
 use Amp\Sql\TransactionIsolationLevel;
 
 /**
- * @extends ConnectionPool<MysqlConfig, MysqlConnection, MysqlResult, MysqlStatement, MysqlTransaction>
+ * @extends ConnectionPool<MysqlConfig, MysqlResult, MysqlStatement, MysqlTransaction, MysqlConnection>
  */
 final class MysqlConnectionPool extends ConnectionPool implements MysqlLink
 {
diff --git a/src/MysqlConnector.php b/src/MysqlConnector.php
deleted file mode 100644
index 011e67e..0000000
--- a/src/MysqlConnector.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-namespace Amp\Mysql;
-
-use Amp\Cancellation;
-use Amp\Sql\SqlConfig;
-use Amp\Sql\SqlConnector;
-use Amp\Sql\SqlException;
-
-/**
- * @extends SqlConnector<MysqlConfig, MysqlConnection>
- */
-interface MysqlConnector extends SqlConnector
-{
-    /**
-     * @throws SqlException
-     */
-    public function connect(SqlConfig $config, ?Cancellation $cancellation = null): MysqlLink;
-}
diff --git a/src/MysqlStatement.php b/src/MysqlStatement.php
index 923f443..efd7142 100644
--- a/src/MysqlStatement.php
+++ b/src/MysqlStatement.php
@@ -4,6 +4,9 @@
 
 use Amp\Sql\Statement;
 
+/**
+ * @extends Statement<MysqlResult>
+ */
 interface MysqlStatement extends Statement
 {
     public function execute(array $params = []): MysqlResult;
diff --git a/src/SocketMysqlConnector.php b/src/SocketMysqlConnector.php
index 6e85236..f3d0c85 100644
--- a/src/SocketMysqlConnector.php
+++ b/src/SocketMysqlConnector.php
@@ -5,8 +5,12 @@
 use Amp\Cancellation;
 use Amp\Socket;
 use Amp\Sql\SqlConfig;
+use Amp\Sql\SqlConnector;
 
-final class SocketMysqlConnector implements MysqlConnector
+/**
+ * @implements SqlConnector<MysqlConfig, MysqlConnection>
+ */
+final class SocketMysqlConnector implements SqlConnector
 {
     public function __construct(private readonly ?Socket\SocketConnector $connector = null)
     {
diff --git a/src/functions.php b/src/functions.php
index 1766a5c..18a4990 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -23,6 +23,10 @@ function mysqlConnector(?SqlConnector $connector = null): SqlConnector
         return $map[$driver] = $connector;
     }
 
+    /**
+     * @psalm-suppress InvalidArgument
+     * @var SqlConnector<MysqlConfig, MysqlConnection>
+     */
     return $map[$driver] ??= new RetrySqlConnector(new SocketMysqlConnector());
 }
 
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 5e5bf04..56c5ed8 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -8,10 +8,10 @@
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlConnection;
 use Amp\Mysql\MysqlConnectionPool;
-use Amp\Mysql\MysqlConnector;
 use Amp\Mysql\MysqlLink;
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\SocketMysqlConnection;
+use Amp\Sql\SqlConnector;
 use Amp\Sql\Transaction as SqlTransaction;
 use PHPUnit\Framework\MockObject\MockObject;
 use function Amp\async;
@@ -26,7 +26,7 @@ protected function getLink(bool $useCompression = false): MysqlLink
 
     protected function createPool(array $connections): MysqlConnectionPool
     {
-        $connector = $this->createMock(MysqlConnector::class);
+        $connector = $this->createMock(SqlConnector::class);
         $connector->method('connect')
             ->will($this->returnCallback(function () use ($connections): MysqlConnection {
                 static $count = 0;

From 25a3111d921431d4e4fa5a761f46f9ecc69b78ca Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 17 Dec 2022 23:27:04 -0600
Subject: [PATCH 135/174] Declare strict types

---
 src/Internal/ConnectionProcessor.php        | 2 +-
 src/Internal/ConnectionState.php            | 2 +-
 src/Internal/GeneratorParser.php            | 4 ++--
 src/Internal/MysqlCommandResult.php         | 2 +-
 src/Internal/MysqlConnectionMetdata.php     | 2 +-
 src/Internal/MysqlConnectionResult.php      | 2 +-
 src/Internal/MysqlConnectionStatement.php   | 2 +-
 src/Internal/MysqlConnectionTransaction.php | 2 +-
 src/Internal/MysqlPooledResult.php          | 2 +-
 src/Internal/MysqlPooledStatement.php       | 2 +-
 src/Internal/MysqlPooledTransaction.php     | 2 +-
 src/Internal/MysqlResultProxy.php           | 2 +-
 src/Internal/MysqlResultProxyState.php      | 2 +-
 src/Internal/MysqlStatementPool.php         | 2 +-
 src/Internal/PublicKeyCache.php             | 2 +-
 src/Internal/SessionStateType.php           | 2 +-
 src/MysqlColumnDefinition.php               | 2 +-
 src/MysqlConfig.php                         | 2 +-
 src/MysqlConnection.php                     | 2 +-
 src/MysqlConnectionPool.php                 | 2 +-
 src/MysqlDataType.php                       | 2 +-
 src/MysqlExecutor.php                       | 2 +-
 src/MysqlLink.php                           | 2 +-
 src/MysqlResult.php                         | 2 +-
 src/MysqlStatement.php                      | 2 +-
 src/MysqlTransaction.php                    | 2 +-
 src/SocketMysqlConnection.php               | 2 +-
 src/SocketMysqlConnector.php                | 2 +-
 src/functions.php                           | 2 +-
 test/ConnectionTest.php                     | 2 +-
 test/LinkTest.php                           | 2 +-
 test/MysqlConfigTest.php                    | 2 +-
 test/MysqlDataTypeTest.php                  | 2 +-
 test/MysqlTestCase.php                      | 2 +-
 test/PoolTest.php                           | 2 +-
 test/ScrambleTest.php                       | 4 ++--
 test/bootstrap.php                          | 2 +-
 test/initialize.php                         | 2 +-
 38 files changed, 40 insertions(+), 40 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 45df86a..374ea4c 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/ConnectionState.php b/src/Internal/ConnectionState.php
index d94ce96..e398436 100644
--- a/src/Internal/ConnectionState.php
+++ b/src/Internal/ConnectionState.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/GeneratorParser.php b/src/Internal/GeneratorParser.php
index b2e2081..9a0ffea 100644
--- a/src/Internal/GeneratorParser.php
+++ b/src/Internal/GeneratorParser.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
@@ -56,4 +56,4 @@ final public function push(string $data): void
             $this->buffers = [\substr($buffer, $offset)];
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/Internal/MysqlCommandResult.php b/src/Internal/MysqlCommandResult.php
index bf675b1..2bce0bd 100644
--- a/src/Internal/MysqlCommandResult.php
+++ b/src/Internal/MysqlCommandResult.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/MysqlConnectionMetdata.php b/src/Internal/MysqlConnectionMetdata.php
index bef8042..8e2663b 100644
--- a/src/Internal/MysqlConnectionMetdata.php
+++ b/src/Internal/MysqlConnectionMetdata.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index 0b0aae3..216cbe3 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/MysqlConnectionStatement.php b/src/Internal/MysqlConnectionStatement.php
index 414a7d7..fc41e3d 100644
--- a/src/Internal/MysqlConnectionStatement.php
+++ b/src/Internal/MysqlConnectionStatement.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/MysqlConnectionTransaction.php b/src/Internal/MysqlConnectionTransaction.php
index a348fd3..d7c5cd5 100644
--- a/src/Internal/MysqlConnectionTransaction.php
+++ b/src/Internal/MysqlConnectionTransaction.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/MysqlPooledResult.php b/src/Internal/MysqlPooledResult.php
index b1732b6..71fdf7b 100644
--- a/src/Internal/MysqlPooledResult.php
+++ b/src/Internal/MysqlPooledResult.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/MysqlPooledStatement.php b/src/Internal/MysqlPooledStatement.php
index e1c78af..896fe3d 100644
--- a/src/Internal/MysqlPooledStatement.php
+++ b/src/Internal/MysqlPooledStatement.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/MysqlPooledTransaction.php b/src/Internal/MysqlPooledTransaction.php
index dae9714..4bfe4df 100644
--- a/src/Internal/MysqlPooledTransaction.php
+++ b/src/Internal/MysqlPooledTransaction.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/MysqlResultProxy.php b/src/Internal/MysqlResultProxy.php
index e6e5a55..c10919f 100644
--- a/src/Internal/MysqlResultProxy.php
+++ b/src/Internal/MysqlResultProxy.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/MysqlResultProxyState.php b/src/Internal/MysqlResultProxyState.php
index 327f551..5692be1 100644
--- a/src/Internal/MysqlResultProxyState.php
+++ b/src/Internal/MysqlResultProxyState.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/MysqlStatementPool.php b/src/Internal/MysqlStatementPool.php
index c945604..b79bc3b 100644
--- a/src/Internal/MysqlStatementPool.php
+++ b/src/Internal/MysqlStatementPool.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/PublicKeyCache.php b/src/Internal/PublicKeyCache.php
index a3bb0d5..edf6783 100644
--- a/src/Internal/PublicKeyCache.php
+++ b/src/Internal/PublicKeyCache.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/Internal/SessionStateType.php b/src/Internal/SessionStateType.php
index 281eeb8..8fa0b10 100644
--- a/src/Internal/SessionStateType.php
+++ b/src/Internal/SessionStateType.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Internal;
 
diff --git a/src/MysqlColumnDefinition.php b/src/MysqlColumnDefinition.php
index 5f3bec7..2f2ac44 100644
--- a/src/MysqlColumnDefinition.php
+++ b/src/MysqlColumnDefinition.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql;
 
diff --git a/src/MysqlConfig.php b/src/MysqlConfig.php
index 5e78507..51bcbed 100644
--- a/src/MysqlConfig.php
+++ b/src/MysqlConfig.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql;
 
diff --git a/src/MysqlConnection.php b/src/MysqlConnection.php
index 038a1ff..39fbae9 100644
--- a/src/MysqlConnection.php
+++ b/src/MysqlConnection.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql;
 
diff --git a/src/MysqlConnectionPool.php b/src/MysqlConnectionPool.php
index bfa906f..6e748da 100644
--- a/src/MysqlConnectionPool.php
+++ b/src/MysqlConnectionPool.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql;
 
diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index cf5c584..bbb907c 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql;
 
diff --git a/src/MysqlExecutor.php b/src/MysqlExecutor.php
index 1b9a7ca..ac0c725 100644
--- a/src/MysqlExecutor.php
+++ b/src/MysqlExecutor.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql;
 
diff --git a/src/MysqlLink.php b/src/MysqlLink.php
index 4362520..f9a41fc 100644
--- a/src/MysqlLink.php
+++ b/src/MysqlLink.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql;
 
diff --git a/src/MysqlResult.php b/src/MysqlResult.php
index b052240..8356c1d 100644
--- a/src/MysqlResult.php
+++ b/src/MysqlResult.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql;
 
diff --git a/src/MysqlStatement.php b/src/MysqlStatement.php
index efd7142..c3ba0af 100644
--- a/src/MysqlStatement.php
+++ b/src/MysqlStatement.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql;
 
diff --git a/src/MysqlTransaction.php b/src/MysqlTransaction.php
index 08ce9d6..03e80dc 100644
--- a/src/MysqlTransaction.php
+++ b/src/MysqlTransaction.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql;
 
diff --git a/src/SocketMysqlConnection.php b/src/SocketMysqlConnection.php
index a08b51e..e6883d5 100644
--- a/src/SocketMysqlConnection.php
+++ b/src/SocketMysqlConnection.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql;
 
diff --git a/src/SocketMysqlConnector.php b/src/SocketMysqlConnector.php
index f3d0c85..baab095 100644
--- a/src/SocketMysqlConnector.php
+++ b/src/SocketMysqlConnector.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql;
 
diff --git a/src/functions.php b/src/functions.php
index 18a4990..335e7c7 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql;
 
diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php
index 6a92777..cbb1a88 100644
--- a/test/ConnectionTest.php
+++ b/test/ConnectionTest.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Test;
 
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 93922ff..1b5bac8 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Test;
 
diff --git a/test/MysqlConfigTest.php b/test/MysqlConfigTest.php
index 1a95611..77741df 100644
--- a/test/MysqlConfigTest.php
+++ b/test/MysqlConfigTest.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Test;
 
diff --git a/test/MysqlDataTypeTest.php b/test/MysqlDataTypeTest.php
index 8e0221e..82cc408 100644
--- a/test/MysqlDataTypeTest.php
+++ b/test/MysqlDataTypeTest.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Test;
 
diff --git a/test/MysqlTestCase.php b/test/MysqlTestCase.php
index d8cc51a..5bafa4e 100644
--- a/test/MysqlTestCase.php
+++ b/test/MysqlTestCase.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Test;
 
diff --git a/test/PoolTest.php b/test/PoolTest.php
index 56c5ed8..a16c96c 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Test;
 
diff --git a/test/ScrambleTest.php b/test/ScrambleTest.php
index 419da5a..b7bd8b8 100644
--- a/test/ScrambleTest.php
+++ b/test/ScrambleTest.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Test;
 
@@ -18,4 +18,4 @@ public function test()
 
         self::assertSame($expected, $actual);
     }
-}
\ No newline at end of file
+}
diff --git a/test/bootstrap.php b/test/bootstrap.php
index 3d59b7d..70f55c7 100644
--- a/test/bootstrap.php
+++ b/test/bootstrap.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Test;
 
diff --git a/test/initialize.php b/test/initialize.php
index c20c623..5f8e789 100644
--- a/test/initialize.php
+++ b/test/initialize.php
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 
 namespace Amp\Mysql\Test;
 

From 506f4452f236445c8bd82b2f499fc43b1df620c0 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Fri, 30 Dec 2022 13:44:30 -0600
Subject: [PATCH 136/174] Revert to using amphp/parser

Performance improvements for length delimiters were back-ported, so reverting to using the common library to reduce code duplication.
---
 composer.json                        |  1 +
 src/Internal/ConnectionProcessor.php |  9 +++--
 src/Internal/GeneratorParser.php     | 59 ----------------------------
 3 files changed, 6 insertions(+), 63 deletions(-)
 delete mode 100644 src/Internal/GeneratorParser.php

diff --git a/composer.json b/composer.json
index 12b714f..1ea2c4b 100644
--- a/composer.json
+++ b/composer.json
@@ -15,6 +15,7 @@
     "require": {
         "php": ">=8.1",
         "amphp/amp": "^3",
+        "amphp/parser": "^1.1",
         "amphp/pipeline": "^1",
         "amphp/socket": "^2",
         "amphp/sql": "^2",
diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 374ea4c..8487d8a 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -9,6 +9,7 @@
 use Amp\Mysql\MysqlColumnDefinition;
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlDataType;
+use Amp\Parser\Parser;
 use Amp\Socket\EncryptableSocket;
 use Amp\Sql\ConnectionException;
 use Amp\Sql\QueryError;
@@ -47,7 +48,7 @@ class ConnectionProcessor implements TransientResource
         ]msxS
         REGEX;
 
-    private GeneratorParser $parser;
+    private Parser $parser;
 
     private int $seqId = -1;
     private int $compressionId = -1;
@@ -136,7 +137,7 @@ public function __construct(EncryptableSocket $socket, MysqlConfig $config)
         $this->deferreds = new \SplQueue();
         $this->onReady = new \SplQueue();
 
-        $this->parser = new GeneratorParser($this->parseMysql());
+        $this->parser = new Parser($this->parseMysql());
     }
 
     public function isClosed(): bool
@@ -1284,7 +1285,7 @@ private function makeCompressedPacket(int $uncompressed, string $packet): string
      *
      * @return \Generator<int, int, string, void>
      */
-    private function parseCompression(GeneratorParser $parser): \Generator
+    private function parseCompression(Parser $parser): \Generator
     {
         while (true) {
             $buffer = yield 7;
@@ -1351,7 +1352,7 @@ private function parsePayload(string $packet): void
             switch (\ord($packet)) {
                 case self::OK_PACKET:
                     if ($this->capabilities & self::CLIENT_COMPRESS) {
-                        $this->parser = new GeneratorParser($this->parseCompression($this->parser));
+                        $this->parser = new Parser($this->parseCompression($this->parser));
                     }
                     $this->connectionState = ConnectionState::Ready;
                     $this->handleOk($packet);
diff --git a/src/Internal/GeneratorParser.php b/src/Internal/GeneratorParser.php
deleted file mode 100644
index 9a0ffea..0000000
--- a/src/Internal/GeneratorParser.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Amp\Mysql\Internal;
-
-final class GeneratorParser
-{
-    private ?\Generator $generator;
-
-    private array $buffers = [];
-
-    private int $bufferLength = 0;
-
-    private int $length;
-
-    public function __construct(\Generator $generator)
-    {
-        $this->generator = $generator;
-        $this->length = $this->generator->current();
-
-        if (!$this->generator->valid()) {
-            $this->generator = null;
-        }
-    }
-
-    final public function cancel(): void
-    {
-        $this->generator = null;
-    }
-
-    final public function isValid(): bool
-    {
-        return $this->generator !== null;
-    }
-
-    final public function push(string $data): void
-    {
-        if ($this->generator === null) {
-            throw new \Error("The parser is no longer writable");
-        }
-
-        $this->buffers[] = $data;
-        $this->bufferLength += \strlen($data);
-
-        if ($this->bufferLength >= $this->length) {
-            $offset = 0;
-            $buffer = \implode('', $this->buffers);
-
-            while ($this->bufferLength >= $this->length) {
-                $send = \substr($buffer, $offset, $this->length);
-                $offset += $this->length;
-                $this->bufferLength -= $this->length;
-
-                $this->length = $this->generator->send($send);
-            }
-
-            $this->buffers = [\substr($buffer, $offset)];
-        }
-    }
-}

From f3f6d350c06cfb341fea664220c00e3980de5331 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 31 Dec 2022 11:03:14 -0600
Subject: [PATCH 137/174] Update to Psalm 5.4

---
 composer.json                          | 2 +-
 src/Internal/ConnectionProcessor.php   | 2 ++
 src/Internal/MysqlCommandResult.php    | 8 ++++++--
 src/Internal/MysqlConnectionResult.php | 6 +++++-
 src/Internal/MysqlPooledResult.php     | 3 ++-
 src/Internal/MysqlResultProxy.php      | 6 ++++++
 6 files changed, 22 insertions(+), 5 deletions(-)

diff --git a/composer.json b/composer.json
index 1ea2c4b..63279f7 100644
--- a/composer.json
+++ b/composer.json
@@ -30,7 +30,7 @@
         "amphp/phpunit-util": "^3",
         "amphp/php-cs-fixer-config": "^2",
         "phpbench/phpbench": "^1.2.6",
-        "psalm/phar": "^4.22"
+        "psalm/phar": "^5.4"
     },
     "minimum-stability": "dev",
     "prefer-stable": true,
diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 8487d8a..ef9a11b 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -1284,6 +1284,8 @@ private function makeCompressedPacket(int $uncompressed, string $packet): string
      * @see 14.4 Compression
      *
      * @return \Generator<int, int, string, void>
+     *
+     * @psalm-suppress InvalidReturnType Psalm confuses this function to have a return of never
      */
     private function parseCompression(Parser $parser): \Generator
     {
diff --git a/src/Internal/MysqlCommandResult.php b/src/Internal/MysqlCommandResult.php
index 2bce0bd..92f15eb 100644
--- a/src/Internal/MysqlCommandResult.php
+++ b/src/Internal/MysqlCommandResult.php
@@ -6,12 +6,16 @@
 use Amp\Mysql\MysqlResult;
 use Amp\Sql\Common\CommandResult;
 
-/** @internal */
+/**
+ * @internal
+ * @psalm-import-type TRow from MysqlResult
+ * @implements \IteratorAggregate<int, never>
+ */
 final class MysqlCommandResult implements MysqlResult, \IteratorAggregate
 {
     private ?int $lastInsertId;
 
-    /** @var CommandResult<MysqlResult> */
+    /** @var CommandResult<TRow, MysqlResult> */
     private readonly CommandResult $delegate;
 
     public function __construct(int $affectedRows, int $lastInsertId)
diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index 216cbe3..0c1ceaf 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -8,7 +8,11 @@
 use Revolt\EventLoop;
 use function Amp\async;
 
-/** @internal */
+/**
+ * @internal
+ * @psalm-import-type TRow from MysqlResult
+ * @implements \IteratorAggregate<int, array<string, TRow>>
+ */
 final class MysqlConnectionResult implements MysqlResult, \IteratorAggregate
 {
     private readonly MysqlResultProxy $result;
diff --git a/src/Internal/MysqlPooledResult.php b/src/Internal/MysqlPooledResult.php
index 71fdf7b..7753d5a 100644
--- a/src/Internal/MysqlPooledResult.php
+++ b/src/Internal/MysqlPooledResult.php
@@ -8,7 +8,8 @@
 
 /**
  * @internal
- * @extends PooledResult<MysqlResult>
+ * @psalm-import-type TRow from MysqlResult
+ * @extends PooledResult<TRow, MysqlResult>
  */
 final class MysqlPooledResult extends PooledResult implements MysqlResult
 {
diff --git a/src/Internal/MysqlResultProxy.php b/src/Internal/MysqlResultProxy.php
index c10919f..953c75a 100644
--- a/src/Internal/MysqlResultProxy.php
+++ b/src/Internal/MysqlResultProxy.php
@@ -47,6 +47,9 @@ public function __construct(
         $this->columnsToFetch = $columnsToFetch ?? $this->columnCount;
     }
 
+    /**
+     * @return list<MysqlColumnDefinition>
+     */
     public function getColumnDefinitions(): array
     {
         if ($this->state === MysqlResultProxyState::Initial) {
@@ -57,6 +60,9 @@ public function getColumnDefinitions(): array
         return $this->columns;
     }
 
+    /**
+     * @return list<MysqlColumnDefinition>
+     */
     public function getParameterDefinitions(): array
     {
         if ($this->state === MysqlResultProxyState::Initial) {

From 66c9d2db75a999aebdfbc97830e1632e7eab238f Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 31 Dec 2022 11:46:58 -0600
Subject: [PATCH 138/174] Update result template types

---
 src/Internal/MysqlCommandResult.php    | 4 ++--
 src/Internal/MysqlConnectionResult.php | 4 ++--
 src/Internal/MysqlPooledResult.php     | 4 ++--
 src/Internal/MysqlResultProxy.php      | 8 ++++----
 src/MysqlResult.php                    | 5 +++--
 5 files changed, 13 insertions(+), 12 deletions(-)

diff --git a/src/Internal/MysqlCommandResult.php b/src/Internal/MysqlCommandResult.php
index 92f15eb..80f6524 100644
--- a/src/Internal/MysqlCommandResult.php
+++ b/src/Internal/MysqlCommandResult.php
@@ -8,14 +8,14 @@
 
 /**
  * @internal
- * @psalm-import-type TRow from MysqlResult
+ * @psalm-import-type TFieldType from MysqlResult
  * @implements \IteratorAggregate<int, never>
  */
 final class MysqlCommandResult implements MysqlResult, \IteratorAggregate
 {
     private ?int $lastInsertId;
 
-    /** @var CommandResult<TRow, MysqlResult> */
+    /** @var CommandResult<TFieldType, MysqlResult> */
     private readonly CommandResult $delegate;
 
     public function __construct(int $affectedRows, int $lastInsertId)
diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index 0c1ceaf..7ca4b7c 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -10,8 +10,8 @@
 
 /**
  * @internal
- * @psalm-import-type TRow from MysqlResult
- * @implements \IteratorAggregate<int, array<string, TRow>>
+ * @psalm-import-type TRowType from MysqlResult
+ * @implements \IteratorAggregate<int, TRowType>
  */
 final class MysqlConnectionResult implements MysqlResult, \IteratorAggregate
 {
diff --git a/src/Internal/MysqlPooledResult.php b/src/Internal/MysqlPooledResult.php
index 7753d5a..998c8a4 100644
--- a/src/Internal/MysqlPooledResult.php
+++ b/src/Internal/MysqlPooledResult.php
@@ -8,8 +8,8 @@
 
 /**
  * @internal
- * @psalm-import-type TRow from MysqlResult
- * @extends PooledResult<TRow, MysqlResult>
+ * @psalm-import-type TFieldType from MysqlResult
+ * @extends PooledResult<TFieldType, MysqlResult>
  */
 final class MysqlPooledResult extends PooledResult implements MysqlResult
 {
diff --git a/src/Internal/MysqlResultProxy.php b/src/Internal/MysqlResultProxy.php
index 953c75a..f85c0e4 100644
--- a/src/Internal/MysqlResultProxy.php
+++ b/src/Internal/MysqlResultProxy.php
@@ -10,7 +10,7 @@
 
 /**
  * @internal
- * @psalm-import-type TRow from MysqlResult
+ * @psalm-import-type TFieldType from MysqlResult
  */
 final class MysqlResultProxy
 {
@@ -22,10 +22,10 @@ final class MysqlResultProxy
     /** @var list<MysqlColumnDefinition> */
     public array $params = [];
 
-    /** @var Queue<list<TRow>> */
+    /** @var Queue<list<TFieldType>> */
     private readonly Queue $rowQueue;
 
-    /** @var ConcurrentIterator<list<TRow>> */
+    /** @var ConcurrentIterator<list<TFieldType>> */
     public readonly ConcurrentIterator $rowIterator;
 
     private ?DeferredFuture $columnDeferred = null;
@@ -83,7 +83,7 @@ public function markDefinitionsFetched(): void
     }
 
     /**
-     * @param list<TRow> $row
+     * @param list<TFieldType> $row
      */
     public function pushRow(array $row): void
     {
diff --git a/src/MysqlResult.php b/src/MysqlResult.php
index 8356c1d..eff4f2e 100644
--- a/src/MysqlResult.php
+++ b/src/MysqlResult.php
@@ -5,9 +5,10 @@
 use Amp\Sql\Result;
 
 /**
- * @psalm-type TRow = int|float|string|null
+ * @psalm-type TFieldType = int|float|string|null
+ * @psalm-type TRowType = array<string, TFieldType>
  *
- * @extends Result<TRow>
+ * @extends Result<TFieldType>
  */
 interface MysqlResult extends Result
 {

From 0c4a4c03ac32741fa2ea015077e20dffde226219 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 12 Feb 2023 09:36:22 -0600
Subject: [PATCH 139/174] Update for amphp/socket v2.0

---
 src/Internal/ConnectionProcessor.php | 6 +++---
 src/SocketMysqlConnection.php        | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index ef9a11b..e9e4651 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -10,7 +10,7 @@
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlDataType;
 use Amp\Parser\Parser;
-use Amp\Socket\EncryptableSocket;
+use Amp\Socket\Socket;
 use Amp\Sql\ConnectionException;
 use Amp\Sql\QueryError;
 use Amp\Sql\SqlException;
@@ -53,7 +53,7 @@ class ConnectionProcessor implements TransientResource
     private int $seqId = -1;
     private int $compressionId = -1;
 
-    private readonly EncryptableSocket $socket;
+    private readonly Socket $socket;
 
     private ?string $query = null;
     private array $named = [];
@@ -127,7 +127,7 @@ class ConnectionProcessor implements TransientResource
     private const EOF_PACKET = 0xfe;
     private const ERR_PACKET = 0xff;
 
-    public function __construct(EncryptableSocket $socket, MysqlConfig $config)
+    public function __construct(Socket $socket, MysqlConfig $config)
     {
         $this->socket = $socket;
         $this->metadata = new MysqlConnectionMetdata();
diff --git a/src/SocketMysqlConnection.php b/src/SocketMysqlConnection.php
index e6883d5..76da5ae 100644
--- a/src/SocketMysqlConnection.php
+++ b/src/SocketMysqlConnection.php
@@ -4,7 +4,7 @@
 
 use Amp\Cancellation;
 use Amp\DeferredFuture;
-use Amp\Socket\EncryptableSocket;
+use Amp\Socket\Socket;
 use Amp\Sql\TransactionIsolation;
 use Amp\Sql\TransactionIsolationLevel;
 use Revolt\EventLoop;
@@ -19,7 +19,7 @@ final class SocketMysqlConnection implements MysqlConnection
     private readonly \Closure $release;
 
     public static function initialize(
-        EncryptableSocket $socket,
+        Socket $socket,
         MysqlConfig $config,
         ?Cancellation $cancellation = null,
     ): self {

From 5d74589b07c5fbb14b5708ca7749edf32f5e4094 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 12 Feb 2023 09:37:13 -0600
Subject: [PATCH 140/174] Fix variable name

---
 src/Internal/MysqlConnectionTransaction.php | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/Internal/MysqlConnectionTransaction.php b/src/Internal/MysqlConnectionTransaction.php
index d7c5cd5..02cc08a 100644
--- a/src/Internal/MysqlConnectionTransaction.php
+++ b/src/Internal/MysqlConnectionTransaction.php
@@ -167,9 +167,9 @@ public function commit(): void
             throw new TransactionError("The transaction has been committed or rolled back");
         }
 
-        $promise = $this->processor->query("COMMIT");
+        $future = $this->processor->query("COMMIT");
         $this->processor = null;
-        $promise->finally($this->onClose->complete(...))->await();
+        $future->finally($this->onClose->complete(...))->await();
     }
 
     /**
@@ -183,9 +183,9 @@ public function rollback(): void
             throw new TransactionError("The transaction has been committed or rolled back");
         }
 
-        $promise = $this->processor->query("ROLLBACK");
+        $future = $this->processor->query("ROLLBACK");
         $this->processor = null;
-        $promise->finally($this->onClose->complete(...))->await();
+        $future->finally($this->onClose->complete(...))->await();
     }
 
     /**

From 857b0adfff0c84d22a81e9a6226efc67f820a7f6 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 12 Feb 2023 09:40:51 -0600
Subject: [PATCH 141/174] Use spread operator for config key map declaration

---
 src/MysqlConfig.php | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/MysqlConfig.php b/src/MysqlConfig.php
index 51bcbed..9a59258 100644
--- a/src/MysqlConfig.php
+++ b/src/MysqlConfig.php
@@ -11,11 +11,7 @@ final class MysqlConfig extends SqlConfig
     public const BIN_CHARSET = 45; // utf8mb4_general_ci
 
     public const KEY_MAP = [
-        'hostname' => 'host',
-        'username' => 'user',
-        'pass' => 'password',
-        'database' => 'db',
-        'dbname' => 'db',
+        ...parent::KEY_MAP,
         'compress' => 'compression',
         'useCompression' => 'compression',
         'cs' => 'charset',

From 7cdfc1656f9b1cc8af225fe4c02c2f75f4a2bfff Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Mon, 1 May 2023 19:26:36 -0500
Subject: [PATCH 142/174] Fix Psalm issues

---
 src/Internal/ConnectionProcessor.php | 9 +++++++--
 src/MysqlConfig.php                  | 2 ++
 2 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index e9e4651..2d2a1c4 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -2,6 +2,7 @@
 
 namespace Amp\Mysql\Internal;
 
+use Amp\ByteStream\ResourceStream;
 use Amp\Cancellation;
 use Amp\DeferredFuture;
 use Amp\File;
@@ -179,14 +180,18 @@ private function ready(): void
         }
 
         $this->resetIds();
-        $this->socket->unreference();
+        if ($this->socket instanceof ResourceStream) {
+            $this->socket->unreference();
+        }
     }
 
     private function enqueueDeferred(DeferredFuture $deferred): void
     {
         \assert(!$this->socket->isClosed(), "The connection has been closed");
         $this->deferreds->push($deferred);
-        $this->socket->reference();
+        if ($this->socket instanceof ResourceStream) {
+            $this->socket->reference();
+        }
     }
 
     public function connect(?Cancellation $cancellation = null): void
diff --git a/src/MysqlConfig.php b/src/MysqlConfig.php
index 9a59258..074da5e 100644
--- a/src/MysqlConfig.php
+++ b/src/MysqlConfig.php
@@ -10,6 +10,7 @@ final class MysqlConfig extends SqlConfig
     public const DEFAULT_PORT = 3306;
     public const BIN_CHARSET = 45; // utf8mb4_general_ci
 
+    /** @var array<non-empty-string, non-empty-string> Psalm has a bug with the spread operator. */
     public const KEY_MAP = [
         ...parent::KEY_MAP,
         'compress' => 'compression',
@@ -36,6 +37,7 @@ final class MysqlConfig extends SqlConfig
 
     public static function fromString(string $connectionString, ConnectContext $context = null): self
     {
+        /** @psalm-suppress InvalidArgument Psalm does not recognize {@see self::KEY_MAP} is in fact a map. */
         $parts = self::parseConnectionString($connectionString, self::KEY_MAP);
 
         if (!isset($parts['host'])) {

From ef4fc6f72c506700f96cb280a6e53a0b15194266 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 7 May 2023 08:27:14 -0500
Subject: [PATCH 143/174] Update MysqlCommandResult

---
 src/Internal/MysqlCommandResult.php | 41 +++++++++--------------------
 1 file changed, 12 insertions(+), 29 deletions(-)

diff --git a/src/Internal/MysqlCommandResult.php b/src/Internal/MysqlCommandResult.php
index 80f6524..4b05646 100644
--- a/src/Internal/MysqlCommandResult.php
+++ b/src/Internal/MysqlCommandResult.php
@@ -9,57 +9,40 @@
 /**
  * @internal
  * @psalm-import-type TFieldType from MysqlResult
- * @implements \IteratorAggregate<int, never>
+ * @extends CommandResult<TFieldType, MysqlResult>
  */
-final class MysqlCommandResult implements MysqlResult, \IteratorAggregate
+final class MysqlCommandResult extends CommandResult implements MysqlResult
 {
     private ?int $lastInsertId;
 
-    /** @var CommandResult<TFieldType, MysqlResult> */
-    private readonly CommandResult $delegate;
-
     public function __construct(int $affectedRows, int $lastInsertId)
     {
         /** @var Future<MysqlResult|null> $future Explicit declaration for Psalm. */
         $future = Future::complete();
 
-        $this->delegate = new CommandResult($affectedRows, $future);
+        parent::__construct($affectedRows, $future);
         $this->lastInsertId = $lastInsertId ?: null; // Convert 0 to null
     }
 
-    public function getIterator(): \Traversable
-    {
-        return $this->delegate->getIterator();
-    }
-
-    public function fetchRow(): ?array
-    {
-        return $this->delegate->fetchRow();
-    }
-
-    public function getRowCount(): int
-    {
-        return $this->delegate->getRowCount();
-    }
-
-    public function getColumnCount(): ?int
+    /**
+     * Changes return type to this library's Result type.
+     */
+    public function getNextResult(): ?MysqlResult
     {
-        return $this->delegate->getColumnCount();
+        return parent::getNextResult();
     }
 
     /**
-     * @return int|null Insert ID of the last auto increment row.
+     * @return int|null Insert ID of the last auto increment row or null if not applicable to the query.
      */
     public function getLastInsertId(): ?int
     {
         return $this->lastInsertId;
     }
 
-    public function getNextResult(): ?MysqlResult
-    {
-        return $this->delegate->getNextResult();
-    }
-
+    /**
+     * @return null Always returns null as command results do not have a field list.
+     */
     public function getColumnDefinitions(): ?array
     {
         return null; // Command results do not have a field list.

From 8a7d4ac964d7efd3f83f4a0a05fa3af2b6978a8f Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 7 May 2023 08:48:11 -0500
Subject: [PATCH 144/174] Nested transactions

---
 src/Internal/ConnectionProcessor.php      | 16 +++++--
 src/Internal/MysqlNestedTransaction.php   | 22 +++++++++
 src/Internal/MysqlPooledTransaction.php   | 37 ++-------------
 src/Internal/MysqlTransactionDelegate.php | 51 ++++++++++++++++++++
 src/MysqlNestableTransaction.php          | 55 +++++++++++++++++++++
 src/SocketMysqlConnection.php             |  3 +-
 test/LinkTest.php                         | 58 ++++++++++++++++++-----
 test/NestedTransactionTest.php            | 32 +++++++++++++
 8 files changed, 222 insertions(+), 52 deletions(-)
 create mode 100644 src/Internal/MysqlNestedTransaction.php
 create mode 100644 src/Internal/MysqlTransactionDelegate.php
 create mode 100644 src/MysqlNestableTransaction.php
 create mode 100644 test/NestedTransactionTest.php

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 2d2a1c4..0031116 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -324,7 +324,11 @@ public function useDatabase(string $database): Future
         });
     }
 
-    /** @see 14.6.4 COM_QUERY */
+    /**
+     * @see 14.6.4 COM_QUERY
+     *
+     * @return Future<MysqlConnectionResult|MysqlCommandResult>
+     */
     public function query(string $query): Future
     {
         return $this->startCommand(function () use ($query): void {
@@ -334,7 +338,11 @@ public function query(string $query): Future
         });
     }
 
-    /** @see 14.7.4 COM_STMT_PREPARE */
+    /**
+     * @see 14.7.4 COM_STMT_PREPARE
+     *
+     * @return Future<MysqlConnectionStatement>
+     */
     public function prepare(string $query): Future
     {
         return $this->startCommand(function () use ($query): void {
@@ -738,7 +746,9 @@ private function parseOk(string $packet): void
     private function handleOk(string $packet): void
     {
         $this->parseOk($packet);
-        $this->dequeueDeferred()->complete(new MysqlCommandResult($this->metadata->affectedRows, $this->metadata->insertId));
+        $this->dequeueDeferred()->complete(
+            new MysqlCommandResult($this->metadata->affectedRows, $this->metadata->insertId),
+        );
         $this->ready();
     }
 
diff --git a/src/Internal/MysqlNestedTransaction.php b/src/Internal/MysqlNestedTransaction.php
new file mode 100644
index 0000000..0799bf5
--- /dev/null
+++ b/src/Internal/MysqlNestedTransaction.php
@@ -0,0 +1,22 @@
+<?php declare(strict_types=1);
+
+namespace Amp\Mysql\Internal;
+
+use Amp\Mysql\MysqlResult;
+use Amp\Mysql\MysqlStatement;
+use Amp\Mysql\MysqlTransaction;
+use Amp\Sql\Common\NestedTransaction;
+
+/**
+ * @internal
+ * @extends NestedTransaction<MysqlResult, MysqlStatement, MysqlTransaction>
+ */
+class MysqlNestedTransaction extends NestedTransaction implements MysqlTransaction
+{
+    use MysqlTransactionDelegate;
+
+    protected function getTransaction(): MysqlTransaction
+    {
+        return $this->transaction;
+    }
+}
diff --git a/src/Internal/MysqlPooledTransaction.php b/src/Internal/MysqlPooledTransaction.php
index 4bfe4df..0cfb787 100644
--- a/src/Internal/MysqlPooledTransaction.php
+++ b/src/Internal/MysqlPooledTransaction.php
@@ -6,8 +6,6 @@
 use Amp\Mysql\MysqlStatement;
 use Amp\Mysql\MysqlTransaction;
 use Amp\Sql\Common\PooledTransaction;
-use Amp\Sql\Result;
-use Amp\Sql\Statement;
 
 /**
  * @internal
@@ -15,39 +13,10 @@
  */
 final class MysqlPooledTransaction extends PooledTransaction implements MysqlTransaction
 {
-    protected function createStatement(Statement $statement, \Closure $release): MysqlStatement
-    {
-        \assert($statement instanceof MysqlStatement);
-        return new MysqlPooledStatement($statement, $release);
-    }
-
-    protected function createResult(Result $result, \Closure $release): MysqlResult
-    {
-        \assert($result instanceof MysqlResult);
-        return new MysqlPooledResult($result, $release);
-    }
-
-    /**
-     * Changes return type to this library's Result type.
-     */
-    public function query(string $sql): MysqlResult
-    {
-        return parent::query($sql);
-    }
-
-    /**
-     * Changes return type to this library's Statement type.
-     */
-    public function prepare(string $sql): MysqlStatement
-    {
-        return parent::prepare($sql);
-    }
+    use MysqlTransactionDelegate;
 
-    /**
-     * Changes return type to this library's Result type.
-     */
-    public function execute(string $sql, array $params = []): MysqlResult
+    protected function getTransaction(): MysqlTransaction
     {
-        return parent::execute($sql, $params);
+        return $this->transaction;
     }
 }
diff --git a/src/Internal/MysqlTransactionDelegate.php b/src/Internal/MysqlTransactionDelegate.php
new file mode 100644
index 0000000..004123e
--- /dev/null
+++ b/src/Internal/MysqlTransactionDelegate.php
@@ -0,0 +1,51 @@
+<?php declare(strict_types=1);
+
+namespace Amp\Mysql\Internal;
+
+use Amp\Mysql\MysqlResult;
+use Amp\Mysql\MysqlStatement;
+use Amp\Mysql\MysqlTransaction;
+use Amp\Sql\Result;
+use Amp\Sql\Statement;
+
+/** @internal */
+trait MysqlTransactionDelegate
+{
+    abstract protected function getTransaction(): MysqlTransaction;
+
+    protected function createStatement(Statement $statement, \Closure $release): MysqlStatement
+    {
+        \assert($statement instanceof MysqlStatement);
+        return new MysqlPooledStatement($statement, $release);
+    }
+
+    protected function createResult(Result $result, \Closure $release): MysqlResult
+    {
+        \assert($result instanceof MysqlResult);
+        return new MysqlPooledResult($result, $release);
+    }
+
+    /**
+     * Changes return type to this library's Result type.
+     */
+    public function query(string $sql): MysqlResult
+    {
+        return parent::query($sql);
+    }
+
+    /**
+     * Changes return type to this library's Statement type.
+     */
+    public function prepare(string $sql): MysqlStatement
+    {
+        return parent::prepare($sql);
+    }
+
+    /**
+     * Changes return type to this library's Result type.
+     */
+    public function execute(string $sql, array $params = []): MysqlResult
+    {
+        return parent::execute($sql, $params);
+    }
+}
diff --git a/src/MysqlNestableTransaction.php b/src/MysqlNestableTransaction.php
new file mode 100644
index 0000000..f89f9a3
--- /dev/null
+++ b/src/MysqlNestableTransaction.php
@@ -0,0 +1,55 @@
+<?php declare(strict_types=1);
+
+namespace Amp\Mysql;
+
+use Amp\Sql\Common\NestableTransaction;
+use Amp\Sql\Transaction;
+use Amp\Sql\TransactionIsolation;
+use Amp\Sql\TransactionIsolationLevel;
+
+/**
+ * @extends NestableTransaction<MysqlResult, MysqlStatement, MysqlTransaction>
+ */
+class MysqlNestableTransaction extends NestableTransaction implements MysqlLink
+{
+    protected function createNestedTransaction(
+        Transaction $transaction,
+        \Closure $release,
+        string $identifier,
+    ): Transaction {
+        return new Internal\MysqlNestedTransaction($transaction, $release, $identifier);
+    }
+
+    /**
+     * Changes return type to this library's Transaction type.
+     */
+    public function beginTransaction(
+        TransactionIsolation $isolation = TransactionIsolationLevel::Committed
+    ): MysqlTransaction {
+        return parent::beginTransaction($isolation);
+    }
+
+    /**
+     * Changes return type to this library's Result type.
+     */
+    public function query(string $sql): MysqlResult
+    {
+        return parent::query($sql);
+    }
+
+    /**
+     * Changes return type to this library's Statement type.
+     */
+    public function prepare(string $sql): MysqlStatement
+    {
+        return parent::prepare($sql);
+    }
+
+    /**
+     * Changes return type to this library's Result type.
+     */
+    public function execute(string $sql, array $params = []): MysqlResult
+    {
+        return parent::execute($sql, $params);
+    }
+}
diff --git a/src/SocketMysqlConnection.php b/src/SocketMysqlConnection.php
index 76da5ae..0d31ca4 100644
--- a/src/SocketMysqlConnection.php
+++ b/src/SocketMysqlConnection.php
@@ -34,8 +34,7 @@ private function __construct(Internal\ConnectionProcessor $processor)
 
         $busy = &$this->busy;
         $this->release = static function () use (&$busy): void {
-            \assert($busy instanceof DeferredFuture);
-            $busy->complete();
+            $busy?->complete();
             $busy = null;
         };
     }
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 1b5bac8..553c722 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -2,6 +2,7 @@
 
 namespace Amp\Mysql\Test;
 
+use Amp\Future;
 use Amp\Mysql\MysqlColumnDefinition;
 use Amp\Mysql\MysqlDataType;
 use Amp\Mysql\MysqlLink;
@@ -9,6 +10,8 @@
 use Amp\Sql\QueryError;
 use Amp\Sql\Result;
 use Amp\Sql\SqlException;
+use function Amp\async;
+use function Amp\delay;
 
 abstract class LinkTest extends MysqlTestCase
 {
@@ -113,7 +116,7 @@ public function testNextResultBeforeConsumption()
         $result->getNextResult();
     }
 
-    public function testQueryWithUnconsumedTupleResult()
+    public function testQueryWithUnconsumedTupleResult(): void
     {
         $db = $this->getLink();
 
@@ -128,7 +131,7 @@ public function testQueryWithUnconsumedTupleResult()
         $this->assertInstanceOf(Result::class, $result);
     }
 
-    public function testUnconsumedMultiResult()
+    public function testUnconsumedMultiResult(): void
     {
         $db = $this->getLink(true);
 
@@ -145,7 +148,34 @@ public function testUnconsumedMultiResult()
         self::assertSame([['a' => 5, 'b' => 6]], $got);
     }
 
-    public function testPrepared()
+    public function testSimultaneousQuery(): void
+    {
+        $db = $this->getLink(true);
+
+        $future1 = async(function () use ($db): void {
+            $result = $db->query("SELECT a FROM main");
+            $got = [];
+            foreach ($result as $row) {
+                $got[] = $row['a'];
+                delay(0.1);
+            }
+            self::assertSame(\range(1, \count($got)), $got);
+        });
+
+        $future2 = async(function () use ($db): void {
+            $result = $db->query("SELECT b FROM main");
+            $got = [];
+            foreach ($result as $row) {
+                $got[] = $row['b'];
+                delay(0.1);
+            }
+            self::assertSame(\range(2, \count($got) + 1), $got);
+        });
+
+        Future\await([$future1, $future2]);
+    }
+
+    public function testPrepared(): void
     {
         $db = $this->getLink(true);
 
@@ -231,7 +261,7 @@ public function testPrepared()
         $this->assertInstanceOf(MysqlResult::class, $result);
     }
 
-    public function testPrepareWithInvalidQuery()
+    public function testPrepareWithInvalidQuery(): void
     {
         $this->expectException(QueryError::class);
         $this->expectExceptionMessage('You have an error in your SQL syntax');
@@ -243,7 +273,7 @@ public function testPrepareWithInvalidQuery()
         $statement->execute(); // Some implementations do not throw until execute() is called.
     }
 
-    public function testBindWithInvalidParamId()
+    public function testBindWithInvalidParamId(): void
     {
         $this->expectException(\Error::class);
         $this->expectExceptionMessage('Parameter 1 is not defined for this prepared statement');
@@ -257,7 +287,7 @@ public function testBindWithInvalidParamId()
         $statement->execute(); // Some implementations do not throw until execute() is called.
     }
 
-    public function testBindWithInvalidParamName()
+    public function testBindWithInvalidParamName(): void
     {
         $this->expectException(\Error::class);
         $this->expectExceptionMessage('Named parameter :b is not defined for this prepared statement');
@@ -271,7 +301,7 @@ public function testBindWithInvalidParamName()
         $statement->execute(); // Some implementations do not throw until execute() is called.
     }
 
-    public function testStatementExecuteWithTooFewParams()
+    public function testStatementExecuteWithTooFewParams(): void
     {
         $this->expectException(\Error::class);
         $this->expectExceptionMessage('Parameter 1 missing for executing prepared statement');
@@ -282,7 +312,7 @@ public function testStatementExecuteWithTooFewParams()
         $stmt->execute([1]);
     }
 
-    public function testExecute()
+    public function testExecute(): void
     {
         $db = $this->getLink();
 
@@ -303,7 +333,7 @@ public function testExecute()
         $this->assertInstanceOf(MysqlResult::class, $result);
     }
 
-    public function testExecuteWithInvalidQuery()
+    public function testExecuteWithInvalidQuery(): void
     {
         $this->expectException(QueryError::class);
         $this->expectExceptionMessage('You have an error in your SQL syntax');
@@ -315,7 +345,7 @@ public function testExecuteWithInvalidQuery()
         $db->close();
     }
 
-    public function testExecuteWithTooFewParams()
+    public function testExecuteWithTooFewParams(): void
     {
         $this->expectException(\Error::class);
         $this->expectExceptionMessage('Parameter 1 missing for executing prepared statement');
@@ -327,7 +357,7 @@ public function testExecuteWithTooFewParams()
         $db->close();
     }
 
-    public function testPreparedWithNegativeValue()
+    public function testPreparedWithNegativeValue(): void
     {
         $db = $this->getLink();
 
@@ -345,7 +375,7 @@ public function testPreparedWithNegativeValue()
         $db->close();
     }
 
-    public function testTransaction()
+    public function testTransaction(): void
     {
         $db = $this->getLink();
 
@@ -356,6 +386,8 @@ public function testTransaction()
         $this->assertInstanceOf(MysqlResult::class, $result);
         $this->assertGreaterThan(5, $result->getLastInsertId());
 
+        $statement->close();
+
         $result = $transaction->query("SELECT * FROM main WHERE a = 6");
 
         $got = [];
@@ -390,7 +422,7 @@ public function testTransaction()
     /**
      * @depends testTransaction
      */
-    public function testInsertSelect()
+    public function testInsertSelect(): void
     {
         $db = $this->getLink();
 
diff --git a/test/NestedTransactionTest.php b/test/NestedTransactionTest.php
new file mode 100644
index 0000000..77669c9
--- /dev/null
+++ b/test/NestedTransactionTest.php
@@ -0,0 +1,32 @@
+<?php declare(strict_types=1);
+
+namespace Amp\Mysql\Test;
+
+use Amp\Mysql\MysqlLink;
+use Amp\Mysql\MysqlNestableTransaction;
+use Amp\Mysql\MysqlTransaction;
+use Amp\Mysql\SocketMysqlConnector;
+
+class NestedTransactionTest extends LinkTest
+{
+    private ?MysqlLink $link;
+    private ?MysqlTransaction $transaction = null;
+
+    public function getLink(bool $useCompression = false): MysqlLink
+    {
+        $this->link = (new SocketMysqlConnector)->connect($this->getConfig($useCompression));
+        $this->transaction = $this->link->beginTransaction();
+        return new MysqlNestableTransaction($this->transaction);
+    }
+
+    public function tearDown(): void
+    {
+        if ($this->transaction?->isActive()) {
+            $this->transaction->rollback();
+        }
+
+        $this->link?->close();
+
+        parent::tearDown();
+    }
+}

From a2678536818c351bf972a28aa3e96d278ab356f5 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 31 Aug 2023 21:56:55 -0500
Subject: [PATCH 145/174] Dispose of current result when next result is
 requested

---
 src/Internal/MysqlConnectionResult.php | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index 7ca4b7c..1de1a63 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -72,9 +72,7 @@ public function fetchRow(): ?array
 
     public function getNextResult(): ?MysqlResult
     {
-        if ($this->generator->valid()) {
-            throw new \Error('Consume entire current result before requesting next result');
-        }
+        self::dispose($this->generator);
 
         $this->nextResult ??= async(function (): ?MysqlResult {
             $deferred = $this->result->next ??= new DeferredFuture;

From 95f55f4327974be6783463fbb8d73b9891dc37c6 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 31 Aug 2023 22:29:33 -0500
Subject: [PATCH 146/174] Fix next result test

---
 test/LinkTest.php | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/test/LinkTest.php b/test/LinkTest.php
index 553c722..12ec3db 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -110,10 +110,12 @@ public function testNextResultBeforeConsumption()
 
         $result = $db->query("SELECT a FROM main; SELECT b FROM main;");
 
-        $this->expectException(\Error::class);
-        $this->expectExceptionMessage('Consume entire current result before requesting next result');
+        $result = $result->getNextResult();
 
-        $result->getNextResult();
+        $columns = $result->getColumnDefinitions();
+        self::assertCount(1, $columns);
+        $column = $columns[0];
+        self::assertSame('b', $column->name);
     }
 
     public function testQueryWithUnconsumedTupleResult(): void

From 8a2a8a207e4f09ff7f3252607cb33515814e06cf Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 31 Aug 2023 22:50:53 -0500
Subject: [PATCH 147/174] Suppress property reference error

---
 psalm.xml | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/psalm.xml b/psalm.xml
index 4e9226b..190bf1b 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -12,4 +12,12 @@
             <directory name="vendor" />
         </ignoreFiles>
     </projectFiles>
+
+    <issueHandlers>
+        <UnsupportedPropertyReferenceUsage>
+            <errorLevel type="suppress">
+                <directory name="src"/>
+            </errorLevel>
+        </UnsupportedPropertyReferenceUsage>
+    </issueHandlers>
 </psalm>

From 3ed04c576c1f43dd67bedda5bd1117eca45deab8 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 31 Aug 2023 23:18:29 -0500
Subject: [PATCH 148/174] Refactor MysqlColumnDefinition to use getters

---
 src/Internal/ConnectionProcessor.php   |  10 +-
 src/Internal/MysqlConnectionResult.php |   8 +-
 src/MysqlColumnDefinition.php          | 124 ++++++++++++++++++++++---
 test/LinkTest.php                      |  12 +--
 4 files changed, 130 insertions(+), 24 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 0031116..8ba4066 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -448,7 +448,7 @@ public function execute(int $stmtId, string $query, array $params, array $prebou
                         $bound = 1;
                     }
 
-                    $paramType = $params[$paramId]->type;
+                    $paramType = $params[$paramId]->getType();
 
                     if (isset($prebound[$paramId])) {
                         if (!$paramType->isBindable()) {
@@ -1042,7 +1042,7 @@ private function parseColumnDefinition(string $packet): MysqlColumnDefinition
             $column["defaults"] = MysqlDataType::decodeString($packet, $offset);
         }
 
-        /** @psalm-suppress InvalidScalarArgument */
+        /** @psalm-suppress InvalidScalarArgument, ArgumentTypeCoercion */
         return new MysqlColumnDefinition(...$column);
     }
 
@@ -1095,7 +1095,7 @@ private function handleTextResultSetRow(string $packet): void
                 $offset += 1;
             } else {
                 $column = $this->result->columns[$i] ?? throw new \RuntimeException("Definition missing for column $i");
-                $fields[] = $column->type->decodeText($packet, $offset, $column->flags);
+                $fields[] = $column->getType()->decodeText($packet, $offset, $column->getFlags());
             }
         }
 
@@ -1130,8 +1130,8 @@ private function handleBinaryResultSetRow(string $packet): void
             }
 
             $column = $this->result->columns[$i] ?? throw new \RuntimeException("Definition missing for column $i");
-            \assert($offset < \strlen($packet));
-            $fields[] = $column->type->decodeBinary($packet, $offset, $column->flags);
+            \assert($offset >= 0 && $offset < \strlen($packet));
+            $fields[] = $column->getType()->decodeBinary($packet, $offset, $column->getFlags());
         }
 
         $this->result->pushRow($fields);
diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index 1de1a63..d7063e7 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -4,6 +4,7 @@
 
 use Amp\DeferredFuture;
 use Amp\Future;
+use Amp\Mysql\MysqlColumnDefinition;
 use Amp\Mysql\MysqlResult;
 use Revolt\EventLoop;
 use function Amp\async;
@@ -29,7 +30,12 @@ public function __construct(MysqlResultProxy $result)
 
     private static function iterate(MysqlResultProxy $result): \Generator
     {
-        $columnNames = \array_column($result->getColumnDefinitions(), 'name');
+        static $mapper;
+
+        $columnNames = \array_map(
+            $mapper ??= static fn(MysqlColumnDefinition $cd) => $cd->getName(),
+            $result->getColumnDefinitions(),
+        );
 
         foreach ($result->rowIterator as $row) {
             yield \array_combine($columnNames, $row);
diff --git a/src/MysqlColumnDefinition.php b/src/MysqlColumnDefinition.php
index 2f2ac44..13d5384 100644
--- a/src/MysqlColumnDefinition.php
+++ b/src/MysqlColumnDefinition.php
@@ -4,19 +4,119 @@
 
 final class MysqlColumnDefinition
 {
+    /**
+     * @param non-empty-string $table
+     * @param non-empty-string $name
+     * @param int<0, max> $length
+     * @param MysqlDataType $type
+     * @param int<0, max> $flags
+     * @param int<0, max> $decimals
+     * @param non-empty-string|null $originalTable
+     * @param non-empty-string|null $originalName
+     * @param int<0, max>|null $charset
+     * @param non-empty-string|null $catalog
+     * @param non-empty-string|null $schema
+     */
     public function __construct(
-        public readonly string $table,
-        public readonly string $name,
-        public readonly int $length,
-        public readonly MysqlDataType $type,
-        public readonly int $flags,
-        public readonly int $decimals,
-        public readonly string $defaults = '',
-        public readonly ?string $originalTable = null,
-        public readonly ?string $originalName = null,
-        public readonly ?int $charset = null,
-        public readonly ?string $catalog = null,
-        public readonly ?string $schema = null,
+        private readonly string $table,
+        private readonly string $name,
+        private readonly int $length,
+        private readonly MysqlDataType $type,
+        private readonly int $flags,
+        private readonly int $decimals,
+        private readonly string $defaults = '',
+        private readonly ?string $originalTable = null,
+        private readonly ?string $originalName = null,
+        private readonly ?int $charset = null,
+        private readonly ?string $catalog = null,
+        private readonly ?string $schema = null,
     ) {
     }
+
+    public function getTable(): string
+    {
+        return $this->table;
+    }
+
+    /**
+     * @return non-empty-string
+     */
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    /**
+     * @return int<0, max>
+     */
+    public function getLength(): int
+    {
+        return $this->length;
+    }
+
+    public function getType(): MysqlDataType
+    {
+        return $this->type;
+    }
+
+    /**
+     * @return int<0, max>
+     */
+    public function getFlags(): int
+    {
+        return $this->flags;
+    }
+
+    /**
+     * @return int<0, max>
+     */
+    public function getDecimals(): int
+    {
+        return $this->decimals;
+    }
+
+    public function getDefaults(): string
+    {
+        return $this->defaults;
+    }
+
+    /**
+     * @return non-empty-string|null
+     */
+    public function getOriginalTable(): ?string
+    {
+        return $this->originalTable;
+    }
+
+    /**
+     * @return non-empty-string|null
+     */
+    public function getOriginalName(): ?string
+    {
+        return $this->originalName;
+    }
+
+    /**
+     * @return int<0, max>|null
+     */
+    public function getCharset(): ?int
+    {
+        return $this->charset;
+    }
+
+    /**
+     * @return non-empty-string|null
+     */
+    public function getCatalog(): ?string
+    {
+        return $this->catalog;
+    }
+
+    /**
+     * @return non-empty-string|null
+     */
+    public function getSchema(): ?string
+    {
+        return $this->schema;
+    }
 }
diff --git a/test/LinkTest.php b/test/LinkTest.php
index 12ec3db..bf3110e 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -95,11 +95,11 @@ public function testMultiStmt()
         $this->assertSame([["d" => 5, "c" => 5], ["d" => 6, "c" => 6]], $got);
 
         $this->assertCount(2, $fields);
-        $this->assertSame($fields[0]->originalName, "b");
-        $this->assertSame($fields[0]->name, "d");
-        $this->assertSame($fields[0]->type, MysqlDataType::Long);
-        $this->assertSame($fields[1]->name, "c");
-        $this->assertSame($fields[1]->type, MysqlDataType::LongLong);
+        $this->assertSame($fields[0]->getOriginalName(), "b");
+        $this->assertSame($fields[0]->getName(), "d");
+        $this->assertSame($fields[0]->getType(), MysqlDataType::Long);
+        $this->assertSame($fields[1]->getName(), "c");
+        $this->assertSame($fields[1]->getType(), MysqlDataType::LongLong);
 
         $this->assertNull($result->getNextResult());
     }
@@ -115,7 +115,7 @@ public function testNextResultBeforeConsumption()
         $columns = $result->getColumnDefinitions();
         self::assertCount(1, $columns);
         $column = $columns[0];
-        self::assertSame('b', $column->name);
+        self::assertSame('b', $column->getName());
     }
 
     public function testQueryWithUnconsumedTupleResult(): void

From ee161bddb16485db2aea69e48fd343540d56589d Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 31 Aug 2023 23:18:50 -0500
Subject: [PATCH 149/174] More specific int types

---
 src/MysqlDataType.php | 88 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 84 insertions(+), 4 deletions(-)

diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index bbb907c..898b2a6 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -105,7 +105,11 @@ private static function encodeValue(mixed $param): array
         }
     }
 
-    /** @see 14.7.3 Binary Protocol Value */
+    /**
+     * @see 14.7.3 Binary Protocol Value
+     *
+     * @param int<0, max> $offset
+     */
     public function decodeBinary(string $string, int &$offset = 0, int $flags = 0): int|float|string|null
     {
         $unsigned = $flags & 0x20;
@@ -177,6 +181,9 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
         }
     }
 
+    /**
+     * @param int<0, max> $offset
+     */
     public function decodeText(string $string, int &$offset = 0, int $flags = 0): int|float|string
     {
         $length = self::decodeUnsigned($string, $offset);
@@ -224,6 +231,9 @@ public function isBindable(): bool
         return $this !== self::Json;
     }
 
+    /**
+     * @param int<0, max> $offset
+     */
     private function decodeDateTime(string $string, int &$offset): string
     {
         $year = $month = $day = $hour = $minute = $second = $microsecond = 0;
@@ -270,6 +280,9 @@ private function decodeDateTime(string $string, int &$offset): string
         return $result;
     }
 
+    /**
+     * @param int<0, max> $offset
+     */
     private function decodeTime(string $string, int &$offset): string
     {
         $negative = $day = $hour = $minute = $second = $microsecond = 0;
@@ -308,6 +321,9 @@ private function decodeTime(string $string, int &$offset): string
         return $result;
     }
 
+    /**
+     * @param int<0, max> $offset
+     */
     public static function decodeNullTerminatedString(string $string, int &$offset = 0): string
     {
         $length = \strpos($string, "\0", $offset);
@@ -318,9 +334,14 @@ public static function decodeNullTerminatedString(string $string, int &$offset =
         $length -= $offset;
         $result = \substr($string, $offset, $length);
         $offset += $length + 1;
+        \assert($offset >= 0);
+
         return $result;
     }
 
+    /**
+     * @param int<0, max> $offset
+     */
     public static function decodeString(string $string, int &$offset = 0): string
     {
         $length = self::decodeUnsigned($string, $offset);
@@ -328,6 +349,11 @@ public static function decodeString(string $string, int &$offset = 0): string
         return \substr($string, $offset - $length, $length);
     }
 
+    /**
+     * @param int<0, max> $offset
+     *
+     * @return int<0, max>
+     */
     public static function decodeUnsigned(string $string, int &$offset = 0): int
     {
         $int = self::decodeUnsigned8($string, $offset);
@@ -344,11 +370,15 @@ public static function decodeUnsigned(string $string, int &$offset = 0): int
         };
     }
 
+    /**
+     * @param int<0, max> $offset
+     * @param int<0, max> $length
+     */
     public static function decodeIntByLength(string $string, int $length, int &$offset = 0): int
     {
         $int = 0;
-        while ($length--) {
-            $int = ($int << 8) + \ord($string[$length + $offset]);
+        while ($length) {
+            $int = ($int << 8) + \ord($string[--$length + $offset]);
         }
 
         $offset += $length;
@@ -356,6 +386,9 @@ public static function decodeIntByLength(string $string, int $length, int &$offs
         return $int;
     }
 
+    /**
+     * @param int<0, max> $offset
+     */
     public static function decodeInt8(string $string, int &$offset = 0): int
     {
         $int = \ord($string[$offset++]);
@@ -366,11 +399,21 @@ public static function decodeInt8(string $string, int &$offset = 0): int
         return $int << $shift >> $shift;
     }
 
+    /**
+     * @param int<0, max> $offset
+     *
+     * @return int<0, max>
+     */
     public static function decodeUnsigned8(string $string, int &$offset = 0): int
     {
-        return \ord($string[$offset++]);
+        $result = \ord($string[$offset++]);
+        \assert($result >= 0);
+        return $result;
     }
 
+    /**
+     * @param int<0, max> $offset
+     */
     public static function decodeInt16(string $string, int &$offset = 0): int
     {
         $int = \unpack("v", $string, $offset)[1];
@@ -382,12 +425,20 @@ public static function decodeInt16(string $string, int &$offset = 0): int
         return $int << $shift >> $shift;
     }
 
+    /**
+     * @param int<0, max> $offset
+     *
+     * @return int<0, max>
+     */
     public static function decodeUnsigned16(string $string, int &$offset = 0): int
     {
         $offset += 2;
         return \unpack("v", $string, $offset - 2)[1];
     }
 
+    /**
+     * @param int<0, max> $offset
+     */
     public static function decodeInt24(string $string, int &$offset = 0): int
     {
         $int = \unpack("V", \substr($string, $offset, 3) . "\x00")[1];
@@ -399,6 +450,11 @@ public static function decodeInt24(string $string, int &$offset = 0): int
         return $int << $shift >> $shift;
     }
 
+    /**
+     * @param int<0, max> $offset
+     *
+     * @return int<0, max>
+     */
     public static function decodeUnsigned24(string $string, int &$offset = 0): int
     {
         $result = \unpack("V", \substr($string, $offset, 3) . "\x00")[1];
@@ -406,6 +462,9 @@ public static function decodeUnsigned24(string $string, int &$offset = 0): int
         return $result;
     }
 
+    /**
+     * @param int<0, max> $offset
+     */
     public static function decodeInt32(string $string, int &$offset = 0): int
     {
         $offset += 4;
@@ -421,6 +480,11 @@ public static function decodeInt32(string $string, int &$offset = 0): int
         return \unpack("V", $string, $offset - 4)[1];
     }
 
+    /**
+     * @param int<0, max> $offset
+     *
+     * @return int<0, max>
+     */
     public static function decodeUnsigned32(string $string, int &$offset = 0): int
     {
         $result = \unpack("V", $string, $offset)[1];
@@ -433,6 +497,11 @@ public static function decodeUnsigned32(string $string, int &$offset = 0): int
         return $result;
     }
 
+    /**
+     * @param int<0, max> $offset
+     *
+     * @return int<0, max>|string
+     */
     public static function decodeUnsigned32WithGmp(string $string, int &$offset = 0): int|string
     {
         $offset += 4;
@@ -446,6 +515,9 @@ public static function decodeUnsigned32WithGmp(string $string, int &$offset = 0)
         return \gmp_strval(\gmp_import(\substr($string, $offset - 4, 4), 1, \GMP_LSW_FIRST));
     }
 
+    /**
+     * @param int<0, max> $offset
+     */
     public static function decodeInt64(string $string, int &$offset = 0): int
     {
         if (\PHP_INT_SIZE > 4) {
@@ -469,6 +541,11 @@ public static function decodeInt64WithGmp(string $string, int &$offset = 0): int
         return \gmp_strval(\gmp_import(\substr($string, $offset - 8, 8), 1, \GMP_LSW_FIRST));
     }
 
+    /**
+     * @param int<0, max> $offset
+     *
+     * @return int<0, max>
+     */
     public static function decodeUnsigned64(string $string, int &$offset = 0): int
     {
         if (\PHP_INT_SIZE <= 4) {
@@ -485,6 +562,9 @@ public static function decodeUnsigned64(string $string, int &$offset = 0): int
         return $result;
     }
 
+    /**
+     * @param int<0, max> $offset
+     */
     public static function decodeUnsigned64WithGmp(string $string, int &$offset = 0): string
     {
         $offset += 8;

From 6f9ea8f77de1a4674e153b6751a64bfeece43df7 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 31 Aug 2023 23:19:47 -0500
Subject: [PATCH 150/174] Style fix

---
 src/Internal/MysqlConnectionResult.php | 2 +-
 src/MysqlColumnDefinition.php          | 1 -
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index d7063e7..a6745d0 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -33,7 +33,7 @@ private static function iterate(MysqlResultProxy $result): \Generator
         static $mapper;
 
         $columnNames = \array_map(
-            $mapper ??= static fn(MysqlColumnDefinition $cd) => $cd->getName(),
+            $mapper ??= static fn (MysqlColumnDefinition $cd) => $cd->getName(),
             $result->getColumnDefinitions(),
         );
 
diff --git a/src/MysqlColumnDefinition.php b/src/MysqlColumnDefinition.php
index 13d5384..f044f0d 100644
--- a/src/MysqlColumnDefinition.php
+++ b/src/MysqlColumnDefinition.php
@@ -8,7 +8,6 @@ final class MysqlColumnDefinition
      * @param non-empty-string $table
      * @param non-empty-string $name
      * @param int<0, max> $length
-     * @param MysqlDataType $type
      * @param int<0, max> $flags
      * @param int<0, max> $decimals
      * @param non-empty-string|null $originalTable

From cdfd246db54083d7dd55c13a6152538825437dc0 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 31 Aug 2023 23:20:48 -0500
Subject: [PATCH 151/174] Test on PHP 8.2

---
 .github/workflows/ci.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ffd4935..5cdf5c2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,6 +12,9 @@ jobs:
           - operating-system: 'ubuntu-latest'
             php-version: '8.1'
 
+          - operating-system: 'ubuntu-latest'
+            php-version: '8.2'
+
     name: PHP ${{ matrix.php-version }} ${{ matrix.job-description }}
 
     runs-on: ${{ matrix.operating-system }}

From 0ac8ffc61d51bbbeefacede9fc081fef8fa5e18f Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 31 Aug 2023 23:23:05 -0500
Subject: [PATCH 152/174] Remove copy/paste leftover

---
 .github/workflows/ci.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5cdf5c2..4f5b379 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -41,7 +41,6 @@ jobs:
         uses: shivammathur/setup-php@v2
         with:
           php-version: ${{ matrix.php-version }}
-          extensions: pq
 
       - name: Get Composer cache directory
         id: composer-cache

From 193ffc19c608cddd6b1edca79a9a6dc5ee509b8b Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 2 Dec 2023 23:01:25 -0600
Subject: [PATCH 153/174] Refactor nested transactions

---
 composer.json                               |   4 +-
 src/Internal/ConnectionProcessor.php        |   8 +-
 src/Internal/MysqlConnectionTransaction.php | 188 ++++++++++++--------
 src/Internal/MysqlNestableExecutor.php      | 110 ++++++++++++
 src/Internal/MysqlNestedTransaction.php     |  28 ++-
 src/Internal/MysqlPooledTransaction.php     |  15 ++
 src/Internal/MysqlStatementPool.php         |   3 +-
 src/Internal/MysqlTransactionDelegate.php   |   8 +
 src/MysqlConnection.php                     |  16 +-
 src/MysqlConnectionPool.php                 |  19 +-
 src/MysqlLink.php                           |   6 +-
 src/MysqlNestableTransaction.php            |  55 ------
 src/MysqlTransaction.php                    |   4 +-
 src/SocketMysqlConnection.php               |  36 ++--
 src/SocketMysqlConnector.php                |   2 +-
 test/LinkTest.php                           |   9 +-
 test/NestedTransactionTest.php              |  10 +-
 test/PoolTest.php                           |   7 +-
 18 files changed, 342 insertions(+), 186 deletions(-)
 create mode 100644 src/Internal/MysqlNestableExecutor.php
 delete mode 100644 src/MysqlNestableTransaction.php

diff --git a/composer.json b/composer.json
index 63279f7..582720e 100644
--- a/composer.json
+++ b/composer.json
@@ -18,8 +18,8 @@
         "amphp/parser": "^1.1",
         "amphp/pipeline": "^1",
         "amphp/socket": "^2",
-        "amphp/sql": "^2",
-        "amphp/sql-common": "^2"
+        "amphp/sql": "dev-refactor-transactions as 2.0",
+        "amphp/sql-common": "dev-refactor-transactions as 2.0"
     },
     "require-dev": {
         "ext-mysqli": "*",
diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 8ba4066..8649e6c 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -10,6 +10,7 @@
 use Amp\Mysql\MysqlColumnDefinition;
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlDataType;
+use Amp\Mysql\MysqlResult;
 use Amp\Parser\Parser;
 use Amp\Socket\Socket;
 use Amp\Sql\ConnectionException;
@@ -274,6 +275,11 @@ public function getMetadata(): MysqlConnectionMetdata
         return clone $this->metadata;
     }
 
+    public function getConfig(): MysqlConfig
+    {
+        return $this->config;
+    }
+
     public function getConnectionId(): int
     {
         return $this->connectionId;
@@ -327,7 +333,7 @@ public function useDatabase(string $database): Future
     /**
      * @see 14.6.4 COM_QUERY
      *
-     * @return Future<MysqlConnectionResult|MysqlCommandResult>
+     * @return Future<MysqlResult>
      */
     public function query(string $query): Future
     {
diff --git a/src/Internal/MysqlConnectionTransaction.php b/src/Internal/MysqlConnectionTransaction.php
index 02cc08a..a1ac1c4 100644
--- a/src/Internal/MysqlConnectionTransaction.php
+++ b/src/Internal/MysqlConnectionTransaction.php
@@ -14,68 +14,83 @@
 /** @internal */
 final class MysqlConnectionTransaction implements MysqlTransaction
 {
-    const SAVEPOINT_PREFIX = "amp_";
-
-    private ?ConnectionProcessor $processor;
-
-    private readonly TransactionIsolation $isolation;
-
     /** @var \Closure():void */
     private readonly \Closure $release;
 
     private int $refCount = 1;
 
+    private bool $active = true;
+
+    private readonly DeferredFuture $onCommit;
+    private readonly DeferredFuture $onRollback;
     private readonly DeferredFuture $onClose;
 
+    private ?DeferredFuture $busy = null;
+
     /**
      * @param \Closure():void $release
      *
      * @throws \Error If the isolation level is invalid.
      */
     public function __construct(
-        ConnectionProcessor $processor,
+        private readonly MysqlNestableExecutor $executor,
         \Closure $release,
-        TransactionIsolation $isolation
+        private readonly TransactionIsolation $isolation,
     ) {
-        $this->processor = $processor;
-        $this->isolation = $isolation;
+        $busy = &$this->busy;
+        $refCount = &$this->refCount;
+        $this->release = static function () use (&$busy, &$refCount, $release): void {
+            $busy?->complete();
+            $busy = null;
 
-        $refCount =& $this->refCount;
-        $this->release = static function () use (&$refCount, $release): void {
             if (--$refCount === 0) {
                 $release();
             }
         };
 
+        $this->onCommit = new DeferredFuture();
+        $this->onRollback = new DeferredFuture();
         $this->onClose = new DeferredFuture();
+
         $this->onClose($this->release);
     }
 
     public function __destruct()
     {
-        if ($this->onClose->isComplete()) {
-            return;
-        }
-
-        $this->onClose->complete();
-
-        if (!$this->processor || $this->processor->isClosed()) {
+        if (!$this->isActive()) {
             return;
         }
 
-        $processor = $this->processor;
-        EventLoop::queue(static function () use ($processor): void {
+        $busy = &$this->busy;
+        $executor = $this->executor;
+        $onRollback = $this->onRollback;
+        $onClose = $this->onClose;
+        EventLoop::queue(static function () use (&$busy, $executor, $onRollback, $onClose): void {
             try {
-                !$processor->isClosed() && $processor->query('ROLLBACK');
+                while ($busy) {
+                    $busy->getFuture()->await();
+                }
+
+                if (!$executor->isClosed()) {
+                    $executor->query('ROLLBACK');
+                }
             } catch (SqlException) {
                 // Ignore failure if connection closes during query.
+            } finally {
+                $onRollback->complete();
+                $onClose->complete();
             }
         });
     }
 
     public function getLastUsedAt(): int
     {
-        return $this->processor?->getLastUsedAt() ?? 0;
+        return $this->executor->getLastUsedAt();
+    }
+
+    public function isNestedTransaction(): bool
+    {
+        return false;
     }
 
     /**
@@ -83,14 +98,14 @@ public function getLastUsedAt(): int
      */
     public function close(): void
     {
-        if ($this->processor) {
+        if ($this->isActive()) {
             $this->rollback(); // Invokes $this->release callback.
         }
     }
 
     public function isClosed(): bool
     {
-        return !$this->processor || $this->processor->isClosed();
+        return !$this->isActive();
     }
 
     public function onClose(\Closure $onClose): void
@@ -103,7 +118,7 @@ public function onClose(\Closure $onClose): void
      */
     public function isActive(): bool
     {
-        return $this->processor !== null;
+        return $this->active;
     }
 
     public function getIsolationLevel(): TransactionIsolation
@@ -116,12 +131,16 @@ public function getIsolationLevel(): TransactionIsolation
      */
     public function query(string $sql): MysqlResult
     {
-        if ($this->processor === null) {
-            throw new TransactionError("The transaction has been committed or rolled back");
-        }
+        $this->awaitPendingNestedTransaction();
 
-        $result = $this->processor->query($sql)->await();
         ++$this->refCount;
+        try {
+            $result = $this->executor->query($sql);
+        } catch (\Throwable $exception) {
+            EventLoop::queue($this->release);
+            throw $exception;
+        }
+
         return new MysqlPooledResult($result, $this->release);
     }
 
@@ -132,11 +151,16 @@ public function query(string $sql): MysqlResult
      */
     public function prepare(string $sql): MysqlStatement
     {
-        if ($this->processor === null) {
-            throw new TransactionError("The transaction has been committed or rolled back");
+        $this->awaitPendingNestedTransaction();
+
+        ++$this->refCount;
+        try {
+            $statement = $this->executor->prepare($sql);
+        } catch (\Throwable $exception) {
+            EventLoop::queue($this->release);
+            throw $exception;
         }
 
-        $statement = $this->processor->prepare($sql)->await();
         return new MysqlPooledStatement($statement, $this->release);
     }
 
@@ -145,15 +169,35 @@ public function prepare(string $sql): MysqlStatement
      */
     public function execute(string $sql, array $params = []): MysqlResult
     {
-        if ($this->processor === null) {
-            throw new TransactionError("The transaction has been committed or rolled back");
+        $this->awaitPendingNestedTransaction();
+
+        ++$this->refCount;
+        try {
+            $statement = $this->executor->prepare($sql);
+            $result = $statement->execute($params);
+        } catch (\Throwable $exception) {
+            EventLoop::queue($this->release);
+            throw $exception;
         }
 
-        $statement = $this->processor->prepare($sql)->await();
-        $result = $statement->execute($params);
+        return new MysqlPooledResult($result, $this->release);
+    }
+
+    public function beginTransaction(): MysqlTransaction
+    {
+        $this->awaitPendingNestedTransaction();
 
         ++$this->refCount;
-        return new MysqlPooledResult($result, $this->release);
+        $this->busy = new DeferredFuture();
+        try {
+            $identifier = \bin2hex(\random_bytes(8));
+            $this->executor->createSavepoint($identifier);
+        } catch (\Throwable $exception) {
+            EventLoop::queue($this->release);
+            throw $exception;
+        }
+
+        return new MysqlNestedTransaction($this, $this->executor, $identifier, $this->release);
     }
 
     /**
@@ -163,13 +207,13 @@ public function execute(string $sql, array $params = []): MysqlResult
      */
     public function commit(): void
     {
-        if ($this->processor === null) {
-            throw new TransactionError("The transaction has been committed or rolled back");
-        }
+        $this->awaitPendingNestedTransaction();
+
+        $this->active = false;
+        $this->executor->query("COMMIT");
 
-        $future = $this->processor->query("COMMIT");
-        $this->processor = null;
-        $future->finally($this->onClose->complete(...))->await();
+        $this->onCommit->complete();
+        $this->onClose->complete();
     }
 
     /**
@@ -179,48 +223,38 @@ public function commit(): void
      */
     public function rollback(): void
     {
-        if ($this->processor === null) {
-            throw new TransactionError("The transaction has been committed or rolled back");
-        }
+        $this->awaitPendingNestedTransaction();
+
+        $this->active = false;
+        $this->executor->query("ROLLBACK");
 
-        $future = $this->processor->query("ROLLBACK");
-        $this->processor = null;
-        $future->finally($this->onClose->complete(...))->await();
+        $this->onRollback->complete();
+        $this->onClose->complete();
     }
 
-    /**
-     * Creates a savepoint with the given identifier.
-     *
-     * @param string $identifier Savepoint identifier.
-     *
-     * @throws TransactionError If the transaction has been committed or rolled back.
-     */
-    public function createSavepoint(string $identifier): void
+    public function onCommit(\Closure $onCommit): void
     {
-        $this->query(\sprintf("SAVEPOINT `%s%s`", self::SAVEPOINT_PREFIX, \sha1($identifier)));
+        $this->onCommit->getFuture()->finally($onCommit);
     }
 
-    /**
-     * Rolls back to the savepoint with the given identifier.
-     *
-     * @param string $identifier Savepoint identifier.
-     *
-     * @throws TransactionError If the transaction has been committed or rolled back.
-     */
-    public function rollbackTo(string $identifier): void
+    public function onRollback(\Closure $onRollback): void
     {
-        $this->query(\sprintf("ROLLBACK TO `%s%s`", self::SAVEPOINT_PREFIX, \sha1($identifier)));
+        $this->onRollback->getFuture()->finally($onRollback);
     }
 
-    /**
-     * Releases the savepoint with the given identifier.
-     *
-     * @param string $identifier Savepoint identifier.
-     *
-     * @throws TransactionError If the transaction has been committed or rolled back.
-     */
-    public function releaseSavepoint(string $identifier): void
+    private function assertOpen(): void
     {
-        $this->query(\sprintf("RELEASE SAVEPOINT `%s%s`", self::SAVEPOINT_PREFIX, \sha1($identifier)));
+        if ($this->isClosed()) {
+            throw new TransactionError("The transaction has been committed or rolled back");
+        }
+    }
+
+    private function awaitPendingNestedTransaction(): void
+    {
+        while ($this->busy) {
+            $this->busy->getFuture()->await();
+        }
+
+        $this->assertOpen();
     }
 }
diff --git a/src/Internal/MysqlNestableExecutor.php b/src/Internal/MysqlNestableExecutor.php
new file mode 100644
index 0000000..36781e6
--- /dev/null
+++ b/src/Internal/MysqlNestableExecutor.php
@@ -0,0 +1,110 @@
+<?php declare(strict_types=1);
+
+namespace Amp\Mysql\Internal;
+
+use Amp\DeferredFuture;
+use Amp\Mysql\MysqlExecutor;
+use Amp\Mysql\MysqlResult;
+use Amp\Mysql\MysqlStatement;
+use Amp\Sql\Common\NestableTransactionExecutor;
+use Amp\Sql\TransactionError;
+
+/**
+ * @internal
+ * @implements NestableTransactionExecutor<MysqlResult, MysqlStatement>
+ */
+final class MysqlNestableExecutor implements MysqlExecutor, NestableTransactionExecutor
+{
+    private ?DeferredFuture $busy = null;
+
+    public function __construct(
+        private readonly ConnectionProcessor $processor,
+    ) {
+    }
+
+    public function isClosed(): bool
+    {
+        return $this->processor->isClosed();
+    }
+
+    /**
+     * @return int Timestamp of the last time this connection was used.
+     */
+    public function getLastUsedAt(): int
+    {
+        return $this->processor->getLastUsedAt();
+    }
+
+    public function close(): void
+    {
+        // Send close command if connection is not already in a closed or closing state
+        if (!$this->processor->isClosed()) {
+            $this->processor->sendClose()->await();
+        }
+    }
+
+    public function onClose(\Closure $onClose): void
+    {
+        $this->processor->onClose($onClose);
+    }
+
+    public function query(string $sql): MysqlResult
+    {
+        while ($this->busy) {
+            $this->busy->getFuture()->await();
+        }
+
+        return $this->processor->query($sql)->await();
+    }
+
+    public function prepare(string $sql): MysqlStatement
+    {
+        while ($this->busy) {
+            $this->busy->getFuture()->await();
+        }
+
+        return $this->processor->prepare($sql)->await();
+    }
+
+    public function execute(string $sql, array $params = []): MysqlResult
+    {
+        $statement = $this->prepare($sql);
+        return $statement->execute($params);
+    }
+
+    /**
+     * Creates a savepoint with the given identifier.
+     *
+     * @param string $identifier Savepoint identifier.
+     *
+     * @throws TransactionError If the transaction has been committed or rolled back.
+     */
+    public function createSavepoint(string $identifier): void
+    {
+        $this->query(\sprintf("SAVEPOINT `%s`", $identifier));
+    }
+
+    /**
+     * Rolls back to the savepoint with the given identifier.
+     *
+     * @param string $identifier Savepoint identifier.
+     *
+     * @throws TransactionError If the transaction has been committed or rolled back.
+     */
+    public function rollbackTo(string $identifier): void
+    {
+        $this->query(\sprintf("ROLLBACK TO `%s`", $identifier));
+    }
+
+    /**
+     * Releases the savepoint with the given identifier.
+     *
+     * @param string $identifier Savepoint identifier.
+     *
+     * @throws TransactionError If the transaction has been committed or rolled back.
+     */
+    public function releaseSavepoint(string $identifier): void
+    {
+        $this->query(\sprintf("RELEASE SAVEPOINT `%s`", $identifier));
+    }
+}
diff --git a/src/Internal/MysqlNestedTransaction.php b/src/Internal/MysqlNestedTransaction.php
index 0799bf5..663c11f 100644
--- a/src/Internal/MysqlNestedTransaction.php
+++ b/src/Internal/MysqlNestedTransaction.php
@@ -5,18 +5,42 @@
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
 use Amp\Mysql\MysqlTransaction;
+use Amp\Sql\Common\NestableTransactionExecutor;
 use Amp\Sql\Common\NestedTransaction;
+use Amp\Sql\Transaction;
 
 /**
  * @internal
- * @extends NestedTransaction<MysqlResult, MysqlStatement, MysqlTransaction>
+ * @extends NestedTransaction<MysqlResult, MysqlStatement, MysqlTransaction, MysqlNestableExecutor>
  */
-class MysqlNestedTransaction extends NestedTransaction implements MysqlTransaction
+final class MysqlNestedTransaction extends NestedTransaction implements MysqlTransaction
 {
     use MysqlTransactionDelegate;
 
+    /**
+     * @param non-empty-string $identifier
+     * @param \Closure():void $release
+     */
+    public function __construct(
+        private readonly MysqlTransaction $transaction,
+        MysqlNestableExecutor $executor,
+        string $identifier,
+        \Closure $release,
+    ) {
+        parent::__construct($transaction, $executor, $identifier, $release);
+    }
+
     protected function getTransaction(): MysqlTransaction
     {
         return $this->transaction;
     }
+
+    protected function createNestedTransaction(
+        Transaction $transaction,
+        NestableTransactionExecutor $executor,
+        string $identifier,
+        \Closure $release,
+    ): MysqlTransaction {
+        return new self($transaction, $executor, $identifier, $release);
+    }
 }
diff --git a/src/Internal/MysqlPooledTransaction.php b/src/Internal/MysqlPooledTransaction.php
index 0cfb787..273313a 100644
--- a/src/Internal/MysqlPooledTransaction.php
+++ b/src/Internal/MysqlPooledTransaction.php
@@ -6,6 +6,7 @@
 use Amp\Mysql\MysqlStatement;
 use Amp\Mysql\MysqlTransaction;
 use Amp\Sql\Common\PooledTransaction;
+use Amp\Sql\Transaction;
 
 /**
  * @internal
@@ -15,8 +16,22 @@ final class MysqlPooledTransaction extends PooledTransaction implements MysqlTra
 {
     use MysqlTransactionDelegate;
 
+    /**
+     * @param \Closure():void $release
+     */
+    public function __construct(private readonly MysqlTransaction $transaction, \Closure $release)
+    {
+        parent::__construct($transaction, $release);
+    }
+
     protected function getTransaction(): MysqlTransaction
     {
         return $this->transaction;
     }
+
+    protected function createTransaction(Transaction $transaction, \Closure $release): Transaction
+    {
+        \assert($transaction instanceof MysqlTransaction);
+        return new MysqlPooledTransaction($transaction, $release);
+    }
 }
diff --git a/src/Internal/MysqlStatementPool.php b/src/Internal/MysqlStatementPool.php
index b79bc3b..d913206 100644
--- a/src/Internal/MysqlStatementPool.php
+++ b/src/Internal/MysqlStatementPool.php
@@ -2,6 +2,7 @@
 
 namespace Amp\Mysql\Internal;
 
+use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
 use Amp\Mysql\MysqlTransaction;
@@ -11,7 +12,7 @@
 
 /**
  * @internal
- * @extends SqlStatementPool<MysqlResult, MysqlStatement, MysqlTransaction>
+ * @extends SqlStatementPool<MysqlConfig, MysqlResult, MysqlStatement, MysqlTransaction>
  */
 final class MysqlStatementPool extends SqlStatementPool implements MysqlStatement
 {
diff --git a/src/Internal/MysqlTransactionDelegate.php b/src/Internal/MysqlTransactionDelegate.php
index 004123e..a3b609c 100644
--- a/src/Internal/MysqlTransactionDelegate.php
+++ b/src/Internal/MysqlTransactionDelegate.php
@@ -48,4 +48,12 @@ public function execute(string $sql, array $params = []): MysqlResult
     {
         return parent::execute($sql, $params);
     }
+
+    /**
+     * Changes return type to this library's Transaction type.
+     */
+    public function beginTransaction(): MysqlTransaction
+    {
+        return parent::beginTransaction();
+    }
 }
diff --git a/src/MysqlConnection.php b/src/MysqlConnection.php
index 39fbae9..af925b2 100644
--- a/src/MysqlConnection.php
+++ b/src/MysqlConnection.php
@@ -2,15 +2,15 @@
 
 namespace Amp\Mysql;
 
-interface MysqlConnection extends MysqlLink
-{
-    /**
-     * Change the active database on the connection.
-     */
-    public function useDatabase(string $database): void;
+use Amp\Sql\Connection;
 
+/**
+ * @extends Connection<MysqlConfig, MysqlResult, MysqlStatement, MysqlTransaction>
+ */
+interface MysqlConnection extends MysqlLink, Connection
+{
     /**
-     * Change the character set used by the connection.
+     * @return MysqlConfig Config object specific to this library.
      */
-    public function useCharacterSet(string $charset, string $collate): void;
+    public function getConfig(): MysqlConfig;
 }
diff --git a/src/MysqlConnectionPool.php b/src/MysqlConnectionPool.php
index 6e748da..3986096 100644
--- a/src/MysqlConnectionPool.php
+++ b/src/MysqlConnectionPool.php
@@ -7,13 +7,11 @@
 use Amp\Sql\SqlConnector;
 use Amp\Sql\Statement;
 use Amp\Sql\Transaction;
-use Amp\Sql\TransactionIsolation;
-use Amp\Sql\TransactionIsolationLevel;
 
 /**
  * @extends ConnectionPool<MysqlConfig, MysqlResult, MysqlStatement, MysqlTransaction, MysqlConnection>
  */
-final class MysqlConnectionPool extends ConnectionPool implements MysqlLink
+final class MysqlConnectionPool extends ConnectionPool implements MysqlConnection
 {
     /**
      * @param positive-int $maxConnections
@@ -51,6 +49,14 @@ protected function createTransaction(Transaction $transaction, \Closure $release
         return new Internal\MysqlPooledTransaction($transaction, $release);
     }
 
+    /**
+     * Changes return type to this library's configuration type.
+     */
+    public function getConfig(): MysqlConfig
+    {
+        return parent::getConfig();
+    }
+
     /**
      * Changes return type to this library's Result type.
      */
@@ -78,9 +84,8 @@ public function execute(string $sql, array $params = []): MysqlResult
     /**
      * Changes return type to this library's Transaction type.
      */
-    public function beginTransaction(
-        TransactionIsolation $isolation = TransactionIsolationLevel::Committed
-    ): MysqlTransaction {
-        return parent::beginTransaction($isolation);
+    public function beginTransaction(): MysqlTransaction
+    {
+        return parent::beginTransaction();
     }
 }
diff --git a/src/MysqlLink.php b/src/MysqlLink.php
index f9a41fc..d1be61d 100644
--- a/src/MysqlLink.php
+++ b/src/MysqlLink.php
@@ -3,8 +3,6 @@
 namespace Amp\Mysql;
 
 use Amp\Sql\Link;
-use Amp\Sql\TransactionIsolation;
-use Amp\Sql\TransactionIsolationLevel;
 
 /**
  * @extends Link<MysqlResult, MysqlStatement, MysqlTransaction>
@@ -14,7 +12,5 @@ interface MysqlLink extends MysqlExecutor, Link
     /**
      * @return MysqlTransaction Transaction object specific to this library.
      */
-    public function beginTransaction(
-        TransactionIsolation $isolation = TransactionIsolationLevel::Committed
-    ): MysqlTransaction;
+    public function beginTransaction(): MysqlTransaction;
 }
diff --git a/src/MysqlNestableTransaction.php b/src/MysqlNestableTransaction.php
deleted file mode 100644
index f89f9a3..0000000
--- a/src/MysqlNestableTransaction.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Amp\Mysql;
-
-use Amp\Sql\Common\NestableTransaction;
-use Amp\Sql\Transaction;
-use Amp\Sql\TransactionIsolation;
-use Amp\Sql\TransactionIsolationLevel;
-
-/**
- * @extends NestableTransaction<MysqlResult, MysqlStatement, MysqlTransaction>
- */
-class MysqlNestableTransaction extends NestableTransaction implements MysqlLink
-{
-    protected function createNestedTransaction(
-        Transaction $transaction,
-        \Closure $release,
-        string $identifier,
-    ): Transaction {
-        return new Internal\MysqlNestedTransaction($transaction, $release, $identifier);
-    }
-
-    /**
-     * Changes return type to this library's Transaction type.
-     */
-    public function beginTransaction(
-        TransactionIsolation $isolation = TransactionIsolationLevel::Committed
-    ): MysqlTransaction {
-        return parent::beginTransaction($isolation);
-    }
-
-    /**
-     * Changes return type to this library's Result type.
-     */
-    public function query(string $sql): MysqlResult
-    {
-        return parent::query($sql);
-    }
-
-    /**
-     * Changes return type to this library's Statement type.
-     */
-    public function prepare(string $sql): MysqlStatement
-    {
-        return parent::prepare($sql);
-    }
-
-    /**
-     * Changes return type to this library's Result type.
-     */
-    public function execute(string $sql, array $params = []): MysqlResult
-    {
-        return parent::execute($sql, $params);
-    }
-}
diff --git a/src/MysqlTransaction.php b/src/MysqlTransaction.php
index 03e80dc..be14c2b 100644
--- a/src/MysqlTransaction.php
+++ b/src/MysqlTransaction.php
@@ -5,8 +5,8 @@
 use Amp\Sql\Transaction;
 
 /**
- * @extends Transaction<MysqlResult, MysqlStatement>
+ * @extends Transaction<MysqlResult, MysqlStatement, MysqlTransaction>
  */
-interface MysqlTransaction extends MysqlExecutor, Transaction
+interface MysqlTransaction extends MysqlLink, Transaction
 {
 }
diff --git a/src/SocketMysqlConnection.php b/src/SocketMysqlConnection.php
index 0d31ca4..e50a63d 100644
--- a/src/SocketMysqlConnection.php
+++ b/src/SocketMysqlConnection.php
@@ -11,14 +11,14 @@
 
 final class SocketMysqlConnection implements MysqlConnection
 {
-    private readonly Internal\ConnectionProcessor $processor;
+    private TransactionIsolation $transactionIsolation = TransactionIsolationLevel::Committed;
 
     private ?DeferredFuture $busy = null;
 
     /** @var \Closure():void Function used to release connection after a transaction has completed. */
     private readonly \Closure $release;
 
-    public static function initialize(
+    public static function connect(
         Socket $socket,
         MysqlConfig $config,
         ?Cancellation $cancellation = null,
@@ -28,10 +28,8 @@ public static function initialize(
         return new self($processor);
     }
 
-    private function __construct(Internal\ConnectionProcessor $processor)
+    private function __construct(private readonly Internal\ConnectionProcessor $processor)
     {
-        $this->processor = $processor;
-
         $busy = &$this->busy;
         $this->release = static function () use (&$busy): void {
             $busy?->complete();
@@ -39,6 +37,16 @@ private function __construct(Internal\ConnectionProcessor $processor)
         };
     }
 
+    public function getConfig(): MysqlConfig
+    {
+        return $this->processor->getConfig();
+    }
+
+    public function setTransactionIsolation(TransactionIsolation $isolation): void
+    {
+        $this->transactionIsolation = $isolation;
+    }
+
     /**
      * @return bool False if the connection has been closed.
      */
@@ -87,25 +95,29 @@ public function query(string $sql): MysqlResult
         return $this->processor->query($sql)->await();
     }
 
-    public function beginTransaction(
-        TransactionIsolation $isolation = TransactionIsolationLevel::Committed
-    ): MysqlTransaction {
+    public function beginTransaction(): MysqlTransaction
+    {
         while ($this->busy) {
             $this->busy->getFuture()->await();
         }
 
-        $this->busy = $deferred = new DeferredFuture;
+        $this->busy = $deferred = new DeferredFuture();
+
+        $sql = \sprintf(
+            "SET SESSION TRANSACTION ISOLATION LEVEL %s; START TRANSACTION",
+            $this->transactionIsolation->toSql(),
+        );
 
         try {
-            $this->processor->query("SET SESSION TRANSACTION ISOLATION LEVEL " . $isolation->toSql())->await();
-            $this->processor->query("START TRANSACTION")->await();
+            $this->processor->query($sql)->await();
         } catch (\Throwable $exception) {
             $this->busy = null;
             $deferred->complete();
             throw $exception;
         }
 
-        return new Internal\MysqlConnectionTransaction($this->processor, $this->release, $isolation);
+        $executor = new Internal\MysqlNestableExecutor($this->processor);
+        return new Internal\MysqlConnectionTransaction($executor, $this->release, $this->transactionIsolation);
     }
 
     public function ping(): void
diff --git a/src/SocketMysqlConnector.php b/src/SocketMysqlConnector.php
index baab095..da1251d 100644
--- a/src/SocketMysqlConnector.php
+++ b/src/SocketMysqlConnector.php
@@ -25,6 +25,6 @@ public function connect(SqlConfig $config, ?Cancellation $cancellation = null):
         $socket = ($this->connector ?? Socket\socketConnector())
             ->connect($config->getConnectionString(), $config->getConnectContext(), $cancellation);
 
-        return SocketMysqlConnection::initialize($socket, $config, $cancellation);
+        return SocketMysqlConnection::connect($socket, $config, $cancellation);
     }
 }
diff --git a/test/LinkTest.php b/test/LinkTest.php
index bf3110e..cad2c04 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -363,13 +363,8 @@ public function testPreparedWithNegativeValue(): void
     {
         $db = $this->getLink();
 
-        $db->query("DROP TABLE IF EXISTS tmp");
-
-        $stmt = $db->prepare("CREATE TABLE tmp SELECT ? AS a");
-        $stmt->execute([-1]);
-
-        $stmt = $db->prepare("SELECT a FROM tmp");
-        $result = $stmt->execute();
+        $stmt = $db->prepare("SELECT ? AS a");
+        $result = $stmt->execute([-1]);
         $result = \iterator_to_array($result);
 
         $this->assertEquals(\array_values(\array_shift($result)), [-1]);
diff --git a/test/NestedTransactionTest.php b/test/NestedTransactionTest.php
index 77669c9..33f2144 100644
--- a/test/NestedTransactionTest.php
+++ b/test/NestedTransactionTest.php
@@ -3,7 +3,6 @@
 namespace Amp\Mysql\Test;
 
 use Amp\Mysql\MysqlLink;
-use Amp\Mysql\MysqlNestableTransaction;
 use Amp\Mysql\MysqlTransaction;
 use Amp\Mysql\SocketMysqlConnector;
 
@@ -11,16 +10,23 @@ class NestedTransactionTest extends LinkTest
 {
     private ?MysqlLink $link;
     private ?MysqlTransaction $transaction = null;
+    private ?MysqlTransaction $nested = null;
 
     public function getLink(bool $useCompression = false): MysqlLink
     {
         $this->link = (new SocketMysqlConnector)->connect($this->getConfig($useCompression));
         $this->transaction = $this->link->beginTransaction();
-        return new MysqlNestableTransaction($this->transaction);
+        $this->nested = $this->transaction->beginTransaction();
+
+        return $this->nested;
     }
 
     public function tearDown(): void
     {
+        if ($this->nested?->isActive()) {
+            $this->nested->rollback();
+        }
+
         if ($this->transaction?->isActive()) {
             $this->transaction->rollback();
         }
diff --git a/test/PoolTest.php b/test/PoolTest.php
index a16c96c..86233c5 100644
--- a/test/PoolTest.php
+++ b/test/PoolTest.php
@@ -139,13 +139,12 @@ public function testMultipleTransactions(int $count)
         $processors = $this->makeProcessorSet($count);
 
         $connection = $processors[0];
-        $result = new MysqlCommandResult(0, 0);
 
-        $connection->expects($this->exactly(3))
+        $connection->expects($this->exactly(2))
             ->method('query')
-            ->willReturn(async(function () use ($result): MysqlResult {
+            ->willReturn(async(function (): MysqlResult {
                 delay(0.01);
-                return $result;
+                return new MysqlCommandResult(0, 0);
             }));
 
         $pool = $this->createPool($this->makeConnectionSet($processors));

From c25cc454b0a6b3a1d6326fc9dbdafab42623fcfd Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 3 Dec 2023 22:08:22 -0600
Subject: [PATCH 154/174] Update to use ConnectionTransaction and add callback
 tests

---
 src/Internal/MysqlConnectionTransaction.php | 265 ++------------------
 src/Internal/MysqlNestableExecutor.php      |  32 +--
 src/Internal/MysqlPooledStatement.php       |  13 +-
 src/Internal/MysqlPooledTransaction.php     |   5 -
 src/Internal/MysqlTransactionDelegate.php   |  11 +-
 test/ConnectionTest.php                     |  38 +++
 test/NestedTransactionTest.php              |  66 ++++-
 7 files changed, 143 insertions(+), 287 deletions(-)

diff --git a/src/Internal/MysqlConnectionTransaction.php b/src/Internal/MysqlConnectionTransaction.php
index a1ac1c4..d1c3777 100644
--- a/src/Internal/MysqlConnectionTransaction.php
+++ b/src/Internal/MysqlConnectionTransaction.php
@@ -2,259 +2,28 @@
 
 namespace Amp\Mysql\Internal;
 
-use Amp\DeferredFuture;
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
 use Amp\Mysql\MysqlTransaction;
-use Amp\Sql\SqlException;
-use Amp\Sql\TransactionError;
-use Amp\Sql\TransactionIsolation;
-use Revolt\EventLoop;
-
-/** @internal */
-final class MysqlConnectionTransaction implements MysqlTransaction
+use Amp\Sql\Common\ConnectionTransaction;
+use Amp\Sql\Common\NestableTransactionExecutor;
+use Amp\Sql\Transaction;
+
+/**
+ * @internal
+ * @extends ConnectionTransaction<MysqlResult, MysqlStatement, MysqlTransaction, MysqlNestableExecutor>
+ */
+final class MysqlConnectionTransaction extends ConnectionTransaction implements MysqlTransaction
 {
-    /** @var \Closure():void */
-    private readonly \Closure $release;
-
-    private int $refCount = 1;
-
-    private bool $active = true;
-
-    private readonly DeferredFuture $onCommit;
-    private readonly DeferredFuture $onRollback;
-    private readonly DeferredFuture $onClose;
+    use MysqlTransactionDelegate;
 
-    private ?DeferredFuture $busy = null;
-
-    /**
-     * @param \Closure():void $release
-     *
-     * @throws \Error If the isolation level is invalid.
-     */
-    public function __construct(
-        private readonly MysqlNestableExecutor $executor,
+    protected function createNestedTransaction(
+        Transaction $transaction,
+        NestableTransactionExecutor $executor,
+        string $identifier,
         \Closure $release,
-        private readonly TransactionIsolation $isolation,
-    ) {
-        $busy = &$this->busy;
-        $refCount = &$this->refCount;
-        $this->release = static function () use (&$busy, &$refCount, $release): void {
-            $busy?->complete();
-            $busy = null;
-
-            if (--$refCount === 0) {
-                $release();
-            }
-        };
-
-        $this->onCommit = new DeferredFuture();
-        $this->onRollback = new DeferredFuture();
-        $this->onClose = new DeferredFuture();
-
-        $this->onClose($this->release);
-    }
-
-    public function __destruct()
-    {
-        if (!$this->isActive()) {
-            return;
-        }
-
-        $busy = &$this->busy;
-        $executor = $this->executor;
-        $onRollback = $this->onRollback;
-        $onClose = $this->onClose;
-        EventLoop::queue(static function () use (&$busy, $executor, $onRollback, $onClose): void {
-            try {
-                while ($busy) {
-                    $busy->getFuture()->await();
-                }
-
-                if (!$executor->isClosed()) {
-                    $executor->query('ROLLBACK');
-                }
-            } catch (SqlException) {
-                // Ignore failure if connection closes during query.
-            } finally {
-                $onRollback->complete();
-                $onClose->complete();
-            }
-        });
-    }
-
-    public function getLastUsedAt(): int
-    {
-        return $this->executor->getLastUsedAt();
-    }
-
-    public function isNestedTransaction(): bool
-    {
-        return false;
-    }
-
-    /**
-     * Closes and rolls back all changes in the transaction.
-     */
-    public function close(): void
-    {
-        if ($this->isActive()) {
-            $this->rollback(); // Invokes $this->release callback.
-        }
-    }
-
-    public function isClosed(): bool
-    {
-        return !$this->isActive();
-    }
-
-    public function onClose(\Closure $onClose): void
-    {
-        $this->onClose->getFuture()->finally($onClose);
-    }
-
-    /**
-     * @return bool True if the transaction is active, false if it has been committed or rolled back.
-     */
-    public function isActive(): bool
-    {
-        return $this->active;
-    }
-
-    public function getIsolationLevel(): TransactionIsolation
-    {
-        return $this->isolation;
-    }
-
-    /**
-     * @throws TransactionError If the transaction has been committed or rolled back.
-     */
-    public function query(string $sql): MysqlResult
-    {
-        $this->awaitPendingNestedTransaction();
-
-        ++$this->refCount;
-        try {
-            $result = $this->executor->query($sql);
-        } catch (\Throwable $exception) {
-            EventLoop::queue($this->release);
-            throw $exception;
-        }
-
-        return new MysqlPooledResult($result, $this->release);
-    }
-
-    /**
-     * @throws TransactionError If the transaction has been committed or rolled back.
-     *
-     * @psalm-suppress InvalidReturnStatement, InvalidReturnType
-     */
-    public function prepare(string $sql): MysqlStatement
-    {
-        $this->awaitPendingNestedTransaction();
-
-        ++$this->refCount;
-        try {
-            $statement = $this->executor->prepare($sql);
-        } catch (\Throwable $exception) {
-            EventLoop::queue($this->release);
-            throw $exception;
-        }
-
-        return new MysqlPooledStatement($statement, $this->release);
-    }
-
-    /**
-     * @throws TransactionError If the transaction has been committed or rolled back.
-     */
-    public function execute(string $sql, array $params = []): MysqlResult
-    {
-        $this->awaitPendingNestedTransaction();
-
-        ++$this->refCount;
-        try {
-            $statement = $this->executor->prepare($sql);
-            $result = $statement->execute($params);
-        } catch (\Throwable $exception) {
-            EventLoop::queue($this->release);
-            throw $exception;
-        }
-
-        return new MysqlPooledResult($result, $this->release);
-    }
-
-    public function beginTransaction(): MysqlTransaction
-    {
-        $this->awaitPendingNestedTransaction();
-
-        ++$this->refCount;
-        $this->busy = new DeferredFuture();
-        try {
-            $identifier = \bin2hex(\random_bytes(8));
-            $this->executor->createSavepoint($identifier);
-        } catch (\Throwable $exception) {
-            EventLoop::queue($this->release);
-            throw $exception;
-        }
-
-        return new MysqlNestedTransaction($this, $this->executor, $identifier, $this->release);
-    }
-
-    /**
-     * Commits the transaction and makes it inactive.
-     *
-     * @throws TransactionError If the transaction has been committed or rolled back.
-     */
-    public function commit(): void
-    {
-        $this->awaitPendingNestedTransaction();
-
-        $this->active = false;
-        $this->executor->query("COMMIT");
-
-        $this->onCommit->complete();
-        $this->onClose->complete();
-    }
-
-    /**
-     * Rolls back the transaction and makes it inactive.
-     *
-     * @throws TransactionError If the transaction has been committed or rolled back.
-     */
-    public function rollback(): void
-    {
-        $this->awaitPendingNestedTransaction();
-
-        $this->active = false;
-        $this->executor->query("ROLLBACK");
-
-        $this->onRollback->complete();
-        $this->onClose->complete();
-    }
-
-    public function onCommit(\Closure $onCommit): void
-    {
-        $this->onCommit->getFuture()->finally($onCommit);
-    }
-
-    public function onRollback(\Closure $onRollback): void
-    {
-        $this->onRollback->getFuture()->finally($onRollback);
-    }
-
-    private function assertOpen(): void
-    {
-        if ($this->isClosed()) {
-            throw new TransactionError("The transaction has been committed or rolled back");
-        }
-    }
-
-    private function awaitPendingNestedTransaction(): void
-    {
-        while ($this->busy) {
-            $this->busy->getFuture()->await();
-        }
-
-        $this->assertOpen();
+    ): MysqlTransaction {
+        \assert($transaction instanceof MysqlTransaction);
+        return new MysqlNestedTransaction($transaction, $executor, $identifier, $release);
     }
 }
diff --git a/src/Internal/MysqlNestableExecutor.php b/src/Internal/MysqlNestableExecutor.php
index 36781e6..4a55b4a 100644
--- a/src/Internal/MysqlNestableExecutor.php
+++ b/src/Internal/MysqlNestableExecutor.php
@@ -7,7 +7,6 @@
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
 use Amp\Sql\Common\NestableTransactionExecutor;
-use Amp\Sql\TransactionError;
 
 /**
  * @internal
@@ -72,37 +71,26 @@ public function execute(string $sql, array $params = []): MysqlResult
         return $statement->execute($params);
     }
 
-    /**
-     * Creates a savepoint with the given identifier.
-     *
-     * @param string $identifier Savepoint identifier.
-     *
-     * @throws TransactionError If the transaction has been committed or rolled back.
-     */
+    public function commit(): void
+    {
+        $this->query("COMMIT");
+    }
+
+    public function rollback(): void
+    {
+        $this->query("ROLLBACK");
+    }
+
     public function createSavepoint(string $identifier): void
     {
         $this->query(\sprintf("SAVEPOINT `%s`", $identifier));
     }
 
-    /**
-     * Rolls back to the savepoint with the given identifier.
-     *
-     * @param string $identifier Savepoint identifier.
-     *
-     * @throws TransactionError If the transaction has been committed or rolled back.
-     */
     public function rollbackTo(string $identifier): void
     {
         $this->query(\sprintf("ROLLBACK TO `%s`", $identifier));
     }
 
-    /**
-     * Releases the savepoint with the given identifier.
-     *
-     * @param string $identifier Savepoint identifier.
-     *
-     * @throws TransactionError If the transaction has been committed or rolled back.
-     */
     public function releaseSavepoint(string $identifier): void
     {
         $this->query(\sprintf("RELEASE SAVEPOINT `%s`", $identifier));
diff --git a/src/Internal/MysqlPooledStatement.php b/src/Internal/MysqlPooledStatement.php
index 896fe3d..60e5fdf 100644
--- a/src/Internal/MysqlPooledStatement.php
+++ b/src/Internal/MysqlPooledStatement.php
@@ -13,15 +13,16 @@
  */
 final class MysqlPooledStatement extends PooledStatement implements MysqlStatement
 {
-    private readonly MysqlStatement $statement;
-
     /**
      * @param \Closure():void $release
+     * @param (\Closure():void)|null $awaitBusyResource
      */
-    public function __construct(MysqlStatement $statement, \Closure $release)
-    {
-        parent::__construct($statement, $release);
-        $this->statement = $statement;
+    public function __construct(
+        private readonly MysqlStatement $statement,
+        \Closure $release,
+        ?\Closure $awaitBusyResource = null,
+    ) {
+        parent::__construct($statement, $release, $awaitBusyResource);
     }
 
     protected function createResult(SqlResult $result, \Closure $release): MysqlResult
diff --git a/src/Internal/MysqlPooledTransaction.php b/src/Internal/MysqlPooledTransaction.php
index 273313a..856574c 100644
--- a/src/Internal/MysqlPooledTransaction.php
+++ b/src/Internal/MysqlPooledTransaction.php
@@ -24,11 +24,6 @@ public function __construct(private readonly MysqlTransaction $transaction, \Clo
         parent::__construct($transaction, $release);
     }
 
-    protected function getTransaction(): MysqlTransaction
-    {
-        return $this->transaction;
-    }
-
     protected function createTransaction(Transaction $transaction, \Closure $release): Transaction
     {
         \assert($transaction instanceof MysqlTransaction);
diff --git a/src/Internal/MysqlTransactionDelegate.php b/src/Internal/MysqlTransactionDelegate.php
index a3b609c..405b07b 100644
--- a/src/Internal/MysqlTransactionDelegate.php
+++ b/src/Internal/MysqlTransactionDelegate.php
@@ -11,12 +11,13 @@
 /** @internal */
 trait MysqlTransactionDelegate
 {
-    abstract protected function getTransaction(): MysqlTransaction;
-
-    protected function createStatement(Statement $statement, \Closure $release): MysqlStatement
-    {
+    protected function createStatement(
+        Statement $statement,
+        \Closure $release,
+        ?\Closure $awaitBusyResource = null,
+    ): MysqlStatement {
         \assert($statement instanceof MysqlStatement);
-        return new MysqlPooledStatement($statement, $release);
+        return new MysqlPooledStatement($statement, $release, $awaitBusyResource);
     }
 
     protected function createResult(Result $result, \Closure $release): MysqlResult
diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php
index cbb1a88..f5b7d49 100644
--- a/test/ConnectionTest.php
+++ b/test/ConnectionTest.php
@@ -7,6 +7,7 @@
 use Amp\Mysql\MysqlConnection;
 use Amp\Mysql\MysqlLink;
 use Amp\Mysql\SocketMysqlConnector;
+use function Amp\delay;
 use function Amp\Mysql\connect;
 
 class ConnectionTest extends LinkTest
@@ -91,4 +92,41 @@ public function testDoubleClose()
 
         $db->close(); // Should not throw an exception.
     }
+
+    public function testTransactionsCallbacksOnCommit(): void
+    {
+        $db = $this->getLink();
+
+        $transaction = $db->beginTransaction();
+        $transaction->onCommit($this->createCallback(1));
+        $transaction->onRollback($this->createCallback(0));
+        $transaction->onClose($this->createCallback(1));
+
+        $transaction->commit();
+    }
+
+    public function testTransactionsCallbacksOnRollback(): void
+    {
+        $db = $this->getLink();
+
+        $transaction = $db->beginTransaction();
+        $transaction->onCommit($this->createCallback(0));
+        $transaction->onRollback($this->createCallback(1));
+        $transaction->onClose($this->createCallback(1));
+
+        $transaction->rollback();
+    }
+
+    public function testTransactionsCallbacksOnDestruct(): void
+    {
+        $db = $this->getLink();
+
+        $transaction = $db->beginTransaction();
+        $transaction->onCommit($this->createCallback(0));
+        $transaction->onRollback($this->createCallback(1));
+        $transaction->onClose($this->createCallback(1));
+
+        unset($transaction);
+        delay(0); // Destructor is async, so give control to the loop to invoke callbacks.
+    }
 }
diff --git a/test/NestedTransactionTest.php b/test/NestedTransactionTest.php
index 33f2144..27902d1 100644
--- a/test/NestedTransactionTest.php
+++ b/test/NestedTransactionTest.php
@@ -2,9 +2,11 @@
 
 namespace Amp\Mysql\Test;
 
+use Amp\Mysql\MysqlConnection;
 use Amp\Mysql\MysqlLink;
 use Amp\Mysql\MysqlTransaction;
 use Amp\Mysql\SocketMysqlConnector;
+use function Amp\async;
 
 class NestedTransactionTest extends LinkTest
 {
@@ -14,7 +16,7 @@ class NestedTransactionTest extends LinkTest
 
     public function getLink(bool $useCompression = false): MysqlLink
     {
-        $this->link = (new SocketMysqlConnector)->connect($this->getConfig($useCompression));
+        $this->link = $this->connect($useCompression);
         $this->transaction = $this->link->beginTransaction();
         $this->nested = $this->transaction->beginTransaction();
 
@@ -35,4 +37,66 @@ public function tearDown(): void
 
         parent::tearDown();
     }
+
+    protected function connect(bool $useCompression = false): MysqlConnection
+    {
+        return (new SocketMysqlConnector)->connect($this->getConfig($useCompression));
+    }
+
+    public function testStatementExecuteWaitsForNestedTransaction(): void
+    {
+        $sql = "SELECT * FROM main WHERE a = :a";
+        $params = ["a" => 1];
+
+        $this->link = (new SocketMysqlConnector)->connect($this->getConfig());
+        $this->transaction = $this->link->beginTransaction();
+
+        $stmt = $this->transaction->prepare($sql);
+        $result = \iterator_to_array($stmt->execute($params));
+
+        $this->nested = $this->transaction->beginTransaction();
+
+        $future = async(fn () => \iterator_to_array($stmt->execute($params)));
+
+        $nestedStmt = $this->nested->prepare($sql);
+        self::assertSame($result, \iterator_to_array($nestedStmt->execute($params)));
+
+        self::assertFalse($future->isComplete());
+
+        $this->nested->commit();
+
+        self::assertSame($result, $future->await());
+    }
+
+    public function testRollbackInnerCommitOuter(): void
+    {
+        $sql = "INSERT INTO main (a, b) VALUES (:a, :b)";
+
+        $this->link = $this->connect();
+
+        $this->transaction = $this->link->beginTransaction();
+
+        $nested1 = $this->transaction->beginTransaction();
+        $nested1->onCommit($this->createCallback(0));
+        $nested1->onRollback($this->createCallback(1));
+
+        $nested1->execute($sql, ["a" => 10, "b" => 20]);
+
+        $nested2 = $nested1->beginTransaction();
+        $nested2->onCommit($this->createCallback(0));
+        $nested2->onRollback($this->createCallback(1));
+
+        $nested2->execute($sql, ["a" => 20, "b" => 30]);
+
+        $nested2->rollback();
+
+        $nested1->commit();
+
+        $sql = "SELECT * FROM main WHERE a = :a AND b = :b";
+
+        self::assertCount(1, \iterator_to_array($this->transaction->execute($sql, ["a" => 10, "b" => 20])));
+        self::assertCount(0, \iterator_to_array($this->transaction->execute($sql, ["a" => 20, "b" => 30])));
+
+        $this->transaction->rollback(); // Trigger onRollback callback within test.
+    }
 }

From 5eed20e7cff4a47aea61726f601701ae5d4fe70d Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Fri, 8 Dec 2023 18:26:42 -0600
Subject: [PATCH 155/174] Fix name typo

---
 src/Internal/ConnectionProcessor.php                        | 6 +++---
 ...sqlConnectionMetdata.php => MysqlConnectionMetadata.php} | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)
 rename src/Internal/{MysqlConnectionMetdata.php => MysqlConnectionMetadata.php} (93%)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 8649e6c..831bea2 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -65,7 +65,7 @@ class ConnectionProcessor implements TransientResource
 
     private MysqlConfig $config;
 
-    private readonly MysqlConnectionMetdata $metadata;
+    private readonly MysqlConnectionMetadata $metadata;
 
     /** @var \SplQueue<DeferredFuture> */
     private readonly \SplQueue $deferreds;
@@ -132,7 +132,7 @@ class ConnectionProcessor implements TransientResource
     public function __construct(Socket $socket, MysqlConfig $config)
     {
         $this->socket = $socket;
-        $this->metadata = new MysqlConnectionMetdata();
+        $this->metadata = new MysqlConnectionMetadata();
         $this->config = $config;
         $this->lastUsedAt = \time();
 
@@ -270,7 +270,7 @@ private function appendTask(\Closure $callback): void
         }
     }
 
-    public function getMetadata(): MysqlConnectionMetdata
+    public function getMetadata(): MysqlConnectionMetadata
     {
         return clone $this->metadata;
     }
diff --git a/src/Internal/MysqlConnectionMetdata.php b/src/Internal/MysqlConnectionMetadata.php
similarity index 93%
rename from src/Internal/MysqlConnectionMetdata.php
rename to src/Internal/MysqlConnectionMetadata.php
index 8e2663b..a1dbd77 100644
--- a/src/Internal/MysqlConnectionMetdata.php
+++ b/src/Internal/MysqlConnectionMetadata.php
@@ -3,7 +3,7 @@
 namespace Amp\Mysql\Internal;
 
 /** @internal */
-final class MysqlConnectionMetdata
+final class MysqlConnectionMetadata
 {
     public int $affectedRows = 0;
     public int $insertId = 0;

From 3608f827d57bf431f4151c5af8a0ba80f443ecf4 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 9 Dec 2023 10:48:18 -0600
Subject: [PATCH 156/174] Remove copy/paste leftover

---
 src/Internal/MysqlNestableExecutor.php | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/src/Internal/MysqlNestableExecutor.php b/src/Internal/MysqlNestableExecutor.php
index 4a55b4a..26c961b 100644
--- a/src/Internal/MysqlNestableExecutor.php
+++ b/src/Internal/MysqlNestableExecutor.php
@@ -2,7 +2,6 @@
 
 namespace Amp\Mysql\Internal;
 
-use Amp\DeferredFuture;
 use Amp\Mysql\MysqlExecutor;
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
@@ -14,8 +13,6 @@
  */
 final class MysqlNestableExecutor implements MysqlExecutor, NestableTransactionExecutor
 {
-    private ?DeferredFuture $busy = null;
-
     public function __construct(
         private readonly ConnectionProcessor $processor,
     ) {
@@ -49,19 +46,11 @@ public function onClose(\Closure $onClose): void
 
     public function query(string $sql): MysqlResult
     {
-        while ($this->busy) {
-            $this->busy->getFuture()->await();
-        }
-
         return $this->processor->query($sql)->await();
     }
 
     public function prepare(string $sql): MysqlStatement
     {
-        while ($this->busy) {
-            $this->busy->getFuture()->await();
-        }
-
         return $this->processor->prepare($sql)->await();
     }
 

From ccac7b14889be6afbe642da8f1f5f1c5a67fe342 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 9 Dec 2023 10:49:37 -0600
Subject: [PATCH 157/174] Update for latest dependency betas

---
 composer.json                 | 4 ++--
 src/SocketMysqlConnection.php | 5 +++++
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/composer.json b/composer.json
index 582720e..7db4e04 100644
--- a/composer.json
+++ b/composer.json
@@ -18,8 +18,8 @@
         "amphp/parser": "^1.1",
         "amphp/pipeline": "^1",
         "amphp/socket": "^2",
-        "amphp/sql": "dev-refactor-transactions as 2.0",
-        "amphp/sql-common": "dev-refactor-transactions as 2.0"
+        "amphp/sql": "^2-beta.6",
+        "amphp/sql-common": "^2-beta.7"
     },
     "require-dev": {
         "ext-mysqli": "*",
diff --git a/src/SocketMysqlConnection.php b/src/SocketMysqlConnection.php
index e50a63d..11be004 100644
--- a/src/SocketMysqlConnection.php
+++ b/src/SocketMysqlConnection.php
@@ -42,6 +42,11 @@ public function getConfig(): MysqlConfig
         return $this->processor->getConfig();
     }
 
+    public function getTransactionIsolation(): TransactionIsolation
+    {
+        return $this->transactionIsolation;
+    }
+
     public function setTransactionIsolation(TransactionIsolation $isolation): void
     {
         $this->transactionIsolation = $isolation;

From 5e321f21a37579f867fda3a806e6dc967367d704 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 3 Sep 2023 14:30:04 -0500
Subject: [PATCH 158/174] Add MysqlEncodedValue

Removed MysqlDataType::encodeBinary().
---
 src/Internal/ConnectionProcessor.php | 11 +++--
 src/MysqlDataType.php                | 66 --------------------------
 src/MysqlEncodedValue.php            | 71 ++++++++++++++++++++++++++++
 test/MysqlDataTypeTest.php           | 21 ++++----
 4 files changed, 88 insertions(+), 81 deletions(-)
 create mode 100644 src/MysqlEncodedValue.php

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 831bea2..1a87388 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -10,6 +10,7 @@
 use Amp\Mysql\MysqlColumnDefinition;
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlDataType;
+use Amp\Mysql\MysqlEncodedValue;
 use Amp\Mysql\MysqlResult;
 use Amp\Parser\Parser;
 use Amp\Socket\Socket;
@@ -465,10 +466,14 @@ public function execute(int $stmtId, string $query, array $params, array $prebou
                         continue;
                     }
 
-                    [$encodedType, $value] = $paramType->encodeBinary($param);
+                    if ($paramType === MysqlDataType::Json && \is_string($param)) {
+                        $encodedValue = MysqlEncodedValue::fromJson($param);
+                    } else {
+                        $encodedValue = MysqlEncodedValue::fromValue($param);
+                    }
 
-                    $types[] = MysqlDataType::encodeInt16($encodedType->value);
-                    $values[] = $value;
+                    $types[] = MysqlDataType::encodeInt16($encodedValue->getType()->value);
+                    $values[] = $encodedValue->getBytes();
                 }
 
                 $payload[] = $paramList;
diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index 898b2a6..f27781e 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -39,72 +39,6 @@ enum MysqlDataType: int
     case String = 0xfe;
     case Geometry = 0xff;
 
-    /**
-     * @return array{self, string}
-     *
-     * @throws SqlException If the given value cannot be encoded.
-     *
-     * @see 14.7.3 Binary Value
-     */
-    public function encodeBinary(int|float|string|bool|\BackedEnum|\Stringable|null $param): array
-    {
-        $encodedPair = self::encodeValue($param);
-
-        if ($this === self::Json) {
-            [$encodedType, $encodedValue] = $encodedPair;
-            if ($encodedType === self::LongBlob) {
-                return [$this, $encodedValue];
-            }
-        }
-
-        return $encodedPair;
-    }
-
-    /**
-     * @return array{self, string}
-     */
-    private static function encodeValue(mixed $param): array
-    {
-        switch (\get_debug_type($param)) {
-            case "bool":
-                $encoded = $param ? "\x01" : "\0";
-                return [self::Tiny, $encoded];
-
-            case "int":
-                if ($param >= -(1 << 15) && $param < (1 << 15)) {
-                    return [self::Short, self::encodeInt16($param)];
-                }
-
-                if ($param >= -(1 << 31) && $param < (1 << 31)) {
-                    return [self::Long, self::encodeInt32($param)];
-                }
-
-                return [self::LongLong, self::encodeInt64($param)];
-
-            case "float":
-                return [self::Double, \pack("e", $param)];
-
-            case "string":
-                return [self::LongBlob, self::encodeInt(\strlen($param)) . $param];
-
-            case "null":
-                return [self::Null, ""];
-
-            default:
-                if (\is_object($param)) {
-                    if ($param instanceof \BackedEnum) {
-                        return self::encodeValue($param->value);
-                    }
-
-                    if ($param instanceof \Stringable) {
-                        return self::encodeValue((string) $param);
-                    }
-                }
-
-                throw new SqlException("Unexpected type for query parameter: " . \get_debug_type($param));
-        }
-    }
-
     /**
      * @see 14.7.3 Binary Protocol Value
      *
diff --git a/src/MysqlEncodedValue.php b/src/MysqlEncodedValue.php
new file mode 100644
index 0000000..f8d5391
--- /dev/null
+++ b/src/MysqlEncodedValue.php
@@ -0,0 +1,71 @@
+<?php declare(strict_types=1);
+
+namespace Amp\Mysql;
+
+final class MysqlEncodedValue
+{
+    public static function fromValue(mixed $param): self
+    {
+        switch (\get_debug_type($param)) {
+            case "string":
+                return new self(MysqlDataType::LongBlob, MysqlDataType::encodeInt(\strlen($param)) . $param);
+
+            case "int":
+                if ($param >= -(1 << 7) && $param < (1 << 7)) {
+                    return new self(MysqlDataType::Tiny, MysqlDataType::encodeInt8($param));
+                }
+
+                if ($param >= -(1 << 15) && $param < (1 << 15)) {
+                    return new self(MysqlDataType::Short, MysqlDataType::encodeInt16($param));
+                }
+
+                if ($param >= -(1 << 31) && $param < (1 << 31)) {
+                    return new self(MysqlDataType::Long, MysqlDataType::encodeInt32($param));
+                }
+
+                return new self(MysqlDataType::LongLong, MysqlDataType::encodeInt64($param));
+
+            case "float":
+                return new self(MysqlDataType::Double, \pack("e", $param));
+
+            case "bool":
+                $encoded = $param ? "\x01" : "\0";
+                return new self(MysqlDataType::Tiny, $encoded);
+
+            case "null":
+                return new self(MysqlDataType::Null, "");
+
+            default:
+                if ($param instanceof \BackedEnum) {
+                    return self::fromValue($param->value);
+                }
+
+                if ($param instanceof \Stringable) {
+                    return self::fromValue((string) $param);
+                }
+
+                throw new \TypeError("Unexpected type for query parameter: " . \get_debug_type($param));
+        }
+    }
+
+    public static function fromJson(string $json): self
+    {
+        return new self(MysqlDataType::Json, MysqlDataType::encodeInt(\strlen($json)) . $json);
+    }
+
+    private function __construct(
+        private readonly MysqlDataType $type,
+        private readonly string $bytes,
+    ) {
+    }
+
+    public function getType(): MysqlDataType
+    {
+        return $this->type;
+    }
+
+    public function getBytes(): string
+    {
+        return $this->bytes;
+    }
+}
diff --git a/test/MysqlDataTypeTest.php b/test/MysqlDataTypeTest.php
index 82cc408..64e9ed8 100644
--- a/test/MysqlDataTypeTest.php
+++ b/test/MysqlDataTypeTest.php
@@ -95,30 +95,27 @@ public function provideJsonData(): array
                 "boolean": true,
                 "null": null
             }
-            JSON, false],
-            'array' => ['[1, 2, 3]', true],
-            'float' => ['3.1415926', true],
-            'integer' => ['42', true],
-            'boolean' => ['true', true],
-            'null' => ['null', true],
+            JSON],
+            'array' => ['[1, 2, 3]'],
+            'string' => ['"string"'],
+            'float' => ['3.1415926'],
+            'integer' => ['42'],
+            'boolean' => ['true'],
+            'null' => ['null'],
         ];
     }
 
     /**
      * @dataProvider provideJsonData
      */
-    public function testJson(mixed $json, bool $same): void
+    public function testJson(mixed $json): void
     {
         $result = $this->connection->execute("SELECT CAST(? AS JSON) AS data", [$json]);
 
         foreach ($result as $row) {
             $expected = \json_decode($json);
             $actual = \json_decode($row['data']);
-            if ($same) {
-                self::assertSame($expected, $actual);
-            } else {
-                self::assertEquals($expected, $actual);
-            }
+            self::assertEquals($expected, $actual);
         }
     }
 }

From c1ff625e526c9ca46fa7bf2d275e2318f058cfd1 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 9 Dec 2023 21:16:58 -0600
Subject: [PATCH 159/174] Pass socket connector to connect()

---
 src/SocketMysqlConnection.php | 15 +++++++++++++--
 src/SocketMysqlConnector.php  |  5 ++---
 2 files changed, 15 insertions(+), 5 deletions(-)

diff --git a/src/SocketMysqlConnection.php b/src/SocketMysqlConnection.php
index 11be004..7c3982f 100644
--- a/src/SocketMysqlConnection.php
+++ b/src/SocketMysqlConnection.php
@@ -4,7 +4,9 @@
 
 use Amp\Cancellation;
 use Amp\DeferredFuture;
-use Amp\Socket\Socket;
+use Amp\Socket\SocketConnector;
+use Amp\Socket\SocketException;
+use Amp\Sql\SqlException;
 use Amp\Sql\TransactionIsolation;
 use Amp\Sql\TransactionIsolationLevel;
 use Revolt\EventLoop;
@@ -19,10 +21,19 @@ final class SocketMysqlConnection implements MysqlConnection
     private readonly \Closure $release;
 
     public static function connect(
-        Socket $socket,
+        SocketConnector $connector,
         MysqlConfig $config,
         ?Cancellation $cancellation = null,
     ): self {
+        try {
+            $socket = $connector->connect($config->getConnectionString(), $config->getConnectContext(), $cancellation);
+        } catch (SocketException $exception) {
+            throw new SqlException(
+                'Connecting to the MySQL server failed: ' . $exception->getMessage(),
+                previous: $exception,
+            );
+        }
+
         $processor = new Internal\ConnectionProcessor($socket, $config);
         $processor->connect($cancellation);
         return new self($processor);
diff --git a/src/SocketMysqlConnector.php b/src/SocketMysqlConnector.php
index da1251d..d9004a7 100644
--- a/src/SocketMysqlConnector.php
+++ b/src/SocketMysqlConnector.php
@@ -22,9 +22,8 @@ public function connect(SqlConfig $config, ?Cancellation $cancellation = null):
             throw new \TypeError(\sprintf("Must provide an instance of %s to MySQL connectors", MysqlConfig::class));
         }
 
-        $socket = ($this->connector ?? Socket\socketConnector())
-            ->connect($config->getConnectionString(), $config->getConnectContext(), $cancellation);
+        $connector = $this->connector ?? Socket\socketConnector();
 
-        return SocketMysqlConnection::connect($socket, $config, $cancellation);
+        return SocketMysqlConnection::connect($connector, $config, $cancellation);
     }
 }

From 90f99de80638e2ef486f786a047a15ab2d114d5e Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 9 Dec 2023 21:54:20 -0600
Subject: [PATCH 160/174] Change param names

---
 src/MysqlDataType.php | 156 +++++++++++++++++++++---------------------
 1 file changed, 78 insertions(+), 78 deletions(-)

diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index f27781e..d758b1b 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -44,7 +44,7 @@ enum MysqlDataType: int
      *
      * @param int<0, max> $offset
      */
-    public function decodeBinary(string $string, int &$offset = 0, int $flags = 0): int|float|string|null
+    public function decodeBinary(string $bytes, int &$offset = 0, int $flags = 0): int|float|string|null
     {
         $unsigned = $flags & 0x20;
 
@@ -63,49 +63,49 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
             case self::Decimal:
             case self::NewDecimal:
             case self::Json:
-                return self::decodeString($string, $offset);
+                return self::decodeString($bytes, $offset);
 
             case self::LongLong:
                 return $unsigned
-                    ? self::decodeUnsigned64($string, $offset)
-                    : self::decodeInt64($string, $offset);
+                    ? self::decodeUnsigned64($bytes, $offset)
+                    : self::decodeInt64($bytes, $offset);
 
             case self::Long:
                 return $unsigned
-                    ? self::decodeUnsigned32($string, $offset)
-                    : self::decodeInt32($string, $offset);
+                    ? self::decodeUnsigned32($bytes, $offset)
+                    : self::decodeInt32($bytes, $offset);
 
             case self::Int24:
                 return $unsigned
-                    ? self::decodeUnsigned24($string, $offset)
-                    : self::decodeInt24($string, $offset);
+                    ? self::decodeUnsigned24($bytes, $offset)
+                    : self::decodeInt24($bytes, $offset);
 
             case self::Short:
             case self::Year:
                 return $unsigned
-                    ? self::decodeUnsigned16($string, $offset)
-                    : self::decodeInt16($string, $offset);
+                    ? self::decodeUnsigned16($bytes, $offset)
+                    : self::decodeInt16($bytes, $offset);
 
             case self::Tiny:
                 return $unsigned
-                    ? self::decodeUnsigned8($string, $offset)
-                    : self::decodeInt8($string, $offset);
+                    ? self::decodeUnsigned8($bytes, $offset)
+                    : self::decodeInt8($bytes, $offset);
 
             case self::Double:
                 $offset += 8;
-                return \unpack("e", $string, $offset - 8)[1];
+                return \unpack("e", $bytes, $offset - 8)[1];
 
             case self::Float:
                 $offset += 4;
-                return \unpack("g", $string, $offset - 4)[1];
+                return \unpack("g", $bytes, $offset - 4)[1];
 
             case self::Date:
             case self::Datetime:
             case self::Timestamp:
-                return $this->decodeDateTime($string, $offset);
+                return $this->decodeDateTime($bytes, $offset);
 
             case self::Time:
-                return $this->decodeTime($string, $offset);
+                return $this->decodeTime($bytes, $offset);
 
             case self::Null:
                 return null;
@@ -118,11 +118,11 @@ public function decodeBinary(string $string, int &$offset = 0, int $flags = 0):
     /**
      * @param int<0, max> $offset
      */
-    public function decodeText(string $string, int &$offset = 0, int $flags = 0): int|float|string
+    public function decodeText(string $bytes, int &$offset = 0, int $flags = 0): int|float|string
     {
-        $length = self::decodeUnsigned($string, $offset);
+        $length = self::decodeUnsigned($bytes, $offset);
         $offset += $length;
-        $data = \substr($string, $offset - $length, $length);
+        $data = \substr($bytes, $offset - $length, $length);
 
         switch ($this) {
             case self::LongLong:
@@ -168,28 +168,28 @@ public function isBindable(): bool
     /**
      * @param int<0, max> $offset
      */
-    private function decodeDateTime(string $string, int &$offset): string
+    private function decodeDateTime(string $bytes, int &$offset): string
     {
         $year = $month = $day = $hour = $minute = $second = $microsecond = 0;
 
-        switch ($length = self::decodeUnsigned8($string, $offset)) {
+        switch ($length = self::decodeUnsigned8($bytes, $offset)) {
             case 11:
                 $position = $offset + 7;
-                $microsecond = self::decodeUnsigned32($string, $position);
+                $microsecond = self::decodeUnsigned32($bytes, $position);
                 // no break
 
             case 7:
                 $position = $offset + 4;
-                $hour = self::decodeUnsigned8($string, $position);
-                $minute = self::decodeUnsigned8($string, $position);
-                $second = self::decodeUnsigned8($string, $position);
+                $hour = self::decodeUnsigned8($bytes, $position);
+                $minute = self::decodeUnsigned8($bytes, $position);
+                $second = self::decodeUnsigned8($bytes, $position);
                 // no break
 
             case 4:
                 $position = $offset;
-                $year = self::decodeUnsigned16($string, $position);
-                $month = self::decodeUnsigned8($string, $position);
-                $day = self::decodeUnsigned8($string, $position);
+                $year = self::decodeUnsigned16($bytes, $position);
+                $month = self::decodeUnsigned8($bytes, $position);
+                $day = self::decodeUnsigned8($bytes, $position);
                 // no break
 
             case 0:
@@ -217,23 +217,23 @@ private function decodeDateTime(string $string, int &$offset): string
     /**
      * @param int<0, max> $offset
      */
-    private function decodeTime(string $string, int &$offset): string
+    private function decodeTime(string $bytes, int &$offset): string
     {
         $negative = $day = $hour = $minute = $second = $microsecond = 0;
 
-        switch ($length = self::decodeUnsigned8($string, $offset)) {
+        switch ($length = self::decodeUnsigned8($bytes, $offset)) {
             case 12:
                 $position = $offset + 8;
-                $microsecond = self::decodeUnsigned32($string, $position);
+                $microsecond = self::decodeUnsigned32($bytes, $position);
                 // no break
 
             case 8:
                 $position = $offset;
-                $negative = self::decodeUnsigned8($string, $position);
-                $day = self::decodeUnsigned32($string, $position);
-                $hour = self::decodeUnsigned8($string, $position);
-                $minute = self::decodeUnsigned8($string, $position);
-                $second = self::decodeUnsigned8($string, $position);
+                $negative = self::decodeUnsigned8($bytes, $position);
+                $day = self::decodeUnsigned32($bytes, $position);
+                $hour = self::decodeUnsigned8($bytes, $position);
+                $minute = self::decodeUnsigned8($bytes, $position);
+                $second = self::decodeUnsigned8($bytes, $position);
                 // no break
 
             case 0:
@@ -258,15 +258,15 @@ private function decodeTime(string $string, int &$offset): string
     /**
      * @param int<0, max> $offset
      */
-    public static function decodeNullTerminatedString(string $string, int &$offset = 0): string
+    public static function decodeNullTerminatedString(string $bytes, int &$offset = 0): string
     {
-        $length = \strpos($string, "\0", $offset);
+        $length = \strpos($bytes, "\0", $offset);
         if ($length === false) {
             throw new \ValueError('Null not found in string');
         }
 
         $length -= $offset;
-        $result = \substr($string, $offset, $length);
+        $result = \substr($bytes, $offset, $length);
         $offset += $length + 1;
         \assert($offset >= 0);
 
@@ -276,11 +276,11 @@ public static function decodeNullTerminatedString(string $string, int &$offset =
     /**
      * @param int<0, max> $offset
      */
-    public static function decodeString(string $string, int &$offset = 0): string
+    public static function decodeString(string $bytes, int &$offset = 0): string
     {
-        $length = self::decodeUnsigned($string, $offset);
+        $length = self::decodeUnsigned($bytes, $offset);
         $offset += $length;
-        return \substr($string, $offset - $length, $length);
+        return \substr($bytes, $offset - $length, $length);
     }
 
     /**
@@ -288,17 +288,17 @@ public static function decodeString(string $string, int &$offset = 0): string
      *
      * @return int<0, max>
      */
-    public static function decodeUnsigned(string $string, int &$offset = 0): int
+    public static function decodeUnsigned(string $bytes, int &$offset = 0): int
     {
-        $int = self::decodeUnsigned8($string, $offset);
+        $int = self::decodeUnsigned8($bytes, $offset);
         if ($int < 0xfb) {
             return $int;
         }
 
         return match ($int) {
-            0xfc => self::decodeUnsigned16($string, $offset),
-            0xfd => self::decodeUnsigned24($string, $offset),
-            0xfe => self::decodeUnsigned64($string, $offset),
+            0xfc => self::decodeUnsigned16($bytes, $offset),
+            0xfd => self::decodeUnsigned24($bytes, $offset),
+            0xfe => self::decodeUnsigned64($bytes, $offset),
             // If this happens connection is borked...
             default => throw new SqlException("$int is not in ranges [0x00, 0xfa] or [0xfc, 0xfe]"),
         };
@@ -308,11 +308,11 @@ public static function decodeUnsigned(string $string, int &$offset = 0): int
      * @param int<0, max> $offset
      * @param int<0, max> $length
      */
-    public static function decodeIntByLength(string $string, int $length, int &$offset = 0): int
+    public static function decodeIntByLength(string $bytes, int $length, int &$offset = 0): int
     {
         $int = 0;
         while ($length) {
-            $int = ($int << 8) + \ord($string[--$length + $offset]);
+            $int = ($int << 8) + \ord($bytes[--$length + $offset]);
         }
 
         $offset += $length;
@@ -323,9 +323,9 @@ public static function decodeIntByLength(string $string, int $length, int &$offs
     /**
      * @param int<0, max> $offset
      */
-    public static function decodeInt8(string $string, int &$offset = 0): int
+    public static function decodeInt8(string $bytes, int &$offset = 0): int
     {
-        $int = \ord($string[$offset++]);
+        $int = \ord($bytes[$offset++]);
         if ($int < (1 << 7)) {
             return $int;
         }
@@ -338,9 +338,9 @@ public static function decodeInt8(string $string, int &$offset = 0): int
      *
      * @return int<0, max>
      */
-    public static function decodeUnsigned8(string $string, int &$offset = 0): int
+    public static function decodeUnsigned8(string $bytes, int &$offset = 0): int
     {
-        $result = \ord($string[$offset++]);
+        $result = \ord($bytes[$offset++]);
         \assert($result >= 0);
         return $result;
     }
@@ -348,9 +348,9 @@ public static function decodeUnsigned8(string $string, int &$offset = 0): int
     /**
      * @param int<0, max> $offset
      */
-    public static function decodeInt16(string $string, int &$offset = 0): int
+    public static function decodeInt16(string $bytes, int &$offset = 0): int
     {
-        $int = \unpack("v", $string, $offset)[1];
+        $int = \unpack("v", $bytes, $offset)[1];
         $offset += 2;
         if ($int < (1 << 15)) {
             return $int;
@@ -364,18 +364,18 @@ public static function decodeInt16(string $string, int &$offset = 0): int
      *
      * @return int<0, max>
      */
-    public static function decodeUnsigned16(string $string, int &$offset = 0): int
+    public static function decodeUnsigned16(string $bytes, int &$offset = 0): int
     {
         $offset += 2;
-        return \unpack("v", $string, $offset - 2)[1];
+        return \unpack("v", $bytes, $offset - 2)[1];
     }
 
     /**
      * @param int<0, max> $offset
      */
-    public static function decodeInt24(string $string, int &$offset = 0): int
+    public static function decodeInt24(string $bytes, int &$offset = 0): int
     {
-        $int = \unpack("V", \substr($string, $offset, 3) . "\x00")[1];
+        $int = \unpack("V", \substr($bytes, $offset, 3) . "\x00")[1];
         $offset += 3;
         if ($int < (1 << 23)) {
             return $int;
@@ -389,9 +389,9 @@ public static function decodeInt24(string $string, int &$offset = 0): int
      *
      * @return int<0, max>
      */
-    public static function decodeUnsigned24(string $string, int &$offset = 0): int
+    public static function decodeUnsigned24(string $bytes, int &$offset = 0): int
     {
-        $result = \unpack("V", \substr($string, $offset, 3) . "\x00")[1];
+        $result = \unpack("V", \substr($bytes, $offset, 3) . "\x00")[1];
         $offset += 3;
         return $result;
     }
@@ -399,19 +399,19 @@ public static function decodeUnsigned24(string $string, int &$offset = 0): int
     /**
      * @param int<0, max> $offset
      */
-    public static function decodeInt32(string $string, int &$offset = 0): int
+    public static function decodeInt32(string $bytes, int &$offset = 0): int
     {
         $offset += 4;
 
         if (\PHP_INT_SIZE > 4) {
-            $int = \unpack("V", $string, $offset - 4)[1];
+            $int = \unpack("V", $bytes, $offset - 4)[1];
             if ($int < (1 << 31)) {
                 return $int;
             }
             return $int << 32 >> 32;
         }
 
-        return \unpack("V", $string, $offset - 4)[1];
+        return \unpack("V", $bytes, $offset - 4)[1];
     }
 
     /**
@@ -419,9 +419,9 @@ public static function decodeInt32(string $string, int &$offset = 0): int
      *
      * @return int<0, max>
      */
-    public static function decodeUnsigned32(string $string, int &$offset = 0): int
+    public static function decodeUnsigned32(string $bytes, int &$offset = 0): int
     {
-        $result = \unpack("V", $string, $offset)[1];
+        $result = \unpack("V", $bytes, $offset)[1];
         $offset += 4;
 
         if ($result < 0) {
@@ -436,43 +436,43 @@ public static function decodeUnsigned32(string $string, int &$offset = 0): int
      *
      * @return int<0, max>|string
      */
-    public static function decodeUnsigned32WithGmp(string $string, int &$offset = 0): int|string
+    public static function decodeUnsigned32WithGmp(string $bytes, int &$offset = 0): int|string
     {
         $offset += 4;
 
         if (\PHP_INT_SIZE > 4) {
-            return \unpack("V", $string, $offset - 4)[1];
+            return \unpack("V", $bytes, $offset - 4)[1];
         }
 
         \assert(\extension_loaded("gmp"), "The GMP extension is required for UNSIGNED INT fields on 32-bit systems");
         /** @psalm-suppress UndefinedConstant */
-        return \gmp_strval(\gmp_import(\substr($string, $offset - 4, 4), 1, \GMP_LSW_FIRST));
+        return \gmp_strval(\gmp_import(\substr($bytes, $offset - 4, 4), 1, \GMP_LSW_FIRST));
     }
 
     /**
      * @param int<0, max> $offset
      */
-    public static function decodeInt64(string $string, int &$offset = 0): int
+    public static function decodeInt64(string $bytes, int &$offset = 0): int
     {
         if (\PHP_INT_SIZE > 4) {
             $offset += 8;
-            return \unpack("P", $string, $offset - 8)[1];
+            return \unpack("P", $bytes, $offset - 8)[1];
         }
 
         throw new \RuntimeException('64-bit integers are not supported by 32-bit builds of PHP');
     }
 
-    public static function decodeInt64WithGmp(string $string, int &$offset = 0): int|string
+    public static function decodeInt64WithGmp(string $bytes, int &$offset = 0): int|string
     {
         $offset += 8;
 
         if (\PHP_INT_SIZE > 4) {
-            return \unpack("P", $string, $offset - 8)[1];
+            return \unpack("P", $bytes, $offset - 8)[1];
         }
 
         \assert(\extension_loaded("gmp"), "The GMP extension is required for BIGINT fields on 32-bit systems");
         /** @psalm-suppress UndefinedConstant */
-        return \gmp_strval(\gmp_import(\substr($string, $offset - 8, 8), 1, \GMP_LSW_FIRST));
+        return \gmp_strval(\gmp_import(\substr($bytes, $offset - 8, 8), 1, \GMP_LSW_FIRST));
     }
 
     /**
@@ -480,13 +480,13 @@ public static function decodeInt64WithGmp(string $string, int &$offset = 0): int
      *
      * @return int<0, max>
      */
-    public static function decodeUnsigned64(string $string, int &$offset = 0): int
+    public static function decodeUnsigned64(string $bytes, int &$offset = 0): int
     {
         if (\PHP_INT_SIZE <= 4) {
             throw new \RuntimeException('64-bit integers are not supported by 32-bit builds of PHP');
         }
 
-        $result = \unpack("P", $string, $offset)[1];
+        $result = \unpack("P", $bytes, $offset)[1];
         $offset += 8;
 
         if ($result < 0) {
@@ -499,13 +499,13 @@ public static function decodeUnsigned64(string $string, int &$offset = 0): int
     /**
      * @param int<0, max> $offset
      */
-    public static function decodeUnsigned64WithGmp(string $string, int &$offset = 0): string
+    public static function decodeUnsigned64WithGmp(string $bytes, int &$offset = 0): string
     {
         $offset += 8;
 
         \assert(\extension_loaded("gmp"), "The GMP extension is required for UNSIGNED BIGINT fields");
         /** @psalm-suppress UndefinedConstant */
-        return \gmp_strval(\gmp_import(\substr($string, $offset - 8, 8), 1, \GMP_LSW_FIRST));
+        return \gmp_strval(\gmp_import(\substr($bytes, $offset - 8, 8), 1, \GMP_LSW_FIRST));
     }
 
     public static function encodeInt(int $int): string

From e11ac003b68cc8401d7d9bf2271b17e4b5f6fc16 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 17 Dec 2023 10:41:55 -0600
Subject: [PATCH 161/174] Update for amphp/sql-common@v2.0.0-beta.9

---
 composer.json                      | 2 +-
 src/Internal/MysqlPooledResult.php | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/composer.json b/composer.json
index 7db4e04..ea78087 100644
--- a/composer.json
+++ b/composer.json
@@ -19,7 +19,7 @@
         "amphp/pipeline": "^1",
         "amphp/socket": "^2",
         "amphp/sql": "^2-beta.6",
-        "amphp/sql-common": "^2-beta.7"
+        "amphp/sql-common": "^2-beta.9"
     },
     "require-dev": {
         "ext-mysqli": "*",
diff --git a/src/Internal/MysqlPooledResult.php b/src/Internal/MysqlPooledResult.php
index 998c8a4..2852481 100644
--- a/src/Internal/MysqlPooledResult.php
+++ b/src/Internal/MysqlPooledResult.php
@@ -24,7 +24,7 @@ public function __construct(MysqlResult $result, \Closure $release)
         $this->result = $result;
     }
 
-    protected function newInstanceFrom(Result $result, \Closure $release): self
+    protected static function newInstanceFrom(Result $result, \Closure $release): self
     {
         \assert($result instanceof MysqlResult);
         return new self($result, $release);

From 60bee0fc6d17b5f1a6a0652c7b5f0782e2df3a64 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 17 Dec 2023 10:42:39 -0600
Subject: [PATCH 162/174] Update Psalm and remove fixed suppression

---
 composer.json       | 2 +-
 src/MysqlConfig.php | 2 --
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/composer.json b/composer.json
index ea78087..8d308db 100644
--- a/composer.json
+++ b/composer.json
@@ -30,7 +30,7 @@
         "amphp/phpunit-util": "^3",
         "amphp/php-cs-fixer-config": "^2",
         "phpbench/phpbench": "^1.2.6",
-        "psalm/phar": "^5.4"
+        "psalm/phar": "^5.18"
     },
     "minimum-stability": "dev",
     "prefer-stable": true,
diff --git a/src/MysqlConfig.php b/src/MysqlConfig.php
index 074da5e..9a59258 100644
--- a/src/MysqlConfig.php
+++ b/src/MysqlConfig.php
@@ -10,7 +10,6 @@ final class MysqlConfig extends SqlConfig
     public const DEFAULT_PORT = 3306;
     public const BIN_CHARSET = 45; // utf8mb4_general_ci
 
-    /** @var array<non-empty-string, non-empty-string> Psalm has a bug with the spread operator. */
     public const KEY_MAP = [
         ...parent::KEY_MAP,
         'compress' => 'compression',
@@ -37,7 +36,6 @@ final class MysqlConfig extends SqlConfig
 
     public static function fromString(string $connectionString, ConnectContext $context = null): self
     {
-        /** @psalm-suppress InvalidArgument Psalm does not recognize {@see self::KEY_MAP} is in fact a map. */
         $parts = self::parseConnectionString($connectionString, self::KEY_MAP);
 
         if (!isset($parts['host'])) {

From da985b8d17203a79bd967938ea50712d0654c952 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 17 Dec 2023 10:51:10 -0600
Subject: [PATCH 163/174] Forbid cloning and serialization

---
 src/Internal/ConnectionProcessor.php      | 5 +++++
 src/Internal/MysqlConnectionResult.php    | 4 ++--
 src/Internal/MysqlConnectionStatement.php | 5 +++++
 src/Internal/MysqlNestableExecutor.php    | 5 +++++
 src/Internal/MysqlResultProxy.php         | 5 +++++
 src/Internal/PublicKeyCache.php           | 4 ++++
 src/MysqlColumnDefinition.php             | 6 ++++++
 src/SocketMysqlConnection.php             | 5 +++++
 src/SocketMysqlConnector.php              | 5 +++++
 9 files changed, 42 insertions(+), 2 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 1a87388..699f5c8 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -6,6 +6,8 @@
 use Amp\Cancellation;
 use Amp\DeferredFuture;
 use Amp\File;
+use Amp\ForbidCloning;
+use Amp\ForbidSerialization;
 use Amp\Future;
 use Amp\Mysql\MysqlColumnDefinition;
 use Amp\Mysql\MysqlConfig;
@@ -28,6 +30,9 @@
 /** @internal */
 class ConnectionProcessor implements TransientResource
 {
+    use ForbidCloning;
+    use ForbidSerialization;
+
     private const COMPRESSION_MINIMUM_LENGTH = 860;
     private const MAX_PACKET_LENGTH = 0xffffff;
 
diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index a6745d0..a62814f 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -78,9 +78,9 @@ public function fetchRow(): ?array
 
     public function getNextResult(): ?MysqlResult
     {
-        self::dispose($this->generator);
-
         $this->nextResult ??= async(function (): ?MysqlResult {
+            self::dispose($this->generator);
+
             $deferred = $this->result->next ??= new DeferredFuture;
             $result = $deferred->getFuture()->await();
 
diff --git a/src/Internal/MysqlConnectionStatement.php b/src/Internal/MysqlConnectionStatement.php
index fc41e3d..b20e193 100644
--- a/src/Internal/MysqlConnectionStatement.php
+++ b/src/Internal/MysqlConnectionStatement.php
@@ -3,6 +3,8 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\DeferredFuture;
+use Amp\ForbidCloning;
+use Amp\ForbidSerialization;
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
 use Amp\Sql\ConnectionException;
@@ -11,6 +13,9 @@
 /** @internal */
 final class MysqlConnectionStatement implements MysqlStatement
 {
+    use ForbidCloning;
+    use ForbidSerialization;
+
     private readonly int $totalParamCount;
     private readonly int $positionalParamCount;
 
diff --git a/src/Internal/MysqlNestableExecutor.php b/src/Internal/MysqlNestableExecutor.php
index 26c961b..904ebbb 100644
--- a/src/Internal/MysqlNestableExecutor.php
+++ b/src/Internal/MysqlNestableExecutor.php
@@ -2,6 +2,8 @@
 
 namespace Amp\Mysql\Internal;
 
+use Amp\ForbidCloning;
+use Amp\ForbidSerialization;
 use Amp\Mysql\MysqlExecutor;
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
@@ -13,6 +15,9 @@
  */
 final class MysqlNestableExecutor implements MysqlExecutor, NestableTransactionExecutor
 {
+    use ForbidCloning;
+    use ForbidSerialization;
+
     public function __construct(
         private readonly ConnectionProcessor $processor,
     ) {
diff --git a/src/Internal/MysqlResultProxy.php b/src/Internal/MysqlResultProxy.php
index f85c0e4..248a9c6 100644
--- a/src/Internal/MysqlResultProxy.php
+++ b/src/Internal/MysqlResultProxy.php
@@ -3,6 +3,8 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\DeferredFuture;
+use Amp\ForbidCloning;
+use Amp\ForbidSerialization;
 use Amp\Mysql\MysqlColumnDefinition;
 use Amp\Mysql\MysqlResult;
 use Amp\Pipeline\ConcurrentIterator;
@@ -14,6 +16,9 @@
  */
 final class MysqlResultProxy
 {
+    use ForbidCloning;
+    use ForbidSerialization;
+
     public int $columnsToFetch = 0;
 
     /** @var list<MysqlColumnDefinition> */
diff --git a/src/Internal/PublicKeyCache.php b/src/Internal/PublicKeyCache.php
index edf6783..417c295 100644
--- a/src/Internal/PublicKeyCache.php
+++ b/src/Internal/PublicKeyCache.php
@@ -17,4 +17,8 @@ public static function loadKey(string $pem): \OpenSSLAsymmetricKey
 
         return self::$cache->computeIfAbsent($pem, fn () => \openssl_pkey_get_public($pem));
     }
+
+    private function __construct()
+    {
+    }
 }
diff --git a/src/MysqlColumnDefinition.php b/src/MysqlColumnDefinition.php
index f044f0d..747f4cd 100644
--- a/src/MysqlColumnDefinition.php
+++ b/src/MysqlColumnDefinition.php
@@ -2,8 +2,14 @@
 
 namespace Amp\Mysql;
 
+use Amp\ForbidCloning;
+use Amp\ForbidSerialization;
+
 final class MysqlColumnDefinition
 {
+    use ForbidCloning;
+    use ForbidSerialization;
+
     /**
      * @param non-empty-string $table
      * @param non-empty-string $name
diff --git a/src/SocketMysqlConnection.php b/src/SocketMysqlConnection.php
index 7c3982f..3b43970 100644
--- a/src/SocketMysqlConnection.php
+++ b/src/SocketMysqlConnection.php
@@ -4,6 +4,8 @@
 
 use Amp\Cancellation;
 use Amp\DeferredFuture;
+use Amp\ForbidCloning;
+use Amp\ForbidSerialization;
 use Amp\Socket\SocketConnector;
 use Amp\Socket\SocketException;
 use Amp\Sql\SqlException;
@@ -13,6 +15,9 @@
 
 final class SocketMysqlConnection implements MysqlConnection
 {
+    use ForbidCloning;
+    use ForbidSerialization;
+
     private TransactionIsolation $transactionIsolation = TransactionIsolationLevel::Committed;
 
     private ?DeferredFuture $busy = null;
diff --git a/src/SocketMysqlConnector.php b/src/SocketMysqlConnector.php
index d9004a7..245dfce 100644
--- a/src/SocketMysqlConnector.php
+++ b/src/SocketMysqlConnector.php
@@ -3,6 +3,8 @@
 namespace Amp\Mysql;
 
 use Amp\Cancellation;
+use Amp\ForbidCloning;
+use Amp\ForbidSerialization;
 use Amp\Socket;
 use Amp\Sql\SqlConfig;
 use Amp\Sql\SqlConnector;
@@ -12,6 +14,9 @@
  */
 final class SocketMysqlConnector implements SqlConnector
 {
+    use ForbidCloning;
+    use ForbidSerialization;
+
     public function __construct(private readonly ?Socket\SocketConnector $connector = null)
     {
     }

From e95eee910fdab24e26d4d0e86d6120e790d88c9b Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 17 Dec 2023 10:54:19 -0600
Subject: [PATCH 164/174] Missed this one

---
 src/Internal/MysqlConnectionResult.php | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/Internal/MysqlConnectionResult.php b/src/Internal/MysqlConnectionResult.php
index a62814f..e57f6d3 100644
--- a/src/Internal/MysqlConnectionResult.php
+++ b/src/Internal/MysqlConnectionResult.php
@@ -3,6 +3,8 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\DeferredFuture;
+use Amp\ForbidCloning;
+use Amp\ForbidSerialization;
 use Amp\Future;
 use Amp\Mysql\MysqlColumnDefinition;
 use Amp\Mysql\MysqlResult;
@@ -16,6 +18,9 @@
  */
 final class MysqlConnectionResult implements MysqlResult, \IteratorAggregate
 {
+    use ForbidCloning;
+    use ForbidSerialization;
+
     private readonly MysqlResultProxy $result;
 
     private readonly \Generator $generator;

From 9b590a0d9e1003c4d106e1d3239e062d6113630a Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 19 Dec 2023 15:07:17 -0600
Subject: [PATCH 165/174] Support JSON columns in bind

---
 src/Internal/ConnectionProcessor.php      | 25 +++++++++++++----------
 src/Internal/MysqlConnectionStatement.php |  2 ++
 src/MysqlDataType.php                     | 13 ------------
 test/LinkTest.php                         | 14 +++++++------
 4 files changed, 24 insertions(+), 30 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 699f5c8..7fa6483 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -430,7 +430,7 @@ public function bindParam(int $stmtId, int $paramId, string $data): void
      * call of this method ...
      *
      * @param list<MysqlColumnDefinition> $params
-     * @param array<int, mixed> $prebound
+     * @param array<int, string> $prebound
      * @param array<int, mixed> $data
      */
     public function execute(int $stmtId, string $query, array $params, array $prebound, array $data = []): Future
@@ -463,19 +463,14 @@ public function execute(int $stmtId, string $query, array $params, array $prebou
                     $paramType = $params[$paramId]->getType();
 
                     if (isset($prebound[$paramId])) {
-                        if (!$paramType->isBindable()) {
-                            throw new SqlException("Cannot use bind with columns of type " . \strtoupper($paramType->name));
-                        }
-
-                        $types[] = MysqlDataType::encodeInt16(MysqlDataType::LongBlob->value);
+                        $types[] = MysqlDataType::encodeInt16($this->getBindType($paramType)->value);
                         continue;
                     }
 
-                    if ($paramType === MysqlDataType::Json && \is_string($param)) {
-                        $encodedValue = MysqlEncodedValue::fromJson($param);
-                    } else {
-                        $encodedValue = MysqlEncodedValue::fromValue($param);
-                    }
+                    $encodedValue = match ($paramType) {
+                        MysqlDataType::Json => MysqlEncodedValue::fromJson((string) $param),
+                        default => MysqlEncodedValue::fromValue($param),
+                    };
 
                     $types[] = MysqlDataType::encodeInt16($encodedValue->getType()->value);
                     $values[] = $encodedValue->getBytes();
@@ -500,6 +495,14 @@ public function execute(int $stmtId, string $query, array $params, array $prebou
         return $deferred->getFuture(); // do not use $this->startCommand(), that might unexpectedly reset the seqId!
     }
 
+    private function getBindType(MysqlDataType $type): MysqlDataType
+    {
+        return match ($type) {
+            MysqlDataType::Json => MysqlDataType::VarString,
+            default => $type,
+        };
+    }
+
     /** @see 14.7.7 COM_STMT_CLOSE */
     public function closeStmt(int $stmtId): void
     {
diff --git a/src/Internal/MysqlConnectionStatement.php b/src/Internal/MysqlConnectionStatement.php
index b20e193..5967ad0 100644
--- a/src/Internal/MysqlConnectionStatement.php
+++ b/src/Internal/MysqlConnectionStatement.php
@@ -20,6 +20,8 @@ final class MysqlConnectionStatement implements MysqlStatement
     private readonly int $positionalParamCount;
 
     private array $named = [];
+
+    /** @var array<string> */
     private array $prebound = [];
 
     private ?ConnectionProcessor $processor;
diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index d758b1b..6483c15 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -152,19 +152,6 @@ public function decodeText(string $bytes, int &$offset = 0, int $flags = 0): int
         }
     }
 
-    public function isInternal(): bool
-    {
-        return match ($this) {
-            self::Timestamp2, self::Datetime2, self::NewDate, self::Time2 => true,
-            default => false,
-        };
-    }
-
-    public function isBindable(): bool
-    {
-        return $this !== self::Json;
-    }
-
     /**
      * @param int<0, max> $offset
      */
diff --git a/test/LinkTest.php b/test/LinkTest.php
index cad2c04..a12a37d 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -9,7 +9,6 @@
 use Amp\Mysql\MysqlResult;
 use Amp\Sql\QueryError;
 use Amp\Sql\Result;
-use Amp\Sql\SqlException;
 use function Amp\async;
 use function Amp\delay;
 
@@ -447,12 +446,15 @@ public function testInsertSelect(): void
 
     public function testBindJson(): void
     {
-        $statement = $this->getLink()->prepare("SELECT CAST(? AS JSON)");
-        $statement->bind(0, '{"key": "value"}');
+        $json = '{"key": "value"}';
 
-        $this->expectException(SqlException::class);
-        $this->expectExceptionMessage("Cannot use bind with columns of type JSON");
+        $statement = $this->getLink()->prepare("SELECT CAST(? AS JSON) AS json_data");
+        $statement->bind(0, $json);
 
-        $statement->execute();
+        $result = $statement->execute();
+
+        foreach ($result as $row) {
+            self::assertSame($json, $row['json_data']);
+        }
     }
 }

From b4ab0d6ffde9aa7cf3b3cc6287ab7355753a1e16 Mon Sep 17 00:00:00 2001
From: Rui Filipe Da Cunha Alves <ruifilipe.dacunhaalves@gmail.com>
Date: Tue, 19 Dec 2023 22:41:21 +0100
Subject: [PATCH 166/174] Add support for sql_mode and fix set charset (#128)

---
 src/Internal/ConnectionProcessor.php |  8 ++++++++
 src/MysqlConfig.php                  | 24 ++++++++++++++++++++++--
 test/LinkTest.php                    |  5 +++--
 3 files changed, 33 insertions(+), 4 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 7fa6483..964cd61 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -221,6 +221,7 @@ public function connect(?Cancellation $cancellation = null): void
             $future = $future->finally(static fn () => $cancellation?->unsubscribe($id));
         }
 
+        // if a charset is specified, we need to set before any query
         if ($this->config->getCharset() !== MysqlConfig::DEFAULT_CHARSET
             || $this->config->getCollation() !== MysqlConfig::DEFAULT_COLLATE
         ) {
@@ -232,6 +233,13 @@ public function connect(?Cancellation $cancellation = null): void
             });
         }
 
+        if ($this->config->getSqlMode()) {
+            $future = $future->map(function (): void {
+                $sqlMode = $this->config->getSqlMode();
+                $this->query("SET SESSION sql_mode='$sqlMode'")->await();
+            });
+        }
+
         $future->await();
     }
 
diff --git a/src/MysqlConfig.php b/src/MysqlConfig.php
index 9a59258..b72b6a8 100644
--- a/src/MysqlConfig.php
+++ b/src/MysqlConfig.php
@@ -8,7 +8,7 @@
 final class MysqlConfig extends SqlConfig
 {
     public const DEFAULT_PORT = 3306;
-    public const BIN_CHARSET = 45; // utf8mb4_general_ci
+    public const BIN_CHARSET = 255; // utf8mb4_0900_ai_ci
 
     public const KEY_MAP = [
         ...parent::KEY_MAP,
@@ -19,7 +19,7 @@ final class MysqlConfig extends SqlConfig
     ];
 
     public const DEFAULT_CHARSET = "utf8mb4";
-    public const DEFAULT_COLLATE = "utf8mb4_general_ci";
+    public const DEFAULT_COLLATE = "utf8mb4_0900_ai_ci";
 
     private bool $useCompression;
 
@@ -34,6 +34,8 @@ final class MysqlConfig extends SqlConfig
     /* @var string private key to use for sha256_password auth method */
     private string $key;
 
+    private ?string $sqlMode;
+
     public static function fromString(string $connectionString, ConnectContext $context = null): self
     {
         $parts = self::parseConnectionString($connectionString, self::KEY_MAP);
@@ -84,6 +86,7 @@ public function __construct(
         ?ConnectContext $context = null,
         string $charset = self::DEFAULT_CHARSET,
         string $collate = self::DEFAULT_COLLATE,
+        ?string $sqlMode = null,
         bool $useCompression = false,
         string $key = '',
         bool $useLocalInfile = false
@@ -93,6 +96,7 @@ public function __construct(
         $this->context = $context ?? (new ConnectContext);
         $this->charset = $charset;
         $this->collate = $collate;
+        $this->sqlMode = $sqlMode;
         $this->useCompression = $useCompression;
         $this->key = $key;
         $this->useLocalInfile = $useLocalInfile;
@@ -173,6 +177,22 @@ public function withCharset(string $charset, string $collate): self
         return $new;
     }
 
+    public function getSqlMode(): ?string
+    {
+        return $this->sqlMode;
+    }
+
+    /**
+     * Set the Server SQL Modes at connection time.
+     * @see https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html
+     */
+    public function withSqlMode(?string $sqlMode): self
+    {
+        $new = clone $this;
+        $new->sqlMode = $sqlMode;
+        return $new;
+    }
+
     public function getKey(): string
     {
         return $this->key;
diff --git a/test/LinkTest.php b/test/LinkTest.php
index a12a37d..e7dbcfd 100644
--- a/test/LinkTest.php
+++ b/test/LinkTest.php
@@ -4,6 +4,7 @@
 
 use Amp\Future;
 use Amp\Mysql\MysqlColumnDefinition;
+use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlDataType;
 use Amp\Mysql\MysqlLink;
 use Amp\Mysql\MysqlResult;
@@ -216,8 +217,8 @@ public function testPrepared(): void
 
         $this->assertEquals([
             new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::LongLong, "flags" => 128])),
-            new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::Datetime, "length" => 104, "decimals" => 6, "charset" => 45])),
-            new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::VarString, "length" => 65532, "decimals" => 31, "charset" => 45])),
+            new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::Datetime, "length" => 104, "decimals" => 6, "charset" => MysqlConfig::BIN_CHARSET])),
+            new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::VarString, "length" => 65532, "decimals" => 31, "charset" => MysqlConfig::BIN_CHARSET])),
         ], $stmt->getParameterDefinitions());
 
         $stmt->bind("data", 'd');

From 4f776c98a6cc1b3b54dd0d2916ba810133db138a Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Tue, 19 Dec 2023 16:30:34 -0600
Subject: [PATCH 167/174] Fix connecting with TLS

---
 src/Internal/ConnectionProcessor.php | 51 +++++++++++++++++-----------
 1 file changed, 32 insertions(+), 19 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 964cd61..891a3df 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -27,7 +27,10 @@
  * 14.2.4 COM_CHANGE_USER
  */
 
-/** @internal */
+/**
+ * @internal
+ * @see https://dev.mysql.com/doc/dev/mysql-server/latest/PAGE_PROTOCOL.html Protocol documentation.
+ */
 class ConnectionProcessor implements TransientResource
 {
     use ForbidCloning;
@@ -92,6 +95,8 @@ class ConnectionProcessor implements TransientResource
 
     private ConnectionState $connectionState = ConnectionState::Unconnected;
 
+    private ?DeferredFuture $paused = null;
+
     private const MAX_PACKET_SIZE = 0xffffff;
     private const MAX_UNCOMPRESSED_BUFLEN = 0xfffffb;
 
@@ -253,6 +258,8 @@ private function read(): void
 
                 $this->parser->push($bytes);
                 $bytes = null; // Free last data read.
+
+                $this->paused?->getFuture()->await(); // Pause next read if negotiating TLS.
             }
         } catch (\Throwable $exception) {
             // $exception used as previous exception below.
@@ -1523,7 +1530,7 @@ private function authSwitchRequest(string $packet): void
      * @see 14.2.5 Connection Phase Packets
      * @see 14.3 Authentication Method
      */
-    private function sendHandshake(bool $inSSL = false): void
+    private function sendHandshake(): void
     {
         if ($this->config->getDatabase() !== null) {
             $this->capabilities |= self::CLIENT_CONNECT_WITH_DB;
@@ -1535,27 +1542,33 @@ private function sendHandshake(bool $inSSL = false): void
 
         $this->capabilities &= $this->serverCapabilities;
 
-        $payload = [];
-        $payload[] = \pack("V", $this->capabilities);
-        $payload[] = \pack("V", self::MAX_PACKET_LENGTH);
-        $payload[] = \chr(MysqlConfig::BIN_CHARSET);
-        $payload[] = \str_repeat("\0", 23); // reserved
+        $tlsEnabled = false;
 
-        if (!$inSSL && ($this->capabilities & self::CLIENT_SSL)) {
-            EventLoop::queue(function () use ($payload): void {
-                try {
-                    $this->write(\implode($payload));
+        do {
+            $payload = [];
+            $payload[] = \pack("V", $this->capabilities);
+            $payload[] = \pack("V", self::MAX_PACKET_LENGTH);
+            $payload[] = \chr(MysqlConfig::BIN_CHARSET);
+            $payload[] = \str_repeat("\0", 23); // reserved
 
-                    $this->socket->setupTls();
+            if ($tlsEnabled || !($this->capabilities & self::CLIENT_SSL)) {
+                break;
+            }
 
-                    $this->sendHandshake(true);
-                } catch (\Throwable $e) {
-                    $this->free($e);
-                }
-            });
+            $paused = $this->paused = new DeferredFuture;
 
-            return;
-        }
+            try {
+                $this->write(\implode($payload));
+                $this->socket->setupTls();
+                $tlsEnabled = true;
+            } catch (\Throwable $e) {
+                $this->free($e);
+                return;
+            } finally {
+                $paused->complete();
+                $this->paused = null;
+            }
+        } while (true);
 
         $payload[] = $this->config->getUser()."\0";
 

From c0144febfe9c53f923454eb06a28272d70322e31 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sat, 30 Dec 2023 12:35:57 -0600
Subject: [PATCH 168/174] Always use VarString for bind type

---
 src/Internal/ConnectionProcessor.php | 10 +---------
 1 file changed, 1 insertion(+), 9 deletions(-)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 891a3df..44d7aa3 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -478,7 +478,7 @@ public function execute(int $stmtId, string $query, array $params, array $prebou
                     $paramType = $params[$paramId]->getType();
 
                     if (isset($prebound[$paramId])) {
-                        $types[] = MysqlDataType::encodeInt16($this->getBindType($paramType)->value);
+                        $types[] = MysqlDataType::encodeInt16(MysqlDataType::VarString->value);
                         continue;
                     }
 
@@ -510,14 +510,6 @@ public function execute(int $stmtId, string $query, array $params, array $prebou
         return $deferred->getFuture(); // do not use $this->startCommand(), that might unexpectedly reset the seqId!
     }
 
-    private function getBindType(MysqlDataType $type): MysqlDataType
-    {
-        return match ($type) {
-            MysqlDataType::Json => MysqlDataType::VarString,
-            default => $type,
-        };
-    }
-
     /** @see 14.7.7 COM_STMT_CLOSE */
     public function closeStmt(int $stmtId): void
     {

From 801509d502857dd683cad12ac253f036499d7606 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 29 Feb 2024 22:22:53 -0600
Subject: [PATCH 169/174] Update for Sql prefixes in common libs

---
 composer.json                                 |  4 +-
 src/Internal/ConnectionProcessor.php          | 38 +++++++++----------
 src/Internal/MysqlCommandResult.php           |  6 +--
 src/Internal/MysqlConnectionStatement.php     |  4 +-
 src/Internal/MysqlConnectionTransaction.php   | 14 +++----
 src/Internal/MysqlNestableExecutor.php        |  6 +--
 src/Internal/MysqlNestedTransaction.php       | 14 +++----
 src/Internal/MysqlPooledResult.php            | 10 ++---
 src/Internal/MysqlPooledStatement.php         |  8 ++--
 src/Internal/MysqlPooledTransaction.php       | 10 ++---
 src/Internal/MysqlStatementPool.php           |  8 ++--
 src/Internal/MysqlTransactionDelegate.php     |  8 ++--
 src/MysqlConnection.php                       |  6 +--
 src/MysqlConnectionPool.php                   | 18 ++++-----
 src/MysqlExecutor.php                         |  6 +--
 src/MysqlLink.php                             |  6 +--
 src/MysqlResult.php                           |  6 +--
 src/MysqlStatement.php                        |  6 +--
 src/MysqlTransaction.php                      |  6 +--
 src/SocketMysqlConnection.php                 | 10 ++---
 ...ectionTest.php => MysqlConnectionTest.php} |  2 +-
 test/{LinkTest.php => MysqlLinkTest.php}      | 15 ++++----
 ...est.php => MysqlNestedTransactionTest.php} |  2 +-
 test/{PoolTest.php => MysqlPoolTest.php}      |  4 +-
 test/initialize.php                           |  2 +-
 25 files changed, 109 insertions(+), 110 deletions(-)
 rename test/{ConnectionTest.php => MysqlConnectionTest.php} (98%)
 rename test/{LinkTest.php => MysqlLinkTest.php} (97%)
 rename test/{NestedTransactionTest.php => MysqlNestedTransactionTest.php} (98%)
 rename test/{PoolTest.php => MysqlPoolTest.php} (99%)

diff --git a/composer.json b/composer.json
index 8d308db..ac92b36 100644
--- a/composer.json
+++ b/composer.json
@@ -18,8 +18,8 @@
         "amphp/parser": "^1.1",
         "amphp/pipeline": "^1",
         "amphp/socket": "^2",
-        "amphp/sql": "^2-beta.6",
-        "amphp/sql-common": "^2-beta.9"
+        "amphp/sql": "2.x-dev",
+        "amphp/sql-common": "2.x-dev"
     },
     "require-dev": {
         "ext-mysqli": "*",
diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index 44d7aa3..dc7ed76 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -16,10 +16,10 @@
 use Amp\Mysql\MysqlResult;
 use Amp\Parser\Parser;
 use Amp\Socket\Socket;
-use Amp\Sql\ConnectionException;
-use Amp\Sql\QueryError;
+use Amp\Sql\SqlConnectionException;
+use Amp\Sql\SqlQueryError;
 use Amp\Sql\SqlException;
-use Amp\Sql\TransientResource;
+use Amp\Sql\SqlTransientResource;
 use Revolt\EventLoop;
 
 /* @TODO
@@ -31,7 +31,7 @@
  * @internal
  * @see https://dev.mysql.com/doc/dev/mysql-server/latest/PAGE_PROTOCOL.html Protocol documentation.
  */
-class ConnectionProcessor implements TransientResource
+class ConnectionProcessor implements SqlTransientResource
 {
     use ForbidCloning;
     use ForbidSerialization;
@@ -238,7 +238,7 @@ public function connect(?Cancellation $cancellation = null): void
             });
         }
 
-        if ($this->config->getSqlMode()) {
+        if ($this->config->getSqlMode() !== null) {
             $future = $future->map(function (): void {
                 $sqlMode = $this->config->getSqlMode();
                 $this->query("SET SESSION sql_mode='$sqlMode'")->await();
@@ -675,7 +675,7 @@ private function handleError(string $packet): void
 
         if ($connecting) {
             // connection failure
-            $this->free(new ConnectionException(\sprintf(
+            $this->free(new SqlConnectionException(\sprintf(
                 'Could not connect to %s: %s',
                 $this->config->getConnectionString(),
                 $this->metadata->errorMsg,
@@ -685,14 +685,14 @@ private function handleError(string $packet): void
 
         if ($this->result === null && $this->deferreds->isEmpty()) {
             // connection killed without pending query or active result
-            $this->free(new ConnectionException('Connection closed after receiving an unexpected error packet'));
+            $this->free(new SqlConnectionException('Connection closed after receiving an unexpected error packet'));
             return;
         }
 
         $deferred = $this->result ?? $this->dequeueDeferred();
 
         // normal error
-        $exception = new QueryError(\sprintf(
+        $exception = new SqlQueryError(\sprintf(
             'MySQL error (%d): %s %s',
             $this->metadata->errorCode,
             $this->metadata->errorState ?? 'Unknown state',
@@ -809,7 +809,7 @@ private function handleHandshake(string $packet): void
         }
 
         if ($protocol !== 0x0a) {
-            throw new ConnectionException("Unsupported protocol version ".\ord($packet)." (Expected: 10)");
+            throw new SqlConnectionException("Unsupported protocol version ".\ord($packet)." (Expected: 10)");
         }
 
         $this->metadata->serverVersion = MysqlDataType::decodeNullTerminatedString($packet, $offset);
@@ -880,7 +880,7 @@ private function handleLocalInfileRequest(string $packet): void
                 }
                 $this->write("");
             } catch (\Throwable $e) {
-                $this->dequeueDeferred()->error(new ConnectionException("Failed to transfer a file to the server", 0, $e));
+                $this->dequeueDeferred()->error(new SqlConnectionException("Failed to transfer a file to the server", 0, $e));
             }
         });
     }
@@ -913,7 +913,7 @@ private function handleQuery(string $packet): void
                 if ($this->config->isLocalInfileEnabled()) {
                     $this->handleLocalInfileRequest($packet);
                 } else {
-                    $this->dequeueDeferred()->error(new ConnectionException("Unexpected LOCAL_INFILE_REQUEST packet"));
+                    $this->dequeueDeferred()->error(new SqlConnectionException("Unexpected LOCAL_INFILE_REQUEST packet"));
                 }
                 return;
             case self::ERR_PACKET:
@@ -1173,7 +1173,7 @@ private function handlePrepare(string $packet): void
                 $this->handleError($packet);
                 return;
             default:
-                throw new ConnectionException("Unexpected value for first byte of COM_STMT_PREPARE Response");
+                throw new SqlConnectionException("Unexpected value for first byte of COM_STMT_PREPARE Response");
         }
 
         $offset = 1;
@@ -1232,8 +1232,8 @@ private function free(?\Throwable $exception = null): void
         $this->parser->cancel();
 
         if (!$this->deferreds->isEmpty() || $this->result) {
-            if (!$exception instanceof ConnectionException) {
-                $exception = new ConnectionException("Connection closed unexpectedly", 0, $exception ?? null);
+            if (!$exception instanceof SqlConnectionException) {
+                $exception = new SqlConnectionException("Connection closed unexpectedly", 0, $exception ?? null);
             }
 
             while (!$this->deferreds->isEmpty()) {
@@ -1433,7 +1433,7 @@ private function parsePayload(string $packet): void
                             }
                             break;
                         default:
-                            throw new ConnectionException(
+                            throw new SqlConnectionException(
                                 "Unexpected EXTRA_AUTH_PACKET in authentication phase for method {$this->authPluginName}"
                             );
                     }
@@ -1464,7 +1464,7 @@ private function parsePayload(string $packet): void
                 // no break
             default:
                 if (!$cb) {
-                    throw new ConnectionException("Unexpected packet type: " . \ord($packet));
+                    throw new SqlConnectionException("Unexpected packet type: " . \ord($packet));
                 }
 
                 $cb($packet);
@@ -1514,7 +1514,7 @@ private function authSwitchRequest(string $packet): void
                 $this->handleError($packet);
                 return;
             default:
-                throw new ConnectionException("AuthSwitchRequest: Expecting 0xfe (or ERR_Packet), got 0x".\dechex(\ord($packet)));
+                throw new SqlConnectionException("AuthSwitchRequest: Expecting 0xfe (or ERR_Packet), got 0x".\dechex(\ord($packet)));
         }
     }
 
@@ -1613,11 +1613,11 @@ private function getAuthData(): string
                 case "caching_sha2_password":
                     return self::sha2Auth($password, $this->authPluginData);
                 case "mysql_old_password":
-                    throw new ConnectionException(
+                    throw new SqlConnectionException(
                         "mysql_old_password is outdated and insecure. Intentionally not implemented!"
                     );
                 default:
-                    throw new ConnectionException(
+                    throw new SqlConnectionException(
                         "Invalid (or unimplemented?) auth method requested by server: {$this->authPluginName}"
                     );
             }
diff --git a/src/Internal/MysqlCommandResult.php b/src/Internal/MysqlCommandResult.php
index 4b05646..d8f83e6 100644
--- a/src/Internal/MysqlCommandResult.php
+++ b/src/Internal/MysqlCommandResult.php
@@ -4,14 +4,14 @@
 
 use Amp\Future;
 use Amp\Mysql\MysqlResult;
-use Amp\Sql\Common\CommandResult;
+use Amp\Sql\Common\SqlCommandResult;
 
 /**
  * @internal
  * @psalm-import-type TFieldType from MysqlResult
- * @extends CommandResult<TFieldType, MysqlResult>
+ * @extends SqlCommandResult<TFieldType, MysqlResult>
  */
-final class MysqlCommandResult extends CommandResult implements MysqlResult
+final class MysqlCommandResult extends SqlCommandResult implements MysqlResult
 {
     private ?int $lastInsertId;
 
diff --git a/src/Internal/MysqlConnectionStatement.php b/src/Internal/MysqlConnectionStatement.php
index 5967ad0..fb99f2c 100644
--- a/src/Internal/MysqlConnectionStatement.php
+++ b/src/Internal/MysqlConnectionStatement.php
@@ -7,7 +7,7 @@
 use Amp\ForbidSerialization;
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
-use Amp\Sql\ConnectionException;
+use Amp\Sql\SqlConnectionException;
 use Revolt\EventLoop;
 
 /** @internal */
@@ -62,7 +62,7 @@ private function getProcessor(): ConnectionProcessor
         }
 
         if ($this->processor->isClosed()) {
-            throw new ConnectionException("Connection went away");
+            throw new SqlConnectionException("Connection went away");
         }
 
         return $this->processor;
diff --git a/src/Internal/MysqlConnectionTransaction.php b/src/Internal/MysqlConnectionTransaction.php
index d1c3777..054dd4c 100644
--- a/src/Internal/MysqlConnectionTransaction.php
+++ b/src/Internal/MysqlConnectionTransaction.php
@@ -5,21 +5,21 @@
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
 use Amp\Mysql\MysqlTransaction;
-use Amp\Sql\Common\ConnectionTransaction;
-use Amp\Sql\Common\NestableTransactionExecutor;
-use Amp\Sql\Transaction;
+use Amp\Sql\Common\SqlConnectionTransaction;
+use Amp\Sql\Common\SqlNestableTransactionExecutor;
+use Amp\Sql\SqlTransaction;
 
 /**
  * @internal
- * @extends ConnectionTransaction<MysqlResult, MysqlStatement, MysqlTransaction, MysqlNestableExecutor>
+ * @extends SqlConnectionTransaction<MysqlResult, MysqlStatement, MysqlTransaction, MysqlNestableExecutor>
  */
-final class MysqlConnectionTransaction extends ConnectionTransaction implements MysqlTransaction
+final class MysqlConnectionTransaction extends SqlConnectionTransaction implements MysqlTransaction
 {
     use MysqlTransactionDelegate;
 
     protected function createNestedTransaction(
-        Transaction $transaction,
-        NestableTransactionExecutor $executor,
+        SqlTransaction $transaction,
+        SqlNestableTransactionExecutor $executor,
         string $identifier,
         \Closure $release,
     ): MysqlTransaction {
diff --git a/src/Internal/MysqlNestableExecutor.php b/src/Internal/MysqlNestableExecutor.php
index 904ebbb..4bbd024 100644
--- a/src/Internal/MysqlNestableExecutor.php
+++ b/src/Internal/MysqlNestableExecutor.php
@@ -7,13 +7,13 @@
 use Amp\Mysql\MysqlExecutor;
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
-use Amp\Sql\Common\NestableTransactionExecutor;
+use Amp\Sql\Common\SqlNestableTransactionExecutor;
 
 /**
  * @internal
- * @implements NestableTransactionExecutor<MysqlResult, MysqlStatement>
+ * @implements SqlNestableTransactionExecutor<MysqlResult, MysqlStatement>
  */
-final class MysqlNestableExecutor implements MysqlExecutor, NestableTransactionExecutor
+final class MysqlNestableExecutor implements MysqlExecutor, SqlNestableTransactionExecutor
 {
     use ForbidCloning;
     use ForbidSerialization;
diff --git a/src/Internal/MysqlNestedTransaction.php b/src/Internal/MysqlNestedTransaction.php
index 663c11f..9281a9b 100644
--- a/src/Internal/MysqlNestedTransaction.php
+++ b/src/Internal/MysqlNestedTransaction.php
@@ -5,15 +5,15 @@
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
 use Amp\Mysql\MysqlTransaction;
-use Amp\Sql\Common\NestableTransactionExecutor;
-use Amp\Sql\Common\NestedTransaction;
-use Amp\Sql\Transaction;
+use Amp\Sql\Common\SqlNestableTransactionExecutor;
+use Amp\Sql\Common\SqlNestedTransaction;
+use Amp\Sql\SqlTransaction;
 
 /**
  * @internal
- * @extends NestedTransaction<MysqlResult, MysqlStatement, MysqlTransaction, MysqlNestableExecutor>
+ * @extends SqlNestedTransaction<MysqlResult, MysqlStatement, MysqlTransaction, MysqlNestableExecutor>
  */
-final class MysqlNestedTransaction extends NestedTransaction implements MysqlTransaction
+final class MysqlNestedTransaction extends SqlNestedTransaction implements MysqlTransaction
 {
     use MysqlTransactionDelegate;
 
@@ -36,8 +36,8 @@ protected function getTransaction(): MysqlTransaction
     }
 
     protected function createNestedTransaction(
-        Transaction $transaction,
-        NestableTransactionExecutor $executor,
+        SqlTransaction $transaction,
+        SqlNestableTransactionExecutor $executor,
         string $identifier,
         \Closure $release,
     ): MysqlTransaction {
diff --git a/src/Internal/MysqlPooledResult.php b/src/Internal/MysqlPooledResult.php
index 2852481..16ffc4c 100644
--- a/src/Internal/MysqlPooledResult.php
+++ b/src/Internal/MysqlPooledResult.php
@@ -3,15 +3,15 @@
 namespace Amp\Mysql\Internal;
 
 use Amp\Mysql\MysqlResult;
-use Amp\Sql\Common\PooledResult;
-use Amp\Sql\Result;
+use Amp\Sql\Common\SqlPooledResult;
+use Amp\Sql\SqlResult;
 
 /**
  * @internal
  * @psalm-import-type TFieldType from MysqlResult
- * @extends PooledResult<TFieldType, MysqlResult>
+ * @extends SqlPooledResult<TFieldType, MysqlResult>
  */
-final class MysqlPooledResult extends PooledResult implements MysqlResult
+final class MysqlPooledResult extends SqlPooledResult implements MysqlResult
 {
     private readonly MysqlResult $result;
 
@@ -24,7 +24,7 @@ public function __construct(MysqlResult $result, \Closure $release)
         $this->result = $result;
     }
 
-    protected static function newInstanceFrom(Result $result, \Closure $release): self
+    protected static function newInstanceFrom(SqlResult $result, \Closure $release): self
     {
         \assert($result instanceof MysqlResult);
         return new self($result, $release);
diff --git a/src/Internal/MysqlPooledStatement.php b/src/Internal/MysqlPooledStatement.php
index 60e5fdf..ab25a17 100644
--- a/src/Internal/MysqlPooledStatement.php
+++ b/src/Internal/MysqlPooledStatement.php
@@ -4,14 +4,14 @@
 
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
-use Amp\Sql\Common\PooledStatement;
-use Amp\Sql\Result as SqlResult;
+use Amp\Sql\Common\SqlPooledStatement;
+use Amp\Sql\SqlResult;
 
 /**
  * @internal
- * @extends PooledStatement<MysqlResult, MysqlStatement>
+ * @extends SqlPooledStatement<MysqlResult, MysqlStatement>
  */
-final class MysqlPooledStatement extends PooledStatement implements MysqlStatement
+final class MysqlPooledStatement extends SqlPooledStatement implements MysqlStatement
 {
     /**
      * @param \Closure():void $release
diff --git a/src/Internal/MysqlPooledTransaction.php b/src/Internal/MysqlPooledTransaction.php
index 856574c..0e67462 100644
--- a/src/Internal/MysqlPooledTransaction.php
+++ b/src/Internal/MysqlPooledTransaction.php
@@ -5,14 +5,14 @@
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
 use Amp\Mysql\MysqlTransaction;
-use Amp\Sql\Common\PooledTransaction;
-use Amp\Sql\Transaction;
+use Amp\Sql\Common\SqlPooledTransaction;
+use Amp\Sql\SqlTransaction;
 
 /**
  * @internal
- * @extends PooledTransaction<MysqlResult, MysqlStatement, MysqlTransaction>
+ * @extends SqlPooledTransaction<MysqlResult, MysqlStatement, MysqlTransaction>
  */
-final class MysqlPooledTransaction extends PooledTransaction implements MysqlTransaction
+final class MysqlPooledTransaction extends SqlPooledTransaction implements MysqlTransaction
 {
     use MysqlTransactionDelegate;
 
@@ -24,7 +24,7 @@ public function __construct(private readonly MysqlTransaction $transaction, \Clo
         parent::__construct($transaction, $release);
     }
 
-    protected function createTransaction(Transaction $transaction, \Closure $release): Transaction
+    protected function createTransaction(SqlTransaction $transaction, \Closure $release): MysqlTransaction
     {
         \assert($transaction instanceof MysqlTransaction);
         return new MysqlPooledTransaction($transaction, $release);
diff --git a/src/Internal/MysqlStatementPool.php b/src/Internal/MysqlStatementPool.php
index d913206..d90199c 100644
--- a/src/Internal/MysqlStatementPool.php
+++ b/src/Internal/MysqlStatementPool.php
@@ -6,9 +6,9 @@
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
 use Amp\Mysql\MysqlTransaction;
-use Amp\Sql\Common\StatementPool as SqlStatementPool;
-use Amp\Sql\Result as SqlResult;
-use Amp\Sql\Statement;
+use Amp\Sql\Common\SqlStatementPool;
+use Amp\Sql\SqlResult;
+use Amp\Sql\SqlStatement;
 
 /**
  * @internal
@@ -36,7 +36,7 @@ protected function pop(): MysqlStatement
         return $statement;
     }
 
-    protected function push(Statement $statement): void
+    protected function push(SqlStatement $statement): void
     {
         if ($statement->isClosed()) {
             return;
diff --git a/src/Internal/MysqlTransactionDelegate.php b/src/Internal/MysqlTransactionDelegate.php
index 405b07b..36ee987 100644
--- a/src/Internal/MysqlTransactionDelegate.php
+++ b/src/Internal/MysqlTransactionDelegate.php
@@ -5,14 +5,14 @@
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\MysqlStatement;
 use Amp\Mysql\MysqlTransaction;
-use Amp\Sql\Result;
-use Amp\Sql\Statement;
+use Amp\Sql\SqlResult;
+use Amp\Sql\SqlStatement;
 
 /** @internal */
 trait MysqlTransactionDelegate
 {
     protected function createStatement(
-        Statement $statement,
+        SqlStatement $statement,
         \Closure $release,
         ?\Closure $awaitBusyResource = null,
     ): MysqlStatement {
@@ -20,7 +20,7 @@ protected function createStatement(
         return new MysqlPooledStatement($statement, $release, $awaitBusyResource);
     }
 
-    protected function createResult(Result $result, \Closure $release): MysqlResult
+    protected function createResult(SqlResult $result, \Closure $release): MysqlResult
     {
         \assert($result instanceof MysqlResult);
         return new MysqlPooledResult($result, $release);
diff --git a/src/MysqlConnection.php b/src/MysqlConnection.php
index af925b2..d2a03ab 100644
--- a/src/MysqlConnection.php
+++ b/src/MysqlConnection.php
@@ -2,12 +2,12 @@
 
 namespace Amp\Mysql;
 
-use Amp\Sql\Connection;
+use Amp\Sql\SqlConnection;
 
 /**
- * @extends Connection<MysqlConfig, MysqlResult, MysqlStatement, MysqlTransaction>
+ * @extends SqlConnection<MysqlConfig, MysqlResult, MysqlStatement, MysqlTransaction>
  */
-interface MysqlConnection extends MysqlLink, Connection
+interface MysqlConnection extends MysqlLink, SqlConnection
 {
     /**
      * @return MysqlConfig Config object specific to this library.
diff --git a/src/MysqlConnectionPool.php b/src/MysqlConnectionPool.php
index 3986096..0bb6795 100644
--- a/src/MysqlConnectionPool.php
+++ b/src/MysqlConnectionPool.php
@@ -2,16 +2,16 @@
 
 namespace Amp\Mysql;
 
-use Amp\Sql\Common\ConnectionPool;
-use Amp\Sql\Result;
+use Amp\Sql\Common\SqlCommonConnectionPool;
+use Amp\Sql\SqlResult;
 use Amp\Sql\SqlConnector;
-use Amp\Sql\Statement;
-use Amp\Sql\Transaction;
+use Amp\Sql\SqlStatement;
+use Amp\Sql\SqlTransaction;
 
 /**
- * @extends ConnectionPool<MysqlConfig, MysqlResult, MysqlStatement, MysqlTransaction, MysqlConnection>
+ * @extends SqlCommonConnectionPool<MysqlConfig, MysqlResult, MysqlStatement, MysqlTransaction, MysqlConnection>
  */
-final class MysqlConnectionPool extends ConnectionPool implements MysqlConnection
+final class MysqlConnectionPool extends SqlCommonConnectionPool implements MysqlConnection
 {
     /**
      * @param positive-int $maxConnections
@@ -27,13 +27,13 @@ public function __construct(
         parent::__construct($config, $connector ?? mysqlConnector(), $maxConnections, $idleTimeout);
     }
 
-    protected function createResult(Result $result, \Closure $release): MysqlResult
+    protected function createResult(SqlResult $result, \Closure $release): MysqlResult
     {
         \assert($result instanceof MysqlResult);
         return new Internal\MysqlPooledResult($result, $release);
     }
 
-    protected function createStatement(Statement $statement, \Closure $release): MysqlStatement
+    protected function createStatement(SqlStatement $statement, \Closure $release): MysqlStatement
     {
         \assert($statement instanceof MysqlStatement);
         return new Internal\MysqlPooledStatement($statement, $release);
@@ -44,7 +44,7 @@ protected function createStatementPool(string $sql, \Closure $prepare): MysqlSta
         return new Internal\MysqlStatementPool($this, $sql, $prepare);
     }
 
-    protected function createTransaction(Transaction $transaction, \Closure $release): MysqlTransaction
+    protected function createTransaction(SqlTransaction $transaction, \Closure $release): MysqlTransaction
     {
         return new Internal\MysqlPooledTransaction($transaction, $release);
     }
diff --git a/src/MysqlExecutor.php b/src/MysqlExecutor.php
index ac0c725..87908eb 100644
--- a/src/MysqlExecutor.php
+++ b/src/MysqlExecutor.php
@@ -2,12 +2,12 @@
 
 namespace Amp\Mysql;
 
-use Amp\Sql\Executor;
+use Amp\Sql\SqlExecutor;
 
 /**
- * @extends Executor<MysqlResult, MysqlStatement>
+ * @extends SqlExecutor<MysqlResult, MysqlStatement>
  */
-interface MysqlExecutor extends Executor
+interface MysqlExecutor extends SqlExecutor
 {
     /**
      * @return MysqlResult Result object specific to this library.
diff --git a/src/MysqlLink.php b/src/MysqlLink.php
index d1be61d..3706f67 100644
--- a/src/MysqlLink.php
+++ b/src/MysqlLink.php
@@ -2,12 +2,12 @@
 
 namespace Amp\Mysql;
 
-use Amp\Sql\Link;
+use Amp\Sql\SqlLink;
 
 /**
- * @extends Link<MysqlResult, MysqlStatement, MysqlTransaction>
+ * @extends SqlLink<MysqlResult, MysqlStatement, MysqlTransaction>
  */
-interface MysqlLink extends MysqlExecutor, Link
+interface MysqlLink extends MysqlExecutor, SqlLink
 {
     /**
      * @return MysqlTransaction Transaction object specific to this library.
diff --git a/src/MysqlResult.php b/src/MysqlResult.php
index eff4f2e..58d91f3 100644
--- a/src/MysqlResult.php
+++ b/src/MysqlResult.php
@@ -2,15 +2,15 @@
 
 namespace Amp\Mysql;
 
-use Amp\Sql\Result;
+use Amp\Sql\SqlResult;
 
 /**
  * @psalm-type TFieldType = int|float|string|null
  * @psalm-type TRowType = array<string, TFieldType>
  *
- * @extends Result<TFieldType>
+ * @extends SqlResult<TFieldType>
  */
-interface MysqlResult extends Result
+interface MysqlResult extends SqlResult
 {
     /**
      * Changes return type to this library's Result type.
diff --git a/src/MysqlStatement.php b/src/MysqlStatement.php
index c3ba0af..dde1d82 100644
--- a/src/MysqlStatement.php
+++ b/src/MysqlStatement.php
@@ -2,12 +2,12 @@
 
 namespace Amp\Mysql;
 
-use Amp\Sql\Statement;
+use Amp\Sql\SqlStatement;
 
 /**
- * @extends Statement<MysqlResult>
+ * @extends SqlStatement<MysqlResult>
  */
-interface MysqlStatement extends Statement
+interface MysqlStatement extends SqlStatement
 {
     public function execute(array $params = []): MysqlResult;
 
diff --git a/src/MysqlTransaction.php b/src/MysqlTransaction.php
index be14c2b..4e58d12 100644
--- a/src/MysqlTransaction.php
+++ b/src/MysqlTransaction.php
@@ -2,11 +2,11 @@
 
 namespace Amp\Mysql;
 
-use Amp\Sql\Transaction;
+use Amp\Sql\SqlTransaction;
 
 /**
- * @extends Transaction<MysqlResult, MysqlStatement, MysqlTransaction>
+ * @extends SqlTransaction<MysqlResult, MysqlStatement, MysqlTransaction>
  */
-interface MysqlTransaction extends MysqlLink, Transaction
+interface MysqlTransaction extends MysqlLink, SqlTransaction
 {
 }
diff --git a/src/SocketMysqlConnection.php b/src/SocketMysqlConnection.php
index 3b43970..131b8eb 100644
--- a/src/SocketMysqlConnection.php
+++ b/src/SocketMysqlConnection.php
@@ -9,8 +9,8 @@
 use Amp\Socket\SocketConnector;
 use Amp\Socket\SocketException;
 use Amp\Sql\SqlException;
-use Amp\Sql\TransactionIsolation;
-use Amp\Sql\TransactionIsolationLevel;
+use Amp\Sql\SqlTransactionIsolation;
+use Amp\Sql\SqlTransactionIsolationLevel;
 use Revolt\EventLoop;
 
 final class SocketMysqlConnection implements MysqlConnection
@@ -18,7 +18,7 @@ final class SocketMysqlConnection implements MysqlConnection
     use ForbidCloning;
     use ForbidSerialization;
 
-    private TransactionIsolation $transactionIsolation = TransactionIsolationLevel::Committed;
+    private SqlTransactionIsolation $transactionIsolation = SqlTransactionIsolationLevel::Committed;
 
     private ?DeferredFuture $busy = null;
 
@@ -58,12 +58,12 @@ public function getConfig(): MysqlConfig
         return $this->processor->getConfig();
     }
 
-    public function getTransactionIsolation(): TransactionIsolation
+    public function getTransactionIsolation(): SqlTransactionIsolation
     {
         return $this->transactionIsolation;
     }
 
-    public function setTransactionIsolation(TransactionIsolation $isolation): void
+    public function setTransactionIsolation(SqlTransactionIsolation $isolation): void
     {
         $this->transactionIsolation = $isolation;
     }
diff --git a/test/ConnectionTest.php b/test/MysqlConnectionTest.php
similarity index 98%
rename from test/ConnectionTest.php
rename to test/MysqlConnectionTest.php
index f5b7d49..369e358 100644
--- a/test/ConnectionTest.php
+++ b/test/MysqlConnectionTest.php
@@ -10,7 +10,7 @@
 use function Amp\delay;
 use function Amp\Mysql\connect;
 
-class ConnectionTest extends LinkTest
+class MysqlConnectionTest extends MysqlLinkTest
 {
     protected function getLink(bool $useCompression = false): MysqlLink
     {
diff --git a/test/LinkTest.php b/test/MysqlLinkTest.php
similarity index 97%
rename from test/LinkTest.php
rename to test/MysqlLinkTest.php
index e7dbcfd..a13e09e 100644
--- a/test/LinkTest.php
+++ b/test/MysqlLinkTest.php
@@ -8,12 +8,11 @@
 use Amp\Mysql\MysqlDataType;
 use Amp\Mysql\MysqlLink;
 use Amp\Mysql\MysqlResult;
-use Amp\Sql\QueryError;
-use Amp\Sql\Result;
+use Amp\Sql\SqlQueryError;
 use function Amp\async;
 use function Amp\delay;
 
-abstract class LinkTest extends MysqlTestCase
+abstract class MysqlLinkTest extends MysqlTestCase
 {
     public const EPOCH = '1970-01-01 00:00:00';
 
@@ -57,7 +56,7 @@ public function testQueryFetchRow()
 
     public function testQueryWithInvalidQuery()
     {
-        $this->expectException(QueryError::class);
+        $this->expectException(SqlQueryError::class);
         $this->expectExceptionMessage('You have an error in your SQL syntax');
 
         $db = $this->getLink();
@@ -124,13 +123,13 @@ public function testQueryWithUnconsumedTupleResult(): void
 
         $result = $db->query("SELECT a FROM main");
 
-        $this->assertInstanceOf(Result::class, $result);
+        $this->assertInstanceOf(MysqlResult::class, $result);
 
         unset($result); // Force destruction of result object.
 
         $result = $db->query("SELECT b FROM main");
 
-        $this->assertInstanceOf(Result::class, $result);
+        $this->assertInstanceOf(MysqlResult::class, $result);
     }
 
     public function testUnconsumedMultiResult(): void
@@ -265,7 +264,7 @@ public function testPrepared(): void
 
     public function testPrepareWithInvalidQuery(): void
     {
-        $this->expectException(QueryError::class);
+        $this->expectException(SqlQueryError::class);
         $this->expectExceptionMessage('You have an error in your SQL syntax');
 
         $db = $this->getLink();
@@ -337,7 +336,7 @@ public function testExecute(): void
 
     public function testExecuteWithInvalidQuery(): void
     {
-        $this->expectException(QueryError::class);
+        $this->expectException(SqlQueryError::class);
         $this->expectExceptionMessage('You have an error in your SQL syntax');
 
         $db = $this->getLink();
diff --git a/test/NestedTransactionTest.php b/test/MysqlNestedTransactionTest.php
similarity index 98%
rename from test/NestedTransactionTest.php
rename to test/MysqlNestedTransactionTest.php
index 27902d1..ffd7a2d 100644
--- a/test/NestedTransactionTest.php
+++ b/test/MysqlNestedTransactionTest.php
@@ -8,7 +8,7 @@
 use Amp\Mysql\SocketMysqlConnector;
 use function Amp\async;
 
-class NestedTransactionTest extends LinkTest
+class MysqlNestedTransactionTest extends MysqlLinkTest
 {
     private ?MysqlLink $link;
     private ?MysqlTransaction $transaction = null;
diff --git a/test/PoolTest.php b/test/MysqlPoolTest.php
similarity index 99%
rename from test/PoolTest.php
rename to test/MysqlPoolTest.php
index 86233c5..e5069e2 100644
--- a/test/PoolTest.php
+++ b/test/MysqlPoolTest.php
@@ -12,12 +12,12 @@
 use Amp\Mysql\MysqlResult;
 use Amp\Mysql\SocketMysqlConnection;
 use Amp\Sql\SqlConnector;
-use Amp\Sql\Transaction as SqlTransaction;
+use Amp\Sql\SqlTransaction;
 use PHPUnit\Framework\MockObject\MockObject;
 use function Amp\async;
 use function Amp\delay;
 
-class PoolTest extends LinkTest
+class MysqlPoolTest extends MysqlLinkTest
 {
     protected function getLink(bool $useCompression = false): MysqlLink
     {
diff --git a/test/initialize.php b/test/initialize.php
index 5f8e789..9e28a7c 100644
--- a/test/initialize.php
+++ b/test/initialize.php
@@ -8,7 +8,7 @@ function initialize(\mysqli $db): void
 
     $db->query("CREATE TABLE test.main (id INT NOT NULL AUTO_INCREMENT, a INT, b INT, c DATETIME, d VARCHAR(255), PRIMARY KEY (id))");
 
-    $epoch = LinkTest::EPOCH;
+    $epoch = MysqlLinkTest::EPOCH;
     $db->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->close();

From 4852479e491453d63e92093a6a9b54be87f105a4 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 29 Feb 2024 23:02:16 -0600
Subject: [PATCH 170/174] Use property promotion

---
 src/MysqlConfig.php | 40 ++++++++++++----------------------------
 1 file changed, 12 insertions(+), 28 deletions(-)

diff --git a/src/MysqlConfig.php b/src/MysqlConfig.php
index b72b6a8..7377d54 100644
--- a/src/MysqlConfig.php
+++ b/src/MysqlConfig.php
@@ -21,22 +21,9 @@ final class MysqlConfig extends SqlConfig
     public const DEFAULT_CHARSET = "utf8mb4";
     public const DEFAULT_COLLATE = "utf8mb4_0900_ai_ci";
 
-    private bool $useCompression;
-
-    private bool $useLocalInfile;
-
     private ConnectContext $context;
 
-    private string $charset;
-
-    private string $collate;
-
-    /* @var string private key to use for sha256_password auth method */
-    private string $key;
-
-    private ?string $sqlMode;
-
-    public static function fromString(string $connectionString, ConnectContext $context = null): self
+    public static function fromString(string $connectionString, ?ConnectContext $context = null): self
     {
         $parts = self::parseConnectionString($connectionString, self::KEY_MAP);
 
@@ -63,7 +50,7 @@ public static function fromAuthority(
         string $user,
         string $password,
         ?string $database = null,
-        ?ConnectContext $context = null
+        ?ConnectContext $context = null,
     ): self {
         [$host, $port] = \explode(':', $authority, 2) + ['', (string) self::DEFAULT_PORT];
 
@@ -77,6 +64,9 @@ public static function fromAuthority(
         );
     }
 
+    /**
+     * @param string $key Private key to use for sha256_password auth method
+     */
     public function __construct(
         string $host,
         int $port = self::DEFAULT_PORT,
@@ -84,22 +74,16 @@ public function __construct(
         ?string $password = null,
         ?string $database = null,
         ?ConnectContext $context = null,
-        string $charset = self::DEFAULT_CHARSET,
-        string $collate = self::DEFAULT_COLLATE,
-        ?string $sqlMode = null,
-        bool $useCompression = false,
-        string $key = '',
-        bool $useLocalInfile = false
+        private string $charset = self::DEFAULT_CHARSET,
+        private string $collate = self::DEFAULT_COLLATE,
+        private ?string $sqlMode = null,
+        private bool $useCompression = false,
+        private string $key = '',
+        private bool $useLocalInfile = false,
     ) {
         parent::__construct($host, $port, $user, $password, $database);
 
-        $this->context = $context ?? (new ConnectContext);
-        $this->charset = $charset;
-        $this->collate = $collate;
-        $this->sqlMode = $sqlMode;
-        $this->useCompression = $useCompression;
-        $this->key = $key;
-        $this->useLocalInfile = $useLocalInfile;
+        $this->context = $context ?? new ConnectContext();
     }
 
     public function getConnectionString(): string

From 2df3f8808518c7e5f4a5d8212c7e31ba4c4f3a06 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 29 Feb 2024 23:06:09 -0600
Subject: [PATCH 171/174] Note thrown exception

---
 src/MysqlDataType.php | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/MysqlDataType.php b/src/MysqlDataType.php
index 6483c15..1c970b3 100644
--- a/src/MysqlDataType.php
+++ b/src/MysqlDataType.php
@@ -43,6 +43,8 @@ enum MysqlDataType: int
      * @see 14.7.3 Binary Protocol Value
      *
      * @param int<0, max> $offset
+     *
+     * @throws SqlException
      */
     public function decodeBinary(string $bytes, int &$offset = 0, int $flags = 0): int|float|string|null
     {
@@ -117,6 +119,8 @@ public function decodeBinary(string $bytes, int &$offset = 0, int $flags = 0): i
 
     /**
      * @param int<0, max> $offset
+     *
+     * @throws SqlException
      */
     public function decodeText(string $bytes, int &$offset = 0, int $flags = 0): int|float|string
     {
@@ -262,6 +266,8 @@ public static function decodeNullTerminatedString(string $bytes, int &$offset =
 
     /**
      * @param int<0, max> $offset
+     *
+     * @throws SqlException
      */
     public static function decodeString(string $bytes, int &$offset = 0): string
     {

From ec35424fb41399420ac9b922b289a5ffc4bf138d Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Thu, 29 Feb 2024 23:08:14 -0600
Subject: [PATCH 172/174] Move MysqlEncodedValue to be internal

---
 src/Internal/ConnectionProcessor.php     | 3 +--
 src/{ => Internal}/MysqlEncodedValue.php | 5 ++++-
 src/MysqlConnectionPool.php              | 2 +-
 3 files changed, 6 insertions(+), 4 deletions(-)
 rename src/{ => Internal}/MysqlEncodedValue.php (96%)

diff --git a/src/Internal/ConnectionProcessor.php b/src/Internal/ConnectionProcessor.php
index dc7ed76..2617f81 100644
--- a/src/Internal/ConnectionProcessor.php
+++ b/src/Internal/ConnectionProcessor.php
@@ -12,13 +12,12 @@
 use Amp\Mysql\MysqlColumnDefinition;
 use Amp\Mysql\MysqlConfig;
 use Amp\Mysql\MysqlDataType;
-use Amp\Mysql\MysqlEncodedValue;
 use Amp\Mysql\MysqlResult;
 use Amp\Parser\Parser;
 use Amp\Socket\Socket;
 use Amp\Sql\SqlConnectionException;
-use Amp\Sql\SqlQueryError;
 use Amp\Sql\SqlException;
+use Amp\Sql\SqlQueryError;
 use Amp\Sql\SqlTransientResource;
 use Revolt\EventLoop;
 
diff --git a/src/MysqlEncodedValue.php b/src/Internal/MysqlEncodedValue.php
similarity index 96%
rename from src/MysqlEncodedValue.php
rename to src/Internal/MysqlEncodedValue.php
index f8d5391..b753276 100644
--- a/src/MysqlEncodedValue.php
+++ b/src/Internal/MysqlEncodedValue.php
@@ -1,7 +1,10 @@
 <?php declare(strict_types=1);
 
-namespace Amp\Mysql;
+namespace Amp\Mysql\Internal;
 
+use Amp\Mysql\MysqlDataType;
+
+/** @internal */
 final class MysqlEncodedValue
 {
     public static function fromValue(mixed $param): self
diff --git a/src/MysqlConnectionPool.php b/src/MysqlConnectionPool.php
index 0bb6795..b5e2b5e 100644
--- a/src/MysqlConnectionPool.php
+++ b/src/MysqlConnectionPool.php
@@ -3,8 +3,8 @@
 namespace Amp\Mysql;
 
 use Amp\Sql\Common\SqlCommonConnectionPool;
-use Amp\Sql\SqlResult;
 use Amp\Sql\SqlConnector;
+use Amp\Sql\SqlResult;
 use Amp\Sql\SqlStatement;
 use Amp\Sql\SqlTransaction;
 

From 45a3c91db8d2e906fb4983aa9ebb4b38b32642e3 Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 10 Mar 2024 12:26:09 -0500
Subject: [PATCH 173/174] Update deps

---
 composer.json | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/composer.json b/composer.json
index ac92b36..561688c 100644
--- a/composer.json
+++ b/composer.json
@@ -17,9 +17,9 @@
         "amphp/amp": "^3",
         "amphp/parser": "^1.1",
         "amphp/pipeline": "^1",
-        "amphp/socket": "^2",
-        "amphp/sql": "2.x-dev",
-        "amphp/sql-common": "2.x-dev"
+        "amphp/socket": "^2.2",
+        "amphp/sql": "^2",
+        "amphp/sql-common": "^2"
     },
     "require-dev": {
         "ext-mysqli": "*",
@@ -30,10 +30,8 @@
         "amphp/phpunit-util": "^3",
         "amphp/php-cs-fixer-config": "^2",
         "phpbench/phpbench": "^1.2.6",
-        "psalm/phar": "^5.18"
+        "psalm/phar": "5.23"
     },
-    "minimum-stability": "dev",
-    "prefer-stable": true,
     "autoload": {
         "psr-4": {
             "Amp\\Mysql\\": "src"

From 0eb9d1df67c206c043b1a1c6ad7ba1bc2aa836bf Mon Sep 17 00:00:00 2001
From: Aaron Piotrowski <aaron@trowski.com>
Date: Sun, 10 Mar 2024 12:33:58 -0500
Subject: [PATCH 174/174] Update readme

---
 README.md | 35 ++++++++++++++++++++++-------------
 1 file changed, 22 insertions(+), 13 deletions(-)

diff --git a/README.md b/README.md
index 0995fd6..3bcc560 100644
--- a/README.md
+++ b/README.md
@@ -2,15 +2,17 @@
 
 AMPHP is a collection of event-driven libraries for PHP designed with fibers and concurrency in mind.
 `amphp/mysql` is an asynchronous MySQL client.
-The library allows to dynamically query a server with multiple MySQL connections concurrently.
-The client transparently distributes these queries across a scalable pool of available connections and does so using 100% userland PHP; there are *no external extension dependencies* (e.g. `ext/mysqli`, `ext/pdo`, etc).
+The library implements concurrent querying by transparently distributing queries across a scalable pool of available connections. The client transparently distributes these queries across a scalable pool of available connections and does so using 100% userland PHP; there are *no external extension dependencies* (e.g. `ext/mysqli`, `ext/pdo`, etc.).
 
 ## Features
 
- - Exposes a non-blocking API for issuing multiple MySQL queries concurrently
- - Transparent connection pooling to overcome MySQL's fundamentally synchronous connection protocol
- - MySQL transfer encoding support (gzip, TLS encryption)
- - *Full* MySQL protocol support including *all*<sup>†</sup> available commands asynchronously
+- Exposes a non-blocking API for issuing multiple MySQL queries concurrently
+- Transparent connection pooling to overcome MySQL's fundamentally synchronous connection protocol
+- MySQL transfer encoding support (gzip, TLS encryption)
+- Support for parameterized prepared statements
+- Nested transactions with commit and rollback event hooks
+- Unbuffered results to reduce memory usage for large result sets
+- *Full* MySQL protocol support including *all*<sup>†</sup> available commands asynchronously
 
 <sup>† As documented in [official Mysql Internals Manual](https://dev.mysql.com/doc/internals/en/client-server-protocol.html)</sup>
 
@@ -22,22 +24,29 @@ This package can be installed as a [Composer](https://getcomposer.org/) dependen
 composer require amphp/mysql
 ```
 
-This package requires PHP 8.1 or later.
+## Requirements
+
+- PHP 8.1+
 
 ## Usage
 
 More extensive code examples reside in the [`examples`](examples) directory.
 
 ```php
-$config = Amp\Mysql\MysqlConfig::fromString(
-    "host=127.0.0.1 user=username password=password db=test"
+use Amp\Mysql\MysqlConfig;
+use Amp\Mysql\MysqlConnectionPool;
+
+$config = MysqlConfig::fromString(
+    "host=localhost user=username password=password db=test"
 );
 
-$pool = new Amp\Mysql\MysqlConnectionPool($config);
+$pool = new MysqlConnectionPool($config);
 
 $statement = $pool->prepare("SELECT * FROM table_name WHERE id = :id");
-foreach ($statement->execute(['id' => 1337]) as $row) {
-    // $row is an associative array of column values. e.g.: $row['column_name']
+
+$result = $statement->execute(['id' => 1337]);
+foreach ($result as $row) {
+    // $row is an associative-array of column values, e.g.: $row['column_name']
 }
 ```
 
@@ -47,7 +56,7 @@ foreach ($statement->execute(['id' => 1337]) as $row) {
 
 ## Security
 
-If you discover any security related issues, please email [`contact@amphp.org`](mailto:contact@amphp.org) instead of using the issue tracker.
+If you discover any security related issues, please use the private security issue reporter instead of using the public issue tracker.
 
 ## License
 
