Full Code of friends-of-reactphp/mysql for AI

0.7.x 6ceb2358c590 cached
42 files
332.4 KB
82.5k tokens
383 symbols
1 requests
Download .txt
Showing preview only (348K chars total). Download the full file or copy to clipboard to get everything.
Repository: friends-of-reactphp/mysql
Branch: 0.7.x
Commit: 6ceb2358c590
Files: 42
Total size: 332.4 KB

Directory structure:
gitextract_ldg5u_7o/

├── .gitattributes
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── examples/
│   ├── 01-query.php
│   ├── 02-query-stream.php
│   ├── 11-interactive.php
│   └── 12-slow-stream.php
├── phpunit.xml.dist
├── phpunit.xml.legacy
├── src/
│   ├── Commands/
│   │   ├── AbstractCommand.php
│   │   ├── AuthenticateCommand.php
│   │   ├── CommandInterface.php
│   │   ├── PingCommand.php
│   │   ├── QueryCommand.php
│   │   └── QuitCommand.php
│   ├── Exception.php
│   ├── Io/
│   │   ├── Buffer.php
│   │   ├── Connection.php
│   │   ├── Constants.php
│   │   ├── Executor.php
│   │   ├── Factory.php
│   │   ├── Parser.php
│   │   ├── Query.php
│   │   └── QueryStream.php
│   ├── MysqlClient.php
│   └── MysqlResult.php
└── tests/
    ├── BaseTestCase.php
    ├── Commands/
    │   └── AuthenticateCommandTest.php
    ├── Io/
    │   ├── BufferTest.php
    │   ├── ConnectionTest.php
    │   ├── FactoryTest.php
    │   ├── ParserTest.php
    │   ├── QueryStreamTest.php
    │   └── QueryTest.php
    ├── MysqlClientTest.php
    ├── NoResultQueryTest.php
    ├── ResultQueryTest.php
    └── wait-for-mysql.sh

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
/.gitattributes export-ignore
/.github/ export-ignore
/.gitignore export-ignore
/examples/ export-ignore
/phpunit.xml.dist export-ignore
/phpunit.xml.legacy export-ignore
/tests/ export-ignore


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
  pull_request:

jobs:
  PHPUnit:
    name: PHPUnit (PHP ${{ matrix.php }} + ${{ matrix.rdbms }})
    runs-on: ubuntu-24.04
    strategy:
      matrix:
        rdbms:
          - mysql:5
        php:
          - 8.4
          - 8.3
          - 8.2
          - 8.1
          - 8.0
          - 7.4
          - 7.3
          - 7.2
          - 7.1
          - 7.0
          - 5.6
          - 5.5
          - 5.4
        include:
          - php: 8.4
            rdbms: mysql:9
          - php: 8.4
            rdbms: mysql:8
          - php: 8.4
            rdbms: mariadb:10
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          coverage: ${{ matrix.php < 8.0 && 'xdebug' || 'pcov' }}
          ini-file: development
      - run: composer install
      - run: docker run -d --name mysql --net=host -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=test -e MYSQL_USER=test -e MYSQL_PASSWORD=test ${{ matrix.rdbms }}
      - run: bash tests/wait-for-mysql.sh
      - run: vendor/bin/phpunit --coverage-text ${{ matrix.php < 7.3 && '-c phpunit.xml.legacy' || '' }}

  PHPUnit-hhvm:
    name: PHPUnit (HHVM)
    runs-on: ubuntu-24.04
    continue-on-error: true
    steps:
      - uses: actions/checkout@v4
      - run: cp "$(which composer)" composer.phar && ./composer.phar self-update --2.2 # downgrade Composer for HHVM
      - name: Run hhvm composer.phar install
        uses: docker://hhvm/hhvm:3.30-lts-latest
        with:
          args: hhvm composer.phar install
      - run: docker run -d --name mysql --net=host -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=test -e MYSQL_USER=test -e MYSQL_PASSWORD=test mysql:5
      - run: bash tests/wait-for-mysql.sh
      - run: docker run -i --rm --workdir=/data -v "$(pwd):/data" --net=host hhvm/hhvm:3.30-lts-latest hhvm vendor/bin/phpunit


================================================
FILE: .gitignore
================================================
/composer.lock
/vendor/


================================================
FILE: CHANGELOG.md
================================================
# Changelog

## 0.6.0 (2023-11-10)

