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..b6875a6174 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'
@@ -146,6 +155,7 @@ export default {
...this.meta.columns.filter(c => !this.column || this.column.id !== c.id).map(c => ({
text: c.title,
type: 'column',
+ icon: getUIDTIcon(c.uidt),
c
})),
...this.availableBinOps.map(op => ({
@@ -534,13 +544,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))
}
@@ -560,6 +575,9 @@ 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()) {
+ this.suggestion = this.suggestion.filter(v => v.type === 'column')
+ }
this.autocomplete = !!this.suggestion.length
},
selectText() {
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 {
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('(', ')');
}
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
});
}