From df2abe5681d0f7b5748524dbdecb4f06ab7192ad Mon Sep 17 00:00:00 2001 From: Chris Brody Date: Sat, 28 Feb 2015 19:55:00 +0100 Subject: [PATCH 01/28] Fix tests of ArrayBuffer blob test & db close failure (ref: pr #170) --- test-www/www/index.html | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/test-www/www/index.html b/test-www/www/index.html index a8ec66287..650f34cdf 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -590,16 +590,24 @@ }); // This test shows that the plugin does not throw an error when trying to serialize - // an unsupported parameter type. Blob becomes an empty dictionary on iOS, for example, + // a non-standard parameter type. Blob becomes an empty dictionary on iOS, for example, // and so this verifies the type is converted to a string and continues. Web SQL does // the same but on the JavaScript side and converts to a string like `[object Blob]`. - if (typeof ArrayBuffer !== "undefined") test(suiteName + "unsupported parameter type as string", function() { + test(suiteName + "INSERT Blob from ArrayBuffer (non-standard parameter type)", function() { + + ok(typeof ArrayBuffer !== "undefined", "ArrayBuffer type exists"); + + // abort the test if ArrayBuffer is undefined + // TODO: consider trying this for multiple non-standard parameter types instead + if (typeof ArrayBuffer === "undefined") return; + var db = openDatabase("Blob-test.db", "1.0", "Demo", DEFAULT_SIZE); ok(!!db, "db object"); stop(1); db.transaction(function(tx) { ok(!!tx, "tx object"); + stop(1); var buffer = new ArrayBuffer(5); var view = new Uint8Array(buffer); @@ -609,17 +617,20 @@ view[3] = 'l'.charCodeAt(); view[4] = 'o'.charCodeAt(); var blob = new Blob([view.buffer], { type:"application/octet-stream" }); - + tx.executeSql('DROP TABLE IF EXISTS test_table'); tx.executeSql('CREATE TABLE IF NOT EXISTS test_table (foo blob)'); tx.executeSql('INSERT INTO test_table VALUES (?)', [blob], function(tx, res) { - ok(true, "insert as string succeeds"); + ok(true, "INSERT blob OK"); + start(1); + }, function(tx, error) { + ok(false, "INSERT blob FAILED"); start(1); }); start(1); }, function(err) { - ok(false, "transaction does not serialize real data but still should not fail: " + err.message); - start(2); + ok(false, "transaction failure with message: " + err.message); + start(1); }); }); @@ -1266,10 +1277,9 @@ }); if (!isWebSql) test (suiteName + ' database.close fails in transaction', function () { - stop(1); + stop(2); var dbName = "Database-Close-fail"; - //var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); var db = openDatabase({name: dbName, location: 1}); db.readTransaction(function(tx) { From beecdbd9f4247b8067117232189595629f545cd6 Mon Sep 17 00:00:00 2001 From: Chris Brody Date: Sat, 28 Feb 2015 22:22:25 +0100 Subject: [PATCH 02/28] Test executeSql parameter as array (reproduce issue #144) --- test-www/www/index.html | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test-www/www/index.html b/test-www/www/index.html index 650f34cdf..6ffbeca63 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -589,6 +589,30 @@ }); }); + // XXX reproduce issue #140: + test(suiteName + "executeSql parameter as array", function() { + stop(); + var db = openDatabase("array-parameter.db", "1.0", "Demo", DEFAULT_SIZE); + db.transaction(function(tx) { + tx.executeSql('DROP TABLE IF EXISTS test_table'); + tx.executeSql('CREATE TABLE IF NOT EXISTS test_table (id integer primary key, data1, data2)'); + }, function(err) { ok(false, err.message) }, function() { + db.transaction(function(tx) { + // create columns with no type affinity + tx.executeSql("insert into test_table (data1, data2) VALUES (?,?)", ['abc', [1,2,3]], function(tx, res) { + equal(res.rowsAffected, 1, "row inserted"); + tx.executeSql("select * from test_table", [], function(tx, res) { + start(); + var row = res.rows.item(0); + strictEqual(row.data1, 'abc', "data1: string"); + // XXX #140 - only works with Web SQL: + if (isWebSql) strictEqual(row.data2, '1,2,3', "data2: array should have been inserted as text (string)"); + }); + }); + }); + }); + }); + // This test shows that the plugin does not throw an error when trying to serialize // a non-standard parameter type. Blob becomes an empty dictionary on iOS, for example, // and so this verifies the type is converted to a string and continues. Web SQL does From c777397e735f786d236bdb406f9f0598cfa1336c Mon Sep 17 00:00:00 2001 From: Chris Brody Date: Sat, 28 Feb 2015 23:19:18 +0100 Subject: [PATCH 03/28] Test with Unicode line separator character (ref #147 - broken for iOS version) --- test-www/www/index.html | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test-www/www/index.html b/test-www/www/index.html index 6ffbeca63..e847bab36 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -934,6 +934,43 @@ }); } + // XXX #147 iOS version of plugin BROKEN: + if (isWebSql || /Android/.test(navigator.userAgent)) test(suiteName + ' handles unicode line separator correctly', function () { + stop(2); + + var dbName = "Unicode-line-separator.db"; + var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); + + db.transaction(function (tx) { + tx.executeSql('DROP TABLE IF EXISTS test', [], function () { + tx.executeSql('CREATE TABLE test (name, id)', [], function() { + tx.executeSql('INSERT INTO test VALUES (?, "id1")', ['hello\u2028world'], function () { + tx.executeSql('SELECT name FROM test', [], function (tx, res) { + var name = res.rows.item(0).name; + + var expected = [ + 'hello\u2028world' + ]; + + ok(expected.indexOf(name) !== -1, 'field value: ' + + JSON.stringify(name) + ' should be in ' + + JSON.stringify(expected)); + + equal(name.length, 11, 'length of field should be 15'); + start(); + }) + }); + }); + }); + }, function(err) { + ok(false, 'unexpected error: ' + err.message); + start(2); + }, function () { + ok(true, 'transaction ok'); + start(); + }); + }); + test(suiteName + "syntax error", function() { var db = openDatabase("Syntax-error-test.db", "1.0", "Demo", DEFAULT_SIZE); ok(!!db, "db object"); From 385d9526e80e8fa7e7c9ac1c160aa114b6ef673f Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Tue, 30 Dec 2014 17:51:40 -0800 Subject: [PATCH 04/28] Propagate statement failures to transaction failures. According to the W3C spec, if a transaction failure callback *does not return false* then the error is propagated to a transaction failure. This means that if a transaction returns true, but more importantly, if it has no return value (undefined) then the error must propagate to the transaction. This fix explicitly requires false to correct the propagation. A subsequent fix will address the fact that the original statement error is not propagated and is replaced here, but that is unrelated to this issue. (@brodybits) from PR #170 thanks @aarononeal, adding www/SQLitePlugin.js as updated from SQLitePlugin.coffee.md --- SQLitePlugin.coffee.md | 2 +- test-www/www/index.html | 3 ++- www/SQLitePlugin.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 46ad846c7..586097117 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -233,7 +233,7 @@ SQLitePluginTransaction::handleStatementFailure = (handler, response) -> if !handler throw new Error "a statement with no error handler failed: " + response.message - if handler(this, response) + if handler(this, response) isnt false throw new Error "a statement error callback did not return false" return diff --git a/test-www/www/index.html b/test-www/www/index.html index e847bab36..2f470c7c9 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -508,11 +508,12 @@ }, function(tx, err) { start(); ok(!!err.message, "should report a valid error message"); + return false; }); }); }, function(err) { start(); - ok(false, "transaction was supposed to succeed"); + ok(false, "transaction was supposed to succeed: " + err.message); }, function() { db.transaction(function(tx) { tx.executeSql("select count(*) as cnt from test_table", [], function(tx, res) { diff --git a/www/SQLitePlugin.js b/www/SQLitePlugin.js index 24788e858..d8d7ddeae 100644 --- a/www/SQLitePlugin.js +++ b/www/SQLitePlugin.js @@ -246,7 +246,7 @@ if (!handler) { throw new Error("a statement with no error handler failed: " + response.message); } - if (handler(this, response)) { + if (handler(this, response) !== false) { throw new Error("a statement error callback did not return false"); } }; From 3567f475b1ea7794a9b44651c6fc6f2158d0ed64 Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Tue, 30 Dec 2014 18:21:23 -0800 Subject: [PATCH 05/28] Fix transaction and statement errors to conform to SQLError. Sometimes the native layer returns errors as {message:string, code:number} and other times as just a string. This fix wraps key places to ensure that errors returned through the API always conform to SQLError per the API contract documented in the W3C spec. In short, errors consistently have a message and code now and propagate from statement failure up to the transaction. (@brodybits) from PR #170 thanks @aarononeal, adding www/SQLitePlugin.js as updated from SQLitePlugin.coffee.md --- SQLitePlugin.coffee.md | 43 ++++++++++++++++++++++++++++++--------- www/SQLitePlugin.js | 46 ++++++++++++++++++++++++++++++++---------- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 586097117..9d1748d19 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -20,6 +20,29 @@ ## utility functions: + # Errors returned to callbacks must conform to `SqlError` with a code and message. + # Some errors are of type `Error` or `string` and must be converted. + toSQLError = (error, code) -> + sqlError = error + code = 0 if !code # unknown by default + + if !sqlError + sqlError = new Error "a plugin had an error but provided no response" + sqlError.code = code + + if typeof sqlError is "string" + sqlError = new Error error + sqlError.code = code + + if !sqlError.code && sqlError.message + sqlError.code = code + + if !sqlError.code && !sqlError.message + sqlError = new Error "an unknown error was returned: " + JSON.stringify(sqlError) + sqlError.code = code + + return sqlError + nextTick = window.setImmediate || (fun) -> window.setTimeout(fun, 0) return @@ -87,14 +110,14 @@ SQLitePlugin::transaction = (fn, error, success) -> if !@openDBs[@dbname] - error('database not open') + error(toSQLError('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') + error(toSQLError('database not open')) return @addTransaction new SQLitePluginTransaction(this, fn, error, success, true, true) return @@ -174,7 +197,7 @@ if txlock @executeSql "BEGIN", [], null, (tx, err) -> - throw new Error("unable to begin transaction: " + err.message) + throw toSQLError("unable to begin transaction: " + err.message, err.code) return @@ -189,7 +212,7 @@ txLocks[@db.dbname].inProgress = false @db.startNextTransaction() if @error - @error err + @error toSQLError(err) return SQLitePluginTransaction::executeSql = (sql, values, success, error) -> @@ -232,9 +255,9 @@ SQLitePluginTransaction::handleStatementFailure = (handler, response) -> if !handler - throw new Error "a statement with no error handler failed: " + response.message + throw toSQLError("a statement with no error handler failed: " + response.message, response.code) if handler(this, response) isnt false - throw new Error "a statement error callback did not return false" + throw toSQLError("a statement error callback did not return false: " + response.message, response.code) return SQLitePluginTransaction::run = -> @@ -252,9 +275,9 @@ if didSucceed tx.handleStatementSuccess batchExecutes[index].success, response else - tx.handleStatementFailure batchExecutes[index].error, response + tx.handleStatementFailure batchExecutes[index].error, toSQLError(response) catch err - txFailure = err unless txFailure + txFailure = toSQLError(err) unless txFailure if --waiting == 0 if txFailure @@ -323,7 +346,7 @@ failed = (tx, err) -> txLocks[tx.db.dbname].inProgress = false tx.db.startNextTransaction() - if tx.error then tx.error new Error("error while trying to roll back: " + err.message) + if tx.error then tx.error toSQLError("error while trying to roll back: " + err.message, err.code) return @finalized = true @@ -349,7 +372,7 @@ failed = (tx, err) -> txLocks[tx.db.dbname].inProgress = false tx.db.startNextTransaction() - if tx.error then tx.error new Error("error while trying to commit: " + err.message) + if tx.error then tx.error toSQLError("error while trying to commit: " + err.message, err.code) return @finalized = true diff --git a/www/SQLitePlugin.js b/www/SQLitePlugin.js index d8d7ddeae..9f23e6244 100644 --- a/www/SQLitePlugin.js +++ b/www/SQLitePlugin.js @@ -1,5 +1,5 @@ (function() { - var READ_ONLY_REGEX, SQLiteFactory, SQLitePlugin, SQLitePluginTransaction, argsArray, dblocations, nextTick, root, txLocks; + var READ_ONLY_REGEX, SQLiteFactory, SQLitePlugin, SQLitePluginTransaction, argsArray, dblocations, nextTick, root, toSQLError, txLocks; root = this; @@ -7,6 +7,30 @@ txLocks = {}; + toSQLError = function(error, code) { + var sqlError; + sqlError = error; + if (!code) { + code = 0; + } + if (!sqlError) { + sqlError = new Error("a plugin had an error but provided no response"); + sqlError.code = code; + } + if (typeof sqlError === "string") { + sqlError = new Error(error); + sqlError.code = code; + } + if (!sqlError.code && sqlError.message) { + sqlError.code = code; + } + if (!sqlError.code && !sqlError.message) { + sqlError = new Error("an unknown error was returned: " + JSON.stringify(sqlError)); + sqlError.code = code; + } + return sqlError; + }; + nextTick = window.setImmediate || function(fun) { window.setTimeout(fun, 0); }; @@ -73,7 +97,7 @@ SQLitePlugin.prototype.transaction = function(fn, error, success) { if (!this.openDBs[this.dbname]) { - error('database not open'); + error(toSQLError('database not open')); return; } this.addTransaction(new SQLitePluginTransaction(this, fn, error, success, true, false)); @@ -81,7 +105,7 @@ SQLitePlugin.prototype.readTransaction = function(fn, error, success) { if (!this.openDBs[this.dbname]) { - error('database not open'); + error(toSQLError('database not open')); return; } this.addTransaction(new SQLitePluginTransaction(this, fn, error, success, true, true)); @@ -181,7 +205,7 @@ this.executes = []; if (txlock) { this.executeSql("BEGIN", [], null, function(tx, err) { - throw new Error("unable to begin transaction: " + err.message); + throw toSQLError("unable to begin transaction: " + err.message, err.code); }); } }; @@ -200,7 +224,7 @@ txLocks[this.db.dbname].inProgress = false; this.db.startNextTransaction(); if (this.error) { - this.error(err); + this.error(toSQLError(err)); } } }; @@ -244,10 +268,10 @@ SQLitePluginTransaction.prototype.handleStatementFailure = function(handler, response) { if (!handler) { - throw new Error("a statement with no error handler failed: " + response.message); + throw toSQLError("a statement with no error handler failed: " + response.message, response.code); } if (handler(this, response) !== false) { - throw new Error("a statement error callback did not return false"); + throw toSQLError("a statement error callback did not return false: " + response.message, response.code); } }; @@ -266,12 +290,12 @@ if (didSucceed) { tx.handleStatementSuccess(batchExecutes[index].success, response); } else { - tx.handleStatementFailure(batchExecutes[index].error, response); + tx.handleStatementFailure(batchExecutes[index].error, toSQLError(response)); } } catch (_error) { err = _error; if (!txFailure) { - txFailure = err; + txFailure = toSQLError(err); } } if (--waiting === 0) { @@ -348,7 +372,7 @@ txLocks[tx.db.dbname].inProgress = false; tx.db.startNextTransaction(); if (tx.error) { - tx.error(new Error("error while trying to roll back: " + err.message)); + tx.error(toSQLError("error while trying to roll back: " + err.message, err.code)); } }; this.finalized = true; @@ -377,7 +401,7 @@ txLocks[tx.db.dbname].inProgress = false; tx.db.startNextTransaction(); if (tx.error) { - tx.error(new Error("error while trying to commit: " + err.message)); + tx.error(toSQLError("error while trying to commit: " + err.message, err.code)); } }; this.finalized = true; From e07a74a0d147b823f2d55a93b4929a54668523c8 Mon Sep 17 00:00:00 2001 From: Chris Brody Date: Sat, 28 Feb 2015 23:54:02 +0100 Subject: [PATCH 06/28] Rename toSQLError to newSQLError in SQLitePlugin.coffee.md (ref: PR #170) & other minor cleanups --- SQLitePlugin.coffee.md | 55 +++++++++++++++++++++++------------------- www/SQLitePlugin.js | 38 ++++++++++++++--------------- 2 files changed, 49 insertions(+), 44 deletions(-) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 9d1748d19..4f69c20ab 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -22,27 +22,27 @@ # Errors returned to callbacks must conform to `SqlError` with a code and message. # Some errors are of type `Error` or `string` and must be converted. - toSQLError = (error, code) -> + newSQLError = (error, code) -> sqlError = error code = 0 if !code # unknown by default - + if !sqlError sqlError = new Error "a plugin had an error but provided no response" sqlError.code = code - + if typeof sqlError is "string" sqlError = new Error error sqlError.code = code - + if !sqlError.code && sqlError.message sqlError.code = code - + if !sqlError.code && !sqlError.message sqlError = new Error "an unknown error was returned: " + JSON.stringify(sqlError) sqlError.code = code - + return sqlError - + nextTick = window.setImmediate || (fun) -> window.setTimeout(fun, 0) return @@ -71,7 +71,7 @@ console.log "SQLitePlugin openargs: #{JSON.stringify openargs}" if !(openargs and openargs['name']) - throw new Error("Cannot create a SQLitePlugin instance without a db name") + throw newSQLError "Cannot create a SQLitePlugin db instance without a db name" dbname = openargs.name @@ -110,15 +110,17 @@ SQLitePlugin::transaction = (fn, error, success) -> if !@openDBs[@dbname] - error(toSQLError('database not open')) + error newSQLError 'database not open' return + @addTransaction new SQLitePluginTransaction(this, fn, error, success, true, false) return SQLitePlugin::readTransaction = (fn, error, success) -> if !@openDBs[@dbname] - error(toSQLError('database not open')) + error newSQLError 'database not open' return + @addTransaction new SQLitePluginTransaction(this, fn, error, success, true, true) return @@ -135,16 +137,18 @@ SQLitePlugin::open = (success, error) -> onSuccess = () => success this - unless @dbname of @openDBs - @openDBs[@dbname] = true - cordova.exec onSuccess, error, "SQLitePlugin", "open", [ @openargs ] - else + + if @dbname of @openDBs ### 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(); + else + @openDBs[@dbname] = true + cordova.exec onSuccess, error, "SQLitePlugin", "open", [ @openargs ] + return SQLitePlugin::close = (success, error) -> @@ -152,7 +156,7 @@ if @dbname of @openDBs if txLocks[@dbname] && txLocks[@dbname].inProgress - error(new Error('database cannot be closed while a transaction is in progress')) + error newSQLError 'database cannot be closed while a transaction is in progress' return delete @openDBs[@dbname] @@ -185,7 +189,7 @@ prevents us from stalling our txQueue if somebody passes a false value for fn. ### - throw new Error("transaction expected a function") + throw newSQLError "transaction expected a function" @db = db @fn = fn @@ -197,7 +201,7 @@ if txlock @executeSql "BEGIN", [], null, (tx, err) -> - throw toSQLError("unable to begin transaction: " + err.message, err.code) + throw newSQLError "unable to begin transaction: " + err.message, err.code return @@ -212,7 +216,7 @@ txLocks[@db.dbname].inProgress = false @db.startNextTransaction() if @error - @error toSQLError(err) + @error newSQLError err return SQLitePluginTransaction::executeSql = (sql, values, success, error) -> @@ -255,9 +259,9 @@ SQLitePluginTransaction::handleStatementFailure = (handler, response) -> if !handler - throw toSQLError("a statement with no error handler failed: " + response.message, response.code) + throw newSQLError "a statement with no error handler failed: " + response.message, response.code if handler(this, response) isnt false - throw toSQLError("a statement error callback did not return false: " + response.message, response.code) + throw newSQLError "a statement error callback did not return false: " + response.message, response.code return SQLitePluginTransaction::run = -> @@ -275,9 +279,10 @@ if didSucceed tx.handleStatementSuccess batchExecutes[index].success, response else - tx.handleStatementFailure batchExecutes[index].error, toSQLError(response) + tx.handleStatementFailure batchExecutes[index].error, newSQLError(response) catch err - txFailure = toSQLError(err) unless txFailure + if !txFailure + txFailure = newSQLError(err) if --waiting == 0 if txFailure @@ -346,7 +351,7 @@ failed = (tx, err) -> txLocks[tx.db.dbname].inProgress = false tx.db.startNextTransaction() - if tx.error then tx.error toSQLError("error while trying to roll back: " + err.message, err.code) + if tx.error then tx.error newSQLError("error while trying to roll back: " + err.message, err.code) return @finalized = true @@ -372,7 +377,7 @@ failed = (tx, err) -> txLocks[tx.db.dbname].inProgress = false tx.db.startNextTransaction() - if tx.error then tx.error toSQLError("error while trying to commit: " + err.message, err.code) + if tx.error then tx.error newSQLError("error while trying to commit: " + err.message, err.code) return @finalized = true @@ -436,7 +441,7 @@ else #console.log "delete db args: #{JSON.stringify first}" - if !(first and first['name']) then throw new Error("Please specify db name") + if !(first and first['name']) then throw new Error "Please specify db name" args.path = first.name dblocation = if !!first.location then dblocations[first.location] else null args.dblocation = dblocation || dblocations[0] diff --git a/www/SQLitePlugin.js b/www/SQLitePlugin.js index 9f23e6244..6a3a20ab1 100644 --- a/www/SQLitePlugin.js +++ b/www/SQLitePlugin.js @@ -1,5 +1,5 @@ (function() { - var READ_ONLY_REGEX, SQLiteFactory, SQLitePlugin, SQLitePluginTransaction, argsArray, dblocations, nextTick, root, toSQLError, txLocks; + var READ_ONLY_REGEX, SQLiteFactory, SQLitePlugin, SQLitePluginTransaction, argsArray, dblocations, newSQLError, nextTick, root, txLocks; root = this; @@ -7,7 +7,7 @@ txLocks = {}; - toSQLError = function(error, code) { + newSQLError = function(error, code) { var sqlError; sqlError = error; if (!code) { @@ -62,7 +62,7 @@ var dbname; console.log("SQLitePlugin openargs: " + (JSON.stringify(openargs))); if (!(openargs && openargs['name'])) { - throw new Error("Cannot create a SQLitePlugin instance without a db name"); + throw newSQLError("Cannot create a SQLitePlugin db instance without a db name"); } dbname = openargs.name; this.openargs = openargs; @@ -97,7 +97,7 @@ SQLitePlugin.prototype.transaction = function(fn, error, success) { if (!this.openDBs[this.dbname]) { - error(toSQLError('database not open')); + error(newSQLError('database not open')); return; } this.addTransaction(new SQLitePluginTransaction(this, fn, error, success, true, false)); @@ -105,7 +105,7 @@ SQLitePlugin.prototype.readTransaction = function(fn, error, success) { if (!this.openDBs[this.dbname]) { - error(toSQLError('database not open')); + error(newSQLError('database not open')); return; } this.addTransaction(new SQLitePluginTransaction(this, fn, error, success, true, true)); @@ -131,10 +131,7 @@ return success(_this); }; })(this); - if (!(this.dbname in this.openDBs)) { - this.openDBs[this.dbname] = true; - cordova.exec(onSuccess, error, "SQLitePlugin", "open", [this.openargs]); - } else { + if (this.dbname in this.openDBs) { /* for a re-open run onSuccess async so that the openDatabase return value @@ -144,13 +141,16 @@ nextTick(function() { return onSuccess(); }); + } else { + this.openDBs[this.dbname] = true; + cordova.exec(onSuccess, error, "SQLitePlugin", "open", [this.openargs]); } }; SQLitePlugin.prototype.close = function(success, error) { if (this.dbname in this.openDBs) { if (txLocks[this.dbname] && txLocks[this.dbname].inProgress) { - error(new Error('database cannot be closed while a transaction is in progress')); + error(newSQLError('database cannot be closed while a transaction is in progress')); return; } delete this.openDBs[this.dbname]; @@ -194,7 +194,7 @@ prevents us from stalling our txQueue if somebody passes a false value for fn. */ - throw new Error("transaction expected a function"); + throw newSQLError("transaction expected a function"); } this.db = db; this.fn = fn; @@ -205,7 +205,7 @@ this.executes = []; if (txlock) { this.executeSql("BEGIN", [], null, function(tx, err) { - throw toSQLError("unable to begin transaction: " + err.message, err.code); + throw newSQLError("unable to begin transaction: " + err.message, err.code); }); } }; @@ -224,7 +224,7 @@ txLocks[this.db.dbname].inProgress = false; this.db.startNextTransaction(); if (this.error) { - this.error(toSQLError(err)); + this.error(newSQLError(err)); } } }; @@ -268,10 +268,10 @@ SQLitePluginTransaction.prototype.handleStatementFailure = function(handler, response) { if (!handler) { - throw toSQLError("a statement with no error handler failed: " + response.message, response.code); + throw newSQLError("a statement with no error handler failed: " + response.message, response.code); } if (handler(this, response) !== false) { - throw toSQLError("a statement error callback did not return false: " + response.message, response.code); + throw newSQLError("a statement error callback did not return false: " + response.message, response.code); } }; @@ -290,12 +290,12 @@ if (didSucceed) { tx.handleStatementSuccess(batchExecutes[index].success, response); } else { - tx.handleStatementFailure(batchExecutes[index].error, toSQLError(response)); + tx.handleStatementFailure(batchExecutes[index].error, newSQLError(response)); } } catch (_error) { err = _error; if (!txFailure) { - txFailure = toSQLError(err); + txFailure = newSQLError(err); } } if (--waiting === 0) { @@ -372,7 +372,7 @@ txLocks[tx.db.dbname].inProgress = false; tx.db.startNextTransaction(); if (tx.error) { - tx.error(toSQLError("error while trying to roll back: " + err.message, err.code)); + tx.error(newSQLError("error while trying to roll back: " + err.message, err.code)); } }; this.finalized = true; @@ -401,7 +401,7 @@ txLocks[tx.db.dbname].inProgress = false; tx.db.startNextTransaction(); if (tx.error) { - tx.error(toSQLError("error while trying to commit: " + err.message, err.code)); + tx.error(newSQLError("error while trying to commit: " + err.message, err.code)); } }; this.finalized = true; From 31c7d2139dfedc95f377f560f1c59678a86be8b7 Mon Sep 17 00:00:00 2001 From: Chris Brody Date: Sun, 1 Mar 2015 00:00:29 +0100 Subject: [PATCH 07/28] Fix iOS sqlite regex check to check for insufficient arguments first See PR #170: similar to commit c19698b but with a different solution --- src/ios/SQLitePlugin.m | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/ios/SQLitePlugin.m b/src/ios/SQLitePlugin.m index 6302f6d47..5a0e943a8 100755 --- a/src/ios/SQLitePlugin.m +++ b/src/ios/SQLitePlugin.m @@ -132,25 +132,31 @@ static int base64_encode_blockend(char* code_out, //LIBB64---END static void sqlite_regexp(sqlite3_context* context, int argc, sqlite3_value** values) { - int ret; - regex_t regex; + if ( argc < 2 ) { + sqlite3_result_error(context, "SQL function regexp() called with missing arguments.", -1); + return; + } + char* reg = (char*)sqlite3_value_text(values[0]); char* text = (char*)sqlite3_value_text(values[1]); - + if ( argc != 2 || reg == 0 || text == 0) { - sqlite3_result_error(context, "SQL function regexp() called with invalid arguments.\n", -1); + sqlite3_result_error(context, "SQL function regexp() called with invalid arguments.", -1); return; } - + + int ret; + regex_t regex; + ret = regcomp(®ex, reg, REG_EXTENDED | REG_NOSUB); if ( ret != 0 ) { sqlite3_result_error(context, "error compiling regular expression", -1); return; } - + ret = regexec(®ex, text , 0, NULL, 0); regfree(®ex); - + sqlite3_result_int(context, (ret != REG_NOMATCH)); } From ef53698d3f3fe4c960d64729c99161dd719a476f Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Wed, 31 Dec 2014 00:18:51 -0800 Subject: [PATCH 08/28] Fix warning regarding test runner viewport format. Parameters use `,` not `;`. (@brodybits) from PR #170 thanks @aarononeal --- test-www/www/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-www/www/index.html b/test-www/www/index.html index 2f470c7c9..09c0618c2 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -1,7 +1,7 @@ - + SQLitePlugin test From 7fa5894929e0cc363c73dbff499534171c327f97 Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Thu, 1 Jan 2015 18:36:36 -0800 Subject: [PATCH 09/28] Fix executeSql to throw on finalized transactions. If executeSql is called on a finalized transaction it should throw. If it does not, timing errors get a lot harder to debug. ES6 promises, for example, generally execute on the next tick and then it becomes unclear why a statement fails to execute. (@brodybits) from PR #170 thanks @aarononeal, adding www/SQLitePlugin.js as updated from SQLitePlugin.coffee.md --- SQLitePlugin.coffee.md | 15 +++++++++++++-- test-www/www/index.html | 32 ++++++++++++++++++++++++++++++++ www/SQLitePlugin.js | 15 +++++++++++++-- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 4f69c20ab..552da28f0 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -221,6 +221,17 @@ SQLitePluginTransaction::executeSql = (sql, values, success, error) -> + if @finalized + throw {message: 'InvalidStateError: DOM Exception 11: This transaction is already finalized. Transactions are committed after its success or failure handlers are called. If you are using a Promise to handle callbacks, be aware that implementations following the A+ standard adhere to run-to-completion semantics and so Promise resolution occurs on a subsequent tick and therefore after the transaction commits.', code: 11} + return + + @_executeSqlInternal(sql, values, success, error) + return + + # This method performs the actual execute but does not check for + # finalization since it is used to execute COMMIT and ROLLBACK. + SQLitePluginTransaction::_executeSqlInternal = (sql, values, success, error) -> + if @readOnly && READ_ONLY_REGEX.test(sql) @handleStatementFailure(error, {message: 'invalid sql for a read-only transaction'}) return @@ -357,7 +368,7 @@ @finalized = true if @txlock - @executeSql "ROLLBACK", [], succeeded, failed + @_executeSqlInternal "ROLLBACK", [], succeeded, failed @run() else succeeded(tx) @@ -383,7 +394,7 @@ @finalized = true if @txlock - @executeSql "COMMIT", [], succeeded, failed + @_executeSqlInternal "COMMIT", [], succeeded, failed @run() else succeeded(tx) diff --git a/test-www/www/index.html b/test-www/www/index.html index 09c0618c2..b10d4beea 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -549,6 +549,38 @@ }); }); }); + + test(suiteName + "executeSql fails outside transaction", function() { + withTestTable(function(db) { + expect(5); + ok(!!db, "db ok"); + var txg; + stop(2); + db.transaction(function(tx) { + ok(!!tx, "tx ok"); + txg = tx; + tx.executeSql("insert into test_table (data, data_num) VALUES (?,?)", ['test', null], function(tx, res) { + equal(res.rowsAffected, 1, 'row inserted'); + }); + start(1); + }, function(err) { + ok(false, err); + start(1); + }, function() { + // this simulates what would happen if a Promise ran on the next tick + // and invoked an execute on the transaction + try { + txg.executeSql("select count(*) as cnt from test_table", [], null, null); + ok(false, "executeSql should have thrown but continued instead"); + } catch(err) { + ok(!!err.message, "error had valid message"); + ok(/InvalidStateError|SQLTransaction/.test(err.message), + "execute must throw InvalidStateError; actual error: " + err.message); + } + start(1); + }); + }); + }); test(suiteName + "all columns should be included in result set (including 'null' columns)", function() { withTestTable(function(db) { diff --git a/www/SQLitePlugin.js b/www/SQLitePlugin.js index 6a3a20ab1..c6cf1d4cc 100644 --- a/www/SQLitePlugin.js +++ b/www/SQLitePlugin.js @@ -230,6 +230,17 @@ }; SQLitePluginTransaction.prototype.executeSql = function(sql, values, success, error) { + if (this.finalized) { + throw { + message: 'InvalidStateError: DOM Exception 11: This transaction is already finalized. Transactions are committed after its success or failure handlers are called. If you are using a Promise to handle callbacks, be aware that implementations following the A+ standard adhere to run-to-completion semantics and so Promise resolution occurs on a subsequent tick and therefore after the transaction commits.', + code: 11 + }; + return; + } + this._executeSqlInternal(sql, values, success, error); + }; + + SQLitePluginTransaction.prototype._executeSqlInternal = function(sql, values, success, error) { var qid; if (this.readOnly && READ_ONLY_REGEX.test(sql)) { this.handleStatementFailure(error, { @@ -377,7 +388,7 @@ }; this.finalized = true; if (this.txlock) { - this.executeSql("ROLLBACK", [], succeeded, failed); + this._executeSqlInternal("ROLLBACK", [], succeeded, failed); this.run(); } else { succeeded(tx); @@ -406,7 +417,7 @@ }; this.finalized = true; if (this.txlock) { - this.executeSql("COMMIT", [], succeeded, failed); + this._executeSqlInternal("COMMIT", [], succeeded, failed); this.run(); } else { succeeded(tx); From 042c2f4b6be9db18991a9ecc19692adf85065ec1 Mon Sep 17 00:00:00 2001 From: Chris Brody Date: Sun, 1 Mar 2015 01:40:08 +0100 Subject: [PATCH 10/28] Remove implementation-dependent error message check (ref: PR #170) --- test-www/www/index.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test-www/www/index.html b/test-www/www/index.html index b10d4beea..ae35a8670 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -552,7 +552,7 @@ test(suiteName + "executeSql fails outside transaction", function() { withTestTable(function(db) { - expect(5); + expect(4); ok(!!db, "db ok"); var txg; stop(2); @@ -574,8 +574,6 @@ ok(false, "executeSql should have thrown but continued instead"); } catch(err) { ok(!!err.message, "error had valid message"); - ok(/InvalidStateError|SQLTransaction/.test(err.message), - "execute must throw InvalidStateError; actual error: " + err.message); } start(1); }); From c482e62ac34f6623db7d4ee81b545dfd610c5618 Mon Sep 17 00:00:00 2001 From: Chris Brody Date: Sun, 1 Mar 2015 01:40:54 +0100 Subject: [PATCH 11/28] Use internal function to add SQL statement in more places (ref: PR #170) --- SQLitePlugin.coffee.md | 21 ++++++++++----------- www/SQLitePlugin.js | 18 +++++++++--------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 552da28f0..a08fb2116 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -170,7 +170,7 @@ myerror = (t, e) -> if !!error then error e myfn = (tx) -> - tx.executeSql(statement, params, mysuccess, myerror) + tx.addStatement(statement, params, mysuccess, myerror) return @addTransaction new SQLitePluginTransaction(this, myfn, null, null, false, false) @@ -200,7 +200,7 @@ @executes = [] if txlock - @executeSql "BEGIN", [], null, (tx, err) -> + @addStatement "BEGIN", [], null, (tx, err) -> throw newSQLError "unable to begin transaction: " + err.message, err.code return @@ -225,17 +225,16 @@ throw {message: 'InvalidStateError: DOM Exception 11: This transaction is already finalized. Transactions are committed after its success or failure handlers are called. If you are using a Promise to handle callbacks, be aware that implementations following the A+ standard adhere to run-to-completion semantics and so Promise resolution occurs on a subsequent tick and therefore after the transaction commits.', code: 11} return - @_executeSqlInternal(sql, values, success, error) - return - - # This method performs the actual execute but does not check for - # finalization since it is used to execute COMMIT and ROLLBACK. - SQLitePluginTransaction::_executeSqlInternal = (sql, values, success, error) -> - if @readOnly && READ_ONLY_REGEX.test(sql) @handleStatementFailure(error, {message: 'invalid sql for a read-only transaction'}) return + @addStatement(sql, values, success, error) + return + + # This method adds the SQL statement to the transaction queue but does not check for + # finalization since it is used to execute COMMIT and ROLLBACK. + SQLitePluginTransaction::addStatement = (sql, values, success, error) -> qid = @executes.length @@ -368,7 +367,7 @@ @finalized = true if @txlock - @_executeSqlInternal "ROLLBACK", [], succeeded, failed + @addStatement "ROLLBACK", [], succeeded, failed @run() else succeeded(tx) @@ -394,7 +393,7 @@ @finalized = true if @txlock - @_executeSqlInternal "COMMIT", [], succeeded, failed + @addStatement "COMMIT", [], succeeded, failed @run() else succeeded(tx) diff --git a/www/SQLitePlugin.js b/www/SQLitePlugin.js index c6cf1d4cc..e66952f80 100644 --- a/www/SQLitePlugin.js +++ b/www/SQLitePlugin.js @@ -175,7 +175,7 @@ } }; myfn = function(tx) { - tx.executeSql(statement, params, mysuccess, myerror); + tx.addStatement(statement, params, mysuccess, myerror); }; this.addTransaction(new SQLitePluginTransaction(this, myfn, null, null, false, false)); }; @@ -204,7 +204,7 @@ this.readOnly = readOnly; this.executes = []; if (txlock) { - this.executeSql("BEGIN", [], null, function(tx, err) { + this.addStatement("BEGIN", [], null, function(tx, err) { throw newSQLError("unable to begin transaction: " + err.message, err.code); }); } @@ -237,17 +237,17 @@ }; return; } - this._executeSqlInternal(sql, values, success, error); - }; - - SQLitePluginTransaction.prototype._executeSqlInternal = function(sql, values, success, error) { - var qid; if (this.readOnly && READ_ONLY_REGEX.test(sql)) { this.handleStatementFailure(error, { message: 'invalid sql for a read-only transaction' }); return; } + this.addStatement(sql, values, success, error); + }; + + SQLitePluginTransaction.prototype.addStatement = function(sql, values, success, error) { + var qid; qid = this.executes.length; this.executes.push({ success: success, @@ -388,7 +388,7 @@ }; this.finalized = true; if (this.txlock) { - this._executeSqlInternal("ROLLBACK", [], succeeded, failed); + this.addStatement("ROLLBACK", [], succeeded, failed); this.run(); } else { succeeded(tx); @@ -417,7 +417,7 @@ }; this.finalized = true; if (this.txlock) { - this._executeSqlInternal("COMMIT", [], succeeded, failed); + this.addStatement("COMMIT", [], succeeded, failed); this.run(); } else { succeeded(tx); From f941d4257a9430bf3fbc91c6224badb6b7f33f4e Mon Sep 17 00:00:00 2001 From: Chris Brody Date: Sun, 1 Mar 2015 08:44:39 +0100 Subject: [PATCH 12/28] Close Android database before removing from map (ref: #150/#153) --- src/android/org/pgsqlite/SQLitePlugin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/android/org/pgsqlite/SQLitePlugin.java b/src/android/org/pgsqlite/SQLitePlugin.java index 0d863f972..41a635115 100755 --- a/src/android/org/pgsqlite/SQLitePlugin.java +++ b/src/android/org/pgsqlite/SQLitePlugin.java @@ -856,10 +856,10 @@ public void run() { if (dbq != null && dbq.close) { try { - dbrmap.remove(dbname); // (should) remove ourself - closeDatabaseNow(dbname); + dbrmap.remove(dbname); // (should) remove ourself + if (!dbq.delete) { dbq.cbc.success(); } else { From 840f33294638286e1235776ed51621b4abbbc451 Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Sun, 1 Mar 2015 09:04:51 +0100 Subject: [PATCH 13/28] REPRODUCE truncation in iOS query result string encoding. Strings with Unicode values like `\u0000` ARE being truncated because the string constructor was using a null terminator to determine size instead of the stored byte length. (@brodybits) from PR #170 thanks @aarononeal. The fix will be committed separately. --- test-www/www/index.html | 50 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test-www/www/index.html b/test-www/www/index.html index ae35a8670..3b49289e7 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -965,6 +965,56 @@ }); } + test(suiteName + ' returns unicode correctly', function () { + stop(); + + var dbName = "Database-Unicode"; + var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); + + db.transaction(function (tx) { + tx.executeSql('DROP TABLE IF EXISTS test', [], function () { + tx.executeSql('CREATE TABLE test (name, id)', [], function() { + tx.executeSql('INSERT INTO test VALUES (?, "id1")', ['\u0000foo'], function () { + tx.executeSql('SELECT name FROM test', [], function (tx, res) { + var name = res.rows.item(0).name; + + var expected = [ + '\u0000foo' + ]; + + // There is a bug in WebKit and Chromium where strings are created + // using methods that rely on '\0' for termination instead of + // the specified byte length. + // + // https://bugs.webkit.org/show_bug.cgi?id=137637 + // + // For now we expect this test to fail there, but when it is fixed + // we would like to know, so the test is coded to fail if it starts + // working there. + if(!isWebSql) { + ok(expected.indexOf(name) !== -1, 'field value: ' + + JSON.stringify(name) + ' should be in ' + + JSON.stringify(expected)); + + equal(name.length, 4, 'length of field === 4'); + } else { + ok(expected.indexOf(name) === -1, 'field value: ' + + JSON.stringify(name) + ' should not be in this until a bug is fixed ' + + JSON.stringify(expected)); + + equal(name.length, 0, 'length of field === 0'); + } + start(); + }) + }); + }); + }); + }, function(err) { + ok(false, 'unexpected error: ' + err.message); + }, function () { + }); + }); + // XXX #147 iOS version of plugin BROKEN: if (isWebSql || /Android/.test(navigator.userAgent)) test(suiteName + ' handles unicode line separator correctly', function () { stop(2); From 51e27dbef64f617fe5dddacc79ed44e3a88b93b9 Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Tue, 30 Dec 2014 20:57:26 -0800 Subject: [PATCH 14/28] Fix truncation in iOS query result string encoding. This fix enables end-to-end encoding of binary data using strings. Strings with Unicode values like `\u0000` were being truncated because the string constructor was using a null terminator to determine size instead of the stored byte length. The fix uses a different constructor and the stored byte length so that the full length string is returned. (@brodybits) from PR #170 thanks @aarononeal. Test was already committed separately. --- src/ios/SQLitePlugin.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ios/SQLitePlugin.m b/src/ios/SQLitePlugin.m index 5a0e943a8..fb626ea9f 100755 --- a/src/ios/SQLitePlugin.m +++ b/src/ios/SQLitePlugin.m @@ -512,7 +512,12 @@ -(CDVPluginResult*) executeSqlWithDict: (NSMutableDictionary*)options andArgs: ( columnValue = [NSNumber numberWithDouble: sqlite3_column_double(statement, i)]; break; case SQLITE_TEXT: - columnValue = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, i)]; + columnValue = [[NSString alloc] initWithBytes:(char *)sqlite3_column_text(statement, i) + length:sqlite3_column_bytes(statement, i) + encoding:NSUTF8StringEncoding]; +#if !__has_feature(objc_arc) + [columnValue autorelease]; +#endif break; case SQLITE_BLOB: //LIBB64 From 6d0e0dc13186a9a14894543466ca66c25315d88e Mon Sep 17 00:00:00 2001 From: Aaron Oneal Date: Thu, 1 Jan 2015 04:38:08 +0100 Subject: [PATCH 15/28] REPLACE inline LIBB64 with CDVNewBase64Encode in iOS version [prepare for improvements to SQL blob storage] (@brodybits) from PR #170 thanks @aarononeal. NOTE: changes related to SQL blob binding are #ifdef'd out (TBD subjet to change). --- src/ios/SQLitePlugin.h | 8 +- src/ios/SQLitePlugin.m | 192 +++++++++-------------------------------- 2 files changed, 43 insertions(+), 157 deletions(-) diff --git a/src/ios/SQLitePlugin.h b/src/ios/SQLitePlugin.h index d5b739e80..d1c22da66 100755 --- a/src/ios/SQLitePlugin.h +++ b/src/ios/SQLitePlugin.h @@ -12,6 +12,7 @@ #import #import +#import #import "AppDelegate.h" @@ -56,9 +57,6 @@ typedef int WebSQLError; +(int)mapSQLiteErrorCode:(int)code; -// LIBB64 -+(id) getBlobAsBase64String:(const char*) blob_chars - withlength: (int) blob_length; -// LIBB64---END - ++(NSString*)getBlobAsBase64String:(const char*) blob_chars + withlength:(int) blob_length; @end diff --git a/src/ios/SQLitePlugin.m b/src/ios/SQLitePlugin.m index fb626ea9f..e6a5f5cf4 100755 --- a/src/ios/SQLitePlugin.m +++ b/src/ios/SQLitePlugin.m @@ -9,128 +9,6 @@ #import "SQLitePlugin.h" #include - -//LIBB64 -typedef enum -{ - step_A, step_B, step_C -} base64_encodestep; - -typedef struct -{ - base64_encodestep step; - char result; - int stepcount; -} base64_encodestate; - -static void base64_init_encodestate(base64_encodestate* state_in) -{ - state_in->step = step_A; - state_in->result = 0; - state_in->stepcount = 0; -} - -static char base64_encode_value(char value_in) -{ - static const char* encoding = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - if (value_in > 63) return '='; - return encoding[(int)value_in]; -} - -static int base64_encode_block(const char* plaintext_in, - int length_in, - char* code_out, - base64_encodestate* state_in, - int line_length) -{ - const char* plainchar = plaintext_in; - const char* const plaintextend = plaintext_in + length_in; - char* codechar = code_out; - char result; - char fragment; - - result = state_in->result; - - switch (state_in->step) - { - while (1) - { - case step_A: - if (plainchar == plaintextend) - { - state_in->result = result; - state_in->step = step_A; - return codechar - code_out; - } - fragment = *plainchar++; - result = (fragment & 0x0fc) >> 2; - *codechar++ = base64_encode_value(result); - result = (fragment & 0x003) << 4; - case step_B: - if (plainchar == plaintextend) - { - state_in->result = result; - state_in->step = step_B; - return codechar - code_out; - } - fragment = *plainchar++; - result |= (fragment & 0x0f0) >> 4; - *codechar++ = base64_encode_value(result); - result = (fragment & 0x00f) << 2; - case step_C: - if (plainchar == plaintextend) - { - state_in->result = result; - state_in->step = step_C; - return codechar - code_out; - } - fragment = *plainchar++; - result |= (fragment & 0x0c0) >> 6; - *codechar++ = base64_encode_value(result); - result = (fragment & 0x03f) >> 0; - *codechar++ = base64_encode_value(result); - - if(line_length > 0) - { - ++(state_in->stepcount); - if (state_in->stepcount == line_length/4) - { - *codechar++ = '\n'; - state_in->stepcount = 0; - } - } - } - } - /* control should not reach here */ - return codechar - code_out; -} - -static int base64_encode_blockend(char* code_out, - base64_encodestate* state_in) -{ - char* codechar = code_out; - - switch (state_in->step) - { - case step_B: - *codechar++ = base64_encode_value(state_in->result); - *codechar++ = '='; - *codechar++ = '='; - break; - case step_C: - *codechar++ = base64_encode_value(state_in->result); - *codechar++ = '='; - break; - case step_A: - break; - } - *codechar++ = '\n'; - - return codechar - code_out; -} - -//LIBB64---END - static void sqlite_regexp(sqlite3_context* context, int argc, sqlite3_value** values) { if ( argc < 2 ) { sqlite3_result_error(context, "SQL function regexp() called with missing arguments.", -1); @@ -520,10 +398,8 @@ -(CDVPluginResult*) executeSqlWithDict: (NSMutableDictionary*)options andArgs: ( #endif break; case SQLITE_BLOB: - //LIBB64 columnValue = [SQLitePlugin getBlobAsBase64String: sqlite3_column_blob(statement, i) - withlength: sqlite3_column_bytes(statement, i) ]; - //LIBB64---END + withLength: sqlite3_column_bytes(statement, i)]; break; case SQLITE_FLOAT: columnValue = [NSNumber numberWithFloat: sqlite3_column_double(statement, i)]; @@ -599,9 +475,28 @@ -(void)bindStatement:(sqlite3_stmt *)statement withArg:(NSObject *)arg atIndex:( } else { stringArg = [arg description]; // convert to text } - - NSData *data = [stringArg dataUsingEncoding:NSUTF8StringEncoding]; - sqlite3_bind_text(statement, argIndex, data.bytes, data.length, SQLITE_TRANSIENT); + +#ifdef INCLUDE_SQL_BLOB_BINDING // TBD subjet to change: + // If the string is a sqlblob URI then decode it and store the binary directly. + // + // A sqlblob URI is formatted similar to a data URI which makes it easy to convert: + // sqlblob:[][;charset=][;base64], + // + // The reason the `sqlblob` prefix is used instead of `data` is because + // applications may want to use data URI strings directly, so the + // `sqlblob` prefix disambiguates the desired behavior. + if ([stringArg hasPrefix:@"sqlblob:"]) { + // convert to data URI, decode, store as blob + stringArg = [stringArg stringByReplacingCharactersInRange:NSMakeRange(0,7) withString:@"data"]; + NSData *data = [NSData dataWithContentsOfURL: [NSURL URLWithString:stringArg]]; + sqlite3_bind_blob(statement, argIndex, data.bytes, data.length, SQLITE_TRANSIENT); + } + else +#endif + { + NSData *data = [stringArg dataUsingEncoding:NSUTF8StringEncoding]; + sqlite3_bind_text(statement, argIndex, data.bytes, data.length, SQLITE_TRANSIENT); + } } } @@ -667,32 +562,25 @@ +(int)mapSQLiteErrorCode:(int)code } } -+(id) getBlobAsBase64String:(const char*) blob_chars - withlength: (int) blob_length ++(NSString*)getBlobAsBase64String:(const char*)blob_chars + withLength:(int)blob_length { - base64_encodestate b64state; - - base64_init_encodestate(&b64state); - - //2* ensures 3 bytes -> 4 Base64 characters + null for NSString init - char* code = malloc (2*blob_length*sizeof(char)); - - int codelength; - int endlength; - - codelength = base64_encode_block(blob_chars,blob_length,code,&b64state,0); - - endlength = base64_encode_blockend(&code[codelength], &b64state); - - //Adding in a null in order to use initWithUTF8String, expecting null terminated char* string - code[codelength+endlength] = '\0'; - - NSString* result = [NSString stringWithUTF8String: code]; - - free(code); - + size_t outputLength = 0; + char* outputBuffer = CDVNewBase64Encode(blob_chars, blob_length, true, &outputLength); + + NSString* result = [[NSString alloc] initWithBytesNoCopy:outputBuffer + length:outputLength + encoding:NSASCIIStringEncoding + freeWhenDone:YES]; +#if !__has_feature(objc_arc) + [result autorelease]; +#endif + +#ifdef INCLUDE_SQL_BLOB_BINDING // TBD subjet to change: + return [@"sqlblob:;base64," stringByAppendingString:result]; +#else return result; +#endif } - @end From c29e10bca2b18032947daaa91c4bf533caf6cea8 Mon Sep 17 00:00:00 2001 From: Chris Brody Date: Sun, 1 Mar 2015 23:38:21 +0100 Subject: [PATCH 16/28] Cleanup header dependencies; cleanup whitespace; replace tabs; move conditionally-included SQL binding code (ref: PR #170) --- src/ios/SQLitePlugin.h | 11 +++-------- src/ios/SQLitePlugin.m | 39 +++++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/ios/SQLitePlugin.h b/src/ios/SQLitePlugin.h index d1c22da66..9db62b92c 100755 --- a/src/ios/SQLitePlugin.h +++ b/src/ios/SQLitePlugin.h @@ -6,15 +6,10 @@ * See http://opensource.org/licenses/alphabetical for full text. */ -#import - -#import "sqlite3.h" - #import -#import -#import -#import "AppDelegate.h" +// Used to remove dependency on sqlite3.h in this header: +struct sqlite3; enum WebSQLError { UNKNOWN_ERR = 0, @@ -53,7 +48,7 @@ typedef int WebSQLError; -(id) getDBPath:(NSString *)dbFile at:(NSString *)atkey; -+(NSDictionary *)captureSQLiteErrorFromDb:(sqlite3 *)db; ++(NSDictionary *)captureSQLiteErrorFromDb:(struct sqlite3 *)db; +(int)mapSQLiteErrorCode:(int)code; diff --git a/src/ios/SQLitePlugin.m b/src/ios/SQLitePlugin.m index e6a5f5cf4..a74ec5a83 100755 --- a/src/ios/SQLitePlugin.m +++ b/src/ios/SQLitePlugin.m @@ -7,8 +7,13 @@ */ #import "SQLitePlugin.h" + +#import "sqlite3.h" + #include +#import + static void sqlite_regexp(sqlite3_context* context, int argc, sqlite3_value** values) { if ( argc < 2 ) { sqlite3_result_error(context, "SQL function regexp() called with missing arguments.", -1); @@ -137,14 +142,14 @@ -(void)open: (CDVInvokedUrlCommand*)command if (![[NSFileManager defaultManager] fileExistsAtPath:dbname]) { NSString *createFromResource = [options objectForKey:@"createFromResource"]; if (createFromResource != NULL) - [self createFromResource:dbfilename withDbname:dbname]; + [self createFromResource:dbfilename withDbname:dbname]; } if (sqlite3_open(name, &db) != SQLITE_OK) { pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Unable to open DB"]; return; } else { - sqlite3_create_function(db, "regexp", 2, SQLITE_ANY, NULL, &sqlite_regexp, NULL, NULL); + sqlite3_create_function(db, "regexp", 2, SQLITE_ANY, NULL, &sqlite_regexp, NULL, NULL); // for SQLCipher version: // NSString *dbkey = [options objectForKey:@"key"]; @@ -188,7 +193,7 @@ -(void)createFromResource:(NSString *)dbfile withDbname:(NSString *)dbname { NSLog(@"Found prepopulated DB: %@", prepopulatedDb); NSError *error; BOOL success = [[NSFileManager defaultManager] copyItemAtPath:prepopulatedDb toPath:dbname error:&error]; - + if(success) NSLog(@"Copied prepopulated DB content to: %@", dbname); else @@ -240,7 +245,7 @@ -(void) delete: (CDVInvokedUrlCommand*)command if (dbFileName==NULL) { // Should not happen: - NSLog(@"No db name specified for delete"); + NSLog(@"No db name specified for delete"); pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"You must specify database path"]; } else { NSString *dbPath = [self getDBPath:dbFileName at:dblocation]; @@ -379,11 +384,11 @@ -(CDVPluginResult*) executeSqlWithDict: (NSMutableDictionary*)options andArgs: ( i = 0; entry = [NSMutableDictionary dictionaryWithCapacity:0]; count = sqlite3_column_count(statement); - + while (i < count) { columnValue = nil; columnName = [NSString stringWithFormat:@"%s", sqlite3_column_name(statement, i)]; - + column_type = sqlite3_column_type(statement, i); switch (column_type) { case SQLITE_INTEGER: @@ -400,6 +405,9 @@ -(CDVPluginResult*) executeSqlWithDict: (NSMutableDictionary*)options andArgs: ( case SQLITE_BLOB: columnValue = [SQLitePlugin getBlobAsBase64String: sqlite3_column_blob(statement, i) withLength: sqlite3_column_bytes(statement, i)]; +#ifdef INCLUDE_SQL_BLOB_BINDING // TBD subjet to change: + columnValue = [@"sqlblob:;base64," stringByAppendingString:columnValue]; +#endif break; case SQLITE_FLOAT: columnValue = [NSNumber numberWithFloat: sqlite3_column_double(statement, i)]; @@ -408,13 +416,12 @@ -(CDVPluginResult*) executeSqlWithDict: (NSMutableDictionary*)options andArgs: ( columnValue = [NSNull null]; break; } - + if (columnValue) { [entry setObject:columnValue forKey:columnName]; } - - i++; + i++; } [resultRows addObject:entry]; break; @@ -469,7 +476,7 @@ -(void)bindStatement:(sqlite3_stmt *)statement withArg:(NSObject *)arg atIndex:( } } else { // NSString NSString *stringArg; - + if ([arg isKindOfClass:[NSString class]]) { stringArg = (NSString *)arg; } else { @@ -523,7 +530,7 @@ -(void)dealloc #endif } -+(NSDictionary *)captureSQLiteErrorFromDb:(sqlite3 *)db ++(NSDictionary *)captureSQLiteErrorFromDb:(struct sqlite3 *)db { int code = sqlite3_errcode(db); int webSQLCode = [SQLitePlugin mapSQLiteErrorCode:code]; @@ -567,7 +574,7 @@ +(NSString*)getBlobAsBase64String:(const char*)blob_chars { size_t outputLength = 0; char* outputBuffer = CDVNewBase64Encode(blob_chars, blob_length, true, &outputLength); - + NSString* result = [[NSString alloc] initWithBytesNoCopy:outputBuffer length:outputLength encoding:NSASCIIStringEncoding @@ -575,12 +582,8 @@ +(NSString*)getBlobAsBase64String:(const char*)blob_chars #if !__has_feature(objc_arc) [result autorelease]; #endif - -#ifdef INCLUDE_SQL_BLOB_BINDING // TBD subjet to change: - return [@"sqlblob:;base64," stringByAppendingString:result]; -#else - return result; -#endif + + return result; } @end From ddbf357a74b813c5e1237e3281eca1b2e23dc7a8 Mon Sep 17 00:00:00 2001 From: Chris Brody Date: Wed, 4 Mar 2015 00:42:13 +0100 Subject: [PATCH 17/28] Remove extra semicolon from CoffeeScript --- SQLitePlugin.coffee.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index a08fb2116..0f588bbcc 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -144,7 +144,7 @@ can be used in the success handler as an alternative to the handler's db argument ### - nextTick () -> onSuccess(); + nextTick () -> onSuccess() else @openDBs[@dbname] = true cordova.exec onSuccess, error, "SQLitePlugin", "open", [ @openargs ] From 73dccacb7078adbea5e7b132a87f89ec8956b654 Mon Sep 17 00:00:00 2001 From: Chris Brody Date: Wed, 4 Mar 2015 14:43:59 +0100 Subject: [PATCH 18/28] Reproduce issue with big (integer) values in WP(8) version (ref: #195) --- test-www/www/index.html | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test-www/www/index.html b/test-www/www/index.html index 3b49289e7..84fbeadc4 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -620,6 +620,31 @@ }); }); + // XXX reproduce issue #195 in WP(8) version ONLY: + test(suiteName + "Big number [integer] value(s)", function() { + stop(); + var db = openDatabase("Big-number-test.db", "1.0", "Demo", DEFAULT_SIZE); + db.transaction(function(tx) { + tx.executeSql('DROP TABLE IF EXISTS tt'); + tx.executeSql('CREATE TABLE IF NOT EXISTS tt (test_date INTEGER)'); + }, function(err) { ok(false, err.message) }, function() { + db.transaction(function(tx) { + tx.executeSql("insert into tt (test_date) VALUES (?)", [1424174959894], function(tx, res) { + equal(res.rowsAffected, 1, "row inserted"); + tx.executeSql("select * from tt", [], function(tx, res) { + start(); + var row = res.rows.item(0); + // XXX BUG in WP(8) version ONLY: + //if (/MSIE/.test(navigator.userAgent)) // XXX BUG in WP(8) version + // ok(row.test_date < 0, "Reproducing big number bug"); + //else + strictEqual(row.test_date, 1424174959894, "Big integer number inserted properly"); + }); + }); + }); + }); + }); + // XXX reproduce issue #140: test(suiteName + "executeSql parameter as array", function() { stop(); From bea5541e810fccc35253da811a433286e920f7ae Mon Sep 17 00:00:00 2001 From: Chris Brody Date: Thu, 5 Mar 2015 12:08:59 +0100 Subject: [PATCH 19/28] Further testing with UNICODE line separator (\u2028) (ref: #147) --- test-www/www/index.html | 87 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 10 deletions(-) diff --git a/test-www/www/index.html b/test-www/www/index.html index 84fbeadc4..749fd8537 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -137,6 +137,66 @@ }); }); + test(suiteName + "UNICODE line separator string to hex", function() { + // NOTE: this test verifies that the UNICODE line separator (\u2028) + // is seen by the sqlite implementation OK: + var db = openDatabase("UNICODE-line-separator-string-1.db", "1.0", "Demo", DEFAULT_SIZE); + + ok(!!db, "db object"); + + stop(2); + + db.transaction(function(tx) { + + start(1); + ok(!!tx, "tx object"); + + var text = 'Abcd\u20281234'; + tx.executeSql("select hex(?) as hexvalue", [text], function (tx, res) { + start(1); + var hexvalue = res.rows.item(0).hexvalue; + + // varies between Chrome-like (UTF-8) + // and Safari-like (UTF-16) + var expected = [ + '41626364E280A831323334', + '410062006300640028203100320033003400' + ]; + + ok(expected.indexOf(hexvalue) !== -1, 'hex matches: ' + + JSON.stringify(hexvalue) + ' should be in ' + + JSON.stringify(expected)); + }); + }); + }); + + // XXX BUG #147 iOS version of plugin BROKEN (no callback received): + if (isWebSql || /Android/.test(navigator.userAgent)) test(suiteName + + ' handles unicode line separator correctly', function () { + + // NOTE: since the above test shows the UNICODE line separator (\u2028) + // is seen by the sqlite implementation OK, it is now concluded that + // the failure is caused by the Objective-C JSON result encoding. + var db = openDatabase("UNICODE-line-separator-string-2.db", "1.0", "Demo", DEFAULT_SIZE); + + ok(!!db, "db object"); + + stop(2); + + db.transaction(function(tx) { + + start(1); + ok(!!tx, "tx object"); + + var text = 'Abcd\u20281234'; + tx.executeSql("select lower(?) as lowertext", [text], function (tx, res) { + start(1); + ok(!!res, "res object"); + equal(res.rows.item(0).lowertext, "abcd\u20281234", "lower case string test with UNICODE line separator"); + }); + }); + }); + test(suiteName + "db transaction test", function() { var db = openDatabase("db-trx-test.db", "1.0", "Demo", DEFAULT_SIZE); @@ -1016,19 +1076,22 @@ // For now we expect this test to fail there, but when it is fixed // we would like to know, so the test is coded to fail if it starts // working there. - if(!isWebSql) { - ok(expected.indexOf(name) !== -1, 'field value: ' + - JSON.stringify(name) + ' should be in ' + - JSON.stringify(expected)); - - equal(name.length, 4, 'length of field === 4'); - } else { + if(isWebSql) { ok(expected.indexOf(name) === -1, 'field value: ' + JSON.stringify(name) + ' should not be in this until a bug is fixed ' + JSON.stringify(expected)); equal(name.length, 0, 'length of field === 0'); + start(); + return; } + + // correct result: + ok(expected.indexOf(name) !== -1, 'field value: ' + + JSON.stringify(name) + ' should be in ' + + JSON.stringify(expected)); + + equal(name.length, 4, 'length of field === 4'); start(); }) }); @@ -1040,13 +1103,17 @@ }); }); - // XXX #147 iOS version of plugin BROKEN: - if (isWebSql || /Android/.test(navigator.userAgent)) test(suiteName + ' handles unicode line separator correctly', function () { - stop(2); + // XXX Brody NOTE: same issue is now reproduced in a string test. + // TODO: combine with other test + // BUG #147 iOS version of plugin BROKEN: + if (isWebSql || /Android/.test(navigator.userAgent)) test(suiteName + + ' handles unicode line separator correctly', function () { var dbName = "Unicode-line-separator.db"; var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); + stop(2); + db.transaction(function (tx) { tx.executeSql('DROP TABLE IF EXISTS test', [], function () { tx.executeSql('CREATE TABLE test (name, id)', [], function() { From d9841960f4eee3a42ccbc5d7de3dc4e073a21395 Mon Sep 17 00:00:00 2001 From: Chris Brody Date: Fri, 6 Mar 2015 11:53:59 +0100 Subject: [PATCH 20/28] Reproduce issue with double-precision REAL number (ref: #199) --- test-www/www/index.html | 49 ++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/test-www/www/index.html b/test-www/www/index.html index 749fd8537..d03b226b0 100755 --- a/test-www/www/index.html +++ b/test-www/www/index.html @@ -9,8 +9,11 @@ - + + + +