*   Feature: Improve Promise v3 support and use template types.
    (#183 and #178 by @clue)

*   Feature: Full PHP 8.3 compatibility.
    (#180 by @clue)

*   Feature / BC break: Update default charset encoding to `utf8mb4` for full UTF-8 support.
    (#165 by @clue)

    This feature updates the MySQL client to use `utf8mb4` as the default charset
    encoding for full UTF-8 support instead of the legacy `utf8mb3` charset encoding.
    For legacy reasons you can still change this to use a different ASCII-compatible
    charset encoding like this:

    ```php
    $factory->createConnection('localhost?charset=utf8mb4');
    ```

*   Feature: Reduce default idle time to 1ms.
    (#182 by @clue)

    The idle time defines the time the client is willing to keep the underlying
    connection alive before automatically closing it. The default idle time was
    previously 60s and can be configured for more specific requirements like this:

    ```php
    $factory->createConnection('localhost?idle=10.0');
    ```

*   Minor documentation improvements.
    (#184 by @yadaiio)

*   Improve test suite, update to use reactphp/async and report failed assertions.
    (#164 and #170 by @clue, #163 by @dinooo13 and #181 by @SimonFrings)

## 0.5.7 (2022-09-15)

*   Feature: Full support for PHP 8.2.
    (#161 by @clue)

*   Feature: Mark passwords and URIs as `#[\SensitiveParameter]` (PHP 8.2+).
    (#162 by @clue)

*   Feature: Forward compatibility with upcoming Promise v3.
    (#157 by @clue)

*   Feature / Fix: Improve protocol parser, emit parser errors and close invalid connections.
    (#158 and #159 by @clue)

*   Improve test suite, fix legacy HHVM build by downgrading Composer.
    (#160 by @clue)

## 0.5.6 (2021-12-14)

*   Feature: Support optional `charset` parameter for full UTF-8 support (`utf8mb4`).
    (#135 by @clue)

    ```php
    $db = $factory->createLazyConnection('localhost?charset=utf8mb4');
    ```

*   Feature: Improve error reporting, include MySQL URI and socket error codes in all connection errors.
    (#141 by @clue and #138 by @SimonFrings)

    For most common use cases this means that simply reporting the `Exception`
    message should give the most relevant details for any connection issues:

    ```php
    $db->query($sql)->then(function (React\MySQL\QueryResult $result) {
        // …
    }, function (Exception $e) {
        echo 'Error:' . $e->getMessage() . PHP_EOL;
    });
    ```

*   Feature: Full support for PHP 8.1 release.
    (#150 by @clue)

*   Feature: Provide limited support for `NO_BACKSLASH_ESCAPES` SQL mode.
    (#139 by @clue)

*   Update project dependencies, simplify socket usage, and improve documentation.
    (#136 and #137 by @SimonFrings)

*   Improve test suite and add `.gitattributes` to exclude dev files from exports.
    Run tests on PHPUnit 9 and PHP 8 and clean up test suite.
    (#142 and #143 by @SimonFrings)

## 0.5.5 (2021-07-19)

*   Feature: Simplify usage by supporting new default loop.
    (#134 by @clue)

    ```php
    // old (still supported)
    $factory = new React\MySQL\Factory($loop);

    // new (using default loop)
    $factory = new React\MySQL\Factory();
    ```

*   Improve test setup, use GitHub actions for continuous integration (CI) and fix minor typo.
    (#132 by @SimonFrings and #129 by @mmoreram)

## 0.5.4 (2019-05-21)

*   Fix: Do not start idle timer when lazy connection is already closed.
    (#110 by @clue)

*   Fix: Fix explicit `close()` on lazy connection when connection is active.
    (#109 by @clue)

## 0.5.3 (2019-04-03)

*   Fix: Ignore unsolicited server error when not executing any commands.
    (#102 by @clue)

*   Fix: Fix decoding URL-encoded special characters in credentials from database connection URI.
    (#98 and #101 by @clue)

## 0.5.2 (2019-02-05)

*   Fix: Fix `ConnectionInterface` return type hint in `Factory`.
    (#93 by @clue)

*   Minor documentation typo fix and improve test suite to test against PHP 7.3,
    add forward compatibility with PHPUnit 7 and use legacy PHPUnit 5 on HHVM.
    (#92 and #94 by @clue)

## 0.5.1 (2019-01-12)

*   Fix: Fix "bad handshake" error when connecting without database name.
    (#91 by @clue)

## 0.5.0 (2018-11-28)

A major feature release with a significant API improvement!

This update does not involve any BC breaks, but we figured the new API provides
significant features that warrant a major version bump. Existing code will
continue to work without changes, but you're highly recommended to consider
using the new lazy connections as detailed below.

*   Feature: Add new `createLazyConnection()` method to only connect on demand and
    implement "idle" timeout to close underlying connection when unused.
    (#87 and #88 by @clue)

    ```php
    // new
    $connection = $factory->createLazyConnection($url);
    $connection->query(…);
    ```

    This method immediately returns a "virtual" connection implementing the
    [`ConnectionInterface`](README.md#connectioninterface) that can be used to
    interface with your MySQL database. Internally, it lazily creates the
    underlying database connection only on demand once the first request is
    invoked on this instance and will queue all outstanding requests until
    the underlying connection is ready. Additionally, it will only keep this
    underlying connection in an "idle" state for 60s by default and will
    automatically end the underlying connection when it is no longer needed.

    From a consumer side this means that you can start sending queries to the
    database right away while the underlying connection may still be
    outstanding. Because creating this underlying connection may take some
    time, it will enqueue all outstanding commands and will ensure that all
    commands will be executed in correct order once the connection is ready.
    In other words, this "virtual" connection behaves just like a "real"
    connection as described in the `ConnectionInterface` and frees you from
    having to deal with its async resolution.

*   Feature: Support connection timeouts.
    (#86 by @clue)

## 0.4.1 (2018-10-18)

*   Feature: Support cancellation of pending connection attempts.
    (#84 by @clue)

*   Feature: Add `warningCount` to `QueryResult`.
    (#82 by @legionth)

*   Feature: Add exception message for invalid MySQL URI.
    (#80 by @CharlotteDunois)

*   Fix: Fix parsing error message during handshake (Too many connections).
    (#83 by @clue)

## 0.4.0 (2018-09-21)

A major feature release with a significant documentation overhaul and long overdue API cleanup!

This update involves a number of BC breaks due to various changes to make the
API more consistent with the ReactPHP ecosystem. In particular, this now uses
promises consistently as return values instead of accepting callback functions
and this now offers an additional streaming API for processing very large result
sets efficiently.

We realize that the changes listed below may seem a bit overwhelming, but we've
tried to be very clear about any possible BC breaks. See below for changes you
have to take care of when updating from an older version.

*   Feature / BC break: Add Factory to simplify connecting and keeping connection state,
    mark `Connection` class as internal and remove `connect()` method.
    (#64 by @clue)

    ```php
    // old
    $connection = new Connection($loop, $options);
    $connection->connect(function (?Exception $error, $connection) {
        if ($error) {
            // an error occurred while trying to connect or authorize client
        } else {
            // client connection established (and authenticated)
        }
    });

    // new
    $factory = new Factory($loop);
    $factory->createConnection($url)->then(
        function (ConnectionInterface $connection) {
            // client connection established (and authenticated)
        },
        function (Exception $e) {
            // an error occurred while trying to connect or authorize client
        }
    );
    ```

*   Feature / BC break: Use promises for `query()` method and resolve with `QueryResult` on success and
    and mark all commands as internal and move its base to Commands namespace.
    (#61 and #62 by @clue)

    ```php
    // old
    $connection->query('CREATE TABLE test');
    $connection->query('DELETE FROM user WHERE id < ?', $id);
    $connection->query('SELECT * FROM user', function (QueryCommand $command) {
        if ($command->hasError()) {
            echo 'Error: ' . $command->getError()->getMessage() . PHP_EOL;
        } elseif (isset($command->resultRows)) {
            var_dump($command->resultRows);
        }
    });

    // new
    $connection->query('CREATE TABLE test');
    $connection->query('DELETE FROM user WHERE id < ?', [$id]);
    $connection->query('SELECT * FROM user')->then(function (QueryResult $result) {
        var_dump($result->resultRows);
    }, function (Exception $error) {
        echo 'Error: ' . $error->getMessage() . PHP_EOL;
    });
    ```

*   Feature / BC break: Add new `queryStream()` method to stream result set rows and
    remove undocumented "results" event.
    (#57 and #77 by @clue)

    ```php
    $stream = $connection->queryStream('SELECT * FROM users');

    $stream->on('data', function ($row) {
        var_dump($row);
    });
    $stream->on('end', function () {
        echo 'DONE' . PHP_EOL;
    });
    ```

*   Feature / BC break: Rename `close()` to `quit()`, use promises for `quit()` method and
    add new `close()` method to force-close the connection.
    (#65 and #76 by @clue)

    ```php
    // old: soft-close/quit
    $connection->close(function () {
        echo 'closed';
    });

    // new: soft-close/quit
    $connection->quit()->then(function () {
        echo 'closed';
    });

    // new: force-close
    $connection->close();
    ```

*   Feature / BC break: Use promises for `ping()` method and resolve with void value on success.
    (#63 and #66 by @clue)

    ```php
    // old
    $connection->ping(function ($error, $connection) {
        if ($error) {
            echo 'Error: ' . $error->getMessage() . PHP_EOL;
        } else {
            echo 'OK' . PHP_EOL;
        }
    });

    // new 
    $connection->ping(function () {
        echo 'OK' . PHP_EOL;
    }, function (Exception $error) {
        echo 'Error: ' . $error->getMessage() . PHP_EOL;
    });
    ```

*   Feature / BC break: Define events on ConnectionInterface
    (#78 by @clue)

*   BC break: Remove unneeded `ConnectionInterface` methods `getState()`,
    `getOptions()`, `setOptions()` and `getServerOptions()`, `selectDb()` and `listFields()` dummy.
    (#60 and #68 by @clue)

*   BC break: Mark all protocol logic classes as internal and move to new Io namespace.
    (#53 and #62 by @clue)

*   Fix: Fix executing queued commands in the order they are enqueued
    (#75 by @clue)

*   Fix: Fix reading all incoming response packets until end
    (#59 by @clue)

*   [maintenance] Internal refactoring to simplify connection and authentication logic
    (#69 by @clue)
*   [maintenance] Internal refactoring to remove unneeded references from Commands
    (#67 by @clue)
*   [maintenance] Internal refactoring to remove unneeded EventEmitter implementation and circular references
    (#56 by @clue)
*   [maintenance] Refactor internal parsing logic to separate Buffer class, remove dead code and improve performance
    (#54 by @clue)

## 0.3.3 (2018-06-18)

*   Fix: Reject pending commands if connection is closed
    (#52 by @clue)

*   Fix: Do not support multiple statements for security and API reasons
    (#51 by @clue)

*   Fix: Fix reading empty rows containing only empty string columns 
    (#46 by @clue)

*   Fix: Report correct field length for fields longer than 16k chars
    (#42 by @clue)

*   Add quickstart example and interactive CLI example
    (#45 by @clue)

## 0.3.2 (2018-04-04)

*   Fix: Fix parameter binding if query contains question marks
    (#40 by @clue)

*   Improve test suite by simplifying test structure, improve test isolation and remove dbunit
    (#39 by @clue)

## 0.3.1 (2018-03-26)

*   Feature: Forward compatibility with upcoming ReactPHP components
    (#37 by @clue)

*   Fix: Consistent `connect()` behavior for all connection states
    (#36 by @clue)

*   Fix: Report connection error to `connect()` callback
    (#35 by @clue)

## 0.3.0 (2018-03-13)

*   This is now a community project managed by @friends-of-reactphp. Thanks to
    @bixuehujin for releasing this project under MIT license and handing over!
    (#12 and #33 by @bixuehujin and @clue)

*   Feature / BC break: Update react/socket to v0.8.0
    (#21 by @Fneufneu)

*   Feature: Support passing custom connector and
    load system default DNS config by default
    (#24 by @flow-control and #30 by @clue)

*   Feature: Add `ConnectionInterface` with documentation
    (#26 by @freedemster)

*   Fix: Last query param is lost if no callback is given
    (#22 by @Fneufneu)

*   Fix: Fix memory increase (memory leak due to keeping incoming receive buffer)
    (#17 by @sukui)

*   Improve test suite by adding test instructions and adding Travis CI
    (#34 by @clue and #25 by @freedemster)

*   Improve documentation
    (#8 by @ovr and #10 by @RafaelKa)

## 0.2.0 (2014-10-15)

*   Now compatible with ReactPHP v0.4

## 0.1.0 (2014-02-18)

*   First tagged release (ReactPHP v0.3)


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016 Jin Hu

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
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.


================================================
FILE: README.md
================================================
# MySQL

[![CI status](https://github.com/friends-of-reactphp/mysql/actions/workflows/ci.yml/badge.svg)](https://github.com/friends-of-reactphp/mysql/actions)

Async MySQL database client for [ReactPHP](https://reactphp.org/).

> **Development version:** This branch contains the code for the upcoming
> version 0.7 release. For the code of the current stable version 0.6 release, check
> out the [`0.6.x` branch](https://github.com/friends-of-reactphp/mysql/tree/0.6.x).
>
> The upcoming version 0.7 release will be the way forward for this package.
> However, we will still actively support version 0.6 for those not yet on the
> latest version.
> See also [installation instructions](#install) for more details.

This is a MySQL database driver for [ReactPHP](https://reactphp.org/).
It implements the MySQL protocol and allows you to access your existing MySQL
database.
It is written in pure PHP and does not require any extensions.

**Table of contents**

* [Quickstart example](#quickstart-example)
* [Usage](#usage)
  * [MysqlClient](#mysqlclient)
    * [__construct()](#__construct)
    * [query()](#query)
    * [queryStream()](#querystream)
    * [ping()](#ping)
    * [quit()](#quit)
    * [close()](#close)
    * [error event](#error-event)
    * [close event](#close-event)
* [Install](#install)
* [Tests](#tests)
* [License](#license)

## Quickstart example

This example runs a simple `SELECT` query and dumps all the records from a `book` table:

```php
<?php

require __DIR__ . '/vendor/autoload.php';

$mysql = new React\Mysql\MysqlClient('user:pass@localhost/bookstore');

$mysql->query('SELECT * FROM book')->then(
    function (React\Mysql\MysqlResult $command) {
        print_r($command->resultFields);
        print_r($command->resultRows);
        echo count($command->resultRows) . ' row(s) in set' . PHP_EOL;
    },
    function (Exception $error) {
        echo 'Error: ' . $error->getMessage() . PHP_EOL;
    }
);
```

See also the [examples](examples).

## Usage

### MysqlClient

The `MysqlClient` is responsible for exchanging messages with your MySQL server
and keeps track of pending queries.

```php
$mysql = new React\Mysql\MysqlClient($uri);

$mysql->query(…);
```

This class represents a connection that is responsible for communicating
with your MySQL server instance, managing the connection state and sending
your database queries. Internally, it creates the underlying database
connection only on demand once the first request is invoked on this
instance and will queue all outstanding requests until the underlying
connection is ready. This underlying connection will be reused for all
requests until it is closed. By default, idle connections will be held
open for 1ms (0.001s) when not used. The next request will either reuse
the existing connection or will automatically create a new underlying
connection if this idle time is expired.

From a consumer side this means that you can start sending queries to the
database right away while the underlying connection may still be
outstanding. Because creating this underlying connection may take some
time, it will enqueue all outstanding commands and will ensure that all
commands will be executed in correct order once the connection is ready.

If the underlying database connection fails, it will reject all
outstanding commands and will return to the initial "idle" state. This
means that you can keep sending additional commands at a later time which
will again try to open a new underlying connection. Note that this may
require special care if you're using transactions that are kept open for
longer than the idle period.

Note that creating the underlying connection will be deferred until the
first request is invoked. Accordingly, any eventual connection issues
will be detected once this instance is first used. You can use the
`quit()` method to ensure that the connection will be soft-closed
and no further commands can be enqueued. Similarly, calling `quit()` on
this instance when not currently connected will succeed immediately and
will not have to wait for an actual underlying connection.

#### __construct()

The `new MysqlClient(string $uri, ?ConnectorInterface $connector = null, ?LoopInterface $loop = null)` constructor can be used to
create a new `MysqlClient` instance.

The `$uri` parameter must contain the database host, optional
authentication, port and database to connect to:

```php
$mysql = new React\Mysql\MysqlClient('user:secret@localhost:3306/database');
```

Note that both the username and password must be URL-encoded (percent-encoded)
if they contain special characters:

```php
$user = 'he:llo';
$pass = 'p@ss';

$mysql = new React\Mysql\MysqlClient(
    rawurlencode($user) . ':' . rawurlencode($pass) . '@localhost:3306/db'
);
```

You can omit the port if you're connecting to default port `3306`:

```php
$mysql = new React\Mysql\MysqlClient('user:secret@localhost/database');
```

If you do not include authentication and/or database, then this method
will default to trying to connect as user `root` with an empty password
and no database selected. This may be useful when initially setting up a
database, but likely to yield an authentication error in a production system:

```php
$mysql = new React\Mysql\MysqlClient('localhost');
```

This method respects PHP's `default_socket_timeout` setting (default 60s)
as a timeout for establishing the underlying connection and waiting for
successful authentication. You can explicitly pass a custom timeout value
in seconds (or use a negative number to not apply a timeout) like this:

```php
$mysql = new React\Mysql\MysqlClient('localhost?timeout=0.5');
```

By default, idle connections will be held open for 1ms (0.001s) when not
used. The next request will either reuse the existing connection or will
automatically create a new underlying connection if this idle time is
expired. This ensures you always get a "fresh" connection and as such
should not be confused with a "keepalive" or "heartbeat" mechanism, as
this will not actively try to probe the connection. You can explicitly
pass a custom idle timeout value in seconds (or use a negative number to
not apply a timeout) like this:

```php
$mysql = new React\Mysql\MysqlClient('localhost?idle=10.0');
```

By default, the connection provides full UTF-8 support (using the
`utf8mb4` charset encoding). This should usually not be changed for most
applications nowadays, but for legacy reasons you can change this to use
a different ASCII-compatible charset encoding like this:

```php
$mysql = new React\Mysql\MysqlClient('localhost?charset=utf8mb4');
```

If you need custom connector settings (DNS resolution, TLS parameters, timeouts,
proxy servers etc.), you can explicitly pass a custom instance of the
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface):

```php
$connector = new React\Socket\Connector([
    'dns' => '127.0.0.1',
    'tcp' => [
        'bindto' => '192.168.10.1:0'
    ],
    'tls' => [
        'verify_peer' => false,
        'verify_peer_name' => false
    )
]);

$mysql = new React\Mysql\MysqlClient('user:secret@localhost:3306/database', $connector);
```

This class takes an optional `LoopInterface|null $loop` parameter that can be used to
pass the event loop instance to use for this object. You can use a `null` value
here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
This value SHOULD NOT be given unless you're sure you want to explicitly use a
given event loop instance.

#### query()

The `query(string $query, list<string|int|float|bool|null> $params = []): PromiseInterface<MysqlResult>` method can be used to
perform an async query.

This method returns a promise that will resolve with a `MysqlResult` on
success or will reject with an `Exception` on error. The MySQL protocol
is inherently sequential, so that all queries will be performed in order
and outstanding queries will be put into a queue to be executed once the
previous queries are completed.

```php
$mysql->query('CREATE TABLE test ...');
$mysql->query('INSERT INTO test (id) VALUES (1)');
```

If this SQL statement returns a result set (such as from a `SELECT`
statement), this method will buffer everything in memory until the result
set is completed and will then resolve the resulting promise. This is
the preferred method if you know your result set to not exceed a few
dozens or hundreds of rows. If the size of your result set is either
unknown or known to be too large to fit into memory, you should use the
[`queryStream()`](#querystream) method instead.

```php
$mysql->query($query)->then(function (React\Mysql\MysqlResult $command) {
    if (isset($command->resultRows)) {
        // this is a response to a SELECT etc. with some rows (0+)
        print_r($command->resultFields);
        print_r($command->resultRows);
        echo count($command->resultRows) . ' row(s) in set' . PHP_EOL;
    } else {
        // this is an OK message in response to an UPDATE etc.
        if ($command->insertId !== 0) {
            var_dump('last insert ID', $command->insertId);
        }
        echo 'Query OK, ' . $command->affectedRows . ' row(s) affected' . PHP_EOL;
    }
}, function (Exception $error) {
    // the query was not executed successfully
    echo 'Error: ' . $error->getMessage() . PHP_EOL;
});
```

You can optionally pass an array of `$params` that will be bound to the
query like this:

```php
$mysql->query('SELECT * FROM user WHERE id > ?', [$id]);
```

The given `$sql` parameter MUST contain a single statement. Support
for multiple statements is disabled for security reasons because it
could allow for possible SQL injection attacks and this API is not
suited for exposing multiple possible results.

#### queryStream()

The `queryStream(string $sql, list<string|int|float|bool|null> $params = []): ReadableStreamInterface` method can be used to
perform an async query and stream the rows of the result set.

This method returns a readable stream that will emit each row of the
result set as a `data` event. It will only buffer data to complete a
single row in memory and will not store the whole result set. This allows
you to process result sets of unlimited size that would not otherwise fit
into memory. If you know your result set to not exceed a few dozens or
hundreds of rows, you may want to use the [`query()`](#query) method instead.

```php
$stream = $mysql->queryStream('SELECT * FROM user');
$stream->on('data', function ($row) {
    echo $row['name'] . PHP_EOL;
});
$stream->on('end', function () {
    echo 'Completed.';
});
```

You can optionally pass an array of `$params` that will be bound to the
query like this:

```php
$stream = $mysql->queryStream('SELECT * FROM user WHERE id > ?', [$id]);
```

This method is specifically designed for queries that return a result set
(such as from a `SELECT` or `EXPLAIN` statement). Queries that do not
return a result set (such as a `UPDATE` or `INSERT` statement) will not
emit any `data` events.

See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface)
for more details about how readable streams can be used in ReactPHP. For
example, you can also use its `pipe()` method to forward the result set
rows to a [`WritableStreamInterface`](https://github.com/reactphp/stream#writablestreaminterface)
like this:

```php
$mysql->queryStream('SELECT * FROM user')->pipe($formatter)->pipe($logger);
```

Note that as per the underlying stream definition, calling `pause()` and
`resume()` on this stream is advisory-only, i.e. the stream MAY continue
emitting some data until the underlying network buffer is drained. Also
notice that the server side limits how long a connection is allowed to be
in a state that has outgoing data. Special care should be taken to ensure
the stream is resumed in time. This implies that using `pipe()` with a
slow destination stream may cause the connection to abort after a while.

The given `$sql` parameter MUST contain a single statement. Support
for multiple statements is disabled for security reasons because it
could allow for possible SQL injection attacks and this API is not
suited for exposing multiple possible results.

#### ping()

The `ping(): PromiseInterface<void>` method can be used to
check that the connection is alive.

This method returns a promise that will resolve (with a void value) on
success or will reject with an `Exception` on error. The MySQL protocol
is inherently sequential, so that all commands will be performed in order
and outstanding command will be put into a queue to be executed once the
previous queries are completed.

```php
$mysql->ping()->then(function () {
    echo 'OK' . PHP_EOL;
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```

#### quit()

The `quit(): PromiseInterface<void>` method can be used to
quit (soft-close) the connection.

This method returns a promise that will resolve (with a void value) on
success or will reject with an `Exception` on error. The MySQL protocol
is inherently sequential, so that all commands will be performed in order
and outstanding commands will be put into a queue to be executed once the
previous commands are completed.

```php
$mysql->query('CREATE TABLE test ...');
$mysql->quit();
```

This method will gracefully close the connection to the MySQL database
server once all outstanding commands are completed. See also
[`close()`](#close) if you want to force-close the connection without
waiting for any commands to complete instead.

#### close()

The `close(): void` method can be used to
force-close the connection.

Unlike the `quit()` method, this method will immediately force-close the
connection and reject all outstanding commands.

```php
$mysql->close();
```

Forcefully closing the connection will yield a warning in the server logs
and should generally only be used as a last resort. See also
[`quit()`](#quit) as a safe alternative.

#### error event

The `error` event will be emitted once a fatal error occurs, such as
when the connection is lost or is invalid.
The event receives a single `Exception` argument for the error instance.

```php
$mysql->on('error', function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```

This event will only be triggered for fatal errors and will be followed
by closing the connection. It is not to be confused with "soft" errors
caused by invalid SQL queries.

#### close event

The `close` event will be emitted once the connection closes (terminates).

```php
$mysql->on('close', function () {
    echo 'Connection closed' . PHP_EOL;
});
```

See also the [`close()`](#close) method.

## Install

The recommended way to install this library is [through Composer](https://getcomposer.org/).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)

Once released, this project will follow [SemVer](https://semver.org/).
At the moment, this will install the latest development version:

```bash
composer require react/mysql:^0.7@dev
```

See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.

This project aims to run on any platform and thus does not require any PHP
extensions and supports running on legacy PHP 5.4 through current PHP 8+ and
HHVM.
It's *highly recommended to use the latest supported PHP version* for this project.

This project supports connecting to a variety of MySQL database versions and
compatible projects using the MySQL protocol. The `caching_sha2_password`
authentication plugin (default in MySQL 8+) requires PHP 7.1+ and `ext-openssl`
to be installed, while the older `mysql_native_password` authentication plugin
(default in MySQL 5.7) is supported across all supported PHP versions.

## Tests

To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org/):

```bash
composer install
```

The test suite contains a number of functional integration tests that send
actual test SQL queries against your local database and thus rely on a local
MySQL test database with appropriate write access.
The test suite creates and modifies a test table in this database, so make sure
to not use a production database!
You can change your test database credentials by passing these ENV variables:

```bash
export DB_HOST=localhost
export DB_PORT=3306
export DB_USER=test
export DB_PASSWD=test
export DB_DBNAME=test
```

For example, to create an empty test database, you can also use a temporary
[`mysql` Docker image](https://hub.docker.com/_/mysql/) like this:

```bash
docker run -it --rm --net=host \
    -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=test \
    -e MYSQL_USER=test -e MYSQL_PASSWORD=test mysql:5
```

To run the test suite, go to the project root and run:

```bash
vendor/bin/phpunit
```

## License

MIT, see [LICENSE file](LICENSE).

This is a community project now managed by
[@friends-of-reactphp](https://github.com/friends-of-reactphp).
The original implementation was created by
[@bixuehujin](https://github.com/bixuehujin) starting in 2013 and has been
migrated to [@friends-of-reactphp](https://github.com/friends-of-reactphp) in
2018 to help with maintenance and upcoming feature development.

The original implementation was made possible thanks to the following projects:

* [phpdaemon](https://github.com/kakserpom/phpdaemon): the MySQL protocol
  implementation is based on code of this project (with permission).
* [node-mysql](https://github.com/felixge/node-mysql): the API design is
  inspired by this project.


================================================
FILE: composer.json
================================================
{
    "name": "react/mysql",
    "description": "Async MySQL database client for ReactPHP.",
    "keywords": ["mysql", "database", "async", "reactphp"],
    "license": "MIT",
    "require": {
        "php": ">=5.4.0",
        "evenement/evenement": "^3.0 || ^2.1 || ^1.1",
        "react/event-loop": "^1.2",
        "react/promise": "^3.2 || ^2.7",
        "react/promise-stream": "^1.6",
        "react/promise-timer": "^1.11",
        "react/socket": "^1.16"
    },
    "require-dev": {
        "phpunit/phpunit": "^9.6 || ^8.5 || ^5.7 || ^4.8.36",
        "react/async": "^4.3 || ^3 || ^2"
    },
    "autoload": {
        "psr-4": {
            "React\\Mysql\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "React\\Tests\\Mysql\\": "tests/"
        }
    }
}


================================================
FILE: examples/01-query.php
================================================
<?php

// $ php examples/01-query.php
// $ MYSQL_URI=test:test@localhost/test php examples/01-query.php "SELECT * FROM book"

require __DIR__ . '/../vendor/autoload.php';

$mysql = new React\Mysql\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test');

$query = isset($argv[1]) ? $argv[1] : 'select * from book';
$mysql->query($query)->then(function (React\Mysql\MysqlResult $command) {
    if (isset($command->resultRows)) {
        // this is a response to a SELECT etc. with some rows (0+)
        print_r($command->resultFields);
        print_r($command->resultRows);
        echo count($command->resultRows) . ' row(s) in set' . PHP_EOL;
    } else {
        // this is an OK message in response to an UPDATE etc.
        if ($command->insertId !== 0) {
            var_dump('last insert ID', $command->insertId);
        }
        echo 'Query OK, ' . $command->affectedRows . ' row(s) affected' . PHP_EOL;
    }
}, function (Exception $error) {
    // the query was not executed successfully
    echo 'Error: ' . $error->getMessage() . PHP_EOL;
});


================================================
FILE: examples/02-query-stream.php
================================================
<?php

// $ php examples/02-query-stream.php "SHOW VARIABLES"
// $ MYSQL_URI=test:test@localhost/test php examples/02-query-stream.php "SELECT * FROM book"

require __DIR__ . '/../vendor/autoload.php';

$mysql = new React\Mysql\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test');

$query = isset($argv[1]) ? $argv[1] : 'select * from book';
$stream = $mysql->queryStream($query);

$stream->on('data', function ($row) {
    echo json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
});

$stream->on('error', function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

$stream->on('close', function () {
    echo 'CLOSED' . PHP_EOL;
});


================================================
FILE: examples/11-interactive.php
================================================
<?php

// $ php examples/11-interactive.php
// $ MYSQL_URI=test:test@localhost/test php examples/11-interactive.php

require __DIR__ . '/../vendor/autoload.php';

$mysql = new React\Mysql\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test');

// open a STDIN stream to read keyboard input (not supported on Windows)
$stdin = new React\Stream\ReadableResourceStream(STDIN);

$stdin->on('data', function ($line) use ($mysql) {
    $query = trim($line);

    if ($query === '') {
        // skip empty commands
        return;
    }
    if ($query === 'exit') {
        // exit command should close the connection
        echo 'bye.' . PHP_EOL;
        $mysql->quit();
        return;
    }

    $time = microtime(true);
    $mysql->query($query)->then(function (React\Mysql\MysqlResult $command) use ($time) {
        if (isset($command->resultRows)) {
            // this is a response to a SELECT etc. with some rows (0+)
            echo implode("\t", array_column($command->resultFields, 'name')) . PHP_EOL;
            foreach ($command->resultRows as $row) {
                echo implode("\t", $row) . PHP_EOL;
            }

            printf(
                '%d row%s in set (%.03f sec)%s',
                count($command->resultRows),
                count($command->resultRows) === 1 ? '' : 's',
                microtime(true) - $time,
                PHP_EOL
            );
        } else {
            // this is an OK message in response to an UPDATE etc.
            // the insertId will only be set if this is
            if ($command->insertId !== 0) {
                var_dump('last insert ID', $command->insertId);
            }

            printf(
                'Query OK, %d row%s affected (%.03f sec)%s',
                $command->affectedRows,
                $command->affectedRows === 1 ? '' : 's',
                microtime(true) - $time,
                PHP_EOL
            );
        }
    }, function (Exception $error) {
        // the query was not executed successfully
        echo 'Error: ' . $error->getMessage() . PHP_EOL;
    });
});

// close connection when STDIN closes (EOF or CTRL+D)
$stdin->on('close', function () use ($mysql) {
    $mysql->quit();
});

// close STDIN (stop reading) when connection closes
$mysql->on('close', function () use ($stdin) {
    $stdin->close();
    echo 'Disconnected.' . PHP_EOL;
});

echo '# Entering interactive mode ready, hit CTRL-D to quit' . PHP_EOL;


================================================
FILE: examples/12-slow-stream.php
================================================
<?php

// $ php examples/12-slow-stream.php "SHOW VARIABLES"
// $ MYSQL_URI=test:test@localhost/test php examples/12-slow-stream.php "SELECT * FROM book"

use React\EventLoop\Loop;

require __DIR__ . '/../vendor/autoload.php';

$mysql = new React\Mysql\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test');

$query = isset($argv[1]) ? $argv[1] : 'select * from book';
$stream = $mysql->queryStream($query);

$ref = new ReflectionProperty($mysql, 'connecting');
$ref->setAccessible(true);
$promise = $ref->getValue($mysql);
assert($promise instanceof React\Promise\PromiseInterface);

$promise->then(function (React\Mysql\Io\Connection $connection) {
    // The protocol parser reads rather large chunks from the underlying connection
    // and as such can yield multiple (dozens to hundreds) rows from a single data
    // chunk. We try to artificially limit the stream chunk size here to try to
    // only ever read a single row so we can demonstrate throttling this stream.
    // It goes without saying this is only a hack! Real world applications rarely
    // have the need to limit the chunk size. As an alternative, consider using
    // a stream decorator that rate-limits and buffers the resulting flow.
    try {
        // accept private "stream" (instanceof React\Socket\ConnectionInterface)
        $ref = new ReflectionProperty($connection, 'stream');
        $ref->setAccessible(true);
        $conn = $ref->getValue($connection);
        assert($conn instanceof React\Socket\ConnectionInterface);

        // access private "input" (instanceof React\Stream\DuplexStreamInterface)
        $ref = new ReflectionProperty($conn, 'input');
        $ref->setAccessible(true);
        $stream = $ref->getValue($conn);
        assert($stream instanceof React\Stream\DuplexStreamInterface);

        // reduce private bufferSize to just a few bytes to slow things down
        $ref = new ReflectionProperty($stream, 'bufferSize');
        $ref->setAccessible(true);
        $ref->setValue($stream, 8);
    } catch (Exception $e) {
        echo 'Warning: Unable to reduce buffer size: ' . $e->getMessage() . PHP_EOL;
    }
});

$throttle = null;
$stream->on('data', function ($row) use (&$throttle, $stream) {
    echo json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

    // simple throttle mechanism: explicitly pause the result stream and
    // resume it again after some time.
    if ($throttle === null) {
        $throttle = Loop::addTimer(1.0, function () use ($stream, &$throttle) {
            $throttle = null;
            $stream->resume();
        });
        $stream->pause();
    }
});

$stream->on('error', function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

$stream->on('close', function () use (&$throttle) {
    echo 'CLOSED' . PHP_EOL;

    if ($throttle) {
        Loop::cancelTimer($throttle);
        $throttle = null;
    }
});

$mysql->quit();


================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>

<!-- PHPUnit configuration file with new format for PHPUnit 9.6+ -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         cacheResult="false"
         colors="true"
         convertDeprecationsToExceptions="true">
    <testsuites>
        <testsuite name="React.MySQL Test Suite">
            <directory>./tests/</directory>
        </testsuite>
    </testsuites>
    <coverage>
        <include>
            <directory>./src/</directory>
        </include>
    </coverage>
    <php>
        <env name="DB_HOST" value="127.0.0.1"/>
        <env name="DB_PORT" value="3306"/>
        <env name="DB_USER" value="test"/>
        <env name="DB_PASSWD" value="test"/>
        <env name="DB_DBNAME" value="test"/>
        <ini name="error_reporting" value="-1" />
        <!-- Evaluate assertions, requires running with "php -d zend.assertions=1 vendor/bin/phpunit" -->
        <!-- <ini name="zend.assertions" value="1" /> -->
        <ini name="assert.active" value="1" />
        <ini name="assert.exception" value="1" />
        <ini name="assert.bail" value="0" />
    </php>
</phpunit>


================================================
FILE: phpunit.xml.legacy
================================================
<?xml version="1.0" encoding="UTF-8"?>

<!-- PHPUnit configuration file with old format for legacy PHPUnit -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/4.8/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite name="React.MySQL Test Suite">
            <directory>./tests/</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist>
            <directory>./src/</directory>
        </whitelist>
    </filter>
    <php>
        <env name="DB_HOST" value="127.0.0.1"/>
        <env name="DB_PORT" value="3306"/>
        <env name="DB_USER" value="test"/>
        <env name="DB_PASSWD" value="test"/>
        <env name="DB_DBNAME" value="test"/>
        <ini name="error_reporting" value="-1" />
        <!-- Evaluate assertions, requires running with "php -d zend.assertions=1 vendor/bin/phpunit" -->
        <!-- <ini name="zend.assertions" value="1" /> -->
        <ini name="assert.active" value="1" />
        <ini name="assert.exception" value="1" />
        <ini name="assert.bail" value="0" />
    </php>
</phpunit>


================================================
FILE: src/Commands/AbstractCommand.php
================================================
<?php

namespace React\Mysql\Commands;

use Evenement\EventEmitter;

/**
 * @internal
 */
abstract class AbstractCommand extends EventEmitter implements CommandInterface
{
    /**
     * (none, this is an internal thread state)
     */
    const SLEEP = 0x00;
    /**
     * mysql_close
     */
    const QUIT = 0x01;
    /**
     * mysql_select_db
     */
    const INIT_DB = 0x02;
    /**
     * mysql_real_query
     */
    const QUERY = 0x03;
    /**
     * mysql_list_fields
     */
    const FIELD_LIST = 0x04;
    /**
     * mysql_create_db (deprecated)
     */
    const CREATE_DB = 0x05;
    /**
     * mysql_drop_db (deprecated)
     */
    const DROP_DB = 0x06;
    /**
     * mysql_refresh
     */
    const REFRESH = 0x07;
    /**
     * mysql_shutdown
     */
    const SHUTDOWN = 0x08;
    /**
     * mysql_stat
     */
    const STATISTICS = 0x09;
    /**
     * mysql_list_processes
     */
    const PROCESS_INFO = 0x0a;
    /**
     * (none, this is an internal thread state)
     */
    const CONNECT = 0x0b;
    /**
     * mysql_kill
     */
    const PROCESS_KILL = 0x0c;
    /**
     * mysql_dump_debug_info
     */
    const DEBUG = 0x0d;
    /**
     * mysql_ping
     */
    const PING = 0x0e;
    /**
     * (none, this is an internal thread state)
     */
    const TIME = 0x0f;
    /**
     * (none, this is an internal thread state)
     */
    const DELAYED_INSERT = 0x10;
    /**
     * mysql_change_user
     */
    const CHANGE_USER = 0x11;
    /**
     * sent by the slave IO thread to request a binlog
     */
    const BINLOG_DUMP = 0x12;
    /**
     * LOAD TABLE ... FROM MASTER (deprecated)
     */
    const TABLE_DUMP = 0x13;
    /**
     * (none, this is an internal thread state)
     */
    const CONNECT_OUT = 0x14;
    /**
     * sent by the slave to register with the master (optional)
     */
    const REGISTER_SLAVE = 0x15;
    /**
     * mysql_stmt_prepare
     */
    const STMT_PREPARE = 0x16;
    /**
     * mysql_stmt_execute
     */
    const STMT_EXECUTE = 0x17;
    /**
     * mysql_stmt_send_long_data
     */
    const STMT_SEND_LONG_DATA = 0x18;
    /**
     * mysql_stmt_close
     */
    const STMT_CLOSE = 0x19;
    /**
     * mysql_stmt_reset
     */
    const STMT_RESET = 0x1a;
    /**
     * mysql_set_server_option
     */
    const SET_OPTION = 0x1b;
    /**
     * mysql_stmt_fetch
     */
    const STMT_FETCH = 0x1c;
}


================================================
FILE: src/Commands/AuthenticateCommand.php
================================================
<?php

namespace React\Mysql\Commands;

use React\Mysql\Io\Buffer;
use React\Mysql\Io\Constants;

/**
 * @internal
 * @link https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_handshake_response.html#sect_protocol_connection_phase_packets_protocol_handshake_response41
 */
class AuthenticateCommand extends AbstractCommand
{
    private $user;
    private $passwd;
    private $dbname;

    private $maxPacketSize = 0x1000000;

    /**
     * @var int
     * @link https://dev.mysql.com/doc/internals/en/character-set.html#packet-Protocol::CharacterSet
     */
    private $charsetNumber;

    /**
     * Mapping from charset name to internal charset ID
     *
     * Note that this map currently only contains ASCII-compatible charset encodings
     * because of quoting rules as defined in the `Query` class.
     *
     * @var array<string,int>
     * @see self::$charsetNumber
     * @see \React\Mysql\Io\Query::$escapeChars
     */
    private static $charsetMap = [
        'latin1' => 8,
        'latin2' => 9,
        'ascii' => 11,
        'latin5' => 30,
        'utf8' => 33,
        'latin7' => 41,
        'utf8mb4' => 45,
        'binary' => 63
    ];

    /**
     * @param string $user
     * @param string $passwd
     * @param string $dbname
     * @param string $charset
     * @throws \InvalidArgumentException for invalid/unknown charset name
     */
    public function __construct(
        $user,
        #[\SensitiveParameter]
        $passwd,
        $dbname,
        $charset
    ) {
        if (!isset(self::$charsetMap[$charset])) {
            throw new \InvalidArgumentException('Unsupported charset selected');
        }

        $this->user = $user;
        $this->passwd = $passwd;
        $this->dbname = $dbname;
        $this->charsetNumber = self::$charsetMap[$charset];
    }

    public function getId()
    {
        return 0;
    }

    /**
     * @param string $scramble
     * @param ?string $authPlugin
     * @param Buffer $buffer
     * @return string
     * @throws \UnexpectedValueException for unsupported authentication plugin
     */
    public function authenticatePacket($scramble, $authPlugin, Buffer $buffer)
    {
        $clientFlags = Constants::CLIENT_LONG_PASSWORD |
            Constants::CLIENT_LONG_FLAG |
            Constants::CLIENT_LOCAL_FILES |
            Constants::CLIENT_PROTOCOL_41 |
            Constants::CLIENT_INTERACTIVE |
            Constants::CLIENT_TRANSACTIONS |
            Constants::CLIENT_SECURE_CONNECTION |
            Constants::CLIENT_CONNECT_WITH_DB;

        if ($authPlugin !== null) {
            $clientFlags |= Constants::CLIENT_PLUGIN_AUTH;
        }

        return pack('VVc', $clientFlags, $this->maxPacketSize, $this->charsetNumber)
            . "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
            . $this->user . "\x00"
            . $buffer->buildStringLen($this->authResponse($scramble, $authPlugin))
            . $this->dbname . "\x00"
            . ($authPlugin !== null ? $authPlugin . "\0" : '');
    }

    /**
     * @param string $scramble
     * @param ?string $authPlugin
     * @return string
     * @throws \UnexpectedValueException for unsupported authentication plugin
     */
    public function authResponse($scramble, $authPlugin)
    {
        if ($authPlugin === null || $authPlugin === 'mysql_native_password') {
            return $this->authMysqlNativePassword($scramble);
        } elseif ($authPlugin === 'caching_sha2_password') {
            return $this->authCachingSha2Password($scramble);
        } else {
            throw new \UnexpectedValueException('Unknown authentication plugin "' . addslashes($authPlugin) . '" requested by server');
        }
    }

    /**
     * @param string $scramble
     * @return string
     */
    private function authMysqlNativePassword($scramble)
    {
        if ($this->passwd === '') {
            return '';
        }

        return \sha1($scramble . \sha1($hash1 = \sha1($this->passwd, true), true), true) ^ $hash1;
    }

    /**
     * @param string $scramble
     * @return string
     * @throws \BadFunctionCallException if SHA256 hash algorithm is not available if ext-hash is missing, only possible in PHP < 7.4
     */
    private function authCachingSha2Password($scramble)
    {
        if ($this->passwd === '') {
            return '';
        }

        if (\PHP_VERSION_ID < 70100 || !\function_exists('hash')) {
            throw new \UnexpectedValueException('Requires PHP 7.1+ with ext-hash for authentication plugin "caching_sha2_password" requested by server');
        }

        \assert(\in_array('sha256', \hash_algos(), true));
        return ($hash1 = \hash('sha256', $this->passwd, true)) ^ \hash('sha256', \hash('sha256', $hash1, true) . $scramble, true);
    }

    /**
     * @param string $scramble
     * @param string $pubkey
     * @return string
     * @throws \UnexpectedValueException if encryption fails (e.g. missing ext-openssl or invalid public key)
     */
    public function authSha256($scramble, $pubkey)
    {
        if (!\function_exists('openssl_public_encrypt')) {
            throw new \UnexpectedValueException('Requires ext-openssl for authentication plugin "caching_sha2_password" requested by server');
        }

        $ret = @\openssl_public_encrypt(
            $this->passwd . "\x00" ^ \str_pad($scramble, \strlen($this->passwd) + 1, $scramble),
            $auth,
            $pubkey,
            \OPENSSL_PKCS1_OAEP_PADDING
        );

        // unlikely: openssl_public_encrypt() may return false if the public key sent by the server is invalid
        if ($ret === false) {
            throw new \UnexpectedValueException('Failed to encrypt password with public key');
        }

        return $auth;
    }
}


================================================
FILE: src/Commands/CommandInterface.php
================================================
<?php

namespace React\Mysql\Commands;

use Evenement\EventEmitterInterface;

/**
 * @internal
 */
interface CommandInterface extends EventEmitterInterface
{
    public function getId();
}


================================================
FILE: src/Commands/PingCommand.php
================================================
<?php

namespace React\Mysql\Commands;

/**
 * @internal
 */
class PingCommand extends AbstractCommand
{
    public function getId()
    {
        return self::PING;
    }

    public function getSql()
    {
        return '';
    }
}


================================================
FILE: src/Commands/QueryCommand.php
================================================
<?php

namespace React\Mysql\Commands;

use React\Mysql\Io\Query;

/**
 * @internal
 */
class QueryCommand extends AbstractCommand
{
    public $query;
    public $fields;
    public $insertId;
    public $affectedRows;
    public $warningCount;

    public function getId()
    {
        return self::QUERY;
    }

    public function getQuery()
    {
        return $this->query;
    }

    public function setQuery($query)
    {
        if ($query instanceof Query) {
            $this->query = $query;
        } elseif (is_string($query)) {
            $this->query = new Query($query);
        } else {
            throw new \InvalidArgumentException('Invalid argument type of query specified.');
        }
    }

    public function getSql()
    {
        $query = $this->query;

        if ($query instanceof Query) {
            return $query->getSql();
        }

        return $query;
    }
}


================================================
FILE: src/Commands/QuitCommand.php
================================================
<?php

namespace React\Mysql\Commands;

/**
 * @internal
 */
class QuitCommand extends AbstractCommand
{
    public function getId()
    {
        return self::QUIT;
    }

    public function getSql()
    {
        return '';
    }
}


================================================
FILE: src/Exception.php
================================================
<?php

namespace React\Mysql;

class Exception extends \Exception
{
}


================================================
FILE: src/Io/Buffer.php
================================================
<?php
namespace React\Mysql\Io;

/**
 * @internal
 */
class Buffer
{
    private $buffer = '';
    private $bufferPos = 0;

    /**
     * appends some data to the end of the buffer without moving buffer position
     *
     * @param string $str
     * @return void
     */
    public function append($str)
    {
        $this->buffer .= $str;
    }

    /**
     * prepends some data to start of buffer and resets buffer position to start
     *
     * @param string $str
     * @return void
     */
    public function prepend($str)
    {
        $this->buffer = $str . \substr($this->buffer, $this->bufferPos);
        $this->bufferPos = 0;
    }

    /**
     * Reads binary string data with given byte length from buffer
     *
     * @param int $len length in bytes, must be positive or zero
     * @return string
     * @throws \UnderflowException
     */
    public function read($len)
    {
        // happy path to return empty string for zero length string
        if ($len === 0) {
            return '';
        }

        // happy path for single byte strings without using substrings
        if ($len === 1 && isset($this->buffer[$this->bufferPos])) {
            return $this->buffer[$this->bufferPos++];
        }

        // ensure buffer size contains $len bytes by checking target buffer position
        if ($len < 0 || !isset($this->buffer[$this->bufferPos + $len - 1])) {
            throw new \UnderflowException('Not enough data in buffer to read ' . $len . ' bytes');
        }
        $buffer = \substr($this->buffer, $this->bufferPos, $len);
        $this->bufferPos += $len;

        return $buffer;
    }

    /**
     * Reads data with given byte length from buffer into a new buffer
     *
     * This class keeps consumed data in memory for performance reasons and only
     * advances the internal buffer position by default. Reading data into a new
     * buffer will clear the data from the original buffer to free memory.
     *
     * @param int $len length in bytes, must be positive or zero
     * @return self
     * @throws \UnderflowException
     */
    public function readBuffer($len)
    {
        // happy path to return empty buffer without any memory access for zero length string
        if ($len === 0) {
            return new self();
        }

        // ensure buffer size contains $len bytes by checking target buffer position
        if ($len < 0 || !isset($this->buffer[$this->bufferPos + $len - 1])) {
            throw new \UnderflowException('Not enough data in buffer to read ' . $len . ' bytes');
        }

        $buffer = new self();
        $buffer->buffer = $this->read($len);

        if (!isset($this->buffer[$this->bufferPos])) {
            $this->buffer = '';
        } else {
            $this->buffer = \substr($this->buffer, $this->bufferPos);
        }
        $this->bufferPos = 0;

        return $buffer;

    }

    /**
     * Skips binary string data with given byte length from buffer
     *
     * This method can be used instead of `read()` if you do not care about the
     * bytes that will be skipped.
     *
     * @param int $len length in bytes, must be positive and non-zero
     * @return void
     * @throws \UnderflowException
     */
    public function skip($len)
    {
        if ($len < 1 || !isset($this->buffer[$this->bufferPos + $len - 1])) {
            throw new \UnderflowException('Not enough data in buffer');
        }
        $this->bufferPos += $len;
    }

    /**
     * returns the buffer length measures in number of bytes
     *
     * @return int
     */
    public function length()
    {
        return \strlen($this->buffer) - $this->bufferPos;
    }

    /**
     * @return int 1 byte / 8 bit integer (0 to 255)
     */
    public function readInt1()
    {
        return \ord($this->read(1));
    }

    /**
     * @return int 2 byte / 16 bit integer (0 to 64 K / 0xFFFF)
     */
    public function readInt2()
    {
        $v = \unpack('v', $this->read(2));
        return $v[1];
    }

    /**
     * @return int 3 byte / 24 bit integer (0 to 16 M / 0xFFFFFF)
     */
    public function readInt3()
    {
        $v = \unpack('V', $this->read(3) . "\0");
        return $v[1];
    }

    /**
     * @return int 4 byte / 32 bit integer (0 to 4 G / 0xFFFFFFFF)
     */
    public function readInt4()
    {
        $v = \unpack('V', $this->read(4));
        return $v[1];
    }

    /**
     * @return int 8 byte / 64 bit integer (0 to 2^64-1)
     * @codeCoverageIgnore
     */
    public function readInt8()
    {
        // PHP < 5.6.3 does not support packing 64 bit ints, so use manual bit shifting
        if (\PHP_VERSION_ID < 50603) {
            $v = \unpack('V*', $this->read(8));
            return $v[1] + ($v[2] << 32);
        }

        $v = \unpack('P', $this->read(8));
        return $v[1];
    }

    /**
     * Parses length-encoded binary integer
     *
     * @return int|null decoded integer 0 to 2^64 or null for special null int
     */
    public function readIntLen()
    {
        $f = $this->readInt1();
        if ($f <= 250) {
            return $f;
        }
        if ($f === 251) {
            return null;
        }
        if ($f === 252) {
            return $this->readInt2();
        }
        if ($f === 253) {
            return $this->readInt3();
        }

        return $this->readInt8();
    }

    /**
     * Parses length-encoded binary string
     *
     * @return string|null decoded string or null if length indicates null
     */
    public function readStringLen()
    {
        $l = $this->readIntLen();
        if ($l === null) {
            return $l;
        }

        return $this->read($l);
    }

    /**
     * Reads string until NULL character
     *
     * @return string
     * @throws \UnderflowException
     */
    public function readStringNull()
    {
        $pos = \strpos($this->buffer, "\0", $this->bufferPos);
        if ($pos === false) {
            throw new \UnderflowException('Missing NULL character');
        }

        $ret = $this->read($pos - $this->bufferPos);
        ++$this->bufferPos;

        return $ret;
    }

    /**
     * @param int $int
     * @return string
     */
    public function buildInt1($int)
    {
        return \chr($int);
    }

    /**
     * @param int $int
     * @return string
     */
    public function buildInt2($int)
    {
        return \pack('v', $int);
    }

    /**
     * @param int $int
     * @return string
     */
    public function buildInt3($int)
    {
        return \substr(\pack('V', $int), 0, 3);
    }

    /**
     * @param int $int
     * @return string
     * @codeCoverageIgnore
     */
    public function buildInt8($int)
    {
        // PHP < 5.6.3 does not support packing 64 bit ints, so use manual bit shifting
        if (\PHP_VERSION_ID < 50603) {
            return \pack('VV', $int, $int >> 32);
        }
        return \pack('P', $int);
    }

    /**
     * Builds length-encoded binary string
     *
     * @param string|null $s
     * @return string Resulting binary string
     */
    public function buildStringLen($s)
    {
        if ($s === NULL) {
            // \xFB (251)
            return "\xFB";
        }

        $l = \strlen($s);

        if ($l <= 250) {
            // this is the only path that is currently used in fact.
            return $this->buildInt1($l) . $s;
        }

        if ($l <= 0xFFFF) {
            // max 2^16: \xFC (252)
            return "\xFC" . $this->buildInt2($l) . $s;
        }

        if ($l <= 0xFFFFFF) {
            // max 2^24: \xFD (253)
            return "\xFD" . $this->buildInt3($l) . $s;
        }

        // max 2^64: \xFE (254)
        return "\xFE" . $this->buildInt8($l) . $s;
    }
}


================================================
FILE: src/Io/Connection.php
================================================
<?php

namespace React\Mysql\Io;

use Evenement\EventEmitter;
use React\EventLoop\LoopInterface;
use React\Mysql\Commands\CommandInterface;
use React\Mysql\Commands\PingCommand;
use React\Mysql\Commands\QueryCommand;
use React\Mysql\Commands\QuitCommand;
use React\Mysql\Exception;
use React\Mysql\MysqlResult;
use React\Promise\Deferred;
use React\Promise\Promise;
use React\Socket\ConnectionInterface as SocketConnectionInterface;

/**
 * @internal
 * @see \React\Mysql\MysqlClient
 */
class Connection extends EventEmitter
{
    const STATE_AUTHENTICATED       = 5;
    const STATE_CLOSING             = 6;
    const STATE_CLOSED              = 7;

    /**
     * @var Executor
     */
    private $executor;

    /**
     * @var int one of the state constants (may change, but should be used readonly from outside)
     * @see self::STATE_*
     */
    public $state = self::STATE_AUTHENTICATED;

    /**
     * @var SocketConnectionInterface
     */
    private $stream;

    /** @var Parser */
    private $parser;

    /** @var LoopInterface */
    private $loop;

    /** @var float */
    private $idlePeriod = 0.001;

    /** @var ?\React\EventLoop\TimerInterface */
    private $idleTimer;

    /** @var int */
    private $pending = 0;

    /**
     * Connection constructor.
     *
     * @param SocketConnectionInterface $stream
     * @param Executor                  $executor
     * @param Parser                    $parser
     * @param LoopInterface             $loop
     * @param ?float                    $idlePeriod
     */
    public function __construct(SocketConnectionInterface $stream, Executor $executor, Parser $parser, LoopInterface $loop, $idlePeriod)
    {
        $this->stream   = $stream;
        $this->executor = $executor;
        $this->parser   = $parser;

        $this->loop = $loop;
        if ($idlePeriod !== null) {
            $this->idlePeriod = $idlePeriod;
        }

        $stream->on('error', [$this, 'handleConnectionError']);
        $stream->on('close', [$this, 'handleConnectionClosed']);
    }

    /**
     * busy executing some command such as query or ping
     *
     * @return bool
     * @throws void
     */
    public function isBusy()
    {
        return $this->parser->isBusy() || !$this->executor->isIdle();
    }

    public function query(Query $query)
    {
        $command = new QueryCommand();
        $command->setQuery($query);
        try {
            $this->_doCommand($command);
        } catch (\Exception $e) {
            return \React\Promise\reject($e);
        }

        $this->awake();
        $deferred = new Deferred();

        // store all result set rows until result set end
        $rows = [];
        $command->on('result', function ($row) use (&$rows) {
            $rows[] = $row;
        });
        $command->on('end', function () use ($command, $deferred, &$rows) {
            $result = new MysqlResult();
            $result->resultFields = $command->fields;
            $result->resultRows = $rows;
            $result->warningCount = $command->warningCount;

            $rows = [];

            $this->idle();
            $deferred->resolve($result);
        });

        // resolve / reject status reply (response without result set)
        $command->on('error', function ($error) use ($deferred) {
            $this->idle();
            $deferred->reject($error);
        });
        $command->on('success', function () use ($command, $deferred) {
            $result = new MysqlResult();
            $result->affectedRows = $command->affectedRows;
            $result->insertId = $command->insertId;
            $result->warningCount = $command->warningCount;

            $this->idle();
            $deferred->resolve($result);
        });

        return $deferred->promise();
    }

    public function queryStream(Query $query)
    {
        $command = new QueryCommand();
        $command->setQuery($query);
        $this->_doCommand($command);
        $this->awake();

        $stream = new QueryStream($command, $this->stream);
        $stream->on('close', function () {
            $this->idle();
        });

        return $stream;
    }

    public function ping()
    {
        return new Promise(function ($resolve, $reject) {
            $command = $this->_doCommand(new PingCommand());
            $this->awake();

            $command->on('success', function () use ($resolve) {
                $this->idle();
                $resolve(null);
            });
            $command->on('error', function ($reason) use ($reject) {
                $this->idle();
                $reject($reason);
            });
        });
    }

    public function quit()
    {
        return new Promise(function ($resolve, $reject) {
            $command = $this->_doCommand(new QuitCommand());
            $this->state = self::STATE_CLOSING;

            // mark connection as "awake" until it is closed, so never "idle"
            $this->awake();

            $command->on('success', function () use ($resolve) {
                $resolve(null);
                $this->close();
            });
            $command->on('error', function ($reason) use ($reject) {
                $reject($reason);
                $this->close();
            });
        });
    }

    public function close()
    {
        if ($this->state === self::STATE_CLOSED) {
            return;
        }

        $this->state = self::STATE_CLOSED;
        $remoteClosed = $this->stream->isReadable() === false && $this->stream->isWritable() === false;
        $this->stream->close();

        if ($this->idleTimer !== null) {
            $this->loop->cancelTimer($this->idleTimer);
            $this->idleTimer = null;
        }

        // reject all pending commands if connection is closed
        while (!$this->executor->isIdle()) {
            $command = $this->executor->dequeue();
            assert($command instanceof CommandInterface);

            if ($remoteClosed) {
                $command->emit('error', [new \RuntimeException(
                    'Connection closed by peer (ECONNRESET)',
                    \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104
                )]);
            } else {
                $command->emit('error', [new \RuntimeException(
                    'Connection closing (ECONNABORTED)',
                    \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
                )]);
            }
        }

        $this->emit('close');
        $this->removeAllListeners();
    }

    /**
     * @param Exception $err Error from socket.
     *
     * @return void
     * @internal
     */
    public function handleConnectionError($err)
    {
        $this->emit('error', [$err, $this]);
    }

    /**
     * @return void
     * @internal
     */
    public function handleConnectionClosed()
    {
        if ($this->state < self::STATE_CLOSING) {
            $this->emit('error', [new \RuntimeException(
                'Connection closed by peer (ECONNRESET)',
                \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104
            )]);
        }

        $this->close();
    }

    /**
     * @param CommandInterface $command The command which should be executed.
     * @return CommandInterface
     * @throws Exception Can't send command
     */
    protected function _doCommand(CommandInterface $command)
    {
        if ($this->state !== self::STATE_AUTHENTICATED) {
            throw new \RuntimeException(
                'Connection ' . ($this->state === self::STATE_CLOSED ? 'closed' : 'closing'). ' (ENOTCONN)',
                \defined('SOCKET_ENOTCONN') ? \SOCKET_ENOTCONN : 107
            );
        }

        return $this->executor->enqueue($command);
    }

    private function awake()
    {
        ++$this->pending;

        if ($this->idleTimer !== null) {
            $this->loop->cancelTimer($this->idleTimer);
            $this->idleTimer = null;
        }
    }

    private function idle()
    {
        --$this->pending;

        if ($this->pending < 1 && $this->idlePeriod >= 0 && $this->state === self::STATE_AUTHENTICATED) {
            $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () {
                // soft-close connection and emit close event afterwards both on success or on error
                $this->idleTimer = null;
                $this->quit()->then(null, function () {
                    // ignore to avoid reporting unhandled rejection
                });
            });
        }
    }
}


================================================
FILE: src/Io/Constants.php
================================================
<?php

namespace React\Mysql\Io;

/**
 * @internal
 */
class Constants
{
    /**
     * new more secure passwords
     */
    const CLIENT_LONG_PASSWORD = 1;
    /**
     * Found instead of affected rows
     */
    const CLIENT_FOUND_ROWS = 2;
    /**
     * Get all column flags
     */
    const CLIENT_LONG_FLAG = 4;
    /**
     * One can specify db on connect
     */
    const CLIENT_CONNECT_WITH_DB = 8;
    /**
     * Don't allow database.table.column
     */
    const CLIENT_NO_SCHEMA = 16;
    /**
     * Can use compression protocol
     */
    const CLIENT_COMPRESS = 32;
    /**
     * Odbc client
     */
    const CLIENT_ODBC = 64;
    /**
     * Can use LOAD DATA LOCAL
     */
    const CLIENT_LOCAL_FILES = 128;
    /**
     * Ignore spaces before '('
     */
    const CLIENT_IGNORE_SPACE = 256;
    /**
     * New 4.1 protocol
     */
    const CLIENT_PROTOCOL_41 = 512;
    /**
     * This is an interactive client
     */
    const CLIENT_INTERACTIVE = 1024;
    /**
     * Switch to SSL after handshake
     */
    const CLIENT_SSL = 2048;
    /**
     * IGNORE sigpipes
     */
    const CLIENT_IGNORE_SIGPIPE = 4096;
    /**
     * Client knows about transactions
     */
    const CLIENT_TRANSACTIONS = 8192;
    /**
     * Old flag for 4.1 protocol
     */
    const CLIENT_RESERVED = 16384;
    /**
     * New 4.1 authentication
     */
    const CLIENT_SECURE_CONNECTION = 32768;
    /**
     * Enable/disable multi-stmt support
     */
    const CLIENT_MULTI_STATEMENTS = 65536;
    /**
     * Enable/disable multi-results
     */
    const CLIENT_MULTI_RESULTS = 131072;

    /**
     * Client supports plugin authentication (1 << 19)
     */
    const CLIENT_PLUGIN_AUTH = 524288;

    const FIELD_TYPE_DECIMAL     = 0x00;
    const FIELD_TYPE_TINY        = 0x01;
    const FIELD_TYPE_SHORT       = 0x02;
    const FIELD_TYPE_LONG        = 0x03;
    const FIELD_TYPE_FLOAT       = 0x04;
    const FIELD_TYPE_DOUBLE      = 0x05;
    const FIELD_TYPE_NULL        = 0x06;
    const FIELD_TYPE_TIMESTAMP   = 0x07;
    const FIELD_TYPE_LONGLONG    = 0x08;
    const FIELD_TYPE_INT24       = 0x09;
    const FIELD_TYPE_DATE        = 0x0a;
    const FIELD_TYPE_TIME        = 0x0b;
    const FIELD_TYPE_DATETIME    = 0x0c;
    const FIELD_TYPE_YEAR        = 0x0d;
    const FIELD_TYPE_NEWDATE     = 0x0e;
    const FIELD_TYPE_VARCHAR     = 0x0f;
    const FIELD_TYPE_BIT         = 0x10;
    const FIELD_TYPE_NEWDECIMAL  = 0xf6;
    const FIELD_TYPE_ENUM        = 0xf7;
    const FIELD_TYPE_SET         = 0xf8;
    const FIELD_TYPE_TINY_BLOB   = 0xf9;
    const FIELD_TYPE_MEDIUM_BLOB = 0xfa;
    const FIELD_TYPE_LONG_BLOB   = 0xfb;
    const FIELD_TYPE_BLOB        = 0xfc;
    const FIELD_TYPE_VAR_STRING  = 0xfd;
    const FIELD_TYPE_STRING      = 0xfe;
    const FIELD_TYPE_GEOMETRY    = 0xff;
    const NOT_NULL_FLAG       = 0x1;
    const PRI_KEY_FLAG        = 0x2;
    const UNIQUE_KEY_FLAG     = 0x4;
    const MULTIPLE_KEY_FLAG   = 0x8;
    const BLOB_FLAG           = 0x10;
    const UNSIGNED_FLAG       = 0x20;
    const ZEROFILL_FLAG       = 0x40;
    const BINARY_FLAG         = 0x80;
    const ENUM_FLAG           = 0x100;
    const AUTO_INCREMENT_FLAG = 0x200;
    const TIMESTAMP_FLAG      = 0x400;
    const SET_FLAG            = 0x800;
}


================================================
FILE: src/Io/Executor.php
================================================
<?php

namespace React\Mysql\Io;

use Evenement\EventEmitter;

/**
 * @internal
 */
class Executor extends EventEmitter
{
    public $queue;

    public function __construct()
    {
        $this->queue = new \SplQueue();
    }

    public function isIdle()
    {
        return $this->queue->isEmpty();
    }

    public function enqueue($command)
    {
        $this->queue->enqueue($command);
        $this->emit('new');

        return $command;
    }

    public function dequeue()
    {
        return $this->queue->dequeue();
    }
}


================================================
FILE: src/Io/Factory.php
================================================
<?php

namespace React\Mysql\Io;

use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Mysql\Commands\AuthenticateCommand;
use React\Mysql\Exception;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use React\Promise\Timer\TimeoutException;
use React\Socket\Connector;
use React\Socket\ConnectorInterface;
use React\Socket\ConnectionInterface as SocketConnectionInterface;

/**
 * @internal
 * @see \React\Mysql\MysqlClient
 */
class Factory
{
    /** @var LoopInterface */
    private $loop;

    /** @var ConnectorInterface */
    private $connector;

    /**
     * The `Factory` is responsible for creating an internal `Connection` instance.
     *
     * ```php
     * $factory = new React\Mysql\Io\Factory();
     * ```
     *
     * This class takes an optional `LoopInterface|null $loop` parameter that can be used to
     * pass the event loop instance to use for this object. You can use a `null` value
     * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
     * This value SHOULD NOT be given unless you're sure you want to explicitly use a
     * given event loop instance.
     *
     * If you need custom connector settings (DNS resolution, TLS parameters, timeouts,
     * proxy servers etc.), you can explicitly pass a custom instance of the
     * [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface):
     *
     * ```php
     * $connector = new React\Socket\Connector([
     *     'dns' => '127.0.0.1',
     *     'tcp' => [
     *         'bindto' => '192.168.10.1:0'
     *     ],
     *     'tls' => [
     *         'verify_peer' => false,
     *         'verify_peer_name' => false
     *     ]
     * ]);
     *
     * $factory = new React\Mysql\Factory(null, $connector);
     * ```
     *
     * @param ?LoopInterface $loop
     * @param ?ConnectorInterface $connector
     */
    public function __construct($loop = null, $connector = null)
    {
        // manual type check to support legacy PHP < 7.1
        assert($loop === null || $loop instanceof LoopInterface);
        assert($connector === null || $connector instanceof ConnectorInterface);

        $this->loop = $loop ?: Loop::get();
        $this->connector = $connector ?: new Connector([], $this->loop);
    }

    /**
     * Creates a new connection.
     *
     * It helps with establishing a TCP/IP connection to your MySQL database
     * and issuing the initial authentication handshake.
     *
     * ```php
     * $factory->createConnection($url)->then(
     *     function (Connection $connection) {
     *         // client connection established (and authenticated)
     *     },
     *     function (Exception $e) {
     *         // an error occurred while trying to connect or authorize client
     *     }
     * );
     * ```
     *
     * The method returns a [Promise](https://github.com/reactphp/promise) that
     * will resolve with an internal `Connection`
     * instance on success or will reject with an `Exception` if the URL is
     * invalid or the connection or authentication fails.
     *
     * The returned Promise is implemented in such a way that it can be
     * cancelled when it is still pending. Cancelling a pending promise will
     * reject its value with an Exception and will cancel the underlying TCP/IP
     * connection attempt and/or MySQL authentication.
     *
     * ```php
     * $promise = $factory->createConnection($url);
     *
     * Loop::addTimer(3.0, function () use ($promise) {
     *     $promise->cancel();
     * });
     * ```
     *
     * The `$url` parameter must contain the database host, optional
     * authentication, port and database to connect to:
     *
     * ```php
     * $factory->createConnection('user:secret@localhost:3306/database');
     * ```
     *
     * Note that both the username and password must be URL-encoded (percent-encoded)
     * if they contain special characters:
     *
     * ```php
     * $user = 'he:llo';
     * $pass = 'p@ss';
     *
     * $promise = $factory->createConnection(
     *     rawurlencode($user) . ':' . rawurlencode($pass) . '@localhost:3306/db'
     * );
     * ```
     *
     * You can omit the port if you're connecting to default port `3306`:
     *
     * ```php
     * $factory->createConnection('user:secret@localhost/database');
     * ```
     *
     * If you do not include authentication and/or database, then this method
     * will default to trying to connect as user `root` with an empty password
     * and no database selected. This may be useful when initially setting up a
     * database, but likely to yield an authentication error in a production system:
     *
     * ```php
     * $factory->createConnection('localhost');
     * ```
     *
     * This method respects PHP's `default_socket_timeout` setting (default 60s)
     * as a timeout for establishing the connection and waiting for successful
     * authentication. You can explicitly pass a custom timeout value in seconds
     * (or use a negative number to not apply a timeout) like this:
     *
     * ```php
     * $factory->createConnection('localhost?timeout=0.5');
     * ```
     *
     * By default, the connection provides full UTF-8 support (using the
     * `utf8mb4` charset encoding). This should usually not be changed for most
     * applications nowadays, but for legacy reasons you can change this to use
     * a different ASCII-compatible charset encoding like this:
     *
     * ```php
     * $factory->createConnection('localhost?charset=utf8mb4');
     * ```
     *
     * @param string $uri
     * @return PromiseInterface<Connection>
     *     Resolves with a `Connection` on success or rejects with an `Exception` on error.
     */
    public function createConnection(
        #[\SensitiveParameter]
        $uri
    ) {
        $parts = parse_url($uri);
        $uri = preg_replace('#:[^:/]*@#', ':***@', $uri);
        assert(is_array($parts) && isset($parts['scheme'], $parts['host']));
        assert($parts['scheme'] === 'mysql');

        $args = [];
        if (isset($parts['query'])) {
            parse_str($parts['query'], $args);
        }

        /** @throws void already validated in MysqlClient ctor */
        $authCommand = new AuthenticateCommand(
            isset($parts['user']) ? rawurldecode($parts['user']) : 'root',
            isset($parts['pass']) ? rawurldecode($parts['pass']) : '',
            isset($parts['path']) ? rawurldecode(ltrim($parts['path'], '/')) : '',
            isset($args['charset']) ? $args['charset'] : 'utf8mb4'
        );

        $connecting = $this->connector->connect(
            $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 3306)
        );

        $deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) {
            // connection cancelled, start with rejecting attempt, then clean up
            $reject(new \RuntimeException(
                'Connection to ' . $uri . ' cancelled (ECONNABORTED)',
                \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
            ));

            // either close successful connection or cancel pending connection attempt
            $connecting->then(function (SocketConnectionInterface $connection) {
                $connection->close();
            }, function () {
                // ignore to avoid reporting unhandled rejection
            });
            $connecting->cancel();
        });

        $idlePeriod = isset($args['idle']) ? (float) $args['idle'] : null;
        $connecting->then(function (SocketConnectionInterface $stream) use ($authCommand, $deferred, $uri, $idlePeriod) {
            $executor = new Executor();
            $parser = new Parser($stream, $executor);

            $connection = new Connection($stream, $executor, $parser, $this->loop, $idlePeriod);
            $command = $executor->enqueue($authCommand);
            $parser->start();

            $command->on('success', function () use ($deferred, $connection) {
                $deferred->resolve($connection);
            });
            $command->on('error', function (\Exception $error) use ($deferred, $stream, $uri) {
                $const = '';
                $errno = $error->getCode();
                if ($error instanceof Exception) {
                    $const = ' (EACCES)';
                    $errno = \defined('SOCKET_EACCES') ? \SOCKET_EACCES : 13;
                }

                $deferred->reject(new \RuntimeException(
                    'Connection to ' . $uri . ' failed during authentication: ' . $error->getMessage() . $const,
                    $errno,
                    $error
                ));
                $stream->close();
            });
        }, function (\Exception $error) use ($deferred, $uri) {
            $deferred->reject(new \RuntimeException(
                'Connection to ' . $uri . ' failed: ' . $error->getMessage(),
                $error->getCode(),
                $error
            ));
        });

        // use timeout from explicit ?timeout=x parameter or default to PHP's default_socket_timeout (60)
        $timeout = (float) isset($args['timeout']) ? $args['timeout'] : ini_get("default_socket_timeout");
        if ($timeout < 0) {
            return $deferred->promise();
        }

        return \React\Promise\Timer\timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) use ($uri) {
            if ($e instanceof TimeoutException) {
                throw new \RuntimeException(
                    'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds (ETIMEDOUT)',
                    \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110
                );
            }
            throw $e;
        });
    }
}


================================================
FILE: src/Io/Parser.php
================================================
<?php

namespace React\Mysql\Io;

use React\Mysql\Commands\AuthenticateCommand;
use React\Mysql\Commands\QueryCommand;
use React\Mysql\Commands\QuitCommand;
use React\Mysql\Exception as MysqlException;
use React\Stream\DuplexStreamInterface;

/**
 * @internal
 */
class Parser
{
    const PHASE_GOT_INIT   = 1;
    const PHASE_AUTH_SENT  = 2;
    const PHASE_AUTH_ERR   = 3;
    const PHASE_HANDSHAKED = 4;

    const RS_STATE_HEADER = 0;
    const RS_STATE_FIELD  = 1;
    const RS_STATE_ROW    = 2;

    const STATE_STANDBY = 0;
    const STATE_BODY    = 1;

    /**
     * The packet header always consists of 4 bytes, 3 bytes packet length + 1 byte sequence number
     *
     * @var integer
     */
    const PACKET_SIZE_HEADER = 4;

    /**
     * Keeps a reference to the command that is currently being processed.
     *
     * The MySQL protocol is inherently sequential, the pending commands will be
     * stored in an `Executor` queue.
     *
     * The MySQL protocol communication starts with the server sending a
     * handshake message, so the current command will be `null` until it's our
     * turn.
     *
     * Similarly, when one command is finished, it will continue processing the
     * next command from the `Executor` queue. If no command is outstanding,
     * this will be reset to the `null` state.
     *
     * @var \React\Mysql\Commands\AbstractCommand|null
     */
    protected $currCommand;

    protected $debug = false;

    protected $state = 0;

    protected $phase = 0;

    public $seq = 0;

    public $warningCount;
    public $message;

    protected $serverVersion;
    protected $threadId;
    protected $scramble;

    protected $serverCaps;
    protected $serverLang;
    protected $serverStatus;

    protected $rsState = 0;

    /**
     * Packet size expected in number of bytes
     *
     * Depending on `self::$state`, the Parser excepts either a packet header
     * (always 4 bytes) or the packet contents (n bytes determined by prior
     * packet header).
     *
     * @var int
     * @see self::$state
     * @see self::PACKET_SIZE_HEADER
     */
    private $pctSize = self::PACKET_SIZE_HEADER;

    protected $resultFields = [];

    protected $insertId;
    protected $affectedRows;

    public $protocolVersion = 0;

    private $buffer;

    protected $connectOptions;

    /**
     * @var \React\Stream\DuplexStreamInterface
     */
    protected $stream;
    /**
     * @var Executor
     */
    protected $executor;

    /**
     * @var ?string authentication plugin name, set if server capabilities include CLIENT_PLUGIN_AUTH
     * @link https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_authentication_methods.html
     */
    private $authPlugin;

    public function __construct(DuplexStreamInterface $stream, Executor $executor)
    {
        $this->stream   = $stream;
        $this->executor = $executor;

        $this->buffer   = new Buffer();
        $executor->on('new', function () {
            $this->nextRequest();
        });
    }

    /**
     * busy executing some command such as query or ping
     *
     * @return bool
     * @throws void
     */
    public function isBusy()
    {
        return $this->currCommand !== null;
    }

    public function start()
    {
        $this->stream->on('data', [$this, 'handleData']);
        $this->stream->on('close', [$this, 'onClose']);
    }

    public function debug($message)
    {
        if ($this->debug) {
            $bt = \debug_backtrace();
            $caller = \array_shift($bt);
            printf("[DEBUG] <%s:%d> %s\n", $caller['class'], $caller['line'], $message);
        }
    }

    /** @var string $data */
    public function handleData($data)
    {
        $this->buffer->append($data);

        if ($this->debug) {
            $this->debug('Received ' . strlen($data) . ' byte(s), buffer now has ' . ($len = $this->buffer->length()) . ' byte(s): ' . wordwrap(bin2hex($b = $this->buffer->read($len)), 2, ' ', true)); $this->buffer->append($b); // @codeCoverageIgnore
        }

        while ($this->buffer->length() >= $this->pctSize) {
            if ($this->state === self::STATE_STANDBY) {
                $this->pctSize = $this->buffer->readInt3();
                //printf("packet size:%d\n", $this->pctSize);
                $this->state = self::STATE_BODY;
                $this->seq = $this->buffer->readInt1() + 1;
            }

            $len = $this->buffer->length();
            if ($len < $this->pctSize) {
                $this->debug('Waiting for complete packet with ' . $len . '/' . $this->pctSize . ' bytes');

                return;
            }

            $packet = $this->buffer->readBuffer($this->pctSize);
            $this->state = self::STATE_STANDBY;
            $this->pctSize = self::PACKET_SIZE_HEADER;

            try {
                $this->parsePacket($packet);
            } catch (\UnderflowException $e) {
                $this->onError(new \UnexpectedValueException('Unexpected protocol error, received malformed packet: ' . $e->getMessage(), 0, $e));
                $this->stream->close();
                return;
            }

            if ($packet->length() !== 0) {
                $this->onError(new \UnexpectedValueException('Unexpected protocol error, received malformed packet with ' . $packet->length() . ' unknown byte(s)'));
                $this->stream->close();
                return;
            }
        }
    }

    /** @return void */
    private function parsePacket(Buffer $packet)
    {
        if ($this->debug) {
            $this->debug('Parse packet#' . $this->seq . ' with ' . ($len = $packet->length()) . ' bytes: ' . wordwrap(bin2hex($b = $packet->read($len)), 2, ' ', true)); $packet->append($b); // @codeCoverageIgnore
        }

        if ($this->phase === 0) {
            $response = $packet->readInt1();
            if ($response === 0xFF) {
                // error packet before handshake means we did not exchange capabilities and error does not include SQL state
                $this->phase   = self::PHASE_AUTH_ERR;

                $code = $packet->readInt2();
                $exception = new MysqlException($packet->read($packet->length()), $code);
                $this->debug(sprintf("Error Packet:%d %s\n", $code, $exception->getMessage()));

                // error during init phase also means we're not currently executing any command
                // simply reject the first outstanding command in the queue (AuthenticateCommand)
                $this->currCommand = $this->executor->dequeue();
                $this->onError($exception);
                return;
            }

            $this->phase = self::PHASE_GOT_INIT;
            $this->protocolVersion = $response;
            $this->debug(sprintf("Protocol Version: %d", $this->protocolVersion));

            $options = &$this->connectOptions;
            $options['serverVersion'] = $packet->readStringNull();
            $options['threadId']      = $packet->readInt4();
            $this->scramble           = $packet->read(8); // 1st part
            $packet->skip(1); // filler
            $options['ServerCaps']    = $packet->readInt2(); // 1st part
            $options['serverLang']    = $packet->readInt1();
            $options['serverStatus']  = $packet->readInt2();
            $options['ServerCaps']   += $packet->readInt2() << 16; // 2nd part
            $packet->skip(11); // plugin length, 6 + 4 filler
            $this->scramble          .= $packet->read(12); // 2nd part
            $packet->skip(1);

            if ($this->connectOptions['ServerCaps'] & Constants::CLIENT_PLUGIN_AUTH) {
                $this->authPlugin = $packet->readStringNull();
                $this->debug('Authentication plugin: ' . $this->authPlugin);
            }

            // init completed, continue with sending AuthenticateCommand
            $this->nextRequest(true);
        } else {
            $fieldCount = $packet->readInt1();

            if ($fieldCount === 0xFF) {
                // error packet
                $code = $packet->readInt2();
                $packet->skip(6); // skip SQL state
                $exception = new MysqlException($packet->read($packet->length()), $code);
                $this->debug(sprintf("Error Packet:%d %s\n", $code, $exception->getMessage()));

                $this->onError($exception);
                $this->nextRequest();
            } elseif ($fieldCount === 0x00 && $this->rsState !== self::RS_STATE_ROW) {
                // Empty OK Packet terminates a query without a result set (UPDATE, INSERT etc.)
                $this->debug('Ok Packet');

                if ($this->phase === self::PHASE_AUTH_SENT) {
                    $this->phase = self::PHASE_HANDSHAKED;
                }

                $this->affectedRows = $packet->readIntLen();
                $this->insertId     = $packet->readIntLen();
                $this->serverStatus = $packet->readInt2();
                $this->warningCount = $packet->readInt2();

                $this->message      = $packet->read($packet->length());

                $this->debug(sprintf("AffectedRows: %d, InsertId: %d, WarningCount:%d", $this->affectedRows, $this->insertId, $this->warningCount));
                $this->onSuccess();
                $this->nextRequest();
            } elseif ($fieldCount === 0xFE && $this->phase !== self::PHASE_AUTH_SENT) {
                // EOF Packet
                $packet->skip(4); // warn, status
                if ($this->rsState === self::RS_STATE_ROW) {
                    // finalize this result set (all rows completed)
                    $this->debug('Result set done');

                    $this->onResultDone();
                    $this->nextRequest();
                } else {
                    // move to next part of result set (header->field->row)
                    $this->debug('Result set next part');
                    ++$this->rsState;
                }
            } elseif ($fieldCount === 0xFE && $this->phase === self::PHASE_AUTH_SENT) {
                // Protocol::AuthSwitchRequest packet
                // https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_switch_request.html
                $this->authPlugin = $packet->readStringNull();
                $this->scramble = $packet->read($packet->length() - 1);
                $packet->skip(1); // 0x00
                $this->debug('Switched to authentication plugin: ' . $this->authPlugin);

                try {
                    assert($this->currCommand instanceof AuthenticateCommand);
                    $this->sendPacket($this->currCommand->authResponse($this->scramble, $this->authPlugin));
                    //$this->sendPacket($this->currCommand->authenticatePacket($this->scramble, $this->authPlugin, $this->buffer));
                } catch (\UnexpectedValueException $e) {
                    $this->onError($e);
                    $this->stream->close();
                }
            } elseif ($fieldCount === 0x01 && $this->phase === self::PHASE_AUTH_SENT && $this->authPlugin === 'caching_sha2_password') {
                // Protocol::AuthMoreData packet
                // https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_more_data.html
                $status = $packet->readInt1();
                if ($status === 0x03 && $packet->length() === 0) {
                    // ignore fast auth success here, will be followed by OK packet
                    $this->debug('Fast auth success');
                } elseif ($status === 0x04 && $packet->length() === 0) {
                    // fast auth failure means we need to request the certificate to send the encrypted password
                    $this->debug('Fast auth failure, request certificate');
                    $this->sendPacket("\x02");
                } else {
                    // extra auth containing certificate data
                    $this->debug('Extra auth certificate received, send encrypted password');
                    $packet->prepend($packet->buildInt1($status));

                    try {
                        assert($this->currCommand instanceof AuthenticateCommand);
                        $this->sendPacket($this->currCommand->authSha256($this->scramble, $packet->read($packet->length())));
                    } catch (\UnexpectedValueException $e) {
                        $this->onError($e);
                        $this->stream->close();
                    }
                }
            } else {
                // Data packet
                $packet->prepend($packet->buildInt1($fieldCount));

                if ($this->rsState === self::RS_STATE_HEADER) {
                    $columns = $packet->readIntLen(); // extra
                    $this->debug('Result set with ' . $columns . ' column(s)');
                    $this->rsState = self::RS_STATE_FIELD;
                } elseif ($this->rsState === self::RS_STATE_FIELD) {
                    $field = [
                        'catalog'   => $packet->readStringLen(),
                        'db'        => $packet->readStringLen(),
                        'table'     => $packet->readStringLen(),
                        'org_table' => $packet->readStringLen(),
                        'name'      => $packet->readStringLen(),
                        'org_name'  => $packet->readStringLen()
                    ];

                    $packet->skip(1); // 0xC0
                    $field['charset']     = $packet->readInt2();
                    $field['length']      = $packet->readInt4();
                    $field['type']        = $packet->readInt1();
                    $field['flags']       = $packet->readInt2();
                    $field['decimals']    = $packet->readInt1();
                    $packet->skip(2); // unused

                    if ($this->debug) {
                        $this->debug('Result set column: ' . json_encode($field, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_INVALID_UTF8_SUBSTITUTE)); // @codeCoverageIgnore
                    }
                    $this->resultFields[] = $field;
                } elseif ($this->rsState === self::RS_STATE_ROW) {
                    $row = [];
                    foreach ($this->resultFields as $field) {
                        $row[$field['name']] = $packet->readStringLen();
                    }

                    if ($this->debug) {
                        $this->debug('Result set row: ' . json_encode($row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_INVALID_UTF8_SUBSTITUTE)); // @codeCoverageIgnore
                    }
                    $this->onResultRow($row);
                }
            }
        }
    }

    private function onResultRow($row)
    {
        // $this->debug('row data: ' . json_encode($row));
        $command = $this->currCommand;
        $command->emit('result', [$row]);
    }

    private function onError(\Exception $error)
    {
        $this->rsState      = self::RS_STATE_HEADER;
        $this->resultFields = [];

        // reject current command with error if we're currently executing any commands
        // ignore unsolicited server error in case we're not executing any commands (connection will be dropped)
        if ($this->currCommand !== null) {
            $command = $this->currCommand;
            $this->currCommand = null;

            $command->emit('error', [$error]);
        }
    }

    protected function onResultDone()
    {
        $command = $this->currCommand;
        $this->currCommand = null;

        assert($command instanceof QueryCommand);
        $command->fields = $this->resultFields;
        $command->emit('end');

        $this->rsState      = self::RS_STATE_HEADER;
        $this->resultFields = [];
    }

    protected function onSuccess()
    {
        $command = $this->currCommand;
        $this->currCommand = null;

        if ($command instanceof QueryCommand) {
            $command->affectedRows = $this->affectedRows;
            $command->insertId     = $this->insertId;
            $command->warningCount = $this->warningCount;
        }
        $command->emit('success');
    }

    public function onClose()
    {
        if ($this->currCommand !== null) {
            $command = $this->currCommand;
            $this->currCommand = null;

            if ($command instanceof QuitCommand) {
                $command->emit('success');
            } else {
                $command->emit('error', [new \RuntimeException(
                    'Connection closing (ECONNABORTED)',
                    \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
                )]);
            }
        }
    }

    public function sendPacket($packet)
    {
        return $this->stream->write($this->buffer->buildInt3(\strlen($packet)) . $this->buffer->buildInt1($this->seq++) . $packet);
    }

    protected function nextRequest($isHandshake = false)
    {
        if (!$isHandshake && $this->phase != self::PHASE_HANDSHAKED) {
            return false;
        }

        if ($this->currCommand === null && !$this->executor->isIdle()) {
            $command = $this->executor->dequeue();
            $this->currCommand = $command;

            if ($command instanceof AuthenticateCommand) {
                $this->phase = self::PHASE_AUTH_SENT;
                try {
                    $this->sendPacket($command->authenticatePacket($this->scramble, $this->authPlugin, $this->buffer));
                } catch (\UnexpectedValueException $e) {
                    $this->onError($e);
                    $this->stream->close();
                }
            } else {
                $this->seq = 0;
                $this->sendPacket($this->buffer->buildInt1($command->getId()) . $command->getSql());
            }
        }

        return true;
    }
}


================================================
FILE: src/Io/Query.php
================================================
<?php

namespace React\Mysql\Io;

/**
 * @internal
 */
class Query
{
    private $sql;

    private $builtSql;

    private $params = [];

    /**
     * Mapping from byte/character to escaped character string
     *
     * Note that this mapping assumes an ASCII-compatible charset encoding such
     * as UTF-8, ISO 8859 and others.
     *
     * Note that `'` will be escaped as `''` instead of `\'` to provide some
     * limited support for the `NO_BACKSLASH_ESCAPES` SQL mode. This assumes all
     * strings will always be enclosed in `'` instead of `"` which is guaranteed
     * as long as this class is only used internally for the `query()` method.
     *
     * @var array<string,string>
     * @see \React\Mysql\Commands\AuthenticateCommand::$charsetMap
     */
    private $escapeChars = [
            //"\x00"   => "\\0",
            //"\r"   => "\\r",
            //"\n"   => "\\n",
            //"\t"   => "\\t",
            //"\b"   => "\\b",
            //"\x1a" => "\\Z",
            "'"    => "''",
            //'"'    => '\"',
            "\\"   => "\\\\",
            //"%"    => "\\%",
            //"_"    => "\\_",
        ];

    /**
     * @param string $sql
     * @param list<string|int|float|bool|null> $params
     * @throws \InvalidArgumentException if given $params are invalid
     */
    public function __construct($sql, array $params = [])
    {
        foreach ($params as $param) {
            if (!\is_scalar($param) && $param !== null) {
                throw new \InvalidArgumentException('Query param must be of type string|int|float|bool|null, ' . (\is_object($param) ? \get_class($param) : \gettype($param)) . ' given');
            }
        }

        $this->sql = $sql;
        $this->builtSql = $params ? null : $sql;
        $this->params = $params;
    }

    public function escape($str)
    {
        return strtr($str, $this->escapeChars);
    }

    /**
     * @param  mixed  $value
     * @return string
     */
    protected function resolveValueForSql($value)
    {
        $type = gettype($value);
        switch ($type) {
            case 'boolean':
                $value = (int) $value;
                break;
            case 'double':
            case 'integer':
                break;
            case 'string':
                $value = "'" . $this->escape($value) . "'";
                break;
            case 'NULL':
                $value = 'NULL';
                break;
        }

        return $value;
    }

    protected function buildSql()
    {
        $sql = $this->sql;

        $offset = strpos($sql, '?');
        foreach ($this->params as $param) {
            $replacement = $this->resolveValueForSql($param);
            $sql = substr_replace($sql, $replacement, $offset, 1);
            $offset = strpos($sql, '?', $offset + strlen($replacement));
        }
        if ($offset !== false) {
            throw new \LogicException('Params not enough to build sql');
        }

        return $sql;
        /*
        $names    = [];
        $inName   = false;
        $currName = '';
        $currIdx  = 0;
        $sql      = $this->sql;
        $len      = strlen($sql);
        $i        = 0;
        do {
            $c    = $sql[$i];
            if ($c === '?') {
                $names[$i] = $c;
            } elseif ($c === ':') {
                $currName .= $c;
                $currIdx  = $i;
                $inName   = true;
            } elseif ($c === ' ') {
                $inName   = false;
                if ($currName) {
                    $names[$currIdx] = $currName;
                    $currName = '';
                }
            } else {
                if ($inName) {
                    $currName .= $c;
                }
            }
        } while (++ $i < $len);

        if ($inName) {
            $names[$currIdx] = $currName;
        }

        $namedMarks = $unnamedMarks = [];
        foreach ($this->params as $arg) {
            if (is_array($arg)) {
                $namedMarks += $arg;
            } else {
                $unnamedMarks[] = $arg;
            }
        }

        $offset = 0;
        foreach ($names as $idx => $value) {
            if ($value === '?') {
                $replacement = array_shift($unnamedMarks);
            } else {
                $replacement = $namedMarks[$value];
            }
            list($arg, $len) = $this->getEscapedStringAndLen($replacement);
            $sql = substr_replace($sql, $arg, $idx + $offset, strlen($value));
            $offset += $len - strlen($value);
        }

        return $sql;
        */
    }

    /**
     * Get the constructed and escaped sql string.
     *
     * @return string
     */
    public function getSql()
    {
        if ($this->builtSql === null) {
            $this->builtSql = $this->buildSql();
        }

        return $this->builtSql;
    }
}


================================================
FILE: src/Io/QueryStream.php
================================================
<?php

namespace React\Mysql\Io;

use Evenement\EventEmitter;
use React\Mysql\Commands\QueryCommand;
use React\Socket\ConnectionInterface;
use React\Stream\ReadableStreamInterface;
use React\Stream\Util;
use React\Stream\WritableStreamInterface;

/**
 * @internal
 * @see Connection::queryStream()
 */
class QueryStream extends EventEmitter implements ReadableStreamInterface
{
    private $connection;
    private $started = false;
    private $closed = false;
    private $paused = false;

    public function __construct(QueryCommand $command, ConnectionInterface $connection)
    {
        $this->connection = $connection;

        // forward result set rows until result set end
        $command->on('result', function ($row) {
            if (!$this->started && $this->paused) {
                $this->connection->pause();
            }
            $this->started = true;

            $this->emit('data', [$row]);
        });
        $command->on('end', function () {
            $this->emit('end');
            $this->close();
        });

        // status reply (response without result set) ends stream without data
        $command->on('success', function () {
            $this->emit('end');
            $this->close();
        });
        $command->on('error', function ($err) {
            $this->emit('error', [$err]);
            $this->close();
        });
    }

    public function isReadable()
    {
        return !$this->closed;
    }

    public function pause()
    {
        $this->paused = true;
        if ($this->started && !$this->closed) {
            $this->connection->pause();
        }
    }

    public function resume()
    {
        $this->paused = false;
        if ($this->started && !$this->closed) {
            $this->connection->resume();
        }
    }

    public function close()
    {
        if ($this->closed) {
            return;
        }

        $this->closed = true;
        if ($this->started && $this->paused) {
            $this->connection->resume();
        }

        $this->emit('close');
        $this->removeAllListeners();
    }

    public function pipe(WritableStreamInterface $dest, array $options = [])
    {
        return Util::pipe($this, $dest, $options);
    }
}


================================================
FILE: src/MysqlClient.php
================================================
<?php

namespace React\Mysql;

use Evenement\EventEmitter;
use React\EventLoop\LoopInterface;
use React\Mysql\Commands\AuthenticateCommand;
use React\Mysql\Io\Connection;
use React\Mysql\Io\Factory;
use React\Mysql\Io\Query;
use React\Promise\Deferred;
use React\Promise\Promise;
use React\Promise\PromiseInterface;
use React\Socket\ConnectorInterface;
use React\Stream\ReadableStreamInterface;

/**
 * This class represents a connection that is responsible for communicating
 * with your MySQL server instance, managing the connection state and sending
 * your database queries.
 *
 * Besides defining a few methods, this class also implements the
 * `EventEmitterInterface` which allows you to react to certain events:
 *
 * error event:
 *     The `error` event will be emitted once a fatal error occurs, such as
 *     when the connection is lost or is invalid.
 *     The event receives a single `Exception` argument for the error instance.
 *
 *     ```php
 *     $mysql->on('error', function (Exception $e) {
 *         echo 'Error: ' . $e->getMessage() . PHP_EOL;
 *     });
 *     ```
 *
 *     This event will only be triggered for fatal errors and will be followed
 *     by closing the connection. It is not to be confused with "soft" errors
 *     caused by invalid SQL queries.
 *
 * close event:
 *     The `close` event will be emitted once the connection closes (terminates).
 *
 *     ```php
 *     $mysql->on('close', function () {
 *         echo 'Connection closed' . PHP_EOL;
 *     });
 *     ```
 *
 *     See also the [`close()`](#close) method.
 *
 * @final
 */
class MysqlClient extends EventEmitter
{
    private $factory;
    private $uri;
    private $closed = false;

    /** @var PromiseInterface<Connection>|null */
    private $connecting;

    /** @var ?Connection */
    private $connection;

    /**
     * array of outstanding connection requests to send next commands once a connection becomes ready
     *
     * @var array<int,Deferred<Connection>>
     */
    private $pending = [];

    /**
     * set to true only between calling `quit()` and the connection closing in response
     *
     * @var bool
     * @see self::quit()
     * @see self::$closed
     */
    private $quitting = false;

    /**
     * @param string $uri
     * @param ?ConnectorInterface $connector
     * @param ?LoopInterface $loop
     * @throws \InvalidArgumentException if $uri is not a valid MySQL URI
     */
    public function __construct(
        #[\SensitiveParameter]
        $uri,
        $connector = null,
        $loop = null
    ) {
        if (strpos($uri, '://') === false) {
            $uri = 'mysql://' . $uri;
        }

        $parts = parse_url($uri);
        if ($parts === false || !isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'mysql') {
            $uri = preg_replace('#:[^:/]*@#', ':***@', $uri);
            throw new \InvalidArgumentException(
                'Invalid MySQL URI "' . $uri . '" (EINVAL)',
                defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22
            );
        }

        if (isset($parts['query'])) {
            $query = [];
            parse_str($parts['query'], $query);

            // validate charset if given
            if (isset($query['charset'])) {
                new AuthenticateCommand('', '', '', $query['charset']);
            }
        }

        if ($connector !== null && !$connector instanceof ConnectorInterface) { // manual type check to support legacy PHP < 7.1
            throw new \InvalidArgumentException('Argument #2 ($connector) expected null|React\Socket\ConnectorInterface');
        }
        if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
            throw new \InvalidArgumentException('Argument #3 ($loop) expected null|React\EventLoop\LoopInterface');
        }

        $this->factory = new Factory($loop, $connector);
        $this->uri = $uri;
    }

    /**
     * Performs an async query.
     *
     * This method returns a promise that will resolve with a `MysqlResult` on
     * success or will reject with an `Exception` on error. The MySQL protocol
     * is inherently sequential, so that all queries will be performed in order
     * and outstanding queries will be put into a queue to be executed once the
     * previous queries are completed.
     *
     * ```php
     * $mysql->query('CREATE TABLE test ...');
     * $mysql->query('INSERT INTO test (id) VALUES (1)');
     * ```
     *
     * If this SQL statement returns a result set (such as from a `SELECT`
     * statement), this method will buffer everything in memory until the result
     * set is completed and will then resolve the resulting promise. This is
     * the preferred method if you know your result set to not exceed a few
     * dozens or hundreds of rows. If the size of your result set is either
     * unknown or known to be too large to fit into memory, you should use the
     * [`queryStream()`](#querystream) method instead.
     *
     * ```php
     * $mysql->query($query)->then(function (React\Mysql\MysqlResult $command) {
     *     if (isset($command->resultRows)) {
     *         // this is a response to a SELECT etc. with some rows (0+)
     *         print_r($command->resultFields);
     *         print_r($command->resultRows);
     *         echo count($command->resultRows) . ' row(s) in set' . PHP_EOL;
     *     } else {
     *         // this is an OK message in response to an UPDATE etc.
     *         if ($command->insertId !== 0) {
     *             var_dump('last insert ID', $command->insertId);
     *         }
     *         echo 'Query OK, ' . $command->affectedRows . ' row(s) affected' . PHP_EOL;
     *     }
     * }, function (Exception $error) {
     *     // the query was not executed successfully
     *     echo 'Error: ' . $error->getMessage() . PHP_EOL;
     * });
     * ```
     *
     * You can optionally pass an array of `$params` that will be bound to the
     * query like this:
     *
     * ```php
     * $mysql->query('SELECT * FROM user WHERE id > ?', [$id]);
     * ```
     *
     * The given `$sql` parameter MUST contain a single statement. Support
     * for multiple statements is disabled for security reasons because it
     * could allow for possible SQL injection attacks and this API is not
     * suited for exposing multiple possible results.
     *
     * @param string $sql SQL statement
     * @param list<string|int|float|bool|null> $params Parameters which should be bound to query
     * @return PromiseInterface<MysqlResult>
     *     Resolves with a `MysqlResult` on success or rejects with an `Exception` on error.
     * @throws \InvalidArgumentException if given $params are invalid
     */
    public function query($sql, array $params = [])
    {
        $query = new Query($sql, $params);

        if ($this->closed || $this->quitting) {
            return \React\Promise\reject(new Exception('Connection closed'));
        }

        return $this->getConnection()->then(function (Connection $connection) use ($query) {
            return $connection->query($query)->then(function (MysqlResult $result) use ($connection) {
                $this->handleConnectionReady($connection);
                return $result;
            }, function (\Exception $e) use ($connection) {
                $this->handleConnectionReady($connection);
                throw $e;
            });
        });
    }

    /**
     * Performs an async query and streams the rows of the result set.
     *
     * This method returns a readable stream that will emit each row of the
     * result set as a `data` event. It will only buffer data to complete a
     * single row in memory and will not store the whole result set. This allows
     * you to process result sets of unlimited size that would not otherwise fit
     * into memory. If you know your result set to not exceed a few dozens or
     * hundreds of rows, you may want to use the [`query()`](#query) method instead.
     *
     * ```php
     * $stream = $mysql->queryStream('SELECT * FROM user');
     * $stream->on('data', function ($row) {
     *     echo $row['name'] . PHP_EOL;
     * });
     * $stream->on('end', function () {
     *     echo 'Completed.';
     * });
     * ```
     *
     * You can optionally pass an array of `$params` that will be bound to the
     * query like this:
     *
     * ```php
     * $stream = $mysql->queryStream('SELECT * FROM user WHERE id > ?', [$id]);
     * ```
     *
     * This method is specifically designed for queries that return a result set
     * (such as from a `SELECT` or `EXPLAIN` statement). Queries that do not
     * return a result set (such as a `UPDATE` or `INSERT` statement) will not
     * emit any `data` events.
     *
     * See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface)
     * for more details about how readable streams can be used in ReactPHP. For
     * example, you can also use its `pipe()` method to forward the result set
     * rows to a [`WritableStreamInterface`](https://github.com/reactphp/stream#writablestreaminterface)
     * like this:
     *
     * ```php
     * $mysql->queryStream('SELECT * FROM user')->pipe($formatter)->pipe($logger);
     * ```
     *
     * Note that as per the underlying stream definition, calling `pause()` and
     * `resume()` on this stream is advisory-only, i.e. the stream MAY continue
     * emitting some data until the underlying network buffer is drained. Also
     * notice that the server side limits how long a connection is allowed to be
     * in a state that has outgoing data. Special care should be taken to ensure
     * the stream is resumed in time. This implies that using `pipe()` with a
     * slow destination stream may cause the connection to abort after a while.
     *
     * The given `$sql` parameter MUST contain a single statement. Support
     * for multiple statements is disabled for security reasons because it
     * could allow for possible SQL injection attacks and this API is not
     * suited for exposing multiple possible results.
     *
     * @param string $sql SQL statement
     * @param list<string|int|float|bool|null> $params Parameters which should be bound to query
     * @return ReadableStreamInterface
     * @throws \InvalidArgumentException if given $params are invalid
     * @throws Exception if connection is already closed/closing
     */
    public function queryStream($sql, array $params = [])
    {
        $query = new Query($sql, $params);

        if ($this->closed || $this->quitting) {
            throw new Exception('Connection closed');
        }

        return \React\Promise\Stream\unwrapReadable(
            $this->getConnection()->then(function (Connection $connection) use ($query) {
                $stream = $connection->queryStream($query);

                $stream->on('end', function () use ($connection) {
                    $this->handleConnectionReady($connection);
                });
                $stream->on('error', function () use ($connection) {
                    $this->handleConnectionReady($connection);
                });

                return $stream;
            })
        );
    }

    /**
     * Checks that the connection is alive.
     *
     * This method returns a promise that will resolve (with a void value) on
     * success or will reject with an `Exception` on error. The MySQL protocol
     * is inherently sequential, so that all commands will be performed in order
     * and outstanding command will be put into a queue to be executed once the
     * previous queries are completed.
     *
     * ```php
     * $mysql->ping()->then(function () {
     *     echo 'OK' . PHP_EOL;
     * }, function (Exception $e) {
     *     echo 'Error: ' . $e->getMessage() . PHP_EOL;
     * });
     * ```
     *
     * @return PromiseInterface<void>
     *     Resolves with a `void` value on success or rejects with an `Exception` on error.
     */
    public function ping()
    {
        if ($this->closed || $this->quitting) {
            return \React\Promise\reject(new Exception('Connection closed'));
        }

        return $this->getConnection()->then(function (Connection $connection) {
            return $connection->ping()->then(function () use ($connection) {
                $this->handleConnectionReady($connection);
            }, function (\Exception $e) use ($connection) {
                $this->handleConnectionReady($connection);
                throw $e;
            });
        });
    }

    /**
     * Quits (soft-close) the connection.
     *
     * This method returns a promise that will resolve (with a void value) on
     * success or will reject with an `Exception` on error. The MySQL protocol
     * is inherently sequential, so that all commands will be performed in order
     * and outstanding commands will be put into a queue to be executed once the
     * previous commands are completed.
     *
     * ```php
     * $mysql->query('CREATE TABLE test ...');
     * $mysql->quit();
     * ```
     *
     * This method will gracefully close the connection to the MySQL database
     * server once all outstanding commands are completed. See also
     * [`close()`](#close) if you want to force-close the connection without
     * waiting for any commands to complete instead.
     *
     * @return PromiseInterface<void>
     *     Resolves with a `void` value on success or rejects with an `Exception` on error.
     */
    public function quit()
    {
        if ($this->closed || $this->quitting) {
            return \React\Promise\reject(new Exception('Connection closed'));
        }

        // not already connecting => no need to connect, simply close virtual connection
        if ($this->connection === null && $this->connecting === null) {
            $this->close();
            return \React\Promise\resolve(null);
        }

        $this->quitting = true;
        return new Promise(function (callable $resolve, callable $reject) {
            $this->getConnection()->then(function (Connection $connection) use ($resolve, $reject) {
                // soft-close connection and emit close event afterwards both on success or on error
                $connection->quit()->then(
                    function () use ($resolve){
                        $resolve(null);
                        $this->close();
                    },
                    function (\Exception $e) use ($reject) {
                        $reject($e);
                        $this->close();
                    }
                );
            }, function (\Exception $e) use ($reject) {
                // emit close event afterwards when no connection can be established
                $reject($e);
                $this->close();
            });
        });
    }

    /**
     * Force-close the connection.
     *
     * Unlike the `quit()` method, this method will immediately force-close the
     * connection and reject all outstanding commands.
     *
     * ```php
     * $mysql->close();
     * ```
     *
     * Forcefully closing the connection will yield a warning in the server logs
     * and should generally only be used as a last resort. See also
     * [`quit()`](#quit) as a safe alternative.
     *
     * @return void
     */
    public function close()
    {
        if ($this->closed) {
            return;
        }

        $this->closed = true;
        $this->quitting = false;

        // either close active connection or cancel pending connection attempt
        // below branches are exclusive, there can only be a single connection
        if ($this->connection !== null) {
            $this->connection->close();
            $this->connection = null;
        } elseif ($this->connecting !== null) {
            $this->connecting->cancel();
            $this->connecting = null;
        }

        // clear all outstanding commands
        foreach ($this->pending as $deferred) {
            $deferred->reject(new \RuntimeException('Connection closed'));
        }
        $this->pending = [];

        $this->emit('close');
        $this->removeAllListeners();
    }


    /**
     * @return PromiseInterface<Connection>
     */
    private function getConnection()
    {
        $deferred = new Deferred();

        // force-close connection if still waiting for previous disconnection due to idle timer
        if ($this->connection !== null && $this->connection->state === Connection::STATE_CLOSING) {
            $this->connection->close();
            $this->connection = null;
        }

        // happy path: reuse existing connection unless it is currently busy executing another command
        if ($this->connection !== null && !$this->connection->isBusy()) {
            $deferred->resolve($this->connection);
            return $deferred->promise();
        }

        // queue pending connection request until connection becomes ready
        $this->pending[] = $deferred;

        // create new connection if not already connected or connecting
        if ($this->connection === null && $this->connecting === null) {
            $this->connecting = $this->factory->createConnection($this->uri);
            $this->connecting->then(function (Connection $connection) {
                // connection completed => remember only until closed
                $this->connecting = null;
                $this->connection = $connection;
                $connection->on('close', function () {
                    $this->connection = null;
                });

                // handle first command from queue when connection is ready
                $this->handleConnectionReady($connection);
            }, function (\Exception $e) {
                // connection failed => discard connection attempt
                $this->connecting = null;

                foreach ($this->pending as $key => $deferred) {
                    $deferred->reject($e);
                    unset($this->pending[$key]);
                }
            });
        }

        return $deferred->promise();
    }

    private function handleConnectionReady(Connection $connection)
    {
        $deferred = \reset($this->pending);
        if ($deferred === false) {
            // nothing to do if there are no outstanding connection requests
            return;
        }

        assert($deferred instanceof Deferred);
        unset($this->pending[\key($this->pending)]);

        $deferred->resolve($connection);
    }
}


================================================
FILE: src/MysqlResult.php
================================================
<?php

namespace React\Mysql;

class MysqlResult
{
    /**
     * last inserted ID (if any)
     * @var int|null
     */
    public $insertId;

    /**
     * number of affected rows (for UPDATE, DELETE etc.)
     *
     * @var int|null
     */
    public $affectedRows;

    /**
     * result set fields (if any)
     *
     * @var array|null
     */
    public $resultFields;

    /**
     * result set rows (if any)
     *
     * @var array|null
     */
    public $resultRows;

    /**
     * number of warnings (if any)
     * @var int|null
     */
    public $warningCount;
}


================================================
FILE: tests/BaseTestCase.php
================================================
<?php

namespace React\Tests\Mysql;

use PHPUnit\Framework\TestCase;
use React\EventLoop\LoopInterface;
use React\Mysql\Io\Connection;
use React\Mysql\Io\Factory;

class BaseTestCase extends TestCase
{
    protected function getConnectionOptions($debug = false)
    {
        // can be controlled through ENV or by changing defaults in phpunit.xml
        return [
            'host'   => getenv('DB_HOST'),
            'port'   => (int)getenv('DB_PORT'),
            'dbname' => getenv('DB_DBNAME'),
            'user'   => getenv('DB_USER'),
            'passwd' => getenv('DB_PASSWD'),
        ] + ($debug ? ['debug' => true] : []);
    }

    protected function getConnectionString($params = [])
    {
        $parts = $params + $this->getConnectionOptions();

        return 'mysql://' . rawurlencode($parts['user']) . ':' . rawurlencode($parts['passwd']) . '@' . $parts['host'] . ':' . $parts['port'] . '/' . rawurlencode($parts['dbname']);
    }

    /**
     * @param LoopInterface $loop
     * @return Connection
     */
    protected function createConnection(LoopInterface $loop)
    {
        $factory = new Factory($loop);
        $promise = $factory->createConnection($this->getConnectionString());

        return \React\Async\await(\React\Promise\Timer\timeout($promise, 10.0));
    }

    protected function getDataTable()
    {
        return <<<SQL
CREATE TABLE `book` (
    `id`      INT(11)      NOT NULL AUTO_INCREMENT,
    `name`    VARCHAR(255) NOT NULL,
    `isbn`    VARCHAR(255) NULL,
    `author`  VARCHAR(255) NULL,
    `created` INT(11)      NULL,
    PRIMARY KEY (`id`)
)
SQL;
    }

    protected function expectCallableOnce()
    {
        $mock = $this->getMockBuilder('stdClass')->setMethods(['__invoke'])->getMock();
        $mock->expects($this->once())->method('__invoke');

        return $mock;
    }

    protected function expectCallableOnceWith($value)
    {
        $mock = $this->getMockBuilder('stdClass')->setMethods(['__invoke'])->getMock();
        $mock->expects($this->once())->method('__invoke')->with($value);

        return $mock;
    }

    protected function expectCallableNever()
    {
        $mock = $this->getMockBuilder('stdClass')->setMethods(['__invoke'])->getMock();
        $mock->expects($this->never())->method('__invoke');

        return $mock;
    }

    public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null)
    {
        if (method_exists($this, 'expectException')) {
            // PHPUnit 5.2+
            $this->expectException($exception);
            if ($exceptionMessage !== '') {
                $this->expectExceptionMessage($exceptionMessage);
            }
            if ($exceptionCode !== null) {
                $this->expectExceptionCode($exceptionCode);
            }
        } else {
            // legacy PHPUnit 4 - PHPUnit 5.1
            parent::setExpectedException($exception, $exceptionMessage, $exceptionCode);
        }
    }
}


================================================
FILE: tests/Commands/AuthenticateCommandTest.php
================================================
<?php

namespace React\Tests\Mysql\Commands;

use PHPUnit\Framework\TestCase;
use React\Mysql\Commands\AuthenticateCommand;
use React\Mysql\Io\Buffer;

class AuthenticateCommandTest extends TestCase
{
    /**
     * @doesNotPerformAssertions
     */
    public function testCtorWithKnownCharset()
    {
        new AuthenticateCommand('Alice', 'secret', '', 'utf8');
    }

    public function testCtorWithUnknownCharsetThrows()
    {
        if (method_exists($this, 'expectException')) {
            $this->expectException('InvalidArgumentException');
        } else {
            // legacy PHPUnit < 5.2
            $this->setExpectedException('InvalidArgumentException');
        }
        new AuthenticateCommand('Alice', 'secret', '', 'utf16');
    }

    public function testAuthenticatePacketWithEmptyPassword()
    {
        $command = new AuthenticateCommand('root', '', 'test', 'utf8mb4');

        $data = $command->authenticatePacket('scramble', null, new Buffer());

        $this->assertEquals("\x8d\xa6\0\0\0\0\0\x01\x2d" . str_repeat("\0", 23) . "root\0" . "\0" . "test\0", $data);
    }

    public function testAuthenticatePacketWithMysqlNativePasswordAuthPluginAndEmptyPassword()
    {
        $command = new AuthenticateCommand('root', '', 'test', 'utf8mb4');

        $data = $command->authenticatePacket('scramble', 'mysql_native_password', new Buffer());

        $this->assertEquals("\x8d\xa6\x08\0\0\0\0\x01\x2d" . str_repeat("\0", 23) . "root\0" . "\0" . "test\0" . "mysql_native_password\0", $data);
    }

    public function testAuthenticatePacketWithCachingSha2PasswordAuthPluginAndEmptyPassword()
    {
        $command = new AuthenticateCommand('root', '', 'test', 'utf8mb4');

        $data = $command->authenticatePacket('scramble', 'caching_sha2_password', new Buffer());

        $this->assertEquals("\x8d\xa6\x08\0\0\0\0\x01\x2d" . str_repeat("\0", 23) . "root\0" . "\0" . "test\0" . "caching_sha2_password\0", $data);
    }

    public function testAuthenticatePacketWithSecretPassword()
    {
        $command = new AuthenticateCommand('root', 'secret', 'test', 'utf8mb4');

        $data = $command->authenticatePacket('scramble', null, new Buffer());

        $this->assertEquals("\x8d\xa6\0\0\0\0\0\x01\x2d" . str_repeat("\0", 23) . "root\0" . "\x14\x18\xd8\x8d\x74\x77\x2c\x27\x22\x89\xe1\xcd\xbc\x4b\x5f\x77\x08\x18\x3c\x6e\xba" . "test\0", $data);
    }

    /**
     * @requires PHP 7.1
     * @requires function hash
     */
    public function testAuthenticatePacketWithCachingSha2PasswordWithSecretPasswordHashed()
    {
        $command = new AuthenticateCommand('root', 'secret', 'test', 'utf8mb4');

        $data = $command->authenticatePacket('scramble', 'caching_sha2_password', new Buffer());

        $this->assertEquals("\x8d\xa6\x08\0\0\0\0\x01\x2d" . str_repeat("\0", 23) . "root\0" . "\x20\x7a\x62\x89\x95\x53\xed\xdd\xa4\x11\x2d\x28\x9a\x02\x72\x12\xbb\x4c\xdd\xfd\xd3\x08\xfe\xc3\x6a\x85\xf1\xe9\x4a\xdb\xcf\x8b\xf3" . "test\0" . "caching_sha2_password\0", $data);
    }

    public function testAuthenticatePacketWithUnknownAuthPluginThrows()
    {
        $command = new AuthenticateCommand('root', 'secret', 'test', 'utf8mb4');

        if (method_exists($this, 'expectException')) {
            $this->expectException('UnexpectedValueException');
            $this->expectExceptionMessage('Unknown authentication plugin "mysql_old_password" requested by server');
        } else {
            // legacy PHPUnit < 5.2
            $this->setExpectedException('UnexpectedValueException', 'Unknown authentication plugin "mysql_old_password" requested by server');
        }
        $command->authenticatePacket('scramble', 'mysql_old_password', new Buffer());
    }

    /**
     * @requires function openssl_public_encrypt
     */
    public function testAuthSha256WithValidPublicKeyReturnsScrambledDataThatCanBeDecryptedByReceiverWithPrivateKey()
    {
        $command = new AuthenticateCommand('root', 'secret', 'test', 'utf8mb4');

        $key = openssl_pkey_new();

        $encrypted = $command->authSha256('scramble', openssl_pkey_get_details($key)['key']);

        $decrypted = '';
        $ok = openssl_private_decrypt($encrypted, $decrypted, $key, OPENSSL_PKCS1_OAEP_PADDING);

        $this->assertTrue($ok);
        $this->assertEquals("secret\0", $decrypted ^ "scramble");
    }

    /**
     * @requires function openssl_public_encrypt
     */
    public function testAuthSha256WithPasswordLongerThanScrambleLengthReturnsScrambledDataThatCanBeDecryptedByReceiverWithPrivateKey()
    {
        $command = new AuthenticateCommand('root', '012345678901234567890123456789', 'test', 'utf8mb4');

        $key = openssl_pkey_new();

        $encrypted = $command->authSha256('scramble', openssl_pkey_get_details($key)['key']);

        $decrypted = '';
        $ok = openssl_private_decrypt($encrypted, $decrypted, $key, OPENSSL_PKCS1_OAEP_PADDING);

        $this->assertTrue($ok);
        $this->assertEquals("012345678901234567890123456789\0", $decrypted ^ "scramblescramblescramblescramblescramble");
    }

    /**
     * @requires function openssl_public_encrypt
     */
    public function testAuthSha256WithInvalidPublicKeyThrows()
    {
        $command = new AuthenticateCommand('root', 'secret', 'test', 'utf8mb4');

        if (method_exists($this, 'expectException')) {
            $this->expectException('UnexpectedValueException');
            $this->expectExceptionMessage('Failed to encrypt password with public key');
        } else {
            // legacy PHPUnit < 5.2
            $this->setExpectedException('UnexpectedValueException', 'Failed to encrypt password with public key');
        }
        $command->authSha256('scramble', 'invalid pubkey');
    }
}


================================================
FILE: tests/Io/BufferTest.php
================================================
<?php

namespace React\Tests\Mysql\Io;

use React\Mysql\Io\Buffer;
use React\Tests\Mysql\BaseTestCase;

class BufferTest extends BaseTestCase
{
    public function testAppendAndReadBinary()
    {
        $buffer = new Buffer();

        $buffer->append('hello');

        $this->assertSame('hello', $buffer->read(5));
    }

    public function testReadBeyondLimitThrows()
    {
        $buffer = new Buffer();

        $buffer->append('hi');

        $this->setExpectedException('UnderflowException');
        $buffer->read(3);
    }

    public function testReadAfterSkipOne()
    {
        $buffer = new Buffer();

        $buffer->append('hi');
        $buffer->skip(1);

        $this->assertSame('i', $buffer->read(1));
    }

    public function testReadBufferEmptyIsNoop()
    {
        $buffer = new Buffer();

        $new = $buffer->readBuffer(0);

        $this->assertSame(0, $buffer->length());
        $this->assertSame(0, $new->length());
    }

    public function testReadBufferReturnsBufferWithOriginalLengthAndClearsOriginalBuffer()
    {
        $buffer = new Buffer();
        $buffer->append('foo');

        $new = $buffer->readBuffer($buffer->length());

        $this->assertSame(0, $buffer->length());
        $this->assertSame(3, $new->length());
    }

    public function testReadBufferBeyondLimitThrows()
    {
        $buffer = new Buffer();

        $this->setExpectedException('UnderflowException');
        $buffer->readBuffer(3);
    }

    public function testSkipZeroThrows()
    {
        $buffer = new Buffer();

        $buffer->append('hi');

        $this->setExpectedException('UnderflowException');
        $buffer->skip(0);
    }

    public function testSkipBeyondLimitThrows()
    {
        $buffer = new Buffer();

        $buffer->append('hi');

        $this->setExpectedException('UnderflowException');
        $buffer->skip(3);
    }

    public function testParseInt1()
    {
        $buffer = new Buffer();

        $buffer->append($buffer->buildInt1(0) . $buffer->buildInt1(255));

        $this->assertSame(0, $buffer->readInt1());
        $this->assertSame(255, $buffer->readInt1());
    }

    public function testParseInt2()
    {
        $buffer = new Buffer();

        $buffer->append($buffer->buildInt2(0) . $buffer->buildInt2(65535));

        $this->assertSame(0, $buffer->readInt2());
        $this->assertSame(65535, $buffer->readInt2());
    }

    public function testParseInt3()
    {
        $buffer = new Buffer();

        $buffer->append($buffer->buildInt3(0) . $buffer->buildInt3(0xFFFFFF));

        $this->assertSame(0, $buffer->readInt3());
        $this->assertSame(0xFFFFFF, $buffer->readInt3());
    }

    public function testParseInt8()
    {
        $buffer = new Buffer();

        $buffer->append($buffer->buildInt8(0) . $buffer->buildInt8(PHP_INT_MAX));

        $this->assertSame(0, $buffer->readInt8());
        $this->assertSame(PHP_INT_MAX, $buffer->readInt8());
    }

    public function testParseIntLen()
    {
        $buffer = new Buffer();

        $buffer->append("\x0A" . "\xFC" . "\x00\x04");

        $this->assertSame(10, $buffer->readIntLen());
        $this->assertSame(1024, $buffer->readIntLen());
    }

    public function testParseStringEmpty()
    {
        $buffer = new Buffer();

        $data = $buffer->buildStringLen('');
        $this->assertEquals("\x00", $data);

        $buffer->append($data);
        $this->assertSame('', $buffer->readStringLen());
    }

    public function testParseStringShort()
    {
        $buffer = new Buffer();

        $data = $buffer->buildStringLen('hello');
        $this->assertEquals("\x05" . "hello", $data);

        $buffer->append($data);
        $this->assertSame('hello', $buffer->readStringLen());
    }

    public function testParseStringKilo()
    {
        $buffer = new Buffer();

        $buffer->append($buffer->buildStringLen(str_repeat('.', 1024)));

        $this->assertSame(1024, strlen($buffer->readStringLen()));
    }

    public function testParseStringMega()
    {
        $buffer = new Buffer();

        $buffer->append($buffer->buildStringLen(str_repeat('.', 1000000)));

        $this->assertSame(1000000, strlen($buffer->readStringLen()));
    }

    /**
     * Test encoding/parsing string larger than 16 MiB. This should not happen
     * in practice as the protocol parser is currently limited to a packet
     * size of 16 MiB.
     */
    public function testParseStringExcessive()
    {
        $buffer = new Buffer();

        $buffer->append($buffer->buildStringLen(str_repeat('.', 17000000)));

        $this->assertSame(17000000, strlen($buffer->readStringLen()));
    }

    public function testParseStringNullLength()
    {
        $buffer = new Buffer();

        $data = $buffer->buildStringLen(null);
        $this->assertEquals("\xFB", $data);

        $buffer->append($data);
        $this->assertNull($buffer->readStringLen());
    }

    public function testParseStringNullCharacterTwice()
    {
        $buffer = new Buffer();
        $buffer->append("hello" . "\x00" . "world" . "\x00");

        $this->assertEquals('hello', $buffer->readStringNull());
        $this->assertEquals('world', $buffer->readStringNull());
    }

    public function testParseStringNullCharacterThrowsIfNullNotFound()
    {
        $buffer = new Buffer();
        $buffer->append("hello");

        $this->setExpectedException('UnderflowException');
        $buffer->readStringNull();
    }
}


================================================
FILE: tests/Io/ConnectionTest.php
================================================
<?php

namespace React\Tests\Mysql\Io;

use React\Mysql\Io\Connection;
use React\Mysql\Io\Query;
use React\Tests\Mysql\BaseTestCase;

class ConnectionTest extends BaseTestCase
{
    public function testIsBusyReturnsTrueWhenParserIsBusy()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue', 'isIdle'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnArgument(0);
        $executor->expects($this->never())->method('isIdle');

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();
        $parser->expects($this->once())->method('isBusy')->willReturn(true);

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $connection->query(new Query('SELECT 1'));

        $this->assertTrue($connection->isBusy());
    }

    public function testIsBusyReturnsFalseWhenParserIsNotBusyAndExecutorIsIdle()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->getMock();
        $executor->expects($this->once())->method('isIdle')->willReturn(true);

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $this->assertFalse($connection->isBusy());
    }

    public function testQueryWillEnqueueOneCommand()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
        $stream->expects($this->never())->method('close');

        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnArgument(0);

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->never())->method('addTimer');

        $conn = new Connection($stream, $executor, $parser, $loop, null);
        $conn->query(new Query('SELECT 1'));
    }

    public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenQueryCommandEmitsSuccess()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer);
        $loop->expects($this->never())->method('cancelTimer');

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $this->assertNull($currentCommand);

        $promise = $connection->query(new Query('SELECT 1'));

        $promise->then($this->expectCallableOnceWith($this->isInstanceOf('React\Mysql\MysqlResult')));

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('success');
    }

    public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenQueryCommandEmitsEnd()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer);
        $loop->expects($this->never())->method('cancelTimer');

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $this->assertNull($currentCommand);

        $promise = $connection->query(new Query('SELECT 1'));

        $promise->then($this->expectCallableOnceWith($this->isInstanceOf('React\Mysql\MysqlResult')));

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('end');
    }

    public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenIdlePeriodIsGivenAndQueryCommandEmitsSuccess()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->once())->method('addTimer')->with(1.0, $this->anything())->willReturn($timer);
        $loop->expects($this->never())->method('cancelTimer');

        $connection = new Connection($stream, $executor, $parser, $loop, 1.0);

        $this->assertNull($currentCommand);

        $promise = $connection->query(new Query('SELECT 1'));

        $promise->then($this->expectCallableOnceWith($this->isInstanceOf('React\Mysql\MysqlResult')));

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('success');
    }

    public function testQueryWillReturnResolvedPromiseAndNotStartIdleTimerWhenIdlePeriodIsNegativeAndQueryCommandEmitsSuccess()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->never())->method('addTimer');

        $connection = new Connection($stream, $executor, $parser, $loop, -1);

        $this->assertNull($currentCommand);

        $promise = $connection->query(new Query('SELECT 1'));

        $promise->then($this->expectCallableOnceWith($this->isInstanceOf('React\Mysql\MysqlResult')));

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('success');
    }

    public function testQueryWillReturnRejectedPromiseAndStartIdleTimerWhenQueryCommandEmitsError()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer);
        $loop->expects($this->never())->method('cancelTimer');

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $this->assertNull($currentCommand);

        $promise = $connection->query(new Query('SELECT 1'));

        $promise->then(null, $this->expectCallableOnce());

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('error', [new \RuntimeException()]);
    }

    public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitCloseEventWhenQuitCommandEmitsSuccess()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
        $stream->expects($this->once())->method('close');

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->any())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $timeout = null;
        $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->once())->method('addTimer')->with(0.001, $this->callback(function ($cb) use (&$timeout) {
            $timeout = $cb;
            return true;
        }))->willReturn($timer);

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $connection->on('close', $this->expectCallableOnce());

        $this->assertNull($currentCommand);

        $connection->query(new Query('SELECT 1'));

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('success');

        $this->assertNotNull($timeout);
        $timeout();

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('success');
    }

    public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitCloseEventWhenQuitCommandEmitsError()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
        $stream->expects($this->once())->method('close');

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->any())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $timeout = null;
        $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->once())->method('addTimer')->with(0.001, $this->callback(function ($cb) use (&$timeout) {
            $timeout = $cb;
            return true;
        }))->willReturn($timer);

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $connection->on('close', $this->expectCallableOnce());

        $this->assertNull($currentCommand);

        $connection->query(new Query('SELECT 1'));

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('success');

        $this->assertNotNull($timeout);
        $timeout();

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('error', [new \RuntimeException()]);
    }

    public function testQueryTwiceWillEnqueueSecondQueryWithoutStartingIdleTimerWhenFirstQueryCommandEmitsSuccess()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->any())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->never())->method('addTimer');

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $this->assertNull($currentCommand);

        $connection->query(new Query('SELECT 1'));
        $connection->query(new Query('SELECT 2'));

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('success');
    }

    public function testQueryTwiceAfterIdleTimerWasStartedWillCancelIdleTimerAndEnqueueSecondCommand()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->any())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer);
        $loop->expects($this->once())->method('cancelTimer')->with($timer);

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $this->assertNull($currentCommand);

        $connection->query(new Query('SELECT 1'));

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('success');

        $connection->query(new Query('SELECT 2'));
    }

    public function testQueryStreamWillEnqueueOneCommand()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
        $stream->expects($this->never())->method('close');

        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnArgument(0);

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->never())->method('addTimer');

        $conn = new Connection($stream, $executor, $parser, $loop, null);
        $conn->queryStream(new Query('SELECT 1'));
    }

    public function testQueryStreamWillReturnStreamThatWillEmitEndEventAndStartIdleTimerWhenQueryCommandEmitsSuccess()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer);
        $loop->expects($this->never())->method('cancelTimer');

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $this->assertNull($currentCommand);

        $stream = $connection->queryStream(new Query('SELECT 1'));

        $stream->on('end', $this->expectCallableOnce());
        $stream->on('close', $this->expectCallableOnce());

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('success');
    }

    public function testQueryStreamWillReturnStreamThatWillEmitErrorEventAndStartIdleTimerWhenQueryCommandEmitsError()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer);
        $loop->expects($this->never())->method('cancelTimer');

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $this->assertNull($currentCommand);

        $stream = $connection->queryStream(new Query('SELECT 1'));

        $stream->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException')));
        $stream->on('close', $this->expectCallableOnce());

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('error', [new \RuntimeException()]);
    }

    public function testPingWillEnqueueOneCommand()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
        $stream->expects($this->never())->method('close');

        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnArgument(0);

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->never())->method('addTimer');

        $conn = new Connection($stream, $executor, $parser, $loop, null);
        $conn->ping();
    }

    public function testPingWillReturnResolvedPromiseAndStartIdleTimerWhenPingCommandEmitsSuccess()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer);
        $loop->expects($this->never())->method('cancelTimer');

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $this->assertNull($currentCommand);

        $promise = $connection->ping();

        $promise->then($this->expectCallableOnce());

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('success');
    }

    public function testPingWillReturnRejectedPromiseAndStartIdleTimerWhenPingCommandEmitsError()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer);
        $loop->expects($this->never())->method('cancelTimer');

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $this->assertNull($currentCommand);

        $promise = $connection->ping();

        $promise->then(null, $this->expectCallableOnce());

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('error', [new \RuntimeException()]);
    }

    public function testQuitWillEnqueueOneCommand()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnArgument(0);

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->never())->method('addTimer');

        $conn = new Connection($stream, $executor, $parser, $loop, null);
        $conn->quit();
    }

    public function testQuitWillResolveBeforeEmittingCloseEventWhenQuitCommandEmitsSuccess()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $pingCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$pingCommand) {
            return $pingCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->never())->method('addTimer');

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $events = '';
        $connection->on('close', function () use (&$events) {
            $events .= 'closed.';
        });

        $this->assertEquals('', $events);

        $promise = $connection->quit();

        $promise->then(function () use (&$events) {
            $events .= 'fulfilled.';
        });

        $this->assertEquals('', $events);

        $this->assertNotNull($pingCommand);
        $pingCommand->emit('success');

        $this->assertEquals('fulfilled.closed.', $events);
    }

    public function testQuitWillRejectBeforeEmittingCloseEventWhenQuitCommandEmitsError()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $pingCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$pingCommand) {
            return $pingCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->never())->method('addTimer');

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $events = '';
        $connection->on('close', function () use (&$events) {
            $events .= 'closed.';
        });

        $this->assertEquals('', $events);

        $promise = $connection->quit();

        $promise->then(null, function () use (&$events) {
            $events .= 'rejected.';
        });

        $this->assertEquals('', $events);

        $this->assertNotNull($pingCommand);
        $pingCommand->emit('error', [new \RuntimeException()]);

        $this->assertEquals('rejected.closed.', $events);
    }

    public function testCloseWillEmitCloseEvent()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->getMock();
        $executor->expects($this->once())->method('isIdle')->willReturn(true);

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->never())->method('addTimer');

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $connection->on('close', $this->expectCallableOnce());

        $connection->close();
    }

    public function testCloseAfterIdleTimerWasStartedWillCancelIdleTimerAndEmitCloseEvent()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

        $currentCommand = null;
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) {
            return $currentCommand = $command;
        });

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock();
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer);
        $loop->expects($this->once())->method('cancelTimer')->with($timer);

        $connection = new Connection($stream, $executor, $parser, $loop, null);

        $this->assertNull($currentCommand);

        $connection->ping();

        $this->assertNotNull($currentCommand);
        $currentCommand->emit('success');

        $connection->on('close', $this->expectCallableOnce());

        $connection->close();
    }

    public function testQueryAfterQuitRejectsImmediately()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnArgument(0);

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();

        $conn = new Connection($stream, $executor, $parser, $loop, null);
        $conn->quit();
        $promise = $conn->query(new Query('SELECT 1'));

        $promise->then(null, $this->expectCallableOnceWith(
            $this->logicalAnd(
                $this->isInstanceOf('RuntimeException'),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getMessage() === 'Connection closing (ENOTCONN)';
                }),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107);
                })
            )
        ));
    }

    public function testQueryAfterCloseRejectsImmediately()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->never())->method('enqueue');

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();

        $conn = new Connection($stream, $executor, $parser, $loop, null);
        $conn->close();
        $promise = $conn->query(new Query('SELECT 1'));

        $promise->then(null, $this->expectCallableOnceWith(
            $this->logicalAnd(
                $this->isInstanceOf('RuntimeException'),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getMessage() === 'Connection closed (ENOTCONN)';
                }),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107);
                })
            )
        ));
    }

    public function testQueryStreamAfterQuitThrows()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnArgument(0);

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();

        $conn = new Connection($stream, $executor, $parser, $loop, null);
        $conn->quit();

        try {
            $conn->queryStream(new Query('SELECT 1'));
        } catch (\RuntimeException $e) {
            $this->assertEquals('Connection closing (ENOTCONN)', $e->getMessage());
            $this->assertEquals(defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107, $e->getCode());
        }
    }

    public function testPingAfterQuitRejectsImmediately()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnArgument(0);

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();

        $conn = new Connection($stream, $executor, $parser, $loop, null);
        $conn->quit();
        $promise = $conn->ping();

        $promise->then(null, $this->expectCallableOnceWith(
            $this->logicalAnd(
                $this->isInstanceOf('RuntimeException'),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getMessage() === 'Connection closing (ENOTCONN)';
                }),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107);
                })
            )
        ));
    }

    public function testQuitAfterQuitRejectsImmediately()
    {
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->once())->method('enqueue')->willReturnArgument(0);

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();

        $conn = new Connection($stream, $executor, $parser, $loop, null);
        $conn->quit();
        $promise = $conn->quit();

        $promise->then(null, $this->expectCallableOnceWith(
            $this->logicalAnd(
                $this->isInstanceOf('RuntimeException'),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getMessage() === 'Connection closing (ENOTCONN)';
                }),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107);
                })
            )
        ));
    }

    public function testCloseStreamEmitsErrorEvent()
    {
        $closeHandler = null;
        $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
        $stream->expects($this->exactly(2))->method('on')->withConsecutive(
            array('error', $this->anything()),
            array('close', $this->callback(function ($arg) use (&$closeHandler) {
                $closeHandler = $arg;
                return true;
            }))
        );
        $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock();
        $executor->expects($this->never())->method('enqueue');

        $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock();

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();

        $conn = new Connection($stream, $executor, $parser, $loop, null);
        $conn->on('error', $this->expectCallableOnceWith(
            $this->logicalAnd(
                $this->isInstanceOf('RuntimeException'),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getMessage() === 'Connection closed by peer (ECONNRESET)';
                }),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getCode() === (defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104);
                })
            )
        ));

        $this->assertNotNull($closeHandler);
        $closeHandler();
    }
}


================================================
FILE: tests/Io/FactoryTest.php
================================================
<?php

namespace React\Tests\Mysql\Io;

use React\EventLoop\Loop;
use React\Mysql\Io\Connection;
use React\Mysql\Io\Factory;
use React\Promise\Promise;
use React\Socket\SocketServer;
use React\Tests\Mysql\BaseTestCase;

class FactoryTest extends BaseTestCase
{
    public function testConstructWithoutLoopAssignsLoopAutomatically()
    {
        $factory = new Factory();

        $ref = new \ReflectionProperty($factory, 'loop');
        $ref->setAccessible(true);
        $loop = $ref->getValue($factory);

        $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop);
    }

    public function testConnectWillUseHostAndDefaultPort()
    {
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $pending = $this->getMockBuilder('React\Promise\PromiseInterface')->getMock();
        $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
        $connector->expects($this->once())->method('connect')->with('127.0.0.1:3306')->willReturn($pending);

        $factory = new Factory($loop, $connector);
        $factory->createConnection('mysql://127.0.0.1');
    }

    public function testConnectWillUseGivenScheme()
    {
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $pending = $this->getMockBuilder('React\Promise\PromiseInterface')->getMock();
        $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
        $connector->expects($this->once())->method('connect')->with('127.0.0.1:3306')->willReturn($pending);

        $factory = new Factory($loop, $connector);
        $factory->createConnection('mysql://127.0.0.1');
    }

    public function testConnectWillUseGivenHostAndGivenPort()
    {
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $pending = $this->getMockBuilder('React\Promise\PromiseInterface')->getMock();
        $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
        $connector->expects($this->once())->method('connect')->with('127.0.0.1:1234')->willReturn($pending);

        $factory = new Factory($loop, $connector);
        $factory->createConnection('mysql://127.0.0.1:1234');
    }

    public function testConnectWillUseGivenUserInfoAsDatabaseCredentialsAfterUrldecoding()
    {
        $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['write'])->getMock();
        $connection->expects($this->once())->method('write')->with($this->stringContains("user!\0"));

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
        $connector->expects($this->once())->method('connect')->with('127.0.0.1:3306')->willReturn(\React\Promise\resolve($connection));

        $factory = new Factory($loop, $connector);
        $promise = $factory->createConnection('mysql://user%21@127.0.0.1');

        $promise->then($this->expectCallableNever(), $this->expectCallableNever());

        $connection->emit('data', ["\x33\0\0\0" . "\x0a" . "mysql\0" . str_repeat("\0", 44)]);
    }

    public function testConnectWillUseGivenPathAsDatabaseNameAfterUrldecoding()
    {
        $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['write'])->getMock();
        $connection->expects($this->once())->method('write')->with($this->stringContains("test database\0"));

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
        $connector->expects($this->once())->method('connect')->with('127.0.0.1:3306')->willReturn(\React\Promise\resolve($connection));

        $factory = new Factory($loop, $connector);
        $promise = $factory->createConnection('mysql://127.0.0.1/test%20database');

        $promise->then($this->expectCallableNever(), $this->expectCallableNever());

        $connection->emit('data', ["\x33\0\0\0" . "\x0a" . "mysql\0" . str_repeat("\0", 44)]);
    }

    public function testConnectWithInvalidHostRejectsWithConnectionError()
    {
        $factory = new Factory();

        $uri = $this->getConnectionString(['host' => 'example.invalid']);
        $promise = $factory->createConnection($uri);

        $promise->then(null, $this->expectCallableOnce());

        Loop::run();
    }

    public function testConnectWithInvalidPassRejectsWithAuthenticationError()
    {
        $factory = new Factory();

        $uri = $this->getConnectionString(['passwd' => 'invalidpass']);
        $promise = $factory->createConnection($uri);

        $promise->then(null, $this->expectCallableOnceWith(
            $this->logicalAnd(
                $this->isInstanceOf('RuntimeException'),
                $this->callback(function (\RuntimeException $e) {
                    return !!preg_match("/^Connection to mysql:\/\/[^ ]* failed during authentication: Access denied for user '.*?'@'.*?' \(using password: YES\) \(EACCES\)$/", $e->getMessage());
                }),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getCode() === (defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
                }),
                $this->callback(function (\RuntimeException $e) {
                    return !!preg_match("/^Access denied for user '.*?'@'.*?' \(using password: YES\)$/", $e->getPrevious()->getMessage());
                })
            )
        ));

        Loop::run();
    }

    public function testConnectWillRejectWhenServerClosesConnection()
    {
        $factory = new Factory();

        $socket = new SocketServer('127.0.0.1:0', []);
        $socket->on('connection', function ($connection) use ($socket) {
            $socket->close();
            $connection->close();
        });

        $parts = parse_url($socket->getAddress());
        $uri = $this->getConnectionString(['host' => $parts['host'], 'port' => $parts['port']]);

        $promise = $factory->createConnection($uri);

        $uri = preg_replace('/:[^:]*@/', ':***@', $uri);

        $promise->then(null, $this->expectCallableOnceWith(
            $this->logicalAnd(
                $this->isInstanceOf('RuntimeException'),
                $this->callback(function (\RuntimeException $e) use ($uri) {
                    return $e->getMessage() === 'Connection to ' . $uri . ' failed during authentication: Connection closed by peer (ECONNRESET)';
                }),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getCode() === (defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104);
                })
            )
        ));

        Loop::run();
    }

    public function testConnectWillRejectOnExplicitTimeoutDespiteValidAuth()
    {
        $factory = new Factory();

        $uri = 'mysql://' . $this->getConnectionString() . '?timeout=0';

        $promise = $factory->createConnection($uri);

        $uri = preg_replace('/:[^:]*@/', ':***@', $uri);

        $promise->then(null, $this->expectCallableOnceWith(
            $this->logicalAnd(
                $this->isInstanceOf('RuntimeException'),
                $this->callback(function (\RuntimeException $e) use ($uri) {
                    return $e->getMessage() === 'Connection to ' . $uri . ' timed out after 0 seconds (ETIMEDOUT)';
                }),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getCode() === (defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110);
                })
            )
        ));

        Loop::run();
    }

    public function testConnectWillRejectOnDefaultTimeoutFromIniDespiteValidAuth()
    {
        $factory = new Factory();

        $uri = $this->getConnectionString();

        $old = ini_get('default_socket_timeout');
        ini_set('default_socket_timeout', '0');
        $promise = $factory->createConnection($uri);
        ini_set('default_socket_timeout', $old);

        $uri = preg_replace('/:[^:]*@/', ':***@', $uri);

        $promise->then(null, $this->expectCallableOnceWith(
            $this->logicalAnd(
                $this->isInstanceOf('RuntimeException'),
                $this->callback(function (\RuntimeException $e) use ($uri) {
                    return $e->getMessage() === 'Connection to ' . $uri . ' timed out after 0 seconds (ETIMEDOUT)';
                }),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getCode() === (defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110);
                })
            )
        ));

        Loop::run();
    }

    public function testConnectWithValidAuthWillRunUntilQuit()
    {
        $this->expectOutputString('connected.closed.');

        $factory = new Factory();

        $uri = $this->getConnectionString();
        $factory->createConnection($uri)->then(function (Connection $connection) {
            echo 'connected.';
            $connection->quit()->then(function () {
                echo 'closed.';
            });
        });

        Loop::run();
    }

    public function testConnectWithValidAuthAndWithoutDbNameWillRunUntilQuit()
    {
        $this->expectOutputString('connected.closed.');

        $factory = new Factory();

        $uri = $this->getConnectionString(['dbname' => '']);
        $factory->createConnection($uri)->then(function (Connection $connection) {
            echo 'connected.';
            $connection->quit()->then(function () {
                echo 'closed.';
            });
        });

        Loop::run();
    }

    public function testConnectWithValidAuthWillIgnoreNegativeTimeoutAndRunUntilQuit()
    {
        $this->expectOutputString('connected.closed.');

        $factory = new Factory();

        $uri = $this->getConnectionString() . '?timeout=-1';
        $factory->createConnection($uri)->then(function (Connection $connection) {
            echo 'connected.';
            $connection->quit()->then(function () {
                echo 'closed.';
            });
        });

        Loop::run();
    }

    public function testConnectWithValidAuthCanPingAndThenQuit()
    {
        $this->expectOutputString('connected.ping.closed.');

        $factory = new Factory();

        $uri = $this->getConnectionString();
        $factory->createConnection($uri)->then(function (Connection $connection) {
            echo 'connected.';
            $connection->ping()->then(function () use ($connection) {
                echo 'ping.';
                $connection->quit()->then(function () {
                    echo 'closed.';
                });
            });
        });

        Loop::run();
    }

    public function testConnectWithValidAuthCanQueuePingAndQuit()
    {
        $this->expectOutputString('connected.ping.closed.');

        $factory = new Factory();

        $uri = $this->getConnectionString();
        $factory->createConnection($uri)->then(function (Connection $connection) {
            echo 'connected.';
            $connection->ping()->then(function () {
                echo 'ping.';
            });
            $connection->quit()->then(function () {
                echo 'closed.';
            });
        });

        Loop::run();
    }

    public function testConnectWithValidAuthQuitOnlyOnce()
    {
        $this->expectOutputString('connected.rejected.closed.');

        $factory = new Factory();

        $uri = $this->getConnectionString();
        $factory->createConnection($uri)->then(function (Connection $connection) {
            echo 'connected.';
            $connection->quit()->then(function () {
                echo 'closed.';
            });
            $connection->quit()->then(function () {
                echo 'never reached.';
            }, function () {
                echo 'rejected.';
            });
        });

        Loop::run();
    }

    public function testConnectWithValidAuthCanCloseOnlyOnce()
    {
        $this->expectOutputString('connected.closed.');

        $factory = new Factory();

        $uri = $this->getConnectionString();
        $factory->createConnection($uri)->then(function (Connection $connection) {
            echo 'connected.';
            $connection->on('close', function () {
                echo 'closed.';
            });
            $connection->on('error', function () {
                echo 'error?';
            });

            $connection->close();
            $connection->close();
        });

        Loop::run();
    }

    public function testConnectWithValidAuthCanCloseAndAbortPing()
    {
        $this->expectOutputString('connected.aborted pending (Connection closing (ECONNABORTED)).aborted queued (Connection closing (ECONNABORTED)).closed.');

        $factory = new Factory();

        $uri = $this->getConnectionString();
        $factory->createConnection($uri)->then(function (Connection $connection) {
            echo 'connected.';
            $connection->on('close', function () {
                echo 'closed.';
            });
            $connection->on('error', function () {
                echo 'error?';
            });

            $connection->ping()->then(null, function ($e) {
                echo 'aborted pending (' . $e->getMessage() .').';
            });
            $connection->ping()->then(null, function ($e) {
                echo 'aborted queued (' . $e->getMessage() . ').';
            });
            $connection->close();
        });

        Loop::run();
    }

    public function testlConnectWillRejectWhenUnderlyingConnectorRejects()
    {
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
        $connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException('Failed', 123)));

        $factory = new Factory($loop, $connector);
        $promise = $factory->createConnection('mysql://user:secret@127.0.0.1');

        $promise->then(null, $this->expectCallableOnceWith(
            $this->logicalAnd(
                $this->isInstanceOf('RuntimeException'),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getMessage() === 'Connection to mysql://user:***@127.0.0.1 failed: Failed';
                }),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getCode() === 123;
                })
            )
        ));
    }

    public function provideUris()
    {
        return [
            [
                'mysql://localhost',
                'mysql://localhost'
            ],
            [
                'mysql://user:pass@localhost',
                'mysql://user:***@localhost'
            ],
            [
                'mysql://user:@localhost',
                'mysql://user:***@localhost'
            ],
            [
                'mysql://user@localhost',
                'mysql://user@localhost'
            ]
        ];
    }

    /**
     * @dataProvider provideUris
     * @param string $uri
     * @param string $safe
     */
    public function testCancelConnectWillCancelPendingConnection($uri, $safe)
    {
        $pending = new Promise(function () { }, $this->expectCallableOnce());
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
        $connector->expects($this->once())->method('connect')->willReturn($pending);

        $factory = new Factory($loop, $connector);
        $promise = $factory->createConnection($uri);

        $promise->cancel();

        $promise->then(null, $this->expectCallableOnceWith(
            $this->logicalAnd(
                $this->isInstanceOf('RuntimeException'),
                $this->callback(function (\RuntimeException $e) use ($safe) {
                    return $e->getMessage() === 'Connection to ' . $safe . ' cancelled (ECONNABORTED)';
                }),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getCode() === (defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103);
                })
            )
        ));
    }

    public function testCancelConnectWillCancelPendingConnectionWithRuntimeException()
    {
        $pending = new Promise(function () { }, function () {
            throw new \UnexpectedValueException('ignored');
        });
        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
        $connector->expects($this->once())->method('connect')->willReturn($pending);

        $factory = new Factory($loop, $connector);
        $promise = $factory->createConnection('mysql://127.0.0.1');

        $promise->cancel();

        $promise->then(null, $this->expectCallableOnceWith(
            $this->logicalAnd(
                $this->isInstanceOf('RuntimeException'),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getMessage() === 'Connection to mysql://127.0.0.1 cancelled (ECONNABORTED)';
                }),
                $this->callback(function (\RuntimeException $e) {
                    return $e->getCode() === (defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103);
                })
            )
        ));
    }

    public function testCancelConnectDuringAuthenticationWillCloseConnection()
    {
        $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
        $connection->expects($this->once())->method('close');

        $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
        $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
        $connector->expects($t
Download .txt
gitextract_ldg5u_7o/

├── .gitattributes
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── examples/
│   ├── 01-query.php
│   ├── 02-query-stream.php
│   ├── 11-interactive.php
│   └── 12-slow-stream.php
├── phpunit.xml.dist
├── phpunit.xml.legacy
├── src/
│   ├── Commands/
│   │   ├── AbstractCommand.php
│   │   ├── AuthenticateCommand.php
│   │   ├── CommandInterface.php
│   │   ├── PingCommand.php
│   │   ├── QueryCommand.php
│   │   └── QuitCommand.php
│   ├── Exception.php
│   ├── Io/
│   │   ├── Buffer.php
│   │   ├── Connection.php
│   │   ├── Constants.php
│   │   ├── Executor.php
│   │   ├── Factory.php
│   │   ├── Parser.php
│   │   ├── Query.php
│   │   └── QueryStream.php
│   ├── MysqlClient.php
│   └── MysqlResult.php
└── tests/
    ├── BaseTestCase.php
    ├── Commands/
    │   └── AuthenticateCommandTest.php
    ├── Io/
    │   ├── BufferTest.php
    │   ├── ConnectionTest.php
    │   ├── FactoryTest.php
    │   ├── ParserTest.php
    │   ├── QueryStreamTest.php
    │   └── QueryTest.php
    ├── MysqlClientTest.php
    ├── NoResultQueryTest.php
    ├── ResultQueryTest.php
    └── wait-for-mysql.sh
Download .txt
SYMBOL INDEX (383 symbols across 28 files)

FILE: src/Commands/AbstractCommand.php
  class AbstractCommand (line 10) | abstract class AbstractCommand extends EventEmitter implements CommandIn...

FILE: src/Commands/AuthenticateCommand.php
  class AuthenticateCommand (line 12) | class AuthenticateCommand extends AbstractCommand
    method __construct (line 54) | public function __construct(
    method getId (line 71) | public function getId()
    method authenticatePacket (line 83) | public function authenticatePacket($scramble, $authPlugin, Buffer $buf...
    method authResponse (line 112) | public function authResponse($scramble, $authPlugin)
    method authMysqlNativePassword (line 127) | private function authMysqlNativePassword($scramble)
    method authCachingSha2Password (line 141) | private function authCachingSha2Password($scramble)
    method authSha256 (line 161) | public function authSha256($scramble, $pubkey)

FILE: src/Commands/CommandInterface.php
  type CommandInterface (line 10) | interface CommandInterface extends EventEmitterInterface
    method getId (line 12) | public function getId();

FILE: src/Commands/PingCommand.php
  class PingCommand (line 8) | class PingCommand extends AbstractCommand
    method getId (line 10) | public function getId()
    method getSql (line 15) | public function getSql()

FILE: src/Commands/QueryCommand.php
  class QueryCommand (line 10) | class QueryCommand extends AbstractCommand
    method getId (line 18) | public function getId()
    method getQuery (line 23) | public function getQuery()
    method setQuery (line 28) | public function setQuery($query)
    method getSql (line 39) | public function getSql()

FILE: src/Commands/QuitCommand.php
  class QuitCommand (line 8) | class QuitCommand extends AbstractCommand
    method getId (line 10) | public function getId()
    method getSql (line 15) | public function getSql()

FILE: src/Exception.php
  class Exception (line 5) | class Exception extends \Exception

FILE: src/Io/Buffer.php
  class Buffer (line 7) | class Buffer
    method append (line 18) | public function append($str)
    method prepend (line 29) | public function prepend($str)
    method read (line 42) | public function read($len)
    method readBuffer (line 75) | public function readBuffer($len)
    method skip (line 111) | public function skip($len)
    method length (line 124) | public function length()
    method readInt1 (line 132) | public function readInt1()
    method readInt2 (line 140) | public function readInt2()
    method readInt3 (line 149) | public function readInt3()
    method readInt4 (line 158) | public function readInt4()
    method readInt8 (line 168) | public function readInt8()
    method readIntLen (line 185) | public function readIntLen()
    method readStringLen (line 209) | public function readStringLen()
    method readStringNull (line 225) | public function readStringNull()
    method buildInt1 (line 242) | public function buildInt1($int)
    method buildInt2 (line 251) | public function buildInt2($int)
    method buildInt3 (line 260) | public function buildInt3($int)
    method buildInt8 (line 270) | public function buildInt8($int)
    method buildStringLen (line 285) | public function buildStringLen($s)

FILE: src/Io/Connection.php
  class Connection (line 21) | class Connection extends EventEmitter
    method __construct (line 67) | public function __construct(SocketConnectionInterface $stream, Executo...
    method isBusy (line 88) | public function isBusy()
    method query (line 93) | public function query(Query $query)
    method queryStream (line 141) | public function queryStream(Query $query)
    method ping (line 156) | public function ping()
    method quit (line 173) | public function quit()
    method close (line 193) | public function close()
    method handleConnectionError (line 236) | public function handleConnectionError($err)
    method handleConnectionClosed (line 245) | public function handleConnectionClosed()
    method _doCommand (line 262) | protected function _doCommand(CommandInterface $command)
    method awake (line 274) | private function awake()
    method idle (line 284) | private function idle()

FILE: src/Io/Constants.php
  class Constants (line 8) | class Constants

FILE: src/Io/Executor.php
  class Executor (line 10) | class Executor extends EventEmitter
    method __construct (line 14) | public function __construct()
    method isIdle (line 19) | public function isIdle()
    method enqueue (line 24) | public function enqueue($command)
    method dequeue (line 32) | public function dequeue()

FILE: src/Io/Factory.php
  class Factory (line 20) | class Factory
    method __construct (line 63) | public function __construct($loop = null, $connector = null)
    method createConnection (line 164) | public function createConnection(

FILE: src/Io/Parser.php
  class Parser (line 14) | class Parser
    method __construct (line 113) | public function __construct(DuplexStreamInterface $stream, Executor $e...
    method isBusy (line 130) | public function isBusy()
    method start (line 135) | public function start()
    method debug (line 141) | public function debug($message)
    method handleData (line 151) | public function handleData($data)
    method parsePacket (line 195) | private function parsePacket(Buffer $packet)
    method onResultRow (line 371) | private function onResultRow($row)
    method onError (line 378) | private function onError(\Exception $error)
    method onResultDone (line 393) | protected function onResultDone()
    method onSuccess (line 406) | protected function onSuccess()
    method onClose (line 419) | public function onClose()
    method sendPacket (line 436) | public function sendPacket($packet)
    method nextRequest (line 441) | protected function nextRequest($isHandshake = false)

FILE: src/Io/Query.php
  class Query (line 8) | class Query
    method __construct (line 49) | public function __construct($sql, array $params = [])
    method escape (line 62) | public function escape($str)
    method resolveValueForSql (line 71) | protected function resolveValueForSql($value)
    method buildSql (line 92) | protected function buildSql()
    method getSql (line 170) | public function getSql()

FILE: src/Io/QueryStream.php
  class QueryStream (line 16) | class QueryStream extends EventEmitter implements ReadableStreamInterface
    method __construct (line 23) | public function __construct(QueryCommand $command, ConnectionInterface...
    method isReadable (line 52) | public function isReadable()
    method pause (line 57) | public function pause()
    method resume (line 65) | public function resume()
    method close (line 73) | public function close()
    method pipe (line 88) | public function pipe(WritableStreamInterface $dest, array $options = [])

FILE: src/MysqlClient.php
  class MysqlClient (line 53) | class MysqlClient extends EventEmitter
    method __construct (line 87) | public function __construct(
    method query (line 187) | public function query($sql, array $params = [])
    method queryStream (line 267) | public function queryStream($sql, array $params = [])
    method ping (line 311) | public function ping()
    method quit (line 349) | public function quit()
    method close (line 399) | public function close()
    method getConnection (line 432) | private function getConnection()
    method handleConnectionReady (line 478) | private function handleConnectionReady(Connection $connection)

FILE: src/MysqlResult.php
  class MysqlResult (line 5) | class MysqlResult

FILE: tests/BaseTestCase.php
  class BaseTestCase (line 10) | class BaseTestCase extends TestCase
    method getConnectionOptions (line 12) | protected function getConnectionOptions($debug = false)
    method getConnectionString (line 24) | protected function getConnectionString($params = [])
    method createConnection (line 35) | protected function createConnection(LoopInterface $loop)
    method getDataTable (line 43) | protected function getDataTable()
    method expectCallableOnce (line 57) | protected function expectCallableOnce()
    method expectCallableOnceWith (line 65) | protected function expectCallableOnceWith($value)
    method expectCallableNever (line 73) | protected function expectCallableNever()
    method setExpectedException (line 81) | public function setExpectedException($exception, $exceptionMessage = '...

FILE: tests/Commands/AuthenticateCommandTest.php
  class AuthenticateCommandTest (line 9) | class AuthenticateCommandTest extends TestCase
    method testCtorWithKnownCharset (line 14) | public function testCtorWithKnownCharset()
    method testCtorWithUnknownCharsetThrows (line 19) | public function testCtorWithUnknownCharsetThrows()
    method testAuthenticatePacketWithEmptyPassword (line 30) | public function testAuthenticatePacketWithEmptyPassword()
    method testAuthenticatePacketWithMysqlNativePasswordAuthPluginAndEmptyPassword (line 39) | public function testAuthenticatePacketWithMysqlNativePasswordAuthPlugi...
    method testAuthenticatePacketWithCachingSha2PasswordAuthPluginAndEmptyPassword (line 48) | public function testAuthenticatePacketWithCachingSha2PasswordAuthPlugi...
    method testAuthenticatePacketWithSecretPassword (line 57) | public function testAuthenticatePacketWithSecretPassword()
    method testAuthenticatePacketWithCachingSha2PasswordWithSecretPasswordHashed (line 70) | public function testAuthenticatePacketWithCachingSha2PasswordWithSecre...
    method testAuthenticatePacketWithUnknownAuthPluginThrows (line 79) | public function testAuthenticatePacketWithUnknownAuthPluginThrows()
    method testAuthSha256WithValidPublicKeyReturnsScrambledDataThatCanBeDecryptedByReceiverWithPrivateKey (line 96) | public function testAuthSha256WithValidPublicKeyReturnsScrambledDataTh...
    method testAuthSha256WithPasswordLongerThanScrambleLengthReturnsScrambledDataThatCanBeDecryptedByReceiverWithPrivateKey (line 114) | public function testAuthSha256WithPasswordLongerThanScrambleLengthRetu...
    method testAuthSha256WithInvalidPublicKeyThrows (line 132) | public function testAuthSha256WithInvalidPublicKeyThrows()

FILE: tests/Io/BufferTest.php
  class BufferTest (line 8) | class BufferTest extends BaseTestCase
    method testAppendAndReadBinary (line 10) | public function testAppendAndReadBinary()
    method testReadBeyondLimitThrows (line 19) | public function testReadBeyondLimitThrows()
    method testReadAfterSkipOne (line 29) | public function testReadAfterSkipOne()
    method testReadBufferEmptyIsNoop (line 39) | public function testReadBufferEmptyIsNoop()
    method testReadBufferReturnsBufferWithOriginalLengthAndClearsOriginalBuffer (line 49) | public function testReadBufferReturnsBufferWithOriginalLengthAndClears...
    method testReadBufferBeyondLimitThrows (line 60) | public function testReadBufferBeyondLimitThrows()
    method testSkipZeroThrows (line 68) | public function testSkipZeroThrows()
    method testSkipBeyondLimitThrows (line 78) | public function testSkipBeyondLimitThrows()
    method testParseInt1 (line 88) | public function testParseInt1()
    method testParseInt2 (line 98) | public function testParseInt2()
    method testParseInt3 (line 108) | public function testParseInt3()
    method testParseInt8 (line 118) | public function testParseInt8()
    method testParseIntLen (line 128) | public function testParseIntLen()
    method testParseStringEmpty (line 138) | public function testParseStringEmpty()
    method testParseStringShort (line 149) | public function testParseStringShort()
    method testParseStringKilo (line 160) | public function testParseStringKilo()
    method testParseStringMega (line 169) | public function testParseStringMega()
    method testParseStringExcessive (line 183) | public function testParseStringExcessive()
    method testParseStringNullLength (line 192) | public function testParseStringNullLength()
    method testParseStringNullCharacterTwice (line 203) | public function testParseStringNullCharacterTwice()
    method testParseStringNullCharacterThrowsIfNullNotFound (line 212) | public function testParseStringNullCharacterThrowsIfNullNotFound()

FILE: tests/Io/ConnectionTest.php
  class ConnectionTest (line 9) | class ConnectionTest extends BaseTestCase
    method testIsBusyReturnsTrueWhenParserIsBusy (line 11) | public function testIsBusyReturnsTrueWhenParserIsBusy()
    method testIsBusyReturnsFalseWhenParserIsNotBusyAndExecutorIsIdle (line 31) | public function testIsBusyReturnsFalseWhenParserIsNotBusyAndExecutorIs...
    method testQueryWillEnqueueOneCommand (line 47) | public function testQueryWillEnqueueOneCommand()
    method testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenQueryCommandEmitsSuccess (line 64) | public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhe...
    method testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenQueryCommandEmitsEnd (line 93) | public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhe...
    method testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenIdlePeriodIsGivenAndQueryCommandEmitsSuccess (line 122) | public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhe...
    method testQueryWillReturnResolvedPromiseAndNotStartIdleTimerWhenIdlePeriodIsNegativeAndQueryCommandEmitsSuccess (line 151) | public function testQueryWillReturnResolvedPromiseAndNotStartIdleTimer...
    method testQueryWillReturnRejectedPromiseAndStartIdleTimerWhenQueryCommandEmitsError (line 178) | public function testQueryWillReturnRejectedPromiseAndStartIdleTimerWhe...
    method testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitCloseEventWhenQuitCommandEmitsSuccess (line 207) | public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnecti...
    method testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitCloseEventWhenQuitCommandEmitsError (line 246) | public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnecti...
    method testQueryTwiceWillEnqueueSecondQueryWithoutStartingIdleTimerWhenFirstQueryCommandEmitsSuccess (line 285) | public function testQueryTwiceWillEnqueueSecondQueryWithoutStartingIdl...
    method testQueryTwiceAfterIdleTimerWasStartedWillCancelIdleTimerAndEnqueueSecondCommand (line 311) | public function testQueryTwiceAfterIdleTimerWasStartedWillCancelIdleTi...
    method testQueryStreamWillEnqueueOneCommand (line 340) | public function testQueryStreamWillEnqueueOneCommand()
    method testQueryStreamWillReturnStreamThatWillEmitEndEventAndStartIdleTimerWhenQueryCommandEmitsSuccess (line 357) | public function testQueryStreamWillReturnStreamThatWillEmitEndEventAnd...
    method testQueryStreamWillReturnStreamThatWillEmitErrorEventAndStartIdleTimerWhenQueryCommandEmitsError (line 387) | public function testQueryStreamWillReturnStreamThatWillEmitErrorEventA...
    method testPingWillEnqueueOneCommand (line 417) | public function testPingWillEnqueueOneCommand()
    method testPingWillReturnResolvedPromiseAndStartIdleTimerWhenPingCommandEmitsSuccess (line 434) | public function testPingWillReturnResolvedPromiseAndStartIdleTimerWhen...
    method testPingWillReturnRejectedPromiseAndStartIdleTimerWhenPingCommandEmitsError (line 463) | public function testPingWillReturnRejectedPromiseAndStartIdleTimerWhen...
    method testQuitWillEnqueueOneCommand (line 492) | public function testQuitWillEnqueueOneCommand()
    method testQuitWillResolveBeforeEmittingCloseEventWhenQuitCommandEmitsSuccess (line 507) | public function testQuitWillResolveBeforeEmittingCloseEventWhenQuitCom...
    method testQuitWillRejectBeforeEmittingCloseEventWhenQuitCommandEmitsError (line 545) | public function testQuitWillRejectBeforeEmittingCloseEventWhenQuitComm...
    method testCloseWillEmitCloseEvent (line 583) | public function testCloseWillEmitCloseEvent()
    method testCloseAfterIdleTimerWasStartedWillCancelIdleTimerAndEmitCloseEvent (line 602) | public function testCloseAfterIdleTimerWasStartedWillCancelIdleTimerAn...
    method testQueryAfterQuitRejectsImmediately (line 633) | public function testQueryAfterQuitRejectsImmediately()
    method testQueryAfterCloseRejectsImmediately (line 660) | public function testQueryAfterCloseRejectsImmediately()
    method testQueryStreamAfterQuitThrows (line 687) | public function testQueryStreamAfterQuitThrows()
    method testPingAfterQuitRejectsImmediately (line 708) | public function testPingAfterQuitRejectsImmediately()
    method testQuitAfterQuitRejectsImmediately (line 735) | public function testQuitAfterQuitRejectsImmediately()
    method testCloseStreamEmitsErrorEvent (line 762) | public function testCloseStreamEmitsErrorEvent()

FILE: tests/Io/FactoryTest.php
  class FactoryTest (line 12) | class FactoryTest extends BaseTestCase
    method testConstructWithoutLoopAssignsLoopAutomatically (line 14) | public function testConstructWithoutLoopAssignsLoopAutomatically()
    method testConnectWillUseHostAndDefaultPort (line 25) | public function testConnectWillUseHostAndDefaultPort()
    method testConnectWillUseGivenScheme (line 36) | public function testConnectWillUseGivenScheme()
    method testConnectWillUseGivenHostAndGivenPort (line 47) | public function testConnectWillUseGivenHostAndGivenPort()
    method testConnectWillUseGivenUserInfoAsDatabaseCredentialsAfterUrldecoding (line 58) | public function testConnectWillUseGivenUserInfoAsDatabaseCredentialsAf...
    method testConnectWillUseGivenPathAsDatabaseNameAfterUrldecoding (line 75) | public function testConnectWillUseGivenPathAsDatabaseNameAfterUrldecod...
    method testConnectWithInvalidHostRejectsWithConnectionError (line 92) | public function testConnectWithInvalidHostRejectsWithConnectionError()
    method testConnectWithInvalidPassRejectsWithAuthenticationError (line 104) | public function testConnectWithInvalidPassRejectsWithAuthenticationErr...
    method testConnectWillRejectWhenServerClosesConnection (line 129) | public function testConnectWillRejectWhenServerClosesConnection()
    method testConnectWillRejectOnExplicitTimeoutDespiteValidAuth (line 161) | public function testConnectWillRejectOnExplicitTimeoutDespiteValidAuth()
    method testConnectWillRejectOnDefaultTimeoutFromIniDespiteValidAuth (line 186) | public function testConnectWillRejectOnDefaultTimeoutFromIniDespiteVal...
    method testConnectWithValidAuthWillRunUntilQuit (line 214) | public function testConnectWithValidAuthWillRunUntilQuit()
    method testConnectWithValidAuthAndWithoutDbNameWillRunUntilQuit (line 231) | public function testConnectWithValidAuthAndWithoutDbNameWillRunUntilQu...
    method testConnectWithValidAuthWillIgnoreNegativeTimeoutAndRunUntilQuit (line 248) | public function testConnectWithValidAuthWillIgnoreNegativeTimeoutAndRu...
    method testConnectWithValidAuthCanPingAndThenQuit (line 265) | public function testConnectWithValidAuthCanPingAndThenQuit()
    method testConnectWithValidAuthCanQueuePingAndQuit (line 285) | public function testConnectWithValidAuthCanQueuePingAndQuit()
    method testConnectWithValidAuthQuitOnlyOnce (line 305) | public function testConnectWithValidAuthQuitOnlyOnce()
    method testConnectWithValidAuthCanCloseOnlyOnce (line 327) | public function testConnectWithValidAuthCanCloseOnlyOnce()
    method testConnectWithValidAuthCanCloseAndAbortPing (line 350) | public function testConnectWithValidAuthCanCloseAndAbortPing()
    method testlConnectWillRejectWhenUnderlyingConnectorRejects (line 378) | public function testlConnectWillRejectWhenUnderlyingConnectorRejects()
    method provideUris (line 400) | public function provideUris()
    method testCancelConnectWillCancelPendingConnection (line 427) | public function testCancelConnectWillCancelPendingConnection($uri, $safe)
    method testCancelConnectWillCancelPendingConnectionWithRuntimeException (line 452) | public function testCancelConnectWillCancelPendingConnectionWithRuntim...
    method testCancelConnectDuringAuthenticationWillCloseConnection (line 479) | public function testCancelConnectDuringAuthenticationWillCloseConnecti...

FILE: tests/Io/ParserTest.php
  class ParserTest (line 14) | class ParserTest extends BaseTestCase
    method testClosingStreamEmitsErrorForCurrentCommand (line 16) | public function testClosingStreamEmitsErrorForCurrentCommand()
    method testParseValidAuthPluginWillSendAuthResponse (line 46) | public function testParseValidAuthPluginWillSendAuthResponse()
    method testUnexpectedAuthPluginShouldEmitErrorOnAuthenticateCommandAndCloseStream (line 69) | public function testUnexpectedAuthPluginShouldEmitErrorOnAuthenticateC...
    method testParseAuthSwitchRequestWillSendAuthSwitchResponsePacket (line 86) | public function testParseAuthSwitchRequestWillSendAuthSwitchResponsePa...
    method testParseAuthSwitchRequestWithUnexpectedAuthPluginWillEmitErrorAndCloseConnection (line 113) | public function testParseAuthSwitchRequestWithUnexpectedAuthPluginWill...
    method testParseAuthMoreDataWithFastAuthSuccessWillPrintDebugLogAndWaitForOkPacketWithoutSendingPacket (line 140) | public function testParseAuthMoreDataWithFastAuthSuccessWillPrintDebug...
    method testParseAuthMoreDataWithFastAuthFailureWillSendCertificateRequest (line 169) | public function testParseAuthMoreDataWithFastAuthFailureWillSendCertif...
    method testParseAuthMoreDataWithCertificateWillSendEncryptedPassword (line 193) | public function testParseAuthMoreDataWithCertificateWillSendEncryptedP...
    method testParseAuthMoreDataWithCertificateWillEmitErrorAndCloseConnectionWhenEncryptingPasswordThrows (line 224) | public function testParseAuthMoreDataWithCertificateWillEmitErrorAndCl...
    method testUnexpectedErrorWithoutCurrentCommandWillBeIgnored (line 256) | public function testUnexpectedErrorWithoutCurrentCommandWillBeIgnored()
    method testReceivingErrorFrameDuringHandshakeShouldEmitErrorOnFollowingCommand (line 271) | public function testReceivingErrorFrameDuringHandshakeShouldEmitErrorO...
    method testReceivingErrorFrameForQueryShouldEmitError (line 296) | public function testReceivingErrorFrameForQueryShouldEmitError()
    method testReceivingErrorFrameForQueryAfterResultSetHeadersShouldEmitError (line 324) | public function testReceivingErrorFrameForQueryAfterResultSetHeadersSh...
    method testReceivingInvalidPacketWithMissingDataShouldEmitErrorAndCloseConnection (line 363) | public function testReceivingInvalidPacketWithMissingDataShouldEmitErr...
    method testReceivingInvalidPacketWithExcessiveDataShouldEmitErrorAndCloseConnection (line 396) | public function testReceivingInvalidPacketWithExcessiveDataShouldEmitE...
    method testReceivingIncompleteErrorFrameDuringHandshakeShouldNotEmitError (line 429) | public function testReceivingIncompleteErrorFrameDuringHandshakeShould...

FILE: tests/Io/QueryStreamTest.php
  class QueryStreamTest (line 9) | class QueryStreamTest extends BaseTestCase
    method testDataEventWillBeForwardedFromCommandResult (line 11) | public function testDataEventWillBeForwardedFromCommandResult()
    method testDataEventWillNotBeForwardedFromCommandResultAfterClosingStream (line 22) | public function testDataEventWillNotBeForwardedFromCommandResultAfterC...
    method testEndEventWillBeForwardedFromCommandResult (line 34) | public function testEndEventWillBeForwardedFromCommandResult()
    method testSuccessEventWillBeForwardedFromCommandResultAsEndWithoutData (line 46) | public function testSuccessEventWillBeForwardedFromCommandResultAsEndW...
    method testErrorEventWillBeForwardedFromCommandResult (line 59) | public function testErrorEventWillBeForwardedFromCommandResult()
    method testPauseForwardsToConnectionAfterResultStarted (line 71) | public function testPauseForwardsToConnectionAfterResultStarted()
    method testPauseForwardsToConnectionWhenResultStarted (line 83) | public function testPauseForwardsToConnectionWhenResultStarted()
    method testPauseDoesNotForwardToConnectionWhenResultIsNotStarted (line 95) | public function testPauseDoesNotForwardToConnectionWhenResultIsNotStar...
    method testPauseDoesNotForwardToConnectionAfterClosing (line 105) | public function testPauseDoesNotForwardToConnectionAfterClosing()
    method testResumeForwardsToConnectionAfterResultStarted (line 116) | public function testResumeForwardsToConnectionAfterResultStarted()
    method testResumeDoesNotForwardToConnectionAfterClosing (line 128) | public function testResumeDoesNotForwardToConnectionAfterClosing()
    method testPipeReturnsDestStream (line 139) | public function testPipeReturnsDestStream()
    method testCloseTwiceEmitsCloseEventOnce (line 152) | public function testCloseTwiceEmitsCloseEventOnce()
    method testCloseForwardsResumeToConnectionIfPreviouslyPaused (line 164) | public function testCloseForwardsResumeToConnectionIfPreviouslyPaused()
    method testCloseDoesNotResumeConnectionIfNotPreviouslyPaused (line 176) | public function testCloseDoesNotResumeConnectionIfNotPreviouslyPaused()
    method testCloseDoesNotResumeConnectionIfPreviouslyPausedWhenResultIsNotActive (line 186) | public function testCloseDoesNotResumeConnectionIfPreviouslyPausedWhen...

FILE: tests/Io/QueryTest.php
  class QueryTest (line 8) | class QueryTest extends BaseTestCase
    method testCtorThrowsForInvalidParams (line 10) | public function testCtorThrowsForInvalidParams()
    method testBindParams (line 16) | public function testBindParams()
    method testGetSqlReturnsQuestionMarkReplacedWhenBound (line 32) | public function testGetSqlReturnsQuestionMarkReplacedWhenBound()
    method testGetSqlReturnsQuestionMarkReplacedWithNullValueWhenBound (line 38) | public function testGetSqlReturnsQuestionMarkReplacedWithNullValueWhen...
    method testGetSqlReturnsQuestionMarkReplacedFromBoundWhenBound (line 44) | public function testGetSqlReturnsQuestionMarkReplacedFromBoundWhenBound()
    method testGetSqlReturnsQuestionMarksAsIsWhenNotBound (line 50) | public function testGetSqlReturnsQuestionMarksAsIsWhenNotBound()
    method testEscapeChars (line 56) | public function testEscapeChars()

FILE: tests/MysqlClientTest.php
  class MysqlClientTest (line 15) | class MysqlClientTest extends BaseTestCase
    method testConstructWithoutConnectorAndLoopAssignsConnectorAndLoopAutomatically (line 17) | public function testConstructWithoutConnectorAndLoopAssignsConnectorAn...
    method testConstructWithConnectorAndLoopAssignsGivenConnectorAndLoop (line 38) | public function testConstructWithConnectorAndLoopAssignsGivenConnector...
    method provideInvalidUris (line 60) | public static function provideInvalidUris()
    method testContructorThrowsExceptionForInvalidUri (line 111) | public function testContructorThrowsExceptionForInvalidUri($uri, $mess...
    method testContructorThrowsExceptionForInvalidCharset (line 121) | public function testContructorThrowsExceptionForInvalidCharset()
    method testContructorThrowsExceptionForInvalidConnector (line 127) | public function testContructorThrowsExceptionForInvalidConnector()
    method testContructorThrowsExceptionForInvalidLoop (line 133) | public function testContructorThrowsExceptionForInvalidLoop()
    method testPingWillNotCloseConnectionWhenPendingConnectionFails (line 139) | public function testPingWillNotCloseConnectionWhenPendingConnectionFai...
    method testConnectionCloseEventAfterPingWillNotEmitCloseEvent (line 162) | public function testConnectionCloseEventAfterPingWillNotEmitCloseEvent()
    method testConnectionErrorEventAfterPingWillNotEmitErrorEvent (line 186) | public function testConnectionErrorEventAfterPingWillNotEmitErrorEvent()
    method testPingAfterConnectionIsInClosingStateDueToIdleTimerWillCloseConnectionBeforeCreatingSecondConnection (line 210) | public function testPingAfterConnectionIsInClosingStateDueToIdleTimerW...
    method testQueryWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionIsPending (line 241) | public function testQueryWillCreateNewConnectionAndReturnPendingPromis...
    method testQueryWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionResolvesAndQueryOnConnectionIsPending (line 259) | public function testQueryWillCreateNewConnectionAndReturnPendingPromis...
    method testQueryWillReturnResolvedPromiseWhenQueryOnConnectionResolves (line 279) | public function testQueryWillReturnResolvedPromiseWhenQueryOnConnectio...
    method testQueryWillReturnRejectedPromiseWhenCreateConnectionRejects (line 300) | public function testQueryWillReturnRejectedPromiseWhenCreateConnection...
    method testQueryWillReturnRejectedPromiseWhenQueryOnConnectionRejectsAfterCreateConnectionResolves (line 317) | public function testQueryWillReturnRejectedPromiseWhenQueryOnConnectio...
    method testQueryTwiceWillCreateSingleConnectionAndReturnPendingPromiseWhenCreateConnectionIsPending (line 337) | public function testQueryTwiceWillCreateSingleConnectionAndReturnPendi...
    method testQueryTwiceWillCallQueryOnConnectionOnlyOnceWhenQueryIsStillPending (line 357) | public function testQueryTwiceWillCallQueryOnConnectionOnlyOnceWhenQue...
    method testQueryTwiceWillReuseConnectionForSecondQueryWhenFirstQueryIsAlreadyResolved (line 380) | public function testQueryTwiceWillReuseConnectionForSecondQueryWhenFir...
    method testQueryTwiceWillCallSecondQueryOnConnectionAfterFirstQueryResolvesWhenBothQueriesAreGivenBeforeCreateConnectionResolves (line 409) | public function testQueryTwiceWillCallSecondQueryOnConnectionAfterFirs...
    method testQueryTwiceWillCreateNewConnectionForSecondQueryWhenFirstConnectionIsClosedAfterFirstQueryIsResolved (line 441) | public function testQueryTwiceWillCreateNewConnectionForSecondQueryWhe...
    method testQueryTwiceWillCloseFirstConnectionAndCreateNewConnectionForSecondQueryWhenFirstConnectionIsInClosingStateDueToIdleTimerAfterFirstQueryIsResolved (line 470) | public function testQueryTwiceWillCloseFirstConnectionAndCreateNewConn...
    method testQueryTwiceWillRejectFirstQueryWhenCreateConnectionRejectsAndWillCreateNewConnectionForSecondQuery (line 502) | public function testQueryTwiceWillRejectFirstQueryWhenCreateConnection...
    method testQueryTwiceWillRejectBothQueriesWhenBothQueriesAreGivenBeforeCreateConnectionRejects (line 526) | public function testQueryTwiceWillRejectBothQueriesWhenBothQueriesAreG...
    method testQueryTriceWillRejectFirstTwoQueriesAndKeepThirdPendingWhenTwoQueriesAreGivenBeforeCreateConnectionRejects (line 548) | public function testQueryTriceWillRejectFirstTwoQueriesAndKeepThirdPen...
    method testQueryTwiceWillCallSecondQueryOnConnectionAfterFirstQueryRejectsWhenBothQueriesAreGivenBeforeCreateConnectionResolves (line 578) | public function testQueryTwiceWillCallSecondQueryOnConnectionAfterFirs...
    method testQueryStreamWillCreateNewConnectionAndReturnReadableStreamWhenConnectionIsPending (line 611) | public function testQueryStreamWillCreateNewConnectionAndReturnReadabl...
    method testQueryStreamWillCreateNewConnectionAndReturnReadableStreamWhenConnectionResolvesAndQueryStreamOnConnectionReturnsReadableStream (line 629) | public function testQueryStreamWillCreateNewConnectionAndReturnReadabl...
    method testQueryStreamTwiceWillCallQueryStreamOnConnectionOnlyOnceWhenQueryStreamIsStillReadable (line 649) | public function testQueryStreamTwiceWillCallQueryStreamOnConnectionOnl...
    method testQueryStreamTwiceWillReuseConnectionForSecondQueryStreamWhenFirstQueryStreamEnds (line 672) | public function testQueryStreamTwiceWillReuseConnectionForSecondQueryS...
    method testQueryStreamTwiceWillReuseConnectionForSecondQueryStreamWhenFirstQueryStreamEmitsError (line 703) | public function testQueryStreamTwiceWillReuseConnectionForSecondQueryS...
    method testQueryStreamTwiceWillWaitForFirstQueryStreamToEndBeforeStartingSecondQueryStreamWhenFirstQueryStreamIsExplicitlyClosed (line 737) | public function testQueryStreamTwiceWillWaitForFirstQueryStreamToEndBe...
    method testQueryStreamTwiceWillCallSecondQueryStreamOnConnectionAfterFirstQueryStreamIsClosedWhenBothQueriesAreGivenBeforeCreateConnectionResolves (line 765) | public function testQueryStreamTwiceWillCallSecondQueryStreamOnConnect...
    method testQueryStreamTwiceWillCreateNewConnectionForSecondQueryStreamWhenFirstConnectionIsClosedAfterFirstQueryStreamIsClosed (line 798) | public function testQueryStreamTwiceWillCreateNewConnectionForSecondQu...
    method testQueryStreamTwiceWillCloseFirstConnectionAndCreateNewConnectionForSecondQueryStreamWhenFirstConnectionIsInClosingStateDueToIdleTimerAfterFirstQueryStreamIsClosed (line 828) | public function testQueryStreamTwiceWillCloseFirstConnectionAndCreateN...
    method testQueryStreamTwiceWillEmitErrorOnFirstQueryStreamWhenCreateConnectionRejectsAndWillCreateNewConnectionForSecondQueryStream (line 861) | public function testQueryStreamTwiceWillEmitErrorOnFirstQueryStreamWhe...
    method testQueryStreamTwiceWillEmitErrorOnBothQueriesWhenBothQueriesAreGivenBeforeCreateConnectionRejects (line 885) | public function testQueryStreamTwiceWillEmitErrorOnBothQueriesWhenBoth...
    method testPingWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionIsPending (line 913) | public function testPingWillCreateNewConnectionAndReturnPendingPromise...
    method testPingWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionResolvesAndPingOnConnectionIsPending (line 931) | public function testPingWillCreateNewConnectionAndReturnPendingPromise...
    method testPingWillReturnResolvedPromiseWhenPingOnConnectionResolves (line 951) | public function testPingWillReturnResolvedPromiseWhenPingOnConnectionR...
    method testPingWillReturnRejectedPromiseWhenCreateConnectionRejects (line 971) | public function testPingWillReturnRejectedPromiseWhenCreateConnectionR...
    method testPingWillReturnRejectedPromiseWhenPingOnConnectionRejectsAfterCreateConnectionResolves (line 988) | public function testPingWillReturnRejectedPromiseWhenPingOnConnectionR...
    method testPingTwiceWillCreateSingleConnectionAndReturnPendingPromiseWhenCreateConnectionIsPending (line 1008) | public function testPingTwiceWillCreateSingleConnectionAndReturnPendin...
    method testPingTwiceWillCallPingOnConnectionOnlyOnceWhenPingIsStillPending (line 1028) | public function testPingTwiceWillCallPingOnConnectionOnlyOnceWhenPingI...
    method testPingTwiceWillReuseConnectionForSecondPingWhenFirstPingIsAlreadyResolved (line 1051) | public function testPingTwiceWillReuseConnectionForSecondPingWhenFirst...
    method testPingTwiceWillCallSecondPingOnConnectionAfterFirstPingResolvesWhenBothQueriesAreGivenBeforeCreateConnectionResolves (line 1077) | public function testPingTwiceWillCallSecondPingOnConnectionAfterFirstP...
    method testPingTwiceWillCreateNewConnectionForSecondPingWhenFirstConnectionIsClosedAfterFirstPingIsResolved (line 1106) | public function testPingTwiceWillCreateNewConnectionForSecondPingWhenF...
    method testPingTwiceWillCloseFirstConnectionAndCreateNewConnectionForSecondPingWhenFirstConnectionIsInClosingStateDueToIdleTimerAfterFirstPingIsResolved (line 1135) | public function testPingTwiceWillCloseFirstConnectionAndCreateNewConne...
    method testPingTwiceWillRejectFirstPingWhenCreateConnectionRejectsAndWillCreateNewConnectionForSecondPing (line 1167) | public function testPingTwiceWillRejectFirstPingWhenCreateConnectionRe...
    method testPingTwiceWillRejectBothQueriesWhenBothQueriesAreGivenBeforeCreateConnectionRejects (line 1191) | public function testPingTwiceWillRejectBothQueriesWhenBothQueriesAreGi...
    method testPingTriceWillRejectFirstTwoQueriesAndKeepThirdPendingWhenTwoQueriesAreGivenBeforeCreateConnectionRejects (line 1213) | public function testPingTriceWillRejectFirstTwoQueriesAndKeepThirdPend...
    method testPingTwiceWillCallSecondPingOnConnectionAfterFirstPingRejectsWhenBothQueriesAreGivenBeforeCreateConnectionResolves (line 1243) | public function testPingTwiceWillCallSecondPingOnConnectionAfterFirstP...
    method testQueryWillResolveWhenQueryFromUnderlyingConnectionResolves (line 1273) | public function testQueryWillResolveWhenQueryFromUnderlyingConnectionR...
    method testPingAfterQueryWillPassPingToConnectionWhenQueryResolves (line 1295) | public function testPingAfterQueryWillPassPingToConnectionWhenQueryRes...
    method testQueryWillRejectWhenQueryFromUnderlyingConnectionRejects (line 1323) | public function testQueryWillRejectWhenQueryFromUnderlyingConnectionRe...
    method testQueryWillRejectWhenUnderlyingConnectionRejects (line 1345) | public function testQueryWillRejectWhenUnderlyingConnectionRejects()
    method testQueryStreamReturnsReadableStreamWhenConnectionIsPending (line 1365) | public function testQueryStreamReturnsReadableStreamWhenConnectionIsPe...
    method testQueryStreamWillReturnStreamFromUnderlyingConnectionWhenResolved (line 1384) | public function testQueryStreamWillReturnStreamFromUnderlyingConnectio...
    method testQueryStreamWillReturnStreamFromUnderlyingConnectionWhenResolvedAndClosed (line 1409) | public function testQueryStreamWillReturnStreamFromUnderlyingConnectio...
    method testQueryStreamWillCloseStreamWithErrorWhenUnderlyingConnectionRejects (line 1437) | public function testQueryStreamWillCloseStreamWithErrorWhenUnderlyingC...
    method testPingReturnsPendingPromiseWhenConnectionIsPending (line 1460) | public function testPingReturnsPendingPromiseWhenConnectionIsPending()
    method testPingWillPingUnderlyingConnectionWhenResolved (line 1479) | public function testPingWillPingUnderlyingConnectionWhenResolved()
    method testPingTwiceWillBothRejectWithSameErrorWhenUnderlyingConnectionRejects (line 1497) | public function testPingTwiceWillBothRejectWithSameErrorWhenUnderlying...
    method testPingWillTryToCreateNewUnderlyingConnectionAfterPreviousPingFailedToCreateUnderlyingConnection (line 1518) | public function testPingWillTryToCreateNewUnderlyingConnectionAfterPre...
    method testPingWillResolveWhenPingFromUnderlyingConnectionResolves (line 1536) | public function testPingWillResolveWhenPingFromUnderlyingConnectionRes...
    method testPingWillRejectWhenPingFromUnderlyingConnectionRejects (line 1556) | public function testPingWillRejectWhenPingFromUnderlyingConnectionReje...
    method testPingWillRejectWhenPingFromUnderlyingConnectionEmitsCloseEventAndRejects (line 1578) | public function testPingWillRejectWhenPingFromUnderlyingConnectionEmit...
    method testQuitResolvesAndEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending (line 1604) | public function testQuitResolvesAndEmitsCloseImmediatelyWhenConnection...
    method testQuitAfterPingReturnsPendingPromiseWhenConnectionIsPending (line 1625) | public function testQuitAfterPingReturnsPendingPromiseWhenConnectionIs...
    method testQuitAfterPingRejectsAndThenEmitsCloseWhenFactoryFailsToCreateUnderlyingConnection (line 1645) | public function testQuitAfterPingRejectsAndThenEmitsCloseWhenFactoryFa...
    method testQuitAfterPingWillQuitUnderlyingConnectionWhenResolved (line 1671) | public function testQuitAfterPingWillQuitUnderlyingConnectionWhenResol...
    method testQuitAfterPingResolvesAndThenEmitsCloseWhenUnderlyingConnectionQuits (line 1691) | public function testQuitAfterPingResolvesAndThenEmitsCloseWhenUnderlyi...
    method testQuitAfterPingRejectsAndThenEmitsCloseWhenUnderlyingConnectionFailsToQuit (line 1721) | public function testQuitAfterPingRejectsAndThenEmitsCloseWhenUnderlyin...
    method testPingAfterQuitWillNotPassPingCommandToConnection (line 1751) | public function testPingAfterQuitWillNotPassPingCommandToConnection()
    method testCloseEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending (line 1779) | public function testCloseEmitsCloseImmediatelyWhenConnectionIsNotAlrea...
    method testCloseAfterPingCancelsPendingConnection (line 1797) | public function testCloseAfterPingCancelsPendingConnection()
    method testCloseTwiceAfterPingWillCloseUnderlyingConnectionWhenResolved (line 1814) | public function testCloseTwiceAfterPingWillCloseUnderlyingConnectionWh...
    method testCloseAfterPingDoesNotEmitConnectionErrorFromAbortedConnection (line 1835) | public function testCloseAfterPingDoesNotEmitConnectionErrorFromAborte...
    method testCloseAfterPingWillCloseUnderlyingConnection (line 1861) | public function testCloseAfterPingWillCloseUnderlyingConnection()
    method testCloseAfterPingHasResolvedWillCloseUnderlyingConnectionWithoutTryingToCancelConnection (line 1882) | public function testCloseAfterPingHasResolvedWillCloseUnderlyingConnec...
    method testCloseAfterQuitAfterPingWillCloseUnderlyingConnectionWhenQuitIsStillPending (line 1903) | public function testCloseAfterQuitAfterPingWillCloseUnderlyingConnecti...
    method testCloseAfterConnectionIsInClosingStateDueToIdleTimerWillCloseUnderlyingConnection (line 1925) | public function testCloseAfterConnectionIsInClosingStateDueToIdleTimer...
    method testCloseTwiceAfterPingEmitsCloseEventOnceWhenConnectionIsPending (line 1951) | public function testCloseTwiceAfterPingEmitsCloseEventOnceWhenConnecti...
    method testQueryReturnsRejectedPromiseAfterConnectionIsClosed (line 1972) | public function testQueryReturnsRejectedPromiseAfterConnectionIsClosed()
    method testQueryThrowsForInvalidQueryParamsWithoutCreatingNewConnection (line 1991) | public function testQueryThrowsForInvalidQueryParamsWithoutCreatingNew...
    method testQueryThrowsForInvalidQueryParamsWhenConnectionIsAlreadyClosed (line 2007) | public function testQueryThrowsForInvalidQueryParamsWhenConnectionIsAl...
    method testQueryStreamThrowsAfterConnectionIsClosed (line 2025) | public function testQueryStreamThrowsAfterConnectionIsClosed()
    method testQueryStreamThrowsForInvalidQueryParamsWithoutCreatingNewConnection (line 2043) | public function testQueryStreamThrowsForInvalidQueryParamsWithoutCreat...
    method testQueryStreamThrowsForInvalidQueryParamsWhenConnectionIsAlreadyClosed (line 2059) | public function testQueryStreamThrowsForInvalidQueryParamsWhenConnecti...
    method testPingReturnsRejectedPromiseAfterConnectionIsClosed (line 2077) | public function testPingReturnsRejectedPromiseAfterConnectionIsClosed()
    method testQuitReturnsRejectedPromiseAfterConnectionIsClosed (line 2096) | public function testQuitReturnsRejectedPromiseAfterConnectionIsClosed()

FILE: tests/NoResultQueryTest.php
  class NoResultQueryTest (line 10) | class NoResultQueryTest extends BaseTestCase
    method setUpDataTable (line 15) | public function setUpDataTable()
    method testUpdateSimpleNonExistentReportsNoAffectedRows (line 27) | public function testUpdateSimpleNonExistentReportsNoAffectedRows()
    method testInsertSimpleReportsFirstInsertId (line 39) | public function testInsertSimpleReportsFirstInsertId()
    method testUpdateSimpleReportsAffectedRow (line 52) | public function testUpdateSimpleReportsAffectedRow()
    method testCreateTableAgainWillAddWarning (line 65) | public function testCreateTableAgainWillAddWarning()
    method testPingMultipleWillBeExecutedInSameOrderTheyAreEnqueuedFromHandlers (line 88) | public function testPingMultipleWillBeExecutedInSameOrderTheyAreEnqueu...
    method testQuitWithAnyAuthWillQuitWithoutRunning (line 110) | public function testQuitWithAnyAuthWillQuitWithoutRunning()
    method testPingWithValidAuthWillRunUntilQuitAfterPing (line 122) | public function testPingWithValidAuthWillRunUntilQuitAfterPing()
    method testPingAndQuitWillFulfillPingBeforeQuitBeforeCloseEvent (line 138) | public function testPingAndQuitWillFulfillPingBeforeQuitBeforeCloseEve...
    method testPingWithValidAuthWillRunUntilIdleTimerAfterPingEvenWithoutQuit (line 160) | public function testPingWithValidAuthWillRunUntilIdleTimerAfterPingEve...
    method testPingWithInvalidAuthWillRejectPingButWillNotEmitErrorOrClose (line 172) | public function testPingWithInvalidAuthWillRejectPingButWillNotEmitErr...
    method testPingWithValidAuthWillPingBeforeQuitButNotAfter (line 185) | public function testPingWithValidAuthWillPingBeforeQuitButNotAfter()

FILE: tests/ResultQueryTest.php
  class ResultQueryTest (line 11) | class ResultQueryTest extends BaseTestCase
    method testSelectStaticText (line 13) | public function testSelectStaticText()
    method provideValuesThatWillBeReturnedAsIs (line 27) | public function provideValuesThatWillBeReturnedAsIs()
    method testSelectStaticValueWillBeReturnedAsIs (line 49) | public function testSelectStaticValueWillBeReturnedAsIs($value)
    method testSelectStaticValueWillBeReturnedAsIsWithNoBackslashEscapesSqlMode (line 68) | public function testSelectStaticValueWillBeReturnedAsIsWithNoBackslash...
    method provideValuesThatWillBeConvertedToString (line 90) | public function provideValuesThatWillBeConvertedToString()
    method testSelectStaticValueWillBeConvertedToString (line 103) | public function testSelectStaticValueWillBeConvertedToString($value, $...
    method testSelectStaticTextWithQuestionMark (line 117) | public function testSelectStaticTextWithQuestionMark()
    method testSelectLongStaticTextHasTypeStringWithValidLength (line 131) | public function testSelectLongStaticTextHasTypeStringWithValidLength()
    method testSelectStaticTextWithEmptyLabel (line 148) | public function testSelectStaticTextWithEmptyLabel()
    method testSelectStaticNullHasTypeNull (line 166) | public function testSelectStaticNullHasTypeNull()
    method testSelectStaticTextTwoRows (line 183) | public function testSelectStaticTextTwoRows()
    method testSelectStaticTextTwoRowsWithNullHasTypeString (line 199) | public function testSelectStaticTextTwoRowsWithNullHasTypeString()
    method testSelectStaticIntegerTwoRowsWithNullHasTypeLongButReturnsIntAsString (line 218) | public function testSelectStaticIntegerTwoRowsWithNullHasTypeLongButRe...
    method testSelectStaticTextTwoRowsWithIntegerHasTypeString (line 237) | public function testSelectStaticTextTwoRowsWithIntegerHasTypeString()
    method testSelectStaticTextTwoRowsWithEmptyRow (line 256) | public function testSelectStaticTextTwoRowsWithEmptyRow()
    method testSelectStaticTextNoRows (line 272) | public function testSelectStaticTextNoRows()
    method testSelectStaticTextTwoColumns (line 287) | public function testSelectStaticTextTwoColumns()
    method testSelectStaticTextTwoColumnsWithOneEmptyColumn (line 303) | public function testSelectStaticTextTwoColumnsWithOneEmptyColumn()
    method testSelectStaticTextTwoColumnsWithBothEmpty (line 319) | public function testSelectStaticTextTwoColumnsWithBothEmpty()
    method testSelectStaticTextTwoColumnsWithSameNameOverwritesValue (line 337) | public function testSelectStaticTextTwoColumnsWithSameNameOverwritesVa...
    method testSelectCharsetDefaultsToUtf8 (line 356) | public function testSelectCharsetDefaultsToUtf8()
    method testSelectWithExplicitCharsetReturnsCharset (line 370) | public function testSelectWithExplicitCharsetReturnsCharset()
    method testSimpleSelect (line 385) | public function testSimpleSelect()
    method testSimpleSelectFromMysqlClientWithoutDatabaseNameReturnsSameData (line 406) | public function testSimpleSelectFromMysqlClientWithoutDatabaseNameRetu...
    method testInvalidSelectShouldFail (line 419) | public function testInvalidSelectShouldFail()
    method testInvalidMultiStatementsShouldFailToPreventSqlInjections (line 437) | public function testInvalidMultiStatementsShouldFailToPreventSqlInject...
    method testSelectAfterDelay (line 458) | public function testSelectAfterDelay()
    method testQueryStreamStaticEmptyEmitsSingleRow (line 480) | public function testQueryStreamStaticEmptyEmitsSingleRow()
    method testQueryStreamBoundVariableEmitsSingleRow (line 493) | public function testQueryStreamBoundVariableEmitsSingleRow()
    method testQueryStreamZeroRowsEmitsEndWithoutData (line 506) | public function testQueryStreamZeroRowsEmitsEndWithoutData()
    method testQueryStreamInvalidStatementEmitsError (line 519) | public function testQueryStreamInvalidStatementEmitsError()
    method testQueryStreamDropStatementEmitsEndWithoutData (line 533) | public function testQueryStreamDropStatementEmitsEndWithoutData()
    method testQueryStreamExplicitCloseEmitsCloseEventWithoutData (line 546) | public function testQueryStreamExplicitCloseEmitsCloseEventWithoutData()
    method testQueryStreamFromMysqlClientEmitsSingleRow (line 560) | public function testQueryStreamFromMysqlClientEmitsSingleRow()
    method testQueryStreamFromMysqlClientWillErrorWhenConnectionIsClosed (line 575) | public function testQueryStreamFromMysqlClientWillErrorWhenConnectionI...
Condensed preview — 42 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (355K chars).
[
  {
    "path": ".gitattributes",
    "chars": 193,
    "preview": "/.gitattributes export-ignore\n/.github/ export-ignore\n/.gitignore export-ignore\n/examples/ export-ignore\n/phpunit.xml.di"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1915,
    "preview": "name: CI\n\non:\n  push:\n  pull_request:\n\njobs:\n  PHPUnit:\n    name: PHPUnit (PHP ${{ matrix.php }} + ${{ matrix.rdbms }})\n"
  },
  {
    "path": ".gitignore",
    "chars": 24,
    "preview": "/composer.lock\n/vendor/\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 13456,
    "preview": "# Changelog\n\n## 0.6.0 (2023-11-10)\n\n*   Feature: Improve Promise v3 support and use template types.\n    (#183 and #178 b"
  },
  {
    "path": "LICENSE",
    "chars": 1073,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Jin Hu\n\nPermission is hereby granted, free of charge, to any person obtaining "
  },
  {
    "path": "README.md",
    "chars": 17558,
    "preview": "# MySQL\n\n[![CI status](https://github.com/friends-of-reactphp/mysql/actions/workflows/ci.yml/badge.svg)](https://github."
  },
  {
    "path": "composer.json",
    "chars": 797,
    "preview": "{\n    \"name\": \"react/mysql\",\n    \"description\": \"Async MySQL database client for ReactPHP.\",\n    \"keywords\": [\"mysql\", \""
  },
  {
    "path": "examples/01-query.php",
    "chars": 1065,
    "preview": "<?php\n\n// $ php examples/01-query.php\n// $ MYSQL_URI=test:test@localhost/test php examples/01-query.php \"SELECT * FROM b"
  },
  {
    "path": "examples/02-query-stream.php",
    "chars": 692,
    "preview": "<?php\n\n// $ php examples/02-query-stream.php \"SHOW VARIABLES\"\n// $ MYSQL_URI=test:test@localhost/test php examples/02-qu"
  },
  {
    "path": "examples/11-interactive.php",
    "chars": 2445,
    "preview": "<?php\n\n// $ php examples/11-interactive.php\n// $ MYSQL_URI=test:test@localhost/test php examples/11-interactive.php\n\nreq"
  },
  {
    "path": "examples/12-slow-stream.php",
    "chars": 2944,
    "preview": "<?php\n\n// $ php examples/12-slow-stream.php \"SHOW VARIABLES\"\n// $ MYSQL_URI=test:test@localhost/test php examples/12-slo"
  },
  {
    "path": "phpunit.xml.dist",
    "chars": 1269,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!-- PHPUnit configuration file with new format for PHPUnit 9.6+ -->\n<phpunit xm"
  },
  {
    "path": "phpunit.xml.legacy",
    "chars": 1194,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!-- PHPUnit configuration file with old format for legacy PHPUnit -->\n<phpunit "
  },
  {
    "path": "src/Commands/AbstractCommand.php",
    "chars": 2393,
    "preview": "<?php\n\nnamespace React\\Mysql\\Commands;\n\nuse Evenement\\EventEmitter;\n\n/**\n * @internal\n */\nabstract class AbstractCommand"
  },
  {
    "path": "src/Commands/AuthenticateCommand.php",
    "chars": 5852,
    "preview": "<?php\n\nnamespace React\\Mysql\\Commands;\n\nuse React\\Mysql\\Io\\Buffer;\nuse React\\Mysql\\Io\\Constants;\n\n/**\n * @internal\n * @l"
  },
  {
    "path": "src/Commands/CommandInterface.php",
    "chars": 189,
    "preview": "<?php\n\nnamespace React\\Mysql\\Commands;\n\nuse Evenement\\EventEmitterInterface;\n\n/**\n * @internal\n */\ninterface CommandInte"
  },
  {
    "path": "src/Commands/PingCommand.php",
    "chars": 235,
    "preview": "<?php\n\nnamespace React\\Mysql\\Commands;\n\n/**\n * @internal\n */\nclass PingCommand extends AbstractCommand\n{\n    public func"
  },
  {
    "path": "src/Commands/QueryCommand.php",
    "chars": 904,
    "preview": "<?php\n\nnamespace React\\Mysql\\Commands;\n\nuse React\\Mysql\\Io\\Query;\n\n/**\n * @internal\n */\nclass QueryCommand extends Abstr"
  },
  {
    "path": "src/Commands/QuitCommand.php",
    "chars": 235,
    "preview": "<?php\n\nnamespace React\\Mysql\\Commands;\n\n/**\n * @internal\n */\nclass QuitCommand extends AbstractCommand\n{\n    public func"
  },
  {
    "path": "src/Exception.php",
    "chars": 70,
    "preview": "<?php\n\nnamespace React\\Mysql;\n\nclass Exception extends \\Exception\n{\n}\n"
  },
  {
    "path": "src/Io/Buffer.php",
    "chars": 7732,
    "preview": "<?php\nnamespace React\\Mysql\\Io;\n\n/**\n * @internal\n */\nclass Buffer\n{\n    private $buffer = '';\n    private $bufferPos = "
  },
  {
    "path": "src/Io/Connection.php",
    "chars": 8539,
    "preview": "<?php\n\nnamespace React\\Mysql\\Io;\n\nuse Evenement\\EventEmitter;\nuse React\\EventLoop\\LoopInterface;\nuse React\\Mysql\\Command"
  },
  {
    "path": "src/Io/Constants.php",
    "chars": 3281,
    "preview": "<?php\n\nnamespace React\\Mysql\\Io;\n\n/**\n * @internal\n */\nclass Constants\n{\n    /**\n     * new more secure passwords\n     *"
  },
  {
    "path": "src/Io/Executor.php",
    "chars": 541,
    "preview": "<?php\n\nnamespace React\\Mysql\\Io;\n\nuse Evenement\\EventEmitter;\n\n/**\n * @internal\n */\nclass Executor extends EventEmitter\n"
  },
  {
    "path": "src/Io/Factory.php",
    "chars": 9815,
    "preview": "<?php\n\nnamespace React\\Mysql\\Io;\n\nuse React\\EventLoop\\Loop;\nuse React\\EventLoop\\LoopInterface;\nuse React\\Mysql\\Commands\\"
  },
  {
    "path": "src/Io/Parser.php",
    "chars": 18012,
    "preview": "<?php\n\nnamespace React\\Mysql\\Io;\n\nuse React\\Mysql\\Commands\\AuthenticateCommand;\nuse React\\Mysql\\Commands\\QueryCommand;\nu"
  },
  {
    "path": "src/Io/Query.php",
    "chars": 4869,
    "preview": "<?php\n\nnamespace React\\Mysql\\Io;\n\n/**\n * @internal\n */\nclass Query\n{\n    private $sql;\n\n    private $builtSql;\n\n    priv"
  },
  {
    "path": "src/Io/QueryStream.php",
    "chars": 2238,
    "preview": "<?php\n\nnamespace React\\Mysql\\Io;\n\nuse Evenement\\EventEmitter;\nuse React\\Mysql\\Commands\\QueryCommand;\nuse React\\Socket\\Co"
  },
  {
    "path": "src/MysqlClient.php",
    "chars": 18569,
    "preview": "<?php\n\nnamespace React\\Mysql;\n\nuse Evenement\\EventEmitter;\nuse React\\EventLoop\\LoopInterface;\nuse React\\Mysql\\Commands\\A"
  },
  {
    "path": "src/MysqlResult.php",
    "chars": 582,
    "preview": "<?php\n\nnamespace React\\Mysql;\n\nclass MysqlResult\n{\n    /**\n     * last inserted ID (if any)\n     * @var int|null\n     */"
  },
  {
    "path": "tests/BaseTestCase.php",
    "chars": 2970,
    "preview": "<?php\n\nnamespace React\\Tests\\Mysql;\n\nuse PHPUnit\\Framework\\TestCase;\nuse React\\EventLoop\\LoopInterface;\nuse React\\Mysql\\"
  },
  {
    "path": "tests/Commands/AuthenticateCommandTest.php",
    "chars": 5757,
    "preview": "<?php\n\nnamespace React\\Tests\\Mysql\\Commands;\n\nuse PHPUnit\\Framework\\TestCase;\nuse React\\Mysql\\Commands\\AuthenticateComma"
  },
  {
    "path": "tests/Io/BufferTest.php",
    "chars": 5478,
    "preview": "<?php\n\nnamespace React\\Tests\\Mysql\\Io;\n\nuse React\\Mysql\\Io\\Buffer;\nuse React\\Tests\\Mysql\\BaseTestCase;\n\nclass BufferTest"
  },
  {
    "path": "tests/Io/ConnectionTest.php",
    "chars": 34530,
    "preview": "<?php\n\nnamespace React\\Tests\\Mysql\\Io;\n\nuse React\\Mysql\\Io\\Connection;\nuse React\\Mysql\\Io\\Query;\nuse React\\Tests\\Mysql\\B"
  },
  {
    "path": "tests/Io/FactoryTest.php",
    "chars": 18886,
    "preview": "<?php\n\nnamespace React\\Tests\\Mysql\\Io;\n\nuse React\\EventLoop\\Loop;\nuse React\\Mysql\\Io\\Connection;\nuse React\\Mysql\\Io\\Fact"
  },
  {
    "path": "tests/Io/ParserTest.php",
    "chars": 16787,
    "preview": "<?php\n\nnamespace React\\Tests\\Mysql\\Io;\n\nuse React\\Mysql\\Commands\\AuthenticateCommand;\nuse React\\Mysql\\Commands\\QueryComm"
  },
  {
    "path": "tests/Io/QueryStreamTest.php",
    "chars": 6815,
    "preview": "<?php\n\nnamespace React\\Tests\\Mysql\\Io;\n\nuse React\\Mysql\\Commands\\QueryCommand;\nuse React\\Mysql\\Io\\QueryStream;\nuse React"
  },
  {
    "path": "tests/Io/QueryTest.php",
    "chars": 2583,
    "preview": "<?php\n\nnamespace React\\Tests\\Mysql\\Io;\n\nuse React\\Mysql\\Io\\Query;\nuse React\\Tests\\Mysql\\BaseTestCase;\n\nclass QueryTest e"
  },
  {
    "path": "tests/MysqlClientTest.php",
    "chars": 91686,
    "preview": "<?php\n\nnamespace React\\Tests\\Mysql;\n\nuse React\\Mysql\\Io\\Connection;\nuse React\\Mysql\\Io\\Query;\nuse React\\Mysql\\MysqlClien"
  },
  {
    "path": "tests/NoResultQueryTest.php",
    "chars": 5602,
    "preview": "<?php\n\nnamespace React\\Tests\\Mysql;\n\nuse React\\EventLoop\\Loop;\nuse React\\Mysql\\Io\\Query;\nuse React\\Mysql\\MysqlClient;\nus"
  },
  {
    "path": "tests/ResultQueryTest.php",
    "chars": 20399,
    "preview": "<?php\n\nnamespace React\\Tests\\Mysql;\n\nuse React\\EventLoop\\Loop;\nuse React\\Mysql\\Io\\Constants;\nuse React\\Mysql\\Io\\Query;\nu"
  },
  {
    "path": "tests/wait-for-mysql.sh",
    "chars": 216,
    "preview": "#!/bin/sh\n\nCONTAINER=\"mysql\"\nUSERNAME=\"test\"\nPASSWORD=\"test\"\nwhile ! docker exec $CONTAINER mysql --host=127.0.0.1 --por"
  }
]

About this extraction

This page contains the full source code of the friends-of-reactphp/mysql GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 42 files (332.4 KB), approximately 82.5k tokens, and a symbol index with 383 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!