diff --git a/README.md b/README.md index d73c548..365d507 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,17 @@ query like this: $db->query('SELECT * FROM user WHERE id > :id', ['id' => $id]); ``` +All placeholder values will automatically be mapped to the native SQLite +datatypes and all result values will automatically be mapped to the +native PHP datatypes. This conversion supports `int`, `float`, `string` +(text) and `null`. SQLite does not have a native boolean type, so `true` +and `false` will be mapped to integer values `1` and `0` respectively. + +> Legacy PHP: Note that on legacy PHP < 5.6.6, a `float` without a + fraction (such as `1.0`) may end up as an `integer` instead. You're + highly recommended to use a supported PHP version or you may have to + use explicit SQL casts to work around this. + #### quit() The `quit(): PromiseInterface` method can be used to diff --git a/res/sqlite-worker.php b/res/sqlite-worker.php index 3fafe07..6cd1284 100644 --- a/res/sqlite-worker.php +++ b/res/sqlite-worker.php @@ -43,11 +43,11 @@ }); $in = new Decoder($through); - $out = new Encoder($stream); + $out = new Encoder($stream, (\PHP_VERSION_ID >= 50606 ? JSON_PRESERVE_ZERO_FRACTION : 0)); } else { // no socket address given, use process I/O pipes $in = new Decoder(new ReadableResourceStream(\STDIN, $loop)); - $out = new Encoder(new WritableResourceStream(\STDOUT, $loop)); + $out = new Encoder(new WritableResourceStream(\STDOUT, $loop), (\PHP_VERSION_ID >= 50606 ? JSON_PRESERVE_ZERO_FRACTION : 0)); } // report error when input is invalid NDJSON @@ -134,10 +134,24 @@ } else { $statement = $db->prepare($data->params[0]); 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 (\is_float($value)) { + $type = \SQLITE3_FLOAT; + } else { + $type = \SQLITE3_TEXT; + } + $statement->bindValue( $index + 1, $value, - $value === null ? \SQLITE3_NULL : \is_int($value) ? \SQLITE3_INTEGER : \SQLITE3_TEXT + $type ); } $result = @$statement->execute(); @@ -173,10 +187,24 @@ } elseif ($data->method === 'query' && $db !== null && \count($data->params) === 2 && \is_string($data->params[0]) && \is_object($data->params[1])) { $statement = $db->prepare($data->params[0]); 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 (\is_float($value)) { + $type = \SQLITE3_FLOAT; + } else { + $type = \SQLITE3_TEXT; + } + $statement->bindValue( $index, $value, - $value === null ? \SQLITE3_NULL : \is_int($value) ? \SQLITE3_INTEGER : \SQLITE3_TEXT + $type ); } $result = @$statement->execute(); diff --git a/src/DatabaseInterface.php b/src/DatabaseInterface.php index 0325bdd..9413d9a 100644 --- a/src/DatabaseInterface.php +++ b/src/DatabaseInterface.php @@ -136,6 +136,17 @@ public function exec($sql); * $db->query('SELECT * FROM user WHERE id > :id', ['id' => $id]); * ``` * + * All placeholder values will automatically be mapped to the native SQLite + * datatypes and all result values will automatically be mapped to the + * native PHP datatypes. This conversion supports `int`, `float`, `string` + * (text) and `null`. SQLite does not have a native boolean type, so `true` + * and `false` will be mapped to integer values `1` and `0` respectively. + * + * > Legacy PHP: Note that on legacy PHP < 5.6.6, a `float` without a + * fraction (such as `1.0`) may end up as an `integer` instead. You're + * highly recommended to use a supported PHP version or you may have to + * use explicit SQL casts to work around this. + * * @param string $sql SQL statement * @param array $params Parameters which should be bound to query * @return PromiseInterface Resolves with Result instance or rejects with Exception diff --git a/src/Io/ProcessIoDatabase.php b/src/Io/ProcessIoDatabase.php index 70d2329..da826e1 100644 --- a/src/Io/ProcessIoDatabase.php +++ b/src/Io/ProcessIoDatabase.php @@ -133,7 +133,7 @@ public function send($method, array $params) 'id' => $id, 'method' => $method, 'params' => $params - ), \JSON_UNESCAPED_SLASHES) . "\n"); + ), \JSON_UNESCAPED_SLASHES | (\PHP_VERSION_ID >= 50606 ? JSON_PRESERVE_ZERO_FRACTION : 0)) . "\n"); $deferred = new Deferred(); $this->pending[$id] = $deferred; diff --git a/tests/FunctionalDatabaseTest.php b/tests/FunctionalDatabaseTest.php index 04a7a4f..00e1eff 100644 --- a/tests/FunctionalDatabaseTest.php +++ b/tests/FunctionalDatabaseTest.php @@ -279,24 +279,44 @@ public function testQueryStringResolvesWithResultWithTypeStringAndRunsUntilQuit( $this->assertSame(array(array('value' => 'hellö')), $data); } + public function provideSqlDataWillBeReturnedWithType() + { + return array_merge( + [ + ['42', 42], + ['2.5', 2.5], + ['null', null], + ['"hello"', 'hello'], + ['"hellö"', 'hellö'] + ], + (PHP_VERSION_ID < 50606) ? [] : [ + // preserving zero fractions is only supported as of PHP 5.6.6 + ['1.0', 1.0] + ], + (SQLite3::version()['versionNumber'] < 3023000) ? [] : [ + // boolean identifiers exist only as of SQLite 3.23.0 (2018-04-02) + // @link https://www.sqlite.org/lang_expr.html#booleanexpr + ['true', 1], + ['false', 0] + ] + ); + } + /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider provideSqlDataWillBeReturnedWithType + * @param mixed $value + * @param mixed $expected */ - public function testQueryIntegerPlaceholderPositionalResolvesWithResultWithTypeIntegerAndRunsUntilQuit($flag) + public function testQueryValueInStatementResolvesWithResultWithTypeAndRunsUntilQuit($value, $expected) { $loop = React\EventLoop\Factory::create(); $factory = new Factory($loop); - $ref = new ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); - $promise = $factory->open(':memory:'); $data = null; - $promise->then(function (DatabaseInterface $db) use (&$data){ - $db->query('SELECT ? AS value', array(1))->then(function (Result $result) use (&$data) { + $promise->then(function (DatabaseInterface $db) use (&$data, $value){ + $db->query('SELECT ' . $value . ' AS value')->then(function (Result $result) use (&$data) { $data = $result->rows; }); @@ -305,27 +325,66 @@ public function testQueryIntegerPlaceholderPositionalResolvesWithResultWithTypeI $loop->run(); - $this->assertSame(array(array('value' => 1)), $data); + $this->assertSame(array(array('value' => $expected)), $data); + } + + public function provideDataWillBeReturnedWithType() + { + return array_merge( + [ + [0], + [1], + [1.5], + [null], + ['hello'], + ['hellö'] + ], + (PHP_VERSION_ID < 50606) ? [] : [ + // preserving zero fractions is only supported as of PHP 5.6.6 + [1.0] + ] + ); } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider provideDataWillBeReturnedWithType + * @param mixed $value */ - public function testQueryIntegerPlaceholderNamedResolvesWithResultWithTypeIntegerAndRunsUntilQuit($flag) + public function testQueryValuePlaceholderPositionalResolvesWithResultWithExactTypeAndRunsUntilQuit($value) { $loop = React\EventLoop\Factory::create(); $factory = new Factory($loop); - $ref = new ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + $promise = $factory->open(':memory:'); + + $data = null; + $promise->then(function (DatabaseInterface $db) use (&$data, $value){ + $db->query('SELECT ? AS value', array($value))->then(function (Result $result) use (&$data) { + $data = $result->rows; + }); + + $db->quit(); + }); + + $loop->run(); + + $this->assertSame(array(array('value' => $value)), $data); + } + + /** + * @dataProvider provideDataWillBeReturnedWithType + * @param mixed $value + */ + public function testQueryValuePlaceholderNamedResolvesWithResultWithExactTypeAndRunsUntilQuit($value) + { + $loop = React\EventLoop\Factory::create(); + $factory = new Factory($loop); $promise = $factory->open(':memory:'); $data = null; - $promise->then(function (DatabaseInterface $db) use (&$data){ - $db->query('SELECT :value AS value', array('value' => 1))->then(function (Result $result) use (&$data) { + $promise->then(function (DatabaseInterface $db) use (&$data, $value){ + $db->query('SELECT :value AS value', array('value' => $value))->then(function (Result $result) use (&$data) { $data = $result->rows; }); @@ -334,27 +393,64 @@ public function testQueryIntegerPlaceholderNamedResolvesWithResultWithTypeIntege $loop->run(); - $this->assertSame(array(array('value' => 1)), $data); + $this->assertSame(array(array('value' => $value)), $data); + } + + public function provideDataWillBeReturnedWithOtherType() + { + return array_merge( + [ + [true, 1], + [false, 0], + ], + (PHP_VERSION_ID >= 50606) ? [] : [ + // preserving zero fractions is supported as of PHP 5.6.6, otherwise cast to int + [1.0, 1] + ] + ); } /** - * @dataProvider provideSocketFlags - * @param bool $flag + * @dataProvider provideDataWillBeReturnedWithOtherType + * @param mixed $value + * @param mixed $expected */ - public function testQueryNullPlaceholderPositionalResolvesWithResultWithTypeNullAndRunsUntilQuit($flag) + public function testQueryValuePlaceholderPositionalResolvesWithResultWithOtherTypeAndRunsUntilQuit($value, $expected) { $loop = React\EventLoop\Factory::create(); $factory = new Factory($loop); - $ref = new ReflectionProperty($factory, 'useSocket'); - $ref->setAccessible(true); - $ref->setValue($factory, $flag); + $promise = $factory->open(':memory:'); + + $data = null; + $promise->then(function (DatabaseInterface $db) use (&$data, $value){ + $db->query('SELECT ? AS value', array($value))->then(function (Result $result) use (&$data) { + $data = $result->rows; + }); + + $db->quit(); + }); + + $loop->run(); + + $this->assertSame(array(array('value' => $expected)), $data); + } + + /** + * @dataProvider provideDataWillBeReturnedWithOtherType + * @param mixed $value + * @param mixed $expected + */ + public function testQueryValuePlaceholderNamedResolvesWithResultWithOtherTypeAndRunsUntilQuit($value, $expected) + { + $loop = React\EventLoop\Factory::create(); + $factory = new Factory($loop); $promise = $factory->open(':memory:'); $data = null; - $promise->then(function (DatabaseInterface $db) use (&$data){ - $db->query('SELECT ? AS value', array(null))->then(function (Result $result) use (&$data) { + $promise->then(function (DatabaseInterface $db) use (&$data, $value){ + $db->query('SELECT :value AS value', array('value' => $value))->then(function (Result $result) use (&$data) { $data = $result->rows; }); @@ -363,7 +459,7 @@ public function testQueryNullPlaceholderPositionalResolvesWithResultWithTypeNull $loop->run(); - $this->assertSame(array(array('value' => null)), $data); + $this->assertSame(array(array('value' => $expected)), $data); } /**