diff --git a/packages/nc-gui/components/smartsheet/grid/GroupBy.vue b/packages/nc-gui/components/smartsheet/grid/GroupBy.vue
index 67919f0934..a0ae323584 100644
--- a/packages/nc-gui/components/smartsheet/grid/GroupBy.vue
+++ b/packages/nc-gui/components/smartsheet/grid/GroupBy.vue
@@ -1,8 +1,10 @@
@@ -227,6 +250,15 @@ const onScroll = (e: Event) => {
+
+
+
+ No mapped value
+
+
{
'font-weight': 500,
}"
>
- {{ grp.key in GROUP_BY_VARS.VAR_TITLES ? GROUP_BY_VARS.VAR_TITLES[grp.key] : grp.key }}
+ {{
+ GROUP_BY_VARS.VAR_TITLES[grp.key]
+ }}
+
+ {{ parseKey(grp)?.join(', ') }}
+
diff --git a/packages/nc-gui/components/smartsheet/grid/GroupByLabel.vue b/packages/nc-gui/components/smartsheet/grid/GroupByLabel.vue
new file mode 100644
index 0000000000..2e9638e344
--- /dev/null
+++ b/packages/nc-gui/components/smartsheet/grid/GroupByLabel.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue b/packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
index 3febd97e8e..94b17c9702 100644
--- a/packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
+++ b/packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
@@ -15,20 +15,10 @@ import {
useNuxtApp,
useSmartsheetStoreOrThrow,
useViewColumnsOrThrow,
+ watch,
} from '#imports'
-const groupingUidt = [
- UITypes.SingleSelect,
- UITypes.MultiSelect,
- UITypes.Checkbox,
- UITypes.Date,
- UITypes.SingleLineText,
- UITypes.Number,
- UITypes.Rollup,
- UITypes.Lookup,
- UITypes.Links,
- UITypes.Formula,
-]
+const excludedGroupingUidt = [UITypes.Attachment]
const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
@@ -62,16 +52,16 @@ const groupedByColumnIds = computed(() => groupBy.value.map((g) => g.fk_column_i
const { eventBus } = useSmartsheetStoreOrThrow()
const { isMobileMode } = useGlobal()
-const btLookups = ref([])
+const supportedLookups = ref([])
const fieldsToGroupBy = computed(() => {
const fields = meta.value?.columns || []
return fields.filter((field) => {
- if (!groupingUidt.includes(field.uidt as UITypes)) return false
+ if (excludedGroupingUidt.includes(field.uidt as UITypes)) return false
if (field.uidt === UITypes.Lookup) {
- return btLookups.value.includes(field.id)
+ return supportedLookups.value.includes(field.id)
}
return true
@@ -161,25 +151,18 @@ watch(open, () => {
}
})
-const loadBtLookups = async () => {
+const loadAllowedLookups = async () => {
const filteredLookupCols = []
try {
for (const col of meta.value?.columns || []) {
if (col.uidt !== UITypes.Lookup) continue
let nextCol = col
- let btLookup = true
-
- // check all the relation of nested lookup columns is bt or not
- // include the column only if all only if all relations are bt
- while (btLookup && nextCol && nextCol.uidt === UITypes.Lookup) {
+ // check the lookup column is supported type or not
+ while (nextCol && nextCol.uidt === UITypes.Lookup) {
const lookupRelation = (await getMeta(nextCol.fk_model_id))?.columns?.find(
(c) => c.id === (nextCol.colOptions as LookupType).fk_relation_column_id,
)
- if ((lookupRelation.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO) {
- btLookup = false
- continue
- }
const relatedTableMeta = await getMeta((lookupRelation.colOptions as LinkToAnotherRecordType).fk_related_model_id)
@@ -190,22 +173,25 @@ const loadBtLookups = async () => {
// if next column is same as root lookup column then break the loop
// since it's going to be a circular loop, and ignore the column
if (nextCol.id === col.id) {
- btLookup = false
break
}
}
- if (btLookup) filteredLookupCols.push(col.id)
+ if (nextCol.uidt !== UITypes.Attachment) filteredLookupCols.push(col.id)
}
- btLookups.value = filteredLookupCols
+ supportedLookups.value = filteredLookupCols
} catch (e) {
console.error(e)
}
}
onMounted(async () => {
- await loadBtLookups()
+ await loadAllowedLookups()
+})
+
+watch(meta, async () => {
+ await loadAllowedLookups()
})
@@ -242,9 +228,7 @@ onMounted(async () => {
, where?: Computed
if (col.uidt === UITypes.Checkbox) {
return value ? GROUP_BY_VARS.TRUE : GROUP_BY_VARS.FALSE
}
+
+ // convert to JSON string if non-string value
+ if (value && typeof value === 'object') {
+ value = JSON.stringify(value)
+ }
+
return value ?? GROUP_BY_VARS.NULL
}
@@ -144,13 +150,13 @@ export const useViewGroupBy = (view: Ref, where?: Computed
const calculateNestedWhere = (nestedIn: GroupNestedIn[], existing = '') => {
return nestedIn.reduce((acc, curr) => {
if (curr.key === GROUP_BY_VARS.NULL) {
- acc += `${acc.length ? '~and' : ''}(${curr.title},blank)`
+ acc += `${acc.length ? '~and' : ''}(${curr.title},gb_null)`
} else if (curr.column_uidt === UITypes.Checkbox) {
acc += `${acc.length ? '~and' : ''}(${curr.title},${curr.key === GROUP_BY_VARS.TRUE ? 'checked' : 'notchecked'})`
} else if ([UITypes.Date, UITypes.DateTime].includes(curr.column_uidt as UITypes)) {
acc += `${acc.length ? '~and' : ''}(${curr.title},eq,exactDate,${curr.key})`
} else {
- acc += `${acc.length ? '~and' : ''}(${curr.title},eq,${curr.key})`
+ acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${curr.key})`
}
return acc
}, existing)
diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts
index 9302691854..35571e1cfa 100644
--- a/packages/nocodb/src/db/BaseModelSqlv2.ts
+++ b/packages/nocodb/src/db/BaseModelSqlv2.ts
@@ -35,7 +35,6 @@ import type {
SelectOption,
} from '~/models';
import type { SortType } from 'nocodb-sdk';
-import generateBTLookupSelectQuery from '~/db/generateBTLookupSelectQuery';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2';
import conditionV2 from '~/db/conditionV2';
@@ -60,10 +59,13 @@ import { HANDLE_WEBHOOK } from '~/services/hook-handler.service';
import {
COMPARISON_OPS,
COMPARISON_SUB_OPS,
+ GROUPBY_COMPARISON_OPS,
IS_WITHIN_COMPARISON_SUB_OPS,
} from '~/utils/globals';
import { extractProps } from '~/helpers/extractProps';
import { defaultLimitConfig } from '~/helpers/extractLimitAndOffset';
+import generateLookupSelectQuery from '~/db/generateLookupSelectQuery';
+import { getAliasGenerator } from '~/utils';
dayjs.extend(utc);
@@ -386,6 +388,7 @@ class BaseModelSqlv2 {
validateFormula: true,
});
}
+
return data?.map((d) => {
d.__proto__ = proto;
return d;
@@ -549,18 +552,32 @@ class BaseModelSqlv2 {
const selectors = [];
const groupBySelectors = [];
+ const getAlias = getAliasGenerator('__nc_gb');
await Promise.all(
args.column_name.split(',').map(async (col) => {
- const column = cols.find(
- (c) => c.column_name === col || c.title === col,
- );
- groupByColumns[column.id] = column;
+ let column = cols.find((c) => c.column_name === col || c.title === col);
if (!column) {
throw NcError.notFound('Column not found');
}
+ // if qrCode or Barcode replace it with value column nd keep the alias
+ if ([UITypes.QrCode, UITypes.Barcode].includes(column.uidt))
+ column = new Column({
+ ...(await column
+ .getColOptions()
+ .then((col) => col.getValueColumn())),
+ title: column.title,
+ });
+
+ groupByColumns[column.id] = column;
+
switch (column.uidt) {
+ case UITypes.Attachment:
+ NcError.badRequest(
+ 'Group by using attachment column is not supported',
+ );
+ break;
case UITypes.Links:
case UITypes.Rollup:
selectors.push(
@@ -599,12 +616,14 @@ class BaseModelSqlv2 {
}
break;
case UITypes.Lookup:
+ case UITypes.LinkToAnotherRecord:
{
- const _selectQb = await generateBTLookupSelectQuery({
+ const _selectQb = await generateLookupSelectQuery({
baseModelSqlv2: this,
column,
alias: null,
model: this.model,
+ getAlias,
});
const selectQb = this.dbDriver.raw(`?? as ??`, [
@@ -695,6 +714,7 @@ class BaseModelSqlv2 {
qb.groupBy(...groupBySelectors);
applyPaginate(qb, rest);
+
return await this.execAndParse(qb);
}
@@ -711,18 +731,34 @@ class BaseModelSqlv2 {
const selectors = [];
const groupBySelectors = [];
+ const getAlias = getAliasGenerator('__nc_gb');
+ // todo: refactor and avoid duplicate code
await this.model.getColumns().then((cols) =>
Promise.all(
args.column_name.split(',').map(async (col) => {
- const column = cols.find(
+ let column = cols.find(
(c) => c.column_name === col || c.title === col,
);
if (!column) {
throw NcError.notFound('Column not found');
}
+ // if qrCode or Barcode replace it with value column nd keep the alias
+ if ([UITypes.QrCode, UITypes.Barcode].includes(column.uidt))
+ column = new Column({
+ ...(await column
+ .getColOptions()
+ .then((col) => col.getValueColumn())),
+ title: column.title,
+ });
+
switch (column.uidt) {
+ case UITypes.Attachment:
+ NcError.badRequest(
+ 'Group by using attachment column is not supported',
+ );
+ break;
case UITypes.Rollup:
case UITypes.Links:
selectors.push(
@@ -764,12 +800,14 @@ class BaseModelSqlv2 {
break;
}
case UITypes.Lookup:
+ case UITypes.LinkToAnotherRecord:
{
- const _selectQb = await generateBTLookupSelectQuery({
+ const _selectQb = await generateLookupSelectQuery({
baseModelSqlv2: this,
column,
alias: null,
model: this.model,
+ getAlias,
});
const selectQb = this.dbDriver.raw(`?? as ??`, [
@@ -5247,7 +5285,7 @@ export function extractFilterFromXwhere(
// mark `op` and `sub_op` any for being assignable to parameter of type
function validateFilterComparison(uidt: UITypes, op: any, sub_op?: any) {
- if (!COMPARISON_OPS.includes(op)) {
+ if (!COMPARISON_OPS.includes(op) && !GROUPBY_COMPARISON_OPS.includes(op)) {
NcError.badRequest(`${op} is not supported.`);
}
diff --git a/packages/nocodb/src/db/conditionV2.ts b/packages/nocodb/src/db/conditionV2.ts
index 4495be7cef..2cee6d72c6 100644
--- a/packages/nocodb/src/db/conditionV2.ts
+++ b/packages/nocodb/src/db/conditionV2.ts
@@ -8,11 +8,14 @@ import type Column from '~/models/Column';
import type LookupColumn from '~/models/LookupColumn';
import type RollupColumn from '~/models/RollupColumn';
import type FormulaColumn from '~/models/FormulaColumn';
+import type { BarcodeColumn, QrCodeColumn } from '~/models';
import { NcError } from '~/helpers/catchError';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2';
import { sanitize } from '~/helpers/sqlSanitize';
import Filter from '~/models/Filter';
+import generateLookupSelectQuery from '~/db/generateLookupSelectQuery';
+import { getAliasGenerator } from '~/utils';
// tod: tobe fixed
// extend(customParseFormat);
@@ -112,6 +115,43 @@ const parseConditionV2 = async (
});
};
} else {
+ // handle group by filter separately,
+ // `gb_eq` is equivalent to `eq` but for lookup it compares on aggregated value returns in group by api
+ // aggregated value will be either json array or `___` separated string
+ // `gb_null` is equivalent to `blank` but for lookup it compares on aggregated value is null
+ if (
+ (filter.comparison_op as any) === 'gb_eq' ||
+ (filter.comparison_op as any) === 'gb_null'
+ ) {
+ const column = await filter.getColumn();
+ if (
+ column.uidt === UITypes.Lookup ||
+ column.uidt === UITypes.LinkToAnotherRecord
+ ) {
+ const model = await column.getModel();
+ const lkQb = await generateLookupSelectQuery({
+ baseModelSqlv2,
+ alias: alias,
+ model,
+ column,
+ getAlias: getAliasGenerator('__gb_filter_lk'),
+ });
+ return (qb) => {
+ if ((filter.comparison_op as any) === 'gb_eq')
+ qb.where(knex.raw('?', [filter.value]), lkQb.builder);
+ else qb.whereNull(knex.raw(lkQb.builder).wrap('(', ')'));
+ };
+ } else {
+ filter.comparison_op =
+ (filter.comparison_op as any) === 'gb_eq' ? 'eq' : 'blank';
+ // if qrCode or Barcode replace it with value column
+ if ([UITypes.QrCode, UITypes.Barcode].includes(column.uidt))
+ filter.fk_column_id = await column
+ .getColOptions()
+ .then((col) => col.fk_column_id);
+ }
+ }
+
const column = await filter.getColumn();
if (!column) {
if (throwErrorIfInvalid) {
diff --git a/packages/nocodb/src/db/generateBTLookupSelectQuery.ts b/packages/nocodb/src/db/generateBTLookupSelectQuery.ts
deleted file mode 100644
index 62835a1a44..0000000000
--- a/packages/nocodb/src/db/generateBTLookupSelectQuery.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-import { RelationTypes, UITypes } from 'nocodb-sdk';
-import type LookupColumn from '../models/LookupColumn';
-import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2';
-import type {
- Column,
- FormulaColumn,
- LinkToAnotherRecordColumn,
- Model,
- RollupColumn,
-} from '~/models';
-import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
-import genRollupSelectv2 from '~/db/genRollupSelectv2';
-import { NcError } from '~/helpers/catchError';
-
-export default async function generateBTLookupSelectQuery({
- column,
- baseModelSqlv2,
- alias,
- model,
-}: {
- column: Column;
- baseModelSqlv2: BaseModelSqlv2;
- alias: string;
- model: Model;
-}): Promise {
- const knex = baseModelSqlv2.dbDriver;
-
- const rootAlias = alias;
-
- {
- let aliasCount = 0,
- selectQb;
- const alias = `__nc_lk_${aliasCount++}`;
- const lookup = await column.getColOptions();
- {
- const relationCol = await lookup.getRelationColumn();
- const relation =
- await relationCol.getColOptions();
-
- // if not belongs to then throw error as we don't support
- if (relation.type !== RelationTypes.BELONGS_TO)
- NcError.badRequest('HasMany/ManyToMany lookup is not supported');
-
- const childColumn = await relation.getChildColumn();
- const parentColumn = await relation.getParentColumn();
- const childModel = await childColumn.getModel();
- await childModel.getColumns();
- const parentModel = await parentColumn.getModel();
- await parentModel.getColumns();
-
- selectQb = knex(
- `${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${alias}`,
- ).where(
- `${alias}.${parentColumn.column_name}`,
- knex.raw(`??`, [
- `${rootAlias || baseModelSqlv2.getTnPath(childModel.table_name)}.${
- childColumn.column_name
- }`,
- ]),
- );
- }
- let lookupColumn = await lookup.getLookupColumn();
- let prevAlias = alias;
- while (lookupColumn.uidt === UITypes.Lookup) {
- const nestedAlias = `__nc_lk_nested_${aliasCount++}`;
- const nestedLookup = await lookupColumn.getColOptions();
- const relationCol = await nestedLookup.getRelationColumn();
- const relation =
- await relationCol.getColOptions();
-
- // if any of the relation in nested lookup is
- // not belongs to then throw error as we don't support
- if (relation.type !== RelationTypes.BELONGS_TO)
- NcError.badRequest('HasMany/ManyToMany lookup is not supported');
-
- const childColumn = await relation.getChildColumn();
- const parentColumn = await relation.getParentColumn();
- const childModel = await childColumn.getModel();
- await childModel.getColumns();
- const parentModel = await parentColumn.getModel();
- await parentModel.getColumns();
-
- selectQb.join(
- `${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${nestedAlias}`,
- `${nestedAlias}.${parentColumn.column_name}`,
- `${prevAlias}.${childColumn.column_name}`,
- );
-
- lookupColumn = await nestedLookup.getLookupColumn();
- prevAlias = nestedAlias;
- }
-
- switch (lookupColumn.uidt) {
- case UITypes.Links:
- case UITypes.Rollup:
- {
- const builder = (
- await genRollupSelectv2({
- baseModelSqlv2,
- knex,
- columnOptions:
- (await lookupColumn.getColOptions()) as RollupColumn,
- alias: prevAlias,
- })
- ).builder;
- selectQb.select(builder);
- }
- break;
- case UITypes.LinkToAnotherRecord:
- {
- const nestedAlias = `__nc_sort${aliasCount++}`;
- const relation =
- await lookupColumn.getColOptions();
- if (relation.type !== 'bt') return;
-
- const colOptions =
- (await column.getColOptions()) as LinkToAnotherRecordColumn;
- const childColumn = await colOptions.getChildColumn();
- const parentColumn = await colOptions.getParentColumn();
- const childModel = await childColumn.getModel();
- await childModel.getColumns();
- const parentModel = await parentColumn.getModel();
- await parentModel.getColumns();
-
- selectQb
- .join(
- `${baseModelSqlv2.getTnPath(
- parentModel.table_name,
- )} as ${nestedAlias}`,
- `${nestedAlias}.${parentColumn.column_name}`,
- `${prevAlias}.${childColumn.column_name}`,
- )
- .select(parentModel?.displayValue?.column_name);
- }
- break;
- case UITypes.Formula:
- {
- const builder = (
- await formulaQueryBuilderv2(
- baseModelSqlv2,
- (
- await column.getColOptions()
- ).formula,
- null,
- model,
- column,
- )
- ).builder;
-
- selectQb.select(builder);
- }
- break;
- default:
- {
- selectQb.select(`${prevAlias}.${lookupColumn.column_name}`);
- }
-
- break;
- }
-
- return { builder: selectQb };
- }
-}
diff --git a/packages/nocodb/src/db/generateLookupSelectQuery.ts b/packages/nocodb/src/db/generateLookupSelectQuery.ts
new file mode 100644
index 0000000000..09187d1fdc
--- /dev/null
+++ b/packages/nocodb/src/db/generateLookupSelectQuery.ts
@@ -0,0 +1,399 @@
+import { RelationTypes, UITypes } from 'nocodb-sdk';
+import type LookupColumn from '../models/LookupColumn';
+import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2';
+import type {
+ BarcodeColumn,
+ Column,
+ FormulaColumn,
+ LinksColumn,
+ LinkToAnotherRecordColumn,
+ QrCodeColumn,
+ RollupColumn,
+} from '~/models';
+import { Model } from '~/models';
+import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
+import genRollupSelectv2 from '~/db/genRollupSelectv2';
+import { getAliasGenerator } from '~/utils';
+import { NcError } from '~/helpers/catchError';
+
+const LOOKUP_VAL_SEPARATOR = '___';
+
+export async function getDisplayValueOfRefTable(
+ relationCol: Column,
+) {
+ return await relationCol
+ .getColOptions()
+ .then((colOpt) => colOpt.getRelatedTable())
+ .then((model) => model.getColumns())
+ .then((cols) => cols.find((col) => col.pv));
+}
+
+export default async function generateLookupSelectQuery({
+ column,
+ baseModelSqlv2,
+ alias,
+ model: _model,
+ getAlias = getAliasGenerator('__lk_slt_'),
+}: {
+ column: Column;
+ baseModelSqlv2: BaseModelSqlv2;
+ alias: string;
+ model: Model;
+ getAlias?: ReturnType;
+}): Promise {
+ const knex = baseModelSqlv2.dbDriver;
+
+ const rootAlias = alias;
+
+ {
+ let selectQb;
+ const alias = getAlias();
+ let lookupColOpt: LookupColumn;
+ let isBtLookup = true;
+
+ if (column.uidt === UITypes.Lookup) {
+ lookupColOpt = await column.getColOptions();
+ } else if (column.uidt !== UITypes.LinkToAnotherRecord) {
+ NcError.badRequest('Invalid column type');
+ }
+
+ await column.getColOptions();
+ {
+ const relationCol = lookupColOpt
+ ? await lookupColOpt.getRelationColumn()
+ : column;
+ const relation =
+ await relationCol.getColOptions();
+
+ // if not belongs to then throw error as we don't support
+ if (relation.type === RelationTypes.BELONGS_TO) {
+ const childColumn = await relation.getChildColumn();
+ const parentColumn = await relation.getParentColumn();
+ const childModel = await childColumn.getModel();
+ await childModel.getColumns();
+ const parentModel = await parentColumn.getModel();
+ await parentModel.getColumns();
+
+ selectQb = knex(
+ `${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${alias}`,
+ ).where(
+ `${alias}.${parentColumn.column_name}`,
+ knex.raw(`??`, [
+ `${rootAlias || baseModelSqlv2.getTnPath(childModel.table_name)}.${
+ childColumn.column_name
+ }`,
+ ]),
+ );
+ }
+
+ // if not belongs to then throw error as we don't support
+ else if (relation.type === RelationTypes.HAS_MANY) {
+ isBtLookup = false;
+ const childColumn = await relation.getChildColumn();
+ const parentColumn = await relation.getParentColumn();
+ const childModel = await childColumn.getModel();
+ await childModel.getColumns();
+ const parentModel = await parentColumn.getModel();
+ await parentModel.getColumns();
+
+ selectQb = knex(
+ `${baseModelSqlv2.getTnPath(childModel.table_name)} as ${alias}`,
+ ).where(
+ `${alias}.${childColumn.column_name}`,
+ knex.raw(`??`, [
+ `${rootAlias || baseModelSqlv2.getTnPath(parentModel.table_name)}.${
+ parentColumn.column_name
+ }`,
+ ]),
+ );
+ }
+
+ // if not belongs to then throw error as we don't support
+ else if (relation.type === RelationTypes.MANY_TO_MANY) {
+ isBtLookup = false;
+ const childColumn = await relation.getChildColumn();
+ const parentColumn = await relation.getParentColumn();
+ const childModel = await childColumn.getModel();
+ await childModel.getColumns();
+ const parentModel = await parentColumn.getModel();
+ await parentModel.getColumns();
+
+ selectQb = knex(
+ `${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${alias}`,
+ );
+
+ const mmTableAlias = getAlias();
+
+ const mmModel = await relation.getMMModel();
+ const mmChildCol = await relation.getMMChildColumn();
+ const mmParentCol = await relation.getMMParentColumn();
+
+ selectQb
+ .innerJoin(
+ baseModelSqlv2.getTnPath(mmModel.table_name, mmTableAlias),
+ knex.ref(`${mmTableAlias}.${mmParentCol.column_name}`),
+ '=',
+ knex.ref(`${alias}.${parentColumn.column_name}`),
+ )
+ .where(
+ knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`),
+ '=',
+ knex.ref(
+ `${
+ rootAlias || baseModelSqlv2.getTnPath(childModel.table_name)
+ }.${childColumn.column_name}`,
+ ),
+ );
+ }
+ }
+ let lookupColumn = lookupColOpt
+ ? await lookupColOpt.getLookupColumn()
+ : await getDisplayValueOfRefTable(column);
+
+ // if lookup column is qr code or barcode extract the referencing column
+ if ([UITypes.QrCode, UITypes.Barcode].includes(lookupColumn.uidt)) {
+ lookupColumn = await lookupColumn
+ .getColOptions()
+ .then((barcode) => barcode.getValueColumn());
+ }
+
+ let prevAlias = alias;
+ while (
+ lookupColumn.uidt === UITypes.Lookup ||
+ lookupColumn.uidt === UITypes.LinkToAnotherRecord
+ ) {
+ const nestedAlias = getAlias();
+
+ let relationCol: Column;
+ let nestedLookupColOpt: LookupColumn;
+
+ if (lookupColumn.uidt === UITypes.Lookup) {
+ nestedLookupColOpt = await lookupColumn.getColOptions();
+ relationCol = await nestedLookupColOpt.getRelationColumn();
+ } else {
+ relationCol = lookupColumn;
+ }
+
+ const relation =
+ await relationCol.getColOptions();
+
+ // if any of the relation in nested lookupColOpt is
+ // not belongs to then throw error as we don't support
+ if (relation.type === RelationTypes.BELONGS_TO) {
+ const childColumn = await relation.getChildColumn();
+ const parentColumn = await relation.getParentColumn();
+ const childModel = await childColumn.getModel();
+ await childModel.getColumns();
+ const parentModel = await parentColumn.getModel();
+ await parentModel.getColumns();
+
+ selectQb.join(
+ `${baseModelSqlv2.getTnPath(
+ parentModel.table_name,
+ )} as ${nestedAlias}`,
+ `${nestedAlias}.${parentColumn.column_name}`,
+ `${prevAlias}.${childColumn.column_name}`,
+ );
+ } else if (relation.type === RelationTypes.HAS_MANY) {
+ isBtLookup = false;
+ const childColumn = await relation.getChildColumn();
+ const parentColumn = await relation.getParentColumn();
+ const childModel = await childColumn.getModel();
+ await childModel.getColumns();
+ const parentModel = await parentColumn.getModel();
+ await parentModel.getColumns();
+
+ selectQb.join(
+ `${baseModelSqlv2.getTnPath(
+ childModel.table_name,
+ )} as ${nestedAlias}`,
+ `${nestedAlias}.${childColumn.column_name}`,
+ `${prevAlias}.${parentColumn.column_name}`,
+ );
+ } else if (relation.type === RelationTypes.MANY_TO_MANY) {
+ isBtLookup = false;
+ const childColumn = await relation.getChildColumn();
+ const parentColumn = await relation.getParentColumn();
+ const childModel = await childColumn.getModel();
+ await childModel.getColumns();
+ const parentModel = await parentColumn.getModel();
+ await parentModel.getColumns();
+
+ const mmTableAlias = getAlias();
+
+ const mmModel = await relation.getMMModel();
+ const mmChildCol = await relation.getMMChildColumn();
+ const mmParentCol = await relation.getMMParentColumn();
+
+ selectQb
+ .innerJoin(
+ baseModelSqlv2.getTnPath(mmModel.table_name, mmTableAlias),
+ knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`),
+ '=',
+ knex.ref(`${prevAlias}.${childColumn.column_name}`),
+ )
+ .innerJoin(
+ knex.raw('?? as ??', [
+ baseModelSqlv2.getTnPath(parentModel.table_name),
+ nestedAlias,
+ ]),
+ knex.ref(`${mmTableAlias}.${mmParentCol.column_name}`),
+ '=',
+ knex.ref(`${nestedAlias}.${parentColumn.column_name}`),
+ )
+ .where(
+ knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`),
+ '=',
+ knex.ref(
+ `${alias || baseModelSqlv2.getTnPath(childModel.table_name)}.${
+ childColumn.column_name
+ }`,
+ ),
+ );
+ }
+
+ if (lookupColumn.uidt === UITypes.Lookup)
+ lookupColumn = await nestedLookupColOpt.getLookupColumn();
+ else lookupColumn = await getDisplayValueOfRefTable(relationCol);
+ prevAlias = nestedAlias;
+ }
+
+ {
+ // get basemodel and model of lookup column
+ const model = await lookupColumn.getModel();
+ const baseModelSqlv2 = await Model.getBaseModelSQL({
+ model,
+ dbDriver: knex,
+ });
+
+ switch (lookupColumn.uidt) {
+ case UITypes.Attachment:
+ NcError.badRequest(
+ 'Group by using attachment column is not supported',
+ );
+ break;
+ case UITypes.Links:
+ case UITypes.Rollup:
+ {
+ const builder = (
+ await genRollupSelectv2({
+ baseModelSqlv2,
+ knex,
+ columnOptions:
+ (await lookupColumn.getColOptions()) as RollupColumn,
+ alias: prevAlias,
+ })
+ ).builder;
+ selectQb.select(builder);
+ }
+ break;
+ case UITypes.Formula:
+ {
+ const builder = (
+ await formulaQueryBuilderv2(
+ baseModelSqlv2,
+ (
+ await lookupColumn.getColOptions()
+ ).formula,
+ lookupColumn.title,
+ model,
+ lookupColumn,
+ await model.getAliasColMapping(),
+ prevAlias,
+ )
+ ).builder;
+
+ selectQb.select(builder);
+ }
+ break;
+ case UITypes.DateTime:
+ {
+ await baseModelSqlv2.selectObject({
+ qb: selectQb,
+ columns: [lookupColumn],
+ alias: prevAlias,
+ });
+ }
+ break;
+ default:
+ {
+ selectQb.select(
+ `${prevAlias}.${lookupColumn.column_name} as ${lookupColumn.title}`,
+ );
+ }
+
+ break;
+ }
+ }
+ // if all relation are belongs to then we don't need to do the aggregation
+ if (isBtLookup) {
+ return {
+ builder: selectQb,
+ };
+ }
+
+ const subQueryAlias = getAlias();
+
+ if (baseModelSqlv2.isPg) {
+ // alternate approach with array_agg
+ return {
+ builder: knex
+ .select(knex.raw('json_agg(??)::text', [lookupColumn.title]))
+ .from(selectQb.as(subQueryAlias)),
+ };
+ /*
+ // alternate approach with array_agg
+ return {
+ builder: knex
+ .select(knex.raw('array_agg(??)', [lookupColumn.title]))
+ .from(selectQb),
+ };*/
+ // alternate approach with string aggregation
+ // return {
+ // builder: knex
+ // .select(
+ // knex.raw('STRING_AGG(??::text, ?)', [
+ // lookupColumn.title,
+ // LOOKUP_VAL_SEPARATOR,
+ // ]),
+ // )
+ // .from(selectQb.as(subQueryAlias)),
+ // };
+ } else if (baseModelSqlv2.isMySQL) {
+ return {
+ builder: knex
+ .select(
+ knex.raw('cast(JSON_ARRAYAGG(??) as NCHAR)', [lookupColumn.title]),
+ )
+ .from(selectQb.as(subQueryAlias)),
+ };
+
+ // return {
+ // builder: knex
+ // .select(
+ // knex.raw('GROUP_CONCAT(?? ORDER BY ?? ASC SEPARATOR ?)', [
+ // lookupColumn.title,
+ // lookupColumn.title,
+ // LOOKUP_VAL_SEPARATOR,
+ // ]),
+ // )
+ // .from(selectQb.as(subQueryAlias)),
+ // };
+ } else if (baseModelSqlv2.isSqlite) {
+ // ref: https://stackoverflow.com/questions/13382856/sqlite3-join-group-concat-using-distinct-with-custom-separator
+ // selectQb.orderBy(`${lookupColumn.title}`, 'asc');
+ return {
+ builder: knex
+ .select(
+ knex.raw(`group_concat(??, ?)`, [
+ lookupColumn.title,
+ LOOKUP_VAL_SEPARATOR,
+ ]),
+ )
+ .from(selectQb.as(subQueryAlias)),
+ };
+ }
+
+ NcError.notImplemented('Database not supported Group by on Lookup');
+ }
+}
diff --git a/packages/nocodb/src/utils/globals.ts b/packages/nocodb/src/utils/globals.ts
index 786320dac7..0a31046558 100644
--- a/packages/nocodb/src/utils/globals.ts
+++ b/packages/nocodb/src/utils/globals.ts
@@ -171,6 +171,11 @@ export enum CacheDelDirection {
CHILD_TO_PARENT = 'CHILD_TO_PARENT',
}
+export const GROUPBY_COMPARISON_OPS = [
+ // these are used for groupby
+ 'gb_eq',
+ 'gb_null',
+];
export const COMPARISON_OPS = [
'eq',
'neq',
diff --git a/packages/nocodb/tests/unit/rest/tests/groupby.test.ts b/packages/nocodb/tests/unit/rest/tests/groupby.test.ts
index 8c61ece1ca..3144d43059 100644
--- a/packages/nocodb/tests/unit/rest/tests/groupby.test.ts
+++ b/packages/nocodb/tests/unit/rest/tests/groupby.test.ts
@@ -299,7 +299,7 @@ function groupByTests() {
expect(response.body.list.length).to.equal(1);
});
- it('Check One GroupBy Column with MM Lookup which is not supported', async function () {
+ it('Check One GroupBy Column with MM Lookup which is supported', async function () {
await createLookupColumn(context, {
base: sakilaProject,
title: 'ActorNames',
@@ -308,15 +308,17 @@ function groupByTests() {
relatedTableColumnTitle: 'FirstName',
});
- const res = await request(context.app)
+ const response = await request(context.app)
.get(`/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/groupby`)
.set('xc-auth', context.token)
.query({
column_name: 'ActorNames',
})
- .expect(400);
+ .expect(200);
- assert.match(res.body.msg, /not supported/);
+ assert.match(response.body.list[1]['ActorNames'], /ADAM|ANNE/);
+ expect(+response.body.list[1]['count']).to.gt(0);
+ expect(response.body.list.length).to.equal(25);
});
it('Check One GroupBy Column with Formula and Formula referring another formula', async function () {