/* Copyright (c) 2012, 2014, 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"), TableMapping = require(mynode.api.TableMapping).TableMapping, FieldMapping = require(mynode.api.TableMapping).FieldMapping, stats_module = require(mynode.api.stats), util = require("util"), udebug = unified_debug.getLogger("DBTableHandler.js"); // forward declaration of DBIndexHandler to avoid lint issue 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. These are the structural parts of a DBT: * mapping, an API TableMapping, either created explicitly or by default. * A TableMetadata object, obtained from the data dictionary. * An internal set of maps between Fields and Columns The mapping and TableMetadata are supplied as arguments to the constructor, which creates the maps. Some terms: column number: column order in table as supplied by DataDictionary field number: an arbitrary ordering of only the mapped fields */ /* getColumnByName() is a utility function used in the building of maps. */ function getColumnByName(dbTable, colName) { udebug.log_detail("getColumnByName", 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; } /* DBTableHandler() constructor IMMEDIATE Create a DBTableHandler for a table and a mapping. The TableMetadata may not be null. If the TableMapping is null, default mapping behavior will be used. Default mapping behavior is to: select all columns when reading use default domainTypeConverters for all data types perform no remapping between field names and column names */ function DBTableHandler(dbtable, tablemapping, ctor) { assert(arguments.length === 3); var i, // an iterator f, // a FieldMapping c, // a ColumnMetadata n, // a field or column number index, // a DBIndex stubFields, // fields created through default mapping foreignKey, // foreign key object from dbTable nMappedFields; stats.constructor_calls++; if(! ( dbtable && dbtable.columns)) { stats.return_null++; return null; } if(typeof stats.created[dbtable.name] === 'undefined') { stats.created[dbtable.name] = 1; } else { stats.created[dbtable.name]++; } this.dbTable = dbtable; if(ctor) { this.newObjectConstructor = ctor; } if(tablemapping) { stats.explicit_mappings++; this.mapping = tablemapping; } else { // Create a default mapping stats.default_mappings++; this.mapping = new TableMapping(this.dbTable.name); this.mapping.database = this.dbTable.database; } /* Default properties */ this.resolvedMapping = null; this.ValueObject = null; this.errorMessages = '\n'; this.isValid = true; this.autoIncFieldName = null; this.autoIncColumnNumber = null; this.numberOfLobColumns = 0; this.numberOfNotPersistentFields = 0; /* New Arrays */ this.columnNumberToFieldMap = []; this.fieldNumberToColumnMap = []; this.fieldNumberToFieldMap = []; this.fieldNameToFieldMap = {}; this.foreignKeyMap = {}; this.dbIndexHandlers = []; this.relationshipFields = []; /* Build the first draft of the columnNumberToFieldMap, using only the explicitly mapped fields. */ if (typeof(this.mapping.fields) === 'undefined') { this.mapping.fields = []; } for(i = 0 ; i < this.mapping.fields.length ; i++) { f = this.mapping.fields[i]; udebug.log_detail('DBTableHandler field:', f, 'persistent', f.persistent, 'relationship', f.relationship); if(f && f.persistent) { if (!f.relationship) { c = getColumnByName(this.dbTable, f.columnName); if(c) { n = c.columnNumber; this.columnNumberToFieldMap[n] = f; f.columnNumber = n; f.defaultValue = c.defaultValue; f.databaseTypeConverter = c.databaseTypeConverter; // use converter or default domain type converter if (f.converter) { udebug.log_detail('domain type converter for ', f.columnName, ' is user-specified ', f.converter); f.domainTypeConverter = f.converter; } else { udebug.log_detail('domain type converter for ', f.columnName, ' is system-specified ', c.domainTypeConverter); f.domainTypeConverter = c.domainTypeConverter; } } else { this.appendErrorMessage( 'for table ' + dbtable.name + ', field ' + f.fieldName + ': column ' + f.columnName + ' does not exist.'); } } else { // relationship field this.relationshipFields.push(f); } } else { // increment not-persistent field count ++this.numberOfNotPersistentFields; } } /* Now build the implicitly mapped fields and add them to the map */ stubFields = []; if(this.mapping.mapAllColumns) { for(i = 0 ; i < this.dbTable.columns.length ; i++) { if(! this.columnNumberToFieldMap[i]) { c = this.dbTable.columns[i]; f = new FieldMapping(c.name); stubFields.push(f); this.columnNumberToFieldMap[i] = f; f.columnNumber = i; f.defaultValue = c.defaultValue; f.databaseTypeConverter = c.databaseTypeConverter; // use converter or default domain type converter if (f.converter) { udebug.log_detail('domain type converter for ', f.columnName, ' is user-specified ', f.converter); f.domainTypeConverter = f.converter; } else { udebug.log_detail('domain type converter for ', f.columnName, ' is system-specified ', c.domainTypeConverter); f.domainTypeConverter = c.domainTypeConverter; } } } } /* Total number of mapped fields */ nMappedFields = this.mapping.fields.length + stubFields.length - this.numberOfNotPersistentFields; /* Create the resolved mapping to be returned by getMapping() */ this.resolvedMapping = {}; this.resolvedMapping.database = this.dbTable.database; this.resolvedMapping.table = this.dbTable.name; this.resolvedMapping.fields = []; /* Build fieldNumberToColumnMap, establishing field order. Detect the autoincrement column. Also build the remaining fieldNameToFieldMap and fieldNumberToFieldMap. */ for(i = 0 ; i < this.dbTable.columns.length ; i++) { c = this.dbTable.columns[i]; f = this.columnNumberToFieldMap[i]; if(c.isAutoincrement) { this.autoIncColumnNumber = i; this.autoIncFieldName = f.fieldName; } if(c.isLob) { this.numberOfLobColumns++; } this.resolvedMapping.fields[i] = {}; if(f) { f.fieldNumber = i; this.fieldNumberToColumnMap.push(c); this.fieldNumberToFieldMap.push(f); this.fieldNameToFieldMap[f.fieldName] = f; this.resolvedMapping.fields[i].columnName = f.columnName; this.resolvedMapping.fields[i].fieldName = f.fieldName; this.resolvedMapping.fields[i].persistent = true; } } var map = this.fieldNameToFieldMap; // add the relationship fields that are not mapped to columns this.relationshipFields.forEach(function(relationship) { map[relationship.fieldName] = relationship; }); if (nMappedFields !== this.fieldNumberToColumnMap.length + this.relationshipFields.length) { this.appendErrorMessage('Mismatch between number of mapped fields and columns for ' + ctor.prototype.constructor.name); } // build dbIndexHandlers; one for each dbIndex, starting with primary key index 0 for (i = 0; i < this.dbTable.indexes.length; ++i) { // a little fix-up for primary key unique index: index = this.dbTable.indexes[i]; udebug.log_detail('DbTableHandler creating DBIndexHandler for', index); if (typeof(index.name) === 'undefined') { index.name = 'PRIMARY'; } 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; } if (!this.isValid) { this.err = new Error(this.errorMessages); } if (ctor) { // cache this in ctor.prototype.mynode.dbTableHandler if (!ctor.prototype.mynode) { ctor.prototype.mynode = {}; } if (!ctor.prototype.mynode.dbTableHandler) { ctor.prototype.mynode.dbTableHandler = this; } } udebug.log("new completed"); udebug.log_detail("DBTableHandler:\n", this); } /** Append an error message and mark this DBTableHandler as invalid. */ DBTableHandler.prototype.appendErrorMessage = function(msg) { this.errorMessages += '\n' + msg; this.isValid = false; }; /* DBTableHandler.newResultObject IMMEDIATE Create a new object using the constructor function (if set). */ DBTableHandler.prototype.newResultObject = function(values, adapter) { udebug.log("newResultObject"); stats.result_objects_created++; var newDomainObj; if(this.newObjectConstructor && this.newObjectConstructor.prototype) { newDomainObj = Object.create(this.newObjectConstructor.prototype); } else { newDomainObj = {}; } if(this.newObjectConstructor) { udebug.log("newResultObject calling user constructor"); this.newObjectConstructor.call(newDomainObj); } if (typeof(values) === 'object') { // copy values into the new domain object this.setFields(newDomainObj, values, adapter); } udebug.log("newResultObject done", 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, adapter, offset, keyFields, nonKeyFields) { var fieldIndex; var rowValue; var field; var newDomainObj; udebug.log("newResultObjectFromRow"); stats.result_objects_created++; if(this.newObjectConstructor && this.newObjectConstructor.prototype) { newDomainObj = Object.create(this.newObjectConstructor.prototype); } else { newDomainObj = {}; } if(this.newObjectConstructor) { udebug.log("newResultObject calling user constructor"); this.newObjectConstructor.call(newDomainObj); } // 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.fieldNumber, rowValue, adapter); } // 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.fieldNumber, rowValue, adapter); } udebug.log("newResultObjectFromRow done", newDomainObj.constructor.name, newDomainObj); return newDomainObj; }; /** applyMappingToResult(object) * IMMEDIATE * Apply the table mapping to the result object. The result object * has properties corresponding to field names whose values came * from the database. If a domain object is needed, a new domain * object is created and values are copied from the result object. * The result (either the original result object or a new domain * object) is returned. * @param obj the object to which to apply mapping * @return the object to return to the user */ DBTableHandler.prototype.applyMappingToResult = function(obj, adapter) { if (this.newObjectConstructor) { // create the domain object from the result obj = this.newResultObject(obj, adapter); } else { this.applyFieldConverters(obj, adapter); } return obj; }; /** applyFieldConverters(object) * IMMEDIATE * Apply the field converters to an existing object */ DBTableHandler.prototype.applyFieldConverters = function(obj, adapter) { var i, f, value, convertedValue; for (i = 0; i < this.fieldNumberToFieldMap.length; i++) { f = this.fieldNumberToFieldMap[i]; var databaseTypeConverter = f.databaseTypeConverter && f.databaseTypeConverter[adapter]; if (databaseTypeConverter) { value = obj[f.fieldName]; convertedValue = databaseTypeConverter.fromDB(value); obj[f.fieldName] = convertedValue; } if(f.domainTypeConverter) { value = obj[f.fieldName]; convertedValue = f.domainTypeConverter.fromDB(value, obj, f); obj[f.fieldName] = convertedValue; } } }; /* 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); } }; /* getMappedFieldCount() IMMEDIATE Returns the number of fields mapped to columns in the table */ DBTableHandler.prototype.getMappedFieldCount = function() { udebug.log_detail("getMappedFieldCount"); return this.fieldNumberToColumnMap.length; }; /* allColumnsMapped() IMMEDIATE Boolean: returns True if all columns are mapped */ DBTableHandler.prototype.allColumnsMapped = function() { return (this.dbTable.columns.length === this.fieldNumberToColumnMap.length); }; /** allFieldsIncluded(values) * IMMEDIATE * returns array of indexes of fields included in values */ DBTableHandler.prototype.allFieldsIncluded = function(values) { // return a list of fields indexes that are found // the caller can easily construct the appropriate database statement var i, f, result = []; for (i = 0; i < this.fieldNumberToFieldMap.length; ++i) { f = this.fieldNumberToFieldMap[i]; if (typeof(values[i]) !== 'undefined') { result.push(i); } } return result; }; /* getColumnMetadata() IMMEDIATE Returns an array containing ColumnMetadata objects in field order */ DBTableHandler.prototype.getColumnMetadata = function() { return this.fieldNumberToColumnMap; }; /* IndexMetadata chooseIndex(dbTableHandler, keys) Returns the index number to use as an access path. From API Context.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. */ function chooseIndex(self, keys, uniqueOnly) { udebug.log("chooseIndex"); var idxs = self.dbTable.indexes; var keyFieldNames, firstIdxFieldName; var i, j, f, n, index, nmatches, x; udebug.log_detail("chooseIndex for:", JSON.stringify(keys)); if(typeof keys === 'number' || typeof keys === 'string') { if(idxs[0].columnNumbers.length === 1) { return 0; } } else { /* Keys is an object */ keyFieldNames = []; for (x in keys) { // only include properties of the keys itself that are defined and not null if (keys.hasOwnProperty(x) && keys[x]) { keyFieldNames.push(x); } } /* First look for a unique index. All columns must match. */ for(i = 0 ; i < idxs.length ; i++) { index = idxs[i]; if(index.isUnique) { udebug.log_detail("Considering:", (index.name || "primary key ") + " for " + JSON.stringify(keys)); // Each key field resolves to a column, which must be in the index nmatches = 0; for(j = 0 ; j < index.columnNumbers.length ; j++) { n = index.columnNumbers[j]; f = self.columnNumberToFieldMap[n]; udebug.log_detail("index part", j, "is column", n, ":", f.fieldName); if(typeof keys[f.fieldName] !== 'undefined') { nmatches++; udebug.log_detail("match! ", nmatches); } } if(nmatches === index.columnNumbers.length) { udebug.log("chooseIndex picked unique index", i); return i; // all columns are found in the key object } } } // if unique only, return failure if (uniqueOnly) { udebug.log("chooseIndex for unique index FAILED"); return -1; } /* Then look for an ordered index. A prefix match is OK. */ /* Return the first suitable index we find (which might not be the best) */ /* TODO: A better algorithm might be to return the one with the longest train of matches */ for(i = 0 ; i < idxs.length ; i++) { index = idxs[i]; if(index.isOrdered) { // f is the field corresponding to the first column in the index f = self.columnNumberToFieldMap[index.columnNumbers[0]]; if(keyFieldNames.indexOf(f.fieldName) >= 0) { udebug.log("chooseIndex picked ordered index", i); return i; // this is an ordered index scan } } } } udebug.log("chooseIndex FAILED"); return -1; // didn't find a suitable index } /** Return the property of obj corresponding to fieldNumber. * If a domain type converter and/or database type converter is defined, convert the value here. * If a fieldValueDefinedListener is passed, notify it via setDefined or setUndefined for each column. * Call setDefined if a column value is defined in the object and setUndefined if not. */ DBTableHandler.prototype.get = function(obj, fieldNumber, adapter, fieldValueDefinedListener) { udebug.log_detail("get", fieldNumber); if (typeof(obj) === 'string' || typeof(obj) === 'number') { if (fieldValueDefinedListener) { fieldValueDefinedListener.setDefined(fieldNumber); } return obj; } var f = this.fieldNumberToFieldMap[fieldNumber]; var result; if (!f) { throw new Error('FatalInternalError: field number does not exist: ' + fieldNumber); } if(f.domainTypeConverter) { result = f.domainTypeConverter.toDB(obj[f.fieldName], obj, f); } else { result = obj[f.fieldName]; } var databaseTypeConverter = f.databaseTypeConverter && f.databaseTypeConverter[adapter]; if (databaseTypeConverter && result !== undefined) { result = databaseTypeConverter.toDB(result); } if (fieldValueDefinedListener) { if (typeof(result) === 'undefined') { fieldValueDefinedListener.setUndefined(fieldNumber); } else { if (this.fieldNumberToColumnMap[fieldNumber].isBinary && result.constructor && result.constructor.name !== 'Buffer') { var err = new Error('Binary field with non-Buffer data for field ' + f.fieldName); err.sqlstate = '22000'; fieldValueDefinedListener.err = err; } fieldValueDefinedListener.setDefined(fieldNumber); } } return result; }; /** Return the property of obj corresponding to fieldNumber. */ DBTableHandler.prototype.getFieldsSimple = function(obj, fieldNumber) { var f; f = this.fieldNumberToFieldMap[fieldNumber]; if(f.domainTypeConverter) { return f.domainTypeConverter.toDB(obj[f.fieldName], obj, f); } return obj[f.fieldName]; }; /* Return an array of values in field order */ DBTableHandler.prototype.getFields = function(obj) { var i, n, fields; fields = []; n = this.getMappedFieldCount(); switch(typeof obj) { case 'number': case 'string': fields.push(obj); break; default: for(i = 0 ; i < n ; i++) { fields.push(this.getFieldsSimple(obj, i)); } } return fields; }; /* Return an array of values in field order */ DBTableHandler.prototype.getFieldsWithListener = function(obj, adapter, fieldValueDefinedListener) { var i, fields = []; for( i = 0 ; i < this.getMappedFieldCount() ; i ++) { fields[i] = this.get(obj, i, adapter, fieldValueDefinedListener); } return fields; }; /* Set field to value */ DBTableHandler.prototype.set = function(obj, fieldNumber, value, adapter) { udebug.log_detail("set", fieldNumber); var f = this.fieldNumberToFieldMap[fieldNumber]; var userValue = value; var databaseTypeConverter; if(f) { databaseTypeConverter = f.databaseTypeConverter && f.databaseTypeConverter[adapter]; if (databaseTypeConverter) { userValue = databaseTypeConverter.fromDB(value); } if(f.domainTypeConverter) { userValue = f.domainTypeConverter.fromDB(userValue, obj, f); } obj[f.fieldName] = userValue; return true; } return false; }; /* Set all member values of object from a value object, which * has properties corresponding to field names. * User-defined column conversion is handled in the set method. */ DBTableHandler.prototype.setFields = function(obj, values, adapter) { var i, f, value, columnName, fieldName; for (i = 0; i < this.fieldNumberToFieldMap.length; ++i) { f = this.fieldNumberToFieldMap[i]; columnName = f.columnName; fieldName = f.fieldName; value = values[fieldName]; if (value !== undefined) { this.set(obj, i, value, adapter); } } }; /* DBIndexHandler constructor and prototype */ DBIndexHandler = function (parent, dbIndex) { udebug.log("DBIndexHandler constructor"); stats.DBIndexHandler_created++; var i, colNo; this.tableHandler = parent; this.dbIndex = dbIndex; this.fieldNumberToColumnMap = []; this.fieldNumberToFieldMap = []; for(i = 0 ; i < dbIndex.columnNumbers.length ; i++) { colNo = dbIndex.columnNumbers[i]; this.fieldNumberToFieldMap[i] = parent.columnNumberToFieldMap[colNo]; this.fieldNumberToColumnMap[i] = parent.dbTable.columns[colNo]; } if(i === 1) { // One-column index this.singleColumn = this.fieldNumberToColumnMap[0]; } else { this.singleColumn = null; } }; /* DBIndexHandler inherits some methods from DBTableHandler */ DBIndexHandler.prototype = { getMappedFieldCount : DBTableHandler.prototype.getMappedFieldCount, get : DBTableHandler.prototype.get, getFieldsSimple : DBTableHandler.prototype.getFieldsSimple, getFields : DBTableHandler.prototype.getFields, getColumnMetadata : DBTableHandler.prototype.getColumnMetadata }; /* DBIndexHandler 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, uniqueOnly) { udebug.log("getIndexHandler"); var idx = chooseIndex(this, keys, uniqueOnly); var handler = null; if (idx !== -1) { handler = this.dbIndexHandlers[idx]; } return handler; }; DBTableHandler.prototype.getForeignKey = function(foreignKeyName) { return this.foreignKeyMap[foreignKeyName]; }; exports.DBTableHandler = DBTableHandler;