Browse Source

Merge pull request #7299 from nocodb/nc-fix/formula-bugs

Nc fix/formula bugs
pull/7300/head
Raju Udava 1 year ago committed by GitHub
parent
commit
2c53c48275
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      packages/nc-gui/components/virtual-cell/Formula.vue
  2. 37
      packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts
  3. 80
      packages/nocodb/src/db/functionMappings/commonFns.ts
  4. 35
      packages/nocodb/src/helpers/formulaHelpers.ts
  5. 24
      packages/nocodb/src/models/Column.ts
  6. 4
      packages/nocodb/src/models/FormulaColumn.ts

6
packages/nc-gui/components/virtual-cell/Formula.vue

@ -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" />

37
packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts

@ -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

80
packages/nocodb/src/db/functionMappings/commonFns.ts

@ -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}`),
};
},
};

35
packages/nocodb/src/helpers/formulaHelpers.ts

@ -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([]));
}

24
packages/nocodb/src/models/Column.ts

@ -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(

4
packages/nocodb/src/models/FormulaColumn.ts

@ -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);
}

Loading…
Cancel
Save