'use strict';
require('insulin').factory('ndm_From',
['ndm_assert', 'ndm_ConditionError', 'ndm_ConditionLexer',
'ndm_ConditionParser', 'ndm_ConditionCompiler', 'ndm_Query',
'ndm_TableMetaList', 'ndm_Column', 'ndm_ParameterList'],
ndm_FromProducer);
function ndm_FromProducer(assert, ConditionError, ConditionLexer,
ConditionParser, ConditionCompiler, Query, TableMetaList, Column,
ParameterList) {
/**
* A From can be used to create a SELECT, DELETE, or UPDATE query.
* @extends Query
*/
class From extends Query {
/**
* Initialize the From instance. WHERE and JOINs can be applied.
* @param {Database} database - A Database instance that will be queried.
* @param {Escaper} escaper - An instance of an Escaper matching the
* database type (i.e. MySQLEscaper or MSSQLEscaper).
* @param {QueryExecuter} queryExecuter - A QueryExecuter instance.
* @param {TableMetaList~TableMeta|string} meta - Either a TableMeta
* instance containing information about the table to query from, or a
* string that can be parsed using the {@link From#parseFromString} method.
*/
constructor(database, escaper, queryExecuter, meta) {
super(database, escaper, queryExecuter);
/**
* A ParameterList instance that holds all of the query parameters.
* @type {ParameterList}
* @name From#paramList
* @public
*/
this.paramList = new ParameterList();
// These are for building conditions (WHERE and ON conditions).
this._condLexer = new ConditionLexer();
this._condParser = new ConditionParser();
this._condCompiler = new ConditionCompiler(this.escaper);
// This is the list of tables that are added. The first table is the
// FROM table, and all the other tables are added via JOINs.
this._tableMetaList = new TableMetaList(this.database);
// Add the FROM table.
if (typeof meta === 'string')
this._tableMetaList.addTable(this.parseFromString(meta));
else
this._tableMetaList.addTable(meta);
}
/**
* Parse the string fromStr and return a table meta object.
* @param {string} fromStr - A from string, in the format
* <table-name>[ [as ]<table-alias>]. For example, any of
* these is valid: 'users'; 'users u'; 'users as u'; 'users AS u'.
* @return {TableMetaList~TableMeta} A meta object that has "table" and
* "as" set.
*/
parseFromString(fromStr) {
let matches, table, as;
// table t.
if ((matches = fromStr.match(/^(\w+)\s+(\w+)$/)))
[, table, as] = matches;
// table
else if ((matches = fromStr.match(/^\w+$/)))
table = as = fromStr;
// table as t
else if ((matches = fromStr.match(/^(\w+)\s+(?:as|AS)\s+(\w+)$/)))
[, table, as] = matches;
else
throw new Error('From must be in the format: <table-name>[ [as ]<table-alias>].');
return {table, as};
}
/**
* Parse the string joinStr and return a table meta object.
*
* If a parent alias is provided, there must be exactly 1 relationship
* between the parent table and the child table, or an exception shall be
* raised.
*
* If the parent table owns the primary key, the relType will be single
* (e.g. it is a many-to-one relationship).
*
* If the child table owns the primary key, the relType will be many (it is
* a one-to-many relationship).
*
* @param {string} joinStr - A join string, in the format
* [<parent-table-alias>.]<table-name>[ [as ]<table-alias>].
* For example, any of these is valid (assuming 'u' is a valid parent table
* alias): 'u.phone_numbers'; 'u.phone_numbers pn'; 'u.phone_numbers as pn';
* 'u.phone_numbers AS pn'; 'phone_numbers'; 'phone_numbers pn';
* 'phone_numbers as pn'; 'phone_numbers AS pn'.
* @return {TableMetaList~TableMeta} A meta object that has at least
* "table" and "as" set. "parent", "relType" and "cond" will also be set if
* a parent table alias is provided.
*/
parseJoinString(joinStr) {
const parentRE = /^(\w+)\./;
let matches, parent, meta;
// Check for a parent.
if ((matches = joinStr.match(parentRE))) {
[, parent] = matches;
joinStr = joinStr.replace(parentRE, '');
}
// Without a parent, the remaining string is the same as a from string.
try {
meta = this.parseFromString(joinStr);
}
catch (ex) { // jshint:ignore line
// The error is customized.
throw new Error('Join must be in the format: ' +
'[<parent-table-alias>.]<table-name>[ [as ]<table-alias>].');
}
// If there is a parent alias provided, figoure out how to join the two
// tables.
if (parent) {
assert(this._tableMetaList.tableMetas.has(parent),
`The parent of table "${meta.table}" ("${parent}") is invalid.`);
const parTblMeta = this._tableMetaList.tableMetas.get(parent);
const parTblName = parTblMeta.table.name;
const fks = this.database.relStore.getRelationships(
meta.table, parTblName);
let parCol, childCol;
assert(fks.length === 1,
'Automatic joins can only be performed if there is exactly one '+
'relationship between the parent and child tables.');
if (fks[0].table === parTblName) {
// Parent owns the foreign key.
parCol = Column.createFQColName(parent, fks[0].column);
childCol = Column.createFQColName(meta.as, fks[0].references.column);
meta.relType = 'single';
}
else {
// Child owns the foreign key.
parCol = Column.createFQColName(parent, fks[0].references.column);
childCol = Column.createFQColName(meta.as, fks[0].column);
meta.relType = 'many';
}
meta.cond = {$eq: {[parCol]: childCol}};
meta.parent = parent;
}
return meta;
}
/**
* Helper method to get the meta data of the FROM table, which is the first
* table in the _tables map.
* @protected
* @return {TableMetaList~TableMeta} A meta object describing the table.
*/
getFromMeta() {
return this._tableMetaList.tableMetas.values().next().value;
}
/**
* Helper method to get the meta data of the JOIN'd in tables.
* @protected
* @return {TableMetaList~TableMeta[]} A meta object describing the table.
*/
getJoinMeta() {
// All but the first table (the first table is the FROM table).
return Array.from(this._tableMetaList.tableMetas.values()).slice(1);
}
/**
* Add a where condition.
* @param {Condition} cond - The condition object. For the format specs,
* reference ConditionCompiler.
* @param {Object} params - An object of key-value pairs that are used to
* replace parameters in the query.
* @return {this}
*/
where(cond, params) {
const fromMeta = this.getFromMeta();
let tokens, tree, columns;
assert(fromMeta.cond === null,
'where already performed on query.');
// Store the parameters in the list.
this.paramList.addParameters(params);
// Lex and parse the condition.
tokens = this._condLexer.parse(cond);
tree = this._condParser.parse(tokens);
// Make sure that each column in the condition is available for selection.
columns = this._condCompiler.getColumns(tree);
for (let i = 0; i < columns.length; ++i) {
if (!this._tableMetaList.isColumnAvailable(columns[i]))
throw new ConditionError(`The column "${columns[i]}" is not available for a where condition.`);
}
fromMeta.cond = this._condCompiler.compile(tree, this.paramList.params);
return this;
}
/**
* Join a table.
* @param {TableMetaList~TableMeta|string} meta - Either a TableMeta
* instance containing information about the table to join, or a
* string that can be parsed using the {@link From#parseJoinString} method.
* @param {Object} params - An object of key-value pairs that are used to
* replace parameters in the query.
* @return {this}
*/
join(meta, params) {
// "on" is a convenience alias for "cond", as "on" is more intuitive when
// joining tables.
const on = meta.on || meta.cond;
let tokens, tree;
// The joinType is required.
assert(meta.joinType, 'joinType is required.');
// Store the parameters in the list.
this.paramList.addParameters(params);
if (on) {
// Lex, parse, and compile the condition.
tokens = this._condLexer.parse(on);
tree = this._condParser.parse(tokens);
meta.cond = this._condCompiler.compile(tree, params);
}
// Add the JOIN table.
this._tableMetaList.addTable(meta);
// Make sure that each column used in the join is available (e.g. belongs to
// one of the tables in the query).
if (on) {
this._condCompiler.getColumns(tree).forEach(function(col) {
if (!this._tableMetaList.isColumnAvailable(col))
throw new ConditionError(`The column "${col}" is not available for an on condition.`);
}, this);
}
return this;
}
/**
* Private helper method for joins.
* @private
* @param {TableMetaList~TableMeta|string} meta - See {@link From#join}
* @param {Object} params - See {@link From#join}
* @param {From.JOIN_TYPE} joinType - The type of join.
* @return {this}
*/
_join(meta, params, joinType) {
if (typeof meta === 'string')
meta = this.parseJoinString(meta);
meta.joinType = joinType;
return this.join(meta, params);
}
/**
* Inner join a table.
* @param {TableMetaList~TableMeta|string} meta - See {@link From#join}
* @param {Object} params - See {@link From#join}
* @return {this}
*/
innerJoin(meta, params) {
return this._join(meta, params, From.JOIN_TYPE.INNER);
}
/**
* Left outer join a table.
* @param {TableMetaList~TableMeta|string} meta - See {@link From#join}
* @param {Object} params - See {@link From#join}
* @return {this}
*/
leftOuterJoin(meta, params) {
return this._join(meta, params, From.JOIN_TYPE.LEFT_OUTER);
}
/**
* Right outer join a table.
* @param {TableMetaList~TableMeta|string} meta - See {@link From#join}
* @param {Object} params - See {@link From#join}
* @return {this}
*/
rightOuterJoin(meta, params) {
return this._join(meta, params, From.JOIN_TYPE.RIGHT_OUTER);
}
/**
* Get the FROM portion of the query as a string.
* @return {string} The FROM portion of the query (FROM <table> AS
* <alias>), escaped.
*/
getFromString() {
const fromMeta = this.getFromMeta();
const fromName = this.escaper.escapeProperty(fromMeta.table.name);
const fromAlias = this.escaper.escapeProperty(fromMeta.as);
return `FROM ${fromName} AS ${fromAlias}`;
}
/**
* Get the JOIN parts of the query string.
* @return {string} The JOIN parts of the query, escaped.
*/
getJoinString() {
const joins = [];
// Add any JOINs. The first table is the FROM table, hence the initial
// next() call on the table iterator.
this.getJoinMeta().forEach(function(tblMeta) {
const joinName = this.escaper.escapeProperty(tblMeta.table.name);
const joinAlias = this.escaper.escapeProperty(tblMeta.as);
let sql = `${tblMeta.joinType} ${joinName} AS ${joinAlias}`;
if (tblMeta.cond)
sql += ` ON ${tblMeta.cond}`;
joins.push(sql);
}, this);
return joins.join('\n');
}
/**
* Get the WHERE portion of the query string.
* @return {string} The WHERE part of the query, or a blank string if there
* is no where clause.
*/
getWhereString() {
const fromMeta = this.getFromMeta();
return fromMeta.cond ? `WHERE ${fromMeta.cond}` : '';
}
/**
* Get the SQL that represents the query.
* @return {string} The actual SQL query (FROM, JOINS, and WHERE).
*/
toString() {
const parts = [
this.getFromString(),
this.getJoinString(),
this.getWhereString()
];
return parts
.filter(part => part !== '')
.join('\n');
}
}
/**
* @typedef From.JOIN_TYPE
* @constant
* @static
* @type {Object}
* @property {string} INNER - INNER JOIN.
* @property {string} LEFT_OUTER - LEFT OUTER JOIN.
* @property {string} RIGHT_OUTER - RIGHT OUTER JOIN.
*/
From.JOIN_TYPE = {
INNER: 'INNER JOIN',
LEFT_OUTER: 'LEFT OUTER JOIN',
RIGHT_OUTER: 'RIGHT OUTER JOIN'
};
return From;
}