Browse Source

fix(nocodb): corrections

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/5392/head
Pranav C 1 year ago
parent
commit
ec150b1a5a
  1. 116
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts
  2. 102
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/commonFns.ts
  3. 146
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts

116
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/formulav2/formulaQueryBuilderv2.ts

@ -628,36 +628,35 @@ async function _formulaQueryBuilder(
break; break;
} }
return knex.raw( return {
`${pt.callee.name}(${pt.arguments builder: knex.raw(
.map((arg) => { `${pt.callee.name}(${pt.arguments
const query = fn(arg).toQuery(); .map((arg) => {
if (pt.callee.name === 'CONCAT') { const query = fn(arg).toQuery();
if (knex.clientType() === 'mysql2') { if (pt.callee.name === 'CONCAT') {
// mysql2: CONCAT() returns NULL if any argument is NULL. if (knex.clientType() === 'mysql2') {
// adding IFNULL to convert NULL values to empty strings // mysql2: CONCAT() returns NULL if any argument is NULL.
return `IFNULL(${query}, '')`; // adding IFNULL to convert NULL values to empty strings
} else { return `IFNULL(${query}, '')`;
// do nothing } else {
// pg / mssql: Concatenate all arguments. NULL arguments are ignored. // do nothing
// sqlite3: special handling - See BinaryExpression // pg / mssql: Concatenate all arguments. NULL arguments are ignored.
// sqlite3: special handling - See BinaryExpression
}
} }
} return query;
return query; })
}) .join()})${colAlias}`.replace(/\?/g, '\\?')
.join()})${colAlias}`.replace(/\?/g, '\\?') ),
); };
} else if (pt.type === 'Literal') { } else if (pt.type === 'Literal') {
return knex.raw(`?${colAlias}`, [pt.value]); return { builder: knex.raw(`?${colAlias}`, [pt.value]) };
} else if (pt.type === 'Identifier') { } else if (pt.type === 'Identifier') {
const { builder } = await aliasToColumn?.[pt.name]() const { builder } = await aliasToColumn?.[pt.name]?.();
if (typeof builder === 'function') { if (typeof builder === 'function') {
return knex.raw( return { builder: knex.raw(`??${colAlias}`, await builder(pt.fnName)) };
`??${colAlias}`,
await builder(pt.fnName)
);
} }
return knex.raw(`??${colAlias}`, [aliasToColumn?.[pt.name] || pt.name]); return { builder: knex.raw(`??${colAlias}`, [builder || pt.name]) };
} else if (pt.type === 'BinaryExpression') { } else if (pt.type === 'BinaryExpression') {
if (pt.operator === '==') { if (pt.operator === '==') {
pt.operator = '='; pt.operator = '=';
@ -678,8 +677,8 @@ async function _formulaQueryBuilder(
pt.left.fnName = pt.left.fnName || 'ARITH'; pt.left.fnName = pt.left.fnName || 'ARITH';
pt.right.fnName = pt.right.fnName || 'ARITH'; pt.right.fnName = pt.right.fnName || 'ARITH';
const left = fn(pt.left, null, pt.operator).toQuery(); const left = (await fn(pt.left, null, pt.operator)).builder.toQuery();
const right = fn(pt.right, null, pt.operator).toQuery(); const right = (await fn(pt.right, null, pt.operator)).builder.toQuery();
let sql = `${left} ${pt.operator} ${right}${colAlias}`; let sql = `${left} ${pt.operator} ${right}${colAlias}`;
// comparing a date with empty string would throw // comparing a date with empty string would throw
@ -788,7 +787,9 @@ async function _formulaQueryBuilder(
return { builder: query }; return { builder: query };
} }
}; };
return { builder: fn(tree, alias) }; const builder = (await fn(tree, alias)).builder;
return { builder };
} }
function getTnPath(tb: Model, knex, tableAlias?: string) { function getTnPath(tb: Model, knex, tableAlias?: string) {
@ -877,3 +878,62 @@ export default async function formulaQueryBuilderv2(
} }
return qb; return qb;
} }
export async function validateFormula(
_tree,
alias,
knex: XKnex,
model: Model,
column?: Column,
aliasToColumn = {},
tableAlias?: string
) {
// register jsep curly hook once only
jsep.plugins.register(jsepCurlyHook);
// generate qb
const qb = await _formulaQueryBuilder(
_tree,
alias,
knex,
model,
aliasToColumn,
tableAlias
);
try {
// dry run qb.builder to see if it will break the grid view or not
// if so, set formula error and show empty selectQb instead
await knex(getTnPath(model, knex, tableAlias))
.select(qb.builder)
.as('dry-run-only');
// if column is provided, i.e. formula has been created
if (column) {
const formula = await column.getColOptions<FormulaColumn>();
// clean the previous formula error if the formula works this time
if (formula.error) {
await FormulaColumn.update(formula.id, {
error: null,
});
}
}
} catch (e) {
console.error(e);
if (column) {
const formula = await column.getColOptions<FormulaColumn>();
// add formula error to show in UI
await FormulaColumn.update(formula.id, {
error: e.message,
});
// update cache to reflect the error in UI
const key = `${CacheScope.COL_FORMULA}:${column.id}`;
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
if (o) {
o = { ...o, error: e.message };
// set cache
await NocoCache.set(key, o);
}
}
throw new Error(`Formula error: ${e.message}`);
}
}

