@op-engineering/op-sqlite 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,32 +8,17 @@
8
8
  </div>
9
9
  <br />
10
10
 
11
- OP SQLite embeds the latest version of SQLite and provides a low-level (JSI-backed) API to execute SQL queries.
11
+ OP SQLite embeds the latest version of SQLite and provides a low-level API to execute SQL queries.
12
12
 
13
13
  **Current SQLite version: 3.44.0**
14
14
 
15
- Created by [@ospfranco](https://twitter.com/ospfranco). Also created `react-native-quick-sqlite`, this is the next version. You can expect a new version once Static Hermes is out.
16
-
17
- **Please consider Sponsoring**, none of this work is for free. I pay for it with my time and knowledge. If you are a company in need of help with your React Native/React apps feel free to reach out. I also do a lot of C++ and nowadays Rust.
18
-
19
- ## Coming up
20
-
21
- I will gladly review bug fixes, but in order for me to continue support and add new features, I ask you to sponsor me. Some of the things that can still be done to make this package faster and more complete:
22
-
23
- - Prepared statements
24
- - Inlining functions
25
- - Batching queries
26
- - Update hook
15
+ Created by [@ospfranco](https://twitter.com/ospfranco). **Please consider Sponsoring**, none of this work is for free. I pay for it with my time and knowledge. If you are a company in need of help with your React Native/React apps feel free to reach out. I also do a lot of C++ and nowadays Rust.
27
16
 
28
17
  ## Benchmarks
29
18
 
30
- You can find the [benchmarking code in the example app](https://github.com/OP-Engineering/op-sqlite/blob/main/example/src/Database.ts#L44). Non JSI libraries are not even a contender anymore, you should expect anywhere between a 5x to a 8x improvement over sqlite-storage, sqlite2 and so on. Loading a 300k record database (in milliseconds).
19
+ You can find the [benchmarking code in the example app](https://github.com/OP-Engineering/op-sqlite/blob/main/example/src/Database.ts#L44). You should expect anywhere between a 5x to a 8x improvement over non-JSI packages, and now a 5x to 8x improvement over quick-sqlite and expo-sqlite. Loading a 300k record database (in milliseconds).
31
20
 
32
- | Library | iPhone 15 Pro | Galaxy S22 |
33
- | ------------ | ------------- | ---------- |
34
- | quick-sqlite | 2719ms | 8851ms |
35
- | expo-sqlite | 2293ms | 10626ms |
36
- | op-sqlite | 507ms | 1125ms |
21
+ ![benchmark](benchmark.png)
37
22
 
38
23
  Memory consumption is also is also 1/4 compared to `react-native-quick-sqlite`. This query used to take 1.2gb of peak memory usage, now runs in 250mbs.
39
24
 
@@ -231,7 +216,7 @@ db.executeAsync(
231
216
 
232
217
  ### Blobs
233
218
 
234
- Blobs are supported via `ArrayBuffer`, you need to be careful about the semantics though. You cannot instanciate an instance of `ArrayBuffer` directly, nor pass a typed array directly. Here is an example:
219
+ Blobs are supported via `ArrayBuffer`, you need to be careful about the semantics though. You cannot instantiate an instance of `ArrayBuffer` directly, nor pass a typed array directly. Here is an example:
235
220
 
236
221
  ```ts
237
222
  db = open({
@@ -296,6 +281,28 @@ const { rowsAffected, commands } = db
296
281
  });
297
282
  ```
298
283
 
284
+ ## Update hook
285
+
286
+ You can subscribe to changes in your database by using an update hook:
287
+
288
+ ```ts
289
+ // Bear in mind: rowId is not your table primary key but the internal rowId sqlite uses
290
+ // to keep track of the table rows
291
+ db.updateHook(({ rowId, table, operation, row = {} }) => {
292
+ console.warn(`Hook has been called, rowId: ${rowId}, ${table}, ${operation}`);
293
+ // Will contain the entire row that changed
294
+ // only on UPDATE and INSERT operations
295
+ console.warn(JSON.stringify(row, null, 2));
296
+ });
297
+
298
+ db.execute('INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', [
299
+ id,
300
+ name,
301
+ age,
302
+ networth,
303
+ ]);
304
+ ```
305
+
299
306
  ## Use built-in SQLite
300
307
 
301
308
  On iOS you can use the embedded SQLite, when running `pod-install` add an environment flag:
@@ -2,94 +2,129 @@
2
2
 
3
3
  namespace opsqlite {
4
4
 
5
- ThreadPool::ThreadPool() : done(false)
6
- {
7
- // This returns the number of threads supported by the system. If the
8
- // function can't figure out this information, it returns 0. 0 is not good,
9
- // so we create at least 1
10
- auto numberOfThreads = std::thread::hardware_concurrency();
11
- if (numberOfThreads == 0)
12
- {
13
- numberOfThreads = 1;
14
- }
15
-
16
- for (unsigned i = 0; i < numberOfThreads; ++i)
17
- {
18
- // The threads will execute the private member `doWork`. Note that we need
19
- // to pass a reference to the function (namespaced with the class name) as
20
- // the first argument, and the current object as second argument
21
- threads.push_back(std::thread(&ThreadPool::doWork, this));
22
- }
23
- }
24
-
25
- // The destructor joins all the threads so the program can exit gracefully.
26
- // This will be executed if there is any exception (e.g. creating the threads)
27
- ThreadPool::~ThreadPool()
28
- {
29
- // So threads know it's time to shut down
30
- done = true;
31
-
32
- // Wake up all the threads, so they can finish and be joined
33
- workQueueConditionVariable.notify_all();
34
- for (auto &thread : threads)
35
- {
36
- if (thread.joinable())
5
+ ThreadPool::ThreadPool() : done(false)
37
6
  {
38
- thread.join();
7
+ // This returns the number of threads supported by the system. If the
8
+ // function can't figure out this information, it returns 0. 0 is not good,
9
+ // so we create at least 1
10
+ auto numberOfThreads = std::thread::hardware_concurrency();
11
+ if (numberOfThreads == 0)
12
+ {
13
+ numberOfThreads = 1;
14
+ }
15
+
16
+ for (unsigned i = 0; i < numberOfThreads; ++i)
17
+ {
18
+ // The threads will execute the private member `doWork`. Note that we need
19
+ // to pass a reference to the function (namespaced with the class name) as
20
+ // the first argument, and the current object as second argument
21
+ threads.push_back(std::thread(&ThreadPool::doWork, this));
22
+ }
39
23
  }
40
- }
41
- }
42
-
43
- // This function will be called by the server every time there is a request
44
- // that needs to be processed by the thread pool
45
- void ThreadPool::queueWork(std::function<void(void)> task)
46
- {
47
- // Grab the mutex
48
- std::lock_guard<std::mutex> g(workQueueMutex);
49
-
50
- // Push the request to the queue
51
- workQueue.push(task);
52
-
53
- // Notify one thread that there are requests to process
54
- workQueueConditionVariable.notify_one();
55
- }
56
-
57
- // Function used by the threads to grab work from the queue
58
- void ThreadPool::doWork()
59
- {
60
- // Loop while the queue is not destructing
61
- while (!done)
62
- {
63
- std::function<void(void)> task;
64
24
 
65
- // Create a scope, so we don't lock the queue for longer than necessary
25
+ // The destructor joins all the threads so the program can exit gracefully.
26
+ // This will be executed if there is any exception (e.g. creating the threads)
27
+ ThreadPool::~ThreadPool()
66
28
  {
67
- std::unique_lock<std::mutex> g(workQueueMutex);
68
- workQueueConditionVariable.wait(g, [&]
69
- {
70
- // Only wake up if there are elements in the queue or the program is
71
- // shutting down
72
- return !workQueue.empty() || done; });
73
-
74
- // If we are shutting down exit witout trying to process more work
75
- if (done)
76
- {
77
- break;
78
- }
29
+ // So threads know it's time to shut down
30
+ done = true;
31
+
32
+ // Wake up all the threads, so they can finish and be joined
33
+ workQueueConditionVariable.notify_all();
34
+
35
+ for (auto &thread : threads)
36
+ {
37
+ if (thread.joinable())
38
+ {
39
+ thread.join();
40
+ }
41
+ }
42
+
43
+ threads.clear();
44
+ }
79
45
 
80
- task = workQueue.front();
81
- workQueue.pop();
46
+ // This function will be called by the server every time there is a request
47
+ // that needs to be processed by the thread pool
48
+ void ThreadPool::queueWork(std::function<void(void)> task)
49
+ {
50
+ // Grab the mutex
51
+ std::lock_guard<std::mutex> g(workQueueMutex);
52
+
53
+ // Push the request to the queue
54
+ workQueue.push(task);
55
+
56
+ // Notify one thread that there are requests to process
57
+ workQueueConditionVariable.notify_one();
82
58
  }
83
- ++busy;
84
- task();
85
- --busy;
86
- }
87
- }
88
59
 
89
- void ThreadPool::waitFinished() {
90
- std::unique_lock<std::mutex> g(workQueueMutex);
91
- workQueueConditionVariable.wait(g, [&]{ return workQueue.empty() && (busy == 0); });
92
- }
60
+ // Function used by the threads to grab work from the queue
61
+ void ThreadPool::doWork()
62
+ {
63
+ // Loop while the queue is not destructing
64
+ while (!done)
65
+ {
66
+ std::function<void(void)> task;
67
+
68
+ // Create a scope, so we don't lock the queue for longer than necessary
69
+ {
70
+ std::unique_lock<std::mutex> g(workQueueMutex);
71
+ workQueueConditionVariable.wait(g, [&]
72
+ {
73
+ // Only wake up if there are elements in the queue or the program is
74
+ // shutting down
75
+ return !workQueue.empty() || done; });
76
+
77
+ // If we are shutting down exit witout trying to process more work
78
+ if (done)
79
+ {
80
+ break;
81
+ }
82
+
83
+ task = workQueue.front();
84
+ workQueue.pop();
85
+ }
86
+ ++busy;
87
+ task();
88
+ --busy;
89
+ }
90
+ }
93
91
 
92
+ void ThreadPool::waitFinished() {
93
+ std::unique_lock<std::mutex> g(workQueueMutex);
94
+ workQueueConditionVariable.wait(g, [&]{ return workQueue.empty() && (busy == 0); });
95
+ }
94
96
 
97
+ void ThreadPool::restartPool() {
98
+ // So threads know it's time to shut down
99
+ done = true;
100
+
101
+ // Wake up all the threads, so they can finish and be joined
102
+ workQueueConditionVariable.notify_all();
103
+
104
+ for (auto &thread : threads)
105
+ {
106
+ if (thread.joinable())
107
+ {
108
+ thread.join();
109
+ }
110
+ }
111
+
112
+ threads.clear();
113
+
114
+ auto numberOfThreads = std::thread::hardware_concurrency();
115
+ if (numberOfThreads == 0)
116
+ {
117
+ numberOfThreads = 1;
118
+ }
119
+
120
+ for (unsigned i = 0; i < numberOfThreads; ++i)
121
+ {
122
+ // The threads will execute the private member `doWork`. Note that we need
123
+ // to pass a reference to the function (namespaced with the class name) as
124
+ // the first argument, and the current object as second argument
125
+ threads.push_back(std::thread(&ThreadPool::doWork, this));
126
+ }
127
+
128
+ done = false;
129
+ }
95
130
  }
package/cpp/ThreadPool.h CHANGED
@@ -13,32 +13,33 @@ namespace opsqlite {
13
13
 
14
14
  class ThreadPool {
15
15
  public:
16
- ThreadPool();
17
- ~ThreadPool();
18
- void queueWork(std::function<void(void)> task);
19
- void waitFinished();
20
-
16
+ ThreadPool();
17
+ ~ThreadPool();
18
+ void queueWork(std::function<void(void)> task);
19
+ void waitFinished();
20
+ void restartPool();
21
+
21
22
  private:
22
- unsigned int busy;
23
- // This condition variable is used for the threads to wait until there is work
24
- // to do
25
- std::condition_variable_any workQueueConditionVariable;
26
-
27
- // We store the threads in a vector, so we can later stop them gracefully
28
- std::vector<std::thread> threads;
29
-
30
- // Mutex to protect workQueue
31
- std::mutex workQueueMutex;
32
-
33
- // Queue of requests waiting to be processed
34
- std::queue<std::function<void(void)>> workQueue;
35
-
36
- // This will be set to true when the thread pool is shutting down. This tells
37
- // the threads to stop looping and finish
38
- bool done;
39
-
40
- // Function used by the threads to grab work from the queue
41
- void doWork();
23
+ unsigned int busy;
24
+ // This condition variable is used for the threads to wait until there is work
25
+ // to do
26
+ std::condition_variable_any workQueueConditionVariable;
27
+
28
+ // We store the threads in a vector, so we can later stop them gracefully
29
+ std::vector<std::thread> threads;
30
+
31
+ // Mutex to protect workQueue
32
+ std::mutex workQueueMutex;
33
+
34
+ // Queue of requests waiting to be processed
35
+ std::queue<std::function<void(void)>> workQueue;
36
+
37
+ // This will be set to true when the thread pool is shutting down. This tells
38
+ // the threads to stop looping and finish
39
+ bool done;
40
+
41
+ // Function used by the threads to grab work from the queue
42
+ void doWork();
42
43
  };
43
44
 
44
45
  }
package/cpp/bindings.cpp CHANGED
@@ -9,6 +9,7 @@
9
9
  #include "macros.h"
10
10
  #include <iostream>
11
11
  #include "DumbHostObject.h"
12
+ #include <unordered_map>
12
13
 
13
14
  namespace opsqlite {
14
15
 
@@ -16,16 +17,27 @@ namespace jsi = facebook::jsi;
16
17
 
17
18
  std::string basePath;
18
19
  std::shared_ptr<react::CallInvoker> invoker;
19
- std::shared_ptr<ThreadPool> pool;
20
+ ThreadPool pool;
21
+ std::unordered_map<std::string, std::shared_ptr<jsi::Value>> updateHooks = std::unordered_map<std::string, std::shared_ptr<jsi::Value>>();
22
+
23
+
24
+ // React native will try to clean the module on JS context invalidation (CodePush/Hot Reload)
25
+ // The clearState function is called and we use this flag to prevent any ongoing
26
+ // operations from continuing work and can return early
27
+ bool invalidated = false;
20
28
 
21
29
  void clearState() {
30
+ invalidated = true;
31
+ // Will terminate all operations and database connections
22
32
  sqliteCloseAll();
33
+ // We then join all the threads before the context gets invalidated
34
+ pool.restartPool();
23
35
  }
24
36
 
25
37
  void install(jsi::Runtime &rt, std::shared_ptr<react::CallInvoker> jsCallInvoker, const char *docPath)
26
38
  {
39
+ invalidated = false;
27
40
  basePath = std::string(docPath);
28
- pool = std::make_shared<ThreadPool>();
29
41
  invoker = jsCallInvoker;
30
42
 
31
43
  auto open = HOSTFN("open", 3) {
@@ -227,21 +239,25 @@ void install(jsi::Runtime &rt, std::shared_ptr<react::CallInvoker> jsCallInvoker
227
239
  const jsi::Value &originalParams = args[2];
228
240
 
229
241
  std::vector<JSVariant> params = toVariantVec(rt, originalParams);
230
-
242
+
231
243
  auto promiseCtr = rt.global().getPropertyAsFunction(rt, "Promise");
232
244
  auto promise = promiseCtr.callAsConstructor(rt, HOSTFN("executor", 2) {
233
245
  auto resolve = std::make_shared<jsi::Value>(rt, args[0]);
234
246
  auto reject = std::make_shared<jsi::Value>(rt, args[1]);
235
247
 
236
248
  auto task =
237
- [&rt, dbName, query, params = std::make_shared<std::vector<JSVariant>>(params), resolve, reject]()
249
+ [&rt, dbName, query, params = std::move(params), resolve, reject]()
238
250
  {
239
251
  try
240
252
  {
241
253
  std::vector<DumbHostObject> results;
242
254
  std::shared_ptr<std::vector<DynamicHostObject>> metadata = std::make_shared<std::vector<DynamicHostObject>>();;
243
255
 
244
- auto status = sqliteExecute(dbName, query, params.get(), &results, metadata);
256
+ auto status = sqliteExecute(dbName, query, &params, &results, metadata);
257
+
258
+ if(invalidated) {
259
+ return;
260
+ }
245
261
 
246
262
  invoker->invokeAsync([&rt,
247
263
  results = std::make_shared<std::vector<DumbHostObject>>(results),
@@ -270,7 +286,7 @@ void install(jsi::Runtime &rt, std::shared_ptr<react::CallInvoker> jsCallInvoker
270
286
  }
271
287
  };
272
288
 
273
- pool->queueWork(task);
289
+ pool.queueWork(task);
274
290
 
275
291
  return {};
276
292
  }));
@@ -363,7 +379,7 @@ void install(jsi::Runtime &rt, std::shared_ptr<react::CallInvoker> jsCallInvoker
363
379
  });
364
380
  }
365
381
  };
366
- pool->queueWork(task);
382
+ pool.queueWork(task);
367
383
 
368
384
  return {};
369
385
  }));
@@ -414,14 +430,54 @@ void install(jsi::Runtime &rt, std::shared_ptr<react::CallInvoker> jsCallInvoker
414
430
  });
415
431
  }
416
432
  };
417
- pool->queueWork(task);
433
+ pool.queueWork(task);
418
434
  return {};
419
435
  }));
