database-jones/Adapter/common/DBTableHandler.js (791 lines of code) (raw):

/* Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ "use strict"; var stats = { "constructor_calls" : 0, "created" : {}, "default_mappings" : 0, "explicit_mappings" : 0, "return_null" : 0, "result_objects_created" : 0, "DBIndexHandler_created" : 0 }; var assert = require("assert"), util = require("util"), jones = require("database-jones"), unified_debug = require("unified_debug"), TableMapping = jones.TableMapping, FieldMapping = require(jones.api.TableMapping).FieldMapping, stats_module = require(jones.api.stats), BitMask = require(jones.common.BitMask), udebug = unified_debug.getLogger("DBTableHandler.js"); var DBIndexHandler; stats_module.register(stats,"spi","DBTableHandler"); /* A DBTableHandler (DBT) combines dictionary metadata with user mappings. It manages setting and getting of columns based on the fields of a user's domain object. It can also choose an index access path by comapring user-supplied key fields of a domain object with a table's indexes. A DBT encapsulates: * A TableMetadata object, obtained from the data dictionary and passed to the DBT constructor. This is immutable. * A resolved TableMapping, created from the TableMapping passed in to the constrcutor (or created by default). * A list of columns, which contains the subset of a table's columns needed to fulfill the TableMapping. Columns are accessed by number. The ordering of columns in a DBT preserves the ordering found in the TableMetadata. * A set of mapped fields. Fields are accessed by name. The set includes all fields explicitly mapped in the TableMapping, plus fields that are implicitly mapped due to the TableMapping.mapAllColumns() flag. The mapping from fields to columns can be 1-to-1, 1-to-many, or many-to-1. Within a DBT, a DBTableHandlerPrivate manages the mapping of fields to columns. */ /* getColumnByName() is a utility function used in the building of maps. */ function getColumnByName(dbTable, colName) { var i, col; for(i = 0 ; i < dbTable.columns.length ; i++) { col = dbTable.columns[i]; if(col.name === colName) { return col; } } udebug.log("getColumnByName", colName, "NOT FOUND."); return null; } ////////////////// /// DBT_Column represents a mapped column ////////////////// function DBT_Column(columnMetadata, converter) { this.columnName = columnMetadata.name; this.fieldNames = []; this.fieldConverters = []; this.isMapped = false; // Has any field mapping this.isShared = false; // Many fields to 1 column this.isPartial = false; // 1 field to many columns this.excludedFieldNames = []; // If column is a container for sparse fields this.typeConverter = converter || columnMetadata.typeConverter; this.getColumnValue = this.getColumnValue_1to1; this.setFieldValues = this.setFieldValues_1to1; } DBT_Column.prototype.addFieldMapping = function(mapping, reportError) { if(mapping.meta && mapping.meta.isShared) { this.isShared = true; this.getColumnValue = this.getColumnValue_Shared; this.setFieldValues = this.setFieldValues_Shared; } this.fieldNames.push(mapping.fieldName); this.fieldConverters.push(mapping.converter); if(this.fieldNames.length > 1 && ! this.isShared) { reportError( "Column " + this.columnName + " is used by multiple fields but field " + mapping.fieldName + " does not mark it as shared." ); } else { this.isMapped = true; } }; DBT_Column.prototype.mapPartial = function(mapping, n) { this.isMapped = true; this.isPartial = true; this.isFirst = (n === 0); this.isLast = (n === mapping.toManyColumns.length - 1); this.fieldNames.push(mapping.fieldName); this.fieldConverters.push(mapping.converter); this.getColumnValue = this.getColumnValue_Partial; this.setFieldValues = this.setFieldValues_Partial; }; DBT_Column.prototype.setSparse = function(tableMapping) { this.isMapped = true; this.isShared = true; this.excludedFieldNames = tableMapping.excludedFieldNames; this.typeConverter = tableMapping.sparseContainer.converter || this.typeConverter || jones.converters.JSONConverter; this.getColumnValue = this.getColumnValue_Sparse; this.setFieldValues = this.setFieldValues_Sparse; }; DBT_Column.prototype.setUnmappable = function() { /* Column has no mapping, but cannot be implicitly mapped because a Field mapping exists using the column name. */ this.getColumnValue = this.getColumnValue_Unmapped; this.setFieldValues = this.setFieldValues_Unmapped; }; DBT_Column.prototype.toDB = function(value) { return this.typeConverter ? this.typeConverter.toDB(value) : value; }; DBT_Column.prototype.fromDB = function(value) { return this.typeConverter ? this.typeConverter.fromDB(value) : value; }; DBT_Column.prototype.getColumnValue_1to1 = function(domainObject) { var value = domainObject[this.fieldNames[0]]; if(this.fieldConverters[0]) { value = this.fieldConverters[0].toDB(value); } return this.toDB(value); }; DBT_Column.prototype.setFieldValues_1to1 = function(domainObject, columnValue) { columnValue = this.fromDB(columnValue); if(this.fieldConverters[0]) { columnValue = this.fieldConverters[0].fromDB(columnValue); } domainObject[this.fieldNames[0]] = columnValue; }; DBT_Column.prototype.getColumnValue_Partial = function(domainObject) { var fieldName, intermediateValue; // writing to db - one field to many columns fieldName = this.fieldNames[0]; // intermediate type is {} if(this.isFirst && this.fieldConverters[0]) { domainObject[fieldName] = this.fieldConverters[0].toDB(domainObject[fieldName]); } intermediateValue = domainObject[fieldName]; if(this.isLast && this.fieldConverters[0]) { domainObject[fieldName] = this.fieldConverters[0].fromDB(intermediateValue); } if(typeof intermediateValue === 'object') { return this.toDB(intermediateValue[this.columnName]); } // fallthrough; value not of proper intermediate type; return undefined. }; DBT_Column.prototype.setFieldValues_Partial = function(domainObject, columnValue) { var fieldName = this.fieldNames[0]; // reading from db if(this.isFirst) { domainObject[fieldName] = {}; // intermediate type; column names are keys } domainObject[fieldName][this.columnName] = this.fromDB(columnValue); if(this.isLast && this.fieldConverters[0]) { domainObject[fieldName] = this.fieldConverters[0].fromDB(domainObject[fieldName]); } }; DBT_Column.prototype.getColumnValue_Shared = function(domainObject) { var i, value, name; // writing to db -- many fields to one column value = {}; // intermediate type; field names are keys for(i = 0 ; i < this.fieldNames.length ; i++) { name = this.fieldNames[i]; value[name] = domainObject[name]; if(this.fieldConverters[i]) { value[name] = this.fieldConverters[i].toDB(value[name]); } } return this.toDB(value); }; DBT_Column.prototype.setFieldValues_Shared = function(domainObject, columnValue) { var i, name, value; // reading from db -- many fields to one column columnValue = this.fromDB(columnValue); for(i = 0 ; i < this.fieldNames.length ; i++) { name = this.fieldNames[i]; value = columnValue[name]; if(this.fieldConverters[i]) { value = this.fieldConverters[i].fromDB(value); } domainObject[name] = value; } }; DBT_Column.prototype.getColumnValue_Sparse = function(domainObject) { var value, candidateField; // writing to db -- all non-excluded fields value = {}; // intermediate type; field names are keys for(candidateField in domainObject) { if(domainObject.hasOwnProperty(candidateField)) { if(this.excludedFieldNames.indexOf(candidateField) === -1) { value[candidateField] = domainObject[candidateField]; } } } return this.toDB(value); }; DBT_Column.prototype.setFieldValues_Sparse = function(domainObject, columnValue) { var candidateField; // reading serialized from database columnValue = this.fromDB(columnValue); if(typeof columnValue === 'object') { for(candidateField in columnValue) { if(columnValue.hasOwnProperty(candidateField)) { domainObject[candidateField] = columnValue[candidateField]; } } } }; DBT_Column.prototype.getColumnValue_Unmapped = function() {}; DBT_Column.prototype.setFieldValues_Unmapped = function() {}; DBT_Column.prototype.hasConverter = function() { return (this.typeConverter || this.fieldConverters[0]); }; ////////////////// /// DBT_Field represents a mapped field /// It stores a BitMask of column numbers in the resolved mapping ////////////////// function DBT_Field(mapping, maskSize) { this.mapping = mapping; if(maskSize > 0) { this.mapping.columnMask = new BitMask(maskSize); this.ncol = 0; } // otherwise, field is a relationship field } DBT_Field.prototype.mapToColumn = function(colNumber) { this.mapping.columnMask.set(colNumber); this.mapping.columnNumber = (++this.ncol === 1 ? colNumber : undefined); }; ////////////////// /// DBTableHandlerPrivate: column-to-field mappings ////////////////// function DBTableHandlerPrivate(dbTableHandler, maxColumns) { this.columnNameToIdMap = {}; this.columns = []; // Array of DBT_Column this.columnMetadata = []; // Array of ColumnMetadata this.fields = {}; // FieldName => DBT_Field this.maxColumns = maxColumns; this.reportError = function(message) { dbTableHandler.appendErrorMessage(message); }; } DBTableHandlerPrivate.prototype[util.inspect.custom] = function() { return ""; }; DBTableHandlerPrivate.prototype.addColumn = function(columnMetadata, converterMap) { var n, col; n = this.columns.length; col = new DBT_Column(columnMetadata, converterMap[columnMetadata.name]); this.columnNameToIdMap[columnMetadata.name] = n; this.columns[n] = col; this.columnMetadata[n] = columnMetadata; }; DBTableHandlerPrivate.prototype.addColumnFromParent = function(parent, colName) { var n, parentColumnId, col, i, fieldName; n = this.columns.length; parentColumnId = parent.columnNameToIdMap[colName]; this.columnNameToIdMap[colName] = n; col = parent.columns[parentColumnId]; this.columns[n] = col; this.columnMetadata[n] = parent.columnMetadata[parentColumnId]; for(i = 0 ; i < col.fieldNames.length ; i++) { fieldName = col.fieldNames[i]; this.fields[fieldName] = parent.fields[fieldName]; } }; DBTableHandlerPrivate.prototype.mapFieldToColumns = function(mapping) { var n, id; if(mapping.toManyColumns) { n = 0; for(id = 0 ; id < this.columns.length ; id++) { // work in column order if(mapping.toManyColumns.indexOf(this.columns[id].columnName) !== -1) { this.fields[mapping.fieldName].mapToColumn(id); this.columns[id].mapPartial(mapping, n++); } } if(mapping.toManyColumns.length !== n) { this.reportError("Bad column list in field " + mapping.fieldName + ": " + mapping.toManyColumns + "(used " + n + ")"); } } else { // map to just one column id = this.columnNameToIdMap[mapping.columnName || mapping.fieldName]; if(id >= 0) { this.columns[id].addFieldMapping(mapping, this.reportError); this.fields[mapping.fieldName].mapToColumn(id); } else if(this.sparseColumnId >= 0) { this.columns[this.sparseColumnId].addFieldMapping(mapping); this.fields[mapping.fieldName].mapToColumn(this.sparseColumnId); } else { this.reportError("No column for field " + mapping.fieldName); } } }; DBTableHandlerPrivate.prototype.addField = function(mapping) { if(this.fields[mapping.fieldName]) { this.reportError("Attempt to map field " + mapping.fieldName + " more than once"); } else { if(mapping.relationship) { this.fields[mapping.fieldName] = new DBT_Field(mapping, 0); } else { this.fields[mapping.fieldName] = new DBT_Field(mapping, this.maxColumns); this.mapFieldToColumns(mapping); } } }; DBTableHandlerPrivate.prototype.setSparseColumn = function(id, tableMapping) { this.sparseColumnId = id; this.columns[id].setSparse(tableMapping); }; DBTableHandlerPrivate.prototype.getColumnMetadata = function(idOrName) { var id = idOrName; if(typeof idOrName === 'string') { id = this.columnNameToIdMap[idOrName]; } if(id !== undefined) { return this.columnMetadata[id]; } }; DBTableHandlerPrivate.prototype.getColumnMapping = function(id) { if(id !== undefined) { return this.columns[id]; } }; DBTableHandlerPrivate.prototype.getColumnMappingByName = function(name) { var id = this.columnNameToIdMap[name]; if(id !== undefined) { return this.columns[id]; } }; DBTableHandlerPrivate.prototype.getNumberOfMappedColumns = function() { var total=0; this.columns.forEach(function(col) { if(col.isMapped && ! col.excludedFieldNames.length) { total++; } }); return total; }; /* DBTableHandler() constructor IMMEDIATE Create a DBTableHandler for a table and a mapping. If dbtable is null or has no columns, this function returns null. If the tablemapping is null, a default TableMapping will be created and used. */ function DBTableHandler(dbtable, tablemapping, ctor) { var i, // an iterator field, // a field col, // a column id, // a column id number index, // a DBIndex columnNames, // array of the mapped column names foreignKey, // foreign key object from dbTable priv; // our DBTableHandlerPrivate var dbTableHandler = this; var invalidateCallback; stats.constructor_calls++; if(! ( dbtable && dbtable.columns)) { stats.return_null++; return null; } if(stats.created[dbtable.name] === undefined) { stats.created[dbtable.name] = 1; } else { stats.created[dbtable.name]++; } /* Default properties */ priv = new DBTableHandlerPrivate(this, dbtable.columns.length); this._private = priv; this.dbTable = dbtable; this.ValueObject = null; this.errorMessages = ""; this.isValid = true; this.autoIncFieldName = null; this.autoIncColumnNumber = null; this.numberOfLobColumns = 0; this.newObjectConstructor = ctor; this.dbIndexHandlers = []; this.foreignKeyMap = {}; this.sparseContainer = null; this.is1to1 = true; this.relationshipFields = []; if(tablemapping) { if(tablemapping.isValid()) { stats.explicit_mappings++; } else { this.errorMessages = tablemapping.error; this.isValid = false; return; } } else { // Create a default mapping stats.default_mappings++; tablemapping = new TableMapping(this.dbTable.name); tablemapping.database = this.dbTable.database; } /* Make a copy of the mapping and store it as the "resolved" mapping */ this.resolvedMapping = new TableMapping(tablemapping); this.resolvedMapping.mapAllColumns = false; // all fields will be present /* Build an array of column names. This will establish column order. */ columnNames = []; function addColumnName(name) { if(name && getColumnByName(dbtable, name) && columnNames.indexOf(name) === -1) { columnNames.push(name); } } if(tablemapping.mapAllColumns) { // Use all columns from dictionary for(i = 0 ; i < dbtable.columns.length ; i++) { columnNames.push(dbtable.columns[i].name); } } else { // Use all mapped columns plus any sparse container column for(i = 0 ; i < tablemapping.fields.length ; i++) { field = tablemapping.fields[i]; if(field.persistent) { if(field.toManyColumns) { field.toManyColumns.forEach(addColumnName); } else { addColumnName(field.columnName || field.fieldName); } } } addColumnName(dbtable.sparseContainer); } udebug.log("DBTableHandler columns", columnNames); /* Build the array of columns. */ for(id = 0 ; id < columnNames.length ; id++) { priv.addColumn(getColumnByName(dbtable, columnNames[id]), tablemapping.columnConverterMap); } /* Build the array of mapped fields. */ /* Start with persistent fields from the TableMapping */ for(i = 0; i < this.resolvedMapping.fields.length ; i++) { field = this.resolvedMapping.fields[i]; if(field.persistent) { priv.addField(field); } } /* Add a default field mapping for any yet-unmapped columns */ if(tablemapping.mapAllColumns) { for(i = 0 ; i < priv.columns.length ; i++) { if(! priv.columns[i].isMapped) { field = priv.columnMetadata[i].name; if(priv.fields[field]) { priv.columns[i].setUnmappable(); } else { this.resolvedMapping.mapField({"fieldName":field,"columnName":field}); priv.addField(this.resolvedMapping.getFieldMapping(field)); } } } } /* Iterate over all columns. * Set internal pointers for notable columns. */ for(id = 0 ; id < columnNames.length ; id++) { col = columnNames[id]; if(this.resolvedMapping.sparseContainer && this.resolvedMapping.sparseContainer.columnName === col) { this.sparseContainer = col; priv.setSparseColumn(id, this.resolvedMapping); } else if(dbtable.sparseContainer === col && ! priv.columns[id].isMapped) { this.resolvedMapping.mapSparseFields(col); this.sparseContainer = col; priv.setSparseColumn(id, this.resolvedMapping); } if(priv.columnMetadata[id].isAutoincrement) { this.autoIncColumnNumber = i; this.autoIncFieldName = priv.columns[id].fieldNames[0]; } if(priv.columnMetadata[id].isLob) { this.numberOfLobColumns++; } if(priv.columns[id].isShared || priv.columns[id].isPartial) { this.is1to1 = false; } } /* Iterate over all field mappings. * Build the array of relationshipFields. * For all persistent fields, resolve field.columnName. */ for(i = 0 ; i < this.resolvedMapping.fields.length ; i++) { field = this.resolvedMapping.fields[i]; if(field.relationship) { this.relationshipFields.push(field); } else if(field.persistent) { if(field.columnName) { if(! this._private.getColumnMappingByName(field.columnName)) { this.resolvedMapping.error += "Column " + field.columnName + " does not exist\n"; } } else if(! field.toManyColumns) { if(this._private.getColumnMappingByName(field.fieldName)) { field.columnName = field.fieldName; } else if(this.sparseContainer) { field.columnName = this.sparseContainer; } else { this.resolvedMapping.error += "No column mapped for field " + field.fieldName + "\n"; } } } if(field.columnName !== this.sparseContainer) { this.resolvedMapping.fieldIsNotSparse(field.fieldName); } } // build a dbIndexHandler for each usable dbIndex for (i = 0; i < this.dbTable.indexes.length; ++i) { index = this.dbTable.indexes[i]; if(this.hasColumnsFromTable(index.columnNumbers)) { this.dbIndexHandlers.push(new DBIndexHandler(this, index)); } } // build foreign key map for (i = 0; i < this.dbTable.foreignKeys.length; ++i) { foreignKey = this.dbTable.foreignKeys[i]; this.foreignKeyMap[foreignKey.name] = foreignKey; } // set the invalidate callback if this db table handler is valid if (dbTableHandler.isValid) { invalidateCallback = function() { udebug.log('invalidateCallback called for dbTableHandler', ctor?'for constructor '+ctor.name:'default', 'for table', dbtable.database+'.'+dbtable.name); dbTableHandler.isValid = false; }; dbtable.registerInvalidateCallback(invalidateCallback); } udebug.log("Constructor completed -- ", this); } DBTableHandler.prototype.getResolvedMapping = function() { return this.resolvedMapping; }; DBTableHandler.prototype.getColumnMapping = function(id) { return this._private.getColumnMapping(id); }; DBTableHandler.prototype.getColumnMetadata = function(idOrName) { return this._private.getColumnMetadata(idOrName); }; DBTableHandler.prototype.getAllColumnMetadata = function() { return this._private.columnMetadata; }; DBTableHandler.prototype.getNumberOfColumns = function() { return this._private.columns.length; }; DBTableHandler.prototype.getFieldMapping = function(fieldName) { if(this._private.fields[fieldName]) { return this._private.fields[fieldName].mapping; } }; DBTableHandler.prototype.getAllQueryFields = function() { return this.resolvedMapping.fields; }; DBTableHandler.prototype.getNumberOfFields = function() { return this.resolvedMapping.fields.length; }; DBTableHandler.prototype.getColumnMaskForField = function(name) { if(this._private.fields[name]) { return this._private.fields[name].mapping.columnMask; } }; DBTableHandler.prototype[util.inspect.custom] = function() { var s, fields, ncol, columns, nrelationships, ctorName; if(this.isValid) { fields = this.getNumberOfFields() == 1 ? " field" : " fields"; ncol = this._private.getNumberOfMappedColumns(); nrelationships = this.relationshipFields.length; columns = (ncol == 1 ? " column" : " columns"); ctorName = this.newObjectConstructor? ' constructor: ' + this.newObjectConstructor.name: ' no constructor'; s = "DBTableHandler for table " + this.dbTable.name + " with " + this.getNumberOfFields() + fields + " mapped to " + ncol + columns + " and " + nrelationships + " relationships" + ctorName; if(this.sparseContainer) { s += " and sparse column " + this.sparseContainer; } } else { s = "Invalid DBTableHandler with error: " + this.errorMessages; } return s; }; /** Append an error message and mark this DBTableHandler as invalid. */ DBTableHandler.prototype.appendErrorMessage = function(msg) { this.errorMessages += '\n' + msg; this.isValid = false; }; DBTableHandler.prototype.createResultObject = function() { var ctor, newDomainObj; ctor = this.newObjectConstructor; if(ctor && ctor.prototype) { udebug.log("createResultObject() with user constructor", ctor.name, "and prototype"); newDomainObj = Object.create(ctor.prototype); ctor.call(newDomainObj); } else if(ctor) { udebug.log("createResultObject() with user constructor", ctor.name); newDomainObj = {}; ctor.call(newDomainObj); } else { udebug.log("createResultObject() [no user constructor]"); newDomainObj = {}; } stats.result_objects_created++; return newDomainObj; }; /* DBTableHandler.newResultObject IMMEDIATE Create a new object using the constructor function (if set). */ DBTableHandler.prototype.newResultObject = function(values) { var newDomainObj = this.createResultObject(); if (typeof values === 'object') { this.setFields(newDomainObj, values); } udebug.log("newResultObject", newDomainObj); return newDomainObj; }; /* DBTableHandler.newResultObjectFromRow * IMMEDIATE * Create a new object using the constructor function (if set). * Values for the object's fields come from the row; first the key fields * and then the non-key fields. The row contains items named '0', '1', etc. * The value for the first key field is in row[offset]. Values obtained * from the row are first processed by the db converter and type converter * if present. */ DBTableHandler.prototype.newResultObjectFromRow = function(row, offset, keyFields, nonKeyFields) { var fieldIndex, rowValue, field, newDomainObj; udebug.log("newResultObjectFromRow"); newDomainObj = this.createResultObject(); // set key field values from row using type converters for (fieldIndex = 0; fieldIndex < keyFields.length; ++fieldIndex) { rowValue = row[offset + fieldIndex]; field = keyFields[fieldIndex]; this.set(newDomainObj, field.columnNumber, rowValue); } // set non-key field values from row using type converters offset += keyFields.length; for (fieldIndex = 0; fieldIndex < nonKeyFields.length; ++fieldIndex) { rowValue = row[offset + fieldIndex]; field = nonKeyFields[fieldIndex]; this.set(newDomainObj, field.columnNumber, rowValue); } udebug.log("newResultObjectFromRow done", newDomainObj); return newDomainObj; }; /* setAutoincrement(object, autoincrementValue) * IMMEDIATE * Store autoincrement value into object */ DBTableHandler.prototype.setAutoincrement = function(object, autoincrementValue) { if(typeof this.autoIncColumnNumber === 'number') { object[this.autoIncFieldName] = autoincrementValue; udebug.log("setAutoincrement", this.autoIncFieldName, ":=", autoincrementValue); } }; /* chooseIndex(keys, allowUnique, allowScan) Returns a preferred DBIndexHandler, or null if none available. From API find(): * The parameter "keys" may be of any type. Keys must uniquely identify * a single row in the database. If keys is a simple type * (number or string), then the parameter type must be the * same type as or compatible with the primary key type of the mapped object. * Otherwise, properties are taken * from the parameter and matched against property names in the * mapping. */ DBTableHandler.prototype.chooseIndex = function(keys, allowUnique, allowScan) { var indexHandler, fieldName, mask, predicate, ncol, pkcol0; udebug.log("chooseIndex for:", keys); indexHandler = null; ncol = this.getNumberOfColumns(); /* Create bitmasks over the key columns */ predicate = { "usedColumnMask" : new BitMask(ncol), // all keys "equalColumnMask" : new BitMask(ncol) // only non-null keys }; if((typeof keys === 'number' || typeof keys === 'string')) { /* A simple key value represents first column of primary key */ pkcol0 = this.dbIndexHandlers[0].indexColumnNumbers[0]; predicate.usedColumnMask.set(pkcol0); predicate.equalColumnMask.set(pkcol0); } else { for (fieldName in keys) { if (keys.hasOwnProperty(fieldName) && keys[fieldName] !== undefined ) { mask = this.getColumnMaskForField(fieldName); if(mask) { predicate.usedColumnMask.orWith(mask); if(keys[fieldName] !== null) { predicate.equalColumnMask.orWith(mask); } } } } } udebug.log("KeyMasks:", predicate); /* Look for a unique index */ if(allowUnique) { indexHandler = this.chooseUniqueIndexForPredicate(predicate); } /* Look for an ordered index */ if(allowScan && ! indexHandler) { indexHandler = this.chooseOrderedIndexForPredicate(predicate); } return indexHandler; }; /* Return the first unique index that matches all predicate columns */ DBTableHandler.prototype.chooseUniqueIndexForPredicate = function(predicate) { var i, idxs, indexHandler, columnMask; columnMask = predicate.equalColumnMask; idxs = this.dbIndexHandlers; for(i = 0 ; i < idxs.length ; i++) { if(idxs[i].dbIndex.isUnique) { indexHandler = idxs[i]; if(columnMask.and(indexHandler.columnMask).isEqualTo(indexHandler.columnMask)) { return indexHandler; } } } return null; }; /* Score all ordered indexes and return the one with the best score */ DBTableHandler.prototype.chooseOrderedIndexForPredicate = function(predicate) { var i, idxs, indexHandler, score, highScore, highScorer; udebug.log("ChooseOrderedIndexForPredicate", predicate, predicate.usedColumnMask); idxs = this.dbIndexHandlers; highScore = 0; highScorer = null; for(i = 0 ; i < idxs.length ; i++) { if(idxs[i].dbIndex.isOrdered) { indexHandler = idxs[i]; score = indexHandler.score(predicate); udebug.log("Ordered index", i, "scored", score); if(score > highScore) { highScore = score; highScorer = indexHandler; } } } return highScorer; }; /** * For domain object obj, return the value of column colNumber. * If a valueDefinedListener is passed, notify it via setDefined or setUndefined. */ DBTableHandler.prototype.get = function(domainObject, colNumber, valueDefinedListener) { var result, columnMetadata; /* Handle the case where obj is a simple string or number value */ if (typeof domainObject === 'string' || typeof domainObject === 'number') { result = domainObject; } else { result = this._private.columns[colNumber].getColumnValue(domainObject); } if(valueDefinedListener) { if(result === undefined) { valueDefinedListener.setUndefined(colNumber); } else { valueDefinedListener.setDefined(colNumber); columnMetadata = this._private.columnMetadata[colNumber]; if(columnMetadata.isBinary && result.constructor && result.constructor.name !== 'Buffer' ) { valueDefinedListener.setError(columnMetadata.name, "22000", "Value for binary column must be a Buffer"); } } } return result; }; /* Return an array of column value */ DBTableHandler.prototype.getColumns = function(obj, valueDefinedListener) { var colummnValues, i; colummnValues = []; for( i = 0 ; i < this.getNumberOfColumns() ; i ++) { colummnValues[i] = this.get(obj, i, valueDefinedListener); } return colummnValues; }; /* Set field to value */ DBTableHandler.prototype.set = function(obj, columnNumber, value) { return this.getColumnMapping(columnNumber).setFieldValues(obj, value); }; /* Set all fields of domainObject from valueObject. * valueObject has properties corresponding to column names. */ DBTableHandler.prototype.setFields = function(obj, values) { var i, value, columnName; for (i = 0; i < this.getNumberOfColumns(); ++i) { columnName = this.getColumnMetadata(i).name; value = values[columnName]; if (value !== undefined) { this.set(obj, i, value); } } }; /* Returns true if a column or field converter applies to a column value */ DBTableHandler.prototype.columnHasConverter = function(columnNumber) { return this.getColumnMapping(columnNumber).hasConverter(); }; /* DBTableHandler getIndexHandler(Object keys) IMMEDIATE Given an object containing keys as defined in API Context.find(), choose an index to use as an access path for the operation, and return a DBIndexHandler for that index. */ DBTableHandler.prototype.getIndexHandler = function(keys) { return this.chooseIndex(keys, true, true); }; DBTableHandler.prototype.getUniqueIndexHandler = function(keys) { return this.chooseIndex(keys, true, false); }; DBTableHandler.prototype.getOrderedIndexHandler = function(keys) { return this.chooseIndex(keys, false, true); }; DBTableHandler.prototype.getForeignKey = function(foreignKeyName) { return this.foreignKeyMap[foreignKeyName]; }; DBTableHandler.prototype.getForeignKeyNames = function() { return Object.keys(this.foreignKeyMap); }; DBTableHandler.prototype.hasColumnsFromTable = function(columnNumbers) { var usable = true; columnNumbers.forEach(function(colNo) { var colName = this.dbTable.columns[colNo].name; if(this._private.columnNameToIdMap[colName] === undefined) { usable = false; } }, this); return usable; }; //////// DBIndexHandler ///////////////// DBIndexHandler = function(parent, dbIndex) { var maxCols, i, tableColNo, handlerColNo, colName, allDbtFields, fieldMappings; maxCols = parent._private.columns.length; stats.DBIndexHandler_created++; this.tableHandler = parent; this.dbIndex = dbIndex; this.indexColumnNumbers = []; this.singleColumn = null; this._private = new DBTableHandlerPrivate(this, maxCols); this.columnMask = new BitMask(maxCols); for(i = 0 ; i < dbIndex.columnNumbers.length ; i++) { tableColNo = dbIndex.columnNumbers[i]; colName = parent.dbTable.columns[tableColNo].name; handlerColNo = parent._private.columnNameToIdMap[colName]; this.indexColumnNumbers.push(handlerColNo); this.columnMask.set(handlerColNo); this._private.addColumnFromParent(parent._private, colName); } fieldMappings = []; allDbtFields = this._private.fields; this._private.columns.forEach(function(dbtColumn) { dbtColumn.fieldNames.forEach(function(fieldName) { fieldMappings.push(allDbtFields[fieldName].mapping); }); }); this.fieldMappings = fieldMappings; if(i === 1) { // One-column index this.singleColumn = this.getColumnMetadata(0); } udebug.log("Constructor completed:", this); }; /* DBIndexHandler inherits some methods from DBTableHandler */ DBIndexHandler.prototype = { getColumnMapping : DBTableHandler.prototype.getColumnMapping, getColumnMetadata : DBTableHandler.prototype.getColumnMetadata, getAllColumnMetadata : DBTableHandler.prototype.getAllColumnMetadata, getNumberOfColumns : DBTableHandler.prototype.getNumberOfColumns, get : DBTableHandler.prototype.get, getColumns : DBTableHandler.prototype.getColumns, appendErrorMessage : DBTableHandler.prototype.appendErrorMessage }; DBIndexHandler.prototype[util.inspect.custom] = function() { return "DBIndexHandler for" + (this.dbIndex.isUnique ? " unique" : "") + (this.dbIndex.isOrdered ? " ordered" : "") + " index " + this.dbIndex.name + " with " + this.fieldMappings.length + " field(s) over column(s) " + this.indexColumnNumbers; }; DBIndexHandler.prototype.getNumberOfFields = function() { return this.fieldMappings.length; }; DBIndexHandler.prototype.getField = function(n) { return this.fieldMappings[n]; }; /* Determine whether index is usable for a particular Query predicate */ DBIndexHandler.prototype.isUsable = function(predicate) { var usable = false; if(this.dbIndex.isUnique) { usable = predicate.equalColumnMask.and(this.columnMask).isEqualTo(this.columnMask); } else if(this.dbIndex.isOrdered) { usable = predicate.usedColumnMask.bitIsSet(this.indexColumnNumbers[0]); } return usable; }; /* Score an index for a Query predicate. Score 1 point for each consecutive key part used plus 1 more point if the column is in QueryEq. */ DBIndexHandler.prototype.score = function(predicate) { var score, point, i, colNo; score = 0; i = 0; do { colNo = this.indexColumnNumbers[i]; point = predicate.usedColumnMask.bitIsSet(colNo); if(point) { score += 1; if(predicate.equalColumnMask.bitIsSet(colNo)) { score += 1; } } i++; } while(point && i < this.indexColumnNumbers.length); udebug.log_detail('score', this.dbIndex.name, 'is', score); return score; }; exports.DBTableHandler = DBTableHandler;