├── README.md ├── test └── index.html └── js └── purejswebsql.js /README.md: -------------------------------------------------------------------------------- 1 | # 100% JavaScript implementation of Web SQL API 2 | Pure-JS-WebSQL is an implementation of [Web SQL Database API](http://www.w3.org/TR/webdatabase/) in pure JavaScript. 3 | The implementation provides a glue between Web SQL Database API and [SQL.js](https://github.com/kripken/sql.js) (SQLite port to JavaScript). The data between sessions is stored in the `localStorage`. 4 | 5 | ## Demo 6 | [Pure-JS-WebSQL Demo](http://yradtsevich.github.io/pure-js-websql/test/index.html). It should work in any Gecko- or WebKit-based browser. 7 | 8 | ## Usage 9 | 10 | ```html 11 | 12 | 13 | 15 | 16 | 17 | 31 | 32 | 33 | ``` 34 | ## License 35 | Pure-JS-WebSQL is released under the [MIT license](http://opensource.org/licenses/MIT). 36 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 45 | 46 | 47 | 48 | 74 |
75 | 76 | 77 |
78 | 79 | 80 | -------------------------------------------------------------------------------- /js/purejswebsql.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Pure-JS-WebSQL JavaScript Library 3 | * 4 | * Copyright 2013 Red Hat, Inc. 5 | * Released under the MIT license 6 | * http://opensource.org/licenses/MIT 7 | * 8 | * Author: Yahor Radtsevich 9 | */ 10 | (function(window) { 11 | 12 | var DOMEx = function(name, code, description) { 13 | this.message = name + ': DOM Exception ' + code; 14 | this.name = name; 15 | this.code = code; 16 | this.stack = (new Error(description)).stack; 17 | }; 18 | DOMEx.prototype = DOMException.prototype; 19 | DOMEx.__proto__ = DOMException.prototype; 20 | DOMEx.prototype.constructor = DOMEx; 21 | 22 | var SQLEr = function(message, code) { 23 | this.message = message; 24 | this.code = code; 25 | this.stack = (new Error(message)).stack; 26 | } 27 | if (window.SQLException) { 28 | SQLEr.prototype = SQLException.prototype; 29 | SQLEr.__proto__ = SQLException.prototype; 30 | } 31 | SQLEr.prototype.constructor = SQLEr; 32 | 33 | SQLEr.prototype.toString = DOMEx.prototype.toString = function() { 34 | return 'Error: ' + this.message; 35 | } 36 | 37 | function asyncExec(f) { 38 | setTimeout(f, 0); 39 | } 40 | 41 | function mysql_real_escape_string(str) { //http://stackoverflow.com/questions/7744912/making-a-javascript-string-sql-friendly 42 | return str.replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, function (char) { 43 | switch (char) { 44 | case "\0": 45 | return "\\0"; 46 | case "\x08": 47 | return "\\b"; 48 | case "\x09": 49 | return "\\t"; 50 | case "\x1a": 51 | return "\\z"; 52 | case "\n": 53 | return "\\n"; 54 | case "\r": 55 | return "\\r"; 56 | case "\"": 57 | case "'": 58 | case "\\": 59 | case "%": 60 | return "\\"+char; // prepends a backslash to backslash, percent, 61 | // and double/single quotes 62 | } 63 | }); 64 | } 65 | 66 | function throwTypeMismatchErrorIfStringOrNumber(value) { 67 | if (typeof(value) === 'string' || typeof(value) === 'number') { 68 | throw new DOMEx('TypeMismatchError', 17 /*DOMException.TYPE_MISMATCH_ERR*/, 'The type of an object was incompatible with the expected type of the parameter associated to the object.'); 69 | } 70 | } 71 | 72 | function convertType(val) { 73 | if (val === 'true') { 74 | return true; 75 | } 76 | if (val === 'false') { 77 | return false; 78 | } 79 | if (val === 'null') { 80 | return null; 81 | } 82 | if (isFinite(val)) { 83 | var n = parseFloat(val); 84 | if (!isNaN(n)) { 85 | return n; 86 | } 87 | } 88 | return val; 89 | } 90 | 91 | 92 | function sqlEscape(value) { 93 | return mysql_real_escape_string(String(value)); 94 | } 95 | function replaceValues(statement, values) { 96 | for (var i = 0; i < values.length; i++) { 97 | statement = statement.replace('?', "'" + sqlEscape(values[i]) + "'"); //TODO: skip escaped question mark 98 | } 99 | return statement; 100 | } 101 | 102 | var dbMap = {}; //XXX: memory leaks here if there are multiple databases - need a weak reference 103 | if (localStorage) { // we can persist our DB only if the browser supports localStorage 104 | window.addEventListener('unload', function() { 105 | for (var name in dbMap) { 106 | var data = dbMap[name].db.exportData(); 107 | localStorage['_db_data_' + name] = String.fromCharCode.apply(null, data); 108 | localStorage['_db_version_' + name] = JSON.stringify(dbMap[name].version); 109 | } 110 | }); 111 | } 112 | 113 | var purejsOpenDatabase = function(name, version, displayName, estimatedSize, creationCallback) { 114 | var Database = function(name, _db) { 115 | this._name = name; 116 | this._db = _db; 117 | }; 118 | var databaseTransaction = function(callback, errorCallback, successCallback) { 119 | var that = this; 120 | asyncExec(function() { 121 | that._db.exec('BEGIN TRANSACTION;'); 122 | 123 | var Transaction = function() { 124 | this._db = that._db; 125 | this._executeSqlQueue = []; 126 | }; 127 | Transaction.prototype.executeSql = function(sqlStatement, values, callback, errorCallback) { 128 | if (arguments.length === 0) { 129 | throw new DOMEx('SyntaxError', 12 /*DOMException.SYNTAX_ERR*/, 'An invalid or illegal string was specified.'); 130 | } 131 | throwTypeMismatchErrorIfStringOrNumber(values); 132 | throwTypeMismatchErrorIfStringOrNumber(callback); 133 | throwTypeMismatchErrorIfStringOrNumber(errorCallback); 134 | 135 | values = values || []; 136 | sqlStatement = String(sqlStatement); 137 | this._executeSqlQueue.push({ 138 | sql : replaceValues(sqlStatement, values), 139 | callback : callback, 140 | errorCallback : errorCallback 141 | }); 142 | }; 143 | var tx = new Transaction(); 144 | callback(tx); 145 | 146 | var success = true; 147 | try { 148 | for (var k = 0; k < tx._executeSqlQueue.length; k++) { 149 | var executeSqlEntry = tx._executeSqlQueue[k]; 150 | 151 | var rows = new Array(); 152 | rows.item = function(i) {return this[i]}; 153 | 154 | var data = null; 155 | var rowsAffected; 156 | var insertId = null; 157 | try { 158 | var previousTotalChanges = that._db.totalChanges; 159 | 160 | data = that._db.exec(executeSqlEntry.sql); 161 | 162 | var lastInfo = that._db.exec('SELECT total_changes(), last_insert_rowid()'); 163 | that._db.totalChanges = lastInfo[0][0].value; 164 | rowsAffected = that._db.totalChanges - previousTotalChanges; 165 | if (rowsAffected > 0) {// XXX: works wrong when DELETE executed 166 | insertId = lastInfo[0][1].value | 0; 167 | } 168 | } catch (e) { 169 | if (typeof(e)==='string') { 170 | e = new SQLEr(e, SQLException.SYNTAX_ERR); 171 | } 172 | if (typeof(executeSqlEntry.errorCallback) === "function") { 173 | var noSuccess = false; 174 | try { 175 | noSuccess = executeSqlEntry.errorCallback(tx, e); 176 | } catch (e) { 177 | noSuccess = true; 178 | } 179 | if (noSuccess) { 180 | throw new SQLEr('the statement callback raised an exception or statement error callback did not return false', SQLException.UNKNOWN_ERR); 181 | } 182 | } else { 183 | throw e; 184 | } 185 | } 186 | 187 | if (data != null) { 188 | for (var i = 0; i < data.length; i++) { 189 | var row = {}; 190 | for (var j = 0; j < data[i].length; j++) { 191 | row[ data[i][j].column ] = convertType(data[i][j].value); // XXX: now converts to the most suitable type, but the type is specified in db 192 | } 193 | rows[i] = row; 194 | } 195 | 196 | if (typeof(executeSqlEntry.callback) === "function") { 197 | var resultSet = { 198 | get insertId() { 199 | if (insertId !== null) { 200 | return insertId; 201 | } else { 202 | throw new DOMEx('InvalidAccessError', 15 /*DOMException.INVALID_ACCESS_ERR*/, 'A parameter or an operation was not supported by the underlying object.'); 203 | } 204 | }, 205 | rowsAffected : rowsAffected, 206 | rows : rows 207 | }; 208 | executeSqlEntry.callback(tx, resultSet); 209 | } 210 | } 211 | } 212 | } catch (e) { 213 | success = false; 214 | that._db.exec('ROLLBACK;'); 215 | if (typeof(errorCallback) === "function") { 216 | errorCallback(e); 217 | } 218 | } 219 | 220 | if (success) { 221 | that._db.exec('COMMIT;'); 222 | if (typeof(successCallback) === "function") { 223 | asyncExec(successCallback); 224 | } 225 | } 226 | }); 227 | }; 228 | Database.prototype = { 229 | transaction : databaseTransaction, 230 | readTransaction : databaseTransaction, // XXX - probably need to remove BEGIN TRANSACTION/COMMIT for this implementation 231 | get version() { 232 | return dbMap[this._name].version; 233 | }, 234 | set version(ver) {// changeVersion() must be used 235 | }, 236 | changeVersion : function(oldVersion, newVersion, callback, errorCallback, successCallback) { 237 | if (oldVersion != this.version) { 238 | if (errorCallback) { 239 | asyncExec(function() { 240 | errorCallback(new SQLEr('current version of the database and `oldVersion` argument do not match', SQLException.VERSION_ERR)); 241 | }); 242 | } 243 | } else { 244 | dbMap[this._name].version = newVersion; 245 | if (callback) { 246 | this.transaction(callback, errorCallback, successCallback); 247 | } else if (successCallback) { 248 | successCallback(); 249 | } 250 | } 251 | } 252 | }; 253 | 254 | var _db; 255 | var created; 256 | if (dbMap[name]) { 257 | _db = dbMap[name].db; 258 | var storedVersion = dbMap[name].version; 259 | 260 | if (version !== '' && storedVersion != version) { 261 | throw new DOMEx('InvalidStateError', 11 /*DOMException.INVALID_STATE_ERR*/, 'An attempt was made to use an object that is not, or is no longer, usable.'); 262 | } 263 | created = false; 264 | } else if (localStorage && localStorage['_db_data_' + name]) { 265 | var data = localStorage['_db_data_' + name].split('').map(function(c) {return c.charCodeAt(0);}); 266 | _db = SQL.open(data); 267 | var storedVersion = JSON.parse(localStorage['_db_version_' + name]); 268 | 269 | if (version !== '' && storedVersion != version) { 270 | throw new DOMEx('InvalidStateError', 11 /*DOMException.INVALID_STATE_ERR*/, 'An attempt was made to use an object that is not, or is no longer, usable.'); 271 | } 272 | created = false; 273 | } else { 274 | _db = SQL.open(); 275 | created = true; 276 | } 277 | 278 | _db.totalChanges = _db.totalChanges | 0; 279 | var database = new Database(name, _db); 280 | dbMap[name] = {db : _db}; 281 | 282 | if (created) { 283 | dbMap[name].version = ''; 284 | if (creationCallback) { 285 | asyncExec(function() { 286 | creationCallback(database); 287 | }); 288 | } else { 289 | dbMap[name].version = version; 290 | } 291 | } else { 292 | dbMap[name].version = storedVersion; 293 | } 294 | 295 | return database; 296 | } 297 | 298 | window.purejsOpenDatabase = purejsOpenDatabase; 299 | 300 | })(window); 301 | --------------------------------------------------------------------------------