Skip to content

Commit 5fc5120

Browse files
authored
Merge pull request #16 from clue-labs/blob
Encode valid UTF-8 strings as TEXT and binary strings as BLOB
2 parents 6f70959 + b4fadeb commit 5fc5120

File tree

5 files changed

+62
-74
lines changed

5 files changed

+62
-74
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,11 @@ $db->query('SELECT * FROM user WHERE id > :id', ['id' => $id]);
205205
All placeholder values will automatically be mapped to the native SQLite
206206
datatypes and all result values will automatically be mapped to the
207207
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.
208+
and `null`. Any `string` that is valid UTF-8 without any control
209+
characters will be mapped to `TEXT`, binary strings will be mapped to
210+
`BLOB`. Both `TEXT` and `BLOB` will always be mapped to `string` . SQLite
211+
does not have a native boolean type, so `true` and `false` will be mapped
212+
to integer values `1` and `0` respectively.
210213

211214
> Legacy PHP: Note that on legacy PHP < 5.6.6, a `float` without a
212215
fraction (such as `1.0`) may end up as an `integer` instead. You're

res/sqlite-worker.php

Lines changed: 13 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@
127127
)
128128
));
129129
}
130-
} elseif ($data->method === 'query' && $db !== null && \count($data->params) === 2 && \is_string($data->params[0]) && \is_array($data->params[1])) {
130+
} 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]))) {
131131
// execute statement and suppress PHP warnings
132-
if (\count($data->params[1]) === 0) {
132+
if ($data->params[1] === []) {
133133
$result = @$db->query($data->params[0]);
134134
} else {
135135
$statement = $db->prepare($data->params[0]);
@@ -144,12 +144,16 @@
144144
$type = \SQLITE3_INTEGER;
145145
} elseif (\is_float($value)) {
146146
$type = \SQLITE3_FLOAT;
147+
} elseif (isset($value->base64)) {
148+
// base64-decode string parameters as BLOB
149+
$type = \SQLITE3_BLOB;
150+
$value = \base64_decode($value->base64);
147151
} else {
148152
$type = \SQLITE3_TEXT;
149153
}
150154

151155
$statement->bindValue(
152-
$index + 1,
156+
\is_int($index) ? $index + 1 : $index,
153157
$value,
154158
$type
155159
);
@@ -170,58 +174,12 @@
170174

171175
$rows = array();
172176
while (($row = $result->fetchArray(\SQLITE3_ASSOC)) !== false) {
173-
$rows[] = $row;
174-
}
175-
$result->finalize();
176-
177-
$out->write(array(
178-
'id' => $data->id,
179-
'result' => array(
180-
'columns' => $columns,
181-
'rows' => $rows,
182-
'insertId' => $db->lastInsertRowID(),
183-
'changed' => $db->changes()
184-
)
185-
));
186-
}
187-
} elseif ($data->method === 'query' && $db !== null && \count($data->params) === 2 && \is_string($data->params[0]) && \is_object($data->params[1])) {
188-
$statement = $db->prepare($data->params[0]);
189-
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-
204-
$statement->bindValue(
205-
$index,
206-
$value,
207-
$type
208-
);
209-
}
210-
$result = @$statement->execute();
211-
212-
if ($result === false) {
213-
$out->write(array(
214-
'id' => $data->id,
215-
'error' => array('message' => $db->lastErrorMsg())
216-
));
217-
} else {
218-
$columns = array();
219-
for ($i = 0, $n = $result->numColumns(); $i < $n; ++$i) {
220-
$columns[] = $result->columnName($i);
221-
}
222-
223-
$rows = array();
224-
while (($row = $result->fetchArray(\SQLITE3_ASSOC)) !== false) {
177+
// base64-encode any string that is not valid UTF-8 without control characters (BLOB)
178+
foreach ($row as &$value) {
179+
if (\is_string($value) && \preg_match('/[\x00-\x08\x11\x12\x14-\x1f\x7f]/u', $value) !== 0) {
180+
$value = ['base64' => \base64_encode($value)];
181+
}
182+
}
225183
$rows[] = $row;
226184
}
227185
$result->finalize();

src/DatabaseInterface.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,11 @@ public function exec($sql);
139139
* All placeholder values will automatically be mapped to the native SQLite
140140
* datatypes and all result values will automatically be mapped to the
141141
* 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.
142+
* and `null`. Any `string` that is valid UTF-8 without any control
143+
* characters will be mapped to `TEXT`, binary strings will be mapped to
144+
* `BLOB`. Both `TEXT` and `BLOB` will always be mapped to `string` . SQLite
145+
* does not have a native boolean type, so `true` and `false` will be mapped
146+
* to integer values `1` and `0` respectively.
144147
*
145148
* > Legacy PHP: Note that on legacy PHP < 5.6.6, a `float` without a
146149
* fraction (such as `1.0`) may end up as an `integer` instead. You're

src/Io/ProcessIoDatabase.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,29 @@ public function exec($sql)
7676

7777
public function query($sql, array $params = array())
7878
{
79+
// base64-encode any string that is not valid UTF-8 without control characters (BLOB)
80+
foreach ($params as &$value) {
81+
if (\is_string($value) && \preg_match('/[\x00-\x08\x11\x12\x14-\x1f\x7f]/u', $value) !== 0) {
82+
$value = ['base64' => \base64_encode($value)];
83+
}
84+
}
85+
7986
return $this->send('query', array($sql, $params))->then(function ($data) {
8087
$result = new Result();
8188
$result->changed = $data['changed'];
8289
$result->insertId = $data['insertId'];
8390
$result->columns = $data['columns'];
84-
$result->rows = $data['rows'];
91+
92+
// base64-decode string result values for BLOBS
93+
$result->rows = [];
94+
foreach ($data['rows'] as $row) {
95+
foreach ($row as &$value) {
96+
if (isset($value['base64'])) {
97+
$value = \base64_decode($value['base64']);
98+
}
99+
}
100+
$result->rows[] = $row;
101+
}
85102

86103
return $result;
87104
});

tests/FunctionalDatabaseTest.php

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,9 @@ public function provideSqlDataWillBeReturnedWithType()
300300
['2.5', 2.5],
301301
['null', null],
302302
['"hello"', 'hello'],
303-
['"hellö"', 'hellö']
303+
['"hellö"', 'hellö'],
304+
['X\'01020300\'', "\x01\x02\x03\x00"],
305+
['X\'3FF3\'', "\x3f\xf3"]
304306
],
305307
(PHP_VERSION_ID < 50606) ? [] : [
306308
// preserving zero fractions is only supported as of PHP 5.6.6
@@ -345,16 +347,21 @@ public function provideDataWillBeReturnedWithType()
345347
{
346348
return array_merge(
347349
[
348-
[0],
349-
[1],
350-
[1.5],
351-
[null],
352-
['hello'],
353-
['hellö']
350+
[0, 'INTEGER'],
351+
[1, 'INTEGER'],
352+
[1.5, 'REAL'],
353+
[null, 'NULL'],
354+
['hello', 'TEXT'],
355+
['hellö', 'TEXT'],
356+
["hello\tworld\r\n", 'TEXT'],
357+
[utf8_decode('hello wörld!'), 'BLOB'],
358+
["hello\x7fö", 'BLOB'],
359+
["\x03\x02\x001", 'BLOB'],
360+
["a\000b", 'BLOB']
354361
],
355362
(PHP_VERSION_ID < 50606) ? [] : [
356363
// preserving zero fractions is only supported as of PHP 5.6.6
357-
[1.0]
364+
[1.0, 'REAL']
358365
]
359366
);
360367
}
@@ -363,7 +370,7 @@ public function provideDataWillBeReturnedWithType()
363370
* @dataProvider provideDataWillBeReturnedWithType
364371
* @param mixed $value
365372
*/
366-
public function testQueryValuePlaceholderPositionalResolvesWithResultWithExactTypeAndRunsUntilQuit($value)
373+
public function testQueryValuePlaceholderPositionalResolvesWithResultWithExactTypeAndRunsUntilQuit($value, $type)
367374
{
368375
$loop = React\EventLoop\Factory::create();
369376
$factory = new Factory($loop);
@@ -372,7 +379,7 @@ public function testQueryValuePlaceholderPositionalResolvesWithResultWithExactTy
372379

373380
$data = null;
374381
$promise->then(function (DatabaseInterface $db) use (&$data, $value){
375-
$db->query('SELECT ? AS value', array($value))->then(function (Result $result) use (&$data) {
382+
$db->query('SELECT ? AS value, UPPER(TYPEOF(?)) as type', array($value, $value))->then(function (Result $result) use (&$data) {
376383
$data = $result->rows;
377384
});
378385

@@ -381,14 +388,14 @@ public function testQueryValuePlaceholderPositionalResolvesWithResultWithExactTy
381388

382389
$loop->run();
383390

384-
$this->assertSame(array(array('value' => $value)), $data);
391+
$this->assertSame(array(array('value' => $value, 'type' => $type)), $data);
385392
}
386393

387394
/**
388395
* @dataProvider provideDataWillBeReturnedWithType
389396
* @param mixed $value
390397
*/
391-
public function testQueryValuePlaceholderNamedResolvesWithResultWithExactTypeAndRunsUntilQuit($value)
398+
public function testQueryValuePlaceholderNamedResolvesWithResultWithExactTypeAndRunsUntilQuit($value, $type)
392399
{
393400
$loop = React\EventLoop\Factory::create();
394401
$factory = new Factory($loop);
@@ -397,7 +404,7 @@ public function testQueryValuePlaceholderNamedResolvesWithResultWithExactTypeAnd
397404

398405
$data = null;
399406
$promise->then(function (DatabaseInterface $db) use (&$data, $value){
400-
$db->query('SELECT :value AS value', array('value' => $value))->then(function (Result $result) use (&$data) {
407+
$db->query('SELECT :value AS value, UPPER(TYPEOF(:value)) AS type', array('value' => $value))->then(function (Result $result) use (&$data) {
401408
$data = $result->rows;
402409
});
403410

@@ -406,7 +413,7 @@ public function testQueryValuePlaceholderNamedResolvesWithResultWithExactTypeAnd
406413

407414
$loop->run();
408415

409-
$this->assertSame(array(array('value' => $value)), $data);
416+
$this->assertSame(array(array('value' => $value, 'type' => $type)), $data);
410417
}
411418

412419
public function provideDataWillBeReturnedWithOtherType()

0 commit comments

Comments
 (0)