├── .circleci ├── config.yml └── wait-and-run-phpunit.sh ├── .editorconfig ├── .formatter.yml ├── .gitignore ├── .php_cs ├── README.md ├── composer.json ├── docker-compose └── docker-compose.yml ├── phpunit.xml ├── src ├── Connection.php ├── ConnectionOptions.php ├── ConnectionPool.php ├── ConnectionPoolInterface.php ├── ConnectionPoolOptions.php ├── ConnectionWorker.php ├── Credentials.php ├── Driver │ ├── AbstractDriver.php │ ├── Driver.php │ ├── Exception.php │ ├── Mysql │ │ ├── EmptyDoctrineMysqlDriver.php │ │ └── MysqlDriver.php │ ├── PlainDriverException.php │ ├── PostgreSQL │ │ ├── EmptyDoctrinePostgreSQLDriver.php │ │ └── PostgreSQLDriver.php │ └── SQLite │ │ ├── EmptyDoctrineSQLiteDriver.php │ │ └── SQLiteDriver.php ├── Mock │ ├── MockedDBALConnection.php │ └── MockedDriver.php ├── Result.php └── SingleConnection.php └── tests ├── ConnectionTest.php ├── CredentialsTest.php ├── Mysql5ConnectionPoolTest.php ├── Mysql5ConnectionTest.php ├── PostgreSQLConnectionPoolTest.php ├── PostgreSQLConnectionTest.php └── SQLiteConnectionTest.php /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | test-php74: 4 | docker: 5 | - image: circleci/php:7.4-cli 6 | - image: postgres:alpine 7 | environment: 8 | POSTGRES_PASSWORD: root 9 | POSTGRES_USER: root 10 | POSTGRES_DB: test 11 | - image: mysql:5 12 | environment: 13 | MYSQL_ROOT_PASSWORD: root 14 | MYSQL_DATABASE: test 15 | 16 | working_directory: ~/project 17 | steps: 18 | - checkout 19 | 20 | - run: 21 | name: Run tests 22 | command: | 23 | composer update -n --prefer-dist 24 | .circleci/wait-and-run-phpunit.sh 25 | 26 | test-php80: 27 | docker: 28 | - image: circleci/php:8.0-cli 29 | - image: postgres:alpine 30 | environment: 31 | POSTGRES_PASSWORD: root 32 | POSTGRES_USER: root 33 | POSTGRES_DB: test 34 | - image: mysql:5 35 | environment: 36 | MYSQL_ROOT_PASSWORD: root 37 | MYSQL_DATABASE: test 38 | 39 | working_directory: ~/project 40 | steps: 41 | - checkout 42 | 43 | - run: 44 | name: Run tests 45 | command: | 46 | composer update -n --prefer-dist 47 | .circleci/wait-and-run-phpunit.sh 48 | 49 | test-php81: 50 | docker: 51 | - image: cimg/php:8.1 52 | - image: postgres:alpine 53 | environment: 54 | POSTGRES_PASSWORD: root 55 | POSTGRES_USER: root 56 | POSTGRES_DB: test 57 | - image: mysql:5 58 | environment: 59 | MYSQL_ROOT_PASSWORD: root 60 | MYSQL_DATABASE: test 61 | 62 | working_directory: ~/project 63 | steps: 64 | - checkout 65 | 66 | - run: 67 | name: Run tests 68 | command: | 69 | composer update -n --prefer-dist 70 | .circleci/wait-and-run-phpunit.sh 71 | 72 | test-php82: 73 | docker: 74 | - image: cimg/php:8.2 75 | - image: postgres:alpine 76 | environment: 77 | POSTGRES_PASSWORD: root 78 | POSTGRES_USER: root 79 | POSTGRES_DB: test 80 | - image: mysql:5 81 | environment: 82 | MYSQL_ROOT_PASSWORD: root 83 | MYSQL_DATABASE: test 84 | 85 | working_directory: ~/project 86 | steps: 87 | - checkout 88 | 89 | - run: 90 | name: Run tests 91 | command: | 92 | composer update -n --prefer-dist 93 | .circleci/wait-and-run-phpunit.sh 94 | 95 | workflows: 96 | version: 2 97 | test: 98 | jobs: 99 | - test-php74 100 | - test-php80 101 | - test-php81 102 | - test-php82 103 | -------------------------------------------------------------------------------- /.circleci/wait-and-run-phpunit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while ! nc -z localhost 3306; 4 | do 5 | echo "Waiting for mysql. Slepping"; 6 | sleep 1; 7 | done; 8 | echo "Connected to mysql!"; 9 | 10 | while ! nc -z localhost 5432; 11 | do 12 | echo "Waiting for Postgresql. Slepping"; 13 | sleep 1; 14 | done; 15 | echo "Connected to Postgresql!"; 16 | 17 | php vendor/bin/phpunit -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; top-most EditorConfig file 2 | root = true 3 | 4 | ; Unix-style newlines 5 | [*] 6 | end_of_line = LF 7 | 8 | [*.php] 9 | indent_style = space 10 | indent_size = 4 -------------------------------------------------------------------------------- /.formatter.yml: -------------------------------------------------------------------------------- 1 | use-sort: 2 | group: 3 | - _main 4 | group-type: each 5 | sort-type: alph 6 | sort-direction: asc 7 | strict: true 8 | header: | 9 | /* 10 | * This file is part of the DriftPHP Project 11 | * 12 | * For the full copyright and license information, please view the LICENSE 13 | * file that was distributed with this source code. 14 | * 15 | * Feel free to edit as you please, and have fun. 16 | * 17 | * @author Marc Morera 18 | */ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /var 3 | /composer.lock 4 | /.php_cs.cache 5 | /.phpunit.result.cache -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | exclude('vendor') 5 | ->exclude('web') 6 | ->exclude('bin') 7 | ->exclude('var') 8 | ->in(__DIR__) 9 | ; 10 | 11 | return PhpCsFixer\Config::create() 12 | ->setRules([ 13 | '@PSR2' => true, 14 | '@Symfony' => true, 15 | 'single_line_after_imports' => false, 16 | 'no_superfluous_phpdoc_tags' => false 17 | ]) 18 | ->setFinder($finder) 19 | ; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DBAL for ReactPHP 2 | 3 | [![CircleCI](https://circleci.com/gh/driftphp/reactphp-dbal.svg?style=svg)](https://circleci.com/gh/driftphp/reactphp-dbal) 4 | 5 | This is a DBAL on top of ReactPHP SQL clients and Doctrine model implementation. 6 | You will be able to use 7 | 8 | - Doctrine QueryBuilder model 9 | - Doctrine Schema model 10 | - Easy-to-use shortcuts for common operations 11 | - and much more support is being added right now 12 | 13 | > Attention. Only for proof of concept ATM. Do not use this library on 14 | > production until the first stable version is tagged. 15 | 16 | ## Example 17 | 18 | Let's create an example of what this library can really do. For this example, we 19 | will create an adapter for Mysql, and will use Doctrine QueryBuilder to create a 20 | new element in database and query for some rows. 21 | 22 | Because we will use Mysql adapter, you should have installed the ReactPHP based 23 | mysql library `react/mysql`. Because this library is on development stage, all 24 | adapters dependencies will be loaded for testing purposes. 25 | 26 | First of all, we need to create a Connection instance with the selected platform 27 | driver. We will have to create as well a Credentials instance with all the 28 | connection data. 29 | 30 | ```php 31 | use Doctrine\DBAL\Platforms\MySqlPlatform; 32 | use Drift\DBAL\Connection; 33 | use Drift\DBAL\Driver\Mysql\MysqlDriver; 34 | use Drift\DBAL\Credentials; 35 | use React\EventLoop\Factory as LoopFactory; 36 | 37 | $loop = LoopFactory::create(); 38 | $mysqlPlatform = new MySqlPlatform(); 39 | $mysqlDriver = new MysqlDriver($loop); 40 | $credentials = new Credentials( 41 | '127.0.0.1', 42 | '3306', 43 | 'root', 44 | 'root', 45 | 'test' 46 | ); 47 | 48 | $connection = Connection::createConnected( 49 | $mysqlDriver, 50 | $credentials, 51 | $mysqlPlatform 52 | ); 53 | ``` 54 | 55 | Once we have the connection, we can create a new register in the database by 56 | using the Doctrine QueryBuilder or direct built-in methods. The result of all 57 | these calls will be a Promise interface that, eventually, will return a Result 58 | instance. 59 | 60 | ```php 61 | use Drift\DBAL\Connection; 62 | use Drift\DBAL\Result; 63 | 64 | /** 65 | * @var Connection $connection 66 | */ 67 | $promise = $connection 68 | ->insert('test', [ 69 | 'id' => '1', 70 | 'field1' => 'val1', 71 | 'field2' => 'val2', 72 | ]) 73 | ->then(function(Result $_) use ($connection) { 74 | $queryBuilder = $connection->createQueryBuilder(); 75 | 76 | return $connection 77 | ->query($queryBuilder) 78 | ->select('*') 79 | ->from('test', 't') 80 | ->where($queryBuilder->expr()->orX( 81 | $queryBuilder->expr()->eq('t.id', '?'), 82 | $queryBuilder->expr()->eq('t.id', '?') 83 | )) 84 | ->setParameters(['1', '2']); 85 | }) 86 | ->then(function(Result $result) { 87 | $numberOfRows = $result->fetchCount(); 88 | $firstRow = $result->fetchFirstRow(); 89 | $allRows = $result->fetchAllRows(); 90 | }); 91 | ``` 92 | 93 | You can use, at this moment, adapters for `mysql`, `postgresql`, and `sqlite`. 94 | 95 | ## Connection shortcuts 96 | 97 | This DBAL introduce some shortcuts useful for your projects on top of Doctrine 98 | query builder and escaping parametrization. 99 | 100 | ### Insert 101 | 102 | Inserts a new row in a table. Needs the table and an array with fields and their 103 | values. Returns a Promise. 104 | 105 | ```php 106 | $connection->insert('test', [ 107 | 'id' => '1', 108 | 'field1' => 'value1' 109 | ]); 110 | ``` 111 | 112 | ### Update 113 | 114 | Updates an existing row from a table. Needs the table, an identifier as array 115 | and an array of fields with their values. Returns a Promise. 116 | 117 | ```php 118 | $connection->update( 119 | 'test', 120 | ['id' => '1'], 121 | [ 122 | 'field1' => 'value1', 123 | 'field2' => 'value2', 124 | ] 125 | ); 126 | ``` 127 | 128 | ### Upsert 129 | 130 | Insert a row if not exists. Otherwise, it will update the existing row with 131 | given values. Needs the table, an identifier as array and an array of fields 132 | with their values. Returns a Promise. 133 | 134 | ```php 135 | $connection->upsert( 136 | 'test', 137 | ['id' => '1'], 138 | [ 139 | 'field1' => 'value1', 140 | 'field2' => 'value2', 141 | ] 142 | ); 143 | ``` 144 | 145 | ### Delete 146 | 147 | Deletes a row if exists. Needs the table and the identifier as array. Returns a 148 | Promise. 149 | 150 | ```php 151 | $connection->delete('test', [ 152 | 'id' => '1' 153 | ]); 154 | ``` 155 | 156 | ### Find one by 157 | 158 | Find a row given a where clause. Needs the table and an array of fields with 159 | their values. Returns a Promise with, eventually, the result as array of all 160 | found rows. 161 | 162 | ```php 163 | $connection 164 | ->findOneById('test', [ 165 | 'id' => '1' 166 | ]) 167 | ->then(function(?array $result) { 168 | if (is_null($result)) { 169 | // Row with ID=1 not found 170 | } else { 171 | // Row with ID=1 found. 172 | echo $result['id']; 173 | } 174 | }); 175 | ``` 176 | 177 | ### Find by 178 | 179 | Find all rows given an array of where clauses. Needs the table and an array of 180 | fields with their values. Returns a Promise with, eventually, the result as 181 | array of all found rows. 182 | 183 | ```php 184 | $connection 185 | ->findBy('test', [ 186 | 'age' => '33' 187 | ]) 188 | ->then(function(array $result) { 189 | echo 'Found ' . count($result) . ' rows'; 190 | }); 191 | ``` 192 | 193 | ### Create table 194 | 195 | You can easily create a new table with basic information. Needs the table name 196 | and an array of fields and types. Strings are considered with length `255`. 197 | First field position in the array will be considered as primary key. Returns a 198 | Promise with, eventually, the connection. 199 | 200 | ```php 201 | $connection->createTable('test', [ 202 | 'id' => 'string', 203 | 'name' => 'string', 204 | ]); 205 | ``` 206 | 207 | This is a basic table creation method. To create more complex tables, you can 208 | use Doctrine's Schema model. You can execute all Schema SQLs generated by using 209 | this method inside Connection named `executeSchema`. You'll find more 210 | information about this Schema model in 211 | [Doctrine documentation](https://www.doctrine-project.org/projects/doctrine-dbal/en/2.10/reference/schema-representation.html) 212 | 213 | ```php 214 | $schema = new Schema(); 215 | $table = $schema->createTable('test'); 216 | $table->addColumn('id', 'string'); 217 | // ... 218 | 219 | $connection->executeSchema($schema); 220 | ``` 221 | 222 | ### Drop table 223 | 224 | You can easily drop an existing table. Needs just the table name, and returns, 225 | eventually, the connection. 226 | 227 | ```php 228 | $connection->dropTable('test'); 229 | ``` 230 | 231 | ### Truncate table 232 | 233 | You can easily truncate an existing table. Needs just the table name, and returns, 234 | eventually, the connection. 235 | 236 | ```php 237 | $connection->truncateTable('test'); 238 | ``` 239 | 240 | ## Tests 241 | 242 | You can run tests by running `docker-compose up` and by doing 243 | `php vendor/bin/phpunit`. 244 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drift/dbal", 3 | "description": "DBAL for ReactPHP on top of Doctrine", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Marc Morera", 9 | "email": "yuhu@mmoreram.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.4 || ^8.0", 14 | "doctrine/dbal": "^3", 15 | "react/event-loop": "^1" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^9", 19 | "clue/block-react": "^1", 20 | "react/mysql": "^0.5", 21 | "clue/reactphp-sqlite": "^1", 22 | "voryx/pgasync": "^2" 23 | }, 24 | "suggest": { 25 | "react/mysql": "MySQL usage", 26 | "clue/reactphp-sqlite": "SQLite usage", 27 | "voryx/pgasync": "PostgreSQL Usage" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Drift\\DBAL\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Drift\\DBAL\\Tests\\": "tests/" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | postgres: 6 | image: "postgres:alpine" 7 | container_name: drift-postgres 8 | environment: 9 | POSTGRES_PASSWORD: root 10 | POSTGRES_USER: root 11 | POSTGRES_DB: test 12 | ports: 13 | - "5432:5432" 14 | 15 | mysql5: 16 | image: "mysql:5" 17 | container_name: drift-mysql5 18 | environment: 19 | MYSQL_ROOT_PASSWORD: root 20 | MYSQL_DATABASE: test 21 | ports: 22 | - "3306:3306" 23 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | tests 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL; 17 | 18 | use Doctrine\DBAL\Exception as DBALException; 19 | use Doctrine\DBAL\Exception\InvalidArgumentException; 20 | use Doctrine\DBAL\Exception\TableExistsException; 21 | use Doctrine\DBAL\Exception\TableNotFoundException; 22 | use Doctrine\DBAL\Platforms\AbstractPlatform; 23 | use Doctrine\DBAL\Query\QueryBuilder; 24 | use Doctrine\DBAL\Schema\Schema; 25 | use Drift\DBAL\Driver\Driver; 26 | use React\Promise\PromiseInterface; 27 | 28 | /** 29 | * Class Connection. 30 | */ 31 | interface Connection 32 | { 33 | /** 34 | * Create new connection. 35 | * 36 | * @param Driver $driver 37 | * @param Credentials $credentials 38 | * @param AbstractPlatform $platform 39 | * @param ConnectionOptions|null $options 40 | * 41 | * @return Connection 42 | */ 43 | public static function create( 44 | Driver $driver, 45 | Credentials $credentials, 46 | AbstractPlatform $platform, 47 | ?ConnectionOptions $options = null 48 | ): Connection; 49 | 50 | /** 51 | * Create new connection. 52 | * 53 | * @param Driver $driver 54 | * @param Credentials $credentials 55 | * @param AbstractPlatform $platform 56 | * @param ConnectionOptions|null $options 57 | * 58 | * @return Connection 59 | */ 60 | public static function createConnected( 61 | Driver $driver, 62 | Credentials $credentials, 63 | AbstractPlatform $platform, 64 | ?ConnectionOptions $options = null 65 | ): Connection; 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function getDriverNamespace(): string; 71 | 72 | /** 73 | * Connect. 74 | * 75 | * @param ConnectionOptions|null $options 76 | */ 77 | public function connect(?ConnectionOptions $options = null); 78 | 79 | /** 80 | * Close. 81 | */ 82 | public function close(); 83 | 84 | /** 85 | * Creates QueryBuilder. 86 | * 87 | * @return QueryBuilder 88 | * 89 | * @throws DBALException 90 | */ 91 | public function createQueryBuilder(): QueryBuilder; 92 | 93 | /** 94 | * Query by query builder. 95 | * 96 | * @param QueryBuilder $queryBuilder 97 | * 98 | * @return PromiseInterface 99 | */ 100 | public function query(QueryBuilder $queryBuilder): PromiseInterface; 101 | 102 | /** 103 | * Query by sql and parameters. 104 | * 105 | * @param string $sql 106 | * @param array $parameters 107 | * 108 | * @return PromiseInterface 109 | */ 110 | public function queryBySQL(string $sql, array $parameters = []): PromiseInterface; 111 | 112 | /** 113 | * Execute, sequentially, an array of sqls. 114 | * 115 | * @param string[] $sqls 116 | * 117 | * @return PromiseInterface 118 | */ 119 | public function executeSQLs(array $sqls): PromiseInterface; 120 | 121 | /** 122 | * Execute an schema. 123 | * 124 | * @param Schema $schema 125 | * 126 | * @return PromiseInterface 127 | */ 128 | public function executeSchema(Schema $schema): PromiseInterface; 129 | 130 | /** 131 | * Shortcuts. 132 | */ 133 | 134 | /** 135 | * Find one by. 136 | * 137 | * connection->findOneById('table', ['id' => 1]); 138 | * 139 | * @param string $table 140 | * @param array $where 141 | * 142 | * @return PromiseInterface 143 | */ 144 | public function findOneBy( 145 | string $table, 146 | array $where 147 | ): PromiseInterface; 148 | 149 | /** 150 | * Find by. 151 | * 152 | * connection->findBy('table', ['id' => 1]); 153 | * 154 | * @param string $table 155 | * @param array $where 156 | * 157 | * @return PromiseInterface 158 | */ 159 | public function findBy( 160 | string $table, 161 | array $where = [] 162 | ): PromiseInterface; 163 | 164 | /** 165 | * @param string $table 166 | * @param array $values 167 | * 168 | * @return PromiseInterface 169 | */ 170 | public function insert( 171 | string $table, 172 | array $values 173 | ): PromiseInterface; 174 | 175 | /** 176 | * @param string $table 177 | * @param array $values 178 | * 179 | * @return PromiseInterface 180 | * 181 | * @throws InvalidArgumentException 182 | */ 183 | public function delete( 184 | string $table, 185 | array $values 186 | ): PromiseInterface; 187 | 188 | /** 189 | * @param string $table 190 | * @param array $id 191 | * @param array $values 192 | * 193 | * @return PromiseInterface 194 | * 195 | * @throws InvalidArgumentException 196 | */ 197 | public function update( 198 | string $table, 199 | array $id, 200 | array $values 201 | ): PromiseInterface; 202 | 203 | /** 204 | * @param string $table 205 | * @param array $id 206 | * @param array $values 207 | * 208 | * @return PromiseInterface 209 | * 210 | * @throws InvalidArgumentException 211 | */ 212 | public function upsert( 213 | string $table, 214 | array $id, 215 | array $values 216 | ): PromiseInterface; 217 | 218 | /** 219 | * Table related shortcuts. 220 | */ 221 | 222 | /** 223 | * Easy shortcut for creating tables. Fields is just a simple key value, 224 | * being the key the name of the field, and the value the type. By default, 225 | * Varchar types have length 255. 226 | * 227 | * First field is considered as primary key. 228 | * 229 | * @param string $name 230 | * @param array $fields 231 | * @param array $extra 232 | * @param bool $autoincrementId 233 | * 234 | * @return PromiseInterface 235 | * 236 | * @throws InvalidArgumentException 237 | * @throws TableExistsException 238 | */ 239 | public function createTable( 240 | string $name, 241 | array $fields, 242 | array $extra = [], 243 | bool $autoincrementId = false 244 | ): PromiseInterface; 245 | 246 | /** 247 | * @param string $name 248 | * 249 | * @return PromiseInterface 250 | * 251 | * @throws TableNotFoundException 252 | */ 253 | public function dropTable(string $name): PromiseInterface; 254 | 255 | /** 256 | * @param string $name 257 | * 258 | * @return PromiseInterface 259 | * 260 | * @throws TableNotFoundException 261 | */ 262 | public function truncateTable(string $name): PromiseInterface; 263 | 264 | /** 265 | * @return \React\Promise\PromiseInterface<\Drift\DBAL\SingleConnection> 266 | */ 267 | public function startTransaction(): PromiseInterface; 268 | 269 | public function commitTransaction(SingleConnection $connection): PromiseInterface; 270 | 271 | public function rollbackTransaction(SingleConnection $connection): PromiseInterface; 272 | } 273 | -------------------------------------------------------------------------------- /src/ConnectionOptions.php: -------------------------------------------------------------------------------- 1 | keepAliveIntervalSec = $keepAliveIntervalSec; 17 | } 18 | 19 | /** 20 | * @return int 21 | */ 22 | public function getKeepAliveIntervalSec(): int 23 | { 24 | return $this->keepAliveIntervalSec; 25 | } 26 | 27 | /** 28 | * @param int $keepAliveIntervalSec 29 | */ 30 | public function setKeepAliveIntervalSec(int $keepAliveIntervalSec): void 31 | { 32 | $this->keepAliveIntervalSec = $keepAliveIntervalSec; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ConnectionPool.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL; 17 | 18 | use Doctrine\DBAL\Exception as DBALException; 19 | use Doctrine\DBAL\Exception\InvalidArgumentException; 20 | use Doctrine\DBAL\Exception\TableExistsException; 21 | use Doctrine\DBAL\Exception\TableNotFoundException; 22 | use Doctrine\DBAL\Platforms\AbstractPlatform; 23 | use Doctrine\DBAL\Query\QueryBuilder; 24 | use Doctrine\DBAL\Schema\Schema; 25 | use Drift\DBAL\Driver\Driver; 26 | use React\Promise\Deferred; 27 | use React\Promise\PromiseInterface; 28 | use RuntimeException; 29 | use SplObjectStorage; 30 | 31 | use function React\Promise\resolve; 32 | 33 | /** 34 | * Class ConnectionPool. 35 | */ 36 | class ConnectionPool implements Connection, ConnectionPoolInterface 37 | { 38 | 39 | /** 40 | * @var SplObjectStorage|array<\Drift\DBAL\Connection, \Drift\DBAL\ConnectionWorker> $connections 41 | */ 42 | private SplObjectStorage $connections; 43 | 44 | /** 45 | * @var SplObjectStorage|Deferred[] $deferreds 46 | */ 47 | private SplObjectStorage $deferreds; 48 | 49 | /** 50 | * Connection constructor. 51 | * 52 | * @param ConnectionWorker[] $workers 53 | */ 54 | private function __construct(array $workers) 55 | { 56 | $this->connections = new SplObjectStorage; 57 | 58 | foreach ($workers as $worker) { 59 | $this->connections->attach($worker->getConnection(), $worker); 60 | } 61 | 62 | $this->deferreds = new SplObjectStorage; 63 | } 64 | 65 | /** 66 | * Create new connection. 67 | * 68 | * @param Driver $driver 69 | * @param Credentials $credentials 70 | * @param AbstractPlatform $platform 71 | * @param ConnectionOptions|null $options 72 | * 73 | * @return Connection 74 | */ 75 | public static function create( 76 | Driver $driver, 77 | Credentials $credentials, 78 | AbstractPlatform $platform, 79 | ?ConnectionOptions $options = null 80 | ): Connection { 81 | $numberOfConnections = $credentials->getConnections() ?? 82 | ConnectionPoolOptions::DEFAULT_NUMBER_OF_CONNECTIONS; 83 | if ($options instanceof ConnectionPoolOptions) { 84 | $numberOfConnections = $options->getNumberOfConnections(); 85 | } 86 | 87 | // Since using transactions with a single connection in an asynchronous environment 88 | // probably doesn't do what you want it to do* we explicitly disallow it. 89 | 90 | if ($numberOfConnections <= 1) { 91 | return SingleConnection::create( 92 | $driver, 93 | $credentials, 94 | $platform, 95 | $options 96 | ); 97 | } 98 | 99 | $workers = []; 100 | for ($i = 0; $i < $numberOfConnections; ++$i) { 101 | $workers[] = new ConnectionWorker( 102 | SingleConnection::create( 103 | clone $driver, 104 | $credentials, 105 | $platform, 106 | $options, 107 | true 108 | ), $i 109 | ); 110 | } 111 | 112 | return new self($workers); 113 | } 114 | 115 | /** 116 | * Create new connection. 117 | * 118 | * @param Driver $driver 119 | * @param Credentials $credentials 120 | * @param AbstractPlatform $platform 121 | * @param ConnectionOptions|null $options 122 | * 123 | * @return Connection 124 | */ 125 | public static function createConnected( 126 | Driver $driver, 127 | Credentials $credentials, 128 | AbstractPlatform $platform, 129 | ?ConnectionOptions $options = null 130 | ): Connection { 131 | $connection = self::create($driver, $credentials, $platform, $options); 132 | $connection->connect($options); 133 | 134 | return $connection; 135 | } 136 | 137 | /** 138 | * @return string 139 | */ 140 | public function getDriverNamespace(): string 141 | { 142 | $connections = clone $this->connections; 143 | $worker = $connections->current(); 144 | return $worker->getDriverNamespace(); 145 | } 146 | 147 | /** 148 | * @param bool $increaseJobs 149 | * 150 | * @return PromiseInterface 151 | */ 152 | private function bestConnectionWorker( 153 | bool $increaseJobs = false 154 | ): PromiseInterface { 155 | $minJobs = 1000000000; 156 | $bestConnection = null; 157 | 158 | foreach ($this->connections as $connection) { 159 | $worker = $this->connections->getInfo(); 160 | if ($worker->getJobs() < $minJobs && !$worker->isLeased()) { 161 | $minJobs = $worker->getJobs(); 162 | $bestConnection = $worker; 163 | } 164 | } 165 | 166 | if ($bestConnection !== null) { 167 | if ($increaseJobs) { 168 | $bestConnection->startJob(); 169 | } 170 | 171 | return resolve($bestConnection); 172 | } 173 | 174 | // We may want to introduce some logging here. 175 | // fwrite(STDOUT, 'no more workers' . PHP_EOL); 176 | 177 | $deferred = new Deferred; 178 | $this->deferreds->attach($deferred); 179 | 180 | if ($increaseJobs) { 181 | $deferred->promise()->then(fn(ConnectionWorker $w) => $w->startJob()); 182 | } 183 | 184 | return $deferred->promise(); 185 | } 186 | 187 | /** 188 | * @param callable $callable 189 | * 190 | * @return PromiseInterface 191 | */ 192 | private function executeInBestConnection(callable $callable): PromiseInterface 193 | { 194 | $worker = null; 195 | 196 | return $this->bestConnectionWorker(true) 197 | ->then(function (ConnectionWorker $connectionWorker) use ($callable, &$worker) { 198 | $worker = $connectionWorker; 199 | return $callable($worker->getConnection()); 200 | }) 201 | ->then(function ($whatever) use (&$worker) { 202 | $worker->stopJob(); 203 | return $whatever; 204 | }); 205 | } 206 | 207 | /** 208 | * Connect. 209 | */ 210 | public function connect(?ConnectionOptions $options = null) 211 | { 212 | foreach ($this->connections as $connection) { 213 | $connection->connect($options); 214 | } 215 | } 216 | 217 | /** 218 | * Close. 219 | */ 220 | public function close() 221 | { 222 | foreach ($this->connections as $connection) { 223 | $connection->close(); 224 | } 225 | } 226 | 227 | /** 228 | * Creates QueryBuilder. 229 | * 230 | * @return QueryBuilder 231 | * 232 | * @throws DBALException 233 | */ 234 | public function createQueryBuilder(): QueryBuilder 235 | { 236 | // We clone the worker storage because we probably can't rely on 237 | // SplObjectStorage::current(...) working reliably in an 238 | // asynchronous environment. 239 | $workers = clone $this->connections; 240 | $workers->rewind(); 241 | 242 | return $workers->current() 243 | ->createQueryBuilder(); 244 | } 245 | 246 | /** 247 | * Query by query builder. 248 | * 249 | * @param QueryBuilder $queryBuilder 250 | * 251 | * @return PromiseInterface 252 | */ 253 | public function query(QueryBuilder $queryBuilder): PromiseInterface 254 | { 255 | return $this->executeInBestConnection(function (Connection $connection) use ($queryBuilder) { 256 | return $connection->query($queryBuilder); 257 | }); 258 | } 259 | 260 | /** 261 | * Query by sql and parameters. 262 | * 263 | * @param string $sql 264 | * @param array $parameters 265 | * 266 | * @return PromiseInterface 267 | */ 268 | public function queryBySQL(string $sql, array $parameters = []): PromiseInterface 269 | { 270 | return $this->executeInBestConnection(function (Connection $connection) use ($sql, $parameters) { 271 | return $connection->queryBySQL($sql, $parameters); 272 | }); 273 | } 274 | 275 | /** 276 | * Execute, sequentially, an array of sqls. 277 | * 278 | * @param string[] $sqls 279 | * 280 | * @return PromiseInterface 281 | */ 282 | public function executeSQLs(array $sqls): PromiseInterface 283 | { 284 | return $this->executeInBestConnection(function (Connection $connection) use ($sqls) { 285 | return $connection->executeSQLs($sqls); 286 | }); 287 | } 288 | 289 | /** 290 | * Execute an schema. 291 | * 292 | * @param Schema $schema 293 | * 294 | * @return PromiseInterface 295 | */ 296 | public function executeSchema(Schema $schema): PromiseInterface 297 | { 298 | return $this->executeInBestConnection(function (Connection $connection) use ($schema) { 299 | return $connection->executeSchema($schema); 300 | }); 301 | } 302 | 303 | /** 304 | * Shortcuts. 305 | */ 306 | 307 | /** 308 | * Find one by. 309 | * 310 | * connection->findOneById('table', ['id' => 1]); 311 | * 312 | * @param string $table 313 | * @param array $where 314 | * 315 | * @return PromiseInterface 316 | */ 317 | public function findOneBy( 318 | string $table, 319 | array $where 320 | ): PromiseInterface { 321 | return $this->executeInBestConnection(function (Connection $connection) use ($table, $where) { 322 | return $connection->findOneBy($table, $where); 323 | }); 324 | } 325 | 326 | /** 327 | * Find by. 328 | * 329 | * connection->findBy('table', ['id' => 1]); 330 | * 331 | * @param string $table 332 | * @param array $where 333 | * 334 | * @return PromiseInterface 335 | */ 336 | public function findBy( 337 | string $table, 338 | array $where = [] 339 | ): PromiseInterface { 340 | return $this->executeInBestConnection(function (Connection $connection) use ($table, $where) { 341 | return $connection->findBy($table, $where); 342 | }); 343 | } 344 | 345 | /** 346 | * @param string $table 347 | * @param array $values 348 | * 349 | * @return PromiseInterface 350 | */ 351 | public function insert( 352 | string $table, 353 | array $values 354 | ): PromiseInterface { 355 | return $this->executeInBestConnection(function (Connection $connection) use ($table, $values) { 356 | return $connection->insert($table, $values); 357 | }); 358 | } 359 | 360 | /** 361 | * @param string $table 362 | * @param array $values 363 | * 364 | * @return PromiseInterface 365 | * 366 | * @throws InvalidArgumentException 367 | */ 368 | public function delete( 369 | string $table, 370 | array $values 371 | ): PromiseInterface { 372 | return $this->executeInBestConnection(function (Connection $connection) use ($table, $values) { 373 | return $connection->delete($table, $values); 374 | }); 375 | } 376 | 377 | /** 378 | * @param string $table 379 | * @param array $id 380 | * @param array $values 381 | * 382 | * @return PromiseInterface 383 | * 384 | * @throws InvalidArgumentException 385 | */ 386 | public function update( 387 | string $table, 388 | array $id, 389 | array $values 390 | ): PromiseInterface { 391 | return $this->executeInBestConnection(function (Connection $connection) use ($table, $id, $values) { 392 | return $connection->update($table, $id, $values); 393 | }); 394 | } 395 | 396 | /** 397 | * @param string $table 398 | * @param array $id 399 | * @param array $values 400 | * 401 | * @return PromiseInterface 402 | * 403 | * @throws InvalidArgumentException 404 | */ 405 | public function upsert( 406 | string $table, 407 | array $id, 408 | array $values 409 | ): PromiseInterface { 410 | return $this->executeInBestConnection(function (Connection $connection) use ($table, $id, $values) { 411 | return $connection->upsert($table, $id, $values); 412 | }); 413 | } 414 | 415 | /** 416 | * Table related shortcuts. 417 | */ 418 | 419 | /** 420 | * Easy shortcut for creating tables. Fields is just a simple key value, 421 | * being the key the name of the field, and the value the type. By default, 422 | * Varchar types have length 255. 423 | * 424 | * First field is considered as primary key. 425 | * 426 | * @param string $name 427 | * @param array $fields 428 | * @param array $extra 429 | * @param bool $autoincrementId 430 | * 431 | * @return PromiseInterface 432 | * 433 | * @throws InvalidArgumentException 434 | * @throws TableExistsException 435 | */ 436 | public function createTable( 437 | string $name, 438 | array $fields, 439 | array $extra = [], 440 | bool $autoincrementId = false 441 | ): PromiseInterface { 442 | return $this->executeInBestConnection(function (Connection $connection) use ( 443 | $name, 444 | $fields, 445 | $extra, 446 | $autoincrementId 447 | ) { 448 | return $connection->createTable($name, $fields, $extra, $autoincrementId); 449 | }); 450 | } 451 | 452 | /** 453 | * @param string $name 454 | * 455 | * @return PromiseInterface 456 | * 457 | * @throws TableNotFoundException 458 | */ 459 | public function dropTable(string $name): PromiseInterface 460 | { 461 | return $this->executeInBestConnection(function (Connection $connection) use ($name) { 462 | return $connection->dropTable($name); 463 | }); 464 | } 465 | 466 | /** 467 | * @param string $name 468 | * 469 | * @return PromiseInterface 470 | * 471 | * @throws TableNotFoundException 472 | */ 473 | public function truncateTable(string $name): PromiseInterface 474 | { 475 | return $this->executeInBestConnection(function (Connection $connection) use ($name) { 476 | return $connection->truncateTable($name); 477 | }); 478 | } 479 | 480 | /** 481 | * Get the Pool's connection workers 482 | * 483 | * @return ConnectionWorker[] 484 | */ 485 | public function getConnections(): array 486 | { 487 | $workers = []; 488 | 489 | $connections = clone $this->connections; 490 | foreach ($connections as $connection) { 491 | $workers[] = $connections->getInfo(); 492 | } 493 | 494 | return $workers; 495 | } 496 | 497 | public function startTransaction(): PromiseInterface 498 | { 499 | return $this->bestConnectionWorker() 500 | ->then(function (ConnectionWorker $worker) { 501 | $worker->setLeased(true); 502 | 503 | $connection = $worker->getConnection(); 504 | 505 | if (!$connection instanceof SingleConnection) { 506 | throw new RuntimeException('connection must be instance of ' . SingleConnection::class); 507 | } 508 | 509 | $connection->startTransaction(); 510 | 511 | return resolve($connection); 512 | }); 513 | } 514 | 515 | public function commitTransaction(SingleConnection $connection): PromiseInterface 516 | { 517 | return $connection->commitTransaction($connection) 518 | ->always(fn() => $this->releaseConnection($connection)); 519 | } 520 | 521 | public function rollbackTransaction(SingleConnection $connection): PromiseInterface 522 | { 523 | return $connection->rollbackTransaction($connection) 524 | ->always(fn() => $this->releaseConnection($connection)); 525 | } 526 | 527 | private function releaseConnection(SingleConnection $connection): PromiseInterface 528 | { 529 | if (count($this->deferreds) === 0) { 530 | /** @var \Drift\DBAL\ConnectionWorker $worker */ 531 | $worker = $this->connections[$connection]; 532 | $worker->setLeased(false); 533 | return resolve(); 534 | } 535 | 536 | $deferred = $this->deferreds->current(); 537 | $this->deferreds->detach($deferred); 538 | 539 | $deferred->resolve($this->connections[$connection]); 540 | return resolve(); 541 | } 542 | 543 | } 544 | -------------------------------------------------------------------------------- /src/ConnectionPoolInterface.php: -------------------------------------------------------------------------------- 1 | numberOfConnections = $numberOfConnections; 20 | parent::__construct($keepAliveIntervalSec); 21 | } 22 | 23 | /** 24 | * @return int 25 | */ 26 | public function getNumberOfConnections(): int 27 | { 28 | return $this->numberOfConnections; 29 | } 30 | 31 | /** 32 | * @param int $numberOfConnections 33 | */ 34 | public function setNumberOfConnections(int $numberOfConnections): void 35 | { 36 | $this->numberOfConnections = $numberOfConnections; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ConnectionWorker.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL; 17 | 18 | /** 19 | * Class ConnectionWorker. 20 | */ 21 | class ConnectionWorker 22 | { 23 | private Connection $connection; 24 | private int $id; 25 | private int $jobs; 26 | private bool $leased = false; 27 | 28 | /** 29 | * @param Connection $connection 30 | * @param int $id 31 | */ 32 | public function __construct(Connection $connection, int $id) 33 | { 34 | $this->connection = $connection; 35 | $this->id = $id; 36 | $this->jobs = 0; 37 | } 38 | 39 | public function startJob() 40 | { 41 | ++$this->jobs; 42 | } 43 | 44 | public function stopJob() 45 | { 46 | --$this->jobs; 47 | } 48 | 49 | /** 50 | * @return Connection 51 | */ 52 | public function getConnection(): Connection 53 | { 54 | return $this->connection; 55 | } 56 | 57 | /** 58 | * @return int 59 | */ 60 | public function getId(): int 61 | { 62 | return $this->id; 63 | } 64 | 65 | /** 66 | * @return int 67 | */ 68 | public function getJobs(): int 69 | { 70 | return $this->jobs; 71 | } 72 | 73 | public function isLeased(): bool 74 | { 75 | return $this->leased; 76 | } 77 | 78 | public function setLeased(bool $leased): ConnectionWorker 79 | { 80 | $this->leased = $leased; 81 | return $this; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/Credentials.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL; 17 | 18 | /** 19 | * Class Credentials. 20 | */ 21 | class Credentials 22 | { 23 | private string $host; 24 | private string $port; 25 | private string $user; 26 | private string $password; 27 | private string $dbName; 28 | 29 | /** 30 | * @deprecated 31 | * @var array options 32 | */ 33 | private array $options; 34 | 35 | /** 36 | * @deprecated 37 | * @var int|null $connections 38 | */ 39 | private ?int $connections = null; 40 | 41 | /** 42 | * Credentials constructor. 43 | * 44 | * @param string $host 45 | * @param string $port 46 | * @param string $user 47 | * @param string $password 48 | * @param string $dbName 49 | */ 50 | public function __construct( 51 | string $host, 52 | string $port, 53 | string $user, 54 | string $password, 55 | string $dbName 56 | ) { 57 | $this->host = $host; 58 | $this->port = $port; 59 | $this->user = $user; 60 | $this->password = $password; 61 | $this->dbName = $dbName; 62 | 63 | if (func_num_args() > 5) { 64 | trigger_error( 65 | '6th argument is deprecated, for options please use ' . ConnectionOptions::class . 66 | ' or and extend of this class instead, when creating a connection', 67 | E_USER_DEPRECATED 68 | ); 69 | $this->options = (array)func_get_arg(5); 70 | } 71 | if (func_num_args() > 6) { 72 | trigger_error( 73 | '7th argument is deprecated, please use ' . ConnectionPoolOptions::class . 74 | ' to set the number of connections, when creating a ' . ConnectionPool::class, 75 | E_USER_DEPRECATED 76 | ); 77 | $this->connections = (int)func_get_arg(6); 78 | } 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getHost(): string 85 | { 86 | return $this->host; 87 | } 88 | 89 | /** 90 | * @return string 91 | */ 92 | public function getPort(): string 93 | { 94 | return $this->port; 95 | } 96 | 97 | /** 98 | * @return string 99 | */ 100 | public function getUser(): string 101 | { 102 | return $this->user; 103 | } 104 | 105 | /** 106 | * @return string 107 | */ 108 | public function getPassword(): string 109 | { 110 | return $this->password; 111 | } 112 | 113 | /** 114 | * @return string 115 | */ 116 | public function getDbName(): string 117 | { 118 | return $this->dbName; 119 | } 120 | 121 | /** 122 | * @deprecated 123 | * @return array 124 | */ 125 | public function getOptions(): array 126 | { 127 | return $this->options; 128 | } 129 | 130 | /** 131 | * @deprecated 132 | * @return int|null 133 | */ 134 | public function getConnections(): ?int 135 | { 136 | return $this->connections; 137 | } 138 | 139 | /** 140 | * To string. 141 | */ 142 | public function toString(): string 143 | { 144 | $asString = sprintf( 145 | '%s:%s@%s:%d/%s', 146 | $this->user, 147 | $this->password, 148 | $this->host, 149 | $this->port, 150 | $this->dbName 151 | ); 152 | 153 | if (0 === strpos($asString, ':@')) { 154 | return rawurldecode( 155 | substr($asString, 2) 156 | ); 157 | } 158 | 159 | return rawurldecode( 160 | str_replace(':@', '@', $asString) 161 | ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Driver/AbstractDriver.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Driver; 17 | 18 | use Doctrine\DBAL\Query\QueryBuilder; 19 | use React\Promise\PromiseInterface; 20 | 21 | /** 22 | * Class AbstractDriver. 23 | */ 24 | abstract class AbstractDriver implements Driver 25 | { 26 | /** 27 | * @param QueryBuilder $queryBuilder 28 | * @param string $table 29 | * @param array $values 30 | * 31 | * @return PromiseInterface 32 | */ 33 | public function insert(QueryBuilder $queryBuilder, string $table, array $values): PromiseInterface 34 | { 35 | $queryBuilder = $this->createInsertQuery($queryBuilder, $table, $values); 36 | 37 | return $this->query($queryBuilder->getSQL(), $queryBuilder->getParameters()); 38 | } 39 | 40 | /** 41 | * @param QueryBuilder $queryBuilder 42 | * @param string $table 43 | * @param array $values 44 | * 45 | * @return QueryBuilder 46 | */ 47 | protected function createInsertQuery(QueryBuilder $queryBuilder, string $table, array $values): QueryBuilder 48 | { 49 | return $queryBuilder->insert($table) 50 | ->values(array_combine( 51 | array_keys($values), 52 | array_fill(0, count($values), '?') 53 | )) 54 | ->setParameters(array_values($values)); 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/Driver/Driver.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Driver; 17 | 18 | use Doctrine\DBAL\Query\QueryBuilder; 19 | use Drift\DBAL\Credentials; 20 | use Drift\DBAL\Result; 21 | use React\Promise\PromiseInterface; 22 | 23 | /** 24 | * Interface Driver. 25 | */ 26 | interface Driver 27 | { 28 | /** 29 | * Attempts to create a connection with the database. 30 | * 31 | * @param Credentials $credentials 32 | */ 33 | public function connect(Credentials $credentials); 34 | 35 | /** 36 | * Make query. 37 | * 38 | * @param string $sql 39 | * @param array $parameters 40 | * 41 | * @return PromiseInterface 42 | */ 43 | public function query( 44 | string $sql, 45 | array $parameters 46 | ): PromiseInterface; 47 | 48 | /** 49 | * @param QueryBuilder $queryBuilder 50 | * @param string $table 51 | * @param array $values 52 | * 53 | * @return PromiseInterface 54 | */ 55 | public function insert( 56 | QueryBuilder $queryBuilder, 57 | string $table, 58 | array $values 59 | ): PromiseInterface; 60 | 61 | /** 62 | * @return void 63 | */ 64 | public function close(): void; 65 | 66 | public function startTransaction(): PromiseInterface; 67 | 68 | public function commitTransaction(): PromiseInterface; 69 | 70 | public function rollbackTransaction(): PromiseInterface; 71 | } 72 | -------------------------------------------------------------------------------- /src/Driver/Exception.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Driver; 17 | 18 | use Throwable; 19 | 20 | /** 21 | * Class Exception. 22 | */ 23 | class Exception extends \Exception implements \Doctrine\DBAL\Driver\Exception 24 | { 25 | /** 26 | * The SQLSTATE of the driver. 27 | * 28 | * @var string|null 29 | */ 30 | private $sqlState; 31 | 32 | /** 33 | * @param string $message The driver error message. 34 | * @param string|null $sqlState The SQLSTATE the driver is in at the time the error occurred, if any. 35 | * @param int $code The driver specific error code if any. 36 | * @param Throwable|null $previous The previous throwable used for the exception chaining. 37 | */ 38 | public function __construct($message, $sqlState = null, $code = 0, ?Throwable $previous = null) 39 | { 40 | parent::__construct($message, $code, $previous); 41 | 42 | $this->sqlState = $sqlState; 43 | } 44 | 45 | public function getSQLState() 46 | { 47 | return $this->sqlState; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Driver/Mysql/EmptyDoctrineMysqlDriver.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Driver\Mysql; 17 | 18 | use Doctrine\DBAL\Driver\AbstractMySQLDriver; 19 | use Doctrine\DBAL\Driver\Connection as DriverConnection; 20 | use Exception; 21 | 22 | /** 23 | * Class EmptyDoctrineMysqlDriver. 24 | */ 25 | final class EmptyDoctrineMysqlDriver extends AbstractMySQLDriver 26 | { 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function connect(array $params, $username = null, $password = null, array $driverOptions = []): DriverConnection 31 | { 32 | throw new Exception('Do not use this method.'); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getName() 39 | { 40 | throw new Exception('Do not use this method.'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Driver/Mysql/MysqlDriver.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Driver\Mysql; 17 | 18 | use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface; 19 | use Doctrine\DBAL\Driver\API\MySQL\ExceptionConverter; 20 | use Doctrine\DBAL\Query; 21 | use Drift\DBAL\Credentials; 22 | use Drift\DBAL\Driver\AbstractDriver; 23 | use Drift\DBAL\Driver\Exception as DoctrineException; 24 | use Drift\DBAL\Result; 25 | use React\EventLoop\LoopInterface; 26 | use React\MySQL\ConnectionInterface; 27 | use React\MySQL\Exception; 28 | use React\MySQL\Factory; 29 | use React\MySQL\QueryResult; 30 | use React\Promise\PromiseInterface; 31 | use React\Socket\ConnectorInterface; 32 | 33 | /** 34 | * Class MysqlDriver. 35 | */ 36 | class MysqlDriver extends AbstractDriver 37 | { 38 | private Factory $factory; 39 | private ConnectionInterface $connection; 40 | private EmptyDoctrineMysqlDriver $doctrineDriver; 41 | private ExceptionConverterInterface $exceptionConverter; 42 | 43 | /** 44 | * MysqlDriver constructor. 45 | * 46 | * @param LoopInterface $loop 47 | * @param ConnectorInterface $connector 48 | */ 49 | public function __construct(LoopInterface $loop, ConnectorInterface $connector = null) 50 | { 51 | $this->doctrineDriver = new EmptyDoctrineMysqlDriver(); 52 | $this->factory = is_null($connector) 53 | ? new Factory($loop) 54 | : new Factory($loop, $connector); 55 | $this->exceptionConverter = new ExceptionConverter(); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function connect(Credentials $credentials, array $options = []) 62 | { 63 | $this->connection = $this 64 | ->factory 65 | ->createLazyConnection($credentials->toString()); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function query( 72 | string $sql, 73 | array $parameters 74 | ): PromiseInterface { 75 | return $this 76 | ->connection 77 | ->query($sql, $parameters) 78 | ->then(function (QueryResult $queryResult) { 79 | return new Result( 80 | $queryResult->resultRows, 81 | $queryResult->insertId, 82 | $queryResult->affectedRows 83 | ); 84 | }) 85 | ->otherwise(function (Exception $exception) use (&$sql, &$parameters) { 86 | throw $this->exceptionConverter->convert(new DoctrineException($exception->getMessage(), null, $exception->getCode()), new Query($sql, $parameters, [])); 87 | }); 88 | } 89 | 90 | /** 91 | * @return void 92 | */ 93 | public function close(): void 94 | { 95 | $this->connection->close(); 96 | } 97 | 98 | public function startTransaction(): PromiseInterface 99 | { 100 | return $this->query('START TRANSACTION', []); 101 | } 102 | 103 | public function commitTransaction(): PromiseInterface 104 | { 105 | return $this->query('COMMIT', []); 106 | } 107 | 108 | public function rollbackTransaction(): PromiseInterface 109 | { 110 | return $this->query('ROLLBACK', []); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/Driver/PlainDriverException.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Driver; 17 | 18 | use Doctrine\DBAL\Driver\DriverException; 19 | use Exception; 20 | 21 | /** 22 | * Class PlainDriverException. 23 | */ 24 | final class PlainDriverException extends Exception implements DriverException 25 | { 26 | /** 27 | * @var string 28 | */ 29 | private $sqlState; 30 | 31 | /** 32 | * Create by sqlstate. 33 | * 34 | * @param string $message 35 | * @param string $sqlState 36 | * 37 | * @return PlainDriverException 38 | */ 39 | public static function createFromMessageAndErrorCode( 40 | string $message, 41 | string $sqlState 42 | ): self { 43 | $exception = new self($message); 44 | $exception->sqlState = $sqlState; 45 | 46 | return $exception; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function getErrorCode() 53 | { 54 | return $this->sqlState; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function getSQLState() 61 | { 62 | return $this->sqlState; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Driver/PostgreSQL/EmptyDoctrinePostgreSQLDriver.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Driver\PostgreSQL; 17 | 18 | use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver; 19 | use Doctrine\DBAL\Driver\Connection as DriverConnection; 20 | use Exception; 21 | 22 | /** 23 | * Class EmptyDoctrinePostgreSQLDriver. 24 | */ 25 | final class EmptyDoctrinePostgreSQLDriver extends AbstractPostgreSQLDriver 26 | { 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function connect(array $params, $username = null, $password = null, array $driverOptions = []): DriverConnection 31 | { 32 | throw new Exception('Do not use this method.'); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getName() 39 | { 40 | throw new Exception('Do not use this method.'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Driver/PostgreSQL/PostgreSQLDriver.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Driver\PostgreSQL; 17 | 18 | use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface; 19 | use Doctrine\DBAL\Driver\API\PostgreSQL\ExceptionConverter; 20 | use Doctrine\DBAL\Exception; 21 | use Doctrine\DBAL\Query; 22 | use Doctrine\DBAL\Query\QueryBuilder; 23 | use Drift\DBAL\Credentials; 24 | use Drift\DBAL\Driver\AbstractDriver; 25 | use Drift\DBAL\Driver\Exception as DoctrineException; 26 | use Drift\DBAL\Result; 27 | use PgAsync\Client; 28 | use PgAsync\Connection; 29 | use PgAsync\ErrorException; 30 | use React\EventLoop\LoopInterface; 31 | use React\Promise\Deferred; 32 | use React\Promise\PromiseInterface; 33 | use function React\Promise\reject; 34 | 35 | /** 36 | * Class PostgreSQLDriver. 37 | */ 38 | class PostgreSQLDriver extends AbstractDriver 39 | { 40 | private Connection $connection; 41 | private LoopInterface $loop; 42 | private EmptyDoctrinePostgreSQLDriver $doctrineDriver; 43 | private ExceptionConverterInterface $exceptionConverter; 44 | private bool $isClosed = false; 45 | 46 | /** 47 | * @param LoopInterface $loop 48 | */ 49 | public function __construct(LoopInterface $loop) 50 | { 51 | $this->doctrineDriver = new EmptyDoctrinePostgreSQLDriver(); 52 | $this->loop = $loop; 53 | $this->exceptionConverter = new ExceptionConverter(); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function connect(Credentials $credentials, array $options = []) 60 | { 61 | $this->connection = 62 | (new Client([ 63 | 'host' => $credentials->getHost(), 64 | 'port' => $credentials->getPort(), 65 | 'user' => $credentials->getUser(), 66 | 'password' => $credentials->getPassword(), 67 | 'database' => $credentials->getDbName(), 68 | ], $this->loop)) 69 | ->getIdleConnection(); 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function query( 76 | string $sql, 77 | array $parameters 78 | ): PromiseInterface { 79 | if ($this->isClosed) { 80 | return reject(new Exception('Connection closed')); 81 | } 82 | 83 | /** 84 | * We should fix the parametrization. 85 | */ 86 | $i = 1; 87 | $sql = preg_replace_callback('~\?~', function ($_) use (&$i) { 88 | return '$'.$i++; 89 | }, $sql); 90 | 91 | $results = []; 92 | $deferred = new Deferred(); 93 | 94 | $this 95 | ->connection 96 | ->executeStatement($sql, $parameters) 97 | ->subscribe(function ($row) use (&$results) { 98 | $results[] = $row; 99 | }, function (ErrorException $exception) use ($deferred, &$sql, &$parameters) { 100 | $errorResponse = $exception->getErrorResponse(); 101 | $code = 0; 102 | foreach ($errorResponse->getErrorMessages() as $messageLine) { 103 | if ('C' === $messageLine['type']) { 104 | $code = $messageLine['message']; 105 | } 106 | } 107 | 108 | $exception = $this->exceptionConverter->convert( 109 | new DoctrineException($exception->getMessage(), \strval($code)), 110 | new Query( 111 | $sql, $parameters, [] 112 | ) 113 | ); 114 | 115 | $deferred->reject($exception); 116 | }, function () use (&$results, $deferred) { 117 | $deferred->resolve($results); 118 | }); 119 | 120 | return $deferred 121 | ->promise() 122 | ->then(function ($results) { 123 | return new Result($results, null, null); 124 | }); 125 | } 126 | 127 | /** 128 | * @param QueryBuilder $queryBuilder 129 | * @param string $table 130 | * @param array $values 131 | * 132 | * @return PromiseInterface 133 | */ 134 | public function insert(QueryBuilder $queryBuilder, string $table, array $values): PromiseInterface 135 | { 136 | if ($this->isClosed) { 137 | return reject(new Exception('Connection closed')); 138 | } 139 | 140 | $queryBuilder = $this->createInsertQuery($queryBuilder, $table, $values); 141 | $query = 'SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = ?'; 142 | 143 | return $this 144 | ->query($query, [$table]) 145 | ->then(function (Result $response) use ($queryBuilder) { 146 | $allRows = $response->fetchAllRows(); 147 | $fields = array_map(function ($item) { 148 | return $item['column_name']; 149 | }, $allRows); 150 | 151 | // When there are no fields, means that the table does not exist 152 | // To make the normal behavior, we make a simple query and let 153 | // the DBAL do the job (no last_inserted_it is expected here 154 | 155 | $returningPart = empty($fields) 156 | ? '' 157 | : ' RETURNING '.implode(',', $fields); 158 | 159 | return $this 160 | ->query($queryBuilder->getSQL().$returningPart, $queryBuilder->getParameters()) 161 | ->then(function (Result $result) use ($fields) { 162 | return 0 === count($fields) 163 | ? new Result(0, null, null) 164 | : new Result([], \intval($result->fetchFirstRow()[$fields[0]]), 1); 165 | }); 166 | }); 167 | } 168 | 169 | /** 170 | * @return void 171 | */ 172 | public function close(): void 173 | { 174 | $this->isClosed = true; 175 | $this 176 | ->connection 177 | ->disconnect(); 178 | } 179 | 180 | public function startTransaction(): PromiseInterface 181 | { 182 | return $this->query('START TRANSACTION', []); 183 | } 184 | 185 | public function commitTransaction(): PromiseInterface 186 | { 187 | return $this->query('COMMIT', []); 188 | } 189 | 190 | public function rollbackTransaction(): PromiseInterface 191 | { 192 | return $this->query('ROLLBACK', []); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Driver/SQLite/EmptyDoctrineSQLiteDriver.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Driver\SQLite; 17 | 18 | use Doctrine\DBAL\Driver\AbstractSQLiteDriver; 19 | use Doctrine\DBAL\Driver\Connection as DriverConnection; 20 | use Exception; 21 | 22 | /** 23 | * Class EmptyDoctrineSQLiteDriver. 24 | */ 25 | final class EmptyDoctrineSQLiteDriver extends AbstractSQLiteDriver 26 | { 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function connect(array $params, $username = null, $password = null, array $driverOptions = []): DriverConnection 31 | { 32 | throw new Exception('Do not use this method.'); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getName() 39 | { 40 | throw new Exception('Do not use this method.'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Driver/SQLite/SQLiteDriver.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Driver\SQLite; 17 | 18 | use Clue\React\SQLite\DatabaseInterface; 19 | use Clue\React\SQLite\Factory; 20 | use Clue\React\SQLite\Result as SQLiteResult; 21 | use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface; 22 | use Doctrine\DBAL\Driver\API\SQLite\ExceptionConverter; 23 | use Doctrine\DBAL\Query; 24 | use Drift\DBAL\Credentials; 25 | use Drift\DBAL\Driver\AbstractDriver; 26 | use Drift\DBAL\Driver\Exception as DoctrineException; 27 | use Drift\DBAL\Result; 28 | use React\EventLoop\LoopInterface; 29 | use React\Promise\PromiseInterface; 30 | use RuntimeException; 31 | 32 | /** 33 | * Class SQLiteDriver. 34 | */ 35 | class SQLiteDriver extends AbstractDriver 36 | { 37 | private Factory $factory; 38 | private DatabaseInterface $database; 39 | private EmptyDoctrineSQLiteDriver $doctrineDriver; 40 | private ExceptionConverterInterface $exceptionConverter; 41 | 42 | /** 43 | * SQLiteDriver constructor. 44 | * 45 | * @param LoopInterface $loop 46 | */ 47 | public function __construct(LoopInterface $loop) 48 | { 49 | $this->doctrineDriver = new EmptyDoctrineSQLiteDriver(); 50 | $this->factory = new Factory($loop); 51 | $this->exceptionConverter = new ExceptionConverter(); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function connect(Credentials $credentials, array $options = []) 58 | { 59 | $this->database = $this 60 | ->factory 61 | ->openLazy($credentials->getDbName()); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function query( 68 | string $sql, 69 | array $parameters 70 | ): PromiseInterface { 71 | return $this 72 | ->database 73 | ->query($sql, $parameters) 74 | ->then(function (SQLiteResult $sqliteResult) { 75 | return new Result( 76 | $sqliteResult->rows, 77 | $sqliteResult->insertId, 78 | $sqliteResult->changed 79 | ); 80 | }) 81 | ->otherwise(function (RuntimeException $exception) use (&$sql, &$parameters) { 82 | throw $this->exceptionConverter->convert(new DoctrineException($exception->getMessage()), new Query($sql, $parameters, [])); 83 | }); 84 | } 85 | 86 | /** 87 | * @return void 88 | */ 89 | public function close(): void 90 | { 91 | $this->database->close(); 92 | } 93 | 94 | public function startTransaction(): PromiseInterface 95 | { 96 | return $this->query('BEGIN TRANSACTION', []); 97 | } 98 | 99 | public function commitTransaction(): PromiseInterface 100 | { 101 | return $this->query('COMMIT', []); 102 | } 103 | 104 | public function rollbackTransaction(): PromiseInterface 105 | { 106 | return $this->query('ROLLBACK', []); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Mock/MockedDBALConnection.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Mock; 17 | 18 | use Doctrine\DBAL\Cache\QueryCacheProfile; 19 | use Doctrine\DBAL\Connection; 20 | use Doctrine\DBAL\ParameterType; 21 | use Doctrine\DBAL\Result; 22 | use Doctrine\DBAL\Statement; 23 | use Doctrine\DBAL\Types\Type; 24 | use Exception; 25 | 26 | /** 27 | * Class MockedDBALConnection. 28 | */ 29 | class MockedDBALConnection extends Connection 30 | { 31 | /** 32 | * Prepares an SQL statement. 33 | * 34 | * @param string $sql the SQL statement to prepare 35 | * 36 | * @throws \Doctrine\DBAL\Exception 37 | */ 38 | public function prepare(string $sql): Statement 39 | { 40 | throw new Exception('Mocked method. Unable to be used'); 41 | } 42 | 43 | /** 44 | * BC layer for a wide-spread use-case of old DBAL APIs. 45 | * 46 | * @deprecated This API is deprecated and will be removed after 2022 47 | */ 48 | public function query(string $sql): Result 49 | { 50 | throw new Exception('Mocked method. Unable to be used'); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | * 56 | * @return mixed 57 | */ 58 | public function quote($input, $type = ParameterType::STRING)/*: mixed // <--- from php 8*/ 59 | { 60 | throw new Exception('Mocked method. Unable to be used'); 61 | } 62 | 63 | /** 64 | * BC layer for a wide-spread use-case of old DBAL APIs. 65 | * 66 | * @deprecated This API is deprecated and will be removed after 2022 67 | */ 68 | public function exec(string $sql): int 69 | { 70 | throw new Exception('Mocked method. Unable to be used'); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | * 76 | * @return string|int|false A string representation of the last inserted ID. 77 | */ 78 | public function lastInsertId($name = null)/*: string|int|false // <--- from php 8 */ 79 | { 80 | throw new Exception('Mocked method. Unable to be used'); 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function beginTransaction(): bool 87 | { 88 | throw new Exception('Mocked method. Unable to be used'); 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function commit(): bool 95 | { 96 | throw new Exception('Mocked method. Unable to be used'); 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function rollBack(): bool 103 | { 104 | throw new Exception('Mocked method. Unable to be used'); 105 | } 106 | 107 | /** 108 | * {@inheritdoc} 109 | */ 110 | public function errorCode() 111 | { 112 | throw new Exception('Mocked method. Unable to be used'); 113 | } 114 | 115 | /** 116 | * {@inheritdoc} 117 | */ 118 | public function errorInfo() 119 | { 120 | throw new Exception('Mocked method. Unable to be used'); 121 | } 122 | 123 | /** 124 | * Executes an, optionally parametrized, SQL query. 125 | * 126 | * If the query is parametrized, a prepared statement is used. 127 | * If an SQLLogger is configured, the execution is logged. 128 | * 129 | * @param string $sql SQL query 130 | * @param list|array $params Query parameters 131 | * @param array|array $types Parameter types 132 | * 133 | * @throws \Doctrine\DBAL\Exception 134 | */ 135 | public function executeQuery( 136 | string $sql, 137 | array $params = [], 138 | $types = [], 139 | ?QueryCacheProfile $qcp = null 140 | ): Result { 141 | throw new Exception('Mocked method. Unable to be used'); 142 | } 143 | 144 | /** 145 | * BC layer for a wide-spread use-case of old DBAL APIs. 146 | * 147 | * @deprecated This API is deprecated and will be removed after 2022 148 | * 149 | * @param array $params The query parameters 150 | * @param array $types The parameter types 151 | */ 152 | public function executeUpdate(string $sql, array $params = [], array $types = []): int 153 | { 154 | throw new Exception('Mocked method. Unable to be used'); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Mock/MockedDriver.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Mock; 17 | 18 | use Doctrine\DBAL\Connection; 19 | use Doctrine\DBAL\Driver; 20 | use Doctrine\DBAL\Driver\Connection as DriverConnection; 21 | use Doctrine\DBAL\Driver\API\ExceptionConverter; 22 | use Doctrine\DBAL\Platforms\AbstractPlatform; 23 | use Doctrine\DBAL\Schema\AbstractSchemaManager; 24 | use Exception; 25 | 26 | /** 27 | * Class MockedDriver. 28 | */ 29 | class MockedDriver implements Driver 30 | { 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function connect(array $params, $username = null, $password = null, array $driverOptions = []): DriverConnection 35 | { 36 | throw new Exception('Mocked method. Unable to be used'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function getDatabasePlatform(): AbstractPlatform 43 | { 44 | throw new Exception('Mocked method. Unable to be used'); 45 | } 46 | 47 | /** 48 | * Gets the SchemaManager that can be used to inspect and change the underlying 49 | * database schema of the platform this driver connects to. 50 | * 51 | * @return AbstractSchemaManager 52 | */ 53 | public function getSchemaManager(Connection $conn, AbstractPlatform $platform): AbstractSchemaManager 54 | { 55 | throw new Exception('Mocked method. Unable to be used'); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function getName() 62 | { 63 | throw new Exception('Mocked method. Unable to be used'); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function getDatabase(Connection $conn) 70 | { 71 | throw new Exception('Mocked method. Unable to be used'); 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function getExceptionConverter(): ExceptionConverter 78 | { 79 | throw new Exception('Mocked method. Unable to be used'); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Result.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL; 17 | 18 | /** 19 | * Class Result. 20 | */ 21 | class Result 22 | { 23 | /** 24 | * @var mixed 25 | */ 26 | private $rows; 27 | 28 | /** 29 | * @var int|null 30 | */ 31 | private $lastInsertedId; 32 | 33 | /** 34 | * @var int|null 35 | */ 36 | private $affectedRows; 37 | 38 | /** 39 | * Result constructor. 40 | * 41 | * @param mixed $rows 42 | * @param int|null $lastInsertedId 43 | * @param int|null $affectedRows 44 | */ 45 | public function __construct( 46 | $rows, 47 | ?int $lastInsertedId, 48 | ?int $affectedRows 49 | ) { 50 | $this->rows = $rows; 51 | $this->lastInsertedId = $lastInsertedId; 52 | $this->affectedRows = $affectedRows; 53 | } 54 | 55 | /** 56 | * Fetch count. 57 | * 58 | * @return int 59 | */ 60 | public function fetchCount(): int 61 | { 62 | return is_array($this->rows) 63 | ? count($this->rows) 64 | : 0; 65 | } 66 | 67 | /** 68 | * Fetch all rows. 69 | * 70 | * @return mixed 71 | */ 72 | public function fetchAllRows() 73 | { 74 | return $this->rows; 75 | } 76 | 77 | /** 78 | * Fetch first row. 79 | * 80 | * @return mixed|null 81 | */ 82 | public function fetchFirstRow() 83 | { 84 | return is_array($this->rows) 85 | && count($this->rows) >= 1 86 | ? reset($this->rows) 87 | : null; 88 | } 89 | 90 | /** 91 | * @return int|null 92 | */ 93 | public function getLastInsertedId(): ?int 94 | { 95 | return $this->lastInsertedId; 96 | } 97 | 98 | /** 99 | * @return int|null 100 | */ 101 | public function getAffectedRows(): ?int 102 | { 103 | return $this->affectedRows; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/SingleConnection.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL; 17 | 18 | use Doctrine\DBAL\Exception as DBALException; 19 | use Doctrine\DBAL\Exception\InvalidArgumentException; 20 | use Doctrine\DBAL\Exception\TableExistsException; 21 | use Doctrine\DBAL\Exception\TableNotFoundException; 22 | use Doctrine\DBAL\Platforms\AbstractPlatform; 23 | use Doctrine\DBAL\Query\QueryBuilder; 24 | use Doctrine\DBAL\Schema\Schema; 25 | use Drift\DBAL\Driver\Driver; 26 | use Drift\DBAL\Mock\MockedDBALConnection; 27 | use Drift\DBAL\Mock\MockedDriver; 28 | use React\EventLoop\Loop; 29 | use React\EventLoop\TimerInterface; 30 | use RuntimeException; 31 | use function React\Promise\map; 32 | use React\Promise\PromiseInterface; 33 | use function React\Promise\resolve; 34 | 35 | class SingleConnection implements Connection 36 | { 37 | private Driver $driver; 38 | private Credentials $credentials; 39 | private AbstractPlatform $platform; 40 | private ?TimerInterface $keepAliveTimer = null; 41 | private bool $allowTransactions; 42 | 43 | /** 44 | * Connection constructor. 45 | * 46 | * @param Driver $driver 47 | * @param Credentials $credentials 48 | * @param AbstractPlatform $platform 49 | */ 50 | private function __construct( 51 | Driver $driver, 52 | Credentials $credentials, 53 | AbstractPlatform $platform, 54 | bool $allowTransactions = false 55 | ) { 56 | $this->driver = $driver; 57 | $this->credentials = $credentials; 58 | $this->platform = $platform; 59 | $this->allowTransactions = $allowTransactions; 60 | } 61 | 62 | public function __destruct() 63 | { 64 | if ($this->keepAliveTimer != null) { 65 | Loop::get()->cancelTimer($this->keepAliveTimer); 66 | } 67 | } 68 | 69 | /** 70 | * Create new connection. 71 | * 72 | * @param Driver $driver 73 | * @param Credentials $credentials 74 | * @param AbstractPlatform $platform 75 | * @param ConnectionOptions|null $options 76 | * 77 | * @return Connection 78 | */ 79 | public static function create( 80 | Driver $driver, 81 | Credentials $credentials, 82 | AbstractPlatform $platform, 83 | ?ConnectionOptions $options = null, 84 | bool $allowTransactions = false 85 | ): Connection { 86 | return new self($driver, $credentials, $platform, $allowTransactions); 87 | } 88 | 89 | /** 90 | * Create new connection. 91 | * 92 | * @param Driver $driver 93 | * @param Credentials $credentials 94 | * @param AbstractPlatform $platform 95 | * @param ConnectionOptions|null $options 96 | * 97 | * @return Connection 98 | */ 99 | public static function createConnected( 100 | Driver $driver, 101 | Credentials $credentials, 102 | AbstractPlatform $platform, 103 | ?ConnectionOptions $options = null 104 | ): Connection { 105 | $connection = self::create($driver, $credentials, $platform); 106 | $connection->connect($options); 107 | 108 | return $connection; 109 | } 110 | 111 | /** 112 | * @return string 113 | */ 114 | public function getDriverNamespace(): string 115 | { 116 | return get_class($this->driver); 117 | } 118 | 119 | /** 120 | * Connect. 121 | */ 122 | public function connect(?ConnectionOptions $options = null) 123 | { 124 | $this 125 | ->driver 126 | ->connect($this->credentials); 127 | 128 | if ($options != null && $options->getKeepAliveIntervalSec() > 0) { 129 | $this->keepAliveTimer = Loop::get()->addPeriodicTimer($options->getKeepAliveIntervalSec(), function() { 130 | $qb = $this->createQueryBuilder(); 131 | $this->query( 132 | $qb 133 | ->select('1') 134 | ); 135 | }); 136 | } 137 | } 138 | 139 | /** 140 | * Close. 141 | */ 142 | public function close() 143 | { 144 | if ($this->keepAliveTimer != null) { 145 | Loop::get()->cancelTimer($this->keepAliveTimer); 146 | $this->keepAliveTimer = null; 147 | } 148 | 149 | $this 150 | ->driver 151 | ->close(); 152 | } 153 | 154 | /** 155 | * Creates QueryBuilder. 156 | * 157 | * @return QueryBuilder 158 | * 159 | * @throws DBALException 160 | */ 161 | public function createQueryBuilder(): QueryBuilder 162 | { 163 | return new QueryBuilder( 164 | new MockedDBALConnection([ 165 | 'platform' => $this->platform, 166 | ], new MockedDriver()) 167 | ); 168 | } 169 | 170 | /** 171 | * Query by query builder. 172 | * 173 | * @param QueryBuilder $queryBuilder 174 | * 175 | * @return PromiseInterface 176 | */ 177 | public function query(QueryBuilder $queryBuilder): PromiseInterface 178 | { 179 | return $this->queryBySQL( 180 | $queryBuilder->getSQL(), 181 | $queryBuilder->getParameters() 182 | ); 183 | } 184 | 185 | /** 186 | * Query by sql and parameters. 187 | * 188 | * @param string $sql 189 | * @param array $parameters 190 | * 191 | * @return PromiseInterface 192 | */ 193 | public function queryBySQL(string $sql, array $parameters = []): PromiseInterface 194 | { 195 | return $this 196 | ->driver 197 | ->query($sql, $parameters); 198 | } 199 | 200 | /** 201 | * Execute, sequentially, an array of sqls. 202 | * 203 | * @param string[] $sqls 204 | * 205 | * @return PromiseInterface 206 | */ 207 | public function executeSQLs(array $sqls): PromiseInterface 208 | { 209 | return 210 | map($sqls, function (string $sql) { 211 | return $this->queryBySQL($sql); 212 | }) 213 | ->then(function () { 214 | return $this; 215 | }); 216 | } 217 | 218 | /** 219 | * Execute an schema. 220 | * 221 | * @param Schema $schema 222 | * 223 | * @return PromiseInterface 224 | */ 225 | public function executeSchema(Schema $schema): PromiseInterface 226 | { 227 | return $this 228 | ->executeSQLs($schema->toSql($this->platform)) 229 | ->then(function () { 230 | return $this; 231 | }); 232 | } 233 | 234 | /** 235 | * Shortcuts. 236 | */ 237 | 238 | /** 239 | * Find one by. 240 | * 241 | * connection->findOneById('table', ['id' => 1]); 242 | * 243 | * @param string $table 244 | * @param array $where 245 | * 246 | * @return PromiseInterface 247 | */ 248 | public function findOneBy( 249 | string $table, 250 | array $where 251 | ): PromiseInterface { 252 | return $this 253 | ->getResultByWhereClause($table, $where) 254 | ->then(function (Result $result) { 255 | return $result->fetchFirstRow(); 256 | }); 257 | } 258 | 259 | /** 260 | * Find by. 261 | * 262 | * connection->findBy('table', ['id' => 1]); 263 | * 264 | * @param string $table 265 | * @param array $where 266 | * 267 | * @return PromiseInterface 268 | */ 269 | public function findBy( 270 | string $table, 271 | array $where = [] 272 | ): PromiseInterface { 273 | return $this 274 | ->getResultByWhereClause($table, $where) 275 | ->then(function (Result $result) { 276 | return $result->fetchAllRows(); 277 | }); 278 | } 279 | 280 | /** 281 | * @param string $table 282 | * @param array $values 283 | * 284 | * @return PromiseInterface 285 | */ 286 | public function insert( 287 | string $table, 288 | array $values 289 | ): PromiseInterface { 290 | $queryBuilder = $this->createQueryBuilder(); 291 | 292 | return $this->driver->insert($queryBuilder, $table, $values); 293 | } 294 | 295 | /** 296 | * @param string $table 297 | * @param array $values 298 | * 299 | * @return PromiseInterface 300 | * 301 | * @throws InvalidArgumentException 302 | */ 303 | public function delete( 304 | string $table, 305 | array $values 306 | ): PromiseInterface { 307 | if (empty($values)) { 308 | throw InvalidArgumentException::fromEmptyCriteria(); 309 | } 310 | 311 | $queryBuilder = $this 312 | ->createQueryBuilder() 313 | ->delete($table); 314 | 315 | $this->applyWhereClausesFromArray($queryBuilder, $values); 316 | 317 | return $this->query($queryBuilder); 318 | } 319 | 320 | /** 321 | * @param string $table 322 | * @param array $id 323 | * @param array $values 324 | * 325 | * @return PromiseInterface 326 | * 327 | * @throws InvalidArgumentException 328 | */ 329 | public function update( 330 | string $table, 331 | array $id, 332 | array $values 333 | ): PromiseInterface { 334 | if (empty($id)) { 335 | throw InvalidArgumentException::fromEmptyCriteria(); 336 | } 337 | 338 | $queryBuilder = $this 339 | ->createQueryBuilder() 340 | ->update($table); 341 | 342 | $parameters = $queryBuilder->getParameters(); 343 | foreach ($values as $field => $value) { 344 | $queryBuilder->set($field, '?'); 345 | $parameters[] = $value; 346 | } 347 | $queryBuilder->setParameters($parameters); 348 | $this->applyWhereClausesFromArray($queryBuilder, $id); 349 | 350 | return $this->query($queryBuilder); 351 | } 352 | 353 | /** 354 | * @param string $table 355 | * @param array $id 356 | * @param array $values 357 | * 358 | * @return PromiseInterface 359 | * 360 | * @throws InvalidArgumentException 361 | */ 362 | public function upsert( 363 | string $table, 364 | array $id, 365 | array $values 366 | ): PromiseInterface { 367 | return $this 368 | ->findOneBy($table, $id) 369 | ->then(function (?array $result) use ($table, $id, $values) { 370 | return is_null($result) 371 | ? $this->insert($table, array_merge($id, $values)) 372 | : $this->update($table, $id, $values); 373 | }); 374 | } 375 | 376 | /** 377 | * Table related shortcuts. 378 | */ 379 | 380 | /** 381 | * Easy shortcut for creating tables. Fields is just a simple key value, 382 | * being the key the name of the field, and the value the type. By default, 383 | * Varchar types have length 255. 384 | * 385 | * First field is considered as primary key. 386 | * 387 | * @param string $name 388 | * @param array $fields 389 | * @param array $extra 390 | * @param bool $autoincrementId 391 | * 392 | * @return PromiseInterface 393 | * 394 | * @throws InvalidArgumentException 395 | * @throws TableExistsException 396 | */ 397 | public function createTable( 398 | string $name, 399 | array $fields, 400 | array $extra = [], 401 | bool $autoincrementId = false 402 | ): PromiseInterface { 403 | if (empty($fields)) { 404 | throw InvalidArgumentException::fromEmptyCriteria(); 405 | } 406 | 407 | $schema = new Schema(); 408 | $table = $schema->createTable($name); 409 | foreach ($fields as $field => $type) { 410 | $extraField = ( 411 | array_key_exists($field, $extra) && 412 | is_array($extra[$field]) 413 | ) ? $extra[$field] : []; 414 | 415 | if ( 416 | 'string' == $type && 417 | !array_key_exists('length', $extraField) 418 | ) { 419 | $extraField = array_merge( 420 | $extraField, 421 | ['length' => 255] 422 | ); 423 | } 424 | 425 | $table->addColumn($field, $type, $extraField); 426 | } 427 | 428 | $id = array_key_first($fields); 429 | $table->setPrimaryKey([$id]); 430 | $table->getColumn($id)->setAutoincrement($autoincrementId); 431 | 432 | return $this->executeSchema($schema); 433 | } 434 | 435 | /** 436 | * @param string $name 437 | * 438 | * @return PromiseInterface 439 | * 440 | * @throws TableNotFoundException 441 | */ 442 | public function dropTable(string $name): PromiseInterface 443 | { 444 | return $this 445 | ->queryBySQL("DROP TABLE $name") 446 | ->then(function () { 447 | return $this; 448 | }); 449 | } 450 | 451 | /** 452 | * @param string $name 453 | * 454 | * @return PromiseInterface 455 | * 456 | * @throws TableNotFoundException 457 | */ 458 | public function truncateTable(string $name): PromiseInterface 459 | { 460 | $truncateTableQuery = $this 461 | ->platform 462 | ->getTruncateTableSQL($name); 463 | 464 | return $this 465 | ->queryBySQL($truncateTableQuery) 466 | ->then(function () { 467 | return $this; 468 | }); 469 | } 470 | 471 | /** 472 | * Get result by where clause. 473 | * 474 | * @param string $table 475 | * @param array $where 476 | * 477 | * @return PromiseInterface 478 | */ 479 | private function getResultByWhereClause( 480 | string $table, 481 | array $where 482 | ): PromiseInterface { 483 | $queryBuilder = $this 484 | ->createQueryBuilder() 485 | ->select('t.*') 486 | ->from($table, 't'); 487 | 488 | $this->applyWhereClausesFromArray($queryBuilder, $where); 489 | 490 | return $this->query($queryBuilder); 491 | } 492 | 493 | /** 494 | * Apply where clauses. 495 | * 496 | * [ 497 | * "id" => 1, 498 | * "name" => "Marc" 499 | * ] 500 | * 501 | * to 502 | * 503 | * [ 504 | * [ "id = ?", "name = ?"], 505 | * [1, "Marc"] 506 | * ] 507 | * 508 | * @param QueryBuilder $queryBuilder 509 | * @param array $array 510 | */ 511 | private function applyWhereClausesFromArray( 512 | QueryBuilder $queryBuilder, 513 | array $array 514 | ) { 515 | $params = $queryBuilder->getParameters(); 516 | foreach ($array as $field => $value) { 517 | if (\is_null($value)) { 518 | $queryBuilder->andWhere( 519 | $queryBuilder->expr()->isNull($field) 520 | ); 521 | continue; 522 | } 523 | 524 | $queryBuilder->andWhere( 525 | $queryBuilder->expr()->eq($field, '?') 526 | ); 527 | 528 | $params[] = $value; 529 | } 530 | 531 | $queryBuilder->setParameters($params); 532 | } 533 | 534 | public function startTransaction(): PromiseInterface 535 | { 536 | if (!$this->allowTransactions) { 537 | throw new RuntimeException('starting a transaction in a SingleConnection is not allowed'); 538 | } 539 | 540 | return $this->driver->startTransaction() 541 | ->then(fn(...$args) => $this); 542 | } 543 | 544 | public function commitTransaction(SingleConnection $connection): PromiseInterface 545 | { 546 | if (!$this->allowTransactions) { 547 | throw new RuntimeException('starting a transaction in a SingleConnection is not allowed'); 548 | } 549 | 550 | return $this->driver->commitTransaction() 551 | ->then(fn(...$args) => $this); 552 | } 553 | 554 | public function rollbackTransaction(SingleConnection $connection): PromiseInterface 555 | { 556 | if (!$this->allowTransactions) { 557 | throw new RuntimeException('starting a transaction in a SingleConnection is not allowed'); 558 | } 559 | 560 | return $this->driver->rollbackTransaction() 561 | ->then(fn(...$args) => $this); 562 | } 563 | } 564 | -------------------------------------------------------------------------------- /tests/ConnectionTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Tests; 17 | 18 | use Doctrine\DBAL\Exception as DBALException; 19 | use Doctrine\DBAL\Exception\InvalidArgumentException; 20 | use Doctrine\DBAL\Exception\TableExistsException; 21 | use Doctrine\DBAL\Exception\TableNotFoundException; 22 | use Doctrine\DBAL\Exception\UniqueConstraintViolationException; 23 | use Drift\DBAL\Connection; 24 | use Drift\DBAL\Result; 25 | use Drift\DBAL\SingleConnection; 26 | use PHPUnit\Framework\TestCase; 27 | use React\EventLoop\Factory; 28 | use React\EventLoop\LoopInterface; 29 | use React\Promise\PromiseInterface; 30 | use RuntimeException; 31 | 32 | use function Clue\React\Block\await; 33 | use function React\Promise\all; 34 | 35 | /** 36 | * Class ConnectionTest. 37 | */ 38 | abstract class ConnectionTest extends TestCase 39 | { 40 | 41 | /** 42 | * The timeout is used to prevent tests from endless waiting. 43 | * Consider this amount of seconds as a reasonable timeout 44 | * to understand that something went wrong. 45 | */ 46 | private const MAX_TIMEOUT = 3; 47 | 48 | /** 49 | * @param LoopInterface $loop 50 | * 51 | * @return Connection 52 | */ 53 | abstract protected function getConnection(LoopInterface $loop): Connection; 54 | 55 | /** 56 | * @param Connection $connection 57 | * @param bool $autoincrementedId 58 | * 59 | * @return PromiseInterface 60 | */ 61 | protected function createInfrastructure( 62 | Connection $connection, 63 | bool $autoincrementedId = false 64 | ): PromiseInterface { 65 | return $connection 66 | ->createTable('test', [ 67 | 'id' => $autoincrementedId ? 'integer' : 'string', 68 | 'field1' => 'string', 69 | 'field2' => 'string', 70 | ], [ 71 | 'field2' => ['notnull' => false], 72 | ], $autoincrementedId) 73 | ->otherwise(function (TableExistsException $_) use ($connection) { 74 | // Silent pass 75 | 76 | return $connection; 77 | }) 78 | ->then(function (Connection $connection) { 79 | return $connection->truncateTable('test'); 80 | }); 81 | } 82 | 83 | /** 84 | * @param Connection $connection 85 | * 86 | * @return PromiseInterface 87 | */ 88 | protected function dropInfrastructure(Connection $connection): PromiseInterface 89 | { 90 | return $connection 91 | ->dropTable('test') 92 | ->otherwise(function (TableNotFoundException $_) use ($connection) { 93 | // Silent pass 94 | 95 | return $connection; 96 | }); 97 | } 98 | 99 | /** 100 | * @param Connection $connection 101 | * @param bool $autoincrementedId 102 | * 103 | * @return PromiseInterface 104 | */ 105 | protected function resetInfrastructure( 106 | Connection $connection, 107 | bool $autoincrementedId = false 108 | ): PromiseInterface { 109 | return $this 110 | ->dropInfrastructure($connection) 111 | ->then(function () use ($connection, $autoincrementedId) { 112 | return $this->createInfrastructure($connection, $autoincrementedId); 113 | }); 114 | } 115 | 116 | /** 117 | * Create loop Loop. 118 | */ 119 | protected function createLoop() 120 | { 121 | return Factory::create(); 122 | } 123 | 124 | /** 125 | * Test that query builder works properly. 126 | */ 127 | public function testQueryBuilder() 128 | { 129 | $loop = $this->createLoop(); 130 | $connection = $this->getConnection($loop); 131 | $sql = $connection 132 | ->createQueryBuilder() 133 | ->select('*') 134 | ->from('user', 'u') 135 | ->where('u.id = :id') 136 | ->setParameter('id', 3) 137 | ->setMaxResults(1) 138 | ->getSQL(); 139 | 140 | $this->assertEquals('SELECT * FROM user u WHERE u.id = :id LIMIT 1', $sql); 141 | } 142 | 143 | /** 144 | * Test create table with empty criteria. 145 | */ 146 | public function testCreateTableWithEmptyCriteria() 147 | { 148 | $loop = $this->createLoop(); 149 | $connection = $this->getConnection($loop); 150 | $this->expectException(InvalidArgumentException::class); 151 | await($connection->createTable('anothertable', []), $loop); 152 | } 153 | 154 | /** 155 | * Test query. 156 | */ 157 | public function testQuery() 158 | { 159 | $loop = $this->createLoop(); 160 | $connection = $this->getConnection($loop); 161 | $promise = $this 162 | ->resetInfrastructure($connection) 163 | ->then(function (Connection $connection) { 164 | return $connection 165 | ->insert('test', [ 166 | 'id' => '1', 167 | 'field1' => 'val1', 168 | 'field2' => 'val2', 169 | ]) 170 | ->then(function () use ($connection) { 171 | return $connection 172 | ->query($connection 173 | ->createQueryBuilder() 174 | ->select('*') 175 | ->from('test', 't') 176 | ->where('t.id = ?') 177 | ->setParameters(['1']) 178 | ->setMaxResults(1) 179 | ); 180 | }) 181 | ->then(function (Result $result) { 182 | $this->assertEquals($result->fetchFirstRow(), [ 183 | 'id' => '1', 184 | 'field1' => 'val1', 185 | 'field2' => 'val2', 186 | ]); 187 | }); 188 | }); 189 | 190 | await($promise, $loop, self::MAX_TIMEOUT); 191 | } 192 | 193 | /** 194 | * Test multiple rows. 195 | */ 196 | public function testMultipleRows() 197 | { 198 | $loop = $this->createLoop(); 199 | $connection = $this->getConnection($loop); 200 | $promise = $this 201 | ->resetInfrastructure($connection) 202 | ->then(function (Connection $connection) { 203 | return $connection 204 | ->insert('test', [ 205 | 'id' => '1', 206 | 'field1' => 'val11', 207 | 'field2' => 'val12', 208 | ]) 209 | ->then(function () use ($connection) { 210 | return $connection->insert('test', [ 211 | 'id' => '2', 212 | 'field1' => 'val21', 213 | 'field2' => 'val22', 214 | ]); 215 | }) 216 | ->then(function () use ($connection) { 217 | return $connection->insert('test', [ 218 | 'id' => '3', 219 | 'field1' => 'val31', 220 | 'field2' => 'val32', 221 | ]); 222 | }); 223 | }) 224 | ->then(function () use ($connection) { 225 | $queryBuilder = $connection->createQueryBuilder(); 226 | 227 | return $connection 228 | ->query($queryBuilder 229 | ->select('*') 230 | ->from('test', 't') 231 | ->where($queryBuilder->expr()->orX( 232 | $queryBuilder->expr()->eq('t.id', '?'), 233 | $queryBuilder->expr()->eq('t.id', '?') 234 | )) 235 | ->setParameters(['1', '2'])); 236 | }) 237 | ->then(function (Result $result) { 238 | $this->assertCount(2, $result->fetchAllRows()); 239 | $this->assertEquals(2, $result->fetchCount()); 240 | }); 241 | 242 | await($promise, $loop, self::MAX_TIMEOUT); 243 | } 244 | 245 | /** 246 | * Test connection exception. 247 | */ 248 | public function testTableDoesntExistException() 249 | { 250 | $loop = $this->createLoop(); 251 | $connection = $this->getConnection($loop); 252 | $promise = $this->dropInfrastructure($connection) 253 | ->then(function (Connection $connection) { 254 | return $connection->insert('test', [ 255 | 'id' => '1', 256 | 'field1' => 'val11', 257 | 'field2' => 'val12', 258 | ]); 259 | }); 260 | 261 | $this->expectException(TableNotFoundException::class); 262 | await($promise, $loop); 263 | } 264 | 265 | /** 266 | * Test find shortcut. 267 | */ 268 | public function testFindShortcut() 269 | { 270 | $loop = $this->createLoop(); 271 | $connection = $this->getConnection($loop); 272 | $promise = $this 273 | ->resetInfrastructure($connection) 274 | ->then(function (Connection $connection) { 275 | return all([ 276 | $connection 277 | ->insert('test', [ 278 | 'id' => '1', 279 | 'field1' => 'val1', 280 | 'field2' => 'val1', 281 | ]), 282 | $connection 283 | ->insert('test', [ 284 | 'id' => '2', 285 | 'field1' => 'val1', 286 | 'field2' => 'val2', 287 | ]), 288 | $connection 289 | ->insert('test', [ 290 | 'id' => '3', 291 | 'field1' => 'valX', 292 | 'field2' => 'val2', 293 | ]), 294 | ]) 295 | ->then(function () use ($connection) { 296 | return all([ 297 | $connection->findOneBy('test', [ 298 | 'id' => '1', 299 | ]), 300 | $connection->findOneBy('test', [ 301 | 'id' => '999', 302 | ]), 303 | $connection->findBy('test', [ 304 | 'field1' => 'val1', 305 | ]), 306 | ]); 307 | }) 308 | ->then(function (array $results) { 309 | $this->assertEquals($results[0], [ 310 | 'id' => '1', 311 | 'field1' => 'val1', 312 | 'field2' => 'val1', 313 | ]); 314 | 315 | $this->assertNull($results[1]); 316 | $listResults = $results[2]; 317 | usort($listResults, function ($a1, $a2) { 318 | return $a1['id'] > $a2['id']; 319 | }); 320 | 321 | $this->assertSame($listResults, [ 322 | [ 323 | 'id' => '1', 324 | 'field1' => 'val1', 325 | 'field2' => 'val1', 326 | ], 327 | [ 328 | 'id' => '2', 329 | 'field1' => 'val1', 330 | 'field2' => 'val2', 331 | ], 332 | ]); 333 | }); 334 | }); 335 | 336 | await($promise, $loop, self::MAX_TIMEOUT); 337 | } 338 | 339 | /** 340 | * Test select by null. 341 | */ 342 | public function testFindByNullValue() 343 | { 344 | $loop = $this->createLoop(); 345 | $connection = $this->getConnection($loop); 346 | $promise = $this 347 | ->resetInfrastructure($connection) 348 | ->then(function (Connection $connection) { 349 | return $connection 350 | ->insert('test', [ 351 | 'id' => '1', 352 | 'field1' => 'val1', 353 | 'field2' => null, 354 | ]); 355 | }) 356 | ->then(function () use ($connection) { 357 | return all([ 358 | $connection->findOneBy('test', [ 359 | 'field2' => null, 360 | ]), 361 | $connection->findBy('test', [ 362 | 'field2' => null, 363 | ]), 364 | ]); 365 | }); 366 | 367 | [$result0, $result1] = await($promise, $loop, self::MAX_TIMEOUT); 368 | 369 | $this->assertEquals('1', $result0['id']); 370 | $this->assertCount(1, $result1); 371 | } 372 | 373 | /** 374 | * Test insert twice exists. 375 | */ 376 | public function testInsertTwice() 377 | { 378 | $loop = $this->createLoop(); 379 | $connection = $this->getConnection($loop); 380 | $promise = $this 381 | ->resetInfrastructure($connection) 382 | ->then(function (Connection $connection) { 383 | return $connection 384 | ->insert('test', [ 385 | 'id' => '1', 386 | 'field1' => 'val1', 387 | 'field2' => 'val1', 388 | ]) 389 | ->then(function () use ($connection) { 390 | return $connection->insert('test', [ 391 | 'id' => '1', 392 | 'field1' => 'val1', 393 | 'field2' => 'val1', 394 | ]); 395 | }); 396 | }); 397 | 398 | $this->expectException(UniqueConstraintViolationException::class); 399 | await($promise, $loop, self::MAX_TIMEOUT); 400 | } 401 | 402 | /** 403 | * Test update. 404 | */ 405 | public function testUpdate() 406 | { 407 | $loop = $this->createLoop(); 408 | $connection = $this->getConnection($loop); 409 | $promise = $this 410 | ->resetInfrastructure($connection) 411 | ->then(function (Connection $connection) { 412 | return $connection->insert('test', [ 413 | 'id' => '1', 414 | 'field1' => 'val1', 415 | 'field2' => 'val1', 416 | ]); 417 | }) 418 | ->then(function () use ($connection) { 419 | return $connection->update('test', [ 420 | 'id' => '1', 421 | ], [ 422 | 'field1' => 'val3', 423 | ]); 424 | }) 425 | ->then(function () use ($connection) { 426 | return $connection->findOneBy('test', ['id' => '1']); 427 | }) 428 | ->then(function (array $result) { 429 | $this->assertEquals('val3', $result['field1']); 430 | }); 431 | 432 | await($promise, $loop, self::MAX_TIMEOUT); 433 | } 434 | 435 | /** 436 | * Test upsert. 437 | */ 438 | public function testUpsert() 439 | { 440 | $loop = $this->createLoop(); 441 | $connection = $this->getConnection($loop); 442 | $promise = $this 443 | ->resetInfrastructure($connection) 444 | ->then(function (Connection $connection) { 445 | return $connection->insert('test', [ 446 | 'id' => '1', 447 | 'field1' => 'val1', 448 | 'field2' => 'val2', 449 | ]); 450 | }) 451 | ->then(function () use ($connection) { 452 | return all([ 453 | $connection->upsert( 454 | 'test', 455 | ['id' => '1'], 456 | ['field1' => 'val3'] 457 | ), 458 | $connection->upsert( 459 | 'test', 460 | ['id' => '2'], 461 | ['field1' => 'val5', 'field2' => 'val6'] 462 | ), 463 | ]); 464 | }) 465 | ->then(function () use ($connection) { 466 | return $connection->findBy('test'); 467 | }) 468 | ->then(function (array $results) { 469 | $this->assertEquals('1', $results[0]['id']); 470 | $this->assertEquals('val3', $results[0]['field1']); 471 | $this->assertEquals('val2', $results[0]['field2']); 472 | $this->assertEquals('2', $results[1]['id']); 473 | $this->assertEquals('val5', $results[1]['field1']); 474 | $this->assertEquals('val6', $results[1]['field2']); 475 | }); 476 | 477 | await($promise, $loop, self::MAX_TIMEOUT); 478 | } 479 | 480 | /** 481 | * Test delete. 482 | */ 483 | public function testDelete() 484 | { 485 | $loop = $this->createLoop(); 486 | $connection = $this->getConnection($loop); 487 | $promise = $this 488 | ->resetInfrastructure($connection) 489 | ->then(function (Connection $connection) { 490 | return $connection->insert('test', [ 491 | 'id' => '1', 492 | 'field1' => 'val1', 493 | 'field2' => 'val2', 494 | ]); 495 | }) 496 | ->then(function () use ($connection) { 497 | return $connection->delete( 498 | 'test', 499 | ['id' => '1'] 500 | ); 501 | }) 502 | ->then(function () use ($connection) { 503 | return $connection->findOneBy('test', [ 504 | 'id' => '1', 505 | ]); 506 | }) 507 | ->then(function ($result) { 508 | $this->assertNull($result); 509 | }); 510 | 511 | await($promise, $loop, self::MAX_TIMEOUT); 512 | } 513 | 514 | /** 515 | * Test get last inserted id. 516 | */ 517 | public function testGetLastInsertedId() 518 | { 519 | $loop = $this->createLoop(); 520 | $connection = $this->getConnection($loop); 521 | $promise = $this 522 | ->resetInfrastructure($connection, true) 523 | ->then(function (Connection $connection) { 524 | return $connection->insert('test', [ 525 | 'field1' => 'val1', 526 | 'field2' => 'val2', 527 | ]); 528 | }) 529 | ->then(function (Result $result) { 530 | $this->assertEquals(1, $result->getLastInsertedId()); 531 | }) 532 | ->then(function () use ($connection) { 533 | return all([ 534 | $connection->insert('test', [ 535 | 'field1' => 'val3', 536 | 'field2' => 'val4', 537 | ]), 538 | $connection->insert('test', [ 539 | 'field1' => 'val5', 540 | 'field2' => 'val6', 541 | ]), 542 | ]); 543 | }) 544 | ->then(function (array $results) { 545 | $this->assertEquals(2, $results[0]->getLastInsertedId()); 546 | $this->assertEquals(3, $results[1]->getLastInsertedId()); 547 | }) 548 | ->then(function () use ($connection) { 549 | return $connection->insert('test', [ 550 | 'field1' => 'val7', 551 | 'field2' => 'val8', 552 | ]); 553 | }) 554 | ->then(function (Result $result) { 555 | $this->assertEquals(4, $result->getLastInsertedId()); 556 | }); 557 | 558 | await($promise, $loop, self::MAX_TIMEOUT); 559 | } 560 | 561 | /** 562 | * Test affected rows. 563 | */ 564 | public function testAffectedRows() 565 | { 566 | if ( 567 | $this instanceof PostgreSQLConnectionTest || 568 | $this instanceof PostgreSQLConnectionPoolTest 569 | ) { 570 | $this->markTestSkipped('This feature is not implemented in the Postgres client'); 571 | } 572 | 573 | $loop = $this->createLoop(); 574 | $connection = $this->getConnection($loop); 575 | $promise = $this 576 | ->resetInfrastructure($connection, true) 577 | ->then(function (Connection $connection) { 578 | return $connection->insert('test', [ 579 | 'field1' => 'val1', 580 | 'field2' => 'val2', 581 | ]); 582 | }) 583 | ->then(function (Result $result) use ($connection) { 584 | $this->assertEquals(1, $result->getAffectedRows()); 585 | 586 | return $connection->insert('test', [ 587 | 'field1' => 'val1', 588 | 'field2' => 'val4', 589 | ]); 590 | }) 591 | ->then(function () use ($connection) { 592 | return $connection->update('test', [ 593 | 'field1' => 'val1', 594 | ], [ 595 | 'field2' => 'new5', 596 | ]); 597 | }) 598 | ->then(function (Result $result) use ($connection) { 599 | $this->assertEquals(2, $result->getAffectedRows()); 600 | 601 | return $connection->insert('test', [ 602 | 'field1' => 'val1', 603 | 'field2' => 'val8', 604 | ]); 605 | }) 606 | ->then(function () use ($connection) { 607 | return $connection->delete('test', [ 608 | 'field1' => 'val1', 609 | ]); 610 | }) 611 | ->then(function (Result $result) { 612 | $this->assertEquals(3, $result->getAffectedRows()); 613 | }); 614 | 615 | await($promise, $loop, self::MAX_TIMEOUT); 616 | } 617 | 618 | /** 619 | * Test close connection. 620 | */ 621 | public function testCloseConnection() 622 | { 623 | $loop = $this->createLoop(); 624 | $connection = $this->getConnection($loop); 625 | $promise = $this 626 | ->resetInfrastructure($connection, true) 627 | ->then(function (Connection $connection) { 628 | return $connection->insert('test', [ 629 | 'field1' => 'val1', 630 | 'field2' => 'val2', 631 | ]); 632 | }) 633 | ->then(function (Result $result) use ($connection) { 634 | $this->assertEquals(1, $result->getAffectedRows()); 635 | $connection->close(); 636 | 637 | return $connection->insert('test', [ 638 | 'field1' => 'val1', 639 | 'field2' => 'val2', 640 | ]); 641 | }) 642 | ->then(function () { 643 | $this->fail('An exception should have been thrown'); 644 | }) 645 | ->otherwise(function (DBALException $exception) { 646 | // Good catch 647 | }); 648 | 649 | await($promise, $loop, self::MAX_TIMEOUT); 650 | } 651 | 652 | public function testTransactionRollback(): void 653 | { 654 | $loop = $this->createLoop(); 655 | $connection = $this->getConnection($loop); 656 | 657 | if ($connection instanceof SingleConnection) { 658 | $this->expectException(RuntimeException::class); 659 | await($connection->startTransaction()); 660 | return; 661 | } 662 | 663 | await($this->resetInfrastructure($connection, true)); 664 | 665 | $transaction = null; 666 | 667 | $promise = $connection->startTransaction() 668 | ->then(function (Connection $c) use (&$transaction) { 669 | $transaction = $c; 670 | 671 | return $transaction->insert('test', [ 672 | 'field1' => 'transaction', 673 | 'field2' => 'rollback', 674 | ]); 675 | }) 676 | ->then(function () use (&$connection) { 677 | // This should happen on a different connection because 678 | // the transaction one has been leased. 679 | return $connection->insert('test', [ 680 | 'field1' => 'other', 681 | 'field2' => 'coroutine', 682 | ]); 683 | }) 684 | ->then(function () use ($connection, &$transaction) { 685 | /** @var SingleConnection $transaction */ 686 | return $connection->rollbackTransaction($transaction); 687 | }); 688 | 689 | await($promise); 690 | 691 | $queryBuilder = $connection->createQueryBuilder() 692 | ->select('*') 693 | ->from('test') 694 | ->where("field1 = 'transaction' and field2 = 'rollback'"); 695 | /** @var Result $result */ 696 | $result = await($connection->query($queryBuilder)); 697 | 698 | self::assertEmpty($result->fetchAllRows()); 699 | 700 | $queryBuilder = $connection->createQueryBuilder() 701 | ->select('*') 702 | ->from('test') 703 | ->where("field1 = 'other' and field2 = 'coroutine'"); 704 | /** @var Result $result */ 705 | $result = await($connection->query($queryBuilder)); 706 | 707 | self::assertEquals(1, $result->fetchCount()); 708 | } 709 | 710 | public function testTransactionCommit(): void 711 | { 712 | $loop = $this->createLoop(); 713 | $connection = $this->getConnection($loop); 714 | 715 | if ($connection instanceof SingleConnection) { 716 | $this->expectException(RuntimeException::class); 717 | await($connection->startTransaction()); 718 | return; 719 | } 720 | 721 | await($this->resetInfrastructure($connection, true)); 722 | 723 | $transaction = null; 724 | 725 | $promise = $connection->startTransaction() 726 | ->then(function (Connection $c) use (&$transaction) { 727 | $transaction = $c; 728 | 729 | return $transaction->insert('test', [ 730 | 'field1' => 'transaction', 731 | 'field2' => 'commit', 732 | ]); 733 | }) 734 | ->then(function () use ($connection, &$transaction) { 735 | /** @var SingleConnection $transaction */ 736 | return $connection->commitTransaction($transaction); 737 | }); 738 | 739 | await($promise); 740 | 741 | $queryBuilder = $connection->createQueryBuilder() 742 | ->select('*') 743 | ->from('test') 744 | ->where("field1 = 'transaction' and field2 = 'commit'"); 745 | /** @var Result $result */ 746 | $result = await($connection->query($queryBuilder)); 747 | 748 | self::assertEquals(1, $result->fetchCount()); 749 | } 750 | } 751 | -------------------------------------------------------------------------------- /tests/CredentialsTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Tests; 17 | 18 | use Drift\DBAL\Credentials; 19 | use PHPUnit\Framework\TestCase; 20 | 21 | /** 22 | * Class ConnectionTest. 23 | */ 24 | class CredentialsTest extends TestCase 25 | { 26 | /** 27 | * Test that credentials works as expected with all the fields filled. 28 | */ 29 | public function testCredentialsCanBeConvertedToString() 30 | { 31 | $credentials = new Credentials('127.0.0.1', '3306', 'user', 'password', 'database'); 32 | 33 | $this->assertEquals('user:password@127.0.0.1:3306/database', $credentials->toString()); 34 | } 35 | 36 | /** 37 | * Test that credentials works as expected without login information. 38 | */ 39 | public function testCredentialsCanBeConvertedToStringWithoutLogin() 40 | { 41 | $credentials = new Credentials('127.0.0.1', '3306', '', '', 'database'); 42 | 43 | $this->assertEquals('127.0.0.1:3306/database', $credentials->toString()); 44 | } 45 | 46 | /** 47 | * Test that credentials works as expected without password. 48 | */ 49 | public function testCredentialsCanBeConvertedToStringWithoutPassword() 50 | { 51 | $credentials = new Credentials('127.0.0.1', '3306', 'user', '', 'database'); 52 | 53 | $this->assertEquals('user@127.0.0.1:3306/database', $credentials->toString()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Mysql5ConnectionPoolTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Tests; 17 | 18 | use Doctrine\DBAL\Platforms\MySQLPlatform; 19 | use Drift\DBAL\Connection; 20 | use Drift\DBAL\ConnectionPool; 21 | use Drift\DBAL\ConnectionPoolOptions; 22 | use Drift\DBAL\Credentials; 23 | use Drift\DBAL\Driver\Mysql\MysqlDriver; 24 | use React\EventLoop\LoopInterface; 25 | 26 | /** 27 | * Class Mysql5ConnectionPoolTest. 28 | */ 29 | class Mysql5ConnectionPoolTest extends ConnectionTest 30 | { 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function getConnection(LoopInterface $loop): Connection 35 | { 36 | $mysqlPlatform = new MySQLPlatform(); 37 | 38 | return ConnectionPool::createConnected(new MysqlDriver( 39 | $loop 40 | ), new Credentials( 41 | '127.0.0.1', 42 | '3306', 43 | 'root', 44 | 'root', 45 | 'test' 46 | ), $mysqlPlatform, new ConnectionPoolOptions(10)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Mysql5ConnectionTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Tests; 17 | 18 | use Doctrine\DBAL\Platforms\MySQLPlatform; 19 | use Drift\DBAL\Connection; 20 | use Drift\DBAL\Credentials; 21 | use Drift\DBAL\Driver\Mysql\MysqlDriver; 22 | use Drift\DBAL\SingleConnection; 23 | use React\EventLoop\LoopInterface; 24 | 25 | /** 26 | * Class Mysql5ConnectionTest. 27 | */ 28 | class Mysql5ConnectionTest extends ConnectionTest 29 | { 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function getConnection(LoopInterface $loop): Connection 34 | { 35 | $mysqlPlatform = new MySQLPlatform(); 36 | 37 | return SingleConnection::createConnected(new MysqlDriver( 38 | $loop 39 | ), new Credentials( 40 | '127.0.0.1', 41 | '3306', 42 | 'root', 43 | 'root', 44 | 'test' 45 | ), $mysqlPlatform); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/PostgreSQLConnectionPoolTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Tests; 17 | 18 | use Doctrine\DBAL\Platforms\PostgreSQLPlatform; 19 | use Drift\DBAL\Connection; 20 | use Drift\DBAL\ConnectionPool; 21 | use Drift\DBAL\ConnectionPoolOptions; 22 | use Drift\DBAL\Credentials; 23 | use Drift\DBAL\Driver\PostgreSQL\PostgreSQLDriver; 24 | use React\EventLoop\LoopInterface; 25 | 26 | /** 27 | * Class PostgreSQLConnectionPoolTest. 28 | */ 29 | class PostgreSQLConnectionPoolTest extends ConnectionTest 30 | { 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function getConnection(LoopInterface $loop): Connection 35 | { 36 | $postgreSQLPlatform = new PostgreSQLPlatform(); 37 | 38 | return ConnectionPool::createConnected(new PostgreSQLDriver($loop), new Credentials( 39 | '127.0.0.1', 40 | '5432', 41 | 'root', 42 | 'root', 43 | 'test', 44 | ), $postgreSQLPlatform, new ConnectionPoolOptions(10)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/PostgreSQLConnectionTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Tests; 17 | 18 | use Doctrine\DBAL\Platforms\PostgreSQLPlatform; 19 | use Drift\DBAL\Connection; 20 | use Drift\DBAL\ConnectionPoolOptions; 21 | use Drift\DBAL\Credentials; 22 | use Drift\DBAL\Driver\PostgreSQL\PostgreSQLDriver; 23 | use Drift\DBAL\SingleConnection; 24 | use React\EventLoop\LoopInterface; 25 | 26 | /** 27 | * Class PostgreSQLConnectionTest. 28 | */ 29 | class PostgreSQLConnectionTest extends ConnectionTest 30 | { 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function getConnection(LoopInterface $loop): Connection 35 | { 36 | $postgreSQLPlatform = new PostgreSQLPlatform(); 37 | 38 | return SingleConnection::createConnected(new PostgreSQLDriver($loop), new Credentials( 39 | '127.0.0.1', 40 | '5432', 41 | 'root', 42 | 'root', 43 | 'test' 44 | ), $postgreSQLPlatform, new ConnectionPoolOptions(10)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/SQLiteConnectionTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Drift\DBAL\Tests; 17 | 18 | use Doctrine\DBAL\Platforms\SqlitePlatform; 19 | use Drift\DBAL\Connection; 20 | use Drift\DBAL\Credentials; 21 | use Drift\DBAL\Driver\SQLite\SQLiteDriver; 22 | use Drift\DBAL\SingleConnection; 23 | use React\EventLoop\LoopInterface; 24 | 25 | /** 26 | * Class SQLiteConnectionTest. 27 | */ 28 | class SQLiteConnectionTest extends ConnectionTest 29 | { 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function getConnection(LoopInterface $loop): Connection 34 | { 35 | $platform = new SqlitePlatform(); 36 | 37 | return SingleConnection::createConnected(new SQLiteDriver( 38 | $loop 39 | ), new Credentials( 40 | '', 41 | '', 42 | 'root', 43 | 'root', 44 | ':memory:' 45 | ), $platform); 46 | } 47 | } 48 | --------------------------------------------------------------------------------