react-native-nitro-sqlite 9.6.0 → 9.8.0

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.
Files changed (68) hide show
  1. package/RNNitroSQLite.podspec +56 -20
  2. package/android/CMakeLists.txt +15 -0
  3. package/android/build.gradle +14 -0
  4. package/cpp/NitroSQLiteException.hpp +5 -0
  5. package/cpp/databaseMigration.cpp +123 -0
  6. package/cpp/databaseMigration.hpp +13 -0
  7. package/cpp/hybridObjects/HybridNitroSQLite.cpp +65 -16
  8. package/cpp/hybridObjects/HybridNitroSQLite.hpp +5 -0
  9. package/cpp/hybridObjects/HybridNitroSQLiteQueryResult.cpp +5 -4
  10. package/cpp/hybridObjects/HybridNitroSQLiteQueryResult.hpp +1 -1
  11. package/cpp/importSqlFile.cpp +10 -5
  12. package/cpp/importSqlFile.hpp +5 -1
  13. package/cpp/operations.cpp +232 -168
  14. package/cpp/operations.hpp +31 -1
  15. package/cpp/sqliteExecuteBatch.cpp +17 -16
  16. package/cpp/sqliteExecuteBatch.hpp +4 -0
  17. package/ios/OnLoad.mm +26 -3
  18. package/lib/commonjs/DatabaseQueue.js +4 -17
  19. package/lib/commonjs/DatabaseQueue.js.map +1 -1
  20. package/lib/commonjs/operations/execute.js +28 -11
  21. package/lib/commonjs/operations/execute.js.map +1 -1
  22. package/lib/commonjs/operations/session.js +34 -9
  23. package/lib/commonjs/operations/session.js.map +1 -1
  24. package/lib/commonjs/operations/transaction.js +5 -5
  25. package/lib/commonjs/operations/transaction.js.map +1 -1
  26. package/lib/module/DatabaseQueue.js +4 -15
  27. package/lib/module/DatabaseQueue.js.map +1 -1
  28. package/lib/module/operations/execute.js +24 -12
  29. package/lib/module/operations/execute.js.map +1 -1
  30. package/lib/module/operations/session.js +36 -11
  31. package/lib/module/operations/session.js.map +1 -1
  32. package/lib/module/operations/transaction.js +6 -6
  33. package/lib/module/operations/transaction.js.map +1 -1
  34. package/lib/typescript/commonjs/DatabaseQueue.d.ts +1 -3
  35. package/lib/typescript/commonjs/DatabaseQueue.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/operations/execute.d.ts +6 -0
  37. package/lib/typescript/commonjs/operations/execute.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/operations/session.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/specs/NitroSQLite.nitro.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/specs/NitroSQLiteQueryResult.nitro.d.ts.map +1 -1
  41. package/lib/typescript/module/DatabaseQueue.d.ts +1 -3
  42. package/lib/typescript/module/DatabaseQueue.d.ts.map +1 -1
  43. package/lib/typescript/module/operations/execute.d.ts +6 -0
  44. package/lib/typescript/module/operations/execute.d.ts.map +1 -1
  45. package/lib/typescript/module/operations/session.d.ts.map +1 -1
  46. package/lib/typescript/module/specs/NitroSQLite.nitro.d.ts.map +1 -1
  47. package/lib/typescript/module/specs/NitroSQLiteQueryResult.nitro.d.ts.map +1 -1
  48. package/nitrogen/generated/android/RNNitroSQLiteOnLoad.cpp +2 -2
  49. package/nitrogen/generated/android/c++/JHybridNitroSQLiteOnLoadSpec.hpp +2 -2
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnnitrosqlite/HybridNitroSQLiteOnLoadSpec.kt +2 -0
  51. package/nitrogen/generated/ios/RNNitroSQLite+autolinking.rb +2 -0
  52. package/package.json +9 -7
  53. package/src/DatabaseQueue.ts +9 -19
  54. package/src/operations/execute.ts +52 -12
  55. package/src/operations/session.ts +57 -11
  56. package/src/operations/transaction.ts +6 -6
  57. package/src/specs/NitroSQLite.nitro.ts +4 -1
  58. package/src/specs/NitroSQLiteQueryResult.nitro.ts +4 -1
  59. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  60. package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
  61. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  62. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  63. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  64. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  65. package/android/.gradle/8.9/gc.properties +0 -0
  66. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  67. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  68. package/android/.gradle/vcs-1/gc.properties +0 -0
@@ -7,68 +7,131 @@
7
7
  #include <cmath>
8
8
  #include <ctime>
9
9
  #include <iostream>
10
+ #include <limits>
10
11
  #include <map>
