Skip to content

Commit b62c955

Browse files
committed
src, test, doc: allow customizing conflict behavior
1 parent 9c31f33 commit b62c955

File tree

3 files changed

+78
-4
lines changed

3 files changed

+78
-4
lines changed

doc/api/sqlite.md

+5
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ Creates and attackes a session to the database. This method is a wrapper around
169169
* `filter` {Function} Optional function that takes the name of a table as the first argument.
170170
When `true` is returned, includes the change, otherwise, it is discarded. When not provided
171171
no changes are filtered, and all are changes are attempted.
172+
* `onConflict` {number} When provided, must be one of `SQLITE_CHANGESET_OMIT`,
173+
`SQLITE_CHANGESET_REPLACE` or `SQLITE_CHANGESET_ABORT`. Determines how conflicts are handled.
174+
Conflicting changes are either omitted, changes replace existing values or the
175+
call is aborted when the first conflicting change is encountered (this is the default).
176+
* Returns: {boolean} Whether the changeset was applied succesfully without being aborted.
172177

173178
An exception is thrown if the database is not
174179
open. This method is a wrapper around [`sqlite3changeset_apply()`][].

src/node_sqlite.cc

+23-2
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,11 @@ void DatabaseSync::CreateSession(const FunctionCallbackInfo<Value>& args) {
276276
args.GetReturnValue().Set(session->object());
277277
}
278278

279+
static std::function<int()> conflictCallback;
280+
279281
static int xConflict(void* pCtx, int eConflict, sqlite3_changeset_iter* pIter) {
280-
return SQLITE_CHANGESET_ABORT;
282+
if (!conflictCallback) return SQLITE_CHANGESET_ABORT;
283+
return conflictCallback();
281284
}
282285

283286
static std::function<bool(std::string)> filterCallback;
@@ -289,6 +292,7 @@ static int xFilter(void* pCtx, const char* zTab) {
289292
}
290293

291294
void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo<Value>& args) {
295+
conflictCallback = nullptr;
292296
filterCallback = nullptr;
293297

294298
DatabaseSync* db;
@@ -313,6 +317,24 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo<Value>& args) {
313317
}
314318

315319
Local<Object> options = args[1].As<Object>();
320+
321+
Local<String> conflictKey = String::NewFromUtf8(env->isolate(), "onConflict", v8::NewStringType::kNormal).ToLocalChecked();
322+
if (options->HasOwnProperty(env->context(), conflictKey).FromJust()) {
323+
Local<Value> conflictValue = options->Get(env->context(), conflictKey).ToLocalChecked();
324+
325+
if (!conflictValue->IsNumber()) {
326+
node::THROW_ERR_INVALID_ARG_TYPE(
327+
env->isolate(),
328+
"The \"options.onConflict\" argument must be an number.");
329+
return;
330+
}
331+
332+
int conflictInt = conflictValue->Int32Value(env->context()).FromJust();
333+
conflictCallback = [conflictInt]() -> int {
334+
return conflictInt;
335+
};
336+
}
337+
316338
Local<String> filterKey = String::NewFromUtf8(
317339
env->isolate(),
318340
"filter",
@@ -351,7 +373,6 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo<Value>& args) {
351373
buf.length(),
352374
const_cast<void *>(static_cast<const void *>(buf.data())),
353375
xFilter,
354-
// TODO(louwers): allow custom conflict handler
355376
xConflict,
356377
nullptr);
357378
if (r == SQLITE_ABORT) {

test/parallel/test-sqlite.js

+50-2
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,7 @@ suite('session extension', () => {
881881
t.assert.strictEqual(database1.prepare(select2).all().length, 2); // data1 should have values in database1
882882
});
883883

884-
test('conflict while applying changeset', (t) => {
884+
const prepareConflict = () => {
885885
const database1 = new DatabaseSync(':memory:');
886886
const database2 = new DatabaseSync(':memory:');
887887

@@ -896,9 +896,57 @@ suite('session extension', () => {
896896
const insertSql = 'INSERT INTO data (key, value) VALUES (?, ?)';
897897
const session = database1.createSession();
898898
database1.prepare(insertSql).run(1, 'hello');
899+
database1.prepare(insertSql).run(2, 'foo');
899900
database2.prepare(insertSql).run(1, 'world');
901+
return {
902+
database2,
903+
changeset: session.changeset()
904+
}
905+
}
906+
907+
test('conflict while applying changeset (default abort)', (t) => {
908+
const { database2, changeset } = prepareConflict();
900909
// When changeset is aborted due to a conflict, applyChangeset should return false
901-
t.assert.strictEqual(database2.applyChangeset(session.changeset()), false);
910+
t.assert.strictEqual(database2.applyChangeset(changeset), false);
911+
t.assert.deepStrictEqual(
912+
database2.prepare('SELECT value from data').all(),
913+
[{ value: 'world' }]); // unchanged
914+
});
915+
916+
test('conflict while applying changeset (explicit abort)', (t) => {
917+
const { database2, changeset } = prepareConflict();
918+
const result = database2.applyChangeset(changeset, {
919+
onConflict: SQLITE_CHANGESET_ABORT
920+
});
921+
// When changeset is aborted due to a conflict, applyChangeset should return false
922+
t.assert.strictEqual(result, false);
923+
t.assert.deepStrictEqual(
924+
database2.prepare('SELECT value from data').all(),
925+
[{ value: 'world' }]); // unchanged
926+
});
927+
928+
test('conflict while applying changeset (replacement)', (t) => {
929+
const { database2, changeset } = prepareConflict();
930+
const result = database2.applyChangeset(changeset, {
931+
onConflict: SQLITE_CHANGESET_REPLACE
932+
});
933+
// Not aborted due to conflict, so should return true
934+
t.assert.strictEqual(result, true);
935+
t.assert.deepStrictEqual(
936+
database2.prepare('SELECT value from data ORDER BY key').all(),
937+
[{ value: 'hello'}, { value: 'foo' }]); // replaced
938+
});
939+
940+
test('conflict while applying changeset (omit)', (t) => {
941+
const { database2, changeset } = prepareConflict();
942+
const result = database2.applyChangeset(changeset, {
943+
onConflict: SQLITE_CHANGESET_OMIT
944+
});
945+
// Not aborted due to conflict, so should return true
946+
t.assert.strictEqual(result, true);
947+
t.assert.deepStrictEqual(
948+
database2.prepare('SELECT value from data ORDER BY key ASC').all(),
949+
[{ value: 'world'}, { value: 'foo' }]); // conflicting change omitted
902950
});
903951

904952
test('constants are defined', (t) => {

0 commit comments

Comments
 (0)