diff --git a/README.md b/README.md index e832977..bc9441a 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,18 @@ environment than your parent process. $factory = new Clue\React\SQLite\Factory(null, '/usr/bin/php6.0'); ``` +Or you may use this parameter to pass an empty PHP binary path which will +cause this project to not spawn a PHP child process for any database +interactions at all. In this case, using SQLite will block the main +process, but continues to provide the exact same async API. This can be +useful if concurrent execution is not needed, especially when running +behind a traditional web server (non-CLI SAPI). + +```php +// advanced usage: empty binary path runs blocking SQLite in same process +$factory = new Clue\React\SQLite\Factory(null, ''); +``` + #### open() The `open(string $filename, int $flags = null): PromiseInterface` method can be used to diff --git a/res/sqlite-worker.php b/res/sqlite-worker.php index a010a32..f2efb67 100644 --- a/res/sqlite-worker.php +++ b/res/sqlite-worker.php @@ -13,6 +13,8 @@ use Clue\React\NDJson\Decoder; use Clue\React\NDJson\Encoder; +use Clue\React\SQLite\Io\BlockingDatabase; +use Clue\React\SQLite\Result; use React\EventLoop\Factory; use React\Stream\DuplexResourceStream; use React\Stream\ReadableResourceStream; @@ -74,35 +76,10 @@ return; } - if ($data->method === 'open' && \count($data->params) === 1 && \is_string($data->params[0])) { - // open database with one parameter: $filename - try { - $db = new SQLite3( - $data->params[0] - ); - - $out->write(array( - 'id' => $data->id, - 'result' => true - )); - } catch (Exception $e) { - $out->write(array( - 'id' => $data->id, - 'error' => array('message' => $e->getMessage()) - )); - } catch (Error $e) { - $out->write(array( - 'id' => $data->id, - 'error' => array('message' => $e->getMessage()) - )); - } - } elseif ($data->method === 'open' && \count($data->params) === 2 && \is_string($data->params[0]) && \is_int($data->params[1])) { + if ($data->method === 'open' && \count($data->params) === 2 && \is_string($data->params[0]) && ($data->params[1] === null || \is_int($data->params[1]))) { // open database with two parameters: $filename, $flags try { - $db = new SQLite3( - $data->params[0], - $data->params[1] - ); + $db = new BlockingDatabase($data->params[0], $data->params[1]); $out->write(array( 'id' => $data->id, @@ -120,78 +97,40 @@ )); } } elseif ($data->method === 'exec' && $db !== null && \count($data->params) === 1 && \is_string($data->params[0])) { - // execute statement and suppress PHP warnings - $ret = @$db->exec($data->params[0]); - - if ($ret === false) { + // execute statement: $db->exec($sql) + $db->exec($data->params[0])->then(function (Result $result) use ($data, $out) { $out->write(array( 'id' => $data->id, - 'error' => array('message' => $db->lastErrorMsg()) + 'result' => array( + 'insertId' => $result->insertId, + 'changed' => $result->changed + ) )); - } else { + }, function (Exception $e) use ($data, $out) { $out->write(array( 'id' => $data->id, - 'result' => array( - 'insertId' => $db->lastInsertRowID(), - 'changed' => $db->changes() - ) + 'error' => array('message' => $e->getMessage()) )); - } + }); } elseif ($data->method === 'query' && $db !== null && \count($data->params) === 2 && \is_string($data->params[0]) && (\is_array($data->params[1]) || \is_object($data->params[1]))) { - // execute statement and suppress PHP warnings - if ($data->params[1] === []) { - $result = @$db->query($data->params[0]); - } else { - $statement = @$db->prepare($data->params[0]); - if ($statement === false) { - $result = false; + // execute statement: $db->query($sql, $params) + $params = []; + foreach ($data->params[1] as $index => $value) { + if (isset($value->float)) { + $params[$index] = (float)$value->float; + } elseif (isset($value->base64)) { + // base64-decode string parameters as BLOB + $params[$index] = \base64_decode($value->base64); } else { - foreach ($data->params[1] as $index => $value) { - if ($value === null) { - $type = \SQLITE3_NULL; - } elseif ($value === true || $value === false) { - // explicitly cast bool to int because SQLite does not have a native boolean - $type = \SQLITE3_INTEGER; - $value = (int)$value; - } elseif (\is_int($value)) { - $type = \SQLITE3_INTEGER; - } elseif (isset($value->float)) { - $type = \SQLITE3_FLOAT; - $value = (float)$value->float; - } elseif (isset($value->base64)) { - // base64-decode string parameters as BLOB - $type = \SQLITE3_BLOB; - $value = \base64_decode($value->base64); - } else { - $type = \SQLITE3_TEXT; - } - - $statement->bindValue( - \is_int($index) ? $index + 1 : $index, - $value, - $type - ); - } - $result = @$statement->execute(); + $params[$index] = $value; } } - if ($result === false) { - $out->write(array( - 'id' => $data->id, - 'error' => array('message' => $db->lastErrorMsg()) - )); - } else { - if ($result->numColumns() !== 0) { - // Fetch all rows only if this result set has any columns. - // INSERT/UPDATE/DELETE etc. do not return any columns, trying - // to fetch the results here will issue the same query again. - $rows = $columns = []; - for ($i = 0, $n = $result->numColumns(); $i < $n; ++$i) { - $columns[] = $result->columnName($i); - } - - while (($row = $result->fetchArray(\SQLITE3_ASSOC)) !== false) { + $db->query($data->params[0], $params)->then(function (Result $result) use ($data, $out) { + $rows = null; + if ($result->rows !== null) { + $rows = []; + foreach ($result->rows as $row) { // base64-encode any string that is not valid UTF-8 without control characters (BLOB) foreach ($row as &$value) { if (\is_string($value) && \preg_match('/[\x00-\x08\x11\x12\x14-\x1f\x7f]/u', $value) !== 0) { @@ -202,21 +141,23 @@ } $rows[] = $row; } - } else { - $rows = $columns = null; } - $result->finalize(); $out->write(array( 'id' => $data->id, 'result' => array( - 'columns' => $columns, + 'columns' => $result->columns, 'rows' => $rows, - 'insertId' => $db->lastInsertRowID(), - 'changed' => $db->changes() + 'insertId' => $result->insertId, + 'changed' => $result->changed ) )); - } + }, function (Exception $e) use ($data, $out) { + $out->write(array( + 'id' => $data->id, + 'error' => array('message' => $e->getMessage()) + )); + }); } elseif ($data->method === 'close' && $db !== null && \count($data->params) === 0) { // close database and remove reference $db->close(); diff --git a/src/Factory.php b/src/Factory.php index fa78c6b..245121e 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -2,6 +2,7 @@ namespace Clue\React\SQLite; +use Clue\React\SQLite\Io\BlockingDatabase; use Clue\React\SQLite\Io\LazyDatabase; use Clue\React\SQLite\Io\ProcessIoDatabase; use React\ChildProcess\Process; @@ -46,6 +47,18 @@ class Factory * $factory = new Clue\React\SQLite\Factory(null, '/usr/bin/php6.0'); * ``` * + * Or you may use this parameter to pass an empty PHP binary path which will + * cause this project to not spawn a PHP child process for any database + * interactions at all. In this case, using SQLite will block the main + * process, but continues to provide the exact same async API. This can be + * useful if concurrent execution is not needed, especially when running + * behind a traditional web server (non-CLI SAPI). + * + * ```php + * // advanced usage: empty binary path runs blocking SQLite in same process + * $factory = new Clue\React\SQLite\Factory(null, ''); + * ``` + * * @param ?LoopInterface $loop * @param ?string $binary */ @@ -109,6 +122,17 @@ public function __construct(LoopInterface $loop = null, $binary = null) public function open($filename, $flags = null) { $filename = $this->resolve($filename); + + if ($this->bin === '') { + try { + return \React\Promise\resolve(new BlockingDatabase($filename, $flags)); + } catch (\Exception $e) { + return \React\Promise\reject(new \RuntimeException($e->getMessage()) ); + } catch (\Error $e) { + return \React\Promise\reject(new \RuntimeException($e->getMessage())); + } + } + return $this->useSocket ? $this->openSocketIo($filename, $flags) : $this->openProcessIo($filename, $flags); } @@ -248,10 +272,7 @@ private function openProcessIo($filename, $flags = null) $process->start($this->loop); $db = new ProcessIoDatabase($process); - $args = array($filename); - if ($flags !== null) { - $args[] = $flags; - } + $args = array($filename, $flags); return $db->send('open', $args)->then(function () use ($db) { return $db; @@ -333,10 +354,7 @@ private function openSocketIo($filename, $flags = null) }); $db = new ProcessIoDatabase($process); - $args = array($filename); - if ($flags !== null) { - $args[] = $flags; - } + $args = array($filename, $flags); $db->send('open', $args)->then(function () use ($deferred, $db) { $deferred->resolve($db); diff --git a/src/Io/BlockingDatabase.php b/src/Io/BlockingDatabase.php new file mode 100644 index 0000000..b6fdd9c --- /dev/null +++ b/src/Io/BlockingDatabase.php @@ -0,0 +1,160 @@ +sqlite = new \SQLite3($filename); + } else { + $this->sqlite = new \SQLite3($filename, $flags); + } + } + + public function exec($sql) + { + if ($this->closed) { + return \React\Promise\reject(new \RuntimeException('Database closed')); + } + + // execute statement and suppress PHP warnings + $ret = @$this->sqlite->exec($sql); + + if ($ret === false) { + return \React\Promise\reject(new \RuntimeException( + $this->sqlite->lastErrorMsg() + )); + } + + $result = new Result(); + $result->changed = $this->sqlite->changes(); + $result->insertId = $this->sqlite->lastInsertRowID(); + + return \React\Promise\resolve($result); + } + + public function query($sql, array $params = array()) + { + if ($this->closed) { + return \React\Promise\reject(new \RuntimeException('Database closed')); + } + + // execute statement and suppress PHP warnings + if ($params === []) { + $result = @$this->sqlite->query($sql); + } else { + $statement = @$this->sqlite->prepare($sql); + if ($statement === false) { + $result = false; + } else { + assert($statement instanceof \SQLite3Stmt); + foreach ($params as $index => $value) { + if ($value === null) { + $type = \SQLITE3_NULL; + } elseif ($value === true || $value === false) { + // explicitly cast bool to int because SQLite does not have a native boolean + $type = \SQLITE3_INTEGER; + $value = (int) $value; + } elseif (\is_int($value)) { + $type = \SQLITE3_INTEGER; + } elseif (\is_float($value)) { + $type = \SQLITE3_FLOAT; + } elseif (\preg_match('/[\x00-\x08\x11\x12\x14-\x1f\x7f]/u', $value) !== 0) { + $type = \SQLITE3_BLOB; + } else { + $type = \SQLITE3_TEXT; + } + + $statement->bindValue( + \is_int($index) ? $index + 1 : $index, + $value, + $type + ); + } + $result = @$statement->execute(); + } + } + + if ($result === false) { + return \React\Promise\reject(new \RuntimeException( + $this->sqlite->lastErrorMsg() + )); + } + + assert($result instanceof \SQLite3Result); + if ($result->numColumns() !== 0) { + // Fetch all rows only if this result set has any columns. + // INSERT/UPDATE/DELETE etc. do not return any columns, trying + // to fetch the results here will issue the same query again. + $rows = $columns = []; + for ($i = 0, $n = $result->numColumns(); $i < $n; ++$i) { + $columns[] = $result->columnName($i); + } + + while (($row = $result->fetchArray(\SQLITE3_ASSOC)) !== false) { + $rows[] = $row; + } + } else { + $rows = $columns = null; + } + $result->finalize(); + + $result = new Result(); + $result->changed = $this->sqlite->changes(); + $result->insertId = $this->sqlite->lastInsertRowID(); + $result->columns = $columns; + $result->rows = $rows; + + return \React\Promise\resolve($result); + } + + public function quit() + { + if ($this->closed) { + return \React\Promise\reject(new \RuntimeException('Database closed')); + } + + $this->close(); + + return \React\Promise\resolve(); + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->sqlite->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } +} diff --git a/tests/FunctionalDatabaseTest.php b/tests/FunctionalDatabaseTest.php index f40f4a3..8eedc2d 100644 --- a/tests/FunctionalDatabaseTest.php +++ b/tests/FunctionalDatabaseTest.php @@ -10,7 +10,7 @@ class FunctionalDatabaseTest extends TestCase { - public function provideSocketFlags() + public function provideSocketFlag() { if (DIRECTORY_SEPARATOR === '\\') { return [[true]]; @@ -19,17 +19,43 @@ public function provideSocketFlags() } } + public function providePhpBinaryAndSocketFlag() + { + return array_merge([ + [ + null, + null + ], + [ + '', + null + ], + [ + null, + true + ] + ], DIRECTORY_SEPARATOR === '\\' ? [] : [ + [ + null, + false + ] + ]); + } + /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testOpenMemoryDatabaseResolvesWithDatabaseAndRunsUntilClose($flag) + public function testOpenMemoryDatabaseResolvesWithDatabaseAndRunsUntilClose($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -45,16 +71,19 @@ public function testOpenMemoryDatabaseResolvesWithDatabaseAndRunsUntilClose($fla } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testOpenMemoryDatabaseResolvesWithDatabaseAndRunsUntilQuit($flag) + public function testOpenMemoryDatabaseResolvesWithDatabaseAndRunsUntilQuit($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -110,16 +139,19 @@ public function testOpenMemoryDatabaseShouldNotInheritActiveFileDescriptors() } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testOpenInvalidPathRejects($flag) + public function testOpenInvalidPathRejects($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open('/dev/foo/bar'); @@ -132,16 +164,19 @@ public function testOpenInvalidPathRejects($flag) } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testOpenInvalidPathWithNullByteRejects($flag) + public function testOpenInvalidPathWithNullByteRejects($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open("test\0.db"); @@ -154,16 +189,19 @@ public function testOpenInvalidPathWithNullByteRejects($flag) } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testOpenInvalidFlagsRejects($flag) + public function testOpenInvalidFlagsRejects($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open('::memory::', SQLITE3_OPEN_READONLY); @@ -176,16 +214,19 @@ public function testOpenInvalidFlagsRejects($flag) } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testQuitResolvesAndRunsUntilQuit($flag) + public function testQuitResolvesAndRunsUntilQuit($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -198,21 +239,24 @@ public function testQuitResolvesAndRunsUntilQuit($flag) } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testQuitResolvesAndRunsUntilQuitWhenParentHasManyFileDescriptors($flag) + public function testQuitResolvesAndRunsUntilQuitWhenParentHasManyFileDescriptors($php, $useSocket) { $servers = array(); for ($i = 0; $i < 100; ++$i) { $servers[] = stream_socket_server('tcp://127.0.0.1:0'); } - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -229,16 +273,19 @@ public function testQuitResolvesAndRunsUntilQuitWhenParentHasManyFileDescriptors } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testQuitTwiceWillRejectSecondCall($flag) + public function testQuitTwiceWillRejectSecondCall($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -252,16 +299,19 @@ public function testQuitTwiceWillRejectSecondCall($flag) } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testQueryIntegerResolvesWithResultWithTypeIntegerAndRunsUntilQuit($flag) + public function testQueryIntegerResolvesWithResultWithTypeIntegerAndRunsUntilQuit($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -280,16 +330,19 @@ public function testQueryIntegerResolvesWithResultWithTypeIntegerAndRunsUntilQui } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testQueryStringResolvesWithResultWithTypeStringAndRunsUntilQuit($flag) + public function testQueryStringResolvesWithResultWithTypeStringAndRunsUntilQuit($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -308,16 +361,19 @@ public function testQueryStringResolvesWithResultWithTypeStringAndRunsUntilQuit( } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testQueryInvalidTableRejectsWithExceptionAndRunsUntilQuit($flag) + public function testQueryInvalidTableRejectsWithExceptionAndRunsUntilQuit($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -336,16 +392,19 @@ public function testQueryInvalidTableRejectsWithExceptionAndRunsUntilQuit($flag) } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testQueryInvalidTableWithPlaceholderRejectsWithExceptionAndRunsUntilQuit($flag) + public function testQueryInvalidTableWithPlaceholderRejectsWithExceptionAndRunsUntilQuit($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -535,16 +594,19 @@ public function testQueryValuePlaceholderNamedResolvesWithResultWithOtherTypeAnd } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testQueryRejectsWhenQueryIsInvalid($flag) + public function testQueryRejectsWhenQueryIsInvalid($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -559,16 +621,16 @@ public function testQueryRejectsWhenQueryIsInvalid($flag) } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider provideSocketFlag + * @param bool $useSocket */ - public function testQueryRejectsWhenClosedImmediately($flag) + public function testQueryRejectsWhenClosedImmediately($useSocket) { $factory = new Factory(); $ref = new \ReflectionProperty($factory, 'useSocket'); $ref->setAccessible(true); - $ref->setValue($factory, $flag); + $ref->setValue($factory, $useSocket); $promise = $factory->open(':memory:'); @@ -583,16 +645,19 @@ public function testQueryRejectsWhenClosedImmediately($flag) } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testExecCreateTableResolvesWithResultWithoutRows($flag) + public function testExecCreateTableResolvesWithResultWithoutRows($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -611,16 +676,19 @@ public function testExecCreateTableResolvesWithResultWithoutRows($flag) } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testExecRejectsWhenClosedImmediately($flag) + public function testExecRejectsWhenClosedImmediately($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -635,16 +703,19 @@ public function testExecRejectsWhenClosedImmediately($flag) } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testExecRejectsWhenAlreadyClosed($flag) + public function testExecRejectsWhenAlreadyClosed($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -658,16 +729,19 @@ public function testExecRejectsWhenAlreadyClosed($flag) } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testQueryInsertResolvesWithEmptyResultSetWithLastInsertIdAndRunsUntilQuit($flag) + public function testQueryInsertResolvesWithEmptyResultSetWithLastInsertIdAndRunsUntilQuit($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); @@ -690,16 +764,19 @@ public function testQueryInsertResolvesWithEmptyResultSetWithLastInsertIdAndRuns } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider providePhpBinaryAndSocketFlag + * @param ?string $php + * @param ?bool $useSocket */ - public function testQuerySelectEmptyResolvesWithEmptyResultSetWithColumnsAndNoRowsAndRunsUntilQuit($flag) + public function testQuerySelectEmptyResolvesWithEmptyResultSetWithColumnsAndNoRowsAndRunsUntilQuit($php, $useSocket) { - $factory = new Factory(); + $factory = new Factory(null, $php); - $ref = new \ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + if ($useSocket !== null) { + $ref = new \ReflectionProperty($factory, 'useSocket'); + $ref->setAccessible(true); + $ref->setValue($factory, $useSocket); + } $promise = $factory->open(':memory:'); diff --git a/tests/Io/BlockingDatabaseTest.php b/tests/Io/BlockingDatabaseTest.php new file mode 100644 index 0000000..66d8020 --- /dev/null +++ b/tests/Io/BlockingDatabaseTest.php @@ -0,0 +1,252 @@ +expectException('Exception'); + } else { + $this->setExpectedException('Exception'); + } + new BlockingDatabase('/dev/foobar'); + } + + public function testExecReturnsRejectedPromiseForInvalidQuery() + { + $db = new BlockingDatabase(':memory:'); + + $promise = $db->exec('FOO-BAR'); + + $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + } + + public function testExecAfterCloseReturnsRejectedPromise() + { + $db = new BlockingDatabase(':memory:'); + + $db->close(); + $promise = $db->exec('CREATE TABLE foo (bar string)'); + + $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + } + + public function testExecReturnsFulfilledPromiseWithEmptyResultFromCreateTableStatement() + { + $db = new BlockingDatabase(':memory:'); + + $promise = $db->exec('CREATE TABLE foo (bar string)'); + + $result = new Result(); + $promise->then($this->expectCallableOnceWith($result)); + } + + public function testQueryReturnsRejectedPromiseForInvalidQuery() + { + $db = new BlockingDatabase(':memory:'); + + $promise = $db->query('FOO-BAR'); + + $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + } + + public function testQueryReturnsRejectedPromiseForWriteQueryToReadonlyDatabase() + { + $db = new BlockingDatabase(':memory:', SQLITE3_OPEN_READONLY); + + $promise = $db->query('CREATE TABLE foo (bar string)'); + + $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + } + + public function testQueryReturnsRejectedPromiseForSelectFromUnknownTableWithPlaceholder() + { + $db = new BlockingDatabase(':memory:', SQLITE3_OPEN_READONLY); + + $promise = $db->query('SELECT ? FROM unknown', [42]); + + $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + } + + public function testQueryAfterCloseReturnsRejectedPromise() + { + $db = new BlockingDatabase(':memory:'); + + $db->close(); + $promise = $db->query('CREATE TABLE foo (bar string)'); + + $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + } + + public function testQueryReturnsFulfilledPromiseWithIntegerResult() + { + $db = new BlockingDatabase(':memory:', SQLITE3_OPEN_READONLY); + + $promise = $db->query('SELECT 1 AS value'); + + $result = new Result(); + $result->columns = ['value']; + $result->rows = [ + [ + 'value' => 1 + ] + ]; + $promise->then($this->expectCallableOnceWith($result)); + } + + public function provideDataWillBeReturnedWithType() + { + return [ + [0, 'INTEGER'], + [1, 'INTEGER'], + [1.5, 'REAL'], + [1.0, 'REAL'], + [null, 'NULL'], + ['hello', 'TEXT'], + ['hellö', 'TEXT'], + ["hello\tworld\r\n", 'TEXT'], + [utf8_decode('hello wörld!'), 'BLOB'], + ["hello\x7fö", 'BLOB'], + ["\x03\x02\x001", 'BLOB'], + ["a\000b", 'BLOB'] + ]; + } + + /** + * @dataProvider provideDataWillBeReturnedWithType + * @param mixed $value + * @param string $type + */ + public function testQueryReturnsFulfilledPromiseWithResultFromPlaceholder($value, $type) + { + $db = new BlockingDatabase(':memory:', SQLITE3_OPEN_READONLY); + + $promise = $db->query('SELECT ? AS value, UPPER(TYPEOF(?)) AS type', [$value, $value]); + + $result = new Result(); + $result->columns = ['value', 'type']; + $result->rows = [ + [ + 'value' => $value, + 'type' => $type + ] + ]; + $promise->then($this->expectCallableOnceWith($result)); + } + + public function provideDataWillBeReturnedWithOtherType() + { + return [ + [true, 1], + [false, 0], + ]; + } + + /** + * @dataProvider provideDataWillBeReturnedWithOtherType + * @param mixed $value + * @param mixed $expected + */ + public function testQueryReturnsFulfilledPromiseWithResultFromPlaceholderCasted($value, $expected) + { + $db = new BlockingDatabase(':memory:', SQLITE3_OPEN_READONLY); + + $promise = $db->query('SELECT ? AS value', [$value]); + + $result = new Result(); + $result->columns = ['value']; + $result->rows = [ + [ + 'value' => $expected + ] + ]; + $promise->then($this->expectCallableOnceWith($result)); + } + + public function testQueryReturnsFulfilledPromiseWithEmptyResultFromCreateTableStatement() + { + $db = new BlockingDatabase(':memory:'); + + $promise = $db->query('CREATE TABLE foo (bar string)'); + + $result = new Result(); + $promise->then($this->expectCallableOnceWith($result)); + } + + public function testQuitReturnsFulfilledPromiseAndEmitsCloseEvent() + { + $db = new BlockingDatabase(':memory:'); + $db->on('close', $this->expectCallableOnce()); + + $promise = $db->quit(); + $promise->then($this->expectCallableOnce()); + } + + public function testQuitAfterCloseReturnsRejectedPromiseAndDoesNotEmitCloseEvent() + { + $db = new BlockingDatabase(':memory:'); + $db->close(); + + $db->on('close', $this->expectCallableNever()); + + $promise = $db->quit(); + $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + } + + public function testCloseEmitsCloseEvent() + { + $db = new BlockingDatabase(':memory:'); + $db->on('close', $this->expectCallableOnce()); + + $db->close(); + } + + public function testCloseTwiceEmitsCloseEventOnce() + { + $db = new BlockingDatabase(':memory:'); + $db->on('close', $this->expectCallableOnce()); + + $db->close(); + $db->close(); + } + + protected function expectCallableNever() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableOnce() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableOnceWith($value) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($value); + + return $mock; + } + + protected function createCallableMock() + { + return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); + } +}