12
+ #include <memory>
13
+ #include <mutex>
11
14
  #include <optional>
12
15
  #include <sqlite3.h>
13
16
  #include <sstream>
14
17
  #include <unistd.h>
15
18
 
19
+ #ifdef NITRO_SQLITE_VEC
20
+ // Angle-bracket so it resolves via -I (CocoaPods intercepts quoted includes).
21
+ #include <registerVectorExtensions.hpp>
22
+ #endif
23
+
16
24
  using namespace facebook;
17
25
  using namespace margelo::nitro;
18
26
  using namespace margelo::nitro::rnnitrosqlite;
19
27
 
20
28
  namespace margelo::rnnitrosqlite {
21
29
 
22
- std::map<std::string, sqlite3*> dbMap = std::map<std::string, sqlite3*>();
30
+ static constexpr double kInt64MinAsDouble = static_cast<double>(std::numeric_limits<int64_t>::min());
31
+ static constexpr double kInt64UpperBoundAsDouble = -kInt64MinAsDouble;
32
+
33
+ namespace {
34
+
35
+ std::map<std::string, SQLiteConnectionPtr> dbMap;
36
+ std::mutex dbMapMutex;
37
+ std::mutex dbLifecycleMutex;
38
+
39
+ } // namespace
40
+
41
+ SQLiteConnection::SQLiteConnection(std::string connectionName, sqlite3* database) : name(std::move(connectionName)), database(database) {}
42
+
43
+ SQLiteConnection::~SQLiteConnection() {
44
+ close();
45
+ }
46
+
47
+ void SQLiteConnection::close() noexcept {
48
+ std::lock_guard lock(mutex);
49
+ if (database == nullptr) {
50
+ return;
51
+ }
52
+
53
+ sqlite3_close_v2(database);
54
+ database = nullptr;
55
+ }
23
56
 
24
57
  void sqliteOpenDb(const std::string& dbName, const std::string& docPath) {
58
+ std::lock_guard lifecycleLock(dbLifecycleMutex);
59
+ {
60
+ std::lock_guard lock(dbMapMutex);
61
+ if (dbMap.contains(dbName)) {
62
+ throw NitroSQLiteException::DatabaseAlreadyOpen(dbName);
63
+ }
64
+ }
65
+
66
+ #ifdef NITRO_SQLITE_VEC
67
+ // Register before opening so the connection exposes vec0 + vec_*.
68
+ margelo::rnnitrosqlitevec::registerVectorExtensions();
69
+ #endif
70
+
25
71
  std::string dbPath = get_db_path(dbName, docPath);
26
72
 
27
73
  int sqlOpenFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX;
28
74
 
29
- sqlite3* db;
30
- int exit = 0;
31
- exit = sqlite3_open_v2(dbPath.c_str(), &db, sqlOpenFlags, nullptr);
75
+ sqlite3* rawDatabase = nullptr;
76
+ const int openStatus = sqlite3_open_v2(dbPath.c_str(), &rawDatabase, sqlOpenFlags, nullptr);
77
+ std::unique_ptr<sqlite3, decltype(&sqlite3_close_v2)> database(rawDatabase, sqlite3_close_v2);
78
+
79
+ if (openStatus != SQLITE_OK) {
80
+ const std::string errorMessage = rawDatabase == nullptr ? sqlite3_errstr(openStatus) : sqlite3_errmsg(rawDatabase);
81
+ throw NitroSQLiteException(NitroSQLiteExceptionType::DatabaseCannotBeOpened, errorMessage);
82
+ }
32
83
 
33
- if (exit != SQLITE_OK) {
34
- throw NitroSQLiteException(NitroSQLiteExceptionType::DatabaseCannotBeOpened, sqlite3_errmsg(db));
35
- } else {
36
- dbMap[dbName] = db;
84
+ auto connection = std::make_shared<SQLiteConnection>(dbName, database.get());
85
+ database.release();
86
+ {
87
+ std::lock_guard lock(dbMapMutex);
88
+ const bool inserted = dbMap.emplace(dbName, connection).second;
89
+ if (!inserted) {
90
+ throw NitroSQLiteException::DatabaseAlreadyOpen(dbName);
91
+ }
37
92
  }
38
93
  }
39
94
 
40
95
  void sqliteCloseDb(const std::string& dbName) {
96
+ std::lock_guard lifecycleLock(dbLifecycleMutex);
97
+ SQLiteConnectionPtr connection;
98
+ {
99
+ std::lock_guard lock(dbMapMutex);
100
+ auto iterator = dbMap.find(dbName);
101
+ if (iterator == dbMap.end()) {
102
+ throw NitroSQLiteException::DatabaseNotOpen(dbName);
103
+ }
41
104
 
42
- if (dbMap.count(dbName) == 0) {
43
- throw NitroSQLiteException::DatabaseNotOpen(dbName);
105
+ connection = std::move(iterator->second);
106
+ dbMap.erase(iterator);
44
107
  }
45
108
 
46
- sqlite3* db = dbMap[dbName];
47
-
48
- sqlite3_close_v2(db);
49
-
50
- dbMap.erase(dbName);
109
+ connection->close();
51
110
  }
52
111
 
53
112
  void sqliteCloseAll() {
54
- for (auto const& x : dbMap) {
55
- // In certain cases, this will return SQLITE_OK, mark the database connection as an unusable "zombie",
56
- // and deallocate the connection later.
57
- sqlite3_close_v2(x.second);
113
+ std::lock_guard lifecycleLock(dbLifecycleMutex);
114
+ std::map<std::string, SQLiteConnectionPtr> connections;
115
+ {
116
+ std::lock_guard lock(dbMapMutex);
117
+ connections.swap(dbMap);
118
+ }
119
+
120
+ for (const auto& [_, connection] : connections) {
121
+ connection->close();
58
122
  }
59
- dbMap.clear();
60
123
  }
61
124
 
62
125
  void sqliteAttachDb(const std::string& mainDBName, const std::string& docPath, const std::string& databaseToAttach,
63
126
  const std::string& alias) {
64
127
  /**
65
- * There is no need to check if mainDBName is opened because sqliteExecuteLiteral will do that.
128
+ * There is no need to check if mainDBName is opened because sqliteExecuteCommand will do that.
66
129
  * */
67
130
  std::string dbPath = get_db_path(databaseToAttach, docPath);
68
131
  std::string statement = "ATTACH DATABASE '" + dbPath + "' AS " + alias;
69
132
 
70
133
  try {
71
- sqliteExecuteLiteral(mainDBName, statement);
134
+ sqliteExecuteCommand(mainDBName, statement);
72
135
  } catch (NitroSQLiteException& e) {
73
136
  throw NitroSQLiteException(NitroSQLiteExceptionType::UnableToAttachToDatabase,
74
137
  mainDBName + " was unable to attach another database: " + std::string(e.what()));
@@ -77,12 +140,12 @@ void sqliteAttachDb(const std::string& mainDBName, const std::string& docPath, c
77
140
 
78
141
  void sqliteDetachDb(const std::string& mainDBName, const std::string& alias) {
79
142
  /**
80
- * There is no need to check if mainDBName is opened because sqliteExecuteLiteral will do that.
143
+ * There is no need to check if mainDBName is opened because sqliteExecuteCommand will do that.
81
144
  * */
82
145
  std::string statement = "DETACH DATABASE " + alias;
83
146
 
84
147
  try {
85
- sqliteExecuteLiteral(mainDBName, statement);
148
+ sqliteExecuteCommand(mainDBName, statement);
86
149
  } catch (NitroSQLiteException& e) {
87
150
  throw NitroSQLiteException(NitroSQLiteExceptionType::UnableToAttachToDatabase,
88
151
  mainDBName + " was unable to detach database: " + std::string(e.what()));
@@ -90,15 +153,26 @@ void sqliteDetachDb(const std::string& mainDBName, const std::string& alias) {
90
153
  }
91
154
 
92
155
  void sqliteRemoveDb(const std::string& dbName, const std::string& docPath) {
93
- if (dbMap.count(dbName) == 1) {
94
- sqliteCloseDb(dbName);
95
- }
96
-
97
- std::string dbFilePath = get_db_path(dbName, docPath);
156
+ std::lock_guard lifecycleLock(dbLifecycleMutex);
157
+ const std::string dbFilePath = get_db_path(dbName, docPath);
98
158
  if (!file_exists(dbFilePath)) {
99
159
  throw NitroSQLiteException::DatabaseFileNotFound(dbFilePath);
100
160
  }
101
161
 
162
+ SQLiteConnectionPtr connection;
163
+ {
164
+ std::lock_guard lock(dbMapMutex);
165
+ auto iterator = dbMap.find(dbName);
166
+ if (iterator != dbMap.end()) {
167
+ connection = std::move(iterator->second);
168
+ dbMap.erase(iterator);
169
+ }
170
+ }
171
+
172
+ if (connection) {
173
+ connection->close();
174
+ }
175
+
102
176
  remove(dbFilePath.c_str());
103
177
  }
104
178
 
@@ -111,7 +185,13 @@ void bindStatement(sqlite3_stmt* statement, const SQLiteQueryParams& values) {
111
185
  } else if (std::holds_alternative<bool>(value)) {
112
186
  sqlite3_bind_int(statement, sqliteIndex, std::get<bool>(value));
113
187
  } else if (std::holds_alternative<double>(value)) {
114
- sqlite3_bind_double(statement, sqliteIndex, std::get<double>(value));
188
+ // Bind whole numbers as INTEGER so vec0 rowid/pk/partition (which reject REAL) work; SQLite still coerces to REAL for REAL columns.
189
+ double doubleValue = std::get<double>(value);
190
+ if (std::trunc(doubleValue) == doubleValue && doubleValue >= kInt64MinAsDouble && doubleValue < kInt64UpperBoundAsDouble) {
191
+ sqlite3_bind_int64(statement, sqliteIndex, static_cast<sqlite3_int64>(doubleValue));
192
+ } else {
193
+ sqlite3_bind_double(statement, sqliteIndex, doubleValue);
194
+ }
115
195
  } else if (std::holds_alternative<std::string>(value)) {
116
196
  const auto stringValue = std::get<std::string>(value);
117
197
  sqlite3_bind_text(statement, sqliteIndex, stringValue.c_str(), stringValue.length(), SQLITE_TRANSIENT);
@@ -122,173 +202,157 @@ void bindStatement(sqlite3_stmt* statement, const SQLiteQueryParams& values) {
122
202
  }
123
203
  }
124
204
 
125
- std::shared_ptr<HybridNitroSQLiteQueryResult> sqliteExecute(const std::string& dbName, const std::string& query,
126
- const std::optional<SQLiteQueryParams>& params) {
127
- if (dbMap.count(dbName) == 0) {
128
- throw NitroSQLiteException::DatabaseNotOpen(dbName);
129
- }
205
+ namespace {
130
206
 
131
- auto db = dbMap[dbName];
207
+ struct SQLiteStatementFinalizer {
208
+ void operator()(sqlite3_stmt* statement) const noexcept {
209
+ if (statement != nullptr) {
210
+ sqlite3_finalize(statement);
211
+ }
212
+ }
213
+ };
214
+
215
+ using SQLiteStatement = std::unique_ptr<sqlite3_stmt, SQLiteStatementFinalizer>;
216
+
217
+ SQLiteStatement prepareStatement(sqlite3* db, const std::string& query, const std::optional<SQLiteQueryParams>& params) {
218
+ sqlite3_stmt* rawStatement = nullptr;
219
+ int statementStatus = sqlite3_prepare_v2(db, query.c_str(), -1, &rawStatement, nullptr);
220
+ SQLiteStatement statement(rawStatement);
221
+
222
+ if (statementStatus != SQLITE_OK) {
223
+ throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(db));
224
+ }
132
225
 
133
- sqlite3_stmt* statement;
134
- int statementStatus = sqlite3_prepare_v2(db, query.c_str(), -1, &statement, NULL);
135
- if (statementStatus == SQLITE_OK) // statement is correct, bind the passed parameters
136
- {
137
226
  if (params) {
138
- bindStatement(statement, *params);
227
+ bindStatement(statement.get(), *params);
139
228
  }
140
- } else {
141
- throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(db));
229
+
230
+ return statement;
142
231
  }
143
232
 
144
- auto isConsuming = true;
145
- auto isFailed = false;
233
+ template <typename OnRow>
234
+ void consumeStatement(sqlite3* db, sqlite3_stmt* statement, OnRow&& onRow) {
235
+ while (true) {
236
+ int result = sqlite3_step(statement);
146
237
 
147
- int result, i, count, column_type;
148
- std::string column_name;
149
- ColumnType column_declared_type;
150
- SQLiteQueryResultRow row;
151
- SQLiteQueryResults results;
152
- std::optional<SQLiteQueryTableMetadata> metadata = std::nullopt;
238
+ if (result == SQLITE_ROW) {
239
+ onRow(statement);
240
+ continue;
241
+ }
153
242
 
154
- while (isConsuming) {
155
- result = sqlite3_step(statement);
156
-
157
- switch (result) {
158
- case SQLITE_ROW:
159
- i = 0;
160
- row = std::unordered_map<std::string, SQLiteValue>();
161
- count = sqlite3_column_count(statement);
162
-
163
- while (i < count) {
164
- column_type = sqlite3_column_type(statement, i);
165
- column_name = sqlite3_column_name(statement, i);
166
- switch (column_type) {
167
-
168
- case SQLITE_INTEGER: {
169
- auto column_value = sqlite3_column_double(statement, i);
170
- row[column_name] = column_value;
171
- break;
172
- }
173
- case SQLITE_FLOAT: {
174
- auto column_value = sqlite3_column_double(statement, i);
175
- row[column_name] = column_value;
176
- break;
177
- }
178
- case SQLITE_TEXT: {
179
- auto column_value = reinterpret_cast<const char*>(sqlite3_column_text(statement, i));
180
- sqlite3_column_bytes(statement, i);
181
- row[column_name] = column_value;
182
- break;
183
- }
184
- case SQLITE_BLOB: {
185
- int blob_size = sqlite3_column_bytes(statement, i);
186
- const void* blob = sqlite3_column_blob(statement, i);
187
- // Copy the SQLite BLOB into a new native ArrayBuffer.
188
- // This avoids manual memory management and unsafe pointer handling.
189
- if (blob_size > 0) {
190
- const auto* blob_data = reinterpret_cast<const uint8_t*>(blob);
191
- row[column_name] = ArrayBuffer::copy(blob_data, static_cast<size_t>(blob_size));
192
- } else {
193
- // Represent empty BLOBs as an empty, but valid, ArrayBuffer.
194
- row[column_name] = ArrayBuffer::allocate(0);
195
- }
196
- break;
197
- }
198
- case SQLITE_NULL:
199
- // Intentionally left blank to switch to default case
200
- default:
201
- row[column_name] = NullType::null;
202
- break;
203
- }
204
- i++;
205
- }
206
- results.push_back(std::move(row));
207
- break;
208
- case SQLITE_DONE:
209
- i = 0;
210
- count = sqlite3_column_count(statement);
211
- while (i < count) {
212
- column_name = sqlite3_column_name(statement, i);
213
- const char* tp = sqlite3_column_decltype(statement, i);
214
- column_declared_type = mapSQLiteTypeToColumnType(tp);
215
- auto columnMeta = NitroSQLiteQueryColumnMetadata(std::move(column_name), std::move(column_declared_type), i);
216
-
217
- if (!metadata) {
218
- metadata = std::make_optional<SQLiteQueryTableMetadata>();
219
- }
220
- metadata->insert({column_name, columnMeta});
221
- i++;
222
- }
223
- isConsuming = false;
224
- break;
225
- default:
226
- isFailed = true;
227
- isConsuming = false;
243
+ if (result == SQLITE_DONE) {
244
+ return;
245
+ }
246
+
247
+ throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(db));
228
248
  }
229
249
  }
230
250
 
231
- sqlite3_finalize(statement);
251
+ } // namespace
232
252
 
233
- if (isFailed) {
234
- throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(db));
253
+ SQLiteConnectionPtr sqliteGetOpenDatabase(const std::string& dbName) {
254
+ std::lock_guard lock(dbMapMutex);
255
+ auto iterator = dbMap.find(dbName);
256
+ if (iterator == dbMap.end()) {
257
+ throw NitroSQLiteException::DatabaseNotOpen(dbName);
235
258
  }
236
259
 
237
- int rowsAffected = sqlite3_changes(db);
238
- long long latestInsertRowId = sqlite3_last_insert_rowid(db);
239
- return std::make_shared<HybridNitroSQLiteQueryResult>(results, static_cast<double>(latestInsertRowId), rowsAffected, metadata);
260
+ return iterator->second;
240
261
  }
241
262
 
242
- SQLiteOperationResult sqliteExecuteLiteral(const std::string& dbName, const std::string& query) {
243
- // Check if db connection is opened
244
- if (dbMap.count(dbName) == 0) {
245
- throw NitroSQLiteException::DatabaseNotOpen(dbName);
263
+ std::shared_ptr<HybridNitroSQLiteQueryResult> sqliteExecute(const std::string& dbName, const std::string& query,
264
+ const std::optional<SQLiteQueryParams>& params) {
265
+ return sqliteExecute(sqliteGetOpenDatabase(dbName), query, params);
266
+ }
267
+
268
+ std::shared_ptr<HybridNitroSQLiteQueryResult> sqliteExecute(const SQLiteConnectionPtr& connection, const std::string& query,
269
+ const std::optional<SQLiteQueryParams>& params) {
270
+ std::lock_guard lock(connection->mutex);
271
+ sqlite3* db = connection->database;
272
+ if (db == nullptr) {
273
+ throw NitroSQLiteException::DatabaseNotOpen(connection->name);
246
274
  }
247
275
 
248
- sqlite3* db = dbMap[dbName];
276
+ auto statement = prepareStatement(db, query, params);
277
+ SQLiteQueryResults results;
249
278
 
250
- // SQLite statements need to be compiled before executed
251
- sqlite3_stmt* statement;
279
+ consumeStatement(db, statement.get(), [&](sqlite3_stmt* currentStatement) {
280
+ SQLiteQueryResultRow row;
281
+ int count = sqlite3_column_count(currentStatement);
282
+
283
+ for (int i = 0; i < count; i++) {
284
+ int columnType = sqlite3_column_type(currentStatement, i);
285
+ std::string columnName = sqlite3_column_name(currentStatement, i);
286
+
287
+ switch (columnType) {
288
+ case SQLITE_INTEGER:
289
+ case SQLITE_FLOAT:
290
+ row[columnName] = sqlite3_column_double(currentStatement, i);
291
+ break;
292
+ case SQLITE_TEXT: {
293
+ auto columnValue = reinterpret_cast<const char*>(sqlite3_column_text(currentStatement, i));
294
+ row[columnName] = columnValue;
295
+ break;
296
+ }
297
+ case SQLITE_BLOB: {
298
+ int blobSize = sqlite3_column_bytes(currentStatement, i);
299
+ const void* blob = sqlite3_column_blob(currentStatement, i);
300
+ if (blobSize > 0) {
301
+ const auto* blobData = reinterpret_cast<const uint8_t*>(blob);
302
+ row[columnName] = ArrayBuffer::copy(blobData, static_cast<size_t>(blobSize));
303
+ } else {
304
+ row[columnName] = ArrayBuffer::allocate(0);
305
+ }
306
+ break;
307
+ }
308
+ case SQLITE_NULL:
309
+ default:
310
+ row[columnName] = NullType::null;
311
+ break;
312
+ }
313
+ }
252
314
 
253
- // Compile and move result into statement memory spot
254
- int statementStatus = sqlite3_prepare_v2(db, query.c_str(), -1, &statement, NULL);
315
+ results.push_back(std::move(row));
316
+ });
255
317
 
256
- if (statementStatus != SQLITE_OK) // statemnet is correct, bind the passed parameters
257
- {
258
- throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(db));
318
+ std::optional<SQLiteQueryTableMetadata> metadata = std::nullopt;
319
+ int count = sqlite3_column_count(statement.get());
320
+ for (int i = 0; i < count; i++) {
321
+ std::string columnName = sqlite3_column_name(statement.get(), i);
322
+ ColumnType columnDeclaredType = mapSQLiteTypeToColumnType(sqlite3_column_decltype(statement.get(), i));
323
+ auto columnMeta = NitroSQLiteQueryColumnMetadata(columnName, std::move(columnDeclaredType), i);
324
+
325
+ if (!metadata) {
326
+ metadata = std::make_optional<SQLiteQueryTableMetadata>();
327
+ }
328
+ metadata->insert({columnName, std::move(columnMeta)});
259
329
  }
260
330
 
261
- bool isConsuming = true;
262
- bool isFailed = false;
263
-
264
- int result;
265
- std::string column_name;
266
-
267
- while (isConsuming) {
268
- result = sqlite3_step(statement);
269
-
270
- switch (result) {
271
- case SQLITE_ROW:
272
- isConsuming = true;
273
- break;
331
+ int rowsAffected = sqlite3_changes(db);
332
+ long long latestInsertRowId = sqlite3_last_insert_rowid(db);
333
+ return std::make_shared<HybridNitroSQLiteQueryResult>(std::move(results), static_cast<double>(latestInsertRowId), rowsAffected,
334
+ std::move(metadata));
335
+ }
274
336
 
275
- case SQLITE_DONE:
276
- isConsuming = false;
277
- break;
337
+ SQLiteOperationResult sqliteExecuteCommand(const std::string& dbName, const std::string& query,
338
+ const std::optional<SQLiteQueryParams>& params) {
339
+ return sqliteExecuteCommand(sqliteGetOpenDatabase(dbName), query, params);
340
+ }
278
341
 
279
- default:
280
- isFailed = true;
281
- isConsuming = false;
282
- }
342
+ SQLiteOperationResult sqliteExecuteCommand(const SQLiteConnectionPtr& connection, const std::string& query,
343
+ const std::optional<SQLiteQueryParams>& params) {
344
+ std::lock_guard lock(connection->mutex);
345
+ sqlite3* db = connection->database;
346
+ if (db == nullptr) {
347
+ throw NitroSQLiteException::DatabaseNotOpen(connection->name);
283
348
  }
284
349
 
285
- sqlite3_finalize(statement);
350
+ auto statement = prepareStatement(db, query, params);
351
+ bool isReadOnly = sqlite3_stmt_readonly(statement.get()) != 0;
286
352
 
287
- if (isFailed) {
288
- throw NitroSQLiteException::SqlExecution(sqlite3_errmsg(db));
289
- }
353
+ consumeStatement(db, statement.get(), [](sqlite3_stmt*) {});
290
354
 
291
- return {.rowsAffected = sqlite3_changes(db)};
355
+ return {.rowsAffected = isReadOnly ? 0 : sqlite3_changes(db)};
292
356
  }
293
357
 
294
358
  } // namespace margelo::rnnitrosqlite
@@ -2,9 +2,32 @@
2
2
 
3
3
  #include "hybridObjects/HybridNitroSQLiteQueryResult.hpp"
4
4
  #include "types.hpp"
5
+ #include <memory>
6
+ #include <mutex>
7
+ #include <sqlite3.h>
8
+ #include <string>
5
9
 
6
10
  namespace margelo::rnnitrosqlite {
7
11
 
12
+ // Calls against one connection are serialized by `mutex`. Separate connections
13
+ // intentionally remain independent, so SQLITE_THREADSAFE=0 still requires the
14
+ // caller to serialize SQLite calls globally.
15
+ struct SQLiteConnection final {
16
+ SQLiteConnection(std::string name, sqlite3* database);
17
+ ~SQLiteConnection();
18
+
19
+ SQLiteConnection(const SQLiteConnection&) = delete;
20
+ SQLiteConnection& operator=(const SQLiteConnection&) = delete;
21
+
22
+ void close() noexcept;
23
+
24
+ const std::string name;
25
+ sqlite3* database;
26
+ std::recursive_mutex mutex;
27
+ };
28
+
29
+ using SQLiteConnectionPtr = std::shared_ptr<SQLiteConnection>;
30
+
8
31
  void sqliteOpenDb(const std::string& dbName, const std::string& docPath);
9
32
 
10
33
  void sqliteCloseDb(const std::string& dbName);
@@ -16,10 +39,17 @@ void sqliteAttachDb(const std::string& mainDBName, const std::string& docPath, c
16
39
 
17
40
  void sqliteDetachDb(const std::string& mainDBName, const std::string& alias);
18
41
 
42
+ SQLiteConnectionPtr sqliteGetOpenDatabase(const std::string& dbName);
43
+
19
44
  std::shared_ptr<HybridNitroSQLiteQueryResult> sqliteExecute(const std::string& dbName, const std::string& query,
20
45
  const std::optional<SQLiteQueryParams>& params);
46
+ std::shared_ptr<HybridNitroSQLiteQueryResult> sqliteExecute(const SQLiteConnectionPtr& connection, const std::string& query,
47
+ const std::optional<SQLiteQueryParams>& params);
21
48
 
22
- SQLiteOperationResult sqliteExecuteLiteral(const std::string& dbName, const std::string& query);
49
+ SQLiteOperationResult sqliteExecuteCommand(const std::string& dbName, const std::string& query,
50
+ const std::optional<SQLiteQueryParams>& params = std::nullopt);
51
+ SQLiteOperationResult sqliteExecuteCommand(const SQLiteConnectionPtr& connection, const std::string& query,
52
+ const std::optional<SQLiteQueryParams>& params = std::nullopt);
23
53
 
24
54
  void sqliteCloseAll();
25
55
 
@@ -33,6 +33,11 @@ std::vector<BatchQuery> batchParamsToCommands(const std::vector<BatchQueryComman
33
33
  }
34
34
 
35
35
  SQLiteOperationResult sqliteExecuteBatch(const std::string& dbName, const std::vector<BatchQuery>& commands) {
36
+ return sqliteExecuteBatch(sqliteGetOpenDatabase(dbName), commands);
37
+ }
38
+
39
+ SQLiteOperationResult sqliteExecuteBatch(const SQLiteConnectionPtr& connection, const std::vector<BatchQuery>& commands) {
40
+ std::lock_guard lock(connection->mutex);
36
41
  size_t commandCount = commands.size();
37
42
  if (commandCount <= 0) {
38
43
  throw NitroSQLiteException(NitroSQLiteExceptionType::NoBatchCommandsProvided, "No SQL batch commands provided");
@@ -40,28 +45,24 @@ SQLiteOperationResult sqliteExecuteBatch(const std::string& dbName, const std::v
40
45
 
41
46
  try {
42
47
  int rowsAffected = 0;
43
- sqliteExecuteLiteral(dbName, "BEGIN EXCLUSIVE TRANSACTION");
44
- for (int i = 0; i < commandCount; i++) {
45
- const auto command = commands.at(i);
46
-
47
- // We do not provide a datas tructure to receive query data because we don't need/want to handle this results in a batch execution
48
- auto results = SQLiteQueryResults();
49
- auto metadata = std::optional<SQLiteQueryTableMetadata>(std::nullopt);
50
- try {
51
- auto result = sqliteExecute(dbName, command.sql, command.params);
52
- rowsAffected += result->getRowsAffected();
53
- } catch (NitroSQLiteException& e) {
54
- sqliteExecuteLiteral(dbName, "ROLLBACK");
55
- throw e;
56
- }
48
+ sqliteExecuteCommand(connection, "BEGIN EXCLUSIVE TRANSACTION");
49
+ for (const auto& command : commands) {
50
+ auto result = sqliteExecuteCommand(connection, command.sql, command.params);
51
+ rowsAffected += result.rowsAffected;
57
52
  }
58
- sqliteExecuteLiteral(dbName, "COMMIT");
53
+
54
+ sqliteExecuteCommand(connection, "COMMIT");
59
55
  return {
60
56
  .rowsAffected = rowsAffected,
61
57
  .commands = (int)commandCount,
62
58
  };
63
59
  } catch (NitroSQLiteException& e) {
64
- sqliteExecuteLiteral(dbName, "ROLLBACK");
60
+ // Roll back exactly once; a failed ROLLBACK must not mask the original error.
61
+ try {
62
+ sqliteExecuteCommand(connection, "ROLLBACK");
63
+ } catch (...) {
64
+ // ignore — surface the original error below
65
+ }
65
66
  throw e;
66
67
  }
67
68
  }
@@ -5,12 +5,15 @@
5
5
 
6
6
  #include "BatchQueryCommand.hpp"
7
7
  #include "types.hpp"
8
+ #include <memory>
8
9
 
9
10
  using namespace facebook;
10
11
  using namespace margelo::nitro;
11
12
 
12
13
  namespace margelo::rnnitrosqlite {
13
14
 
15
+ struct SQLiteConnection;
16
+
14
17
  struct BatchQuery {
15
18
  std::string sql;
16
19
  std::optional<SQLiteQueryParams> params;
@@ -26,5 +29,6 @@ std::vector<BatchQuery> batchParamsToCommands(const std::vector<BatchQueryComman
26
29
  * Execute a batch of commands in a exclusive transaction
27
30
  */
28
31
  SQLiteOperationResult sqliteExecuteBatch(const std::string& dbName, const std::vector<BatchQuery>& commands);
32
+ SQLiteOperationResult sqliteExecuteBatch(const std::shared_ptr<SQLiteConnection>& connection, const std::vector<BatchQuery>& commands);
29
33
 
30
34
  } // namespace margelo::rnnitrosqlite
package/ios/OnLoad.mm CHANGED
@@ -30,9 +30,32 @@ using namespace margelo::nitro::rnnitrosqlite;
30
30
 
31
31
  documentPath = [storeUrl path];
32
32
  } else {
33
- // Get iOS app's document directory (to safely store database .sqlite3 file)
34
- NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true);
35
- documentPath = [paths objectAtIndex:0];
33
+ NSString *databaseLocation = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"RNNitroSQLite_DatabaseLocation"];
34
+
35
+ if ([databaseLocation isEqualToString:@"ApplicationSupport"]) {
36
+ // Library/Application Support is persistent, backed up, and never exposed to the user
37
+ // via the Files app (unlike the Documents directory, which becomes user-visible when
38
+ // the app enables file sharing).
39
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true);
40
+ documentPath = [paths objectAtIndex:0];
41
+
42
+ NSFileManager *fileManager = [NSFileManager defaultManager];
43
+ if (![fileManager fileExistsAtPath:documentPath]) {
44
+ [fileManager createDirectoryAtPath:documentPath withIntermediateDirectories:YES attributes:nil error:nil];
45
+ }
46
+
47
+ // Databases created before this option was enabled still live in Documents; each one is
48
+ // moved over when it is opened (see HybridNitroSQLite::open).
49
+ NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true) objectAtIndex:0];
50
+ HybridNitroSQLite::migrationDocPath = [documentsDirectory UTF8String];
51
+ } else {
52
+ if (databaseLocation != nil && ![databaseLocation isEqualToString:@"Documents"]) {
53
+ NSLog(@"Invalid RNNitroSQLite_DatabaseLocation value provided (%@). Supported values are \"Documents\" and \"ApplicationSupport\". Falling back to \"Documents\".", databaseLocation);
54
+ }
55
+ // Get iOS app's document directory (to safely store database .sqlite3 file)
56
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true);
57
+ documentPath = [paths objectAtIndex:0];
58
+ }
36
59
  }
37
60
 
38
61
  HybridNitroSQLite::docPath = [documentPath UTF8String];