UNPKG

ydn.db

Version:

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

1,326 lines (1,143 loc) 35.7 kB
// Copyright 2012 YDN Authors. 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 Represent object store. * * @author kyawtun@yathit.com (Kyaw Tun) */ goog.provide('ydn.db.schema.Store'); goog.require('goog.array.ArrayLike'); goog.require('ydn.db.KeyRange'); goog.require('ydn.db.Request.Method'); goog.require('ydn.db.schema.Index'); /** * Create a store schema. * @param {string} name object store name or TABLE name. * @param {(Array.<string>|string)=} opt_key_path indexedDB keyPath, like * 'feed.id.$t'. A path to extract primary key from record value. * @param {boolean=} opt_autoIncrement If true, the object store has a key * generator. Defaults to false. * @param {string|ydn.db.schema.DataType=} opt_type data type for keyPath. This * value is only used by WebSQL for column data type. * <code>ydn.db.schema.DataType.INTEGER</code> if opt_autoIncrement is * <code>true.</code> * @param {!Array.<!ydn.db.schema.Index>=} opt_indexes list of indexes. * @param {boolean=} opt_dispatch_events if true, storage instance should * dispatch event on record values changes. * @param {boolean=} opt_is_fixed fixed store schema. Websql TABLE, has a * default column to store JSON stringify data. A fixed store schema TABLE, * do not hae that default column. * @param {boolean=} opt_encrypted store is encrypted. * @constructor * @struct */ ydn.db.schema.Store = function(name, opt_key_path, opt_autoIncrement, opt_type, opt_indexes, opt_dispatch_events, opt_is_fixed, opt_encrypted) { if (!goog.isString(name)) { throw new ydn.debug.error.ArgumentException('store name must be a string'); } /** * @private * @final * @type {string} */ this.name_ = name; /** * @final * @type {(Array<string>|string)} */ this.keyPath = goog.isDef(opt_key_path) ? opt_key_path : null; /** * @final * @type {boolean} */ this.isComposite = goog.isArrayLike(this.keyPath); if (!goog.isNull(this.keyPath) && !goog.isString(this.keyPath) && !this.isComposite) { throw new ydn.debug.error.ArgumentException( 'keyPath must be a string or array'); } if (goog.DEBUG) { if (goog.isDefAndNotNull(opt_autoIncrement) && !goog.isBoolean(opt_autoIncrement)) { throw new ydn.debug.error.ArgumentException('invalid autoIncrement value ' + 'in store "' + name + '"'); } } /** * IE10 do not reflect autoIncrement, so that make undefined as an option. * @final * @type {boolean|undefined} */ this.autoIncrement = !!opt_autoIncrement; var type; if (goog.isDefAndNotNull(opt_type)) { type = ydn.db.schema.Index.toType(opt_type); if (!goog.isDef(type)) { throw new ydn.debug.error.ArgumentException('type "' + opt_type + '" for primary key in store "' + this.name_ + '" is invalid.'); } if (this.isComposite) { throw new ydn.debug.error.ArgumentException( 'composite key for store "' + this.name_ + '" must not specified type'); } } /** * @final * @type {ydn.db.schema.DataType|undefined} */ this.type = goog.isDefAndNotNull(type) ? type : this.autoIncrement ? ydn.db.schema.DataType.INTEGER : undefined; /** * @final * @type {!Array<string>} */ this.keyPaths = goog.isString(this.keyPath) ? this.keyPath.split('.') : []; /** * @final * @type {!Array.<!ydn.db.schema.Index>} */ this.indexes = opt_indexes || []; var names = []; for (var i = 0; i < this.indexes.length; i++) { var i_name = this.indexes[i].getName(); if (names.indexOf(i_name) >= 0) { throw new ydn.debug.error.ArgumentException('index "' + i_name + '" already defined in store: ' + this.name_); } names.push(i_name); } /** * @final * @type {boolean} */ this.dispatch_events = !!opt_dispatch_events; /** * @final * @type {boolean} */ this.fixed = !!opt_is_fixed; /** * @final * @private * @type {ydn.db.schema.DataType} */ this.keyColumnType_ = goog.isString(this.type) ? this.type : ydn.db.schema.DataType.TEXT; /** * @final * @private * @type {string} */ this.primary_column_name_ = goog.isArray(this.keyPath) ? this.keyPath.join(',') : goog.isString(this.keyPath) ? this.keyPath : ydn.db.base.SQLITE_SPECIAL_COLUNM_NAME; /** * @final * @private * @type {string} */ this.primary_column_name_quoted_ = goog.string.quote(this.primary_column_name_); /** * @final * @type {boolean} * @private */ this.is_encrypted_ = !!opt_encrypted; if (goog.DEBUG && this.is_encrypted_) { if (this.keyPath) { throw new ydn.debug.error.ArgumentException('encrypted store "' + this.name_ + '" must not use inline key'); } if (this.isAutoIncrement()) { throw new ydn.debug.error.ArgumentException('encrypted store "' + this.name_ + '" must not use key generator'); } } /** * @final * @type {Array.<function(!ydn.db.Request, goog.array.ArrayLike)>} hookers. * @private */ this.hooks_ = []; }; /** * @enum {string} */ ydn.db.schema.Store.FetchStrategy = { LAST_UPDATED: 'last-updated', ASCENDING_KEY: 'ascending-key', DESCENDING_KEY: 'descending-key' }; /** * @const * @type {Array.<ydn.db.schema.Store.FetchStrategy>} */ ydn.db.schema.Store.FetchStrategies = [ ydn.db.schema.Store.FetchStrategy.LAST_UPDATED, ydn.db.schema.Store.FetchStrategy.ASCENDING_KEY, ydn.db.schema.Store.FetchStrategy.DESCENDING_KEY]; /** * @type {boolean} * @private */ ydn.db.schema.Store.prototype.isComposite; /** * @type {(!Array.<string>|string)?} */ ydn.db.schema.Store.prototype.keyPath; /** * @type {boolean|undefined} */ ydn.db.schema.Store.prototype.autoIncrement; /** * @type {ydn.db.schema.DataType|undefined} // */ ydn.db.schema.Store.prototype.type; /** * @private * @type {ydn.db.schema.DataType} */ ydn.db.schema.Store.prototype.keyColumnType_; /** * @protected * @type {!Array.<string>} */ ydn.db.schema.Store.prototype.keyPaths; /** * @type {!Array.<!ydn.db.schema.Index>} * @protected */ ydn.db.schema.Store.prototype.indexes; /** * @type {boolean} */ ydn.db.schema.Store.prototype.dispatch_events = false; /** * A fixed schema cannot store arbitrary data structure. This is used only * in WebSQL. A arbitrery data structure require default blob column. * @type {boolean} */ ydn.db.schema.Store.prototype.fixed = false; /** * @inheritDoc */ ydn.db.schema.Store.prototype.toJSON = function() { var indexes = []; for (var i = 0; i < this.indexes.length; i++) { indexes.push(this.indexes[i].toJSON()); } return { 'name': this.name_, 'keyPath': this.keyPath, 'autoIncrement': this.autoIncrement, 'type': this.type, 'indexes': indexes }; }; /** * * @param {!StoreSchema} json Restore from json stream. * @return {!ydn.db.schema.Store} create new store schema from JSON string. */ ydn.db.schema.Store.fromJSON = function(json) { if (goog.DEBUG) { var fields = ['name', 'keyPath', 'autoIncrement', 'type', 'indexes', 'dispatchEvents', 'fixed', 'Sync', 'encrypted']; for (var key in json) { if (json.hasOwnProperty(key) && goog.array.indexOf(fields, key) == -1) { throw new ydn.debug.error.ArgumentException('Unknown attribute "' + key + '"'); } } } var indexes = []; var indexes_json = json.indexes || []; if (goog.isArray(indexes_json)) { for (var i = 0; i < indexes_json.length; i++) { var index = ydn.db.schema.Index.fromJSON(indexes_json[i]); if (goog.isDef(index.keyPath) && index.keyPath === json.keyPath) { continue; // key do not need indexing. } indexes.push(index); } } var type = json.type === 'undefined' || json.type === 'null' ? undefined : json.type; return new ydn.db.schema.Store(json.name, json.keyPath, json.autoIncrement, type, indexes, json.dispatchEvents, json.fixed, json.encrypted); }; /** * * @param {!Array} params sql parameter list. * @param {ydn.db.base.QueryMethod} method query method. * @param {string|undefined} index_column name. * @param {IDBKeyRange} key_range to retrieve. * @param {boolean} reverse ordering. * @param {boolean} unique unique column. * @return {string} sql statement. */ ydn.db.schema.Store.prototype.toSql = function(params, method, index_column, key_range, reverse, unique) { var out = this.inSql(params, method, index_column, key_range, reverse, unique); var sql = ''; if (method != ydn.db.base.QueryMethod.NONE) { sql += 'SELECT ' + out.select; } sql += ' FROM ' + out.from; if (out.where) { sql += ' WHERE ' + out.where; } if (out.group) { sql += ' GROUP BY ' + out.group; } if (out.order) { sql += ' ORDER BY ' + out.order; } return sql; }; /** * @typedef {{ * select: string, * from: string, * where: string, * group: string, * order: string * }} */ ydn.db.schema.Store.SqlParts; /** * * @param {!Array} params sql parameter list. * @param {ydn.db.base.QueryMethod} method query method. * @param {string|undefined} index_column name. * @param {ydn.db.KeyRange|IDBKeyRange} key_range to retrieve. * @param {boolean} reverse ordering. * @param {boolean} unique unique. * @return {ydn.db.schema.Store.SqlParts} */ ydn.db.schema.Store.prototype.inSql = function(params, method, index_column, key_range, reverse, unique) { var out = { select: '', from: '', where: '', group: '', order: '' }; var key_column = this.primary_column_name_; var q_key_column = this.primary_column_name_quoted_; var index = null; if (index_column !== key_column && goog.isString(index_column)) { index = this.getIndex(index_column); } var is_index = !!index; var effective_column = index_column || key_column; var q_effective_column = goog.string.quote(effective_column); var key_path = is_index ? index.getKeyPath() : this.getKeyPath(); var type = is_index ? index.getType() : this.getType(); var is_multi_entry = is_index && index.isMultiEntry(); out.from = this.getQuotedName(); if (method === ydn.db.base.QueryMethod.COUNT) { // primary key is always unqiue. out.select = 'COUNT(' + q_key_column + ')'; } else if (method === ydn.db.base.QueryMethod.LIST_KEYS || method === ydn.db.base.QueryMethod.LIST_KEY || method === ydn.db.base.QueryMethod.LIST_PRIMARY_KEY) { out.select = q_key_column; if (goog.isDefAndNotNull(index_column) && index_column != key_column) { out.select += ', ' + q_effective_column; } } else { out.select = '*'; } var dist = unique ? 'DISTINCT ' : ''; var wheres = []; if (is_multi_entry) { var idx_store_name = goog.string.quote( ydn.db.base.PREFIX_MULTIENTRY + this.getName() + ':' + index.getName()); if (method === ydn.db.base.QueryMethod.COUNT) { out.select = 'COUNT(' + dist + idx_store_name + '.' + q_effective_column + ')'; } else if (method === ydn.db.base.QueryMethod.LIST_KEYS || method === ydn.db.base.QueryMethod.LIST_KEY || method === ydn.db.base.QueryMethod.LIST_PRIMARY_KEY) { out.select = 'DISTINCT ' + this.getQuotedName() + '.' + q_key_column + ', ' + idx_store_name + '.' + q_effective_column + ' AS ' + effective_column; } else { out.select = 'DISTINCT ' + this.getQuotedName() + '.*' + ', ' + idx_store_name + '.' + q_effective_column + ' AS ' + effective_column; } out.from = idx_store_name + ' INNER JOIN ' + this.getQuotedName() + ' USING (' + q_key_column + ')'; var col = idx_store_name + '.' + q_effective_column; if (goog.isDefAndNotNull(key_range)) { ydn.db.KeyRange.toSql(col, type, key_range, wheres, params); if (wheres.length > 0) { if (out.where) { out.where += ' AND ' + wheres.join(' AND '); } else { out.where = wheres.join(' AND '); } } } } else { if (goog.isDefAndNotNull(key_range)) { ydn.db.KeyRange.toSql(q_effective_column, type, key_range, wheres, params); if (wheres.length > 0) { if (out.where) { out.where += ' AND ' + wheres.join(' AND '); } else { out.where = wheres.join(' AND '); } } } } if (is_index && !index.isUnique() && unique) { out.group = q_effective_column; } var dir = reverse ? 'DESC' : 'ASC'; out.order = q_effective_column + ' ' + dir; if (is_index) { out.order += ', ' + q_key_column + ' ' + dir; } return out; }; /** * Continue to given effective key position. * @param {ydn.db.base.QueryMethod} method query method. * @param {!Array.<string>} params sql params. * @param {string?} index_name index name. * @param {IDBKeyRange|ydn.db.KeyRange} key_range key range. * @param {boolean} reverse ordering. * @param {boolean} unique unique. * @param {IDBKey} key effective key. * @param {boolean} open open bound. * @return {string} sql. */ ydn.db.schema.Store.prototype.sqlContinueEffectiveKey = function(method, params, index_name, key_range, reverse, unique, key, open) { var p_sql; /** @type {IDBKey} */ var lower; /** @type {IDBKey} */ var upper; var lowerOpen, upperOpen; if (goog.isDefAndNotNull(key_range)) { lower = /** @type {IDBKey} */ (key_range.lower); upper = /** @type {IDBKey} */ (key_range.upper); lowerOpen = key_range.lowerOpen; upperOpen = key_range.upperOpen; if (reverse) { if (goog.isDefAndNotNull(upper)) { var u_cmp = ydn.db.cmp(key, upper); if (u_cmp == -1) { upper = key; upperOpen = open; } else if (u_cmp == 0) { upperOpen = open || upperOpen; } } else { upper = key; upperOpen = open; } } else { if (goog.isDefAndNotNull(lower)) { var l_cmp = ydn.db.cmp(key, lower); if (l_cmp == 1) { lower = key; lowerOpen = open; } else if (l_cmp == 0) { lowerOpen = open || lowerOpen; } } else { lower = key; lowerOpen = open; } } } else { if (reverse) { upper = key; upperOpen = open; } else { lower = key; lowerOpen = open; } } key_range = new ydn.db.KeyRange(lower, upper, !!lowerOpen, !!upperOpen); var index = index_name ? this.getIndex(index_name) : null; var column = index ? index.getSQLIndexColumnName() : this.getSQLKeyColumnName(); var e_sql = this.inSql(params, method, column, key_range, reverse, unique); var sql = 'SELECT ' + e_sql.select + ' FROM ' + e_sql.from + (e_sql.where ? ' WHERE ' + e_sql.where : '') + (e_sql.group ? ' GROUP BY ' + e_sql.group : '') + ' ORDER BY ' + e_sql.order; if (index) { var order = reverse ? 'DESC' : 'ASC'; sql += ', ' + this.getSQLKeyColumnNameQuoted() + order; } return sql; }; /** * Continue to given effective key position. * @param {ydn.db.base.QueryMethod} method query method. * @param {!Array.<string>} params sql params. * @param {string} index_name index name. * @param {IDBKeyRange|ydn.db.KeyRange} key_range key range. * @param {IDBKey} key effective key. * @param {boolean} open open. * @param {IDBKey} primary_key primary key. * @param {boolean} reverse ordering. * @param {boolean} unique unique. * @return {string} sql. */ ydn.db.schema.Store.prototype.sqlContinueIndexEffectiveKey = function(method, params, index_name, key_range, key, open, primary_key, reverse, unique) { var index = this.getIndex(index_name); var index_column = index.getSQLIndexColumnName(); var q_index_column = index.getSQLIndexColumnNameQuoted(); var primary_column = this.getSQLKeyColumnName(); var q_primary_column = this.getSQLKeyColumnNameQuoted(); var op = reverse ? ' <' : ' >'; if (open) { op += ' '; } else { op += '= '; } var encode_key = ydn.db.schema.Index.js2sql(key, index.getType()); var encode_primary_key = ydn.db.schema.Index.js2sql(primary_key, this.getType()); var e_sql; var or = ''; if (key_range) { e_sql = this.inSql(params, method, index_column, key_range, reverse, unique); e_sql.where += ' AND '; or = q_index_column + op + '?'; params.push(encode_key); } else { key_range = reverse ? ydn.db.KeyRange.upperBound(key, true) : ydn.db.KeyRange.lowerBound(key, true); e_sql = this.inSql(params, method, index_column, key_range, reverse, unique); or = e_sql.where; e_sql.where = ''; } e_sql.where += '(' + or + ' OR (' + q_index_column + ' = ? AND ' + q_primary_column + op + '?))'; params.push(encode_key); params.push(encode_primary_key); return 'SELECT ' + e_sql.select + ' FROM ' + e_sql.from + ' WHERE ' + e_sql.where + (e_sql.group ? ' GROUP BY ' + e_sql.group : '') + ' ORDER BY ' + e_sql.order; }; /** * * @return {!ydn.db.schema.Store} clone this database schema. */ ydn.db.schema.Store.prototype.clone = function() { return ydn.db.schema.Store.fromJSON( /** @type {!StoreSchema} */ (this.toJSON())); }; /** * * @return {number} */ ydn.db.schema.Store.prototype.countIndex = function() { return this.indexes.length; }; /** * * @param {number} idx index of index. * @return {ydn.db.schema.Index} */ ydn.db.schema.Store.prototype.index = function(idx) { return this.indexes[idx] || null; }; /** * * @param {string} name index name. * @return {ydn.db.schema.Index} index if found. */ ydn.db.schema.Store.prototype.getIndex = function(name) { return /** @type {ydn.db.schema.Index} */ (goog.array.find(this.indexes, function(x) { return x.getName() == name; })); }; /** * Query index from index key path. * @param {string|!Array.<string>} key_path key path. * @return {ydn.db.schema.Index} resulting index. */ ydn.db.schema.Store.prototype.getIndexByKeyPath = function(key_path) { for (var i = 0; i < this.indexes.length; i++) { if (this.indexes[i].equalsKeyPath(key_path)) { return this.indexes[i]; } } return null; }; /** * @return {boolean} return true if store is fixed. */ ydn.db.schema.Store.prototype.isFixed = function() { return this.fixed; }; /** * @return {boolean} return true if store is encrypted. */ ydn.db.schema.Store.prototype.isEncrypted = function() { return this.is_encrypted_; }; /** * @see #hasIndexByKeyPath * @param {string} name index name. * @return {boolean} return true if name is found in the index or primary * keyPath. */ ydn.db.schema.Store.prototype.hasIndex = function(name) { if (name === this.keyPath) { return true; } return goog.array.some(this.indexes, function(x) { return x.getName() == name; }); }; /** * Check given key_path is equals to store key path. * @param {(string|goog.array.ArrayLike)=} opt_key_path * @return {boolean} */ ydn.db.schema.Store.prototype.isKeyPath = function(opt_key_path) { if (goog.isDef(this.keyPath)) { if (this.keyPaths.length == 1) { return this.keyPath === opt_key_path; } else if (goog.isArrayLike(opt_key_path)) { return goog.array.equals(this.keyPaths, /** @type {goog.array.ArrayLike} */ (opt_key_path)); } else { return false; } } else { return false; } }; /** * @see #hasIndex * @param {string|!Array.<string>} key_path index key path. * @return {boolean} return true if key_path is found in the index including * primary keyPath. */ ydn.db.schema.Store.prototype.hasIndexByKeyPath = function(key_path) { if (this.keyPath && goog.isNull(ydn.db.schema.Index.compareKeyPath(this.keyPath, key_path))) { return true; } return goog.array.some(this.indexes, function(x) { return goog.isDefAndNotNull(x.keyPath) && goog.isNull(ydn.db.schema.Index.compareKeyPath(x.keyPath, key_path)); }); }; /** * Return quoted keyPath. In case undefined return default key column. * @return {string} return quoted keyPath. If keyPath is array, they are * join by ',' and quoted. If keyPath is not define, default sqlite column * name is used. */ ydn.db.schema.Store.prototype.getSQLKeyColumnNameQuoted = function() { return this.primary_column_name_quoted_; }; /** * Return quoted keyPath. In case undefined return default key column. * @return {string} return quoted keyPath. If keyPath is array, they are * join by ',' and quoted. If keyPath is not define, default sqlite column * name is used. */ ydn.db.schema.Store.prototype.getSQLKeyColumnName = function() { return this.primary_column_name_; }; /** * @type {string} * @private */ ydn.db.schema.Store.prototype.primary_column_name_; /** * @type {string} * @private */ ydn.db.schema.Store.prototype.primary_column_name_quoted_; /** * * @return {string} return quoted name. */ ydn.db.schema.Store.prototype.getQuotedName = function() { return goog.string.quote(this.name_); }; /** * @return {Array.<string>} return name of indexed. It is used as column name * in WebSql. */ ydn.db.schema.Store.prototype.getColumns = function() { if (this.columns_ && this.columns_.length != this.indexes.length) { /** * @private * @final * @type {Array.<string>} */ this.columns_ = []; for (var i = 0; i < this.indexes.length; i++) { this.columns_.push(this.indexes[i].getName()); } } return this.columns_; }; /** * Update store schema with given guided store schema for * indexeddb. * these include: * 1. blob column data type * @param {!ydn.db.schema.Store} that guided store schema. */ ydn.db.schema.Store.prototype.hintForIdb = function(that) { for (var i = 0; i < that.indexes.length; i++) { var index = that.indexes[i]; if (!this.hasIndex(index.getName()) && index.getType() == ydn.db.schema.DataType.BLOB) { var clone = new ydn.db.schema.Index( index.getKeyPath(), index.getType(), index.isUnique(), index.isMultiEntry(), index.getName()); this.indexes.push(clone); } } }; /** * Create a new update store schema with given guided store schema. * NOTE: This is used in websql for checking table schema sniffed from the * connection is similar to requested table schema. The fact is that * some schema information are not able to reconstruct from the connection, * these include: * 1. composite index: in which a composite index is blown up to multiple * columns. @see ydn.db.con.WebSql.prototype.prepareTableSchema_. * @param {ydn.db.schema.Store} that guided store schema. * @return {!ydn.db.schema.Store} updated store schema. */ ydn.db.schema.Store.prototype.hintForWebSql = function(that) { if (!that) { return this; } goog.asserts.assert(this.name_ == that.name_, 'store name: ' + this.name_ + ' != ' + that.name_); var autoIncrement = this.autoIncrement; var keyPath = goog.isArray(this.keyPath) ? goog.array.clone(/** @type {goog.array.ArrayLike} */ (this.keyPath)) : this.keyPath; var type = this.type; var indexes = goog.array.map(this.indexes, function(index) { return index.clone(); }); if (!goog.isDef(that.type) && type == 'TEXT') { // composite are converted into TEXT type = undefined; } if (goog.isArray(that.keyPath) && goog.isString(keyPath) && keyPath == that.keyPath.join(',')) { keyPath = goog.array.clone( /** @type {goog.array.ArrayLike} */ (that.keyPath)); } // update composite index for (var i = 0, n = that.indexes.length; i < n; i++) { if (that.indexes[i].isComposite()) { var name = that.indexes[i].getName(); for (var j = indexes.length - 1; j >= 0; j--) { if (name.indexOf(indexes[j].getName()) >= 0) { indexes[j] = that.indexes[i].clone(); break; } } } } for (var i = 0; i < indexes.length; i++) { var that_index = that.getIndex(indexes[i].getName()); if (that_index) { indexes[i] = indexes[i].hint(that_index); } } return new ydn.db.schema.Store( that.name_, keyPath, autoIncrement, type, indexes); }; /** * * @return {string} store name. */ ydn.db.schema.Store.prototype.getName = function() { return this.name_; }; /** * * @return {boolean|undefined} autoIncrement. */ ydn.db.schema.Store.prototype.isAutoIncrement = function() { return this.autoIncrement; }; /** * * @return {Array.<string>|string} keyPath. */ ydn.db.schema.Store.prototype.getKeyPath = function() { return this.keyPath; }; /** * * @return {boolean} true if inline key is in used. */ ydn.db.schema.Store.prototype.usedInlineKey = function() { return !!this.keyPath; }; /** * * @return {!Array.<string>} list of index names. */ ydn.db.schema.Store.prototype.getIndexNames = function() { return this.indexes.map(function(x) {return x.getName();}); }; /** * * @return {ydn.db.schema.DataType|undefined} */ ydn.db.schema.Store.prototype.getType = function() { return this.type; }; /** * * @return {ydn.db.schema.DataType} */ ydn.db.schema.Store.prototype.getSqlType = function() { return this.keyColumnType_; }; /** * * @return {!Array.<string>} list of index keyPath. */ ydn.db.schema.Store.prototype.getIndexKeyPaths = function() { return this.indexes.map(function(x) {return x.keyPath;}); }; /** * * @param {string} name column name or keyPath. * @param {ydn.db.schema.DataType=} opt_type optional column data type. * @param {boolean=} opt_unique unique. * @param {boolean=} opt_multiEntry true for array index to index individual * element. */ ydn.db.schema.Store.prototype.addIndex = function(name, opt_type, opt_unique, opt_multiEntry) { this.indexes.push(new ydn.db.schema.Index(name, opt_type, opt_unique, opt_multiEntry)); }; /** * Extract primary key value of keyPath from a given object. * @param {Object} record record value. * @param {IDBKey=} opt_key out-of-line key. * @return {!IDBKey|undefined} extracted primary key. */ ydn.db.schema.Store.prototype.extractKey = function(record, opt_key) { if (!record) { return undefined; } if (!this.usedInlineKey() && goog.isDefAndNotNull(opt_key)) { return opt_key; } // http://www.w3.org/TR/IndexedDB/#key-construct if (this.isComposite) { var arr = []; for (var i = 0; i < this.keyPath.length; i++) { arr.push(ydn.db.utils.getValueByKeys(record, this.keyPath[i])); } return arr; } else if (this.keyPath) { return /** @type {!IDBKey} */ (goog.object.getValueByKeys( record, this.keyPaths)); } else { return undefined; } }; /** * Extract value of keyPath from a row of SQL results * @param {!Object} obj record value. * @return {!Array|number|string|undefined} return key value. */ ydn.db.schema.Store.prototype.getRowValue = function(obj) { if (goog.isDefAndNotNull(this.keyPath)) { var value = obj[this.keyPath]; if (this.type == ydn.db.schema.DataType.DATE) { value = Date.parse(value); } else if (this.type == ydn.db.schema.DataType.NUMERIC) { value = parseFloat(value); } else if (this.type == ydn.db.schema.DataType.INTEGER) { value = parseInt(value, 10); } return value; } else { return undefined; } }; /** * Generated a key starting from 0 with increment of 1. * NOTE: Use only by simple store. * @return {number} generated key. */ ydn.db.schema.Store.prototype.generateKey = function() { if (!goog.isDef(this.current_key_)) { /** * @type {number} * @private */ this.current_key_ = 0; } return this.current_key_++; }; /** * Set keyPath field of the object with given value. * @see #getKeyValue * @param {!Object} obj get key value from its keyPath field. * @param {*} value key value to set. */ ydn.db.schema.Store.prototype.setKeyValue = function(obj, value) { for (var i = 0; i < this.keyPaths.length; i++) { var key = this.keyPaths[i]; if (i == this.keyPaths.length - 1) { obj[key] = value; return; } if (!goog.isDef(obj[key])) { obj[key] = {}; } obj = obj[key]; } }; /** * Prepare SQL column name and values. * @param {!Object} obj get values of indexed fields. * @param {IDBKey=} opt_key optional key. * @param {boolean=} opt_exclude_unique_column exclude unique constrained * columns. * @return {{ * columns: Array.<string>, * slots: Array.<string>, * values: Array.<string>, * key: (IDBKey|undefined) * }} return list of values as it appear on the indexed fields. */ ydn.db.schema.Store.prototype.sqlNamesValues = function(obj, opt_key, opt_exclude_unique_column) { // since corretness of the inline, offline, auto are already checked, // here we don't check again. this method should not throw error for // these reason. If error must be throw it has to be InternalError. var values = []; var columns = []; var key = goog.isDef(opt_key) ? opt_key : this.extractKey(obj); if (goog.isDef(key)) { columns.push(this.getSQLKeyColumnNameQuoted()); values.push(ydn.db.schema.Index.js2sql(key, this.getType())); } for (var i = 0; i < this.indexes.length; i++) { var index = this.indexes[i]; if (index.isMultiEntry() || index.getName() === this.keyPath || index.getName() == ydn.db.base.DEFAULT_BLOB_COLUMN || (!!opt_exclude_unique_column && index.isUnique())) { continue; } var idx_key = index.extractKey(obj); if (goog.isDefAndNotNull(idx_key)) { values.push(ydn.db.schema.Index.js2sql(idx_key, index.getType())); columns.push(index.getSQLIndexColumnNameQuoted()); } } if (!this.fixed) { values.push(ydn.json.stringify(obj)); columns.push(ydn.db.base.DEFAULT_BLOB_COLUMN); } else if (this.isFixed() && !this.usedInlineKey() && this.countIndex() == 0) { // check for blob var BASE64_MARKER = ';base64,'; if (goog.isString(obj) && obj.indexOf(BASE64_MARKER) == -1) { values.push(obj); columns.push(ydn.db.base.DEFAULT_BLOB_COLUMN); } else { values.push(ydn.json.stringify(obj)); columns.push(ydn.db.base.DEFAULT_BLOB_COLUMN); } } var slots = []; for (var i = values.length - 1; i >= 0; i--) { slots[i] = '?'; } return { columns: columns, slots: slots, values: values, key: key }; }; /** * Compare two stores. * @see #similar * @param {ydn.db.schema.Store} store store schema to test. * @return {boolean} true if store schema is exactly equal to this schema. */ ydn.db.schema.Store.prototype.equals = function(store) { return this.name_ === store.name_ && ydn.object.equals(this.toJSON(), store.toJSON()); }; /** * Compare two stores. * @see #equals * @param {ydn.db.schema.Store} store * @return {string} explination for difference, empty string for similar. */ ydn.db.schema.Store.prototype.difference = function(store) { if (!store) { return 'missing store: ' + this.name_; } if (this.name_ != store.name_) { return 'store name, expect: ' + this.name_ + ', but: ' + store.name_; } var msg = ydn.db.schema.Index.compareKeyPath(this.keyPath, store.keyPath); if (msg) { return 'keyPath, ' + msg; } if (goog.isDef(this.autoIncrement) && goog.isDef(store.autoIncrement) && this.autoIncrement != store.autoIncrement) { return 'autoIncrement, expect: ' + this.autoIncrement + ', but: ' + store.autoIncrement; } if (this.indexes.length != store.indexes.length) { return 'indexes length, expect: ' + this.indexes.length + ', but: ' + store.indexes.length; } if (goog.isDef(this.type) && goog.isDef(store.type) && (goog.isArrayLike(this.type) ? !goog.array.equals( /** @type {goog.array.ArrayLike} */ (this.type), /** @type {goog.array.ArrayLike} */ (store.type)) : this.type != store.type)) { return 'data type, expect: ' + this.type + ', but: ' + store.type; } for (var i = 0; i < this.indexes.length; i++) { var index = store.getIndex(this.indexes[i].getName()); var index_msg = this.indexes[i].difference(index); if (index_msg.length > 0) { return 'index "' + this.indexes[i].getName() + '" ' + index_msg; } } return ''; }; /** * * @param {ydn.db.schema.Store} store schema. * @return {boolean} true if given store schema is similar to this. */ ydn.db.schema.Store.prototype.similar = function(store) { return this.difference(store).length == 0; }; /** * @type {Object.<Function>} index generator function for each index. */ ydn.db.schema.Store.prototype.index_generators; /** * Add index by generator. * @param {Object} obj record value. */ ydn.db.schema.Store.prototype.generateIndex = function(obj) { if (!obj) { return; } for (var i = 0; i < this.indexes.length; i++) { this.indexes[i].generateIndex(obj); } }; /** * @param {function(!ydn.db.Request, goog.array.ArrayLike)} hook database * pre-hook function. * @return {number} internal hook index. */ ydn.db.schema.Store.prototype.addHook = function(hook) { this.hooks_.push(hook); return this.hooks_.length - 1; }; /** * Invoke hook functions. * Database hook to call before persisting into the database. * Override this function to attach the hook. The default implementation is * immediately invoke the given callback with first variable argument. * to preserve database operation order, preHook call is not waited. * @param {!ydn.db.Request} df deferred from database operation. * @param {goog.array.ArrayLike} args arguments to the db method. * @param {number=} opt_hook_idx hook index to ignore. * @param {*=} opt_scope * @final */ ydn.db.schema.Store.prototype.hook = function(df, args, opt_hook_idx, opt_scope) { for (var i = 0; i < this.hooks_.length; i++) { if (opt_hook_idx !== i) { this.hooks_[i].call(opt_scope, df, args); } } }; /** * Lookup index from the schema. * @param {!Array.<string>|string} index_name_or_key_path index name or * key path. * @return {string} index name. */ ydn.db.schema.Store.prototype.getIndexName = function(index_name_or_key_path) { var index; var index_name = index_name_or_key_path; if (goog.isArray(index_name_or_key_path)) { index = this.getIndexByKeyPath(index_name_or_key_path); index_name = index_name_or_key_path.join(', '); } else { index = this.getIndex(index_name_or_key_path); } if (goog.DEBUG && !index) { throw new ydn.debug.error.ArgumentException('require index "' + index_name + '" not found in store "' + this.getName() + '"'); } return index.getName(); }; if (goog.DEBUG) { /** * @inheritDoc */ ydn.db.schema.Store.prototype.toString = function() { return 'Store:' + this.name_ + '[' + this.countIndex() + 'index]'; }; }