#include #include "macros.h" #include "database.h" #include "statement.h" using namespace node_sqlite3; Napi::FunctionReference Database::constructor; Napi::Object Database::Init(Napi::Env env, Napi::Object exports) { Napi::HandleScope scope(env); Napi::Function t = DefineClass(env, "Database", { InstanceMethod("close", &Database::Close), InstanceMethod("exec", &Database::Exec), InstanceMethod("wait", &Database::Wait), InstanceMethod("loadExtension", &Database::LoadExtension), InstanceMethod("serialize", &Database::Serialize), InstanceMethod("parallelize", &Database::Parallelize), InstanceMethod("configure", &Database::Configure), InstanceMethod("interrupt", &Database::Interrupt), InstanceAccessor("open", &Database::OpenGetter, nullptr) }); constructor = Napi::Persistent(t); constructor.SuppressDestruct(); exports.Set("Database", t); return exports; } void Database::Process() { Napi::Env env = this->Env(); Napi::HandleScope scope(env); if (!open && locked && !queue.empty()) { EXCEPTION(Napi::String::New(env, "Database handle is closed"), SQLITE_MISUSE, exception); Napi::Value argv[] = { exception }; bool called = false; // Call all callbacks with the error object. while (!queue.empty()) { Call* call = queue.front(); Napi::Function cb = call->baton->callback.Value(); if (!cb.IsUndefined() && cb.IsFunction()) { TRY_CATCH_CALL(this->Value(), cb, 1, argv); called = true; } queue.pop(); // We don't call the actual callback, so we have to make sure that // the baton gets destroyed. delete call->baton; delete call; } // When we couldn't call a callback function, emit an error on the // Database object. if (!called) { Napi::Value info[] = { Napi::String::New(env, "error"), exception }; EMIT_EVENT(Value(), 2, info); } return; } while (open && (!locked || pending == 0) && !queue.empty()) { Call* call = queue.front(); if (call->exclusive && pending > 0) { break; } queue.pop(); locked = call->exclusive; call->callback(call->baton); delete call; if (locked) break; } } void Database::Schedule(Work_Callback callback, Baton* baton, bool exclusive) { Napi::Env env = this->Env(); Napi::HandleScope scope(env); if (!open && locked) { EXCEPTION(Napi::String::New(env, "Database is closed"), SQLITE_MISUSE, exception); Napi::Function cb = baton->callback.Value(); if (!cb.IsUndefined() && cb.IsFunction()) { Napi::Value argv[] = { exception }; TRY_CATCH_CALL(Value(), cb, 1, argv); } else { Napi::Value argv[] = { Napi::String::New(env, "error"), exception }; EMIT_EVENT(Value(), 2, argv); } return; } if (!open || ((locked || exclusive || serialize) && pending > 0)) { queue.push(new Call(callback, baton, exclusive || serialize)); } else { locked = exclusive; callback(baton); } } Database::Database(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { init(); Napi::Env env = info.Env(); if (info.Length() <= 0 || !info[0].IsString()) { Napi::TypeError::New(env, "String expected").ThrowAsJavaScriptException(); return; } std::string filename = info[0].As(); unsigned int pos = 1; int mode; if (info.Length() >= pos && info[pos].IsNumber() && OtherIsInt(info[pos].As())) { mode = info[pos++].As().Int32Value(); } else { mode = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX; } Napi::Function callback; if (info.Length() >= pos && info[pos].IsFunction()) { callback = info[pos++].As(); } info.This().As().DefineProperty(Napi::PropertyDescriptor::Value("filename", info[0].As(), napi_default)); info.This().As().DefineProperty(Napi::PropertyDescriptor::Value("mode", Napi::Number::New(env, mode), napi_default)); // Start opening the database. OpenBaton* baton = new OpenBaton(this, callback, filename.c_str(), mode); Work_BeginOpen(baton); } void Database::Work_BeginOpen(Baton* baton) { Napi::Env env = baton->db->Env(); int status = napi_create_async_work( env, NULL, Napi::String::New(env, "sqlite3.Database.Open"), Work_Open, Work_AfterOpen, baton, &baton->request ); assert(status == 0); napi_queue_async_work(env, baton->request); } void Database::Work_Open(napi_env e, void* data) { OpenBaton* baton = static_cast(data); Database* db = baton->db; baton->status = sqlite3_open_v2( baton->filename.c_str(), &db->_handle, baton->mode, NULL ); if (baton->status != SQLITE_OK) { baton->message = std::string(sqlite3_errmsg(db->_handle)); sqlite3_close(db->_handle); db->_handle = NULL; } else { // Set default database handle values. sqlite3_busy_timeout(db->_handle, 1000); } } void Database::Work_AfterOpen(napi_env e, napi_status status, void* data) { OpenBaton* baton = static_cast(data); Database* db = baton->db; Napi::Env env = db->Env(); Napi::HandleScope scope(env); Napi::Value argv[1]; if (baton->status != SQLITE_OK) { EXCEPTION(Napi::String::New(env, baton->message.c_str()), baton->status, exception); argv[0] = exception; } else { db->open = true; argv[0] = env.Null(); } Napi::Function cb = baton->callback.Value(); if (!cb.IsUndefined() && cb.IsFunction()) { TRY_CATCH_CALL(db->Value(), cb, 1, argv); } else if (!db->open) { Napi::Value info[] = { Napi::String::New(env, "error"), argv[0] }; EMIT_EVENT(db->Value(), 2, info); } if (db->open) { Napi::Value info[] = { Napi::String::New(env, "open") }; EMIT_EVENT(db->Value(), 1, info); db->Process(); } napi_delete_async_work(e, baton->request); delete baton; } Napi::Value Database::OpenGetter(const Napi::CallbackInfo& info) { Napi::Env env = this->Env(); Database* db = this; return Napi::Boolean::New(env, db->open); } Napi::Value Database::Close(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); Database* db = this; OPTIONAL_ARGUMENT_FUNCTION(0, callback); Baton* baton = new Baton(db, callback); db->Schedule(Work_BeginClose, baton, true); return info.This(); } void Database::Work_BeginClose(Baton* baton) { assert(baton->db->locked); assert(baton->db->open); assert(baton->db->_handle); assert(baton->db->pending == 0); baton->db->RemoveCallbacks(); baton->db->closing = true; Napi::Env env = baton->db->Env(); int status = napi_create_async_work( env, NULL, Napi::String::New(env, "sqlite3.Database.Close"), Work_Close, Work_AfterClose, baton, &baton->request ); assert(status == 0); napi_queue_async_work(env, baton->request); } void Database::Work_Close(napi_env e, void* data) { Baton* baton = static_cast(data); Database* db = baton->db; baton->status = sqlite3_close(db->_handle); if (baton->status != SQLITE_OK) { baton->message = std::string(sqlite3_errmsg(db->_handle)); } else { db->_handle = NULL; } } void Database::Work_AfterClose(napi_env e, napi_status status, void* data) { Baton* baton = static_cast(data); Database* db = baton->db; Napi::Env env = db->Env(); Napi::HandleScope scope(env); db->closing = false; Napi::Value argv[1]; if (baton->status != SQLITE_OK) { EXCEPTION(Napi::String::New(env, baton->message.c_str()), baton->status, exception); argv[0] = exception; } else { db->open = false; // Leave db->locked to indicate that this db object has reached // the end of its life. argv[0] = env.Null(); } Napi::Function cb = baton->callback.Value(); // Fire callbacks. if (!cb.IsUndefined() && cb.IsFunction()) { TRY_CATCH_CALL(db->Value(), cb, 1, argv); } else if (db->open) { Napi::Value info[] = { Napi::String::New(env, "error"), argv[0] }; EMIT_EVENT(db->Value(), 2, info); } if (!db->open) { Napi::Value info[] = { Napi::String::New(env, "close"), argv[0] }; EMIT_EVENT(db->Value(), 1, info); db->Process(); } napi_delete_async_work(e, baton->request); delete baton; } Napi::Value Database::Serialize(const Napi::CallbackInfo& info) { Napi::Env env = this->Env(); Database* db = this; OPTIONAL_ARGUMENT_FUNCTION(0, callback); bool before = db->serialize; db->serialize = true; if (!callback.IsEmpty() && callback.IsFunction()) { TRY_CATCH_CALL(info.This(), callback, 0, NULL); db->serialize = before; } db->Process(); return info.This(); } Napi::Value Database::Parallelize(const Napi::CallbackInfo& info) { Napi::Env env = this->Env(); Database* db = this; OPTIONAL_ARGUMENT_FUNCTION(0, callback); bool before = db->serialize; db->serialize = false; if (!callback.IsEmpty() && callback.IsFunction()) { TRY_CATCH_CALL(info.This(), callback, 0, NULL); db->serialize = before; } db->Process(); return info.This(); } Napi::Value Database::Configure(const Napi::CallbackInfo& info) { Napi::Env env = this->Env(); Database* db = this; REQUIRE_ARGUMENTS(2); if (info[0].StrictEquals( Napi::String::New(env, "trace"))) { Napi::Function handle; Baton* baton = new Baton(db, handle); db->Schedule(RegisterTraceCallback, baton); } else if (info[0].StrictEquals( Napi::String::New(env, "profile"))) { Napi::Function handle; Baton* baton = new Baton(db, handle); db->Schedule(RegisterProfileCallback, baton); } else if (info[0].StrictEquals( Napi::String::New(env, "busyTimeout"))) { if (!info[1].IsNumber()) { Napi::TypeError::New(env, "Value must be an integer").ThrowAsJavaScriptException(); return env.Null(); } Napi::Function handle; Baton* baton = new Baton(db, handle); baton->status = info[1].As().Int32Value(); db->Schedule(SetBusyTimeout, baton); } else { Napi::TypeError::New(env, (StringConcat( #if V8_MAJOR_VERSION > 6 info.GetIsolate(), #endif info[0].As(), Napi::String::New(env, " is not a valid configuration option") )).Utf8Value().c_str()).ThrowAsJavaScriptException(); return env.Null(); } db->Process(); return info.This(); } Napi::Value Database::Interrupt(const Napi::CallbackInfo& info) { Napi::Env env = this->Env(); Database* db = this; if (!db->open) { Napi::Error::New(env, "Database is not open").ThrowAsJavaScriptException(); return env.Null(); } if (db->closing) { Napi::Error::New(env, "Database is closing").ThrowAsJavaScriptException(); return env.Null(); } sqlite3_interrupt(db->_handle); return info.This(); } void Database::SetBusyTimeout(Baton* baton) { assert(baton->db->open); assert(baton->db->_handle); // Abuse the status field for passing the timeout. sqlite3_busy_timeout(baton->db->_handle, baton->status); delete baton; } void Database::RegisterTraceCallback(Baton* baton) { assert(baton->db->open); assert(baton->db->_handle); Database* db = baton->db; if (db->debug_trace == NULL) { // Add it. db->debug_trace = new AsyncTrace(db, TraceCallback); sqlite3_trace(db->_handle, TraceCallback, db); } else { // Remove it. sqlite3_trace(db->_handle, NULL, NULL); db->debug_trace->finish(); db->debug_trace = NULL; } delete baton; } void Database::TraceCallback(void* db, const char* sql) { // Note: This function is called in the thread pool. // Note: Some queries, such as "EXPLAIN" queries, are not sent through this. static_cast(db)->debug_trace->send(new std::string(sql)); } void Database::TraceCallback(Database* db, std::string* sql) { // Note: This function is called in the main V8 thread. Napi::Env env = db->Env(); Napi::HandleScope scope(env); Napi::Value argv[] = { Napi::String::New(env, "trace"), Napi::String::New(env, sql->c_str()) }; EMIT_EVENT(db->Value(), 2, argv); delete sql; } void Database::RegisterProfileCallback(Baton* baton) { assert(baton->db->open); assert(baton->db->_handle); Database* db = baton->db; if (db->debug_profile == NULL) { // Add it. db->debug_profile = new AsyncProfile(db, ProfileCallback); sqlite3_profile(db->_handle, ProfileCallback, db); } else { // Remove it. sqlite3_profile(db->_handle, NULL, NULL); db->debug_profile->finish(); db->debug_profile = NULL; } delete baton; } void Database::ProfileCallback(void* db, const char* sql, sqlite3_uint64 nsecs) { // Note: This function is called in the thread pool. // Note: Some queries, such as "EXPLAIN" queries, are not sent through this. ProfileInfo* info = new ProfileInfo(); info->sql = std::string(sql); info->nsecs = nsecs; static_cast(db)->debug_profile->send(info); } void Database::ProfileCallback(Database *db, ProfileInfo* info) { Napi::Env env = db->Env(); Napi::HandleScope scope(env); Napi::Value argv[] = { Napi::String::New(env, "profile"), Napi::String::New(env, info->sql.c_str()), Napi::Number::New(env, (double)info->nsecs / 1000000.0) }; EMIT_EVENT(db->Value(), 3, argv); delete info; } void Database::RegisterUpdateCallback(Baton* baton) { assert(baton->db->open); assert(baton->db->_handle); Database* db = baton->db; if (db->update_event == NULL) { // Add it. db->update_event = new AsyncUpdate(db, UpdateCallback); sqlite3_update_hook(db->_handle, UpdateCallback, db); } else { // Remove it. sqlite3_update_hook(db->_handle, NULL, NULL); db->update_event->finish(); db->update_event = NULL; } delete baton; } void Database::UpdateCallback(void* db, int type, const char* database, const char* table, sqlite3_int64 rowid) { // Note: This function is called in the thread pool. // Note: Some queries, such as "EXPLAIN" queries, are not sent through this. UpdateInfo* info = new UpdateInfo(); info->type = type; info->database = std::string(database); info->table = std::string(table); info->rowid = rowid; static_cast(db)->update_event->send(info); } void Database::UpdateCallback(Database *db, UpdateInfo* info) { Napi::Env env = db->Env(); Napi::HandleScope scope(env); Napi::Value argv[] = { Napi::String::New(env, sqlite_authorizer_string(info->type)), Napi::String::New(env, info->database.c_str()), Napi::String::New(env, info->table.c_str()), Napi::Number::New(env, info->rowid), }; EMIT_EVENT(db->Value(), 4, argv); delete info; } Napi::Value Database::Exec(const Napi::CallbackInfo& info) { Napi::Env env = this->Env(); Database* db = this; REQUIRE_ARGUMENT_STRING(0, sql); OPTIONAL_ARGUMENT_FUNCTION(1, callback); Baton* baton = new ExecBaton(db, callback, sql.c_str()); db->Schedule(Work_BeginExec, baton, true); return info.This(); } void Database::Work_BeginExec(Baton* baton) { assert(baton->db->locked); assert(baton->db->open); assert(baton->db->_handle); assert(baton->db->pending == 0); Napi::Env env = baton->db->Env(); int status = napi_create_async_work( env, NULL, Napi::String::New(env, "sqlite3.Database.Exec"), Work_Exec, Work_AfterExec, baton, &baton->request ); assert(status == 0); napi_queue_async_work(env, baton->request); } void Database::Work_Exec(napi_env e, void* data) { ExecBaton* baton = static_cast(data); char* message = NULL; baton->status = sqlite3_exec( baton->db->_handle, baton->sql.c_str(), NULL, NULL, &message ); if (baton->status != SQLITE_OK && message != NULL) { baton->message = std::string(message); sqlite3_free(message); } } void Database::Work_AfterExec(napi_env e, napi_status status, void* data) { ExecBaton* baton = static_cast(data); Database* db = baton->db; Napi::Env env = db->Env(); Napi::HandleScope scope(env); Napi::Function cb = baton->callback.Value(); if (baton->status != SQLITE_OK) { EXCEPTION(Napi::String::New(env, baton->message.c_str()), baton->status, exception); if (!cb.IsUndefined() && cb.IsFunction()) { Napi::Value argv[] = { exception }; TRY_CATCH_CALL(db->Value(), cb, 1, argv); } else { Napi::Value info[] = { Napi::String::New(env, "error"), exception }; EMIT_EVENT(db->Value(), 2, info); } } else if (!cb.IsUndefined() && cb.IsFunction()) { Napi::Value argv[] = { env.Null() }; TRY_CATCH_CALL(db->Value(), cb, 1, argv); } db->Process(); napi_delete_async_work(e, baton->request); delete baton; } Napi::Value Database::Wait(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); Database* db = this; OPTIONAL_ARGUMENT_FUNCTION(0, callback); Baton* baton = new Baton(db, callback); db->Schedule(Work_Wait, baton, true); return info.This(); } void Database::Work_Wait(Baton* baton) { Napi::Env env = baton->db->Env(); Napi::HandleScope scope(env); assert(baton->db->locked); assert(baton->db->open); assert(baton->db->_handle); assert(baton->db->pending == 0); Napi::Function cb = baton->callback.Value(); if (!cb.IsUndefined() && cb.IsFunction()) { Napi::Value argv[] = { env.Null() }; TRY_CATCH_CALL(baton->db->Value(), cb, 1, argv); } baton->db->Process(); delete baton; } Napi::Value Database::LoadExtension(const Napi::CallbackInfo& info) { Napi::Env env = this->Env(); Database* db = this; REQUIRE_ARGUMENT_STRING(0, filename); OPTIONAL_ARGUMENT_FUNCTION(1, callback); Baton* baton = new LoadExtensionBaton(db, callback, filename.c_str()); db->Schedule(Work_BeginLoadExtension, baton, true); return info.This(); } void Database::Work_BeginLoadExtension(Baton* baton) { assert(baton->db->locked); assert(baton->db->open); assert(baton->db->_handle); assert(baton->db->pending == 0); Napi::Env env = baton->db->Env(); int status = napi_create_async_work( env, NULL, Napi::String::New(env, "sqlite3.Database.LoadExtension"), Work_LoadExtension, Work_AfterLoadExtension, baton, &baton->request ); assert(status == 0); napi_queue_async_work(env, baton->request); } void Database::Work_LoadExtension(napi_env e, void* data) { LoadExtensionBaton* baton = static_cast(data); sqlite3_enable_load_extension(baton->db->_handle, 1); char* message = NULL; baton->status = sqlite3_load_extension( baton->db->_handle, baton->filename.c_str(), 0, &message ); sqlite3_enable_load_extension(baton->db->_handle, 0); if (baton->status != SQLITE_OK && message != NULL) { baton->message = std::string(message); sqlite3_free(message); } } void Database::Work_AfterLoadExtension(napi_env e, napi_status status, void* data) { LoadExtensionBaton* baton = static_cast(data); Database* db = baton->db; Napi::Env env = db->Env(); Napi::HandleScope scope(env); Napi::Function cb = baton->callback.Value(); if (baton->status != SQLITE_OK) { EXCEPTION(Napi::String::New(env, baton->message.c_str()), baton->status, exception); if (!cb.IsUndefined() && cb.IsFunction()) { Napi::Value argv[] = { exception }; TRY_CATCH_CALL(db->Value(), cb, 1, argv); } else { Napi::Value info[] = { Napi::String::New(env, "error"), exception }; EMIT_EVENT(db->Value(), 2, info); } } else if (!cb.IsUndefined() && cb.IsFunction()) { Napi::Value argv[] = { env.Null() }; TRY_CATCH_CALL(db->Value(), cb, 1, argv); } db->Process(); napi_delete_async_work(e, baton->request); delete baton; } void Database::RemoveCallbacks() { if (debug_trace) { debug_trace->finish(); debug_trace = NULL; } if (debug_profile) { debug_profile->finish(); debug_profile = NULL; } }