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;
}
return knex.raw(
`${pt.callee.name}(${pt.arguments
.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 {
builder: knex.raw(
`${pt.callee.name}(${pt.arguments
.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}`.replace(/\?/g, '\\?')
);
return query;
})
.join()})${colAlias}`.replace(/\?/g, '\\?')
),
};
} else if (pt.type === 'Literal') {
return knex.raw(`?${colAlias}`, [pt.value]);
return { builder: knex.raw(`?${colAlias}`, [pt.value]) };
} else if (pt.type === 'Identifier') {
const { builder } = await aliasToColumn?.[pt.name]()
const { builder } = await aliasToColumn?.[pt.name]?.();
if (typeof builder === 'function') {
return knex.raw(
`??${colAlias}`,
await builder(pt.fnName)
);
return { builder: knex.raw(`??${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') {
if (pt.operator === '==') {
pt.operator = '=';
@ -678,8 +677,8 @@ async function _formulaQueryBuilder(
pt.left.fnName = pt.left.fnName || 'ARITH';
pt.right.fnName = pt.right.fnName || 'ARITH';
const left = fn(pt.left, null, pt.operator).toQuery();
const right = fn(pt.right, null, pt.operator).toQuery();
const left = (await fn(pt.left, null, pt.operator)).builder.toQuery();
const right = (await fn(pt.right, null, pt.operator)).builder.toQuery();
let sql = `${left} ${pt.operator} ${right}${colAlias}`;
// comparing a date with empty string would throw
@ -788,7 +787,9 @@ async function _formulaQueryBuilder(
return { builder: query };
}
};
return { builder: fn(tree, alias) };
const builder = (await fn(tree, alias)).builder;
return { builder };
}
function getTnPath(tb: Model, knex, tableAlias?: string) {
@ -877,3 +878,62 @@ export default async function formulaQueryBuilderv2(
}
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();
}
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) => {
let query = args.knex
@ -42,62 +46,74 @@ export default {
.toQuery();
if (args.pt.arguments[2]) {
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();
}
return args.knex.raw(`CASE ${query}\n END${args.colAlias}`);
return { builder: args.knex.raw(`CASE ${query}\n END${args.colAlias}`) };
},
TRUE: (_args) => 1,
FALSE: (_args) => 0,
TRUE: 1,
FALSE: 0,
AND: async (args: MapFnArgs) => {
return args.knex.raw(
`${args.knex
.raw(
`${(
await Promise.all(
args.pt.arguments.map(async (ar) =>
(await args.fn(ar)).builder.toQuery()
return {
builder: args.knex.raw(
`${args.knex
.raw(
`${(
await Promise.all(
args.pt.arguments.map(async (ar) =>
(await args.fn(ar)).builder.toQuery()
)
)
)
).join(' AND ')}`
)
.wrap('(', ')')
.toQuery()}${args.colAlias}`
);
).join(' AND ')}`
)
.wrap('(', ')')
.toQuery()}${args.colAlias}`
),
};
},
OR: async (args: MapFnArgs) => {
return args.knex.raw(
`${args.knex
.raw(
`${(
await Promise.all(
args.pt.arguments.map(async (ar) =>
(await args.fn(ar)).builder.toQuery()
return {
builder: args.knex.raw(
`${args.knex
.raw(
`${(
await Promise.all(
args.pt.arguments.map(async (ar) =>
(await args.fn(ar)).builder.toQuery()
)
)
)
).join(' OR ')}`
)
.wrap('(', ')')
.toQuery()}${args.colAlias}`
);
).join(' OR ')}`
)
.wrap('(', ')')
.toQuery()}${args.colAlias}`
),
};
},
AVG: (args: MapFnArgs) => {
if (args.pt.arguments.length > 1) {
return args.fn(
{
type: 'BinaryExpression',
operator: '/',
left: { ...args.pt, callee: { name: 'SUM' } },
right: { type: 'Literal', value: args.pt.arguments.length },
},
args.a,
args.prevBinaryOp
);
return {
builder: args.fn(
{
type: 'BinaryExpression',
operator: '/',
left: { ...args.pt, callee: { name: 'SUM' } },
right: { type: 'Literal', value: args.pt.arguments.length },
},
args.a,
args.prevBinaryOp
),
};
} 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) => {
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',
SQRT: 'sqrt',
SEARCH: async (args: MapFnArgs) => {
return args.knex.raw(
`POSITION(${args.knex.raw(
(await args.fn(args.pt.arguments[1])).builder.toQuery()
)} in ${args.knex
.raw((await args.fn(args.pt.arguments[0])).builder)
.toQuery()})${args.colAlias}`
);
return {
builder: args.knex.raw(
`POSITION(${args.knex.raw(
(await args.fn(args.pt.arguments[1])).builder.toQuery()
)} in ${args.knex
.raw((await args.fn(args.pt.arguments[0])).builder)
.toQuery()})${args.colAlias}`
),
};
},
INT(args: MapFnArgs) {
// todo: correction
return args.knex.raw(
`REGEXP_REPLACE(COALESCE(${args.fn(
args.pt.arguments[0]
)}::character varying, '0'), '[^0-9]+|\\.[0-9]+' ,'')${args.colAlias}`
);
return {
builder: args.knex.raw(
`REGEXP_REPLACE(COALESCE(${args.fn(
args.pt.arguments[0]
)}::character varying, '0'), '[^0-9]+|\\.[0-9]+' ,'')${args.colAlias}`
),
};
},
MID: 'SUBSTR',
FLOAT: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
return knex
.raw(`CAST(${fn(pt.arguments[0])} as DOUBLE PRECISION)${colAlias}`)
.wrap('(', ')');
return {
builder: knex
.raw(`CAST(${fn(pt.arguments[0])} as DOUBLE PRECISION)${colAlias}`)
.wrap('(', ')'),
};
},
ROUND: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
return knex.raw(
`ROUND((${fn(pt.arguments[0])})::numeric, ${
pt?.arguments[1] ? fn(pt.arguments[1]) : 0
}) ${colAlias}`
);
return {
builder: knex.raw(
`ROUND((${fn(pt.arguments[0])})::numeric, ${
pt?.arguments[1] ? fn(pt.arguments[1]) : 0
}) ${colAlias}`
),
};
},
DATEADD: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
return knex.raw(
`${fn(pt.arguments[0])} + (${fn(pt.arguments[1])} ||
return {
builder: knex.raw(
`${fn(pt.arguments[0])} + (${fn(pt.arguments[1])} ||
'${String(fn(pt.arguments[2])).replace(
/["']/g,
''
)}')::interval${colAlias}`
);
),
};
},
DATETIME_DIFF: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const datetime_expr1 = fn(pt.arguments[0]);
@ -99,61 +109,75 @@ const pg = {
default:
sql = '';
}
return knex.raw(`${sql} ${colAlias}`);
return { builder: knex.raw(`${sql} ${colAlias}`) };
},
WEEKDAY: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
// isodow: the day of the week as Monday (1) to Sunday (7)
// WEEKDAY() returns an index from 0 to 6 for Monday to Sunday
return knex.raw(
`(EXTRACT(ISODOW FROM ${
pt.arguments[0].type === 'Literal'
? `date '${dayjs((await fn(pt.arguments[0])).builder).format(
'YYYY-MM-DD'
)}'`
: fn(pt.arguments[0])
}) - 1 - ${getWeekdayByText(
pt?.arguments[1]?.value
)} % 7 + 7) ::INTEGER % 7 ${colAlias}`
);
return {
builder: knex.raw(
`(EXTRACT(ISODOW FROM ${
pt.arguments[0].type === 'Literal'
? `date '${dayjs((await fn(pt.arguments[0])).builder).format(
'YYYY-MM-DD'
)}'`
: fn(pt.arguments[0])
}) - 1 - ${getWeekdayByText(
pt?.arguments[1]?.value
)} % 7 + 7) ::INTEGER % 7 ${colAlias}`
),
};
},
AND: async (args: MapFnArgs) => {
return args.knex.raw(
`CASE WHEN ${args.knex
.raw(
`${args.pt.arguments
.map(async (ar) => (await args.fn(ar, '', 'AND')).builder.toQuery())
.join(' AND ')}`
)
.wrap('(', ')')
.toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}`
);
return {
builder: args.knex.raw(
`CASE WHEN ${args.knex
.raw(
`${args.pt.arguments
.map(async (ar) =>
(await args.fn(ar, '', 'AND')).builder.toQuery()
)
.join(' AND ')}`
)
.wrap('(', ')')
.toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}`
),
};
},
OR: async (args: MapFnArgs) => {
return args.knex.raw(
`CASE WHEN ${args.knex
.raw(
`${args.pt.arguments
.map(async (ar) => (await args.fn(ar, '', 'OR')).builder.toQuery())
.join(' OR ')}`
)
.wrap('(', ')')
.toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}`
);
return {
builder: args.knex.raw(
`CASE WHEN ${args.knex
.raw(
`${args.pt.arguments
.map(async (ar) =>
(await args.fn(ar, '', 'OR')).builder.toQuery()
)
.join(' OR ')}`
)
.wrap('(', ')')
.toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}`
),
};
},
SUBSTR: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const str = fn(pt.arguments[0]);
const positionFrom = fn(pt.arguments[1] ?? 1);
const numberOfCharacters = fn(pt.arguments[2] ?? '');
return knex.raw(
`SUBSTR(${str}::TEXT, ${positionFrom}${
numberOfCharacters ? ', ' + numberOfCharacters : ''
})${colAlias}`
);
return {
builder: knex.raw(
`SUBSTR(${str}::TEXT, ${positionFrom}${
numberOfCharacters ? ', ' + numberOfCharacters : ''
})${colAlias}`
),
};
},
MOD: ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const x = fn(pt.arguments[0]);
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