├── 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 |
--------------------------------------------------------------------------------