diff --git a/lib/database.js b/lib/database.js index 14e057392..13f74c132 100644 --- a/lib/database.js +++ b/lib/database.js @@ -81,6 +81,9 @@ Database.prototype.function = require('./methods/function'); Database.prototype.aggregate = require('./methods/aggregate'); Database.prototype.table = require('./methods/table'); Database.prototype.loadExtension = wrappers.loadExtension; +Database.prototype.updateHook = wrappers.updateHook; +Database.prototype.commitHook = wrappers.commitHook; +Database.prototype.rollbackHook = wrappers.rollbackHook; Database.prototype.exec = wrappers.exec; Database.prototype.close = wrappers.close; Database.prototype.defaultSafeIntegers = wrappers.defaultSafeIntegers; diff --git a/lib/methods/wrappers.js b/lib/methods/wrappers.js index 1d4673e58..4924c9558 100644 --- a/lib/methods/wrappers.js +++ b/lib/methods/wrappers.js @@ -20,6 +20,21 @@ exports.loadExtension = function loadExtension(...args) { return this; }; +exports.updateHook = function updateHook(...args) { + this[cppdb].updateHook(...args); + return this; +}; + +exports.commitHook = function commitHook(...args) { + this[cppdb].commitHook(...args); + return this; +}; + +exports.rollbackHook = function rollbackHook(...args) { + this[cppdb].rollbackHook(...args); + return this; +}; + exports.defaultSafeIntegers = function defaultSafeIntegers(...args) { this[cppdb].defaultSafeIntegers(...args); return this; diff --git a/src/better_sqlite3.cpp b/src/better_sqlite3.cpp index 8fe2c6828..68689cba0 100644 --- a/src/better_sqlite3.cpp +++ b/src/better_sqlite3.cpp @@ -2,7 +2,7 @@ // #include "better_sqlite3.hpp" -#line 154 "./src/util/macros.lzz" +#line 165 "./src/util/macros.lzz" void SetPrototypeGetter( v8::Isolate* isolate, v8::Local data, @@ -30,7 +30,7 @@ void SetPrototypeGetter( ); #endif } -#line 184 "./src/util/macros.lzz" +#line 195 "./src/util/macros.lzz" #ifndef V8_COMPRESS_POINTERS_IN_SHARED_CAGE #define SAFE_NEW_BUFFER(env, data, length, finalizeCallback, finalizeHint) node::Buffer::New(env, data, length, finalizeCallback, finalizeHint) #else @@ -118,18 +118,18 @@ void ThrowRangeError (char const * message) #line 40 "./src/util/macros.lzz" { v8 :: Isolate * isolate = v8 :: Isolate :: GetCurrent ( ) ; isolate->ThrowException(v8::Exception::RangeError(StringFromUtf8(isolate, message, -1))); } -#line 106 "./src/util/macros.lzz" +#line 117 "./src/util/macros.lzz" v8::Local NewConstructorTemplate (v8::Isolate * isolate, v8::Local data, v8::FunctionCallback func, char const * name) -#line 111 "./src/util/macros.lzz" +#line 122 "./src/util/macros.lzz" { v8::Local t = v8::FunctionTemplate::New(isolate, func, data); t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(InternalizedFromLatin1(isolate, name)); return t; } -#line 117 "./src/util/macros.lzz" +#line 128 "./src/util/macros.lzz" void SetPrototypeMethod (v8::Isolate * isolate, v8::Local data, v8::Local recv, char const * name, v8::FunctionCallback func) -#line 123 "./src/util/macros.lzz" +#line 134 "./src/util/macros.lzz" { v8::HandleScope scope(isolate); recv->PrototypeTemplate()->Set( @@ -137,9 +137,9 @@ void SetPrototypeMethod (v8::Isolate * isolate, v8::Local data, v v8::FunctionTemplate::New(isolate, func, data, v8::Signature::New(isolate, recv)) ); } -#line 130 "./src/util/macros.lzz" +#line 141 "./src/util/macros.lzz" void SetPrototypeSymbolMethod (v8::Isolate * isolate, v8::Local data, v8::Local recv, v8::Local symbol, v8::FunctionCallback func) -#line 136 "./src/util/macros.lzz" +#line 147 "./src/util/macros.lzz" { v8::HandleScope scope(isolate); recv->PrototypeTemplate()->Set( @@ -156,8 +156,16 @@ v8::Local CS::Code (v8::Isolate * isolate, int code) return StringFromUtf8(isolate, (std::string("UNKNOWN_SQLITE_ERROR_") + std::to_string(code)).c_str(), -1); } #line 10 "./src/util/constants.lzz" -CS::CS (v8::Isolate * isolate) +v8::Local CS::Op (v8::Isolate * isolate, int op) #line 10 "./src/util/constants.lzz" + { + auto element = ops.find(op); + if (element != ops.end()) return element->second.Get(isolate); + return StringFromUtf8(isolate, (std::string("UNKNOWN_SQLITE_OP_") + std::to_string(op)).c_str(), -1); +} +#line 16 "./src/util/constants.lzz" +CS::CS (v8::Isolate * isolate) +#line 16 "./src/util/constants.lzz" { SetString(isolate, database, "database"); SetString(isolate, reader, "reader"); @@ -286,21 +294,33 @@ CS::CS (v8::Isolate * isolate) SetCode(isolate, SQLITE_AUTH_USER, "SQLITE_AUTH_USER"); SetCode(isolate, SQLITE_OK_LOAD_PERMANENTLY, "SQLITE_OK_LOAD_PERMANENTLY"); SetCode(isolate, SQLITE_OK_SYMLINK, "SQLITE_OK_SYMLINK"); + + SetOp(isolate, SQLITE_DELETE, "SQLITE_DELETE"); + SetOp(isolate, SQLITE_INSERT, "SQLITE_INSERT"); + SetOp(isolate, SQLITE_UPDATE, "SQLITE_UPDATE"); } -#line 161 "./src/util/constants.lzz" +#line 171 "./src/util/constants.lzz" void CS::SetString (v8::Isolate * isolate, v8::Global & constant, char const * str) -#line 161 "./src/util/constants.lzz" +#line 171 "./src/util/constants.lzz" { constant.Reset(isolate, InternalizedFromLatin1(isolate, str)); } -#line 165 "./src/util/constants.lzz" +#line 175 "./src/util/constants.lzz" void CS::SetCode (v8::Isolate * isolate, int code, char const * str) -#line 165 "./src/util/constants.lzz" +#line 175 "./src/util/constants.lzz" { codes.emplace(std::piecewise_construct, std::forward_as_tuple(code), std::forward_as_tuple(isolate, InternalizedFromLatin1(isolate, str))); } +#line 181 "./src/util/constants.lzz" +void CS::SetOp (v8::Isolate * isolate, int op, char const * str) +#line 181 "./src/util/constants.lzz" + { + ops.emplace(std::piecewise_construct, + std::forward_as_tuple(op), + std::forward_as_tuple(isolate, InternalizedFromLatin1(isolate, str))); +} #line 19 "./src/util/bind-map.lzz" BindMap::Pair::Pair (v8::Isolate * isolate, char const * name, int index) #line 20 "./src/util/bind-map.lzz" @@ -364,6 +384,9 @@ v8::Local Database::Init (v8::Isolate * isolate, v8::Local Database::Init (v8::Isolate * isolate, v8::Local GetFunction( isolate -> GetCurrentContext ( ) ).ToLocalChecked(); } -#line 24 "./src/objects/database.lzz" +#line 27 "./src/objects/database.lzz" bool Database::CompareDatabase::operator () (Database const * const a, Database const * const b) const -#line 24 "./src/objects/database.lzz" +#line 27 "./src/objects/database.lzz" { return a < b; } -#line 29 "./src/objects/database.lzz" +#line 32 "./src/objects/database.lzz" bool Database::CompareStatement::operator () (Statement const * const a, Statement const * const b) const -#line 29 "./src/objects/database.lzz" +#line 32 "./src/objects/database.lzz" { return Statement::Compare(a, b); } -#line 34 "./src/objects/database.lzz" +#line 37 "./src/objects/database.lzz" bool Database::CompareBackup::operator () (Backup const * const a, Backup const * const b) const -#line 34 "./src/objects/database.lzz" +#line 37 "./src/objects/database.lzz" { return Backup::Compare(a, b); } -#line 40 "./src/objects/database.lzz" +#line 43 "./src/objects/database.lzz" void Database::ThrowDatabaseError () -#line 40 "./src/objects/database.lzz" +#line 43 "./src/objects/database.lzz" { if (was_js_error) was_js_error = false; else ThrowSqliteError(addon, db_handle); } -#line 44 "./src/objects/database.lzz" +#line 47 "./src/objects/database.lzz" void Database::ThrowSqliteError (Addon * addon, sqlite3 * db_handle) -#line 44 "./src/objects/database.lzz" +#line 47 "./src/objects/database.lzz" { assert(db_handle != NULL); ThrowSqliteError(addon, sqlite3_errmsg(db_handle), sqlite3_extended_errcode(db_handle)); } -#line 48 "./src/objects/database.lzz" +#line 51 "./src/objects/database.lzz" void Database::ThrowSqliteError (Addon * addon, char const * message, int code) -#line 48 "./src/objects/database.lzz" +#line 51 "./src/objects/database.lzz" { assert(message != NULL); assert((code & 0xff) != SQLITE_OK); @@ -420,9 +443,9 @@ void Database::ThrowSqliteError (Addon * addon, char const * message, int code) ->NewInstance( isolate -> GetCurrentContext ( ) , 2, args) .ToLocalChecked()); } -#line 64 "./src/objects/database.lzz" +#line 67 "./src/objects/database.lzz" bool Database::Log (v8::Isolate * isolate, sqlite3_stmt * handle) -#line 64 "./src/objects/database.lzz" +#line 67 "./src/objects/database.lzz" { assert(was_js_error == false); if (!has_logger) return false; @@ -434,39 +457,142 @@ bool Database::Log (v8::Isolate * isolate, sqlite3_stmt * handle) if (expanded) sqlite3_free(expanded); return was_js_error; } -#line 107 "./src/objects/database.lzz" +#line 110 "./src/objects/database.lzz" void Database::CloseHandles () -#line 107 "./src/objects/database.lzz" +#line 110 "./src/objects/database.lzz" { if (open) { open = false; + for (Statement* stmt : stmts) stmt->CloseHandles(); for (Backup* backup : backups) backup->CloseHandles(); stmts.clear(); backups.clear(); + + UpdateHook* oldUpdateHook = static_cast(sqlite3_update_hook(db_handle, NULL, NULL)); + if (oldUpdateHook) { + delete oldUpdateHook; + } + CommitHook* oldCommitHook = static_cast(sqlite3_commit_hook(db_handle, NULL, NULL)); + if (oldCommitHook) { + delete oldCommitHook; + } + RollbackHook* oldRollbackHook = static_cast(sqlite3_rollback_hook(db_handle, NULL, NULL)); + if (oldRollbackHook) { + delete oldRollbackHook; + } + int status = sqlite3_close(db_handle); assert(status == SQLITE_OK); ((void)status); } } -#line 119 "./src/objects/database.lzz" +#line 137 "./src/objects/database.lzz" Database::~ Database () -#line 119 "./src/objects/database.lzz" +#line 137 "./src/objects/database.lzz" { if (open) addon->dbs.erase(this); CloseHandles(); } -#line 126 "./src/objects/database.lzz" +#line 146 "./src/objects/database.lzz" +Database::UpdateHook::UpdateHook (Addon * addon, v8::Isolate * isolate, Database * db, v8::Local fn) +#line 151 "./src/objects/database.lzz" + : addon (addon), isolate (isolate), db (db), fn (isolate, fn) +#line 155 "./src/objects/database.lzz" + {} +#line 157 "./src/objects/database.lzz" +void Database::UpdateHook::invoke (void * data, int op, char const * dbName, char const * tableName, sqlite3_int64 rowid) +#line 157 "./src/objects/database.lzz" + { + UpdateHook * self = static_cast(data); + + v8::Isolate * isolate = self->isolate; + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + + v8::Local context = isolate->GetCurrentContext(); + + v8::Local opArg = self->addon->cs.Op(isolate, op); + v8::Local dbNameArg = v8::String::NewFromUtf8(isolate, dbName, v8::NewStringType::kNormal).ToLocalChecked(); + v8::Local tableNameArg = v8::String::NewFromUtf8(isolate, tableName, v8::NewStringType::kNormal).ToLocalChecked(); + v8::Local rowidArg = v8::BigInt::New(isolate, rowid); + + const int argc = 4; + v8::Local argv[argc] = { opArg, dbNameArg, tableNameArg, rowidArg }; + + v8::MaybeLocal maybeReturnValue = self->fn.Get(isolate)->Call(context, v8::Undefined(isolate), argc, argv); + if (maybeReturnValue.IsEmpty()) { + self->db->GetState()->was_js_error = true; + } +} +#line 190 "./src/objects/database.lzz" +Database::CommitHook::CommitHook (v8::Isolate * isolate, Database * db, v8::Local fn) +#line 194 "./src/objects/database.lzz" + : isolate (isolate), db (db), fn (isolate, fn) +#line 197 "./src/objects/database.lzz" + {} +#line 199 "./src/objects/database.lzz" +int Database::CommitHook::invoke (void * data) +#line 199 "./src/objects/database.lzz" + { + CommitHook * self = static_cast(data); + + v8::Isolate * isolate = self->isolate; + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + + v8::Local context = isolate->GetCurrentContext(); + + const int argc = 0; + v8::Local argv[argc] = {}; + + v8::MaybeLocal maybeReturnValue = self->fn.Get(isolate)->Call(context, v8::Undefined(isolate), argc, argv); + v8::Local returnValue; + if (maybeReturnValue.ToLocal(&returnValue)) { + return returnValue->BooleanValue(isolate) ? 0 : 1; + } else { + self->db->GetState()->was_js_error = true; + return 1; + } +} +#line 230 "./src/objects/database.lzz" +Database::RollbackHook::RollbackHook (v8::Isolate * isolate, Database * db, v8::Local fn) +#line 234 "./src/objects/database.lzz" + : isolate (isolate), db (db), fn (isolate, fn) +#line 237 "./src/objects/database.lzz" + {} +#line 239 "./src/objects/database.lzz" +void Database::RollbackHook::invoke (void * data) +#line 239 "./src/objects/database.lzz" + { + RollbackHook * self = static_cast(data); + + v8::Isolate * isolate = self->isolate; + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + + v8::Local context = isolate->GetCurrentContext(); + + const int argc = 0; + v8::Local argv[argc] = {}; + + v8::TryCatch try_catch(isolate); + v8::MaybeLocal maybeReturnValue = self->fn.Get(isolate)->Call(context, v8::Undefined(isolate), argc, argv); + if (maybeReturnValue.IsEmpty()) { + self->db->GetState()->was_js_error = true; + } +} +#line 264 "./src/objects/database.lzz" Database::Database (v8::Isolate * isolate, Addon * addon, sqlite3 * db_handle, v8::Local logger) -#line 131 "./src/objects/database.lzz" +#line 269 "./src/objects/database.lzz" : node::ObjectWrap (), db_handle (db_handle), open (true), busy (false), safe_ints (false), unsafe_mode (false), was_js_error (false), has_logger (logger->IsFunction()), iterators (0), addon (addon), logger (isolate, logger), stmts (), backups () -#line 144 "./src/objects/database.lzz" +#line 282 "./src/objects/database.lzz" { assert(db_handle != NULL); addon->dbs.insert(this); } -#line 149 "./src/objects/database.lzz" +#line 287 "./src/objects/database.lzz" void Database::JS_new (v8::FunctionCallbackInfo const & info) -#line 149 "./src/objects/database.lzz" +#line 287 "./src/objects/database.lzz" { assert(info.IsConstructCall()); if ( info . Length ( ) <= ( 0 ) || ! info [ 0 ] -> IsString ( ) ) return ThrowTypeError ( "Expected " "first" " argument to be " "a string" ) ; v8 :: Local < v8 :: String > filename = ( info [ 0 ] . As < v8 :: String > ( ) ) ; @@ -518,9 +644,9 @@ void Database::JS_new (v8::FunctionCallbackInfo const & info) info.GetReturnValue().Set(info.This()); } -#line 201 "./src/objects/database.lzz" +#line 339 "./src/objects/database.lzz" void Database::JS_prepare (v8::FunctionCallbackInfo const & info) -#line 201 "./src/objects/database.lzz" +#line 339 "./src/objects/database.lzz" { if ( info . Length ( ) <= ( 0 ) || ! info [ 0 ] -> IsString ( ) ) return ThrowTypeError ( "Expected " "first" " argument to be " "a string" ) ; v8 :: Local < v8 :: String > source = ( info [ 0 ] . As < v8 :: String > ( ) ) ; if ( info . Length ( ) <= ( 1 ) || ! info [ 1 ] -> IsObject ( ) ) return ThrowTypeError ( "Expected " "second" " argument to be " "an object" ) ; v8 :: Local < v8 :: Object > database = ( info [ 1 ] . As < v8 :: Object > ( ) ) ; @@ -536,9 +662,9 @@ void Database::JS_prepare (v8::FunctionCallbackInfo const & info) addon->privileged_info = NULL; if (!maybeStatement.IsEmpty()) info.GetReturnValue().Set(maybeStatement.ToLocalChecked()); } -#line 217 "./src/objects/database.lzz" +#line 355 "./src/objects/database.lzz" void Database::JS_exec (v8::FunctionCallbackInfo const & info) -#line 217 "./src/objects/database.lzz" +#line 355 "./src/objects/database.lzz" { Database* db = node :: ObjectWrap :: Unwrap (info.This()); if ( info . Length ( ) <= ( 0 ) || ! info [ 0 ] -> IsString ( ) ) return ThrowTypeError ( "Expected " "first" " argument to be " "a string" ) ; v8 :: Local < v8 :: String > source = ( info [ 0 ] . As < v8 :: String > ( ) ) ; @@ -578,9 +704,9 @@ void Database::JS_exec (v8::FunctionCallbackInfo const & info) db->ThrowDatabaseError(); } } -#line 257 "./src/objects/database.lzz" +#line 395 "./src/objects/database.lzz" void Database::JS_backup (v8::FunctionCallbackInfo const & info) -#line 257 "./src/objects/database.lzz" +#line 395 "./src/objects/database.lzz" { if ( info . Length ( ) <= ( 0 ) || ! info [ 0 ] -> IsObject ( ) ) return ThrowTypeError ( "Expected " "first" " argument to be " "an object" ) ; v8 :: Local < v8 :: Object > database = ( info [ 0 ] . As < v8 :: Object > ( ) ) ; if ( info . Length ( ) <= ( 1 ) || ! info [ 1 ] -> IsString ( ) ) return ThrowTypeError ( "Expected " "second" " argument to be " "a string" ) ; v8 :: Local < v8 :: String > attachedName = ( info [ 1 ] . As < v8 :: String > ( ) ) ; @@ -598,9 +724,9 @@ void Database::JS_backup (v8::FunctionCallbackInfo const & info) addon->privileged_info = NULL; if (!maybeBackup.IsEmpty()) info.GetReturnValue().Set(maybeBackup.ToLocalChecked()); } -#line 275 "./src/objects/database.lzz" +#line 413 "./src/objects/database.lzz" void Database::JS_serialize (v8::FunctionCallbackInfo const & info) -#line 275 "./src/objects/database.lzz" +#line 413 "./src/objects/database.lzz" { Database* db = node :: ObjectWrap :: Unwrap (info.This()); if ( info . Length ( ) <= ( 0 ) || ! info [ 0 ] -> IsString ( ) ) return ThrowTypeError ( "Expected " "first" " argument to be " "a string" ) ; v8 :: Local < v8 :: String > attachedName = ( info [ 0 ] . As < v8 :: String > ( ) ) ; @@ -622,9 +748,9 @@ void Database::JS_serialize (v8::FunctionCallbackInfo const & info SAFE_NEW_BUFFER(isolate, reinterpret_cast(data), length, FreeSerialization, NULL).ToLocalChecked() ); } -#line 297 "./src/objects/database.lzz" +#line 435 "./src/objects/database.lzz" void Database::JS_function (v8::FunctionCallbackInfo const & info) -#line 297 "./src/objects/database.lzz" +#line 435 "./src/objects/database.lzz" { Database* db = node :: ObjectWrap :: Unwrap (info.This()); if ( info . Length ( ) <= ( 0 ) || ! info [ 0 ] -> IsFunction ( ) ) return ThrowTypeError ( "Expected " "first" " argument to be " "a function" ) ; v8 :: Local < v8 :: Function > fn = ( info [ 0 ] . As < v8 :: Function > ( ) ) ; @@ -648,9 +774,9 @@ void Database::JS_function (v8::FunctionCallbackInfo const & info) db->ThrowDatabaseError(); } } -#line 321 "./src/objects/database.lzz" +#line 459 "./src/objects/database.lzz" void Database::JS_aggregate (v8::FunctionCallbackInfo const & info) -#line 321 "./src/objects/database.lzz" +#line 459 "./src/objects/database.lzz" { Database* db = node :: ObjectWrap :: Unwrap (info.This()); if ( info . Length ( ) <= ( 0 ) ) return ThrowTypeError ( "Expected a " "first" " argument" ) ; v8 :: Local < v8 :: Value > start = info [ 0 ] ; @@ -679,9 +805,9 @@ void Database::JS_aggregate (v8::FunctionCallbackInfo const & info db->ThrowDatabaseError(); } } -#line 350 "./src/objects/database.lzz" +#line 488 "./src/objects/database.lzz" void Database::JS_table (v8::FunctionCallbackInfo const & info) -#line 350 "./src/objects/database.lzz" +#line 488 "./src/objects/database.lzz" { Database* db = node :: ObjectWrap :: Unwrap (info.This()); if ( info . Length ( ) <= ( 0 ) || ! info [ 0 ] -> IsFunction ( ) ) return ThrowTypeError ( "Expected " "first" " argument to be " "a function" ) ; v8 :: Local < v8 :: Function > factory = ( info [ 0 ] . As < v8 :: Function > ( ) ) ; @@ -701,9 +827,9 @@ void Database::JS_table (v8::FunctionCallbackInfo const & info) } db->busy = false; } -#line 370 "./src/objects/database.lzz" +#line 508 "./src/objects/database.lzz" void Database::JS_loadExtension (v8::FunctionCallbackInfo const & info) -#line 370 "./src/objects/database.lzz" +#line 508 "./src/objects/database.lzz" { Database* db = node :: ObjectWrap :: Unwrap (info.This()); v8::Local entryPoint; @@ -725,9 +851,79 @@ void Database::JS_loadExtension (v8::FunctionCallbackInfo const & } sqlite3_free(error); } -#line 392 "./src/objects/database.lzz" +#line 530 "./src/objects/database.lzz" +void Database::JS_updateHook (v8::FunctionCallbackInfo const & info) +#line 530 "./src/objects/database.lzz" + { + Database* db = node :: ObjectWrap :: Unwrap (info.This()); + v8 :: MaybeLocal < v8 :: Function > fn = ( { bool isSupplied = info . Length ( ) > ( 0 ) && ! info [ 0 ] -> IsNullOrUndefined ( ) ; if ( isSupplied && ! info [ 0 ] -> IsFunction ( ) ) return ThrowTypeError ( "Expected " "first" " argument to be " "a function" ", null, or undefined" ) ; ( isSupplied ? info [ 0 ] . As < v8 :: Function > ( ) : v8 :: MaybeLocal < v8 :: Function > ( ) ) ; } ) ; + if ( ! db -> open ) return ThrowTypeError ( "The database connection is not open" ) ; + if ( db -> busy ) return ThrowTypeError ( "This database connection is busy executing a query" ) ; + if ( db -> iterators ) return ThrowTypeError ( "This database connection is busy executing a query" ) ; + + Addon * addon = static_cast < Addon * > ( info . Data ( ) . As < v8 :: External > ( ) -> Value ( ) ) ; + v8 :: Isolate * isolate = info . GetIsolate ( ) ; + + UpdateHook* oldUpdateHook = static_cast( + sqlite3_update_hook( + db->db_handle, + fn.IsEmpty() ? NULL : UpdateHook::invoke, + fn.IsEmpty() ? NULL : new UpdateHook(addon, isolate, db, fn.ToLocalChecked()) + ) + ); + if (oldUpdateHook) { + delete oldUpdateHook; + } +} +#line 552 "./src/objects/database.lzz" +void Database::JS_commitHook (v8::FunctionCallbackInfo const & info) +#line 552 "./src/objects/database.lzz" + { + Database* db = node :: ObjectWrap :: Unwrap (info.This()); + v8 :: MaybeLocal < v8 :: Function > fn = ( { bool isSupplied = info . Length ( ) > ( 0 ) && ! info [ 0 ] -> IsNullOrUndefined ( ) ; if ( isSupplied && ! info [ 0 ] -> IsFunction ( ) ) return ThrowTypeError ( "Expected " "first" " argument to be " "a function" ", null, or undefined" ) ; ( isSupplied ? info [ 0 ] . As < v8 :: Function > ( ) : v8 :: MaybeLocal < v8 :: Function > ( ) ) ; } ) ; + if ( ! db -> open ) return ThrowTypeError ( "The database connection is not open" ) ; + if ( db -> busy ) return ThrowTypeError ( "This database connection is busy executing a query" ) ; + if ( db -> iterators ) return ThrowTypeError ( "This database connection is busy executing a query" ) ; + + v8 :: Isolate * isolate = info . GetIsolate ( ) ; + + CommitHook* oldCommitHook = static_cast( + sqlite3_commit_hook( + db->db_handle, + fn.IsEmpty() ? NULL : CommitHook::invoke, + fn.IsEmpty() ? NULL : new CommitHook(isolate, db, fn.ToLocalChecked()) + ) + ); + if (oldCommitHook) { + delete oldCommitHook; + } +} +#line 573 "./src/objects/database.lzz" +void Database::JS_rollbackHook (v8::FunctionCallbackInfo const & info) +#line 573 "./src/objects/database.lzz" + { + Database* db = node :: ObjectWrap :: Unwrap (info.This()); + v8 :: MaybeLocal < v8 :: Function > fn = ( { bool isSupplied = info . Length ( ) > ( 0 ) && ! info [ 0 ] -> IsNullOrUndefined ( ) ; if ( isSupplied && ! info [ 0 ] -> IsFunction ( ) ) return ThrowTypeError ( "Expected " "first" " argument to be " "a function" ", null, or undefined" ) ; ( isSupplied ? info [ 0 ] . As < v8 :: Function > ( ) : v8 :: MaybeLocal < v8 :: Function > ( ) ) ; } ) ; + if ( ! db -> open ) return ThrowTypeError ( "The database connection is not open" ) ; + if ( db -> busy ) return ThrowTypeError ( "This database connection is busy executing a query" ) ; + if ( db -> iterators ) return ThrowTypeError ( "This database connection is busy executing a query" ) ; + + v8 :: Isolate * isolate = info . GetIsolate ( ) ; + + RollbackHook* oldRollbackHook = static_cast( + sqlite3_rollback_hook( + db->db_handle, + fn.IsEmpty() ? NULL : RollbackHook::invoke, + fn.IsEmpty() ? NULL : new RollbackHook(isolate, db, fn.ToLocalChecked()) + ) + ); + if (oldRollbackHook) { + delete oldRollbackHook; + } +} +#line 594 "./src/objects/database.lzz" void Database::JS_close (v8::FunctionCallbackInfo const & info) -#line 392 "./src/objects/database.lzz" +#line 594 "./src/objects/database.lzz" { Database* db = node :: ObjectWrap :: Unwrap (info.This()); if (db->open) { @@ -737,39 +933,39 @@ void Database::JS_close (v8::FunctionCallbackInfo const & info) db->CloseHandles(); } } -#line 402 "./src/objects/database.lzz" +#line 604 "./src/objects/database.lzz" void Database::JS_defaultSafeIntegers (v8::FunctionCallbackInfo const & info) -#line 402 "./src/objects/database.lzz" +#line 604 "./src/objects/database.lzz" { Database* db = node :: ObjectWrap :: Unwrap (info.This()); if (info.Length() == 0) db->safe_ints = true; else { if ( info . Length ( ) <= ( 0 ) || ! info [ 0 ] -> IsBoolean ( ) ) return ThrowTypeError ( "Expected " "first" " argument to be " "a boolean" ) ; db -> safe_ints = ( info [ 0 ] . As < v8 :: Boolean > ( ) ) -> Value ( ) ; } } -#line 408 "./src/objects/database.lzz" +#line 610 "./src/objects/database.lzz" void Database::JS_unsafeMode (v8::FunctionCallbackInfo const & info) -#line 408 "./src/objects/database.lzz" +#line 610 "./src/objects/database.lzz" { Database* db = node :: ObjectWrap :: Unwrap (info.This()); if (info.Length() == 0) db->unsafe_mode = true; else { if ( info . Length ( ) <= ( 0 ) || ! info [ 0 ] -> IsBoolean ( ) ) return ThrowTypeError ( "Expected " "first" " argument to be " "a boolean" ) ; db -> unsafe_mode = ( info [ 0 ] . As < v8 :: Boolean > ( ) ) -> Value ( ) ; } sqlite3_db_config(db->db_handle, SQLITE_DBCONFIG_DEFENSIVE, static_cast(!db->unsafe_mode), NULL); } -#line 415 "./src/objects/database.lzz" +#line 617 "./src/objects/database.lzz" void Database::JS_open (v8::Local _, v8::PropertyCallbackInfo const & info) -#line 415 "./src/objects/database.lzz" +#line 617 "./src/objects/database.lzz" { info.GetReturnValue().Set( node :: ObjectWrap :: Unwrap (info.This())->open); } -#line 419 "./src/objects/database.lzz" +#line 621 "./src/objects/database.lzz" void Database::JS_inTransaction (v8::Local _, v8::PropertyCallbackInfo const & info) -#line 419 "./src/objects/database.lzz" +#line 621 "./src/objects/database.lzz" { Database* db = node :: ObjectWrap :: Unwrap (info.This()); info.GetReturnValue().Set(db->open && !static_cast(sqlite3_get_autocommit(db->db_handle))); } -#line 424 "./src/objects/database.lzz" +#line 626 "./src/objects/database.lzz" bool Database::Deserialize (v8::Local buffer, Addon * addon, sqlite3 * db_handle, bool readonly) -#line 424 "./src/objects/database.lzz" +#line 626 "./src/objects/database.lzz" { size_t length = node::Buffer::Length(buffer); unsigned char* data = (unsigned char*)sqlite3_malloc64(length); @@ -794,15 +990,15 @@ bool Database::Deserialize (v8::Local buffer, Addon * addon, sqlite return true; } -#line 449 "./src/objects/database.lzz" +#line 651 "./src/objects/database.lzz" void Database::FreeSerialization (char * data, void * _) -#line 449 "./src/objects/database.lzz" +#line 651 "./src/objects/database.lzz" { sqlite3_free(data); } -#line 453 "./src/objects/database.lzz" +#line 655 "./src/objects/database.lzz" int const Database::MAX_BUFFER_SIZE; -#line 454 "./src/objects/database.lzz" +#line 656 "./src/objects/database.lzz" int const Database::MAX_STRING_SIZE; #line 4 "./src/objects/statement.lzz" v8::Local Statement::Init (v8::Isolate * isolate, v8::Local data) diff --git a/src/better_sqlite3.hpp b/src/better_sqlite3.hpp index 27ae83b0e..af1655662 100644 --- a/src/better_sqlite3.hpp +++ b/src/better_sqlite3.hpp @@ -16,7 +16,7 @@ #include #include #include -#line 145 "./src/util/macros.lzz" +#line 156 "./src/util/macros.lzz" void SetPrototypeGetter( v8::Isolate* isolate, v8::Local data, @@ -43,21 +43,21 @@ void ThrowError (char const * message); void ThrowTypeError (char const * message); #line 40 "./src/util/macros.lzz" void ThrowRangeError (char const * message); -#line 92 "./src/util/macros.lzz" +#line 103 "./src/util/macros.lzz" bool IS_SKIPPED (char c); -#line 97 "./src/util/macros.lzz" +#line 108 "./src/util/macros.lzz" template -#line 97 "./src/util/macros.lzz" +#line 108 "./src/util/macros.lzz" T * ALLOC_ARRAY (size_t count); -#line 102 "./src/util/macros.lzz" +#line 113 "./src/util/macros.lzz" template -#line 102 "./src/util/macros.lzz" +#line 113 "./src/util/macros.lzz" void FREE_ARRAY (T * array_pointer); -#line 106 "./src/util/macros.lzz" -v8::Local NewConstructorTemplate (v8::Isolate * isolate, v8::Local data, v8::FunctionCallback func, char const * name); #line 117 "./src/util/macros.lzz" +v8::Local NewConstructorTemplate (v8::Isolate * isolate, v8::Local data, v8::FunctionCallback func, char const * name); +#line 128 "./src/util/macros.lzz" void SetPrototypeMethod (v8::Isolate * isolate, v8::Local data, v8::Local recv, char const * name, v8::FunctionCallback func); -#line 130 "./src/util/macros.lzz" +#line 141 "./src/util/macros.lzz" void SetPrototypeSymbolMethod (v8::Isolate * isolate, v8::Local data, v8::Local recv, v8::Local symbol, v8::FunctionCallback func); #line 1 "./src/util/constants.lzz" class CS @@ -67,51 +67,57 @@ class CS #line 4 "./src/util/constants.lzz" v8::Local Code (v8::Isolate * isolate, int code); #line 10 "./src/util/constants.lzz" + v8::Local Op (v8::Isolate * isolate, int op); +#line 16 "./src/util/constants.lzz" explicit CS (v8::Isolate * isolate); -#line 140 "./src/util/constants.lzz" +#line 150 "./src/util/constants.lzz" v8::Global database; -#line 141 "./src/util/constants.lzz" +#line 151 "./src/util/constants.lzz" v8::Global reader; -#line 142 "./src/util/constants.lzz" +#line 152 "./src/util/constants.lzz" v8::Global source; -#line 143 "./src/util/constants.lzz" +#line 153 "./src/util/constants.lzz" v8::Global memory; -#line 144 "./src/util/constants.lzz" +#line 154 "./src/util/constants.lzz" v8::Global readonly; -#line 145 "./src/util/constants.lzz" +#line 155 "./src/util/constants.lzz" v8::Global name; -#line 146 "./src/util/constants.lzz" +#line 156 "./src/util/constants.lzz" v8::Global next; -#line 147 "./src/util/constants.lzz" +#line 157 "./src/util/constants.lzz" v8::Global length; -#line 148 "./src/util/constants.lzz" +#line 158 "./src/util/constants.lzz" v8::Global done; -#line 149 "./src/util/constants.lzz" +#line 159 "./src/util/constants.lzz" v8::Global value; -#line 150 "./src/util/constants.lzz" +#line 160 "./src/util/constants.lzz" v8::Global changes; -#line 151 "./src/util/constants.lzz" +#line 161 "./src/util/constants.lzz" v8::Global lastInsertRowid; -#line 152 "./src/util/constants.lzz" +#line 162 "./src/util/constants.lzz" v8::Global statement; -#line 153 "./src/util/constants.lzz" +#line 163 "./src/util/constants.lzz" v8::Global column; -#line 154 "./src/util/constants.lzz" +#line 164 "./src/util/constants.lzz" v8::Global table; -#line 155 "./src/util/constants.lzz" +#line 165 "./src/util/constants.lzz" v8::Global type; -#line 156 "./src/util/constants.lzz" +#line 166 "./src/util/constants.lzz" v8::Global totalPages; -#line 157 "./src/util/constants.lzz" +#line 167 "./src/util/constants.lzz" v8::Global remainingPages; -#line 159 "./src/util/constants.lzz" +#line 169 "./src/util/constants.lzz" private: -#line 161 "./src/util/constants.lzz" +#line 171 "./src/util/constants.lzz" static void SetString (v8::Isolate * isolate, v8::Global & constant, char const * str); -#line 165 "./src/util/constants.lzz" +#line 175 "./src/util/constants.lzz" void SetCode (v8::Isolate * isolate, int code, char const * str); -#line 171 "./src/util/constants.lzz" +#line 181 "./src/util/constants.lzz" + void SetOp (v8::Isolate * isolate, int op, char const * str); +#line 187 "./src/util/constants.lzz" std::unordered_map > codes; +#line 188 "./src/util/constants.lzz" + std::unordered_map > ops; }; #line 1 "./src/util/bind-map.lzz" class BindMap @@ -174,139 +180,201 @@ class Database : public node::ObjectWrap public: #line 4 "./src/objects/database.lzz" static v8::Local Init (v8::Isolate * isolate, v8::Local data); -#line 23 "./src/objects/database.lzz" +#line 26 "./src/objects/database.lzz" class CompareDatabase { -#line 23 "./src/objects/database.lzz" +#line 26 "./src/objects/database.lzz" public: -#line 24 "./src/objects/database.lzz" +#line 27 "./src/objects/database.lzz" bool operator () (Database const * const a, Database const * const b) const; }; -#line 28 "./src/objects/database.lzz" +#line 31 "./src/objects/database.lzz" class CompareStatement { -#line 28 "./src/objects/database.lzz" +#line 31 "./src/objects/database.lzz" public: -#line 29 "./src/objects/database.lzz" +#line 32 "./src/objects/database.lzz" bool operator () (Statement const * const a, Statement const * const b) const; }; -#line 33 "./src/objects/database.lzz" +#line 36 "./src/objects/database.lzz" class CompareBackup { -#line 33 "./src/objects/database.lzz" +#line 36 "./src/objects/database.lzz" public: -#line 34 "./src/objects/database.lzz" +#line 37 "./src/objects/database.lzz" bool operator () (Backup const * const a, Backup const * const b) const; }; -#line 40 "./src/objects/database.lzz" +#line 43 "./src/objects/database.lzz" void ThrowDatabaseError (); -#line 44 "./src/objects/database.lzz" +#line 47 "./src/objects/database.lzz" static void ThrowSqliteError (Addon * addon, sqlite3 * db_handle); -#line 48 "./src/objects/database.lzz" +#line 51 "./src/objects/database.lzz" static void ThrowSqliteError (Addon * addon, char const * message, int code); -#line 64 "./src/objects/database.lzz" +#line 67 "./src/objects/database.lzz" bool Log (v8::Isolate * isolate, sqlite3_stmt * handle); -#line 77 "./src/objects/database.lzz" +#line 80 "./src/objects/database.lzz" void AddStatement (Statement * stmt); -#line 78 "./src/objects/database.lzz" - void RemoveStatement (Statement * stmt); #line 81 "./src/objects/database.lzz" + void RemoveStatement (Statement * stmt); +#line 84 "./src/objects/database.lzz" void AddBackup (Backup * backup); -#line 82 "./src/objects/database.lzz" +#line 85 "./src/objects/database.lzz" void RemoveBackup (Backup * backup); -#line 86 "./src/objects/database.lzz" +#line 89 "./src/objects/database.lzz" struct State { -#line 87 "./src/objects/database.lzz" +#line 90 "./src/objects/database.lzz" bool const open; -#line 88 "./src/objects/database.lzz" +#line 91 "./src/objects/database.lzz" bool busy; -#line 89 "./src/objects/database.lzz" +#line 92 "./src/objects/database.lzz" bool const safe_ints; -#line 90 "./src/objects/database.lzz" +#line 93 "./src/objects/database.lzz" bool const unsafe_mode; -#line 91 "./src/objects/database.lzz" +#line 94 "./src/objects/database.lzz" bool was_js_error; -#line 92 "./src/objects/database.lzz" +#line 95 "./src/objects/database.lzz" bool const has_logger; -#line 93 "./src/objects/database.lzz" +#line 96 "./src/objects/database.lzz" unsigned short int iterators; -#line 94 "./src/objects/database.lzz" +#line 97 "./src/objects/database.lzz" Addon * const addon; }; -#line 96 "./src/objects/database.lzz" - State * GetState (); #line 99 "./src/objects/database.lzz" - sqlite3 * GetHandle (); + State * GetState (); #line 102 "./src/objects/database.lzz" + sqlite3 * GetHandle (); +#line 105 "./src/objects/database.lzz" Addon * GetAddon (); -#line 107 "./src/objects/database.lzz" +#line 110 "./src/objects/database.lzz" void CloseHandles (); -#line 119 "./src/objects/database.lzz" +#line 137 "./src/objects/database.lzz" ~ Database (); -#line 124 "./src/objects/database.lzz" +#line 142 "./src/objects/database.lzz" private: -#line 126 "./src/objects/database.lzz" +#line 143 "./src/objects/database.lzz" + class UpdateHook + { +#line 144 "./src/objects/database.lzz" + public: +#line 146 "./src/objects/database.lzz" + explicit UpdateHook (Addon * addon, v8::Isolate * isolate, Database * db, v8::Local fn); +#line 157 "./src/objects/database.lzz" + static void invoke (void * data, int op, char const * dbName, char const * tableName, sqlite3_int64 rowid); +#line 180 "./src/objects/database.lzz" + private: +#line 181 "./src/objects/database.lzz" + Addon * const addon; +#line 182 "./src/objects/database.lzz" + v8::Isolate * const isolate; +#line 183 "./src/objects/database.lzz" + Database * const db; +#line 184 "./src/objects/database.lzz" + v8::Global fn; + }; +#line 187 "./src/objects/database.lzz" + class CommitHook + { +#line 188 "./src/objects/database.lzz" + public: +#line 190 "./src/objects/database.lzz" + explicit CommitHook (v8::Isolate * isolate, Database * db, v8::Local fn); +#line 199 "./src/objects/database.lzz" + static int invoke (void * data); +#line 221 "./src/objects/database.lzz" + private: +#line 222 "./src/objects/database.lzz" + v8::Isolate * const isolate; +#line 223 "./src/objects/database.lzz" + Database * const db; +#line 224 "./src/objects/database.lzz" + v8::Global fn; + }; +#line 227 "./src/objects/database.lzz" + class RollbackHook + { +#line 228 "./src/objects/database.lzz" + public: +#line 230 "./src/objects/database.lzz" + explicit RollbackHook (v8::Isolate * isolate, Database * db, v8::Local fn); +#line 239 "./src/objects/database.lzz" + static void invoke (void * data); +#line 258 "./src/objects/database.lzz" + private: +#line 259 "./src/objects/database.lzz" + v8::Isolate * const isolate; +#line 260 "./src/objects/database.lzz" + Database * const db; +#line 261 "./src/objects/database.lzz" + v8::Global fn; + }; +#line 264 "./src/objects/database.lzz" explicit Database (v8::Isolate * isolate, Addon * addon, sqlite3 * db_handle, v8::Local logger); -#line 149 "./src/objects/database.lzz" +#line 287 "./src/objects/database.lzz" static void JS_new (v8::FunctionCallbackInfo const & info); -#line 201 "./src/objects/database.lzz" +#line 339 "./src/objects/database.lzz" static void JS_prepare (v8::FunctionCallbackInfo const & info); -#line 217 "./src/objects/database.lzz" +#line 355 "./src/objects/database.lzz" static void JS_exec (v8::FunctionCallbackInfo const & info); -#line 257 "./src/objects/database.lzz" +#line 395 "./src/objects/database.lzz" static void JS_backup (v8::FunctionCallbackInfo const & info); -#line 275 "./src/objects/database.lzz" +#line 413 "./src/objects/database.lzz" static void JS_serialize (v8::FunctionCallbackInfo const & info); -#line 297 "./src/objects/database.lzz" +#line 435 "./src/objects/database.lzz" static void JS_function (v8::FunctionCallbackInfo const & info); -#line 321 "./src/objects/database.lzz" +#line 459 "./src/objects/database.lzz" static void JS_aggregate (v8::FunctionCallbackInfo const & info); -#line 350 "./src/objects/database.lzz" +#line 488 "./src/objects/database.lzz" static void JS_table (v8::FunctionCallbackInfo const & info); -#line 370 "./src/objects/database.lzz" +#line 508 "./src/objects/database.lzz" static void JS_loadExtension (v8::FunctionCallbackInfo const & info); -#line 392 "./src/objects/database.lzz" +#line 530 "./src/objects/database.lzz" + static void JS_updateHook (v8::FunctionCallbackInfo const & info); +#line 552 "./src/objects/database.lzz" + static void JS_commitHook (v8::FunctionCallbackInfo const & info); +#line 573 "./src/objects/database.lzz" + static void JS_rollbackHook (v8::FunctionCallbackInfo const & info); +#line 594 "./src/objects/database.lzz" static void JS_close (v8::FunctionCallbackInfo const & info); -#line 402 "./src/objects/database.lzz" +#line 604 "./src/objects/database.lzz" static void JS_defaultSafeIntegers (v8::FunctionCallbackInfo const & info); -#line 408 "./src/objects/database.lzz" +#line 610 "./src/objects/database.lzz" static void JS_unsafeMode (v8::FunctionCallbackInfo const & info); -#line 415 "./src/objects/database.lzz" +#line 617 "./src/objects/database.lzz" static void JS_open (v8::Local _, v8::PropertyCallbackInfo const & info); -#line 419 "./src/objects/database.lzz" +#line 621 "./src/objects/database.lzz" static void JS_inTransaction (v8::Local _, v8::PropertyCallbackInfo const & info); -#line 424 "./src/objects/database.lzz" +#line 626 "./src/objects/database.lzz" static bool Deserialize (v8::Local buffer, Addon * addon, sqlite3 * db_handle, bool readonly); -#line 449 "./src/objects/database.lzz" +#line 651 "./src/objects/database.lzz" static void FreeSerialization (char * data, void * _); -#line 453 "./src/objects/database.lzz" +#line 655 "./src/objects/database.lzz" static int const MAX_BUFFER_SIZE = node::Buffer::kMaxLength > INT_MAX ? INT_MAX : static_cast(node::Buffer::kMaxLength); -#line 454 "./src/objects/database.lzz" +#line 656 "./src/objects/database.lzz" static int const MAX_STRING_SIZE = v8::String::kMaxLength > INT_MAX ? INT_MAX : static_cast(v8::String::kMaxLength); -#line 456 "./src/objects/database.lzz" +#line 658 "./src/objects/database.lzz" sqlite3 * const db_handle; -#line 457 "./src/objects/database.lzz" +#line 659 "./src/objects/database.lzz" bool open; -#line 458 "./src/objects/database.lzz" +#line 660 "./src/objects/database.lzz" bool busy; -#line 459 "./src/objects/database.lzz" +#line 661 "./src/objects/database.lzz" bool safe_ints; -#line 460 "./src/objects/database.lzz" +#line 662 "./src/objects/database.lzz" bool unsafe_mode; -#line 461 "./src/objects/database.lzz" +#line 663 "./src/objects/database.lzz" bool was_js_error; -#line 462 "./src/objects/database.lzz" +#line 664 "./src/objects/database.lzz" bool const has_logger; -#line 463 "./src/objects/database.lzz" +#line 665 "./src/objects/database.lzz" unsigned short int iterators; -#line 464 "./src/objects/database.lzz" +#line 666 "./src/objects/database.lzz" Addon * const addon; -#line 465 "./src/objects/database.lzz" +#line 667 "./src/objects/database.lzz" v8::Global const logger; -#line 466 "./src/objects/database.lzz" +#line 668 "./src/objects/database.lzz" std::set stmts; -#line 467 "./src/objects/database.lzz" +#line 669 "./src/objects/database.lzz" std::set backups; }; #line 1 "./src/objects/statement.lzz" @@ -838,25 +906,25 @@ LZZ_INLINE void SetFrozen (v8::Isolate * isolate, v8::Local ctx, v { obj->DefineOwnProperty(ctx, key.Get(isolate), value, static_cast(v8::DontDelete | v8::ReadOnly)).FromJust(); } -#line 92 "./src/util/macros.lzz" +#line 103 "./src/util/macros.lzz" LZZ_INLINE bool IS_SKIPPED (char c) -#line 92 "./src/util/macros.lzz" +#line 103 "./src/util/macros.lzz" { return c == ' ' || c == ';' || (c >= '\t' && c <= '\r'); } -#line 97 "./src/util/macros.lzz" +#line 108 "./src/util/macros.lzz" template -#line 97 "./src/util/macros.lzz" +#line 108 "./src/util/macros.lzz" LZZ_INLINE T * ALLOC_ARRAY (size_t count) -#line 97 "./src/util/macros.lzz" +#line 108 "./src/util/macros.lzz" { return static_cast(::operator new[](count * sizeof(T))); } -#line 102 "./src/util/macros.lzz" +#line 113 "./src/util/macros.lzz" template -#line 102 "./src/util/macros.lzz" +#line 113 "./src/util/macros.lzz" LZZ_INLINE void FREE_ARRAY (T * array_pointer) -#line 102 "./src/util/macros.lzz" +#line 113 "./src/util/macros.lzz" { ::operator delete[](array_pointer); } @@ -884,41 +952,41 @@ LZZ_INLINE int BindMap::GetSize () { return length; } -#line 77 "./src/objects/database.lzz" +#line 80 "./src/objects/database.lzz" LZZ_INLINE void Database::AddStatement (Statement * stmt) -#line 77 "./src/objects/database.lzz" +#line 80 "./src/objects/database.lzz" { stmts.insert(stmts.end(), stmt); } -#line 78 "./src/objects/database.lzz" +#line 81 "./src/objects/database.lzz" LZZ_INLINE void Database::RemoveStatement (Statement * stmt) -#line 78 "./src/objects/database.lzz" +#line 81 "./src/objects/database.lzz" { stmts.erase(stmt); } -#line 81 "./src/objects/database.lzz" +#line 84 "./src/objects/database.lzz" LZZ_INLINE void Database::AddBackup (Backup * backup) -#line 81 "./src/objects/database.lzz" +#line 84 "./src/objects/database.lzz" { backups.insert(backups.end(), backup); } -#line 82 "./src/objects/database.lzz" +#line 85 "./src/objects/database.lzz" LZZ_INLINE void Database::RemoveBackup (Backup * backup) -#line 82 "./src/objects/database.lzz" +#line 85 "./src/objects/database.lzz" { backups.erase(backup); } -#line 96 "./src/objects/database.lzz" +#line 99 "./src/objects/database.lzz" LZZ_INLINE Database::State * Database::GetState () -#line 96 "./src/objects/database.lzz" +#line 99 "./src/objects/database.lzz" { return reinterpret_cast(&open); } -#line 99 "./src/objects/database.lzz" +#line 102 "./src/objects/database.lzz" LZZ_INLINE sqlite3 * Database::GetHandle () -#line 99 "./src/objects/database.lzz" +#line 102 "./src/objects/database.lzz" { return db_handle; } -#line 102 "./src/objects/database.lzz" +#line 105 "./src/objects/database.lzz" LZZ_INLINE Addon * Database::GetAddon () -#line 102 "./src/objects/database.lzz" +#line 105 "./src/objects/database.lzz" { return addon; } diff --git a/src/objects/database.lzz b/src/objects/database.lzz index 28294fa3b..0c267788c 100644 --- a/src/objects/database.lzz +++ b/src/objects/database.lzz @@ -11,6 +11,9 @@ public: SetPrototypeMethod(isolate, data, t, "aggregate", JS_aggregate); SetPrototypeMethod(isolate, data, t, "table", JS_table); SetPrototypeMethod(isolate, data, t, "loadExtension", JS_loadExtension); + SetPrototypeMethod(isolate, data, t, "updateHook", JS_updateHook); + SetPrototypeMethod(isolate, data, t, "commitHook", JS_commitHook); + SetPrototypeMethod(isolate, data, t, "rollbackHook", JS_rollbackHook); SetPrototypeMethod(isolate, data, t, "close", JS_close); SetPrototypeMethod(isolate, data, t, "defaultSafeIntegers", JS_defaultSafeIntegers); SetPrototypeMethod(isolate, data, t, "unsafeMode", JS_unsafeMode); @@ -107,10 +110,25 @@ public: void CloseHandles() { if (open) { open = false; + for (Statement* stmt : stmts) stmt->CloseHandles(); for (Backup* backup : backups) backup->CloseHandles(); stmts.clear(); backups.clear(); + + UpdateHook* oldUpdateHook = static_cast(sqlite3_update_hook(db_handle, NULL, NULL)); + if (oldUpdateHook) { + delete oldUpdateHook; + } + CommitHook* oldCommitHook = static_cast(sqlite3_commit_hook(db_handle, NULL, NULL)); + if (oldCommitHook) { + delete oldCommitHook; + } + RollbackHook* oldRollbackHook = static_cast(sqlite3_rollback_hook(db_handle, NULL, NULL)); + if (oldRollbackHook) { + delete oldRollbackHook; + } + int status = sqlite3_close(db_handle); assert(status == SQLITE_OK); ((void)status); } @@ -122,6 +140,126 @@ public: } private: + class UpdateHook { + public: + + explicit UpdateHook( + Addon * addon, + v8::Isolate * isolate, + Database * db, + v8::Local fn + ) : + addon(addon), + isolate(isolate), + db(db), + fn(isolate, fn) {} + + static void invoke(void * data, int op, char const * dbName, char const * tableName, sqlite3_int64 rowid) { + UpdateHook * self = static_cast(data); + + v8::Isolate * isolate = self->isolate; + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + + v8::Local context = isolate->GetCurrentContext(); + + v8::Local opArg = self->addon->cs.Op(isolate, op); + v8::Local dbNameArg = v8::String::NewFromUtf8(isolate, dbName, v8::NewStringType::kNormal).ToLocalChecked(); + v8::Local tableNameArg = v8::String::NewFromUtf8(isolate, tableName, v8::NewStringType::kNormal).ToLocalChecked(); + v8::Local rowidArg = v8::BigInt::New(isolate, rowid); + + const int argc = 4; + v8::Local argv[argc] = { opArg, dbNameArg, tableNameArg, rowidArg }; + + v8::MaybeLocal maybeReturnValue = self->fn.Get(isolate)->Call(context, v8::Undefined(isolate), argc, argv); + if (maybeReturnValue.IsEmpty()) { + self->db->GetState()->was_js_error = true; + } + } + + private: + Addon * const addon; + v8::Isolate * const isolate; + Database * const db; + v8::Global fn; + }; + + class CommitHook { + public: + + explicit CommitHook( + v8::Isolate * isolate, + Database * db, + v8::Local fn + ) : + isolate(isolate), + db(db), + fn(isolate, fn) {} + + static int invoke(void * data) { + CommitHook * self = static_cast(data); + + v8::Isolate * isolate = self->isolate; + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + + v8::Local context = isolate->GetCurrentContext(); + + const int argc = 0; + v8::Local argv[argc] = {}; + + v8::MaybeLocal maybeReturnValue = self->fn.Get(isolate)->Call(context, v8::Undefined(isolate), argc, argv); + v8::Local returnValue; + if (maybeReturnValue.ToLocal(&returnValue)) { + return returnValue->BooleanValue(isolate) ? 0 : 1; + } else { + self->db->GetState()->was_js_error = true; + return 1; + } + } + + private: + v8::Isolate * const isolate; + Database * const db; + v8::Global fn; + }; + + class RollbackHook { + public: + + explicit RollbackHook( + v8::Isolate * isolate, + Database * db, + v8::Local fn + ) : + isolate(isolate), + db(db), + fn(isolate, fn) {} + + static void invoke(void * data) { + RollbackHook * self = static_cast(data); + + v8::Isolate * isolate = self->isolate; + v8::Isolate::Scope isolateScope(isolate); + v8::HandleScope handleScope(isolate); + + v8::Local context = isolate->GetCurrentContext(); + + const int argc = 0; + v8::Local argv[argc] = {}; + + v8::TryCatch try_catch(isolate); + v8::MaybeLocal maybeReturnValue = self->fn.Get(isolate)->Call(context, v8::Undefined(isolate), argc, argv); + if (maybeReturnValue.IsEmpty()) { + self->db->GetState()->was_js_error = true; + } + } + + private: + v8::Isolate * const isolate; + Database * const db; + v8::Global fn; + }; explicit Database( v8::Isolate* isolate, @@ -389,6 +527,70 @@ private: sqlite3_free(error); } + NODE_METHOD(JS_updateHook) { + Database* db = Unwrap(info.This()); + REQUIRE_OPTIONAL_ARGUMENT_FUNCTION(first, v8::MaybeLocal fn); + REQUIRE_DATABASE_OPEN(db); + REQUIRE_DATABASE_NOT_BUSY(db); + REQUIRE_DATABASE_NO_ITERATORS(db); + + UseAddon; + UseIsolate; + + UpdateHook* oldUpdateHook = static_cast( + sqlite3_update_hook( + db->db_handle, + fn.IsEmpty() ? NULL : UpdateHook::invoke, + fn.IsEmpty() ? NULL : new UpdateHook(addon, isolate, db, fn.ToLocalChecked()) + ) + ); + if (oldUpdateHook) { + delete oldUpdateHook; + } + } + + NODE_METHOD(JS_commitHook) { + Database* db = Unwrap(info.This()); + REQUIRE_OPTIONAL_ARGUMENT_FUNCTION(first, v8::MaybeLocal fn); + REQUIRE_DATABASE_OPEN(db); + REQUIRE_DATABASE_NOT_BUSY(db); + REQUIRE_DATABASE_NO_ITERATORS(db); + + UseIsolate; + + CommitHook* oldCommitHook = static_cast( + sqlite3_commit_hook( + db->db_handle, + fn.IsEmpty() ? NULL : CommitHook::invoke, + fn.IsEmpty() ? NULL : new CommitHook(isolate, db, fn.ToLocalChecked()) + ) + ); + if (oldCommitHook) { + delete oldCommitHook; + } + } + + NODE_METHOD(JS_rollbackHook) { + Database* db = Unwrap(info.This()); + REQUIRE_OPTIONAL_ARGUMENT_FUNCTION(first, v8::MaybeLocal fn); + REQUIRE_DATABASE_OPEN(db); + REQUIRE_DATABASE_NOT_BUSY(db); + REQUIRE_DATABASE_NO_ITERATORS(db); + + UseIsolate; + + RollbackHook* oldRollbackHook = static_cast( + sqlite3_rollback_hook( + db->db_handle, + fn.IsEmpty() ? NULL : RollbackHook::invoke, + fn.IsEmpty() ? NULL : new RollbackHook(isolate, db, fn.ToLocalChecked()) + ) + ); + if (oldRollbackHook) { + delete oldRollbackHook; + } + } + NODE_METHOD(JS_close) { Database* db = Unwrap(info.This()); if (db->open) { diff --git a/src/util/constants.lzz b/src/util/constants.lzz index d1b6e1376..9fcb47877 100644 --- a/src/util/constants.lzz +++ b/src/util/constants.lzz @@ -7,6 +7,12 @@ public: return StringFromUtf8(isolate, (std::string("UNKNOWN_SQLITE_ERROR_") + std::to_string(code)).c_str(), -1); } + v8::Local Op(v8::Isolate* isolate, int op) { + auto element = ops.find(op); + if (element != ops.end()) return element->second.Get(isolate); + return StringFromUtf8(isolate, (std::string("UNKNOWN_SQLITE_OP_") + std::to_string(op)).c_str(), -1); + } + explicit CS(v8::Isolate* isolate) { SetString(isolate, database, "database"); SetString(isolate, reader, "reader"); @@ -135,6 +141,10 @@ public: SetCode(isolate, SQLITE_AUTH_USER, "SQLITE_AUTH_USER"); SetCode(isolate, SQLITE_OK_LOAD_PERMANENTLY, "SQLITE_OK_LOAD_PERMANENTLY"); SetCode(isolate, SQLITE_OK_SYMLINK, "SQLITE_OK_SYMLINK"); + + SetOp(isolate, SQLITE_DELETE, "SQLITE_DELETE"); + SetOp(isolate, SQLITE_INSERT, "SQLITE_INSERT"); + SetOp(isolate, SQLITE_UPDATE, "SQLITE_UPDATE"); } v8::Global database; @@ -168,5 +178,12 @@ private: std::forward_as_tuple(isolate, InternalizedFromLatin1(isolate, str))); } + void SetOp(v8::Isolate* isolate, int op, const char* str) { + ops.emplace(std::piecewise_construct, + std::forward_as_tuple(op), + std::forward_as_tuple(isolate, InternalizedFromLatin1(isolate, str))); + } + std::unordered_map > codes; + std::unordered_map > ops; }; diff --git a/src/util/macros.lzz b/src/util/macros.lzz index a69329421..314571063 100644 --- a/src/util/macros.lzz +++ b/src/util/macros.lzz @@ -49,6 +49,14 @@ void ThrowRangeError(const char* message) { EasyIsolate; isolate->ThrowException return ThrowTypeError("Expected "#at" argument to be "#message); \ var = (info[at()].As())__VA_ARGS__ +#define _REQUIRE_OPTIONAL_ARGUMENT(at, var, Type, message, ...) \ + var = ({ \ + bool isSupplied = info.Length() > (at()) && !info[at()]->IsNullOrUndefined(); \ + if (isSupplied && !info[at()]->Is##Type()) \ + return ThrowTypeError("Expected "#at" argument to be "#message", null, or undefined"); \ + (isSupplied ? info[at()].As() : v8::MaybeLocal())__VA_ARGS__; \ + }) + #define REQUIRE_ARGUMENT_INT32(at, var) \ _REQUIRE_ARGUMENT(at, var, Int32, a 32-bit signed integer, ->Value()) #define REQUIRE_ARGUMENT_BOOLEAN(at, var) \ @@ -60,6 +68,9 @@ void ThrowRangeError(const char* message) { EasyIsolate; isolate->ThrowException #define REQUIRE_ARGUMENT_FUNCTION(at, var) \ _REQUIRE_ARGUMENT(at, var, Function, a function) +#define REQUIRE_OPTIONAL_ARGUMENT_FUNCTION(at, var) \ + _REQUIRE_OPTIONAL_ARGUMENT(at, var, Function, a function) + #define REQUIRE_DATABASE_OPEN(db) \ if (!db->open) \ return ThrowTypeError("The database connection is not open") diff --git a/test/38.database.hooks.js b/test/38.database.hooks.js new file mode 100644 index 000000000..b36bc9938 --- /dev/null +++ b/test/38.database.hooks.js @@ -0,0 +1,205 @@ +'use strict'; +const Database = require('../.'); + +describe('Database hooks', function () { + beforeEach(function () { + this.db = new Database(util.next()); + }); + afterEach(function () { + this.db.close(); + }); + + it('should invoke the correct callbacks when changes are successfully committed', function () { + const updates = []; + let commits = 0; + let rollbacks = 0; + + this.db.updateHook((op, dbName, tableName, rowid) => { + updates.push({ op, dbName, tableName, rowid }); + }); + this.db.commitHook(() => { + ++commits; + return true; + }); + this.db.rollbackHook(() => { + ++rollbacks; + }); + + this.db.exec('CREATE TABLE entries (a TEXT, b INTEGER)'); + expect(updates).to.have.lengthOf(0); + expect(commits).to.be.equal(1); + expect(rollbacks).to.be.equal(0); + + this.db.exec("INSERT INTO entries VALUES ('foobar', 44)"); + expect(updates).to.have.lengthOf(1); + expect(updates[0].op).to.equal('SQLITE_INSERT'); + expect(updates[0].dbName).to.equal('main'); + expect(updates[0].tableName).to.equal('entries'); + expect(updates[0].rowid).to.be.equal(1n); + expect(commits).to.be.equal(2); + expect(rollbacks).to.be.equal(0); + + this.db.exec("UPDATE entries SET b = 99 WHERE a = 'foobar'"); + expect(updates).to.have.lengthOf(2); + expect(updates[1].op).to.equal('SQLITE_UPDATE'); + expect(updates[1].dbName).to.equal('main'); + expect(updates[1].tableName).to.equal('entries'); + expect(updates[1].rowid).to.be.equal(1n); + expect(commits).to.be.equal(3); + expect(rollbacks).to.be.equal(0); + + this.db.exec('DELETE FROM entries WHERE b = 99'); + expect(updates).to.have.lengthOf(3); + expect(updates[2].op).to.equal('SQLITE_DELETE'); + expect(updates[2].dbName).to.equal('main'); + expect(updates[2].tableName).to.equal('entries'); + expect(updates[2].rowid).to.be.equal(1n); + expect(commits).to.be.equal(4); + expect(rollbacks).to.be.equal(0); + }); + + it('should correctly handle rollbacks', function () { + let attemptedCommits = 0; + let rollbacks = 0; + + this.db.commitHook(() => { + return ++attemptedCommits <= 1; + }); + this.db.rollbackHook(() => { + ++rollbacks; + }); + + this.db.exec('CREATE TABLE entries (a TEXT, b INTEGER)'); + expect(attemptedCommits).to.be.equal(1); + expect(rollbacks).to.be.equal(0); + + expect(() => { + this.db.exec("INSERT INTO entries VALUES ('foobar', 44)"); + }).to.throw(Database.SqliteError, 'constraint failed'); + expect(attemptedCommits).to.be.equal(2); + expect(rollbacks).to.be.equal(1); + }); + + it('should turn exceptions in commit hooks into rollbacks and propagate errors', function () { + let attemptedCommits = 0; + let rollbacks = 0; + + const commitError = new Error('commit error'); + this.db.commitHook(() => { + if (++attemptedCommits > 1) { + throw commitError; + } + return true; + }); + this.db.rollbackHook(() => { + ++rollbacks; + }); + + this.db.exec('CREATE TABLE entries (a TEXT, b INTEGER)'); + expect(attemptedCommits).to.be.equal(1); + expect(rollbacks).to.be.equal(0); + + expect(() => { + this.db.exec("INSERT INTO entries VALUES ('foobar', 44)"); + }).to.throw(commitError); + expect(attemptedCommits).to.be.equal(2); + expect(rollbacks).to.be.equal(1); + }); + + it('should allow changing hooks', function () { + const updatesA = []; + let commitsA = 0; + let rollbacksA = 0; + + this.db.exec('CREATE TABLE entries (a TEXT, b INTEGER)'); + + this.db.updateHook((op, dbName, tableName, rowid) => { + updatesA.push({ op, dbName, tableName, rowid }); + }); + this.db.commitHook(() => { + ++commitsA; + return true; + }); + this.db.rollbackHook(() => { + ++rollbacksA; + }); + this.db.exec("INSERT INTO entries VALUES ('foobar', 44)"); + + expect(updatesA).to.have.lengthOf(1); + expect(updatesA[0].op).to.equal('SQLITE_INSERT'); + expect(updatesA[0].dbName).to.equal('main'); + expect(updatesA[0].tableName).to.equal('entries'); + expect(updatesA[0].rowid).to.be.equal(1n); + + expect(commitsA).to.be.equal(1); + expect(rollbacksA).to.be.equal(0); + + const updatesB = []; + let attemptedCommitsB = 0; + let rollbacksB = 0; + + this.db.updateHook((op, dbName, tableName, rowid) => { + updatesB.push({ op, dbName, tableName, rowid }); + }); + this.db.commitHook(() => { + ++attemptedCommitsB; + return false; + }); + this.db.rollbackHook(() => { + ++rollbacksB; + }); + expect(() => { + this.db.exec("INSERT INTO entries VALUES ('bazquux', 99)"); + }).to.throw(Database.SqliteError, 'constraint failed'); + + expect(updatesB).to.have.lengthOf(1); + expect(updatesB[0].op).to.equal('SQLITE_INSERT'); + expect(updatesB[0].dbName).to.equal('main'); + expect(updatesB[0].tableName).to.equal('entries'); + expect(updatesB[0].rowid).to.be.equal(2n); + + expect(commitsA).to.be.equal(1); + expect(rollbacksA).to.be.equal(0); + + expect(attemptedCommitsB).to.be.equal(1); + expect(rollbacksB).to.be.equal(1); + }); + + it('should allow removing hooks', function () { + const updates = []; + let commits = 0; + let rollbacks = 0; + + this.db.exec('CREATE TABLE entries (a TEXT, b INTEGER)'); + + this.db.updateHook((op, dbName, tableName, rowid) => { + updates.push({ op, dbName, tableName, rowid }); + }); + this.db.commitHook(() => { + ++commits; + return true; + }); + this.db.rollbackHook(() => { + ++rollbacks; + }); + this.db.exec("INSERT INTO entries VALUES ('foobar', 44)"); + + expect(updates).to.have.lengthOf(1); + expect(updates[0].op).to.equal('SQLITE_INSERT'); + expect(updates[0].dbName).to.equal('main'); + expect(updates[0].tableName).to.equal('entries'); + expect(updates[0].rowid).to.be.equal(1n); + + expect(commits).to.be.equal(1); + expect(rollbacks).to.be.equal(0); + + this.db.updateHook(null); + this.db.commitHook(undefined); + this.db.rollbackHook(); + this.db.exec("INSERT INTO entries VALUES ('bazquux', 99)"); + + expect(updates).to.have.lengthOf(1); + expect(commits).to.be.equal(1); + expect(rollbacks).to.be.equal(0); + }); +});