Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 13 additions & 55 deletions res/sqlite-worker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -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
);
Expand All @@ -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();
Expand Down
7 changes: 5 additions & 2 deletions src/DatabaseInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion src/Io/ProcessIoDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down
35 changes: 21 additions & 14 deletions tests/FunctionalDatabaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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']
]
);
}
Expand All @@ -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);
Expand All @@ -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;
});

Expand All @@ -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);
Expand All @@ -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;
});

Expand All @@ -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()
Expand Down