Skip to content

Commit 6f70959

Browse files
authored
Merge pull request #14 from clue-labs/types
Support type mapping for float and boolean values
2 parents 58216c5 + 1141a46 commit 6f70959

File tree

5 files changed

+179
-33
lines changed

5 files changed

+179
-33
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,17 @@ query like this:
202202
$db->query('SELECT * FROM user WHERE id > :id', ['id' => $id]);
203203
```
204204

205+
All placeholder values will automatically be mapped to the native SQLite
206+
datatypes and all result values will automatically be mapped to the
207+
native PHP datatypes. This conversion supports `int`, `float`, `string`
208+
(text) and `null`. SQLite does not have a native boolean type, so `true`
209+
and `false` will be mapped to integer values `1` and `0` respectively.
210+
211+
> Legacy PHP: Note that on legacy PHP < 5.6.6, a `float` without a
212+
fraction (such as `1.0`) may end up as an `integer` instead. You're
213+
highly recommended to use a supported PHP version or you may have to
214+
use explicit SQL casts to work around this.
215+
205216
#### quit()
206217

207218
The `quit(): PromiseInterface<void, Exception>` method can be used to

res/sqlite-worker.php

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@
4343
});
4444

4545
$in = new Decoder($through);
46-
$out = new Encoder($stream);
46+
$out = new Encoder($stream, (\PHP_VERSION_ID >= 50606 ? JSON_PRESERVE_ZERO_FRACTION : 0));
4747
} else {
4848
// no socket address given, use process I/O pipes
4949
$in = new Decoder(new ReadableResourceStream(\STDIN, $loop));
50-
$out = new Encoder(new WritableResourceStream(\STDOUT, $loop));
50+
$out = new Encoder(new WritableResourceStream(\STDOUT, $loop), (\PHP_VERSION_ID >= 50606 ? JSON_PRESERVE_ZERO_FRACTION : 0));
5151
}
5252

5353
// report error when input is invalid NDJSON
@@ -134,10 +134,24 @@
134134
} else {
135135
$statement = $db->prepare($data->params[0]);
136136
foreach ($data->params[1] as $index => $value) {
137+
if ($value === null) {
138+
$type = \SQLITE3_NULL;
139+
} elseif ($value === true || $value === false) {
140+
// explicitly cast bool to int because SQLite does not have a native boolean
141+
$type = \SQLITE3_INTEGER;
142+
$value = (int)$value;
143+
} elseif (\is_int($value)) {
144+
$type = \SQLITE3_INTEGER;
145+
} elseif (\is_float($value)) {
146+
$type = \SQLITE3_FLOAT;
147+
} else {
148+
$type = \SQLITE3_TEXT;
149+
}
150+
137151
$statement->bindValue(
138152
$index + 1,
139153
$value,
140-
$value === null ? \SQLITE3_NULL : \is_int($value) ? \SQLITE3_INTEGER : \SQLITE3_TEXT
154+
$type
141155
);
142156
}
143157
$result = @$statement->execute();
@@ -173,10 +187,24 @@
173187
} elseif ($data->method === 'query' && $db !== null && \count($data->params) === 2 && \is_string($data->params[0]) && \is_object($data->params[1])) {
174188
$statement = $db->prepare($data->params[0]);
175189
foreach ($data->params[1] as $index => $value) {
190+
if ($value === null) {
191+
$type = \SQLITE3_NULL;
192+
} elseif ($value === true || $value === false) {
193+
// explicitly cast bool to int because SQLite does not have a native boolean
194+
$type = \SQLITE3_INTEGER;
195+
$value = (int)$value;
196+
} elseif (\is_int($value)) {
197+
$type = \SQLITE3_INTEGER;
198+
} elseif (\is_float($value)) {
199+
$type = \SQLITE3_FLOAT;
200+
} else {
201+
$type = \SQLITE3_TEXT;
202+
}
203+
176204
$statement->bindValue(
177205
$index,
178206
$value,
179-
$value === null ? \SQLITE3_NULL : \is_int($value) ? \SQLITE3_INTEGER : \SQLITE3_TEXT
207+
$type
180208
);
181209
}
182210
$result = @$statement->execute();

src/DatabaseInterface.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,17 @@ public function exec($sql);
136136
* $db->query('SELECT * FROM user WHERE id > :id', ['id' => $id]);
137137
* ```
138138
*
139+
* All placeholder values will automatically be mapped to the native SQLite
140+
* datatypes and all result values will automatically be mapped to the
141+
* native PHP datatypes. This conversion supports `int`, `float`, `string`
142+
* (text) and `null`. SQLite does not have a native boolean type, so `true`
143+
* and `false` will be mapped to integer values `1` and `0` respectively.
144+
*
145+
* > Legacy PHP: Note that on legacy PHP < 5.6.6, a `float` without a
146+
* fraction (such as `1.0`) may end up as an `integer` instead. You're
147+
* highly recommended to use a supported PHP version or you may have to
148+
* use explicit SQL casts to work around this.
149+
*
139150
* @param string $sql SQL statement
140151
* @param array $params Parameters which should be bound to query
141152
* @return PromiseInterface<Result> Resolves with Result instance or rejects with Exception

