From 1d94b7577a765100474dcb19e5e09908b136ed6f Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Wed, 25 May 2022 13:52:46 +0800 Subject: [PATCH 1/9] enhancement: treat NULL values as empty strings for CONCAT function --- .../sql/formulav2/formulaQueryBuilderv2.ts | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/nocodb/src/lib/dataMapper/lib/sql/formulav2/formulaQueryBuilderv2.ts b/packages/nocodb/src/lib/dataMapper/lib/sql/formulav2/formulaQueryBuilderv2.ts index 35e2ecab30..a73841eb05 100644 --- a/packages/nocodb/src/lib/dataMapper/lib/sql/formulav2/formulaQueryBuilderv2.ts +++ b/packages/nocodb/src/lib/dataMapper/lib/sql/formulav2/formulaQueryBuilderv2.ts @@ -612,7 +612,21 @@ export default async function formulaQueryBuilderv2( return knex.raw( `${pt.callee.name}(${pt.arguments - .map(arg => fn(arg).toQuery()) + .map(arg => { + const query = fn(arg).toQuery(); + if (pt.callee.name === 'CONCAT') { + if (knex.clientType() === 'mysql2') { + // mysql2: CONCAT() returns NULL if any argument is NULL. + // adding IFNULL to convert NULL values to empty strings + return `IFNULL(${query}, '')`; + } else { + // do nothing + // pg / mssql: Concatenate all arguments. NULL arguments are ignored. + // sqlite3: special handling - See BinaryExpression + } + } + return query; + }) .join()})${colAlias}` ); } else if (pt.type === 'Literal') { @@ -637,13 +651,16 @@ export default async function formulaQueryBuilderv2( pt.left.fnName = pt.left.fnName || 'ARITH'; pt.right.fnName = pt.right.fnName || 'ARITH'; - const query = knex.raw( - `${fn(pt.left, null, pt.operator).toQuery()} ${pt.operator} ${fn( - pt.right, - null, - pt.operator - ).toQuery()}${colAlias}` - ); + const left = fn(pt.left, null, pt.operator).toQuery(); + const right = fn(pt.right, null, pt.operator).toQuery(); + let sql = `${left} ${pt.operator} ${right}${colAlias}`; + + // handle NULL values when calling CONCAT for sqlite3 + if (pt.left.fnName === 'CONCAT' && knex.clientType() === 'sqlite3') { + sql = `COALESCE(${left}, '') ${pt.operator} COALESCE(${right},'')${colAlias}`; + } + + const query = knex.raw(sql); if (prevBinaryOp && pt.operator !== prevBinaryOp) { query.wrap('(', ')'); } From ab32efbc9108c10904d7a82ac4b0139174d8c704 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Wed, 25 May 2022 16:44:40 +0800 Subject: [PATCH 2/9] fix: incorrect IDs when calling FormulaColumn.update --- packages/nocodb/src/lib/noco-models/Column.ts | 2 +- packages/nocodb/src/lib/noco/meta/api/columnApis.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nocodb/src/lib/noco-models/Column.ts b/packages/nocodb/src/lib/noco-models/Column.ts index 63d942599f..0592a79bc2 100644 --- a/packages/nocodb/src/lib/noco-models/Column.ts +++ b/packages/nocodb/src/lib/noco-models/Column.ts @@ -541,7 +541,7 @@ export default class Column implements ColumnType { title: col?.title }) ) - await FormulaColumn.update(formula.id, formula, ncMeta); + await FormulaColumn.update(formulaCol.id, formula, ncMeta); } } diff --git a/packages/nocodb/src/lib/noco/meta/api/columnApis.ts b/packages/nocodb/src/lib/noco/meta/api/columnApis.ts index 5c02957984..b00d90e6ee 100644 --- a/packages/nocodb/src/lib/noco/meta/api/columnApis.ts +++ b/packages/nocodb/src/lib/noco/meta/api/columnApis.ts @@ -649,7 +649,7 @@ export async function columnUpdate(req: Request, res: Response) { formula, [new_column] ); - await FormulaColumn.update(f.id, { + await FormulaColumn.update(c.id, { formula_raw: new_formula_raw }); } From b8c9f987c997c9504c3d38821ff9f5ff43d0e377 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Wed, 25 May 2022 19:09:40 +0800 Subject: [PATCH 3/9] chore: include ID & Foreign Key --- .../project/spreadsheet/helpers/uiTypes.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/nc-gui/components/project/spreadsheet/helpers/uiTypes.js b/packages/nc-gui/components/project/spreadsheet/helpers/uiTypes.js index 693b147017..11f85acfec 100644 --- a/packages/nc-gui/components/project/spreadsheet/helpers/uiTypes.js +++ b/packages/nc-gui/components/project/spreadsheet/helpers/uiTypes.js @@ -151,10 +151,20 @@ const uiTypes = [ ] const getUIDTIcon = (uidt) => { - return ([...uiTypes, { - name: 'CreateTime', - icon: 'mdi-calendar-clock' - }].find(t => t.name === uidt) || {}).icon + return ([...uiTypes, + { + name: 'CreateTime', + icon: 'mdi-calendar-clock' + }, + { + name: 'ID', + icon: 'mdi-identifier' + }, + { + name: 'ForeignKey', + icon: 'mdi-link-variant' + } + ].find(t => t.name === uidt) || {}).icon } export { From bdaf9eb5fe9fd399fa16710aebe4b5a9cae2f642 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Wed, 25 May 2022 19:10:37 +0800 Subject: [PATCH 4/9] enhancement: add icon next to each formula option --- .../components/editColumn/FormulaOptions.vue | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 c607a8ccbd..38664f4800 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue @@ -43,6 +43,9 @@ + + mdi-function + {{ it.text }} @@ -70,10 +73,12 @@ + + {{ it.icon }} + {{ it.text }} - Column @@ -87,6 +92,9 @@ + + mdi-calculator-variant + {{ it.text }} @@ -109,6 +117,7 @@ import debounce from 'debounce' import jsep from 'jsep' import { UITypes, jsepCurlyHook } from 'nocodb-sdk' +import { getUIDTIcon } from '../../helpers/uiTypes' import formulaList, { formulas, formulaTypes } from '../../../../../helpers/formulaList' import { getWordUntilCaret, insertAtCursor } from '@/helpers' import NcAutocompleteTree from '@/helpers/NcAutocompleteTree' @@ -145,7 +154,9 @@ export default { })), ...this.meta.columns.filter(c => !this.column || this.column.id !== c.id).map(c => ({ text: c.title, + uidt: c.uidt, type: 'column', + icon: getUIDTIcon(c.uidt), c })), ...this.availableBinOps.map(op => ({ From 5dc423a924f5afeff0efee8158f4273d20adcde0 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Wed, 25 May 2022 21:40:17 +0800 Subject: [PATCH 5/9] enhancement: show column list only when column is the only option --- .../components/editColumn/FormulaOptions.vue | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 38664f4800..95aaefbdd7 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue @@ -568,9 +568,15 @@ export default { this.selected = 0 this.suggestion = null const query = getWordUntilCaret(this.$refs.input.$el.querySelector('input')) - const parts = query.split(/\W+/) - this.wordToComplete = parts.pop() - this.suggestion = this.acTree.complete(this.wordToComplete)?.sort((x, y) => this.sortOrder[x.type] - this.sortOrder[y.type]) + // count number of opening curly brackets and closing curly brackets + const cntCurlyBrackets = (this.$refs.input.$el.querySelector('input').value.match(/\{|}/g) || []).reduce((acc, cur) => (acc[cur] = (acc[cur] || 0) + 1, acc), {}) + if (Math.abs((cntCurlyBrackets['{'] || 0) - (cntCurlyBrackets['}'] || 0))) { + this.suggestion = this.suggestionsList.filter(v => v.type === 'column') + } else { + const parts = query.split(/\W+/) + this.wordToComplete = parts.pop() + this.suggestion = this.acTree.complete(this.wordToComplete)?.sort((x, y) => this.sortOrder[x.type] - this.sortOrder[y.type]) + } this.autocomplete = !!this.suggestion.length }, selectText() { From ad26dda3eda349a616451ae7ee51c0a1220d9798 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Thu, 26 May 2022 12:10:29 +0800 Subject: [PATCH 6/9] fix: AC tree on columns after an opening curly bracket --- .../spreadsheet/components/editColumn/FormulaOptions.vue | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 95aaefbdd7..79b990a1a5 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue @@ -568,14 +568,13 @@ export default { this.selected = 0 this.suggestion = null const query = getWordUntilCaret(this.$refs.input.$el.querySelector('input')) + const parts = query.split(/\W+/) + this.wordToComplete = parts.pop() + this.suggestion = this.acTree.complete(this.wordToComplete)?.sort((x, y) => this.sortOrder[x.type] - this.sortOrder[y.type]) // count number of opening curly brackets and closing curly brackets const cntCurlyBrackets = (this.$refs.input.$el.querySelector('input').value.match(/\{|}/g) || []).reduce((acc, cur) => (acc[cur] = (acc[cur] || 0) + 1, acc), {}) if (Math.abs((cntCurlyBrackets['{'] || 0) - (cntCurlyBrackets['}'] || 0))) { - this.suggestion = this.suggestionsList.filter(v => v.type === 'column') - } else { - const parts = query.split(/\W+/) - this.wordToComplete = parts.pop() - this.suggestion = this.acTree.complete(this.wordToComplete)?.sort((x, y) => this.sortOrder[x.type] - this.sortOrder[y.type]) + this.suggestion = this.suggestion.filter(v => v.type === 'column') } this.autocomplete = !!this.suggestion.length }, From 11335c3a72735fcd6b85eed6efa632b400bf58fe Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Thu, 26 May 2022 12:31:14 +0800 Subject: [PATCH 7/9] fix: insertAtCursor position when calling appendText & refactor isCurlyBracketBalanced --- .../components/editColumn/FormulaOptions.vue | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 79b990a1a5..e0745fc036 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue @@ -545,13 +545,18 @@ export default { return 'N/A' } }, + isCurlyBracketBalanced() { + // count number of opening curly brackets and closing curly brackets + const cntCurlyBrackets = (this.$refs.input.$el.querySelector('input').value.match(/\{|}/g) || []).reduce((acc, cur) => (acc[cur] = (acc[cur] || 0) + 1, acc), {}) + return (cntCurlyBrackets['{'] || 0) === (cntCurlyBrackets['}'] || 0) + }, appendText(it) { const text = it.text const len = this.wordToComplete.length if (it.type === 'function') { this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len, 1)) } else if (it.type === 'column') { - this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), '{' + text + '}', len)) + this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), '{' + text + '}', len + (!this.isCurlyBracketBalanced()))) } else { this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len)) } @@ -571,9 +576,7 @@ export default { const parts = query.split(/\W+/) this.wordToComplete = parts.pop() this.suggestion = this.acTree.complete(this.wordToComplete)?.sort((x, y) => this.sortOrder[x.type] - this.sortOrder[y.type]) - // count number of opening curly brackets and closing curly brackets - const cntCurlyBrackets = (this.$refs.input.$el.querySelector('input').value.match(/\{|}/g) || []).reduce((acc, cur) => (acc[cur] = (acc[cur] || 0) + 1, acc), {}) - if (Math.abs((cntCurlyBrackets['{'] || 0) - (cntCurlyBrackets['}'] || 0))) { + if (this.isCurlyBracketBalanced()) { this.suggestion = this.suggestion.filter(v => v.type === 'column') } this.autocomplete = !!this.suggestion.length From bb78694040787c92519f7a5d9a555148c643b7b5 Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Thu, 26 May 2022 12:47:21 +0800 Subject: [PATCH 8/9] fix: only show column list for unbalanced curly brackets --- .../spreadsheet/components/editColumn/FormulaOptions.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e0745fc036..3e311f47c9 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue @@ -576,7 +576,7 @@ export default { const parts = query.split(/\W+/) this.wordToComplete = parts.pop() this.suggestion = this.acTree.complete(this.wordToComplete)?.sort((x, y) => this.sortOrder[x.type] - this.sortOrder[y.type]) - if (this.isCurlyBracketBalanced()) { + if (!this.isCurlyBracketBalanced()) { this.suggestion = this.suggestion.filter(v => v.type === 'column') } this.autocomplete = !!this.suggestion.length From 0bd70ce4fe4e105920d4371aa56ac9861b8907ec Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Thu, 26 May 2022 13:14:15 +0800 Subject: [PATCH 9/9] chore: remove uidt --- .../project/spreadsheet/components/editColumn/FormulaOptions.vue | 1 - 1 file changed, 1 deletion(-) 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 3e311f47c9..b6875a6174 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue @@ -154,7 +154,6 @@ export default { })), ...this.meta.columns.filter(c => !this.column || this.column.id !== c.id).map(c => ({ text: c.title, - uidt: c.uidt, type: 'column', icon: getUIDTIcon(c.uidt), c