From 876defc6e50ecc3dac300b181b5b2940f3e0705e Mon Sep 17 00:00:00 2001 From: "Christopher J. Brody" Date: Tue, 18 Apr 2017 00:05:08 +0200 Subject: [PATCH 01/11] cordova-sqlite-legacy-express-core 1.0.0-pre1 fixes - Update Android/iOS src copyright year for 2017 in this version branch - Drop engine constraints in package.json & plugin.xml (in this version branch) - Support macOS platform with builtin libsqlite3.dylib framework in this version branch - Regenerate plugin JavaScript with CoffeeScript 1.12.4 - Test fixes - doc fixes --- AUTHORS.md | 3 +- CHANGES.md | 5 + LICENSE.md | 9 +- README.md | 254 ++++++++++++------ package.json | 14 +- plugin.xml | 26 +- spec/config.xml | 6 +- spec/www/spec/basic-misc-test.js | 5 +- spec/www/spec/browser-check-startup.js | 15 +- spec/www/spec/db-tx-sql-results.js | 5 +- spec/www/spec/db-tx-string-test.js | 32 ++- spec/www/spec/db-tx-value-bindings-test.js | 29 +- spec/www/spec/misc-tx-legacy.js | 43 ++- spec/www/spec/tx-semantics-test.js | 7 +- .../io/sqlc/SQLiteAndroidDatabase.java | 2 +- src/android/io/sqlc/SQLitePlugin.java | 2 +- src/ios/SQLitePlugin.h | 2 +- src/ios/SQLitePlugin.m | 2 +- www/SQLitePlugin.js | 4 +- 19 files changed, 279 insertions(+), 186 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 12cdf16f6..76e8a28f7 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -16,7 +16,7 @@ - Fixes to support old Android versions by @nolanlawson - Thanks to Mark Oppenheim for fixes to open/close callbacks and repeated open/close/delete operations -## iOS version +## iOS/macOS version - Original authors: @davibe (Davide Bertola ) and @joenoon (Joe Noon ) - Cordova 2.7+ port with background processing by @j3k0 (Jean-Christophe Hoelt ) @@ -26,3 +26,4 @@ - ~~SQLiteProxy.js by @vldmrrr (Vladimir Avdonin) and @brodybits (Chris Brody)~~ - ~~Using SQLite3-WinRT C++ classes and SQLite3JS (Javascript part) by @doo (doo GmbH)~~ +- ~~Thanks to @AllJoyn-Cordova for idea how to integrate Windows 8.1/Windows Phone 8.1 Visual C++ projects in plugin.xml~~ diff --git a/CHANGES.md b/CHANGES.md index 93f869398..7b096b9ee 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # Changes +###### cordova-sqlite-legacy-express-core 1.0.0-pre1 + +- Drop engine constraints in package.json & plugin.xml (in this version branch) +- Support macOS platform with builtin libsqlite3.dylib framework in this version branch + ## 1.2.2 - Self-test function to verify ability to open/populate/read/delete a test database diff --git a/LICENSE.md b/LICENSE.md index ff1fb366f..e621188a3 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -8,11 +8,13 @@ MIT or Apache 2.0 MIT or Apache 2.0 -## iOS version +## iOS/macOS version MIT only -## Windows (8.1) version +based on Phonegap-SQLitePlugin by @davibe (Davide Bertola ) and @joenoon (Joe Noon ) + +## REMOVED from this version branch: Windows (8.1/...) version MIT or Apache 2.0 @@ -22,3 +24,6 @@ by @doo (doo GmbH) MIT License +## SQLite3 + +Public domain diff --git a/README.md b/README.md index 78da5a44e..8967c103c 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# Cordova/PhoneGap sqlite storage adapter (core version) +# Cordova/PhoneGap sqlite storage adapter (legacy express core version branch) -Native interface to sqlite in a Cordova/PhoneGap plugin for Android and iOS, with API similar to HTML5/[Web SQL API](http://www.w3.org/TR/webdatabase/). +Native interface to sqlite in a Cordova/PhoneGap plugin for Android/iOS/macOS, with API similar to HTML5/[Web SQL API](http://www.w3.org/TR/webdatabase/). -License for Android version: MIT or Apache 2.0 +License for Android platform version: MIT or Apache 2.0 -License for iOS version: MIT only +License for iOS/macOS platform version: MIT only -This version contains the source code for the Android and iOS versions. This version does not contain any libraries or source code from www.sqlite.org. This version uses the built-in sqlite libraries on Android and iOS. +## About this version branch -|Android Circle-CI (**full** suite)|iOS Travis-CI (*very* limited suite)| -|-----------------------|----------------------| -|[![Circle CI](https://circleci.com/gh/litehelpers/Cordova-sqlite-storage.svg?style=svg)](https://circleci.com/gh/litehelpers/Cordova-sqlite-storage)|[![Build Status](https://travis-ci.org/litehelpers/Cordova-sqlite-storage.svg?branch=core-master)](https://travis-ci.org/litehelpers/Cordova-sqlite-storage)| +This version branch contains the source code for the Android/iOS/macOS platforms (legacy express core version branch). This version does not contain any libraries or source code from www.sqlite.org. This version branch uses the built-in sqlite libraries on Android, iOS, and macOS. + + ## BREAKING CHANGE: Database location parameter is now mandatory @@ -43,66 +43,67 @@ Use the `location` or `iosDatabaseLocation` option in `sqlitePlugin.openDatabase **NOTE:** Changing `BackupWebStorage` in `config.xml` has no effect on a database created by this plugin. `BackupWebStorage` applies only to local storage and/or Web SQL storage created in the WebView (*not* using this plugin). For reference: [phonegap/build#338 (comment)](https://github.com/phonegap/build/issues/338#issuecomment-113328140) -## Available for hire - -The primary author and maintainer [@brodybits (Christopher J. Brody aka Chris Brody)](https://github.com/brodybits) is available for contracting assignments. Part-time assignments would really help keep this project alive. ([@brodybits](https://github.com/brodybits) dedicates a significant amount of working time and has turned down multiple full-time work opportunities to maintain and improve this project.) - -[@brodybits](https://github.com/brodybits) can be contacted at: -- -- -- LinkedIn: - -Some other projects by [@brodybits](https://github.com/brodybits): -- [liteglue / Android-sqlite-connector](https://github.com/liteglue/Android-sqlite-connector) - Lightweight SQLiteConnection library with abstraction layer over [liteglue / Android-sqlite-native-driver](https://github.com/liteglue/Android-sqlite-native-driver) -- [brodybits / node-uvhttp](https://github.com/brodybits/node-uvhttp) - HTTP server library that serves static content from native code - *experimental and extremely limited* -- [brodybits / java-node](https://github.com/brodybits/java-node) - two-way binding interface between Java and Node.js (Javascript) - *experimental and extremely limited* - ## Status -- A recent version of the Cordova CLI (such as `6.1.1`) is recommended. Cordova versions older than `6.0.0` are not supported by this project. Use of other systems such as PhoneGap CLI, PhoneGap Build, or plugman is not tested and no longer supported. +- A recent version of the Cordova CLI (such as `6.5.0`) is recommended. Cordova versions older than `6.0.0` are missing the `cordova-ios@4.0.0` security fixes. In addition it is *required* to use `cordova prepare` in case of cordova-ios older than `4.3.0` (Cordova CLI `6.4.0`). - iOS database location is now mandatory, as documented below. -- Android version in this version branch is now using the built-in Android SQLite database classes. Integration with the lightweight [Android-sqlite-connector](https://github.com/liteglue/Android-sqlite-connector) is available in [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) and [litehelpers / Cordova-sqlite-legacy](https://github.com/litehelpers/Cordova-sqlite-legacy). -- Windows 8.1/Windows Phone 8.1/Windows 10 version using the performant [doo / SQLite3-WinRT](https://github.com/doo/SQLite3-WinRT) component is available in [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) (with pre-populated database support) and [litehelpers / Cordova-sqlite-legacy](https://github.com/litehelpers/Cordova-sqlite-legacy). -- WP8 support (along with Windows 8.1/Windows Phone 8.1/Windows 10 support) is available in [litehelpers / Cordova-sqlite-legacy](https://github.com/litehelpers/Cordova-sqlite-legacy). +- Android platform version in this version branch is now using the built-in Android SQLite database classes. Integration with the lightweight [Android-sqlite-connector](https://github.com/liteglue/Android-sqlite-connector) is available in the default [litehelpers / Cordova-sqlite-storage](https://github.com/litehelpers/Cordova-sqlite-storage) version branch as well as other versions such as [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) and [litehelpers / Cordova-sqlite-legacy](https://github.com/litehelpers/Cordova-sqlite-legacy). +- iOS/macOS platform version in this version branch uses builtin `libsqlite3.dylib` framework library. Other versions such as the default [litehelpers / Cordova-sqlite-storage](https://github.com/litehelpers/Cordova-sqlite-storage) version branch, [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext), and [litehelpers / Cordova-sqlite-legacy](https://github.com/litehelpers/Cordova-sqlite-legacy) include a build of a recent sqlite3 amalgamation. +- Windows 10 UWP version using the performant [doo / SQLite3-WinRT](https://github.com/doo/SQLite3-WinRT) component is available in the default [litehelpers / Cordova-sqlite-storage](https://github.com/litehelpers/Cordova-sqlite-storage) version branch as well as other versions such as [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext). +- Support for WP8 along with Windows 8.1/Windows Phone 8.1/Windows 10 using Visual Studio 2015 is available in [litehelpers / Cordova-sqlite-legacy](https://github.com/litehelpers/Cordova-sqlite-legacy). - The following features are available in [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext): - - REGEXP support (Android/iOS) - - Pre-populated database (all platforms Android/iOS/Windows) + - REGEXP (Android/iOS/macOS) + - SELECT BLOB data in Base64 format (all platforms Android/iOS/macOS/Windows) + - Pre-populated database (Android/iOS/macOS/Windows) - Amazon Fire-OS is dropped due to lack of support by Cordova. Android version should be used to deploy to Fire-OS 5.0(+) devices. For reference: [cordova/cordova-discuss#32 (comment)](https://github.com/cordova/cordova-discuss/issues/32#issuecomment-167021676) -- FTS3 and FTS4 are tested working OK in this version branch (for all target platforms in this version branch Android/iOS) -- R-Tree is tested and supported for iOS *only* in this version branch +- FTS3 and FTS4 are tested working OK in this version branch (for all target platforms in this version branch Android/iOS/macOS) +- R-Tree is *not* tested or supported for Android in this version branch. - Android is supported back to SDK 10 (a.k.a. Gingerbread, Android 2.3.3); support for older versions is available upon request. -- iOS versions supported: 7.x/8.x/9.x -- In case of memory issues please use smaller transactions or use the version (with a different licensing scheme) at: [litehelpers / Cordova-sqlite-enterprise-free](https://github.com/litehelpers/Cordova-sqlite-enterprise-free) +- iOS versions supported: 8.x/9.x/10.x +- In case of memory issues please use smaller transactions or use the version (with GPL or commercial license options) at: [litehelpers / Cordova-sqlite-evcore-extbuild-free](https://github.com/litehelpers/Cordova-sqlite-evcore-extbuild-free) + + ## Announcements +- [brodybits / cordova-sqlite-test-app](https://github.com/brodybits/cordova-sqlite-test-app) project is a CC0 (public domain) starting point (NOTE that this plugin must be added) and may also be used to reproduce issues with this plugin. +- The Lawnchair adapter is now supported in [litehelpers / cordova-sqlite-lawnchair-adapter](https://github.com/litehelpers/cordova-sqlite-lawnchair-adapter). +- [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) now supports SELECT BLOB data in Base64 format on all platforms in addition to REGEXP (Android/iOS/macOS) and pre-populated database (all platforms). +- [brodybits / sql-promise-helper](https://github.com/brodybits/sql-promise-helper) provides a Promise-based API wrapper. +- [nolanlawson / pouchdb-adapter-cordova-sqlite](https://github.com/nolanlawson/pouchdb-adapter-cordova-sqlite) supports this plugin along with other implementations such as [nolanlawson / sqlite-plugin-2](https://github.com/nolanlawson/sqlite-plugin-2) and [Microsoft / cordova-plugin-websql](https://github.com/Microsoft/cordova-plugin-websql). +- macOS ("osx" platform) is now supported +- New [litehelpers / Cordova-sqlite-evcore-extbuild-free](https://github.com/litehelpers/Cordova-sqlite-evcore-extbuild-free) version with Android JSON and SQL statement handling implemented in C, as well as support for PhoneGap Build, Intel XDK, etc., available with GPL or commercial license options. Handles large SQL batches in less than half the time as this version. Also supports arbitrary database location on Android. +- Published [brodybits / Cordova-quick-start-checklist](https://github.com/brodybits/Cordova-quick-start-checklist) and [brodybits / Avoiding-some-Cordova-pitfalls](https://github.com/brodybits/Avoiding-some-Cordova-pitfalls). - Self-test functions to verify proper installation and operation of this plugin -- Windows 8.1/Windows Phone 8.1/Windows 10 version is available in [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) (with pre-populated database support) and [litehelpers / Cordova-sqlite-legacy](https://github.com/litehelpers/Cordova-sqlite-legacy) (with WP8 support). - More explicit `openDatabase` and `deleteDatabase` `iosDatabaseLocation` option -- Added simple sql batch query function -- All iOS operations are now using background processing (reported to resolve intermittent problems with cordova-ios@4.0.1) +- Added simple sql batch function - Published [brodybits / Cordova-quick-start-checklist](https://github.com/brodybits/Cordova-quick-start-checklist) and [brodybits / Cordova-troubleshooting-guide](https://github.com/brodybits/Cordova-troubleshooting-guide) -- A version with support for web workers is available (with a different licensing scheme) at: [litehelpers / cordova-sqlite-workers-evfree](https://github.com/litehelpers/cordova-sqlite-workers-evfree) -- ~~PhoneGap Build is now supported through the npm package: http://phonegap.com/blog/2015/05/26/npm-plugins-available/~~ (no longer supported due to reported issues) - [MetaMemoryT / websql-promise](https://github.com/MetaMemoryT/websql-promise) now provides a Promises-based interface to both Web SQL and this plugin -- [SQLCipher](https://www.zetetic.net/sqlcipher/) for Android/iOS/Windows is supported by [litehelpers / Cordova-sqlcipher-adapter](https://github.com/litehelpers/Cordova-sqlcipher-adapter) +- [SQLCipher](https://www.zetetic.net/sqlcipher/) for Android/iOS/macOS/Windows is supported by [litehelpers / Cordova-sqlcipher-adapter](https://github.com/litehelpers/Cordova-sqlcipher-adapter) + + ## Highlights - Drop-in replacement for HTML5/[Web SQL API](http://www.w3.org/TR/webdatabase/): the only change should be to replace the static `window.openDatabase()` factory call with `window.sqlitePlugin.openDatabase()`, with parameters as documented below. - Failure-safe nested transactions with batch processing optimizations (according to HTML5/[Web SQL API](http://www.w3.org/TR/webdatabase/)) +- API (based on HTML5/[Web SQL API](http://www.w3.org/TR/webdatabase/)) is designed to be as flexible as possible but does not allow the application to leave any transactions hanging open. - As described in [this posting](http://brodyspark.blogspot.com/2012/12/cordovaphonegap-sqlite-plugins-offer.html): - - Keeps sqlite database in a user data location that is known; can be reconfigured (iOS version); and synchronized to iCloud by default (iOS version; can be disabled as described below). + - Keeps sqlite database in a user data location that is known; can be reconfigured (iOS/macOS platform version); and may be synchronized to iCloud (iOS platform version). - No 5MB maximum, more information at: http://www.sqlite.org/limits.html - This project is self-contained: no dependencies on other plugins such as cordova-plugin-file -- Windows 8.1/Windows Phone 8.1/Windows 10 version available in [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) and [litehelpers / Cordova-sqlite-legacy](https://github.com/litehelpers/Cordova-sqlite-legacy) uses the performant C++ [doo / SQLite3-WinRT](https://github.com/doo/SQLite3-WinRT) component. -- [SQLCipher](https://www.zetetic.net/sqlcipher/) support for Android/iOS/Windows is available at: [litehelpers / Cordova-sqlcipher-adapter](https://github.com/litehelpers/Cordova-sqlcipher-adapter) +- Windows 10 UWP platform version available in [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) and Windows 8.1/Windows Phone 8.1/Windows 10 platform version available in [litehelpers / Cordova-sqlite-legacy](https://github.com/litehelpers/Cordova-sqlite-legacy) use the performant C++ [doo / SQLite3-WinRT](https://github.com/doo/SQLite3-WinRT) component. +- [SQLCipher](https://www.zetetic.net/sqlcipher/) support for Android/iOS/macOS/Windows is available in: [litehelpers / Cordova-sqlcipher-adapter](https://github.com/litehelpers/Cordova-sqlcipher-adapter) - Intellectual property: - All source code is tracked to the original author in git - Major authors are tracked in AUTHORS.md - - Licensing of each component is tracked in LICENSE.md + - License of each component is tracked in LICENSE.md - History of this project is also described in HISTORY.md +**TIP:** It is possible to migrate from Cordova to a pure native solution and continue using the data stored by this plugin. + + + ## Some apps using this plugin - [Trailforks Mountain Bike Trail Map App](http://www.trailforks.com/apps/map/) with a couple of nice videos at: @@ -111,43 +112,62 @@ Some other projects by [@brodybits](https://github.com/brodybits): - [Larkwire](http://www.larkwire.com/) (iOS version): Learn bird songs the fun way - [Tangorin](https://play.google.com/store/apps/details?id=com.tangorin.app) (Android) Japanese Dictionary at [tangorin.com](http://tangorin.com/) + + ## Known issues -- iOS version does not support certain rapidly repeated open-and-close or open-and-delete test scenarios due to how the implementation handles background processing +- iOS/macOS platform version does not support certain rapidly repeated open-and-close or open-and-delete test scenarios due to how the implementation handles background processing - As described below, auto-vacuum is NOT enabled by default. +- It is possible to request a SQL statement list such as "SELECT 1; SELECT 2" within a single SQL statement string, however the plugin will only execute the first statement and silently ignore the others ref: [litehelpers/Cordova-sqlite-storage#551](https://github.com/litehelpers/Cordova-sqlite-storage/issues/551) - INSERT statement that affects multiple rows (due to SELECT cause or using TRIGGER(s), for example) does not report proper rowsAffected on Android -- Memory issue observed when adding a large number of records due to the JSON implementation which is improved in [litehelpers / Cordova-sqlite-enterprise-free](https://github.com/litehelpers/Cordova-sqlite-enterprise-free) (available with a different licensing scheme) +- Memory issue observed when adding a large number of records due to the JSON implementation which is improved in [litehelpers / Cordova-sqlite-evcore-extbuild-free](https://github.com/litehelpers/Cordova-sqlite-evcore-extbuild-free) (available with GPL or commercial license options) +- Infinity (positive or negative) values are not supported on Android/iOS/macOS due to issues described above including a possible crash on iOS/macOS ref: [litehelpers/Cordova-sqlite-storage#405](https://github.com/litehelpers/Cordova-sqlite-storage/issues/405) - A stability issue was reported on the iOS version when in use together with [SockJS](http://sockjs.org/) client such as [pusher-js](https://github.com/pusher/pusher-js) at the same time (see [litehelpers/Cordova-sqlite-storage#196](https://github.com/litehelpers/Cordova-sqlite-storage/issues/196)). The workaround is to call sqlite functions and [SockJS](http://sockjs.org/) client functions in separate ticks (using setTimeout with 0 timeout). - If a sql statement fails for which there is no error handler or the error handler does not return `false` to signal transaction recovery, the plugin fires the remaining sql callbacks before aborting the transaction. -- In case of an error, the error `code` member is bogus on Android (fixed for Android in [litehelpers / Cordova-sqlite-enterprise-free](https://github.com/litehelpers/Cordova-sqlite-enterprise-free)). -- Possible crash on Android when using Unicode emoji characters due to [Android bug 81341](https://code.google.com/p/android/issues/detail?id=81341), which _should_ be fixed in Android 6.x +- Possible crash on Android when using Unicode emoji and other 4-octet UTF-8 characters due to [Android bug 81341](https://code.google.com/p/android/issues/detail?id=81341), which *should* be fixed in Android 6.x - Close/delete database bugs described below. -- When a database is opened and deleted without closing, the iOS version is known to leak resources. -- It is NOT possible to open multiple databases with the same name but in different locations (iOS version). +- When a database is opened and deleted without closing, the iOS/macOS platform version is known to leak resources. +- It is NOT possible to open multiple databases with the same name but in different locations (iOS/macOS platform version). - Incorrect or missing insertId/rowsAffected in results for INSERT/UPDATE/DELETE SQL statements with extra semicolon(s) in the beginning for Android (android.database implementation) - readTransaction does *not* reject modification SQL statements with extra semicolon(s) in the beginning - Problems reported with PhoneGap Build in the past: - PhoneGap Build Hydration. - Apparently FIXED: ~~PhoneGap Build may fail to build the iOS version unless the name of the app starts with an uppercase and contains no spaces (see [litehelpers/Cordova-sqlite-storage#243](https://github.com/litehelpers/Cordova-sqlite-storage/issues/243); [Wizcorp/phonegap-facebook-plugin#830](https://github.com/Wizcorp/phonegap-facebook-plugin/issues/830); [phonegap/build#431](https://github.com/phonegap/build/issues/431)).~~ +Issues fixed in some newer version branches: +- In case of an error, the error `code` member is bogus on Android +- iOS platform version generates extra logging in release version +- iOS platform version may crash if deleteDatabase is called with an object in place of the database name +- readTransaction does not reject ALTER, REINDEX, and REPLACE operations +- readTransaction does not reject modification statements with extra semicolon(s) in the beginning +- extra executeSql callbacks triggered in a transaction after a failure that was not recovered by an error callback that returns false +- does not signal an error in case of excess parameter argument values given on iOS/macOS + + + ## Other limitations - ~~The db version, display name, and size parameter values are not supported and will be ignored.~~ (No longer supported by the API) - Absolute and relative subdirectory path(s) are not tested or supported. - This plugin will not work before the callback for the 'deviceready' event has been fired, as described in **Usage**. (This is consistent with the other Cordova plugins.) -- This version will not work within a web worker (not properly supported by the Cordova framework). Use within a web worker is supported for Android and iOS in: [litehelpers / cordova-sqlite-workers-evfree](https://github.com/litehelpers/cordova-sqlite-workers-evfree) (available with a different licensing scheme) +- Extremely large records are not supported by this plugin version. TBD: specify maximum record; FUTURE TBD: to be fixed in [litehelpers / Cordova-sqlite-evcore-extbuild-free](https://github.com/litehelpers/Cordova-sqlite-evcore-extbuild-free) (available with GPL or commercial license options) +- This plugin version will not work within a web worker (not properly supported by the Cordova framework). Use within a web worker is supported for Android/iOS in: [litehelpers / Cordova-sqlite-evplus-legacy-workers-free](https://github.com/litehelpers/Cordova-sqlite-evplus-legacy-workers-free) (available with GPL or premium commercial license options) - In-memory database `db=window.sqlitePlugin.openDatabase({name: ':memory:', ...})` is currently not supported. - The Android version cannot work with more than 100 open db files (due to the threading model used). -- UNICODE `\u2028` (line separator) and `\u2029` (paragraph separator) characters are currently not supported and known to be broken in iOS version due to [Cordova bug CB-9435](https://issues.apache.org/jira/browse/CB-9435). There *may* be a similar issue with certain other UNICODE characters in the iOS version (needs further investigation). This is fixed in: [litehelpers / Cordova-sqlite-enterprise-free](https://github.com/litehelpers/Cordova-sqlite-enterprise-free) (available with a different licensing scheme) -- BLOB type is currently not supported. +- UNICODE `\u2028` (line separator) and `\u2029` (paragraph separator) characters are currently not supported and known to be broken in iOS, macOS, and Android version due to JSON issues reported in [Cordova bug CB-9435](https://issues.apache.org/jira/browse/CB-9435) and [cordova/cordova-discuss#57](https://github.com/cordova/cordova-discuss/issues/57). There *may* be a similar issue with certain other UNICODE characters in the iOS/macOS version (needs further investigation). This is fixed for iOS in: [litehelpers / Cordova-sqlite-evplus-legacy-free](https://github.com/litehelpers/Cordova-sqlite-evplus-legacy-free) and [litehelpers / Cordova-sqlite-evplus-legacy-attach-detach-free](https://github.com/litehelpers/Cordova-sqlite-evplus-legacy-attach-detach-free) (available with GPL or special commercial license options) as well as [litehelpers / Cordova-sqlite-evplus-legacy-workers-free](https://github.com/litehelpers/Cordova-sqlite-evplus-legacy-workers-free) (available with GPL or premium commercial license options) +- The BLOB data type is not fully supported by this version branch. SELECT BLOB in Base64 format is supported by [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) (permissive license terms) and [litehelpers / Cordova-sqlite-evcore-extbuild-free](https://github.com/litehelpers/Cordova-sqlite-evcore-extbuild-free) (GPL or commercial license options). +- Truncation in case of UNICODE `\u0000` (same as `\0`) character on Android - Case-insensitive matching and other string manipulations on Unicode characters, which is provided by optional ICU integration in the sqlite source and working with recent versions of Android, is not supported for any target platforms. -- iOS version uses a thread pool but with only one thread working at a time due to "synchronized" database access -- Large query result can be slow, also due to JSON implementation -- ATTACH to another database file is not supported by this version. Attach/detach is supported (along with the memory and iOS UNICODE `\u2028` line separator / `\u2029` paragraph separator fixes) in: [litehelpers / Cordova-sqlite-evfree-ext](https://github.com/litehelpers/Cordova-sqlite-evfree-ext) (available with a different licensing scheme) -- UPDATE/DELETE with LIMIT or ORDER BY is not supported and known to be missing in older Android/iOS versions. +- iOS/macOS platform version uses a thread pool but with only one thread working at a time due to "synchronized" database access +- Some large query results may be slow, also due to the JSON implementation. +- ATTACH to another database file is not supported by this version. Attach/detach is supported (along with the memory and iOS UNICODE `\u2028` line separator / `\u2029` paragraph separator fixes) in: [litehelpers / Cordova-sqlite-evplus-legacy-attach-detach-free](https://github.com/litehelpers/Cordova-sqlite-evplus-legacy-attach-detach-free) (available with GPL or special commercial license options) +- UPDATE/DELETE with LIMIT or ORDER BY is not supported. +- WITH clause is not supported by some older Android/iOS versions. - User-defined savepoints are not supported and not expected to be compatible with the transaction locking mechanism used by this plugin. In addition, the use of BEGIN/COMMIT/ROLLBACK statements is not supported. - Problems have been reported when using this plugin with Crosswalk (for Android). It may help to install Crosswalk as a plugin instead of using Crosswalk to create the project. -- Does not work with [axemclion / react-native-cordova-plugin](https://github.com/axemclion/react-native-cordova-plugin) since the `window.sqlitePlugin` object *not* proprly exported (ES5 feature). It is recommended to use [andpor / react-native-sqlite-storage](https://github.com/andpor/react-native-sqlite-storage) for SQLite database access with React Native Android/iOS instead. +- Does not work with [axemclion / react-native-cordova-plugin](https://github.com/axemclion/react-native-cordova-plugin) since the `window.sqlitePlugin` object is *not* properly exported (ES5 feature). It is recommended to use [andpor / react-native-sqlite-storage](https://github.com/andpor/react-native-sqlite-storage) for SQLite database access with React Native Android/iOS instead. + + ## Further testing needed @@ -200,31 +220,37 @@ Some other projects by [@brodybits](https://github.com/brodybits): ### Other versions -- [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) - version with [Android-sqlite-connector](https://github.com/liteglue/Android-sqlite-connector), Windows 8.1/Windows Phone 8.1/Windows 10 (using [doo / SQLite3-WinRT](https://github.com/doo/SQLite3-WinRT)), REGEXP support for Android/iOS, and pre-populated database support for Android/iOS/Windows -- [litehelpers / Cordova-sqlite-legacy](https://github.com/litehelpers/Cordova-sqlite-legacy) - maintenance of WP8 version along with the other supported platforms Android/iOS/Windows 8.1/Windows Phone 8.1/Windows 10 -- [litehelpers / Cordova-sqlcipher-adapter](https://github.com/litehelpers/Cordova-sqlcipher-adapter) - supports [SQLCipher](https://www.zetetic.net/sqlcipher/) for Android, iOS, and Windows 8.1(+)/Windows Phone 8.1(+) -- [litehelpers / Cordova-sqlite-enterprise-free](https://github.com/litehelpers/Cordova-sqlite-enterprise-free) - internal memory improvements to support larger transactions (Android/iOS) and fix to support all Unicode characters (iOS) - with a different licensing scheme -- [litehelpers / Cordova-sqlite-evfree-ext](https://github.com/litehelpers/Cordova-sqlite-evfree-ext) - version with support for ATTACH, includes internal memory improvements to support larger transactions (Android/iOS) and fix to support all Unicode characters (with a different licensing scheme) -- [litehelpers / cordova-sqlite-workers-evfree](https://github.com/litehelpers/cordova-sqlite-workers-evfree) - version with support for web workers, includes internal memory improvements to support larger transactions (Android/iOS) and fix to support all Unicode characters (iOS) (with a different licensing scheme) +- [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) - version with REGEXP (Android/iOS/macOS), SELECT BLOB in Base64 format (all platforms Android/iOS/macOS/Windows), and pre-populated databases (all platforms Android/iOS/macOS/Windows) +- [litehelpers / Cordova-sqlite-legacy-build-support](https://github.com/litehelpers/Cordova-sqlite-legacy-build-support) - maintenance of WP8 version along with Windows 8.1/Windows Phone 8.1 and the other supported platforms Android/iOS/macOS/Windows 10; limited support for PhoneGap CLI/PhoneGap Build/plugman/Intel XDK; limited testing; limited updates +- [litehelpers / Cordova-sqlcipher-adapter](https://github.com/litehelpers/Cordova-sqlcipher-adapter) - supports [SQLCipher](https://www.zetetic.net/sqlcipher/) for Android/iOS/macOS/Windows +- [litehelpers / Cordova-sqlite-evcore-extbuild-free](https://github.com/litehelpers/Cordova-sqlite-evcore-extbuild-free) - Enhancements for Android: JSON and SQL statement handling implemented in C, supports larger transactions and handles large SQL batches in less than half the time as this version. Supports arbitrary database location on Android. Support for build environments such as PhoneGap Build and Intel XDK. Available with GPL or commercial license options. Also includes REGEXP (Android/iOS/macOS) and SELECT BLOB in Base64 format (all platforms Android/iOS/macOS/Windows). +- [litehelpers / Cordova-sqlite-evplus-legacy-workers-free](https://github.com/litehelpers/Cordova-sqlite-evplus-legacy-workers-free) - version with support for web workers, includes internal memory improvements to support larger transactions (Android/iOS) and fix to support all Unicode characters (iOS) (with GPL or premium commercial license options) +- [litehelpers / Cordova-sqlite-evplus-legacy-free](https://github.com/litehelpers/Cordova-sqlite-evplus-legacy-free) - internal memory improvements to support larger transactions (Android/iOS) and fix to support all Unicode characters (iOS) - with GPL or special commercial license options +- [litehelpers / Cordova-sqlite-evplus-legacy-attach-detach-free](https://github.com/litehelpers/Cordova-sqlite-evplus-legacy-attach-detach-free) - version with support for ATTACH, includes internal memory improvements to support larger transactions (Android/iOS) and fix to support all Unicode characters (with GPL or special commercial license options) - Adaptation for React Native Android and iOS: [andpor / react-native-sqlite-storage](https://github.com/andpor/react-native-sqlite-storage) - Original version for iOS (with a slightly different transaction API): [davibe / Phonegap-SQLitePlugin](https://github.com/davibe/Phonegap-SQLitePlugin) + + ### Other SQLite adapter projects - [object-layer / AnySQL](https://github.com/object-layer/anysql) - Unified SQL API over multiple database engines - [samikrc / CordovaSQLite](https://github.com/samikrc/CordovaSQLite) - Simpler sqlite plugin with a simpler API and browser platform -- [nolanlawson / sqlite-plugin-2](https://github.com/nolanlawson/sqlite-plugin-2) - Simpler fork/partial rewrite (TBD not sure how much of the code, such as iOS code by [@davibe](https://github.com/davibe), is copied/reused) +- [nolanlawson / sqlite-plugin-2](https://github.com/nolanlawson/sqlite-plugin-2) - Simpler fork/rewrite - [nolanlawson / node-websql](https://github.com/nolanlawson/node-websql) - Web SQL API implementation for Node.js - [an-rahulpandey / cordova-plugin-dbcopy](https://github.com/an-rahulpandey/cordova-plugin-dbcopy) - Alternative way to copy pre-populated database - [EionRobb / phonegap-win8-sqlite](https://github.com/EionRobb/phonegap-win8-sqlite) - WebSQL add-on for Win8/Metro apps (perhaps with a different API), using an old version of the C++ library from [SQLite3-WinRT Component](https://github.com/doo/SQLite3-WinRT) (as referenced by [01org / cordova-win8](https://github.com/01org/cordova-win8)) - [SQLite3-WinRT Component](https://github.com/doo/SQLite3-WinRT) - C++ component that provides a nice SQLite API with promises for WinJS - [01org / cordova-win8](https://github.com/01org/cordova-win8) - old, unofficial version of Cordova API support for Windows 8 Metro that includes an old version of the C++ [SQLite3-WinRT Component](https://github.com/doo/SQLite3-WinRT) -- [MSOpenTech / cordova-plugin-websql](https://github.com/MSOpenTech/cordova-plugin-websql) - Windows 8(+) and Windows Phone 8(+) WebSQL plugin versions in C# -- [Thinkwise / cordova-plugin-websql](https://github.com/Thinkwise/cordova-plugin-websql) - fork of [MSOpenTech / cordova-plugin-websql](https://github.com/MSOpenTech/cordova-plugin-websql) that supports asynchronous execution +- [Microsoft / cordova-plugin-websql](https://github.com/Microsoft/cordova-plugin-websql) - Windows 8(+) and Windows Phone 8(+) WebSQL plugin versions in C# +- [Thinkwise / cordova-plugin-websql](https://github.com/Thinkwise/cordova-plugin-websql) - fork of [Microsoft / cordova-plugin-websql](https://github.com/Microsoft/cordova-plugin-websql) that supports asynchronous execution - [MetaMemoryT / websql-client](https://github.com/MetaMemoryT/websql-client) - provides the same API and connects to [websql-server](https://github.com/MetaMemoryT/websql-server) through WebSockets. + + ### Alternative solutions +- Use [phearme / cordova-ContentProviderPlugin](https://github.com/phearme/cordova-ContentProviderPlugin) to query content providers on Android devices - [ABB-Austin / cordova-plugin-indexeddb-async](https://github.com/ABB-Austin/cordova-plugin-indexeddb-async) - Asynchronous IndexedDB plugin for Cordova that uses [axemclion / IndexedDBShim](https://github.com/axemclion/IndexedDBShim) (Browser/iOS/Android/Windows) and [Thinkwise / cordova-plugin-websql](https://github.com/Thinkwise/cordova-plugin-websql) - (Windows) - Another sqlite binding for React-Native (iOS version): [almost/react-native-sqlite](https://github.com/almost/react-native-sqlite) - Use [NativeScript](https://www.nativescript.org) with its web view and [NathanaelA / nativescript-sqlite](https://github.com/Natha @@ -232,6 +258,8 @@ naelA/nativescript-sqlite) (Android and/or iOS) - Standard HTML5 [local storage](https://en.wikipedia.org/wiki/Web_storage#localStorage) - [Realm.io](https://realm.io/) + + # Usage ## Self-test functions @@ -266,7 +294,7 @@ var db = window.sqlitePlugin.openDatabase({name: 'my.db', location: 'default'}, **WARNING:** The new "default" location value is *NOT* the same as the old default location and would break an upgrade for an app that was using the old default value (0) on iOS. -To specify a different location (affects iOS *only*): +To specify a different location (affects iOS/macOS *only*): ```js var db = window.sqlitePlugin.openDatabase({name: 'my.db', iosDatabaseLocation: 'Library'}, successcb, errorcb); @@ -316,12 +344,19 @@ window.sqlitePlugin.openDatabase({name: 'my.db', location: 'default'}, function( If any sql statements or transactions are attempted on a database object before the openDatabase result is known, they will be queued and will be aborted in case the database cannot be opened. +**DATABASE NAME NOTES:** + +- Database file names with slash (`/`) character(s) are not supported and not expected to work. +- Database file names with ASCII control characters such as tab, vertical tab, carriage return, line feed, form feed, and backspace are not supported and do not work on Windows. +- Some other ASCII characters not supported and not working on Windows: `*` `<` `>` `?` `\` `"` `|` +- Database file names with emojis and other 4-octet UTF-8 characters are NOT RECOMMENDED. + **OTHER NOTES:** - The database file name should include the extension, if desired. - It is possible to open multiple database access handle objects for the same database. - The database handle access object can be closed as described below. -**TIP:** +**Web SQL replacement tip:** To overwrite `window.openDatabase`: @@ -337,23 +372,34 @@ window.openDatabase = function(dbname, ignored1, ignored2, ignored3) { - (sometimes) there is an unexpected database lock - the data that was inserted is lost. -This issue is suspected to be caused by [this Android sqlite commit](https://github.com/android/platform_external_sqlite/commit/d4f30d0d1544f8967ee5763c4a1680cb0553039f), which references and includes the sqlite commit at: http://www.sqlite.org/src/info/6c4c2b7dba +The cause of this issue remains unknown. Of interest: [android / platform_external_sqlite commit d4f30d0d15](https://github.com/android/platform_external_sqlite/commit/d4f30d0d1544f8967ee5763c4a1680cb0553039f) which references and includes the sqlite commit at: http://www.sqlite.org/src/info/6c4c2b7dba There is an optional workaround that simply closes and reopens the database file at the end of every transaction that is committed. The workaround is enabled by opening the database with options as follows: ```js -var db = window.sqlitePlugin.openDatabase({name: 'my.db', location: 'default', androidLockWorkaround: 1}); +var db = window.sqlitePlugin.openDatabase({ + name: 'my.db', + location: 'default', + androidDatabaseImplementation: 2, + androidLockWorkaround: 1 +}); ``` -**IMPORTANT NOTE:** This workaround is *only* applied when using `db.transaction()`, *not* applied when running `executeSql()` on the database object. +**IMPORTANT NOTE:** This workaround is *only* applied when using `db.sqlBatch` or `db.transaction()`, *not* applied when running `executeSql()` on the database object. + + ## SQL transactions The following types of SQL transactions are supported by this version: - Single-statement transactions -- SQL batch query transactions +- SQL batch transactions - Standard asynchronous transactions +**NOTE:** Transaction requests are kept in one queue per database and executed in sequential order, according to the HTML5/[Web SQL API](http://www.w3.org/TR/webdatabase/). + +**WARNING:** It is possible to request a SQL statement list such as "SELECT 1; SELECT 2" within a single SQL statement string, however the plugin will only execute the first statement and silently ignore the others. This could result in data loss if such a SQL statement list with any INSERT or UPDATE statement(s) are included. For reference: [litehelpers/Cordova-sqlite-storage#551](https://github.com/litehelpers/Cordova-sqlite-storage/issues/551) + ### Single-statement transactions Sample with INSERT: @@ -388,7 +434,9 @@ db.executeSql("SELECT UPPER('First') AS uppertext", [], function (resultSet) { }); ``` -### SQL batch query transactions + + +### SQL batch transactions Sample: @@ -408,6 +456,8 @@ db.sqlBatch([ In case of an error, all changes in a sql batch are automatically discarded using ROLLBACK. + + ### Standard asynchronous transactions Standard asynchronous transactions follow the HTML5/[Web SQL API](http://www.w3.org/TR/webdatabase/) which is very well documented and uses BEGIN and COMMIT or ROLLBACK to keep the transactions failure-safe. Here is a simple example: @@ -419,7 +469,7 @@ db.transaction(function(tx) { tx.executeSql('INSERT INTO MyTable VALUES (?)', ['test-value'], function(tx, resultSet) { console.log('resultSet.insertId: ' + resultSet.insertId); console.log('resultSet.rowsAffected: ' + resultSet.rowsAffected); - }, function(error) { + }, function(tx, error) { console.log('INSERT error: ' + error.message); }); }, function(error) { @@ -435,7 +485,7 @@ In case of a read-only transaction, it is possible to use `readTransaction` whic db.readTransaction(function(tx) { tx.executeSql("SELECT UPPER('Some US-ASCII text') AS uppertext", [], function(tx, resultSet) { console.log("resultSet.rows.item(0).uppertext: " + resultSet.rows.item(0).uppertext); - }, function(error) { + }, function(tx, error) { console.log('SELECT error: ' + error.message); }); }, function(error) { @@ -492,11 +542,15 @@ db.readTransaction(function(tx) { **FUTURE TBD:** It should be possible to get a row result object using `resultSet.rows[rowNumber]`, also in case of a single-statement transaction. This is non-standard but is supported by the Chrome desktop browser. + + ## Background processing The threading model depends on which version is used: - For Android, one background thread per db; -- for iOS, background processing using a very limited thread pool (only one thread working at a time). +- for iOS/macOS, background processing using a very limited thread pool (only one thread working at a time). + + ## Sample with PRAGMA feature @@ -539,6 +593,8 @@ function onDeviceReady() { **NOTE:** PRAGMA statements must be executed in `executeSql()` on the database object (i.e. `db.executeSql()`) and NOT within a transaction. + + ## Sample with transaction-level nesting In this case, the same transaction in the first executeSql() callback is being reused to run executeSql() again. @@ -564,7 +620,7 @@ function onDeviceReady() { console.log("res.rows.item(0).cnt: " + res.rows.item(0).cnt + " -- should be 1"); }); - }, function(e) { + }, function(tx, e) { console.log("ERROR: " + e.message); }); }); @@ -573,6 +629,8 @@ function onDeviceReady() { This case will also works with Safari (WebKit), assuming you replace `window.sqlitePlugin.openDatabase` with `window.openDatabase`. + + ## Close a database object This will invalidate **all** handle access handle objects for the database that is closed: @@ -643,17 +701,21 @@ db.executeSql("SELECT LENGTH('tenletters') AS stringlength", [], function (res) **FUTURE TBD:** `dispose` method on the database access handle object, such that a database is closed once **all** access handle objects are disposed. + + ## Delete a database ```js window.sqlitePlugin.deleteDatabase({name: 'my.db', location: 'default'}, successcb, errorcb); ``` -with `location` or `iosDatabaseLocation` parameter *required* as described above for `openDatabase` (affects iOS *only*) +with `location` or `iosDatabaseLocation` parameter *required* as described above for `openDatabase` (affects iOS/macOS *only*) + +**BUG:** When a database is deleted, any queued transactions for that database are left hanging. TODO: All pending transactions should be errored when a database is deleted. -**BUG:** When a database is deleted, any queued transactions for that database are left hanging. All pending transactions should be errored when a database is deleted. + -# Database schema versions +## Database schema versions The transactional nature of the API makes it relatively straightforward to manage a database schema that may be upgraded over time (adding new columns or new tables, for example). Here is the recommended procedure to follow upon app startup: - Check your database schema version number (you can use `db.executeSql` since it should be a very simple query) @@ -663,6 +725,8 @@ The transactional nature of the API makes it relatively straightforward to manag **IMPORTANT:** Since we cannot be certain when the users will actually update their apps, old schema versions will have to be supported for a very long time. + + ## Use with Ionic/ngCordova/Angular It is recommended to follow the tutorial at: https://blog.nraboy.com/2014/11/use-sqlite-instead-local-storage-ionic-framework/ @@ -687,20 +751,27 @@ cordova platform rm ios cordova platform add ios ``` -You can find some more details in a nice writeup (though with old links and package names): . +or more drastically: + +```shell +rm -rf platforms +cordova platform add ios +``` + + ## Plugin installation sources - `cordova-sqlite-storage` - stable npm package version - https://github.com/litehelpers/Cordova-sqlite-storage - latest version + + ## Source tree - `SQLitePlugin.coffee.md`: platform-independent (Literate coffee-script, can be read by recent coffee-script compiler) - `www`: `SQLitePlugin.js` platform-independent Javascript as generated from `SQLitePlugin.coffee.md` (and checked in!) -- `src`: platform-specific source code: - - `android` - Java plugin code for Android - - `ios` - Objective-C plugin code for iOS +- `src`: platform-specific source code - `spec`: test suite using Jasmine (2.2.0) - `tests`: very simple Jasmine test suite that is run on Circle CI (Android version) and Travis CI (iOS version) (used as a placeholder) - `Lawnchair-adapter`: Lawnchair adaptor, based on the version from the Lawnchair repository, with the basic Lawnchair test suite in `test-www` subdirectory @@ -725,6 +796,8 @@ Assuming your app has a recent template as used by the Cordova create script, ad }); ``` + + # Support ## Policy @@ -774,7 +847,7 @@ Free support for issues with Angular/"ngCordova"/Ionic will only be provided if ## What information is needed for help Please include the following: -- Which platform(s) (Android or iOS) +- Which platform(s) (Android/iOS/macOS) - Clear description of the issue - A small, complete, self-contained program that demonstrates the problem, preferably as a Github project. ZIP/TGZ/BZ2 archive available from a public link is OK. No RAR or other such formats please! - A Cordova project is highly preferred. Intel, MS IDE, or similar project formats should be avoided. @@ -815,12 +888,16 @@ To run from a windows powershell (here is a sample for android target): .\bin\test.ps1 android + + # Adapters ## Lawnchair Adapter **BROKEN:** The Lawnchair adapter does not support the `openDatabase` options such as `location` or `iosDatabaseLocation` options and is therefore *not* expected to work with this plugin. +NOW SUPPORTED IN: [litehelpers / cordova-sqlite-lawnchair-adapter](https://github.com/litehelpers/cordova-sqlite-lawnchair-adapter) + ### Common adapter Please look at the `Lawnchair-adapter` tree that contains a common adapter, which should also work with the Android version, along with a test-www directory. @@ -857,10 +934,7 @@ ingredients = new Lawnchair({db: "cookbook", name: "ingredients", ...}, myCallba ## PouchDB -The adapter is part of [PouchDB](http://pouchdb.com/) as documented at: -- -- -- . +- [nolanlawson / pouchdb-adapter-cordova-sqlite](https://github.com/nolanlawson/pouchdb-adapter-cordova-sqlite) # Contributing @@ -882,6 +956,8 @@ The adapter is part of [PouchDB](http://pouchdb.com/) as documented at: - Always use `git mv` to move files & directories; - Never mix a move/rename operation with any other changes in the same commit. -# Contact + + +## Contact diff --git a/package.json b/package.json index 2f3989150..ca479ab82 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "name": "cordova-sqlite-storage", - "version": "1.2.2", - "description": "Native interface to SQLite for PhoneGap/Cordova (core version)", + "name": "cordova-sqlite-legacy-express-core", + "version": "1.0.0-pre1", + "description": "Native interface to SQLite for PhoneGap/Cordova (legacy express core version)", "cordova": { - "id": "cordova-sqlite-storage", + "id": "cordova-sqlite-legacy-express-core", "platforms": [ "android", "ios" @@ -19,12 +19,6 @@ "cordova-android", "cordova-ios" ], - "engines": [ - { - "name": "cordova", - "version": ">=3.3.0" - } - ], "author": "various", "license": "MIT", "bugs": { diff --git a/plugin.xml b/plugin.xml index ef947748a..ffd6362c2 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,10 +1,10 @@ + id="cordova-sqlite-legacy-express-core" + version="1.0.0-pre1"> - Cordova sqlite storage plugin (core version) + Cordova sqlite storage plugin - legacy express core version MIT @@ -13,12 +13,6 @@ Native interface to SQLite for PhoneGap/Cordova. Allows you to use more storage and provides more flexibility than the standard Web SQL database (window.openDatabase). Litehelpers/Various - - - - - - @@ -34,7 +28,6 @@ - @@ -49,7 +42,20 @@ + + + + + + + + + + + + + diff --git a/spec/config.xml b/spec/config.xml index d018622a7..4177ccb82 100644 --- a/spec/config.xml +++ b/spec/config.xml @@ -1,12 +1,12 @@ - Cordova SQLite Plugin Test Runner + Cordova-sqlite-spec Runs the unit tests suite for the Cordova SQLite plugin. - - Nolan Lawson + + Various diff --git a/spec/www/spec/basic-misc-test.js b/spec/www/spec/basic-misc-test.js index cf289b6ab..2f9418705 100644 --- a/spec/www/spec/basic-misc-test.js +++ b/spec/www/spec/basic-misc-test.js @@ -229,7 +229,7 @@ var mytests = function() { }, MYTIMEOUT); it(suiteName + 'create virtual table using R-Tree', function(done) { - if (isWebSql) pending('BROKEN (NOT IMPLEMENTED) for Web SQL'); + if (isWebSql) pending('SKIP for (Android/iOS WebKit) Web SQL'); if (isWP8) pending('NOT IMPLEMENTED for WP(8)'); // NOT IMPLEMENTED in CSharp-SQLite if (isAndroid) pending('NOT IMPLEMENTED for all versions of Android'); // NOT IMPLEMENTED for all versions of Android database (failed in Circle CI) @@ -261,7 +261,8 @@ var mytests = function() { }); }, MYTIMEOUT); - it(suiteName + 'DELETE LIMIT', function(done) { + // NOT supported by SQLite amalgamation ... + xit(suiteName + 'DELETE LIMIT', function(done) { if (isWP8) pending('NOT IMPLEMENTED for WP(8)'); if (isWindows) pending('NOT IMPLEMENTED for Windows'); if (isAndroid && !isWebSql) pending('SKIP for Android plugin'); // FUTURE TBD test with newer versions (android.database) diff --git a/spec/www/spec/browser-check-startup.js b/spec/www/spec/browser-check-startup.js index be0a4938a..0e31eeb1a 100644 --- a/spec/www/spec/browser-check-startup.js +++ b/spec/www/spec/browser-check-startup.js @@ -2,18 +2,15 @@ var MYTIMEOUT = 12000; -var isAndroid = /Android/.test(navigator.userAgent); var isWP8 = /IEMobile/.test(navigator.userAgent); // Matches WP(7/8/8.1) -//var isWindows = /Windows NT/.test(navigator.userAgent); // Windows [NT] (8.1) var isWindows = /Windows /.test(navigator.userAgent); // Windows (8.1) -//var isWindowsPC = /Windows NT/.test(navigator.userAgent); // Windows [NT] (8.1) -//var isWindowsPhone_8_1 = /Windows Phone 8.1/.test(navigator.userAgent); // Windows Phone 8.1 -//var isIE = isWindows || isWP8 || isWindowsPhone_8_1; -var isIE = isWindows || isWP8; -var isWebKit = !isIE; // TBD [Android or iOS] +var isAndroid = !isWindows && /Android/.test(navigator.userAgent); +var isMac = /Macintosh/.test(navigator.userAgent); window.hasBrowser = true; -window.hasWebKitBrowser = isWebKit; +// XXX FUTURE TODO rename to something like window.hasWebKitWebSQL here +// and in actual test scripts +window.hasWebKitBrowser = (!isWindows && !isWP8 && !isMac && (isAndroid || !(window.webkit && window.webkit.messageHandlers))); describe('check startup', function() { it('receives deviceready event', function(done) { @@ -24,7 +21,7 @@ describe('check startup', function() { }, MYTIMEOUT); it('has openDatabase', function() { - if (isWebKit) expect(window.openDatabase).toBeDefined(); + if (window.hasWebKitBrowser) expect(window.openDatabase).toBeDefined(); expect(window.sqlitePlugin).toBeDefined(); expect(window.sqlitePlugin.openDatabase).toBeDefined(); }); diff --git a/spec/www/spec/db-tx-sql-results.js b/spec/www/spec/db-tx-sql-results.js index 04c606148..c89bb5679 100644 --- a/spec/www/spec/db-tx-sql-results.js +++ b/spec/www/spec/db-tx-sql-results.js @@ -328,8 +328,9 @@ var mytests = function() { if (isWebSql) { // Web SQL STANDARD: - // 1. this is a native object that is NOT affected by the change: - expect(temp1.data).toBe('test'); + // 1. this is a native object that is NOT affected by the change (SKIP for Android 5.x/+): + if (!isAndroid || /Android [1-4]/.test(navigator.userAgent)) + expect(temp1.data).toBe('test'); // 2. object returned by second resultSet.rows.item call not affected: expect(temp2.data).toBe('test'); } else { diff --git a/spec/www/spec/db-tx-string-test.js b/spec/www/spec/db-tx-string-test.js index 1ead5e3bb..32848b464 100755 --- a/spec/www/spec/db-tx-string-test.js +++ b/spec/www/spec/db-tx-string-test.js @@ -200,10 +200,13 @@ var mytests = function() { }); }); - // NOTE: the next two tests show that for iOS: - // - UNICODE \u2028 line separator from Javascript to Objective-C is working ok - // - UNICODE \u2028 line separator from Objective-C to Javascript is BROKEN - // ref: litehelpers/Cordova-sqlite-storage#147 + // NOTE: the next 2 tests show that for iOS/macOS/Android: + // - UNICODE \u2028 line separator from JavaScript to native (Objective-C/Java) is working OK + // - UNICODE \u2028 line separator from native (Objective-C/Java) to JavaScript is BROKEN + // For reference: + // - litehelpers/Cordova-sqlite-storage#147 + // - Apache Cordova CB-9435 (issue with cordova-ios, also affects macOS) + // - cordova/cordova-discuss#57 (issue with cordova-android) it(suiteName + "UNICODE \\u2028 line separator string length", function(done) { if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] Certain UNICODE characters not working with WP(8) @@ -225,9 +228,9 @@ var mytests = function() { }); it(suiteName + ' handles UNICODE \\u2028 line separator correctly [string test]', function (done) { - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) - if (!(isWebSql || isAndroid || isIE)) pending('BROKEN for iOS'); // XXX [BUG #147] (no callback received) + if (!isWebSql && !isWindows && isAndroid) pending('SKIP for Android plugin (cordova-android 6.x BUG: cordova/cordova-discuss#57)'); + if (!isWebSql && !isWindows && !isAndroid && !isWP8) pending('SKIP for iOS/macOS plugin (Cordova BUG: CB-9435)'); // NOTE: since the above test shows the UNICODE line separator (\u2028) // is seen by the sqlite implementation OK, it is now concluded that @@ -248,11 +251,14 @@ var mytests = function() { }); }); - // NOTE: the next two tests repeat the above for UNICODE \u2029 paragraph separator - // for iOS: - // - UNICODE \u2029 line separator from Javascript to Objective-C is working ok - // - UNICODE \u2029 line separator from Objective-C to Javascript is BROKEN - // ref: litehelpers/Cordova-sqlite-storage#147 + // NOTE: the next 2 tests repeat the above for UNICODE \u2029 paragraph separator + // on iOS/macOS/Android: + // - UNICODE \u2029 paragraph separator from JavaScript to native (Objective-C/Java) is working OK + // - UNICODE \u2029 paragraph separator from native (Objective-C/Java) to JavaScript is BROKEN + // For reference: + // - litehelpers/Cordova-sqlite-storage#147 + // - Apache Cordova CB-9435 (issue with cordova-ios, also affects macOS) + // - cordova/cordova-discuss#57 (issue with cordova-android) it(suiteName + "UNICODE \\u2029 line separator string length", function(done) { if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] Certain UNICODE characters not working with WP(8) @@ -275,9 +281,9 @@ var mytests = function() { }); it(suiteName + ' handles UNICODE \\u2029 line separator correctly [string test]', function (done) { - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) - if (!(isWebSql || isAndroid || isIE)) pending('BROKEN for iOS'); // XXX [BUG #147] (no callback received) + if (!isWebSql && !isWindows && isAndroid) pending('SKIP for Android plugin (cordova-android 6.x BUG: cordova/cordova-discuss#57)'); + if (!isWebSql && !isWindows && !isAndroid && !isWP8) pending('SKIP for iOS/macOS plugin (Cordova BUG: CB-9435)'); // NOTE: since the above test shows the UNICODE paragraph separator (\u2029) // is seen by the sqlite implementation OK, it is now concluded that diff --git a/spec/www/spec/db-tx-value-bindings-test.js b/spec/www/spec/db-tx-value-bindings-test.js index 0ae8604ef..674d7e88b 100755 --- a/spec/www/spec/db-tx-value-bindings-test.js +++ b/spec/www/spec/db-tx-value-bindings-test.js @@ -30,15 +30,11 @@ function start(n) { if (wait == 0) test_it_done(); } -var isAndroid = /Android/.test(navigator.userAgent); var isWP8 = /IEMobile/.test(navigator.userAgent); // Matches WP(7/8/8.1) -//var isWindows = /Windows NT/.test(navigator.userAgent); // Windows [NT] (8.1) var isWindows = /Windows /.test(navigator.userAgent); // Windows (8.1) -//var isWindowsPC = /Windows NT/.test(navigator.userAgent); // Windows [NT] (8.1) -//var isWindowsPhone_8_1 = /Windows Phone 8.1/.test(navigator.userAgent); // Windows Phone 8.1 -//var isIE = isWindows || isWP8 || isWindowsPhone_8_1; -var isIE = isWindows || isWP8; -var isWebKit = !isIE; // TBD [Android or iOS] +var isAndroid = !isWindows && /Android/.test(navigator.userAgent); +var isMac = /Macintosh/.test(navigator.userAgent); +var isWKWebView = !isWindows && !isAndroid && !isWP8 && !isMac && !!window.webkit && !!window.webkit.messageHandlers; // NOTE: In the core-master branch there is no difference between the default // implementation and implementation #2. But the test will also apply @@ -180,10 +176,9 @@ var mytests = function() { var row = res.rows.item(0); expect(row.test_date).toBe(1424174959894); - // NOTE: storing big integer in TEXT field WORKING OK with WP(8) version. - // It is now suspected that the issue lies with the results handling. - // XXX Brody TODO: storing big number in TEXT field is different for Plugin vs. Web SQL! - if (isWebSql) + // NOTE: big number stored in field with TEXT affinity with different conversion + // in case of plugin (certain platforms) vs. Android/iOS WebKit Web SQL + if (isWebSql || isMac || isWKWebView) expect(row.test_text).toBe("1424174959894.0"); // ([Big] number inserted as string ok) else expect(row.test_text).toBe("1424174959894"); // (Big integer number inserted as string ok) @@ -377,13 +372,17 @@ var mytests = function() { }); }); - // XXX Brody NOTE: same issue is now reproduced in a string test. - // TBD ???: combine with other test - // BUG #147 iOS version of plugin BROKEN: + + // Issue with iOS/macOS/Android + // For reference: + // - litehelpers/Cordova-sqlite-storage#147 + // - Apache Cordova CB-9435 (issue with cordova-ios, also affects macOS) + // - cordova/cordova-discuss#57 (issue with cordova-android) test_it(suiteName + ' handles UNICODE \\u2028 line separator correctly [in database]', function () { if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) - if (!(isWebSql || isAndroid || isIE)) pending('BROKEN for iOS'); // XXX [BUG #147] (no callback received) + if (!isWebSql && !isWindows && isAndroid) pending('SKIP for Android plugin (cordova-android 6.x BUG: cordova/cordova-discuss#57)'); + if (!isWebSql && !isWindows && !isAndroid && !isWP8) pending('SKIP for iOS/macOS plugin (Cordova BUG: CB-9435)'); var dbName = "Unicode-line-separator.db"; var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); diff --git a/spec/www/spec/misc-tx-legacy.js b/spec/www/spec/misc-tx-legacy.js index 4cf617a33..e06806b47 100755 --- a/spec/www/spec/misc-tx-legacy.js +++ b/spec/www/spec/misc-tx-legacy.js @@ -32,13 +32,7 @@ function start(n) { var isAndroid = /Android/.test(navigator.userAgent); var isWP8 = /IEMobile/.test(navigator.userAgent); // Matches WP(7/8/8.1) -//var isWindows = /Windows NT/.test(navigator.userAgent); // Windows [NT] (8.1) var isWindows = /Windows /.test(navigator.userAgent); // Windows (8.1) -//var isWindowsPC = /Windows NT/.test(navigator.userAgent); // Windows [NT] (8.1) -//var isWindowsPhone_8_1 = /Windows Phone 8.1/.test(navigator.userAgent); // Windows Phone 8.1 -//var isIE = isWindows || isWP8 || isWindowsPhone_8_1; -var isIE = isWindows || isWP8; -var isWebKit = !isIE; // TBD [Android or iOS] // NOTE: In the core-master branch there is no difference between the default // implementation and implementation #2. But the test will also apply @@ -57,7 +51,7 @@ var mytests = function() { for (var i=0; i Date: Thu, 27 Apr 2017 16:40:26 +0200 Subject: [PATCH 02/11] Some open/close/delete test updates --- spec/www/spec/db-open-close-delete-test.js | 230 ++++++++++----------- 1 file changed, 111 insertions(+), 119 deletions(-) diff --git a/spec/www/spec/db-open-close-delete-test.js b/spec/www/spec/db-open-close-delete-test.js index 3117d821b..33957b6fd 100755 --- a/spec/www/spec/db-open-close-delete-test.js +++ b/spec/www/spec/db-open-close-delete-test.js @@ -30,19 +30,14 @@ function start(n) { if (wait == 0) test_it_done(); } -var isAndroid = /Android/.test(navigator.userAgent); var isWP8 = /IEMobile/.test(navigator.userAgent); // Matches WP(7/8/8.1) -//var isWindows = /Windows NT/.test(navigator.userAgent); // Windows [NT] (8.1) -var isWindows = /Windows /.test(navigator.userAgent); // Windows (8.1) -//var isWindowsPC = /Windows NT/.test(navigator.userAgent); // Windows [NT] (8.1) -//var isWindowsPhone_8_1 = /Windows Phone 8.1/.test(navigator.userAgent); // Windows Phone 8.1 -//var isIE = isWindows || isWP8 || isWindowsPhone_8_1; -var isIE = isWindows || isWP8; -var isWebKit = !isIE; // TBD [Android or iOS] - -// NOTE: In the core-master branch there is no difference between the default -// implementation and implementation #2. But the test will also apply -// the androidLockWorkaround: 1 option in the case of implementation #2. +var isWindows = /Windows /.test(navigator.userAgent); +var isAndroid = !isWindows && /Android/.test(navigator.userAgent); + +// NOTE: While in certain version branches there is no difference between +// the default Android implementation and implementation #2, +// this test script will also apply the androidLockWorkaround: 1 option +// in case of implementation #2. var pluginScenarioList = [ isAndroid ? 'Plugin-implementation-default' : 'Plugin', 'Plugin-implementation-2' @@ -52,59 +47,47 @@ var pluginScenarioCount = isAndroid ? 2 : 1; var mytests = function() { - describe('Plugin - BASIC sqlitePlugin.openDatabase test(s)', function() { + describe('Basic sqlitePlugin.openDatabase parameter test(s)', function() { var suiteName = 'plugin: '; - it(suiteName + 'Open plugin database with Web SQL parameters (REJECTED with exception)', function(done) { + it('Open plugin database with Web SQL parameters - REJECTED with exception', function(done) { try { - var db = window.sqlitePlugin.openDatabase('open-with-web-sql-parameters-test.db', "1.0", "Demo", DEFAULT_SIZE); + // EXPECTED to throw: + var db = window.sqlitePlugin.openDatabase('open-with-web-sql-parameters-test.db', '1.0', 'test', DEFAULT_SIZE); - // NOT EXPECTED: - // window.sqlitePlugin.openDatabase did not throw + // NOT EXPECTED to get here: expect(false).toBe(true); - - // check returned db object: - expect(db).toBeDefined(); - expect(db.executeSql).toBeDefined(); - expect(db.transaction).toBeDefined(); - expect(db.close).toBeDefined(); - - //done(); - // IMPORTANT FIX: avoid the risk of over 100 db handles open when running the full test suite - db.close(done, done); + done(); } catch (e) { // EXPECTED RESULT: - expect(true).toBe(true); + expect(e).toBeDefined(); done(); } }, MYTIMEOUT); - // NOTE: this was an issue due to the inconsistency ng cordova documentation and source code which - // triggered problems reported in litehelpers/Cordova-sqlite-storage#246 and + // NOTE: this was an issue due to a past inconsistency between the + // ngCordova documentation and source code which triggered problems + // reported in litehelpers/Cordova-sqlite-storage#246 and // litehelpers/Cordova-sqlcipher-adapter#5. // The implementation now avoids this problem *by throwing an exception*. - // It could be nicer to just signal an error in the error callback, if present, - // through throwing an exception does prevent the user from using an invalid db object. // Brody TBD: check how the Web SQL API would handle this condition? it(suiteName + 'check that db name is really a string', function(done) { - var p1 = { name: 'my.db.name', location: 1 }; + var p1 = { name: 'my.db.name', location: 'default' }; try { - window.sqlitePlugin.openDatabase({ name: p1 }, function(db) { - // not expected: + window.sqlitePlugin.openDatabase({ name: p1, location: 'default' }, function(db) { + // NOT EXPECTED: expect(false).toBe(true); done(); }, function(error) { // OK but NOT EXPECTED: - expect(true).toBe(true); - // XXX BRODY TODO: - //expect('Behavior changed, please update this test').toBe('--'); + expect('Behavior changed, please update this test').toBe('--'); done(); }); } catch (e) { - // stopped by the implementation: - expect(true).toBe(true); - done(); + // EXPECTED RESULT - stopped by the implementation: + expect(e).toBeDefined(); + done(); } }, MYTIMEOUT); @@ -117,14 +100,10 @@ var mytests = function() { describe(pluginScenarioList[i] + ': basic sqlitePlugin.deleteDatabase test(s)', function() { var scenarioName = pluginScenarioList[i]; var suiteName = scenarioName + ': '; - var isOldAndroidImpl = (i === 1); + var isImpl2 = (i === 1); // NOTE: MUST be defined in function scope, NOT outer scope: var openDatabase = function(first, second, third, fourth, fifth, sixth) { - //if (!isOldAndroidImpl) { - // return window.sqlitePlugin.openDatabase(first, second, third, fourth, fifth, sixth); - //} - var dbname, okcb, errorcb; if (first.constructor === String ) { @@ -137,25 +116,30 @@ var mytests = function() { errorcb = third; } - if (!isOldAndroidImpl) { - return window.sqlitePlugin.openDatabase({name: dbname, location: 0}, okcb, errorcb); + if (!isImpl2) { + return window.sqlitePlugin.openDatabase({name: dbname, iosDatabaseLocation: 'default'}, okcb, errorcb); } var dbopts = { name: 'i2-'+dbname, + // database location setting needed in this version branch: + location: 1, // (value ignored on Android) androidDatabaseImplementation: 2, - androidLockWorkaround: 1, - location: 1 + androidLockWorkaround: 1 }; return window.sqlitePlugin.openDatabase(dbopts, okcb, errorcb); } var deleteDatabase = function(first, second, third) { - if (!isOldAndroidImpl) { - window.sqlitePlugin.deleteDatabase({name: first, location: 0}, second, third); + if (!isImpl2) { + window.sqlitePlugin.deleteDatabase({name: first, iosDatabaseLocation: 'default'}, second, third); } else { - window.sqlitePlugin.deleteDatabase({name: 'i2-'+first, location: 0}, second, third); + window.sqlitePlugin.deleteDatabase({ + name: 'i2-'+first, + // database location setting needed in this version branch: + location: 1 // (value ignored on Android) + }, second, third); } } @@ -236,11 +220,11 @@ var mytests = function() { describe(pluginScenarioList[i] + ': basic plugin open-close test(s)', function() { var scenarioName = pluginScenarioList[i]; var suiteName = scenarioName + ': '; - var isOldAndroidImpl = (i === 1); + var isImpl2 = (i === 1); // NOTE: MUST be defined in function scope, NOT outer scope: var openDatabase = function(first, second, third, fourth, fifth, sixth) { - //if (!isOldAndroidImpl) { + //if (!isImpl2) { // return window.sqlitePlugin.openDatabase(first, second, third, fourth, fifth, sixth); //} @@ -256,15 +240,17 @@ var mytests = function() { errorcb = third; } - if (!isOldAndroidImpl) { - return window.sqlitePlugin.openDatabase({name: dbname, location: 0}, okcb, errorcb); + if (!isImpl2) { + // database location setting needed in this version branch: + return window.sqlitePlugin.openDatabase({name: dbname, location: 2}, okcb, errorcb); } var dbopts = { name: 'i2-'+dbname, + // database location setting needed in this version branch: + location: 1, // (value ignored on Android) androidDatabaseImplementation: 2, - androidLockWorkaround: 1, - location: 1 + androidLockWorkaround: 1 }; return window.sqlitePlugin.openDatabase(dbopts, okcb, errorcb); @@ -285,8 +271,8 @@ var mytests = function() { }); test_it(suiteName + ' database.close (immediately after open) calls its success callback', function () { - // XXX POSSIBLY BROKEN on iOS due to current background processing implementation - if (!(isAndroid || isIE)) pending('POSSIBLY BROKEN on iOS (background processing implementation)'); + // TBD POSSIBLY BROKEN on iOS/macOS due to current background processing implementation: + if (!isAndroid && !isWindows && !isWP8) pending('POSSIBLY BROKEN on iOS/macOS (background processing implementation)'); // asynch test coming up stop(1); @@ -327,9 +313,9 @@ var mytests = function() { test_it(suiteName + ' database.close fails in transaction', function () { stop(2); + var dbName = "Database-Close-fail.db"; - var dbName = "Database-Close-fail"; - var db = openDatabase({name: dbName, location: 1}); + var db = openDatabase({name: dbName, location: 'default'}); db.readTransaction(function(tx) { tx.executeSql('SELECT 1', [], function(tx, results) { @@ -353,11 +339,11 @@ var mytests = function() { }); test_it(suiteName + ' attempt to close db twice', function () { - var dbName = "close-db-twice.db"; + var dbName = 'close-db-twice.db'; stop(1); - openDatabase({name: dbName}, function(db) { + openDatabase({name: dbName, location: 'default'}, function(db) { ok(!!db, 'valid db object'); db.close(function () { ok(true, 'db.close() success callback (first time)'); @@ -419,7 +405,7 @@ var mytests = function() { it(suiteName + ' REPRODUCE BUG: close DB in db.executeSql() callback', function (done) { var dbName = "Close-DB-in-db-executeSql-callback.db"; - openDatabase({name: dbName}, function (db) { + openDatabase({name: dbName, location: 'default'}, function (db) { db.executeSql("CREATE TABLE IF NOT EXISTS tt (test_data)", [], function() { db.close(function () { // FUTURE TBD EXPECTED RESULT: @@ -461,12 +447,13 @@ var mytests = function() { // Needed to support some large-scale applications: test_it(suiteName + ' open same database twice in [same] specified location works', function () { // XXX TODO [BROKEN]: same db name, different location should be different db! - var dbName = 'open-twice-same-location.db'; - stop(2); - var db1 = openDatabase({name: dbName, location: 2}, function () { - var db2 = openDatabase({name: dbName, location: 2}, function () { + var dbName = 'test-open-twice-in-same-location.db'; + var dbargs = {name: dbName, location: 1}; + + var db1 = openDatabase(dbargs, function () { + var db2 = openDatabase(dbargs, function () { db1.readTransaction(function(tx1) { tx1.executeSql('SELECT 1', [], function(tx1d, results) { ok(true, 'db1 transaction working'); @@ -499,14 +486,15 @@ var mytests = function() { test_it(suiteName + ' close then re-open (2x) allows subsequent queries to run', function () { // asynch test coming up stop(1); - - var dbName = "Database-Close-and-Reopen"; - openDatabase({name: dbName, location: 0}, function (db) { + var dbName = 'test-database-close-and-reopen.db'; + var dbargs = {name: dbName, location: 0}; + + openDatabase(dbargs, function (db) { db.close(function () { - openDatabase({name: dbName, location: 0}, function (db) { + openDatabase(dbargs, function (db) { db.close(function () { - openDatabase({name: dbName, location: 0}, function (db) { + openDatabase(dbargs, function (db) { db.readTransaction(function (tx) { tx.executeSql('SELECT 1', [], function (tx, results) { ok(true, 'database re-opened succesfully'); @@ -521,7 +509,7 @@ var mytests = function() { }, function(tx) { // close on transaction success not while executing // or commit will fail - db.close(); + db.close(); }); }, function (error) { ok(false, error.message); @@ -547,15 +535,16 @@ var mytests = function() { // Needed to support some large-scale applications: test_it(suiteName + " delete then re-open (location: 'default') allows subsequent queries to run", function () { - var dbName = "Database-delete-and-Reopen.db"; + var dbName = "test-database-delete-and-reopen.db"; + var dbargs = {name: dbName, iosDatabaseLocation: 'default'}; // async test coming up stop(1); - var db = openDatabase({name: dbName, location: 'default'}, function () { + var db = openDatabase(dbargs, function () { // success CB - deleteDatabase({name: dbName, location: 'default'}, function () { - db = openDatabase({name: dbName, location: 'default'}, function () { + deleteDatabase(dbargs, function () { + db = openDatabase(dbargs, function () { db.readTransaction(function (tx) { tx.executeSql('SELECT 1', [], function (tx, results) { ok(true, 'database re-opened succesfully'); @@ -588,20 +577,20 @@ var mytests = function() { // XXX SEE BELOW: repeat scenario but wait for open callback before close/delete/reopen // Needed to support some large-scale applications: test_it(suiteName + ' immediate close, then delete then re-open allows subsequent queries to run', function () { - - // XXX POSSIBLY BROKEN on iOS due to current background processing implementation - if (!(isAndroid || isIE)) pending('POSSIBLY BROKEN on iOS (background processing implementation)'); + // TBD POSSIBLY BROKEN on iOS/macOS due to current background processing implementation: + if (!isAndroid && !isWindows && !isWP8) pending('POSSIBLY BROKEN on iOS/macOS (background processing implementation)'); var dbName = "Immediate-close-delete-Reopen.db"; + var dbargs = {name: dbName, location: 'default'}; // asynch test coming up stop(1); - var db1 = openDatabase({name: dbName, iosDatabaseLocation: 'Documents'}); + var db1 = openDatabase(dbargs); db1.close(function () { - deleteDatabase({name: dbName, iosDatabaseLocation: 'Documents'}, function () { - openDatabase({name: dbName, iosDatabaseLocation: 'Documents'}, function(db) { + deleteDatabase(dbargs, function () { + openDatabase(dbargs, function(db) { db.readTransaction(function (tx) { tx.executeSql('SELECT 1', [], function (tx, results) { ok(true, 'database re-opened succesfully'); @@ -628,18 +617,18 @@ var mytests = function() { }); }); - test_it(suiteName + ' close (after open cb), then delete then re-open allows subsequent queries to run', function () { - - var dbName = "Close-after-opencb-delete-reopen.db"; + test_it(suiteName + ' close (after open cb), then delete & re-open allows subsequent queries to run', function () { + var dbName = 'test-close-after-opencb-then-delete-and-reopen.db'; + var dbargs = {name: dbName, iosDatabaseLocation: 'Library'}; // asynch test coming up stop(1); - openDatabase({name: dbName, iosDatabaseLocation: 'Library'}, function(db1) { + openDatabase(dbargs, function(db1) { db1.close(function () { - deleteDatabase({name: dbName, iosDatabaseLocation: 'Library'}, function () { - openDatabase({name: dbName, iosDatabaseLocation: 'Library'}, function(db) { + deleteDatabase(dbargs, function () { + openDatabase(dbargs, function(db) { db.readTransaction(function (tx) { tx.executeSql('SELECT 1', [], function (tx, results) { ok(true, 'database re-opened succesfully'); @@ -673,27 +662,28 @@ var mytests = function() { }); test_it(suiteName + ' repeatedly open and close database (4x)', function () { - var dbName = "repeatedly-open-and-close-db-4x.db"; + var dbName = 'test-repeatedly-open-and-close-db-4x.db'; + var dbargs = {name: dbName, location: 0}; // async test coming up stop(1); - openDatabase({name: dbName, location: 0}, function(db) { + openDatabase(dbargs, function(db) { ok(!!db, 'valid db object 1/4'); db.close(function () { ok(true, 'success 1/4'); - openDatabase({name: dbName, location: 0}, function(db) { + openDatabase(dbargs, function(db) { ok(!!db, 'valid db object 2/4'); db.close(function () { ok(true, 'success 2/4'); - openDatabase({name: dbName, location: 0}, function(db) { + openDatabase(dbargs, function(db) { ok(!!db, 'valid db object 3/4'); db.close(function () { ok(true, 'success 3/4'); - openDatabase({name: dbName, location: 0}, function(db) { + openDatabase(dbargs, function(db) { ok(!!db, 'valid db object 4/4'); db.close(function () { ok(true, 'success 4/4'); @@ -734,35 +724,36 @@ var mytests = function() { }); test_it(suiteName + ' repeatedly open and close database faster (5x)', function () { - // XXX CURRENTLY BROKEN on iOS due to current background processing implementation - if (!(isAndroid || isIE)) pending('CURRENTLY BROKEN on iOS (background processing implementation)'); + // TBD CURRENTLY BROKEN on iOS/macOS due to current background processing implementation: + if (!isAndroid && !isWindows && !isWP8) pending('CURRENTLY BROKEN on iOS/macOS (background processing implementation)'); - var dbName = "repeatedly-open-and-close-faster-5x.db"; + var dbName = 'repeatedly-open-and-close-faster-5x.db'; + var dbargs = {name: dbName, location: 'default'}; // async test coming up stop(1); - var db = openDatabase({name: dbName, location: 0}); + var db = openDatabase(dbargs); ok(!!db, 'valid db object 1/5'); db.close(function () { ok(true, 'success 1/5'); - db = openDatabase({name: dbName, location: 0}); + db = openDatabase(dbargs); ok(!!db, 'valid db object 2/5'); db.close(function () { ok(true, 'success 2/5'); - db = openDatabase({name: dbName, location: 0}); + db = openDatabase(dbargs); ok(!!db, 'valid db object 3/5'); db.close(function () { ok(true, 'success 3/5'); - db = openDatabase({name: dbName, location: 0}); + db = openDatabase(dbargs); ok(!!db, 'valid db object 4/5'); db.close(function () { ok(true, 'success 4/5'); - db = openDatabase({name: dbName, location: 0}); + db = openDatabase(dbargs); ok(!!db, 'valid db object 5/5'); db.close(function () { ok(true, 'success 5/5'); @@ -792,7 +783,7 @@ var mytests = function() { // Needed to support some large-scale applications: test_it(suiteName + ' repeatedly open and delete database (4x)', function () { - var dbName = "repeatedly-open-and-delete-4x.db"; + var dbName = 'test-repeatedly-open-and-delete-4x.db'; var dbargs = {name: dbName, iosDatabaseLocation: 'Documents'}; // async test coming up @@ -855,37 +846,38 @@ var mytests = function() { // Needed to support some large-scale applications: test_it(suiteName + ' repeatedly open and delete database faster (5x)', function () { - // XXX CURRENTLY BROKEN on iOS due to current background processing implementation - if (!(isAndroid || isIE)) pending('CURRENTLY BROKEN on iOS (background processing implementation)'); + // TBD CURRENTLY BROKEN on iOS/macOS due to current background processing implementation: + if (!isAndroid && !isWindows && !isWP8) pending('CURRENTLY BROKEN on iOS/macOS (background processing implementation)'); - var dbName = "repeatedly-open-and-delete-faster-5x.db"; + var dbName = 'repeatedly-open-and-delete-faster-5x.db'; + var dbargs = {name: dbName, location: 'default'}; // async test coming up stop(1); - var db = openDatabase({name: dbName, location: 0}); + var db = openDatabase(dbargs); ok(!!db, 'valid db object 1/5'); - sqlitePlugin.deleteDatabase({name: dbName, location: 0}, function () { + sqlitePlugin.deleteDatabase(dbargs, function () { ok(true, 'success 1/5'); - db = openDatabase({name: dbName, location: 0}); + db = openDatabase(dbargs); ok(!!db, 'valid db object 2/5'); - sqlitePlugin.deleteDatabase({name: dbName, location: 0}, function () { + sqlitePlugin.deleteDatabase(dbargs, function () { ok(true, 'success 2/5'); - db = openDatabase({name: dbName, location: 0}); + db = openDatabase(dbargs); ok(!!db, 'valid db object 3/5'); - sqlitePlugin.deleteDatabase({name: dbName, location: 0}, function () { + sqlitePlugin.deleteDatabase(dbargs, function () { ok(true, 'success 3/5'); - db = openDatabase({name: dbName, location: 0}); + db = openDatabase(dbargs); ok(!!db, 'valid db object 4/5'); - sqlitePlugin.deleteDatabase({name: dbName, location: 0}, function () { + sqlitePlugin.deleteDatabase(dbargs, function () { ok(true, 'success 4/5'); - db = openDatabase({name: dbName, location: 0}); + db = openDatabase(dbargs); ok(!!db, 'valid db object 5/5'); - sqlitePlugin.deleteDatabase({name: dbName, location: 0}, function () { + sqlitePlugin.deleteDatabase(dbargs, function () { ok(true, 'success 5/5'); start(1); From 05f5750bf92a653530b60103b91c3bab2afbf8dc Mon Sep 17 00:00:00 2001 From: "Christopher J. Brody" Date: Fri, 14 Apr 2017 02:12:55 +0200 Subject: [PATCH 03/11] selfTest simulate scenario in BUG litehelpers/Cordova-sqlite-storage#666 Includes string test and test of effects of location reload/change in this version branch along with another internal check NOTE: selfTest with these changes also succeeds on Windows & WP8 platforms. --- CHANGES.md | 3 +- README.md | 3 +- SQLitePlugin.coffee.md | 139 ++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- plugin.xml | 2 +- www/SQLitePlugin.js | 120 +++++++++++++++++++++++++++++++++-- 6 files changed, 256 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7b096b9ee..c2c73d7cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,8 @@ # Changes -###### cordova-sqlite-legacy-express-core 1.0.0-pre1 +###### cordova-sqlite-legacy-express-core 1.0.0-pre2 +- selfTest simulate scenario in BUG litehelpers/Cordova-sqlite-storage#666 (also includes string test and test of effects of location reload/change in this version branch, along with another internal check) - Drop engine constraints in package.json & plugin.xml (in this version branch) - Support macOS platform with builtin libsqlite3.dylib framework in this version branch diff --git a/README.md b/README.md index 8967c103c..315df5407 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ Use the `location` or `iosDatabaseLocation` option in `sqlitePlugin.openDatabase - As described in [this posting](http://brodyspark.blogspot.com/2012/12/cordovaphonegap-sqlite-plugins-offer.html): - Keeps sqlite database in a user data location that is known; can be reconfigured (iOS/macOS platform version); and may be synchronized to iCloud (iOS platform version). - No 5MB maximum, more information at: http://www.sqlite.org/limits.html +- Also tested for multi-page applications with window location changes - This project is self-contained: no dependencies on other plugins such as cordova-plugin-file - Windows 10 UWP platform version available in [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) and Windows 8.1/Windows Phone 8.1/Windows 10 platform version available in [litehelpers / Cordova-sqlite-legacy](https://github.com/litehelpers/Cordova-sqlite-legacy) use the performant C++ [doo / SQLite3-WinRT](https://github.com/doo/SQLite3-WinRT) component. - [SQLCipher](https://www.zetetic.net/sqlcipher/) support for Android/iOS/macOS/Windows is available in: [litehelpers / Cordova-sqlcipher-adapter](https://github.com/litehelpers/Cordova-sqlcipher-adapter) @@ -116,6 +117,7 @@ Use the `location` or `iosDatabaseLocation` option in `sqlitePlugin.openDatabase ## Known issues +- Transaction problem after page change WITH POSSIBLE DATA LOSS ref: [litehelpers/Cordova-sqlite-storage#666](https://github.com/litehelpers/Cordova-sqlite-storage/issues/666) - iOS/macOS platform version does not support certain rapidly repeated open-and-close or open-and-delete test scenarios due to how the implementation handles background processing - As described below, auto-vacuum is NOT enabled by default. - It is possible to request a SQL statement list such as "SELECT 1; SELECT 2" within a single SQL statement string, however the plugin will only execute the first statement and silently ignore the others ref: [litehelpers/Cordova-sqlite-storage#551](https://github.com/litehelpers/Cordova-sqlite-storage/issues/551) @@ -172,7 +174,6 @@ Issues fixed in some newer version branches: ## Further testing needed - Integration with PhoneGap developer app -- Multi-page apps - Use within [InAppBrowser](http://docs.phonegap.com/en/edge/cordova_inappbrowser_inappbrowser.md.html) - Use within an iframe (see [litehelpers/Cordova-sqlite-storage#368 (comment)](https://github.com/litehelpers/Cordova-sqlite-storage/issues/368#issuecomment-154046367)) - Actual behavior when using SAVEPOINT(s) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index a5b187ffb..4ff8e9694 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -27,6 +27,7 @@ # applications that repeatedly open and close the database. # [BUG #210] TODO: better to abort and clean up the pending transaction state. # XXX TBD this will be renamed and include some more per-db state. + # NOTE: In case txLocks is renamed or replaced the selfTest has to be adapted as well. txLocks = {} ## utility functions: @@ -116,6 +117,7 @@ # Keep track of state of open db connections # XXX TBD this will be moved and renamed or # combined with txLocks. + # NOTE: In case txLocks is renamed or replaced the selfTest has to be adapted as well. SQLitePlugin::openDBs = {} SQLitePlugin::addTransaction = (t) -> @@ -690,11 +692,127 @@ start: (successcb, errorcb) -> SQLiteFactory.deleteDatabase {name: SelfTest.DBNAME, location: 'default'}, - (-> SelfTest.start2(successcb, errorcb)), - (-> SelfTest.start2(successcb, errorcb)) + (-> SelfTest.step1(successcb, errorcb)), + (-> SelfTest.step1(successcb, errorcb)) + return + + step1: (successcb, errorcb) -> + SQLiteFactory.openDatabase {name: SelfTest.DBNAME, location: 'default'}, (db) -> + check1 = false + db.transaction (tx) -> + tx.executeSql 'SELECT UPPER("Test") AS upperText', [], (ignored, resutSet) -> + if !resutSet.rows + return SelfTest.finishWithError errorcb, 'Missing resutSet.rows' + + if !resutSet.rows.length + return SelfTest.finishWithError errorcb, 'Missing resutSet.rows.length' + + if resutSet.rows.length isnt 1 + return SelfTest.finishWithError errorcb, + "Incorrect resutSet.rows.length value: #{resutSet.rows.length} (expected: 1)" + + if !resutSet.rows.item(0).upperText + return SelfTest.finishWithError errorcb, + 'Missing resutSet.rows.item(0).upperText' + + if resutSet.rows.item(0).upperText isnt 'TEST' + return SelfTest.finishWithError errorcb, + "Incorrect resutSet.rows.item(0).upperText value: #{resutSet.rows.item(0).upperText} (expected: 'TEST')" + + check1 = true + return + + , (ignored, tx_sql_err) -> + return SelfTest.finishWithError errorcb, "TX SQL error: #{tx_sql_err}" + + return + + , (tx_err) -> + return SelfTest.finishWithError errorcb, "TRANSACTION error: #{tx_err}" + + , () -> + # tx success: + if !check1 + return SelfTest.finishWithError errorcb, + 'Did not get expected upperText result data' + + # SIMULATE SCENARIO IN BUG litehelpers/Cordova-sqlite-storage#666: + db.executeSql 'BEGIN', null, (ignored) -> nextTick -> # (nextTick needed for Windows) + # DELETE INTERNAL STATE to simulate the effects of location refresh or change: + delete db.openDBs[SelfTest.DBNAME] + delete txLocks[SelfTest.DBNAME] + nextTick -> + # VERIFY INTERNAL STATE IS DELETED: + db.transaction (tx2) -> + tx2.executeSql 'SELECT 1' + return + , (tx_err) -> + # EXPECTED RESULT: + if !tx_err + return SelfTest.finishWithError errorcb, 'Missing error object' + SelfTest.step2 successcb, errorcb + return + , () -> + # NOT EXPECTED: + return SelfTest.finishWithError errorcb, 'Missing error object' + return + return + + return + return - start2: (successcb, errorcb) -> + , (open_err) -> + SelfTest.finishWithError errorcb, "Open database error: #{open_err}" + return + + step2: (successcb, errorcb) -> + SQLiteFactory.openDatabase {name: SelfTest.DBNAME, location: 'default'}, (db) -> + # TX FAILURE EXPECTED DUE TO BUG litehelpers/Cordova-sqlite-storage#666: + db.transaction (tx) -> + tx.executeSql 'SELECT ? AS myResult', [null], (ignored, resutSet) -> + # Extra sql success callback ignored: + return + return + + , (txError) -> + # EXPECTED RESULT DUE TO BUG litehelpers/Cordova-sqlite-storage#666: + if !txError + return SelfTest.finishWithError errorcb, 'Missing txError object' + # second try should work: + db.transaction (tx2) -> + tx2.executeSql 'SELECT ? AS myResult', [null], (ignored, resutSet) -> + if !resutSet.rows + return SelfTest.finishWithError errorcb, 'Missing resutSet.rows' + if !resutSet.rows.length + return SelfTest.finishWithError errorcb, 'Missing resutSet.rows.length' + if resutSet.rows.length isnt 1 + return SelfTest.finishWithError errorcb, + SelfTest.step3 successcb, errorcb + return + return + , (tx2_err) -> + return SelfTest.finishWithError errorcb, "UNEXPECTED TRANSACTION ERROR: #{tx2_err}" + return + + , () -> + # TX SUCCESS POSSIBLE FOR Android (android.database) ONLY + # NOTE: Windows 10 (UWP) mobile platform also shows "Android" in navigator.userAgent, + # filtered out here. + # FUTURE TBD android.database implementation should be fixed to report error in this case. + if /Android/.test(navigator.userAgent) and not /Windows /.test(navigator.userAgent) + return SelfTest.step3 successcb, errorcb + # OTHERWISE: + # TX SUCCESS NOT EXPECTED DUE TO BUG litehelpers/Cordova-sqlite-storage#666: + return SelfTest.finishWithError errorcb, 'UNEXPECTED SUCCESS ref: litehelpers/Cordova-sqlite-storage#666' + return + + , (open_err) -> + SelfTest.finishWithError errorcb, "Open database error: #{open_err}" + return + + step3: (successcb, errorcb) -> SQLiteFactory.openDatabase {name: SelfTest.DBNAME, location: 'default'}, (db) -> + # FUTURE TBD TEST CRUD OPERATIONS (already fixed in a newer version branch) db.sqlBatch [ 'CREATE TABLE TestTable(TestColumn);' [ 'INSERT INTO TestTable (TestColumn) VALUES (?);', ['test-value'] ] @@ -751,9 +869,19 @@ # CLEANUP & FINISH: db.close () -> SQLiteFactory.deleteDatabase {name: SelfTest.DBNAME, location: 'default'}, successcb, (cleanup_err)-> + # TBD IGNORE THIS ERROR on Windows (and WP8): + if /Windows /.test(navigator.userAgent) or /IEMobile/.test(navigator.userAgent) + console.log "IGNORE CLEANUP (DELETE) ERROR: #{JSON.stringify cleanup_err} (Windows/WP8)" + successcb() + return SelfTest.finishWithError errorcb, "Cleanup error: #{cleanup_err}" , (close_err) -> + # TBD IGNORE THIS ERROR on Windows (and WP8): + if /Windows /.test(navigator.userAgent) or /IEMobile/.test(navigator.userAgent) + console.log "IGNORE close ERROR: #{JSON.stringify close_err} (Windows/WP8)" + SQLiteFactory.deleteDatabase {name: SelfTest.DBNAME, location: 'default'}, successcb, successcb + return SelfTest.finishWithError errorcb, "close error: #{close_err}" , (select_err) -> @@ -764,11 +892,16 @@ , (open_err) -> SelfTest.finishWithError errorcb, "Open database error: #{open_err}" + return finishWithError: (errorcb, message) -> + console.log "selfTest ERROR with message: #{message}" SQLiteFactory.deleteDatabase {name: SelfTest.DBNAME, location: 'default'}, -> errorcb newSQLError message + # FUTURE TODO: return + # FUTURE TODO log err2 , (err2)-> errorcb newSQLError "Cleanup error: #{err2} for error: #{message}" + return ## Exported API: diff --git a/package.json b/package.json index ca479ab82..be8847a4b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-sqlite-legacy-express-core", - "version": "1.0.0-pre1", + "version": "1.0.0-pre2", "description": "Native interface to SQLite for PhoneGap/Cordova (legacy express core version)", "cordova": { "id": "cordova-sqlite-legacy-express-core", diff --git a/plugin.xml b/plugin.xml index ffd6362c2..5e027b26e 100644 --- a/plugin.xml +++ b/plugin.xml @@ -2,7 +2,7 @@ + version="1.0.0-pre2"> Cordova sqlite storage plugin - legacy express core version diff --git a/www/SQLitePlugin.js b/www/SQLitePlugin.js index f6fd37fc7..1b6c7da23 100644 --- a/www/SQLitePlugin.js +++ b/www/SQLitePlugin.js @@ -606,17 +606,111 @@ SelfTest = { DBNAME: '___$$$___litehelpers___$$$___test___$$$___.db', start: function(successcb, errorcb) { - return SQLiteFactory.deleteDatabase({ + SQLiteFactory.deleteDatabase({ name: SelfTest.DBNAME, location: 'default' }, (function() { - return SelfTest.start2(successcb, errorcb); + return SelfTest.step1(successcb, errorcb); }), (function() { - return SelfTest.start2(successcb, errorcb); + return SelfTest.step1(successcb, errorcb); })); }, - start2: function(successcb, errorcb) { - return SQLiteFactory.openDatabase({ + step1: function(successcb, errorcb) { + SQLiteFactory.openDatabase({ + name: SelfTest.DBNAME, + location: 'default' + }, function(db) { + var check1; + check1 = false; + db.transaction(function(tx) { + tx.executeSql('SELECT UPPER("Test") AS upperText', [], function(ignored, resutSet) { + if (!resutSet.rows) { + return SelfTest.finishWithError(errorcb, 'Missing resutSet.rows'); + } + if (!resutSet.rows.length) { + return SelfTest.finishWithError(errorcb, 'Missing resutSet.rows.length'); + } + if (resutSet.rows.length !== 1) { + return SelfTest.finishWithError(errorcb, "Incorrect resutSet.rows.length value: " + resutSet.rows.length + " (expected: 1)"); + } + if (!resutSet.rows.item(0).upperText) { + return SelfTest.finishWithError(errorcb, 'Missing resutSet.rows.item(0).upperText'); + } + if (resutSet.rows.item(0).upperText !== 'TEST') { + return SelfTest.finishWithError(errorcb, "Incorrect resutSet.rows.item(0).upperText value: " + (resutSet.rows.item(0).upperText) + " (expected: 'TEST')"); + } + check1 = true; + }, function(ignored, tx_sql_err) { + return SelfTest.finishWithError(errorcb, "TX SQL error: " + tx_sql_err); + }); + }, function(tx_err) { + return SelfTest.finishWithError(errorcb, "TRANSACTION error: " + tx_err); + }, function() { + if (!check1) { + return SelfTest.finishWithError(errorcb, 'Did not get expected upperText result data'); + } + db.executeSql('BEGIN', null, function(ignored) { + return nextTick(function() { + delete db.openDBs[SelfTest.DBNAME]; + delete txLocks[SelfTest.DBNAME]; + nextTick(function() { + db.transaction(function(tx2) { + tx2.executeSql('SELECT 1'); + }, function(tx_err) { + if (!tx_err) { + return SelfTest.finishWithError(errorcb, 'Missing error object'); + } + SelfTest.step2(successcb, errorcb); + }, function() { + return SelfTest.finishWithError(errorcb, 'Missing error object'); + }); + }); + }); + }); + }); + }, function(open_err) { + return SelfTest.finishWithError(errorcb, "Open database error: " + open_err); + }); + }, + step2: function(successcb, errorcb) { + SQLiteFactory.openDatabase({ + name: SelfTest.DBNAME, + location: 'default' + }, function(db) { + db.transaction(function(tx) { + tx.executeSql('SELECT ? AS myResult', [null], function(ignored, resutSet) {}); + }, function(txError) { + if (!txError) { + return SelfTest.finishWithError(errorcb, 'Missing txError object'); + } + db.transaction(function(tx2) { + tx2.executeSql('SELECT ? AS myResult', [null], function(ignored, resutSet) { + if (!resutSet.rows) { + return SelfTest.finishWithError(errorcb, 'Missing resutSet.rows'); + } + if (!resutSet.rows.length) { + return SelfTest.finishWithError(errorcb, 'Missing resutSet.rows.length'); + } + if (resutSet.rows.length !== 1) { + return SelfTest.finishWithError(errorcb); + } + SelfTest.step3(successcb, errorcb); + }); + }, function(tx2_err) { + return SelfTest.finishWithError(errorcb, "UNEXPECTED TRANSACTION ERROR: " + tx2_err); + }); + }, function() { + if (/Android/.test(navigator.userAgent) && !/Windows /.test(navigator.userAgent)) { + return SelfTest.step3(successcb, errorcb); + } + return SelfTest.finishWithError(errorcb, 'UNEXPECTED SUCCESS ref: litehelpers/Cordova-sqlite-storage#666'); + }); + }, function(open_err) { + return SelfTest.finishWithError(errorcb, "Open database error: " + open_err); + }); + }, + step3: function(successcb, errorcb) { + SQLiteFactory.openDatabase({ name: SelfTest.DBNAME, location: 'default' }, function(db) { @@ -673,9 +767,22 @@ name: SelfTest.DBNAME, location: 'default' }, successcb, function(cleanup_err) { + if (/Windows /.test(navigator.userAgent) || /IEMobile/.test(navigator.userAgent)) { + console.log("IGNORE CLEANUP (DELETE) ERROR: " + (JSON.stringify(cleanup_err)) + " (Windows/WP8)"); + successcb(); + return; + } return SelfTest.finishWithError(errorcb, "Cleanup error: " + cleanup_err); }); }, function(close_err) { + if (/Windows /.test(navigator.userAgent) || /IEMobile/.test(navigator.userAgent)) { + console.log("IGNORE close ERROR: " + (JSON.stringify(close_err)) + " (Windows/WP8)"); + SQLiteFactory.deleteDatabase({ + name: SelfTest.DBNAME, + location: 'default' + }, successcb, successcb); + return; + } return SelfTest.finishWithError(errorcb, "close error: " + close_err); }); }); @@ -691,7 +798,8 @@ }); }, finishWithError: function(errorcb, message) { - return SQLiteFactory.deleteDatabase({ + console.log("selfTest ERROR with message: " + message); + SQLiteFactory.deleteDatabase({ name: SelfTest.DBNAME, location: 'default' }, function() { From a760cc65c85f4ae4b82dd565392b30dfbf438fb9 Mon Sep 17 00:00:00 2001 From: "Christopher J. Brody" Date: Thu, 20 Apr 2017 20:34:51 +0200 Subject: [PATCH 04/11] Workaround solution to BUG litehelpers/Cordova-sqlite-storage#666 Internally verified by selfTest function. --- CHANGES.md | 5 ++- README.md | 2 +- SQLitePlugin.coffee.md | 98 ++++++++++++++++++++++++------------------ package.json | 2 +- plugin.xml | 2 +- www/SQLitePlugin.js | 46 +++++++++----------- 6 files changed, 81 insertions(+), 74 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c2c73d7cc..c4230c52a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,9 @@ # Changes -###### cordova-sqlite-legacy-express-core 1.0.0-pre2 +###### cordova-sqlite-legacy-express-core 1.0.0-pre3 -- selfTest simulate scenario in BUG litehelpers/Cordova-sqlite-storage#666 (also includes string test and test of effects of location reload/change in this version branch, along with another internal check) +- Workaround solution to BUG litehelpers/Cordova-sqlite-storage#666 (hanging transaction in case of location reload/change) +- selfTest simulate scenario & test solution to BUG litehelpers/Cordova-sqlite-storage#666 (also includes string test and test of effects of location reload/change in this version branch, along with another internal check) - Drop engine constraints in package.json & plugin.xml (in this version branch) - Support macOS platform with builtin libsqlite3.dylib framework in this version branch diff --git a/README.md b/README.md index 315df5407..0045455c3 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Use the `location` or `iosDatabaseLocation` option in `sqlitePlugin.openDatabase ## Announcements +- Resolved transaction problem after window.location (page) change with possible data loss ref: [litehelpers/Cordova-sqlite-storage#666](https://github.com/litehelpers/Cordova-sqlite-storage/issues/666) - [brodybits / cordova-sqlite-test-app](https://github.com/brodybits/cordova-sqlite-test-app) project is a CC0 (public domain) starting point (NOTE that this plugin must be added) and may also be used to reproduce issues with this plugin. - The Lawnchair adapter is now supported in [litehelpers / cordova-sqlite-lawnchair-adapter](https://github.com/litehelpers/cordova-sqlite-lawnchair-adapter). - [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) now supports SELECT BLOB data in Base64 format on all platforms in addition to REGEXP (Android/iOS/macOS) and pre-populated database (all platforms). @@ -117,7 +118,6 @@ Use the `location` or `iosDatabaseLocation` option in `sqlitePlugin.openDatabase ## Known issues -- Transaction problem after page change WITH POSSIBLE DATA LOSS ref: [litehelpers/Cordova-sqlite-storage#666](https://github.com/litehelpers/Cordova-sqlite-storage/issues/666) - iOS/macOS platform version does not support certain rapidly repeated open-and-close or open-and-delete test scenarios due to how the implementation handles background processing - As described below, auto-vacuum is NOT enabled by default. - It is possible to request a SQL statement list such as "SELECT 1; SELECT 2" within a single SQL statement string, however the plugin will only execute the first statement and silently ignore the others ref: [litehelpers/Cordova-sqlite-storage#551](https://github.com/litehelpers/Cordova-sqlite-storage/issues/551) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 4ff8e9694..6e1313003 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -84,7 +84,7 @@ SQLitePlugin = (openargs, openSuccess, openError) -> # console.log "SQLitePlugin openargs: #{JSON.stringify openargs}" - # _should_ already be checked by openDatabase: + # SHOULD already be checked by openDatabase: if !(openargs and openargs['name']) throw newSQLError "Cannot create a SQLitePlugin db instance without a db name" @@ -115,8 +115,8 @@ SQLitePlugin::databaseFeatures = isSQLitePluginDatabase: true # Keep track of state of open db connections - # XXX TBD this will be moved and renamed or - # combined with txLocks. + # XXX FUTURE TBD this *may* be moved and renamed, + # or even combined with txLocks if possible. # NOTE: In case txLocks is renamed or replaced the selfTest has to be adapted as well. SQLitePlugin::openDBs = {} @@ -128,14 +128,17 @@ } txLocks[@dbname].queue.push t if @dbname of @openDBs && @openDBs[@dbname] isnt DB_STATE_INIT - # XXX TODO: only when queue has length of 1 [and test it!!] + # FUTURE TBD: rename startNextTransaction to something like + # triggerTransactionQueue + # ALT TBD: only when queue has length of 1 (and test)?? @startNextTransaction() else if @dbname of @openDBs console.log 'new transaction is waiting for open operation' else - # XXX TBD TODO: in this case (which should not happen), should abort and discard the transaction. + # XXX SHOULD NOT GET HERE. + # FUTURE TBD TODO: in this exceptional case abort and discard the transaction. console.log 'database is closed, new transaction is [stuck] waiting until db is opened again!' return @@ -220,7 +223,7 @@ #if !@openDBs[@dbname] then call open error cb, and abort pending tx if any if !@openDBs[@dbname] console.log 'database was closed during open operation' - # XXX TODO [BUG #210] (and test!!): + # XXX TODO (WITH TEST) ref BUG litehelpers/Cordova-sqlite-storage#210: # if !!error then error newSQLError 'database closed during open operation' # @abortAllPendingTransactions() @@ -245,6 +248,18 @@ # store initial DB state: @openDBs[@dbname] = DB_STATE_INIT + # As a WORKAROUND SOLUTION to BUG litehelpers/Cordova-sqlite-storage#666: + # If the database was never opened on the JavaScript side + # start an extra ROLLBACK statement to abort any pending transaction + # (does not matter whether it succeeds or fails here). + # FUTURE TBD a better solution would be to send a special signal or parameter + # if the database was never opened on the JavaScript side. + if not txLocks[@dbname] + myfn = (tx) -> + tx.addStatement 'ROLLBACK' + return + @addTransaction new SQLitePluginTransaction @, myfn, null, null, false, false + cordova.exec opensuccesscb, openerrorcb, "SQLitePlugin", "open", [ @openargs ] return @@ -252,20 +267,30 @@ SQLitePlugin::close = (success, error) -> if @dbname of @openDBs if txLocks[@dbname] && txLocks[@dbname].inProgress - # XXX TBD: wait for current tx then close (??) + # FUTURE TBD TODO ref BUG litehelpers/Cordova-sqlite-storage#210: + # Wait for current tx to finish then close, + # then abort any other pending transactions + # (and cleanup any other internal resources). + # (This would need testing!!) console.log 'cannot close: transaction is in progress' error newSQLError 'database cannot be closed while a transaction is in progress' return console.log 'CLOSE database: ' + @dbname - # XXX [BUG #209] closing one db handle disables other handles to same db + # NOTE: closing one db handle disables other handles to same db + # FUTURE TBD TODO ref litehelpers/Cordova-sqlite-storage#210: + # Add a dispose method to simply invalidate the + # current database object ("this") delete @openDBs[@dbname] if txLocks[@dbname] then console.log 'closing db with transaction queue length: ' + txLocks[@dbname].queue.length else console.log 'closing db with no transaction lock state' - # XXX [BUG #210] TODO: when closing or deleting a db, abort any pending transactions [and test it!!] + # XXX TODO BUG litehelpers/Cordova-sqlite-storage#210: + # abort all pending transactions (with error callback) + # when closing a database (needs testing!!) + # (and cleanup any other internal resources) cordova.exec success, error, "SQLitePlugin", "close", [ { path: @dbname } ] @@ -645,6 +670,12 @@ new SQLitePlugin openargs, okcb, errorcb deleteDatabase: (first, success, error) -> + # XXX TODO BUG litehelpers/Cordova-sqlite-storage#367: + # abort all pending transactions (with error callback) + # when deleting a database + # (and cleanup any other internal resources) + # NOTE: This should properly close the database + # (at least on the JavaScript side) before deleting. args = {} if first.constructor == String @@ -681,7 +712,10 @@ args.dblocation = dblocation - # XXX [BUG #210] TODO: when closing or deleting a db, abort any pending transactions (with error callback) + # XXX TODO BUG litehelpers/Cordova-sqlite-storage#367 (repeated here): + # abort all pending transactions (with error callback) + # when deleting a database + # (and cleanup any other internal resources) delete SQLitePlugin::openDBs[args.path] cordova.exec success, error, "SQLitePlugin", "delete", [ args ] @@ -767,45 +801,23 @@ step2: (successcb, errorcb) -> SQLiteFactory.openDatabase {name: SelfTest.DBNAME, location: 'default'}, (db) -> - # TX FAILURE EXPECTED DUE TO BUG litehelpers/Cordova-sqlite-storage#666: + # TX SHOULD SUCCEED to demonstrate solution to BUG litehelpers/Cordova-sqlite-storage#666: db.transaction (tx) -> tx.executeSql 'SELECT ? AS myResult', [null], (ignored, resutSet) -> - # Extra sql success callback ignored: + if !resutSet.rows + return SelfTest.finishWithError errorcb, 'Missing resutSet.rows' + if !resutSet.rows.length + return SelfTest.finishWithError errorcb, 'Missing resutSet.rows.length' + if resutSet.rows.length isnt 1 + return SelfTest.finishWithError errorcb, + "Incorrect resutSet.rows.length value: #{resutSet.rows.length} (expected: 1)" + SelfTest.step3 successcb, errorcb return return - , (txError) -> - # EXPECTED RESULT DUE TO BUG litehelpers/Cordova-sqlite-storage#666: - if !txError - return SelfTest.finishWithError errorcb, 'Missing txError object' - # second try should work: - db.transaction (tx2) -> - tx2.executeSql 'SELECT ? AS myResult', [null], (ignored, resutSet) -> - if !resutSet.rows - return SelfTest.finishWithError errorcb, 'Missing resutSet.rows' - if !resutSet.rows.length - return SelfTest.finishWithError errorcb, 'Missing resutSet.rows.length' - if resutSet.rows.length isnt 1 - return SelfTest.finishWithError errorcb, - SelfTest.step3 successcb, errorcb - return - return - , (tx2_err) -> - return SelfTest.finishWithError errorcb, "UNEXPECTED TRANSACTION ERROR: #{tx2_err}" - return - - , () -> - # TX SUCCESS POSSIBLE FOR Android (android.database) ONLY - # NOTE: Windows 10 (UWP) mobile platform also shows "Android" in navigator.userAgent, - # filtered out here. - # FUTURE TBD android.database implementation should be fixed to report error in this case. - if /Android/.test(navigator.userAgent) and not /Windows /.test(navigator.userAgent) - return SelfTest.step3 successcb, errorcb - # OTHERWISE: - # TX SUCCESS NOT EXPECTED DUE TO BUG litehelpers/Cordova-sqlite-storage#666: - return SelfTest.finishWithError errorcb, 'UNEXPECTED SUCCESS ref: litehelpers/Cordova-sqlite-storage#666' + # NOT EXPECTED: + return SelfTest.finishWithError errorcb, "UNEXPECTED TRANSACTION ERROR: #{txError}" return - , (open_err) -> SelfTest.finishWithError errorcb, "Open database error: #{open_err}" return diff --git a/package.json b/package.json index be8847a4b..04e2417d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-sqlite-legacy-express-core", - "version": "1.0.0-pre2", + "version": "1.0.0-pre3", "description": "Native interface to SQLite for PhoneGap/Cordova (legacy express core version)", "cordova": { "id": "cordova-sqlite-legacy-express-core", diff --git a/plugin.xml b/plugin.xml index 5e027b26e..386d19d13 100644 --- a/plugin.xml +++ b/plugin.xml @@ -2,7 +2,7 @@ + version="1.0.0-pre3"> Cordova sqlite storage plugin - legacy express core version diff --git a/www/SQLitePlugin.js b/www/SQLitePlugin.js index 1b6c7da23..476349c80 100644 --- a/www/SQLitePlugin.js +++ b/www/SQLitePlugin.js @@ -162,7 +162,7 @@ }; SQLitePlugin.prototype.open = function(success, error) { - var openerrorcb, opensuccesscb; + var myfn, openerrorcb, opensuccesscb; if (this.dbname in this.openDBs) { console.log('database already open: ' + this.dbname); nextTick((function(_this) { @@ -201,6 +201,12 @@ }; })(this); this.openDBs[this.dbname] = DB_STATE_INIT; + if (!txLocks[this.dbname]) { + myfn = function(tx) { + tx.addStatement('ROLLBACK'); + }; + this.addTransaction(new SQLitePluginTransaction(this, myfn, null, null, false, false)); + } cordova.exec(opensuccesscb, openerrorcb, "SQLitePlugin", "open", [this.openargs]); } }; @@ -678,32 +684,20 @@ location: 'default' }, function(db) { db.transaction(function(tx) { - tx.executeSql('SELECT ? AS myResult', [null], function(ignored, resutSet) {}); - }, function(txError) { - if (!txError) { - return SelfTest.finishWithError(errorcb, 'Missing txError object'); - } - db.transaction(function(tx2) { - tx2.executeSql('SELECT ? AS myResult', [null], function(ignored, resutSet) { - if (!resutSet.rows) { - return SelfTest.finishWithError(errorcb, 'Missing resutSet.rows'); - } - if (!resutSet.rows.length) { - return SelfTest.finishWithError(errorcb, 'Missing resutSet.rows.length'); - } - if (resutSet.rows.length !== 1) { - return SelfTest.finishWithError(errorcb); - } - SelfTest.step3(successcb, errorcb); - }); - }, function(tx2_err) { - return SelfTest.finishWithError(errorcb, "UNEXPECTED TRANSACTION ERROR: " + tx2_err); + tx.executeSql('SELECT ? AS myResult', [null], function(ignored, resutSet) { + if (!resutSet.rows) { + return SelfTest.finishWithError(errorcb, 'Missing resutSet.rows'); + } + if (!resutSet.rows.length) { + return SelfTest.finishWithError(errorcb, 'Missing resutSet.rows.length'); + } + if (resutSet.rows.length !== 1) { + return SelfTest.finishWithError(errorcb, "Incorrect resutSet.rows.length value: " + resutSet.rows.length + " (expected: 1)"); + } + SelfTest.step3(successcb, errorcb); }); - }, function() { - if (/Android/.test(navigator.userAgent) && !/Windows /.test(navigator.userAgent)) { - return SelfTest.step3(successcb, errorcb); - } - return SelfTest.finishWithError(errorcb, 'UNEXPECTED SUCCESS ref: litehelpers/Cordova-sqlite-storage#666'); + }, function(txError) { + return SelfTest.finishWithError(errorcb, "UNEXPECTED TRANSACTION ERROR: " + txError); }); }, function(open_err) { return SelfTest.finishWithError(errorcb, "Open database error: " + open_err); From b751ea907ea82d5568fa74f28eb6512c5a31c1d4 Mon Sep 17 00:00:00 2001 From: "Christopher J. Brody" Date: Fri, 21 Apr 2017 15:45:35 +0200 Subject: [PATCH 05/11] Remove Lawnchair adapter from this version branch --- CHANGES.md | 3 +- Lawnchair-adapter/Lawnchair-sqlitePlugin.js | 201 --- .../test-www/Lawnchair-sqlitePlugin.js | 198 --- Lawnchair-adapter/test-www/index.html | 76 - Lawnchair-adapter/test-www/lawnchair-spec.js | 431 ----- Lawnchair-adapter/test-www/lib/json2.js | 482 ------ Lawnchair-adapter/test-www/lib/lawnchair.js | 151 -- Lawnchair-adapter/test-www/lib/qunit.css | 225 --- Lawnchair-adapter/test-www/lib/qunit.js | 1448 ----------------- README.md | 41 +- package.json | 2 +- plugin.xml | 2 +- 12 files changed, 6 insertions(+), 3254 deletions(-) delete mode 100644 Lawnchair-adapter/Lawnchair-sqlitePlugin.js delete mode 100644 Lawnchair-adapter/test-www/Lawnchair-sqlitePlugin.js delete mode 100644 Lawnchair-adapter/test-www/index.html delete mode 100755 Lawnchair-adapter/test-www/lawnchair-spec.js delete mode 100644 Lawnchair-adapter/test-www/lib/json2.js delete mode 100644 Lawnchair-adapter/test-www/lib/lawnchair.js delete mode 100644 Lawnchair-adapter/test-www/lib/qunit.css delete mode 100644 Lawnchair-adapter/test-www/lib/qunit.js diff --git a/CHANGES.md b/CHANGES.md index c4230c52a..f4a1df964 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,11 @@ # Changes -###### cordova-sqlite-legacy-express-core 1.0.0-pre3 +###### cordova-sqlite-legacy-express-core 1.0.0-pre4 - Workaround solution to BUG litehelpers/Cordova-sqlite-storage#666 (hanging transaction in case of location reload/change) - selfTest simulate scenario & test solution to BUG litehelpers/Cordova-sqlite-storage#666 (also includes string test and test of effects of location reload/change in this version branch, along with another internal check) - Drop engine constraints in package.json & plugin.xml (in this version branch) +- Remove Lawnchair adapter from this version branch - Support macOS platform with builtin libsqlite3.dylib framework in this version branch ## 1.2.2 diff --git a/Lawnchair-adapter/Lawnchair-sqlitePlugin.js b/Lawnchair-adapter/Lawnchair-sqlitePlugin.js deleted file mode 100644 index 9af1033f6..000000000 --- a/Lawnchair-adapter/Lawnchair-sqlitePlugin.js +++ /dev/null @@ -1,201 +0,0 @@ -Lawnchair.adapter('cordova-sqlite', (function () { - // private methods - var fail = function (e, i) { console.log('error in sqlite adaptor!', e, i) } - , now = function () { return new Date() } // FIXME need to use better date fn - // not entirely sure if this is needed... - if (!Function.prototype.bind) { - Function.prototype.bind = function( obj ) { - var slice = [].slice - , args = slice.call(arguments, 1) - , self = this - , nop = function () {} - , bound = function () { - return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments))) - } - nop.prototype = self.prototype - bound.prototype = new nop() - return bound - } - } - - // public methods - return { - - valid: function() { return !!(sqlitePlugin.openDatabase) }, - - init: function (options, callback) { - var that = this - , cb = that.fn(that.name, callback) - , dbname = options.db || this.name - , bgType = options.bgType || 1 - , create = "CREATE TABLE IF NOT EXISTS " + this.name + " (id NVARCHAR(32) UNIQUE PRIMARY KEY, value TEXT, timestamp REAL)" - , win = function(){ return cb.call(that, that); } - // open a connection and create the db if it doesn't exist - this.db = sqlitePlugin.openDatabase({name:dbname,bgType:bgType}) - this.db.transaction(function (t) { - t.executeSql(create, [], win, fail) - }) - }, - - keys: function (callback) { - var cb = this.lambda(callback) - , that = this - , keys = "SELECT id FROM " + this.name + " ORDER BY timestamp DESC" - - this.db.transaction(function(t) { - var win = function (xxx, results) { - if (results.rows.length == 0 ) { - cb.call(that, []) - } else { - var r = []; - for (var i = 0, l = results.rows.length; i < l; i++) { - r.push(results.rows.item(i).id); - } - cb.call(that, r) - } - } - t.executeSql(keys, [], win, fail) - }) - return this - }, - // you think thats air you're breathing now? - save: function (obj, callback) { - var that = this - , id = obj.key || that.uuid() - , ins = "INSERT INTO " + this.name + " (value, timestamp, id) VALUES (?,?,?)" - , up = "UPDATE " + this.name + " SET value=?, timestamp=? WHERE id=?" - , win = function () { if (callback) { obj.key = id; that.lambda(callback).call(that, obj) }} - , val = [now(), id] - // existential - that.exists(obj.key, function(exists) { - // transactions are like condoms - that.db.transaction(function(t) { - // TODO move timestamp to a plugin - var insert = function (obj) { - val.unshift(JSON.stringify(obj)) - t.executeSql(ins, val, win, fail) - } - // TODO move timestamp to a plugin - var update = function (obj) { - delete(obj.key) - val.unshift(JSON.stringify(obj)) - t.executeSql(up, val, win, fail) - } - // pretty - exists ? update(obj) : insert(obj) - }) - }); - return this - }, - - // FIXME this should be a batch insert / just getting the test to pass... - batch: function (objs, cb) { - - var results = [] - , done = false - , that = this - - var updateProgress = function(obj) { - results.push(obj) - done = results.length === objs.length - } - - var checkProgress = setInterval(function() { - if (done) { - if (cb) that.lambda(cb).call(that, results) - clearInterval(checkProgress) - } - }, 200) - - for (var i = 0, l = objs.length; i < l; i++) - this.save(objs[i], updateProgress) - - return this - }, - - get: function (keyOrArray, cb) { - var that = this - , sql = '' - // batch selects support - if (this.isArray(keyOrArray)) { - sql = 'SELECT id, value FROM ' + this.name + " WHERE id IN ('" + keyOrArray.join("','") + "')" - } else { - sql = 'SELECT id, value FROM ' + this.name + " WHERE id = '" + keyOrArray + "'" - } - // FIXME - // will always loop the results but cleans it up if not a batch return at the end.. - // in other words, this could be faster - var win = function (xxx, results) { - var o = null - , r = [] - if (results.rows.length) { - for (var i = 0, l = results.rows.length; i < l; i++) { - o = JSON.parse(results.rows.item(i).value) - o.key = results.rows.item(i).id - r.push(o) - } - } - if (!that.isArray(keyOrArray)) r = r.length ? r[0] : null - if (cb) that.lambda(cb).call(that, r) - } - this.db.transaction(function(t){ t.executeSql(sql, [], win, fail) }) - return this - }, - - exists: function (key, cb) { - var is = "SELECT * FROM " + this.name + " WHERE id = ?" - , that = this - , win = function(xxx, results) { if (cb) that.fn('exists', cb).call(that, (results.rows.length > 0)) } - this.db.transaction(function(t){ t.executeSql(is, [key], win, fail) }) - return this - }, - - all: function (callback) { - var that = this - , all = "SELECT * FROM " + this.name - , r = [] - , cb = this.fn(this.name, callback) || undefined - , win = function (xxx, results) { - if (results.rows.length != 0) { - for (var i = 0, l = results.rows.length; i < l; i++) { - var obj = JSON.parse(results.rows.item(i).value) - obj.key = results.rows.item(i).id - r.push(obj) - } - } - if (cb) cb.call(that, r) - } - - this.db.transaction(function (t) { - t.executeSql(all, [], win, fail) - }) - return this - }, - - remove: function (keyOrObj, cb) { - var that = this - , key = typeof keyOrObj === 'string' ? keyOrObj : (Array.isArray(keyOrObj) ? null : keyOrObj.key) - , del = "DELETE FROM " + this.name + (Array.isArray(keyOrObj) ? " WHERE id IN ('" + keyOrObj.join("','") + "')" : " WHERE id = ?") - , win = function () { if (cb) that.lambda(cb).call(that) } - - this.db.transaction(function (t) { - if (key == null) - t.executeSql(del, [], win, fail); - else - t.executeSql(del, [key], win, fail); - }); - - return this; - }, - - nuke: function (cb) { - var nuke = "DELETE FROM " + this.name - , that = this - , win = cb ? function() { that.lambda(cb).call(that) } : function(){} - this.db.transaction(function (t) { - t.executeSql(nuke, [], win, fail) - }) - return this - } -////// -}})()) diff --git a/Lawnchair-adapter/test-www/Lawnchair-sqlitePlugin.js b/Lawnchair-adapter/test-www/Lawnchair-sqlitePlugin.js deleted file mode 100644 index e4ca0d580..000000000 --- a/Lawnchair-adapter/test-www/Lawnchair-sqlitePlugin.js +++ /dev/null @@ -1,198 +0,0 @@ -Lawnchair.adapter('webkit-sqlite', (function () { - // private methods - var fail = function (e, i) { console.log('error in sqlite adaptor!', e, i) } - , now = function () { return new Date() } // FIXME need to use better date fn - // not entirely sure if this is needed... - if (!Function.prototype.bind) { - Function.prototype.bind = function( obj ) { - var slice = [].slice - , args = slice.call(arguments, 1) - , self = this - , nop = function () {} - , bound = function () { - return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments))) - } - nop.prototype = self.prototype - bound.prototype = new nop() - return bound - } - } - - // public methods - return { - - valid: function() { return !!(sqlitePlugin.openDatabase) }, - - init: function (options, callback) { - var that = this - , cb = that.fn(that.name, callback) - , dbname = options.db || this.name - , bgType = options.bgType || 1 - , create = "CREATE TABLE IF NOT EXISTS " + this.name + " (id NVARCHAR(32) UNIQUE PRIMARY KEY, value TEXT, timestamp REAL)" - , win = function(){ return cb.call(that, that); } - // open a connection and create the db if it doesn't exist - this.db = sqlitePlugin.openDatabase({name:dbname,bgType:bgType}) - this.db.transaction(function (t) { - t.executeSql(create, [], win, fail) - }) - }, - - keys: function (callback) { - var cb = this.lambda(callback) - , that = this - , keys = "SELECT id FROM " + this.name + " ORDER BY timestamp DESC" - - this.db.transaction(function(t) { - var win = function (xxx, results) { - if (results.rows.length == 0 ) { - cb.call(that, []) - } else { - var r = []; - for (var i = 0, l = results.rows.length; i < l; i++) { - r.push(results.rows.item(i).id); - } - cb.call(that, r) - } - } - t.executeSql(keys, [], win, fail) - }) - return this - }, - // you think thats air you're breathing now? - save: function (obj, callback) { - var that = this - , id = obj.key || that.uuid() - , ins = "INSERT INTO " + this.name + " (value, timestamp, id) VALUES (?,?,?)" - , up = "UPDATE " + this.name + " SET value=?, timestamp=? WHERE id=?" - , win = function () { if (callback) { obj.key = id; that.lambda(callback).call(that, obj) }} - , val = [now(), id] - // existential - that.exists(obj.key, function(exists) { - // transactions are like condoms - that.db.transaction(function(t) { - // TODO move timestamp to a plugin - var insert = function (obj) { - val.unshift(JSON.stringify(obj)) - t.executeSql(ins, val, win, fail) - } - // TODO move timestamp to a plugin - var update = function (obj) { - delete(obj.key) - val.unshift(JSON.stringify(obj)) - t.executeSql(up, val, win, fail) - } - // pretty - exists ? update(obj) : insert(obj) - }) - }); - return this - }, - - // FIXME this should be a batch insert / just getting the test to pass... - batch: function (objs, cb) { - - var results = [] - , done = false - , that = this - - var updateProgress = function(obj) { - results.push(obj) - done = results.length === objs.length - } - - var checkProgress = setInterval(function() { - if (done) { - if (cb) that.lambda(cb).call(that, results) - clearInterval(checkProgress) - } - }, 200) - - for (var i = 0, l = objs.length; i < l; i++) - this.save(objs[i], updateProgress) - - return this - }, - - get: function (keyOrArray, cb) { - var that = this - , sql = '' - // batch selects support - if (this.isArray(keyOrArray)) { - sql = 'SELECT id, value FROM ' + this.name + " WHERE id IN ('" + keyOrArray.join("','") + "')" - } else { - sql = 'SELECT id, value FROM ' + this.name + " WHERE id = '" + keyOrArray + "'" - } - // FIXME - // will always loop the results but cleans it up if not a batch return at the end.. - // in other words, this could be faster - var win = function (xxx, results) { - var o = null - , r = [] - if (results.rows.length) { - for (var i = 0, l = results.rows.length; i < l; i++) { - o = JSON.parse(results.rows.item(i).value) - o.key = results.rows.item(i).id - r.push(o) - } - } - if (!that.isArray(keyOrArray)) r = r.length ? r[0] : null - if (cb) that.lambda(cb).call(that, r) - } - this.db.transaction(function(t){ t.executeSql(sql, [], win, fail) }) - return this - }, - - exists: function (key, cb) { - var is = "SELECT * FROM " + this.name + " WHERE id = ?" - , that = this - , win = function(xxx, results) { if (cb) that.fn('exists', cb).call(that, (results.rows.length > 0)) } - this.db.transaction(function(t){ t.executeSql(is, [key], win, fail) }) - return this - }, - - all: function (callback) { - var that = this - , all = "SELECT * FROM " + this.name - , r = [] - , cb = this.fn(this.name, callback) || undefined - , win = function (xxx, results) { - if (results.rows.length != 0) { - for (var i = 0, l = results.rows.length; i < l; i++) { - var obj = JSON.parse(results.rows.item(i).value) - obj.key = results.rows.item(i).id - r.push(obj) - } - } - if (cb) cb.call(that, r) - } - - this.db.transaction(function (t) { - t.executeSql(all, [], win, fail) - }) - return this - }, - - remove: function (keyOrObj, cb) { - var that = this - , key = typeof keyOrObj === 'string' ? keyOrObj : keyOrObj.key - , del = "DELETE FROM " + this.name + " WHERE id = ?" - , win = function () { if (cb) that.lambda(cb).call(that) } - - this.db.transaction( function (t) { - t.executeSql(del, [key], win, fail); - }); - - return this; - }, - - nuke: function (cb) { - var nuke = "DELETE FROM " + this.name - , that = this - , win = cb ? function() { that.lambda(cb).call(that) } : function(){} - this.db.transaction(function (t) { - t.executeSql(nuke, [], win, fail) - }) - return this - } -////// -}})()) diff --git a/Lawnchair-adapter/test-www/index.html b/Lawnchair-adapter/test-www/index.html deleted file mode 100644 index f07373d16..000000000 --- a/Lawnchair-adapter/test-www/index.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - Lawnchair Spec - - - - - - - - - - - - - - - - - - - -

