diff --git a/packages/superset-ui-chart/src/index.ts b/packages/superset-ui-chart/src/index.ts index 19e0dcb404..86a459e39e 100644 --- a/packages/superset-ui-chart/src/index.ts +++ b/packages/superset-ui-chart/src/index.ts @@ -22,7 +22,7 @@ export { default as DatasourceKey } from './query/DatasourceKey'; export { default as ChartDataProvider } from './components/ChartDataProvider'; export * from './types/Annotation'; -export * from './types/Datasource'; export * from './types/ChartFormData'; -export * from './types/Query'; +export * from './types/Datasource'; export * from './types/Metric'; +export * from './types/Query'; diff --git a/packages/superset-ui-chart/src/query/Metrics.ts b/packages/superset-ui-chart/src/query/Metrics.ts deleted file mode 100644 index 824721e1a9..0000000000 --- a/packages/superset-ui-chart/src/query/Metrics.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ChartFormData } from '../types/ChartFormData'; -import { MetricKey, Metric, FormDataMetric, AdhocMetric, ExpressionType } from '../types/Metric'; - -export const LABEL_MAX_LENGTH = 43; - -export default class Metrics { - // Use Array to maintain insertion order for metrics that are order sensitive - private metrics: Metric[]; - - constructor(formData: ChartFormData) { - this.metrics = []; - Object.keys(MetricKey).forEach(key => { - const metric = formData[MetricKey[key as keyof typeof MetricKey]]; - if (metric) { - if (Array.isArray(metric)) { - metric.forEach(m => this.addMetric(m)); - } else { - this.addMetric(metric); - } - } - }); - } - - public getMetrics() { - return this.metrics; - } - - public getLabels() { - return this.metrics.map(m => m.label); - } - - static formatMetric(metric: FormDataMetric): Metric { - let formattedMetric; - if (typeof metric === 'string') { - formattedMetric = { - label: metric, - }; - } else { - // Note we further sanitize the metric label for BigQuery datasources - // TODO: move this logic to the client once client has more info on the - // the datasource - const label = metric.label || this.getDefaultLabel(metric); - formattedMetric = { - ...metric, - label, - }; - } - - return formattedMetric; - } - - private addMetric(metric: FormDataMetric) { - this.metrics.push(Metrics.formatMetric(metric)); - } - - static getDefaultLabel(metric: AdhocMetric) { - let label: string; - if (metric.expressionType === ExpressionType.SIMPLE) { - label = `${metric.aggregate}(${metric.column.columnName})`; - } else { - label = metric.sqlExpression; - } - - return label.length <= LABEL_MAX_LENGTH - ? label - : `${label.substring(0, LABEL_MAX_LENGTH - 3)}...`; - } -} diff --git a/packages/superset-ui-chart/src/query/buildQueryObject.ts b/packages/superset-ui-chart/src/query/buildQueryObject.ts index ee23a4d6d8..dadd72d992 100644 --- a/packages/superset-ui-chart/src/query/buildQueryObject.ts +++ b/packages/superset-ui-chart/src/query/buildQueryObject.ts @@ -1,11 +1,15 @@ -import Metrics from './Metrics'; +/* eslint-disable camelcase */ import { QueryObject } from '../types/Query'; -import { ChartFormData, DruidFormData, SqlaFormData } from '../types/ChartFormData'; +import { ChartFormData, isSqlaFormData } from '../types/ChartFormData'; +import convertMetric from './convertMetric'; +import processFilters from './processFilters'; +import processMetrics from './processMetrics'; +import processExtras from './processExtras'; -const DTTM_ALIAS = '__timestamp'; +export const DTTM_ALIAS = '__timestamp'; -function getGranularity(formData: ChartFormData): string { - return 'granularity_sqla' in formData ? formData.granularity_sqla : formData.granularity; +function processGranularity(formData: ChartFormData): string { + return isSqlaFormData(formData) ? formData.granularity_sqla : formData.granularity; } // Build the common segments of all query objects (e.g. the granularity field derived from @@ -14,38 +18,40 @@ function getGranularity(formData: ChartFormData): string { // Note the type of the formData argument passed in here is the type of the formData for a // specific viz, which is a subtype of the generic formData shared among all viz types. export default function buildQueryObject(formData: T): QueryObject { - const extras = { - druid_time_origin: (formData as DruidFormData).druid_time_origin || '', - having: (formData as SqlaFormData).having || '', - having_druid: (formData as DruidFormData).having_druid || '', - time_grain_sqla: (formData as SqlaFormData).time_grain_sqla || '', - where: formData.where || '', - }; + const { + time_range, + since, + until, + columns = [], + groupby = [], + order_desc, + row_limit, + limit, + timeseries_limit_metric, + } = formData; - const { columns = [], groupby = [] } = formData; const groupbySet = new Set([...columns, ...groupby]); - const limit = formData.limit ? Number(formData.limit) : 0; - const rowLimit = Number(formData.row_limit); - const orderDesc = formData.order_desc === undefined ? true : formData.order_desc; - const isTimeseries = groupbySet.has(DTTM_ALIAS); - return { - extras, - granularity: getGranularity(formData), + const queryObject: QueryObject = { + extras: processExtras(formData), + granularity: processGranularity(formData), groupby: Array.from(groupbySet), is_prequery: false, - is_timeseries: isTimeseries, - metrics: new Metrics(formData).getMetrics(), - order_desc: orderDesc, + is_timeseries: groupbySet.has(DTTM_ALIAS), + metrics: processMetrics(formData), + order_desc: typeof order_desc === 'undefined' ? true : order_desc, orderby: [], prequeries: [], - row_limit: rowLimit, - since: formData.since, - time_range: formData.time_range, - timeseries_limit: limit, - timeseries_limit_metric: formData.timeseries_limit_metric - ? Metrics.formatMetric(formData.timeseries_limit_metric) + row_limit: Number(row_limit), + since, + time_range, + timeseries_limit: limit ? Number(limit) : 0, + timeseries_limit_metric: timeseries_limit_metric + ? convertMetric(timeseries_limit_metric) : null, - until: formData.until, + until, + ...processFilters(formData), }; + + return queryObject; } diff --git a/packages/superset-ui-chart/src/query/convertFilter.ts b/packages/superset-ui-chart/src/query/convertFilter.ts new file mode 100644 index 0000000000..713ed9374d --- /dev/null +++ b/packages/superset-ui-chart/src/query/convertFilter.ts @@ -0,0 +1,30 @@ +import { SimpleAdhocFilter, isBinaryAdhocFilter, isUnaryAdhocFilter } from '../types/Filter'; +import { QueryObjectFilterClause } from '../types/Query'; + +export default function convertFilter(filter: SimpleAdhocFilter): QueryObjectFilterClause { + const { subject } = filter; + if (isUnaryAdhocFilter(filter)) { + const { operator } = filter; + + return { + col: subject, + op: operator, + }; + } else if (isBinaryAdhocFilter(filter)) { + const { operator } = filter; + + return { + col: subject, + op: operator, + val: filter.comparator, + }; + } + + const { operator } = filter; + + return { + col: subject, + op: operator, + val: filter.comparator, + }; +} diff --git a/packages/superset-ui-chart/src/query/convertMetric.ts b/packages/superset-ui-chart/src/query/convertMetric.ts new file mode 100644 index 0000000000..d6b3930e6b --- /dev/null +++ b/packages/superset-ui-chart/src/query/convertMetric.ts @@ -0,0 +1,38 @@ +import { ChartFormDataMetric } from '../types/ChartFormData'; +import { QueryObjectMetric } from '../types/Query'; +import { AdhocMetric } from '../types/Metric'; + +export const LABEL_MAX_LENGTH = 43; + +function getDefaultLabel(metric: AdhocMetric) { + let label: string; + if (metric.expressionType === 'SIMPLE') { + label = `${metric.aggregate}(${metric.column.columnName})`; + } else { + label = metric.sqlExpression; + } + + return label.length <= LABEL_MAX_LENGTH + ? label + : `${label.substring(0, LABEL_MAX_LENGTH - 3)}...`; +} + +export default function convertMetric(metric: ChartFormDataMetric): QueryObjectMetric { + let formattedMetric; + if (typeof metric === 'string') { + formattedMetric = { + label: metric, + }; + } else { + // Note we further sanitize the metric label for BigQuery datasources + // TODO: move this logic to the client once client has more info on the + // the datasource + const label = metric.label || getDefaultLabel(metric); + formattedMetric = { + ...metric, + label, + }; + } + + return formattedMetric; +} diff --git a/packages/superset-ui-chart/src/query/processExtras.ts b/packages/superset-ui-chart/src/query/processExtras.ts new file mode 100644 index 0000000000..4c3a4724f2 --- /dev/null +++ b/packages/superset-ui-chart/src/query/processExtras.ts @@ -0,0 +1,17 @@ +/* eslint-disable camelcase */ +import { ChartFormData, isDruidFormData } from '../types/ChartFormData'; +import { QueryObjectExtras } from '../types/Query'; + +export default function processExtras(formData: ChartFormData): QueryObjectExtras { + const { where = '' } = formData; + + if (isDruidFormData(formData)) { + const { druid_time_origin, having_druid } = formData; + + return { druid_time_origin, having_druid, where }; + } + + const { time_grain_sqla, having } = formData; + + return { having, time_grain_sqla, where }; +} diff --git a/packages/superset-ui-chart/src/query/processFilters.ts b/packages/superset-ui-chart/src/query/processFilters.ts new file mode 100644 index 0000000000..0b7954a3ad --- /dev/null +++ b/packages/superset-ui-chart/src/query/processFilters.ts @@ -0,0 +1,56 @@ +import { ChartFormData } from '../types/ChartFormData'; +import { QueryObjectFilterClause } from '../types/Query'; +import { isSimpleAdhocFilter } from '../types/Filter'; +import convertFilter from './convertFilter'; + +/** Logic formerly in viz.py's process_query_filters */ +export default function processFilters(formData: ChartFormData) { + // TODO: Implement + // utils.convert_legacy_filters_into_adhoc(self.form_data) + + // TODO: Implement + // merge_extra_filters(self.form_data) + + // Split adhoc_filters into four fields according to + // (1) clause (WHERE or HAVING) + // (2) expressionType + // 2.1 SIMPLE (subject + operator + comparator) + // 2.2 SQL (freeform SQL expression)) + + // eslint-disable-next-line camelcase + const { adhoc_filters } = formData; + if (Array.isArray(adhoc_filters)) { + const simpleWhere: QueryObjectFilterClause[] = []; + const simpleHaving: QueryObjectFilterClause[] = []; + const freeformWhere: string[] = []; + const freeformHaving: string[] = []; + + adhoc_filters.forEach(filter => { + const { clause } = filter; + if (isSimpleAdhocFilter(filter)) { + const filterClause = convertFilter(filter); + if (clause === 'WHERE') { + simpleWhere.push(filterClause); + } else { + simpleHaving.push(filterClause); + } + } else { + const { sqlExpression } = filter; + if (clause === 'WHERE') { + freeformWhere.push(sqlExpression); + } else { + freeformHaving.push(sqlExpression); + } + } + }); + + return { + filters: simpleWhere, + having: freeformHaving.map(exp => `(${exp})`).join(' AND '), + having_filters: simpleHaving, + where: freeformWhere.map(exp => `(${exp})`).join(' AND '), + }; + } + + return {}; +} diff --git a/packages/superset-ui-chart/src/query/processMetrics.ts b/packages/superset-ui-chart/src/query/processMetrics.ts new file mode 100644 index 0000000000..7276088c03 --- /dev/null +++ b/packages/superset-ui-chart/src/query/processMetrics.ts @@ -0,0 +1,25 @@ +import { ChartFormData } from '../types/ChartFormData'; +import { QueryObjectMetric } from '../types/Query'; +import { MetricKey } from '../types/Metric'; +import convertMetric from './convertMetric'; + +export default function processMetrics(formData: ChartFormData) { + // Use Array to maintain insertion order + // for metrics that are order sensitive + const metrics: QueryObjectMetric[] = []; + + Object.keys(MetricKey).forEach(key => { + const metric = formData[MetricKey[key as keyof typeof MetricKey]]; + if (metric) { + if (Array.isArray(metric)) { + metric.forEach(m => { + metrics.push(convertMetric(m)); + }); + } else { + metrics.push(convertMetric(metric)); + } + } + }); + + return metrics; +} diff --git a/packages/superset-ui-chart/src/types/ChartFormData.ts b/packages/superset-ui-chart/src/types/ChartFormData.ts index 59877bdc12..5b6f71c3b8 100644 --- a/packages/superset-ui-chart/src/types/ChartFormData.ts +++ b/packages/superset-ui-chart/src/types/ChartFormData.ts @@ -1,34 +1,56 @@ /* eslint camelcase: 0 */ /* eslint-disable import/prefer-default-export */ // FormData uses snake_cased keys. -import { FormDataMetric, MetricKey } from './Metric'; +import { MetricKey, AdhocMetric } from './Metric'; import { AnnotationLayerMetadata } from './Annotation'; +import { TimeRange } from './Time'; +import { AdhocFilter } from './Filter'; -// Type signature and utility functions for formData shared by all viz types -// It will be gradually filled out as we build out the query object +export type ChartFormDataMetric = string | AdhocMetric; // Define mapped type separately to work around a limitation of TypeScript // https://github.com/Microsoft/TypeScript/issues/13573 // The Metrics in formData is either a string or a proper metric. It will be // unified into a proper Metric type during buildQuery (see `/query/Metrics.ts`). -export type Metrics = Partial>; +export type ChartFormDataMetrics = Partial< + Record +>; + +// Type signature for formData shared by all viz types +// It will be gradually filled out as we build out the query object export type BaseFormData = { + /** datasource identifier ${id}_${type} */ datasource: string; + /** + * visualization type + * - necessary if you use the plugin and want to use + * buildQuery function from the plugin. + * This must match the key used when registering the plugin. + * - not necessary if you do not plan to use the + * buildQuery function from the plugin. + * Can put "custom" (or any string) in this field in that case. + */ viz_type: string; - annotation_layers?: AnnotationLayerMetadata[]; - where?: string; + /** list of columns to group by */ groupby?: string[]; + where?: string; columns?: string[]; all_columns?: string[]; + /** list of filters */ + adhoc_filters?: AdhocFilter[]; + /** order descending */ + order_desc?: boolean; + /** limit number of time series */ limit?: number; + /** limit number of row in the results */ row_limit?: number; - order_desc?: boolean; - timeseries_limit_metric?: FormDataMetric; - time_range?: string; - since?: string; - until?: string; -} & Metrics; + /** The metric used to order timeseries for limiting */ + timeseries_limit_metric?: ChartFormDataMetric; + + annotation_layers?: AnnotationLayerMetadata[]; +} & TimeRange & + ChartFormDataMetrics; // FormData is either sqla-based or druid-based export type SqlaFormData = { @@ -44,3 +66,15 @@ export type DruidFormData = { } & BaseFormData; export type ChartFormData = SqlaFormData | DruidFormData; + +//--------------------------------------------------- +// Type guards +//--------------------------------------------------- + +export function isDruidFormData(formData: ChartFormData): formData is DruidFormData { + return 'granularity' in formData; +} + +export function isSqlaFormData(formData: ChartFormData): formData is SqlaFormData { + return 'granularity_sqla' in formData; +} diff --git a/packages/superset-ui-chart/src/types/Datasource.ts b/packages/superset-ui-chart/src/types/Datasource.ts index 480dac5a3d..03f23a29e4 100644 --- a/packages/superset-ui-chart/src/types/Datasource.ts +++ b/packages/superset-ui-chart/src/types/Datasource.ts @@ -1,5 +1,5 @@ import { Column } from './Column'; -import { Metric } from './Metric'; +import { QueryObjectMetric } from './Query'; export enum DatasourceType { Table = 'table', @@ -13,5 +13,5 @@ export interface Datasource { description?: string; type: DatasourceType; columns: Column[]; - metrics: Metric[]; + metrics: QueryObjectMetric[]; } diff --git a/packages/superset-ui-chart/src/types/Filter.ts b/packages/superset-ui-chart/src/types/Filter.ts new file mode 100644 index 0000000000..8d800c4ff7 --- /dev/null +++ b/packages/superset-ui-chart/src/types/Filter.ts @@ -0,0 +1,58 @@ +import { + UnaryOperator, + BinaryOperator, + SetOperator, + isUnaryOperator, + isBinaryOperator, + isSetOperator, +} from './Operator'; + +interface BaseSimpleAdhocFilter { + expressionType: 'SIMPLE'; + clause: 'WHERE' | 'HAVING'; + subject: string; +} + +export type UnaryAdhocFilter = BaseSimpleAdhocFilter & { + operator: UnaryOperator; +}; + +export type BinaryAdhocFilter = BaseSimpleAdhocFilter & { + operator: BinaryOperator; + comparator: string; +}; + +export type SetAdhocFilter = BaseSimpleAdhocFilter & { + operator: SetOperator; + comparator: string[]; +}; + +export type SimpleAdhocFilter = UnaryAdhocFilter | BinaryAdhocFilter | SetAdhocFilter; + +export interface FreeFormAdhocFilter { + expressionType: 'SQL'; + clause: 'WHERE' | 'HAVING'; + sqlExpression: string; +} + +export type AdhocFilter = SimpleAdhocFilter | FreeFormAdhocFilter; + +//--------------------------------------------------- +// Type guards +//--------------------------------------------------- + +export function isSimpleAdhocFilter(filter: AdhocFilter): filter is SimpleAdhocFilter { + return filter.expressionType === 'SIMPLE'; +} + +export function isUnaryAdhocFilter(filter: SimpleAdhocFilter): filter is UnaryAdhocFilter { + return isUnaryOperator(filter.operator); +} + +export function isBinaryAdhocFilter(filter: SimpleAdhocFilter): filter is BinaryAdhocFilter { + return isBinaryOperator(filter.operator); +} + +export function isSetAdhocFilter(filter: SimpleAdhocFilter): filter is SetAdhocFilter { + return isSetOperator(filter.operator); +} diff --git a/packages/superset-ui-chart/src/types/Metric.ts b/packages/superset-ui-chart/src/types/Metric.ts index 07916aea2c..a7155c97a5 100644 --- a/packages/superset-ui-chart/src/types/Metric.ts +++ b/packages/superset-ui-chart/src/types/Metric.ts @@ -15,28 +15,16 @@ export enum MetricKey { SIZE = 'size', } -export enum Aggregate { - AVG = 'AVG', - COUNT = 'COUNT ', - COUNT_DISTINCT = 'COUNT_DISTINCT', - MAX = 'MAX', - MIN = 'MIN', - SUM = 'SUM', -} - -export enum ExpressionType { - SIMPLE = 'SIMPLE', - SQL = 'SQL', -} +export type Aggregate = 'AVG' | 'COUNT' | 'COUNT_DISTINCT' | 'MAX' | 'MIN' | 'SUM'; interface AdhocMetricSimple { - expressionType: ExpressionType.SIMPLE; + expressionType: 'SIMPLE'; column: Column; aggregate: Aggregate; } interface AdhocMetricSQL { - expressionType: ExpressionType.SQL; + expressionType: 'SQL'; sqlExpression: string; } @@ -44,12 +32,3 @@ export type AdhocMetric = { label?: string; optionName?: string; } & (AdhocMetricSimple | AdhocMetricSQL); - -// Type of metrics in form data -export type FormDataMetric = string | AdhocMetric; - -// Type of Metric the client provides to server after unifying various forms -// of metrics in form data -export type Metric = { - label: string; -} & Partial; diff --git a/packages/superset-ui-chart/src/types/Operator.ts b/packages/superset-ui-chart/src/types/Operator.ts new file mode 100644 index 0000000000..cfd9153f0a --- /dev/null +++ b/packages/superset-ui-chart/src/types/Operator.ts @@ -0,0 +1,43 @@ +/** List of operators that do not require another operand */ +const UNARY_OPERATORS = ['IS NOT NULL', 'IS NULL'] as const; + +/** List of operators that require another operand that is a single value */ +const BINARY_OPERATORS = ['=', '!=', '>', '<', '>=', '<=', 'like', 'regex'] as const; + +/** List of operators that require another operand that is a set */ +const SET_OPERATORS = ['in', 'not in'] as const; + +//--------------------------------------------------- +// Derived types +//--------------------------------------------------- + +/** An operator that does not require another operand */ +export type UnaryOperator = typeof UNARY_OPERATORS[number]; + +/** An operator that requires another operand that is a single value */ +export type BinaryOperator = typeof BINARY_OPERATORS[number]; + +/** An operator that require another operand that is a set */ +export type SetOperator = typeof SET_OPERATORS[number]; + +//--------------------------------------------------- +// Type guards +//--------------------------------------------------- + +const unaryOperatorSet = new Set(UNARY_OPERATORS); + +export function isUnaryOperator(operator: string): operator is UnaryOperator { + return unaryOperatorSet.has(operator); +} + +const binaryOperatorSet = new Set(BINARY_OPERATORS); + +export function isBinaryOperator(operator: string): operator is BinaryOperator { + return binaryOperatorSet.has(operator); +} + +const setOperatorSet = new Set(SET_OPERATORS); + +export function isSetOperator(operator: string): operator is SetOperator { + return setOperatorSet.has(operator); +} diff --git a/packages/superset-ui-chart/src/types/Query.ts b/packages/superset-ui-chart/src/types/Query.ts index fcb7577f63..9b363737ff 100644 --- a/packages/superset-ui-chart/src/types/Query.ts +++ b/packages/superset-ui-chart/src/types/Query.ts @@ -1,28 +1,80 @@ /* eslint camelcase: 0 */ import { DatasourceType } from './Datasource'; import { ChartFormData } from './ChartFormData'; -import { Metric } from './Metric'; +import { AdhocMetric } from './Metric'; import ChartProps from '../models/ChartProps'; +import { BinaryOperator, SetOperator, UnaryOperator } from './Operator'; +import { TimeRange } from './Time'; -export interface QueryObject { - granularity: string; +export type QueryObjectFilterClause = { + col: string; +} & ( + | { + op: BinaryOperator; + val: string; + } + | { + op: SetOperator; + val: string[]; + } + | { + op: UnaryOperator; + }); + +export type QueryObjectMetric = { + label: string; +} & Partial; + +export type QueryObjectExtras = Partial<{ + /** HAVING condition for Druid */ + having_druid: string; + druid_time_origin: string; + /** HAVING condition for SQLAlchemy */ + having: string; + time_grain_sqla: string; + /** WHERE condition */ + where: string; +}>; + +export type QueryObject = { + /** Columns to group by */ groupby?: string[]; - metrics?: Metric[]; - extras?: { - [key: string]: string; - }; - timeseries_limit?: number; - timeseries_limit_metric?: Metric | null; - time_range?: string; - since?: string; - until?: string; + /** Metrics */ + metrics?: QueryObjectMetric[]; + + extras?: QueryObjectExtras; + + /** Granularity (for steps in time series) */ + granularity: string; + + /** Free-form WHERE SQL: multiple clauses are concatenated by AND */ + where?: string; + /** Free-form HAVING SQL, multiple clauses are concatenated by AND */ + having?: string; + /** SIMPLE having filters */ + having_filters?: QueryObjectFilterClause[]; + /** SIMPLE where filters */ + filters?: QueryObjectFilterClause[]; + + /** Maximum numbers of rows to return */ row_limit?: number; + /** Maximum number of series */ + timeseries_limit?: number; + /** The metric used to sort the returned result. */ + timeseries_limit_metric?: QueryObjectMetric | null; + + orderby?: Array<[QueryObjectMetric, boolean]>; + /** Direction to ordered by */ order_desc?: boolean; + + /** If set, will group by timestamp */ is_timeseries?: boolean; - prequeries?: string[]; + + /** TODO: Doc */ is_prequery?: boolean; - orderby?: Array<[Metric, boolean]>; -} + /** TODO: Doc */ + prequeries?: string[]; +} & TimeRange; export interface QueryContext { datasource: { diff --git a/packages/superset-ui-chart/src/types/Time.ts b/packages/superset-ui-chart/src/types/Time.ts new file mode 100644 index 0000000000..6c74b383d1 --- /dev/null +++ b/packages/superset-ui-chart/src/types/Time.ts @@ -0,0 +1,8 @@ +// eslint-disable-next-line import/prefer-default-export +export type TimeRange = { + /** Time range of the query [from, to] */ + // eslint-disable-next-line camelcase + time_range?: string; + since?: string; + until?: string; +}; diff --git a/packages/superset-ui-chart/test/query/Metrics.test.ts b/packages/superset-ui-chart/test/query/Metrics.test.ts deleted file mode 100644 index 524052a46c..0000000000 --- a/packages/superset-ui-chart/test/query/Metrics.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { ColumnType } from '../../src/types/Column'; -import { AdhocMetric, Aggregate, ExpressionType } from '../../src/types/Metric'; -import Metrics, { LABEL_MAX_LENGTH } from '../../src/query/Metrics'; - -describe('Metrics', () => { - let metrics: Metrics; - const formData = { - datasource: '5__table', - granularity_sqla: 'ds', - viz_type: 'word_cloud', - }; - - it('should build metrics for built-in metric keys', () => { - metrics = new Metrics({ - ...formData, - metric: 'sum__num', - }); - expect(metrics.getMetrics()).toEqual([{ label: 'sum__num' }]); - expect(metrics.getLabels()).toEqual(['sum__num']); - }); - - it('should build metrics for simple adhoc metrics', () => { - const adhocMetric: AdhocMetric = { - aggregate: Aggregate.AVG, - column: { - columnName: 'sum_girls', - id: 5, - type: ColumnType.BIGINT, - }, - expressionType: ExpressionType.SIMPLE, - }; - metrics = new Metrics({ - ...formData, - metric: adhocMetric, - }); - expect(metrics.getMetrics()).toEqual([ - { - aggregate: 'AVG', - column: { - columnName: 'sum_girls', - id: 5, - type: ColumnType.BIGINT, - }, - expressionType: 'SIMPLE', - label: 'AVG(sum_girls)', - }, - ]); - expect(metrics.getLabels()).toEqual(['AVG(sum_girls)']); - }); - - it('should build metrics for SQL adhoc metrics', () => { - const adhocMetric: AdhocMetric = { - expressionType: ExpressionType.SQL, - sqlExpression: 'COUNT(sum_girls)', - }; - metrics = new Metrics({ - ...formData, - metric: adhocMetric, - }); - expect(metrics.getMetrics()).toEqual([ - { - expressionType: 'SQL', - label: 'COUNT(sum_girls)', - sqlExpression: 'COUNT(sum_girls)', - }, - ]); - expect(metrics.getLabels()).toEqual(['COUNT(sum_girls)']); - }); - - it('should build metrics for adhoc metrics with custom labels', () => { - const adhocMetric: AdhocMetric = { - expressionType: ExpressionType.SQL, - label: 'foo', - sqlExpression: 'COUNT(sum_girls)', - }; - metrics = new Metrics({ - ...formData, - metric: adhocMetric, - }); - expect(metrics.getMetrics()).toEqual([ - { - expressionType: 'SQL', - label: 'foo', - sqlExpression: 'COUNT(sum_girls)', - }, - ]); - expect(metrics.getLabels()).toEqual(['foo']); - }); - - it('should truncate labels if they are too long', () => { - const adhocMetric: AdhocMetric = { - expressionType: ExpressionType.SQL, - sqlExpression: 'COUNT(verrrrrrrrry_loooooooooooooooooooooong_string)', - }; - metrics = new Metrics({ - ...formData, - metric: adhocMetric, - }); - expect(metrics.getLabels()[0].length).toBeLessThanOrEqual(LABEL_MAX_LENGTH); - }); - - it('should handle metrics arrays in form data', () => { - metrics = new Metrics({ - ...formData, - metrics: ['sum__num'], - }); - expect(metrics.getMetrics()).toEqual([{ label: 'sum__num' }]); - expect(metrics.getLabels()).toEqual(['sum__num']); - }); -}); diff --git a/packages/superset-ui-chart/test/query/convertFilter.test.ts b/packages/superset-ui-chart/test/query/convertFilter.test.ts new file mode 100644 index 0000000000..32f3e3a38d --- /dev/null +++ b/packages/superset-ui-chart/test/query/convertFilter.test.ts @@ -0,0 +1,49 @@ +import convertFilter from '../../src/query/convertFilter'; + +describe('convertFilter', () => { + it('should handle unary filter', () => { + expect( + convertFilter({ + expressionType: 'SIMPLE', + clause: 'WHERE', + subject: 'topping', + operator: 'IS NOT NULL', + }), + ).toEqual({ + col: 'topping', + op: 'IS NOT NULL', + }); + }); + + it('should convert binary filter', () => { + expect( + convertFilter({ + expressionType: 'SIMPLE', + clause: 'WHERE', + subject: 'topping', + operator: '=', + comparator: 'grass jelly', + }), + ).toEqual({ + col: 'topping', + op: '=', + val: 'grass jelly', + }); + }); + + it('should convert set filter', () => { + expect( + convertFilter({ + expressionType: 'SIMPLE', + clause: 'WHERE', + subject: 'toppings', + operator: 'in', + comparator: ['boba', 'grass jelly'], + }), + ).toEqual({ + col: 'toppings', + op: 'in', + val: ['boba', 'grass jelly'], + }); + }); +}); diff --git a/packages/superset-ui-chart/test/query/convertMetric.test.ts b/packages/superset-ui-chart/test/query/convertMetric.test.ts new file mode 100644 index 0000000000..6162ed7a9d --- /dev/null +++ b/packages/superset-ui-chart/test/query/convertMetric.test.ts @@ -0,0 +1,67 @@ +import { ColumnType } from '../../src/types/Column'; +import convertMetric, { LABEL_MAX_LENGTH } from '../../src/query/convertMetric'; + +describe('convertMetric', () => { + it('should handle string metric name', () => { + expect(convertMetric('sum__num')).toEqual({ label: 'sum__num' }); + }); + + it('should handle simple adhoc metrics', () => { + expect( + convertMetric({ + expressionType: 'SIMPLE', + aggregate: 'AVG', + column: { + columnName: 'sum_girls', + id: 5, + type: ColumnType.BIGINT, + }, + }), + ).toEqual({ + aggregate: 'AVG', + column: { + columnName: 'sum_girls', + id: 5, + type: ColumnType.BIGINT, + }, + expressionType: 'SIMPLE', + label: 'AVG(sum_girls)', + }); + }); + + it('should handle SQL adhoc metrics', () => { + expect( + convertMetric({ + expressionType: 'SQL', + sqlExpression: 'COUNT(sum_girls)', + }), + ).toEqual({ + expressionType: 'SQL', + label: 'COUNT(sum_girls)', + sqlExpression: 'COUNT(sum_girls)', + }); + }); + + it('should handle adhoc metrics with custom labels', () => { + expect( + convertMetric({ + expressionType: 'SQL', + label: 'foo', + sqlExpression: 'COUNT(sum_girls)', + }), + ).toEqual({ + expressionType: 'SQL', + label: 'foo', + sqlExpression: 'COUNT(sum_girls)', + }); + }); + + it('should truncate labels if they are too long', () => { + expect( + convertMetric({ + expressionType: 'SQL', + sqlExpression: 'COUNT(verrrrrrrrry_loooooooooooooooooooooong_string)', + }).label.length, + ).toBeLessThanOrEqual(LABEL_MAX_LENGTH); + }); +}); diff --git a/packages/superset-ui-chart/test/query/processFilters.test.ts b/packages/superset-ui-chart/test/query/processFilters.test.ts new file mode 100644 index 0000000000..faf94c2f7c --- /dev/null +++ b/packages/superset-ui-chart/test/query/processFilters.test.ts @@ -0,0 +1,114 @@ +import processFilters from '../../src/query/processFilters'; + +describe('processFilters', () => { + it('should handle non-array adhoc_filters', () => { + expect( + processFilters({ + granularity: 'something', + viz_type: 'custom', + datasource: 'boba', + }), + ).toEqual({}); + }); + + it('should handle an empty array', () => { + expect( + processFilters({ + granularity: 'something', + viz_type: 'custom', + datasource: 'boba', + adhoc_filters: [], + }), + ).toEqual({ + filters: [], + having: '', + having_filters: [], + where: '', + }); + }); + + it('should put adhoc_filters into the correct group and format accordingly', () => { + expect( + processFilters({ + granularity: 'something', + viz_type: 'custom', + datasource: 'boba', + adhoc_filters: [ + { + expressionType: 'SIMPLE', + clause: 'WHERE', + subject: 'milk', + operator: 'IS NOT NULL', + }, + { + expressionType: 'SIMPLE', + clause: 'WHERE', + subject: 'milk', + operator: '=', + comparator: 'almond', + }, + { + expressionType: 'SIMPLE', + clause: 'HAVING', + subject: 'sweetness', + operator: '>', + comparator: '0', + }, + { + expressionType: 'SIMPLE', + clause: 'HAVING', + subject: 'sweetness', + operator: '<=', + comparator: '50', + }, + { + expressionType: 'SQL', + clause: 'WHERE', + sqlExpression: 'tea = "jasmine"', + }, + { + expressionType: 'SQL', + clause: 'WHERE', + sqlExpression: 'cup = "large"', + }, + { + expressionType: 'SQL', + clause: 'HAVING', + sqlExpression: 'ice = 25 OR ice = 50', + }, + { + expressionType: 'SQL', + clause: 'HAVING', + sqlExpression: 'waitTime <= 180', + }, + ], + }), + ).toEqual({ + filters: [ + { + col: 'milk', + op: 'IS NOT NULL', + }, + { + col: 'milk', + op: '=', + val: 'almond', + }, + ], + having: '(ice = 25 OR ice = 50) AND (waitTime <= 180)', + having_filters: [ + { + col: 'sweetness', + op: '>', + val: '0', + }, + { + col: 'sweetness', + op: '<=', + val: '50', + }, + ], + where: '(tea = "jasmine") AND (cup = "large")', + }); + }); +}); diff --git a/packages/superset-ui-chart/test/query/processMetrics.test.ts b/packages/superset-ui-chart/test/query/processMetrics.test.ts new file mode 100644 index 0000000000..04a751f268 --- /dev/null +++ b/packages/superset-ui-chart/test/query/processMetrics.test.ts @@ -0,0 +1,66 @@ +import { ColumnType } from '../../src/types/Column'; +import processMetrics from '../../src/query/processMetrics'; + +describe('processMetrics', () => { + const formData = { + datasource: '5__table', + granularity_sqla: 'ds', + viz_type: 'word_cloud', + }; + + it('should handle single metric', () => { + const metrics = processMetrics({ + ...formData, + metric: 'sum__num', + }); + expect(metrics).toEqual([{ label: 'sum__num' }]); + }); + + it('should handle an array of metrics', () => { + const metrics = processMetrics({ + ...formData, + metrics: ['sum__num'], + }); + expect(metrics).toEqual([{ label: 'sum__num' }]); + }); + + it('should handle multiple types of metrics', () => { + const metrics = processMetrics({ + ...formData, + metrics: [ + 'sum__num', + { + aggregate: 'AVG', + column: { + columnName: 'sum_girls', + id: 5, + type: ColumnType.BIGINT, + }, + expressionType: 'SIMPLE', + }, + { + expressionType: 'SQL', + sqlExpression: 'COUNT(sum_girls)', + }, + ], + }); + expect(metrics).toEqual([ + { label: 'sum__num' }, + { + aggregate: 'AVG', + column: { + columnName: 'sum_girls', + id: 5, + type: ColumnType.BIGINT, + }, + expressionType: 'SIMPLE', + label: 'AVG(sum_girls)', + }, + { + expressionType: 'SQL', + label: 'COUNT(sum_girls)', + sqlExpression: 'COUNT(sum_girls)', + }, + ]); + }); +}); diff --git a/packages/superset-ui-chart/test/types/Filter.test.ts b/packages/superset-ui-chart/test/types/Filter.test.ts new file mode 100644 index 0000000000..0f6fb53378 --- /dev/null +++ b/packages/superset-ui-chart/test/types/Filter.test.ts @@ -0,0 +1,75 @@ +import { isUnaryAdhocFilter, isBinaryAdhocFilter, isSetAdhocFilter } from '../../src/types/Filter'; + +describe('Filter type guards', () => { + describe('isUnaryAdhocFilter', () => { + it('should return true when it is the correct type', () => { + expect( + isUnaryAdhocFilter({ + expressionType: 'SIMPLE', + clause: 'WHERE', + subject: 'tea', + operator: 'IS NOT NULL', + }), + ).toEqual(true); + }); + it('should return false otherwise', () => { + expect( + isUnaryAdhocFilter({ + expressionType: 'SIMPLE', + clause: 'WHERE', + subject: 'tea', + operator: '=', + comparator: 'matcha', + }), + ).toEqual(false); + }); + }); + + describe('isBinaryAdhocFilter', () => { + it('should return true when it is the correct type', () => { + expect( + isBinaryAdhocFilter({ + expressionType: 'SIMPLE', + clause: 'WHERE', + subject: 'tea', + operator: '!=', + comparator: 'matcha', + }), + ).toEqual(true); + }); + it('should return false otherwise', () => { + expect( + isBinaryAdhocFilter({ + expressionType: 'SIMPLE', + clause: 'WHERE', + subject: 'tea', + operator: 'IS NOT NULL', + }), + ).toEqual(false); + }); + }); + + describe('isSetAdhocFilter', () => { + it('should return true when it is the correct type', () => { + expect( + isSetAdhocFilter({ + expressionType: 'SIMPLE', + clause: 'WHERE', + subject: 'tea', + operator: 'in', + comparator: ['hojicha', 'earl grey'], + }), + ).toEqual(true); + }); + it('should return false otherwise', () => { + expect( + isSetAdhocFilter({ + expressionType: 'SIMPLE', + clause: 'WHERE', + subject: 'tea', + operator: 'IS NOT NULL', + }), + ).toEqual(false); + }); + }); +}); diff --git a/packages/superset-ui-demo/.storybook/webpack.config.js b/packages/superset-ui-demo/.storybook/webpack.config.js index 98f6cdb79f..42533ec3b3 100644 --- a/packages/superset-ui-demo/.storybook/webpack.config.js +++ b/packages/superset-ui-demo/.storybook/webpack.config.js @@ -9,6 +9,7 @@ module.exports = async ({ config }) => { ], }, test: /\.tsx?$/, + exclude: /node_modules/, }); config.resolve.extensions.push('.ts', '.tsx');