From cab00648345fa3c476ac083754f3f63c2f3fce63 Mon Sep 17 00:00:00 2001 From: Tom Robinson <tlrobinson@gmail.com> Date: Sat, 10 Dec 2016 13:35:13 -0800 Subject: [PATCH] Only show valid aggregations and fields --- .../src/metabase/lib/expressions/formatter.js | 4 +- .../src/metabase/lib/expressions/index.js | 21 +++-- .../src/metabase/lib/expressions/parser.js | 78 +++++++++---------- .../expressions/ExpressionEditorTextfield.jsx | 2 +- 4 files changed, 53 insertions(+), 52 deletions(-) diff --git a/frontend/src/metabase/lib/expressions/formatter.js b/frontend/src/metabase/lib/expressions/formatter.js index 2e3a2f670c1..ae88983bdfb 100644 --- a/frontend/src/metabase/lib/expressions/formatter.js +++ b/frontend/src/metabase/lib/expressions/formatter.js @@ -48,7 +48,7 @@ function formatField([, fieldId], { tableMetadata: { fields } }) { if (!field) { throw 'field with ID does not exist: ' + fieldId; } - return formatFieldName(field.display_name); + return formatFieldName(field); } function formatMetric([, metricId], { tableMetadata: { metrics } }) { @@ -56,7 +56,7 @@ function formatMetric([, metricId], { tableMetadata: { metrics } }) { if (!metric) { throw 'metric with ID does not exist: ' + metricId; } - return formatMetricName(metric.name) + "()"; + return formatMetricName(metric) + "()"; } function formatExpressionReference([, expressionName]) { diff --git a/frontend/src/metabase/lib/expressions/index.js b/frontend/src/metabase/lib/expressions/index.js index 05b2e156484..c6d40065628 100644 --- a/frontend/src/metabase/lib/expressions/index.js +++ b/frontend/src/metabase/lib/expressions/index.js @@ -16,23 +16,30 @@ export const VALID_AGGREGATIONS = new Map(Object.entries({ "sum": "Sum", "cum_sum": "CumulativeSum", "distinct": "Distinct", + "stddev": "StandardDeviation", "avg": "Average", "min": "Min", "max": "Max" })); -export function formatMetricName(metricName) { - return titleize(metricName).replace(/\W+/g, "") +export function formatAggregationName(aggregationOption) { + return VALID_AGGREGATIONS.get(aggregationOption.short); } -export function formatFieldName(fieldName) { - return /^\w+$/.test(fieldName) ? - fieldName : - JSON.stringify(fieldName); +export function formatMetricName(metric) { + return titleize(metric.name).replace(/\W+/g, "") +} + +export function formatFieldName(field) { + return /^\w+$/.test(field.display_name) ? + field.display_name : + JSON.stringify(field.display_name); } export function formatExpressionName(name) { - return formatFieldName(name); + return /^\w+$/.test(name) ? + name : + JSON.stringify(name); } // move to query lib diff --git a/frontend/src/metabase/lib/expressions/parser.js b/frontend/src/metabase/lib/expressions/parser.js index 1b49afef099..3b5004c569e 100644 --- a/frontend/src/metabase/lib/expressions/parser.js +++ b/frontend/src/metabase/lib/expressions/parser.js @@ -1,18 +1,7 @@ const { Lexer, Parser, extendToken, getImage } = require("chevrotain"); const _ = require("underscore"); -import { VALID_AGGREGATIONS, formatFieldName, formatMetricName, formatExpressionName } from "../expressions"; - -export const AGGREGATION_ARITY = new Map([ - ["Count", 0], - ["CumulativeCount", 0], - ["Sum", 1], - ["CumulativeSum", 1], - ["Distinct", 1], - ["Average", 1], - ["Min", 1], - ["Max", 1] -]); +import { VALID_AGGREGATIONS, formatFieldName, formatMetricName, formatExpressionName, formatAggregationName } from "../expressions"; const AdditiveOperator = extendToken("AdditiveOperator", Lexer.NA); const Plus = extendToken("Plus", /\+/, AdditiveOperator); @@ -190,7 +179,7 @@ class ExpressionsParser extends Parser { getMetricForName(metricName) { const metrics = this._options.tableMetadata && this._options.tableMetadata.metrics; - return _.find(metrics, (metric) => formatMetricName(metric.name) === metricName); + return _.find(metrics, (metric) => formatMetricName(metric) === metricName); } } @@ -245,6 +234,9 @@ export function suggest(source, { let finalSuggestions = [] + // TODO: is there a better way to figure out which aggregation we're inside of? + const currentAggregationToken = _.find(assistanceTokenVector.slice().reverse(), (t) => t instanceof Aggregation); + for (const suggestion of syntacticSuggestions) { const { nextTokenType, ruleStack } = suggestion; // no nesting of aggregations or field references outside of aggregations @@ -258,8 +250,8 @@ export function suggest(source, { type: "operators", name: getTokenSource(token), text: " " + getTokenSource(token) + " ", - prefixTrim: /\s+$/, - postfixTrim: /^\s+/ + prefixTrim: /\s*$/, + postfixTrim: /^\s*[*/+-]?\s*/ }))) } else if (nextTokenType === LParen) { finalSuggestions.push({ @@ -267,53 +259,55 @@ export function suggest(source, { name: "(", text: " (", postfixText: ")", - prefixTrim: /\s+$/, - postfixTrim: /^\s+/ + prefixTrim: /\s*$/, + postfixTrim: /^\s*\(?\s*/ }); } else if (nextTokenType === RParen) { finalSuggestions.push({ type: "other", name: ")", text: ") ", - prefixTrim: /\s+$/, - postfixTrim: /^\s+/ + prefixTrim: /\s*$/, + postfixTrim: /^\s*\)?\s*/ }); } else if (nextTokenType === Identifier || nextTokenType === StringLiteral) { - if (!outsideAggregation) { - finalSuggestions.push(...tableMetadata.fields.map(field => ({ - type: "fields", - name: field.display_name, - text: formatFieldName(field.display_name) + " ", - prefixTrim: /\w+$/, - postfixTrim: /^\w+\s*/ - }))) - finalSuggestions.push(...Object.keys(customFields || {}).map(expressionName => ({ - type: "fields", - name: expressionName, - text: formatExpressionName(expressionName) + " ", - prefixTrim: /\w+$/, - postfixTrim: /^\w+\s*/ - }))) + if (!outsideAggregation && currentAggregationToken) { + let aggregationShort = aggregationsMap.get(getImage(currentAggregationToken)); + let aggregationOption = _.findWhere(tableMetadata.aggregation_options, { short: aggregationShort }); + if (aggregationOption && aggregationOption.fields.length > 0) { + finalSuggestions.push(...aggregationOption.fields[0].map(field => ({ + type: "fields", + name: field.display_name, + text: formatFieldName(field) + " ", + prefixTrim: /\w+$/, + postfixTrim: /^\w+\s*/ + }))) + finalSuggestions.push(...Object.keys(customFields || {}).map(expressionName => ({ + type: "fields", + name: expressionName, + text: formatExpressionName(expressionName) + " ", + prefixTrim: /\w+$/, + postfixTrim: /^\w+\s*/ + }))) + } } } else if (nextTokenType === Aggregation) { if (outsideAggregation) { - let tokens = getSubTokenTypes(nextTokenType); - finalSuggestions.push(...tokens.map(token => { - let text = getTokenSource(token); - let arity = AGGREGATION_ARITY.get(text); + finalSuggestions.push(...tableMetadata.aggregation_options.filter(a => formatAggregationName(a)).map(aggregationOption => { + const arity = aggregationOption.fields.length; return { type: "aggregations", - name: text, - text: text + "(" + (arity > 0 ? "" : ")"), + name: formatAggregationName(aggregationOption), + text: formatAggregationName(aggregationOption) + "(" + (arity > 0 ? "" : ")"), postfixText: arity > 0 ? ")" : "", prefixTrim: /\w+$/, postfixTrim: /^\w+\(\)?/ - } + }; })); finalSuggestions.push(...tableMetadata.metrics.map(metric => ({ type: "metrics", name: metric.name, - text: formatMetricName(metric.name) + "() ", + text: formatMetricName(metric) + "() ", prefixTrim: /\w+$/, postfixTrim: /^\w+\(\)?/ }))) diff --git a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx index dc5e8585aae..72fc3ccf5e0 100644 --- a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx +++ b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx @@ -245,7 +245,7 @@ export default class ExpressionEditorTextfield extends Component { targetAttachment: 'bottom left' }} > - <ul style={{minWidth: 150, maxHeight: 342, overflow: "hidden"}}> + <ul style={{minWidth: 150, overflow: "hidden"}}> {suggestions.map((suggestion, i) => // insert section title. assumes they're sorted by type [(i === 0 || suggestion.type !== suggestions[i - 1].type) && -- GitLab