Skip to content

Commit d194f1a

Browse files
louwersaduh95
authored andcommitted
sqlite: pass conflict type to conflict resolution handler
PR-URL: #56352 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 54f6d68 commit d194f1a

File tree

4 files changed

+298
-42
lines changed

4 files changed

+298
-42
lines changed

doc/api/sqlite.md

+60-7
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,27 @@ added: v23.3.0
230230
* `options` {Object} The configuration options for how the changes will be applied.
231231
* `filter` {Function} Skip changes that, when targeted table name is supplied to this function, return a truthy value.
232232
By default, all changes are attempted.
233-
* `onConflict` {number} Determines how conflicts are handled. **Default**: `SQLITE_CHANGESET_ABORT`.
234-
* `SQLITE_CHANGESET_OMIT`: conflicting changes are omitted.
235-
* `SQLITE_CHANGESET_REPLACE`: conflicting changes replace existing values.
236-
* `SQLITE_CHANGESET_ABORT`: abort on conflict and roll back database.
233+
* `onConflict` {Function} A function that determines how to handle conflicts. The function receives one argument,
234+
which can be one of the following values:
235+
236+
* `SQLITE_CHANGESET_DATA`: A `DELETE` or `UPDATE` change does not contain the expected "before" values.
237+
* `SQLITE_CHANGESET_NOTFOUND`: A row matching the primary key of the `DELETE` or `UPDATE` change does not exist.
238+
* `SQLITE_CHANGESET_CONFLICT`: An `INSERT` change results in a duplicate primary key.
239+
* `SQLITE_CHANGESET_FOREIGN_KEY`: Applying a change would result in a foreign key violation.
240+
* `SQLITE_CHANGESET_CONSTRAINT`: Applying a change results in a `UNIQUE`, `CHECK`, or `NOT NULL` constraint
241+
violation.
242+
243+
The function should return one of the following values:
244+
245+
* `SQLITE_CHANGESET_OMIT`: Omit conflicting changes.
246+
* `SQLITE_CHANGESET_REPLACE`: Replace existing values with conflicting changes (only valid with
247+
`SQLITE_CHANGESET_DATA` or `SQLITE_CHANGESET_CONFLICT` conflicts).
248+
* `SQLITE_CHANGESET_ABORT`: Abort on conflict and roll back the database.
249+
250+
When an error is thrown in the conflict handler or when any other value is returned from the handler,
251+
applying the changeset is aborted and the database is rolled back.
252+
253+
**Default**: A function that returns `SQLITE_CHANGESET_ABORT`.
237254
* Returns: {boolean} Whether the changeset was applied succesfully without being aborted.
238255

239256
An exception is thrown if the database is not
@@ -486,9 +503,42 @@ An object containing commonly used constants for SQLite operations.
486503

487504
The following constants are exported by the `sqlite.constants` object.
488505

489-
#### Conflict-resolution constants
506+
#### Conflict resolution constants
507+
508+
One of the following constants is available as an argument to the `onConflict`
509+
conflict resolution handler passed to [`database.applyChangeset()`][]. See also
510+
[Constants Passed To The Conflict Handler][] in the SQLite documentation.
511+
512+
<table>
513+
<tr>
514+
<th>Constant</th>
515+
<th>Description</th>
516+
</tr>
517+
<tr>
518+
<td><code>SQLITE_CHANGESET_DATA</code></td>
519+
<td>The conflict handler is invoked with this constant when processing a DELETE or UPDATE change if a row with the required PRIMARY KEY fields is present in the database, but one or more other (non primary-key) fields modified by the update do not contain the expected "before" values.</td>
520+
</tr>
521+
<tr>
522+
<td><code>SQLITE_CHANGESET_NOTFOUND</code></td>
523+
<td>The conflict handler is invoked with this constant when processing a DELETE or UPDATE change if a row with the required PRIMARY KEY fields is not present in the database.</td>
524+
</tr>
525+
<tr>
526+
<td><code>SQLITE_CHANGESET_CONFLICT</code></td>
527+
<td>This constant is passed to the conflict handler while processing an INSERT change if the operation would result in duplicate primary key values.</td>
528+
</tr>
529+
<tr>
530+
<td><code>SQLITE_CHANGESET_CONSTRAINT</code></td>
531+
<td>If foreign key handling is enabled, and applying a changeset leaves the database in a state containing foreign key violations, the conflict handler is invoked with this constant exactly once before the changeset is committed. If the conflict handler returns <code>SQLITE_CHANGESET_OMIT</code>, the changes, including those that caused the foreign key constraint violation, are committed. Or, if it returns <code>SQLITE_CHANGESET_ABORT</code>, the changeset is rolled back.</td>
532+
</tr>
533+
<tr>
534+
<td><code>SQLITE_CHANGESET_FOREIGN_KEY</code></td>
535+
<td>If any other constraint violation occurs while applying a change (i.e. a UNIQUE, CHECK or NOT NULL constraint), the conflict handler is invoked with this constant.</td>
536+
</tr>
537+
</table>
490538

