blob: e02ab5f4fdb84fae2dfb09b79f70ed89c5ad61b2 [file] [log] [blame]
/*
* 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 { Column, SqlBase, SqlQuery } from 'druid-query-toolkit';
import { C, F, filterMap, L, SqlAlias, SqlExpression, SqlFunction } from 'druid-query-toolkit';
import { uniq } from '../../../utils';
import type { ExpressionMetaValue } from './expression-meta';
import { ExpressionMeta } from './expression-meta';
import { MeasurePattern } from './measure-pattern';
export interface MeasureValue extends ExpressionMetaValue {
formatter?: (v: any) => string;
}
export class Measure extends ExpressionMeta {
static AGGREGATE = 'AGGREGATE';
static MAX_NAME_LENGTH = 100;
static COUNT: Measure;
static getAggregateMeasureName(expression: SqlBase): string | undefined {
if (
expression instanceof SqlFunction &&
expression.getEffectiveFunctionName() === Measure.AGGREGATE
) {
const arg0 = expression.getArgAsString(0);
if (arg0) return arg0;
}
return;
}
static inflate(value: any): Measure | undefined {
if (!value) return;
const expression = SqlExpression.maybeParse(value.expression);
if (!expression) return;
return new Measure({ ...value, expression });
}
static inflateArray(value: any): Measure[] {
if (!Array.isArray(value)) return [];
return filterMap(value, Measure.inflate);
}
static columnToAggregateExpression(column: Column): SqlExpression | undefined {
const { name, sqlType } = column;
const c = C(name);
switch (sqlType) {
case 'BIGINT':
case 'FLOAT':
case 'DOUBLE':
return F.sum(c);
case 'VARCHAR':
return F.countDistinct(c);
default:
return;
}
}
static getPossibleMeasuresForColumn(column: Column): Measure[] {
if (column.isTimeColumn()) {
return [
new Measure({
expression: F.max(C(column.name)),
}),
new Measure({
expression: F.min(C(column.name)),
}),
];
}
switch (column.nativeType) {
case 'LONG':
case 'FLOAT':
case 'DOUBLE':
return [
new Measure({
expression: F.sum(C(column.name)),
}),
new Measure({
expression: F.max(C(column.name)),
}),
new Measure({
expression: F.min(C(column.name)),
}),
new Measure({
as: `P98 ${column.name}`,
expression: F('APPROX_QUANTILE_DS', C(column.name), 0.98),
}),
new Measure({
expression: SqlFunction.countDistinct(C(column.name)),
}),
];
case 'STRING':
case 'COMPLEX':
case 'COMPLEX<hyperUnique>':
return [
new Measure({
expression: SqlFunction.countDistinct(C(column.name)),
}),
];
case 'COMPLEX<HLLSketch>':
return [
new Measure({
expression: F('APPROX_COUNT_DISTINCT_DS_HLL', C(column.name)),
}),
];
case 'COMPLEX<thetaSketch>':
return [
new Measure({
expression: F('APPROX_COUNT_DISTINCT_DS_THETA', C(column.name)),
}),
];
case 'COMPLEX<quantilesDoublesSketch>':
return [
new Measure({
as: `P98 ${column.name}`,
expression: F('APPROX_QUANTILE_DS', C(column.name), 0.98),
}),
new Measure({
as: `P95 ${column.name}`,
expression: F('APPROX_QUANTILE_DS', C(column.name), 0.95),
}),
new Measure({
as: `Median ${column.name}`,
expression: F('APPROX_QUANTILE_DS', C(column.name), 0.5),
}),
];
default:
return [];
}
}
static extractQueryMeasures(query: SqlQuery): Measure[] {
if (query.hasGroupBy()) return [];
return filterMap(query.getSpace('preFromClause', '').split('\n'), line => {
const m = /^\s*--:MEASURE\s+(.+)$/i.exec(line);
if (!m) return;
const ex = SqlExpression.maybeParse(m[1]);
if (!(ex instanceof SqlAlias)) return;
return new Measure({
as: ex.getAliasName(),
expression: ex.getUnderlyingExpression(),
});
});
}
static addMeasuresToQuery(query: SqlQuery, measures: Measure[]): SqlQuery {
if (query.hasGroupBy()) throw new Error('can not addMeasure comments to a Group by query');
return query.changeSpace(
'preFromClause',
'\n' +
measures.map(measure => ` --:MEASURE ${measure.expression.as(measure.name)}`).join('\n') +
'\n',
);
}
static defaultNameFromExpression(expression: SqlExpression): string {
const measurePattern = MeasurePattern.fit(expression);
if (measurePattern) {
return measurePattern.prettyPrint();
}
const aggregateMeasureName = Measure.getAggregateMeasureName(expression);
if (aggregateMeasureName) return aggregateMeasureName;
return ExpressionMeta.defaultNameFromExpression(expression);
}
public readonly formatter?: (v: any) => string;
constructor(value: MeasureValue) {
super(value);
(this as any).name = this.as || Measure.defaultNameFromExpression(this.expression);
}
public equals(other: Measure | undefined): boolean {
return (
other instanceof Measure &&
this.name === other.name &&
this.expression.equals(other.expression)
);
}
public equivalent(other: Measure | undefined): boolean {
if (!other || this.name !== other.name) return false;
if (Measure.getAggregateMeasureName(this.expression) === other.name) {
return true;
}
if (Measure.getAggregateMeasureName(other.expression) === this.name) {
return true;
}
return this.expression.equals(other.expression);
}
public isSavedMeasure(): boolean {
const { expression } = this;
return (
expression instanceof SqlFunction && expression.getEffectiveFunctionName() === 'AGGREGATE'
);
}
public toAggregateBasedMeasure(): Measure {
return this.changeExpression(F(Measure.AGGREGATE, L(this.name)));
}
public getAggregateMeasureName(): string | undefined {
return Measure.getAggregateMeasureName(this.expression);
}
public getUsedAggregates(): string[] {
return uniq(
filterMap(
this.expression.collect((ex): ex is SqlFunction =>
Boolean(Measure.getAggregateMeasureName(ex)),
),
ex => ex.getArgAsString(0),
),
);
}
}
Measure.COUNT = new Measure({
expression: SqlFunction.COUNT_STAR,
});