mirror of
https://github.com/nocodb/nocodb.git
synced 2026-04-25 04:35:09 +00:00
Merge pull request #7299 from nocodb/nc-fix/formula-bugs
Nc fix/formula bugs
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { handleTZ } from 'nocodb-sdk'
|
||||
import type { ColumnType } from 'nocodb-sdk'
|
||||
import type { Ref } from 'vue'
|
||||
import { CellValueInj, ColumnInj, computed, inject, renderValue, replaceUrlsWithLink, useBase } from '#imports'
|
||||
import { CellValueInj, ColumnInj, computed, inject, renderValue, replaceUrlsWithLink, useBase, useGlobal } from '#imports'
|
||||
|
||||
// todo: column type doesn't have required property `error` - throws in typecheck
|
||||
const column = inject(ColumnInj) as Ref<ColumnType & { colOptions: { error: any } }>
|
||||
@@ -11,6 +11,8 @@ const cellValue = inject(CellValueInj)
|
||||
|
||||
const { isPg } = useBase()
|
||||
|
||||
const { showNull } = useGlobal()
|
||||
|
||||
const result = computed(() =>
|
||||
isPg(column.value.source_id) ? renderValue(handleTZ(cellValue?.value)) : renderValue(cellValue?.value),
|
||||
)
|
||||
@@ -30,6 +32,8 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
|
||||
<span>ERR!</span>
|
||||
</a-tooltip>
|
||||
|
||||
<span v-else-if="cellValue === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
|
||||
|
||||
<div v-else class="py-2" @dblclick="activateShowEditNonEditableFieldWarning">
|
||||
<div v-if="urls" v-html="urls" />
|
||||
|
||||
|
||||
@@ -873,6 +873,43 @@ async function _formulaQueryBuilder(
|
||||
);
|
||||
}
|
||||
|
||||
// if operator is == or !=, then handle comparison with BLANK which should accept NULL and empty string
|
||||
if (pt.operator === '==' || pt.operator === '!=') {
|
||||
if (pt.left.callee?.name !== pt.right.callee?.name) {
|
||||
// if left/right is BLANK, accept both NULL and empty string
|
||||
for (const operand of ['left', 'right']) {
|
||||
if (
|
||||
pt[operand].type === 'CallExpression' &&
|
||||
pt[operand].callee.name === 'BLANK'
|
||||
) {
|
||||
const isString =
|
||||
pt[operand === 'left' ? 'right' : 'left'].dataType ===
|
||||
FormulaDataTypes.STRING;
|
||||
let calleeName;
|
||||
|
||||
if (pt.operator === '==') {
|
||||
calleeName = isString ? 'ISBLANK' : 'ISNULL';
|
||||
} else {
|
||||
calleeName = isString ? 'ISNOTBLANK' : 'ISNOTNULL';
|
||||
}
|
||||
|
||||
return fn(
|
||||
{
|
||||
type: 'CallExpression',
|
||||
arguments: [operand === 'left' ? pt.right : pt.left],
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
name: calleeName,
|
||||
},
|
||||
},
|
||||
alias,
|
||||
prevBinaryOp,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pt.operator === '==') {
|
||||
pt.operator = '=';
|
||||
// if left/right is of different type, convert to string and compare
|
||||
|
||||
@@ -33,7 +33,6 @@ async function treatArgAsConditionalExp(
|
||||
}
|
||||
|
||||
export default {
|
||||
// todo: handle default case
|
||||
SWITCH: async (args: MapFnArgs) => {
|
||||
const count = Math.floor((args.pt.arguments.length - 1) / 2);
|
||||
let query = '';
|
||||
@@ -55,6 +54,9 @@ export default {
|
||||
|
||||
const switchVal = (await args.fn(args.pt.arguments[0])).builder.toQuery();
|
||||
|
||||
// used it for null value check
|
||||
let elseValPrefix = '';
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let val;
|
||||
// cast to string if the return value types are different
|
||||
@@ -73,13 +75,34 @@ export default {
|
||||
val = (await args.fn(args.pt.arguments[i * 2 + 2])).builder.toQuery();
|
||||
}
|
||||
|
||||
query += args.knex
|
||||
.raw(
|
||||
`\n\tWHEN ${(
|
||||
await args.fn(args.pt.arguments[i * 2 + 1])
|
||||
).builder.toQuery()} THEN ${val}`,
|
||||
)
|
||||
.toQuery();
|
||||
if (
|
||||
args.pt.arguments[i * 2 + 1].type === 'CallExpression' &&
|
||||
args.pt.arguments[i * 2 + 1].callee?.name === 'BLANK'
|
||||
) {
|
||||
elseValPrefix += args.knex
|
||||
.raw(
|
||||
`\n\tWHEN ${switchVal} IS NULL ${
|
||||
args.pt.arguments[i * 2 + 1].dataType === FormulaDataTypes.STRING
|
||||
? `OR ${switchVal} = ''`
|
||||
: ''
|
||||
} THEN ${val}`,
|
||||
)
|
||||
.toQuery();
|
||||
} else if (
|
||||
args.pt.arguments[i * 2 + 1].dataType === FormulaDataTypes.NULL
|
||||
) {
|
||||
elseValPrefix += args.knex
|
||||
.raw(`\n\tWHEN ${switchVal} IS NULL THEN ${val}`)
|
||||
.toQuery();
|
||||
} else {
|
||||
query += args.knex
|
||||
.raw(
|
||||
`\n\tWHEN ${(
|
||||
await args.fn(args.pt.arguments[i * 2 + 1])
|
||||
).builder.toQuery()} THEN ${val}`,
|
||||
)
|
||||
.toQuery();
|
||||
}
|
||||
}
|
||||
if (args.pt.arguments.length % 2 === 0) {
|
||||
let val;
|
||||
@@ -100,8 +123,13 @@ export default {
|
||||
await args.fn(args.pt.arguments[args.pt.arguments.length - 1])
|
||||
).builder.toQuery();
|
||||
}
|
||||
|
||||
query += `\n\tELSE ${val}`;
|
||||
if (elseValPrefix) {
|
||||
query += `\n\tELSE (CASE ${elseValPrefix} ELSE ${val} END)`;
|
||||
} else {
|
||||
query += `\n\tELSE ${val}`;
|
||||
}
|
||||
} else if (elseValPrefix) {
|
||||
query += `\n\tELSE (CASE ${elseValPrefix} END)`;
|
||||
}
|
||||
return {
|
||||
builder: args.knex.raw(
|
||||
@@ -321,4 +349,36 @@ export default {
|
||||
),
|
||||
};
|
||||
},
|
||||
ISBLANK: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
|
||||
const { builder: valueBuilder } = await fn(pt.arguments[0]);
|
||||
|
||||
return {
|
||||
builder: knex.raw(
|
||||
`(${valueBuilder} IS NULL OR ${valueBuilder} = '')${colAlias}`,
|
||||
),
|
||||
};
|
||||
},
|
||||
ISNULL: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
|
||||
const { builder: valueBuilder } = await fn(pt.arguments[0]);
|
||||
|
||||
return {
|
||||
builder: knex.raw(`(${valueBuilder} IS NULL)${colAlias}`),
|
||||
};
|
||||
},
|
||||
ISNOTBLANK: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
|
||||
const { builder: valueBuilder } = await fn(pt.arguments[0]);
|
||||
|
||||
return {
|
||||
builder: knex.raw(
|
||||
`(${valueBuilder} IS NOT NULL AND ${valueBuilder} != '')${colAlias}`,
|
||||
),
|
||||
};
|
||||
},
|
||||
ISNOTNULL: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
|
||||
const { builder: valueBuilder } = await fn(pt.arguments[0]);
|
||||
|
||||
return {
|
||||
builder: knex.raw(`(${valueBuilder} IS NOT NULL)${colAlias}`),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import jsep from 'jsep';
|
||||
import { UITypes } from 'nocodb-sdk';
|
||||
import type FormulaColumn from '../models/FormulaColumn';
|
||||
import type { Column } from '~/models';
|
||||
|
||||
export async function getFormulasReferredTheColumn({
|
||||
column,
|
||||
columns,
|
||||
}: {
|
||||
column: Column;
|
||||
columns: Column[];
|
||||
}): Promise<Column[]> {
|
||||
const fn = (pt) => {
|
||||
if (pt.type === 'CallExpression') {
|
||||
return pt.arguments.some((arg) => fn(arg));
|
||||
} else if (pt.type === 'Literal') {
|
||||
} else if (pt.type === 'Identifier') {
|
||||
return [column.id, column.title].includes(pt.name);
|
||||
} else if (pt.type === 'BinaryExpression') {
|
||||
return fn(pt.left) || fn(pt.right);
|
||||
}
|
||||
};
|
||||
|
||||
return columns.reduce(async (columnsPromise, c) => {
|
||||
const columns = await columnsPromise;
|
||||
if (c.uidt !== UITypes.Formula) return columns;
|
||||
|
||||
const formula = await c.getColOptions<FormulaColumn>();
|
||||
|
||||
if (fn(jsep(formula.formula))) {
|
||||
columns.push(c);
|
||||
}
|
||||
return columns;
|
||||
}, Promise.resolve([]));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isLinksOrLTAR,
|
||||
UITypes,
|
||||
} from 'nocodb-sdk';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import type { ColumnReqType, ColumnType } from 'nocodb-sdk';
|
||||
import FormulaColumn from '~/models/FormulaColumn';
|
||||
import LinkToAnotherRecordColumn from '~/models/LinkToAnotherRecordColumn';
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
} from '~/utils/globals';
|
||||
import NocoCache from '~/cache/NocoCache';
|
||||
import { parseMetaProp, stringifyMetaProp } from '~/utils/modelUtils';
|
||||
import { getFormulasReferredTheColumn } from '~/helpers/formulaHelpers';
|
||||
|
||||
const selectColors = [
|
||||
'#cfdffe',
|
||||
@@ -42,6 +44,8 @@ const selectColors = [
|
||||
'#eeeeee',
|
||||
];
|
||||
|
||||
const logger = new Logger('Column');
|
||||
|
||||
export default class Column<T = any> implements ColumnType {
|
||||
public fk_model_id: string;
|
||||
public base_id: string;
|
||||
@@ -1159,6 +1163,26 @@ export default class Column<T = any> implements ColumnType {
|
||||
|
||||
// on column update, delete any optimised single query cache
|
||||
await NocoCache.delAll(CacheScope.SINGLE_QUERY, `${oldCol.fk_model_id}:*`);
|
||||
|
||||
const updatedColumn = await Column.get({ colId });
|
||||
|
||||
// invalidate formula parsed-tree in which current column is used
|
||||
// whenever a new request comes for that formula, it will be populated again
|
||||
getFormulasReferredTheColumn({
|
||||
column: updatedColumn,
|
||||
columns: await Column.list({ fk_model_id: column.fk_model_id }),
|
||||
})
|
||||
.then(async (formulas) => {
|
||||
for (const formula of formulas) {
|
||||
await FormulaColumn.update(formula.id, {
|
||||
parsed_tree: null,
|
||||
});
|
||||
}
|
||||
})
|
||||
// ignore the error and continue, if formula is no longer valid it will be captured in the next run
|
||||
.catch((err) => {
|
||||
logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
static async updateAlias(
|
||||
|
||||
@@ -74,8 +74,6 @@ export default class FormulaColumn {
|
||||
'parsed_tree',
|
||||
]);
|
||||
|
||||
updateObj.parsed_tree = stringifyMetaProp(updateObj, 'parsed_tree');
|
||||
|
||||
// get existing cache
|
||||
const key = `${CacheScope.COL_FORMULA}:${id}`;
|
||||
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
|
||||
@@ -84,6 +82,8 @@ export default class FormulaColumn {
|
||||
// set cache
|
||||
await NocoCache.set(key, o);
|
||||
}
|
||||
if ('parsed_tree' in updateObj)
|
||||
updateObj.parsed_tree = stringifyMetaProp(updateObj, 'parsed_tree');
|
||||
// set meta
|
||||
await ncMeta.metaUpdate(null, null, MetaTable.COL_FORMULA, updateObj, id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user