diff --git a/README.md b/README.md index 365d507..7e02dc7 100644 --- a/README.md +++ b/README.md @@ -205,8 +205,11 @@ $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. +and `null`. Any `string` that is valid UTF-8 without any control +characters will be mapped to `TEXT`, binary strings will be mapped to +`BLOB`. Both `TEXT` and `BLOB` will always be mapped to `string` . 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 diff --git a/res/sqlite-worker.php b/res/sqlite-worker.php index 6cd1284..e900f20 100644 --- a/res/sqlite-worker.php +++ b/res/sqlite-worker.php @@ -127,9 +127,9 @@ ) )); } - } elseif ($data->method === 'query' && $db !== null && \count($data->params) === 2 && \is_string($data->params[0]) && \is_array($data->params[1])) { + } 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 (\count($data->params[1]) === 0) { + if ($data->params[1] === []) { $result = @$db->query($data->params[0]); } else { $statement = $db->prepare($data->params[0]); @@ -144,12 +144,16 @@ $type = \SQLITE3_INTEGER; } elseif (\is_float($value)) { $type = \SQLITE3_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( - $index + 1, + \is_int($index) ? $index + 1 : $index, $value, $type ); @@ -170,58 +174,12 @@ $rows = array(); while (($row = $result->fetchArray(\SQLITE3_ASSOC)) !== false) { - $rows[] = $row; - } - $result->finalize(); - - $out->write(array( - 'id' => $data->id, - 'result' => array( - 'columns' => $columns, - 'rows' => $rows, - 'insertId' => $db->lastInsertRowID(), - 'changed' => $db->changes() - ) - )); - } - } 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, - $type - ); - } - $result = @$statement->execute(); - - if ($result === false) { - $out->write(array( - 'id' => $data->id, - 'error' => array('message' => $db->lastErrorMsg()) - )); - } else { - $columns = array(); - for ($i = 0, $n = $result->numColumns(); $i < $n; ++$i) { - $columns[] = $result->columnName($i); - } - - $rows = array(); - while (($row = $result->fetchArray(\SQLITE3_ASSOC)) !== false) { + // 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) { + $value = ['base64' => \base64_encode($value)]; + } + } $rows[] = $row; } $result->finalize(); diff --git a/src/DatabaseInterface.php b/src/DatabaseInterface.php index 9413d9a..4730c47 100644 --- a/src/DatabaseInterface.php +++ b/src/DatabaseInterface.php @@ -139,8 +139,11 @@ public function exec($sql); * 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. + * and `null`. Any `string` that is valid UTF-8 without any control + * characters will be mapped to `TEXT`, binary strings will be mapped to + * `BLOB`. Both `TEXT` and `BLOB` will always be mapped to `string` . 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 diff --git a/src/Io/ProcessIoDatabase.php b/src/Io/ProcessIoDatabase.php index da826e1..281cfa5 100644 --- a/src/Io/ProcessIoDatabase.php +++ b/src/Io/ProcessIoDatabase.php @@ -76,12 +76,29 @@ public function exec($sql) public function query($sql, array $params = array()) { + // base64-encode any string that is not valid UTF-8 without control characters (BLOB) + foreach ($params as &$value) { + if (\is_string($value) && \preg_match('/[\x00-\x08\x11\x12\x14-\x1f\x7f]/u', $value) !== 0) { + $value = ['base64' => \base64_encode($value)]; + } + } + return $this->send('query', array($sql, $params))->then(function ($data) { $result = new Result(); $result->changed = $data['changed']; $result->insertId = $data['insertId']; $result->columns = $data['columns']; - $result->rows = $data['rows']; + + // base64-decode string result values for BLOBS + $result->rows = []; + foreach ($data['rows'] as $row) { + foreach ($row as &$value) { + if (isset($value['base64'])) { + $value = \base64_decode($value['base64']); + } + } + $result->rows[] = $row; + } return $result; }); diff --git a/tests/FunctionalDatabaseTest.php b/tests/FunctionalDatabaseTest.php index baaba43..1a5ca8a 100644 --- a/tests/FunctionalDatabaseTest.php +++ b/tests/FunctionalDatabaseTest.php @@ -300,7 +300,9 @@ public function provideSqlDataWillBeReturnedWithType() ['2.5', 2.5], ['null', null], ['"hello"', 'hello'], - ['"hellö"', 'hellö'] + ['"hellö"', 'hellö'], + ['X\'01020300\'', "\x01\x02\x03\x00"], + ['X\'3FF3\'', "\x3f\xf3"] ], (PHP_VERSION_ID < 50606) ? [] : [ // preserving zero fractions is only supported as of PHP 5.6.6 @@ -345,16 +347,21 @@ public function provideDataWillBeReturnedWithType() { return array_merge( [ - [0], - [1], - [1.5], - [null], - ['hello'], - ['hellö'] + [0, 'INTEGER'], + [1, 'INTEGER'], + [1.5, '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'] ], (PHP_VERSION_ID < 50606) ? [] : [ // preserving zero fractions is only supported as of PHP 5.6.6 - [1.0] + [1.0, 'REAL'] ] ); } @@ -363,7 +370,7 @@ public function provideDataWillBeReturnedWithType() * @dataProvider provideDataWillBeReturnedWithType * @param mixed $value */ - public function testQueryValuePlaceholderPositionalResolvesWithResultWithExactTypeAndRunsUntilQuit($value) + public function testQueryValuePlaceholderPositionalResolvesWithResultWithExactTypeAndRunsUntilQuit($value, $type) { $loop = React\EventLoop\Factory::create(); $factory = new Factory($loop); @@ -372,7 +379,7 @@ public function testQueryValuePlaceholderPositionalResolvesWithResultWithExactTy $data = null; $promise->then(function (DatabaseInterface $db) use (&$data, $value){ - $db->query('SELECT ? AS value', array($value))->then(function (Result $result) use (&$data) { + $db->query('SELECT ? AS value, UPPER(TYPEOF(?)) as type', array($value, $value))->then(function (Result $result) use (&$data) { $data = $result->rows; }); @@ -381,14 +388,14 @@ public function testQueryValuePlaceholderPositionalResolvesWithResultWithExactTy $loop->run(); - $this->assertSame(array(array('value' => $value)), $data); + $this->assertSame(array(array('value' => $value, 'type' => $type)), $data); } /** * @dataProvider provideDataWillBeReturnedWithType * @param mixed $value */ - public function testQueryValuePlaceholderNamedResolvesWithResultWithExactTypeAndRunsUntilQuit($value) + public function testQueryValuePlaceholderNamedResolvesWithResultWithExactTypeAndRunsUntilQuit($value, $type) { $loop = React\EventLoop\Factory::create(); $factory = new Factory($loop); @@ -397,7 +404,7 @@ public function testQueryValuePlaceholderNamedResolvesWithResultWithExactTypeAnd $data = null; $promise->then(function (DatabaseInterface $db) use (&$data, $value){ - $db->query('SELECT :value AS value', array('value' => $value))->then(function (Result $result) use (&$data) { + $db->query('SELECT :value AS value, UPPER(TYPEOF(:value)) AS type', array('value' => $value))->then(function (Result $result) use (&$data) { $data = $result->rows; }); @@ -406,7 +413,7 @@ public function testQueryValuePlaceholderNamedResolvesWithResultWithExactTypeAnd $loop->run(); - $this->assertSame(array(array('value' => $value)), $data); + $this->assertSame(array(array('value' => $value, 'type' => $type)), $data); } public function provideDataWillBeReturnedWithOtherType()