@finos/legend-data-cube
Version:
966 lines (901 loc) • 29.5 kB
text/typescript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
PRIMITIVE_TYPE,
V1_AppliedFunction,
V1_AppliedProperty,
V1_CBoolean,
V1_CDateTime,
V1_CDecimal,
V1_CFloat,
V1_CInteger,
V1_CStrictDate,
V1_CStrictTime,
V1_CString,
V1_ClassInstance,
V1_ColSpec,
V1_ColSpecArray,
V1_GenericTypeInstance,
V1_Variable,
type V1_ValueSpecification,
V1_PrimitiveValueSpecification,
extractElementNameFromPath as _name,
matchFunctionName,
V1_RelationType,
V1_PackageableType,
type V1_GenericType,
V1_Collection,
type V1_Lambda,
} from '@finos/legend-graph';
import { _findCol, type DataCubeColumn } from './model/DataCubeColumn.js';
import {
assertErrorThrown,
assertTrue,
assertType,
at,
deepEqual,
guaranteeNonNullable,
guaranteeType,
uniq,
UnsupportedOperationError,
type Clazz,
} from '@finos/legend-shared';
import {
DataCubeFunction,
DataCubeOperationAdvancedValueType,
DataCubeQueryFilterGroupOperator,
DataCubeQuerySortDirection,
DEFAULT_LAMBDA_VARIABLE_NAME,
getDataType,
getPivotResultColumnBaseColumnName,
isPivotResultColumnName,
TREE_COLUMN_VALUE_SEPARATOR,
type DataCubeOperationValue,
} from './DataCubeQueryEngine.js';
import type {
DataCubeSnapshotAggregateColumn,
DataCubeSnapshotFilter,
DataCubeSnapshotFilterCondition,
DataCubeSnapshotGroupBy,
DataCubeSnapshotPivot,
} from './DataCubeSnapshot.js';
import type { DataCubeQueryFilterOperation } from './filter/DataCubeQueryFilterOperation.js';
import type { DataCubeEngine } from './DataCubeEngine.js';
import {
_cols,
_colSpec,
_function,
_lambda,
_synthesizeMinimalSourceQuery,
} from './DataCubeQueryBuilderUtils.js';
import {
INTERNAL__DataCubeSource,
type DataCubeSource,
} from './model/DataCubeSource.js';
import type { DataCubeQueryAggregateOperation } from './aggregation/DataCubeQueryAggregateOperation.js';
// --------------------------------- UTILITIES ---------------------------------
export function _var(variable: V1_Variable) {
assertTrue(
variable.name === DEFAULT_LAMBDA_VARIABLE_NAME,
`Can't process variable '${variable.name}': expected variable name to be '${DEFAULT_LAMBDA_VARIABLE_NAME}'`,
);
}
export function _propertyCol(
property: V1_AppliedProperty,
columnGetter: (name: string) => DataCubeColumn,
) {
assertTrue(
property.parameters.length === 1,
`Can't process property '${property.property}': expected exactly 1 parameter`,
);
const variable = guaranteeType(
at(property.parameters, 0),
V1_Variable,
`Can't process property '${property.property}': failed to extract variable`,
);
_var(variable);
return columnGetter(property.property);
}
export function _param<T extends V1_ValueSpecification>(
func: V1_AppliedFunction,
paramIdx: number,
clazz: Clazz<T>,
message?: string | undefined,
): T {
assertTrue(
func.parameters.length >= paramIdx + 1,
`Can't process ${_name(func.function)}() expression: expected at least ${paramIdx + 1} parameter(s)`,
);
return guaranteeType(
func.parameters[paramIdx],
clazz,
message ??
`Can't process ${_name(func.function)}() expression: found unexpected type for parameter at index ${paramIdx}`,
);
}
export function _colSpecParam(func: V1_AppliedFunction, paramIdx: number) {
return guaranteeType(
_param(func, paramIdx, V1_ClassInstance).value,
V1_ColSpec,
`Can't process ${_name(func.function)}() expression: expected parameter at index ${paramIdx} to be a column specification`,
);
}
export function _colSpecArrayParam(func: V1_AppliedFunction, paramIdx: number) {
return guaranteeType(
_param(func, paramIdx, V1_ClassInstance).value,
V1_ColSpecArray,
`Can't process ${_name(func.function)}() expression: expected parameter at index ${paramIdx} to be a column specification list`,
);
}
export function _genericTypeParam(func: V1_AppliedFunction, paramIdx: number) {
return _param(
func,
paramIdx,
V1_GenericTypeInstance,
`Can't process ${_name(func.function)}: expected parameter at index ${paramIdx} to be a generic type instance`,
);
}
export function _unwrapLambda(lambda: V1_Lambda, message?: string | undefined) {
assertTrue(
lambda.body.length === 1,
`${message ?? `Can't process lambda`}: expected lambda body to have exactly 1 expression`,
);
assertTrue(
lambda.parameters.length === 1,
`${message ?? `Can't process lambda`}: expected lambda to have exactly 1 parameter`,
);
_var(at(lambda.parameters, 0));
return at(lambda.body, 0);
}
export function _funcMatch(
value: V1_ValueSpecification | undefined,
functionNames: string | string[],
) {
assertType(
value,
V1_AppliedFunction,
`Can't process function: found unexpected value specification type`,
);
assertTrue(
matchFunctionName(
value.function,
Array.isArray(functionNames) ? functionNames : [functionNames],
),
`Can't process function: expected function to be one of [${uniq((Array.isArray(functionNames) ? functionNames : [functionNames]).map(_name)).join(', ')}]`,
);
return value;
}
export function _relationType(genericType: V1_GenericType) {
return guaranteeType(
genericType.typeArguments[0]?.rawType,
V1_RelationType,
`Can't process generic type: failed to extract relation type`,
);
}
export function _packageableType(genericType: V1_GenericType) {
return guaranteeType(
genericType.rawType,
V1_PackageableType,
`Can't process generic type: failed to extract packageable type`,
);
}
export function _operationPrimitiveValue(
value: V1_PrimitiveValueSpecification,
): DataCubeOperationValue {
if (value instanceof V1_CString) {
return { value: value.value, type: PRIMITIVE_TYPE.STRING };
} else if (value instanceof V1_CBoolean) {
return { value: value.value, type: PRIMITIVE_TYPE.BOOLEAN };
} else if (value instanceof V1_CDecimal) {
return { value: value.value, type: PRIMITIVE_TYPE.DECIMAL };
} else if (value instanceof V1_CInteger) {
return { value: value.value, type: PRIMITIVE_TYPE.INTEGER };
} else if (value instanceof V1_CFloat) {
return { value: value.value, type: PRIMITIVE_TYPE.FLOAT };
} else if (value instanceof V1_CStrictDate) {
return { value: value.value, type: PRIMITIVE_TYPE.STRICTDATE };
} else if (value instanceof V1_CDateTime) {
return { value: value.value, type: PRIMITIVE_TYPE.DATETIME };
} else if (value instanceof V1_CStrictTime) {
return { value: value.value, type: PRIMITIVE_TYPE.STRICTTIME };
}
throw new UnsupportedOperationError(
`Can't process unsupported operation primitive value`,
);
}
export function _operationValue(
value: V1_ValueSpecification | undefined,
columnGetter: (name: string) => DataCubeColumn,
columnChecker?: ((column: DataCubeColumn) => void) | undefined,
) {
if (value instanceof V1_PrimitiveValueSpecification) {
return _operationPrimitiveValue(value);
} else if (value instanceof V1_AppliedProperty) {
const column = _propertyCol(value, columnGetter);
columnChecker?.(column);
return {
value: column.name,
type: DataCubeOperationAdvancedValueType.COLUMN,
};
} else if (value === undefined) {
return {
type: DataCubeOperationAdvancedValueType.VOID,
};
}
throw new UnsupportedOperationError(
`Can't process unsupported operation value`,
);
}
export function _checkDuplicateColumns(
columns: DataCubeColumn[],
message?: ((colName: string) => string) | undefined,
) {
const cols = new Set<string>();
columns.forEach((col) => {
if (cols.has(col.name)) {
throw new Error(
message?.(col.name) ??
`Can't process expression: found duplicate columns '${col.name}'`,
);
} else {
cols.add(col.name);
}
});
}
// --------------------------------- BUILDING BLOCKS ---------------------------------
/**
* This method prunes expanded paths that are no longer valid due to changes in group by columns.
* It finds the last common group by column between the previous and current group by columns and
* prune the expanded paths beyond that point.
*/
export function _pruneExpandedPaths(
prevGroupByCols: DataCubeColumn[],
currentGroupByCols: DataCubeColumn[],
expandedPaths: string[],
) {
const length = Math.min(prevGroupByCols.length, currentGroupByCols.length);
if (!length) {
return [];
}
let lastCommonIndex = -1;
for (let i = 0; i < length; i++) {
if (
at(prevGroupByCols, i).name !== at(currentGroupByCols, i).name ||
at(prevGroupByCols, i).type !== at(currentGroupByCols, i).type
) {
break;
}
lastCommonIndex = i;
}
return expandedPaths
.filter(
(path) =>
path.split(TREE_COLUMN_VALUE_SEPARATOR).length <= lastCommonIndex + 1,
)
.sort();
}
export async function _extractExtendedColumns(
funcs: V1_AppliedFunction[],
currentColumns: DataCubeColumn[],
engine: DataCubeEngine,
source?: DataCubeSource,
) {
const colSpecs = funcs.map((extendFunc) => {
// TODO: support extend() with window (OLAP), this assertion will no longer work
const _colSpecs = _colSpecArrayParam(extendFunc, 0).colSpecs;
assertTrue(
_colSpecs.length === 1,
`Can't process extend() expression: expected 1 column specification, got ${_colSpecs.length}`,
);
return at(_colSpecs, 0);
});
// get the types
const sourceQuery = _synthesizeMinimalSourceQuery(currentColumns);
const sequence = colSpecs.map((colSpec) =>
_function(DataCubeFunction.EXTEND, [
_cols([
_colSpec(
colSpec.name,
guaranteeNonNullable(
colSpec.function1,
`Can't process extend() expression: expected a transformation function expression for column '${colSpec.name}'`,
),
colSpec.function2,
),
]),
]),
);
for (let i = 0; i < sequence.length; i++) {
at(sequence, i).parameters.unshift(
i === 0 ? sourceQuery : at(sequence, i - 1),
);
}
const query = at(sequence, sequence.length - 1);
let columns: DataCubeColumn[] = [];
try {
columns = (
await engine.getQueryRelationReturnType(
_lambda([], [query]),
source ? source : new INTERNAL__DataCubeSource(),
)
).columns;
} catch (error) {
assertErrorThrown(error);
throw new Error(
`Can't process extend() expression: failed to retrieve type information for columns. Error: ${error.message}`,
);
}
return colSpecs.map((colSpec) => ({
name: colSpec.name,
type: guaranteeNonNullable(
_findCol(columns, colSpec.name),
`Can't process extend() expression: failed to retrieve type information for column '${colSpec.name}'`,
).type,
mapFn: engine.serializeValueSpecification(
guaranteeNonNullable(
colSpec.function1,
`Can't process extend() expression: expected a transformation function expression for column '${colSpec.name}'`,
),
),
reduceFn: colSpec.function2
? engine.serializeValueSpecification(colSpec.function2)
: undefined,
}));
}
export function _filter(
value: V1_ValueSpecification,
columnGetter: (name: string) => DataCubeColumn,
filterOperations: DataCubeQueryFilterOperation[],
): DataCubeSnapshotFilter {
if (!(value instanceof V1_AppliedFunction)) {
throw new Error(
`Can't process filter() expression: expected a function expression`,
);
}
const group: DataCubeSnapshotFilter = {
// default to AND group for case where there is only one condition
groupOperator: DataCubeQueryFilterGroupOperator.AND,
conditions: [],
};
if (matchFunctionName(value.function, DataCubeFunction.AND)) {
value.parameters.forEach((param) => {
group.conditions.push(
_filterCondition(param, columnGetter, filterOperations),
);
});
} else if (matchFunctionName(value.function, DataCubeFunction.OR)) {
group.groupOperator = DataCubeQueryFilterGroupOperator.OR;
value.parameters.forEach((param) => {
group.conditions.push(
_filterCondition(param, columnGetter, filterOperations),
);
});
} else {
// handles the case where the root is a simple condition or a NOT condition
group.conditions.push(
_filterCondition(value, columnGetter, filterOperations),
);
}
return group;
}
export function _unwrapNotFilterCondition(func: V1_AppliedFunction) {
assertTrue(
matchFunctionName(func.function, DataCubeFunction.NOT),
`Can't process filter condition expression: failed to unwrap not() function`,
);
assertTrue(
func.parameters.length === 1,
`Can't process not() function: expected 1 parameter`,
);
return _param(func, 0, V1_AppliedFunction);
}
function _filterCondition(
value: V1_ValueSpecification,
columnGetter: (name: string) => DataCubeColumn,
filterOperations: DataCubeQueryFilterOperation[],
): DataCubeSnapshotFilterCondition | DataCubeSnapshotFilter {
if (!(value instanceof V1_AppliedFunction)) {
throw new UnsupportedOperationError(
`Can't process filter condition expression: expected a function expression`,
);
}
// handle group condition
if (
matchFunctionName(value.function, [
DataCubeFunction.AND,
DataCubeFunction.OR,
])
) {
return _filter(value, columnGetter, filterOperations);
}
// run through the list of supported filter operations to find the one that can process the condition
for (const filterOperation of filterOperations) {
const condition = filterOperation.buildConditionSnapshot(
value,
columnGetter,
);
if (condition) {
return condition;
}
}
// if no match found, proceed to unwrap if it's a NOT condition
if (matchFunctionName(value.function, DataCubeFunction.NOT)) {
// run through the list of supported filter operations to find the one that can process the condition
// again. Processing in this order ensures cases like x != y can be recognized as NOT_EQUAL operator
// instead of NOT(x == y)
const unwrapped = _unwrapNotFilterCondition(value);
for (const filterOperation of filterOperations) {
const condition = filterOperation.buildConditionSnapshot(
unwrapped,
columnGetter,
);
if (condition) {
condition.not = true;
return condition;
}
}
// if no match found, try to see if the condition is a group condition and process
if (
unwrapped instanceof V1_AppliedFunction &&
matchFunctionName(unwrapped.function, [
DataCubeFunction.AND,
DataCubeFunction.OR,
])
) {
const condition = _filter(unwrapped, columnGetter, filterOperations);
condition.not = true;
return condition;
}
}
// if no match found, throw error, we encountered a filter condition form that we don't support
throw new Error(`Can't process filter condition: no matching operator found`);
}
/**
* Processes filter conditions of form: column | operator | value, e.g.
* $x.Age > 5
* $x.Name == 'abc'
* $x.Name->startsWith('abc')
* $x.Age > $x.Age2
* $x.Name == $x.Name2
*/
export function _filterCondition_base(
expression: V1_AppliedFunction | undefined,
func: string,
columnGetter: (name: string) => DataCubeColumn,
) {
if (!expression) {
return undefined;
}
try {
if (matchFunctionName(expression.function, func)) {
if (
expression.parameters.length !== 2 &&
expression.parameters.length !== 1
) {
return undefined;
}
let column: DataCubeColumn | undefined;
if (expression.parameters[0] instanceof V1_AppliedProperty) {
column = _propertyCol(expression.parameters[0], columnGetter);
}
if (!column) {
return undefined;
}
const value = _operationValue(
expression.parameters[1],
columnGetter,
(_column) => {
if (getDataType(column.type) !== getDataType(_column.type)) {
throw new Error(
`Can't process filter condition: found incompatible columns`,
);
}
},
);
return {
column,
value,
};
}
} catch {
return undefined;
}
return undefined;
}
/**
* Processes filter conditions of form: column (case-insensitive) | operator | value (case-insensitive), e.g.
* $x.Name->toLower() == 'abc'->toLower()
* $x.Name->toLower() == $x.Name2->toLower()
*/
export function _filterCondition_caseSensitive(
expression: V1_AppliedFunction | undefined,
func: string,
columnGetter: (name: string) => DataCubeColumn,
) {
if (!expression) {
return undefined;
}
try {
if (matchFunctionName(expression.function, func)) {
if (expression.parameters.length !== 2) {
return undefined;
}
const param1 = expression.parameters[0];
if (
!(param1 instanceof V1_AppliedFunction) ||
!matchFunctionName(param1.function, DataCubeFunction.TO_LOWERCASE)
) {
return undefined;
}
if (param1.parameters.length !== 1) {
return undefined;
}
let column: DataCubeColumn | undefined;
if (param1.parameters[0] instanceof V1_AppliedProperty) {
column = _propertyCol(param1.parameters[0], columnGetter);
}
if (!column) {
return undefined;
}
const param2 = expression.parameters[1];
if (
!(param2 instanceof V1_AppliedFunction) ||
!matchFunctionName(param2.function, DataCubeFunction.TO_LOWERCASE)
) {
return undefined;
}
if (param2.parameters.length !== 1) {
return undefined;
}
const value = _operationValue(
param2.parameters[0],
columnGetter,
(_column) => {
if (getDataType(column.type) !== getDataType(_column.type)) {
throw new Error(
`Can't process filter condition: found incompatible columns`,
);
}
},
);
return {
column,
value,
};
}
} catch {
return undefined;
}
return undefined;
}
export function _aggCol(
colSpec: V1_ColSpec,
columnGetter: (name: string) => DataCubeColumn,
aggregateOperations: DataCubeQueryAggregateOperation[],
) {
for (const operation of aggregateOperations) {
const col = operation.buildAggregateColumnSnapshot(colSpec, columnGetter);
if (col) {
return col;
}
}
throw new Error(
`Can't process aggregate column '${colSpec.name}': no matching operator found`,
);
}
export function _agg_base(
colSpec: V1_ColSpec,
func: string,
columnGetter: (name: string) => DataCubeColumn,
) {
try {
if (colSpec.function1 && colSpec.function2) {
const mapper = _unwrapLambda(colSpec.function1);
const reducer = _unwrapLambda(colSpec.function2);
if (
mapper instanceof V1_AppliedProperty &&
reducer instanceof V1_AppliedFunction &&
reducer.parameters.length >= 1 &&
matchFunctionName(reducer.function, func)
) {
const column = _propertyCol(mapper, columnGetter);
assertTrue(
column.name === colSpec.name,
`Can't process aggregate column: column name must match mapper column name`,
);
const variable = _param(reducer, 0, V1_Variable);
_var(variable);
return {
column,
paramterValues: reducer.parameters
.slice(1)
.map((value) => _operationValue(value, columnGetter)),
};
}
}
} catch {
return undefined;
}
return undefined;
}
export function _pivotSort(
func: V1_AppliedFunction,
pivotColumns: DataCubeColumn[],
columnGetter: (name: string) => DataCubeColumn,
) {
const sortColumns = _sort(func, columnGetter);
const groupColumns = new Set(pivotColumns.map((col) => col.name));
const columnsToSort = new Set(pivotColumns.map((col) => col.name));
sortColumns.forEach((col) => {
if (groupColumns.has(col.name)) {
columnsToSort.delete(col.name);
} else {
throw new Error(
`Can't process pivot() expression: sort column '${col.name}' must be a pivot column`,
);
}
});
if (columnsToSort.size !== 0) {
throw new Error(
`Can't process pivot() expression: found unsorted pivot column(s) (${Array.from(
columnsToSort.values(),
)
.sort()
.map((col) => `'${col}'`)
.join(', ')})`,
);
}
_checkDuplicateColumns(
sortColumns,
(colName) =>
`Can't process pivot() expression: found duplicate sort columns '${colName}'`,
);
return sortColumns;
}
export function _validatePivot(
pivot: DataCubeSnapshotPivot,
pivotAggColumns: DataCubeSnapshotAggregateColumn[],
availableColumns: DataCubeColumn[],
) {
// check for duplicate columns
const pivotColumns = pivot.columns;
const castColumns = pivot.castColumns;
_checkDuplicateColumns(
pivotColumns,
(colName) =>
`Can't process pivot() expression: found duplicate pivot columns '${colName}'`,
);
_checkDuplicateColumns(
pivotAggColumns,
(colName) =>
`Can't process pivot() expression: found duplicate aggregate columns '${colName}'`,
);
_checkDuplicateColumns(
castColumns,
(colName) =>
`Can't process pivot() expression: found duplicate cast columns '${colName}'`,
);
// check pivot columns are not aggregated on
pivotAggColumns.forEach((col) => {
if (_findCol(pivotColumns, col.name)) {
throw new Error(
`Can't process pivot() expression: pivot column '${col.name}' must not be aggregated on`,
);
}
});
// check cast columns
// NOTE: we cannot and should not do strict checks here as cast columns are dependent on the data
// check that the columns used by pivot() as group columns are present in cast columns
const pivotGroupColumns = availableColumns.filter(
(col) =>
!(
_findCol(pivotColumns, col.name) ?? _findCol(pivotAggColumns, col.name)
),
);
pivotGroupColumns.forEach((col) => {
if (!_findCol(castColumns, col.name)) {
throw new Error(
`Can't process pivot() expression: expected pivot group column '${col.name}' in cast columns`,
);
}
});
// check that columns used in pivot() should not show up in cast columns
pivotColumns.forEach((col) => {
if (_findCol(castColumns, col.name)) {
throw new Error(
`Can't process pivot() expression: expected pivot column '${col.name}' to not present in cast columns`,
);
}
});
pivotAggColumns.forEach((col) => {
if (_findCol(castColumns, col.name)) {
throw new Error(
`Can't process pivot() expression: expected pivot aggregate column '${col.name}' to not present in cast columns`,
);
}
});
// check that cast column resulted from an aggregation (usually has name of form VAL1__|__COL1)
// has a matching aggregate column (i.e. COL1)
castColumns
.filter((col) => isPivotResultColumnName(col.name))
.forEach((col) => {
const aggColName = getPivotResultColumnBaseColumnName(col.name);
if (!_findCol(pivotAggColumns, aggColName)) {
throw new Error(
`Can't process pivot() expression: fail to match cast column '${col.name}' to a specified aggregate column`,
);
}
});
}
export function _groupBySort(
func: V1_AppliedFunction,
groupByColumns: DataCubeColumn[],
columnGetter: (name: string) => DataCubeColumn,
) {
const sortColumns = _sort(func, columnGetter);
const groupColumns = new Set(groupByColumns.map((col) => col.name));
const columnsToSort = new Set(groupByColumns.map((col) => col.name));
let sortDirection: DataCubeQuerySortDirection | undefined = undefined;
sortColumns.forEach((col) => {
if (groupColumns.has(col.name)) {
columnsToSort.delete(col.name);
} else {
throw new Error(
`Can't process groupBy() expression: sort column '${col.name}' must be a group column`,
);
}
if (!sortDirection) {
sortDirection = col.direction;
} else if (col.direction !== sortDirection) {
throw new Error(
`Can't process groupBy() expression: all group columns must be sorted in the same direction`,
);
}
});
if (columnsToSort.size !== 0) {
throw new Error(
`Can't process groupBy() expression: found unsorted group column(s) (${Array.from(
columnsToSort.values(),
)
.sort()
.map((col) => `'${col}'`)
.join(', ')})`,
);
}
_checkDuplicateColumns(
sortColumns,
(colName) =>
`Can't process groupBy() expression: found duplicate sort columns '${colName}'`,
);
return sortColumns;
}
export function _validateGroupBy(
groupBy: DataCubeSnapshotGroupBy,
groupByAggColumns: DataCubeSnapshotAggregateColumn[],
pivot: DataCubeSnapshotPivot | undefined,
pivotAggColumns: DataCubeSnapshotAggregateColumn[],
availableColumns: DataCubeColumn[],
) {
// check for duplicate columns
const groupByColumns = groupBy.columns;
_checkDuplicateColumns(
groupByColumns,
(colName) =>
`Can't process groupBy() expression: found duplicate group columns '${colName}'`,
);
_checkDuplicateColumns(
groupByAggColumns,
(colName) =>
`Can't process groupBy() expression: found duplicate aggregate columns '${colName}'`,
);
// check group columns are not aggregated on
groupByAggColumns.forEach((col) => {
if (_findCol(groupByColumns, col.name)) {
throw new Error(
`Can't process groupBy() expression: group column '${col.name}' must not be aggregated on`,
);
}
});
// check all available columns are either grouped on or aggregated on
availableColumns.forEach((col) => {
if (
!(
_findCol(groupByColumns, col.name) ??
_findCol(groupByAggColumns, col.name)
)
) {
throw new Error(
`Can't process groupBy() expression: column '${col.name}' is neither grouped nor aggregated on`,
);
}
});
// check against pivot if present
if (pivot) {
const aggCols = new Map<string, DataCubeSnapshotAggregateColumn>();
// check if aggregation specification is consistent (i.e. same name, operator, parameterValues)
// between groupBy aggregate columns
// NOTE: we should not check type here as it can change dynamically due to aggregation, e.g.
// an average aggregation on an integer-value column will result in a float-value column
groupByAggColumns
.filter((col) => isPivotResultColumnName(col.name))
.forEach((col) => {
const aggColName = getPivotResultColumnBaseColumnName(col.name);
const aggCol = {
...col,
name: aggColName,
};
const existingAggCol = aggCols.get(aggColName);
if (!existingAggCol) {
aggCols.set(aggColName, aggCol);
} else if (
// type should not be compared here as it can change dynamically due to aggregation
!deepEqual(
{ ...existingAggCol, type: undefined },
{ ...aggCol, type: undefined },
)
) {
throw new Error(
`Can't process groupBy() expression: found conflicting aggregation specification for column '${aggColName}'`,
);
}
});
// check if groupBy() aggregate columns are consistent with pivot() aggregate columns
pivotAggColumns.forEach((pivotAggCol) => {
const existingAggCol = aggCols.get(pivotAggCol.name);
if (!existingAggCol) {
throw new Error(
`Can't process groupBy() expression: column '${pivotAggCol.name}' is aggregated in pivot() expression but not in groupBy() expression`,
);
}
if (
// type should not be compared here as it can change dynamically due to aggregation
!deepEqual(
{ ...existingAggCol, type: undefined },
{ ...pivotAggCol, type: undefined },
)
) {
throw new Error(
`Can't process groupBy() expression: found conflicting aggregation specification for column '${pivotAggCol.name}'`,
);
}
});
}
}
export function _sort(
func: V1_AppliedFunction,
columnGetter: (name: string) => DataCubeColumn,
) {
return _param(
func,
0,
V1_Collection,
`Can't process sort() expression: expected parameter at index 0 to be a collection`,
).values.map((value) => {
const sortColFunc = _funcMatch(value, [
DataCubeFunction.ASCENDING,
DataCubeFunction.DESCENDING,
]);
return {
...columnGetter(_colSpecParam(sortColFunc, 0).name),
direction: matchFunctionName(
sortColFunc.function,
DataCubeFunction.ASCENDING,
)
? DataCubeQuerySortDirection.ASCENDING
: DataCubeQuerySortDirection.DESCENDING,
};
});
}