diff --git a/packages/nc-gui/components/project/spreadsheet/components/ColumnFilter.vue b/packages/nc-gui/components/project/spreadsheet/components/ColumnFilter.vue index f5b4007fa1..32b8f26604 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/ColumnFilter.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/ColumnFilter.vue @@ -108,6 +108,7 @@ class="flex-shrink-1 flex-grow-0 caption nc-filter-operation-select" :items="filterComparisonOp(filter)" :placeholder="$t('labels.operation')" + v-show="filter && filter.fk_column_id" solo flat style="max-width: 120px" @@ -142,6 +143,7 @@ v-else :key="i + '_7'" v-model="filter.value" + v-show="filter && filter.fk_column_id" solo flat hide-details @@ -324,13 +326,23 @@ export default { if ( f && f.fk_column_id && - this.columnsById[f.fk_column_id] && - this.columnsById[f.fk_column_id].uidt === - UITypes.LinkToAnotherRecord && - this.columnsById[f.fk_column_id].uidt === UITypes.Lookup + this.columnsById[f.fk_column_id] ) { - return !['notempty', 'empty', 'notnull', 'null'].includes(op.value) - } + const uidt = this.columnsById[f.fk_column_id].uidt + if (uidt === UITypes.Lookup) { + // TODO: handle it later + return !['notempty', 'empty', 'notnull', 'null'].includes(op.value) + } else if (uidt === UITypes.LinkToAnotherRecord) { + const type = this.columnsById[f.fk_column_id].colOptions.type + if (type === 'hm' || type === 'mm') { + // exclude notnull & null + return !['notnull', 'null'].includes(op.value) + } else if (type === 'bt') { + // exclude notempty & empty + return !['notempty', 'empty'].includes(op.value) + } + } + } return true }) }, diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts index 67ad3ca1f7..cbd810af3f 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts @@ -50,11 +50,11 @@ const parseConditionV2 = async ( } if (Array.isArray(_filter)) { const qbs = await Promise.all( - _filter.map(child => parseConditionV2(child, knex, aliasCount)) + _filter.map((child) => parseConditionV2(child, knex, aliasCount)) ); - return qbP => { - qbP.where(qb => { + return (qbP) => { + qbP.where((qb) => { for (const [i, qb1] of Object.entries(qbs)) { qb[getLogicalOpMethod(_filter[i])](qb1); } @@ -64,11 +64,11 @@ const parseConditionV2 = async ( const children = await filter.getChildren(); const qbs = await Promise.all( - (children || []).map(child => parseConditionV2(child, knex, aliasCount)) + (children || []).map((child) => parseConditionV2(child, knex, aliasCount)) ); - return qbP => { - qbP[getLogicalOpMethod(filter)](qb => { + return (qbP) => { + qbP[getLogicalOpMethod(filter)]((qb) => { for (const [i, qb1] of Object.entries(qbs)) { qb[getLogicalOpMethod(children[i])](qb1); } @@ -78,7 +78,8 @@ const parseConditionV2 = async ( const column = await filter.getColumn(); if (!column) return () => {}; if (column.uidt === UITypes.LinkToAnotherRecord) { - const colOptions = (await column.getColOptions()) as LinkToAnotherRecordColumn; + const colOptions = + (await column.getColOptions()) as LinkToAnotherRecordColumn; const childColumn = await colOptions.getChildColumn(); const parentColumn = await colOptions.getParentColumn(); const childModel = await childColumn.getModel(); @@ -86,6 +87,28 @@ const parseConditionV2 = async ( const parentModel = await parentColumn.getModel(); await parentModel.getColumns(); if (colOptions.type === RelationTypes.HAS_MANY) { + if ( + filter.comparison_op === 'empty' || + filter.comparison_op === 'notempty' + ) { + const selectHmCount = knex(childModel.table_name) + .count(childColumn.column_name) + .where( + childColumn.column_name, + knex.raw('??.??', [ + alias || parentModel.table_name, + parentColumn.column_name, + ]) + ); + + return (qb) => { + if (filter.comparison_op === 'empty') { + qb.where(knex.raw('0'), selectHmCount); + } else { + qb.whereNot(knex.raw('0'), selectHmCount); + } + }; + } const selectQb = knex(childModel.table_name).select( childColumn.column_name ); @@ -97,7 +120,7 @@ const parseConditionV2 = async ( ? negatedMapping[filter.comparison_op] : {}), fk_model_id: childModel.id, - fk_column_id: childModel?.primaryValue?.id + fk_column_id: childModel?.primaryValue?.id, }), knex, aliasCount @@ -110,6 +133,17 @@ const parseConditionV2 = async ( else qbP.whereIn(parentColumn.column_name, selectQb); }; } else if (colOptions.type === RelationTypes.BELONGS_TO) { + if (filter.comparison_op === 'null') { + return (qb) => { + qb.whereNull(childColumn.column_name); + }; + } + if (filter.comparison_op === 'notnull') { + return (qb) => { + qb.whereNotNull(childColumn.column_name); + }; + } + const selectQb = knex(parentModel.table_name).select( parentColumn.column_name ); @@ -121,7 +155,7 @@ const parseConditionV2 = async ( ? negatedMapping[filter.comparison_op] : {}), fk_model_id: parentModel.id, - fk_column_id: parentModel?.primaryValue?.id + fk_column_id: parentModel?.primaryValue?.id, }), knex, aliasCount @@ -138,6 +172,29 @@ const parseConditionV2 = async ( const mmParentColumn = await colOptions.getMMParentColumn(); const mmChildColumn = await colOptions.getMMChildColumn(); + if ( + filter.comparison_op === 'empty' || + filter.comparison_op === 'notempty' + ) { + const selectMmCount = knex(mmModel.table_name) + .count(mmChildColumn.column_name) + .where( + mmChildColumn.column_name, + knex.raw('??.??', [ + alias || childModel.table_name, + childColumn.column_name, + ]) + ); + + return (qb) => { + if (filter.comparison_op === 'empty') { + qb.where(knex.raw('0'), selectMmCount); + } else { + qb.whereNot(knex.raw('0'), selectMmCount); + } + }; + } + const selectQb = knex(mmModel.table_name) .select(mmChildColumn.column_name) .join( @@ -153,7 +210,7 @@ const parseConditionV2 = async ( ? negatedMapping[filter.comparison_op] : {}), fk_model_id: parentModel.id, - fk_column_id: parentModel?.primaryValue?.id + fk_column_id: parentModel?.primaryValue?.id, }), knex, aliasCount @@ -167,7 +224,7 @@ const parseConditionV2 = async ( }; } - return _qb => {}; + return (_qb) => {}; } else if (column.uidt === UITypes.Lookup) { return await generateLookupCondition(column, filter, knex, aliasCount); } else if (column.uidt === UITypes.Rollup && !customWhereClause) { @@ -175,7 +232,7 @@ const parseConditionV2 = async ( await genRollupSelectv2({ knex, alias, - columnOptions: (await column.getColOptions()) as RollupColumn + columnOptions: (await column.getColOptions()) as RollupColumn, }) ).builder; return parseConditionV2( @@ -213,7 +270,7 @@ const parseConditionV2 = async ( ); let val = customWhereClause ? customWhereClause : filter.value; - return qb => { + return (qb) => { switch (filter.comparison_op) { case 'eq': qb = qb.where(field, val); @@ -321,7 +378,7 @@ const parseConditionV2 = async ( const negatedMapping = { nlike: { comparison_op: 'like' }, - neq: { comparison_op: 'eq' } + neq: { comparison_op: 'eq' }, }; function getAlias(aliasCount: { count: number }) { @@ -337,9 +394,8 @@ async function generateLookupCondition( ): Promise { const colOptions = await col.getColOptions(); const relationColumn = await colOptions.getRelationColumn(); - const relationColumnOptions = await relationColumn.getColOptions< - LinkToAnotherRecordColumn - >(); + const relationColumnOptions = + await relationColumn.getColOptions(); // const relationModel = await relationColumn.getModel(); const lookupColumn = await colOptions.getLookupColumn(); const alias = getAlias(aliasCount); @@ -362,7 +418,7 @@ async function generateLookupCondition( ...filter, ...(filter.comparison_op in negatedMapping ? negatedMapping[filter.comparison_op] - : {}) + : {}), }, lookupColumn, qb, @@ -385,7 +441,7 @@ async function generateLookupCondition( ...filter, ...(filter.comparison_op in negatedMapping ? negatedMapping[filter.comparison_op] - : {}) + : {}), }, lookupColumn, qb, @@ -419,7 +475,7 @@ async function generateLookupCondition( ...filter, ...(filter.comparison_op in negatedMapping ? negatedMapping[filter.comparison_op] - : {}) + : {}), }, lookupColumn, qb, @@ -455,9 +511,8 @@ async function nestedConditionJoin( await lookupColumn.getColOptions() ).getRelationColumn() : lookupColumn; - const relationColOptions = await relationColumn.getColOptions< - LinkToAnotherRecordColumn - >(); + const relationColOptions = + await relationColumn.getColOptions(); const relAlias = `__nc${aliasCount.count++}`; const childColumn = await relationColOptions.getChildColumn(); @@ -528,7 +583,7 @@ async function nestedConditionJoin( new Filter({ ...filter, fk_model_id: childModel.id, - fk_column_id: childModel.primaryValue?.id + fk_column_id: childModel.primaryValue?.id, }), knex, aliasCount, @@ -544,7 +599,7 @@ async function nestedConditionJoin( new Filter({ ...filter, fk_model_id: parentModel.id, - fk_column_id: parentModel?.primaryValue?.id + fk_column_id: parentModel?.primaryValue?.id, }), knex, aliasCount, @@ -560,7 +615,7 @@ async function nestedConditionJoin( new Filter({ ...filter, fk_model_id: parentModel.id, - fk_column_id: parentModel.primaryValue?.id + fk_column_id: parentModel.primaryValue?.id, }), knex, aliasCount, @@ -577,7 +632,7 @@ async function nestedConditionJoin( new Filter({ ...filter, fk_model_id: (await lookupColumn.getModel()).id, - fk_column_id: lookupColumn?.id + fk_column_id: lookupColumn?.id, }), knex, aliasCount, diff --git a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/sanitize.ts b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/sanitize.ts index a98be3b9e7..15b309fb63 100644 --- a/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/sanitize.ts +++ b/packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/sanitize.ts @@ -1,9 +1,11 @@ export function sanitize(v) { + if (typeof v !== 'string') return v; return v?.replace(/([^\\]|^)(\?+)/g, (_, m1, m2) => { return `${m1}${m2.split('?').join('\\?')}`; }); } export function unsanitize(v) { + if (typeof v !== 'string') return v; return v?.replace(/\\[?]/g, '?'); }