420
436
 
421
437
  return promise;
422
438
  });
423
439
 
424
-
440
+ auto updateHook = HOSTFN("updateHook", 2)
441
+ {
442
+ if (sizeof(args) < 2)
443
+ {
444
+ throw jsi::JSError(rt, "[op-sqlite][loadFileAsync] Incorrect parameters: dbName and callback needed");
445
+ return {};
446
+ }
447
+
448
+ auto dbName = args[0].asString(rt).utf8(rt);
449
+ auto callback = std::make_shared<jsi::Value>(rt, args[1]);
450
+ updateHooks[dbName] = callback;
451
+
452
+ auto hook = [&rt, callback](std::string dbName, std::string tableName, std::string operation, int rowId) {
453
+ std::vector<JSVariant> params;
454
+ std::vector<DumbHostObject> results;
455
+ std::shared_ptr<std::vector<DynamicHostObject>> metadata = std::make_shared<std::vector<DynamicHostObject>>();;
456
+
457
+ if(operation != "DELETE") {
458
+ std::string query = "SELECT * FROM " + tableName + " where rowid = " + std::to_string(rowId) + ";";
459
+ sqliteExecute(dbName, query, &params, &results, metadata);
460
+ }
461
+
462
+ invoker->invokeAsync([&rt, results = std::make_shared<std::vector<DumbHostObject>>(results), callback, tableName = std::move(tableName), operation = std::move(operation), &rowId]
463
+ {
464
+ auto res = jsi::Object(rt);
465
+ res.setProperty(rt, "table", jsi::String::createFromUtf8(rt, tableName));
466
+ res.setProperty(rt, "operation", jsi::String::createFromUtf8(rt, operation));
467
+ res.setProperty(rt, "rowId", jsi::Value(rowId));
468
+ if(results->size() != 0) {
469
+ res.setProperty(rt, "row", jsi::Object::createFromHostObject(rt, std::make_shared<DumbHostObject>(results->at(0))));
470
+ }
471
+
472
+ callback->asObject(rt).asFunction(rt).call(rt, res);
473
+
474
+ });
475
+ };
476
+
477
+ registerUpdateHook(dbName, std::move(hook));
478
+
479
+ return {};
480
+ });
425
481
 
426
482
  jsi::Object module = jsi::Object(rt);
427
483
 
@@ -435,6 +491,7 @@ void install(jsi::Runtime &rt, std::shared_ptr<react::CallInvoker> jsCallInvoker
435
491
  module.setProperty(rt, "executeBatch", std::move(executeBatch));
436
492
  module.setProperty(rt, "executeBatchAsync", std::move(executeBatchAsync));
437
493
  module.setProperty(rt, "loadFile", std::move(loadFile));
494
+ module.setProperty(rt, "updateHook", std::move(updateHook));
438
495
 
439
496
  rt.global().setProperty(rt, "__OPSQLiteProxy", std::move(module));
440
497
  }