diff --git a/lib/sqlite3.d.ts b/lib/sqlite3.d.ts index b27b0cf51..9f57a673d 100644 --- a/lib/sqlite3.d.ts +++ b/lib/sqlite3.d.ts @@ -93,12 +93,26 @@ export class Statement extends events.EventEmitter { each(...params: any[]): this; } +export class Backup extends events.EventEmitter { + step(pages: number, callback?: (error: Error, backup: Backup) => void): Backup; + finish(callback?: (error: Error, backup: Backup) => void): Backup; + + get idle(): boolean; + get completed(): boolean; + get failed(): boolean; + get remaining(): number; + get pageCount(): number; +} + export class Database extends events.EventEmitter { constructor(filename: string, callback?: (err: Error | null) => void); constructor(filename: string, mode?: number, callback?: (err: Error | null) => void); close(callback?: (err: Error | null) => void): void; + backup(destination: string | Database, destName: string, sourceName: string, filenameIsDest = true, callback?: (this: Backup, err: Error | null, backup: Backup) => void): this; + backup(destination: string | Database, callback?: (this: Backup, err: Error | null, backup: Backup) => void): this; + run(sql: string, callback?: (this: RunResult, err: Error | null) => void): this; run(sql: string, params: any, callback?: (this: RunResult, err: Error | null) => void): this; run(sql: string, ...params: any[]): this; @@ -201,5 +215,6 @@ export interface sqlite3 { RunResult: RunResult; Statement: typeof Statement; Database: typeof Database; + Backup: typeof Backup; verbose(): this; } \ No newline at end of file diff --git a/src/backup.cc b/src/backup.cc index 9f3893b96..cb1a85eb5 100644 --- a/src/backup.cc +++ b/src/backup.cc @@ -133,8 +133,8 @@ Backup::Backup(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) Napi::TypeError::New(env, "Database object expected").ThrowAsJavaScriptException(); return; } - else if (length <= 1 || !info[1].IsString()) { - Napi::TypeError::New(env, "Filename expected").ThrowAsJavaScriptException(); + else if (length <= 1 || !(info[1].IsString() || info[1].IsObject())) { + Napi::TypeError::New(env, "Filename or database object expected").ThrowAsJavaScriptException(); return; } else if (length <= 2 || !info[2].IsString()) { @@ -155,7 +155,20 @@ Backup::Backup(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) } Database* db = Napi::ObjectWrap::Unwrap(info[0].As()); - Napi::String filename = info[1].As(); + Database* otherDb = NULL; + + Napi::String filename; + + if (info[1].IsObject()) { + // A database instance was passed instead of a filename + otherDb = Napi::ObjectWrap::Unwrap(info[1].As()); + otherDb->Ref(); + + filename = Napi::String::New(env, ""); + } else { + filename = info[1].As(); + } + Napi::String sourceName = info[2].As(); Napi::String destName = info[3].As(); Napi::Boolean filenameIsDest = info[4].As(); @@ -165,14 +178,28 @@ Backup::Backup(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) info.This().As().DefineProperty(Napi::PropertyDescriptor::Value("destName", destName)); info.This().As().DefineProperty(Napi::PropertyDescriptor::Value("filenameIsDest", filenameIsDest)); - init(db); + init(db, otherDb); InitializeBaton* baton = new InitializeBaton(db, info[5].As(), this); + baton->otherDb = otherDb; baton->filename = filename.Utf8Value(); baton->sourceName = sourceName.Utf8Value(); baton->destName = destName.Utf8Value(); baton->filenameIsDest = filenameIsDest.Value(); - db->Schedule(Work_BeginInitialize, baton); + + if (otherDb) { + otherDb->Schedule(Work_BeforeInitialize, baton, true); + } else { + db->Schedule(Work_BeginInitialize, baton); + } +} + +void Backup::Work_BeforeInitialize(Database::Baton* baton) { + InitializeBaton *initBaton = static_cast(baton); + // at this point, the target database object is locked (it is + // important that its database connection remains unused). + initBaton->otherDb->pending++; + baton->db->Schedule(Work_BeginInitialize, baton); } void Backup::Work_BeginInitialize(Database::Baton* baton) { @@ -195,22 +222,37 @@ void Backup::Work_Initialize(napi_env e, void* data) { sqlite3_mutex* mtx = sqlite3_db_mutex(baton->db->_handle); sqlite3_mutex_enter(mtx); - backup->status = sqlite3_open(baton->filename.c_str(), &backup->_otherDb); + backup->message = ""; + if (baton->otherDb) { + // If another database instance was passed, + // link other (locked) db to the backup state + backup->otherDb = baton->otherDb; + backup->_otherDbHandle = baton->otherDb->_handle; + + backup->status = SQLITE_OK; + if (!baton->filenameIsDest) { + backup->status = SQLITE_MISUSE; + backup->message = "do not toggle filenameIsDest when backing up between sqlite3.Database instances"; + } + } else { + // Do not initialize otherDb and continue with normal + // initialization by using the filename that was provided + backup->otherDb = NULL; + backup->status = sqlite3_open(baton->filename.c_str(), &backup->_otherDbHandle); + } if (backup->status == SQLITE_OK) { backup->_handle = sqlite3_backup_init( - baton->filenameIsDest ? backup->_otherDb : backup->db->_handle, + baton->filenameIsDest ? backup->_otherDbHandle : backup->db->_handle, baton->destName.c_str(), - baton->filenameIsDest ? backup->db->_handle : backup->_otherDb, + baton->filenameIsDest ? backup->db->_handle : backup->_otherDbHandle, baton->sourceName.c_str()); } - backup->_destDb = baton->filenameIsDest ? backup->_otherDb : backup->db->_handle; + backup->_destDbHandle = baton->filenameIsDest ? backup->_otherDbHandle : backup->db->_handle; if (backup->status != SQLITE_OK) { - backup->message = std::string(sqlite3_errmsg(backup->_destDb)); - sqlite3_close(backup->_otherDb); - backup->_otherDb = NULL; - backup->_destDb = NULL; + if (backup->message == "") backup->message = std::string(sqlite3_errmsg(backup->_destDbHandle)); + backup->FinishSqlite(); } sqlite3_mutex_leave(mtx); @@ -231,8 +273,8 @@ void Backup::Work_AfterInitialize(napi_env e, napi_status status, void* data) { backup->inited = true; Napi::Function cb = baton->callback.Value(); if (!cb.IsEmpty() && cb.IsFunction()) { - Napi::Value argv[] = { env.Null() }; - TRY_CATCH_CALL(backup->Value(), cb, 1, argv); + Napi::Value argv[] = { env.Null(), backup->Value() }; + TRY_CATCH_CALL(backup->Value(), cb, 2, argv); } } BACKUP_END(); @@ -354,6 +396,14 @@ void Backup::FinishAll() { CleanQueue(); FinishSqlite(); db->Unref(); + + if (otherDb) { + assert(otherDb->locked); + otherDb->pending--; + otherDb->Process(); + otherDb->Unref(); + otherDb = NULL; + } } void Backup::FinishSqlite() { @@ -361,11 +411,15 @@ void Backup::FinishSqlite() { sqlite3_backup_finish(_handle); _handle = NULL; } - if (_otherDb) { - sqlite3_close(_otherDb); - _otherDb = NULL; + if (_otherDbHandle) { + if (!otherDb) { + // Only close the database if it was + // not passed as a descriptor already. + sqlite3_close(_otherDbHandle); + } + _otherDbHandle = NULL; } - _destDb = NULL; + _destDbHandle = NULL; } Napi::Value Backup::IdleGetter(const Napi::CallbackInfo& info) { diff --git a/src/backup.h b/src/backup.h index c15b77bfe..e3387678a 100644 --- a/src/backup.h +++ b/src/backup.h @@ -113,6 +113,7 @@ class Backup : public Napi::ObjectWrap { struct InitializeBaton : Database::Baton { Backup* backup; + Database* otherDb; std::string filename; std::string sourceName; std::string destName; @@ -145,11 +146,12 @@ class Backup : public Napi::ObjectWrap { Baton* baton; }; - void init(Database* db_) { + void init(Database* db_, Database* otherDb_) { db = db_; + otherDb = otherDb_; _handle = NULL; - _otherDb = NULL; - _destDb = NULL; + _otherDbHandle = NULL; + _destDbHandle = NULL; inited = false; locked = true; completed = false; @@ -183,6 +185,7 @@ class Backup : public Napi::ObjectWrap { void RetryErrorSetter(const Napi::CallbackInfo& info, const Napi::Value& value); protected: + static void Work_BeforeInitialize(Database::Baton* baton); static void Work_BeginInitialize(Database::Baton* baton); static void Work_Initialize(napi_env env, void* data); static void Work_AfterInitialize(napi_env env, napi_status status, void* data); @@ -197,10 +200,11 @@ class Backup : public Napi::ObjectWrap { void GetRetryErrors(std::set& retryErrorsSet); Database* db; + Database* otherDb; sqlite3_backup* _handle; - sqlite3* _otherDb; - sqlite3* _destDb; + sqlite3* _otherDbHandle; + sqlite3* _destDbHandle; int status; std::string message; diff --git a/test/backup.test.js b/test/backup.test.js index c05c298f8..e9d99eb30 100644 --- a/test/backup.test.js +++ b/test/backup.test.js @@ -276,4 +276,55 @@ describe('backup', function() { }); }); }); + + it ('can backup between two sqlite3.Database instances', function(done) { + var src = new sqlite3.Database(':memory:', function(err) { + if (err) throw err; + src.exec("CREATE TABLE space (txt TEXT)", function(err) { + if (err) throw err; + src.exec("INSERT INTO space(txt) VALUES('monkey')", function(err) { + if (err) throw err; + var dest = new sqlite3.Database(':memory:', function(err) { + if (err) throw err; + var backup = src.backup(dest); + backup.step(-1); + backup.finish(function(err) { + if (err) throw err; + assertRowsMatchDb(src, 'space', dest, 'space', done); + }); + }); + }); + }); + }); + }); + + it ('locks destination when backing up between two sqlite3.Database instances', function(done) { + var src = new sqlite3.Database(':memory:', function(err) { + if (err) throw err; + src.exec("CREATE TABLE space (txt TEXT)", function(err) { + if (err) throw err; + src.exec("INSERT INTO space(txt) VALUES('monkey')", function(err) { + if (err) throw err; + var dest = new sqlite3.Database(':memory:', function(err) { + if (err) throw err; + var backup = src.backup(dest, function(err) { + if (err) throw err; + var finished = false; + // This action on dest db should be held until backup finishes. + dest.exec("CREATE TABLE space2 (txt TEXT)", function(err) { + if (err) throw err; + assert(finished); + done(); + }); + backup.step(-1); + backup.finish(function(err) { + if (err) throw err; + finished = true; + }); + }); + }); + }); + }); + }); + }); });