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

Lines changed: 60 additions & 7 deletions
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

Lines changed: 4 additions & 1 deletion
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

Lines changed: 51 additions & 15 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy