UNPKG

ydn.db

Version:

Javascript database library for IndexedDB, WebDatabase (WebSQL) and WebStorage (localStorage) storage mechanisms supporting version migration, advanced query and transaction workflow.

491 lines (400 loc) 10.1 kB
// Copyright 2012 YDN Authors, Yathit. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview A SQL node represent . * * Analyze SQL statement and extract execution scope. */ goog.provide('ydn.db.Sql'); goog.require('goog.functions'); goog.require('ydn.db.KeyRange'); goog.require('ydn.db.Where'); goog.require('ydn.db.schema.Database'); goog.require('ydn.db.sql.req.IdbQuery'); goog.require('ydn.error.ArgumentException'); goog.require('ydn.math.Expression'); goog.require('ydn.string'); /** * @param {string} sql The sql statement. * @constructor */ ydn.db.Sql = function(sql) { /* * A query has the following form: * * <Query> := SELECT <SelList> FROM <FromList> WHERE <Condition> * */ if (!goog.isString(sql)) { throw new ydn.error.ArgumentException(); } this.sql_ = sql; /** * * @type {ydn.db.base.TransactionMode} * @private */ this.mode_ = ydn.db.base.TransactionMode.READ_WRITE; /** * @private * @type {Array.<string>} */ this.store_names_ = []; this.parseBasic_(sql); this.last_error_ = ''; this.has_parsed_ = false; }; /** * @private * @type {string} sql statement. */ ydn.db.Sql.prototype.sql_ = ''; /** * @private * @type {string} */ ydn.db.Sql.prototype.modifier_; /** * @private * @type {string} */ ydn.db.Sql.prototype.condition_; /** * * @type {string|undefined} * @private */ ydn.db.Sql.prototype.aggregate_; /** * * @type {string|undefined} * @private */ ydn.db.Sql.prototype.order_; /** * * @type {number} * @private */ ydn.db.Sql.prototype.limit_ = NaN; /** * * @type {number} * @private */ ydn.db.Sql.prototype.offset_ = NaN; /** * * @type {boolean} * @private */ ydn.db.Sql.prototype.reverse_ = false; /** * @type {string} * @private */ ydn.db.Sql.prototype.selList_; /** * * @type {string} * @private */ ydn.db.Sql.prototype.last_error_ = ''; /** * * @type {boolean} * @private */ ydn.db.Sql.prototype.has_parsed_ = false; /** * * @param {string} sql * @private */ ydn.db.Sql.prototype.parseBasic_ = function(sql) { var from_parts = sql.split(/\sFROM\s/i); if (from_parts.length != 2) { // throw new ydn.db.SqlParseError('FROM required.'); return; } var pre_from = from_parts[0]; var post_from = from_parts[1]; // Parse Pre-FROM var pre_from_parts = pre_from.match( /\s*?(SELECT|INSERT|UPDATE|DELETE)\s+(.*)/i); if (pre_from_parts.length != 3) { // throw new ydn.db.SqlParseError('Unable to parse: ' + sql); return; } // action this.action_ = pre_from_parts[1].toUpperCase(); if (this.action_ == 'SELECT') { this.mode_ = ydn.db.base.TransactionMode.READ_ONLY; } else if (this.action_ == 'INSERT') { this.mode_ = ydn.db.base.TransactionMode.READ_WRITE; } else if (this.action_ == 'UPDATE') { this.mode_ = ydn.db.base.TransactionMode.READ_WRITE; } else if (this.action_ == 'DELETE') { this.mode_ = ydn.db.base.TransactionMode.READ_WRITE; } else { return; } var selList = pre_from_parts[2].trim(); var agg = selList.match(/^(MIN|MAX|COUNT|AVG|SUM)/i); if (agg) { this.aggregate_ = agg[0].toUpperCase(); selList = selList.replace(/^(MIN|MAX|COUNT|AVG|SUM)/i, '').trim(); } else { this.aggregate_ = undefined; } // remove parentheses if it has if (selList.charAt(0) == '(') { if (selList.charAt(selList.length - 1) == ')') { selList = selList.substring(1, selList.length - 1); } else { new ydn.db.SqlParseError('missing closing parentheses'); } } this.selList_ = selList; // collect modifiers var mod_idx = post_from.search(/(ORDER BY|LIMIT|OFFSET)/i); if (mod_idx > 0) { this.modifier_ = post_from.substring(mod_idx); post_from = post_from.substring(0, mod_idx); } else { this.modifier_ = ''; } // collect condition var where_idx = post_from.search(/WHERE/i); if (where_idx > 0) { this.condition_ = post_from.substring(where_idx + 6).trim(); post_from = post_from.substring(0, where_idx); } else { this.condition_ = ''; } var stores = post_from.trim().split(','); this.store_names_ = stores.map(function(x) { x = goog.string.stripQuotes(x, '"'); x = goog.string.stripQuotes(x, "'"); return x.trim(); }); this.has_parsed_ = true; }; /** * @param {Array=} params SQL parameters. * @return {string} empty if successfully parse */ ydn.db.Sql.prototype.parse = function(params) { if (params) { for (var i = 0; i < params.length; i++) { this.sql_ = this.sql_.replace('?', params[i]); } this.parseBasic_(this.sql_); } this.wheres_ = this.parseConditions(); if (!this.wheres_) { return this.last_error_; } var start_idx = this.modifier_.length; var offset_result = /OFFSET\s+(\d+)/i.exec(this.modifier_); if (offset_result) { this.offset_ = parseInt(offset_result[1], 10); start_idx = this.modifier_.search(/OFFSET/i); } var limit_result = /LIMIT\s+(\d+)/i.exec(this.modifier_); if (limit_result) { this.limit_ = parseInt(limit_result[1], 10); var idx = this.modifier_.search(/LIMIT/i); if (idx < start_idx) { start_idx = idx; } } var order_str = this.modifier_.substr(0, start_idx); var order_result = /ORDER BY\s+(.+)/i.exec(order_str); if (order_result) { var order = order_result[1].trim(); var asc_desc = order.match(/(ASC|DESC)/i); if (asc_desc) { this.reverse_ = asc_desc[0].toUpperCase() == 'DESC'; order = order.replace(/\s+(ASC|DESC)/i, ''); } else { this.reverse_ = false; } this.order_ = goog.string.stripQuotes( goog.string.stripQuotes(order, '"'), "'"); goog.asserts.assert(this.order_.length > 0, 'Invalid order by field'); } else { this.order_ = undefined; } this.has_parsed_ = true; return ''; }; /** * Get select field list. * @return {Array.<string>} return null if selection is '*'. Field names are * trimmed. */ ydn.db.Sql.prototype.getSelList = function() { if (this.selList_ == '*') { return null; } else { var fields = this.selList_.split(','); fields = fields.map(function(s) { return goog.string.stripQuotes(s.trim(), '"'); }); return fields; } }; /** * * @return {string} */ ydn.db.Sql.prototype.getSql = function() { return this.sql_; }; /** * @inheritDoc */ ydn.db.Sql.prototype.toJSON = function() { return { 'sql': this.sql_ }; }; /** * * @return {!Array.<string>} store name. */ ydn.db.Sql.prototype.getStoreNames = function() { return goog.array.clone(this.store_names_); }; /** * * @return {ydn.db.base.TransactionMode} store name. */ ydn.db.Sql.prototype.getMode = function() { return this.mode_; }; /** * * @return {number} */ ydn.db.Sql.prototype.getLimit = function() { return this.limit_; }; /** * * @return {number} */ ydn.db.Sql.prototype.getOffset = function() { return this.offset_; }; /** * * @return {string|undefined} */ ydn.db.Sql.prototype.getOrderBy = function() { return this.order_; }; /** * * @return {boolean} */ ydn.db.Sql.prototype.isReversed = function() { return this.reverse_; }; /** * Get condition as array of Where clause. * @return {!Array.<ydn.db.Where>} */ ydn.db.Sql.prototype.getConditions = function() { return this.wheres_; }; /** * Get condition as array of Where clause. * @return {Array.<ydn.db.Where>} */ ydn.db.Sql.prototype.parseConditions = function() { var wheres = []; var re_op = /(.+?)(<=|>=|=|>|<)(.+)/i; var findIndex = function(field) { return goog.array.findIndex(wheres, function(w) { return w.getField() == field; }); }; if (this.condition_.length > 0) { var conds = this.condition_.split('AND'); for (var i = 0; i < conds.length; i++) { var cond = conds[i]; var result = re_op.exec(cond); if (result) { var field = result[1].trim(); field = goog.string.stripQuotes(field, '"'); field = goog.string.stripQuotes(field, "'"); if (field.length > 0) { var value = result[3].trim(); if (goog.string.startsWith(value, '"')) { value = goog.string.stripQuotes(value, '"'); } else if (goog.string.startsWith(value, "'")) { value = goog.string.stripQuotes(value, "'"); } else { value = parseFloat(value); //console.log([cond, result[1], result[2], result[3], value]); } var op = result[2]; var where = new ydn.db.Where(field, op, value); var ex_idx = findIndex(field); if (ex_idx >= 0) { wheres[ex_idx] = wheres[ex_idx].and(where); if (!wheres[ex_idx]) { this.last_error_ = 'where clause "' + cond + '" conflict'; return null; } } else { wheres.push(where); } } else { this.last_error_ = 'Invalid clause "' + cond + '"'; return null; } } else { this.last_error_ = 'Invalid clause "' + cond + '"'; return null; } } } return wheres; }; /** * * @return {string} store name. */ ydn.db.Sql.prototype.getAction = function() { return this.action_; }; /** * @override */ ydn.db.Sql.prototype.toString = function() { if (goog.DEBUG) { return 'query:' + this.sql_; } else { return goog.base(this, 'toString'); } }; /** * @return {string|undefined} return aggregate or undefined. */ ydn.db.Sql.prototype.getAggregate = function() { return this.aggregate_; };