// *** DEBUG START ***
// Remove comments for testing in NODE
import { Table } from './Table.js';
import { BindData } from './Sql.js';
import { DerivedTable, TableFields, TableField, CalculatedField, SqlServerFunctions } from './Views.js';
export { JoinTables, JoinTablesRecordIds };
// *** DEBUG END ***/
/**
* @classdesc Handle the various JOIN table types.
*/
class JoinTables { // skipcq: JS-0128
constructor() {
/** @property {JoinTablesRecordIds} */
this.joinTableIDs = new JoinTablesRecordIds(this);
/** @property {TableFields} */
this.tableFields = null;
/** @property {BindData} */
this.bindVariables = null;
/** @property {Map<String,Table>} */
this.tableInfo = null;
}
/**
* Info for all tables referenced in join.
* @param {Map<String,Table>} tableInfo - Map of table info.
* @returns {JoinTables}
*/
setTableInfo(tableInfo) {
this.tableInfo = tableInfo;
this.joinTableIDs.setTableInfo(tableInfo);
return this;
}
/**
* Add info about all known tables and their fields.
* @param {TableFields} tableFields
* @returns {JoinTables}
*/
setTableFields(tableFields) {
this.tableFields = tableFields;
this.joinTableIDs.setTableFields(tableFields);
return this;
}
/**
* Add data set on command line to be used when evaulating SELECT WHERE
* @param {BindData} bindVariables - Bind variable data.
* @returns {JoinTables}
*/
setBindVariables(bindVariables) {
this.bindVariables = bindVariables;
this.joinTableIDs.setBindVariables(bindVariables);
return this;
}
/**
* The "FROM" table.
* @param {Table} primaryTableInfo
* @returns {JoinTables}
*/
setPrimaryTableInfo(primaryTableInfo) {
this.primaryTableInfo = primaryTableInfo;
this.joinTableIDs.setPrimaryTableInfo(primaryTableInfo);
return this;
}
/**
* Join the tables and create a derived table with the combined data from all.
* @param {Object} ast - AST list of tables to join.
*/
load(ast) {
/** @property {DerivedTable} - result table after tables are joined */
this.derivedTable = new DerivedTable();
for (const joinTable of ast.JOIN) {
this.joinNextTable(joinTable, ast.FROM.table.toUpperCase());
}
}
/**
* Updates derived table with join to new table.
* @param {Object} astJoin
* @param {String} leftTableName
*/
joinNextTable(astJoin, leftTableName) {
const recIds = this.joinCondition(astJoin, leftTableName);
const joinFieldsInfo = this.joinTableIDs.getJoinFieldsInfo();
this.derivedTable = JoinTables.joinTables(joinFieldsInfo, astJoin, recIds);
// Field locations have changed to the derived table, so update our
// virtual field list with proper settings.
this.tableFields.updateDerivedTableVirtualFields(this.derivedTable);
}
/**
* Find the record ID's from table that match specified conditions.
* @param {Object} conditions
* @param {String} leftTableName
* @returns {MatchingJoinRecordIDs}
*/
joinCondition(conditions, leftTableName) {
let recIds = null;
const rightTableName = conditions.table;
const joinType = conditions.type;
if (typeof conditions.cond.logic === 'undefined') {
recIds = this.resolveCondition("OR", [conditions], joinType, rightTableName, leftTableName);
}
else {
recIds = this.resolveCondition(conditions.cond.logic, conditions.cond.terms, joinType, rightTableName, leftTableName);
}
return recIds;
}
/**
* Apply logic and conditions between the two tables to find the record ID's from LEFT and RIGHT tables.
* @param {String} logic - AND, OR
* @param {Object} astConditions
* @param {String} joinType - inner, full, left, right
* @param {String} rightTableName - right join table.
* @param {String} leftTableName - left join table name
* @returns {MatchingJoinRecordIDs}
*/
resolveCondition(logic, astConditions, joinType, rightTableName, leftTableName) {
let leftJoinRecordIDs = [];
let rightJoinRecordIDs = [];
/** @type {MatchingJoinRecordIDs} */
let matchedIDs = null;
this.joinTableIDs
.setLeftTableName(leftTableName)
.setRightTableName(rightTableName)
.setJoinType(joinType)
.setTableFields(this.tableFields);
for (const cond of astConditions) {
if (typeof cond.logic === 'undefined') {
matchedIDs = this.joinTableIDs.getRecordIDs(cond);
}
else {
matchedIDs = this.resolveCondition(cond.logic, cond.terms, joinType, rightTableName, leftTableName);
}
leftJoinRecordIDs.push(matchedIDs.leftJoinRecordIDs);
rightJoinRecordIDs.push(matchedIDs.rightJoinRecordIDs);
}
if (logic === "AND") {
leftJoinRecordIDs = JoinTables.andJoinIds(leftJoinRecordIDs);
rightJoinRecordIDs = JoinTables.andJoinIds(rightJoinRecordIDs);
}
if (logic === "OR") {
leftJoinRecordIDs = JoinTables.orJoinIds(leftJoinRecordIDs);
rightJoinRecordIDs = JoinTables.orJoinIds(rightJoinRecordIDs);
}
return { leftJoinRecordIDs, rightJoinRecordIDs };
}
/**
* AND logic applied to the record ID's
* @param {Array} recIds
* @returns {Array}
*/
static andJoinIds(recIds) {
const result = [];
for (let i = 0; i < recIds[0].length; i++) {
const temp = [];
for (const rec of recIds) {
temp.push(typeof rec[i] === 'undefined' ? [] : rec[i]);
}
const row = temp.reduce((a, b) => a.filter(c => b.includes(c)));
if (row.length > 0) {
result[i] = row;
}
}
return result;
}
/**
* OR logic applied to the record ID's
* @param {Array} recIds
* @returns {Array}
*/
static orJoinIds(recIds) {
const result = [];
for (let i = 0; i < recIds[0].length; i++) {
let temp = [];
for (const rec of recIds) {
temp = temp.concat(rec[i]);
}
if (typeof temp[0] !== 'undefined') {
result[i] = Array.from(new Set(temp));
}
}
return result;
}
/**
* Does this object contain a derived (joined) table.
* @returns {Boolean}
*/
isDerivedTable() {
if (typeof this.derivedTable === 'undefined') {
return false;
}
return this.derivedTable.isDerivedTable();
}
/**
* Get derived table after tables are joined.
* @returns {Table}
*/
getJoinedTableInfo() {
return this.derivedTable.getTableData();
}
/**
* Join two tables and create a derived table that contains all data from both tables.
* @param {LeftRightJoinFields} leftRightFieldInfo - left table field of join
* @param {Object} joinTable - AST that contains join type.
* @param {MatchingJoinRecordIDs} recIds
* @returns {DerivedTable} - new derived table after join of left and right tables.
*/
static joinTables(leftRightFieldInfo, joinTable, recIds) {
let derivedTable = null;
let rightDerivedTable = null;
switch (joinTable.type) {
case "left":
derivedTable = new DerivedTable()
.setLeftField(leftRightFieldInfo.leftSideInfo.fieldInfo)
.setRightField(leftRightFieldInfo.rightSideInfo.fieldInfo)
.setLeftRecords(recIds.leftJoinRecordIDs)
.setIsOuterJoin(true)
.createTable();
break;
case "inner":
derivedTable = new DerivedTable()
.setLeftField(leftRightFieldInfo.leftSideInfo.fieldInfo)
.setRightField(leftRightFieldInfo.rightSideInfo.fieldInfo)
.setLeftRecords(recIds.leftJoinRecordIDs)
.setIsOuterJoin(false)
.createTable();
break;
case "right":
derivedTable = new DerivedTable()
.setLeftField(leftRightFieldInfo.rightSideInfo.fieldInfo)
.setRightField(leftRightFieldInfo.leftSideInfo.fieldInfo)
.setLeftRecords(recIds.leftJoinRecordIDs)
.setIsOuterJoin(true)
.createTable();
break;
case "full":
derivedTable = new DerivedTable()
.setLeftField(leftRightFieldInfo.leftSideInfo.fieldInfo)
.setRightField(leftRightFieldInfo.rightSideInfo.fieldInfo)
.setLeftRecords(recIds.leftJoinRecordIDs)
.setIsOuterJoin(true)
.createTable();
rightDerivedTable = new DerivedTable()
.setLeftField(leftRightFieldInfo.rightSideInfo.fieldInfo)
.setRightField(leftRightFieldInfo.leftSideInfo.fieldInfo)
.setLeftRecords(recIds.rightJoinRecordIDs)
.setIsOuterJoin(true)
.createTable();
derivedTable.tableInfo.concat(rightDerivedTable.tableInfo); // skipcq: JS-D008
break;
default:
throw new Error(`Internal error. No support for join type: ${joinTable.type}`);
}
return derivedTable;
}
}
/**
* @classdesc
* Find record ID's for matching JOINed table records.
*/
class JoinTablesRecordIds {
/**
* @param {JoinTables} joinTables
*/
constructor(joinTables) {
/** @property {JoinTables} */
this.dataJoin = joinTables;
/** @property {TableFields} */
this.tableFields = null;
/** @property {LeftRightJoinFields} */
this.joinFields = null;
/** @property {TableFields} */
this.tableFields = null;
/** @property {Map<String,Table>} */
this.tableInfo = null;
/** @property {BindData} */
this.bindVariables = null;
/** @property {Table} */
this.primaryTableInfo = null
/** @property {Table} */
this.masterTable = null;
/** @property {String} */
this.rightTableName = "";
/** @property {String} */
this.leftTableName = "";
/** @property {String} */
this.joinType = "";
}
/**
*
* @param {Object} conditionAst
* @returns {MatchingJoinRecordIDs}
*/
getRecordIDs(conditionAst) {
/** @type {Table} */
this.masterTable = this.dataJoin.isDerivedTable() ? this.dataJoin.getJoinedTableInfo() : this.primaryTableInfo;
this.calcSqlField = new CalculatedField(this.masterTable, this.primaryTableInfo, this.tableFields);
this.joinFields = this.getLeftRightFieldInfo(conditionAst);
return this.getMatchedRecordIds();
}
/**
*
* @param {TableFields} tableFields
* @returns {JoinTablesRecordIds}
*/
setTableFields(tableFields) {
this.tableFields = tableFields;
return this;
}
/**
*
* @param {Map<String,Table>} tableInfo - Map of table info.
* @returns {JoinTablesRecordIds}
*/
setTableInfo(tableInfo) {
this.tableInfo = tableInfo;
return this;
}
/**
*
* @param {BindData} bindVariables - Bind variable data.
* @returns {JoinTablesRecordIds}
*/
setBindVariables(bindVariables) {
this.bindVariables = bindVariables;
return this;
}
/**
*
* @param {String} name
* @returns {JoinTablesRecordIds}
*/
setRightTableName(name) {
this.rightTableName = name;
return this;
}
/**
*
* @param {String} name
* @returns {JoinTablesRecordIds}
*/
setLeftTableName(name) {
this.leftTableName = name;
return this;
}
/**
*
* @param {String} joinType
* @returns {JoinTablesRecordIds}
*/
setJoinType(joinType) {
this.joinType = joinType;
return this;
}
/**
*
* @param {Table} primaryTableInfo
* @returns {JoinTablesRecordIds}
*/
setPrimaryTableInfo(primaryTableInfo) {
this.primaryTableInfo = primaryTableInfo;
return this;
}
/**
*
* @returns {LeftRightJoinFields}
*/
getJoinFieldsInfo() {
return this.joinFields;
}
/**
* @typedef {Object} LeftRightJoinFields
* @property {JoinSideInfo} leftSideInfo
* @property {JoinSideInfo} rightSideInfo
*
*/
/**
* @typedef {Object} JoinSideInfo
* @property {TableField} fieldInfo
* @property {String} column
*/
/**
* Find the LEFT table and RIGHT table joining fields from AST.
* @param {Object} astJoin
* @returns {LeftRightJoinFields}
*/
getLeftRightFieldInfo(astJoin) {
/** @type {TableField} */
let leftFieldInfo = null;
/** @type {TableField} */
let rightFieldInfo = null;
const left = typeof astJoin.cond === 'undefined' ? astJoin.left : astJoin.cond.left;
const right = typeof astJoin.cond === 'undefined' ? astJoin.right : astJoin.cond.right;
leftFieldInfo = this.getTableInfoFromCalculatedField(left);
rightFieldInfo = this.getTableInfoFromCalculatedField(right);
/** @type {JoinSideInfo} */
const leftSideInfo = {
fieldInfo: leftFieldInfo,
column: left
};
/** @type {JoinSideInfo} */
const rightSideInfo = {
fieldInfo: rightFieldInfo,
column: right
}
// joinTable.table is the RIGHT table, so switch if equal to condition left.
if (typeof leftFieldInfo !== 'undefined' && this.rightTableName === leftFieldInfo.originalTable) {
return {
leftSideInfo: rightSideInfo,
rightSideInfo: leftSideInfo
};
}
return { leftSideInfo, rightSideInfo };
}
/**
* Look for referenced columns in expression to determine table.
* @param {String} calcField - Expression to parse.
* @returns {TableField} - All SQL function parameters found. It will include COLUMN names and constant data.
*/
getTableInfoFromCalculatedField(calcField) {
let foundTableField = this.tableFields.getFieldInfo(calcField);
if (typeof foundTableField === 'undefined' && calcField !== '') {
// Calculated expression.
foundTableField = this.getReferencedTableInfo(calcField);
}
return foundTableField;
}
/**
* Find the referenced table within the calculated field.
* @param {String} calcField
* @returns {TableField}
*/
getReferencedTableInfo(calcField) {
let foundTableField = null;
const sqlFunc = new SqlServerFunctions();
// A side effect when converting an expression to Javascript is that we have a list
// of referenced column data (referenced in SQL functions)
sqlFunc.convertToJs(calcField, this.tableFields.allFields);
const columns = sqlFunc.getReferencedColumns();
foundTableField = this.searchColumnsForTable(calcField, columns);
if (foundTableField !== null) {
return foundTableField;
}
// No functions with parameters were used in 'calcField', so we don't know table yet.
// We search the calcField for valid columns - except within quotes.
const quotedConstantsRegEx = /["'](.*?)["']/g;
const opRegEx = /[+\-/*()]/g;
const results = calcField.replace(quotedConstantsRegEx, "");
let parts = results.split(opRegEx);
parts = parts.map(a => a.trim()).filter(a => a !== '');
foundTableField = this.searchColumnsForTable(calcField, parts);
if (foundTableField === null) {
throw new Error(`Failed to JOIN: ${calcField}`);
}
return foundTableField;
}
/**
*
* @param {String} calcField
* @param {String[]} columns
* @returns {Object}
*/
searchColumnsForTable(calcField, columns) {
let fieldInfo = null;
let foundTableField = null;
for (const col of columns) {
fieldInfo = this.tableFields.getFieldInfo(col);
if (typeof fieldInfo !== 'undefined') {
foundTableField = {...fieldInfo};
foundTableField.calculatedFormula = calcField;
return foundTableField;
}
}
return foundTableField;
}
/**
* @typedef {Object} MatchingJoinRecordIDs
* @property {Number[][]} leftJoinRecordIDs
* @property {Number[][]} rightJoinRecordIDs
*/
/**
* Apply JOIN TYPE logic on left and right tables to find the matching record ID's from both left and right tables.
* @returns {MatchingJoinRecordIDs}
*/
getMatchedRecordIds() {
/** @type {Number[][]} */
let leftJoinRecordIDs = [];
let rightJoinRecordIDs = [];
switch (this.joinType) {
case "left":
leftJoinRecordIDs = this.leftRightJoin(this.joinFields.leftSideInfo, this.joinFields.rightSideInfo, this.joinType);
break;
case "inner":
leftJoinRecordIDs = this.leftRightJoin(this.joinFields.leftSideInfo, this.joinFields.rightSideInfo, this.joinType);
break;
case "right":
leftJoinRecordIDs = this.leftRightJoin(this.joinFields.rightSideInfo, this.joinFields.leftSideInfo, this.joinType);
break;
case "full":
leftJoinRecordIDs = this.leftRightJoin(this.joinFields.leftSideInfo, this.joinFields.rightSideInfo, this.joinType);
rightJoinRecordIDs = this.leftRightJoin(this.joinFields.rightSideInfo, this.joinFields.leftSideInfo, "outer");
break;
default:
throw new Error(`Invalid join type: ${this.joinType}`);
}
return { leftJoinRecordIDs, rightJoinRecordIDs };
}
/**
* Returns array of each matching record ID from right table for every record in left table.
* If the right table entry could NOT be found, -1 is set for that record index.
* @param {JoinSideInfo} leftField - left table field
* @param {JoinSideInfo} rightField - right table field
* @param {String} type - either 'inner' or 'outer'.
* @returns {Number[][]} - first index is record ID of left table, second index is a list of the matching record ID's in right table.
*/
leftRightJoin(leftField, rightField, type) {
const leftRecordsIDs = [];
// First record is the column title.
leftRecordsIDs.push([0]);
const leftTableData = leftField.fieldInfo.tableInfo.tableData;
// Map the RIGHT JOIN key to record numbers.
const keyFieldMap = this.createKeyFieldRecordMap(rightField);
let keyMasterJoinField = null;
for (let leftTableRecordNum = 1; leftTableRecordNum < leftTableData.length; leftTableRecordNum++) {
keyMasterJoinField = this.getJoinColumnData(leftField, leftTableRecordNum);
const joinRows = !keyFieldMap.has(keyMasterJoinField) ? [] : keyFieldMap.get(keyMasterJoinField);
// For the current LEFT TABLE record, record the linking RIGHT TABLE records.
if (joinRows.length === 0) {
if (type === "inner")
continue;
leftRecordsIDs[leftTableRecordNum] = [-1];
}
else {
// Excludes all match recordgs (is outer the right word for this?)
if (type === "outer")
continue;
leftRecordsIDs[leftTableRecordNum] = joinRows;
}
}
return leftRecordsIDs;
}
/**
* Find (or calculate) the field data for the specified record number.
* @param {JoinSideInfo} fieldInfo
* @param {Number} recordNumber
* @returns {String}
*/
getJoinColumnData(fieldInfo, recordNumber) {
let keyMasterJoinField = null;
const tableColumnNumber = fieldInfo.fieldInfo.tableColumn;
if (typeof tableColumnNumber !== 'undefined') {
keyMasterJoinField = fieldInfo.fieldInfo.tableInfo.tableData[recordNumber][tableColumnNumber];
}
else {
keyMasterJoinField = this.calcSqlField.evaluateCalculatedField(fieldInfo.column, recordNumber);
}
if (keyMasterJoinField !== null) {
keyMasterJoinField = keyMasterJoinField.toString();
}
return keyMasterJoinField;
}
/**
* Find all KEYS in table mapped to an array of record ID's where key is located in table.
* @param {JoinSideInfo} rightField
* @returns {Map<String, Number[]>}
*/
createKeyFieldRecordMap(rightField) {
let keyFieldMap = null;
if (typeof rightField.fieldInfo.tableColumn !== 'undefined') {
keyFieldMap = rightField.fieldInfo.tableInfo.createKeyFieldRecordMap(rightField.fieldInfo.fieldName);
}
else {
// We have to evalulate the expression for every record and put into the key map (with record ID's)
const rightSideCalculator = new CalculatedField(rightField.fieldInfo.tableInfo, rightField.fieldInfo.tableInfo, this.tableFields);
keyFieldMap = rightField.fieldInfo.tableInfo.createCalcFieldRecordMap(rightSideCalculator, rightField.fieldInfo.calculatedFormula);
}
return keyFieldMap;
}
}