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
5 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