Skip to content

Commit a96e730

Browse files
authored
feat: Improve Array buffer memory allocation and thread-safety and add tests (#260)
* fix: use `ArrayBuffer::copy` * test: add ArrayBuffer tests * fix: missing global `tsconfig.json` * fix: copy params before sending them to async operation * test: simplify ArrayBuffer tests * fix: copy commands in `executeBatchAsync` as well
1 parent b53311f commit a96e730

4 files changed

Lines changed: 193 additions & 7 deletions

File tree

example/src/tests/db.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ export function resetTestDb() {
3636
}
3737
}
3838

39+
export function createArrayBufferTestDb(name: string) {
40+
// Use a dedicated database so ArrayBuffer tests do not interfere
41+
// with the default test database used in other specs.
42+
const db = open({ name })
43+
44+
db.execute('DROP TABLE IF EXISTS BlobData;')
45+
db.execute(
46+
'CREATE TABLE BlobData (id INTEGER PRIMARY KEY, data BLOB NOT NULL) STRICT;',
47+
)
48+
49+
return db
50+
}
51+
3952
const LARGE_DB_NAME = 'large'
4053

4154
// Copyright 2024 Oscar Franco

example/src/tests/unit/specs/operations/execute.spec.ts

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { chance, expect, isNitroSQLiteError } from '../../common'
22
import { describe, it } from '../../../MochaRNAdapter'
3-
import { testDb } from '../../../db'
3+
import { createArrayBufferTestDb, testDb } from '../../../db'
44

55
export default function registerExecuteUnitTests() {
66
describe('execute', () => {
@@ -136,5 +136,122 @@ export default function registerExecuteUnitTests() {
136136
])
137137
})
138138
})
139+
140+
describe('ArrayBuffer support', () => {
141+
describe('execute', () => {
142+
it('stores and reads ArrayBuffer values from BLOB columns', () => {
143+
const dbName = 'array_buffer_read'
144+
const db = createArrayBufferTestDb(dbName)
145+
146+
const originalBytes = new Uint8Array([10, 20, 30, 40])
147+
const originalBuffer = originalBytes.buffer
148+
149+
try {
150+
db.execute('INSERT INTO BlobData (id, data) VALUES (?, ?)', [
151+
1,
152+
originalBuffer,
153+
])
154+
155+
const result = db.execute(
156+
'SELECT data FROM BlobData WHERE id = ?',
157+
[1],
158+
)
159+
160+
expect(result.rowsAffected).to.equal(1)
161+
expect(result.rows?.length).to.equal(1)
162+
163+
const row = result.results[0]
164+
// const row = result.rows?.item(0)
165+
expect(row).to.not.equal(undefined)
166+
167+
const value = row?.data
168+
expect(value).to.be.instanceOf(ArrayBuffer)
169+
170+
const returnedBytes = new Uint8Array(value as ArrayBuffer)
171+
expect(Array.from(returnedBytes)).to.eql(Array.from(originalBytes))
172+
} finally {
173+
db.close()
174+
db.delete()
175+
}
176+
})
177+
})
178+
179+
describe('executeAsync', () => {
180+
it('stores and reads ArrayBuffer values from BLOB columns', async () => {
181+
const dbName = 'array_buffer_read'
182+
const db = createArrayBufferTestDb(dbName)
183+
184+
const originalBytes = new Uint8Array([10, 20, 30, 40])
185+
const originalBuffer = originalBytes.buffer
186+
187+
try {
188+
await db.executeAsync(
189+
'INSERT INTO BlobData (id, data) VALUES (?, ?)',
190+
[1, originalBuffer],
191+
)
192+
193+
const result = await db.executeAsync(
194+
'SELECT data FROM BlobData WHERE id = ?',
195+
[1],
196+
)
197+
198+
expect(result.rowsAffected).to.equal(1)
199+
expect(result.rows?.length).to.equal(1)
200+
201+
const row = result.results[0]
202+
// const row = result.rows?.item(0)
203+
expect(row).to.not.equal(undefined)
204+
205+
const value = row?.data
206+
expect(value).to.be.instanceOf(ArrayBuffer)
207+
208+
const returnedBytes = new Uint8Array(value as ArrayBuffer)
209+
expect(Array.from(returnedBytes)).to.eql(Array.from(originalBytes))
210+
} finally {
211+
db.close()
212+
db.delete()
213+
}
214+
})
215+
})
216+
217+
describe('executeBatchAsync', () => {
218+
it('stores ArrayBuffer values in BLOB columns', async () => {
219+
const dbName = 'array_buffer_batch_async'
220+
const db = createArrayBufferTestDb(dbName)
221+
222+
const originalBytes = new Uint8Array([1, 2, 3, 4, 5])
223+
const originalBuffer = originalBytes.buffer
224+
225+
try {
226+
await db.executeBatchAsync([
227+
{
228+
query: 'INSERT INTO BlobData (id, data) VALUES (?, ?)',
229+
params: [1, originalBuffer],
230+
},
231+
])
232+
233+
const result = db.execute(
234+
'SELECT data FROM BlobData WHERE id = ?',
235+
[1],
236+
)
237+
238+
expect(result.rowsAffected).to.equal(1)
239+
expect(result.rows?.length).to.equal(1)
240+
241+
const row = result.results[0]
242+
expect(row).to.not.equal(undefined)
243+
244+
const value = row?.data
245+
expect(value).to.be.instanceOf(ArrayBuffer)
246+
247+
const returnedBytes = new Uint8Array(value as ArrayBuffer)
248+
expect(Array.from(returnedBytes)).to.eql(Array.from(originalBytes))
249+
} finally {
250+
db.close()
251+
db.delete()
252+
}
253+
})
254+
})
255+
})
139256
})
140257
}

