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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<void, Exception>` method can be used to
Expand Down
36 changes: 32 additions & 4 deletions res/sqlite-worker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
11 changes: 11 additions & 0 deletions src/DatabaseInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Result> Resolves with Result instance or rejects with Exception
Expand Down
2 changes: 1 addition & 1 deletion src/Io/ProcessIoDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
152 changes: 124 additions & 28 deletions tests/FunctionalDatabaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});

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

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

Expand All @@ -363,7 +459,7 @@ public function testQueryNullPlaceholderPositionalResolvesWithResultWithTypeNull

$loop->run();

$this->assertSame(array(array('value' => null)), $data);
$this->assertSame(array(array('value' => $expected)), $data);
}

/**
Expand Down