mirror of https://github.com/nocodb/nocodb
Browse Source
* feat: basic ui for aggregation * feat: update aggregation in ui * feat: aggregation api implementation * feat: attachment aggregation.ts * fix: some changes * fix: rebase * feat: aggregation for links, rollup, ltar, formula, lookup * fix: type errors * fix: move from data-alias controller, service to data-table service, controller * chore: inline docs for aggregations * fix: handle edge cases * fix: ui bugs * feat: working ui aggregation * fix: minor issue * fix: rollup and links fix count * fix: handle ID Column * fix: minor fixes * fix: update aggregation on data change * fix: round to 2 decimal places * fix: stddev computation error replace with stddev_pop * fix: use pg age function * feat: new record layout * fix: shared view aggregations * feat: aggregations based on formula result * fix: temp pagination * feat: ncpagination v2 * feat: ncpagination v2 * fix: playwright tests * fix: pending changes * fix: failing tests * feat: mysql2 aggregations * fix: build * fix: record count * fix: cleanup * fix: disable count aggregation * feat: expiremental sqlite3 aggregation * fix: mysql2 median * fix:minor issues * refactor: rename column to column_query fix: remove default aggregations fix: disable aggregation for specific dbtype and Foreign Key * fix: remove unwanted else case * fix: aggregation not loading * fix: rebase * fix: rebase * fix: pagination fixed height * fix: respect locked mode for aggregations * fix: pagination component * fix: pagination component * fix: replace Math.randompull/8840/head
Anbarasu
3 months ago
committed by
GitHub
37 changed files with 3157 additions and 16 deletions
@ -0,0 +1,241 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import NcTooltip from '~/components/nc/Tooltip.vue' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
current: number |
||||||
|
total: number |
||||||
|
pageSize: number |
||||||
|
entityName?: string |
||||||
|
mode?: 'simple' | 'full' |
||||||
|
prevPageTooltip?: string |
||||||
|
nextPageTooltip?: string |
||||||
|
firstPageTooltip?: string |
||||||
|
lastPageTooltip?: string |
||||||
|
showSizeChanger?: boolean |
||||||
|
}>() |
||||||
|
|
||||||
|
const emits = defineEmits(['update:current', 'update:pageSize']) |
||||||
|
|
||||||
|
const { total, showSizeChanger } = toRefs(props) |
||||||
|
|
||||||
|
const current = useVModel(props, 'current', emits) |
||||||
|
|
||||||
|
const pageSize = useVModel(props, 'pageSize', emits) |
||||||
|
|
||||||
|
const { gridViewPageSize, setGridViewPageSize } = useGlobal() |
||||||
|
|
||||||
|
const localPageSize = computed({ |
||||||
|
get: () => { |
||||||
|
if (!showSizeChanger.value) return pageSize.value |
||||||
|
|
||||||
|
const storedPageSize = gridViewPageSize.value || 25 |
||||||
|
|
||||||
|
if (pageSize.value !== storedPageSize) { |
||||||
|
pageSize.value = storedPageSize |
||||||
|
} |
||||||
|
|
||||||
|
return pageSize.value |
||||||
|
}, |
||||||
|
set: (val) => { |
||||||
|
setGridViewPageSize(val) |
||||||
|
|
||||||
|
pageSize.value = val |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
const entityName = computed(() => props.entityName || 'item') |
||||||
|
|
||||||
|
const totalPages = computed(() => Math.max(Math.ceil(total.value / localPageSize.value), 1)) |
||||||
|
|
||||||
|
const { isMobileMode } = useGlobal() |
||||||
|
|
||||||
|
const mode = computed(() => props.mode || (isMobileMode.value ? 'simple' : 'full')) |
||||||
|
|
||||||
|
const changePage = ({ increase, set }: { increase?: boolean; set?: number }) => { |
||||||
|
if (set) { |
||||||
|
current.value = set |
||||||
|
} else if (increase && current.value < totalPages.value) { |
||||||
|
current.value = current.value + 1 |
||||||
|
} else if (current.value > 0) { |
||||||
|
current.value = current.value - 1 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const goToLastPage = () => { |
||||||
|
current.value = totalPages.value |
||||||
|
} |
||||||
|
|
||||||
|
const goToFirstPage = () => { |
||||||
|
current.value = 1 |
||||||
|
} |
||||||
|
|
||||||
|
const pagesList = computed(() => { |
||||||
|
return Array.from({ length: totalPages.value }, (_, i) => ({ |
||||||
|
value: i + 1, |
||||||
|
label: i + 1, |
||||||
|
})) |
||||||
|
}) |
||||||
|
|
||||||
|
const pageSizeOptions = [ |
||||||
|
{ |
||||||
|
value: 25, |
||||||
|
label: '25 / page', |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: 50, |
||||||
|
label: '50 / page', |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: 75, |
||||||
|
label: '75 / page', |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: 100, |
||||||
|
label: '100 / page', |
||||||
|
}, |
||||||
|
] |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="nc-pagination flex flex-row items-center gap-x-0.25"> |
||||||
|
<template v-if="totalPages > 1"> |
||||||
|
<component :is="props.firstPageTooltip && mode === 'full' ? NcTooltip : 'div'" v-if="mode === 'full'"> |
||||||
|
<template v-if="props.firstPageTooltip" #title> |
||||||
|
{{ props.firstPageTooltip }} |
||||||
|
</template> |
||||||
|
<NcButton |
||||||
|
v-e="[`a:pagination:${entityName}:first-page`]" |
||||||
|
class="first-page !border-0" |
||||||
|
type="text" |
||||||
|
size="xsmall" |
||||||
|
:disabled="current === 1" |
||||||
|
@click="goToFirstPage" |
||||||
|
> |
||||||
|
<GeneralIcon icon="doubleLeftArrow" class="nc-pagination-icon" /> |
||||||
|
</NcButton> |
||||||
|
</component> |
||||||
|
|
||||||
|
<component :is="props.prevPageTooltip && mode === 'full' ? NcTooltip : 'div'"> |
||||||
|
<template v-if="props.prevPageTooltip" #title> |
||||||
|
{{ props.prevPageTooltip }} |
||||||
|
</template> |
||||||
|
<NcButton |
||||||
|
v-e="[`a:pagination:${entityName}:prev-page`]" |
||||||
|
class="prev-page !border-0" |
||||||
|
type="secondary" |
||||||
|
size="xsmall" |
||||||
|
:disabled="current === 1" |
||||||
|
@click="changePage({ increase: false })" |
||||||
|
> |
||||||
|
<GeneralIcon icon="arrowLeft" class="nc-pagination-icon" /> |
||||||
|
</NcButton> |
||||||
|
</component> |
||||||
|
|
||||||
|
<div v-if="!isMobileMode" class="text-gray-500"> |
||||||
|
<NcDropdown placement="top" overlay-class-name="!shadow-none"> |
||||||
|
<NcButton class="!border-0 nc-select-page" type="secondary" size="xsmall"> |
||||||
|
<div class="flex gap-1 items-center px-2"> |
||||||
|
<span class="nc-current-page"> |
||||||
|
{{ current }} |
||||||
|
</span> |
||||||
|
<GeneralIcon icon="arrowDown" class="text-gray-800 mt-0.5 nc-select-expand-btn" /> |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
|
||||||
|
<template #overlay> |
||||||
|
<NcMenu class="nc-scrollbar-thin nc-pagination-menu max-h-54 overflow-y-auto"> |
||||||
|
<NcSubMenu :key="`${localPageSize}page`" class="bg-gray-100 z-20 top-0 !sticky"> |
||||||
|
<template #title> |
||||||
|
<div class="rounded-lg text-[13px] font-medium w-full">{{ localPageSize }} / page</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<NcMenuItem v-for="option in pageSizeOptions" :key="option.value" @click="localPageSize = option.value"> |
||||||
|
<span |
||||||
|
class="text-[13px]" |
||||||
|
:class="{ |
||||||
|
'!text-brand-500': option.value === localPageSize, |
||||||
|
}" |
||||||
|
> |
||||||
|
{{ option.value }} / page |
||||||
|
</span> |
||||||
|
</NcMenuItem> |
||||||
|
</NcSubMenu> |
||||||
|
|
||||||
|
<div :key="localPageSize" class="flex flex-col mt-1 max-h-48 overflow-hidden nc-scrollbar-md gap-1"> |
||||||
|
<NcMenuItem |
||||||
|
v-for="x in pagesList" |
||||||
|
:key="`${localPageSize}${x.value}`" |
||||||
|
@click.stop=" |
||||||
|
changePage({ |
||||||
|
set: x.value, |
||||||
|
}) |
||||||
|
" |
||||||
|
> |
||||||
|
<div |
||||||
|
:class="{ |
||||||
|
'text-brand-500': x.value === current, |
||||||
|
}" |
||||||
|
class="flex text-[13px] !w-full text-gray-800 items-center justify-between" |
||||||
|
> |
||||||
|
{{ x.label }} |
||||||
|
</div> |
||||||
|
</NcMenuItem> |
||||||
|
</div> |
||||||
|
</NcMenu> |
||||||
|
</template> |
||||||
|
</NcDropdown> |
||||||
|
</div> |
||||||
|
|
||||||
|
<component :is="props.nextPageTooltip && mode === 'full' ? NcTooltip : 'div'"> |
||||||
|
<template v-if="props.nextPageTooltip" #title> |
||||||
|
{{ props.nextPageTooltip }} |
||||||
|
</template> |
||||||
|
<NcButton |
||||||
|
v-e="[`a:pagination:${entityName}:next-page`]" |
||||||
|
class="next-page !border-0" |
||||||
|
type="secondary" |
||||||
|
size="xsmall" |
||||||
|
:disabled="current === totalPages" |
||||||
|
@click="changePage({ increase: true })" |
||||||
|
> |
||||||
|
<GeneralIcon icon="arrowRight" class="nc-pagination-icon" /> |
||||||
|
</NcButton> |
||||||
|
</component> |
||||||
|
|
||||||
|
<component :is="props.lastPageTooltip && mode === 'full' ? NcTooltip : 'div'" v-if="mode === 'full'"> |
||||||
|
<template v-if="props.lastPageTooltip" #title> |
||||||
|
{{ props.lastPageTooltip }} |
||||||
|
</template> |
||||||
|
<NcButton |
||||||
|
v-e="[`a:pagination:${entityName}:last-page`]" |
||||||
|
class="last-page !border-0" |
||||||
|
type="secondary" |
||||||
|
size="xsmall" |
||||||
|
:disabled="current === totalPages" |
||||||
|
@click="goToLastPage" |
||||||
|
> |
||||||
|
<GeneralIcon icon="doubleRightArrow" class="nc-pagination-icon" /> |
||||||
|
</NcButton> |
||||||
|
</component> |
||||||
|
</template> |
||||||
|
<div v-if="showSizeChanger && !isMobileMode" class="text-gray-500"></div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.nc-pagination-icon { |
||||||
|
@apply w-4 h-4; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-dropdown-menu-title-content) { |
||||||
|
@apply justify-center; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.nc-button:not(:disabled)) { |
||||||
|
.nc-pagination-icon { |
||||||
|
@apply !text-gray-500; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
|
|
||||||
|
<style lang="scss"></style> |
@ -0,0 +1,145 @@ |
|||||||
|
import type { Ref } from 'vue' |
||||||
|
import { type ColumnType, CommonAggregations, type TableType, UITypes, type ViewType, getAvailableAggregations } from 'nocodb-sdk' |
||||||
|
|
||||||
|
const [useProvideViewAggregate, useViewAggregate] = useInjectionState( |
||||||
|
( |
||||||
|
view: Ref<ViewType | undefined>, |
||||||
|
meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>, |
||||||
|
where?: ComputedRef<string | undefined>, |
||||||
|
) => { |
||||||
|
const { $api: api } = useNuxtApp() |
||||||
|
|
||||||
|
const fields = inject(FieldsInj, ref([])) |
||||||
|
|
||||||
|
const isPublic = inject(IsPublicInj, ref(false)) |
||||||
|
|
||||||
|
const { gridViewCols, updateGridViewColumn } = useViewColumnsOrThrow() |
||||||
|
|
||||||
|
const { nestedFilters } = useSmartsheetStoreOrThrow() |
||||||
|
|
||||||
|
const { fetchAggregatedData } = useSharedView() |
||||||
|
|
||||||
|
const aggregations = ref({}) as Ref<Record<string, any>> |
||||||
|
|
||||||
|
const reloadAggregate = inject(ReloadAggregateHookInj, createEventHook()) |
||||||
|
|
||||||
|
const visibleFieldsComputed = computed(() => { |
||||||
|
const fie = fields.value.map((field, index) => ({ field, index })).filter((f) => f.index !== 0) |
||||||
|
|
||||||
|
return fie.map((f) => { |
||||||
|
const gridField = gridViewCols.value[f.field.id!] |
||||||
|
|
||||||
|
if (!gridField) { |
||||||
|
return { field: null, index: f.index } |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
value: aggregations.value[f.field.title] ?? null, |
||||||
|
field: gridField, |
||||||
|
column: f.field, |
||||||
|
index: f.index, |
||||||
|
width: `${Number(gridField.width.replace('px', ''))}px` || '180px', |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
const displayFieldComputed = computed(() => { |
||||||
|
if (!fields.value?.length || !gridViewCols.value) |
||||||
|
return { |
||||||
|
field: null, |
||||||
|
width: '180px', |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
value: aggregations.value[fields.value[0].title] ?? null, |
||||||
|
column: fields.value[0], |
||||||
|
field: gridViewCols.value[fields.value[0].id!], |
||||||
|
width: `${Number((gridViewCols.value[fields.value[0]!.id!].width ?? '').replace('px', '')) + 64}px` || '244px', |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const getAggregations = (column: ColumnType) => { |
||||||
|
if (column.uidt === UITypes.Formula && (column.colOptions as any)?.parsed_tree?.dataType) { |
||||||
|
return getAvailableAggregations(column.uidt!, (column.colOptions as any).parsed_tree) |
||||||
|
} |
||||||
|
return getAvailableAggregations(column.uidt!) |
||||||
|
} |
||||||
|
|
||||||
|
const loadViewAggregate = async ( |
||||||
|
fields?: Array<{ |
||||||
|
field: string |
||||||
|
type: string |
||||||
|
}>, |
||||||
|
) => { |
||||||
|
if (!meta.value?.id || !view.value?.id) return |
||||||
|
|
||||||
|
try { |
||||||
|
const data = !isPublic.value |
||||||
|
? await api.dbDataTableAggregate.dbDataTableAggregate(meta.value.id, { |
||||||
|
viewId: view.value.id, |
||||||
|
where: where?.value, |
||||||
|
...(fields ? { aggregation: fields } : {}), |
||||||
|
}) |
||||||
|
: await fetchAggregatedData({ |
||||||
|
where: where?.value, |
||||||
|
filtersArr: nestedFilters.value, |
||||||
|
...(fields ? { aggregation: fields } : {}), |
||||||
|
}) |
||||||
|
|
||||||
|
Object.assign(aggregations.value, data) |
||||||
|
} catch (error) { |
||||||
|
console.log(error) |
||||||
|
message.error(await extractSdkResponseErrorMsgv2(error as any)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const updateAggregate = async (fieldId: string, agg: string) => { |
||||||
|
loadViewAggregate([ |
||||||
|
{ |
||||||
|
field: fieldId, |
||||||
|
type: agg, |
||||||
|
}, |
||||||
|
]) |
||||||
|
await updateGridViewColumn(fieldId, { aggregation: agg }) |
||||||
|
} |
||||||
|
|
||||||
|
reloadAggregate?.on(async (_fields) => { |
||||||
|
if (!_fields || !_fields.field?.length) { |
||||||
|
await loadViewAggregate() |
||||||
|
} |
||||||
|
if (_fields?.field) { |
||||||
|
const fieldAggregateMapping = _fields.field.reduce((acc, field) => { |
||||||
|
const f = fields.value.find((f) => f.title === field) |
||||||
|
|
||||||
|
acc[f.id] = gridViewCols.value[f.id].aggregation ?? CommonAggregations.None |
||||||
|
|
||||||
|
return acc |
||||||
|
}, {} as Record<string, string>) |
||||||
|
|
||||||
|
await loadViewAggregate( |
||||||
|
Object.entries(fieldAggregateMapping).map(([field, type]) => ({ |
||||||
|
field, |
||||||
|
type, |
||||||
|
})), |
||||||
|
) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
loadViewAggregate, |
||||||
|
isPublic, |
||||||
|
updateAggregate, |
||||||
|
getAggregations, |
||||||
|
displayFieldComputed, |
||||||
|
visibleFieldsComputed, |
||||||
|
} |
||||||
|
}, |
||||||
|
) |
||||||
|
|
||||||
|
export { useProvideViewAggregate } |
||||||
|
|
||||||
|
export function useViewAggregateOrThrow() { |
||||||
|
const viewAggregate = useViewAggregate() |
||||||
|
if (viewAggregate == null) throw new Error('Please call `useProvideViewAggregate` on the appropriate parent component') |
||||||
|
return viewAggregate |
||||||
|
} |
@ -0,0 +1,133 @@ |
|||||||
|
import { |
||||||
|
AttachmentAggregations, |
||||||
|
BooleanAggregations, |
||||||
|
type ColumnType, |
||||||
|
CommonAggregations, |
||||||
|
DateAggregations, |
||||||
|
UITypes, |
||||||
|
dateFormats, |
||||||
|
timeFormats, |
||||||
|
} from 'nocodb-sdk' |
||||||
|
import dayjs from 'dayjs' |
||||||
|
|
||||||
|
const getDateValue = (modelValue: string | null | number, col: ColumnType, isSystemCol?: boolean) => { |
||||||
|
const dateFormat = !isSystemCol ? parseProp(col.meta)?.date_format ?? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss' |
||||||
|
if (!modelValue || !dayjs(modelValue).isValid()) { |
||||||
|
return '' |
||||||
|
} |
||||||
|
return dayjs(/^\d+$/.test(String(modelValue)) ? +modelValue : modelValue).format(dateFormat) |
||||||
|
} |
||||||
|
|
||||||
|
const roundTo = (num: unknown, precision = 1) => { |
||||||
|
if (!num || Number.isNaN(num)) return num |
||||||
|
const factor = 10 ** precision |
||||||
|
return Math.round(+num * factor) / factor |
||||||
|
} |
||||||
|
|
||||||
|
const getDateTimeValue = (modelValue: string | null, col: ColumnType, isXcdbBase?: boolean) => { |
||||||
|
if (!modelValue || !dayjs(modelValue).isValid()) { |
||||||
|
return '' |
||||||
|
} |
||||||
|
|
||||||
|
const dateFormat = parseProp(col?.meta)?.date_format ?? dateFormats[0] |
||||||
|
const timeFormat = parseProp(col?.meta)?.time_format ?? timeFormats[0] |
||||||
|
const dateTimeFormat = `${dateFormat} ${timeFormat}` |
||||||
|
|
||||||
|
if (!isXcdbBase) { |
||||||
|
return dayjs(/^\d+$/.test(modelValue) ? +modelValue : modelValue, dateTimeFormat).format(dateTimeFormat) |
||||||
|
} |
||||||
|
|
||||||
|
return dayjs(modelValue).utc().local().format(dateTimeFormat) |
||||||
|
} |
||||||
|
|
||||||
|
const getCurrencyValue = (modelValue: string | number | null | undefined, col: ColumnType): string => { |
||||||
|
const currencyMeta = { |
||||||
|
currency_locale: 'en-US', |
||||||
|
currency_code: 'USD', |
||||||
|
...parseProp(col.meta), |
||||||
|
} |
||||||
|
try { |
||||||
|
if (modelValue === null || modelValue === undefined || Number.isNaN(modelValue)) { |
||||||
|
return modelValue === null || modelValue === undefined ? '' : (modelValue as string) |
||||||
|
} |
||||||
|
return new Intl.NumberFormat(currencyMeta.currency_locale || 'en-US', { |
||||||
|
style: 'currency', |
||||||
|
currency: currencyMeta.currency_code || 'USD', |
||||||
|
}).format(+modelValue) |
||||||
|
} catch (e) { |
||||||
|
return modelValue as string |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function formatBytes(bytes, decimals = 2) { |
||||||
|
if (!+bytes) return '0 Bytes' |
||||||
|
|
||||||
|
const k = 1024 |
||||||
|
const dm = decimals < 0 ? 0 : decimals |
||||||
|
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] |
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k)) |
||||||
|
|
||||||
|
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}` |
||||||
|
} |
||||||
|
|
||||||
|
const formatAggregation = (aggregation: any, value: any, column: ColumnType) => { |
||||||
|
if ([DateAggregations.EarliestDate, DateAggregations.LatestDate].includes(aggregation)) { |
||||||
|
if (column.uidt === UITypes.DateTime) { |
||||||
|
return getDateTimeValue(value, column) |
||||||
|
} else if (column.uidt === UITypes.Date) { |
||||||
|
return getDateValue(value, column) |
||||||
|
} |
||||||
|
return getDateTimeValue(value, column) |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
[ |
||||||
|
CommonAggregations.PercentEmpty, |
||||||
|
CommonAggregations.PercentFilled, |
||||||
|
CommonAggregations.PercentUnique, |
||||||
|
BooleanAggregations.PercentChecked, |
||||||
|
BooleanAggregations.PercentUnchecked, |
||||||
|
].includes(aggregation) |
||||||
|
) { |
||||||
|
return `${roundTo(value, 1) ?? 0}%` |
||||||
|
} |
||||||
|
|
||||||
|
if ([DateAggregations.MonthRange, DateAggregations.DateRange].includes(aggregation)) { |
||||||
|
return aggregation === DateAggregations.DateRange ? `${value ?? 0} days` : `${value ?? 0} months` |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
[ |
||||||
|
CommonAggregations.Count, |
||||||
|
CommonAggregations.CountEmpty, |
||||||
|
CommonAggregations.CountFilled, |
||||||
|
CommonAggregations.CountUnique, |
||||||
|
].includes(aggregation) |
||||||
|
) { |
||||||
|
return value |
||||||
|
} |
||||||
|
|
||||||
|
if ([AttachmentAggregations.AttachmentSize].includes(aggregation)) { |
||||||
|
return formatBytes(value ?? 0) |
||||||
|
} |
||||||
|
|
||||||
|
if (column.uidt === UITypes.Currency) { |
||||||
|
return getCurrencyValue(value, column) |
||||||
|
} |
||||||
|
|
||||||
|
if (column.uidt === UITypes.Percent) { |
||||||
|
return `${roundTo(value, 1)}%` |
||||||
|
} |
||||||
|
|
||||||
|
if (column.uidt === UITypes.Duration) { |
||||||
|
return convertMS2Duration(value, parseProp(column.meta)?.duration || 0) |
||||||
|
} |
||||||
|
if (typeof value === 'number') { |
||||||
|
return roundTo(value, 1) ?? '∞' |
||||||
|
} |
||||||
|
|
||||||
|
return value |
||||||
|
} |
||||||
|
|
||||||
|
export { formatAggregation } |
@ -0,0 +1,136 @@ |
|||||||
|
import UITypes from './UITypes'; |
||||||
|
import { FormulaDataTypes } from '~/lib/formulaHelpers'; |
||||||
|
|
||||||
|
enum NumericalAggregations { |
||||||
|
Sum = 'sum', |
||||||
|
Min = 'min', |
||||||
|
Max = 'max', |
||||||
|
Avg = 'avg', |
||||||
|
Median = 'median', |
||||||
|
StandardDeviation = 'std_dev', |
||||||
|
// Histogram = 'histogram',
|
||||||
|
Range = 'range', |
||||||
|
} |
||||||
|
|
||||||
|
enum CommonAggregations { |
||||||
|
Count = 'count', |
||||||
|
CountEmpty = 'count_empty', |
||||||
|
CountFilled = 'count_filled', |
||||||
|
CountUnique = 'count_unique', |
||||||
|
PercentEmpty = 'percent_empty', |
||||||
|
PercentFilled = 'percent_filled', |
||||||
|
PercentUnique = 'percent_unique', |
||||||
|
None = 'none', |
||||||
|
} |
||||||
|
|
||||||
|
enum AttachmentAggregations { |
||||||
|
AttachmentSize = 'attachment_size', |
||||||
|
} |
||||||
|
|
||||||
|
enum BooleanAggregations { |
||||||
|
Checked = 'checked', |
||||||
|
Unchecked = 'unchecked', |
||||||
|
PercentChecked = 'percent_checked', |
||||||
|
PercentUnchecked = 'percent_unchecked', |
||||||
|
} |
||||||
|
|
||||||
|
enum DateAggregations { |
||||||
|
EarliestDate = 'earliest_date', |
||||||
|
LatestDate = 'latest_date', |
||||||
|
DateRange = 'date_range', |
||||||
|
MonthRange = 'month_range', |
||||||
|
} |
||||||
|
|
||||||
|
const AllAggregations = { |
||||||
|
...CommonAggregations, |
||||||
|
...NumericalAggregations, |
||||||
|
...AttachmentAggregations, |
||||||
|
...BooleanAggregations, |
||||||
|
...DateAggregations, |
||||||
|
}; |
||||||
|
|
||||||
|
const getAvailableAggregations = (type: string, parsed_tree?): string[] => { |
||||||
|
let returnAggregations = []; |
||||||
|
if (type === UITypes.Formula && parsed_tree?.dataType) { |
||||||
|
switch (parsed_tree.dataType) { |
||||||
|
case FormulaDataTypes.BOOLEAN: |
||||||
|
returnAggregations = [ |
||||||
|
...Object.values(BooleanAggregations), |
||||||
|
CommonAggregations.None, |
||||||
|
]; |
||||||
|
break; |
||||||
|
case FormulaDataTypes.DATE: |
||||||
|
returnAggregations = [ |
||||||
|
...Object.values(DateAggregations), |
||||||
|
...Object.values(CommonAggregations), |
||||||
|
]; |
||||||
|
break; |
||||||
|
case FormulaDataTypes.NUMERIC: |
||||||
|
returnAggregations = [ |
||||||
|
...Object.values(NumericalAggregations), |
||||||
|
...Object.values(CommonAggregations), |
||||||
|
]; |
||||||
|
|
||||||
|
break; |
||||||
|
default: |
||||||
|
returnAggregations = [...Object.values(CommonAggregations)]; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
switch (type) { |
||||||
|
case UITypes.Number: |
||||||
|
case UITypes.Decimal: |
||||||
|
case UITypes.Currency: |
||||||
|
case UITypes.Percent: |
||||||
|
case UITypes.Duration: |
||||||
|
case UITypes.Rating: |
||||||
|
case UITypes.Rollup: |
||||||
|
case UITypes.Links: |
||||||
|
returnAggregations = [ |
||||||
|
...Object.values(NumericalAggregations), |
||||||
|
...Object.values(CommonAggregations), |
||||||
|
]; |
||||||
|
break; |
||||||
|
case UITypes.Attachment: |
||||||
|
returnAggregations = [ |
||||||
|
...Object.values(AttachmentAggregations), |
||||||
|
...Object.values(CommonAggregations), |
||||||
|
]; |
||||||
|
break; |
||||||
|
case UITypes.Checkbox: |
||||||
|
returnAggregations = [ |
||||||
|
...Object.values(BooleanAggregations), |
||||||
|
CommonAggregations.None, |
||||||
|
]; |
||||||
|
break; |
||||||
|
case UITypes.Date: |
||||||
|
case UITypes.DateTime: |
||||||
|
case UITypes.LastModifiedTime: |
||||||
|
case UITypes.CreatedTime: |
||||||
|
returnAggregations = [ |
||||||
|
...Object.values(DateAggregations), |
||||||
|
...Object.values(CommonAggregations), |
||||||
|
]; |
||||||
|
break; |
||||||
|
case UITypes.SpecificDBType: |
||||||
|
case UITypes.ForeignKey: |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
if (!returnAggregations.length) { |
||||||
|
returnAggregations = [...Object.values(CommonAggregations)]; |
||||||
|
} |
||||||
|
|
||||||
|
return returnAggregations.filter((item) => item !== CommonAggregations.Count); |
||||||
|
}; |
||||||
|
|
||||||
|
export { |
||||||
|
getAvailableAggregations, |
||||||
|
NumericalAggregations, |
||||||
|
CommonAggregations, |
||||||
|
BooleanAggregations, |
||||||
|
DateAggregations, |
||||||
|
AttachmentAggregations, |
||||||
|
AllAggregations, |
||||||
|
}; |
@ -0,0 +1,202 @@ |
|||||||
|
import { |
||||||
|
AttachmentAggregations, |
||||||
|
BooleanAggregations, |
||||||
|
CommonAggregations, |
||||||
|
DateAggregations, |
||||||
|
getAvailableAggregations, |
||||||
|
NumericalAggregations, |
||||||
|
UITypes, |
||||||
|
} from 'nocodb-sdk'; |
||||||
|
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2'; |
||||||
|
import type { BarcodeColumn, QrCodeColumn, RollupColumn } from '~/models'; |
||||||
|
import { Column } from '~/models'; |
||||||
|
import { NcError } from '~/helpers/catchError'; |
||||||
|
import genRollupSelectv2 from '~/db/genRollupSelectv2'; |
||||||
|
import generateLookupSelectQuery from '~/db/generateLookupSelectQuery'; |
||||||
|
import { genPgAggregateQuery } from '~/db/aggregations/pg'; |
||||||
|
import { genMysql2AggregatedQuery } from '~/db/aggregations/mysql2'; |
||||||
|
import { genSqlite3AggregateQuery } from '~/db/aggregations/sqlite3'; |
||||||
|
|
||||||
|
const validateColType = (column: Column, aggregation: string) => { |
||||||
|
const agg = getAvailableAggregations( |
||||||
|
column.uidt, |
||||||
|
column.colOptions?.parsed_tree, |
||||||
|
); |
||||||
|
|
||||||
|
if (!agg.includes(aggregation)) { |
||||||
|
NcError.badRequest( |
||||||
|
`Aggregation ${aggregation} is not available for column type ${column.uidt}`, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
Object.values(BooleanAggregations).includes( |
||||||
|
aggregation as BooleanAggregations, |
||||||
|
) |
||||||
|
) { |
||||||
|
return 'boolean'; |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
Object.values(CommonAggregations).includes( |
||||||
|
aggregation as CommonAggregations, |
||||||
|
) |
||||||
|
) { |
||||||
|
return 'common'; |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
Object.values(DateAggregations).includes(aggregation as DateAggregations) |
||||||
|
) { |
||||||
|
return 'date'; |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
Object.values(NumericalAggregations).includes( |
||||||
|
aggregation as NumericalAggregations, |
||||||
|
) |
||||||
|
) { |
||||||
|
return 'numerical'; |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
Object.values(AttachmentAggregations).includes( |
||||||
|
aggregation as AttachmentAggregations, |
||||||
|
) |
||||||
|
) { |
||||||
|
return 'attachment'; |
||||||
|
} |
||||||
|
|
||||||
|
return 'unknown'; |
||||||
|
}; |
||||||
|
|
||||||
|
export default async function applyAggregation({ |
||||||
|
baseModelSqlv2, |
||||||
|
aggregation, |
||||||
|
column, |
||||||
|
}: { |
||||||
|
baseModelSqlv2: BaseModelSqlv2; |
||||||
|
aggregation: string; |
||||||
|
column: Column; |
||||||
|
}): Promise<string | undefined> { |
||||||
|
if (!aggregation || !column) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const { context, dbDriver: knex, model } = baseModelSqlv2; |
||||||
|
|
||||||
|
/* |
||||||
|
All aggregations are not available for all UITypes. We validate the column type |
||||||
|
and the aggregation type to make sure that the aggregation is available for the column type. |
||||||
|
We also return the type of aggregation that has to be applied on the column. |
||||||
|
The return value can be one of the following: |
||||||
|
- common - common aggregations like count, count empty, count filled, count unique, etc. |
||||||
|
- numerical - numerical aggregations like sum, avg, min, max, etc. |
||||||
|
- boolean - boolean aggregations like checked, unchecked, percent checked, percent unchecked, etc. |
||||||
|
- date - date aggregations like earliest date, latest date, date range, month range, etc. |
||||||
|
- attachment - attachment aggregations like attachment size. |
||||||
|
- unknown - if the aggregation is not supported yet |
||||||
|
*/ |
||||||
|
const aggType = validateColType(column, aggregation); |
||||||
|
|
||||||
|
// If the aggregation is not available for the column type, we throw an error.
|
||||||
|
if (aggType === 'unknown') { |
||||||
|
NcError.notImplemented(`Aggregation ${aggregation} is not implemented yet`); |
||||||
|
} |
||||||
|
|
||||||
|
// If the column is a barcode or qr code column, we fetch the column that the virtual column refers to.
|
||||||
|
if (column.uidt === UITypes.Barcode || column.uidt === UITypes.QrCode) { |
||||||
|
column = new Column({ |
||||||
|
...(await column |
||||||
|
.getColOptions<BarcodeColumn | QrCodeColumn>(context) |
||||||
|
.then((col) => col.getValueColumn(context))), |
||||||
|
id: column.id, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
let column_name_query = column.column_name; |
||||||
|
|
||||||
|
if (column.uidt === UITypes.CreatedTime && !column.column_name) |
||||||
|
column_name_query = 'created_at'; |
||||||
|
if (column.uidt === UITypes.LastModifiedTime && !column.column_name) |
||||||
|
column_name_query = 'updated_at'; |
||||||
|
if (column.uidt === UITypes.CreatedBy && !column.column_name) |
||||||
|
column_name_query = 'created_by'; |
||||||
|
if (column.uidt === UITypes.LastModifiedBy && !column.column_name) |
||||||
|
column_name_query = 'updated_by'; |
||||||
|
|
||||||
|
const parsedFormulaType = column.colOptions?.parsed_tree?.dataType; |
||||||
|
|
||||||
|
/* The following column types require special handling for aggregation: |
||||||
|
* - Links |
||||||
|
* - Rollup |
||||||
|
* - Formula |
||||||
|
* - Lookup |
||||||
|
* - LinkToAnotherRecord |
||||||
|
* These column types require special handling because they are virtual columns and do not have a direct column name. |
||||||
|
* We generate the select query for these columns and use the generated query for aggregation. |
||||||
|
* */ |
||||||
|
switch (column.uidt) { |
||||||
|
case UITypes.Links: |
||||||
|
case UITypes.Rollup: |
||||||
|
column_name_query = ( |
||||||
|
await genRollupSelectv2({ |
||||||
|
baseModelSqlv2, |
||||||
|
knex, |
||||||
|
columnOptions: (await column.getColOptions(context)) as RollupColumn, |
||||||
|
}) |
||||||
|
).builder; |
||||||
|
break; |
||||||
|
|
||||||
|
case UITypes.Formula: |
||||||
|
column_name_query = ( |
||||||
|
await baseModelSqlv2.getSelectQueryBuilderForFormula(column) |
||||||
|
).builder; |
||||||
|
break; |
||||||
|
|
||||||
|
case UITypes.LinkToAnotherRecord: |
||||||
|
case UITypes.Lookup: |
||||||
|
column_name_query = ( |
||||||
|
await generateLookupSelectQuery({ |
||||||
|
baseModelSqlv2, |
||||||
|
column: column, |
||||||
|
alias: null, |
||||||
|
model, |
||||||
|
}) |
||||||
|
).builder; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
if (knex.client.config.client === 'pg') { |
||||||
|
return genPgAggregateQuery({ |
||||||
|
column, |
||||||
|
baseModelSqlv2, |
||||||
|
aggregation, |
||||||
|
column_query: column_name_query, |
||||||
|
parsedFormulaType, |
||||||
|
aggType, |
||||||
|
}); |
||||||
|
} else if (knex.client.config.client === 'mysql2') { |
||||||
|
return genMysql2AggregatedQuery({ |
||||||
|
column, |
||||||
|
baseModelSqlv2, |
||||||
|
aggregation, |
||||||
|
column_query: column_name_query, |
||||||
|
parsedFormulaType, |
||||||
|
aggType, |
||||||
|
}); |
||||||
|
} else if (knex.client.config.client === 'sqlite3') { |
||||||
|
return genSqlite3AggregateQuery({ |
||||||
|
column, |
||||||
|
baseModelSqlv2, |
||||||
|
aggregation, |
||||||
|
column_query: column_name_query, |
||||||
|
parsedFormulaType, |
||||||
|
aggType, |
||||||
|
}); |
||||||
|
} else { |
||||||
|
NcError.notImplemented( |
||||||
|
`Aggregation is not implemented for ${knex.client.config.client} yet.`, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,405 @@ |
|||||||
|
import { |
||||||
|
AttachmentAggregations, |
||||||
|
BooleanAggregations, |
||||||
|
CommonAggregations, |
||||||
|
DateAggregations, |
||||||
|
FormulaDataTypes, |
||||||
|
NumericalAggregations, |
||||||
|
UITypes, |
||||||
|
} from 'nocodb-sdk'; |
||||||
|
import type { Column } from '~/models'; |
||||||
|
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2'; |
||||||
|
import type { Knex } from 'knex'; |
||||||
|
|
||||||
|
export function genMysql2AggregatedQuery({ |
||||||
|
column, |
||||||
|
baseModelSqlv2, |
||||||
|
aggregation, |
||||||
|
column_query, |
||||||
|
parsedFormulaType, |
||||||
|
aggType, |
||||||
|
}: { |
||||||
|
column: Column; |
||||||
|
baseModelSqlv2: BaseModelSqlv2; |
||||||
|
aggregation: string; |
||||||
|
column_query: string; |
||||||
|
parsedFormulaType?: FormulaDataTypes; |
||||||
|
aggType: |
||||||
|
| 'common' |
||||||
|
| 'numerical' |
||||||
|
| 'boolean' |
||||||
|
| 'date' |
||||||
|
| 'attachment' |
||||||
|
| 'unknown'; |
||||||
|
}) { |
||||||
|
let aggregationSql: Knex.Raw | undefined; |
||||||
|
|
||||||
|
const { dbDriver: knex } = baseModelSqlv2; |
||||||
|
|
||||||
|
let secondaryCondition: any = "''"; |
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
secondaryCondition = 'NULL'; |
||||||
|
} else if ([UITypes.Rating].includes(column.uidt)) { |
||||||
|
secondaryCondition = 0; |
||||||
|
} |
||||||
|
|
||||||
|
if (aggType === 'common') { |
||||||
|
switch (aggregation) { |
||||||
|
case CommonAggregations.Count: |
||||||
|
aggregationSql = knex.raw(`COUNT(*) AS ??`, [column.id]); |
||||||
|
break; |
||||||
|
case CommonAggregations.CountEmpty: |
||||||
|
if ([UITypes.JSON].includes(column.uidt)) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`SUM(CASE WHEN JSON_LENGTH(??) IS NULL THEN 1 ELSE 0 END) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`SUM(CASE WHEN (??) IS NULL OR (??) = ${secondaryCondition} THEN 1 ELSE 0 END) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.CountFilled: |
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.JSON, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
UITypes.LinkToAnotherRecord, |
||||||
|
UITypes.Lookup, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`SUM(CASE WHEN (??) IS NOT NULL THEN 1 ELSE 0 END) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`SUM(CASE WHEN (??) IS NOT NULL AND (??) != ${secondaryCondition} THEN 1 ELSE 0 END) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.CountUnique: |
||||||
|
if ([UITypes.JSON].includes(column.uidt)) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(DISTINCT JSON_UNQUOTE(JSON_EXTRACT(??, '$'))) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
UITypes.LinkToAnotherRecord, |
||||||
|
UITypes.Lookup, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(DISTINCT CASE WHEN (??) IS NOT NULL THEN (??) END) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(DISTINCT CASE WHEN ?? IS NOT NULL AND ?? != ${secondaryCondition} THEN ?? END) AS ??`, |
||||||
|
[column_query, column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.PercentEmpty: |
||||||
|
if ([UITypes.JSON].includes(column.uidt)) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SUM(CASE WHEN JSON_LENGTH(??) IS NULL THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SUM(CASE WHEN (??) IS NULL OR (??) = ${secondaryCondition} THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.PercentFilled: |
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.JSON, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
UITypes.LinkToAnotherRecord, |
||||||
|
UITypes.Lookup, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SUM(CASE WHEN (??) IS NOT NULL THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SUM(CASE WHEN (??) IS NOT NULL AND (??) != ${secondaryCondition} THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.PercentUnique: |
||||||
|
if ([UITypes.JSON].includes(column.uidt)) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(DISTINCT JSON_UNQUOTE(JSON_EXTRACT((??), '$'))) * 100.0 / NULLIF(COUNT(*), 0) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
UITypes.LinkToAnotherRecord, |
||||||
|
UITypes.Lookup, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(COUNT(DISTINCT CASE WHEN ?? IS NOT NULL THEN ?? END) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(COUNT(DISTINCT CASE WHEN ?? IS NOT NULL AND ?? != ${secondaryCondition} THEN ?? END) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.None: |
||||||
|
break; |
||||||
|
} |
||||||
|
} else if (aggType === 'numerical') { |
||||||
|
switch (aggregation) { |
||||||
|
case NumericalAggregations.Avg: |
||||||
|
if (column.uidt === UITypes.Rating) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`AVG(CASE WHEN (??) != ${secondaryCondition} THEN (??) ELSE NULL END) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw(`AVG((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.Max: |
||||||
|
aggregationSql = knex.raw(`MAX((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.Min: |
||||||
|
if (column.uidt === UITypes.Rating) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`MIN(CASE WHEN (??) != ${secondaryCondition} THEN (??) ELSE NULL END) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw(`MIN((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.Sum: |
||||||
|
aggregationSql = knex.raw(`SUM((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.StandardDeviation: |
||||||
|
if (column.uidt === UITypes.Rating) { |
||||||
|
aggregationSql = knex.raw(`STDDEV((??)) AS ??`, [ |
||||||
|
column_query, |
||||||
|
column.id, |
||||||
|
]); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw(`STDDEV((??)) AS ??`, [ |
||||||
|
column_query, |
||||||
|
column.id, |
||||||
|
]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.Range: |
||||||
|
if (column.uidt === UITypes.Rating) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(MAX((??)) - MIN(CASE WHEN (??) != ${secondaryCondition} THEN (??) ELSE NULL END)) AS ??`, |
||||||
|
[column_query, column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw(`(MAX((??)) - MIN((??))) AS ??`, [ |
||||||
|
column_query, |
||||||
|
column_query, |
||||||
|
column.id, |
||||||
|
]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.Median: |
||||||
|
// This is the sqlite3 port median query. Need to use native mysql median query
|
||||||
|
aggregationSql = knex.raw( |
||||||
|
` |
||||||
|
( |
||||||
|
SELECT AVG(??) |
||||||
|
FROM ( |
||||||
|
SELECT ?? |
||||||
|
FROM ?? |
||||||
|
ORDER BY ?? |
||||||
|
LIMIT 2 - (SELECT COUNT(*) FROM ??) % 2 -- Handle even/odd number of rows |
||||||
|
OFFSET (SELECT (COUNT(*) - 1) / 2 FROM ??) -- Calculate the median offset |
||||||
|
) AS median_subquery |
||||||
|
) AS ??`,
|
||||||
|
[ |
||||||
|
column_query, |
||||||
|
column_query, |
||||||
|
baseModelSqlv2.tnPath, |
||||||
|
column_query, |
||||||
|
baseModelSqlv2.tnPath, |
||||||
|
baseModelSqlv2.tnPath, |
||||||
|
column.id, |
||||||
|
column.id, |
||||||
|
], |
||||||
|
); |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
} else if (aggType === 'boolean') { |
||||||
|
switch (aggregation) { |
||||||
|
case BooleanAggregations.Checked: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`SUM(CASE WHEN ?? = true THEN 1 ELSE 0 END) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case BooleanAggregations.Unchecked: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`SUM(CASE WHEN ?? = false OR ?? IS NULL THEN 1 ELSE 0 END) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case BooleanAggregations.PercentChecked: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SUM(CASE WHEN ?? = true THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case BooleanAggregations.PercentUnchecked: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SUM(CASE WHEN ?? = false OR ?? IS NULL THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
} else if (aggType === 'date') { |
||||||
|
switch (aggregation) { |
||||||
|
case DateAggregations.EarliestDate: |
||||||
|
aggregationSql = knex.raw(`MIN(??) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case DateAggregations.LatestDate: |
||||||
|
aggregationSql = knex.raw(`MAX(??) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case DateAggregations.DateRange: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`TIMESTAMPDIFF(DAY, MIN(??), MAX(??)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case DateAggregations.MonthRange: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`PERIOD_DIFF(DATE_FORMAT(MAX(??), '%Y%m'), DATE_FORMAT(MIN(??), '%Y%m')) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
} else if (aggType === 'attachment') { |
||||||
|
switch (aggregation) { |
||||||
|
case AttachmentAggregations.AttachmentSize: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SELECT SUM(JSON_EXTRACT(json_object, '$.size')) FROM ?? CROSS JOIN JSON_TABLE(CAST(?? AS JSON), '$[*]' COLUMNS (json_object JSON PATH '$')) AS json_array) AS ??`, |
||||||
|
[baseModelSqlv2.tnPath, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return aggregationSql?.toQuery(); |
||||||
|
} |
@ -0,0 +1,398 @@ |
|||||||
|
import { |
||||||
|
AttachmentAggregations, |
||||||
|
BooleanAggregations, |
||||||
|
CommonAggregations, |
||||||
|
DateAggregations, |
||||||
|
FormulaDataTypes, |
||||||
|
NumericalAggregations, |
||||||
|
UITypes, |
||||||
|
} from 'nocodb-sdk'; |
||||||
|
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2'; |
||||||
|
import type { Knex } from 'knex'; |
||||||
|
import type { Column } from '~/models'; |
||||||
|
|
||||||
|
export function genPgAggregateQuery({ |
||||||
|
column, |
||||||
|
baseModelSqlv2, |
||||||
|
aggregation, |
||||||
|
column_query, |
||||||
|
parsedFormulaType, |
||||||
|
aggType, |
||||||
|
}: { |
||||||
|
column: Column; |
||||||
|
column_query: string; |
||||||
|
baseModelSqlv2: BaseModelSqlv2; |
||||||
|
aggregation: string; |
||||||
|
parsedFormulaType?: FormulaDataTypes; |
||||||
|
aggType: |
||||||
|
| 'common' |
||||||
|
| 'numerical' |
||||||
|
| 'boolean' |
||||||
|
| 'date' |
||||||
|
| 'attachment' |
||||||
|
| 'unknown'; |
||||||
|
}) { |
||||||
|
let aggregationSql: Knex.Raw | undefined; |
||||||
|
|
||||||
|
const { dbDriver: knex } = baseModelSqlv2; |
||||||
|
|
||||||
|
let secondaryCondition: any = "''"; |
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
secondaryCondition = 'NULL'; |
||||||
|
} else if ([UITypes.Rating].includes(column.uidt)) { |
||||||
|
secondaryCondition = 0; |
||||||
|
} |
||||||
|
|
||||||
|
if (aggType === 'common') { |
||||||
|
switch (aggregation) { |
||||||
|
case CommonAggregations.Count: |
||||||
|
aggregationSql = knex.raw(`COUNT(*) AS ??`, [column.id]); |
||||||
|
break; |
||||||
|
case CommonAggregations.CountEmpty: |
||||||
|
if ([UITypes.JSON].includes(column.uidt)) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(*) FILTER (WHERE (??) IS NULL) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(*) FILTER (WHERE (??) IS NULL OR (??) = ${secondaryCondition}) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
|
||||||
|
break; |
||||||
|
case CommonAggregations.CountFilled: |
||||||
|
// The condition IS NOT NULL AND (column_query) != 'NULL' is not same for the following column_query types:
|
||||||
|
// Hence we need to handle them separately.
|
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.JSON, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
UITypes.LinkToAnotherRecord, |
||||||
|
UITypes.Lookup, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(*) FILTER (WHERE (??) IS NOT NULL) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
// For other column_query types, the condition is IS NOT NULL AND (column_query) != 'NULL'
|
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(*) FILTER (WHERE (??) IS NOT NULL AND (??) != ${secondaryCondition}) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.CountUnique: |
||||||
|
// JSON Does not support DISTINCT for json column_query type. Hence we need to cast the column_query to text.
|
||||||
|
if ([UITypes.JSON].includes(column.uidt)) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(DISTINCT ((??)::text)) FILTER (WHERE (??) IS NOT NULL) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
// The condition IS NOT NULL AND (column_query) != 'NULL' is not same for the following column_query types:
|
||||||
|
// Hence we need to handle them separately.
|
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
UITypes.LinkToAnotherRecord, |
||||||
|
UITypes.Lookup, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(DISTINCT (??)) FILTER (WHERE (??) IS NOT NULL) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(DISTINCT (??)) FILTER (WHERE (??) IS NOT NULL AND (??) != ${secondaryCondition}) AS ??`, |
||||||
|
[column_query, column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.PercentEmpty: |
||||||
|
if ([UITypes.JSON].includes(column.uidt)) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(COUNT(*) FILTER (WHERE (??) IS NULL) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(COUNT(*) FILTER (WHERE (??) IS NULL OR (??) = ${secondaryCondition}) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.PercentFilled: |
||||||
|
// The condition IS NOT NULL AND (column_query) != 'NULL' is not same for the following column_query types:
|
||||||
|
// Hence we need to handle them separately.
|
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.JSON, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
UITypes.LinkToAnotherRecord, |
||||||
|
UITypes.Lookup, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(COUNT(*) FILTER (WHERE (??) IS NOT NULL) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(COUNT(*) FILTER (WHERE (??) IS NOT NULL AND (??) != ${secondaryCondition}) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.PercentUnique: |
||||||
|
// JSON Does not support DISTINCT for json column_query type. Hence we need to cast the column_query to text.
|
||||||
|
if ([UITypes.JSON].includes(column.uidt)) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(COUNT(DISTINCT ((??)::text)) FILTER (WHERE (??) IS NOT NULL) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
// The condition IS NOT NULL AND (column_query) != 'NULL' is not same for the following column_query types:
|
||||||
|
// Hence we need to handle them separately.
|
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
UITypes.LinkToAnotherRecord, |
||||||
|
UITypes.Lookup, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(COUNT(DISTINCT (??)) FILTER (WHERE (??) IS NOT NULL) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(COUNT(DISTINCT (??)) FILTER (WHERE (??) IS NOT NULL AND (??) != ${secondaryCondition}) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.None: |
||||||
|
break; |
||||||
|
} |
||||||
|
} else if (aggType === 'numerical') { |
||||||
|
switch (aggregation) { |
||||||
|
case NumericalAggregations.Avg: |
||||||
|
if (column.uidt === UITypes.Rating) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`AVG((??)) FILTER (WHERE (??) != ??) AS ??`, |
||||||
|
[column_query, column_query, secondaryCondition, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw(`AVG((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.Max: |
||||||
|
aggregationSql = knex.raw(`MAX((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.Min: |
||||||
|
if (column.uidt === UITypes.Rating) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`MIN((??)) FILTER (WHERE (??) != ??) AS ??`, |
||||||
|
[column_query, column_query, secondaryCondition, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
aggregationSql = knex.raw(`MIN((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.Sum: |
||||||
|
aggregationSql = knex.raw(`SUM((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.StandardDeviation: |
||||||
|
if (column.uidt === UITypes.Rating) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`stddev_pop((??)) FILTER (WHERE (??) != ??) AS ??`, |
||||||
|
[column_query, column_query, secondaryCondition, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw(`stddev_pop((??)) AS ??`, [ |
||||||
|
column_query, |
||||||
|
column.id, |
||||||
|
]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.Range: |
||||||
|
aggregationSql = knex.raw(`MAX((??)) - MIN((??)) AS ??`, [ |
||||||
|
column_query, |
||||||
|
column_query, |
||||||
|
column.id, |
||||||
|
]); |
||||||
|
break; |
||||||
|
|
||||||
|
case NumericalAggregations.Median: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`percentile_cont(0.5) within group (order by (??)) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
} else if (aggType === 'boolean') { |
||||||
|
switch (aggregation) { |
||||||
|
case BooleanAggregations.Checked: |
||||||
|
aggregationSql = knex.raw(`COUNT(*) FILTER (WHERE (??) = true) AS ??`, [ |
||||||
|
column_query, |
||||||
|
column.id, |
||||||
|
]); |
||||||
|
break; |
||||||
|
case BooleanAggregations.Unchecked: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(*) FILTER (WHERE (??) = false OR (??) = NULL) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case BooleanAggregations.PercentChecked: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(COUNT(*) FILTER (WHERE (??) = true) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case BooleanAggregations.PercentUnchecked: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(COUNT(*) FILTER (WHERE (??) = false OR (??) = NULL) * 100.0 / NULLIF(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
} else if (aggType === 'date') { |
||||||
|
switch (aggregation) { |
||||||
|
case DateAggregations.EarliestDate: |
||||||
|
aggregationSql = knex.raw(`MIN((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case DateAggregations.LatestDate: |
||||||
|
aggregationSql = knex.raw(`MAX((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
|
||||||
|
// The Date, DateTime, CreatedTime, LastModifiedTime columns are casted to DATE.
|
||||||
|
case DateAggregations.DateRange: |
||||||
|
aggregationSql = knex.raw(`MAX((??)::date) - MIN((??)::date) AS ??`, [ |
||||||
|
column_query, |
||||||
|
column_query, |
||||||
|
column.id, |
||||||
|
]); |
||||||
|
break; |
||||||
|
// The Date, DateTime, CreatedTime, LastModifiedTime columns are casted to DATE.
|
||||||
|
case DateAggregations.MonthRange: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`DATE_PART('year', AGE(MAX((??)::date), MIN((??)::date))) * 12 +
|
||||||
|
DATE_PART('month', AGE(MAX((??)::date), MIN((??)::date))) AS ??`,
|
||||||
|
[column_query, column_query, column_query, column_query, column.id], |
||||||
|
); |
||||||
|
|
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
} else if (aggType === 'attachment') { |
||||||
|
switch (aggregation) { |
||||||
|
case AttachmentAggregations.AttachmentSize: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SELECT SUM((json_object ->> 'size')::int) FROM ?? CROSS JOIN LATERAL jsonb_array_elements(??::jsonb) AS json_array(json_object)) AS ??`, |
||||||
|
[baseModelSqlv2.tnPath, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return aggregationSql?.toQuery(); |
||||||
|
} |
@ -0,0 +1,423 @@ |
|||||||
|
import { |
||||||
|
AttachmentAggregations, |
||||||
|
BooleanAggregations, |
||||||
|
CommonAggregations, |
||||||
|
DateAggregations, |
||||||
|
FormulaDataTypes, |
||||||
|
NumericalAggregations, |
||||||
|
UITypes, |
||||||
|
} from 'nocodb-sdk'; |
||||||
|
import type { Column } from '~/models'; |
||||||
|
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2'; |
||||||
|
import type { Knex } from 'knex'; |
||||||
|
|
||||||
|
export function genSqlite3AggregateQuery({ |
||||||
|
column, |
||||||
|
baseModelSqlv2, |
||||||
|
aggregation, |
||||||
|
column_query, |
||||||
|
parsedFormulaType, |
||||||
|
aggType, |
||||||
|
}: { |
||||||
|
column: Column; |
||||||
|
baseModelSqlv2: BaseModelSqlv2; |
||||||
|
aggregation: string; |
||||||
|
column_query: string; |
||||||
|
parsedFormulaType?: FormulaDataTypes; |
||||||
|
aggType: |
||||||
|
| 'common' |
||||||
|
| 'numerical' |
||||||
|
| 'boolean' |
||||||
|
| 'date' |
||||||
|
| 'attachment' |
||||||
|
| 'unknown'; |
||||||
|
}) { |
||||||
|
let aggregationSql: Knex.Raw | undefined; |
||||||
|
|
||||||
|
const { dbDriver: knex } = baseModelSqlv2; |
||||||
|
|
||||||
|
let secondaryCondition: any = "''"; |
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
secondaryCondition = 'NULL'; |
||||||
|
} else if ([UITypes.Rating].includes(column.uidt)) { |
||||||
|
secondaryCondition = 0; |
||||||
|
} |
||||||
|
|
||||||
|
if (aggType === 'common') { |
||||||
|
switch (aggregation) { |
||||||
|
case CommonAggregations.Count: |
||||||
|
aggregationSql = knex.raw(`COUNT(*) AS ??`, [column.id]); |
||||||
|
break; |
||||||
|
case CommonAggregations.CountEmpty: |
||||||
|
if ([UITypes.JSON].includes(column.uidt)) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`SUM(CASE WHEN json_array_length(??) IS NULL THEN 1 ELSE 0 END) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`SUM(CASE WHEN (??) IS NULL OR (??) = ${secondaryCondition} THEN 1 ELSE 0 END) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.CountFilled: |
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.JSON, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
UITypes.LinkToAnotherRecord, |
||||||
|
UITypes.Lookup, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`SUM(CASE WHEN (??) IS NOT NULL THEN 1 ELSE 0 END) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`SUM(CASE WHEN (??) IS NOT NULL AND (??) != ${secondaryCondition} THEN 1 ELSE 0 END) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.CountUnique: |
||||||
|
if ([UITypes.JSON].includes(column.uidt)) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(DISTINCT json_extract(??, '$')) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
UITypes.LinkToAnotherRecord, |
||||||
|
UITypes.Lookup, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(DISTINCT CASE WHEN (??) IS NOT NULL THEN (??) END) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(DISTINCT CASE WHEN (??) IS NOT NULL AND (??) != ${secondaryCondition} THEN ?? END) AS ??`, |
||||||
|
[column_query, column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.PercentEmpty: |
||||||
|
if ([UITypes.JSON].includes(column.uidt)) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SUM(CASE WHEN json_array_length(??) IS NULL THEN 1 ELSE 0 END) * 100.0 / IFNULL(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SUM(CASE WHEN (??) IS NULL OR (??) = ${secondaryCondition} THEN 1 ELSE 0 END) * 100.0 / IFNULL(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.PercentFilled: |
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.JSON, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
UITypes.LinkToAnotherRecord, |
||||||
|
UITypes.Lookup, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SUM(CASE WHEN (??) IS NOT NULL THEN 1 ELSE 0 END) * 100.0 / IFNULL(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SUM(CASE WHEN (??) IS NOT NULL AND (??) != ${secondaryCondition} THEN 1 ELSE 0 END) * 100.0 / IFNULL(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.PercentUnique: |
||||||
|
if ([UITypes.JSON].includes(column.uidt)) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`COUNT(DISTINCT json_extract((??), '$')) * 100.0 / IFNULL(COUNT(*), 0) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.CreatedTime, |
||||||
|
UITypes.LastModifiedTime, |
||||||
|
UITypes.Date, |
||||||
|
UITypes.DateTime, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Year, |
||||||
|
UITypes.Time, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Rollup, |
||||||
|
UITypes.Links, |
||||||
|
UITypes.ID, |
||||||
|
UITypes.LinkToAnotherRecord, |
||||||
|
UITypes.Lookup, |
||||||
|
].includes(column.uidt) || |
||||||
|
[FormulaDataTypes.DATE, FormulaDataTypes.NUMERIC].includes( |
||||||
|
parsedFormulaType, |
||||||
|
) |
||||||
|
) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(COUNT(DISTINCT CASE WHEN (??) IS NOT NULL THEN (??) END) * 100.0 / IFNULL(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(COUNT(DISTINCT CASE WHEN (??) IS NOT NULL AND (??) != ${secondaryCondition} THEN (??) END) * 100.0 / IFNULL(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case CommonAggregations.None: |
||||||
|
break; |
||||||
|
} |
||||||
|
} else if (aggType === 'numerical') { |
||||||
|
switch (aggregation) { |
||||||
|
case NumericalAggregations.Avg: |
||||||
|
if (column.uidt === UITypes.Rating) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`AVG(CASE WHEN (??) != ${secondaryCondition} THEN (??) ELSE NULL END) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw(`AVG((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.Max: |
||||||
|
aggregationSql = knex.raw(`MAX((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.Min: |
||||||
|
if (column.uidt === UITypes.Rating) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`MIN(CASE WHEN (??) != ${secondaryCondition} THEN (??) ELSE NULL END) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw(`MIN((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.Sum: |
||||||
|
aggregationSql = knex.raw(`SUM((??)) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.StandardDeviation: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(*) > 0 THEN
|
||||||
|
SQRT(SUM(((??) - avg_value) * ((??) - avg_value)) / COUNT(*)) |
||||||
|
ELSE
|
||||||
|
NULL
|
||||||
|
END AS ?? |
||||||
|
FROM ( |
||||||
|
SELECT
|
||||||
|
(??),
|
||||||
|
(SELECT AVG((??)) FROM ??) AS avg_value |
||||||
|
FROM
|
||||||
|
?? |
||||||
|
) |
||||||
|
) AS ??`,
|
||||||
|
[ |
||||||
|
column_query, |
||||||
|
column_query, |
||||||
|
column.id, |
||||||
|
column_query, |
||||||
|
column_query, |
||||||
|
baseModelSqlv2.tnPath, |
||||||
|
baseModelSqlv2.tnPath, |
||||||
|
column.id, |
||||||
|
], |
||||||
|
); |
||||||
|
|
||||||
|
break; |
||||||
|
case NumericalAggregations.Range: |
||||||
|
if (column.uidt === UITypes.Rating) { |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(MAX((??)) - MIN(CASE WHEN (??) != ${secondaryCondition} THEN (??) ELSE NULL END)) AS ??`, |
||||||
|
[column_query, column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
} |
||||||
|
aggregationSql = knex.raw(`(MAX((??)) - MIN((??))) AS ??`, [ |
||||||
|
column_query, |
||||||
|
column_query, |
||||||
|
column.id, |
||||||
|
]); |
||||||
|
break; |
||||||
|
case NumericalAggregations.Median: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(
|
||||||
|
SELECT AVG((??)) |
||||||
|
FROM ( |
||||||
|
SELECT (??) |
||||||
|
FROM ?? |
||||||
|
ORDER BY (??) |
||||||
|
LIMIT 2 - (SELECT COUNT(*) FROM ??) % 2 -- Handle even/odd number of rows |
||||||
|
OFFSET (SELECT (COUNT(*) - 1) / 2 FROM ??) -- Calculate the median offset |
||||||
|
) |
||||||
|
) AS ??`,
|
||||||
|
[ |
||||||
|
column_query, |
||||||
|
column_query, |
||||||
|
baseModelSqlv2.tnPath, |
||||||
|
column_query, |
||||||
|
baseModelSqlv2.tnPath, |
||||||
|
baseModelSqlv2.tnPath, |
||||||
|
column.id, |
||||||
|
], |
||||||
|
); |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
} else if (aggType === 'boolean') { |
||||||
|
switch (aggregation) { |
||||||
|
case BooleanAggregations.Checked: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`SUM(CASE WHEN ?? = 1 THEN 1 ELSE 0 END) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case BooleanAggregations.Unchecked: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`SUM(CASE WHEN ?? = 0 OR ?? IS NULL THEN 1 ELSE 0 END) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case BooleanAggregations.PercentChecked: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SUM(CASE WHEN ?? = 1 THEN 1 ELSE 0 END) * 100.0 / IFNULL(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case BooleanAggregations.PercentUnchecked: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SUM(CASE WHEN ?? = 0 OR ?? IS NULL THEN 1 ELSE 0 END) * 100.0 / IFNULL(COUNT(*), 0)) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
} else if (aggType === 'date') { |
||||||
|
switch (aggregation) { |
||||||
|
case DateAggregations.EarliestDate: |
||||||
|
aggregationSql = knex.raw(`MIN(??) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case DateAggregations.LatestDate: |
||||||
|
aggregationSql = knex.raw(`MAX(??) AS ??`, [column_query, column.id]); |
||||||
|
break; |
||||||
|
case DateAggregations.DateRange: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`CAST(JULIANDAY(MAX(??)) - JULIANDAY(MIN(??)) AS INTEGER) AS ??`, |
||||||
|
[column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
case DateAggregations.MonthRange: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`((strftime('%Y', MAX(??)) * 12 + strftime('%m', MAX(??))) -
|
||||||
|
(strftime('%Y', MIN(??)) * 12 + strftime('%m', MIN(??)))) AS ??`,
|
||||||
|
[column_query, column_query, column_query, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
} else if (aggType === 'attachment') { |
||||||
|
switch (aggregation) { |
||||||
|
case AttachmentAggregations.AttachmentSize: |
||||||
|
aggregationSql = knex.raw( |
||||||
|
`(SELECT SUM(CAST(json_extract(value, '$.size') AS INTEGER))
|
||||||
|
FROM ??, json_each(??)) AS ??`,
|
||||||
|
[baseModelSqlv2.tnPath, column_query, column.id], |
||||||
|
); |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return aggregationSql?.toQuery(); |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import type { Knex } from 'knex'; |
||||||
|
import { MetaTable } from '~/utils/globals'; |
||||||
|
|
||||||
|
const up = async (knex: Knex) => { |
||||||
|
await knex.schema.alterTable(MetaTable.GRID_VIEW_COLUMNS, (table) => { |
||||||
|
table.string('aggregation', 30).nullable().defaultTo(null); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
const down = async (knex: Knex) => { |
||||||
|
await knex.schema.alterTable(MetaTable.GRID_VIEW_COLUMNS, (table) => { |
||||||
|
table.dropColumn('aggregation'); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
export { up, down }; |
@ -0,0 +1,11 @@ |
|||||||
|
import BasePage from '../../Base'; |
||||||
|
import { GridPage } from './index'; |
||||||
|
|
||||||
|
export class AggregaionBarPage extends BasePage { |
||||||
|
readonly parent: GridPage; |
||||||
|
|
||||||
|
constructor(parent: GridPage) { |
||||||
|
super(parent.rootPage); |
||||||
|
this.parent = parent; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue