Skip to content

Commit 5fa17be

Browse files
committed
src, test: allow passing custom filter callback
1 parent 18280b0 commit 5fa17be

File tree

2 files changed

+97
-4
lines changed

2 files changed

+97
-4
lines changed

src/node_sqlite.cc

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,17 @@ static int xConflict(void* pCtx, int eConflict, sqlite3_changeset_iter* pIter) {
280280
return SQLITE_CHANGESET_ABORT;
281281
}
282282

283+
static std::function<bool(std::string)> filterCallback;
284+
285+
static int xFilter(void* pCtx, const char* zTab) {
286+
if (!filterCallback) return 1;
287+
288+
return filterCallback(zTab) ? 1 : 0;
289+
}
290+
283291
void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo<Value>& args) {
292+
filterCallback = nullptr;
293+
284294
DatabaseSync* db;
285295
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
286296
Environment* env = Environment::GetCurrent(args);
@@ -293,12 +303,54 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo<Value>& args) {
293303
"The \"changeset\" argument must be a Uint8Array.");
294304
return;
295305
}
306+
307+
if (args.Length() > 1 && !args[1]->IsUndefined()) {
308+
if (!args[1]->IsObject()) {
309+
node::THROW_ERR_INVALID_ARG_TYPE(
310+
env->isolate(),
311+
"The second argument, if provided, must be an options object.");
312+
return;
313+
}
314+
315+
Local<Object> options = args[1].As<Object>();
316+
Local<String> filterKey = String::NewFromUtf8(
317+
env->isolate(),
318+
"filter",
319+
v8::NewStringType::kNormal).ToLocalChecked();
320+
321+
if (options->HasOwnProperty(env->context(),
322+
filterKey).FromJust()) {
323+
Local<Value> filterValue = options->Get(env->context(),
324+
filterKey).ToLocalChecked();
325+
326+
if (!filterValue->IsFunction()) {
327+
node::THROW_ERR_INVALID_ARG_TYPE(
328+
env->isolate(),
329+
"The \"options.filter\" argument must be a function.");
330+
return;
331+
}
332+
333+
Local<v8::Function> filterFunc = filterValue.As<v8::Function>();
334+
335+
filterCallback = [env, filterFunc](std::string item) -> bool {
336+
Local<Value> argv[] = {
337+
String::NewFromUtf8(env->isolate(),
338+
item.c_str(),
339+
v8::NewStringType::kNormal).ToLocalChecked()
340+
};
341+
Local<Value> result = filterFunc->Call(
342+
env->context(),
343+
Null(env->isolate()), 1, argv).ToLocalChecked();
344+
return result->BooleanValue(env->isolate());
345+
};
346+
}
347+
}
348+
296349
ArrayBufferViewContents<uint8_t> buf(args[0]);
297350
int r = sqlite3changeset_apply(db->connection_,
298351
buf.length(),
299352
const_cast<void *>(static_cast<const void *>(buf.data())),
300-
// TODO(louwers): allow passing filter callback
301-
nullptr,
353+
xFilter,
302354
// TODO(louwers): allow custom conflict handler
303355
xConflict,
304356
nullptr);
@@ -851,6 +903,10 @@ static void Initialize(Local<Object> target,
851903
target,
852904
"StatementSync",
853905
StatementSync::GetConstructorTemplate(env));
906+
907+
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_OMIT);
908+
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_REPLACE);
909+
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_ABORT);
854910
}
855911

856912
} // namespace sqlite

test/parallel/test-sqlite.js

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ const { spawnPromisified } = require('../common');
44
const tmpdir = require('../common/tmpdir');
55
const { existsSync } = require('node:fs');
66
const { join } = require('node:path');
7-
const { DatabaseSync, StatementSync } = require('node:sqlite');
7+
const {
8+
DatabaseSync,
9+
StatementSync,
10+
SQLITE_CHANGESET_OMIT,
11+
SQLITE_CHANGESET_REPLACE,
12+
SQLITE_CHANGESET_ABORT
13+
} = require('node:sqlite');
814
const { suite, test } = require('node:test');
915
let cnt = 0;
1016

@@ -891,7 +897,38 @@ suite('session extension', () => {
891897
const session = database1.createSession();
892898
database1.prepare(insertSql).run(1, 'hello');
893899
database2.prepare(insertSql).run(1, 'world');
894-
// When changeset is aborted due to a conflict,applyChangeset should return false
900+
// When changeset is aborted due to a conflict, applyChangeset should return false
895901
t.assert.strictEqual(database2.applyChangeset(session.changeset()), false);
896902
});
903+
904+
test('constants are defined', (t) => {
905+
t.assert.strictEqual(SQLITE_CHANGESET_OMIT, 0);
906+
t.assert.strictEqual(SQLITE_CHANGESET_REPLACE, 1);
907+
t.assert.strictEqual(SQLITE_CHANGESET_ABORT, 2);
908+
});
909+
910+
test('allow filtering changes', (t) => {
911+
const database1 = new DatabaseSync(':memory:');
912+
const database2 = new DatabaseSync(':memory:');
913+
const createTableSql = 'CREATE TABLE data1(key INTEGER PRIMARY KEY); CREATE TABLE data2(key INTEGER PRIMARY KEY);';
914+
database1.exec(createTableSql);
915+
database2.exec(createTableSql);
916+
917+
const session = database1.createSession();
918+
919+
database1.exec('INSERT INTO data1 (key) VALUES (1), (2), (3)');
920+
database1.exec('INSERT INTO data2 (key) VALUES (1), (2), (3), (4), (5)');
921+
922+
database2.applyChangeset(session.changeset(), {
923+
filter: (tableName) => tableName === 'data2'
924+
});
925+
926+
const data1Rows = database2.prepare('SELECT * FROM data1').all();
927+
const data2Rows = database2.prepare('SELECT * FROM data2').all();
928+
929+
// Expect no rows since all changes where filtered out
930+
t.assert.strictEqual(data1Rows.length, 0);
931+
// Expect 5 rows since these changes where not filtered out
932+
t.assert.strictEqual(data2Rows.length, 5);
933+
});
897934
});

0 commit comments

Comments
 (0)