database-jones/Adapter/common/SQLBuilder.js (396 lines of code) (raw):
/*
Copyright (c) 2014, 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 udebug = unified_debug.getLogger("SQLBuilder.js"),
assert = require("assert");
function SQLBuilder() {
}
/** Create the INSERT and INSERT... DUPLICATE SQL statements corresponding to the fieldValueDefinedKey.
* If fieldValueDefinedKey is undefined, include all columns in the statements.
* If fieldValueDefinedKey contains a string, e.g. 'DUUUD', include only those
* columns that have a 'D' in the corresponding position.
*/
SQLBuilder.prototype.createInsertSQL = function (dbTableHandler, fieldValueDefinedKey) {
// create the insert SQL statement from the table metadata and field values defined key
var insertSQL = 'INSERT INTO ' + dbTableHandler.dbTable.database + '.' + dbTableHandler.dbTable.name + ' (';
var valuesSQL = ' VALUES (';
var duplicateSQL = ' ON DUPLICATE KEY UPDATE ';
var columns = dbTableHandler.getAllColumnMetadata();
udebug.log_detail('getMetadata with columns', columns);
// loop over the columns and extract the column name
var columnSeparator = '';
var duplicateSeparator = '';
var i, column;
for (i = 0; i < columns.length; ++i) {
if ((!fieldValueDefinedKey) || fieldValueDefinedKey[i] === 'D') {
column = columns[i];
insertSQL += columnSeparator + column.name;
valuesSQL += columnSeparator + '?';
columnSeparator = ', ';
if (!column.isInPrimaryKey) {
duplicateSQL += duplicateSeparator + column.name + ' = VALUES (' + column.name + ') ';
duplicateSeparator = ', ';
}
}
}
valuesSQL += ')';
insertSQL += ')' + valuesSQL;
if (fieldValueDefinedKey === undefined) {
dbTableHandler[this.name].insertSQL = insertSQL;
dbTableHandler[this.name].duplicateSQL = insertSQL + duplicateSQL;
udebug.log_detail('insertSQL:', insertSQL);
udebug.log_detail('duplicateSQL:', insertSQL + duplicateSQL);
} else {
dbTableHandler[this.name].insertPartialSQL[fieldValueDefinedKey] = insertSQL;
dbTableHandler[this.name].duplicatePartialSQL[fieldValueDefinedKey] = insertSQL + duplicateSQL;
udebug.log_detail('insertPartialSQL[', fieldValueDefinedKey, ']:', insertSQL);
udebug.log_detail('duplicatePartialSQL[', fieldValueDefinedKey, ']:', insertSQL + duplicateSQL);
}
};
/** Get the INSERT SQL corresponding to the fieldValueDefinedKey which is a string
* with a 'D' for each defined value and 'U' for each undefined value.
* For example, for a table with 5 columns, if the first and last columns have values
* the value of fieldValueDefinedKey is 'DUUUD'.
*/
SQLBuilder.prototype.getInsertSQL = function(dbTableHandler, fieldValueDefinedKey) {
var insertSQL = dbTableHandler[this.name].insertPartialSQL[fieldValueDefinedKey];
if (insertSQL) {
// insert all columns
return insertSQL;
}
// create the partial SQL for fieldValueDefinedKey
this.createInsertSQL(dbTableHandler, fieldValueDefinedKey);
return dbTableHandler[this.name].insertPartialSQL[fieldValueDefinedKey];
};
/** Get the INSERT... DUPLICATE SQL corresponding to the fieldValueDefinedKey which is a string
* with a 'D' for each defined value and 'U' for each undefined value.
* For example, for a table with 5 columns, if the first and last columns have values
* the value of fieldValueDefinedKey is 'DUUUD'.
*/
SQLBuilder.prototype.getDuplicateSQL = function(dbTableHandler, fieldValueDefinedKey) {
var duplicateSQL = dbTableHandler[this.name].duplicatePartialSQL[fieldValueDefinedKey];
if (duplicateSQL) {
// insert all columns on duplicate key update
return duplicateSQL;
}
// create the duplicate partial SQL for fieldValueDefinedKey
this.createInsertSQL(dbTableHandler, fieldValueDefinedKey);
return dbTableHandler[this.name].duplicatePartialSQL[fieldValueDefinedKey];
};
SQLBuilder.prototype.createDeleteSQL = function(dbTableHandler, index) {
// create the delete SQL statement from the table metadata for the named index
var deleteSQL;
if (!index) {
deleteSQL = 'DELETE FROM ' + dbTableHandler.dbTable.database + '.' + dbTableHandler.dbTable.name;
// return non-index delete statement
} else {
deleteSQL = dbTableHandler[this.name].deleteTableScanSQL + ' WHERE ';
// find the index metadata from the dbTableHandler index section
// loop over the columns in the index and extract the column name
var indexMetadatas = dbTableHandler.dbTable.indexes;
var columns = dbTableHandler.getAllColumnMetadata();
var separator = '';
var i, j, indexMetadata;
for (i = 0; i < indexMetadatas.length; ++i) {
if (indexMetadatas[i].name === index) {
indexMetadata = indexMetadatas[i];
udebug.log_detail('createDeleteSQL indexMetadata: ', indexMetadata);
for (j = 0; j < indexMetadata.columnNumbers.length; ++j) {
deleteSQL += separator + columns[indexMetadata.columnNumbers[j]].name + ' = ?';
separator = ' AND ';
}
// for unique btree indexes the first one is the unique index we are interested in
break;
}
}
}
udebug.log_detail('getMetadata deleteSQL for', index, ':', deleteSQL);
return deleteSQL;
};
SQLBuilder.prototype.createSelectSQL = function (dbTableHandler, index) {
var selectSQL;
var whereSQL;
var separator = '';
var i, j, columns;
var indexMetadatas, indexMetadata;
columns = dbTableHandler.getAllColumnMetadata();
if (!index) {
selectSQL = 'SELECT ';
var fromSQL = ' FROM ' + dbTableHandler.dbTable.database + '.' + dbTableHandler.dbTable.name;
// loop over the mapped column names in order
for (i = 0; i < columns.length; ++i) {
selectSQL += separator + columns[i].name;
separator = ', ';
}
selectSQL += fromSQL;
} else {
// create the select SQL statement from the table metadata for the named index
selectSQL = dbTableHandler.mysql.selectTableScanSQL;
whereSQL = ' WHERE ';
// loop over the index columns
// find the index metadata from the dbTableHandler index section
// loop over the columns in the index and extract the column name
indexMetadatas = dbTableHandler.dbTable.indexes;
separator = '';
for (i = 0; i < indexMetadatas.length; ++i) {
if (indexMetadatas[i].name === index) {
indexMetadata = indexMetadatas[i];
for (j = 0; j < indexMetadata.columnNumbers.length; ++j) {
whereSQL += separator + columns[indexMetadata.columnNumbers[j]].name + ' = ? ';
separator = ' AND ';
}
// for unique btree indexes the first one is the unique index we are interested in
break;
}
}
selectSQL += whereSQL;
}
udebug.log_detail('getMetadata selectSQL for', index +':', selectSQL);
return selectSQL;
};
SQLBuilder.prototype.createWhereSQL = function(dbTableHandler, index) {
var whereSQL = '';
var separator = '';
var i, j, columns;
columns = dbTableHandler.getAllColumnMetadata();
if (index) {
// create the where SQL clause from the table metadata for the named index
whereSQL = ' WHERE ';
// loop over the index columns
// find the index metadata from the dbTableHandler index section
// loop over the columns in the index and extract the column name
var indexMetadatas = dbTableHandler.dbTable.indexes;
var indexMetadata;
separator = '';
for (i = 0; i < indexMetadatas.length; ++i) {
if (indexMetadatas[i].name === index) {
indexMetadata = indexMetadatas[i];
for (j = 0; j < indexMetadata.columnNumbers.length; ++j) {
whereSQL += separator + 't0.' + columns[indexMetadata.columnNumbers[j]].name + ' = ? ';
separator = ' AND ';
}
// for unique btree indexes the first one is the unique index we are interested in
break;
}
}
}
return whereSQL;
};
SQLBuilder.prototype.getMetadata = function(dbTableHandler) {
if (dbTableHandler[this.name]) {
return;
}
udebug.log_detail('getMetadata with dbTableHandler', dbTableHandler.dbTable.name);
dbTableHandler[this.name] = {};
dbTableHandler[this.name].indexes = {};
dbTableHandler[this.name].deleteSQL = {};
dbTableHandler[this.name].deleteTableScanSQL= this.createDeleteSQL(dbTableHandler);
dbTableHandler[this.name].selectSQL = {};
dbTableHandler[this.name].selectTableScanSQL = this.createSelectSQL(dbTableHandler);
dbTableHandler[this.name].whereSQL = {};
dbTableHandler[this.name].insertPartialSQL = {};
dbTableHandler[this.name].duplicatePartialSQL = {};
this.createInsertSQL(dbTableHandler);
var i, indexes, index;
// create a delete statement and select statement per index
indexes = dbTableHandler.dbTable.indexes;
for (i = 0; i < indexes.length; ++i) {
index = dbTableHandler.dbTable.indexes[i];
dbTableHandler[this.name].deleteSQL[index.name] = this.createDeleteSQL(dbTableHandler, index.name);
dbTableHandler[this.name].selectSQL[index.name] = this.createSelectSQL(dbTableHandler, index.name);
dbTableHandler[this.name].whereSQL[index.name] = this.createWhereSQL(dbTableHandler, index.name);
}
};
/** Initialize the projection object for use with a SQL adapter.
* The projection object is organized into sectors, one for each domain object.
* A sector contains a count of fields, a list of field names, and the offset
* column corresponding to the first column mapped to the first field. All
* primary key fields are always included, plus any fields identified in the
* fields array of the corresponding projection for the domain object.
* Build the sql statement to use for the projection:
* SELECT... FROM... WHERE... ORDER BY...
* For each sector, add the mapped table to the FROM clause including the
* join condition. Add the key and non-key columns to the SELECT clause.
* Add ORDER BY key columns for multi-value relationships.
*/
SQLBuilder.prototype.initializeProjection = function(projection) {
var i, j;
var sector, sectorName;
var relatedSectorName;
var select, from, on, alias, order;
var thisOn, otherOn, and;
var selectDelimiter, fromDelimiter, orderDelimiter;
var columnName;
var joinType, joinIndex;
var offset;
var keyField, nonKeyField;
var mysql = {};
projection[this.name] = mysql;
// create the sql query for the find method.
select = 'SELECT ';
from = ' FROM ';
order = '';
selectDelimiter = '';
fromDelimiter = '';
orderDelimiter = '';
alias = 0;
offset = 0;
for (i = 0; i < projection.sectors.length; ++i) {
sector = projection.sectors[i];
udebug.log_detail('initializeProjection for sector\n', sector);
// offset of each sector into column in row
sector.offset = offset;
offset += sector.keyFields.length + sector.nonKeyFields.length;
// set up the table names
sector.tableName = sector.tableHandler.dbTable.database + '.' + sector.tableHandler.dbTable.name;
sectorName = 't' + i;
relatedSectorName = 't' + (i - 1);
joinType = '';
on = '';
if (sector.relatedFieldMapping && i > 0) {
sector.relatedTableName = sector.relatedTableHandler.dbTable.database + '.' + sector.relatedTableHandler.dbTable.name;
if (sector.relatedFieldMapping.toMany && sector.relatedFieldMapping.manyTo) {
// join table mapping
// create a join table reference based on current table name
// join tables are "between" tables that are joined for many-to-many relationships
// ... t1 LEFT OUTER JOIN customerdiscount AS t15 on [t1.k = t15.k and...]
// LEFT OUTER JOIN discount AS t2 on [t15.k = t2.k and ...]
sector.joinTableName = sector.joinTableHandler.dbTable.database + '.' + sector.joinTableHandler.dbTable.name;
sector.joinTableAlias = relatedSectorName + 'J';
udebug.log_detail('initializeProjection join table handling for', sector.joinTableName, 'AS', sector.joinTableAlias,
'thisForeignKey.columnNames', sector.relatedFieldMapping.thisForeignKey.columnNames,
'otherForeignKey.columnNames', sector.relatedFieldMapping.otherForeignKey.columnNames);
// generate the join from the previous domain table to the join table
joinType = ' LEFT OUTER JOIN ';
thisOn = ' ON ';
and = '';
for (joinIndex = 0; joinIndex < sector.relatedFieldMapping.thisForeignKey.columnNames.length; ++joinIndex) {
thisOn += and + relatedSectorName + '.' + sector.relatedFieldMapping.thisForeignKey.targetColumnNames[joinIndex] + ' = ' +
sector.joinTableAlias + '.' + sector.relatedFieldMapping.thisForeignKey.columnNames[joinIndex];
and = ' AND ';
}
from += fromDelimiter + joinType + sector.joinTableName + ' AS ' + sector.joinTableAlias + thisOn;
// generate the join from the join table to this domain table
otherOn = ' ON ';
and = '';
for (joinIndex = 0; joinIndex < sector.relatedFieldMapping.otherForeignKey.columnNames.length; ++joinIndex) {
otherOn += and + sector.joinTableAlias + '.' + sector.relatedFieldMapping.otherForeignKey.columnNames[joinIndex] + ' = ' +
sectorName + '.' + sector.relatedFieldMapping.otherForeignKey.targetColumnNames[joinIndex];
and = ' AND ';
}
from += fromDelimiter + joinType + sector.tableName + ' AS ' + sectorName + otherOn;
} else {
// foreign key mapping for one-to-one, one-to-many, and many-to-one relationships
joinType = ' LEFT OUTER JOIN ';
on = ' ON ';
and = '';
for (joinIndex = 0; joinIndex < sector.thisJoinColumns.length; ++joinIndex) {
on += and + relatedSectorName + '.' + sector.otherJoinColumns[joinIndex] + ' = ' +
sectorName + '.' + sector.thisJoinColumns[joinIndex];
and = ' AND ';
}
if (sector.relatedFieldMapping.toMany) {
// order by key columns that can have multiple values (toMany relationships)
for (j = 0; j < sector.keyFields.length; ++j) {
keyField = sector.keyFields[j];
columnName = keyField.columnName;
order += orderDelimiter + sectorName + '.' + columnName;
orderDelimiter = ', ';
}
}
from += fromDelimiter + joinType + sector.tableName + ' AS ' + sectorName + on;
}
} else {
// first table is always t0
from += sector.tableName + ' AS ' + sectorName;
fromDelimiter = ' ';
}
// add key column names to SELECT clause
for (j = 0; j < sector.keyFields.length; ++j) {
keyField = sector.keyFields[j];
columnName = keyField.columnName;
select += selectDelimiter + sectorName + '.' + columnName + ' AS \'' + alias++ + '\'';
selectDelimiter = ', ';
}
// add non-key column names to SELECT clause
for (j = 0; j < sector.nonKeyFields.length; ++j) {
nonKeyField = sector.nonKeyFields[j];
columnName = nonKeyField.columnName;
select += selectDelimiter + sectorName + '.' + columnName + ' AS \'' + alias++ + '\'';
selectDelimiter = ', ';
}
}
mysql.select = select;
mysql.from = from;
mysql.sectors = projection.sectors;
if (order) {
mysql.order = 'ORDER BY ' + order;
} else {
mysql.order = '';
}
// mark this as having been processed
mysql.id = projection.id;
};
function defaultFieldMeta(fieldMapping) {
if (fieldMapping.fieldName == 'id') {
return 'id INT PRIMARY KEY';
}
return fieldMapping.fieldName + ' VARCHAR(32) ';
}
function pn(nullable) {return nullable? '': ' NOT NULL ';}
function pu(unsigned) {return unsigned? ' UNSIGNED' : '';}
var translateMeta = {};
translateMeta.binary = function(length, nullable) {return 'BINARY(' + length + ')' + pn(nullable);};
translateMeta.char = function(length, lob, nullable, generated, characterset, collate) {
var result;
if (!lob) {
result = 'CHAR(' + length + ')' + pn(nullable);
} else {
// lob
result = 'TEXT(' + length + ')' + pn(nullable);
}
if (characterset !== undefined) {result += ' CHARACTER SET ' + characterset;}
if (collate !== undefined) {result += ' COLLATE ' + collate;}
return result;
};
translateMeta.date = function(nullable) {return 'DATE' + pn(nullable);};
translateMeta.datetime = function(fsp, nullable, generated) {
var sql = 'DATETIME(' + fsp + ')' + pn(nullable);
if(generated) { sql += ' DEFAULT CURRENT_TIMESTAMP'; }
return sql;
};
translateMeta.decimal = function(precision, scale, nullable) {return 'DECIMAL(' + precision + ', ' + scale + ')' + pn(nullable);};
translateMeta.double = function(nullable) {return 'DOUBLE' + pn(nullable);};
translateMeta.float = function(nullable) {return 'FLOAT' + pn(nullable);};
translateMeta.integer = function(bits, unsigned, nullable, generated) {
var u = pu(unsigned);
var n = pn(nullable);
var autoinc = generated ? " AUTO_INCREMENT" : "";
if (bits < 8) {return 'BIT' + u + n;}
if (bits == 8) {return 'TINYINT' + u + n + autoinc;}
if (bits <= 16) {return 'SMALLINT' + u + n + autoinc;}
if (bits <= 24) {return 'MEDIUMINT' + u + n + autoinc;}
if (bits <= 32) {return 'INT' + u + n + autoinc;}
/* else */ return 'BIGINT' + u + n + autoinc;
};
translateMeta.interval = function(fsp, nullable) {return 'TIME' + pn(nullable);};
translateMeta.time = function(fsp, nullable) {return 'TIME' + pn(nullable);};
translateMeta.timestamp = function(fsp, nullable, generated) {
var sql = 'TIMESTAMP' + pn(nullable);
if(generated) { sql += ' DEFAULT CURRENT_TIMESTAMP'; }
return sql;
};
translateMeta.varbinary = function(length, lob, nullable) {
if (lob) {
return 'BLOB(' + length + ')' + pn(nullable);
}
return 'VARBINARY(' + length + ')' + pn(nullable);
};
translateMeta.varchar = function(length, lob, nullable, characterset, collate) {
var result;
if (lob) {
result = 'TEXT(' + length + ')';
} else {
result = 'VARCHAR(' + length + ')';
}
result += pn(nullable);
if (characterset !== undefined) {result += ' CHARACTER SET ' + characterset;}
if (collate !== undefined) {result += ' COLLATE ' + collate;}
return result;
};
translateMeta.year = function(nullable) {return 'YEAR' + pn(nullable);};
SQLBuilder.prototype.getSqlForTableCreation = function (tableMapping, engine) {
udebug.log_detail('sqlForTableCreation tableMapping', tableMapping, engine);
var i, field, delimiter = '';
var fieldMeta, tableMeta, columnMeta;
var sparseField;
var sql = 'CREATE TABLE ';
sql += tableMapping.database;
sql += '.';
sql += tableMapping.table;
sql += '(';
// append SQL for a field
function appendFieldDefinition(field) {
sql += delimiter;
delimiter = ', ';
sql += field.columnName || field.fieldName;
sql += ' ';
fieldMeta = field.meta;
if (fieldMeta) {
columnMeta = fieldMeta.doit(translateMeta);
if(fieldMeta.defaultVal) {
columnMeta += ' DEFAULT "' + fieldMeta.defaultVal + '"';
}
sql += columnMeta;
if(fieldMeta.hasIndex) {
sql += fieldMeta.isUnique ? ' UNIQUE' : '';
sql += ' KEY ';
} else {
sql += fieldMeta.isPrimaryKey? ' PRIMARY KEY' : '';
}
udebug.log_detail('sqlForTableCreation field:', field.fieldName, 'column:', field.columnName, 'fieldMeta:', fieldMeta, 'columnMeta:', columnMeta);
} else {
sql += defaultFieldMeta(field);
}
}
// append SQL for each defined field
for (i = 0; i < tableMapping.fields.length; ++i) {
field = tableMapping.fields[i];
appendFieldDefinition(field);
}
// append SQL for the optional sparse field container
sparseField = tableMapping.sparseContainer;
if (sparseField) {
appendFieldDefinition(sparseField);
}
// process meta for the table
// need to support PRIMARY and HASH
for (i = 0; i < tableMapping.meta.length; ++i) {
tableMeta = tableMapping.meta[i];
if (tableMeta.isIndex) {
sql += delimiter;
if(tableMeta.isPrimaryKey) {
sql += ' PRIMARY KEY ';
} else {
sql += (tableMeta.isUnique?' UNIQUE ': ' ') + 'INDEX ';
sql += tableMeta.name || "";
}
if(tableMeta.isHash) { sql += ' USING HASH'; }
sql += ' ( ' + tableMeta.columns + ') ';
}
}
sql += ")";
if(engine) {
sql += ' ENGINE=' + engine;
}
sql += ";";
udebug.log('sqlForTableMapping sql: ', sql);
return sql;
};
module.exports = SQLBuilder;