diff --git a/.gitignore b/.gitignore index fa51500d3..c262bb5f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .DS_Store .plugin +.metadata/* +*.swp +*~ diff --git a/README.md b/README.md index 956357126..e41488e55 100644 --- a/README.md +++ b/README.md @@ -385,7 +385,7 @@ Available for integration with SQLCipher. # Unit test(s) -Unit testing is done in `test-www/`. To run the tests, simply do either: +Unit testing is done in `test-www/`. To run the tests from *nix shell, simply do either: ./bin/test.sh ios @@ -393,6 +393,14 @@ or in Android: ./bin/test.sh android +To run then from a windows powershell do either + + .\bin\test.ps1 android + +or for Windows Phone 8: + + .\bin\test.ps1 wp8 + # Adapters ## Lawnchair Adapter diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 275c2229d..67a470ef9 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -86,10 +86,16 @@ License for common Javascript: MIT or Apache return SQLitePlugin::transaction = (fn, error, success) -> + if !@openDBs[@dbname] + error('database not open') + return @addTransaction new SQLitePluginTransaction(this, fn, error, success, true, false) return SQLitePlugin::readTransaction = (fn, error, success) -> + if !@openDBs[@dbname] + error('database not open') + return @addTransaction new SQLitePluginTransaction(this, fn, error, success, true, true) return @@ -105,10 +111,17 @@ License for common Javascript: MIT or Apache return SQLitePlugin::open = (success, error) -> + onSuccess = () => success this unless @dbname of @openDBs @openDBs[@dbname] = true - cordova.exec success, error, "SQLitePlugin", "open", [ @openargs ] - + cordova.exec onSuccess, error, "SQLitePlugin", "open", [ @openargs ] + else + ### + for a re-open run onSuccess async so that the openDatabase return value + can be used in the success handler as an alternative to the handler's + db argument + ### + nextTick () -> onSuccess(); return SQLitePlugin::close = (success, error) -> @@ -117,7 +130,7 @@ License for common Javascript: MIT or Apache if @dbname of @openDBs delete @openDBs[@dbname] - cordova.exec null, null, "SQLitePlugin", "close", [ { path: @dbname } ] + cordova.exec success, error, "SQLitePlugin", "close", [ { path: @dbname } ] return @@ -381,6 +394,7 @@ License for common Javascript: MIT or Apache new SQLitePlugin openargs, okcb, errorcb deleteDb: (databaseName, success, error) -> + delete SQLitePlugin::openDBs[databaseName] cordova.exec success, error, "SQLitePlugin", "delete", [{ path: databaseName }] ### Exported API: diff --git a/bin/test.ps1 b/bin/test.ps1 new file mode 100644 index 000000000..fde5b188e --- /dev/null +++ b/bin/test.ps1 @@ -0,0 +1,57 @@ +# Automated cordova tests. Installs the correct cordova platform, +# installs the plugin, installs the test app, and then runs it on +# a device or emulator. +# +# usage: .\bin\test.ps1 [android|ios|wp8] + +# N.B. if you functionally change this script you _must_ change .\bin\test.sh too. + +param([string]$platform) + +if (! $platform) { + echo "usage: .\bin\test.sh [android|ios|wp8]" + exit 1 +} + +if (! (get-command coffee) ) { + echo "you need coffeescript. please install with:" + echo "npm install -g coffee-script" + exit 1 +} + +if (! (get-command cordova) ) { + echo "you need cordova. please install with:" + echo "npm install -g cordova" + exit 1 +} + + +pushd test-www +if (!$?) { # run from the bin/ directory + echo "re-pushing" + pushd ../test-www +} +try { + # compile coffeescript + coffee --no-header -cl -o ../www ../SQLitePlugin.coffee.md + if (!$?) { + echo "coffeescript compilation failed" + exit 1 + } + echo "compiled coffeescript to javascript" + + # move everything to a temp folder to avoid infinite recursion errors + if (test-path ../.plugin) { + rm -force -recurse ../.plugin -ErrorAction ignore + } + mkdir -ErrorAction ignore ../.plugin | out-null + cp -recurse ../src, ../plugin.xml, ../www ../.plugin + + # update the plugin, run the test app + cordova platform add $platform + cordova plugin rm com.phonegap.plugins.sqlite + cordova plugin add ../.plugin + cordova run $platform +} finally { + popd +} diff --git a/bin/test.sh b/bin/test.sh index 88b49c859..8a780023d 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -7,6 +7,8 @@ # usage: ./bin/test.sh [android|ios] # +# N.B. if you functionally change this script you _must_ change ./bin/test.ps1 too. + platform=$1 if [[ -z $platform ]]; then @@ -22,7 +24,7 @@ fi if [[ ! -x $(which cordova) ]]; then echo "you need cordova. please install with:" - echo "npm install -g coffee-script" + echo "npm install -g cordova" exit 1 fi diff --git a/src/android/org/pgsqlite/SQLitePlugin.java b/src/android/org/pgsqlite/SQLitePlugin.java index 0a86d175d..193b14c3b 100755 --- a/src/android/org/pgsqlite/SQLitePlugin.java +++ b/src/android/org/pgsqlite/SQLitePlugin.java @@ -22,7 +22,11 @@ import java.lang.Number; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -100,34 +104,23 @@ private boolean executeAndPossiblyThrow(Action action, JSONArray args, CallbackC case open: o = args.getJSONObject(0); dbname = o.getString("name"); - - DBRunner r = new DBRunner(dbname); - this.rmap.put(dbname, r); - this.cordova.getThreadPool().execute(r); - // TODO should send an async callback + // open database and start reading its queue + this.startDatabase(dbname, cbc); break; case close: o = args.getJSONObject(0); dbname = o.getString("path"); // put request in the q to close the db - this.closeDatabase(dbname); - // TODO should send an async callback + this.closeDatabase(dbname, cbc); break; case delete: o = args.getJSONObject(0); dbname = o.getString("path"); - // TODO: if the db is open, must put request in the q to close & delete the db - status = this.deleteDatabase(dbname); + deleteDatabase(dbname, cbc); - // deleteDatabase() requires an async callback - if (status) { - cbc.success(); - } else { - cbc.error("couldn't delete database"); - } break; case executeSqlBatch: @@ -164,8 +157,17 @@ private boolean executeAndPossiblyThrow(Action action, JSONArray args, CallbackC // put db query in the queue to be executed in the db thread: DBQuery q = new DBQuery(queries, queryIDs, jsonparams, cbc); - r = rmap.get(dbname); - if (r != null) try { r.q.put(q); } catch(Exception e) {} + DBRunner r = rmap.get(dbname); + if (r != null) { + try { + r.q.put(q); + } catch(Exception e) { + Log.e(SQLitePlugin.class.getSimpleName(), "couldn't add to queue", e); + cbc.error("couldn't add to queue"); + } + } else { + cbc.error("database not open"); + } break; } @@ -179,7 +181,7 @@ private boolean executeAndPossiblyThrow(Action action, JSONArray args, CallbackC public void onDestroy() { while (!dbmap.isEmpty()) { String dbname = dbmap.keySet().iterator().next(); - // TODO should stop the db thread(s) instead (!!) + // TODO should stop the db thread(s) instead (!!) this.closeDatabaseNow(dbname); dbmap.remove(dbname); } @@ -189,28 +191,50 @@ public void onDestroy() { // LOCAL METHODS // -------------------------------------------------------------------------- + private void startDatabase(String dbname, CallbackContext cbc) { + // TODO: is it an issue that we can orphan an existing thread? What should we do here? + // If we re-use the existing DBRunner it might be in the process of closing... + DBRunner r = rmap.get(dbname); + if (r != null) { + // don't orphan the existing thread; just re-open the existing database. + // In the worst case it might be in the process of closing, but even that's less serious + // than orphaning the old DBRunner. + cbc.success(); + } else { + r = new DBRunner(dbname, cbc); + rmap.put(dbname, r); + this.cordova.getThreadPool().execute(r); + } + } /** * Open a database. * * @param dbName The name of the database file */ - private void openDatabase(String dbname) { - if (this.getDatabase(dbname) != null) { - // TODO should wait for the db thread(s) to stop (!!) - this.closeDatabase(dbname); - } + private void openDatabase(String dbname, CallbackContext cbc) { + try { + if (this.getDatabase(dbname) != null) { + // this should not happen - should be blocked at the execute("open") level + cbc.error("database already open"); + } - File dbfile = this.cordova.getActivity().getDatabasePath(dbname); + File dbfile = this.cordova.getActivity().getDatabasePath(dbname); - if (!dbfile.exists()) { - dbfile.getParentFile().mkdirs(); - } + if (!dbfile.exists()) { + dbfile.getParentFile().mkdirs(); + } - Log.v("info", "Open sqlite db: " + dbfile.getAbsolutePath()); + Log.v("info", "Open sqlite db: " + dbfile.getAbsolutePath()); - SQLiteDatabase mydb = SQLiteDatabase.openOrCreateDatabase(dbfile, null); + SQLiteDatabase mydb = SQLiteDatabase.openOrCreateDatabase(dbfile, null); - dbmap.put(dbname, mydb); + dbmap.put(dbname, mydb); + + cbc.success(); + } catch (SQLiteException e) { + cbc.error("can't open database " + e); + throw e; + } } /** @@ -218,14 +242,21 @@ private void openDatabase(String dbname) { * * @param dbName The name of the database file */ - private void closeDatabase(String dbName) { + private void closeDatabase(String dbName, CallbackContext cbc) { DBRunner r = rmap.get(dbName); if (r != null) { try { - r.q.put(new DBQuery(true, null)); + r.q.put(new DBQuery(false, cbc)); } catch(Exception e) { + if (cbc != null) { + cbc.error("couldn't close database" + e); + } Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database", e); } + } else { + if (cbc != null) { + cbc.success(); + } } } @@ -243,6 +274,26 @@ private void closeDatabaseNow(String dbName) { } } + private void deleteDatabase(String dbname, CallbackContext cbc) { + DBRunner r = rmap.get(dbname); + if (r != null) { + try { + r.q.put(new DBQuery(true, cbc)); + } catch(Exception e) { + if (cbc != null) { + cbc.error("couldn't close database" + e); + } + Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database", e); + } + } else { + boolean deleteResult = this.deleteDatabaseNow(dbname); + if (deleteResult) { + cbc.success(); + } else { + cbc.error("couldn't delete database"); + } + } + } /** * Delete a database. * @@ -251,25 +302,20 @@ private void closeDatabaseNow(String dbName) { * @return true if successful or false if an exception was encountered */ @SuppressLint("NewApi") - private boolean deleteDatabase(String dbname) { - if (this.getDatabase(dbname) != null) { - // TODO: if the db is open, put request in the q to close & delete the db (instead) - this.closeDatabaseNow(dbname); - } - + private boolean deleteDatabaseNow(String dbname) { File dbfile = this.cordova.getActivity().getDatabasePath(dbname); if (android.os.Build.VERSION.SDK_INT >= 11) { // Use try & catch just in case android.os.Build.VERSION.SDK_INT >= 16 was lying: try { - return SQLiteDatabase.deleteDatabase(dbfile); + return SQLiteDatabase.deleteDatabase(dbfile); } catch (Exception e) { Log.e(SQLitePlugin.class.getSimpleName(), "couldn't delete because old SDK_INT", e); return deleteDatabasePreHoneycomb(dbfile); } } else { // use old API - return deleteDatabasePreHoneycomb(dbfile); + return deleteDatabasePreHoneycomb(dbfile); } } @@ -307,12 +353,9 @@ private void executeSqlBatch(String dbname, String[] queryarr, JSONArray[] jsonp SQLiteDatabase mydb = getDatabase(dbname); if (mydb == null) { - // auto-open; this is something we have to support - // since the API allows the user to delete a database and then re-use it - // TBD/TODO: this case should not be allowed. The JS API should - // invalidate its internal db handle if the db file is deleted. - openDatabase(dbname); - mydb = getDatabase(dbname); + // not allowed - can only happen if someone has closed (and possibly deleted) a database and then re-used the database + cbc.error("database has been closed"); + return; } @@ -384,19 +427,22 @@ private void executeSqlBatch(String dbname, String[] queryarr, JSONArray[] jsonp try { insertId = myStatement.executeInsert(); + + // statement has finished with no constraint violation: + queryResult = new JSONObject(); + if (insertId != -1) { + queryResult.put("insertId", insertId); + queryResult.put("rowsAffected", 1); + } else { + queryResult.put("rowsAffected", 0); + } } catch (SQLiteException ex) { + // report error result with the error message + // could be constraint violation or some other error ex.printStackTrace(); errorMessage = ex.getMessage(); Log.v("executeSqlBatch", "SQLiteDatabase.executeInsert(): Error=" + errorMessage); } - - queryResult = new JSONObject(); - if (insertId != -1) { - queryResult.put("insertId", insertId); - queryResult.put("rowsAffected", 1); - } else { - queryResult.put("rowsAffected", 0); - } } if (queryType == QueryType.begin) { @@ -713,31 +759,63 @@ private void bindPreHoneycomb(JSONObject row, String key, Cursor cursor, int i) private class DBRunner implements Runnable { final String dbname; final BlockingQueue q; + final CallbackContext openCbc; - DBRunner(final String dbname) { + DBRunner(final String dbname, CallbackContext cbc) { this.dbname = dbname; this.q = new LinkedBlockingQueue(); + this.openCbc = cbc; } public void run() { - openDatabase(dbname); + openDatabase(dbname, this.openCbc); + DBQuery dbq; try { - DBQuery dbq = q.take(); + dbq = q.take(); while (!dbq.stop) { executeSqlBatch(dbname, dbq.queries, dbq.jsonparams, dbq.queryIDs, dbq.cbc); dbq = q.take(); } - } catch (Exception e) { } - closeDatabaseNow(dbname); + try { + if (!rmap.remove(dbname, this)) { + Log.w(SQLitePlugin.class.getSimpleName(), "Couldn't remove ourself"); // TODO: remove + } + closeDatabaseNow(dbname); + + if (!dbq.delete) { + dbq.cbc.success(); + } else { + try { + boolean deleteResult = deleteDatabaseNow(dbname); + if (deleteResult) { + dbq.cbc.success(); + } else { + dbq.cbc.error("couldn't delete database"); + } + } catch (Exception e) { + Log.e(SQLitePlugin.class.getSimpleName(), "couldn't delete database", e); + dbq.cbc.error("couldn't delete database: " + e); + } + } + } catch (Exception e) { + Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database", e); + if (dbq.cbc != null) { + dbq.cbc.error("couldn't close database: " + e); + } + } + } catch (Exception e) { + Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error", e); + } } } private final class DBQuery { final boolean stop; + final boolean delete; final String[] queries; final String[] queryIDs; final JSONArray[] jsonparams; @@ -745,14 +823,16 @@ private final class DBQuery { DBQuery(String[] myqueries, String[] qids, JSONArray[] params, CallbackContext c) { this.stop = false; + this.delete = false; this.queries = myqueries; this.queryIDs = qids; this.jsonparams = params; this.cbc = c; } - DBQuery(boolean stop, CallbackContext cbc) { + DBQuery(boolean delete, CallbackContext cbc) { this.stop = true; + this.delete = delete; this.queries = null; this.queryIDs = null; this.jsonparams = null; diff --git a/test-www/www/index.html b/test-www/www/index.html index 6a164067c..a7ee56db5 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -20,8 +20,10 @@ document.addEventListener("deviceready", doAllTests, false); function doAllTests() { + QUnit.config.testTimeout = 25000; // 25 sec. doTest(false, 'Plugin: ', window.sqlitePlugin.openDatabase); - doTest(true, 'HTML5: ', window.openDatabase); + if (!/MSIE/.test(navigator.userAgent)) + doTest(true, 'HTML5: ', window.openDatabase); } function doTest(isWebSql, suiteName, openDatabase) { @@ -49,12 +51,7 @@ }); }); - test(suiteName + ' UNICODE encoding test', function () { - if (/MSIE/.test(navigator.userAgent)) { - ok(true, 'skip'); - return; - } - + if (!/MSIE/.test(navigator.userAgent)) test(suiteName + ' UNICODE encoding test', function () { stop(); var dbName = "Unicode-hex-test"; @@ -96,9 +93,8 @@ }); }); - /** FUTURE (only on recent versions of Android): - test(suiteName + "ICU-UNICODE String test", function() { - + // ICU-UNICODE only functional on recent versions of Android: + if (/Android 4\.[3-9]/.test(navigator.userAgent)) test(suiteName + "ICU-UNICODE String test", function() { var db = openDatabase("String-test.db", "1.0", "Demo", DEFAULT_SIZE); ok(!!db, "db object"); @@ -118,45 +114,9 @@ equal(res.rows.item(0).uppertext, "КАКОЙ-ТО КИРИЛЛИЧЕСКИЙ ТЕКСТ", "Try ''Some Cyrillic text''"); }); }); - }); **/ - - test(suiteName + "DB String result test", function() { - if (isWebSql) { - ok('skipped'); - return; - } - - var db = openDatabase("String-test.db", "1.0", "Demo", DEFAULT_SIZE); - - var expected = [ 'FIRST', 'SECOND' ]; - var i=0; - - ok(!!db, "db object"); - - stop(2); - - var okcb = function(res) { - if (i > 1) { - ok(false, "unexpected result: " + JSON.stringify(res)); - console.log("discarding unexpected result: " + JSON.stringify(res)) - return; - } - - ok(!!res, "valid object"); - - // do not count res if undefined: - if (!!res) { // will freeze the test if res is undefined: - console.log("res.rows.item(0).uppertext: " + res.rows.item(0).uppertext); - equal(res.rows.item(0).uppertext, expected[i], "Check result " + i); - i++; - start(1); - } - }; - - db.executeSql("select upper('first') as uppertext", [], okcb); - db.executeSql("select upper('second') as uppertext", [], okcb); }); + test(suiteName + "CR-LF String test", function() { var db = openDatabase("CR-LF-String-test.db", "1.0", "Demo", DEFAULT_SIZE); ok(!!db, "db object"); @@ -179,13 +139,7 @@ }); test(suiteName + "db transaction test", function() { - - if (isWebSql) { - ok('skipped'); - return; - } - - var db = openDatabase({name: "my.db"}); + var db = openDatabase("db-trx-test.db", "1.0", "Demo", DEFAULT_SIZE); ok(!!db, "db object"); @@ -357,7 +311,7 @@ tx.executeSql('CREATE TABLE IF NOT EXISTS characters (name unique, creator, fav tinyint(1))'); tx.executeSql('DROP TABLE IF EXISTS companies'); tx.executeSql('CREATE TABLE IF NOT EXISTS companies (name unique, fav tinyint(1))'); - // INSERT or IGNORE with the real thing: + // INSERT or IGNORE with the real thing: tx.executeSql('INSERT or IGNORE INTO characters VALUES (?,?,?)', ['Sonic', 'Sega', 0], function (tx, res) { equal(res.rowsAffected, 1); tx.executeSql('INSERT INTO characters VALUES (?,?,?)', ['Tails', 'Sega', 0], function (tx, res) { @@ -446,14 +400,17 @@ }); }; - test(suiteName + "transaction encompasses all callbacks", function() { + // XXX (Uncaught) Error is reported in the case of Web SQL, needs investigation! + if (!isWebSql) test(suiteName + "transaction encompasses all callbacks", function() { + stop(); + var db = openDatabase("tx-all-callbacks.db", "1.0", "Demo", DEFAULT_SIZE); - if (isWebSql) { - ok('skipped'); - return; - } + db.transaction(function(tx) { + + start(); + tx.executeSql('DROP TABLE IF EXISTS test_table'); + tx.executeSql('CREATE TABLE IF NOT EXISTS test_table (id integer primary key, data text, data_num integer)'); - withTestTable(function(db) { stop(); db.transaction(function(tx) { tx.executeSql('INSERT INTO test_table (data, data_num) VALUES (?,?)', ['test', 100], function(tx, res) { @@ -466,7 +423,7 @@ }); }, function(error) { start(); - equal(error.message, "deliberately aborting transaction"); + if (!isWebSql) equal(error.message, "deliberately aborting transaction"); stop(); db.transaction(function(tx) { tx.executeSql("select count(*) as cnt from test_table", [], function(tx, res) { @@ -481,24 +438,27 @@ }); }); - test(suiteName + "exception from transaction handler causes failure", function() { - - if (isWebSql) { - ok('skipped'); - return; - } + // XXX (Uncaught) Error is reported in the case of Web SQL, needs investigation! + if (!isWebSql) test(suiteName + "exception from transaction handler causes failure", function() { + stop(); + var db = openDatabase("exception-causes-failure.db", "1.0", "Demo", DEFAULT_SIZE); - withTestTable(function(db) { - stop(); + try { db.transaction(function(tx) { throw new Error("boom"); }, function(err) { + ok(!!err, "valid error object"); + ok(err.hasOwnProperty('message'), "error.message exists"); start(); - equal(err.message, 'boom'); + if (!isWebSql) equal(err.message, 'boom'); }, function() { ok(false, "not supposed to succeed"); + start(); }); - }); + ok(true, "db.transaction() did not throw an error"); + } catch(err) { + ok(true, "db.transaction() DID throw an error"); + } }); test(suiteName + "error handler returning true causes rollback", function() { @@ -621,7 +581,7 @@ start(); var row = res.rows.item(0); strictEqual(row.data_text, "3.14159", "data_text should have inserted data as text"); - strictEqual(row.data_int, 314159, "data_int should have inserted data as an integer"); + if (!/MSIE/.test(navigator.userAgent)) strictEqual(row.data_int, 314159, "data_int should have inserted data as an integer"); ok(Math.abs(row.data_real - 3.14159) < 0.000001, "data_real should have inserted data as a real"); }); }); @@ -629,67 +589,6 @@ }); }); - test(suiteName + ' test deleteDatabase()', function () { - - if (isWebSql || /MSIE/.test(navigator.userAgent)) { - ok(true, 'skip'); // doesn't exist in native WebSQL - return; - } - - stop(); - var db = openDatabase("DB-Deletable", "1.0", "Demo", DEFAULT_SIZE); - - function createAndInsertStuff() { - - db.transaction(function(tx) { - tx.executeSql('DROP TABLE IF EXISTS test'); - tx.executeSql('CREATE TABLE IF NOT EXISTS test (name)', [], function () { - tx.executeSql('INSERT INTO test VALUES (?)', ['foo']); - }); - }, function (e) { ok(false, 'error: ' + e); }, function () { - // check that we can read it - db.transaction(function(tx) { - tx.executeSql('SELECT * FROM test', [], function (tx, res) { - equal(res.rows.item(0).name, 'foo'); - }); - }, function (e) { ok(false, 'error: ' + e); }, function () { - deleteAndConfirmDeleted(); - }); - }); - } - - function deleteAndConfirmDeleted() { - - window.sqlitePlugin.deleteDatabase("DB-Deletable", function () { - // check that the data's gone - db.transaction(function(tx) { - tx.executeSql('SELECT * FROM test'); - }, function (e) { - ok(true, 'got error like we expected'); - testDeleteError(); - }, - function () { - ok(false, 'expected an error'); - }); - }, function (e) { - ok(false, 'error: ' + e); - }); - } - - function testDeleteError() { - // should throw an error if the db doesn't exist - window.sqlitePlugin.deleteDatabase("Foo-Doesnt-Exist", function () { - ok(false, 'expected error'); - }, function (err) { - start(); - ok(!!err, 'got error like we expected'); - }); - } - - createAndInsertStuff(); - - }); - test(suiteName + "readTransaction should throw on modification", function() { stop(); var db = openDatabase("Database-readonly", "1.0", "Demo", DEFAULT_SIZE); @@ -771,7 +670,6 @@ blocked = false; }); - test(suiteName + ' test simultaneous transactions', function () { stop(); @@ -845,6 +743,7 @@ }); }); }); + test(suiteName + ' test simultaneous transactions, different dbs', function () { stop(); @@ -903,12 +802,7 @@ }); - test(suiteName + ' stores unicode correctly', function () { - if (/MSIE/.test(navigator.userAgent)) { - ok(true, 'skip'); - return; - } - + if (!/MSIE/.test(navigator.userAgent)) test(suiteName + ' stores unicode correctly', function () { stop(); var dbName = "Database-Unicode"; @@ -971,9 +865,115 @@ }); } + test(suiteName + "syntax error", function() { + var db = openDatabase("Syntax-error-test.db", "1.0", "Demo", DEFAULT_SIZE); + ok(!!db, "db object"); + + stop(2); + db.transaction(function(tx) { + tx.executeSql('DROP TABLE IF EXISTS test_table'); + tx.executeSql('CREATE TABLE IF NOT EXISTS test_table (data unique)'); + + // This insertion has a sql syntax error + tx.executeSql("insert into test_table (data) VALUES ", [123], function(tx) { + ok(false, "unexpected success"); + start(); + throw new Error('abort tx'); + }, function(tx, error) { + ok(!!error, "valid error object"); + + if (isWebSql || (!/Android/.test(navigator.userAgent) && !/MSIE/.test(navigator.userAgent))) + ok(!!error['code'], "valid error.code exists"); + + ok(error.hasOwnProperty('message'), "error.message exists"); + if (isWebSql || (!/Android/.test(navigator.userAgent) && !/MSIE/.test(navigator.userAgent))) + strictEqual(error.code, 5, "error.code === SQLException.SYNTAX_ERR (5)"); + //equal(error.message, "Request failed: insert into test_table (data) VALUES ,123", "error.message"); + start(); + + // We want this error to fail the entire transaction + return true; + }); + }, function (error) { + ok(!!error, "valid error object"); + ok(error.hasOwnProperty('message'), "error.message exists"); + start(); + }); + }); + + test(suiteName + "constraint violation", function() { + var db = openDatabase("Constraint-violation-test.db", "1.0", "Demo", DEFAULT_SIZE); + ok(!!db, "db object"); + + stop(2); + db.transaction(function(tx) { + tx.executeSql('DROP TABLE IF EXISTS test_table'); + tx.executeSql('CREATE TABLE IF NOT EXISTS test_table (data unique)'); + + tx.executeSql("insert into test_table (data) VALUES (?)", [123], null, function(tx, error) { + ok(false, error.message); + }); + + // This insertion will violate the unique constraint + tx.executeSql("insert into test_table (data) VALUES (?)", [123], function(tx) { + ok(false, "unexpected success"); + ok(!!res['rowsAffected'] || !(res.rowsAffected >= 1), "should not have positive rowsAffected"); + start(); + throw new Error('abort tx'); + }, function(tx, error) { + ok(!!error, "valid error object"); + + if (isWebSql || (!/Android/.test(navigator.userAgent) && !/MSIE/.test(navigator.userAgent))) + ok(!!error['code'], "valid error.code exists"); + + ok(error.hasOwnProperty('message'), "error.message exists"); + //strictEqual(error.code, 6, "error.code === SQLException.CONSTRAINT_ERR (6)"); + //equal(error.message, "Request failed: insert into test_table (data) VALUES (?),123", "error.message"); + start(); + + // We want this error to fail the entire transaction + return true; + }); + }, function(error) { + ok(!!error, "valid error object"); + ok(error.hasOwnProperty('message'), "error.message exists"); + start(); + }); + }); + + if (!isWebSql) test(suiteName + "DB String result test", function() { + var db = openDatabase("String-test.db", "1.0", "Demo", DEFAULT_SIZE); + + var expected = [ 'FIRST', 'SECOND' ]; + var i=0; + + ok(!!db, "db object"); + + stop(2); + + var okcb = function(res) { + if (i > 1) { + ok(false, "unexpected result: " + JSON.stringify(res)); + console.log("discarding unexpected result: " + JSON.stringify(res)) + return; + } + + ok(!!res, "valid object"); + + // do not count res if undefined: + if (!!res) { // will freeze the test if res is undefined: + console.log("res.rows.item(0).uppertext: " + res.rows.item(0).uppertext); + equal(res.rows.item(0).uppertext, expected[i], "Check result " + i); + i++; + start(1); + } + }; + + db.executeSql("select upper('first') as uppertext", [], okcb); + db.executeSql("select upper('second') as uppertext", [], okcb); + }); - /** - test(suiteName + "PRAGMA & multiple databases", function() { + if (!isWebSql) test(suiteName + "PRAGMAs & multiple databases", function() { var db = openDatabase("DB1", "1.0", "Demo", DEFAULT_SIZE); var db2 = openDatabase("DB2", "1.0", "Demo", DEFAULT_SIZE); @@ -1015,85 +1015,307 @@ equal(res.rows.item(2).name, "data_num2", "DB2 table number field name"); }); }); - }); - var db; - module("Error codes", { - setup: function() { - stop(); - db = openDatabase("Database", "1.0", "Demo", DEFAULT_SIZE); + if (!isWebSql && !/MSIE/.test(navigator.userAgent)) test(suiteName + ' test sqlitePlugin.deleteDatabase()', function () { + + stop(); + var db = openDatabase("DB-Deletable", "1.0", "Demo", DEFAULT_SIZE); + + function createAndInsertStuff() { + db.transaction(function(tx) { - tx.executeSql('DROP TABLE IF EXISTS test_table'); - tx.executeSql('CREATE TABLE IF NOT EXISTS test_table (data unique)'); - }, function(error) { - ok(false, error.message); - start(); - }, function() { - start(); + tx.executeSql('DROP TABLE IF EXISTS test'); + tx.executeSql('CREATE TABLE IF NOT EXISTS test (name)', [], function () { + tx.executeSql('INSERT INTO test VALUES (?)', ['foo']); + }); + }, function (e) { ok(false, 'error: ' + e); }, function () { + // check that we can read it + db.transaction(function(tx) { + tx.executeSql('SELECT * FROM test', [], function (tx, res) { + equal(res.rows.item(0).name, 'foo'); + }); + }, function (e) { ok(false, 'error: ' + e); }, function () { + deleteAndConfirmDeleted(); + }); }); - }, - teardown: function() { - stop(); - db.transaction(function(tx) { - tx.executeSql('DROP TABLE IF EXISTS test_table'); - }, function(error) { - ok(false, error.message); - start(); - }, function() { + } + + function deleteAndConfirmDeleted() { + + window.sqlitePlugin.deleteDatabase("DB-Deletable", function () { + + // check that the data's gone + db.transaction(function (tx) { + tx.executeSql('SELECT name FROM test', []); + }, function (e) { + ok(true, 'got an expected transaction error'); + testDeleteError(); + }, function () { + ok(false, 'expected a transaction error'); + }); + }, function (e) { + ok(false, 'error: ' + e); + }); + } + + function testDeleteError() { + // should throw an error if the db doesn't exist + window.sqlitePlugin.deleteDatabase("Foo-Doesnt-Exist", function () { + ok(false, 'expected error'); + }, function (err) { start(); + ok(!!err, 'got error like we expected'); }); } + + createAndInsertStuff(); }); - test(suiteName + "syntax error", function() { + if (!isWebSql) test(suiteName + ' database.open calls its success callback', function () { + + // asynch test coming up + stop(1); + + var dbName = "Database-Open-callback"; + openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function (db) { + ok(true, 'expected close success callback to be called after database is closed'); + start(1); + }, function (error) { + ok(false, 'expected close error callback not to be called after database is closed'); + start(1); + }); + }); + + if (!isWebSql && !/MSIE/.test(navigator.userAgent)) test(suiteName + ' database.close calls its success callback', function () { + + // asynch test coming up + stop(1); + + var dbName = "Database-Close-callback"; + var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); + + // close database - need to run tests directly in callbacks as nothing is guarenteed to be queued after a close + db.close(function () { + ok(true, 'expected close success callback to be called after database is closed'); + start(1); + }, function (error) { + ok(false, 'expected close error callback not to be called after database is closed'); + start(1); + }); + }); + + if (!isWebSql && !/MSIE/.test(navigator.userAgent)) test(suiteName + ' open same database twice works', function () { + stop(2); - db.transaction(function(tx) { - // This insertion has a sql syntax error - tx.executeSql("insert into test_table (data) VALUES ", [123], function(tx) { - ok(false, "unexpected success"); - start(); - }, function(tx, error) { - strictEqual(error.code, SQLException.SYNTAX_ERR, "error.code === SYNTAX_ERR"); - equal(error.message, "Request failed: insert into test_table (data) VALUES ,123", "error.message"); - start(); - // We want this error to fail the entire transaction - return true; + var dbName = 'open-twice'; + + var db1 = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function () { + var db2 = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function () { + db1.readTransaction(function(tx1) { + tx1.executeSql('SELECT 1', [], function(tx1d, results) { + ok(true, 'db1 transaction working'); + start(1); + }, function(error) { + ok(false, error); + }); + }, function(error) { + ok(false, error); + }); + db2.readTransaction(function(tx2) { + tx2.executeSql('SELECT 1', [], function(tx2d, results) { + ok(true, 'db2 transaction working'); + start(1); + }, function(error) { + ok(false, error); + }); + }, function(error) { + ok(false, error); + }); + }, function (error) { + ok(false, error); }); }, function (error) { - strictEqual(error.code, SQLException.SYNTAX_ERR, "error.code === SYNTAX_ERR"); - equal(error.message, "Request failed: insert into test_table (data) VALUES ,123", "error.message"); - start(); + ok(false, error); }); }); - test(suiteName + "constraint violation", function() { - stop(2); - db.transaction(function(tx) { - tx.executeSql("insert into test_table (data) VALUES (?)", [123], null, function(tx, error) { + if (!isWebSql && !/MSIE/.test(navigator.userAgent)) test(suiteName + ' close then re-open allows subsequent queries to run', function () { + + // asynch test coming up + stop(1); + + var dbName = "Database-Close-and-Reopen"; + var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function () { + db.close(function () { + db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function () { + db.close(function () { + db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function () { + db.readTransaction(function (tx) { + tx.executeSql('SELECT 1', [], function (tx, results) { + ok(true, 'database re-opened succesfully'); + db.close(); + start(1); + }, function (error) { + ok(false, error.message); + start(1); + }); + }, function (error) { + ok(false, error.message); + start(1); + }); + }, function (error) { + ok(false, error.message); + start(1); + }); + }, function (error) { + ok(false, error.message); + start(1); + }); + }, function (error) { + ok(false, error.message); + start(1); + }); + }, function (error) { ok(false, error.message); + start(1); }); + }, function (error) { + ok(false, error.message); + start(1); + }); + }); - // This insertion will violate the unique constraint - tx.executeSql("insert into test_table (data) VALUES (?)", [123], function(tx) { - ok(false, "unexpected success"); - start(); - }, function(tx, error) { - strictEqual(error.code, SQLException.CONSTRAINT_ERR, "error.code === CONSTRAINT_ERR"); - equal(error.message, "Request failed: insert into test_table (data) VALUES (?),123", "error.message"); - start(); + if (!isWebSql && !/MSIE/.test(navigator.userAgent)) test(suiteName + ' delete then re-open allows subsequent queries to run', function () { + + // asynch test coming up + stop(1); + + var dbName = "Database-delete-and-Reopen"; + var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function () { + // success CB + window.sqlitePlugin.deleteDatabase(dbName, function () { + db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function () { + db.readTransaction(function (tx) { + tx.executeSql('SELECT 1', [], function (tx, results) { + ok(true, 'database re-opened succesfully'); + start(1); + }, function (error) { + ok(false, error); + start(1); + }, function (error) { + ok(false, error); + start(1); + }); + }, function (error) { + ok(false, error); + start(1); + }); + }, function (error) { + ok(false, error); + start(1); + }); + }, function (error) { + ok(false, error); + start(1); + }); + }, function (error) { + ok(false, error); + start(1); + }); + }); - // We want this error to fail the entire transaction - return true; + if (!isWebSql && !/MSIE/.test(navigator.userAgent)) test(suiteName + ' close, then delete then re-open allows subsequent queries to run', function () { + + // asynch test coming up + stop(1); + + var dbName = "Database-Close-delete-Reopen"; + var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); + db.close(function () { + window.sqlitePlugin.deleteDatabase(dbName, function () { + db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE, function () { + db.readTransaction(function (tx) { + tx.executeSql('SELECT 1', [], function (tx, results) { + ok(true, 'database re-opened succesfully'); + start(1); + }, function (e) { + ok(false, 'error: ' + e); + start(1); + }); + }, function (e) { + ok(false, 'error: ' + e); + start(1); + }); + }, function (e) { + ok(false, 'error: ' + e); + start(1); + }); + }, function (e) { + ok(false, 'error: ' + e); + start(1); }); - }, function(error) { - strictEqual(error.code, SQLException.CONSTRAINT_ERR, "error.code === CONSTRAINT_ERR"); - equal(error.message, "Request failed: insert into test_table (data) VALUES (?),123", "error.message"); - start(); + }, function (e) { + ok(false, 'error: ' + e); + start(1); + }); + }); + + if (!isWebSql) test(suiteName + ' repeatedly open and delete database succeeds', function () { + + // asynch test coming up + stop(5); + + var dbName = "repeatedly-open-and-delete"; + + var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); + window.sqlitePlugin.deleteDatabase(dbName, function () { + + db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); + window.sqlitePlugin.deleteDatabase(dbName, function () { + + db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); + window.sqlitePlugin.deleteDatabase(dbName, function () { + + db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); + window.sqlitePlugin.deleteDatabase(dbName, function () { + + db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); + window.sqlitePlugin.deleteDatabase(dbName, function () { + ok(true, 'success 5/5'); + + start(1); + }, function (error) { + ok(false, 'expected delete 5/5 error callback not to be called for an open database' + error); + start(1); + }); + + start(1); + }, function (error) { + ok(false, 'expected delete 4/5 error callback not to be called for an open database' + error); + start(1); + }); + + start(1); + }, function (error) { + ok(false, 'expected delete 3/5 error callback not to be called for an open database' + error); + start(1); + }); + + start(1); + }, function (error) { + ok(false, 'expected delete 2/5 error callback not to be called for an open database' + error); + start(1); + }); + + start(1); + }, function (error) { + ok(false, 'expected delete 1/5 error callback not to be called for an open database' + error); + start(5); }); }); - **/ + } })(); diff --git a/www/SQLitePlugin.js b/www/SQLitePlugin.js index fb829e571..28baa5348 100644 --- a/www/SQLitePlugin.js +++ b/www/SQLitePlugin.js @@ -72,10 +72,18 @@ }; SQLitePlugin.prototype.transaction = function(fn, error, success) { + if (!this.openDBs[this.dbname]) { + error('database not open'); + return; + } this.addTransaction(new SQLitePluginTransaction(this, fn, error, success, true, false)); }; SQLitePlugin.prototype.readTransaction = function(fn, error, success) { + if (!this.openDBs[this.dbname]) { + error('database not open'); + return; + } this.addTransaction(new SQLitePluginTransaction(this, fn, error, success, true, true)); }; @@ -93,16 +101,32 @@ }; SQLitePlugin.prototype.open = function(success, error) { + var onSuccess; + onSuccess = (function(_this) { + return function() { + return success(_this); + }; + })(this); if (!(this.dbname in this.openDBs)) { this.openDBs[this.dbname] = true; - cordova.exec(success, error, "SQLitePlugin", "open", [this.openargs]); + cordova.exec(onSuccess, error, "SQLitePlugin", "open", [this.openargs]); + } else { + + /* + for a re-open run onSuccess async so that the openDatabase return value + can be used in the success handler as an alternative to the handler's + db argument + */ + nextTick(function() { + return onSuccess(); + }); } }; SQLitePlugin.prototype.close = function(success, error) { if (this.dbname in this.openDBs) { delete this.openDBs[this.dbname]; - cordova.exec(null, null, "SQLitePlugin", "close", [ + cordova.exec(success, error, "SQLitePlugin", "close", [ { path: this.dbname } @@ -401,6 +425,7 @@ return new SQLitePlugin(openargs, okcb, errorcb); }), deleteDb: function(databaseName, success, error) { + delete SQLitePlugin.prototype.openDBs[databaseName]; return cordova.exec(success, error, "SQLitePlugin", "delete", [ { path: databaseName