Lawnchair Spec

-

-

-
    - - diff --git a/Lawnchair-adapter/test-www/lawnchair-spec.js b/Lawnchair-adapter/test-www/lawnchair-spec.js deleted file mode 100755 index 7b373ac36..000000000 --- a/Lawnchair-adapter/test-www/lawnchair-spec.js +++ /dev/null @@ -1,431 +0,0 @@ -module('Lawnchair construction/destruction', { - setup:function() { - }, - teardown:function() { - } -}); - -test('ctor requires callbacks in each form', function() { - QUnit.stop(); - QUnit.expect(6); - - // raise exception if no ctor callback is supplied - try { - var lc2 = new Lawnchair(); - } catch(e) { - ok(true, 'exception raised if no callback supplied to init'); - } - try { - var lc3 = new Lawnchair({}, {}); - } catch(e) { - ok(true, 'exception raised if no callback supplied to init, but two args are present'); - } - try { - var lc3 = new Lawnchair({}); - } catch(e) { - ok(true, 'exception raised if no callback supplied to init, but one arg is present'); - } - - var lc = new Lawnchair({name:store.name}, function(ref) { - ok(true, 'should call passed in callback when using obj+function ctor form') - equals(this, ref, "lawnchair callback scoped to lawnchair instance") - equals(ref, this, "lawnchair passes self into callback too") - QUnit.start() - }); -}); - -/** NOTE: may cause a failure due to difference in SQLitePlugin database initialization. -test('independent data stores', function() { - - var store1 = new Lawnchair({name: "store1"}, function() {}); - - store1 .save({key: 'apple', quantity: 3}, function() { - - var store2 = new Lawnchair({name: "store2"}, function() {}); - - store1.all(function(r) { - equals(r.length, 1); - }); - - store2.all(function(r) { - equals(r.length, 0); - }); - - }) - - -}) -**/ - -module('all()', { - setup:function() { - QUnit.stop(); - - // I like to make all my variables globals. Starting a new trend. - me = {name:'brian', age:30}; - store.nuke(function() { QUnit.start(); }); - }, - teardown:function() { - me = null; - } -}) - -test('chainable', function() { - QUnit.stop(); - QUnit.expect(1); - - same(store.all(function(r) { QUnit.start(); }), store, 'should be chainable (return itself)'); -}) - -test('full callback syntax', function() { - QUnit.stop(); - QUnit.expect(4); - - store.all(function(r) { - ok(true, 'calls callback'); - ok(r instanceof Array, 'should provide array as parameter'); - equals(r.length, 0, 'parameter should initially have zero length'); - same(this, store, '"this" should be scoped to the lawnchair object inside callback'); - QUnit.start(); - }); -}) - -test('adding, nuking and size tests', function() { - QUnit.stop(); - QUnit.expect(2); - - store.save(me, function() { - store.all(function(r) { - equals(r.length, 1, 'parameter should have length 1 after saving a single record'); - store.nuke(function() { - store.all(function(r) { - equals(r.length, 0, 'parameter should have length 0 after nuking'); - QUnit.start(); - }); - }); - }); - }); -}) - -test( 'shorthand callback syntax', function() { - QUnit.stop(); - QUnit.expect(2); - - store.all('ok(true, "shorthand syntax callback gets evaled"); same(this, store, "`this` should be scoped to the Lawnchair instance"); QUnit.start();'); - - // Is this test block necessary? - // - // var tmp = new Lawnchair({name:'temps', record:'tmp'}, function(){ - // QUnit.start() - // var Temps = this; - // equals(this, Temps, 'this is bound to Lawnchair') - // QUnit.stop() - // Temps.all('ok(temps, "this.name is passed to all callback"); QUnit.start()') - // }) -}) - -/** TBD issue with Android: -test('scoped variable in shorthand callback', function() { - QUnit.expect(1); - QUnit.stop(); - - // FIXME fkn qunit being weird here... expect(1) - var tmp = new Lawnchair({name:'temps', record:'tmp'}, function() { - this.nuke(function() { - this.save({a:1}, function() { - this.each('ok(tmp, "this.record is passed to each callback"); QUnit.start()') - }) - }) - }) -}) -**/ - -module('nuke()', { - setup:function() { - QUnit.stop(); - store.nuke(function() { - QUnit.start() - }); - }, - teardown:function() { - } -}) - -test( 'chainable', function() { - QUnit.expect(1); - QUnit.stop() - - same(store.nuke(function() { QUnit.start() }), store, 'should be chainable'); -}) - -test( 'full callback syntax', function() { - QUnit.stop(); - QUnit.expect(2); - - store.nuke(function() { - ok(true, "should call callback in nuke"); - same(this, store, '"this" should be scoped to the Lawnchair instance'); - QUnit.start(); - }); -}) - -test( 'shorthand callback syntax', function() { - QUnit.stop(); - QUnit.expect(2); - - store.nuke('ok(true, "shorthand syntax callback gets evaled"); same(this, store, "`this` should be scoped to the Lawnchair instance"); QUnit.start();'); -}) - -module('save()', { - setup:function() { - QUnit.stop(); - - // I like to make all my variables globals. Starting a new trend. - me = {name:'brian', age:30}; - store.nuke(function() { QUnit.start(); }); - }, - teardown:function() { - me = null; - } -}) - -test( 'chainable', function() { - QUnit.stop(); - QUnit.expect(1); - - same(store.save(me, function() { QUnit.start(); }), store, 'should be chainable'); -}) - -test( 'full callback syntax', function() { - QUnit.stop(); - QUnit.expect(2); - - store.save(me, function(it) { - ok(true, 'should call passed in callback'); - same(it, me, 'should pass in original saved object in callback'); - QUnit.start(); - }); -}) - -test( 'shorthand callback syntax', function() { - QUnit.stop(); - QUnit.expect(2); - - store.save(me, 'ok(true, "shorthand syntax callback gets evaled"); same(this, store, "`this` should be scoped to the Lawnchair instance"); QUnit.start();'); -}) - -test( 'saving objects', function() { - QUnit.stop(); - QUnit.expect(1); - - store.save(me, function() { - store.save({key:"something", value:"else"}, function(r) { - store.all(function(r) { - equals(r.length, 2, 'after saving two keys, num. records should equal to 2'); - QUnit.start(); - }); - }); - }) -}) - -test( 'save without callback', function() { - - QUnit.stop(); - QUnit.expect(1); - - store.save(me, function(obj) { - var key = obj.key; - store.save(obj); - equals(obj.key, key, "save without callback retains key"); - QUnit.start(); - }) - -}); - -module('batch()', { - setup:function() { - QUnit.stop(); - - // I like to make all my variables globals. Starting a new trend. - me = {name:'brian', age:30}; - store.nuke(function() { QUnit.start(); }); - }, - teardown:function() { - me = null; - } -}) - -test('batch insertion', function(){ - QUnit.expect(3); - QUnit.stop(); - - ok(store.batch, 'batch implemented'); - equals(store.batch([]), store, 'chainable') - - store.batch([{i:1},{i:2}], function() { - store.all(function(r){ - equals(r.length, 2, 'should be two records from batch insert with array of two objects'); - QUnit.start(); - }); - }); -}) - -test( 'full callback syntax', function() { - QUnit.stop(1500); // timing changed by batch processing improvements - QUnit.expect(2); - - store.batch([{j:'k'}], function() { - ok(true, 'callback called with full syntax'); - same(this, store, '"this" should be the LAwnchair instance'); - QUnit.start(); - }) -}) - -test( 'shorthand callback syntax', function() { - QUnit.stop(1500); // timing changed by batch processing improvements - QUnit.expect(2); - - store.batch([{o:'k'}], 'ok(true, "shorthand syntax callback gets evaled"); same(this, store, "`this` should be scoped to the Lawnchair instance"); QUnit.start();') -}) - -module('get()', { - setup:function() { - QUnit.stop(); - - // I like to make all my variables globals. Starting a new trend. - me = {name:'brian', age:30}; - store.nuke(function() { QUnit.start(); }); - }, - teardown:function() { - me = null; - } -}); - -test( 'should it be chainable?', function() { - QUnit.expect(1); - QUnit.stop(); - - equals(store.get('foo', function() { QUnit.start(); }), store, 'get chainable'); -}); - -test('get functionality', function() { - QUnit.expect(4); - QUnit.stop(); - - store.save({key:'xyz', name:'tim'}, function() { - store.get('xyz', function(r) { - equals(r.key, 'xyz', 'should return key in loaded object'); - equals(r.name, 'tim', 'should return proper object when calling get with a key'); - store.get('doesntexist', function(s) { - ok(true, 'should call callback even for non-existent key'); - equals(s, null, 'should return null for non-existent key'); - QUnit.start(); - }); - }); - }); -}); - -test('get batch functionality', function() { - QUnit.expect(3); - QUnit.stop(1500); // timing changed by batch processing improvements - - var t = [{key:'test-get'},{key:'test-get-1'}] - store.batch(t, function() { - this.get(['test-get','test-get-1'], function(r) { - equals(r[0].key, 'test-get', "get first object"); - equals(r[1].key, 'test-get-1', "get second object"); - equals(r.length, t.length, "should batch get") - QUnit.start() - }) - }) -}); - -test( 'full callback syntax', function() { - QUnit.stop(); - QUnit.expect(2); - - store.get('somekey', function(r){ - ok(true, 'callback got called'); - same(this, store, '"this" should be teh Lawnchair instance'); - QUnit.start(); - }); -}); - -test('short callback syntax', function() { - QUnit.stop(); - QUnit.expect(2); - - store.get('somekey', 'ok(true, "shorthand syntax callback gets evaled"); same(this, store, "`this` should be scoped to the Lawnchair instance"); QUnit.start();'); -}); - -module('remove()', { - setup:function() { - QUnit.stop(); - - // I like to make all my variables globals. Starting a new trend. - me = {name:'brian', age:30}; - store.nuke(function() { QUnit.start(); }); - }, - teardown:function() { - me = null; - } -}); - - -test( 'chainable', function() { - QUnit.expect(1); - QUnit.stop(); - - store.save({key:'me', name:'brian'}, function() { - same(store.remove('me', function() { - QUnit.start(); - }), store, 'should be chainable'); - - }); -}); - -test( 'full callback syntax', function() { - QUnit.stop(); - QUnit.expect(2); - - store.save({key:'somekey', name:'something'}, function() { - store.remove('somekey', function(r){ - ok(true, 'callback got called'); - same(this, store, '"this" should be teh Lawnchair instance'); - QUnit.start(); - }); - }); -}); - -test('short callback syntax', function() { - QUnit.stop(); - QUnit.expect(2); - - store.save({key:'somekey', name:'something'}, function() { - store.remove('somekey', 'ok(true, "shorthand syntax callback gets evaled"); same(this, store, "`this` should be scoped to the Lawnchair instance"); QUnit.start();'); - }); -}); - -// FIXME need to add tests for batch deletion -test( 'remove functionality', function() { - QUnit.stop(); - QUnit.expect(2); - - store.save({name:'joni'}, function(r) { - //store.find("r.name == 'joni'", function(r){ - store.remove(r, function(r) { - store.all(function(all) { - equals(all.length, 0, "should have length 0 after saving, finding, and removing a record using entire object"); - store.save({key:'die', name:'dudeman'}, function(r) { - store.remove('die', function(r){ - store.all(function(rec) { - equals(rec.length, 0, "should have length 0 after saving and removing by string key"); - QUnit.start(); - }); - }); - }); - }); - }); - //}); - }); -}); diff --git a/Lawnchair-adapter/test-www/lib/json2.js b/Lawnchair-adapter/test-www/lib/json2.js deleted file mode 100644 index a1a3b170c..000000000 --- a/Lawnchair-adapter/test-www/lib/json2.js +++ /dev/null @@ -1,482 +0,0 @@ -/* - http://www.JSON.org/json2.js - 2010-03-20 - - Public Domain. - - NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. - - See http://www.JSON.org/js.html - - - This code should be minified before deployment. - See http://javascript.crockford.com/jsmin.html - - USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO - NOT CONTROL. - - - This file creates a global JSON object containing two methods: stringify - and parse. - - JSON.stringify(value, replacer, space) - value any JavaScript value, usually an object or array. - - replacer an optional parameter that determines how object - values are stringified for objects. It can be a - function or an array of strings. - - space an optional parameter that specifies the indentation - of nested structures. If it is omitted, the text will - be packed without extra whitespace. If it is a number, - it will specify the number of spaces to indent at each - level. If it is a string (such as '\t' or ' '), - it contains the characters used to indent at each level. - - This method produces a JSON text from a JavaScript value. - - When an object value is found, if the object contains a toJSON - method, its toJSON method will be called and the result will be - stringified. A toJSON method does not serialize: it returns the - value represented by the name/value pair that should be serialized, - or undefined if nothing should be serialized. The toJSON method - will be passed the key associated with the value, and this will be - bound to the value - - For example, this would serialize Dates as ISO strings. - - Date.prototype.toJSON = function (key) { - function f(n) { - // Format integers to have at least two digits. - return n < 10 ? '0' + n : n; - } - - return this.getUTCFullYear() + '-' + - f(this.getUTCMonth() + 1) + '-' + - f(this.getUTCDate()) + 'T' + - f(this.getUTCHours()) + ':' + - f(this.getUTCMinutes()) + ':' + - f(this.getUTCSeconds()) + 'Z'; - }; - - You can provide an optional replacer method. It will be passed the - key and value of each member, with this bound to the containing - object. The value that is returned from your method will be - serialized. If your method returns undefined, then the member will - be excluded from the serialization. - - If the replacer parameter is an array of strings, then it will be - used to select the members to be serialized. It filters the results - such that only members with keys listed in the replacer array are - stringified. - - Values that do not have JSON representations, such as undefined or - functions, will not be serialized. Such values in objects will be - dropped; in arrays they will be replaced with null. You can use - a replacer function to replace those with JSON values. - JSON.stringify(undefined) returns undefined. - - The optional space parameter produces a stringification of the - value that is filled with line breaks and indentation to make it - easier to read. - - If the space parameter is a non-empty string, then that string will - be used for indentation. If the space parameter is a number, then - the indentation will be that many spaces. - - Example: - - text = JSON.stringify(['e', {pluribus: 'unum'}]); - // text is '["e",{"pluribus":"unum"}]' - - - text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); - // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' - - text = JSON.stringify([new Date()], function (key, value) { - return this[key] instanceof Date ? - 'Date(' + this[key] + ')' : value; - }); - // text is '["Date(---current time---)"]' - - - JSON.parse(text, reviver) - This method parses a JSON text to produce an object or array. - It can throw a SyntaxError exception. - - The optional reviver parameter is a function that can filter and - transform the results. It receives each of the keys and values, - and its return value is used instead of the original value. - If it returns what it received, then the structure is not modified. - If it returns undefined then the member is deleted. - - Example: - - // Parse the text. Values that look like ISO date strings will - // be converted to Date objects. - - myData = JSON.parse(text, function (key, value) { - var a; - if (typeof value === 'string') { - a = -/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); - if (a) { - return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], - +a[5], +a[6])); - } - } - return value; - }); - - myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { - var d; - if (typeof value === 'string' && - value.slice(0, 5) === 'Date(' && - value.slice(-1) === ')') { - d = new Date(value.slice(5, -1)); - if (d) { - return d; - } - } - return value; - }); - - - This is a reference implementation. You are free to copy, modify, or - redistribute. -*/ - -/*jslint evil: true, strict: false */ - -/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, - call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, - getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, - lastIndex, length, parse, prototype, push, replace, slice, stringify, - test, toJSON, toString, valueOf -*/ - - -// Create a JSON object only if one does not already exist. We create the -// methods in a closure to avoid creating global variables. - -if (!this.JSON) { - this.JSON = {}; -} - -(function () { - - function f(n) { - // Format integers to have at least two digits. - return n < 10 ? '0' + n : n; - } - - if (typeof Date.prototype.toJSON !== 'function') { - - Date.prototype.toJSON = function (key) { - - return isFinite(this.valueOf()) ? - this.getUTCFullYear() + '-' + - f(this.getUTCMonth() + 1) + '-' + - f(this.getUTCDate()) + 'T' + - f(this.getUTCHours()) + ':' + - f(this.getUTCMinutes()) + ':' + - f(this.getUTCSeconds()) + 'Z' : null; - }; - - String.prototype.toJSON = - Number.prototype.toJSON = - Boolean.prototype.toJSON = function (key) { - return this.valueOf(); - }; - } - - var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - gap, - indent, - meta = { // table of character substitutions - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"' : '\\"', - '\\': '\\\\' - }, - rep; - - - function quote(string) { - -// If the string contains no control characters, no quote characters, and no -// backslash characters, then we can safely slap some quotes around it. -// Otherwise we must also replace the offending characters with safe escape -// sequences. - - escapable.lastIndex = 0; - return escapable.test(string) ? - '"' + string.replace(escapable, function (a) { - var c = meta[a]; - return typeof c === 'string' ? c : - '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }) + '"' : - '"' + string + '"'; - } - - - function str(key, holder) { - -// Produce a string from holder[key]. - - var i, // The loop counter. - k, // The member key. - v, // The member value. - length, - mind = gap, - partial, - value = holder[key]; - -// If the value has a toJSON method, call it to obtain a replacement value. - - if (value && typeof value === 'object' && - typeof value.toJSON === 'function') { - value = value.toJSON(key); - } - -// If we were called with a replacer function, then call the replacer to -// obtain a replacement value. - - if (typeof rep === 'function') { - value = rep.call(holder, key, value); - } - -// What happens next depends on the value's type. - - switch (typeof value) { - case 'string': - return quote(value); - - case 'number': - -// JSON numbers must be finite. Encode non-finite numbers as null. - - return isFinite(value) ? String(value) : 'null'; - - case 'boolean': - case 'null': - -// If the value is a boolean or null, convert it to a string. Note: -// typeof null does not produce 'null'. The case is included here in -// the remote chance that this gets fixed someday. - - return String(value); - -// If the type is 'object', we might be dealing with an object or an array or -// null. - - case 'object': - -// Due to a specification blunder in ECMAScript, typeof null is 'object', -// so watch out for that case. - - if (!value) { - return 'null'; - } - -// Make an array to hold the partial results of stringifying this object value. - - gap += indent; - partial = []; - -// Is the value an array? - - if (Object.prototype.toString.apply(value) === '[object Array]') { - -// The value is an array. Stringify every element. Use null as a placeholder -// for non-JSON values. - - length = value.length; - for (i = 0; i < length; i += 1) { - partial[i] = str(i, value) || 'null'; - } - -// Join all of the elements together, separated with commas, and wrap them in -// brackets. - - v = partial.length === 0 ? '[]' : - gap ? '[\n' + gap + - partial.join(',\n' + gap) + '\n' + - mind + ']' : - '[' + partial.join(',') + ']'; - gap = mind; - return v; - } - -// If the replacer is an array, use it to select the members to be stringified. - - if (rep && typeof rep === 'object') { - length = rep.length; - for (i = 0; i < length; i += 1) { - k = rep[i]; - if (typeof k === 'string') { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } else { - -// Otherwise, iterate through all of the keys in the object. - - for (k in value) { - if (Object.hasOwnProperty.call(value, k)) { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } - -// Join all of the member texts together, separated with commas, -// and wrap them in braces. - - v = partial.length === 0 ? '{}' : - gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + - mind + '}' : '{' + partial.join(',') + '}'; - gap = mind; - return v; - } - } - -// If the JSON object does not yet have a stringify method, give it one. - - if (typeof JSON.stringify !== 'function') { - JSON.stringify = function (value, replacer, space) { - -// The stringify method takes a value and an optional replacer, and an optional -// space parameter, and returns a JSON text. The replacer can be a function -// that can replace values, or an array of strings that will select the keys. -// A default replacer method can be provided. Use of the space parameter can -// produce text that is more easily readable. - - var i; - gap = ''; - indent = ''; - -// If the space parameter is a number, make an indent string containing that -// many spaces. - - if (typeof space === 'number') { - for (i = 0; i < space; i += 1) { - indent += ' '; - } - -// If the space parameter is a string, it will be used as the indent string. - - } else if (typeof space === 'string') { - indent = space; - } - -// If there is a replacer, it must be a function or an array. -// Otherwise, throw an error. - - rep = replacer; - if (replacer && typeof replacer !== 'function' && - (typeof replacer !== 'object' || - typeof replacer.length !== 'number')) { - throw new Error('JSON.stringify'); - } - -// Make a fake root object containing our value under the key of ''. -// Return the result of stringifying the value. - - return str('', {'': value}); - }; - } - - -// If the JSON object does not yet have a parse method, give it one. - - if (typeof JSON.parse !== 'function') { - JSON.parse = function (text, reviver) { - -// The parse method takes a text and an optional reviver function, and returns -// a JavaScript value if the text is a valid JSON text. - - var j; - - function walk(holder, key) { - -// The walk method is used to recursively walk the resulting structure so -// that modifications can be made. - - var k, v, value = holder[key]; - if (value && typeof value === 'object') { - for (k in value) { - if (Object.hasOwnProperty.call(value, k)) { - v = walk(value, k); - if (v !== undefined) { - value[k] = v; - } else { - delete value[k]; - } - } - } - } - return reviver.call(holder, key, value); - } - - -// Parsing happens in four stages. In the first stage, we replace certain -// Unicode characters with escape sequences. JavaScript handles many characters -// incorrectly, either silently deleting them, or treating them as line endings. - - text = String(text); - cx.lastIndex = 0; - if (cx.test(text)) { - text = text.replace(cx, function (a) { - return '\\u' + - ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }); - } - -// In the second stage, we run the text against regular expressions that look -// for non-JSON patterns. We are especially concerned with '()' and 'new' -// because they can cause invocation, and '=' because it can cause mutation. -// But just to be safe, we want to reject all unexpected forms. - -// We split the second stage into 4 regexp operations in order to work around -// crippling inefficiencies in IE's and Safari's regexp engines. First we -// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we -// replace all simple value tokens with ']' characters. Third, we delete all -// open brackets that follow a colon or comma or that begin the text. Finally, -// we look to see that the remaining characters are only whitespace or ']' or -// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. - - if (/^[\],:{}\s]*$/. -test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). -replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). -replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { - -// In the third stage we use the eval function to compile the text into a -// JavaScript structure. The '{' operator is subject to a syntactic ambiguity -// in JavaScript: it can begin a block or an object literal. We wrap the text -// in parens to eliminate the ambiguity. - - j = eval('(' + text + ')'); - -// In the optional fourth stage, we recursively walk the new structure, passing -// each name/value pair to a reviver function for possible transformation. - - return typeof reviver === 'function' ? - walk({'': j}, '') : j; - } - -// If the text is not JSON parseable, then a SyntaxError is thrown. - - throw new SyntaxError('JSON.parse'); - }; - } -}()); diff --git a/Lawnchair-adapter/test-www/lib/lawnchair.js b/Lawnchair-adapter/test-www/lib/lawnchair.js deleted file mode 100644 index 3f67835af..000000000 --- a/Lawnchair-adapter/test-www/lib/lawnchair.js +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Lawnchair! - * --- - * clientside json store - * - */ -var Lawnchair = function (options, callback) { - // ensure Lawnchair was called as a constructor - if (!(this instanceof Lawnchair)) return new Lawnchair(options, callback); - - // lawnchair requires json - if (!JSON) throw 'JSON unavailable! Include http://www.json.org/json2.js to fix.' - // options are optional; callback is not - if (arguments.length <= 2 && arguments.length > 0) { - callback = (typeof arguments[0] === 'function') ? arguments[0] : arguments[1]; - options = (typeof arguments[0] === 'function') ? {} : arguments[0]; - } else { - throw 'Incorrect # of ctor args!' - } - // TODO perhaps allow for pub/sub instead? - if (typeof callback !== 'function') throw 'No callback was provided'; - - // default configuration - this.record = options.record || 'record' // default for records - this.name = options.name || 'records' // default name for underlying store - - // mixin first valid adapter - var adapter - // if the adapter is passed in we try to load that only - if (options.adapter) { - for (var i = 0, l = Lawnchair.adapters.length; i < l; i++) { - if (Lawnchair.adapters[i].adapter === options.adapter) { - adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined; - break; - } - } - // otherwise find the first valid adapter for this env - } - else { - for (var i = 0, l = Lawnchair.adapters.length; i < l; i++) { - adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined - if (adapter) break - } - } - - // we have failed - if (!adapter) throw 'No valid adapter.' - - // yay! mixin the adapter - for (var j in adapter) - this[j] = adapter[j] - - // call init for each mixed in plugin - for (var i = 0, l = Lawnchair.plugins.length; i < l; i++) - Lawnchair.plugins[i].call(this) - - // init the adapter - this.init(options, callback) -} - -Lawnchair.adapters = [] - -/** - * queues an adapter for mixin - * === - * - ensures an adapter conforms to a specific interface - * - */ -Lawnchair.adapter = function (id, obj) { - // add the adapter id to the adapter obj - // ugly here for a cleaner dsl for implementing adapters - obj['adapter'] = id - // methods required to implement a lawnchair adapter - var implementing = 'adapter valid init keys save batch get exists all remove nuke'.split(' ') - , indexOf = this.prototype.indexOf - // mix in the adapter - for (var i in obj) { - if (indexOf(implementing, i) === -1) throw 'Invalid adapter! Nonstandard method: ' + i - } - // if we made it this far the adapter interface is valid - // insert the new adapter as the preferred adapter - Lawnchair.adapters.splice(0,0,obj) -} - -Lawnchair.plugins = [] - -/** - * generic shallow extension for plugins - * === - * - if an init method is found it registers it to be called when the lawnchair is inited - * - yes we could use hasOwnProp but nobody here is an asshole - */ -Lawnchair.plugin = function (obj) { - for (var i in obj) - i === 'init' ? Lawnchair.plugins.push(obj[i]) : this.prototype[i] = obj[i] -} - -/** - * helpers - * - */ -Lawnchair.prototype = { - - isArray: Array.isArray || function(o) { return Object.prototype.toString.call(o) === '[object Array]' }, - - /** - * this code exists for ie8... for more background see: - * http://www.flickr.com/photos/westcoastlogic/5955365742/in/photostream - */ - indexOf: function(ary, item, i, l) { - if (ary.indexOf) return ary.indexOf(item) - for (i = 0, l = ary.length; i < l; i++) if (ary[i] === item) return i - return -1 - }, - - // awesome shorthand callbacks as strings. this is shameless theft from dojo. - lambda: function (callback) { - return this.fn(this.record, callback) - }, - - // first stab at named parameters for terse callbacks; dojo: first != best // ;D - fn: function (name, callback) { - return typeof callback == 'string' ? new Function(name, callback) : callback - }, - - // returns a unique identifier (by way of Backbone.localStorage.js) - // TODO investigate smaller UUIDs to cut on storage cost - uuid: function () { - var S4 = function () { - return (((1+Math.random())*0x10000)|0).toString(16).substring(1); - } - return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); - }, - - // a classic iterator - each: function (callback) { - var cb = this.lambda(callback) - // iterate from chain - if (this.__results) { - for (var i = 0, l = this.__results.length; i < l; i++) cb.call(this, this.__results[i], i) - } - // otherwise iterate the entire collection - else { - this.all(function(r) { - for (var i = 0, l = r.length; i < l; i++) cb.call(this, r[i], i) - }) - } - return this - } -// -- -}; diff --git a/Lawnchair-adapter/test-www/lib/qunit.css b/Lawnchair-adapter/test-www/lib/qunit.css deleted file mode 100644 index b3c6db523..000000000 --- a/Lawnchair-adapter/test-www/lib/qunit.css +++ /dev/null @@ -1,225 +0,0 @@ -/** - * QUnit - A JavaScript Unit Testing Framework - * - * http://docs.jquery.com/QUnit - * - * Copyright (c) 2011 John Resig, Jörn Zaefferer - * Dual licensed under the MIT (MIT-LICENSE.txt) - * or GPL (GPL-LICENSE.txt) licenses. - */ - -/** Font Family and Sizes */ - -#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { - font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; -} - -#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } -#qunit-tests { font-size: smaller; } - - -/** Resets */ - -#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { - margin: 0; - padding: 0; -} - - -/** Header */ - -#qunit-header { - padding: 0.5em 0 0.5em 1em; - - color: #8699a4; - background-color: #0d3349; - - font-size: 1.5em; - line-height: 1em; - font-weight: normal; - - border-radius: 15px 15px 0 0; - -moz-border-radius: 15px 15px 0 0; - -webkit-border-top-right-radius: 15px; - -webkit-border-top-left-radius: 15px; -} - -#qunit-header a { - text-decoration: none; - color: #c2ccd1; -} - -#qunit-header a:hover, -#qunit-header a:focus { - color: #fff; -} - -#qunit-banner { - height: 5px; -} - -#qunit-testrunner-toolbar { - padding: 0.5em 0 0.5em 2em; - color: #5E740B; - background-color: #eee; -} - -#qunit-userAgent { - padding: 0.5em 0 0.5em 2.5em; - background-color: #2b81af; - color: #fff; - text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; -} - - -/** Tests: Pass/Fail */ - -#qunit-tests { - list-style-position: inside; -} - -#qunit-tests li { - padding: 0.4em 0.5em 0.4em 2.5em; - border-bottom: 1px solid #fff; - list-style-position: inside; -} - -#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { - display: none; -} - -#qunit-tests li strong { - cursor: pointer; -} - -#qunit-tests li a { - padding: 0.5em; - color: #c2ccd1; - text-decoration: none; -} -#qunit-tests li a:hover, -#qunit-tests li a:focus { - color: #000; -} - -#qunit-tests ol { - margin-top: 0.5em; - padding: 0.5em; - - background-color: #fff; - - border-radius: 15px; - -moz-border-radius: 15px; - -webkit-border-radius: 15px; - - box-shadow: inset 0px 2px 13px #999; - -moz-box-shadow: inset 0px 2px 13px #999; - -webkit-box-shadow: inset 0px 2px 13px #999; -} - -#qunit-tests table { - border-collapse: collapse; - margin-top: .2em; -} - -#qunit-tests th { - text-align: right; - vertical-align: top; - padding: 0 .5em 0 0; -} - -#qunit-tests td { - vertical-align: top; -} - -#qunit-tests pre { - margin: 0; - white-space: pre-wrap; - word-wrap: break-word; -} - -#qunit-tests del { - background-color: #e0f2be; - color: #374e0c; - text-decoration: none; -} - -#qunit-tests ins { - background-color: #ffcaca; - color: #500; - text-decoration: none; -} - -/*** Test Counts */ - -#qunit-tests b.counts { color: black; } -#qunit-tests b.passed { color: #5E740B; } -#qunit-tests b.failed { color: #710909; } - -#qunit-tests li li { - margin: 0.5em; - padding: 0.4em 0.5em 0.4em 0.5em; - background-color: #fff; - border-bottom: none; - list-style-position: inside; -} - -/*** Passing Styles */ - -#qunit-tests li li.pass { - color: #5E740B; - background-color: #fff; - border-left: 26px solid #C6E746; -} - -#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } -#qunit-tests .pass .test-name { color: #366097; } - -#qunit-tests .pass .test-actual, -#qunit-tests .pass .test-expected { color: #999999; } - -#qunit-banner.qunit-pass { background-color: #C6E746; } - -/*** Failing Styles */ - -#qunit-tests li li.fail { - color: #710909; - background-color: #fff; - border-left: 26px solid #EE5757; -} - -#qunit-tests > li:last-child { - border-radius: 0 0 15px 15px; - -moz-border-radius: 0 0 15px 15px; - -webkit-border-bottom-right-radius: 15px; - -webkit-border-bottom-left-radius: 15px; -} - -#qunit-tests .fail { color: #000000; background-color: #EE5757; } -#qunit-tests .fail .test-name, -#qunit-tests .fail .module-name { color: #000000; } - -#qunit-tests .fail .test-actual { color: #EE5757; } -#qunit-tests .fail .test-expected { color: green; } - -#qunit-banner.qunit-fail { background-color: #EE5757; } - - -/** Result */ - -#qunit-testresult { - padding: 0.5em 0.5em 0.5em 2.5em; - - color: #2b81af; - background-color: #D2E0E6; - - border-bottom: 1px solid white; -} - -/** Fixture */ - -#qunit-fixture { - position: absolute; - top: -10000px; - left: -10000px; -} diff --git a/Lawnchair-adapter/test-www/lib/qunit.js b/Lawnchair-adapter/test-www/lib/qunit.js deleted file mode 100644 index 3d640c73c..000000000 --- a/Lawnchair-adapter/test-www/lib/qunit.js +++ /dev/null @@ -1,1448 +0,0 @@ -/** - * QUnit - A JavaScript Unit Testing Framework - * - * http://docs.jquery.com/QUnit - * - * Copyright (c) 2011 John Resig, Jörn Zaefferer - * Dual licensed under the MIT (MIT-LICENSE.txt) - * or GPL (GPL-LICENSE.txt) licenses. - */ - -(function(window) { - -var defined = { - setTimeout: typeof window.setTimeout !== "undefined", - sessionStorage: (function() { - try { - return !!sessionStorage.getItem; - } catch(e){ - return false; - } - })() -}; - -var testId = 0; - -var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { - this.name = name; - this.testName = testName; - this.expected = expected; - this.testEnvironmentArg = testEnvironmentArg; - this.async = async; - this.callback = callback; - this.assertions = []; -}; -Test.prototype = { - init: function() { - var tests = id("qunit-tests"); - if (tests) { - var b = document.createElement("strong"); - b.innerHTML = "Running " + this.name; - var li = document.createElement("li"); - li.appendChild( b ); - li.className = "running"; - li.id = this.id = "test-output" + testId++; - tests.appendChild( li ); - } - }, - setup: function() { - if (this.module != config.previousModule) { - if ( config.previousModule ) { - QUnit.moduleDone( { - name: config.previousModule, - failed: config.moduleStats.bad, - passed: config.moduleStats.all - config.moduleStats.bad, - total: config.moduleStats.all - } ); - } - config.previousModule = this.module; - config.moduleStats = { all: 0, bad: 0 }; - QUnit.moduleStart( { - name: this.module - } ); - } - - config.current = this; - this.testEnvironment = extend({ - setup: function() {}, - teardown: function() {} - }, this.moduleTestEnvironment); - if (this.testEnvironmentArg) { - extend(this.testEnvironment, this.testEnvironmentArg); - } - - QUnit.testStart( { - name: this.testName - } ); - - // allow utility functions to access the current test environment - // TODO why?? - QUnit.current_testEnvironment = this.testEnvironment; - - try { - if ( !config.pollution ) { - saveGlobal(); - } - - this.testEnvironment.setup.call(this.testEnvironment); - } catch(e) { - QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); - } - }, - run: function() { - if ( this.async ) { - QUnit.stop(); - } - - if ( config.notrycatch ) { - this.callback.call(this.testEnvironment); - return; - } - try { - this.callback.call(this.testEnvironment); - } catch(e) { - fail("Test " + this.testName + " died, exception and test follows", e, this.callback); - QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); - // else next test will carry the responsibility - saveGlobal(); - - // Restart the tests if they're blocking - if ( config.blocking ) { - start(); - } - } - }, - teardown: function() { - try { - this.testEnvironment.teardown.call(this.testEnvironment); - checkPollution(); - } catch(e) { - QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); - } - }, - finish: function() { - if ( this.expected && this.expected != this.assertions.length ) { - QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); - } - - var good = 0, bad = 0, - tests = id("qunit-tests"); - - config.stats.all += this.assertions.length; - config.moduleStats.all += this.assertions.length; - - if ( tests ) { - var ol = document.createElement("ol"); - - for ( var i = 0; i < this.assertions.length; i++ ) { - var assertion = this.assertions[i]; - - var li = document.createElement("li"); - li.className = assertion.result ? "pass" : "fail"; - li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); - ol.appendChild( li ); - - if ( assertion.result ) { - good++; - } else { - bad++; - config.stats.bad++; - config.moduleStats.bad++; - } - } - - // store result when possible - if ( QUnit.config.reorder && defined.sessionStorage ) { - if (bad) { - sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad); - } else { - sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName); - } - } - - if (bad == 0) { - ol.style.display = "none"; - } - - var b = document.createElement("strong"); - b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; - - var a = document.createElement("a"); - a.innerHTML = "Rerun"; - a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); - - addEvent(b, "click", function() { - var next = b.nextSibling.nextSibling, - display = next.style.display; - next.style.display = display === "none" ? "block" : "none"; - }); - - addEvent(b, "dblclick", function(e) { - var target = e && e.target ? e.target : window.event.srcElement; - if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { - target = target.parentNode; - } - if ( window.location && target.nodeName.toLowerCase() === "strong" ) { - window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); - } - }); - - var li = id(this.id); - li.className = bad ? "fail" : "pass"; - li.removeChild( li.firstChild ); - li.appendChild( b ); - li.appendChild( a ); - li.appendChild( ol ); - - } else { - for ( var i = 0; i < this.assertions.length; i++ ) { - if ( !this.assertions[i].result ) { - bad++; - config.stats.bad++; - config.moduleStats.bad++; - } - } - } - - try { - QUnit.reset(); - } catch(e) { - fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); - } - - QUnit.testDone( { - name: this.testName, - failed: bad, - passed: this.assertions.length - bad, - total: this.assertions.length - } ); - }, - - queue: function() { - var test = this; - synchronize(function() { - test.init(); - }); - function run() { - // each of these can by async - synchronize(function() { - test.setup(); - }); - synchronize(function() { - test.run(); - }); - synchronize(function() { - test.teardown(); - }); - synchronize(function() { - test.finish(); - }); - } - // defer when previous test run passed, if storage is available - var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); - if (bad) { - run(); - } else { - synchronize(run); - }; - } - -}; - -var QUnit = { - - // call on start of module test to prepend name to all tests - module: function(name, testEnvironment) { - config.currentModule = name; - config.currentModuleTestEnviroment = testEnvironment; - }, - - asyncTest: function(testName, expected, callback) { - if ( arguments.length === 2 ) { - callback = expected; - expected = 0; - } - - QUnit.test(testName, expected, callback, true); - }, - - test: function(testName, expected, callback, async) { - var name = '' + testName + '', testEnvironmentArg; - - if ( arguments.length === 2 ) { - callback = expected; - expected = null; - } - // is 2nd argument a testEnvironment? - if ( expected && typeof expected === 'object') { - testEnvironmentArg = expected; - expected = null; - } - - if ( config.currentModule ) { - name = '' + config.currentModule + ": " + name; - } - - if ( !validTest(config.currentModule + ": " + testName) ) { - return; - } - - var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); - test.module = config.currentModule; - test.moduleTestEnvironment = config.currentModuleTestEnviroment; - test.queue(); - }, - - /** - * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. - */ - expect: function(asserts) { - config.current.expected = asserts; - }, - - /** - * Asserts true. - * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); - */ - ok: function(a, msg) { - a = !!a; - var details = { - result: a, - message: msg - }; - msg = escapeHtml(msg); - QUnit.log(details); - config.current.assertions.push({ - result: a, - message: msg - }); - }, - - /** - * Checks that the first two arguments are equal, with an optional message. - * Prints out both actual and expected values. - * - * Prefered to ok( actual == expected, message ) - * - * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); - * - * @param Object actual - * @param Object expected - * @param String message (optional) - */ - equal: function(actual, expected, message) { - QUnit.push(expected == actual, actual, expected, message); - }, - - notEqual: function(actual, expected, message) { - QUnit.push(expected != actual, actual, expected, message); - }, - - deepEqual: function(actual, expected, message) { - QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); - }, - - notDeepEqual: function(actual, expected, message) { - QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); - }, - - strictEqual: function(actual, expected, message) { - QUnit.push(expected === actual, actual, expected, message); - }, - - notStrictEqual: function(actual, expected, message) { - QUnit.push(expected !== actual, actual, expected, message); - }, - - raises: function(block, expected, message) { - var actual, ok = false; - - if (typeof expected === 'string') { - message = expected; - expected = null; - } - - try { - block(); - } catch (e) { - actual = e; - } - - if (actual) { - // we don't want to validate thrown error - if (!expected) { - ok = true; - // expected is a regexp - } else if (QUnit.objectType(expected) === "regexp") { - ok = expected.test(actual); - // expected is a constructor - } else if (actual instanceof expected) { - ok = true; - // expected is a validation function which returns true is validation passed - } else if (expected.call({}, actual) === true) { - ok = true; - } - } - - QUnit.ok(ok, message); - }, - - start: function() { - config.semaphore--; - if (config.semaphore > 0) { - // don't start until equal number of stop-calls - return; - } - if (config.semaphore < 0) { - // ignore if start is called more often then stop - config.semaphore = 0; - } - // A slight delay, to avoid any current callbacks - if ( defined.setTimeout ) { - window.setTimeout(function() { - if ( config.timeout ) { - clearTimeout(config.timeout); - } - - config.blocking = false; - process(); - }, 13); - } else { - config.blocking = false; - process(); - } - }, - - stop: function(timeout) { - config.semaphore++; - config.blocking = true; - - if ( timeout && defined.setTimeout ) { - clearTimeout(config.timeout); - config.timeout = window.setTimeout(function() { - QUnit.ok( false, "Test timed out" ); - QUnit.start(); - }, timeout); - } - } -}; - -// Backwards compatibility, deprecated -QUnit.equals = QUnit.equal; -QUnit.same = QUnit.deepEqual; - -// Maintain internal state -var config = { - // The queue of tests to run - queue: [], - - // block until document ready - blocking: true, - - // by default, run previously failed tests first - // very useful in combination with "Hide passed tests" checked - reorder: true, - - noglobals: false, - notrycatch: false -}; - -// Load paramaters -(function() { - var location = window.location || { search: "", protocol: "file:" }, - params = location.search.slice( 1 ).split( "&" ), - length = params.length, - urlParams = {}, - current; - - if ( params[ 0 ] ) { - for ( var i = 0; i < length; i++ ) { - current = params[ i ].split( "=" ); - current[ 0 ] = decodeURIComponent( current[ 0 ] ); - // allow just a key to turn on a flag, e.g., test.html?noglobals - current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; - urlParams[ current[ 0 ] ] = current[ 1 ]; - if ( current[ 0 ] in config ) { - config[ current[ 0 ] ] = current[ 1 ]; - } - } - } - - QUnit.urlParams = urlParams; - config.filter = urlParams.filter; - - // Figure out if we're running the tests from a server or not - QUnit.isLocal = !!(location.protocol === 'file:'); -})(); - -// Expose the API as global variables, unless an 'exports' -// object exists, in that case we assume we're in CommonJS -if ( typeof exports === "undefined" || typeof require === "undefined" ) { - extend(window, QUnit); - window.QUnit = QUnit; -} else { - extend(exports, QUnit); - exports.QUnit = QUnit; -} - -// define these after exposing globals to keep them in these QUnit namespace only -extend(QUnit, { - config: config, - - // Initialize the configuration options - init: function() { - extend(config, { - stats: { all: 0, bad: 0 }, - moduleStats: { all: 0, bad: 0 }, - started: +new Date, - updateRate: 1000, - blocking: false, - autostart: true, - autorun: false, - filter: "", - queue: [], - semaphore: 0 - }); - - var tests = id( "qunit-tests" ), - banner = id( "qunit-banner" ), - result = id( "qunit-testresult" ); - - if ( tests ) { - tests.innerHTML = ""; - } - - if ( banner ) { - banner.className = ""; - } - - if ( result ) { - result.parentNode.removeChild( result ); - } - - if ( tests ) { - result = document.createElement( "p" ); - result.id = "qunit-testresult"; - result.className = "result"; - tests.parentNode.insertBefore( result, tests ); - result.innerHTML = 'Running...
     '; - } - }, - - /** - * Resets the test setup. Useful for tests that modify the DOM. - * - * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. - */ - reset: function() { - if ( window.jQuery ) { - jQuery( "#qunit-fixture" ).html( config.fixture ); - } else { - var main = id( 'qunit-fixture' ); - if ( main ) { - main.innerHTML = config.fixture; - } - } - }, - - /** - * Trigger an event on an element. - * - * @example triggerEvent( document.body, "click" ); - * - * @param DOMElement elem - * @param String type - */ - triggerEvent: function( elem, type, event ) { - if ( document.createEvent ) { - event = document.createEvent("MouseEvents"); - event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, - 0, 0, 0, 0, 0, false, false, false, false, 0, null); - elem.dispatchEvent( event ); - - } else if ( elem.fireEvent ) { - elem.fireEvent("on"+type); - } - }, - - // Safe object type checking - is: function( type, obj ) { - return QUnit.objectType( obj ) == type; - }, - - objectType: function( obj ) { - if (typeof obj === "undefined") { - return "undefined"; - - // consider: typeof null === object - } - if (obj === null) { - return "null"; - } - - var type = Object.prototype.toString.call( obj ) - .match(/^\[object\s(.*)\]$/)[1] || ''; - - switch (type) { - case 'Number': - if (isNaN(obj)) { - return "nan"; - } else { - return "number"; - } - case 'String': - case 'Boolean': - case 'Array': - case 'Date': - case 'RegExp': - case 'Function': - return type.toLowerCase(); - } - if (typeof obj === "object") { - return "object"; - } - return undefined; - }, - - push: function(result, actual, expected, message) { - var details = { - result: result, - message: message, - actual: actual, - expected: expected - }; - - message = escapeHtml(message) || (result ? "okay" : "failed"); - message = '' + message + ""; - expected = escapeHtml(QUnit.jsDump.parse(expected)); - actual = escapeHtml(QUnit.jsDump.parse(actual)); - var output = message + ''; - if (actual != expected) { - output += ''; - output += ''; - } - if (!result) { - var source = sourceFromStacktrace(); - if (source) { - details.source = source; - output += ''; - } - } - output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + source +'
    "; - - QUnit.log(details); - - config.current.assertions.push({ - result: !!result, - message: output - }); - }, - - url: function( params ) { - params = extend( extend( {}, QUnit.urlParams ), params ); - var querystring = "?", - key; - for ( key in params ) { - querystring += encodeURIComponent( key ) + "=" + - encodeURIComponent( params[ key ] ) + "&"; - } - return window.location.pathname + querystring.slice( 0, -1 ); - }, - - // Logging callbacks; all receive a single argument with the listed properties - // run test/logs.html for any related changes - begin: function() {}, - // done: { failed, passed, total, runtime } - done: function() {}, - // log: { result, actual, expected, message } - log: function() {}, - // testStart: { name } - testStart: function() {}, - // testDone: { name, failed, passed, total } - testDone: function() {}, - // moduleStart: { name } - moduleStart: function() {}, - // moduleDone: { name, failed, passed, total } - moduleDone: function() {} -}); - -if ( typeof document === "undefined" || document.readyState === "complete" ) { - config.autorun = true; -} - -addEvent(window, "load", function() { - QUnit.begin({}); - - // Initialize the config, saving the execution queue - var oldconfig = extend({}, config); - QUnit.init(); - extend(config, oldconfig); - - config.blocking = false; - - var userAgent = id("qunit-userAgent"); - if ( userAgent ) { - userAgent.innerHTML = navigator.userAgent; - } - var banner = id("qunit-header"); - if ( banner ) { - banner.innerHTML = ' ' + banner.innerHTML + ' ' + - '' + - ''; - addEvent( banner, "change", function( event ) { - var params = {}; - params[ event.target.name ] = event.target.checked ? true : undefined; - window.location = QUnit.url( params ); - }); - } - - var toolbar = id("qunit-testrunner-toolbar"); - if ( toolbar ) { - var filter = document.createElement("input"); - filter.type = "checkbox"; - filter.id = "qunit-filter-pass"; - addEvent( filter, "click", function() { - var ol = document.getElementById("qunit-tests"); - if ( filter.checked ) { - ol.className = ol.className + " hidepass"; - } else { - var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; - ol.className = tmp.replace(/ hidepass /, " "); - } - if ( defined.sessionStorage ) { - if (filter.checked) { - sessionStorage.setItem("qunit-filter-passed-tests", "true"); - } else { - sessionStorage.removeItem("qunit-filter-passed-tests"); - } - } - }); - if ( defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { - filter.checked = true; - var ol = document.getElementById("qunit-tests"); - ol.className = ol.className + " hidepass"; - } - toolbar.appendChild( filter ); - - var label = document.createElement("label"); - label.setAttribute("for", "qunit-filter-pass"); - label.innerHTML = "Hide passed tests"; - toolbar.appendChild( label ); - } - - var main = id('qunit-fixture'); - if ( main ) { - config.fixture = main.innerHTML; - } - - if (config.autostart) { - QUnit.start(); - } -}); - -function done() { - config.autorun = true; - - // Log the last module results - if ( config.currentModule ) { - QUnit.moduleDone( { - name: config.currentModule, - failed: config.moduleStats.bad, - passed: config.moduleStats.all - config.moduleStats.bad, - total: config.moduleStats.all - } ); - } - - var banner = id("qunit-banner"), - tests = id("qunit-tests"), - runtime = +new Date - config.started, - passed = config.stats.all - config.stats.bad, - html = [ - 'Tests completed in ', - runtime, - ' milliseconds.
    ', - '', - passed, - ' tests of ', - config.stats.all, - ' passed, ', - config.stats.bad, - ' failed.' - ].join(''); - - if ( banner ) { - banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); - } - - if ( tests ) { - id( "qunit-testresult" ).innerHTML = html; - } - - if ( typeof document !== "undefined" && document.title ) { - // show ✖ for good, ✔ for bad suite result in title - // use escape sequences in case file gets loaded with non-utf-8-charset - document.title = (config.stats.bad ? "\u2716" : "\u2714") + " " + document.title; - } - - QUnit.done( { - failed: config.stats.bad, - passed: passed, - total: config.stats.all, - runtime: runtime - } ); -} - -function validTest( name ) { - var filter = config.filter, - run = false; - - if ( !filter ) { - return true; - } - - var not = filter.charAt( 0 ) === "!"; - if ( not ) { - filter = filter.slice( 1 ); - } - - if ( name.indexOf( filter ) !== -1 ) { - return !not; - } - - if ( not ) { - run = true; - } - - return run; -} - -// so far supports only Firefox, Chrome and Opera (buggy) -// could be extended in the future to use something like https://github.com/csnover/TraceKit -function sourceFromStacktrace() { - try { - throw new Error(); - } catch ( e ) { - if (e.stacktrace) { - // Opera - return e.stacktrace.split("\n")[6]; - } else if (e.stack) { - // Firefox, Chrome - return e.stack.split("\n")[4]; - } - } -} - -function escapeHtml(s) { - if (!s) { - return ""; - } - s = s + ""; - return s.replace(/[\&"<>\\]/g, function(s) { - switch(s) { - case "&": return "&"; - case "\\": return "\\\\"; - case '"': return '\"'; - case "<": return "<"; - case ">": return ">"; - default: return s; - } - }); -} - -function synchronize( callback ) { - config.queue.push( callback ); - - if ( config.autorun && !config.blocking ) { - process(); - } -} - -function process() { - var start = (new Date()).getTime(); - - while ( config.queue.length && !config.blocking ) { - if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { - config.queue.shift()(); - } else { - window.setTimeout( process, 13 ); - break; - } - } - if (!config.blocking && !config.queue.length) { - done(); - } -} - -function saveGlobal() { - config.pollution = []; - - if ( config.noglobals ) { - for ( var key in window ) { - config.pollution.push( key ); - } - } -} - -function checkPollution( name ) { - var old = config.pollution; - saveGlobal(); - - var newGlobals = diff( config.pollution, old ); - if ( newGlobals.length > 0 ) { - ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); - } - - var deletedGlobals = diff( old, config.pollution ); - if ( deletedGlobals.length > 0 ) { - ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); - } -} - -// returns a new Array with the elements that are in a but not in b -function diff( a, b ) { - var result = a.slice(); - for ( var i = 0; i < result.length; i++ ) { - for ( var j = 0; j < b.length; j++ ) { - if ( result[i] === b[j] ) { - result.splice(i, 1); - i--; - break; - } - } - } - return result; -} - -function fail(message, exception, callback) { - if ( typeof console !== "undefined" && console.error && console.warn ) { - console.error(message); - console.error(exception); - console.warn(callback.toString()); - - } else if ( window.opera && opera.postError ) { - opera.postError(message, exception, callback.toString); - } -} - -function extend(a, b) { - for ( var prop in b ) { - if ( b[prop] === undefined ) { - delete a[prop]; - } else { - a[prop] = b[prop]; - } - } - - return a; -} - -function addEvent(elem, type, fn) { - if ( elem.addEventListener ) { - elem.addEventListener( type, fn, false ); - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, fn ); - } else { - fn(); - } -} - -function id(name) { - return !!(typeof document !== "undefined" && document && document.getElementById) && - document.getElementById( name ); -} - -// Test for equality any JavaScript type. -// Discussions and reference: http://philrathe.com/articles/equiv -// Test suites: http://philrathe.com/tests/equiv -// Author: Philippe Rathé -QUnit.equiv = function () { - - var innerEquiv; // the real equiv function - var callers = []; // stack to decide between skip/abort functions - var parents = []; // stack to avoiding loops from circular referencing - - // Call the o related callback with the given arguments. - function bindCallbacks(o, callbacks, args) { - var prop = QUnit.objectType(o); - if (prop) { - if (QUnit.objectType(callbacks[prop]) === "function") { - return callbacks[prop].apply(callbacks, args); - } else { - return callbacks[prop]; // or undefined - } - } - } - - var callbacks = function () { - - // for string, boolean, number and null - function useStrictEquality(b, a) { - if (b instanceof a.constructor || a instanceof b.constructor) { - // to catch short annotaion VS 'new' annotation of a declaration - // e.g. var i = 1; - // var j = new Number(1); - return a == b; - } else { - return a === b; - } - } - - return { - "string": useStrictEquality, - "boolean": useStrictEquality, - "number": useStrictEquality, - "null": useStrictEquality, - "undefined": useStrictEquality, - - "nan": function (b) { - return isNaN(b); - }, - - "date": function (b, a) { - return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); - }, - - "regexp": function (b, a) { - return QUnit.objectType(b) === "regexp" && - a.source === b.source && // the regex itself - a.global === b.global && // and its modifers (gmi) ... - a.ignoreCase === b.ignoreCase && - a.multiline === b.multiline; - }, - - // - skip when the property is a method of an instance (OOP) - // - abort otherwise, - // initial === would have catch identical references anyway - "function": function () { - var caller = callers[callers.length - 1]; - return caller !== Object && - typeof caller !== "undefined"; - }, - - "array": function (b, a) { - var i, j, loop; - var len; - - // b could be an object literal here - if ( ! (QUnit.objectType(b) === "array")) { - return false; - } - - len = a.length; - if (len !== b.length) { // safe and faster - return false; - } - - //track reference to avoid circular references - parents.push(a); - for (i = 0; i < len; i++) { - loop = false; - for(j=0;j= 0) { - type = "array"; - } else { - type = typeof obj; - } - return type; - }, - separator:function() { - return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; - }, - indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing - if ( !this.multiline ) - return ''; - var chr = this.indentChar; - if ( this.HTML ) - chr = chr.replace(/\t/g,' ').replace(/ /g,' '); - return Array( this._depth_ + (extra||0) ).join(chr); - }, - up:function( a ) { - this._depth_ += a || 1; - }, - down:function( a ) { - this._depth_ -= a || 1; - }, - setParser:function( name, parser ) { - this.parsers[name] = parser; - }, - // The next 3 are exposed so you can use them - quote:quote, - literal:literal, - join:join, - // - _depth_: 1, - // This is the list of parsers, to modify them, use jsDump.setParser - parsers:{ - window: '[Window]', - document: '[Document]', - error:'[ERROR]', //when no parser is found, shouldn't happen - unknown: '[Unknown]', - 'null':'null', - 'undefined':'undefined', - 'function':function( fn ) { - var ret = 'function', - name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE - if ( name ) - ret += ' ' + name; - ret += '('; - - ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); - return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); - }, - array: array, - nodelist: array, - arguments: array, - object:function( map ) { - var ret = [ ]; - QUnit.jsDump.up(); - for ( var key in map ) - ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(map[key]) ); - QUnit.jsDump.down(); - return join( '{', ret, '}' ); - }, - node:function( node ) { - var open = QUnit.jsDump.HTML ? '<' : '<', - close = QUnit.jsDump.HTML ? '>' : '>'; - - var tag = node.nodeName.toLowerCase(), - ret = open + tag; - - for ( var a in QUnit.jsDump.DOMAttrs ) { - var val = node[QUnit.jsDump.DOMAttrs[a]]; - if ( val ) - ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); - } - return ret + close + open + '/' + tag + close; - }, - functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function - var l = fn.length; - if ( !l ) return ''; - - var args = Array(l); - while ( l-- ) - args[l] = String.fromCharCode(97+l);//97 is 'a' - return ' ' + args.join(', ') + ' '; - }, - key:quote, //object calls it internally, the key part of an item in a map - functionCode:'[code]', //function calls it internally, it's the content of the function - attribute:quote, //node calls it internally, it's an html attribute value - string:quote, - date:quote, - regexp:literal, //regex - number:literal, - 'boolean':literal - }, - DOMAttrs:{//attributes to dump from nodes, name=>realName - id:'id', - name:'name', - 'class':'className' - }, - HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) - indentChar:' ',//indentation unit - multiline:true //if true, items in a collection, are separated by a \n, else just a space. - }; - - return jsDump; -})(); - -// from Sizzle.js -function getText( elems ) { - var ret = "", elem; - - for ( var i = 0; elems[i]; i++ ) { - elem = elems[i]; - - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; - - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += getText( elem.childNodes ); - } - } - - return ret; -}; - -/* - * Javascript Diff Algorithm - * By John Resig (http://ejohn.org/) - * Modified by Chu Alan "sprite" - * - * Released under the MIT license. - * - * More Info: - * http://ejohn.org/projects/javascript-diff-algorithm/ - * - * Usage: QUnit.diff(expected, actual) - * - * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" - */ -QUnit.diff = (function() { - function diff(o, n){ - var ns = new Object(); - var os = new Object(); - - for (var i = 0; i < n.length; i++) { - if (ns[n[i]] == null) - ns[n[i]] = { - rows: new Array(), - o: null - }; - ns[n[i]].rows.push(i); - } - - for (var i = 0; i < o.length; i++) { - if (os[o[i]] == null) - os[o[i]] = { - rows: new Array(), - n: null - }; - os[o[i]].rows.push(i); - } - - for (var i in ns) { - if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { - n[ns[i].rows[0]] = { - text: n[ns[i].rows[0]], - row: os[i].rows[0] - }; - o[os[i].rows[0]] = { - text: o[os[i].rows[0]], - row: ns[i].rows[0] - }; - } - } - - for (var i = 0; i < n.length - 1; i++) { - if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && - n[i + 1] == o[n[i].row + 1]) { - n[i + 1] = { - text: n[i + 1], - row: n[i].row + 1 - }; - o[n[i].row + 1] = { - text: o[n[i].row + 1], - row: i + 1 - }; - } - } - - for (var i = n.length - 1; i > 0; i--) { - if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && - n[i - 1] == o[n[i].row - 1]) { - n[i - 1] = { - text: n[i - 1], - row: n[i].row - 1 - }; - o[n[i].row - 1] = { - text: o[n[i].row - 1], - row: i - 1 - }; - } - } - - return { - o: o, - n: n - }; - } - - return function(o, n){ - o = o.replace(/\s+$/, ''); - n = n.replace(/\s+$/, ''); - var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); - - var str = ""; - - var oSpace = o.match(/\s+/g); - if (oSpace == null) { - oSpace = [" "]; - } - else { - oSpace.push(" "); - } - var nSpace = n.match(/\s+/g); - if (nSpace == null) { - nSpace = [" "]; - } - else { - nSpace.push(" "); - } - - if (out.n.length == 0) { - for (var i = 0; i < out.o.length; i++) { - str += '' + out.o[i] + oSpace[i] + ""; - } - } - else { - if (out.n[0].text == null) { - for (n = 0; n < out.o.length && out.o[n].text == null; n++) { - str += '' + out.o[n] + oSpace[n] + ""; - } - } - - for (var i = 0; i < out.n.length; i++) { - if (out.n[i].text == null) { - str += '' + out.n[i] + nSpace[i] + ""; - } - else { - var pre = ""; - - for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { - pre += '' + out.o[n] + oSpace[n] + ""; - } - str += " " + out.n[i].text + nSpace[i] + pre; - } - } - } - - return str; - }; -})(); - -})(this); \ No newline at end of file diff --git a/README.md b/README.md index 0045455c3..3c2b0b3fa 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Use the `location` or `iosDatabaseLocation` option in `sqlitePlugin.openDatabase - Resolved transaction problem after window.location (page) change with possible data loss ref: [litehelpers/Cordova-sqlite-storage#666](https://github.com/litehelpers/Cordova-sqlite-storage/issues/666) - [brodybits / cordova-sqlite-test-app](https://github.com/brodybits/cordova-sqlite-test-app) project is a CC0 (public domain) starting point (NOTE that this plugin must be added) and may also be used to reproduce issues with this plugin. -- The Lawnchair adapter is now supported in [litehelpers / cordova-sqlite-lawnchair-adapter](https://github.com/litehelpers/cordova-sqlite-lawnchair-adapter). +- The Lawnchair adapter is now moved to [litehelpers / cordova-sqlite-lawnchair-adapter](https://github.com/litehelpers/cordova-sqlite-lawnchair-adapter). - [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) now supports SELECT BLOB data in Base64 format on all platforms in addition to REGEXP (Android/iOS/macOS) and pre-populated database (all platforms). - [brodybits / sql-promise-helper](https://github.com/brodybits/sql-promise-helper) provides a Promise-based API wrapper. - [nolanlawson / pouchdb-adapter-cordova-sqlite](https://github.com/nolanlawson/pouchdb-adapter-cordova-sqlite) supports this plugin along with other implementations such as [nolanlawson / sqlite-plugin-2](https://github.com/nolanlawson/sqlite-plugin-2) and [Microsoft / cordova-plugin-websql](https://github.com/Microsoft/cordova-plugin-websql). @@ -775,7 +775,6 @@ cordova platform add ios - `src`: platform-specific source code - `spec`: test suite using Jasmine (2.2.0) - `tests`: very simple Jasmine test suite that is run on Circle CI (Android version) and Travis CI (iOS version) (used as a placeholder) -- `Lawnchair-adapter`: Lawnchair adaptor, based on the version from the Lawnchair repository, with the basic Lawnchair test suite in `test-www` subdirectory ## Installation test @@ -895,43 +894,7 @@ To run from a windows powershell (here is a sample for android target): ## Lawnchair Adapter -**BROKEN:** The Lawnchair adapter does not support the `openDatabase` options such as `location` or `iosDatabaseLocation` options and is therefore *not* expected to work with this plugin. - -NOW SUPPORTED IN: [litehelpers / cordova-sqlite-lawnchair-adapter](https://github.com/litehelpers/cordova-sqlite-lawnchair-adapter) - -### Common adapter - -Please look at the `Lawnchair-adapter` tree that contains a common adapter, which should also work with the Android version, along with a test-www directory. - -### Included files - -Include the following Javascript files in your HTML: - -- `cordova.js` (don't forget!) -- `lawnchair.js` (you provide) -- `SQLitePlugin.js` (in case of Cordova pre-3.0) -- `Lawnchair-sqlitePlugin.js` (must come after `SQLitePlugin.js` in case of Cordova pre-3.0) - -### Sample - -The `name` option determines the sqlite database filename, *with no extension automatically added*. Optionally, you can change the db filename using the `db` option. - -In this example, you would be using/creating a database with filename `kvstore`: - -```Javascript -kvstore = new Lawnchair({name: "kvstore"}, function() { - // do stuff -); -``` - -Using the `db` option you can specify the filename with the desired extension and be able to create multiple stores in the same database file. (There will be one table per store.) - -```Javascript -recipes = new Lawnchair({db: "cookbook", name: "recipes", ...}, myCallback()); -ingredients = new Lawnchair({db: "cookbook", name: "ingredients", ...}, myCallback()); -``` - -**KNOWN ISSUE:** the new db options are *not* supported by the Lawnchair adapter. The workaround is to first open the database file using `sqlitePlugin.openDatabase()`. +- [litehelpers / cordova-sqlite-lawnchair-adapter](https://github.com/litehelpers/cordova-sqlite-lawnchair-adapter) ## PouchDB diff --git a/package.json b/package.json index 04e2417d0..15c419575 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-sqlite-legacy-express-core", - "version": "1.0.0-pre3", + "version": "1.0.0-pre4", "description": "Native interface to SQLite for PhoneGap/Cordova (legacy express core version)", "cordova": { "id": "cordova-sqlite-legacy-express-core", diff --git a/plugin.xml b/plugin.xml index 386d19d13..da9c2ced5 100644 --- a/plugin.xml +++ b/plugin.xml @@ -2,7 +2,7 @@ + version="1.0.0-pre4"> Cordova sqlite storage plugin - legacy express core version From df326d4c78a761f5eca6a626289e257281a303c4 Mon Sep 17 00:00:00 2001 From: "Christopher J. Brody" Date: Wed, 26 Apr 2017 07:27:41 +0200 Subject: [PATCH 06/11] Some general test updates --- spec/www/index.html | 2 - spec/www/spec/basic-misc-test.js | 34 +++++++----- .../spec/db-simultaneous-tx-access-test.js | 21 ++++---- spec/www/spec/db-tx-sql-results.js | 19 ++++--- spec/www/spec/db-tx-string-test.js | 50 +++++++++--------- spec/www/spec/db-tx-value-bindings-test.js | 34 ++++++------ spec/www/spec/ext-tx-blob-test.js | 28 +++++----- spec/www/spec/misc-tx-legacy.js | 52 +++++++++++-------- spec/www/spec/regexp-test.js | 20 +++---- spec/www/spec/sql-batch-test.js | 24 ++++----- spec/www/spec/tx-semantics-test.js | 23 ++++---- 11 files changed, 162 insertions(+), 145 deletions(-) diff --git a/spec/www/index.html b/spec/www/index.html index aa9f660b8..d84a43e65 100644 --- a/spec/www/index.html +++ b/spec/www/index.html @@ -15,8 +15,6 @@ - - diff --git a/spec/www/spec/basic-misc-test.js b/spec/www/spec/basic-misc-test.js index 2f9418705..acb820481 100644 --- a/spec/www/spec/basic-misc-test.js +++ b/spec/www/spec/basic-misc-test.js @@ -8,9 +8,10 @@ var isWP8 = /IEMobile/.test(navigator.userAgent); // Matches WP(7/8/8.1) var isWindows = /Windows /.test(navigator.userAgent); // Windows var isAndroid = !isWindows && /Android/.test(navigator.userAgent); -// NOTE: In the core-master branch there is no difference between the default -// implementation and implementation #2. But the test will also apply -// the androidLockWorkaround: 1 option in the case of implementation #2. +// NOTE: While in certain version branches there is no difference between +// the default Android implementation and implementation #2, +// this test script will also apply the androidLockWorkaround: 1 option +// in case of implementation #2. var scenarioList = [ isAndroid ? 'Plugin-implementation-default' : 'Plugin', 'HTML5', @@ -19,7 +20,8 @@ var scenarioList = [ var scenarioCount = (!!window.hasWebKitBrowser) ? (isAndroid ? 3 : 2) : 1; -// XXX FUTURE TBD: split this into db feature & tx error handling test scripts +// FUTURE TBD (already done in newer version branches): +// Split this into db feature & tx error handling test scripts var mytests = function() { @@ -29,23 +31,25 @@ var mytests = function() { var scenarioName = scenarioList[i]; var suiteName = scenarioName + ': '; var isWebSql = (i === 1); - var isOldImpl = (i === 2); + var isImpl2 = (i === 2); // NOTE: MUST be defined in function scope, NOT outer scope: var openDatabase = function(name, ignored1, ignored2, ignored3) { - if (isOldImpl) { + if (isImpl2) { return window.sqlitePlugin.openDatabase({ // prevent reuse of database from default db implementation: name: 'i2-'+name, + // explicit database location: + location: 'default', androidDatabaseImplementation: 2, - androidLockWorkaround: 1, - location: 1 + androidLockWorkaround: 1 }); } if (isWebSql) { return window.openDatabase(name, '1.0', 'Test', DEFAULT_SIZE); } else { - return window.sqlitePlugin.openDatabase({name: name, location: 0}); + // explicit database location: + return window.sqlitePlugin.openDatabase({name: name, location: 'default'}); } } @@ -307,23 +311,25 @@ var mytests = function() { var scenarioName = scenarioList[i]; var suiteName = scenarioName + ': '; var isWebSql = (i === 1); - var isOldImpl = (i === 2); + var isImpl2 = (i === 2); // NOTE: MUST be defined in function scope, NOT outer scope: var openDatabase = function(name, ignored1, ignored2, ignored3) { - if (isOldImpl) { + if (isImpl2) { return window.sqlitePlugin.openDatabase({ // prevent reuse of database from default db implementation: name: 'i2-'+name, + // explicit database location: + location: 'default', androidDatabaseImplementation: 2, - androidLockWorkaround: 1, - location: 1 + androidLockWorkaround: 1 }); } if (isWebSql) { return window.openDatabase(name, '1.0', 'Test', DEFAULT_SIZE); } else { - return window.sqlitePlugin.openDatabase({name: name, location: 0}); + // explicit database location: + return window.sqlitePlugin.openDatabase({name: name, location: 'default'}); } } diff --git a/spec/www/spec/db-simultaneous-tx-access-test.js b/spec/www/spec/db-simultaneous-tx-access-test.js index 2c8fdd8ff..929ef775d 100755 --- a/spec/www/spec/db-simultaneous-tx-access-test.js +++ b/spec/www/spec/db-simultaneous-tx-access-test.js @@ -32,9 +32,10 @@ function start(n) { var isAndroid = /Android/.test(navigator.userAgent); -// NOTE: In the core-master branch there is no difference between the default -// implementation and implementation #2. But the test will also apply -// the androidLockWorkaround: 1 option in the case of implementation #2. +// NOTE: While in certain version branches there is no difference between +// the default Android implementation and implementation #2, +// this test script will also apply the androidLockWorkaround: 1 option +// in case of implementation #2. var scenarioList = [ isAndroid ? 'Plugin-implementation-default' : 'Plugin', 'HTML5', @@ -51,23 +52,25 @@ var mytests = function() { var scenarioName = scenarioList[i]; var suiteName = scenarioName + ': '; var isWebSql = (i === 1); - var isOldImpl = (i === 2); + var isImpl2 = (i === 2); // NOTE: MUST be defined in function scope, NOT outer scope: var openDatabase = function(name, ignored1, ignored2, ignored3) { - if (isOldImpl) { + if (isImpl2) { return window.sqlitePlugin.openDatabase({ // prevent reuse of database from default db implementation: name: 'i2-'+name, + // explicit database location: + location: 'default', androidDatabaseImplementation: 2, - androidLockWorkaround: 1, - location: 1 + androidLockWorkaround: 1 }); } if (isWebSql) { - return window.openDatabase(name, "1.0", "Demo", DEFAULT_SIZE); + return window.openDatabase(name, '1.0', 'Test', DEFAULT_SIZE); } else { - return window.sqlitePlugin.openDatabase({name: name, location: 0}); + // explicit database location: + return window.sqlitePlugin.openDatabase({name: name, location: 'default'}); } } diff --git a/spec/www/spec/db-tx-sql-results.js b/spec/www/spec/db-tx-sql-results.js index c89bb5679..71182a822 100644 --- a/spec/www/spec/db-tx-sql-results.js +++ b/spec/www/spec/db-tx-sql-results.js @@ -8,9 +8,10 @@ var isWP8 = /IEMobile/.test(navigator.userAgent); // Matches WP(7/8/8.1) var isWindows = /Windows /.test(navigator.userAgent); // Windows var isAndroid = !isWindows && /Android/.test(navigator.userAgent); -// NOTE: In the core-master branch there is no difference between the default -// implementation and implementation #2. But the test will also apply -// the androidLockWorkaround: 1 option in the case of implementation #2. +// NOTE: While in certain version branches there is no difference between +// the default Android implementation and implementation #2, +// this test script will also apply the androidLockWorkaround: 1 option +// in case of implementation #2. var scenarioList = [ isAndroid ? 'Plugin-implementation-default' : 'Plugin', 'HTML5', @@ -27,23 +28,25 @@ var mytests = function() { var scenarioName = scenarioList[i]; var suiteName = scenarioName + ': '; var isWebSql = (i === 1); - var isOldImpl = (i === 2); + var isImpl2 = (i === 2); // NOTE: MUST be defined in function scope, NOT outer scope: var openDatabase = function(name, ignored1, ignored2, ignored3) { - if (isOldImpl) { + if (isImpl2) { return window.sqlitePlugin.openDatabase({ // prevent reuse of database from default db implementation: name: 'i2-'+name, + // explicit database location: + location: 'default', androidDatabaseImplementation: 2, - androidLockWorkaround: 1, - location: 1 + androidLockWorkaround: 1 }); } if (isWebSql) { return window.openDatabase(name, '1.0', 'Test', DEFAULT_SIZE); } else { - return window.sqlitePlugin.openDatabase({name: name, location: 0}); + // explicit database location: + return window.sqlitePlugin.openDatabase({name: name, location: 'default'}); } } diff --git a/spec/www/spec/db-tx-string-test.js b/spec/www/spec/db-tx-string-test.js index 32848b464..c4768269a 100755 --- a/spec/www/spec/db-tx-string-test.js +++ b/spec/www/spec/db-tx-string-test.js @@ -10,17 +10,12 @@ function equal(a, b, desc) { expect(a).toEqual(b); } // '==' var isAndroid = /Android/.test(navigator.userAgent); var isWP8 = /IEMobile/.test(navigator.userAgent); // Matches WP(7/8/8.1) -//var isWindows = /Windows NT/.test(navigator.userAgent); // Windows [NT] (8.1) var isWindows = /Windows /.test(navigator.userAgent); // Windows (8.1) -//var isWindowsPC = /Windows NT/.test(navigator.userAgent); // Windows [NT] (8.1) -//var isWindowsPhone_8_1 = /Windows Phone 8.1/.test(navigator.userAgent); // Windows Phone 8.1 -//var isIE = isWindows || isWP8 || isWindowsPhone_8_1; -var isIE = isWindows || isWP8; -var isWebKit = !isIE; // TBD [Android or iOS] - -// NOTE: In the core-master branch there is no difference between the default -// implementation and implementation #2. But the test will also apply -// the androidLockWorkaround: 1 option in the case of implementation #2. + +// NOTE: While in certain version branches there is no difference between +// the default Android implementation and implementation #2, +// this test script will also apply the androidLockWorkaround: 1 option +// in case of implementation #2. var scenarioList = [ isAndroid ? 'Plugin-implementation-default' : 'Plugin', 'HTML5', @@ -37,23 +32,25 @@ var mytests = function() { var scenarioName = scenarioList[i]; var suiteName = scenarioName + ': '; var isWebSql = (i === 1); - var isOldImpl = (i === 2); + var isImpl2 = (i === 2); // NOTE: MUST be defined in function scope, NOT outer scope: var openDatabase = function(name, ignored1, ignored2, ignored3) { - if (isOldImpl) { + if (isImpl2) { return window.sqlitePlugin.openDatabase({ // prevent reuse of database from default db implementation: name: 'i2-'+name, + // explicit database location: + location: 'default', androidDatabaseImplementation: 2, - androidLockWorkaround: 1, - location: 1 + androidLockWorkaround: 1 }); } if (isWebSql) { - return window.openDatabase(name, "1.0", "Demo", DEFAULT_SIZE); + return window.openDatabase(name, '1.0', 'Test', DEFAULT_SIZE); } else { - return window.sqlitePlugin.openDatabase({name: name, location: 0}); + // explicit database location: + return window.sqlitePlugin.openDatabase({name: name, location: 'default'}); } } @@ -79,9 +76,10 @@ var mytests = function() { }); it(suiteName + ' string encoding test with UNICODE \\u0000', function (done) { - if (isWindows) pending('BROKEN for Windows'); // XXX - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) - if (isAndroid && !isWebSql && !isOldImpl) pending('BROKEN for Android (default sqlite-connector version)'); // XXX + if (isWP8) pending('BROKEN on WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) + if (isWindows) pending('BROKEN on Windows'); // TBD (truncates on Windows) + // TBD NOT BROKEN for Android (no Android-sqlite-connector implementation) in this version branch: + //if (!isWebSql && !isWindows && isAndroid && !isImpl2) pending('BROKEN on Android-sqlite-connector implementation)'); var dbName = "Unicode-hex-test"; var db = openDatabase(dbName, "1.0", "Demo", DEFAULT_SIZE); @@ -153,7 +151,7 @@ var mytests = function() { }); it(suiteName + "String vertical tab test", function(done) { - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) + if (isWP8) pending('BROKEN on WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) var db = openDatabase("String-vertical-tab-test.db", "1.0", "Demo", DEFAULT_SIZE); expect(db).toBeDefined(); @@ -169,7 +167,7 @@ var mytests = function() { }); it(suiteName + "String form feed test", function(done) { - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) + if (isWP8) pending('BROKEN on WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) var db = openDatabase("String-form-feed-test.db", "1.0", "Demo", DEFAULT_SIZE); expect(db).toBeDefined(); @@ -185,7 +183,7 @@ var mytests = function() { }); it(suiteName + "String backspace test", function(done) { - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) + if (isWP8) pending('BROKEN on WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) var db = openDatabase("String-backspace-test.db", "1.0", "Demo", DEFAULT_SIZE); expect(db).toBeDefined(); @@ -208,7 +206,7 @@ var mytests = function() { // - Apache Cordova CB-9435 (issue with cordova-ios, also affects macOS) // - cordova/cordova-discuss#57 (issue with cordova-android) it(suiteName + "UNICODE \\u2028 line separator string length", function(done) { - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] Certain UNICODE characters not working with WP(8) + if (isWP8) pending('BROKEN on WP(8)'); // [BUG #202] Certain UNICODE characters not working with WP(8) // NOTE: this test verifies that the UNICODE line separator (\u2028) // is seen by the sqlite implementation OK: @@ -228,7 +226,7 @@ var mytests = function() { }); it(suiteName + ' handles UNICODE \\u2028 line separator correctly [string test]', function (done) { - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) + if (isWP8) pending('BROKEN on WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) if (!isWebSql && !isWindows && isAndroid) pending('SKIP for Android plugin (cordova-android 6.x BUG: cordova/cordova-discuss#57)'); if (!isWebSql && !isWindows && !isAndroid && !isWP8) pending('SKIP for iOS/macOS plugin (Cordova BUG: CB-9435)'); @@ -260,7 +258,7 @@ var mytests = function() { // - Apache Cordova CB-9435 (issue with cordova-ios, also affects macOS) // - cordova/cordova-discuss#57 (issue with cordova-android) it(suiteName + "UNICODE \\u2029 line separator string length", function(done) { - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] Certain UNICODE characters not working with WP(8) + if (isWP8) pending('BROKEN on WP(8)'); // [BUG #202] Certain UNICODE characters not working with WP(8) // NOTE: this test verifies that the UNICODE paragraph separator (\u2029) // is seen by the sqlite implementation OK: @@ -281,7 +279,7 @@ var mytests = function() { }); it(suiteName + ' handles UNICODE \\u2029 line separator correctly [string test]', function (done) { - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) + if (isWP8) pending('BROKEN on WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) if (!isWebSql && !isWindows && isAndroid) pending('SKIP for Android plugin (cordova-android 6.x BUG: cordova/cordova-discuss#57)'); if (!isWebSql && !isWindows && !isAndroid && !isWP8) pending('SKIP for iOS/macOS plugin (Cordova BUG: CB-9435)'); diff --git a/spec/www/spec/db-tx-value-bindings-test.js b/spec/www/spec/db-tx-value-bindings-test.js index 674d7e88b..23a760b5c 100755 --- a/spec/www/spec/db-tx-value-bindings-test.js +++ b/spec/www/spec/db-tx-value-bindings-test.js @@ -36,9 +36,10 @@ var isAndroid = !isWindows && /Android/.test(navigator.userAgent); var isMac = /Macintosh/.test(navigator.userAgent); var isWKWebView = !isWindows && !isAndroid && !isWP8 && !isMac && !!window.webkit && !!window.webkit.messageHandlers; -// NOTE: In the core-master branch there is no difference between the default -// implementation and implementation #2. But the test will also apply -// the androidLockWorkaround: 1 option in the case of implementation #2. +// NOTE: While in certain version branches there is no difference between +// the default Android implementation and implementation #2, +// this test script will also apply the androidLockWorkaround: 1 option +// in case of implementation #2. var scenarioList = [ isAndroid ? 'Plugin-implementation-default' : 'Plugin', 'HTML5', @@ -55,23 +56,25 @@ var mytests = function() { var scenarioName = scenarioList[i]; var suiteName = scenarioName + ': '; var isWebSql = (i === 1); - var isOldImpl = (i === 2); + var isImpl2 = (i === 2); // NOTE: MUST be defined in function scope, NOT outer scope: var openDatabase = function(name, ignored1, ignored2, ignored3) { - if (isOldImpl) { + if (isImpl2) { return window.sqlitePlugin.openDatabase({ // prevent reuse of database from default db implementation: name: 'i2-'+name, + // explicit database location: + location: 'default', androidDatabaseImplementation: 2, - androidLockWorkaround: 1, - location: 1 + androidLockWorkaround: 1 }); } if (isWebSql) { - return window.openDatabase(name, "1.0", "Demo", DEFAULT_SIZE); + return window.openDatabase(name, '1.0', 'Test', DEFAULT_SIZE); } else { - return window.sqlitePlugin.openDatabase({name: name, location: 0}); + // explicit database location: + return window.sqlitePlugin.openDatabase({name: name, location: 'default'}); } } @@ -155,7 +158,7 @@ var mytests = function() { }); it(suiteName + "Big [integer] value bindings", function(done) { - if (isWP8) pending('BROKEN for WP(8)'); // XXX [BUG #195] + if (isWP8) pending('BROKEN on WP(8)'); // XXX [BUG #195] var db = openDatabase("Big-int-bindings.db", "1.0", "Demo", DEFAULT_SIZE); db.transaction(function(tx) { @@ -249,9 +252,10 @@ var mytests = function() { // FUTURE TODO: fix these tests to follow the Jasmine style: test_it(suiteName + ' stores [Unicode] string with \\u0000 correctly', function () { - if (isWindows) pending('BROKEN on Windows'); // XXX - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) - if (isAndroid && !isWebSql && !isOldImpl) pending('BROKEN for Android (default sqlite-connector version)'); // XXX + if (isWP8) pending('BROKEN on WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) + if (isWindows) pending('BROKEN on Windows'); // TBD (truncates on Windows) + // TBD NOT BROKEN for Android (no Android-sqlite-connector implementation) in this version branch: + //if (!isWebSql && !isWindows && isAndroid && !isImpl2) pending('BROKEN on Android-sqlite-connector implementation)'); stop(); @@ -318,7 +322,7 @@ var mytests = function() { test_it(suiteName + ' returns [Unicode] string with \\u0000 correctly', function () { if (isWindows) pending('BROKEN on Windows'); // XXX - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) + if (isWP8) pending('BROKEN on WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) stop(); @@ -380,7 +384,7 @@ var mytests = function() { // - cordova/cordova-discuss#57 (issue with cordova-android) test_it(suiteName + ' handles UNICODE \\u2028 line separator correctly [in database]', function () { - if (isWP8) pending('BROKEN for WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) + if (isWP8) pending('BROKEN on WP(8)'); // [BUG #202] UNICODE characters not working with WP(8) if (!isWebSql && !isWindows && isAndroid) pending('SKIP for Android plugin (cordova-android 6.x BUG: cordova/cordova-discuss#57)'); if (!isWebSql && !isWindows && !isAndroid && !isWP8) pending('SKIP for iOS/macOS plugin (Cordova BUG: CB-9435)'); diff --git a/spec/www/spec/ext-tx-blob-test.js b/spec/www/spec/ext-tx-blob-test.js index a88cb2640..1d78694b5 100755 --- a/spec/www/spec/ext-tx-blob-test.js +++ b/spec/www/spec/ext-tx-blob-test.js @@ -9,17 +9,12 @@ function ok(test, desc) { expect(test).toBe(true); } var isAndroid = /Android/.test(navigator.userAgent); var isWP8 = /IEMobile/.test(navigator.userAgent); // Matches WP(7/8/8.1) -//var isWindows = /Windows NT/.test(navigator.userAgent); // Windows [NT] (8.1) var isWindows = /Windows /.test(navigator.userAgent); // Windows (8.1) -//var isWindowsPC = /Windows NT/.test(navigator.userAgent); // Windows [NT] (8.1) -//var isWindowsPhone_8_1 = /Windows Phone 8.1/.test(navigator.userAgent); // Windows Phone 8.1 -//var isIE = isWindows || isWP8 || isWindowsPhone_8_1; -var isIE = isWindows || isWP8; -var isWebKit = !isIE; // TBD [Android or iOS] - -// NOTE: In the core-master branch there is no difference between the default -// implementation and implementation #2. But the test will also apply -// the androidLockWorkaround: 1 option in the case of implementation #2. + +// NOTE: While in certain version branches there is no difference between +// the default Android implementation and implementation #2, +// this test script will also apply the androidLockWorkaround: 1 option +// in case of implementation #2. var scenarioList = [ isAndroid ? 'Plugin-implementation-default' : 'Plugin', 'HTML5', @@ -32,26 +27,29 @@ var mytests = function() { for (var i=0; i Date: Thu, 27 Apr 2017 22:11:51 +0200 Subject: [PATCH 07/11] cordova-sqlite-legacy-express-core 1.0.0 --- CHANGES.md | 64 +++++++++++++++++++-------------------- README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++----- package.json | 2 +- plugin.xml | 2 +- 4 files changed, 109 insertions(+), 44 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f4a1df964..4bd1f14a2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Changes -###### cordova-sqlite-legacy-express-core 1.0.0-pre4 +###### cordova-sqlite-legacy-express-core 1.0.0 - Workaround solution to BUG litehelpers/Cordova-sqlite-storage#666 (hanging transaction in case of location reload/change) - selfTest simulate scenario & test solution to BUG litehelpers/Cordova-sqlite-storage#666 (also includes string test and test of effects of location reload/change in this version branch, along with another internal check) @@ -8,52 +8,48 @@ - Remove Lawnchair adapter from this version branch - Support macOS platform with builtin libsqlite3.dylib framework in this version branch -## 1.2.2 +### cordova-sqlite-storage 1.2.2 - Self-test function to verify ability to open/populate/read/delete a test database - Read BLOB as Base-64 DISABLED in Android version (was already disabled for iOS) -## 1.2.1 +### cordova-sqlite-storage 1.2.1 - Close Android SQLiteStatement after INSERT/UPDATE/DELETE - Specify minimum Cordova version 6.0.0 - Lawnchair adapter fix: Changed remove method to work with key array -## 1.2.0 +### cordova-sqlite-storage 1.2.0 - Rename Lawnchair adapter to prevent clash with standard webkit-sqlite adapter - Support location: 'default' setting in openDatabase & deleteDatabase -## 0.8.5 +### cordova-sqlite-storage 0.8.5 - More explicit iosDatabaseLocation option - iOS database location is now mandatory - Split-up of some more spec test scripts -## 0.8.2 +### cordova-sqlite-storage 0.8.2 - Workaround fix for empty readTransaction issue (litehelpers/Cordova-sqlite-storage#409) - Split spec/www/spec/legacy.js into db-open-close-delete-test.js & tx-extended.js -## 0.8.0 +### cordova-sqlite-storage 0.8.0 - Simple sql batch transaction function - Echo test function -- Remove extra runInBackground: step from iOS version -- Android-sqlite-connector (NDK) support removed from this version branch -- Windows version removed from this version branch -- Java source of Android version now using io.sqlc package - -## 0.7.15-pre - - All iOS operations are now using background processing (reported to resolve intermittent problems with cordova-ios@4.0.1) +- Java source of Android version now using io.sqlc package +- Drop Android-sqlite-connector support +- Drop WP(8) and Windows support -## 0.7.14 +### 0.7.14 - REGEXP support completely removed from this version branch - Remove src/android/libs/.gitignore (inadvertently added in 0.7.13) -## 0.7.13 +### 0.7.13 - REGEXP support partially removed from this version branch - Rename Windows C++ Database close function to closedb to resolve conflict for Windows Store certification @@ -62,79 +58,79 @@ - Amazon Fire-OS support removed - Fix conversion warnings in iOS version -## 0.7.12 +### 0.7.12 - Fix to Windows "Universal" version to support big integers - Implement database close and delete operations for Windows "Universal" - Fix readTransaction to skip BEGIN/COMMIT/ROLLBACK -## 0.7.11 +### 0.7.11 - Fix plugin ID in plugin.xml to match npm package ID - Unpacked sqlite-native-driver.so libraries from jar - Fix conversion of INTEGER type (iOS version) - Disable code to read BLOB as Base-64 (iOS version) due to https://issues.apache.org/jira/browse/CB-9638 -## 0.7.10 +### 0.7.10 - Use Android-sqlite-connector instead of sqlite4java -## 0.7.9 +### 0.7.9 - Build iOS and Windows versions with sqlite 3.8.10.2 embedded - Fix plugin id to match npm package id -## 0.7.8 +### 0.7.8 - Support FTS3/FTS4 and R-Tree in iOS and Windows "Universal" (8.1) versions - Build ARM target with Function Level Linking ref: http://www.monkey-x.com/Community/posts.php?topic=7739 - SQLite3.Windows.vcxproj and SQLite3.WindowsPhone.vcxproj in their own directories to avoid problems due to temporary files -## 0.7.7 +### 0.7.7 - include build of sqlite4java for Android x86_64 and arm-64 - clean publish to plugins.cordova.io -## 0.7.6 +### 0.7.6 - Small fix to plugin id - Disable use of gethostuuid() in sqlite3.c (only used in iOS version) - published to plugins.cordova.io - [BUG] published extra junk in workarea, causing problems with Windows (Universal) version -## 0.7.5 +### 0.7.5 - Windows (Universal) version now supports both Windows 8.1 and Windows Phone 8.1 - iOS and Windows versions are now built with sqlite 3.8.9 embedded - Improved locking style and other optimizations applied for iOS version -## 0.7.4 +### 0.7.4 - iOS and Windows (8.1) versions built to keep non-essential temporary sqlite files in memory - Option to use legacy Android database library, with Android locking/closing issue (BUG #193) workaround included again -## 0.7.3 +### 0.7.3 - insertId & rowsAffected implemented for Windows (8.1) - plugin id changed -## 0.7.2 +### 0.7.2 - Android version with sqlite4java (sqlite 3.8.7 embedded), which solves BUG #193: Android closing/locking issue (ICU-UNICODE integration is now missing) - iOS version fixed to override the correct pluginInitialize method and built with sqlite 3.8.8.3 embedded -## 0.7.1 +### 0.7.1 - Project renamed - Initial version for Windows (8.1) [with sqlite 3.8.8.3 embedded] - Abort initially pending transactions for db handle (due to incorrect password key, for example) [from Cordova-sqlcipher-storage] - WP7 build enabled (NOT TESTED) -## 1.0.6 +### 1.0.6 - Proper handling of transactions that may be requested before the database open operation is completed - Report an error upon attempt to close a database handle object multiple times. -## 1.0.5 +### 1.0.5 - Workaround for Android db locking/closing issue - Fix double-precision REAL values in result (iOS version) @@ -143,22 +139,22 @@ - Fix closing of Android database - Some fixes for SQL API error handling to be consistent with Web SQL -## 1.0.4 +### 1.0.4 - Pre-populated database option (Android/iOS) - Option to select database location to disable iCloud backup (iOS ONLY) - Safeguard against closing of database while transaction is pending - Fix to prevent double marshaling of data -## 1.0.3 +### 1.0.3 - Fixed issue with multi-page apps on Android (due to problem when closing & re-opening app) -## 1.0.2 +### 1.0.2 - Workaround for issue with multiple UPDATE statements WP(8) (#128) -## 1.0.1 +### 1.0.1 - Support Cordova 3.3.0/3.4.0 to support Amazon-FireOS - Fixes for WP(8): diff --git a/README.md b/README.md index 3c2b0b3fa..5f6e44d3b 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Use the `location` or `iosDatabaseLocation` option in `sqlitePlugin.openDatabase ## Status - A recent version of the Cordova CLI (such as `6.5.0`) is recommended. Cordova versions older than `6.0.0` are missing the `cordova-ios@4.0.0` security fixes. In addition it is *required* to use `cordova prepare` in case of cordova-ios older than `4.3.0` (Cordova CLI `6.4.0`). -- iOS database location is now mandatory, as documented below. +- The iOS database location is now mandatory, as documented below. - Android platform version in this version branch is now using the built-in Android SQLite database classes. Integration with the lightweight [Android-sqlite-connector](https://github.com/liteglue/Android-sqlite-connector) is available in the default [litehelpers / Cordova-sqlite-storage](https://github.com/litehelpers/Cordova-sqlite-storage) version branch as well as other versions such as [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext) and [litehelpers / Cordova-sqlite-legacy](https://github.com/litehelpers/Cordova-sqlite-legacy). - iOS/macOS platform version in this version branch uses builtin `libsqlite3.dylib` framework library. Other versions such as the default [litehelpers / Cordova-sqlite-storage](https://github.com/litehelpers/Cordova-sqlite-storage) version branch, [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext), and [litehelpers / Cordova-sqlite-legacy](https://github.com/litehelpers/Cordova-sqlite-legacy) include a build of a recent sqlite3 amalgamation. - Windows 10 UWP version using the performant [doo / SQLite3-WinRT](https://github.com/doo/SQLite3-WinRT) component is available in the default [litehelpers / Cordova-sqlite-storage](https://github.com/litehelpers/Cordova-sqlite-storage) version branch as well as other versions such as [litehelpers / cordova-sqlite-ext](https://github.com/litehelpers/cordova-sqlite-ext). @@ -116,6 +116,32 @@ Use the `location` or `iosDatabaseLocation` option in `sqlitePlugin.openDatabase +## Security + +### Security of sensitive data + +According to [Web SQL Database API 7.2 Sensitivity of data](https://www.w3.org/TR/webdatabase/#sensitivity-of-data): +>User agents should treat persistently stored data as potentially sensitive; it's quite possible for e-mails, calendar appointments, health records, or other confidential documents to be stored in this mechanism. +> +>To this end, user agents should ensure that when deleting data, it is promptly deleted from the underlying storage. + +Unfortunately this plugin will not actually overwrite the deleted content unless the [secure_delete PRAGMA](https://www.sqlite.org/pragma.html#pragma_secure_delete) is used. + +### SQL injection + +As "strongly recommended" by [Web SQL Database API 8.5 SQL injection](https://www.w3.org/TR/webdatabase/#sql-injection): +>Authors are strongly recommended to make use of the `?` placeholder feature of the `executeSql()` method, and to never construct SQL statements on the fly. + + + +# Avoiding data loss + +- Double-check that the application code follows the documented API for SQL statements, parameter values, success callbacks, and error callbacks. +- For standard Web SQL transactions include a transaction error callback with the proper logic that indicates to the user if data cannot be stored for any reason. In case of individual SQL error handlers be sure to indicate to the user if there is any issue with storing data. +- For single statement and batch transactions include an error callback with logic that indicates to the user if data cannot be stored for any reason. + + + ## Known issues - iOS/macOS platform version does not support certain rapidly repeated open-and-close or open-and-delete test scenarios due to how the implementation handles background processing @@ -176,36 +202,59 @@ Issues fixed in some newer version branches: - Integration with PhoneGap developer app - Use within [InAppBrowser](http://docs.phonegap.com/en/edge/cordova_inappbrowser_inappbrowser.md.html) - Use within an iframe (see [litehelpers/Cordova-sqlite-storage#368 (comment)](https://github.com/litehelpers/Cordova-sqlite-storage/issues/368#issuecomment-154046367)) +- Date/time handling +- Maximum record size supported - Actual behavior when using SAVEPOINT(s) - R-Tree is not fully tested with Android - UNICODE characters not fully tested - Use with TRIGGER(s), JOIN and ORDER BY RANDOM - UPDATE/DELETE with LIMIT or ORDER BY (newer Android/iOS versions) -- WITH clause (not supported by older sqlite3 versions) - Integration with JXCore for Cordova (must be built without sqlite(3) built-in) - Delete an open database inside a statement or transaction callback. +- WITH clause (not supported by some older sqlite3 versions) +- Handling of invalid transaction and transaction.executeSql arguments +- Use of database locations on macOS +- Extremely large and small INTEGER and REAL values ref: [litehelpers/Cordova-sqlite-storage#627](https://github.com/litehelpers/Cordova-sqlite-storage/issues/627)) +- More emojis and other 4-octet UTF-8 characters +- `?NNN`/`:AAA`/`@AAAA`/`$AAAA` parameter placeholders ref: , ) +- Single-statement and SQL batch transaction calls with invalid arguments (TBD behavior subject to change) + + ## Some tips and tricks - If you run into problems and your code follows the asynchronous HTML5/[Web SQL](http://www.w3.org/TR/webdatabase/) transaction API, you can try opening a test database using `window.openDatabase` and see if you get the same problems. - In case your database schema may change, it is recommended to keep a table with one row and one column to keep track of your own schema version number. It is possible to add it later. The recommended schema update procedure is described below. -## Common pitfall(s) + + +## Pitfalls +### Some common pitfall(s) + +- If a database is opened using the standard `window.openDatabase` call it will not have any of the benefits of this plugin and features such as the `sqlBatch` call would not be available. - It is NOT allowed to execute sql statements on a transaction that has already finished, as described below. This is consistent with the HTML5/[Web SQL API](http://www.w3.org/TR/webdatabase/). - The plugin class name starts with "SQL" in capital letters, but in Javascript the `sqlitePlugin` object name starts with "sql" in small letters. - Attempting to open a database before receiving the 'deviceready' event callback. - Inserting STRING into ID field - Auto-vacuum is NOT enabled by default. It is recommended to periodically VACUUM the database. +- Transactions on a database are run sequentially. A large transaction could block smaller transactions requested afterwards. -## Weird pitfall(s) +### Some weird pitfall(s) - intent whitelist: blocked intent such as external URL intent *may* cause this and perhaps certain Cordova plugin(s) to misbehave (see [litehelpers/Cordova-sqlite-storage#396](https://github.com/litehelpers/Cordova-sqlite-storage/issues/396)) -## Angular/ngCordova/Ionic-related pitfalls +### Angular/ngCordova/Ionic-related pitfalls - Angular/ngCordova/Ionic controller/factory/service callbacks may be triggered before the 'deviceready' event is fired - As discussed in [litehelpers/Cordova-sqlite-storage#355](https://github.com/litehelpers/Cordova-sqlite-storage/issues/355), it may be necessary to install ionic-plugin-keyboard +- Navigation items such as root page can be tricky on Ionic 2 ref: [litehelpers/Cordova-sqlite-storage#613](https://github.com/litehelpers/Cordova-sqlite-storage/issues/613) + +### General Cordova pitfalls + +Documented in: [brodybits / Avoiding-some-Cordova-pitfalls](https://github.com/brodybits/Avoiding-some-Cordova-pitfalls) + + ## Major TODOs @@ -281,7 +330,9 @@ window.sqlitePlugin.selfTest(successCallback, errorCallback); ## General -The idea is to emulate the HTML5/[Web SQL API](http://www.w3.org/TR/webdatabase/) as closely as possible. The only major change is to use `window.sqlitePlugin.openDatabase()` (or `sqlitePlugin.openDatabase()`) *with parameters as documented below* instead of `window.openDatabase()`. If you see any other major change please report it, it is probably a bug. +- Drop-in replacement for HTML5/[Web SQL API](http://www.w3.org/TR/webdatabase/): the only change should be to replace the static `window.openDatabase()` factory call with `window.sqlitePlugin.openDatabase()`, with parameters as documented below. (Some known deviations are documented in newer version branches.) +- Single-page application design is recommended. +- In case of a multi-page application the JavaScript used by each page must use `sqlitePlugin.openDatabase` to open the database access handle object before it can access the data. **NOTE:** If a sqlite statement in a transaction fails with an error, the error handler *must* return `false` in order to recover the transaction. This is correct according to the HTML5/[Web SQL API](http://www.w3.org/TR/webdatabase/) standard. This is different from the WebKit implementation of Web SQL in Android and iOS which recovers the transaction if a sql error hander returns a non-`true` value. @@ -730,9 +781,27 @@ The transactional nature of the API makes it relatively straightforward to manag ## Use with Ionic/ngCordova/Angular -It is recommended to follow the tutorial at: https://blog.nraboy.com/2014/11/use-sqlite-instead-local-storage-ionic-framework/ +### Ionic 2 + +Tutorials with Ionic 2: +- (title is somewhat misleading, "SQL storage" *does* use this sqlite plugin) +- (older tutorial) + +Sample for Ionic 2 wanted ref: [litehelpers/Cordova-sqlite-storage#585](https://github.com/litehelpers/Cordova-sqlite-storage/issues/585) + +### Ionic 1 + +Tutorial with Ionic 1: + +A sample for Ionic 1 is provided at: [litehelpers / Ionic-sqlite-database-example](https://github.com/litehelpers/Ionic-sqlite-database-example) + +Documentation at: + +Other resource (apparently for Ionic 1): + +**NOTE:** Some Ionic and other Angular pitfalls are described above. -Documentation at: http://ngcordova.com/docs/plugins/sqlite/ + # Installing diff --git a/package.json b/package.json index 15c419575..e2077ab48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-sqlite-legacy-express-core", - "version": "1.0.0-pre4", + "version": "1.0.0", "description": "Native interface to SQLite for PhoneGap/Cordova (legacy express core version)", "cordova": { "id": "cordova-sqlite-legacy-express-core", diff --git a/plugin.xml b/plugin.xml index da9c2ced5..7cb41b37c 100644 --- a/plugin.xml +++ b/plugin.xml @@ -2,7 +2,7 @@ + version="1.0.0"> Cordova sqlite storage plugin - legacy express core version From 1e0fddfd1de8bb337176faa0c894b4bc05180c6a Mon Sep 17 00:00:00 2001 From: "Christopher J. Brody" Date: Fri, 27 Oct 2017 16:20:50 -0400 Subject: [PATCH 08/11] Fix bug 666 workaround to trigger ROLLBACK in next event tick (needed to support version with pre-populated database on Windows) --- CHANGES.md | 4 ++++ SQLitePlugin.coffee.md | 21 ++++++++++++--------- package.json | 2 +- plugin.xml | 2 +- spec/www/spec/db-open-close-delete-test.js | 12 ++++++++---- www/SQLitePlugin.js | 20 +++++++++++++------- 6 files changed, 39 insertions(+), 22 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4bd1f14a2..6b1c2daf8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ # Changes +###### cordova-sqlite-legacy-express-core 1.0.1 + +- Fix bug 666 workaround to trigger ROLLBACK in the next event tick (needed to support version with pre-populated database on Windows) + ###### cordova-sqlite-legacy-express-core 1.0.0 - Workaround solution to BUG litehelpers/Cordova-sqlite-storage#666 (hanging transaction in case of location reload/change) diff --git a/SQLitePlugin.coffee.md b/SQLitePlugin.coffee.md index 6e1313003..a3dc8c066 100644 --- a/SQLitePlugin.coffee.md +++ b/SQLitePlugin.coffee.md @@ -213,12 +213,14 @@ success @ return + # (done) + else console.log 'OPEN database: ' + @dbname opensuccesscb = => # NOTE: the db state is NOT stored (in @openDBs) if the db was closed or deleted. - # console.log 'OPEN database: ' + @dbname + ' succeeded' + console.log 'OPEN database: ' + @dbname + ' ok' #if !@openDBs[@dbname] then call open error cb, and abort pending tx if any if !@openDBs[@dbname] @@ -248,19 +250,21 @@ # store initial DB state: @openDBs[@dbname] = DB_STATE_INIT - # As a WORKAROUND SOLUTION to BUG litehelpers/Cordova-sqlite-storage#666: + # As a WORKAROUND SOLUTION to BUG litehelpers/Cordova-sqlite-storage#666 + # (in the next event tick): # If the database was never opened on the JavaScript side # start an extra ROLLBACK statement to abort any pending transaction # (does not matter whether it succeeds or fails here). # FUTURE TBD a better solution would be to send a special signal or parameter # if the database was never opened on the JavaScript side. - if not txLocks[@dbname] - myfn = (tx) -> - tx.addStatement 'ROLLBACK' - return - @addTransaction new SQLitePluginTransaction @, myfn, null, null, false, false + nextTick => + if not txLocks[@dbname] + myfn = (tx) -> + tx.addStatement 'ROLLBACK' + return + @addTransaction new SQLitePluginTransaction @, myfn, null, null, false, false - cordova.exec opensuccesscb, openerrorcb, "SQLitePlugin", "open", [ @openargs ] + cordova.exec opensuccesscb, openerrorcb, "SQLitePlugin", "open", [ @openargs ] return @@ -942,4 +946,3 @@ #### vim: set filetype=coffee : #### vim: set expandtab : - diff --git a/package.json b/package.json index e2077ab48..65ee03f73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-sqlite-legacy-express-core", - "version": "1.0.0", + "version": "1.0.1", "description": "Native interface to SQLite for PhoneGap/Cordova (legacy express core version)", "cordova": { "id": "cordova-sqlite-legacy-express-core", diff --git a/plugin.xml b/plugin.xml index 7cb41b37c..c67c46975 100644 --- a/plugin.xml +++ b/plugin.xml @@ -2,7 +2,7 @@ + version="1.0.1"> Cordova sqlite storage plugin - legacy express core version diff --git a/spec/www/spec/db-open-close-delete-test.js b/spec/www/spec/db-open-close-delete-test.js index 33957b6fd..31d710f8c 100755 --- a/spec/www/spec/db-open-close-delete-test.js +++ b/spec/www/spec/db-open-close-delete-test.js @@ -577,8 +577,10 @@ var mytests = function() { // XXX SEE BELOW: repeat scenario but wait for open callback before close/delete/reopen // Needed to support some large-scale applications: test_it(suiteName + ' immediate close, then delete then re-open allows subsequent queries to run', function () { - // TBD POSSIBLY BROKEN on iOS/macOS due to current background processing implementation: - if (!isAndroid && !isWindows && !isWP8) pending('POSSIBLY BROKEN on iOS/macOS (background processing implementation)'); + // TBD POSSIBLY BROKEN on iOS/macOS ... + // if (!isAndroid && !isWindows && !isWP8) pending(...); + // TBD CURRENTLY BROKEN DUE TO BUG 666 WORKAROUND HACK + pending('CURRENTLY BROKEN DUE TO BUG 666 WORKAROUND HACK'); var dbName = "Immediate-close-delete-Reopen.db"; var dbargs = {name: dbName, location: 'default'}; @@ -846,8 +848,10 @@ var mytests = function() { // Needed to support some large-scale applications: test_it(suiteName + ' repeatedly open and delete database faster (5x)', function () { - // TBD CURRENTLY BROKEN on iOS/macOS due to current background processing implementation: - if (!isAndroid && !isWindows && !isWP8) pending('CURRENTLY BROKEN on iOS/macOS (background processing implementation)'); + // TBD POSSIBLY BROKEN on iOS/macOS ... + // if (!isAndroid && !isWindows && !isWP8) pending(...); + // TBD CURRENTLY BROKEN DUE TO BUG 666 WORKAROUND HACK + pending('CURRENTLY BROKEN DUE TO BUG 666 WORKAROUND HACK'); var dbName = 'repeatedly-open-and-delete-faster-5x.db'; var dbargs = {name: dbName, location: 'default'}; diff --git a/www/SQLitePlugin.js b/www/SQLitePlugin.js index 476349c80..6d612d448 100644 --- a/www/SQLitePlugin.js +++ b/www/SQLitePlugin.js @@ -162,7 +162,7 @@ }; SQLitePlugin.prototype.open = function(success, error) { - var myfn, openerrorcb, opensuccesscb; + var openerrorcb, opensuccesscb; if (this.dbname in this.openDBs) { console.log('database already open: ' + this.dbname); nextTick((function(_this) { @@ -175,6 +175,7 @@ opensuccesscb = (function(_this) { return function() { var txLock; + console.log('OPEN database: ' + _this.dbname + ' ok'); if (!_this.openDBs[_this.dbname]) { console.log('database was closed during open operation'); } @@ -201,13 +202,18 @@ }; })(this); this.openDBs[this.dbname] = DB_STATE_INIT; - if (!txLocks[this.dbname]) { - myfn = function(tx) { - tx.addStatement('ROLLBACK'); + nextTick((function(_this) { + return function() { + var myfn; + if (!txLocks[_this.dbname]) { + myfn = function(tx) { + tx.addStatement('ROLLBACK'); + }; + _this.addTransaction(new SQLitePluginTransaction(_this, myfn, null, null, false, false)); + } + return cordova.exec(opensuccesscb, openerrorcb, "SQLitePlugin", "open", [_this.openargs]); }; - this.addTransaction(new SQLitePluginTransaction(this, myfn, null, null, false, false)); - } - cordova.exec(opensuccesscb, openerrorcb, "SQLitePlugin", "open", [this.openargs]); + })(this)); } }; From 7fa8c53a503829adf4c75d53accaa4676cc97882 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 5 Sep 2017 19:36:04 -0400 Subject: [PATCH 09/11] @craig-at-rsg add PSPDFThreadSafeMutableDictionary for iOS/macOS PSPDFThreadSafeMutableDictionary.m from https://gist.github.com/steipete/5928916 ref: litehelpers/Cordova-sqlite-storage#716 --- LICENSE.md | 2 + package.json | 2 +- plugin.xml | 2 +- src/ios/PSPDFThreadSafeMutableDictionary.h | 28 ++++ src/ios/PSPDFThreadSafeMutableDictionary.m | 159 +++++++++++++++++++++ 5 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 src/ios/PSPDFThreadSafeMutableDictionary.h create mode 100644 src/ios/PSPDFThreadSafeMutableDictionary.m diff --git a/LICENSE.md b/LICENSE.md index e621188a3..d257c7fcf 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -14,6 +14,8 @@ MIT only based on Phonegap-SQLitePlugin by @davibe (Davide Bertola ) and @joenoon (Joe Noon ) +includes PSPDFThreadSafeMutableDictionary (PSPDFThreadSafeMutableDictionary.m ) MIT license by @steipete () + ## REMOVED from this version branch: Windows (8.1/...) version MIT or Apache 2.0 diff --git a/package.json b/package.json index 65ee03f73..d2050a2d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-sqlite-legacy-express-core", - "version": "1.0.1", + "version": "1.0.2-pre00", "description": "Native interface to SQLite for PhoneGap/Cordova (legacy express core version)", "cordova": { "id": "cordova-sqlite-legacy-express-core", diff --git a/plugin.xml b/plugin.xml index c67c46975..74aad5c4b 100644 --- a/plugin.xml +++ b/plugin.xml @@ -2,7 +2,7 @@ + version="1.0.2-pre00"> Cordova sqlite storage plugin - legacy express core version diff --git a/src/ios/PSPDFThreadSafeMutableDictionary.h b/src/ios/PSPDFThreadSafeMutableDictionary.h new file mode 100644 index 000000000..05cd9ac1f --- /dev/null +++ b/src/ios/PSPDFThreadSafeMutableDictionary.h @@ -0,0 +1,28 @@ +// PSPDFThreadSafeMutableDictionary.h header copied from +// PSPDFThreadSafeMutableDictionary.m +// +// Copyright (c) 2013 Peter Steinberger, PSPDFKit GmbH. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +// Dictionary-Subclasss whose primitive operations are thread safe. +@interface PSPDFThreadSafeMutableDictionary : NSMutableDictionary +@end diff --git a/src/ios/PSPDFThreadSafeMutableDictionary.m b/src/ios/PSPDFThreadSafeMutableDictionary.m new file mode 100644 index 000000000..f8c53e210 --- /dev/null +++ b/src/ios/PSPDFThreadSafeMutableDictionary.m @@ -0,0 +1,159 @@ +// +// PSPDFThreadSafeMutableDictionary.m +// +// Copyright (c) 2013 Peter Steinberger, PSPDFKit GmbH. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +// Dictionary-Subclasss whose primitive operations are thread safe. +@interface PSPDFThreadSafeMutableDictionary : NSMutableDictionary +@end + +// ---------------------------------------------------------------- + +// +// PSPDFThreadSafeMutableDictionary.m +// PSPDFKit +// +// Copyright (c) 2013 PSPDFKit GmbH. All rights reserved. +// + +#import "PSPDFThreadSafeMutableDictionary.h" +#import + +#define LOCKED(...) OSSpinLockLock(&_lock); \ +__VA_ARGS__; \ +OSSpinLockUnlock(&_lock); + +@implementation PSPDFThreadSafeMutableDictionary { + OSSpinLock _lock; + NSMutableDictionary *_dictionary; // Class Cluster! +} + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - NSObject + +- (id)init { + return [self initWithCapacity:0]; +} + +- (id)initWithObjects:(NSArray *)objects forKeys:(NSArray *)keys { + if ((self = [self initWithCapacity:objects.count])) { + [objects enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + _dictionary[keys[idx]] = obj; + }]; + } + return self; +} + +- (id)initWithCapacity:(NSUInteger)capacity { + if ((self = [super init])) { + _dictionary = [[NSMutableDictionary alloc] initWithCapacity:capacity]; + _lock = OS_SPINLOCK_INIT; + } + return self; +} + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - NSMutableDictionary + +- (void)setObject:(id)anObject forKey:(id)aKey { + LOCKED(_dictionary[aKey] = anObject) +} + +- (void)addEntriesFromDictionary:(NSDictionary *)otherDictionary { + LOCKED([_dictionary addEntriesFromDictionary:otherDictionary]); +} + +- (void)setDictionary:(NSDictionary *)otherDictionary { + LOCKED([_dictionary setDictionary:otherDictionary]); +} + +- (void)removeObjectForKey:(id)aKey { + LOCKED([_dictionary removeObjectForKey:aKey]) +} + +- (void)removeAllObjects { + LOCKED([_dictionary removeAllObjects]); +} + +- (NSUInteger)count { + LOCKED(NSUInteger count = _dictionary.count) + return count; +} + +- (NSArray *)allKeys { + LOCKED(NSArray *allKeys = _dictionary.allKeys) + return allKeys; +} + +- (NSArray *)allValues { + LOCKED(NSArray *allValues = _dictionary.allValues) + return allValues; +} + +- (id)objectForKey:(id)aKey { + LOCKED(id obj = _dictionary[aKey]) + return obj; +} + +- (NSEnumerator *)keyEnumerator { + LOCKED(NSEnumerator *keyEnumerator = [_dictionary keyEnumerator]) + return keyEnumerator; +} + +- (id)copyWithZone:(NSZone *)zone { + return [self mutableCopyWithZone:zone]; +} + +- (id)mutableCopyWithZone:(NSZone *)zone { + LOCKED(id copiedDictionary = [[self.class allocWithZone:zone] initWithDictionary:_dictionary]) + return copiedDictionary; +} + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state + objects:(id __unsafe_unretained [])stackbuf + count:(NSUInteger)len { + LOCKED(NSUInteger count = [[_dictionary copy] countByEnumeratingWithState:state objects:stackbuf count:len]); + return count; +} + +- (void)performLockedWithDictionary:(void (^)(NSDictionary *dictionary))block { + if (block) LOCKED(block(_dictionary)); +} + +- (BOOL)isEqual:(id)object { + if (object == self) return YES; + + if ([object isKindOfClass:PSPDFThreadSafeMutableDictionary.class]) { + PSPDFThreadSafeMutableDictionary *other = object; + __block BOOL isEqual = NO; + [other performLockedWithDictionary:^(NSDictionary *dictionary) { + [self performLockedWithDictionary:^(NSDictionary *otherDictionary) { + isEqual = [dictionary isEqual:otherDictionary]; + }]; + }]; + return isEqual; + } + return NO; +} + +@end From 570d5e8a793f7441bb7996d809abd884569dc729 Mon Sep 17 00:00:00 2001 From: "Christopher J. Brody" Date: Tue, 5 Sep 2017 19:36:04 -0400 Subject: [PATCH 10/11] Fix PSPDFThreadSafeMutableDictionary.m redeclaration issue --- src/ios/PSPDFThreadSafeMutableDictionary.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ios/PSPDFThreadSafeMutableDictionary.m b/src/ios/PSPDFThreadSafeMutableDictionary.m index f8c53e210..2b39604b9 100644 --- a/src/ios/PSPDFThreadSafeMutableDictionary.m +++ b/src/ios/PSPDFThreadSafeMutableDictionary.m @@ -23,9 +23,11 @@ #import +/* ** ALREADY INCLUDED BY #import "PSPDFThreadSafeMutableDictionary.h" // Dictionary-Subclasss whose primitive operations are thread safe. @interface PSPDFThreadSafeMutableDictionary : NSMutableDictionary @end +// */ // ---------------------------------------------------------------- From 6d946d0cb0c2acd93a37fb404e94ccf0d690e6dd Mon Sep 17 00:00:00 2001 From: craig-at-rsg Date: Tue, 5 Sep 2017 19:37:43 -0400 Subject: [PATCH 11/11] Update SQLitePlugin.m & plugin.xml to use PSPDFThreadSafeMutableDictionary (iOS/macOS) with minor changes by @brodybits: - import PSPDFThreadSafeMutableDictionary in SQLitePlugin.m instead of SQLitePlugin.h - update SQLitePlugin.m & plugin.xml in single commit - update plugin version - update docs ref: litehelpers/Cordova-sqlite-storage#716 --- CHANGES.md | 4 ++++ LICENSE.md | 2 +- README.md | 1 + package.json | 2 +- plugin.xml | 10 +++++++++- src/ios/SQLitePlugin.m | 4 +++- 6 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6b1c2daf8..968eb4fe2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ # Changes +###### cordova-sqlite-legacy-express-core 1.0.2 + +- Use PSPDFThreadSafeMutableDictionary for iOS/macOS to avoid threading issue ref: litehelpers/Cordova-sqlite-storage#716 + ###### cordova-sqlite-legacy-express-core 1.0.1 - Fix bug 666 workaround to trigger ROLLBACK in the next event tick (needed to support version with pre-populated database on Windows) diff --git a/LICENSE.md b/LICENSE.md index d257c7fcf..23233c14e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -14,7 +14,7 @@ MIT only based on Phonegap-SQLitePlugin by @davibe (Davide Bertola ) and @joenoon (Joe Noon ) -includes PSPDFThreadSafeMutableDictionary (PSPDFThreadSafeMutableDictionary.m ) MIT license by @steipete () +includes and uses PSPDFThreadSafeMutableDictionary (PSPDFThreadSafeMutableDictionary.m ) MIT license by @steipete () ## REMOVED from this version branch: Windows (8.1/...) version diff --git a/README.md b/README.md index 5f6e44d3b..4e99f8554 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Use the `location` or `iosDatabaseLocation` option in `sqlitePlugin.openDatabase ## Announcements +- Fixed iOS/macOS platform version to use [PSPDFThreadSafeMutableDictionary.m](https://gist.github.com/steipete/5928916) to avoid threading issue ref: [litehelpers/Cordova-sqlite-storage#716](https://github.com/litehelpers/Cordova-sqlite-storage/issues/716) - Resolved transaction problem after window.location (page) change with possible data loss ref: [litehelpers/Cordova-sqlite-storage#666](https://github.com/litehelpers/Cordova-sqlite-storage/issues/666) - [brodybits / cordova-sqlite-test-app](https://github.com/brodybits/cordova-sqlite-test-app) project is a CC0 (public domain) starting point (NOTE that this plugin must be added) and may also be used to reproduce issues with this plugin. - The Lawnchair adapter is now moved to [litehelpers / cordova-sqlite-lawnchair-adapter](https://github.com/litehelpers/cordova-sqlite-lawnchair-adapter). diff --git a/package.json b/package.json index d2050a2d7..9950f0d74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-sqlite-legacy-express-core", - "version": "1.0.2-pre00", + "version": "1.0.2", "description": "Native interface to SQLite for PhoneGap/Cordova (legacy express core version)", "cordova": { "id": "cordova-sqlite-legacy-express-core", diff --git a/plugin.xml b/plugin.xml index 74aad5c4b..a0a873850 100644 --- a/plugin.xml +++ b/plugin.xml @@ -2,7 +2,7 @@ + version="1.0.2"> Cordova sqlite storage plugin - legacy express core version @@ -41,6 +41,10 @@ + + + + @@ -55,6 +59,10 @@ + + + + diff --git a/src/ios/SQLitePlugin.m b/src/ios/SQLitePlugin.m index d1ff85c4e..a56bf5ad2 100755 --- a/src/ios/SQLitePlugin.m +++ b/src/ios/SQLitePlugin.m @@ -10,6 +10,8 @@ #import "sqlite3.h" +#import "PSPDFThreadSafeMutableDictionary.h" + // FUTURE TBD (in another version branch): //#define READ_BLOB_AS_BASE64 @@ -27,7 +29,7 @@ -(void)pluginInitialize NSLog(@"Initializing SQLitePlugin"); { - openDBs = [NSMutableDictionary dictionaryWithCapacity:0]; + openDBs = [PSPDFThreadSafeMutableDictionary dictionaryWithCapacity:0]; appDBPaths = [NSMutableDictionary dictionaryWithCapacity:0]; #if !__has_feature(objc_arc) [openDBs retain];