Views.js

  1. // *** DEBUG START ***
  2. // Remove comments for testing in NODE
  3. export { DERIVEDTABLE, VirtualFields, VirtualField, SelectTables, TableFields, TableField, CalculatedField, SqlServerFunctions, DerivedTable };
  4. import { Table } from './Table.js';
  5. import { Sql, BindData, TableExtract } from './Sql.js';
  6. import { SqlParse } from './SimpleParser.js';
  7. import { JoinTables } from './JoinTables.js';
  8. // *** DEBUG END ***/
  9. const DERIVEDTABLE = "::DERIVEDTABLE::";
  10. /**
  11. * @classdesc
  12. * Perform SQL SELECT operations to retrieve requested data.
  13. */
  14. class SelectTables {
  15. /**
  16. * @param {Object} ast - Abstract Syntax Tree
  17. * @param {Map<String,Table>} tableInfo - Map of table info.
  18. * @param {BindData} bindVariables - List of bind data.
  19. */
  20. constructor(ast, tableInfo, bindVariables) {
  21. /** @property {String} - primary table name. */
  22. this.primaryTable = ast.FROM.table;
  23. /** @property {Object} - AST of SELECT fields */
  24. this.astFields = ast.SELECT;
  25. /** @property {Map<String,Table>} tableInfo - Map of table info. */
  26. this.tableInfo = tableInfo;
  27. /** @property {BindData} - Bind variable data. */
  28. this.bindVariables = bindVariables;
  29. /** @property {TableFields} */
  30. this.tableFields = new TableFields();
  31. /** @property {Table} - Primary table info. */
  32. this.primaryTableInfo = tableInfo.get(this.primaryTable.toUpperCase());
  33. /** @property {JoinTables} - Join table object. */
  34. this.dataJoin = new JoinTables()
  35. .setTableFields(this.tableFields)
  36. .setTableInfo(this.tableInfo)
  37. .setBindVariables(bindVariables)
  38. .setPrimaryTableInfo(this.primaryTableInfo);
  39. if (!tableInfo.has(this.primaryTable.toUpperCase()))
  40. throw new Error(`Invalid table name: ${this.primaryTable}`);
  41. // Keep a list of all possible fields from all tables.
  42. this.tableFields.loadVirtualFields(this.primaryTable, tableInfo);
  43. }
  44. /**
  45. * Update internal FIELDS list to indicate those fields that are in the SELECT fields - that will be returned in data.
  46. * @param {Object} ast
  47. * @returns {void}
  48. */
  49. updateSelectedFields(ast) {
  50. let astFields = ast.SELECT;
  51. const tableInfo = !this.dataJoin.isDerivedTable() ? this.primaryTableInfo : this.dataJoin.derivedTable.tableInfo;
  52. // Expand any 'SELECT *' fields and add the actual field names into 'astFields'.
  53. astFields = VirtualFields.expandWildcardFields(tableInfo, astFields);
  54. // Define the data source of each field in SELECT field list.
  55. this.tableFields.updateSelectFieldList(astFields, 0, false);
  56. // These are fields REFERENCED, but not actually in the SELECT FIELDS.
  57. // So columns referenced by GROUP BY, ORDER BY and not in SELECT.
  58. // These temp columns need to be removed after processing.
  59. if (typeof ast["GROUP BY"] !== 'undefined') {
  60. this.tableFields.updateSelectFieldList(ast["GROUP BY"], this.tableFields.getNextSelectColumnNumber(), true);
  61. }
  62. if (typeof ast["ORDER BY"] !== 'undefined') {
  63. this.tableFields.updateSelectFieldList(ast["ORDER BY"], this.tableFields.getNextSelectColumnNumber(), true);
  64. }
  65. }
  66. /**
  67. * Process any JOIN condition.
  68. * @param {Object} ast - Abstract Syntax Tree
  69. * @returns {void}
  70. */
  71. join(ast) {
  72. if (typeof ast.JOIN !== 'undefined')
  73. this.dataJoin.load(ast);
  74. }
  75. /**
  76. * Retrieve filtered record ID's.
  77. * @param {Object} ast - Abstract Syntax Tree
  78. * @returns {Number[]} - Records ID's that match WHERE condition.
  79. */
  80. whereCondition(ast) {
  81. let sqlData = [];
  82. let conditions = {};
  83. if (typeof ast.WHERE !== 'undefined') {
  84. conditions = ast.WHERE;
  85. }
  86. else if (typeof ast["GROUP BY"] === 'undefined' && typeof ast.HAVING !== 'undefined') {
  87. // This will work in mySql as long as select field is in having clause.
  88. conditions = ast.HAVING;
  89. }
  90. else {
  91. // Entire table is selected.
  92. conditions = { operator: "=", left: "\"A\"", right: "\"A\"" };
  93. }
  94. if (typeof conditions.logic === 'undefined')
  95. sqlData = this.resolveCondition("OR", [conditions]);
  96. else
  97. sqlData = this.resolveCondition(conditions.logic, conditions.terms);
  98. return sqlData;
  99. }
  100. /**
  101. * Recursively resolve WHERE condition and then apply AND/OR logic to results.
  102. * @param {String} logic - logic condition (AND/OR) between terms
  103. * @param {Object} terms - terms of WHERE condition (value compared to value)
  104. * @returns {Number[]} - record ID's
  105. */
  106. resolveCondition(logic, terms) {
  107. const recordIDs = [];
  108. for (const cond of terms) {
  109. if (typeof cond.logic === 'undefined') {
  110. recordIDs.push(this.getRecordIDs(cond));
  111. }
  112. else {
  113. recordIDs.push(this.resolveCondition(cond.logic, cond.terms));
  114. }
  115. }
  116. let result = [];
  117. if (logic === "AND") {
  118. result = recordIDs.reduce((a, b) => a.filter(c => b.includes(c)));
  119. }
  120. if (logic === "OR") {
  121. // OR Logic
  122. let tempArr = [];
  123. for (const arr of recordIDs) {
  124. tempArr = tempArr.concat(arr);
  125. }
  126. result = Array.from(new Set(tempArr));
  127. }
  128. return result;
  129. }
  130. /**
  131. * Find record ID's where condition is TRUE.
  132. * @param {Object} condition - WHERE test condition
  133. * @returns {Number[]} - record ID's which are true.
  134. */
  135. getRecordIDs(condition) {
  136. /** @type {Number[]} */
  137. const recordIDs = [];
  138. const leftFieldConditions = this.resolveFieldCondition(condition.left);
  139. const rightFieldConditions = this.resolveFieldCondition(condition.right);
  140. const conditionFunction = FieldComparisons.getComparisonFunction(condition.operator);
  141. /** @type {Table} */
  142. this.masterTable = this.dataJoin.isDerivedTable() ? this.dataJoin.getJoinedTableInfo() : this.primaryTableInfo;
  143. const calcSqlField = new CalculatedField(this.masterTable, this.primaryTableInfo, this.tableFields);
  144. for (let masterRecordID = 1; masterRecordID < this.masterTable.tableData.length; masterRecordID++) {
  145. let leftValue = SelectTables.getConditionValue(leftFieldConditions, calcSqlField, masterRecordID);
  146. let rightValue = SelectTables.getConditionValue(rightFieldConditions, calcSqlField, masterRecordID);
  147. if (leftValue instanceof Date || rightValue instanceof Date) {
  148. leftValue = SelectTables.dateToMs(leftValue);
  149. rightValue = SelectTables.dateToMs(rightValue);
  150. }
  151. if (conditionFunction(leftValue, rightValue))
  152. recordIDs.push(masterRecordID);
  153. }
  154. return recordIDs;
  155. }
  156. /**
  157. * Evaulate value on left/right side of condition
  158. * @param {ResolvedFieldCondition} fieldConditions - the value to be found will come from:
  159. * * constant data
  160. * * field data
  161. * * calculated field
  162. * * sub-query
  163. * @param {CalculatedField} calcSqlField - data to resolve the calculated field.
  164. * @param {Number} masterRecordID - current record in table to grab field data from
  165. * @returns {any} - resolve value.
  166. */
  167. static getConditionValue(fieldConditions, calcSqlField, masterRecordID) {
  168. let fieldValue = fieldConditions.constantData;
  169. if (fieldConditions.columnNumber >= 0) {
  170. fieldValue = fieldConditions.fieldConditionTableInfo.tableData[masterRecordID][fieldConditions.columnNumber];
  171. }
  172. else if (fieldConditions.calculatedField !== "") {
  173. if (fieldConditions.calculatedField.toUpperCase() === "NULL") {
  174. fieldValue = "NULL";
  175. }
  176. else {
  177. fieldValue = calcSqlField.evaluateCalculatedField(fieldConditions.calculatedField, masterRecordID);
  178. }
  179. }
  180. else if (fieldConditions.subQuery !== null) {
  181. const arrayResult = fieldConditions.subQuery.select(masterRecordID, calcSqlField);
  182. if (typeof arrayResult !== 'undefined' && arrayResult !== null && arrayResult.length > 0)
  183. fieldValue = arrayResult[0][0];
  184. }
  185. return fieldValue;
  186. }
  187. /**
  188. * Retrieve the data for the record ID's specified for ALL SELECT fields.
  189. * @param {Number[]} recordIDs - record ID's which are SELECTed.
  190. * @returns {any[][]} - double array of select data. No column title is included here.
  191. */
  192. getViewData(recordIDs) {
  193. const virtualData = [];
  194. const calcSqlField = new CalculatedField(this.masterTable, this.primaryTableInfo, this.tableFields);
  195. const subQuery = new CorrelatedSubQuery(this.tableInfo, this.tableFields, this.bindVariables);
  196. for (const masterRecordID of recordIDs) {
  197. const newRow = [];
  198. for (const field of this.tableFields.getSelectFields()) {
  199. if (field.tableInfo !== null)
  200. newRow.push(field.getData(masterRecordID));
  201. else if (field.subQueryAst !== null) {
  202. const result = subQuery.select(masterRecordID, calcSqlField, field.subQueryAst);
  203. newRow.push(result[0][0]);
  204. }
  205. else if (field.calculatedFormula !== "") {
  206. const result = calcSqlField.evaluateCalculatedField(field.calculatedFormula, masterRecordID);
  207. newRow.push(result);
  208. }
  209. }
  210. virtualData.push(newRow);
  211. }
  212. return virtualData;
  213. }
  214. /**
  215. * Returns the entire string in UPPER CASE - except for anything between quotes.
  216. * @param {String} srcString - source string to convert.
  217. * @returns {String} - converted string.
  218. */
  219. static toUpperCaseExceptQuoted(srcString) {
  220. let finalString = "";
  221. let inQuotes = "";
  222. for (let i = 0; i < srcString.length; i++) {
  223. let ch = srcString.charAt(i);
  224. if (inQuotes === "") {
  225. if (ch === '"' || ch === "'")
  226. inQuotes = ch;
  227. ch = ch.toUpperCase();
  228. }
  229. else if (ch === inQuotes) {
  230. inQuotes = "";
  231. }
  232. finalString += ch;
  233. }
  234. return finalString;
  235. }
  236. /**
  237. * Parse input string for 'func' and then parse if found.
  238. * @param {String} functionString - Select field which may contain a function.
  239. * @param {String} func - Function name to parse for.
  240. * @returns {String[]} - Parsed function string.
  241. * * null if function not found,
  242. * * string array[0] - original string, e.g. **sum(quantity)**
  243. * * string array[1] - function parameter, e.g. **quantity**
  244. */
  245. static parseForFunctions(functionString, func) {
  246. const args = [];
  247. const expMatch = "%1\\s*\\(";
  248. const matchStr = new RegExp(expMatch.replace("%1", func));
  249. const startMatchPos = functionString.search(matchStr);
  250. if (startMatchPos !== -1) {
  251. const searchStr = functionString.substring(startMatchPos);
  252. let i = searchStr.indexOf("(");
  253. const startLeft = i;
  254. let leftBracket = 1;
  255. for (i = i + 1; i < searchStr.length; i++) {
  256. const ch = searchStr.charAt(i);
  257. if (ch === "(") leftBracket++;
  258. if (ch === ")") leftBracket--;
  259. if (leftBracket === 0) {
  260. args.push(searchStr.substring(0, i + 1));
  261. args.push(searchStr.substring(startLeft + 1, i));
  262. return args;
  263. }
  264. }
  265. }
  266. return null;
  267. }
  268. /**
  269. * Parse the input for a calculated field.
  270. * String split on comma, EXCEPT if comma is within brackets (i.e. within an inner function)
  271. * or within a string like ", "
  272. * @param {String} paramString - Search and parse this string for parameters.
  273. * @returns {String[]} - List of function parameters.
  274. */
  275. static parseForParams(paramString, startBracket = "(", endBracket = ")") {
  276. const args = [];
  277. let bracketCount = 0;
  278. let inQuotes = "";
  279. let start = 0;
  280. for (let i = 0; i < paramString.length; i++) {
  281. const ch = paramString.charAt(i);
  282. if (ch === "," && bracketCount === 0 && inQuotes === "") {
  283. args.push(paramString.substring(start, i));
  284. start = i + 1;
  285. }
  286. else {
  287. bracketCount += SelectTables.functionBracketCounter(ch, startBracket, endBracket);
  288. }
  289. inQuotes = SelectTables.checkIfWithinString(ch, inQuotes);
  290. }
  291. const lastStr = paramString.substring(start);
  292. if (lastStr !== "")
  293. args.push(lastStr);
  294. return args;
  295. }
  296. /**
  297. * Track net brackets encountered in string.
  298. * @param {String} ch
  299. * @param {String} startBracket
  300. * @param {String} endBracket
  301. * @returns {Number}
  302. */
  303. static functionBracketCounter(ch, startBracket, endBracket) {
  304. if (ch === startBracket)
  305. return 1;
  306. else if (ch === endBracket)
  307. return -1;
  308. return 0;
  309. }
  310. /**
  311. * Track if current ch(ar) is within quotes.
  312. * @param {String} ch
  313. * @param {String} inQuotes
  314. * @returns {String} - Returns empty string if not within a string constant.
  315. * If it is within a string, it will return either a single or double quote so we can
  316. * determine when the string ends (it will match the starting quote.)
  317. */
  318. static checkIfWithinString(ch, inQuotes) {
  319. if (inQuotes === "") {
  320. if (ch === '"' || ch === "'")
  321. return ch;
  322. }
  323. else if (ch === inQuotes) {
  324. return "";
  325. }
  326. return inQuotes;
  327. }
  328. /**
  329. * Compress the table data so there is one record per group (fields in GROUP BY).
  330. * The other fields MUST be aggregate calculated fields that works on the data in that group.
  331. * @param {Object} ast - Abstract Syntax Tree
  332. * @param {any[][]} viewTableData - Table data.
  333. * @returns {any[][]} - Aggregated table data.
  334. */
  335. groupBy(ast, viewTableData) {
  336. let groupedTableData = viewTableData;
  337. if (typeof ast['GROUP BY'] !== 'undefined') {
  338. groupedTableData = this.groupByFields(ast['GROUP BY'], viewTableData);
  339. if (typeof ast.HAVING !== 'undefined') {
  340. groupedTableData = this.having(ast.HAVING, groupedTableData);
  341. }
  342. }
  343. // If any conglomerate field functions (SUM, COUNT,...)
  344. // we summarize all records into ONE.
  345. else if (this.tableFields.getConglomerateFieldCount() > 0) {
  346. const compressedData = [];
  347. const conglomerate = new ConglomerateRecord(this.tableFields.getSelectFields());
  348. compressedData.push(conglomerate.squish(viewTableData));
  349. groupedTableData = compressedData;
  350. }
  351. return groupedTableData;
  352. }
  353. /**
  354. * Group table data by group fields.
  355. * @param {any[]} astGroupBy - AST group by fields.
  356. * @param {any[][]} selectedData - table data
  357. * @returns {any[][]} - compressed table data
  358. */
  359. groupByFields(astGroupBy, selectedData) {
  360. if (selectedData.length === 0)
  361. return selectedData;
  362. // Sort the least important first, and most important last.
  363. astGroupBy.reverse();
  364. for (const orderField of astGroupBy) {
  365. const selectColumn = this.tableFields.getSelectFieldColumn(orderField.name);
  366. if (selectColumn !== -1) {
  367. SelectTables.sortByColumnASC(selectedData, selectColumn);
  368. }
  369. }
  370. const groupedData = [];
  371. let groupRecords = [];
  372. const conglomerate = new ConglomerateRecord(this.tableFields.getSelectFields());
  373. let lastKey = this.createGroupByKey(selectedData[0], astGroupBy);
  374. for (const row of selectedData) {
  375. const newKey = this.createGroupByKey(row, astGroupBy);
  376. if (newKey !== lastKey) {
  377. groupedData.push(conglomerate.squish(groupRecords));
  378. lastKey = newKey;
  379. groupRecords = [];
  380. }
  381. groupRecords.push(row);
  382. }
  383. if (groupRecords.length > 0)
  384. groupedData.push(conglomerate.squish(groupRecords));
  385. return groupedData;
  386. }
  387. /**
  388. * Create a composite key that is comprised from all field data in group by clause.
  389. * @param {any[]} row - current row of data.
  390. * @param {any[]} astGroupBy - group by fields
  391. * @returns {String} - group key
  392. */
  393. createGroupByKey(row, astGroupBy) {
  394. let key = "";
  395. for (const orderField of astGroupBy) {
  396. const selectColumn = this.tableFields.getSelectFieldColumn(orderField.name);
  397. if (selectColumn !== -1)
  398. key += row[selectColumn].toString();
  399. }
  400. return key;
  401. }
  402. /**
  403. * Take the compressed data from GROUP BY and then filter those records using HAVING conditions.
  404. * @param {Object} astHaving - AST HAVING conditons
  405. * @param {any[][]} selectedData - compressed table data (from group by)
  406. * @returns {any[][]} - filtered data using HAVING conditions.
  407. */
  408. having(astHaving, selectedData) {
  409. // Add in the title row for now
  410. selectedData.unshift(this.tableFields.getColumnNames());
  411. // Create our virtual GROUP table with data already selected.
  412. const groupTable = new Table(this.primaryTable).loadArrayData(selectedData);
  413. /** @type {Map<String, Table>} */
  414. const tableMapping = new Map();
  415. tableMapping.set(this.primaryTable.toUpperCase(), groupTable);
  416. // Set up for our SQL.
  417. const inSQL = new Sql().setTables(tableMapping);
  418. // Fudge the HAVING to look like a SELECT.
  419. const astSelect = {};
  420. astSelect.FROM = { table: this.primaryTable, as: '' };
  421. astSelect.SELECT = [{ name: "*" }];
  422. astSelect.WHERE = astHaving;
  423. return inSQL.execute(astSelect);
  424. }
  425. /**
  426. * Take select data and sort by columns specified in ORDER BY clause.
  427. * @param {Object} ast - Abstract Syntax Tree for SELECT
  428. * @param {any[][]} selectedData - Table data to sort. On function return, this array is sorted.
  429. */
  430. orderBy(ast, selectedData) {
  431. if (typeof ast['ORDER BY'] === 'undefined')
  432. return;
  433. const astOrderby = ast['ORDER BY']
  434. // Sort the least important first, and most important last.
  435. const reverseOrderBy = astOrderby.reverse();
  436. for (const orderField of reverseOrderBy) {
  437. const selectColumn = this.tableFields.getSelectFieldColumn(orderField.name);
  438. if (selectColumn === -1) {
  439. throw new Error(`Invalid ORDER BY: ${orderField.name}`);
  440. }
  441. if (orderField.order.toUpperCase() === "DESC") {
  442. SelectTables.sortByColumnDESC(selectedData, selectColumn);
  443. }
  444. else {
  445. SelectTables.sortByColumnASC(selectedData, selectColumn);
  446. }
  447. }
  448. }
  449. /**
  450. * Removes temporary fields from return data. These temporary fields were needed to generate
  451. * the final table data, but are not included in the SELECT fields for final output.
  452. * @param {any[][]} viewTableData - table data that may contain temporary columns.
  453. * @returns {any[][]} - table data with temporary columns removed.
  454. */
  455. removeTempColumns(viewTableData) {
  456. const tempColumns = this.tableFields.getTempSelectedColumnNumbers();
  457. if (tempColumns.length === 0)
  458. return viewTableData;
  459. for (const row of viewTableData) {
  460. for (const col of tempColumns) {
  461. row.splice(col, 1);
  462. }
  463. }
  464. return viewTableData;
  465. }
  466. /**
  467. * @param {Object} ast
  468. * @param {any[][]} viewTableData
  469. * @returns {any[][]}
  470. */
  471. static limit(ast, viewTableData) {
  472. if (typeof ast.LIMIT !== 'undefined') {
  473. const maxItems = ast.LIMIT.nb;
  474. if (viewTableData.length > maxItems)
  475. viewTableData.splice(maxItems);
  476. }
  477. return viewTableData;
  478. }
  479. /**
  480. * Sort the table data from lowest to highest using the data in colIndex for sorting.
  481. * @param {any[][]} tableData - table data to sort.
  482. * @param {Number} colIndex - column index which indicates which column to use for sorting.
  483. * @returns {any[][]} - sorted table data.
  484. */
  485. static sortByColumnASC(tableData, colIndex) {
  486. tableData.sort(sortFunction);
  487. /**
  488. *
  489. * @param {any} a
  490. * @param {any} b
  491. * @returns {Number}
  492. */
  493. function sortFunction(a, b) {
  494. if (a[colIndex] === b[colIndex]) {
  495. return 0;
  496. }
  497. return (a[colIndex] < b[colIndex]) ? -1 : 1;
  498. }
  499. return tableData;
  500. }
  501. /**
  502. * Sort the table data from highest to lowest using the data in colIndex for sorting.
  503. * @param {any[][]} tableData - table data to sort.
  504. * @param {Number} colIndex - column index which indicates which column to use for sorting.
  505. * @returns {any[][]} - sorted table data.
  506. */
  507. static sortByColumnDESC(tableData, colIndex) {
  508. tableData.sort(sortFunction);
  509. /**
  510. *
  511. * @param {any} a
  512. * @param {any} b
  513. * @returns {Number}
  514. */
  515. function sortFunction(a, b) {
  516. if (a[colIndex] === b[colIndex]) {
  517. return 0;
  518. }
  519. return (a[colIndex] > b[colIndex]) ? -1 : 1;
  520. }
  521. return tableData;
  522. }
  523. /**
  524. * @typedef {Object} ResolvedFieldCondition
  525. * @property {Table} fieldConditionTableInfo
  526. * @property {Number} columnNumber - use column data from this column, unless -1.
  527. * @property {String} constantData - constant data used for column, unless null.
  528. * @property {String} calculatedField - calculation of data for column, unless empty.
  529. * @property {CorrelatedSubQuery} subQuery - use this correlated subquery object if not null.
  530. *
  531. */
  532. /**
  533. * Determine what the source of value is for the current field condition.
  534. * @param {Object} fieldCondition - left or right portion of condition
  535. * @returns {ResolvedFieldCondition}
  536. */
  537. resolveFieldCondition(fieldCondition) {
  538. /** @type {String} */
  539. let constantData = null;
  540. /** @type {Number} */
  541. let columnNumber = -1;
  542. /** @type {Table} */
  543. let fieldConditionTableInfo = null;
  544. /** @type {String} */
  545. let calculatedField = "";
  546. /** @type {CorrelatedSubQuery} */
  547. let subQuery = null;
  548. if (typeof fieldCondition.SELECT !== 'undefined') {
  549. // Maybe a SELECT within...
  550. [subQuery, constantData] = this.resolveSubQuery(fieldCondition);
  551. }
  552. else if (SelectTables.isStringConstant(fieldCondition))
  553. // String constant
  554. constantData = SelectTables.extractStringConstant(fieldCondition);
  555. else if (fieldCondition.startsWith('?')) {
  556. // Bind variable data.
  557. constantData = this.resolveBindData(fieldCondition);
  558. }
  559. else if (!isNaN(fieldCondition)) {
  560. // Literal number.
  561. constantData = fieldCondition;
  562. }
  563. else if (this.tableFields.hasField(fieldCondition)) {
  564. // Table field.
  565. columnNumber = this.tableFields.getFieldColumn(fieldCondition);
  566. fieldConditionTableInfo = this.tableFields.getTableInfo(fieldCondition);
  567. }
  568. else {
  569. // Calculated field?
  570. calculatedField = fieldCondition;
  571. }
  572. return { fieldConditionTableInfo, columnNumber, constantData, calculatedField, subQuery };
  573. }
  574. /**
  575. * Handle subquery. If correlated subquery, return object to handle, otherwise resolve and return constant data.
  576. * @param {Object} fieldCondition - left or right portion of condition
  577. * @returns {any[]}
  578. */
  579. resolveSubQuery(fieldCondition) {
  580. /** @type {CorrelatedSubQuery} */
  581. let subQuery = null;
  582. /** @type {String} */
  583. let constantData = null;
  584. if (SelectTables.isCorrelatedSubQuery(fieldCondition)) {
  585. subQuery = new CorrelatedSubQuery(this.tableInfo, this.tableFields, this.bindVariables, fieldCondition);
  586. }
  587. else {
  588. const subQueryTableInfo = SelectTables.getSubQueryTableSet(fieldCondition, this.tableInfo);
  589. const inData = new Sql()
  590. .setTables(subQueryTableInfo)
  591. .setBindValues(this.bindVariables)
  592. .execute(fieldCondition);
  593. constantData = inData.join(",");
  594. }
  595. return [subQuery, constantData];
  596. }
  597. /**
  598. * Get constant bind data
  599. * @param {String} fieldCondition - left or right portion of condition
  600. * @returns {any}
  601. */
  602. resolveBindData(fieldCondition) {
  603. // Bind variable data.
  604. const constantData = this.bindVariables.get(fieldCondition);
  605. if (typeof constantData === 'undefined') {
  606. if (fieldCondition === '?') {
  607. throw new Error("Bind variable naming is ?1, ?2... where ?1 is first bind data point in list.")
  608. }
  609. else {
  610. throw new Error(`Bind variable ${fieldCondition} was not found`);
  611. }
  612. }
  613. return constantData;
  614. }
  615. /**
  616. *
  617. * @param {Object} ast
  618. * @returns {Boolean}
  619. */
  620. static isCorrelatedSubQuery(ast) {
  621. const tableSet = new Map();
  622. TableExtract.extractAstTables(ast, tableSet);
  623. const tableSetCorrelated = new Map();
  624. if (typeof ast.WHERE !== 'undefined') {
  625. TableExtract.getTableNamesWhereCondition(ast.WHERE, tableSetCorrelated);
  626. }
  627. // @ts-ignore
  628. for (const tableName of tableSetCorrelated.keys()) {
  629. let isFound = false;
  630. // @ts-ignore
  631. for (const outerTable of tableSet.keys()) {
  632. if (outerTable === tableName || tableSet.get(outerTable) === tableName) {
  633. isFound = true;
  634. break;
  635. }
  636. }
  637. if (!isFound) {
  638. return true;
  639. }
  640. }
  641. return false;
  642. }
  643. /**
  644. * Create a set of tables that are used in sub-query.
  645. * @param {Object} ast - Sub-query AST.
  646. * @param {Map<String,Table>} tableInfo - Master set of tables used for entire select.
  647. * @returns {Map<String,Table>} - table set for sub-query.
  648. */
  649. static getSubQueryTableSet(ast, tableInfo) {
  650. const tableSubSet = new Map();
  651. const selectTables = TableExtract.getReferencedTableNamesFromAst(ast);
  652. for (const found of selectTables) {
  653. if (found[0] !== "" && !tableSubSet.has(found[0])) {
  654. tableSubSet.set(found[0], tableInfo.get(found[0]));
  655. }
  656. if (found[1] !== "" && !tableSubSet.has(found[1])) {
  657. tableSubSet.set(found[1], tableInfo.get(found[1]));
  658. }
  659. }
  660. return tableSubSet;
  661. }
  662. /**
  663. * Is the string a constant in the SELECT condition.
  664. * @param {String} value - condition to test
  665. * @returns {Boolean} - Is this string a constant.
  666. */
  667. static isStringConstant(value) {
  668. return value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'");
  669. }
  670. /**
  671. * Extract the string literal out of condition. This removes surrounding quotes.
  672. * @param {String} value - String that encloses literal string data.
  673. * @returns {String} - String with quotes removed.
  674. */
  675. static extractStringConstant(value) {
  676. if (value.startsWith('"') && value.endsWith('"'))
  677. return value.replace(/"/g, '');
  678. if (value.startsWith("'") && value.endsWith("'"))
  679. return value.replace(/'/g, '');
  680. return value;
  681. }
  682. /**
  683. * Convert input into milliseconds.
  684. * @param {any} value - date as as Date or String.
  685. * @returns {Number} - date as ms.
  686. */
  687. static dateToMs(value) {
  688. let year = 0;
  689. let month = 0;
  690. let dayNum = 0;
  691. if (value instanceof Date) {
  692. year = value.getFullYear();
  693. month = value.getMonth();
  694. dayNum = value.getDate();
  695. }
  696. else if (typeof value === "string") {
  697. const dateParts = value.split("/");
  698. if (dateParts.length === 3) {
  699. year = Number(dateParts[2]);
  700. month = Number(dateParts[0]) - 1;
  701. dayNum = Number(dateParts[1]);
  702. }
  703. }
  704. const newDate = new Date(Date.UTC(year, month, dayNum, 12, 0, 0, 0));
  705. return newDate.getTime();
  706. }
  707. /**
  708. * Return a list of column titles for this table.
  709. * @param {String} columnTableNameReplacement
  710. * @returns {String[]} - column titles
  711. */
  712. getColumnTitles(columnTableNameReplacement) {
  713. return this.tableFields.getColumnTitles(columnTableNameReplacement);
  714. }
  715. }
  716. /**
  717. * @classdesc
  718. * Finds a function to be used for doing data comparisons.
  719. * The WHERE condition needs to execute the exact same data comparison for all records, so
  720. * there is no need to find (through the switch) what to execute for every record.
  721. */
  722. class FieldComparisons {
  723. /**
  724. * Returns a function to be used for data comparisons.
  725. * @param {String} operator SQL comparison operator.
  726. * @returns {function}
  727. }}
  728. */
  729. static getComparisonFunction(operator) {
  730. switch (operator.toUpperCase()) {
  731. case "=":
  732. return (leftValue, rightValue) => { return leftValue == rightValue }; // skipcq: JS-0050
  733. case ">":
  734. return (leftValue, rightValue) => { return leftValue > rightValue };
  735. case "<":
  736. return (leftValue, rightValue) => { return leftValue < rightValue };
  737. case ">=":
  738. return (leftValue, rightValue) => { return leftValue >= rightValue };
  739. case "<=":
  740. return (leftValue, rightValue) => { return leftValue <= rightValue };
  741. case "<>":
  742. return (leftValue, rightValue) => { return leftValue != rightValue }; // skipcq: JS-0050
  743. case "!=":
  744. return (leftValue, rightValue) => { return leftValue != rightValue }; // skipcq: JS-0050
  745. case "LIKE":
  746. return (leftValue, rightValue) => { return FieldComparisons.likeCondition(leftValue, rightValue) };
  747. case "NOT LIKE":
  748. return (leftValue, rightValue) => { return FieldComparisons.notLikeCondition(leftValue, rightValue) };
  749. case "IN":
  750. return (leftValue, rightValue) => { return FieldComparisons.inCondition(leftValue, rightValue) };
  751. case "NOT IN":
  752. return (leftValue, rightValue) => { return !(FieldComparisons.inCondition(leftValue, rightValue)) };
  753. case "IS NOT":
  754. return (leftValue, rightValue) => { return !(FieldComparisons.isCondition(leftValue, rightValue)) };
  755. case "IS":
  756. return (leftValue, rightValue) => { return FieldComparisons.isCondition(leftValue, rightValue) };
  757. case "EXISTS":
  758. return (leftValue, rightValue) => { return FieldComparisons.existsCondition(rightValue) };
  759. case "NOT EXISTS":
  760. return (leftValue, rightValue) => { return !(FieldComparisons.existsCondition(rightValue)) };
  761. default:
  762. throw new Error(`Invalid Operator: ${operator}`);
  763. }
  764. }
  765. /**
  766. * Compare strings in LIKE condition
  767. * @param {String} leftValue - string for comparison
  768. * @param {String} rightValue - string with wildcard
  769. * @returns {Boolean} - Do strings match?
  770. */
  771. static likeCondition(leftValue, rightValue) {
  772. if ((leftValue === null || rightValue === null) && !(leftValue === null && rightValue === null)) {
  773. return false;
  774. }
  775. return FieldComparisons.likeConditionMatch(leftValue, rightValue) !== -1;
  776. }
  777. /**
  778. * Compare strings in NOT LIKE condition
  779. * @param {String} leftValue - string for comparison
  780. * @param {String} rightValue - string with wildcard
  781. * @returns {Boolean} - Do strings NOT match?
  782. */
  783. static notLikeCondition(leftValue, rightValue) {
  784. if ((leftValue === null || rightValue === null) && !(leftValue === null && rightValue === null)) {
  785. return false;
  786. }
  787. return FieldComparisons.likeConditionMatch(leftValue, rightValue) === -1;
  788. }
  789. /**
  790. * Compare strings in (NOT) LIKE condition
  791. * @param {String} leftValue - string for comparison
  792. * @param {String} rightValue - string with wildcard
  793. * @returns {Number} - Found position (not found === -1)
  794. */
  795. static likeConditionMatch(leftValue, rightValue) {
  796. // @ts-ignore
  797. const expanded = `^${rightValue.replace(/%/g, ".*").replace(/_/g, ".")}`;
  798. const result = leftValue.search(expanded);
  799. return result;
  800. }
  801. /**
  802. * Check if leftValue is contained in list in rightValue
  803. * @param {any} leftValue - value to find in right value
  804. * @param {String} rightValue - list of comma separated values
  805. * @returns {Boolean} - Is contained IN list.
  806. */
  807. static inCondition(leftValue, rightValue) {
  808. let items = [];
  809. if (typeof rightValue === 'string') {
  810. items = rightValue.split(",");
  811. }
  812. else {
  813. // select * from table WHERE IN (select number from table)
  814. // @ts-ignore
  815. items = [rightValue.toString()];
  816. }
  817. items = items.map(a => a.trim());
  818. let index = items.indexOf(leftValue);
  819. if (index === -1 && typeof leftValue === 'number') {
  820. index = items.indexOf(leftValue.toString());
  821. }
  822. return index !== -1;
  823. }
  824. /**
  825. * If leftValue is empty (we will consider that as NULL), condition will be true
  826. * @param {any} leftValue - test this value for NULL
  827. * @param {any} rightValue - 'NULL' considered as NULL.
  828. * @returns {Boolean} - Is leftValue NULL (like).
  829. */
  830. static isCondition(leftValue, rightValue) {
  831. return (leftValue === "" && rightValue === "NULL");
  832. }
  833. /**
  834. * Test if input is not empty
  835. * @param {*} rightValue - value to check if empty
  836. * @returns - true if NOT empty
  837. */
  838. static existsCondition(rightValue) {
  839. return rightValue !== '';
  840. }
  841. }
  842. /**
  843. * @classdesc
  844. * Evaulate calculated fields in SELECT statement. This is achieved by converting the request
  845. * into javascript and then using 'Function' to evaulate it.
  846. */
  847. class CalculatedField {
  848. /**
  849. *
  850. * @param {Table} masterTable - JOINed table (unless not joined, then primary table)
  851. * @param {Table} primaryTable - First table in SELECT
  852. * @param {TableFields} tableFields - All fields from all tables
  853. */
  854. constructor(masterTable, primaryTable, tableFields) {
  855. /** @property {Table} */
  856. this.masterTable = masterTable;
  857. /** @property {Table} */
  858. this.primaryTable = primaryTable;
  859. /** @property {Map<String,String>} - Map key=calculated field in SELECT, value=javascript equivalent code */
  860. this.sqlServerFunctionCache = new Map();
  861. /** @property {TableField[]} */
  862. this.masterFields = tableFields.allFields.filter((vField) => this.masterTable === vField.tableInfo);
  863. /** @property {Map<String, TableField>} */
  864. this.mapMasterFields = new Map();
  865. this.masterFields.forEach(fld => this.mapMasterFields.set(fld.fieldName, fld));
  866. }
  867. /**
  868. * Get data from the table for the requested field name and record number
  869. * @param {String} fldName - Name of field to get data for.
  870. * @param {Number} masterRecordID - The row number in table to extract data from.
  871. * @returns {any} - Data from table. undefined if not found.
  872. */
  873. getData(fldName, masterRecordID) {
  874. const vField = this.mapMasterFields.get(fldName);
  875. if (typeof vField === 'undefined')
  876. return vField;
  877. return vField.getData(masterRecordID);
  878. }
  879. /**
  880. * Evaluate the calculated field for the current table record and return a value.
  881. * @param {String} calculatedFormula - calculation from SELECT statement
  882. * @param {Number} masterRecordID - current record ID.
  883. * @returns {any} - Evaluated data from calculation.
  884. */
  885. evaluateCalculatedField(calculatedFormula, masterRecordID) {
  886. let result = "";
  887. // e.g. special case. count(*)
  888. if (calculatedFormula === "*") {
  889. return "*";
  890. }
  891. const functionString = this.sqlServerCalcFields(calculatedFormula, masterRecordID);
  892. try {
  893. result = new Function(functionString)();
  894. }
  895. catch (ex) {
  896. if (calculatedFormula !== '') {
  897. throw new Error(`Invalid select field: ${calculatedFormula}`);
  898. }
  899. throw new Error(`Calculated Field Error: ${ex.message}. ${functionString}`);
  900. }
  901. return result;
  902. }
  903. /**
  904. * The program is attempting to build some javascript code which we can then execute to
  905. * find the value of the calculated field. There are two parts.
  906. * 1) Build LET statements to assign to all possible field name variants,
  907. * 2) Add the 'massaged' calculated field so that it can be run in javascript.
  908. * @param {String} calculatedFormula - calculation from SELECT statement
  909. * @param {Number} masterRecordID - current table record ID.
  910. * @returns {String} - String to be executed. It is valid javascript lines of code.
  911. */
  912. sqlServerCalcFields(calculatedFormula, masterRecordID) {
  913. // Working on a calculated field.
  914. const objectsDeclared = new Map();
  915. const variablesDeclared = new Map();
  916. let myVars = "";
  917. for (/** @type {TableField} */ const vField of this.masterFields) {
  918. // Get the DATA from this field. We then build a series of LET statments
  919. // and we assign that data to the field name that might be found in a calculated field.
  920. let varData = vField.getData(masterRecordID);
  921. if (typeof varData === "string") {
  922. varData = `'${varData.replace(/'/g, "\\'")}'`;
  923. }
  924. else if (varData instanceof Date) {
  925. varData = `'${varData}'`;
  926. }
  927. myVars += this.createAssignmentStatments(vField, objectsDeclared, variablesDeclared, varData);
  928. }
  929. const functionString = this.sqlServerFunctions(calculatedFormula);
  930. return `${myVars} return ${functionString}`;
  931. }
  932. /**
  933. * Creates a javascript code block. For the current field (vField), a variable is assigned the appropriate
  934. * value from 'varData'. For example, if the column was 'ID' and the table was 'BOOKS'.
  935. * ```
  936. * "let BOOKS = {};BOOKS.ID = '9';"
  937. * ```
  938. * If the BOOKS object had already been declared, later variables would just be:
  939. * ```
  940. * "BOOKS.NAME = 'To Kill a Blue Jay';"
  941. * ```
  942. * @param {TableField} vField - current field that LET statements will be assigning to.
  943. * @param {Map<String, Boolean>} objectsDeclared - tracks if TABLE name was been encountered yet.
  944. * @param {Map<String, Boolean>} variablesDeclared - tracks if variables has already been assigned.
  945. * @param {String} varData - the data from the table that will be assigned to the variable.
  946. * @returns {String} - the javascript code block.
  947. */
  948. createAssignmentStatments(vField, objectsDeclared, variablesDeclared, varData) {
  949. let myVars = "";
  950. for (const aliasName of vField.aliasNames) {
  951. if ((this.primaryTable.tableName !== vField.tableInfo.tableName && aliasName.indexOf(".") === -1)) {
  952. continue;
  953. }
  954. if (aliasName.indexOf(".") === -1) {
  955. if (!variablesDeclared.has(aliasName)) {
  956. myVars += `let ${aliasName} = ${varData};`;
  957. variablesDeclared.set(aliasName, true);
  958. }
  959. }
  960. else {
  961. const parts = aliasName.split(".");
  962. if (!objectsDeclared.has(parts[0])) {
  963. myVars += `let ${parts[0]} = {};`;
  964. objectsDeclared.set(parts[0], true);
  965. }
  966. myVars += `${aliasName} = ${varData};`;
  967. }
  968. }
  969. return myVars;
  970. }
  971. /**
  972. *
  973. * @param {String} calculatedFormula
  974. * @returns {String}
  975. */
  976. sqlServerFunctions(calculatedFormula) {
  977. // If this calculated field formula has already been put into the required format,
  978. // pull this out of our cache rather than redo.
  979. if (this.sqlServerFunctionCache.has(calculatedFormula))
  980. return this.sqlServerFunctionCache.get(calculatedFormula);
  981. const func = new SqlServerFunctions();
  982. const functionString = func.convertToJs(calculatedFormula, this.masterFields);
  983. // No need to recalculate for each row.
  984. this.sqlServerFunctionCache.set(calculatedFormula, functionString);
  985. return functionString;
  986. }
  987. }
  988. /**
  989. * @classdesc
  990. * Correlated Sub-Query requires special lookups for every record in the primary table. */
  991. class CorrelatedSubQuery {
  992. /**
  993. *
  994. * @param {Map<String, Table>} tableInfo - Map of table info.
  995. * @param {TableFields} tableFields - Fields from all tables.
  996. * @param {BindData} bindData - List of bind data.
  997. * @param {Object} defaultSubQuery - Select AST
  998. */
  999. constructor(tableInfo, tableFields, bindData, defaultSubQuery = null) {
  1000. /** @property {Map<String, Table>} - Map of table info. */
  1001. this.tableInfo = tableInfo;
  1002. /** @property {TableFields} - Fields from all tables.*/
  1003. this.tableFields = tableFields;
  1004. /** @property {BindData} */
  1005. this.bindVariables = bindData;
  1006. /** @property {Object} - AST can be set here and skipped in select() statement. */
  1007. this.defaultSubQuery = defaultSubQuery;
  1008. }
  1009. /**
  1010. * Perform SELECT on sub-query using data from current record in outer table.
  1011. * @param {Number} masterRecordID - Current record number in outer table.
  1012. * @param {CalculatedField} calcSqlField - Calculated field object.
  1013. * @param {Object} ast - Sub-query AST.
  1014. * @returns {any[][]} - double array of selected table data.
  1015. */
  1016. select(masterRecordID, calcSqlField, ast = this.defaultSubQuery) {
  1017. const innerTableInfo = this.tableInfo.get(ast.FROM.table.toUpperCase());
  1018. if (typeof innerTableInfo === 'undefined')
  1019. throw new Error(`No table data found: ${ast.FROM.table}`);
  1020. // Add BIND variable for all matching fields in WHERE.
  1021. const tempAst = JSON.parse(JSON.stringify(ast));
  1022. const tempBindVariables = new BindData();
  1023. tempBindVariables.addList(this.bindVariables.getBindDataList());
  1024. this.replaceOuterFieldValueInCorrelatedWhere(calcSqlField, masterRecordID, tempAst, tempBindVariables);
  1025. const inData = new Sql()
  1026. .setTables(this.tableInfo)
  1027. .setBindValues(tempBindVariables)
  1028. .execute(tempAst);
  1029. return inData;
  1030. }
  1031. /**
  1032. * If we find the field name in the AST, just replace with '?' and add to bind data variable list.
  1033. * @param {CalculatedField} calcSqlField - List of fields in outer query. If any are found in subquery, the value of that field for the current record is inserted into subquery before it is executed.
  1034. * @param {Number} masterRecordID - current record number in outer query.
  1035. * @param {Object} tempAst - AST for subquery. Any field names found from outer query will be replaced with bind place holder '?'.
  1036. * @param {BindData} bindData
  1037. */
  1038. replaceOuterFieldValueInCorrelatedWhere(calcSqlField, masterRecordID, tempAst, bindData) {
  1039. const where = tempAst.WHERE;
  1040. if (typeof where === 'undefined')
  1041. return;
  1042. if (typeof where.logic === 'undefined')
  1043. this.traverseWhere(calcSqlField, [where], masterRecordID, bindData);
  1044. else
  1045. this.traverseWhere(calcSqlField, where.terms, masterRecordID, bindData);
  1046. }
  1047. /**
  1048. * Search the WHERE portion of the subquery to find all references to the table in the outer query.
  1049. * @param {CalculatedField} calcSqlField - List of fields in outer query.
  1050. * @param {Object} terms - terms of WHERE. It is modified with bind variable placeholders when outer table fields are located.
  1051. * @param {Number} masterRecordID
  1052. * @param {BindData} bindData
  1053. */
  1054. traverseWhere(calcSqlField, terms, masterRecordID, bindData) {
  1055. for (const cond of terms) {
  1056. if (typeof cond.logic === 'undefined') {
  1057. let result = calcSqlField.masterFields.find(item => item.fieldName === cond.left.toUpperCase());
  1058. if (typeof result !== 'undefined') {
  1059. cond.left = bindData.add(calcSqlField.getData(cond.left.toUpperCase(), masterRecordID));
  1060. }
  1061. result = calcSqlField.masterFields.find(item => item.fieldName === cond.right.toUpperCase());
  1062. if (typeof result !== 'undefined') {
  1063. cond.right = bindData.add(calcSqlField.getData(cond.right.toUpperCase(), masterRecordID));
  1064. }
  1065. }
  1066. else {
  1067. this.traverseWhere(calcSqlField, [cond.terms], masterRecordID, bindData);
  1068. }
  1069. }
  1070. }
  1071. }
  1072. /**
  1073. * @classdesc
  1074. * Tracks all fields in a table (including derived tables when there is a JOIN).
  1075. */
  1076. class VirtualFields {
  1077. constructor() {
  1078. /** @property {Map<String, VirtualField>} - Map to field for fast access. Field name is key. */
  1079. this.virtualFieldMap = new Map();
  1080. /** @property {VirtualField[]} - List of all fields for table. */
  1081. this.virtualFieldList = [];
  1082. }
  1083. /**
  1084. * Adds info for one field into master list of fields for table.
  1085. * @param {VirtualField} field - Information for one field in the table.
  1086. * @param {Boolean} checkForDuplicates - throws error if adding a duplicate field name.
  1087. */
  1088. add(field, checkForDuplicates = false) {
  1089. if (checkForDuplicates && this.virtualFieldMap.has(field.fieldName)) {
  1090. throw new Error(`Duplicate field name: ${field.fieldName}`);
  1091. }
  1092. this.virtualFieldMap.set(field.fieldName, field);
  1093. this.virtualFieldList.push(field);
  1094. }
  1095. /**
  1096. * Returns a list of all fields in table.
  1097. * @returns {VirtualField[]}
  1098. */
  1099. getAllVirtualFields() {
  1100. return this.virtualFieldList;
  1101. }
  1102. /**
  1103. * When the wildcard '*' is found in the SELECT, it will add all fields in table to the AST used in the SELECT.
  1104. * @param {Table} masterTableInfo - The wildcard '*' (if found) will add fields from THIS table to the AST.
  1105. * @param {any[]} astFields - existing SELECT fields list.
  1106. * @returns {any[]} - original AST field list PLUS expanded list of fields if '*' was encountered.
  1107. */
  1108. static expandWildcardFields(masterTableInfo, astFields) {
  1109. for (let i = 0; i < astFields.length; i++) {
  1110. if (astFields[i].name === "*") {
  1111. // Replace wildcard will actual field names from master table.
  1112. const masterTableFields = [];
  1113. const allExpandedFields = masterTableInfo.getAllExtendedNotationFieldNames();
  1114. for (const virtualField of allExpandedFields) {
  1115. const selField = { name: virtualField };
  1116. masterTableFields.push(selField);
  1117. }
  1118. astFields.splice(i, 1, ...masterTableFields);
  1119. break;
  1120. }
  1121. }
  1122. return astFields;
  1123. }
  1124. }
  1125. /**
  1126. * @classdesc
  1127. * Defines all possible table fields including '*' and long/short form (i.e. table.column).
  1128. */
  1129. class VirtualField { // skipcq: JS-0128
  1130. /**
  1131. * @param {String} fieldName - field name
  1132. */
  1133. constructor(fieldName) {
  1134. /** @property {String} - field name */
  1135. this._fieldName = fieldName;
  1136. }
  1137. get fieldName() {
  1138. return this._fieldName;
  1139. }
  1140. }
  1141. /**
  1142. * @classdesc
  1143. * The JOIN creates a new logical table.
  1144. */
  1145. class DerivedTable { // skipcq: JS-0128
  1146. constructor() {
  1147. /** @property {Table} */
  1148. this.tableInfo = null;
  1149. /** @property {TableField} */
  1150. this.leftField = null;
  1151. /** @property {TableField} */
  1152. this.rightField = null;
  1153. /** @property {Number[][]} */
  1154. this.leftRecords = null;
  1155. /** @property {Boolean} */
  1156. this.isOuterJoin = null;
  1157. }
  1158. /**
  1159. * Left side of join condition.
  1160. * @param {TableField} leftField
  1161. * @returns {DerivedTable}
  1162. */
  1163. setLeftField(leftField) {
  1164. this.leftField = leftField;
  1165. return this;
  1166. }
  1167. /**
  1168. * Right side of join condition
  1169. * @param {TableField} rightField
  1170. * @returns {DerivedTable}
  1171. */
  1172. setRightField(rightField) {
  1173. this.rightField = rightField;
  1174. return this;
  1175. }
  1176. /**
  1177. *
  1178. * @param {Number[][]} leftRecords - first index is record ID of left table, second index is a list of the matching record ID's in right table.
  1179. * @returns {DerivedTable}
  1180. */
  1181. setLeftRecords(leftRecords) {
  1182. this.leftRecords = leftRecords;
  1183. return this;
  1184. }
  1185. /**
  1186. * Indicate if outer or inner join.
  1187. * @param {Boolean} isOuterJoin - true for outer, false for inner
  1188. * @returns {DerivedTable}
  1189. */
  1190. setIsOuterJoin(isOuterJoin) {
  1191. this.isOuterJoin = isOuterJoin;
  1192. return this;
  1193. }
  1194. /**
  1195. * Create derived table from the two tables that are joined.
  1196. * @returns {DerivedTable}
  1197. */
  1198. createTable() {
  1199. const columnCount = this.rightField.tableInfo.getColumnCount();
  1200. const emptyRightRow = Array(columnCount).fill(null);
  1201. const joinedData = [DerivedTable.getCombinedColumnTitles(this.leftField, this.rightField)];
  1202. for (let i = 1; i < this.leftField.tableInfo.tableData.length; i++) {
  1203. if (typeof this.leftRecords[i] !== "undefined") {
  1204. if (typeof this.rightField.tableInfo.tableData[this.leftRecords[i][0]] === "undefined")
  1205. joinedData.push(this.leftField.tableInfo.tableData[i].concat(emptyRightRow));
  1206. else {
  1207. const maxJoin = this.leftRecords[i].length;
  1208. for (let j = 0; j < maxJoin; j++) {
  1209. joinedData.push(this.leftField.tableInfo.tableData[i].concat(this.rightField.tableInfo.tableData[this.leftRecords[i][j]]));
  1210. }
  1211. }
  1212. }
  1213. }
  1214. /** @type {Table} */
  1215. this.tableInfo = new Table(DERIVEDTABLE).loadArrayData(joinedData);
  1216. return this;
  1217. }
  1218. /**
  1219. * Is this a derived table - one that has been joined.
  1220. * @returns {Boolean}
  1221. */
  1222. isDerivedTable() {
  1223. return this.tableInfo !== null;
  1224. }
  1225. /**
  1226. * Get derived table info.
  1227. * @returns {Table}
  1228. */
  1229. getTableData() {
  1230. return this.tableInfo;
  1231. }
  1232. /**
  1233. * Create title row from LEFT and RIGHT table.
  1234. * @param {TableField} leftField
  1235. * @param {TableField} rightField
  1236. * @returns {String[]}
  1237. */
  1238. static getCombinedColumnTitles(leftField, rightField) {
  1239. const titleRow = leftField.tableInfo.getAllExtendedNotationFieldNames();
  1240. const rightFieldNames = rightField.tableInfo.getAllExtendedNotationFieldNames();
  1241. return titleRow.concat(rightFieldNames);
  1242. }
  1243. }
  1244. /**
  1245. * @classdesc
  1246. * Convert SQL CALCULATED fields into javascript code that can be evaulated and converted to data.
  1247. */
  1248. class SqlServerFunctions {
  1249. /**
  1250. * Convert SQL formula to javascript code.
  1251. * @param {String} calculatedFormula - contains SQL formula and parameter(s)
  1252. * @param {TableField[]} masterFields - table fields
  1253. * @returns {String} - javascript code
  1254. */
  1255. convertToJs(calculatedFormula, masterFields) {
  1256. const sqlFunctions = ["ABS", "ADDDATE", "CASE", "CEILING", "CHARINDEX", "COALESCE", "CONCAT", "CONCAT_WS", "CONVERT", "CURDATE",
  1257. "DAY", "DATEDIFF", "FLOOR", "IF", "LEFT", "LEN", "LENGTH", "LOG", "LOG10", "LOWER",
  1258. "LTRIM", "MONTH", "NOW", "POWER", "RAND", "REPLICATE", "REVERSE", "RIGHT", "ROUND", "RTRIM",
  1259. "SPACE", "STUFF", "SUBSTR", "SUBSTRING", "SQRT", "TRIM", "UPPER", "YEAR"];
  1260. /** @property {String} - regex to find components of CASE statement. */
  1261. this.matchCaseWhenThenStr = /WHEN(.*?)THEN(.*?)(?=WHEN|ELSE|$)|ELSE(.*?)(?=$)/;
  1262. /** @property {String} - Original CASE statement. */
  1263. this.originalCaseStatement = "";
  1264. /** @property {String} - Existing state of function string when CASE encountered. */
  1265. this.originalFunctionString = "";
  1266. /** @property {Boolean} - when working on each WHEN/THEN in CASE, is this the first one encountered. */
  1267. this.firstCase = true;
  1268. /** @property {String[]} */
  1269. this.referencedTableColumns = [];
  1270. let functionString = SelectTables.toUpperCaseExceptQuoted(calculatedFormula);
  1271. for (const func of sqlFunctions) {
  1272. let args = SelectTables.parseForFunctions(functionString, func);
  1273. [args, functionString] = this.caseStart(func, args, functionString);
  1274. while (args !== null && args.length > 0) {
  1275. // Split on COMMA, except within brackets.
  1276. const parms = typeof args[1] === 'undefined' ? [] : SelectTables.parseForParams(args[1]);
  1277. let replacement = "";
  1278. try {
  1279. replacement = this[func.toLocaleLowerCase()](parms, args, masterFields);
  1280. }
  1281. catch (ex) {
  1282. throw new Error(`Internal Error. Function is missing. ${func}`);
  1283. }
  1284. functionString = functionString.replace(args[0], replacement);
  1285. args = this.parseFunctionArgs(func, functionString);
  1286. }
  1287. functionString = this.caseEnd(func, functionString);
  1288. }
  1289. return functionString;
  1290. }
  1291. /**
  1292. *
  1293. * @returns {String[]}
  1294. */
  1295. getReferencedColumns() {
  1296. return this.referencedTableColumns;
  1297. }
  1298. // START SQL SUPPORTED FUNCTIONS
  1299. // Supported SQL functions entered here!!. If a new function is to be added, add a new function below
  1300. // which returns a STRING that can be executed as a Javascript statement.
  1301. abs(parms) {
  1302. this.referencedTableColumns.push(parms[0]);
  1303. return `Math.abs(${parms[0]})`;
  1304. }
  1305. /**
  1306. * @param {String[]} parms
  1307. * @returns {String}
  1308. */
  1309. adddate(parms) { // skipcq: JS-0105
  1310. return SqlServerFunctions.adddate(parms);
  1311. }
  1312. /**
  1313. * @param {String[]} _parms
  1314. * @param {String[]} args
  1315. * @returns {String}
  1316. */
  1317. case(_parms, args) {
  1318. return this.caseWhen(args);
  1319. }
  1320. /**
  1321. * @param {String[]} parms
  1322. * @returns {String}
  1323. */
  1324. ceiling(parms) {
  1325. this.referencedTableColumns.push(parms[0]);
  1326. return `Math.ceil(${parms[0]})`;
  1327. }
  1328. /**
  1329. * @param {String[]} parms
  1330. * @returns {String}
  1331. */
  1332. charindex(parms) { // skipcq: JS-0105
  1333. return SqlServerFunctions.charIndex(parms);
  1334. }
  1335. /**
  1336. * @param {String[]} parms
  1337. * @returns {String}
  1338. */
  1339. coalesce(parms) { // skipcq: JS-0105
  1340. return SqlServerFunctions.coalesce(parms);
  1341. }
  1342. /**
  1343. * @param {String[]} parms
  1344. * @param {String[]} _args
  1345. * @param {TableField[]} masterFields
  1346. * @returns {String}
  1347. */
  1348. concat(parms, _args, masterFields) { // skipcq: JS-0105
  1349. return SqlServerFunctions.concat(parms, masterFields);
  1350. }
  1351. /**
  1352. * @param {String[]} parms
  1353. * @param {String[]} _args
  1354. * @param {TableField[]} masterFields
  1355. * @returns {String}
  1356. */
  1357. concat_ws(parms, _args, masterFields) { // skipcq: JS-0105
  1358. return SqlServerFunctions.concat_ws(parms, masterFields);
  1359. }
  1360. /**
  1361. * @param {String[]} parms
  1362. * @returns {String}
  1363. */
  1364. convert(parms) { // skipcq: JS-0105
  1365. return SqlServerFunctions.convert(parms);
  1366. }
  1367. /**
  1368. * @returns {String}
  1369. */
  1370. curdate() { // skipcq: JS-0105
  1371. return "new Date().toLocaleString().split(',')[0]";
  1372. }
  1373. /**
  1374. * @param {String[]} parms
  1375. * @returns {String}
  1376. */
  1377. datediff(parms) { // skipcq: JS-0105
  1378. return SqlServerFunctions.datediff(parms);
  1379. }
  1380. /**
  1381. * @param {String[]} parms
  1382. * @returns {String}
  1383. */
  1384. day(parms) {
  1385. this.referencedTableColumns.push(parms[0]);
  1386. return `new Date(${parms[0]}).getDate()`;
  1387. }
  1388. /**
  1389. * @param {String[]} parms
  1390. * @returns {String}
  1391. */
  1392. floor(parms) {
  1393. this.referencedTableColumns.push(parms[0]);
  1394. return `Math.floor(${parms[0]})`;
  1395. }
  1396. /**
  1397. * @param {String[]} parms
  1398. * @returns {String}
  1399. */
  1400. if(parms) { // skipcq: JS-0105
  1401. const ifCond = SqlParse.sqlCondition2JsCondition(parms[0]);
  1402. return `${ifCond} ? ${parms[1]} : ${parms[2]};`;
  1403. }
  1404. /**
  1405. * @param {String[]} parms
  1406. * @returns {String}
  1407. */
  1408. left(parms) {
  1409. this.referencedTableColumns.push(parms[0]);
  1410. return `${parms[0]}.substring(0,${parms[1]})`;
  1411. }
  1412. /**
  1413. * @param {String[]} parms
  1414. * @returns {String}
  1415. */
  1416. len(parms) {
  1417. this.referencedTableColumns.push(parms[0]);
  1418. return `${parms[0]}.length`;
  1419. }
  1420. /**
  1421. * @param {String[]} parms
  1422. * @returns {String}
  1423. */
  1424. length(parms) {
  1425. this.referencedTableColumns.push(parms[0]);
  1426. return `${parms[0]}.length`;
  1427. }
  1428. /**
  1429. * @param {String[]} parms
  1430. * @returns {String}
  1431. */
  1432. log(parms) {
  1433. this.referencedTableColumns.push(parms[0]);
  1434. return `Math.log2(${parms[0]})`;
  1435. }
  1436. /**
  1437. * @param {String[]} parms
  1438. * @returns {String}
  1439. */
  1440. log10(parms) {
  1441. this.referencedTableColumns.push(parms[0]);
  1442. return `Math.log10(${parms[0]})`;
  1443. }
  1444. /**
  1445. * @param {String[]} parms
  1446. * @returns {String}
  1447. */
  1448. lower(parms) {
  1449. this.referencedTableColumns.push(parms[0]);
  1450. return `${parms[0]}.toLowerCase()`;
  1451. }
  1452. /**
  1453. * @param {String[]} parms
  1454. * @returns {String}
  1455. */
  1456. ltrim(parms) {
  1457. this.referencedTableColumns.push(parms[0]);
  1458. return `${parms[0]}.trimStart()`;
  1459. }
  1460. /**
  1461. * @param {String[]} parms
  1462. * @returns {String}
  1463. */
  1464. month(parms) {
  1465. this.referencedTableColumns.push(parms[0]);
  1466. return `new Date(${parms[0]}).getMonth() + 1`;
  1467. }
  1468. /**
  1469. * @returns {String}
  1470. */
  1471. now() { // skipcq: JS-0105
  1472. return "new Date().toLocaleString()";
  1473. }
  1474. /**
  1475. * @param {String[]} parms
  1476. * @returns {String}
  1477. */
  1478. power(parms) {
  1479. this.referencedTableColumns.push(parms[0]);
  1480. return `Math.pow(${parms[0]},${parms[1]})`;
  1481. }
  1482. /**
  1483. * @returns {String}
  1484. */
  1485. rand() { // skipcq: JS-0105
  1486. return "Math.random()";
  1487. }
  1488. /**
  1489. * @param {String[]} parms
  1490. * @returns {String}
  1491. */
  1492. replicate(parms) {
  1493. this.referencedTableColumns.push(parms[0]);
  1494. return `${parms[0]}.toString().repeat(${parms[1]})`;
  1495. }
  1496. /**
  1497. * @param {String[]} parms
  1498. * @returns {String}
  1499. */
  1500. reverse(parms) {
  1501. this.referencedTableColumns.push(parms[0]);
  1502. return `${parms[0]}.toString().split("").reverse().join("")`;
  1503. }
  1504. /**
  1505. * @param {String[]} parms
  1506. * @returns {String}
  1507. */
  1508. right(parms) {
  1509. this.referencedTableColumns.push(parms[0]);
  1510. return `${parms[0]}.toString().slice(${parms[0]}.length - ${parms[1]})`;
  1511. }
  1512. /**
  1513. * @param {String[]} parms
  1514. * @returns {String}
  1515. */
  1516. round(parms) {
  1517. this.referencedTableColumns.push(parms[0]);
  1518. return `Math.round(${parms[0]})`;
  1519. }
  1520. /**
  1521. * @param {String[]} parms
  1522. * @returns {String}
  1523. */
  1524. rtrim(parms) {
  1525. this.referencedTableColumns.push(parms[0]);
  1526. return `${parms[0]}.toString().trimEnd()`;
  1527. }
  1528. /**
  1529. * @param {String[]} parms
  1530. * @returns {String}
  1531. */
  1532. space(parms) { // skipcq: JS-0105
  1533. return `' '.repeat(${parms[0]})`;
  1534. }
  1535. /**
  1536. * @param {String[]} parms
  1537. * @returns {String}
  1538. */
  1539. stuff(parms) { // skipcq: JS-0105
  1540. return `${parms[0]}.toString().substring(0,${parms[1]}-1) + ${parms[3]} + ${parms[0]}.toString().substring(${parms[1]} + ${parms[2]} - 1)`;
  1541. }
  1542. /**
  1543. * @param {String[]} parms
  1544. * @returns {String}
  1545. */
  1546. substr(parms) {
  1547. this.referencedTableColumns.push(parms[0]);
  1548. return `${parms[0]}.toString().substring(${parms[1]} - 1, ${parms[1]} + ${parms[2]} - 1)`;
  1549. }
  1550. /**
  1551. * @param {String[]} parms
  1552. * @returns {String}
  1553. */
  1554. substring(parms) {
  1555. this.referencedTableColumns.push(parms[0]);
  1556. return `${parms[0]}.toString().substring(${parms[1]} - 1, ${parms[1]} + ${parms[2]} - 1)`;
  1557. }
  1558. /**
  1559. * @param {String[]} parms
  1560. * @returns {String}
  1561. */
  1562. sqrt(parms) {
  1563. this.referencedTableColumns.push(parms[0]);
  1564. return `Math.sqrt(${parms[0]})`;
  1565. }
  1566. /**
  1567. * @param {String[]} parms
  1568. * @returns {String}
  1569. */
  1570. trim(parms) {
  1571. this.referencedTableColumns.push(parms[0]);
  1572. return `${parms[0]}.toString().trim()`;
  1573. }
  1574. /**
  1575. * @param {String[]} parms
  1576. * @returns {String}
  1577. */
  1578. upper(parms) {
  1579. this.referencedTableColumns.push(parms[0]);
  1580. return `${parms[0]}.toString().toUpperCase()`;
  1581. }
  1582. /**
  1583. * @param {String[]} parms
  1584. * @returns {String}
  1585. */
  1586. year(parms) {
  1587. this.referencedTableColumns.push(parms[0]);
  1588. return `new Date(${parms[0]}).getFullYear()`;
  1589. }
  1590. // END SQL SUPPORTED FUNCTIONS
  1591. /**
  1592. * Search for SELECT function arguments for specified 'func' only. Special case for 'CASE'. It breaks down one WHEN condition at a time.
  1593. * @param {String} func - an SQL function name.
  1594. * @param {String} functionString - SELECT SQL string to search
  1595. * @returns {String[]}
  1596. */
  1597. parseFunctionArgs(func, functionString) {
  1598. let args = [];
  1599. if (func === "CASE")
  1600. args = this.matchCaseWhenThenStr.exec(functionString);
  1601. else
  1602. args = SelectTables.parseForFunctions(functionString, func);
  1603. return args;
  1604. }
  1605. /**
  1606. * Find the position of a substring within a field - in javascript code.
  1607. * @param {any[]} parms -
  1608. * * parms[0] - string to search for
  1609. * * parms[1] - field name
  1610. * * parms[2] - start to search from this position (starts at 1)
  1611. * @returns {String} - javascript code to find substring position.
  1612. */
  1613. static charIndex(parms) {
  1614. let replacement = "";
  1615. if (typeof parms[2] === 'undefined')
  1616. replacement = `${parms[1]}.toString().indexOf(${parms[0]}) + 1`;
  1617. else
  1618. replacement = `${parms[1]}.toString().indexOf(${parms[0]},${parms[2]} -1) + 1`;
  1619. return replacement;
  1620. }
  1621. /**
  1622. * Returns first non-empty value in a list, in javascript code.
  1623. * @param {any[]} parms - coalesce parameters - no set limit for number of inputs.
  1624. * @returns {String} - javascript to solve
  1625. */
  1626. static coalesce(parms) {
  1627. let replacement = "";
  1628. for (const parm of parms) {
  1629. replacement += `${parm} !== '' ? ${parm} : `;
  1630. }
  1631. replacement += "''";
  1632. return replacement;
  1633. }
  1634. /**
  1635. *
  1636. * @param {any[]} parms
  1637. * @param {TableField[]} masterFields
  1638. * @returns {String}
  1639. */
  1640. static concat(parms, masterFields) {
  1641. parms.unshift("''");
  1642. return SqlServerFunctions.concat_ws(parms, masterFields);
  1643. }
  1644. /**
  1645. * Concatenate all data and use separator between concatenated fields.
  1646. * @param {any[]} parms -
  1647. * * parm[0] - separator string
  1648. * * parms... - data to concatenate.
  1649. * @param {TableField[]} masterFields - fields in table.
  1650. * @returns {String} - javascript to concatenate all data.
  1651. */
  1652. static concat_ws(parms, masterFields) {
  1653. if (parms.length === 0) {
  1654. return "";
  1655. }
  1656. let replacement = "";
  1657. const separator = parms[0];
  1658. let concatFields = [];
  1659. for (let i = 1; i < parms.length; i++) {
  1660. if (parms[i].trim() === "*") {
  1661. const allTableFields = TableField.getAllExtendedAliasNames(masterFields);
  1662. concatFields = concatFields.concat(allTableFields);
  1663. }
  1664. else {
  1665. concatFields.push(parms[i]);
  1666. }
  1667. }
  1668. for (const field of concatFields) {
  1669. if (replacement !== "") {
  1670. replacement += ` + ${separator} + `;
  1671. }
  1672. replacement += `${field}`;
  1673. }
  1674. return replacement;
  1675. }
  1676. /**
  1677. * Convert data to another type.
  1678. * @param {any[]} parms -
  1679. * * parm[0] - value to convert
  1680. * * parms[1] - data type.
  1681. * @returns {String} - javascript to convert data to specified type.
  1682. */
  1683. static convert(parms) {
  1684. let replacement = "";
  1685. const dataType = parms[1].toUpperCase().trim();
  1686. switch (dataType) {
  1687. case "SIGNED":
  1688. replacement = `isNaN(parseInt(${parms[0]}, 10))?0:parseInt(${parms[0]}, 10)`;
  1689. break;
  1690. case "DECIMAL":
  1691. replacement = `isNaN(parseFloat(${parms[0]}))?0:parseFloat(${parms[0]})`;
  1692. break;
  1693. case "CHAR":
  1694. replacement = `${parms[0]}.toString()`;
  1695. break;
  1696. default:
  1697. throw new Error(`Unrecognized data type ${dataType} in CONVERT`);
  1698. }
  1699. return replacement;
  1700. }
  1701. /**
  1702. * Add number of days to a date and return JS code to return this date.
  1703. * @param {any[]} parms
  1704. * parms[0] - A date.
  1705. * parms[1] - Number of days to add to the date.
  1706. * @returns {String}
  1707. */
  1708. static adddate(parms) {
  1709. if (parms.length < 2) {
  1710. throw new Error("ADDDATE expecting at least two parameters");
  1711. }
  1712. const parm1 = `(new Date(${parms[0]})).getTime()`;
  1713. const parm2 = `(${parms[1]} * (1000 * 3600 * 24))`;
  1714. const totalMs = `(${parm1} + ${parm2})`;
  1715. return `new Date(${totalMs})`;
  1716. }
  1717. /**
  1718. * DATEDIFF(date1, date2) = date1 - date2 (as days)
  1719. * @param {any[]} parms
  1720. * @returns {String}
  1721. */
  1722. static datediff(parms) {
  1723. if (parms.length !== 2) {
  1724. throw new Error("DATEDIFF expecting two parameters");
  1725. }
  1726. let parm1 = `(new Date(${parms[0]}).getTime())/(1000 * 3600 * 24)`;
  1727. let parm2 = `(new Date(${parms[1]}).getTime())/(1000 * 3600 * 24)`;
  1728. parm1 = `Math.floor(${parm1})`;
  1729. parm2 = `Math.floor(${parm2})`;
  1730. return `${parm1} - ${parm2}`;
  1731. }
  1732. /**
  1733. * When examining the SQL Select CASE, parse for next WHEN,END condition.
  1734. * @param {String} func - current function worked on. If <> 'CASE', ignore.
  1735. * @param {any[]} args - default return value.
  1736. * @param {String} functionString
  1737. * @returns {any[]}
  1738. */
  1739. caseStart(func, args, functionString) {
  1740. let caseArguments = args;
  1741. let caseString = functionString;
  1742. if (func === "CASE") {
  1743. caseArguments = /CASE(.*?)END/i.exec(functionString);
  1744. if (caseArguments !== null && caseArguments.length > 1) {
  1745. this.firstCase = true;
  1746. this.originalFunctionString = functionString;
  1747. this.originalCaseStatement = caseArguments[0];
  1748. caseString = caseArguments[1];
  1749. caseArguments = caseArguments[1].match(this.matchCaseWhenThenStr);
  1750. }
  1751. }
  1752. return [caseArguments, caseString];
  1753. }
  1754. /**
  1755. * Convert SQL CASE to javascript executeable code to solve case options.
  1756. * @param {any[]} args - current CASE WHEN strings.
  1757. * * args[0] - entire WHEN ... THEN ...
  1758. * * args[1] - parsed string after WHEN, before THEN
  1759. * * args[2] - parse string after THEN
  1760. * @returns {String} - js code to handle this WHEN case.
  1761. */
  1762. caseWhen(args) {
  1763. let replacement = "";
  1764. if (args.length > 2) {
  1765. if (typeof args[1] === 'undefined' && typeof args[2] === 'undefined') {
  1766. replacement = `else return ${args[3]};`;
  1767. }
  1768. else {
  1769. if (this.firstCase) {
  1770. replacement = "(() => {if (";
  1771. this.firstCase = false;
  1772. }
  1773. else
  1774. replacement = "else if (";
  1775. replacement += `${SqlParse.sqlCondition2JsCondition(args[1])}) return ${args[2]} ;`;
  1776. }
  1777. }
  1778. return replacement;
  1779. }
  1780. /**
  1781. * Finish up the javascript code to handle the select CASE.
  1782. * @param {String} func - current function being processed. If <> 'CASE', ignore.
  1783. * @param {String} funcString - current SQL/javascript string in the process of being converted to js.
  1784. * @returns {String} - updated js code
  1785. */
  1786. caseEnd(func, funcString) {
  1787. let functionString = funcString;
  1788. if (func === "CASE" && this.originalFunctionString !== "") {
  1789. functionString += "})();"; // end of lambda.
  1790. functionString = this.originalFunctionString.replace(this.originalCaseStatement, functionString);
  1791. }
  1792. return functionString;
  1793. }
  1794. }
  1795. /**
  1796. * @classdesc
  1797. * Used to create a single row from multiple rows for GROUP BY expressions.
  1798. */
  1799. class ConglomerateRecord {
  1800. /**
  1801. * @param {TableField[]} virtualFields
  1802. */
  1803. constructor(virtualFields) {
  1804. /** @property {TableField[]} */
  1805. this.selectVirtualFields = virtualFields;
  1806. }
  1807. /**
  1808. * Compress group records to a single row by applying appropriate aggregate functions.
  1809. * @param {any[][]} groupRecords - a group of table data records to compress.
  1810. * @returns {any[]} - compressed record.
  1811. * * If column is not an aggregate function, value from first row of group records is selected. (should all be the same)
  1812. * * If column has aggregate function, that function is applied to all rows from group records.
  1813. */
  1814. squish(groupRecords) {
  1815. const row = [];
  1816. if (groupRecords.length === 0)
  1817. return row;
  1818. let i = 0;
  1819. for (/** @type {TableField} */ const field of this.selectVirtualFields) {
  1820. if (field.aggregateFunction === "")
  1821. row.push(groupRecords[0][i]);
  1822. else {
  1823. row.push(ConglomerateRecord.aggregateColumn(field, groupRecords, i));
  1824. }
  1825. i++;
  1826. }
  1827. return row;
  1828. }
  1829. /**
  1830. * Apply aggregate function to all rows on specified column and return result.
  1831. * @param {TableField} field - field with aggregate function
  1832. * @param {any[]} groupRecords - group of records we apply function to.
  1833. * @param {Number} columnIndex - the column index where data is read from and function is applied on.
  1834. * @returns {any} - value of aggregate function for all group rows.
  1835. */
  1836. static aggregateColumn(field, groupRecords, columnIndex) {
  1837. let groupValue = 0;
  1838. const aggregator = new AggregateTrack(field);
  1839. for (const groupRow of groupRecords) {
  1840. if (groupRow[columnIndex] === 'null')
  1841. continue;
  1842. const numericData = ConglomerateRecord.aggregateColumnToNumeric(groupRow[columnIndex]);
  1843. switch (field.aggregateFunction) {
  1844. case "SUM":
  1845. groupValue = aggregator.sum(numericData);
  1846. break;
  1847. case "COUNT":
  1848. groupValue = aggregator.count(groupRow[columnIndex]);
  1849. break;
  1850. case "MIN":
  1851. groupValue = aggregator.minCase(numericData);
  1852. break;
  1853. case "MAX":
  1854. groupValue = aggregator.maxCase(numericData);
  1855. break;
  1856. case "AVG":
  1857. aggregator.sum(numericData);
  1858. break;
  1859. case "GROUP_CONCAT":
  1860. aggregator.addGroupConcatItem(groupRow[columnIndex]);
  1861. break;
  1862. default:
  1863. throw new Error(`Invalid aggregate function: ${field.aggregateFunction}`);
  1864. }
  1865. }
  1866. if (field.aggregateFunction === "AVG") {
  1867. groupValue = aggregator.getAverage();
  1868. }
  1869. if (field.aggregateFunction === "GROUP_CONCAT") {
  1870. return aggregator.getGroupConcat();
  1871. }
  1872. return groupValue;
  1873. }
  1874. /**
  1875. *
  1876. * @param {any} columnData
  1877. * @returns {Number}
  1878. */
  1879. static aggregateColumnToNumeric(columnData) {
  1880. /** @type {any} */
  1881. let numericData = 0;
  1882. if (columnData instanceof Date) {
  1883. numericData = columnData;
  1884. }
  1885. else {
  1886. numericData = Number(columnData);
  1887. numericData = (isNaN(numericData)) ? 0 : numericData;
  1888. }
  1889. return numericData;
  1890. }
  1891. }
  1892. /**
  1893. * @classdesc Accumulator methods for the various aggregate functions.
  1894. */
  1895. class AggregateTrack {
  1896. constructor(field) {
  1897. this.groupValue = 0;
  1898. this.groupConcat = [];
  1899. this.isDistinct = field.distinctSetting === "DISTINCT";
  1900. this.distinctSet = new Set();
  1901. this.first = true;
  1902. this.avgCounter = 0;
  1903. }
  1904. /**
  1905. *
  1906. * @param {Number} numericData
  1907. * @returns {Number}
  1908. */
  1909. minCase(numericData) {
  1910. this.groupValue = this.first ? numericData : this.groupValue;
  1911. this.first = false;
  1912. this.groupValue = numericData < this.groupValue ? numericData : this.groupValue;
  1913. return this.groupValue;
  1914. }
  1915. /**
  1916. *
  1917. * @param {Number} numericData
  1918. * @returns {Number}
  1919. */
  1920. maxCase(numericData) {
  1921. this.groupValue = this.first ? numericData : this.groupValue;
  1922. this.first = false;
  1923. this.groupValue = numericData > this.groupValue ? numericData : this.groupValue;
  1924. return this.groupValue;
  1925. }
  1926. /**
  1927. *
  1928. * @param {Number} numericData
  1929. * @returns {Number}
  1930. */
  1931. sum(numericData) {
  1932. this.avgCounter++;
  1933. this.groupValue += numericData;
  1934. return this.groupValue;
  1935. }
  1936. /**
  1937. *
  1938. * @returns {Number}
  1939. */
  1940. getAverage() {
  1941. return this.groupValue / this.avgCounter;
  1942. }
  1943. /**
  1944. *
  1945. * @param {any} columnData
  1946. * @returns {Number}
  1947. */
  1948. count(columnData) {
  1949. this.groupValue++;
  1950. if (this.isDistinct) {
  1951. this.distinctSet.add(columnData);
  1952. this.groupValue = this.distinctSet.size;
  1953. }
  1954. return this.groupValue;
  1955. }
  1956. /**
  1957. *
  1958. * @param {any} columnData
  1959. * @returns {void}
  1960. */
  1961. addGroupConcatItem(columnData) {
  1962. if (this.isDistinct) {
  1963. this.distinctSet.add(columnData);
  1964. }
  1965. else {
  1966. this.groupConcat.push(columnData);
  1967. }
  1968. }
  1969. /**
  1970. * All data from column returned as single string with items separated by comma.
  1971. * @returns {String}
  1972. */
  1973. getGroupConcat() {
  1974. if (this.isDistinct) {
  1975. this.groupConcat = Array.from(this.distinctSet.keys());
  1976. }
  1977. this.groupConcat.sort((a, b) => {
  1978. if (a > b) {
  1979. return 1;
  1980. }
  1981. if (b > a) {
  1982. return -1;
  1983. }
  1984. return 0;
  1985. });
  1986. return this.groupConcat.join();
  1987. }
  1988. }
  1989. /**
  1990. * @classdesc
  1991. * Fields from all tables.
  1992. * */
  1993. class TableFields {
  1994. constructor() {
  1995. /** @property {TableField[]} */
  1996. this.allFields = [];
  1997. /** @property {Map<String, TableField>} */
  1998. this.fieldNameMap = new Map();
  1999. /** @property {Map<String, TableField>} */
  2000. this.tableColumnMap = new Map();
  2001. }
  2002. /**
  2003. * Iterate through all table fields and create a list of these VirtualFields.
  2004. * @param {String} primaryTable - primary FROM table name in select.
  2005. * @param {Map<String,Table>} tableInfo - map of all loaded tables.
  2006. */
  2007. loadVirtualFields(primaryTable, tableInfo) {
  2008. /** @type {String} */
  2009. let tableName = "";
  2010. /** @type {Table} */
  2011. let tableObject = null;
  2012. // @ts-ignore
  2013. for ([tableName, tableObject] of tableInfo.entries()) {
  2014. const validFieldNames = tableObject.getAllFieldNames();
  2015. for (const field of validFieldNames) {
  2016. const tableColumn = tableObject.getFieldColumn(field);
  2017. let virtualField = this.findTableField(tableName, tableColumn);
  2018. if (virtualField !== null) {
  2019. virtualField.addAlias(field);
  2020. }
  2021. else {
  2022. virtualField = new TableField()
  2023. .setOriginalTable(tableName)
  2024. .setOriginalTableColumn(tableColumn)
  2025. .addAlias(field)
  2026. .setIsPrimaryTable(primaryTable.toUpperCase() === tableName.toUpperCase())
  2027. .setTableInfo(tableObject);
  2028. this.allFields.push(virtualField);
  2029. }
  2030. this.indexTableField(virtualField, primaryTable.toUpperCase() === tableName.toUpperCase());
  2031. }
  2032. }
  2033. this.allFields.sort(TableFields.sortPrimaryFields);
  2034. }
  2035. /**
  2036. * Sort function for table fields list.
  2037. * @param {TableField} fldA
  2038. * @param {TableField} fldB
  2039. */
  2040. static sortPrimaryFields(fldA, fldB) {
  2041. let keyA = fldA.isPrimaryTable ? 0 : 1000;
  2042. let keyB = fldB.isPrimaryTable ? 0 : 1000;
  2043. keyA += fldA.originalTableColumn;
  2044. keyB += fldB.originalTableColumn;
  2045. if (keyA < keyB)
  2046. return -1;
  2047. else if (keyA > keyB)
  2048. return 1;
  2049. return 0;
  2050. }
  2051. /**
  2052. * Set up mapping to quickly find field info - by all (alias) names, by table+column.
  2053. * @param {TableField} field - field info.
  2054. * @param {Boolean} isPrimaryTable - is this a field from the SELECT FROM TABLE.
  2055. */
  2056. indexTableField(field, isPrimaryTable = false) {
  2057. for (const aliasField of field.aliasNames) {
  2058. const fieldInfo = this.fieldNameMap.get(aliasField);
  2059. if (typeof fieldInfo === 'undefined' || isPrimaryTable) {
  2060. this.fieldNameMap.set(aliasField, field);
  2061. }
  2062. }
  2063. // This is something referenced in GROUP BY but is NOT in the SELECTED fields list.
  2064. if (field.tempField && !this.fieldNameMap.has(field.columnName.toUpperCase())) {
  2065. this.fieldNameMap.set(field.columnName.toUpperCase(), field);
  2066. }
  2067. if (field.originalTableColumn !== -1) {
  2068. const key = `${field.originalTable}:${field.originalTableColumn}`;
  2069. if (!this.tableColumnMap.has(key))
  2070. this.tableColumnMap.set(key, field);
  2071. }
  2072. }
  2073. /**
  2074. * Quickly find field info for TABLE + COLUMN NUMBER (key of map)
  2075. * @param {String} tableName - Table name to search for.
  2076. * @param {Number} tableColumn - Column number to search for.
  2077. * @returns {TableField} -located table info (null if not found).
  2078. */
  2079. findTableField(tableName, tableColumn) {
  2080. const key = `${tableName}:${tableColumn}`;
  2081. return !this.tableColumnMap.has(key) ? null : this.tableColumnMap.get(key);
  2082. }
  2083. /**
  2084. * Is this field in our map.
  2085. * @param {String} field - field name
  2086. * @returns {Boolean} - found in map if true.
  2087. */
  2088. hasField(field) {
  2089. return this.fieldNameMap.has(field.toUpperCase());
  2090. }
  2091. /**
  2092. * Get field info.
  2093. * @param {String} field - table column name to find
  2094. * @returns {TableField} - table info (undefined if not found)
  2095. */
  2096. getFieldInfo(field) {
  2097. return this.fieldNameMap.get(field.toUpperCase());
  2098. }
  2099. /**
  2100. * Get table associated with field name.
  2101. * @param {String} field - field name to search for
  2102. * @returns {Table} - associated table info (undefined if not found)
  2103. */
  2104. getTableInfo(field) {
  2105. const fldInfo = this.getFieldInfo(field);
  2106. return typeof fldInfo !== 'undefined' ? fldInfo.tableInfo : fldInfo;
  2107. }
  2108. /**
  2109. * Get column number for field.
  2110. * @param {String} field - field name
  2111. * @returns {Number} - column number in table for field (-1 if not found)
  2112. */
  2113. getFieldColumn(field) {
  2114. const fld = this.getFieldInfo(field);
  2115. return fld !== null ? fld.tableColumn : -1;
  2116. }
  2117. /**
  2118. * Get field column number.
  2119. * @param {String} field - field name
  2120. * @returns {Number} - column number.
  2121. */
  2122. getSelectFieldColumn(field) {
  2123. const fld = this.getFieldInfo(field);
  2124. if (typeof fld !== 'undefined' && fld.selectColumn !== -1) {
  2125. return fld.selectColumn;
  2126. }
  2127. return -1;
  2128. }
  2129. /**
  2130. * @typedef {Object} SelectFieldParameters
  2131. * @property {Object} selField
  2132. * @property {Object} parsedField
  2133. * @property {String} columnTitle
  2134. * @property {Number} nextColumnPosition
  2135. * @property {Boolean} isTempField
  2136. */
  2137. /**
  2138. * Updates internal SELECTED (returned in data) field list.
  2139. * @param {Object} astFields - AST from SELECT
  2140. * @param {Number} nextColumnPosition
  2141. * @param {Boolean} isTempField
  2142. */
  2143. updateSelectFieldList(astFields, nextColumnPosition, isTempField) {
  2144. for (const selField of astFields) {
  2145. const parsedField = this.parseAstSelectField(selField);
  2146. const columnTitle = (typeof selField.as !== 'undefined' && selField.as !== "" ? selField.as : selField.name);
  2147. /** @type {SelectFieldParameters} */
  2148. const selectedFieldParms = {
  2149. selField, parsedField, columnTitle, nextColumnPosition, isTempField
  2150. };
  2151. if (parsedField.calculatedField === null && this.hasField(parsedField.columnName)) {
  2152. this.updateColumnAsSelected(selectedFieldParms);
  2153. nextColumnPosition = selectedFieldParms.nextColumnPosition;
  2154. }
  2155. else if (parsedField.calculatedField !== null) {
  2156. this.updateCalculatedAsSelected(selectedFieldParms);
  2157. nextColumnPosition++;
  2158. }
  2159. else {
  2160. this.updateConstantAsSelected(selectedFieldParms);
  2161. nextColumnPosition++;
  2162. }
  2163. }
  2164. }
  2165. /**
  2166. *
  2167. * @param {SelectFieldParameters} selectedFieldParms
  2168. * @returns {void}
  2169. */
  2170. updateColumnAsSelected(selectedFieldParms) {
  2171. let fieldInfo = this.getFieldInfo(selectedFieldParms.parsedField.columnName);
  2172. // If GROUP BY field is in our SELECT field list - we can ignore.
  2173. if (selectedFieldParms.isTempField && fieldInfo.selectColumn !== -1)
  2174. return;
  2175. if (selectedFieldParms.parsedField.aggregateFunctionName !== "" || fieldInfo.selectColumn !== -1) {
  2176. // A new SELECT field, not from existing.
  2177. const newFieldInfo = new TableField();
  2178. Object.assign(newFieldInfo, fieldInfo);
  2179. fieldInfo = newFieldInfo;
  2180. this.allFields.push(fieldInfo);
  2181. }
  2182. fieldInfo
  2183. .setAggregateFunction(selectedFieldParms.parsedField.aggregateFunctionName)
  2184. .setColumnTitle(selectedFieldParms.columnTitle)
  2185. .setColumnName(selectedFieldParms.selField.name)
  2186. .setDistinctSetting(selectedFieldParms.parsedField.fieldDistinct)
  2187. .setSelectColumn(selectedFieldParms.nextColumnPosition)
  2188. .setIsTempField(selectedFieldParms.isTempField);
  2189. selectedFieldParms.nextColumnPosition++;
  2190. this.indexTableField(fieldInfo);
  2191. }
  2192. /**
  2193. *
  2194. * @param {SelectFieldParameters} selectedFieldParms
  2195. */
  2196. updateCalculatedAsSelected(selectedFieldParms) {
  2197. const fieldInfo = new TableField();
  2198. this.allFields.push(fieldInfo);
  2199. fieldInfo
  2200. .setColumnTitle(selectedFieldParms.columnTitle)
  2201. .setColumnName(selectedFieldParms.selField.name)
  2202. .setSelectColumn(selectedFieldParms.nextColumnPosition)
  2203. .setCalculatedFormula(selectedFieldParms.selField.name)
  2204. .setSubQueryAst(selectedFieldParms.selField.subQuery)
  2205. .setIsTempField(selectedFieldParms.isTempField);
  2206. this.indexTableField(fieldInfo);
  2207. }
  2208. /**
  2209. *
  2210. * @param {SelectFieldParameters} selectedFieldParms
  2211. */
  2212. updateConstantAsSelected(selectedFieldParms) {
  2213. const fieldInfo = new TableField();
  2214. this.allFields.push(fieldInfo);
  2215. fieldInfo
  2216. .setCalculatedFormula(selectedFieldParms.parsedField.columnName)
  2217. .setAggregateFunction(selectedFieldParms.parsedField.aggregateFunctionName)
  2218. .setSelectColumn(selectedFieldParms.nextColumnPosition)
  2219. .setColumnName(selectedFieldParms.selField.name)
  2220. .setColumnTitle(selectedFieldParms.columnTitle)
  2221. .setIsTempField(selectedFieldParms.isTempField);
  2222. this.indexTableField(fieldInfo);
  2223. }
  2224. /**
  2225. * Find next available column number in selected field list.
  2226. * @returns {Number} - column number
  2227. */
  2228. getNextSelectColumnNumber() {
  2229. let next = -1;
  2230. for (const fld of this.getSelectFields()) {
  2231. next = fld.selectColumn > next ? fld.selectColumn : next;
  2232. }
  2233. return next === -1 ? next : ++next;
  2234. }
  2235. /**
  2236. * Return a list of temporary column numbers in select field list.
  2237. * @returns {Number[]} - sorted list of temp column numbers.
  2238. */
  2239. getTempSelectedColumnNumbers() {
  2240. /** @type {Number[]} */
  2241. const tempCols = [];
  2242. for (const fld of this.getSelectFields()) {
  2243. if (fld.tempField) {
  2244. tempCols.push(fld.selectColumn);
  2245. }
  2246. }
  2247. tempCols.sort((a, b) => (b - a));
  2248. return tempCols;
  2249. }
  2250. /**
  2251. * Get a sorted list (by column number) of selected fields.
  2252. * @returns {TableField[]} - selected fields
  2253. */
  2254. getSelectFields() {
  2255. const selectedFields = this.allFields.filter((a) => a.selectColumn !== -1);
  2256. selectedFields.sort((a, b) => a.selectColumn - b.selectColumn);
  2257. return selectedFields;
  2258. }
  2259. /**
  2260. * Get SELECTED Field names sorted list of column number.
  2261. * @returns {String[]} - Table field names
  2262. */
  2263. getColumnNames() {
  2264. const columnNames = [];
  2265. this.getSelectFields().forEach(fld => columnNames.push(fld.columnName));
  2266. return columnNames;
  2267. }
  2268. /**
  2269. * Get column titles. If alias was set, that column would be the alias, otherwise it is column name.
  2270. * @param {String} columnTableNameReplacement
  2271. * @returns {String[]} - column titles
  2272. */
  2273. getColumnTitles(columnTableNameReplacement) {
  2274. const columnTitles = [];
  2275. for (const fld of this.getSelectFields()) {
  2276. if (!fld.tempField) {
  2277. let columnOutput = fld.columnTitle;
  2278. // When subquery table data becomes data for the derived table name, references to
  2279. // original table names in column output needs to be changed to new derived table name.
  2280. if (columnTableNameReplacement !== null) {
  2281. const matchingTableIndex = columnOutput.toUpperCase().indexOf(`${fld.originalTable}.`);
  2282. columnOutput = matchingTableIndex === 0 ? columnTableNameReplacement + columnOutput.slice(matchingTableIndex + fld.originalTable.length) : columnOutput;
  2283. }
  2284. columnTitles.push(columnOutput);
  2285. }
  2286. }
  2287. return columnTitles;
  2288. }
  2289. /**
  2290. * Derived tables will cause an update to any TableField. It updates with a new column number and new table (derived) info.
  2291. * @param {DerivedTable} derivedTable - derived table info.
  2292. */
  2293. updateDerivedTableVirtualFields(derivedTable) {
  2294. const derivedTableFields = derivedTable.tableInfo.getAllVirtualFields();
  2295. let fieldNo = 0;
  2296. for (const field of derivedTableFields) {
  2297. if (this.hasField(field.fieldName)) {
  2298. const originalField = this.getFieldInfo(field.fieldName);
  2299. originalField.derivedTableColumn = fieldNo;
  2300. originalField.tableInfo = derivedTable.tableInfo;
  2301. }
  2302. fieldNo++;
  2303. }
  2304. }
  2305. /**
  2306. * @typedef {Object} ParsedSelectField
  2307. * @property {String} columnName
  2308. * @property {String} aggregateFunctionName
  2309. * @property {Object} calculatedField
  2310. * @property {String} fieldDistinct
  2311. */
  2312. /**
  2313. * Parse SELECT field in AST (may include functions or calculations)
  2314. * @param {Object} selField
  2315. * @returns {ParsedSelectField}
  2316. */
  2317. parseAstSelectField(selField) {
  2318. let columnName = selField.name;
  2319. let aggregateFunctionName = "";
  2320. let fieldDistinct = "";
  2321. const calculatedField = (typeof selField.terms === 'undefined') ? null : selField.terms;
  2322. if (calculatedField === null && !this.hasField(columnName)) {
  2323. const functionNameRegex = /^\w+\s*(?=\()/;
  2324. let matches = columnName.match(functionNameRegex)
  2325. if (matches !== null && matches.length > 0)
  2326. aggregateFunctionName = matches[0].trim();
  2327. matches = SelectTables.parseForFunctions(columnName, aggregateFunctionName);
  2328. if (matches !== null && matches.length > 1) {
  2329. columnName = matches[1];
  2330. // e.g. select count(distinct field) OR select count(all field)
  2331. [columnName, fieldDistinct] = TableFields.getSelectCountModifiers(columnName);
  2332. }
  2333. }
  2334. return { columnName, aggregateFunctionName, calculatedField, fieldDistinct };
  2335. }
  2336. /**
  2337. * Parse for any SELECT COUNT modifiers like 'DISTINCT' or 'ALL'.
  2338. * @param {String} originalColumnName - column (e.g. 'distinct customer_id')
  2339. * @returns {String[]} - [0] - parsed column name, [1] - count modifier
  2340. */
  2341. static getSelectCountModifiers(originalColumnName) {
  2342. let fieldDistinct = "";
  2343. let columnName = originalColumnName;
  2344. // e.g. count(distinct field)
  2345. const distinctParts = columnName.split(" ");
  2346. if (distinctParts.length > 1) {
  2347. const distinctModifiers = ["DISTINCT", "ALL"];
  2348. if (distinctModifiers.includes(distinctParts[0].toUpperCase())) {
  2349. fieldDistinct = distinctParts[0].toUpperCase();
  2350. columnName = distinctParts[1];
  2351. }
  2352. }
  2353. // Edge case for group_concat(distinct(field))
  2354. if (fieldDistinct === '') {
  2355. const matches = SelectTables.parseForFunctions(columnName.toUpperCase(), "DISTINCT");
  2356. if (matches !== null && matches.length > 1) {
  2357. columnName = matches[1];
  2358. fieldDistinct = "DISTINCT";
  2359. }
  2360. }
  2361. return [columnName, fieldDistinct];
  2362. }
  2363. /**
  2364. * Counts the number of conglomerate field functions in SELECT field list.
  2365. * @returns {Number} - Number of conglomerate functions.
  2366. */
  2367. getConglomerateFieldCount() {
  2368. return this.getSelectFields().filter(field => field.aggregateFunction !== "").length;
  2369. }
  2370. }
  2371. /**
  2372. * @classdesc
  2373. * Table column information.
  2374. */
  2375. class TableField {
  2376. constructor() {
  2377. /** @property {String} */
  2378. this.originalTable = "";
  2379. /** @property {Number} */
  2380. this.originalTableColumn = -1;
  2381. /** @property {String[]} */
  2382. this.aliasNames = [];
  2383. /** @property {String} */
  2384. this.fieldName = "";
  2385. /** @property {Number} */
  2386. this.derivedTableColumn = -1;
  2387. /** @property {Number} */
  2388. this.selectColumn = -1;
  2389. /** @property {Boolean} */
  2390. this.tempField = false;
  2391. /** @property {String} */
  2392. this.calculatedFormula = "";
  2393. /** @property {String} */
  2394. this.aggregateFunction = "";
  2395. /** @property {String} */
  2396. this.columnTitle = "";
  2397. /** @property {String} */
  2398. this.columnName = "";
  2399. /** @property {String} */
  2400. this.distinctSetting = "";
  2401. /** @property {Object} */
  2402. this.subQueryAst = null;
  2403. /** @property {Boolean} */
  2404. this._isPrimaryTable = false;
  2405. /** @property {Table} */
  2406. this.tableInfo = null;
  2407. }
  2408. /**
  2409. * Get field column number.
  2410. * @returns {Number} - column number
  2411. */
  2412. get tableColumn() {
  2413. return this.derivedTableColumn === -1 ? this.originalTableColumn : this.derivedTableColumn;
  2414. }
  2415. /**
  2416. * Original table name before any derived table updates.
  2417. * @param {String} table - original table name
  2418. * @returns {TableField}
  2419. */
  2420. setOriginalTable(table) {
  2421. this.originalTable = table.trim().toUpperCase();
  2422. return this;
  2423. }
  2424. /**
  2425. * Column name found in column title row.
  2426. * @param {Number} column
  2427. * @returns {TableField}
  2428. */
  2429. setOriginalTableColumn(column) {
  2430. this.originalTableColumn = column;
  2431. return this;
  2432. }
  2433. /**
  2434. * Alias name assigned to field in select statement.
  2435. * @param {String} columnAlias - alias name
  2436. * @returns {TableField}
  2437. */
  2438. addAlias(columnAlias) {
  2439. const alias = columnAlias.trim().toUpperCase();
  2440. if (this.fieldName === "" || alias.indexOf(".") !== -1) {
  2441. this.fieldName = alias;
  2442. }
  2443. if (this.aliasNames.indexOf(alias) === -1) {
  2444. this.aliasNames.push(alias);
  2445. }
  2446. return this;
  2447. }
  2448. /**
  2449. * Set column number in table data for field.
  2450. * @param {Number} column - column number.
  2451. * @returns {TableField}
  2452. */
  2453. setSelectColumn(column) {
  2454. this.selectColumn = column;
  2455. return this;
  2456. }
  2457. /**
  2458. * Fields referenced BUT not in final output.
  2459. * @param {Boolean} value
  2460. * @returns {TableField}
  2461. */
  2462. setIsTempField(value) {
  2463. this.tempField = value;
  2464. return this;
  2465. }
  2466. /**
  2467. * Aggregate function number used (e.g. 'SUM')
  2468. * @param {String} value - aggregate function name or ''
  2469. * @returns {TableField}
  2470. */
  2471. setAggregateFunction(value) {
  2472. this.aggregateFunction = value.toUpperCase();
  2473. return this;
  2474. }
  2475. /**
  2476. * Calculated formula for field (e.g. 'CASE WHEN QUANTITY >= 100 THEN 1 ELSE 0 END')
  2477. * @param {String} value
  2478. * @returns {TableField}
  2479. */
  2480. setCalculatedFormula(value) {
  2481. this.calculatedFormula = value;
  2482. return this;
  2483. }
  2484. /**
  2485. * The AST from just the subquery in the SELECT.
  2486. * @param {Object} ast - subquery ast.
  2487. * @returns {TableField}
  2488. */
  2489. setSubQueryAst(ast) {
  2490. this.subQueryAst = ast;
  2491. return this;
  2492. }
  2493. /**
  2494. * Set column TITLE. If an alias is available, that is used - otherwise it is column name.
  2495. * @param {String} columnTitle - column title used in output
  2496. * @returns {TableField}
  2497. */
  2498. setColumnTitle(columnTitle) {
  2499. this.columnTitle = columnTitle;
  2500. return this;
  2501. }
  2502. /**
  2503. * Set the columnname.
  2504. * @param {String} columnName
  2505. * @returns {TableField}
  2506. */
  2507. setColumnName(columnName) {
  2508. this.columnName = columnName;
  2509. return this;
  2510. }
  2511. /**
  2512. * Set any count modified like 'DISTINCT' or 'ALL'.
  2513. * @param {String} distinctSetting
  2514. * @returns {TableField}
  2515. */
  2516. setDistinctSetting(distinctSetting) {
  2517. this.distinctSetting = distinctSetting;
  2518. return this
  2519. }
  2520. /**
  2521. * Set if this field belongs to primary table (i.e. select * from table), rather than a joined tabled.
  2522. * @param {Boolean} isPrimary - true if from primary table.
  2523. * @returns {TableField}
  2524. */
  2525. setIsPrimaryTable(isPrimary) {
  2526. this._isPrimaryTable = isPrimary;
  2527. return this;
  2528. }
  2529. /**
  2530. * Is this field in the primary table.
  2531. * @returns {Boolean}
  2532. */
  2533. get isPrimaryTable() {
  2534. return this._isPrimaryTable;
  2535. }
  2536. /**
  2537. * Link this field to the table info.
  2538. * @param {Table} tableInfo
  2539. * @returns {TableField}
  2540. */
  2541. setTableInfo(tableInfo) {
  2542. this.tableInfo = tableInfo;
  2543. return this;
  2544. }
  2545. /**
  2546. * Retrieve field data for tableRow
  2547. * @param {Number} tableRow - row to read data from
  2548. * @returns {any} - data
  2549. */
  2550. getData(tableRow) {
  2551. const columnNumber = this.derivedTableColumn === -1 ? this.originalTableColumn : this.derivedTableColumn;
  2552. return this.tableInfo.tableData[tableRow][columnNumber];
  2553. }
  2554. /**
  2555. * Search through list of fields and return a list of those that include the table name (e.g. TABLE.COLUMN vs COLUMN)
  2556. * @param {TableField[]} masterFields
  2557. * @returns {String[]}
  2558. */
  2559. static getAllExtendedAliasNames(masterFields) {
  2560. const concatFields = [];
  2561. for (const vField of masterFields) {
  2562. for (const aliasName of vField.aliasNames) {
  2563. if (aliasName.indexOf(".") !== -1) {
  2564. concatFields.push(aliasName);
  2565. }
  2566. }
  2567. }
  2568. return concatFields;
  2569. }
  2570. }