/** * 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): 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); } }