From 189c915d7434c610f503ade1bc45a755d75348a8 Mon Sep 17 00:00:00 2001 From: Dustin Graham Date: Sat, 7 May 2016 12:47:16 -0700 Subject: [PATCH 1/5] Complete rewrite of async query flow. --- src/Connection.php | 22 ++- src/ConnectionFactory.php | 15 +- src/ConnectionPool.php | 26 ++++ src/Database.php | 124 +++++++++++++++++ tests/DatabaseMock.php | 21 +++ tests/DatabaseTest.php | 29 ++-- tests/RebuildTest.php | 277 +++++++++++++++++++++++++++++++++++++ tests/TestCaseDatabase.php | 34 +++-- tests/TestCaseTest.php | 55 -------- 9 files changed, 502 insertions(+), 101 deletions(-) create mode 100644 tests/DatabaseMock.php create mode 100644 tests/RebuildTest.php delete mode 100644 tests/TestCaseTest.php diff --git a/src/Connection.php b/src/Connection.php index a487491..59d1095 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -4,32 +4,28 @@ use React\EventLoop\Timer\TimerInterface; use React\Promise\Deferred; -class Connection +class Connection extends \mysqli { /** - * @var LoopInterface + * @var int */ - public $loop; + protected static $nextId = 0; /** - * @var \mysqli + * @var int */ - protected $mysqli; - - /** - * @var float - */ - protected $pollInterval = 0.01; + public $id; /** * @var bool|string */ protected $currentQuery = false; - public function __construct(\mysqli $mysqli, LoopInterface $loop) + public function __construct($host = null, $username = null, $passwd = null, $dbname = null, $port = null, $socket = null) { - $this->mysqli = $mysqli; - $this->loop = $loop; + parent::__construct($host, $username, $passwd, $dbname, $port, $socket); + + $this->id = self::$nextId++; } /** diff --git a/src/ConnectionFactory.php b/src/ConnectionFactory.php index 856bd12..a6cb199 100644 --- a/src/ConnectionFactory.php +++ b/src/ConnectionFactory.php @@ -7,7 +7,7 @@ class ConnectionFactory /** * @var LoopInterface */ - public static $loop; + //public static $loop; /** * @var array @@ -16,31 +16,24 @@ class ConnectionFactory public static function init(LoopInterface $loop, $credentials) { - self::$loop = $loop; + //self::$loop = $loop; self::$credentials = $credentials; } public static function createConnection() { - if (is_null(self::$loop)) - { - throw new \Exception('Loop not provided.'); - } - - $mysqli = new \mysqli( + $connection = new Connection( self::$credentials[0], self::$credentials[1], self::$credentials[2], self::$credentials[3] ); - if ($mysqli === false) + if ($connection === false) { throw new \Exception(mysqli_connect_error()); } - $connection = new Connection($mysqli, self::$loop); - return $connection; } } diff --git a/src/ConnectionPool.php b/src/ConnectionPool.php index 9613110..0ab156f 100644 --- a/src/ConnectionPool.php +++ b/src/ConnectionPool.php @@ -33,9 +33,35 @@ public function __construct() $this->waiting = new \SplQueue(); } + public function withConnection($cb) + { + // First check idle connections. + if ($this->available->count() > 0) + { + $connection = $this->available->dequeue(); + + $cb($connection); + return; + } + + // Check if we have max connections + if ($this->pool->count() >= $this->maxConnections) + { + $this->waiting->enqueue($cb); + } + + // Otherwise, create a new connection + $connection = ConnectionFactory::createConnection(); + + $this->pool->attach($connection); + + $cb($connection); + } + /** * We use a promise in case all connections are busy. * + * @deprecated Use withConnection * @return \React\Promise\Promise */ public function getConnection() diff --git a/src/Database.php b/src/Database.php index 44d989c..eb2bd6d 100644 --- a/src/Database.php +++ b/src/Database.php @@ -1,5 +1,8 @@ loop = Factory::create(); + $this->initLoop(); + $this->pool = new ConnectionPool(); } @@ -27,6 +44,7 @@ public function createCommand($sql = null, $params = []) } /** + * @deprecated Use statement * @param Command $command * @return \React\Promise\Promise */ @@ -63,10 +81,116 @@ public function executeCommand(Command $command) } /** + * @deprecated Remove from tests. + * * @return ConnectionPool */ public function getPool() { return $this->pool; } + + public function statement($sql) + { + $deferred = new Deferred(); + + $this->pool->withConnection(function($connection) use ($sql, $deferred) + { + $connection->query($sql, MYSQLI_ASYNC); + + $this->conns[$connection->id] = [ + 'mysqli' => $connection, + 'deferred' => $deferred, + ]; + }); + + return $deferred->promise(); + } + + public function initLoop() + { + $this->loop->addPeriodicTimer( + $this->pollInterval, + [$this, 'loopTick'] + ); + } + + public $conns = []; + + public $shuttingDown = false; + + public function loopTick(TimerInterface $timer) + { + if (count($this->conns) == 0) + { + // If we are shutting down, and have nothing to check, kill the timer. + if ($this->shuttingDown) + { + $timer->cancel(); + } + + // Nothing in the queue. + return; + } + + $reads = []; + foreach($this->conns as $conn) + { + $reads[] = $conn['mysqli']; + } + + // Returns immediately, the non-blocking magic! + if (mysqli_poll($reads, $errors = [], $rejects = [], 0) < 1) return; + + /** @var Connection $read */ + foreach($reads as $read) + { + /** @var Deferred $deferred */ + $deferred = $this->conns[$read->id]['deferred']; + $result = $read->reap_async_query(); + if ($result !== false) + { + $deferred->resolve($result); + $result->free(); + } + else + { + $deferred->reject($read->error); + } + + // Release the connection + $this->pool->releaseConnection($read); + + unset($this->conns[$read->id]); + } + + // Check error pile. + // Current understanding is that this would only happen if the connection + // was closed, or not opened correctly. + foreach($errors as $error) + { + $this->pool->releaseConnection($error); + unset($this->conns[$error->id]); + + throw new \Exception('Unexpected mysqli_poll $error.'); + } + + // Check rejection pile. + // Current understanding is that this would only happen if we passed a + // connection that was already reaped. But... maybe not. + foreach($rejects as $reject) + { + $this->pool->releaseConnection($reject); + unset($this->conns[$reject->id]); + + throw new \Exception('Unexpected mysqli_poll $reject.'); + } + + // Duplicated check to avoid one extra tick! + // If we are shutting down, cancel timer once connections finish. + if ($this->shuttingDown && count($this->conns) == 0) + { + $timer->cancel(); + } + } } diff --git a/tests/DatabaseMock.php b/tests/DatabaseMock.php new file mode 100644 index 0000000..f93a6fc --- /dev/null +++ b/tests/DatabaseMock.php @@ -0,0 +1,21 @@ +loops++; + if ($this->loops > 200) + { + throw new \Exception('time out failure'); + } + + parent::loopTick($timer); + } +} diff --git a/tests/DatabaseTest.php b/tests/DatabaseTest.php index ed12dfd..3142844 100644 --- a/tests/DatabaseTest.php +++ b/tests/DatabaseTest.php @@ -8,7 +8,12 @@ class DatabaseTest extends TestCaseDatabase { - public function testCommandClass() + public function testOne() + { + $this->assertTrue(true); + } + + public function XtestCommandClass() { $db = new Database(); @@ -19,7 +24,7 @@ public function testCommandClass() $command->bindValues([]); } - public function testMysqliConnection() + public function disabled_testMysqliConnection() { $c = $this->getMysqliConnection(); @@ -39,7 +44,7 @@ public function testMysqliConnection() $this->assertEquals('00000', $c->sqlstate); } - public function testMysqliSynchronous() + public function disabled_testMysqliSynchronous() { $c = $this->getMysqliConnection(); @@ -63,7 +68,7 @@ public function testMysqliSynchronous() $stmt->close(); } - public function testMysqliAsynchronous() + public function disabled_testMysqliAsynchronous() { $c = $this->getMysqliConnection(); @@ -73,7 +78,7 @@ public function testMysqliAsynchronous() $this->assertEquals(3, $result->num_rows); } - public function testCreateCommandGetPromise() + public function disabled_testCreateCommandGetPromise() { $db = new Database(); @@ -98,7 +103,7 @@ public function testCreateCommandGetPromise() } // TODO: This test is still todo. - public function testParamCounting() + public function disabled_testParamCounting() { // Note: Used a comma rather than => so it was failing. // param count would detect this sooner. @@ -127,7 +132,7 @@ public function testParamCounting() $connection->close(); } - public function testCommandResolvedResults() + public function disabled_testCommandResolvedResults() { $rowCount = false; $failedReason = false; @@ -156,12 +161,12 @@ public function testCommandResolvedResults() $this->assertEquals(1, $rowCount); } - public function testAssertStrings() + public function disabled_testAssertStrings() { $this->assertStringEqualsIgnoreSpacing('yes no', 'yes no'); } - public function testSimpleCommandParameterBinding() + public function disabled_testSimpleCommandParameterBinding() { $db = new Database(); $cmd = $db->createCommand(); @@ -174,7 +179,7 @@ public function testSimpleCommandParameterBinding() $this->assertEquals('SELECT * FROM simple_table WHERE id = 1', $query); } - public function testComplexCommandParameterBinding() + public function disabled_testComplexCommandParameterBinding() { $db = new Database(); $cmd = $db->createCommand(); @@ -211,7 +216,7 @@ public function testComplexCommandParameterBinding() $connection->close(); } - public function testBadQuery() + public function disabled_testBadQuery() { $db = new Database(); @@ -228,7 +233,7 @@ public function testBadQuery() $this->assertNotNull($failedReason); } - public function testPool() + public function disabled_testPool() { $db = new Database(); $connection = null; diff --git a/tests/RebuildTest.php b/tests/RebuildTest.php new file mode 100644 index 0000000..1880dde --- /dev/null +++ b/tests/RebuildTest.php @@ -0,0 +1,277 @@ +assertTrue(true); + } + + public function testWithClasses() + { + $db = $this->getDatabase(); + + for($loops = 0; $loops < 3; $loops++) + { + for ($i = 0; $i < 3; $i++) + { + $sql = 'SELECT * FROM simple_table WHERE id = ' . $i; + //$sql = 'SELECT SLEEP(0.1);'; + $db->statement($sql)->then(function (\mysqli_result $result) + { + $rows = $result->fetch_all(MYSQLI_ASSOC); + $this->assertLessThanOrEqual(1, count($rows)); + + //$rowCount = count($rows); + //echo $rowCount; + })->done(); + } + + while(count($db->conns)) + { + usleep(1000); + $db->loop->tick(); + } + } + } + + public function testShutdownWithNothing() + { + $db = $this->getDatabase(); + + $db->shuttingDown = true; + $db->loop->run(); + } + + public function testWithSleepFail() + { + $db = $this->getDatabase(); + + $errorCount = 0; + + $sqls = [ + 'SELECT * FROM simple_table WHERE id = 1', + 'SELECT foo FROM', + 'SELECT SLEEP(0.2);', + 'SELECT foo FROM', + 'SELECT SLEEP(0.3);', + 'SELECT foo FROM', + 'SELECT SLEEP(0.1);', + ]; + foreach($sqls as $sql) + { + $db->statement($sql)->then(function(\mysqli_result $result) + { + $rows = $result->fetch_all(MYSQLI_ASSOC); + $this->assertCount(1, $rows); + }) + ->otherwise(function($error) use (&$errorCount) { + $errorCount++; + })->done(); + } + + $db->shuttingDown = true; + $db->loop->run(); + + $this->assertSame(3, $errorCount); + } + + /** + * Works Brilliantly + */ + public function disabled_testTheConcept() + { + echo PHP_EOL; + $pool = []; + + for($i = 0; $i < 0; $i++) + { + $mysqli = $this->getNewMysqliConnection(); + $mysqli_id = $mysqli->thread_id; + $mysqli_my = $mysqli->countId; + + $sql = 'SELECT SLEEP('.$i.');'; + + $mysqli->query($sql, MYSQLI_ASYNC); + + $deferred = new Deferred(); + + $pool[$mysqli->thread_id] = [ + 'mysqli' => $mysqli, + 'deferred' => $deferred, + ]; + + $deferred->promise()->then(function(\mysqli_result $result) use (&$rowCount) + { + $rows = $result->fetch_all(MYSQLI_ASSOC); + $rowCount = count($rows); + + echo $rowCount; + }); + } + + for($i = 0; $i < 3; $i++) + { + $mysqli = $this->getNewMysqliConnection(); + $mysqli_id = $mysqli->thread_id; + + $sql = 'SELECT foo FROM'; + + $mysqli->query($sql, MYSQLI_ASYNC); + + $deferred = new Deferred(); + + $pool[$mysqli->thread_id] = [ + 'mysqli' => $mysqli, + 'deferred' => $deferred, + ]; + + $deferred->promise()->then(function(\mysqli_result $result) use (&$rowCount) { + echo 'M'; + $rows = $result->fetch_all(MYSQLI_ASSOC); + $rowCount = count($rows); + + echo $rowCount; + + $result->close(); + }); + } + + for($i = 0; $i < 6; $i++) + { + $mysqli = $this->getNewMysqliConnection(); + $mysqli_id = $mysqli->thread_id; + + $sql = 'SELECT * FROM simple_table WHERE id = '.$i; + + $mysqli->query($sql, MYSQLI_ASYNC); + + $deferred = new Deferred(); + + $pool[$mysqli->thread_id] = [ + 'mysqli' => $mysqli, + 'deferred' => $deferred, + ]; + + $deferred->promise()->then(function(\mysqli_result $result) use (&$rowCount) { + + $rows = $result->fetch_all(MYSQLI_ASSOC); + $rowCount = count($rows); + + echo $rowCount; + + $result->close(); + }); + } + + $loop = Factory::create(); + + $loop->addPeriodicTimer(0.01, function($timer) use (&$pool) + { + $reads = []; + foreach($pool as $p) + { + $reads[] = $p['mysqli']; + } + + if (count($reads) < 1) return; + + if (mysqli_poll($reads, $errors = [], $rejects = [], 0) < 1) return; + + echo '('.count($reads).'/'.count($errors).'/'.count($rejects).')'; + + /** @var \mysqli $read */ + foreach($reads as $read) + { + //echo '{'.$read->thread_id.'}'; + $deferred = $pool[$read->thread_id]['deferred']; + $result = $read->reap_async_query(); + if ($result === false) + { + echo 'W'; + } + $deferred->resolve($result); + + unset($pool[$read->thread_id]); + } + + foreach($errors as $error) + { + echo 'A'; + unset($pool[$error->thread_id]); + } + + foreach($rejects as $reject) + { + echo 'B'; + unset($pool[$reject->thread_id]); + } + + if (count($pool) == 0) + { + $timer->cancel(); + } + }); + + $loop->run(); + + //$this->assertEquals(1, $rowCount); + + //$this->assertEquals($mysqli_id, $mysqli->thread_id); + } + + + public function XtestExtendedAssert() + { + foreach ([ + [ + 'a b', + 'a c', + ], + [ + 'alpha beta', + 'alpha delta', + ], + [ + 'ab', + 'a b', + ], + [ + ' a bc', + ' abc', + ], + ] as $test) + { + $this->assertStringNotEqualsIgnoreSpacing($test[0], $test[1]); + } + + foreach ([ + [ + // variable internal spacing + 'a b', + 'a b', + ], + [ + // variable spacing, longer text, more instances + 'alpha beta delta gamma', + 'alpha beta delta gamma', + ], + [ + // Trailing and Leading spaces. + ' a b c', + 'a b c ', + ], + ] as $test) + { + $this->assertStringEqualsIgnoreSpacing($test[0], $test[1]); + } + } +} diff --git a/tests/TestCaseDatabase.php b/tests/TestCaseDatabase.php index a407f1d..2747ccc 100644 --- a/tests/TestCaseDatabase.php +++ b/tests/TestCaseDatabase.php @@ -70,19 +70,33 @@ protected function getPdoConnection() return self::$pdo; } + protected function getDatabase() + { + return new DatabaseMock(); + } + /** * Note, do not close the connection. It is reused throughout the tests. * * @return \mysqli */ - protected function getMysqliConnection() - { - if (is_null(self::$mysqli)) - { - list($host, $user, $pass, $name) = $this->getCredentials(); - self::$mysqli = new \mysqli($host, $user, $pass, $name); - } - - return self::$mysqli; - } +// protected function getMysqliConnection() +// { +// if (is_null(self::$mysqli)) +// { +// self::$mysqli = $this->getNewMysqliConnection(); +// } +// +// return self::$mysqli; +// } + +// protected $connCount = 1; +// protected function getNewMysqliConnection() +// { +// list($host, $user, $pass, $name) = $this->getCredentials(); +// $mysqli = new Thing($host, $user, $pass, $name); +// $mysqli->countId = $this->connCount; +// $this->connCount++; +// return $mysqli; +// } } diff --git a/tests/TestCaseTest.php b/tests/TestCaseTest.php deleted file mode 100644 index 17f6846..0000000 --- a/tests/TestCaseTest.php +++ /dev/null @@ -1,55 +0,0 @@ -assertTrue(true); - } - - public function testExtendedAssert() - { - foreach ([ - [ - 'a b', - 'a c', - ], - [ - 'alpha beta', - 'alpha delta', - ], - [ - 'ab', - 'a b', - ], - [ - ' a bc', - ' abc', - ], - ] as $test) - { - $this->assertStringNotEqualsIgnoreSpacing($test[0], $test[1]); - } - - foreach ([ - [ - // variable internal spacing - 'a b', - 'a b', - ], - [ - // variable spacing, longer text, more instances - 'alpha beta delta gamma', - 'alpha beta delta gamma', - ], - [ - // Trailing and Leading spaces. - ' a b c', - 'a b c ', - ], - ] as $test) - { - $this->assertStringEqualsIgnoreSpacing($test[0], $test[1]); - } - } -} From 18990b9910524f5a6fbc8cbbb9c4dab8d13adc1a Mon Sep 17 00:00:00 2001 From: Dustin Graham Date: Sat, 7 May 2016 12:55:20 -0700 Subject: [PATCH 2/5] Fix strict error, enable all errors on unit tests. --- src/Database.php | 4 ++-- tests/bootstrap.php | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Database.php b/src/Database.php index eb2bd6d..58e2595 100644 --- a/src/Database.php +++ b/src/Database.php @@ -133,14 +133,14 @@ public function loopTick(TimerInterface $timer) return; } - $reads = []; + $reads = $errors = $rejects = []; foreach($this->conns as $conn) { $reads[] = $conn['mysqli']; } // Returns immediately, the non-blocking magic! - if (mysqli_poll($reads, $errors = [], $rejects = [], 0) < 1) return; + if (mysqli_poll($reads, $errors, $rejects, 0) < 1) return; /** @var Connection $read */ foreach($reads as $read) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index d21c14d..ad5c2ee 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,3 +1,5 @@ Date: Sat, 7 May 2016 16:31:50 -0700 Subject: [PATCH 3/5] Fix bug with doubled up free results. Improved readme, updated example. --- README.md | 70 ++++++++++++++++++++++++-------------- example | 50 +++++++++++++++++++++++++++ run | 59 -------------------------------- src/Command.php | 23 +++++++------ src/Connection.php | 4 ++- src/ConnectionFactory.php | 13 ++++--- src/Database.php | 24 ++++++++++--- tests/RebuildTest.php | 37 ++++++++++++++++++-- tests/TestCaseDatabase.php | 5 ++- 9 files changed, 171 insertions(+), 114 deletions(-) create mode 100755 example delete mode 100755 run diff --git a/README.md b/README.md index 1ba17fe..85f2998 100644 --- a/README.md +++ b/README.md @@ -3,38 +3,59 @@ Non-blocking MySQLi database access with PHP. Designed to work with [reactphp/react](https://github.com/reactphp/react). +[![Build Status](https://travis-ci.org/dustingraham/react-mysql.svg?branch=master)](https://travis-ci.org/dustingraham/react-mysql) + +## Quickstart + + $db = new \DustinGraham\ReactMysql\Database( + ['localhost', 'apache', 'apache', 'react_mysql_test'] + ); + + $db->statement('SELECT * FROM simple_table WHERE id = :test', [':test' => 2]) + ->then(function(\mysqli_result $result) + { + $rows = $result->fetch_all(MYSQLI_ASSOC); + }); + + $db->shuttingDown = true; + $db->loop->run(); + +Setting `shuttingDown` to true will allow the loop to exit once the query has resolved. ## Working -This __is__ working. But it is nowhere near complete. +This __is__ working. But it is nowhere near complete. Check out the example file +as well as the unit tests for more examples. - $ ./run - Starting loop... - DB Created. + $ ./example + Creating database....done! Run Query: 0 Found rows: 0 Run Query: 1 Found rows: 1 - Current memory usage: 735.117K + Current memory usage: 868.164K Run Query: 2 - Found rows: 0 + Found rows: 1 Run Query: 3 Found rows: 1 Run Query: 4 - Found rows: 1 - Current memory usage: 735.117K + Found rows: 0 + Current memory usage: 868.164K Run Query: 5 Found rows: 0 - Current memory usage: 733.602K - Current memory usage: 733.602K - Current memory usage: 733.602K + Current memory usage: 865.719K + Current memory usage: 865.719K + Current memory usage: 865.719K Loop finished, all timers halted. This won't work out of the box without the database configured. -As of this point, database configuration is hard coded. -Still need to pull out the configs. You will also need to -set up a database with some data to query. Check back later -for more! +You will also need to set up a database with some data to query. + +## Unit Tests + +The example and unit tests expect a database called `react_mysql_test` which it +will populate with the proper tables each time it runs. It also expects `localhost` +and a user `apache` with password `apache`. ## TODO @@ -52,23 +73,20 @@ These are just plans for now. It may change wildly as we develop. Here is an example of what is currently working for the most part. - $loop = React\EventLoop\Factory::create(); - - ConnectionFactory::init($loop, ['db_host', 'db_user', 'db_pass', 'db_name']); - - $db = new \DustinGraham\ReactMysql\Database(); + $db = new \DustinGraham\ReactMysql\Database( + ['localhost', 'apache', 'apache', 'react_mysql_test'] + ); - $db->createCommand("SELECT * FROM `table` WHERE id = :id;", [':id' => $id]) - ->execute()->then( - function($result) + $db->statement('SELECT * FROM simple_table WHERE id = :test', [':test' => 2]) + ->then(function(\mysqli_result $result) { $rows = $result->fetch_all(MYSQLI_ASSOC); - $result->close(); // Do something with $rows. - } - ); + }); + $db->shuttingDown = true; + $db->loop->run(); ### Original Big Picture Plans diff --git a/example b/example new file mode 100755 index 0000000..bbee089 --- /dev/null +++ b/example @@ -0,0 +1,50 @@ +#!/usr/bin/env php +loop->addPeriodicTimer(0.3, function (\React\EventLoop\Timer\TimerInterface $timer) use (&$j) +{ + $memory = memory_get_usage() / 1024; + $formatted = number_format($memory, 3).'K'; + echo "Current memory usage: {$formatted}\n"; + + if ($j++ > 3) $timer->cancel(); +}); + +$i = 0; +$db->loop->addPeriodicTimer(0.1, function (\React\EventLoop\Timer\TimerInterface $timer) use (&$i, $db) +{ + echo "Run Query: $i\n"; + + $db->statement( + 'SELECT * FROM `simple_table` WHERE id = :test', + [':test' => $i] + )->then(function(\mysqli_result $result) + { + $rows = $result->fetch_all(MYSQLI_ASSOC); + echo 'Found rows: '.count($rows).PHP_EOL; + })->done(); + + if ($i++ >= 5) + { + // All queries added. + $db->shuttingDown = true; + $timer->cancel(); + } +}); + +$db->loop->run(); + +echo 'Loop finished, all timers halted.'.PHP_EOL; diff --git a/run b/run deleted file mode 100755 index 767ff39..0000000 --- a/run +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env php -addPeriodicTimer(0.3, function ($timer) use (&$j) -{ - $memory = memory_get_usage() / 1024; - $formatted = number_format($memory, 3).'K'; - echo "Current memory usage: {$formatted}\n"; - - if ($j++ > 3) $timer->cancel(); -}); - -$i = 0; -$loop->addPeriodicTimer(0.1, function ($timer) use (&$i, $db) -{ - echo "Run Query: $i\n"; - - $db->createCommand( - 'SELECT * FROM `simple_table` WHERE id = :test', - [':test' => $i] - )->execute()->then( - function($result) - { - if (is_null($result)) - { - echo 'Null result...'.PHP_EOL.PHP_EOL; - exit; - } - - $rows = $result->fetch_all(MYSQLI_ASSOC); - $result->close(); - - echo 'Found rows: '.count($rows).PHP_EOL; - } - ); - - if ($i++ >= 5) $timer->cancel(); -}); - -$loop->run(); - -echo 'Loop finished, all timers halted.'.PHP_EOL; diff --git a/src/Command.php b/src/Command.php index c1fcb14..f6e9675 100644 --- a/src/Command.php +++ b/src/Command.php @@ -24,10 +24,10 @@ class Command 'NOW()', ]; - public function __construct(Database $database, $sql = null) + public function __construct($sql = null, $params = null) { - $this->db = $database; $this->sql = $sql; + $this->bind($params); } /** @@ -39,10 +39,12 @@ public function bind($key, $value = null) { if (is_array($key)) { - // TODO: Is this cludgy? - $this->bindValues($key); + foreach ($key as $k => $v) + { + $this->params[$k] = $v; + } } - else + else if (!is_null($key)) { $this->params[$key] = $value; } @@ -51,17 +53,14 @@ public function bind($key, $value = null) } /** + * @deprecated + * * @param $params * @return $this */ public function bindValues($params) { - foreach ($params as $k => $v) - { - $this->params[$k] = $v; - } - - return $this; + return $this->bind($params); } /** @@ -113,6 +112,8 @@ protected function quoteIntoSql(Connection $connection) } /** + * @deprecated + * * @return \React\Promise\Promise */ public function execute() diff --git a/src/Connection.php b/src/Connection.php index 59d1095..d30ff99 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -36,10 +36,12 @@ public function __construct($host = null, $username = null, $passwd = null, $dbn */ public function escape($string) { - return $this->mysqli->real_escape_string($string); + return $this->real_escape_string($string); } /** + * @deprecated + * * Close the mysqli connection. */ public function close() diff --git a/src/ConnectionFactory.php b/src/ConnectionFactory.php index a6cb199..317a706 100644 --- a/src/ConnectionFactory.php +++ b/src/ConnectionFactory.php @@ -4,24 +4,23 @@ class ConnectionFactory { - /** - * @var LoopInterface - */ - //public static $loop; - /** * @var array */ protected static $credentials; - public static function init(LoopInterface $loop, $credentials) + public static function init($credentials) { - //self::$loop = $loop; self::$credentials = $credentials; } public static function createConnection() { + if (is_null(self::$credentials)) + { + throw new \Exception('Database credentials not set.'); + } + $connection = new Connection( self::$credentials[0], self::$credentials[1], diff --git a/src/Database.php b/src/Database.php index 58e2595..000c14e 100644 --- a/src/Database.php +++ b/src/Database.php @@ -22,9 +22,13 @@ class Database */ protected $pollInterval = 0.01; - - public function __construct() + public function __construct($credentials = null) { + if (!is_null($credentials)) + { + ConnectionFactory::init($credentials); + } + $this->loop = Factory::create(); $this->initLoop(); @@ -90,12 +94,16 @@ public function getPool() return $this->pool; } - public function statement($sql) + public function statement($sql, $params = null) { + $command = new Command($sql, $params); + $deferred = new Deferred(); - $this->pool->withConnection(function($connection) use ($sql, $deferred) + $this->pool->withConnection(function(Connection $connection) use ($command, $deferred) { + $sql = $command->getPreparedQuery($connection); + $connection->query($sql, MYSQLI_ASYNC); $this->conns[$connection->id] = [ @@ -126,6 +134,8 @@ public function loopTick(TimerInterface $timer) // If we are shutting down, and have nothing to check, kill the timer. if ($this->shuttingDown) { + // TODO: Possible race condition if shutdown also queues queries, such as a final save. + // This could be prematurely cancelled. $timer->cancel(); } @@ -151,7 +161,11 @@ public function loopTick(TimerInterface $timer) if ($result !== false) { $deferred->resolve($result); - $result->free(); + + // If userland code has already freed the result, this will throw a warning. + // No need to throw a warning here... + // If you know how to check if the result has already been freed, please PR! + @$result->free(); } else { diff --git a/tests/RebuildTest.php b/tests/RebuildTest.php index 1880dde..95eede3 100644 --- a/tests/RebuildTest.php +++ b/tests/RebuildTest.php @@ -57,7 +57,7 @@ public function testWithSleepFail() $errorCount = 0; - $sqls = [ + $queries = [ 'SELECT * FROM simple_table WHERE id = 1', 'SELECT foo FROM', 'SELECT SLEEP(0.2);', @@ -66,7 +66,7 @@ public function testWithSleepFail() 'SELECT foo FROM', 'SELECT SLEEP(0.1);', ]; - foreach($sqls as $sql) + foreach($queries as $sql) { $db->statement($sql)->then(function(\mysqli_result $result) { @@ -84,6 +84,39 @@ public function testWithSleepFail() $this->assertSame(3, $errorCount); } + public function testSimpleBind() + { + $db = $this->getDatabase(); + + $db->statement('SELECT * FROM simple_table WHERE id = :test', [':test' => 2]) + ->then(function(\mysqli_result $result) + { + $this->assertCount(1, $result->fetch_all(MYSQLI_ASSOC)); + }) + ->done(); + + $db->shuttingDown = true; + $db->loop->run(); + } + + public function testFreeResult() + { + $db = $this->getDatabase(); + + $db->statement('SELECT * FROM simple_table WHERE id = :test', [':test' => 2]) + ->then(function(\mysqli_result $result) + { + $this->assertCount(1, $result->fetch_all(MYSQLI_ASSOC)); + + // Ensure warning is not thrown. + $result->free(); + }) + ->done(); + + $db->shuttingDown = true; + $db->loop->run(); + } + /** * Works Brilliantly */ diff --git a/tests/TestCaseDatabase.php b/tests/TestCaseDatabase.php index 2747ccc..f74f011 100644 --- a/tests/TestCaseDatabase.php +++ b/tests/TestCaseDatabase.php @@ -21,13 +21,12 @@ class TestCaseDatabase extends TestCase public function setUp() { parent::setUp(); + + $this->initDatabase(); ConnectionFactory::init( - Factory::create(), $this->getCredentials() ); - - $this->initDatabase(); } protected function getCredentials() From 47550415e9c028596f4f66e1d581b20c909f499b Mon Sep 17 00:00:00 2001 From: Dustin Graham Date: Sat, 7 May 2016 16:42:19 -0700 Subject: [PATCH 4/5] Scrub some deprecated methods. Code formatting. --- src/Command.php | 23 ------------ src/Connection.php | 11 ------ src/ConnectionFactory.php | 2 -- src/ConnectionPool.php | 1 + src/Database.php | 14 ++++---- tests/DatabaseTest.php | 76 --------------------------------------- tests/RebuildTest.php | 40 +++++++++++++++++++++ 7 files changed, 48 insertions(+), 119 deletions(-) diff --git a/src/Command.php b/src/Command.php index f6e9675..34c8fb3 100644 --- a/src/Command.php +++ b/src/Command.php @@ -52,17 +52,6 @@ public function bind($key, $value = null) return $this; } - /** - * @deprecated - * - * @param $params - * @return $this - */ - public function bindValues($params) - { - return $this->bind($params); - } - /** * @param Connection $connection * @return string @@ -110,16 +99,4 @@ protected function quoteIntoSql(Connection $connection) return strtr($quotedSql, $quotedParams); } - - /** - * @deprecated - * - * @return \React\Promise\Promise - */ - public function execute() - { - $thing = $this->db->executeCommand($this); - - return $thing; - } } diff --git a/src/Connection.php b/src/Connection.php index d30ff99..92560d0 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -1,6 +1,5 @@ real_escape_string($string); } - /** - * @deprecated - * - * Close the mysqli connection. - */ - public function close() - { - $this->mysqli->close(); - } - public function execute(Command $command) { if ($this->currentQuery) diff --git a/src/ConnectionFactory.php b/src/ConnectionFactory.php index 317a706..2d109ad 100644 --- a/src/ConnectionFactory.php +++ b/src/ConnectionFactory.php @@ -1,7 +1,5 @@ available->dequeue(); $cb($connection); + return; } diff --git a/src/Database.php b/src/Database.php index 000c14e..639f3de 100644 --- a/src/Database.php +++ b/src/Database.php @@ -44,7 +44,7 @@ public function createCommand($sql = null, $params = []) { $command = new Command($this, $sql); - return $command->bindValues($params); + return $command->bind($params); } /** @@ -86,7 +86,7 @@ public function executeCommand(Command $command) /** * @deprecated Remove from tests. - * + * * @return ConnectionPool */ public function getPool() @@ -100,7 +100,7 @@ public function statement($sql, $params = null) $deferred = new Deferred(); - $this->pool->withConnection(function(Connection $connection) use ($command, $deferred) + $this->pool->withConnection(function (Connection $connection) use ($command, $deferred) { $sql = $command->getPreparedQuery($connection); @@ -144,7 +144,7 @@ public function loopTick(TimerInterface $timer) } $reads = $errors = $rejects = []; - foreach($this->conns as $conn) + foreach ($this->conns as $conn) { $reads[] = $conn['mysqli']; } @@ -153,7 +153,7 @@ public function loopTick(TimerInterface $timer) if (mysqli_poll($reads, $errors, $rejects, 0) < 1) return; /** @var Connection $read */ - foreach($reads as $read) + foreach ($reads as $read) { /** @var Deferred $deferred */ $deferred = $this->conns[$read->id]['deferred']; @@ -181,7 +181,7 @@ public function loopTick(TimerInterface $timer) // Check error pile. // Current understanding is that this would only happen if the connection // was closed, or not opened correctly. - foreach($errors as $error) + foreach ($errors as $error) { $this->pool->releaseConnection($error); unset($this->conns[$error->id]); @@ -192,7 +192,7 @@ public function loopTick(TimerInterface $timer) // Check rejection pile. // Current understanding is that this would only happen if we passed a // connection that was already reaped. But... maybe not. - foreach($rejects as $reject) + foreach ($rejects as $reject) { $this->pool->releaseConnection($reject); unset($this->conns[$reject->id]); diff --git a/tests/DatabaseTest.php b/tests/DatabaseTest.php index 3142844..e4f5284 100644 --- a/tests/DatabaseTest.php +++ b/tests/DatabaseTest.php @@ -20,8 +20,6 @@ public function XtestCommandClass() $command = $db->createCommand(); $this->assertInstanceOf(Command::class, $command); - - $command->bindValues([]); } public function disabled_testMysqliConnection() @@ -78,30 +76,6 @@ public function disabled_testMysqliAsynchronous() $this->assertEquals(3, $result->num_rows); } - public function disabled_testCreateCommandGetPromise() - { - $db = new Database(); - - $cmd = $db->createCommand(); - - $cmd->sql = 'SELECT * FROM simple_table WHERE id = :id'; - $cmd->bind(':id', 1); - - $promise = $cmd->execute(); - $this->assertInstanceOf(Promise::class, $promise); - - //// - - $promise = $db->createCommand( - 'SELECT * FROM simple_table WHERE id = :test', - [ - ':test', - 1, - ] - )->execute(); - $this->assertInstanceOf(Promise::class, $promise); - } - // TODO: This test is still todo. public function disabled_testParamCounting() { @@ -128,37 +102,6 @@ public function disabled_testParamCounting() $query, 'SELECT * FROM simple_table WHERE id = :test' ); - - $connection->close(); - } - - public function disabled_testCommandResolvedResults() - { - $rowCount = false; - $failedReason = false; - - $db = new Database(); - - $db->createCommand( - 'SELECT * FROM simple_table WHERE id = :test', - [':test' => 1,] - ) - ->execute() - ->then(function (\mysqli_result $results) use (&$rowCount) - { - $rows = $results->fetch_all(MYSQLI_ASSOC); - $rowCount = count($rows); - }) - ->otherwise(function ($reason) use (&$failedReason) - { - $failedReason = $reason; - }); - - ConnectionFactory::$loop->run(); - - $this->assertFalse($failedReason); - - $this->assertEquals(1, $rowCount); } public function disabled_testAssertStrings() @@ -212,25 +155,6 @@ public function disabled_testComplexCommandParameterBinding() "INSERT INTO simple_table ( `id`, `name`, `value`, `created_at` ) VALUES ( NULL, 'John Cash', 7, NOW() );", $query ); - - $connection->close(); - } - - public function disabled_testBadQuery() - { - $db = new Database(); - - $failedReason = null; - $db->createCommand('SELECT * FROM `nonexistant_table`;') - ->execute() - ->otherwise(function ($reason) use (&$failedReason) - { - $failedReason = $reason; - }); - - ConnectionFactory::$loop->run(); - - $this->assertNotNull($failedReason); } public function disabled_testPool() diff --git a/tests/RebuildTest.php b/tests/RebuildTest.php index 95eede3..e09add7 100644 --- a/tests/RebuildTest.php +++ b/tests/RebuildTest.php @@ -7,6 +7,7 @@ use React\EventLoop\Factory; use React\Promise\Deferred; use React\Promise\Promise; +use React\Promise\UnhandledRejectionException; class RebuildTest extends TestCaseDatabase { @@ -117,6 +118,45 @@ public function testFreeResult() $db->loop->run(); } + public function testBadQuery() + { + $db = $this->getDatabase(); + + $errorTriggered = false; + $db->statement('SELECT foo FROM') + ->then(function(\mysqli_result $result) + { + $this->fail(); + }) + ->otherwise(function($error) use (&$errorTriggered) + { + $errorTriggered = !!$error; + }) + ->done(); + + $db->shuttingDown = true; + $db->loop->run(); + + $this->assertTrue($errorTriggered, 'Error was sent to otherwise callback.'); + } + + public function testUnhandledBadQuery() + { + $db = $this->getDatabase(); + + $db->statement('SELECT foo FROM') + ->then(function(\mysqli_result $result) + { + $this->fail(); + }) + ->done(); + + $this->setExpectedException(UnhandledRejectionException::class); + + $db->shuttingDown = true; + $db->loop->run(); + } + /** * Works Brilliantly */ From b645e80fe4ed4ba2dec1052809be413c2b4b390c Mon Sep 17 00:00:00 2001 From: Dustin Graham Date: Sat, 7 May 2016 19:23:51 -0700 Subject: [PATCH 5/5] Improve code coverage, remove old code. --- README.md | 2 - src/Command.php | 13 +-- src/Connection.php | 74 +------------ src/ConnectionFactory.php | 7 ++ src/ConnectionPool.php | 41 +------- src/Database.php | 59 ----------- tests/CommandTest.php | 104 +++++++++++++++++++ tests/ConnectionFactoryTest.php | 31 ++++++ tests/DatabaseTest.php | 177 +------------------------------- tests/PoolTest.php | 63 ++++++++++++ tests/RebuildTest.php | 62 ++++++----- tests/TestCase.php | 88 ++++++++++++++++ tests/TestCaseDatabase.php | 101 ------------------ 13 files changed, 336 insertions(+), 486 deletions(-) create mode 100644 tests/CommandTest.php create mode 100644 tests/ConnectionFactoryTest.php create mode 100644 tests/PoolTest.php delete mode 100644 tests/TestCaseDatabase.php diff --git a/README.md b/README.md index 85f2998..ba640c9 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,6 @@ and a user `apache` with password `apache`. ## TODO -A lot. - This is not production ready. Still tons to do on the query builder. While I hate to reinvent the wheel, I have not found a lightweight injectable query builder that is not tied to a massive framework. diff --git a/src/Command.php b/src/Command.php index 34c8fb3..c625b5d 100644 --- a/src/Command.php +++ b/src/Command.php @@ -2,11 +2,6 @@ class Command { - /** - * @var Database the command is associated with. - */ - public $db; - /** * @var string */ @@ -18,6 +13,8 @@ class Command protected $params = []; /** + * TODO: Find all of these + * * @var array */ protected $reserved_words = [ @@ -58,13 +55,9 @@ public function bind($key, $value = null) */ public function getPreparedQuery(Connection $connection) { - $quotedSql = $this->quoteIntoSql($connection); - - return $quotedSql; + return $this->quoteIntoSql($connection); } - // TODO: Find all of these... - /** * TODO: This is exactly what I don't want to do. "Roll my own" SQL handler. * However, the requirements for this package have led to this point for now. diff --git a/src/Connection.php b/src/Connection.php index 92560d0..2cad272 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -1,8 +1,5 @@ real_escape_string($string); } - - public function execute(Command $command) - { - if ($this->currentQuery) - { - throw new \Exception('Another query is already pending for this connection.'); - } - - $this->currentQuery = $command->getPreparedQuery($this); - - $status = $this->mysqli->query($this->currentQuery, MYSQLI_ASYNC); - if ($status === false) - { - throw new \Exception($this->mysqli->error); - } - - $deferred = new Deferred(); - - $this->loop->addPeriodicTimer( - $this->pollInterval, - function (TimerInterface $timer) use ($deferred) - { - $reads = $errors = $rejects = [$this->mysqli]; - - // Non-blocking requires a zero wait time. - $this->mysqli->poll($reads, $errors, $rejects, 0); - - $read = in_array($this->mysqli, $reads, true); - $error = in_array($this->mysqli, $errors, true); - $reject = in_array($this->mysqli, $rejects, true); - - if ($read) - { - $result = $this->mysqli->reap_async_query(); - if ($result === false) - { - $deferred->reject($this->mysqli->error); - } - else - { - // Success!! - $deferred->resolve($result); - } - } - else - { - if ($error) - { - $deferred->reject($this->mysqli->error); - } - else - { - if ($reject) - { - $deferred->reject($this->mysqli->error); - } - } - } - - // If poll yielded something for this connection, we're done! - if ($read || $error || $reject) - { - $this->currentQuery = false; - $timer->cancel(); - } - } - ); - - return $deferred->promise(); - } } diff --git a/src/ConnectionFactory.php b/src/ConnectionFactory.php index 2d109ad..2fe26b9 100644 --- a/src/ConnectionFactory.php +++ b/src/ConnectionFactory.php @@ -7,11 +7,18 @@ class ConnectionFactory */ protected static $credentials; + /** + * @param array $credentials + */ public static function init($credentials) { self::$credentials = $credentials; } + /** + * @return Connection + * @throws \Exception + */ public static function createConnection() { if (is_null(self::$credentials)) diff --git a/src/ConnectionPool.php b/src/ConnectionPool.php index 2517eef..c4b8f66 100644 --- a/src/ConnectionPool.php +++ b/src/ConnectionPool.php @@ -1,7 +1,5 @@ available->count() > 0) - { - $connection = $this->available->dequeue(); - - return \React\Promise\resolve($connection); - } - - // Check if we have max connections - if ($this->pool->count() >= $this->maxConnections) - { - $deferred = new Deferred(); - $this->waiting->enqueue($deferred); - - return $deferred->promise(); - } - - // Otherwise, create a new connection - $connection = ConnectionFactory::createConnection(); - - $this->pool->attach($connection); - - return \React\Promise\resolve($connection); - } - /** * Once a connection has finished being used... * @param Connection $connection @@ -101,7 +66,11 @@ public function releaseConnection(Connection $connection) // If we have any promises waiting for the connection, pass it along. if ($this->waiting->count() > 0) { - $this->waiting->dequeue()->resolve($connection); + $cb = $this->waiting->dequeue(); + + $cb($connection); + + return; } // Otherwise, move it to the idle queue. diff --git a/src/Database.php b/src/Database.php index 639f3de..6ed3f82 100644 --- a/src/Database.php +++ b/src/Database.php @@ -35,65 +35,6 @@ public function __construct($credentials = null) $this->pool = new ConnectionPool(); } - /** - * @param string|null $sql - * @param array $params - * @return Command - */ - public function createCommand($sql = null, $params = []) - { - $command = new Command($this, $sql); - - return $command->bind($params); - } - - /** - * @deprecated Use statement - * @param Command $command - * @return \React\Promise\Promise - */ - public function executeCommand(Command $command) - { - $deferred = new Deferred(); - - $this->pool->getConnection() - ->then(function (Connection $connection) use ($command, $deferred) - { - // Connection was retrieved from the pool. Execute the command. - $connection->execute($command) - ->then(function (\mysqli_result $result) use ($deferred) - { - // We must resolve first so that the result can be closed. - $deferred->resolve($result); - - // Doesn't hurt to close it again. - $result->close(); - }) - ->otherwise(function ($reason) use ($deferred) - { - // If the connection execution fails, pass the failure back to the command. - $deferred->reject($reason); - }) - ->always(function () use ($connection) - { - // Ensure we always return the connection to the pool. - $this->pool->releaseConnection($connection); - }); - }); - - return $deferred->promise(); - } - - /** - * @deprecated Remove from tests. - * - * @return ConnectionPool - */ - public function getPool() - { - return $this->pool; - } - public function statement($sql, $params = null) { $command = new Command($sql, $params); diff --git a/tests/CommandTest.php b/tests/CommandTest.php new file mode 100644 index 0000000..a207584 --- /dev/null +++ b/tests/CommandTest.php @@ -0,0 +1,104 @@ + null, + ':name' => 'John\'s Name', + ':num' => 7, + ':datetime' => 'NOW()', + ]); + + $connection = ConnectionFactory::createConnection(); + + $query = $command->getPreparedQuery($connection); + + $this->assertStringEqualsIgnoreSpacing( + "INSERT INTO simple_table ( `id`, `name`, `value`, `created_at` ) VALUES ( NULL, 'John\'s Name', 7, NOW() );", + $query + ); + } + + public function testAssertStrings() + { + $this->assertStringEqualsIgnoreSpacing('yes no ', 'yes no'); + } + + public function testSingleBind() + { + $command = new Command(" + SELECT * FROM simple_table WHERE id = :id + "); + + $command->bind(':id', 1); + + $connection = ConnectionFactory::createConnection(); + + $command->getPreparedQuery($connection); + } + + public function testParameterReplacing() + { + $command = new Command; + $command->sql = 'SELECT * FROM simple_table WHERE id = :id'; + $command->bind(':id', 2); + + $connection = ConnectionFactory::createConnection(); + + $query = $command->getPreparedQuery($connection); + + $this->assertStringEqualsIgnoreSpacing( + 'SELECT * FROM simple_table WHERE id = 2', + $query + ); + } + + /** + * TODO: This test is still todo. + * + * @throws \Exception + */ + public function testParamCounting() + { + // Note: Used a comma rather than => so it was failing. + // param count would detect this sooner. + + // Intentionally bad parameters to ensure check. + $badParams = [':test', 1,]; + // The programmer's intent was: + // $goodParams = [ ':test' => 1, ] + + $command = new Command( + 'SELECT * FROM simple_table WHERE id = :test', + $badParams + ); + + $connection = ConnectionFactory::createConnection(); + + $query = $command->getPreparedQuery($connection); + + // TODO: Here is the bad result, :test should have been 1 + // TODO: GetPreparedQuery should error on param mismatch + $this->assertStringEqualsIgnoreSpacing( + 'SELECT * FROM simple_table WHERE id = :test', + $query + ); + } +} diff --git a/tests/ConnectionFactoryTest.php b/tests/ConnectionFactoryTest.php new file mode 100644 index 0000000..38bdd35 --- /dev/null +++ b/tests/ConnectionFactoryTest.php @@ -0,0 +1,31 @@ +assertTrue(true); - } - - public function XtestCommandClass() - { - $db = new Database(); - - $command = $db->createCommand(); - - $this->assertInstanceOf(Command::class, $command); - } - - public function disabled_testMysqliConnection() + public function testMysqliConnection() { $c = $this->getMysqliConnection(); @@ -42,160 +25,8 @@ public function disabled_testMysqliConnection() $this->assertEquals('00000', $c->sqlstate); } - public function disabled_testMysqliSynchronous() + public function testForCoverage() { - $c = $this->getMysqliConnection(); - - $result = $c->query('SELECT * FROM simple_table;'); - $this->assertEquals(3, $result->num_rows); - - $tempTableName = 'temptable123'; - $c->query('CREATE TEMPORARY TABLE ' . $tempTableName . ' LIKE simple_table;'); - $result = $c->query('SELECT * FROM ' . $tempTableName); - $this->assertEquals(0, $result->num_rows); - - $stmt = $c->prepare('INSERT INTO ' . $tempTableName . ' (`id`, `name`) VALUES (?, ?)'); - - $id = null; - $name = 'john'; - - $stmt->bind_param('is', $id, $name); - - $stmt->execute(); - $this->assertEquals(1, $stmt->affected_rows, 'Did not insert the row.'); - $stmt->close(); - } - - public function disabled_testMysqliAsynchronous() - { - $c = $this->getMysqliConnection(); - - $c->query('SELECT * FROM simple_table;', MYSQLI_ASYNC); - - $result = $c->reap_async_query(); - $this->assertEquals(3, $result->num_rows); - } - - // TODO: This test is still todo. - public function disabled_testParamCounting() - { - // Note: Used a comma rather than => so it was failing. - // param count would detect this sooner. - - // Intentionally bad parameters to ensure check. - $badParams = [':test', 1,]; - // The programmer's intent was: - // $goodParams = [ ':test' => 1, ] - - $db = new Database(); - $command = $db->createCommand( - 'SELECT * FROM simple_table WHERE id = :test', - $badParams - ); - - $connection = ConnectionFactory::createConnection(); - $query = $command->getPreparedQuery($connection); - - // TODO: Here is the bad result, :test should have been 1 - // TODO: GetPreparedQuery should error on param mismatch - $this->assertEquals( - $query, - 'SELECT * FROM simple_table WHERE id = :test' - ); - } - - public function disabled_testAssertStrings() - { - $this->assertStringEqualsIgnoreSpacing('yes no', 'yes no'); - } - - public function disabled_testSimpleCommandParameterBinding() - { - $db = new Database(); - $cmd = $db->createCommand(); - $cmd->sql = 'SELECT * FROM simple_table WHERE id = :id'; - $cmd->bind(':id', 1); - - $connection = ConnectionFactory::createConnection(); - $query = $cmd->getPreparedQuery($connection); - - $this->assertEquals('SELECT * FROM simple_table WHERE id = 1', $query); - } - - public function disabled_testComplexCommandParameterBinding() - { - $db = new Database(); - $cmd = $db->createCommand(); - $cmd->sql = " - INSERT INTO simple_table ( - `id`, - `name`, - `value`, - `created_at` - ) VALUES ( - :id, - :name, - :num, - :datetime - ); - "; - - $cmd->bind([ - ':id' => null, - ':name' => 'John Cash', - ':num' => 7, - ':datetime' => 'NOW()', - ]); - - $connection = ConnectionFactory::createConnection(); - - $query = $cmd->getPreparedQuery($connection); - - $this->assertStringEqualsIgnoreSpacing( - "INSERT INTO simple_table ( `id`, `name`, `value`, `created_at` ) VALUES ( NULL, 'John Cash', 7, NOW() );", - $query - ); - } - - public function disabled_testPool() - { - $db = new Database(); - $connection = null; - $db->getPool()->getConnection() - ->then(function (Connection $conn) use (&$connection, $db) - { - $connection = $conn; - - // Send it back from whence it came. - $db->getPool()->releaseConnection($conn); - }); - - $sameConnection = false; - $db->getPool()->getConnection() - ->then(function (Connection $conn) use ($connection, &$sameConnection, $db) - { - // Did we get the same one again? - $sameConnection = $conn === $connection; - - $db->getPool()->releaseConnection($conn); - }); - - $this->assertTrue($sameConnection, 'Ensure it is the same exact connection.'); - - // This will cause code coverage to cover the getConnection and releaseConnection - // parts that deal with excess of 100 connections. - $promises = []; - for ($i = 0; $i < 120; $i++) - { - // Pool has up to 100... so pull them all! - $promises[] = $db->getPool()->getConnection(); - } - foreach ($promises as $promise) - { - $promise->then(function (Connection $conn) use ($db) - { - $db->getPool()->releaseConnection($conn); - }); - } + new Database($this->getCredentials()); } } diff --git a/tests/PoolTest.php b/tests/PoolTest.php new file mode 100644 index 0000000..0d9311d --- /dev/null +++ b/tests/PoolTest.php @@ -0,0 +1,63 @@ +getDatabase(); + + // Spin up 120 queries. + for ($i = 0; $i < 120; $i++) + { + $sql = 'SELECT * FROM simple_table WHERE id = ' . $i; + $db->statement($sql)->then(function (\mysqli_result $result) + { + $result->free(); + })->done(); + } + + $db->shuttingDown = true; + $db->loop->run(); + } + + public function testSameConnectionIds() + { + $pool = new ConnectionPool(); + + $id = null; + $pool->withConnection(function ($connection) use ($pool, &$id) + { + $id = $connection->id; + $pool->releaseConnection($connection); + }); + + $pool->withConnection(function ($connection) use ($pool, &$id) + { + $this->assertEquals($id, $connection->id); + + $pool->releaseConnection($connection); + }); + } + + public function testIdenticalConnections() + { + $pool = new ConnectionPool(); + + $connection = null; + + $pool->withConnection(function ($conn) use ($pool, &$connection) + { + $connection = $conn; + $pool->releaseConnection($conn); + }); + + $pool->withConnection(function ($conn) use ($pool, $connection) + { + $this->assertSame($connection, $conn); + + $pool->releaseConnection($conn); + }); + } +} diff --git a/tests/RebuildTest.php b/tests/RebuildTest.php index e09add7..9d2e58d 100644 --- a/tests/RebuildTest.php +++ b/tests/RebuildTest.php @@ -1,15 +1,10 @@ getDatabase(); - for($loops = 0; $loops < 3; $loops++) + for ($loops = 0; $loops < 3; $loops++) { for ($i = 0; $i < 3; $i++) { @@ -36,7 +31,7 @@ public function testWithClasses() })->done(); } - while(count($db->conns)) + while (count($db->conns)) { usleep(1000); $db->loop->tick(); @@ -67,16 +62,17 @@ public function testWithSleepFail() 'SELECT foo FROM', 'SELECT SLEEP(0.1);', ]; - foreach($queries as $sql) + foreach ($queries as $sql) { - $db->statement($sql)->then(function(\mysqli_result $result) + $db->statement($sql)->then(function (\mysqli_result $result) { $rows = $result->fetch_all(MYSQLI_ASSOC); $this->assertCount(1, $rows); }) - ->otherwise(function($error) use (&$errorCount) { - $errorCount++; - })->done(); + ->otherwise(function ($error) use (&$errorCount) + { + $errorCount++; + })->done(); } $db->shuttingDown = true; @@ -90,7 +86,7 @@ public function testSimpleBind() $db = $this->getDatabase(); $db->statement('SELECT * FROM simple_table WHERE id = :test', [':test' => 2]) - ->then(function(\mysqli_result $result) + ->then(function (\mysqli_result $result) { $this->assertCount(1, $result->fetch_all(MYSQLI_ASSOC)); }) @@ -105,7 +101,7 @@ public function testFreeResult() $db = $this->getDatabase(); $db->statement('SELECT * FROM simple_table WHERE id = :test', [':test' => 2]) - ->then(function(\mysqli_result $result) + ->then(function (\mysqli_result $result) { $this->assertCount(1, $result->fetch_all(MYSQLI_ASSOC)); @@ -124,11 +120,11 @@ public function testBadQuery() $errorTriggered = false; $db->statement('SELECT foo FROM') - ->then(function(\mysqli_result $result) + ->then(function (\mysqli_result $result) { $this->fail(); }) - ->otherwise(function($error) use (&$errorTriggered) + ->otherwise(function ($error) use (&$errorTriggered) { $errorTriggered = !!$error; }) @@ -145,7 +141,7 @@ public function testUnhandledBadQuery() $db = $this->getDatabase(); $db->statement('SELECT foo FROM') - ->then(function(\mysqli_result $result) + ->then(function (\mysqli_result $result) { $this->fail(); }) @@ -165,13 +161,13 @@ public function disabled_testTheConcept() echo PHP_EOL; $pool = []; - for($i = 0; $i < 0; $i++) + for ($i = 0; $i < 0; $i++) { $mysqli = $this->getNewMysqliConnection(); $mysqli_id = $mysqli->thread_id; $mysqli_my = $mysqli->countId; - $sql = 'SELECT SLEEP('.$i.');'; + $sql = 'SELECT SLEEP(' . $i . ');'; $mysqli->query($sql, MYSQLI_ASYNC); @@ -182,7 +178,7 @@ public function disabled_testTheConcept() 'deferred' => $deferred, ]; - $deferred->promise()->then(function(\mysqli_result $result) use (&$rowCount) + $deferred->promise()->then(function (\mysqli_result $result) use (&$rowCount) { $rows = $result->fetch_all(MYSQLI_ASSOC); $rowCount = count($rows); @@ -191,7 +187,7 @@ public function disabled_testTheConcept() }); } - for($i = 0; $i < 3; $i++) + for ($i = 0; $i < 3; $i++) { $mysqli = $this->getNewMysqliConnection(); $mysqli_id = $mysqli->thread_id; @@ -207,7 +203,8 @@ public function disabled_testTheConcept() 'deferred' => $deferred, ]; - $deferred->promise()->then(function(\mysqli_result $result) use (&$rowCount) { + $deferred->promise()->then(function (\mysqli_result $result) use (&$rowCount) + { echo 'M'; $rows = $result->fetch_all(MYSQLI_ASSOC); $rowCount = count($rows); @@ -218,12 +215,12 @@ public function disabled_testTheConcept() }); } - for($i = 0; $i < 6; $i++) + for ($i = 0; $i < 6; $i++) { $mysqli = $this->getNewMysqliConnection(); $mysqli_id = $mysqli->thread_id; - $sql = 'SELECT * FROM simple_table WHERE id = '.$i; + $sql = 'SELECT * FROM simple_table WHERE id = ' . $i; $mysqli->query($sql, MYSQLI_ASYNC); @@ -234,7 +231,8 @@ public function disabled_testTheConcept() 'deferred' => $deferred, ]; - $deferred->promise()->then(function(\mysqli_result $result) use (&$rowCount) { + $deferred->promise()->then(function (\mysqli_result $result) use (&$rowCount) + { $rows = $result->fetch_all(MYSQLI_ASSOC); $rowCount = count($rows); @@ -247,10 +245,10 @@ public function disabled_testTheConcept() $loop = Factory::create(); - $loop->addPeriodicTimer(0.01, function($timer) use (&$pool) + $loop->addPeriodicTimer(0.01, function ($timer) use (&$pool) { $reads = []; - foreach($pool as $p) + foreach ($pool as $p) { $reads[] = $p['mysqli']; } @@ -259,10 +257,10 @@ public function disabled_testTheConcept() if (mysqli_poll($reads, $errors = [], $rejects = [], 0) < 1) return; - echo '('.count($reads).'/'.count($errors).'/'.count($rejects).')'; + echo '(' . count($reads) . '/' . count($errors) . '/' . count($rejects) . ')'; /** @var \mysqli $read */ - foreach($reads as $read) + foreach ($reads as $read) { //echo '{'.$read->thread_id.'}'; $deferred = $pool[$read->thread_id]['deferred']; @@ -276,13 +274,13 @@ public function disabled_testTheConcept() unset($pool[$read->thread_id]); } - foreach($errors as $error) + foreach ($errors as $error) { echo 'A'; unset($pool[$error->thread_id]); } - foreach($rejects as $reject) + foreach ($rejects as $reject) { echo 'B'; unset($pool[$reject->thread_id]); diff --git a/tests/TestCase.php b/tests/TestCase.php index 45f2651..952d143 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,10 +1,71 @@ initDatabase(); + + ConnectionFactory::init( + $this->getCredentials() + ); + } + + protected function initDatabase() + { + if (!self::$initialized) + { + // While this package is focused on mysqli async, we can use + // PDO to initialize the database structure efficiently. + $this->getPdoConnection() + ->exec(file_get_contents(__DIR__ . '/sql.sql')); + + self::$initialized = true; + } + } + + protected function getPdoConnection() + { + if (is_null(self::$pdo)) + { + list($host, $user, $pass, $name) = $this->getCredentials(); + $dsn = 'mysql:host=' . $host . ';dbname=' . $name; + self::$pdo = new \PDO($dsn, $user, $pass); + } + + return self::$pdo; + } + + protected function getCredentials() + { + $host = getenv('DB_HOST') !== false ? getenv('DB_HOST') : 'localhost'; + $user = getenv('DB_USER') !== false ? getenv('DB_USER') : 'apache'; + $pass = getenv('DB_PASS') !== false ? getenv('DB_PASS') : 'apache'; + $name = getenv('DB_NAME') !== false ? getenv('DB_NAME') : 'react_mysql_test'; + + return [ + $host, + $user, + $pass, + $name, + ]; } public function assertStringEqualsIgnoreSpacing($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) @@ -22,4 +83,31 @@ public function assertStringNotEqualsIgnoreSpacing($expected, $actual, $message $this->assertNotEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase); } + + protected function getDatabase() + { + return new DatabaseMock(); + } + + /** + * Note, do not close the connection. It is reused throughout the tests. + * + * @return \mysqli + */ + protected function getMysqliConnection() + { + if (is_null(self::$mysqli)) + { + self::$mysqli = $this->getNewMysqliConnection(); + } + + return self::$mysqli; + } + + protected function getNewMysqliConnection() + { + list($host, $user, $pass, $name) = $this->getCredentials(); + + return new \mysqli($host, $user, $pass, $name); + } } diff --git a/tests/TestCaseDatabase.php b/tests/TestCaseDatabase.php deleted file mode 100644 index f74f011..0000000 --- a/tests/TestCaseDatabase.php +++ /dev/null @@ -1,101 +0,0 @@ -initDatabase(); - - ConnectionFactory::init( - $this->getCredentials() - ); - } - - protected function getCredentials() - { - $host = getenv('DB_HOST') !== false ? getenv('DB_HOST') : 'localhost'; - $user = getenv('DB_USER') !== false ? getenv('DB_USER') : 'apache'; - $pass = getenv('DB_PASS') !== false ? getenv('DB_PASS') : 'apache'; - $name = getenv('DB_NAME') !== false ? getenv('DB_NAME') : 'react_mysql_test'; - - return [ - $host, - $user, - $pass, - $name, - ]; - } - - protected function initDatabase() - { - if (!self::$initialized) - { - // While this package is focused on mysqli async, we can use - // PDO to initialize the database structure efficiently. - $this->getPdoConnection() - ->exec(file_get_contents(__DIR__ . '/sql.sql')); - - self::$initialized = true; - } - } - - protected function getPdoConnection() - { - if (is_null(self::$pdo)) - { - list($host, $user, $pass, $name) = $this->getCredentials(); - $dsn = 'mysql:host=' . $host . ';dbname=' . $name; - self::$pdo = new \PDO($dsn, $user, $pass); - } - - return self::$pdo; - } - - protected function getDatabase() - { - return new DatabaseMock(); - } - - /** - * Note, do not close the connection. It is reused throughout the tests. - * - * @return \mysqli - */ -// protected function getMysqliConnection() -// { -// if (is_null(self::$mysqli)) -// { -// self::$mysqli = $this->getNewMysqliConnection(); -// } -// -// return self::$mysqli; -// } - -// protected $connCount = 1; -// protected function getNewMysqliConnection() -// { -// list($host, $user, $pass, $name) = $this->getCredentials(); -// $mysqli = new Thing($host, $user, $pass, $name); -// $mysqli->countId = $this->connCount; -// $this->connCount++; -// return $mysqli; -// } -}