491-
The following constants are meant for use with [`database.applyChangeset()`](#databaseapplychangesetchangeset-options).
539+
One of the following constants must be returned from the `onConflict` conflict
540+
resolution handler passed to [`database.applyChangeset()`][]. See also
541+
[Constants Returned From The Conflict Handler][] in the SQLite documentation.
492542

493543
<table>
494544
<tr>
@@ -501,7 +551,7 @@ The following constants are meant for use with [`database.applyChangeset()`](#da
501551
</tr>
502552
<tr>
503553
<td><code>SQLITE_CHANGESET_REPLACE</code></td>
504-
<td>Conflicting changes replace existing values.</td>
554+
<td>Conflicting changes replace existing values. Note that this value can only be returned when the type of conflict is either <code>SQLITE_CHANGESET_DATA</code> or <code>SQLITE_CHANGESET_CONFLICT</code>.</td>
505555
</tr>
506556
<tr>
507557
<td><code>SQLITE_CHANGESET_ABORT</code></td>
@@ -510,11 +560,14 @@ The following constants are meant for use with [`database.applyChangeset()`](#da
510560
</table>
511561

512562
[Changesets and Patchsets]: https://www.sqlite.org/sessionintro.html#changesets_and_patchsets
563+
[Constants Passed To The Conflict Handler]: https://www.sqlite.org/session/c_changeset_conflict.html
564+
[Constants Returned From The Conflict Handler]: https://www.sqlite.org/session/c_changeset_abort.html
513565
[SQL injection]: https://en.wikipedia.org/wiki/SQL_injection
514566
[`ATTACH DATABASE`]: https://www.sqlite.org/lang_attach.html
515567
[`PRAGMA foreign_keys`]: https://www.sqlite.org/pragma.html#pragma_foreign_keys
516568
[`SQLITE_DETERMINISTIC`]: https://www.sqlite.org/c3ref/c_deterministic.html
517569
[`SQLITE_DIRECTONLY`]: https://www.sqlite.org/c3ref/c_deterministic.html
570+
[`database.applyChangeset()`]: #databaseapplychangesetchangeset-options
518571
[`sqlite3_changes64()`]: https://www.sqlite.org/c3ref/changes.html
519572
[`sqlite3_close_v2()`]: https://www.sqlite.org/c3ref/close.html
520573
[`sqlite3_create_function_v2()`]: https://www.sqlite.org/c3ref/create_function.html

src/env_properties.h

+4-1
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,13 @@
144144
V(entry_type_string, "entryType") \
145145
V(env_pairs_string, "envPairs") \
146146
V(env_var_settings_string, "envVarSettings") \
147+
V(err_sqlite_error_string, "ERR_SQLITE_ERROR") \
148+
V(errcode_string, "errcode") \
147149
V(errno_string, "errno") \
148150
V(error_string, "error") \
149-
V(events, "events") \
151+
V(errstr_string, "errstr") \
150152
V(events_waiting, "eventsWaiting") \
153+
V(events, "events") \
151154
V(exchange_string, "exchange") \
152155
V(expire_string, "expire") \
153156
V(exponent_string, "exponent") \

src/node_sqlite.cc

+51-15
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ using v8::Number;
4242
using v8::Object;
4343
using v8::SideEffectType;
4444
using v8::String;
45+
using v8::TryCatch;
4546
using v8::Uint8Array;
4647
using v8::Value;
4748

@@ -66,13 +67,14 @@ inline MaybeLocal<Object> CreateSQLiteError(Isolate* isolate,
6667
const char* message) {
6768
Local<String> js_msg;
6869
Local<Object> e;
70+
Environment* env = Environment::GetCurrent(isolate);
6971
if (!String::NewFromUtf8(isolate, message).ToLocal(&js_msg) ||
7072
!Exception::Error(js_msg)
7173
->ToObject(isolate->GetCurrentContext())
7274
.ToLocal(&e) ||
7375
e->Set(isolate->GetCurrentContext(),
74-
OneByteString(isolate, "code"),
75-
OneByteString(isolate, "ERR_SQLITE_ERROR"))
76+
env->code_string(),
77+
env->err_sqlite_error_string())
7678
.IsNothing()) {
7779
return MaybeLocal<Object>();
7880
}
@@ -85,15 +87,14 @@ inline MaybeLocal<Object> CreateSQLiteError(Isolate* isolate, sqlite3* db) {
8587
const char* errmsg = sqlite3_errmsg(db);
8688
Local<String> js_errmsg;
8789
Local<Object> e;
90+
Environment* env = Environment::GetCurrent(isolate);
8891
if (!String::NewFromUtf8(isolate, errstr).ToLocal(&js_errmsg) ||
8992
!CreateSQLiteError(isolate, errmsg).ToLocal(&e) ||
9093
e->Set(isolate->GetCurrentContext(),
91-
OneByteString(isolate, "errcode"),
94+
env->errcode_string(),
9295
Integer::New(isolate, errcode))
9396
.IsNothing() ||
94-
e->Set(isolate->GetCurrentContext(),
95-
OneByteString(isolate, "errstr"),
96-
js_errmsg)
97+
e->Set(isolate->GetCurrentContext(), env->errstr_string(), js_errmsg)
9798
.IsNothing()) {
9899
return MaybeLocal<Object>();
99100
}
@@ -114,6 +115,19 @@ inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, const char* message) {
114115
}
115116
}
116117

118+
inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, int errcode) {
119+
const char* errstr = sqlite3_errstr(errcode);
120+
121+
Environment* env = Environment::GetCurrent(isolate);
122+
auto error = CreateSQLiteError(isolate, errstr).ToLocalChecked();
123+
error
124+
->Set(isolate->GetCurrentContext(),
125+
env->errcode_string(),
126+
Integer::New(isolate, errcode))
127+
.ToChecked();
128+
isolate->ThrowException(error);
129+
}
130+
117131
class UserDefinedFunction {
118132
public:
119133
explicit UserDefinedFunction(Environment* env,
@@ -731,11 +745,11 @@ void DatabaseSync::CreateSession(const FunctionCallbackInfo<Value>& args) {
731745

732746
// the reason for using static functions here is that SQLite needs a
733747
// function pointer
734-
static std::function<int()> conflictCallback;
748+
static std::function<int(int)> conflictCallback;
735749

736750
static int xConflict(void* pCtx, int eConflict, sqlite3_changeset_iter* pIter) {
737751
if (!conflictCallback) return SQLITE_CHANGESET_ABORT;
738-
return conflictCallback();
752+
return conflictCallback(eConflict);
739753
}
740754

741755
static std::function<bool(std::string)> filterCallback;
@@ -773,15 +787,27 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo<Value>& args) {
773787
options->Get(env->context(), env->onconflict_string()).ToLocalChecked();
774788

775789
if (!conflictValue->IsUndefined()) {
776-
if (!conflictValue->IsNumber()) {
790+
if (!conflictValue->IsFunction()) {
777791
THROW_ERR_INVALID_ARG_TYPE(
778792
env->isolate(),
779-
"The \"options.onConflict\" argument must be a number.");
793+
"The \"options.onConflict\" argument must be a function.");
780794
return;
781795
}
782-
783-
int conflictInt = conflictValue->Int32Value(env->context()).FromJust();
784-
conflictCallback = [conflictInt]() -> int { return conflictInt; };
796+
Local<Function> conflictFunc = conflictValue.As<Function>();
797+
conflictCallback = [env, conflictFunc](int conflictType) -> int {
798+
Local<Value> argv[] = {Integer::New(env->isolate(), conflictType)};
799+
TryCatch try_catch(env->isolate());
800+
Local<Value> result =
801+
conflictFunc->Call(env->context(), Null(env->isolate()), 1, argv)
802+
.FromMaybe(Local<Value>());
803+
if (try_catch.HasCaught()) {
804+
try_catch.ReThrow();
805+
return SQLITE_CHANGESET_ABORT;
806+
}
807+
constexpr auto invalid_value = -1;
808+
if (!result->IsInt32()) return invalid_value;
809+
return result->Int32Value(env->context()).FromJust();
810+
};
785811
}
786812

787813
if (options->HasOwnProperty(env->context(), env->filter_string())
@@ -819,12 +845,16 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo<Value>& args) {
819845
xFilter,
820846
xConflict,
821847
nullptr);
848+
if (r == SQLITE_OK) {
849+
args.GetReturnValue().Set(true);
850+
return;
851+
}
822852
if (r == SQLITE_ABORT) {
853+
// this is not an error, return false
823854
args.GetReturnValue().Set(false);
824855
return;
825856
}
826-
CHECK_ERROR_OR_THROW(env->isolate(), db->connection_, r, SQLITE_OK, void());
827-
args.GetReturnValue().Set(true);
857+
THROW_ERR_SQLITE_ERROR(env->isolate(), r);
828858
}
829859

830860
void DatabaseSync::EnableLoadExtension(
@@ -1662,6 +1692,12 @@ void DefineConstants(Local<Object> target) {
16621692
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_OMIT);
16631693
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_REPLACE);
16641694
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_ABORT);
1695+
1696+
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_DATA);
1697+
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_NOTFOUND);
1698+
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_CONFLICT);
1699+
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_CONSTRAINT);
1700+
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_FOREIGN_KEY);
16651701
}
16661702

16671703
static void Initialize(Local<Object> target,

0 commit comments

Comments
 (0)