Skip to content

Commit c4d46db

Browse files
authored
Merge pull request #24 from OP-Engineering/prepared_statements
2 parents d1521bf + a5d6e81 commit c4d46db

21 files changed

+1546
-1359
lines changed

README.md

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ You can find the [benchmarking code in the example app](https://github.com/OP-En
2222

2323
Memory consumption is also 1/4 compared to `react-native-quick-sqlite`. This query used to take 1.2 GB of peak memory usage, and now runs in 250mbs.
2424

25+
You can also turn on Memory Mapping to make your queries even faster by skipping the kernel during I/O, this comes with some disadvantages though. If you want even more speed and you can re-use your queries you can use prepared statements.
26+
2527
# Encryption
2628

2729
If you need to encrypt your entire database, there is [`op-sqlcipher`](https://github.com/OP-Engineering/op-sqlcipher), which is a fork of this library that uses [SQLCipher](https://github.com/sqlcipher/sqlcipher). It completely encrypts the database with minimal overhead.
@@ -102,6 +104,21 @@ const largeDb = open({
102104
});
103105
```
104106

107+
# Speed
108+
109+
op-sqlite is already the fastest solution it can be, but it doesn't mean you cannot tweak SQLite to be faster (at the cost of some disadvantages). One possible tweak is turning on [Memory Mapping](https://www.sqlite.org/mmap.html). It allows to read/write to/from the disk without going through the kernel. However, if your queries throw an error your application might crash.
110+
111+
To turn on Memory Mapping, execute the following pragma statement after opening a db:
112+
113+
```ts
114+
const db = open({
115+
name: 'mydb.sqlite',
116+
});
117+
118+
// 0 turns of memory mapping, any other number enables it with the cache size
119+
db.execute('PRAGMA mmap_size=268435456');
120+
```
121+
105122
# API
106123

107124
```typescript
@@ -134,7 +151,7 @@ db = {
134151
}
135152
```
136153

137-
### Simple queries
154+
## Simple queries
138155

139156
The basic query is **synchronous**, it will block rendering on large operations, further below you will find async versions.
140157

@@ -162,7 +179,7 @@ try {
162179
}
163180
```
164181

165-
### Multiple statements in a single string
182+
## Multiple statements in a single string
166183

167184
You can execute multiple statements in a single operation. The API however is not really thought for this use case and the results (and their metadata) will be mangled, so you can discard it.
168185

@@ -186,7 +203,7 @@ let t2name = db.execute(
186203
console.log(t2name.rows?._array[0].name); // outputs "T2"
187204
```
188205

189-
### Transactions
206+
## Transactions
190207

191208
Throwing an error inside the callback will ROLLBACK the transaction.
192209

@@ -215,7 +232,7 @@ await db.transaction('myDatabase', (tx) => {
215232
});
216233
```
217234

218-
### Batch operation
235+
## Batch operation
219236

220237
Batch execution allows the transactional execution of a set of commands
221238

@@ -232,7 +249,7 @@ const res = db.executeSqlBatch('myDatabase', commands);
232249
console.log(`Batch affected ${result.rowsAffected} rows`);
233250
```
234251

235-
### Dynamic Column Metadata
252+
## Dynamic Column Metadata
236253

237254
In some scenarios, dynamic applications may need to get some metadata information about the returned result set.
238255

@@ -253,7 +270,7 @@ metadata.forEach((column) => {
253270
});
254271
```
255272

256-
### Async operations
273+
## Async operations
257274

258275
You might have too much SQL to process and it will cause your application to freeze. There are async versions for some of the operations. This will offload the SQLite processing to a different thread.
259276

@@ -267,7 +284,7 @@ db.executeAsync(
267284
);
268285
```
269286

270-
### Blobs
287+
## Blobs
271288

272289
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:
273290

@@ -295,7 +312,22 @@ const result = db.execute('SELECT content FROM BlobTable');
295312
const finalUint8 = new Uint8Array(result.rows!._array[0].content);
296313
```
297314

298-
### Attach or Detach other databases
315+
## Prepared statements
316+
317+
A lot of the work when executing queries is not iterating through the result set itself but, sometimes, planning the execution. If you have a query which is expensive but you can re-use (even if you have to change the arguments) you can use a `prepared statement`:
318+
319+
```ts
320+
const statement = db.prepareStatement('SELECT * FROM User WHERE name = ?;');
321+
statement.bind(['Oscar']);
322+
let results1 = statement.execute();
323+
324+
statement.bind(['Carlos']);
325+
let results2 = statement.execute();
326+
```
327+
328+
You only pay the price of parsing the query once, and each subsequent execution should be faster.
329+
330+
# Attach or Detach other databases
299331

300332
SQLite supports attaching or detaching other database files into your main database connection through an alias.
301333
You can do any operation you like on this attached database like JOIN results across tables in different schemas, or update data or objects.
@@ -322,7 +354,7 @@ if (!detachResult.status) {
322354
}
323355
```
324356

325-
### Loading SQL Dump Files
357+
# Loading SQL Dump Files
326358

327359
If you have a SQL dump file, you can load it directly, with low memory consumption:
328360

@@ -334,7 +366,7 @@ const { rowsAffected, commands } = db
334366
});
335367
```
336368

337-
## Hooks
369+
# Hooks
338370

339371
You can subscribe to changes in your database by using an update hook:
340372

@@ -396,7 +428,7 @@ db.commitHook(null);
396428
db.rollbackHook(null);
397429
```
398430

399-
## Use built-in SQLite
431+
# Use built-in SQLite
400432

401433
On iOS you can use the embedded SQLite, when running `pod-install` add an environment flag:
402434

@@ -406,11 +438,11 @@ OP_SQLITE_USE_PHONE_VERSION=1 npx pod-install
406438

407439
On Android, it is not possible to link the OS SQLite. It is also a bad idea due to vendor changes, old android bugs, etc. Unfortunately, this means this library will add some megabytes to your app size.
408440

409-
## Enable compile-time options
441+
# Enable compile-time options
410442

411443
By specifying pre-processor flags, you can enable optional features like FTS5, Geopoly, etc.
412444

413-
### iOS
445+
## iOS
414446

415447
Add a `post_install` block to your `<PROJECT_ROOT>/ios/Podfile` like so:
416448

@@ -429,26 +461,26 @@ end
429461
Replace the `<SQLITE_FLAGS>` part with the flags you want to add.
430462
For example, you could add `SQLITE_ENABLE_FTS5=1` to `GCC_PREPROCESSOR_DEFINITIONS` to enable FTS5 in the iOS project.
431463

432-
### Android
464+
## Android
433465

434466
You can specify flags via `<PROJECT_ROOT>/android/gradle.properties` like so:
435467

436468
```
437469
OPSQLiteFlags="-DSQLITE_ENABLE_FTS5=1"
438470
```
439471

440-
## Additional configuration
472+
# Additional configuration
441473

442-
### App groups (iOS only)
474+
## App groups (iOS only)
443475

444476
On iOS, the SQLite database can be placed in an app group, in order to make it accessible from other apps in that app group. E.g. for sharing capabilities.
445477

446478
To use an app group, add the app group ID as the value for the `OPSQLite_AppGroup` key in your project's `Info.plist` file. You'll also need to configure the app group in your project settings. (Xcode -> Project Settings -> Signing & Capabilities -> Add Capability -> App Groups)
447479

448-
## Contribute
480+
# Contribute
449481

450482
You need to have clang-format installed (`brew install clang-format`)
451483

452-
## License
484+
# License
453485

454486
MIT License.

android/CMakeLists.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ add_library(
2929
../cpp/ThreadPool.cpp
3030
../cpp/sqlbatchexecutor.h
3131
../cpp/sqlbatchexecutor.cpp
32-
../cpp/DynamicHostObject.cpp
33-
../cpp/DynamicHostObject.h
32+
../cpp/SmartHostObject.cpp
33+
../cpp/SmartHostObject.h
34+
../cpp/PreparedStatementHostObject.h
35+
../cpp/PreparedStatementHostObject.cpp
3436
../cpp/DumbHostObject.cpp
3537
../cpp/DumbHostObject.h
3638
../cpp/macros.h

benchmark.png

23.7 KB
Loading

cpp/DumbHostObject.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "DumbHostObject.h"
2+
#include "SmartHostObject.h"
23
#include "utils.h"
34
#include <iostream>
45

@@ -7,7 +8,7 @@ namespace opsqlite {
78
namespace jsi = facebook::jsi;
89

910
DumbHostObject::DumbHostObject(
10-
std::shared_ptr<std::vector<DynamicHostObject>> metadata) {
11+
std::shared_ptr<std::vector<SmartHostObject>> metadata) {
1112
this->metadata = metadata;
1213
};
1314

cpp/DumbHostObject.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
#include <stdio.h>
55

6-
#include "DynamicHostObject.h"
6+
#include "SmartHostObject.h"
77
#include "types.h"
88
#include <any>
99
#include <jsi/jsi.h>
@@ -17,15 +17,15 @@ class JSI_EXPORT DumbHostObject : public jsi::HostObject {
1717
public:
1818
DumbHostObject(){};
1919

20-
DumbHostObject(std::shared_ptr<std::vector<DynamicHostObject>> metadata);
20+
DumbHostObject(std::shared_ptr<std::vector<SmartHostObject>> metadata);
2121

2222
std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime &rt);
2323

2424
jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propNameID);
2525

2626
std::vector<JSVariant> values;
2727

28-
std::shared_ptr<std::vector<DynamicHostObject>> metadata;
28+
std::shared_ptr<std::vector<SmartHostObject>> metadata;
2929
};
3030

3131
} // namespace opsqlite
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//
2+
// PreparedStatementHostObject.cpp
3+
// op-sqlite
4+
//
5+
// Created by Oscar Franco on 5/12/23.
6+
//
7+
8+
#include "PreparedStatementHostObject.h"
9+
#include "bridge.h"
10+
#include "macros.h"
11+
#include "utils.h"
12+
13+
namespace opsqlite {
14+
15+
namespace jsi = facebook::jsi;
16+
17+
PreparedStatementHostObject::PreparedStatementHostObject(
18+
std::string dbName, sqlite3_stmt *statementPtr)
19+
: _dbName(dbName), _statement(statementPtr) {}
20+
21+
std::vector<jsi::PropNameID>
22+
PreparedStatementHostObject::getPropertyNames(jsi::Runtime &rt) {
23+
std::vector<jsi::PropNameID> keys;
24+
25+
// for (auto field : fields) {
26+
// keys.push_back(jsi::PropNameID::forAscii(rt, field.first));
27+
// }
28+
29+
return keys;
30+
}
31+
32+
jsi::Value PreparedStatementHostObject::get(jsi::Runtime &rt,
33+
const jsi::PropNameID &propNameID) {
34+
auto name = propNameID.utf8(rt);
35+
36+
if (name == "bind") {
37+
return HOSTFN("bind", 1) {
38+
if (_statement == NULL) {
39+
throw std::runtime_error("statement has been freed");
40+
}
41+
42+
std::vector<JSVariant> params;
43+
44+
const jsi::Value &originalParams = args[0];
45+
params = toVariantVec(rt, originalParams);
46+
47+
std::vector<DumbHostObject> results;
48+
std::shared_ptr<std::vector<SmartHostObject>> metadata =
49+
std::make_shared<std::vector<SmartHostObject>>();
50+
51+
sqlite_bind_statement(_statement, &params);
52+
53+
return {};
54+
});
55+
}
56+
57+
if (name == "execute") {
58+
return HOSTFN("execute", 1) {
59+
if (_statement == NULL) {
60+
throw std::runtime_error("statement has been freed");
61+
}
62+
std::vector<DumbHostObject> results;
63+
std::shared_ptr<std::vector<SmartHostObject>> metadata =
64+
std::make_shared<std::vector<SmartHostObject>>();
65+
66+
auto status = sqlite_execute_prepared_statement(_dbName, _statement,
67+
&results, metadata);
68+
69+
if (status.type == SQLiteError) {
70+
throw std::runtime_error(status.message);
71+
}
72+
73+
auto jsiResult = createResult(rt, status, &results, metadata);
74+
return jsiResult;
75+
});
76+
}
77+
78+
return {};
79+
}
80+
81+
PreparedStatementHostObject::~PreparedStatementHostObject() {
82+
if (_statement != NULL) {
83+
sqlite3_finalize(_statement);
84+
_statement = NULL;
85+
}
86+
}
87+
88+
} // namespace opsqlite

cpp/PreparedStatementHostObject.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// PreparedStatementHostObject.hpp
3+
// op-sqlite
4+
//
5+
// Created by Oscar Franco on 5/12/23.
6+
//
7+
8+
#ifndef PreparedStatementHostObject_h
9+
#define PreparedStatementHostObject_h
10+
11+
#include <jsi/jsi.h>
12+
#include <memory>
13+
#include <sqlite3.h>
14+
#include <string>
15+
16+
namespace opsqlite {
17+
namespace jsi = facebook::jsi;
18+
19+
class PreparedStatementHostObject : public jsi::HostObject {
20+
public:
21+
PreparedStatementHostObject(std::string dbName, sqlite3_stmt *statement);
22+
virtual ~PreparedStatementHostObject();
23+
24+
std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime &rt);
25+
26+
jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propNameID);
27+
28+
private:
29+
std::string _dbName;
30+
// This shouldn't be de-allocated until sqlite3_finalize is called on it
31+
sqlite3_stmt *_statement;
32+
};
33+
34+
} // namespace opsqlite
35+
36+
#endif /* PreparedStatementHostObject_hpp */

cpp/DynamicHostObject.cpp renamed to cpp/SmartHostObject.cpp

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
#include "DynamicHostObject.h"
1+
#include "SmartHostObject.h"
22
#include "utils.h"
3-
#include <iostream>
43

54
namespace opsqlite {
65

76
namespace jsi = facebook::jsi;
87

98
std::vector<jsi::PropNameID>
10-
DynamicHostObject::getPropertyNames(jsi::Runtime &rt) {
9+
SmartHostObject::getPropertyNames(jsi::Runtime &rt) {
1110
std::vector<jsi::PropNameID> keys;
1211

1312
for (auto field : fields) {
@@ -17,8 +16,8 @@ DynamicHostObject::getPropertyNames(jsi::Runtime &rt) {
1716
return keys;
1817
}
1918

20-
jsi::Value DynamicHostObject::get(jsi::Runtime &rt,
21-
const jsi::PropNameID &propNameID) {
19+
jsi::Value SmartHostObject::get(jsi::Runtime &rt,
20+
const jsi::PropNameID &propNameID) {
2221
auto name = propNameID.utf8(rt);
2322

2423
for (auto field : fields) {

0 commit comments

Comments
 (0)