Files
superset2/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.ts
2026-01-06 10:52:58 -08:00

227 lines
6.9 KiB
TypeScript

/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 type { AdhocMetric as CoreAdhocMetric } from '@superset-ui/core';
import {
sqlaAutoGeneratedMetricRegex,
AGGREGATES,
} from 'src/explore/constants';
export const EXPRESSION_TYPES = {
SIMPLE: 'SIMPLE',
SQL: 'SQL',
};
interface ColumnType {
column_name: string;
verbose_name?: string;
// Allow additional properties from ColumnMeta and other column types
[key: string]: unknown;
}
interface AdhocMetricInput {
expressionType?: string;
column?: ColumnType | null;
aggregate?: string | null;
sqlExpression?: string | null;
datasourceWarning?: boolean;
hasCustomLabel?: boolean;
label?: string;
optionName?: string;
// Additional properties that may be passed in
metric_name?: string;
expression?: string;
error_text?: string;
uuid?: string;
[key: string]: unknown;
}
function inferSqlExpressionColumn(
adhocMetric: AdhocMetricInput,
): string | null {
if (
adhocMetric.sqlExpression &&
sqlaAutoGeneratedMetricRegex.test(adhocMetric.sqlExpression)
) {
const indexFirstCloseParen = adhocMetric.sqlExpression.indexOf(')');
const indexPairedOpenParen = adhocMetric.sqlExpression
.substring(0, indexFirstCloseParen)
.lastIndexOf('(');
if (indexFirstCloseParen > 0 && indexPairedOpenParen > 0) {
return adhocMetric.sqlExpression.substring(
indexPairedOpenParen + 1,
indexFirstCloseParen,
);
}
}
return null;
}
function inferSqlExpressionAggregate(
adhocMetric: AdhocMetricInput,
): string | null {
if (
adhocMetric.sqlExpression &&
sqlaAutoGeneratedMetricRegex.test(adhocMetric.sqlExpression)
) {
const indexFirstOpenParen = adhocMetric.sqlExpression.indexOf('(');
if (indexFirstOpenParen > 0) {
return adhocMetric.sqlExpression.substring(0, indexFirstOpenParen);
}
}
return null;
}
/**
* Adapter function to create an AdhocMetric instance from a core AdhocMetric type.
* This bridges the type gap between @superset-ui/core's AdhocMetric and the local class.
*/
export function fromCoreAdhocMetric(metric: CoreAdhocMetric): AdhocMetric {
return new AdhocMetric(metric as AdhocMetricInput);
}
/**
* Type guard to check if an object can be used to construct an AdhocMetric.
* Returns true for plain objects that have metric-like properties.
*/
export function isDictionaryForAdhocMetric(
value: unknown,
): value is AdhocMetricInput {
return (
typeof value === 'object' &&
value !== null &&
!(value instanceof AdhocMetric) &&
('expressionType' in value ||
'column' in value ||
'aggregate' in value ||
'sqlExpression' in value ||
'metric_name' in value)
);
}
export default class AdhocMetric {
expressionType: string;
column?: ColumnType | null;
aggregate?: string | null;
sqlExpression?: string | null;
datasourceWarning: boolean;
hasCustomLabel: boolean;
label: string;
optionName: string;
constructor(adhocMetric: AdhocMetricInput) {
this.expressionType = adhocMetric.expressionType || EXPRESSION_TYPES.SIMPLE;
if (this.expressionType === EXPRESSION_TYPES.SIMPLE) {
// try to be clever in the case of transitioning from Sql expression back to simple expression
const inferredColumn = inferSqlExpressionColumn(adhocMetric);
this.column =
adhocMetric.column ??
(inferredColumn ? { column_name: inferredColumn } : null);
this.aggregate =
adhocMetric.aggregate || inferSqlExpressionAggregate(adhocMetric);
this.sqlExpression = null;
} else if (this.expressionType === EXPRESSION_TYPES.SQL) {
this.sqlExpression = adhocMetric.sqlExpression;
this.column = null;
this.aggregate = null;
}
this.datasourceWarning = !!adhocMetric.datasourceWarning;
this.hasCustomLabel = !!(adhocMetric.hasCustomLabel && adhocMetric.label);
this.label = this.hasCustomLabel
? (adhocMetric.label ?? this.getDefaultLabel())
: this.getDefaultLabel();
this.optionName =
adhocMetric.optionName ||
`metric_${Math.random().toString(36).substring(2, 15)}_${Math.random()
.toString(36)
.substring(2, 15)}`;
}
getDefaultLabel(): string {
return this.translateToSql({ useVerboseName: true });
}
translateToSql(
params: { useVerboseName?: boolean; transformCountDistinct?: boolean } = {
useVerboseName: false,
transformCountDistinct: false,
},
): string {
if (this.expressionType === EXPRESSION_TYPES.SIMPLE) {
const aggregate = this.aggregate || '';
// eslint-disable-next-line camelcase
const column =
params.useVerboseName && this.column?.verbose_name
? `(${this.column.verbose_name})`
: this.column?.column_name
? `(${this.column.column_name})`
: '';
// transform from `count_distinct(column)` to `count(distinct column)`
if (
params.transformCountDistinct &&
aggregate === AGGREGATES.COUNT_DISTINCT &&
/^\(.*\)$/.test(column)
) {
return `COUNT(DISTINCT ${column.slice(1, -1)})`;
}
return aggregate + column;
}
if (this.expressionType === EXPRESSION_TYPES.SQL) {
return this.sqlExpression ?? '';
}
return '';
}
duplicateWith(nextFields: Partial<AdhocMetricInput>): AdhocMetric {
return new AdhocMetric({
...this,
...nextFields,
});
}
equals(adhocMetric: AdhocMetric): boolean {
return (
adhocMetric.label === this.label &&
adhocMetric.expressionType === this.expressionType &&
adhocMetric.sqlExpression === this.sqlExpression &&
adhocMetric.aggregate === this.aggregate &&
(adhocMetric.column && adhocMetric.column.column_name) ===
(this.column && this.column.column_name)
);
}
isValid(): boolean {
if (this.expressionType === EXPRESSION_TYPES.SIMPLE) {
return !!(this.column && this.aggregate);
}
if (this.expressionType === EXPRESSION_TYPES.SQL) {
return !!this.sqlExpression;
}
return false;
}
inferSqlExpressionAggregate(): string | null {
return inferSqlExpressionAggregate(this as unknown as AdhocMetricInput);
}
inferSqlExpressionColumn(): string | null {
return inferSqlExpressionColumn(this as unknown as AdhocMetricInput);
}
}