Sql.js

  1. // @author Chris Demmings - https://demmings.github.io/
  2. // *** DEBUG START ***
  3. // Remove comments for testing in NODE
  4. export { Sql, gsSQL, GasSql, BindData, TableExtract };
  5. import { Table } from './Table.js';
  6. import { TableData } from './TableData.js';
  7. import { SqlParse } from './SimpleParser.js';
  8. import { SelectTables } from './Views.js';
  9. class Logger {
  10. static log(msg) {
  11. console.log(msg);
  12. }
  13. }
  14. // *** DEBUG END ***/
  15. /**
  16. * Query any sheet range using standard SQL SELECT syntax.
  17. * @example
  18. * gsSQL("select * from expenses where type = ?1", "expenses", A1:B, true, "travel")
  19. *
  20. * @param {String} statement - SQL string
  21. * @param {...any} parms - "table name", SheetRange, [..."table name", SheetRange], OutputTitles (true/false), [...Bind Variable]
  22. * @returns {any[][]} - Double array of selected data. First index ROW, Second index COLUMN.
  23. * @customfunction
  24. */
  25. function gsSQL(statement, ...parms) { // skipcq: JS-0128
  26. return GasSql.execute(statement, parms);
  27. }
  28. /**
  29. * @classdesc
  30. * Top level class used by Google Sheets custom function to process SELECT and return table data.
  31. */
  32. class GasSql {
  33. /**
  34. * Run SELECT command statement and returns data in a table format (double array).
  35. * The appropriate functions are selected to be run based on the format of the command line parameters.
  36. * @param {String} statement
  37. * @param {any[]} parms
  38. * @returns {any[][]}
  39. */
  40. static execute(statement, parms) {
  41. if (parms.length === 0 || (parms.length > 0 && (Array.isArray(parms[0]) || parms[0] === ''))) {
  42. return GasSql.executeSqlv1(statement, parms);
  43. }
  44. else if (parms.length > 0 && typeof parms[0] === 'string') {
  45. return GasSql.executeSqlv2(statement, parms);
  46. }
  47. else {
  48. throw new Error("Invalid gsSQL() parameter list.");
  49. }
  50. }
  51. /**
  52. * Processes SQL SELECT using original command line syntax. This syntax does not update automatically if the
  53. * data changes, so is not recommended anymore.
  54. * @param {String} statement
  55. * @param {any[]} parms
  56. * @returns {any[][]}
  57. */
  58. static executeSqlv1(statement, parms) {
  59. const sqlCmd = new Sql();
  60. let columnTitle = true;
  61. const bindings = [];
  62. // If first item of parms is an array, the parms are assumed to be:
  63. // @param {any[][]} tableArr - {{"tableName", "sheetRange", cacheSeconds, hasColumnTitle}; {"name","range",cache,true};...}"
  64. // @param {Boolean} columnTitle - TRUE will add column title to output (default=TRUE)
  65. // @param {...any} bindings - Bind variables to match '?' in SQL statement.
  66. const tableArr = parms.length > 0 ? parms[0] : [];
  67. const tableList = GasSql.parseTableSettings(tableArr, statement);
  68. Logger.log(`gsSQL: tableList=${tableList}. Statement=${statement}. List Len=${tableList.length}`);
  69. for (const tableDef of tableList) {
  70. sqlCmd.addTableData(tableDef[0], tableDef[1], tableDef[2], tableDef[3]);
  71. }
  72. columnTitle = parms.length > 1 ? parms[1] : true;
  73. for (let i = 2; i < parms.length; i++) {
  74. bindings.push(parms[i]);
  75. }
  76. sqlCmd.enableColumnTitle(columnTitle);
  77. for (const bind of bindings) {
  78. sqlCmd.addBindParameter(bind);
  79. }
  80. return sqlCmd.execute(statement);
  81. }
  82. /**
  83. * Process SQL SELECT using new command line syntax. Using this syntax ensures that the select data is refreshed
  84. * if any of the selected table data changes - and is therefore the recommended usage.
  85. * @param {String} statement
  86. * @param {any[]} parms
  87. * @returns {any[][]}
  88. */
  89. static executeSqlv2(statement, parms) {
  90. const sqlCmd = new Sql();
  91. let columnTitle = true;
  92. const bindings = [];
  93. // We expect: "tableName", tableData[], ...["tableName", tableData[]], includeColumnOutput, ...bindings
  94. let i = 0;
  95. while (i + 1 < parms.length && typeof parms[i] !== 'boolean') {
  96. Logger.log(`Add Table: ${parms[i]}. Items=${parms[i + 1].length}`);
  97. sqlCmd.addTableData(parms[i], parms[i + 1], 0, true);
  98. i += 2;
  99. }
  100. if (i < parms.length && typeof parms[i] === 'boolean') {
  101. columnTitle = parms[i];
  102. i++
  103. }
  104. Logger.log(`Column Titles: ${columnTitle}`);
  105. while (i < parms.length) {
  106. Logger.log(`Add BIND Variable: ${parms[i]}`);
  107. bindings.push(parms[i]);
  108. i++
  109. }
  110. sqlCmd.enableColumnTitle(columnTitle);
  111. for (const bind of bindings) {
  112. sqlCmd.addBindParameter(bind);
  113. }
  114. return sqlCmd.execute(statement);
  115. }
  116. /**
  117. *
  118. * @param {any[][]} tableArr - Referenced Table list. This is normally the second parameter in gsSQL() custom function.
  119. * It is a double array with first index for TABLE, and the second index are settings in the table.
  120. * The setting index for each table is as follows:
  121. * * 0 - Table Name.
  122. * * 1 - Sheet Range.
  123. * * 2 - Cache seconds.
  124. * * 3 - First row contains title (for field name)
  125. * @param {String} statement - SQL SELECT statement. If no data specified in 'tableArr', the SELECT is
  126. * parsed and each referenced table is assumed to be a TAB name on the sheet.
  127. * @param {Boolean} randomOrder - Returned table list is randomized.
  128. * @returns {any[][]} - Data from 'tableArr' PLUS any extracted tables referenced from SELECT statement.
  129. * It is a double array with first index for TABLE, and the second index are settings in the table.
  130. * The setting index for each table is as follows:
  131. * * 0 - Table Name.
  132. * * 1 - Sheet Range.
  133. * * 2 - Cache seconds.
  134. * * 3 - First row contains title (for field name)
  135. */
  136. static parseTableSettings(tableArr, statement = "", randomOrder = true) {
  137. let tableList = [];
  138. let referencedTableSettings = tableArr;
  139. // Get table names from the SELECT statement when no table range info is given.
  140. if (tableArr.length === 0 && statement !== "") {
  141. referencedTableSettings = TableExtract.getReferencedTableNames(statement);
  142. }
  143. if (referencedTableSettings.length === 0) {
  144. throw new Error('Missing table definition {{"name","range",cache};{...}}');
  145. }
  146. Logger.log(`tableArr = ${referencedTableSettings}`);
  147. for (/** @type {any[]} */ const table of referencedTableSettings) {
  148. if (table.length === 1)
  149. table.push(table[0]); // if NO RANGE, assumes table name is sheet name.
  150. if (table.length === 2)
  151. table.push(60); // default 0 second cache.
  152. if (table.length === 3)
  153. table.push(true); // default HAS column title row.
  154. if (table[1] === "")
  155. table[1] = table[0]; // If empty range, assumes TABLE NAME is the SHEET NAME and loads entire sheet.
  156. if (table.length !== 4)
  157. throw new Error("Invalid table definition [name,range,cache,hasTitle]");
  158. tableList.push(table);
  159. }
  160. // If called at the same time, loading similar tables in similar order - all processes
  161. // just wait for table - but if loaded in different order, each process could be loading something.
  162. if (randomOrder)
  163. tableList = tableList.sort(() => Math.random() - 0.5);
  164. return tableList;
  165. }
  166. }
  167. /**
  168. * @classdesc
  169. * Perform SQL SELECT using this class.
  170. */
  171. class Sql {
  172. constructor() {
  173. /** @property {Map<String,Table>} - Map of referenced tables.*/
  174. this.tables = new Map();
  175. /** @property {Boolean} - Are column tables to be ouptout? */
  176. this.columnTitle = false;
  177. /** @property {BindData} - List of BIND data linked to '?' in statement. */
  178. this.bindData = new BindData();
  179. /** @property {String} - derived table name to output in column title replacing source table name. */
  180. this.columnTableNameReplacement = null;
  181. }
  182. /**
  183. * Parse SQL SELECT statement, performs SQL query and returns data ready for custom function return.
  184. * <br>Execute() can be called multiple times for different SELECT statements, provided that all required
  185. * table data was loaded in the constructor.
  186. * Methods that would be used PRIOR to execute are:
  187. * <br>**enableColumnTitle()** - turn on/off column title in output
  188. * <br>**addBindParameter()** - If bind data is needed in select. e.g. "select * from table where id = ?"
  189. * <br>**addTableData()** - At least ONE table needs to be added prior to execute. This tells **execute** where to find the data.
  190. * <br>**Example SELECT and RETURN Data**
  191. * ```js
  192. * let stmt = "SELECT books.id, books.title, books.author_id " +
  193. * "FROM books " +
  194. * "WHERE books.author_id IN ('11','12') " +
  195. * "ORDER BY books.title";
  196. *
  197. * let data = new Sql()
  198. * .addTableData("books", this.bookTable())
  199. * .enableColumnTitle(true)
  200. * .execute(stmt);
  201. *
  202. * Logger.log(data);
  203. *
  204. * [["books.id", "books.title", "books.author_id"],
  205. * ["4", "Dream Your Life", "11"],
  206. * ["8", "My Last Book", "11"],
  207. * ["5", "Oranges", "12"],
  208. * ["1", "Time to Grow Up!", "11"]]
  209. * ```
  210. * @param {any} statement - SELECT statement as STRING or AST of SELECT statement.
  211. * @returns {any[][]} - Double array where first index is ROW and second index is COLUMN.
  212. */
  213. execute(statement) {
  214. this.ast = (typeof statement === 'string') ? SqlParse.sql2ast(statement) : statement;
  215. // "SELECT * from (select a,b,c from table) as derivedtable"
  216. // Sub query data is loaded and given the name 'derivedtable' (using ALIAS from AS)
  217. // The AST.FROM is updated from the sub-query to the new derived table name.
  218. this.selectFromSubQuery();
  219. // A JOIN table can a sub-query. When this is the case, the sub-query SELECT is
  220. // evaluated and the return data is given the ALIAS (as) name. The AST is then
  221. // updated to use the new table.
  222. this.selectJoinSubQuery();
  223. TableAlias.setTableAlias(this.tables, this.ast);
  224. Sql.loadSchema(this.tables);
  225. let selectResults = this.select(this.ast);
  226. // Apply SET rules to SELECTs (UNION, UNION ALL, EXCEPT, INTERSECT)
  227. selectResults = this.selectSet(selectResults, this.ast);
  228. return selectResults;
  229. }
  230. /**
  231. * Modifies AST when FROM is a sub-query rather than a table name.
  232. */
  233. selectFromSubQuery() {
  234. if (typeof this.ast.FROM !== 'undefined' && typeof this.ast.FROM.SELECT !== 'undefined') {
  235. const data = new Sql()
  236. .setTables(this.tables)
  237. .enableColumnTitle(true)
  238. .replaceColumnTableNameWith(this.ast.FROM.table)
  239. .execute(this.ast.FROM);
  240. if (typeof this.ast.FROM.table !== 'undefined') {
  241. this.addTableData(this.ast.FROM.table, data);
  242. }
  243. if (this.ast.FROM.table === '') {
  244. throw new Error("Every derived table must have its own alias");
  245. }
  246. this.ast.FROM.as = '';
  247. }
  248. }
  249. /**
  250. * Checks if the JOINed table is a sub-query.
  251. * The sub-query is evaluated and assigned the alias name.
  252. * The AST is adjusted to use the new JOIN TABLE.
  253. * @returns {void}
  254. */
  255. selectJoinSubQuery() {
  256. if (typeof this.ast.JOIN === 'undefined')
  257. return;
  258. for (const joinAst of this.ast.JOIN) {
  259. if (typeof joinAst.table !== 'string') {
  260. const data = new Sql()
  261. .setTables(this.tables)
  262. .enableColumnTitle(true)
  263. .replaceColumnTableNameWith(joinAst.as)
  264. .execute(joinAst.table);
  265. if (typeof joinAst.as !== 'undefined') {
  266. this.addTableData(joinAst.as, data);
  267. }
  268. if (joinAst.as === '') {
  269. throw new Error("Every derived table must have its own alias");
  270. }
  271. joinAst.table = joinAst.as;
  272. joinAst.as = '';
  273. }
  274. }
  275. }
  276. /**
  277. * Apply set rules to each select result.
  278. * @param {any[][]} leftTableData
  279. * @param {Object} unionAst
  280. * @returns {any[][]}
  281. */
  282. selectSet(leftTableData, unionAst) {
  283. if (! SqlSets.isSqlSet(unionAst)) {
  284. return leftTableData;
  285. }
  286. // If the column titles are in the data, we need to remove and add back in later.
  287. let columnTitles = [];
  288. if (this.areColumnTitlesOutput() && leftTableData.length > 0) {
  289. columnTitles = leftTableData.shift();
  290. }
  291. this.enableColumnTitle(false);
  292. let ast = unionAst;
  293. while (SqlSets.isSqlSet(ast)) {
  294. const setType = SqlSets.getSetType(ast);
  295. ast = ast[setType][0];
  296. const rightTableData = this.select(ast);
  297. leftTableData = SqlSets.applySet(setType, leftTableData, rightTableData);
  298. }
  299. if (columnTitles.length > 0) {
  300. leftTableData.unshift(columnTitles);
  301. }
  302. return leftTableData;
  303. }
  304. /**
  305. * Load SELECT data and return in double array.
  306. * @param {Object} selectAst - Abstract Syntax Tree of SELECT
  307. * @returns {any[][]} - double array useable by Google Sheet in custom function return value.
  308. * * First row of data will be column name if column title output was requested.
  309. * * First Array Index - ROW
  310. * * Second Array Index - COLUMN
  311. */
  312. select(selectAst) {
  313. let ast = selectAst;
  314. Sql.errorCheckSelectAST(ast);
  315. // Manipulate AST to add GROUP BY if DISTINCT keyword.
  316. ast = Sql.distinctField(ast);
  317. // Manipulate AST add pivot fields.
  318. ast = Pivot.pivotField(ast, this.tables, this.bindData);
  319. const view = new SelectTables(ast, this.tables, this.bindData);
  320. // JOIN tables to create a derived table.
  321. view.join(ast); // skipcq: JS-D008
  322. view.updateSelectedFields(ast);
  323. // Get the record ID's of all records matching WHERE condition.
  324. const recordIDs = view.whereCondition(ast);
  325. // Get selected data records.
  326. let viewTableData = view.getViewData(recordIDs);
  327. // Compress the data.
  328. viewTableData = view.groupBy(ast, viewTableData);
  329. // Sort our selected data.
  330. view.orderBy(ast, viewTableData);
  331. // Remove fields referenced but not included in SELECT field list.
  332. view.removeTempColumns(viewTableData);
  333. // Limit rows returned.
  334. viewTableData = SelectTables.limit(ast, viewTableData);
  335. // Add column titles
  336. viewTableData = this.addColumnTitles(viewTableData, view);
  337. // Deal with empty dataset.
  338. viewTableData = Sql.cleanUp(viewTableData);
  339. return viewTableData;
  340. }
  341. /**
  342. * Add data for each referenced table in SELECT, before EXECUTE().
  343. * @param {String} tableName - Name of table referenced in SELECT.
  344. * @param {any} tableData - Either double array or a named range.
  345. * @param {Number} cacheSeconds - How long should loaded data be cached (default=0)
  346. * @param {Boolean} hasColumnTitle - Is first data row the column title?
  347. * @returns {Sql}
  348. */
  349. addTableData(tableName, tableData, cacheSeconds = 0, hasColumnTitle = true) {
  350. let tableInfo = null;
  351. if (Array.isArray(tableData)) {
  352. tableInfo = new Table(tableName)
  353. .setHasColumnTitle(hasColumnTitle)
  354. .loadArrayData(tableData);
  355. }
  356. else {
  357. tableInfo = new Table(tableName)
  358. .setHasColumnTitle(hasColumnTitle)
  359. .loadNamedRangeData(tableData, cacheSeconds);
  360. }
  361. this.tables.set(tableName.toUpperCase(), tableInfo);
  362. return this;
  363. }
  364. /**
  365. * Copies the data from an external tableMap to this instance.
  366. * It copies a reference to outside array data only.
  367. * The schema would need to be re-loaded.
  368. * @param {Map<String,Table>} tableMap
  369. */
  370. copyTableData(tableMap) {
  371. // @ts-ignore
  372. for (const tableName of tableMap.keys()) {
  373. const tableInfo = tableMap.get(tableName);
  374. this.addTableData(tableName, tableInfo.tableData);
  375. }
  376. return this;
  377. }
  378. /**
  379. * Include column headers in return data.
  380. * @param {Boolean} value - true will return column names in first row of return data.
  381. * @returns {Sql}
  382. */
  383. enableColumnTitle(value) {
  384. this.columnTitle = value;
  385. return this;
  386. }
  387. /**
  388. * Derived table data that requires the ALIAS table name in column title.
  389. * @param {String} replacementTableName - derived table name to replace original table name. To disable, set to null.
  390. * @returns {Sql}
  391. */
  392. replaceColumnTableNameWith(replacementTableName) {
  393. this.columnTableNameReplacement = replacementTableName;
  394. return this;
  395. }
  396. /**
  397. * Query if this instance of Sql() will generate column titles.
  398. * @returns {Boolean}
  399. */
  400. areColumnTitlesOutput() {
  401. return this.columnTitle;
  402. }
  403. /**
  404. * Add a bind data value. Must be added in order. If bind data is a named range, use addBindNamedRangeParameter().
  405. * @param {any} value - literal data.
  406. * @returns {Sql}
  407. */
  408. addBindParameter(value) {
  409. this.bindData.add(value);
  410. return this;
  411. }
  412. /**
  413. * List of bind data added so far.
  414. * @returns {any[]}
  415. */
  416. getBindData() {
  417. return this.bindData.getBindDataList();
  418. }
  419. /**
  420. * The BIND data is a sheet named range that will be read and used for bind data.
  421. * @param {String} value - Sheets Named Range for SINGLE CELL only.
  422. * @returns {Sql}
  423. */
  424. addBindNamedRangeParameter(value) {
  425. const namedValue = TableData.getValueCached(value, 30);
  426. this.bindData.add(namedValue);
  427. Logger.log(`BIND=${value} = ${namedValue}`);
  428. return this;
  429. }
  430. /**
  431. * Set all bind data at once using array.
  432. * @param {BindData} value - Bind data.
  433. * @returns {Sql}
  434. */
  435. setBindValues(value) {
  436. this.bindData = value;
  437. return this;
  438. }
  439. /**
  440. * Clears existing BIND data so Sql() instance can be used again with new bind parameters.
  441. * @returns {Sql}
  442. */
  443. clearBindParameters() {
  444. this.bindData.clear();
  445. return this;
  446. }
  447. /**
  448. * Updates 'tables' with table column information.
  449. * @param {Map<String,Table>} tables
  450. */
  451. static loadSchema(tables) {
  452. // @ts-ignore
  453. for (const table of tables.keys()) {
  454. const tableInfo = tables.get(table.toUpperCase());
  455. tableInfo.loadSchema();
  456. }
  457. }
  458. /**
  459. * Sets all tables referenced SELECT.
  460. * @param {Map<String,Table>} mapOfTables - Map of referenced tables indexed by TABLE name.
  461. */
  462. setTables(mapOfTables) {
  463. this.tables = mapOfTables;
  464. return this;
  465. }
  466. /**
  467. * Returns a map of all tables configured for this SELECT.
  468. * @returns {Map<String,Table>} - Map of referenced tables indexed by TABLE name.
  469. */
  470. getTables() {
  471. return this.tables;
  472. }
  473. /**
  474. * Basic sanity check of AST for a SELECT statement.
  475. * @param {object} ast
  476. */
  477. static errorCheckSelectAST(ast) {
  478. if (typeof ast.SELECT === 'undefined') {
  479. throw new Error("Only SELECT statements are supported.");
  480. }
  481. if (typeof ast.FROM === 'undefined') {
  482. throw new Error("Missing keyword FROM");
  483. }
  484. }
  485. /**
  486. * If 'GROUP BY' is not set and 'DISTINCT' column is specified, update AST to add 'GROUP BY'.
  487. * @param {Object} ast - Abstract Syntax Tree for SELECT.
  488. * @returns {Object} - Updated AST to include GROUP BY when DISTINCT field used.
  489. */
  490. static distinctField(ast) {
  491. const astFields = ast.SELECT;
  492. if (astFields.length === 0)
  493. return ast;
  494. const firstField = astFields[0].name.toUpperCase();
  495. if (firstField.startsWith("DISTINCT")) {
  496. astFields[0].name = firstField.replace("DISTINCT", "").trim();
  497. if (typeof ast['GROUP BY'] === 'undefined') {
  498. const groupBy = [];
  499. for (const astItem of astFields) {
  500. groupBy.push({ name: astItem.name, as: '' });
  501. }
  502. ast["GROUP BY"] = groupBy;
  503. }
  504. }
  505. return ast;
  506. }
  507. /**
  508. * Add column titles to data if needed.
  509. * @param {any[][]} viewTableData
  510. * @param {SelectTables} view
  511. * @returns {any[][]}
  512. */
  513. addColumnTitles(viewTableData, view) {
  514. if (this.columnTitle) {
  515. viewTableData.unshift(view.getColumnTitles(this.columnTableNameReplacement));
  516. }
  517. return viewTableData;
  518. }
  519. /**
  520. * If no data and no titles, create empty double array so sheets function does not have an error.
  521. * @param {any[][]} viewTableData
  522. * @returns {any[][]}
  523. */
  524. static cleanUp(viewTableData) {
  525. if (viewTableData.length === 0) {
  526. viewTableData.push([""]);
  527. }
  528. if (viewTableData.length === 1 && viewTableData[0].length === 0) {
  529. viewTableData[0] = [""];
  530. }
  531. return viewTableData;
  532. }
  533. }
  534. /**
  535. * @classdesc Deals with the table ALIAS inside select AST.
  536. */
  537. class TableAlias {
  538. /**
  539. * Updates 'tables' with associated table ALIAS name found in ast.
  540. * @param {Map<String,Table>} tables
  541. * @param {Object} ast
  542. */
  543. static setTableAlias(tables, ast) {
  544. // @ts-ignore
  545. for (const table of tables.keys()) {
  546. const tableAlias = TableAlias.getTableAlias(table, ast);
  547. const tableInfo = tables.get(table.toUpperCase());
  548. tableInfo.setTableAlias(tableAlias);
  549. }
  550. }
  551. /**
  552. * Find table alias name (if any) for input actual table name.
  553. * @param {String} tableName - Actual table name.
  554. * @param {Object} ast - Abstract Syntax Tree for SQL.
  555. * @returns {String} - Table alias. Empty string if not found.
  556. */
  557. static getTableAlias(tableName, ast) {
  558. let tableAlias = "";
  559. const ucTableName = tableName.toUpperCase();
  560. tableAlias = TableAlias.getTableAliasFromJoin(tableAlias, ucTableName, ast);
  561. tableAlias = TableAlias.getTableAliasUnion(tableAlias, ucTableName, ast);
  562. tableAlias = TableAlias.getTableAliasWhereIn(tableAlias, ucTableName, ast);
  563. tableAlias = TableAlias.getTableAliasWhereTerms(tableAlias, ucTableName, ast);
  564. return tableAlias;
  565. }
  566. /**
  567. * Searches the FROM and JOIN components of a SELECT to find the table alias.
  568. * @param {String} tableAlias - Default alias name
  569. * @param {String} tableName - table name to search for.
  570. * @param {Object} ast - Abstract Syntax Tree to search
  571. * @returns {String} - Table alias name.
  572. */
  573. static getTableAliasFromJoin(tableAlias, tableName, ast) {
  574. const astTableBlocks = ['FROM', 'JOIN'];
  575. let aliasNameFound = tableAlias;
  576. let i = 0;
  577. while (aliasNameFound === "" && i < astTableBlocks.length) {
  578. aliasNameFound = TableAlias.locateAstTableAlias(tableName, ast, astTableBlocks[i]);
  579. i++;
  580. }
  581. return aliasNameFound;
  582. }
  583. /**
  584. * Search a property of AST for table alias name.
  585. * @param {String} tableName - Table name to find in AST.
  586. * @param {Object} ast - AST of SELECT.
  587. * @param {String} astBlock - AST property to search.
  588. * @returns {String} - Alias name or "" if not found.
  589. */
  590. static locateAstTableAlias(tableName, ast, astBlock) {
  591. if (typeof ast[astBlock] === 'undefined')
  592. return "";
  593. let block = [ast[astBlock]];
  594. if (TableAlias.isIterable(ast[astBlock])) {
  595. block = ast[astBlock];
  596. }
  597. for (const astItem of block) {
  598. if (typeof astItem.table === 'string' && tableName === astItem.table.toUpperCase() && astItem.as !== "") {
  599. return astItem.as;
  600. }
  601. }
  602. return "";
  603. }
  604. /**
  605. * Check if input is iterable.
  606. * @param {any} input - Check this object to see if it can be iterated.
  607. * @returns {Boolean} - true - can be iterated. false - cannot be iterated.
  608. */
  609. static isIterable(input) {
  610. if (input === null || input === undefined) {
  611. return false
  612. }
  613. return typeof input[Symbol.iterator] === 'function'
  614. }
  615. /**
  616. * Searches the UNION portion of the SELECT to locate the table alias.
  617. * @param {String} tableAlias - default table alias.
  618. * @param {String} tableName - table name to search for.
  619. * @param {Object} ast - Abstract Syntax Tree to search
  620. * @returns {String} - table alias
  621. */
  622. static getTableAliasUnion(tableAlias, tableName, ast) {
  623. const astRecursiveTableBlocks = ['UNION', 'UNION ALL', 'INTERSECT', 'EXCEPT'];
  624. let extractedAlias = tableAlias;
  625. let i = 0;
  626. while (extractedAlias === "" && i < astRecursiveTableBlocks.length) {
  627. if (typeof ast[astRecursiveTableBlocks[i]] !== 'undefined') {
  628. for (const unionAst of ast[astRecursiveTableBlocks[i]]) {
  629. extractedAlias = TableAlias.getTableAlias(tableName, unionAst);
  630. if (extractedAlias !== "")
  631. break;
  632. }
  633. }
  634. i++;
  635. }
  636. return extractedAlias;
  637. }
  638. /**
  639. * Search WHERE IN component of SELECT to find table alias.
  640. * @param {String} tableAlias - default table alias
  641. * @param {String} tableName - table name to search for
  642. * @param {Object} ast - Abstract Syntax Tree to search
  643. * @returns {String} - table alias
  644. */
  645. static getTableAliasWhereIn(tableAlias, tableName, ast) {
  646. let extractedAlias = tableAlias;
  647. if (tableAlias === "" && typeof ast.WHERE !== 'undefined' && ast.WHERE.operator === "IN") {
  648. extractedAlias = TableAlias.getTableAlias(tableName, ast.WHERE.right);
  649. }
  650. if (extractedAlias === "" && ast.operator === "IN") {
  651. extractedAlias = TableAlias.getTableAlias(tableName, ast.right);
  652. }
  653. return extractedAlias;
  654. }
  655. /**
  656. * Search WHERE terms of SELECT to find table alias.
  657. * @param {String} tableAlias - default table alias
  658. * @param {String} tableName - table name to search for.
  659. * @param {Object} ast - Abstract Syntax Tree to search.
  660. * @returns {String} - table alias
  661. */
  662. static getTableAliasWhereTerms(tableAlias, tableName, ast) {
  663. let extractedTableAlias = tableAlias;
  664. if (tableAlias === "" && typeof ast.WHERE !== 'undefined' && typeof ast.WHERE.terms !== 'undefined') {
  665. for (const term of ast.WHERE.terms) {
  666. if (extractedTableAlias === "")
  667. extractedTableAlias = TableAlias.getTableAlias(tableName, term);
  668. }
  669. }
  670. return extractedTableAlias;
  671. }
  672. }
  673. /**
  674. * @classdesc Deals with extracting all TABLE names referenece inside SELECT.
  675. */
  676. class TableExtract {
  677. /**
  678. * Create table definition array from select string.
  679. * @param {String} statement - full sql select statement.
  680. * @returns {String[][]} - table definition array.
  681. */
  682. static getReferencedTableNames(statement) {
  683. const ast = SqlParse.sql2ast(statement);
  684. return TableExtract.getReferencedTableNamesFromAst(ast);
  685. }
  686. /**
  687. * Create table definition array from select AST.
  688. * @param {Object} ast - AST for SELECT.
  689. * @returns {any[]} - table definition array.
  690. * * [0] - table name.
  691. * * [1] - sheet tab name
  692. * * [2] - cache seconds
  693. * * [3] - output column title flag
  694. */
  695. static getReferencedTableNamesFromAst(ast) {
  696. const DEFAULT_CACHE_SECONDS = 60;
  697. const DEFAULT_COLUMNS_OUTPUT = true;
  698. const tableSet = new Map();
  699. TableExtract.extractAstTables(ast, tableSet);
  700. const tableList = [];
  701. // @ts-ignore
  702. for (const key of tableSet.keys()) {
  703. const tableDef = [key, key, DEFAULT_CACHE_SECONDS, DEFAULT_COLUMNS_OUTPUT];
  704. tableList.push(tableDef);
  705. }
  706. return tableList;
  707. }
  708. /**
  709. * Search for all referenced tables in SELECT.
  710. * @param {Object} ast - AST for SELECT.
  711. * @param {Map<String,String>} tableSet - Function updates this map of table names and alias name.
  712. */
  713. static extractAstTables(ast, tableSet) {
  714. TableExtract.getTableNamesFrom(ast, tableSet);
  715. TableExtract.getTableNamesJoin(ast, tableSet);
  716. TableExtract.getTableNamesUnion(ast, tableSet);
  717. TableExtract.getTableNamesWhereIn(ast, tableSet);
  718. TableExtract.getTableNamesWhereTerms(ast, tableSet);
  719. TableExtract.getTableNamesCorrelatedSelect(ast, tableSet);
  720. }
  721. /**
  722. * Search for referenced table in FROM or JOIN part of select.
  723. * @param {Object} ast - AST for SELECT.
  724. * @param {Map<String,String>} tableSet - Function updates this map of table names and alias name.
  725. */
  726. static getTableNamesFrom(ast, tableSet) {
  727. let fromAst = ast.FROM;
  728. while (typeof fromAst !== 'undefined') {
  729. if (typeof fromAst.isDerived === 'undefined') {
  730. tableSet.set(fromAst.table.toUpperCase(), typeof fromAst.as === 'undefined' ? '' : fromAst.as.toUpperCase());
  731. }
  732. else {
  733. TableExtract.extractAstTables(fromAst.FROM, tableSet);
  734. TableExtract.getTableNamesUnion(fromAst, tableSet);
  735. }
  736. fromAst = fromAst.FROM;
  737. }
  738. }
  739. /**
  740. * Search for referenced table in FROM or JOIN part of select.
  741. * @param {Object} ast - AST for SELECT.
  742. * @param {Map<String,String>} tableSet - Function updates this map of table names and alias name.
  743. */
  744. static getTableNamesJoin(ast, tableSet) {
  745. if (typeof ast.JOIN === 'undefined')
  746. return;
  747. for (const astItem of ast.JOIN) {
  748. if (typeof astItem.table === 'string') {
  749. tableSet.set(astItem.table.toUpperCase(), typeof astItem.as === 'undefined' ? '' : astItem.as.toUpperCase());
  750. }
  751. else {
  752. TableExtract.extractAstTables(astItem.table, tableSet);
  753. }
  754. }
  755. }
  756. /**
  757. * Searches for table names within SELECT (union, intersect, except) statements.
  758. * @param {Object} ast - AST for SELECT
  759. * @param {Map<String,String>} tableSet - Function updates this map of table names and alias name.
  760. */
  761. static getTableNamesUnion(ast, tableSet) {
  762. const astRecursiveTableBlocks = ['UNION', 'UNION ALL', 'INTERSECT', 'EXCEPT'];
  763. for (const block of astRecursiveTableBlocks) {
  764. if (typeof ast[block] !== 'undefined') {
  765. for (const unionAst of ast[block]) {
  766. this.extractAstTables(unionAst, tableSet);
  767. }
  768. }
  769. }
  770. }
  771. /**
  772. * Searches for tables names within SELECT (in, exists) statements.
  773. * @param {Object} ast - AST for SELECT
  774. * @param {Map<String,String>} tableSet - Function updates this map of table names and alias name.
  775. */
  776. static getTableNamesWhereIn(ast, tableSet) {
  777. // where IN ().
  778. const subQueryTerms = ["IN", "NOT IN", "EXISTS", "NOT EXISTS"]
  779. if (typeof ast.WHERE !== 'undefined' && (subQueryTerms.indexOf(ast.WHERE.operator) !== -1)) {
  780. this.extractAstTables(ast.WHERE.right, tableSet);
  781. }
  782. if (subQueryTerms.indexOf(ast.operator) !== -1) {
  783. this.extractAstTables(ast.right, tableSet);
  784. }
  785. }
  786. /**
  787. * Search WHERE to find referenced table names.
  788. * @param {Object} ast - AST to search.
  789. * @param {Map<String,String>} tableSet - Function updates this map of table names and alias name.
  790. */
  791. static getTableNamesWhereTerms(ast, tableSet) {
  792. if (typeof ast.WHERE !== 'undefined' && typeof ast.WHERE.terms !== 'undefined') {
  793. for (const term of ast.WHERE.terms) {
  794. this.extractAstTables(term, tableSet);
  795. }
  796. }
  797. }
  798. /**
  799. * Search for table references in the WHERE condition.
  800. * @param {Object} ast - AST to search.
  801. * @param {Map<String,String>} tableSet - Function updates this map of table names and alias name.
  802. */
  803. static getTableNamesWhereCondition(ast, tableSet) {
  804. const lParts = typeof ast.left === 'string' ? ast.left.split(".") : [];
  805. if (lParts.length > 1) {
  806. tableSet.set(lParts[0].toUpperCase(), "");
  807. }
  808. const rParts = typeof ast.right === 'string' ? ast.right.split(".") : [];
  809. if (rParts.length > 1) {
  810. tableSet.set(rParts[0].toUpperCase(), "");
  811. }
  812. if (typeof ast.terms !== 'undefined') {
  813. for (const term of ast.terms) {
  814. TableExtract.getTableNamesWhereCondition(term, tableSet);
  815. }
  816. }
  817. }
  818. /**
  819. * Search CORRELATES sub-query for table names.
  820. * @param {*} ast - AST to search
  821. * @param {*} tableSet - Function updates this map of table names and alias name.
  822. */
  823. static getTableNamesCorrelatedSelect(ast, tableSet) {
  824. if (typeof ast.SELECT !== 'undefined') {
  825. for (const term of ast.SELECT) {
  826. if (typeof term.subQuery !== 'undefined' && term.subQuery !== null) {
  827. this.extractAstTables(term.subQuery, tableSet);
  828. }
  829. }
  830. }
  831. }
  832. }
  833. /**
  834. * @classdesc Manipulation of AST to handle PIVOT statement.
  835. */
  836. class Pivot {
  837. /**
  838. * Add new column to AST for every AGGREGATE function and unique pivot column data.
  839. * @param {Object} ast - AST which is checked to see if a PIVOT is used.
  840. * @param {Map<String,Table>} tables - Map of table info.
  841. * @param {BindData} bindData - List of bind data.
  842. * @returns {Object} - Updated AST containing SELECT FIELDS for the pivot data OR original AST if no pivot.
  843. */
  844. static pivotField(ast, tables, bindData) {
  845. // If we are doing a PIVOT, it then requires a GROUP BY.
  846. if (typeof ast.PIVOT !== 'undefined') {
  847. if (typeof ast['GROUP BY'] === 'undefined')
  848. throw new Error("PIVOT requires GROUP BY");
  849. }
  850. else {
  851. return ast;
  852. }
  853. // These are all of the unique PIVOT field data points.
  854. const pivotFieldData = Pivot.getUniquePivotData(ast, tables, bindData);
  855. ast.SELECT = Pivot.addCalculatedPivotFieldsToAst(ast, pivotFieldData);
  856. return ast;
  857. }
  858. /**
  859. * Find distinct pivot column data.
  860. * @param {Object} ast - Abstract Syntax Tree containing the PIVOT option.
  861. * @returns {any[][]} - All unique data points found in the PIVOT field for the given SELECT.
  862. */
  863. static getUniquePivotData(ast, tables, bindData) {
  864. const pivotAST = {};
  865. pivotAST.SELECT = ast.PIVOT;
  866. pivotAST.SELECT[0].name = `DISTINCT ${pivotAST.SELECT[0].name}`;
  867. pivotAST.FROM = ast.FROM;
  868. pivotAST.WHERE = ast.WHERE;
  869. const pivotSql = new Sql()
  870. .enableColumnTitle(false)
  871. .setBindValues(bindData)
  872. .copyTableData(tables);
  873. // These are all of the unique PIVOT field data points.
  874. const tableData = pivotSql.execute(pivotAST);
  875. return tableData;
  876. }
  877. /**
  878. * Add new calculated fields to the existing SELECT fields. A field is add for each combination of
  879. * aggregate function and unqiue pivot data points. The CASE function is used for each new field.
  880. * A test is made if the column data equal the pivot data. If it is, the aggregate function data
  881. * is returned, otherwise null. The GROUP BY is later applied and the appropiate pivot data will
  882. * be calculated.
  883. * @param {Object} ast - AST to be updated.
  884. * @param {any[][]} pivotFieldData - Table data with unique pivot field data points.
  885. * @returns {Object} - Abstract Sytax Tree with new SELECT fields with a CASE for each pivot data and aggregate function.
  886. */
  887. static addCalculatedPivotFieldsToAst(ast, pivotFieldData) {
  888. const newPivotAstFields = [];
  889. for (const selectField of ast.SELECT) {
  890. // If this is an aggregrate function, we will add one for every pivotFieldData item
  891. const functionNameRegex = /^\w+\s*(?=\()/;
  892. const matches = selectField.name.match(functionNameRegex)
  893. if (matches !== null && matches.length > 0) {
  894. const args = SelectTables.parseForFunctions(selectField.name, matches[0].trim());
  895. for (const fld of pivotFieldData) {
  896. const caseTxt = `${matches[0]}(CASE WHEN ${ast.PIVOT[0].name} = '${fld}' THEN ${args[1]} ELSE 'null' END)`;
  897. const asField = `${fld[0]} ${typeof selectField.as !== 'undefined' && selectField.as !== "" ? selectField.as : selectField.name}`;
  898. newPivotAstFields.push({ name: caseTxt, as: asField });
  899. }
  900. }
  901. else {
  902. newPivotAstFields.push(selectField);
  903. }
  904. }
  905. return newPivotAstFields;
  906. }
  907. }
  908. /**
  909. * @classdesc Deals with processing SET theory on SELECT table results.
  910. */
  911. class SqlSets {
  912. /**
  913. * Get list of valid set types.
  914. * @returns {String[]}
  915. */
  916. static getUnionTypes() {
  917. return ['UNION', 'UNION ALL', 'INTERSECT', 'EXCEPT'];
  918. }
  919. /**
  920. * Determine what set type is applied to the select results.
  921. * @param {Object} ast
  922. * @returns {String}
  923. */
  924. static getSetType(ast) {
  925. for (const type of SqlSets.getUnionTypes()) {
  926. if (typeof ast[type] !== 'undefined') {
  927. return type;
  928. }
  929. }
  930. return "";
  931. }
  932. /**
  933. * Apply set theory to data.
  934. * @param {String} type ("UNION", "UNION ALL", "INTERSECT", "EXCEPT")
  935. * @param {any[][]} leftTableData
  936. * @param {any[][]} rightTableData
  937. * @returns {any[][]}
  938. */
  939. static applySet(type, leftTableData, rightTableData) {
  940. if (leftTableData.length > 0 && rightTableData.length > 0 && leftTableData[0].length !== rightTableData[0].length) {
  941. throw new Error(`Invalid ${type}. Selected field counts do not match.`);
  942. }
  943. switch (type) {
  944. case "UNION":
  945. leftTableData = leftTableData.concat(rightTableData);
  946. leftTableData = SqlSets.removeDuplicateRows(leftTableData);
  947. break;
  948. case "UNION ALL":
  949. // Allow duplicates.
  950. leftTableData = leftTableData.concat(rightTableData);
  951. break;
  952. case "INTERSECT":
  953. // Must exist in BOTH tables.
  954. leftTableData = SqlSets.intersectRows(leftTableData, rightTableData);
  955. break;
  956. case "EXCEPT":
  957. // Remove from first table all rows that match in second table.
  958. leftTableData = SqlSets.exceptRows(leftTableData, rightTableData);
  959. break;
  960. default:
  961. throw new Error(`Internal error. Unsupported UNION type: ${type}`);
  962. }
  963. return leftTableData;
  964. }
  965. /**
  966. *
  967. * @param {Object} ast
  968. * @returns {Boolean}
  969. */
  970. static isSqlSet(ast) {
  971. for (const type of SqlSets.getUnionTypes()) {
  972. if (typeof ast[type] !== 'undefined') {
  973. return true;
  974. }
  975. }
  976. return false;
  977. }
  978. /**
  979. * Remove all duplicate table rows
  980. * @param {any[][]} srcData
  981. * @returns {any[][]}
  982. */
  983. static removeDuplicateRows(srcData) {
  984. const newTableData = [];
  985. const srcDataRecordKeys = new Map();
  986. for (const row of srcData) {
  987. const key = row.join("::");
  988. if (!srcDataRecordKeys.has(key)) {
  989. newTableData.push(row);
  990. srcDataRecordKeys.set(key, true);
  991. }
  992. }
  993. return newTableData;
  994. }
  995. /**
  996. * Finds the rows that are common between srcData and newData
  997. * @param {any[][]} srcData - table data
  998. * @param {any[][]} newData - table data
  999. * @returns {any[][]} - returns only rows that intersect srcData and newData.
  1000. */
  1001. static intersectRows(srcData, newData) {
  1002. const srcMap = new Map();
  1003. const intersectTable = [];
  1004. for (const srcRow of srcData) {
  1005. srcMap.set(srcRow.join("::"), true);
  1006. }
  1007. for (const newRow of newData) {
  1008. if (srcMap.has(newRow.join("::"))) {
  1009. intersectTable.push(newRow);
  1010. }
  1011. }
  1012. return intersectTable;
  1013. }
  1014. /**
  1015. * Returns all rows in srcData MINUS any rows that match it from newData.
  1016. * @param {any[][]} srcData - starting table
  1017. * @param {any[][]} newData - minus table (if it matches srcData row)
  1018. * @returns {any[][]} - srcData MINUS newData
  1019. */
  1020. static exceptRows(srcData, newData) {
  1021. const srcMap = new Map();
  1022. let rowNum = 0;
  1023. for (const srcRow of srcData) {
  1024. srcMap.set(srcRow.join("::"), rowNum);
  1025. rowNum++;
  1026. }
  1027. const removeRowNum = [];
  1028. for (const newRow of newData) {
  1029. const key = newRow.join("::");
  1030. if (srcMap.has(key)) {
  1031. removeRowNum.push(srcMap.get(key));
  1032. }
  1033. }
  1034. removeRowNum.sort((a, b) => b - a);
  1035. for (rowNum of removeRowNum) {
  1036. srcData.splice(rowNum, 1);
  1037. }
  1038. return srcData;
  1039. }
  1040. }
  1041. /**
  1042. * @classdesc
  1043. * Store and retrieve bind data for use in WHERE portion of SELECT statement.
  1044. */
  1045. class BindData {
  1046. constructor() {
  1047. this.clear();
  1048. }
  1049. /**
  1050. * Reset the bind data.
  1051. */
  1052. clear() {
  1053. this.next = 1;
  1054. this.bindMap = new Map();
  1055. this.bindQueue = [];
  1056. }
  1057. /**
  1058. * Add bind data
  1059. * @param {any} data - bind data
  1060. * @returns {String} - bind variable name for reference in SQL. e.g. first data point would return '?1'.
  1061. */
  1062. add(data) {
  1063. const key = `?${this.next.toString()}`;
  1064. this.bindMap.set(key, data);
  1065. this.bindQueue.push(data);
  1066. this.next++;
  1067. return key;
  1068. }
  1069. /**
  1070. * Add a list of bind data points.
  1071. * @param {any[]} bindList
  1072. */
  1073. addList(bindList) {
  1074. for (const data of bindList) {
  1075. this.add(data);
  1076. }
  1077. }
  1078. /**
  1079. * Pull out a bind data entry.
  1080. * @param {String} name - Get by name or get NEXT if empty.
  1081. * @returns {any}
  1082. */
  1083. get(name = "") {
  1084. return name === '' ? this.bindQueue.shift() : this.bindMap.get(name);
  1085. }
  1086. /**
  1087. * Return the ordered list of bind data.
  1088. * @returns {any[]} - Current list of bind data.
  1089. */
  1090. getBindDataList() {
  1091. return this.bindQueue;
  1092. }
  1093. }