src/Io/ProcessIoDatabase.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public function send($method, array $params)
133133
'id' => $id,
134134
'method' => $method,
135135
'params' => $params
136-
), \JSON_UNESCAPED_SLASHES) . "\n");
136+
), \JSON_UNESCAPED_SLASHES | (\PHP_VERSION_ID >= 50606 ? JSON_PRESERVE_ZERO_FRACTION : 0)) . "\n");
137137

138138
$deferred = new Deferred();
139139
$this->pending[$id] = $deferred;

tests/FunctionalDatabaseTest.php

Lines changed: 124 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -292,24 +292,44 @@ public function testQueryStringResolvesWithResultWithTypeStringAndRunsUntilQuit(
292292
$this->assertSame(array(array('value' => 'hellö')), $data);
293293
}
294294

295+
public function provideSqlDataWillBeReturnedWithType()
296+
{
297+
return array_merge(
298+
[
299+
['42', 42],
300+
['2.5', 2.5],
301+
['null', null],
302+
['"hello"', 'hello'],
303+
['"hellö"', 'hellö']
304+
],
305+
(PHP_VERSION_ID < 50606) ? [] : [
306+
// preserving zero fractions is only supported as of PHP 5.6.6
307+
['1.0', 1.0]
308+
],
309+
(SQLite3::version()['versionNumber'] < 3023000) ? [] : [
310+
// boolean identifiers exist only as of SQLite 3.23.0 (2018-04-02)
311+
// @link https://www.sqlite.org/lang_expr.html#booleanexpr
312+
['true', 1],
313+
['false', 0]
314+
]
315+
);
316+
}
317+
295318
/**
296-
* @dataProvider provideSocketFlags
297-
* @param bool $flag
319+
* @dataProvider provideSqlDataWillBeReturnedWithType
320+
* @param mixed $value
321+
* @param mixed $expected
298322
*/
299-
public function testQueryIntegerPlaceholderPositionalResolvesWithResultWithTypeIntegerAndRunsUntilQuit($flag)
323+
public function testQueryValueInStatementResolvesWithResultWithTypeAndRunsUntilQuit($value, $expected)
300324
{
301325
$loop = React\EventLoop\Factory::create();
302326
$factory = new Factory($loop);
303327

304-
$ref = new ReflectionProperty($factory, 'useSocket');
305-
$ref->setAccessible(true);
306-
$ref->setValue($factory, $flag);
307-
308328
$promise = $factory->open(':memory:');
309329

310330
$data = null;
311-
$promise->then(function (DatabaseInterface $db) use (&$data){
312-
$db->query('SELECT ? AS value', array(1))->then(function (Result $result) use (&$data) {
331+
$promise->then(function (DatabaseInterface $db) use (&$data, $value){
332+
$db->query('SELECT ' . $value . ' AS value')->then(function (Result $result) use (&$data) {
313333
$data = $result->rows;
314334
});
315335

@@ -318,27 +338,66 @@ public function testQueryIntegerPlaceholderPositionalResolvesWithResultWithTypeI
318338

319339
$loop->run();
320340

321-
$this->assertSame(array(array('value' => 1)), $data);
341+
$this->assertSame(array(array('value' => $expected)), $data);
342+
}
343+
344+
public function provideDataWillBeReturnedWithType()
345+
{
346+
return array_merge(
347+
[
348+
[0],
349+
[1],
350+
[1.5],
351+
[null],
352+
['hello'],
353+
['hellö']
354+
],
355+
(PHP_VERSION_ID < 50606) ? [] : [
356+
// preserving zero fractions is only supported as of PHP 5.6.6
357+
[1.0]
358+
]
359+
);
322360
}
323361

324362
/**
325-
* @dataProvider provideSocketFlags
326-
* @param bool $flag
363+
* @dataProvider provideDataWillBeReturnedWithType
364+
* @param mixed $value
327365
*/
328-
public function testQueryIntegerPlaceholderNamedResolvesWithResultWithTypeIntegerAndRunsUntilQuit($flag)
366+
public function testQueryValuePlaceholderPositionalResolvesWithResultWithExactTypeAndRunsUntilQuit($value)
329367
{
330368
$loop = React\EventLoop\Factory::create();
331369
$factory = new Factory($loop);
332370

333-
$ref = new ReflectionProperty($factory, 'useSocket');
334-
$ref->setAccessible(true);
335-
$ref->setValue($factory, $flag);
371+
$promise = $factory->open(':memory:');
372+
373+
$data = null;
374+
$promise->then(function (DatabaseInterface $db) use (&$data, $value){
375+
$db->query('SELECT ? AS value', array($value))->then(function (Result $result) use (&$data) {
376+
$data = $result->rows;
377+
});
378+
379+
$db->quit();
380+
});
381+
382+
$loop->run();
383+
384+
$this->assertSame(array(array('value' => $value)), $data);
385+
}
386+
387+
/**
388+
* @dataProvider provideDataWillBeReturnedWithType
389+
* @param mixed $value
390+
*/
391+
public function testQueryValuePlaceholderNamedResolvesWithResultWithExactTypeAndRunsUntilQuit($value)
392+
{
393+
$loop = React\EventLoop\Factory::create();
394+
$factory = new Factory($loop);
336395

337396
$promise = $factory->open(':memory:');
338397

339398
$data = null;
340-
$promise->then(function (DatabaseInterface $db) use (&$data){
341-
$db->query('SELECT :value AS value', array('value' => 1))->then(function (Result $result) use (&$data) {
399+
$promise->then(function (DatabaseInterface $db) use (&$data, $value){
400+
$db->query('SELECT :value AS value', array('value' => $value))->then(function (Result $result) use (&$data) {
342401
$data = $result->rows;
343402
});
344403

@@ -347,27 +406,64 @@ public function testQueryIntegerPlaceholderNamedResolvesWithResultWithTypeIntege
347406

348407
$loop->run();
349408

350-
$this->assertSame(array(array('value' => 1)), $data);
409+
$this->assertSame(array(array('value' => $value)), $data);
410+
}
411+
412+
public function provideDataWillBeReturnedWithOtherType()
413+
{
414+
return array_merge(
415+
[
416+
[true, 1],
417+
[false, 0],
418+
],
419+
(PHP_VERSION_ID >= 50606) ? [] : [
420+
// preserving zero fractions is supported as of PHP 5.6.6, otherwise cast to int
421+
[1.0, 1]
422+
]
423+
);
351424
}
352425

353426
/**
354-
* @dataProvider provideSocketFlags
355-
* @param bool $flag
427+
* @dataProvider provideDataWillBeReturnedWithOtherType
428+
* @param mixed $value
429+
* @param mixed $expected
356430
*/
357-
public function testQueryNullPlaceholderPositionalResolvesWithResultWithTypeNullAndRunsUntilQuit($flag)
431+
public function testQueryValuePlaceholderPositionalResolvesWithResultWithOtherTypeAndRunsUntilQuit($value, $expected)
358432
{
359433
$loop = React\EventLoop\Factory::create();
360434
$factory = new Factory($loop);
361435

362-
$ref = new ReflectionProperty($factory, 'useSocket');
363-
$ref->setAccessible(true);
364-
$ref->setValue($factory, $flag);
436+
$promise = $factory->open(':memory:');
437+
438+
$data = null;
439+
$promise->then(function (DatabaseInterface $db) use (&$data, $value){
440+
$db->query('SELECT ? AS value', array($value))->then(function (Result $result) use (&$data) {
441+
$data = $result->rows;
442+
});
443+
444+
$db->quit();
445+
});
446+
447+
$loop->run();
448+
449+
$this->assertSame(array(array('value' => $expected)), $data);
450+
}
451+
452+
/**
453+
* @dataProvider provideDataWillBeReturnedWithOtherType
454+
* @param mixed $value
455+
* @param mixed $expected
456+
*/
457+
public function testQueryValuePlaceholderNamedResolvesWithResultWithOtherTypeAndRunsUntilQuit($value, $expected)
458+
{
459+
$loop = React\EventLoop\Factory::create();
460+
$factory = new Factory($loop);
365461

366462
$promise = $factory->open(':memory:');
367463

368464
$data = null;
369-
$promise->then(function (DatabaseInterface $db) use (&$data){
370-
$db->query('SELECT ? AS value', array(null))->then(function (Result $result) use (&$data) {
465+
$promise->then(function (DatabaseInterface $db) use (&$data, $value){
466+
$db->query('SELECT :value AS value', array('value' => $value))->then(function (Result $result) use (&$data) {
371467
$data = $result->rows;
372468
});
373469

@@ -376,7 +472,7 @@ public function testQueryNullPlaceholderPositionalResolvesWithResultWithTypeNull
376472

377473
$loop->run();
378474

379-
$this->assertSame(array(array('value' => null)), $data);
475+
$this->assertSame(array(array('value' => $expected)), $data);
380476
}
381477

382478
/**

0 commit comments

Comments
 (0)