From 8b29877f237438855613ccad380a25b72b4b06f2 Mon Sep 17 00:00:00 2001 From: Pranav C <61551451+pranavxc@users.noreply.github.com> Date: Fri, 6 Aug 2021 20:14:55 +0530 Subject: [PATCH] feat: add support to more formula functions Added - 'MIN','MAX','CEILING', 'FLOOR', 'ROUND', 'MOD', 'REPEAT', 'LOG', 'EXP', 'POWER', 'SQRT', 'ABS', 'NOW', 'REPLACE Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com> --- .../components/editColumn/formulaOptions.vue | 10 +- packages/nc-gui/plugins/ncApis/restApi.js | 2 +- .../lib/sql/formulaQueryBuilderFromString.ts | 8 +- .../lib/dataMapper/lib/sql/getFunctionName.ts | 44 ------- .../lib/dataMapper/lib/sql/mapFunctionName.ts | 116 ++++++++++++++++++ 5 files changed, 131 insertions(+), 49 deletions(-) delete mode 100644 packages/nocodb/src/lib/dataMapper/lib/sql/getFunctionName.ts create mode 100644 packages/nocodb/src/lib/dataMapper/lib/sql/mapFunctionName.ts diff --git a/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue b/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue index 352542b432..e63d6711f5 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue @@ -75,7 +75,15 @@ export default { 'AVG', 'ADD', 'CONCAT', 'TRIM', 'UPPER', 'LOWER', - 'LEN' + 'LEN', + 'MIN', + 'MAX', + 'CEILING', 'FLOOR', 'ROUND', + 'MOD', 'REPEAT', + 'LOG', 'EXP', 'POWER', 'SQRT', // todo: remove in sqlite + 'ABS', + 'NOW', + 'REPLACE' ], availableBinOps: ['+', '-', '*', '/'], autocomplete: false, diff --git a/packages/nc-gui/plugins/ncApis/restApi.js b/packages/nc-gui/plugins/ncApis/restApi.js index 8cf8e4e6de..1c2b014807 100644 --- a/packages/nc-gui/plugins/ncApis/restApi.js +++ b/packages/nc-gui/plugins/ncApis/restApi.js @@ -76,7 +76,7 @@ export default class RestApi { prevValue: oldData[colName] }]) - return res + return res.data } async insert(data) { diff --git a/packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilderFromString.ts b/packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilderFromString.ts index 3578486542..25615eb00a 100644 --- a/packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilderFromString.ts +++ b/packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilderFromString.ts @@ -1,5 +1,5 @@ import jsep from 'jsep'; -import getFunctionName from "./getFunctionName"; +import mapFunctionName from "./mapFunctionName"; // todo: switch function based on database @@ -53,8 +53,10 @@ export default function formulaQueryBuilder(tree, alias, knex, aliasToColumn = { } } break; - default: - pt.callee.name = getFunctionName(pt.callee.name, knex) + default: { + const res = mapFunctionName({pt, knex, alias, aliasToCol: aliasToColumn, fn}) + if (res) return res; + } break } diff --git a/packages/nocodb/src/lib/dataMapper/lib/sql/getFunctionName.ts b/packages/nocodb/src/lib/dataMapper/lib/sql/getFunctionName.ts deleted file mode 100644 index 4daa2c4e6a..0000000000 --- a/packages/nocodb/src/lib/dataMapper/lib/sql/getFunctionName.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {XKnex} from "../../index"; - -const pg = { - LEN: 'length' -} - -const mssql = { - LEN: 'LEN' -} - -const mysql2 = { - LEN: 'CHAR_LENGTH' -} - - -const sqlite3 = { - LEN: 'LENGTH' -} - - -const getFunctionName = (name, knex: XKnex) => { - - switch (knex.clientType()) { - - case 'mysql': - case 'mysql2': - return mysql2[name] || name; - break; - case 'pg': - case 'postgre': - return pg[name] || name; - break; - case 'mssql': - return mssql[name] || name; - break; - case 'sqlite': - case 'sqlite3': - return sqlite3[name] || name; - break; - } -} - - -export default getFunctionName; \ No newline at end of file diff --git a/packages/nocodb/src/lib/dataMapper/lib/sql/mapFunctionName.ts b/packages/nocodb/src/lib/dataMapper/lib/sql/mapFunctionName.ts new file mode 100644 index 0000000000..3c5a04ad2e --- /dev/null +++ b/packages/nocodb/src/lib/dataMapper/lib/sql/mapFunctionName.ts @@ -0,0 +1,116 @@ +import {XKnex} from "../../index"; + +interface MapFnArgs { + pt: any, + aliasToCol: { [alias: string]: string }, + knex: XKnex, + alias: string, + fn: (...args: any) => any +} + +const MOD = (pt) => { + Object.assign(pt, { + type: 'BinaryExpression', + operator: '%', + left: pt.arguments[0], + right: pt.arguments[1] + }) +} +const mysql2 = { + LEN: 'CHAR_LENGTH', + MIN: 'LEAST', + MAX: 'GREATEST', +} + +const pg = { + LEN: 'length', + MIN: 'least', + MAX: 'greatest', + CEILING: 'ceil', + ROUND: 'round', + POWER: 'pow', + SQRT: 'sqrt' +} + +const mssql = { + MIN: (args: MapFnArgs) => { + if (args.pt.arguments.length === 1) { + return args.fn(args.pt.arguments[0]) + } + let query = ''; + for (const [i, arg] of Object.entries(args.pt.arguments)) { + if (+i === args.pt.arguments.length - 1) { + query += args.knex.raw(`\n\tElse ${args.fn(arg).toQuery()}`).toQuery() + } else { + query += args.knex.raw(`\n\tWhen ${args.pt.arguments.filter((_, j) => +i !== j).map(arg1 => `${args.fn(arg).toQuery()} < ${args.fn(arg1).toQuery()}`).join(' And ')} Then ${args.fn(arg).toQuery()}`).toQuery() + } + } + + return args.knex.raw(`Case ${query}\n End as ${args.alias}`) + }, + MAX: (args: MapFnArgs) => { + if (args.pt.arguments.length === 1) { + return args.fn(args.pt.arguments[0]) + } + let query = ''; + for (const [i, arg] of Object.entries(args.pt.arguments)) { + if (+i === args.pt.arguments.length - 1) { + query += args.knex.raw(`\nElse ${args.fn(arg).toQuery()}`).toQuery() + } else { + query += args.knex.raw(`\nWhen ${args.pt.arguments.filter((_, j) => +i !== j).map(arg1 => `${args.fn(arg).toQuery()} > ${args.fn(arg1).toQuery()}`).join(' And ')} Then ${args.fn(arg).toQuery()}`).toQuery() + } + } + + return args.knex.raw(`Case ${query}\n End as ${args.alias}`) + }, + MOD, + REPEAT: 'REPLICATE', + NOW: 'getdate' +} + + +const sqlite3 = { + LEN: 'LENGTH', + CEILING(_pt) { + // todo: + }, FLOOR(_pt) { + // todo: + }, + MOD, + REPEAT(args: MapFnArgs) { + return args.knex.raw(`replace(printf('%.' || ${args.fn(args.pt.arguments[1])} || 'c', '/'),'/',${args.fn(args.pt.arguments[0])}) ${args.alias}`) + }, + NOW: 'DATE' +} + + +const mapFunctionName = (args: MapFnArgs): any => { + const name = args.pt.callee.name; + let val; + switch (args.knex.clientType()) { + case 'mysql': + case 'mysql2': + val = mysql2[name] || name; + break; + case 'pg': + case 'postgre': + val = pg[name] || name; + break; + case 'mssql': + val = mssql[name] || name; + break; + case 'sqlite': + case 'sqlite3': + val = sqlite3[name] || name; + break; + } + + if (typeof val === 'function') { + return val(args) + } else if (typeof val === 'string') { + args.pt.callee.name = val; + } +} + + +export default mapFunctionName; \ No newline at end of file