Skip to content

Commit e94c9f0

Browse files
authored
Merge pull request #48 from clue-labs/blocking
Support using blocking SQLite adapter when using an empty binary path
2 parents 9d16848 + 34d55c3 commit e94c9f0

File tree

6 files changed

+694
-234
lines changed

6 files changed

+694
-234
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,18 @@ environment than your parent process.
113113
$factory = new Clue\React\SQLite\Factory(null, '/usr/bin/php6.0');
114114
```
115115

116+
Or you may use this parameter to pass an empty PHP binary path which will
117+
cause this project to not spawn a PHP child process for any database
118+
interactions at all. In this case, using SQLite will block the main
119+
process, but continues to provide the exact same async API. This can be
120+
useful if concurrent execution is not needed, especially when running
121+
behind a traditional web server (non-CLI SAPI).
122+
123+
```php
124+
// advanced usage: empty binary path runs blocking SQLite in same process
125+
$factory = new Clue\React\SQLite\Factory(null, '');
126+
```
127+
116128
#### open()
117129

118130
The `open(string $filename, int $flags = null): PromiseInterface<DatabaseInterface>` method can be used to

res/sqlite-worker.php

Lines changed: 36 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
use Clue\React\NDJson\Decoder;
1515
use Clue\React\NDJson\Encoder;
16+
use Clue\React\SQLite\Io\BlockingDatabase;
17+
use Clue\React\SQLite\Result;
1618
use React\EventLoop\Factory;
1719
use React\Stream\DuplexResourceStream;
1820
use React\Stream\ReadableResourceStream;
@@ -74,35 +76,10 @@
7476
return;
7577
}
7678

77-
if ($data->method === 'open' && \count($data->params) === 1 && \is_string($data->params[0])) {
78-
// open database with one parameter: $filename
79-
try {
80-
$db = new SQLite3(
81-
$data->params[0]
82-
);
83-
84-
$out->write(array(
85-
'id' => $data->id,
86-
'result' => true
87-
));
88-
} catch (Exception $e) {
89-
$out->write(array(
90-
'id' => $data->id,
91-
'error' => array('message' => $e->getMessage())
92-
));
93-
} catch (Error $e) {
94-
$out->write(array(
95-
'id' => $data->id,
96-
'error' => array('message' => $e->getMessage())
97-
));
98-
}
99-
} elseif ($data->method === 'open' && \count($data->params) === 2 && \is_string($data->params[0]) && \is_int($data->params[1])) {
79+
if ($data->method === 'open' && \count($data->params) === 2 && \is_string($data->params[0]) && ($data->params[1] === null || \is_int($data->params[1]))) {
10080
// open database with two parameters: $filename, $flags
10181
try {
102-
$db = new SQLite3(
103-
$data->params[0],
104-
$data->params[1]
105-
);
82+
$db = new BlockingDatabase($data->params[0], $data->params[1]);
10683

10784
$out->write(array(
10885
'id' => $data->id,
@@ -120,78 +97,40 @@
12097
));
12198
}
12299
} elseif ($data->method === 'exec' && $db !== null && \count($data->params) === 1 && \is_string($data->params[0])) {
123-
// execute statement and suppress PHP warnings
124-
$ret = @$db->exec($data->params[0]);
125-
126-
if ($ret === false) {
100+
// execute statement: $db->exec($sql)
101+
$db->exec($data->params[0])->then(function (Result $result) use ($data, $out) {
127102
$out->write(array(
128103
'id' => $data->id,
129-
'error' => array('message' => $db->lastErrorMsg())
104+
'result' => array(
105+
'insertId' => $result->insertId,
106+
'changed' => $result->changed
107+
)
130108
));
131-
} else {
109+
}, function (Exception $e) use ($data, $out) {
132110
$out->write(array(
133111
'id' => $data->id,
134-
'result' => array(
135-
'insertId' => $db->lastInsertRowID(),
136-
'changed' => $db->changes()
137-
)
112+
'error' => array('message' => $e->getMessage())
138113
));
139-
}
114+
});
140115
} 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]))) {
141-
// execute statement and suppress PHP warnings
142-
if ($data->params[1] === []) {
143-
$result = @$db->query($data->params[0]);
144-
} else {
145-
$statement = @$db->prepare($data->params[0]);
146-
if ($statement === false) {
147-
$result = false;
116+
// execute statement: $db->query($sql, $params)
117+
$params = [];
118+
foreach ($data->params[1] as $index => $value) {
119+
if (isset($value->float)) {
120+
$params[$index] = (float)$value->float;
121+
} elseif (isset($value->base64)) {
122+
// base64-decode string parameters as BLOB
123+
$params[$index] = \base64_decode($value->base64);
148124
} else {
149-
foreach ($data->params[1] as $index => $value) {
150-
if ($value === null) {
151-
$type = \SQLITE3_NULL;
152-
} elseif ($value === true || $value === false) {
153-
// explicitly cast bool to int because SQLite does not have a native boolean
154-
$type = \SQLITE3_INTEGER;
155-
$value = (int)$value;
156-
} elseif (\is_int($value)) {
157-
$type = \SQLITE3_INTEGER;
158-
} elseif (isset($value->float)) {
159-
$type = \SQLITE3_FLOAT;
160-
$value = (float)$value->float;
161-
} elseif (isset($value->base64)) {
162-
// base64-decode string parameters as BLOB
163-
$type = \SQLITE3_BLOB;
164-
$value = \base64_decode($value->base64);
165-
} else {
166-
$type = \SQLITE3_TEXT;
167-
}
168-
169-
$statement->bindValue(
170-
\is_int($index) ? $index + 1 : $index,
171-
$value,
172-
$type
173-
);
174-
}
175-
$result = @$statement->execute();
125+
$params[$index] = $value;
176126
}
177127
}
178128

179-
if ($result === false) {
180-
$out->write(array(
181-
'id' => $data->id,
182-
'error' => array('message' => $db->lastErrorMsg())
183-
));
184-
} else {
185-
if ($result->numColumns() !== 0) {
186-
// Fetch all rows only if this result set has any columns.
187-
// INSERT/UPDATE/DELETE etc. do not return any columns, trying
188-
// to fetch the results here will issue the same query again.
189-
$rows = $columns = [];
190-
for ($i = 0, $n = $result->numColumns(); $i < $n; ++$i) {
191-
$columns[] = $result->columnName($i);
192-
}
193-
194-
while (($row = $result->fetchArray(\SQLITE3_ASSOC)) !== false) {
129+
$db->query($data->params[0], $params)->then(function (Result $result) use ($data, $out) {
130+
$rows = null;
131+
if ($result->rows !== null) {
132+
$rows = [];
133+
foreach ($result->rows as $row) {
195134
// base64-encode any string that is not valid UTF-8 without control characters (BLOB)
196135
foreach ($row as &$value) {
197136
if (\is_string($value) && \preg_match('/[\x00-\x08\x11\x12\x14-\x1f\x7f]/u', $value) !== 0) {
@@ -202,21 +141,23 @@
202141
}
203142
$rows[] = $row;
204143
}
205-
} else {
206-
$rows = $columns = null;
207144
}
208-
$result->finalize();
209145

210146
$out->write(array(
211147
'id' => $data->id,
212148
'result' => array(
213-
'columns' => $columns,
149+
'columns' => $result->columns,
214150
'rows' => $rows,
215-
'insertId' => $db->lastInsertRowID(),
216-
'changed' => $db->changes()
151+
'insertId' => $result->insertId,
152+
'changed' => $result->changed
217153
)
218154
));
219-
}
155+
}, function (Exception $e) use ($data, $out) {
156+
$out->write(array(
157+
'id' => $data->id,
158+
'error' => array('message' => $e->getMessage())
159+
));
160+
});
220161
} elseif ($data->method === 'close' && $db !== null && \count($data->params) === 0) {
221162
// close database and remove reference
222163
$db->close();

src/Factory.php

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Clue\React\SQLite;
44

5+
use Clue\React\SQLite\Io\BlockingDatabase;
56
use Clue\React\SQLite\Io\LazyDatabase;
67
use Clue\React\SQLite\Io\ProcessIoDatabase;
78
use React\ChildProcess\Process;
@@ -46,6 +47,18 @@ class Factory
4647
* $factory = new Clue\React\SQLite\Factory(null, '/usr/bin/php6.0');
4748
* ```
4849
*
50+
* Or you may use this parameter to pass an empty PHP binary path which will
51+
* cause this project to not spawn a PHP child process for any database
52+
* interactions at all. In this case, using SQLite will block the main
53+
* process, but continues to provide the exact same async API. This can be
54+
* useful if concurrent execution is not needed, especially when running
55+
* behind a traditional web server (non-CLI SAPI).
56+
*
57+
* ```php
58+
* // advanced usage: empty binary path runs blocking SQLite in same process
59+
* $factory = new Clue\React\SQLite\Factory(null, '');
60+
* ```
61+
*
4962
* @param ?LoopInterface $loop
5063
* @param ?string $binary
5164
*/
@@ -109,6 +122,17 @@ public function __construct(LoopInterface $loop = null, $binary = null)
109122
public function open($filename, $flags = null)
110123
{
111124
$filename = $this->resolve($filename);
125+
126+
if ($this->bin === '') {
127+
try {
128+
return \React\Promise\resolve(new BlockingDatabase($filename, $flags));
129+
} catch (\Exception $e) {
130+
return \React\Promise\reject(new \RuntimeException($e->getMessage()) );
131+
} catch (\Error $e) {
132+
return \React\Promise\reject(new \RuntimeException($e->getMessage()));
133+
}
134+
}
135+
112136
return $this->useSocket ? $this->openSocketIo($filename, $flags) : $this->openProcessIo($filename, $flags);
113137
}
114138

@@ -248,10 +272,7 @@ private function openProcessIo($filename, $flags = null)
248272
$process->start($this->loop);
249273

250274
$db = new ProcessIoDatabase($process);
251-
$args = array($filename);
252-
if ($flags !== null) {
253-
$args[] = $flags;
254-
}
275+
$args = array($filename, $flags);
255276

256277
return $db->send('open', $args)->then(function () use ($db) {
257278
return $db;
@@ -333,10 +354,7 @@ private function openSocketIo($filename, $flags = null)
333354
});
334355

335356
$db = new ProcessIoDatabase($process);
336-
$args = array($filename);
337-
if ($flags !== null) {
338-
$args[] = $flags;
339-
}
357+
$args = array($filename, $flags);
340358

341359
$db->send('open', $args)->then(function () use ($deferred, $db) {
342360
$deferred->resolve($db);

0 commit comments

Comments
 (0)