package/cpp/operations.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,15 @@ std::shared_ptr<HybridNitroSQLiteQueryResult> sqliteExecute(const std::string& d
184184
case SQLITE_BLOB: {
185185
int blob_size = sqlite3_column_bytes(statement, i);
186186
const void* blob = sqlite3_column_blob(statement, i);
187-
uint8_t* data = new uint8_t[blob_size];
188-
memcpy(data, blob, blob_size);
189-
row[column_name] = ArrayBuffer::wrap(data, blob_size, [&data]() -> void { delete[] data; });
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+
}
190196
break;
191197
}
192198
case SQLITE_NULL:

package/cpp/specs/HybridNitroSQLite.cpp

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,54 @@
88
#include "sqliteExecuteBatch.hpp"
99
#include <iostream>
1010
#include <map>
11+
#include <optional>
1112
#include <string>
13+
#include <variant>
1214
#include <vector>
1315

1416
namespace margelo::nitro::rnnitrosqlite {
1517

18+
// Copy any JS-backed ArrayBuffers on the JS thread so they can be safely
19+
// accessed from the background thread used by Promise::async.
20+
static std::optional<SQLiteQueryParams> copyArrayBufferParamsForBackground(const std::optional<SQLiteQueryParams>& params) {
21+
if (!params) {
22+
return std::nullopt;
23+
}
24+
25+
SQLiteQueryParams copiedParams;
26+
copiedParams.reserve(params->size());
27+
28+
for (const auto& value : *params) {
29+
if (std::holds_alternative<std::shared_ptr<ArrayBuffer>>(value)) {
30+
const auto& buffer = std::get<std::shared_ptr<ArrayBuffer>>(value);
31+
const auto copiedBuffer = ArrayBuffer::copy(buffer);
32+
copiedParams.push_back(copiedBuffer);
33+
} else {
34+
copiedParams.push_back(value);
35+
}
36+
}
37+
38+
return copiedParams;
39+
}
40+
41+
// Overload for batch execution: copy ArrayBuffer params inside each BatchQuery.
42+
static std::vector<BatchQuery> copyArrayBufferParamsForBackground(const std::vector<BatchQuery>& commands) {
43+
std::vector<BatchQuery> copiedCommands;
44+
copiedCommands.reserve(commands.size());
45+
46+
for (const auto& command : commands) {
47+
BatchQuery copiedCommand = command;
48+
49+
if (command.params) {
50+
copiedCommand.params = copyArrayBufferParamsForBackground(command.params);
51+
}
52+
53+
copiedCommands.push_back(std::move(copiedCommand));
54+
}
55+
56+
return copiedCommands;
57+
}
58+
1659
const std::string getDocPath(const std::optional<std::string>& location) {
1760
std::string tempDocPath = std::string(HybridNitroSQLite::docPath);
1861
if (location) {
@@ -57,9 +100,11 @@ std::shared_ptr<HybridNitroSQLiteQueryResultSpec> HybridNitroSQLite::execute(con
57100

58101
std::shared_ptr<Promise<std::shared_ptr<HybridNitroSQLiteQueryResultSpec>>>
59102
HybridNitroSQLite::executeAsync(const std::string& dbName, const std::string& query, const std::optional<SQLiteQueryParams>& params) {
103+
const auto copiedParams = copyArrayBufferParamsForBackground(params);
104+
60105
return Promise<std::shared_ptr<HybridNitroSQLiteQueryResultSpec>>::async(
61106
[=, this]() -> std::shared_ptr<HybridNitroSQLiteQueryResultSpec> {
62-
auto result = execute(dbName, query, params);
107+
auto result = sqliteExecute(dbName, query, copiedParams);
63108
return result;
64109
});
65110
};
@@ -73,9 +118,14 @@ BatchQueryResult HybridNitroSQLite::executeBatch(const std::string& dbName, cons
73118

74119
std::shared_ptr<Promise<BatchQueryResult>> HybridNitroSQLite::executeBatchAsync(const std::string& dbName,
75120
const std::vector<BatchQueryCommand>& batchParams) {
121+
// Convert BatchQueryCommand objects on the JS thread and copy any JS-backed
122+
// ArrayBuffers into native buffers before going off-thread.
123+
const auto commands = batchParamsToCommands(batchParams);
124+
const auto copiedCommands = copyArrayBufferParamsForBackground(commands);
125+
76126
return Promise<BatchQueryResult>::async([=, this]() -> BatchQueryResult {
77-
auto result = executeBatch(dbName, batchParams);
78-
return result;
127+
auto result = sqliteExecuteBatch(dbName, copiedCommands);
128+
return BatchQueryResult(result.rowsAffected);
79129
});
80130
};
81131

0 commit comments

Comments
 (0)