102
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/commonFns.ts

@ -28,7 +28,11 @@ export default {
) )
.toQuery(); .toQuery();
} }
return args.knex.raw(`CASE ${switchVal} ${query}\n END${args.colAlias}`); return {
builder: args.knex.raw(
`CASE ${switchVal} ${query}\n END${args.colAlias}`
),
};
}, },
IF: async (args: MapFnArgs) => { IF: async (args: MapFnArgs) => {
let query = args.knex let query = args.knex
@ -42,62 +46,74 @@ export default {
.toQuery(); .toQuery();
if (args.pt.arguments[2]) { if (args.pt.arguments[2]) {
query += args.knex query += args.knex
.raw(`\n\tELSE ${(await args.fn(args.pt.arguments[2])).builder.toQuery()}`) .raw(
`\n\tELSE ${(await args.fn(args.pt.arguments[2])).builder.toQuery()}`
)
.toQuery(); .toQuery();
} }
return args.knex.raw(`CASE ${query}\n END${args.colAlias}`); return { builder: args.knex.raw(`CASE ${query}\n END${args.colAlias}`) };
}, },
TRUE: (_args) => 1, TRUE: 1,
FALSE: (_args) => 0, FALSE: 0,
AND: async (args: MapFnArgs) => { AND: async (args: MapFnArgs) => {
return args.knex.raw( return {
`${args.knex builder: args.knex.raw(
.raw( `${args.knex
`${( .raw(
await Promise.all( `${(
args.pt.arguments.map(async (ar) => await Promise.all(
(await args.fn(ar)).builder.toQuery() args.pt.arguments.map(async (ar) =>
(await args.fn(ar)).builder.toQuery()
)
) )
) ).join(' AND ')}`
).join(' AND ')}` )
) .wrap('(', ')')
.wrap('(', ')') .toQuery()}${args.colAlias}`
.toQuery()}${args.colAlias}` ),
); };
}, },
OR: async (args: MapFnArgs) => { OR: async (args: MapFnArgs) => {
return args.knex.raw( return {
`${args.knex builder: args.knex.raw(
.raw( `${args.knex
`${( .raw(
await Promise.all( `${(
args.pt.arguments.map(async (ar) => await Promise.all(
(await args.fn(ar)).builder.toQuery() args.pt.arguments.map(async (ar) =>
(await args.fn(ar)).builder.toQuery()
)
) )
) ).join(' OR ')}`
).join(' OR ')}` )
) .wrap('(', ')')
.wrap('(', ')') .toQuery()}${args.colAlias}`
.toQuery()}${args.colAlias}` ),
); };
}, },
AVG: (args: MapFnArgs) => { AVG: (args: MapFnArgs) => {
if (args.pt.arguments.length > 1) { if (args.pt.arguments.length > 1) {
return args.fn( return {
{ builder: args.fn(
type: 'BinaryExpression', {
operator: '/', type: 'BinaryExpression',
left: { ...args.pt, callee: { name: 'SUM' } }, operator: '/',
right: { type: 'Literal', value: args.pt.arguments.length }, left: { ...args.pt, callee: { name: 'SUM' } },
}, right: { type: 'Literal', value: args.pt.arguments.length },
args.a, },
args.prevBinaryOp args.a,
); args.prevBinaryOp
),
};
} else { } else {
return args.fn(args.pt.arguments[0], args.a, args.prevBinaryOp); return {
builder: args.fn(args.pt.arguments[0], args.a, args.prevBinaryOp),
};
} }
}, },
FLOAT: async (args: MapFnArgs) => { FLOAT: async (args: MapFnArgs) => {
return (await args.fn(args.pt?.arguments?.[0])).builder.wrap('(', ')'); return {
builder: (await args.fn(args.pt?.arguments?.[0])).builder.wrap('(', ')'),
};
}, },
}; };

146
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/functionMappings/pg.ts

@ -13,43 +13,53 @@ const pg = {
POWER: 'pow', POWER: 'pow',
SQRT: 'sqrt', SQRT: 'sqrt',
SEARCH: async (args: MapFnArgs) => { SEARCH: async (args: MapFnArgs) => {
return args.knex.raw( return {
`POSITION(${args.knex.raw( builder: args.knex.raw(
(await args.fn(args.pt.arguments[1])).builder.toQuery() `POSITION(${args.knex.raw(
)} in ${args.knex (await args.fn(args.pt.arguments[1])).builder.toQuery()
.raw((await args.fn(args.pt.arguments[0])).builder) )} in ${args.knex
.toQuery()})${args.colAlias}` .raw((await args.fn(args.pt.arguments[0])).builder)
); .toQuery()})${args.colAlias}`
),
};
}, },
INT(args: MapFnArgs) { INT(args: MapFnArgs) {
// todo: correction // todo: correction
return args.knex.raw( return {
`REGEXP_REPLACE(COALESCE(${args.fn( builder: args.knex.raw(
args.pt.arguments[0] `REGEXP_REPLACE(COALESCE(${args.fn(
)}::character varying, '0'), '[^0-9]+|\\.[0-9]+' ,'')${args.colAlias}` args.pt.arguments[0]
); )}::character varying, '0'), '[^0-9]+|\\.[0-9]+' ,'')${args.colAlias}`
),
};
}, },
MID: 'SUBSTR', MID: 'SUBSTR',
FLOAT: ({ fn, knex, pt, colAlias }: MapFnArgs) => { FLOAT: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
return knex return {
.raw(`CAST(${fn(pt.arguments[0])} as DOUBLE PRECISION)${colAlias}`) builder: knex
.wrap('(', ')'); .raw(`CAST(${fn(pt.arguments[0])} as DOUBLE PRECISION)${colAlias}`)
.wrap('(', ')'),
};
}, },
ROUND: ({ fn, knex, pt, colAlias }: MapFnArgs) => { ROUND: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
return knex.raw( return {
`ROUND((${fn(pt.arguments[0])})::numeric, ${ builder: knex.raw(
pt?.arguments[1] ? fn(pt.arguments[1]) : 0 `ROUND((${fn(pt.arguments[0])})::numeric, ${
}) ${colAlias}` pt?.arguments[1] ? fn(pt.arguments[1]) : 0
); }) ${colAlias}`
),
};
}, },
DATEADD: ({ fn, knex, pt, colAlias }: MapFnArgs) => { DATEADD: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
return knex.raw( return {
`${fn(pt.arguments[0])} + (${fn(pt.arguments[1])} || builder: knex.raw(
`${fn(pt.arguments[0])} + (${fn(pt.arguments[1])} ||
'${String(fn(pt.arguments[2])).replace( '${String(fn(pt.arguments[2])).replace(
/["']/g, /["']/g,
'' ''
)}')::interval${colAlias}` )}')::interval${colAlias}`
); ),
};
}, },
DATETIME_DIFF: async ({ fn, knex, pt, colAlias }: MapFnArgs) => { DATETIME_DIFF: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const datetime_expr1 = fn(pt.arguments[0]); const datetime_expr1 = fn(pt.arguments[0]);
@ -99,61 +109,75 @@ const pg = {
default: default:
sql = ''; sql = '';
} }
return knex.raw(`${sql} ${colAlias}`); return { builder: knex.raw(`${sql} ${colAlias}`) };
}, },
WEEKDAY: async ({ fn, knex, pt, colAlias }: MapFnArgs) => { WEEKDAY: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// isodow: the day of the week as Monday (1) to Sunday (7) // isodow: the day of the week as Monday (1) to Sunday (7)
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday // WEEKDAY() returns an index from 0 to 6 for Monday to Sunday
return knex.raw( return {
`(EXTRACT(ISODOW FROM ${ builder: knex.raw(
pt.arguments[0].type === 'Literal' `(EXTRACT(ISODOW FROM ${
? `date '${dayjs((await fn(pt.arguments[0])).builder).format( pt.arguments[0].type === 'Literal'
'YYYY-MM-DD' ? `date '${dayjs((await fn(pt.arguments[0])).builder).format(
)}'` 'YYYY-MM-DD'
: fn(pt.arguments[0]) )}'`
}) - 1 - ${getWeekdayByText( : fn(pt.arguments[0])
pt?.arguments[1]?.value }) - 1 - ${getWeekdayByText(
)} % 7 + 7) ::INTEGER % 7 ${colAlias}` pt?.arguments[1]?.value
); )} % 7 + 7) ::INTEGER % 7 ${colAlias}`
),
};
}, },
AND: async (args: MapFnArgs) => { AND: async (args: MapFnArgs) => {
return args.knex.raw( return {
`CASE WHEN ${args.knex builder: args.knex.raw(
.raw( `CASE WHEN ${args.knex
`${args.pt.arguments .raw(
.map(async (ar) => (await args.fn(ar, '', 'AND')).builder.toQuery()) `${args.pt.arguments
.join(' AND ')}` .map(async (ar) =>
) (await args.fn(ar, '', 'AND')).builder.toQuery()
.wrap('(', ')') )
.toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}` .join(' AND ')}`
); )
.wrap('(', ')')
.toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}`
),
};
}, },
OR: async (args: MapFnArgs) => { OR: async (args: MapFnArgs) => {
return args.knex.raw( return {
`CASE WHEN ${args.knex builder: args.knex.raw(
.raw( `CASE WHEN ${args.knex
`${args.pt.arguments .raw(
.map(async (ar) => (await args.fn(ar, '', 'OR')).builder.toQuery()) `${args.pt.arguments
.join(' OR ')}` .map(async (ar) =>
) (await args.fn(ar, '', 'OR')).builder.toQuery()
.wrap('(', ')') )
.toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}` .join(' OR ')}`
); )
.wrap('(', ')')
.toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}`
),
};
}, },
SUBSTR: ({ fn, knex, pt, colAlias }: MapFnArgs) => { SUBSTR: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const str = fn(pt.arguments[0]); const str = fn(pt.arguments[0]);
const positionFrom = fn(pt.arguments[1] ?? 1); const positionFrom = fn(pt.arguments[1] ?? 1);
const numberOfCharacters = fn(pt.arguments[2] ?? ''); const numberOfCharacters = fn(pt.arguments[2] ?? '');
return knex.raw( return {
`SUBSTR(${str}::TEXT, ${positionFrom}${ builder: knex.raw(
numberOfCharacters ? ', ' + numberOfCharacters : '' `SUBSTR(${str}::TEXT, ${positionFrom}${
})${colAlias}` numberOfCharacters ? ', ' + numberOfCharacters : ''
); })${colAlias}`
),
};
}, },
MOD: ({ fn, knex, pt, colAlias }: MapFnArgs) => { MOD: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const x = fn(pt.arguments[0]); const x = fn(pt.arguments[0]);
const y = fn(pt.arguments[1]); const y = fn(pt.arguments[1]);
return knex.raw(`MOD((${x})::NUMERIC, (${y})::NUMERIC) ${colAlias}`); return {
builder: knex.raw(`MOD((${x})::NUMERIC, (${y})::NUMERIC) ${colAlias}`),
};
}, },
}; };

Loading…
Cancel
Save