From 9f1e07d65ec653469126ba506d06c2f01b308805 Mon Sep 17 00:00:00 2001 From: Tom Robinson <tlrobinson@gmail.com> Date: Wed, 14 Dec 2016 14:11:36 -0800 Subject: [PATCH] Update parser/formatter to use string literal or identifier for metric names --- .../src/metabase/lib/expressions/formatter.js | 2 +- .../src/metabase/lib/expressions/index.js | 3 +- .../src/metabase/lib/expressions/parser.js | 46 ++++++++++--------- .../unit/lib/expressions/formatter.spec.js | 2 +- .../test/unit/lib/expressions/parser.spec.js | 6 +-- 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/frontend/src/metabase/lib/expressions/formatter.js b/frontend/src/metabase/lib/expressions/formatter.js index 84c0645f6d0..c089d8cd1d1 100644 --- a/frontend/src/metabase/lib/expressions/formatter.js +++ b/frontend/src/metabase/lib/expressions/formatter.js @@ -56,7 +56,7 @@ function formatMetric([, metricId], { tableMetadata: { metrics } }) { if (!metric) { throw 'metric with ID does not exist: ' + metricId; } - return formatMetricName(metric) + "()"; + 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 39a549a9d27..e12463eba03 100644 --- a/frontend/src/metabase/lib/expressions/index.js +++ b/frontend/src/metabase/lib/expressions/index.js @@ -1,7 +1,6 @@ import _ from "underscore"; import { mbqlEq } from "../query/util"; -import { titleize } from "../formatting"; import { VALID_OPERATORS, VALID_AGGREGATIONS } from "./tokens"; @@ -18,7 +17,7 @@ function formatIdentifier(name) { } export function formatMetricName(metric) { - return titleize(metric.name).replace(/\W+/g, "") + return formatIdentifier(metric.name); } export function formatFieldName(field) { diff --git a/frontend/src/metabase/lib/expressions/parser.js b/frontend/src/metabase/lib/expressions/parser.js index 44ce34131e9..bed0d71e813 100644 --- a/frontend/src/metabase/lib/expressions/parser.js +++ b/frontend/src/metabase/lib/expressions/parser.js @@ -7,7 +7,7 @@ import { formatFieldName, formatMetricName, formatExpressionName, formatAggregat import { VALID_AGGREGATIONS, allTokens, - LParen, RParen, Comma, + LParen, RParen, AdditiveOperator, MultiplicativeOperator, Aggregation, NullaryAggregation, UnaryAggregation, StringLiteral, NumberLiteral, @@ -20,7 +20,14 @@ const aggregationsMap = new Map(Array.from(VALID_AGGREGATIONS).map(([a,b]) => [b class ExpressionsParser extends Parser { constructor(input, options = {}) { - super(input, allTokens/*, { recoveryEnabled: false }*/); + const parserOptions = { + // recoveryEnabled: false, + ignoredIssues: { + // uses GATE to disambiguate fieldName and metricName + atomicExpression: { OR1: true } + } + }; + super(input, allTokens, parserOptions); let $ = this; @@ -88,19 +95,16 @@ class ExpressionsParser extends Parser { }); $.RULE("metricExpression", () => { - const metricName = $.SUBRULE($.identifier); - const lParen = $.CONSUME(LParen); - const rParen = $.CONSUME(RParen); - // const metricName = $.OR([ - // {ALT: () => $.SUBRULE($.stringLiteral) }, - // {ALT: () => $.SUBRULE($.identifier) } - // ]); + const metricName = $.OR([ + {ALT: () => $.SUBRULE($.stringLiteral) }, + {ALT: () => $.SUBRULE($.identifier) } + ]); const metric = this.getMetricForName(this._toString(metricName)); if (metric != null) { - return this._metricReference(metricName, lParen, rParen, metric.id); + return this._metricReference(metricName, metric.id); } - return this._unknownMetric(metricName, lParen, rParen); + return this._unknownMetric(metricName); }); $.RULE("fieldExpression", () => { @@ -169,7 +173,7 @@ class ExpressionsParser extends Parser { getMetricForName(metricName) { const metrics = this._options.tableMetadata && this._options.tableMetadata.metrics; - return _.find(metrics, (metric) => formatMetricName(metric) === metricName); + return _.find(metrics, (metric) => metric.name.toLowerCase() === metricName.toLowerCase()); } } @@ -189,7 +193,7 @@ class ExpressionsParserMBQL extends ExpressionsParser { const agg = aggregationsMap.get(aggregation.image) return arg == null ? [agg] : [agg, arg]; } - _metricReference(metricName, lParen, rParen, metricId) { + _metricReference(metricName, metricId) { return ["METRIC", metricId]; } _fieldReference(fieldName, fieldId) { @@ -201,7 +205,7 @@ class ExpressionsParserMBQL extends ExpressionsParser { _unknownField(fieldName) { throw new Error("Unknown field \"" + fieldName + "\""); } - _unknownMetric(metricName, lParen, rParen) { + _unknownMetric(metricName) { throw new Error("Unknown metric \"" + metricName + "\""); } @@ -240,8 +244,8 @@ class ExpressionsParserSyntax extends ExpressionsParser { _aggregation(aggregation, lParen, arg, rParen) { return syntax("aggregation", token(aggregation), token(lParen), arg, token(rParen)); } - _metricReference(metricName, lParen, rParen, metricId) { - return syntax("metric", metricName, token(lParen), token(rParen)); + _metricReference(metricName, metricId) { + return syntax("metric", metricName); } _fieldReference(fieldName, fieldId) { return syntax("field", fieldName); @@ -252,8 +256,8 @@ class ExpressionsParserSyntax extends ExpressionsParser { _unknownField(fieldName) { return syntax("unknown", fieldName); } - _unknownMetric(metricName, lParen, rParen) { - return syntax("unknown", metricName, lParen, rParen); + _unknownMetric(metricName) { + return syntax("unknown", metricName); } _identifier(identifier) { @@ -399,7 +403,7 @@ export function suggest(source, { postfixTrim: /^\w+\s*/ }))); } - } else if (nextTokenType === Aggregation || nextTokenType === NullaryAggregation || nextTokenType === UnaryAggregation) { + } else if (nextTokenType === Aggregation || nextTokenType === NullaryAggregation || nextTokenType === UnaryAggregation || nextTokenType === Identifier || nextTokenType === StringLiteral) { if (outsideAggregation) { finalSuggestions.push(...tableMetadata.aggregation_options.filter(a => formatAggregationName(a)).map(aggregationOption => { const arity = aggregationOption.fields.length; @@ -415,9 +419,9 @@ export function suggest(source, { finalSuggestions.push(...tableMetadata.metrics.map(metric => ({ type: "metrics", name: metric.name, - text: formatMetricName(metric) + "()", + text: formatMetricName(metric), prefixTrim: /\w+$/, - postfixTrim: /^\w+(\(\)?|$)/ + postfixTrim: /^\w+\s*/ }))) } } else if (nextTokenType === NumberLiteral) { diff --git a/frontend/test/unit/lib/expressions/formatter.spec.js b/frontend/test/unit/lib/expressions/formatter.spec.js index e56204e7159..0cd7715fcd1 100644 --- a/frontend/test/unit/lib/expressions/formatter.spec.js +++ b/frontend/test/unit/lib/expressions/formatter.spec.js @@ -44,7 +44,7 @@ describe("lib/expressions/parser", () => { }); it("expression with metric", () => { - expect(format(["+", 1, ["METRIC", 1]], mockMetadata)).toEqual("1 + FooBar()"); + expect(format(["+", 1, ["METRIC", 1]], mockMetadata)).toEqual("1 + \"foo bar\""); }); it("expression with custom field", () => { diff --git a/frontend/test/unit/lib/expressions/parser.spec.js b/frontend/test/unit/lib/expressions/parser.spec.js index a3a7d020dab..b35b2763aeb 100644 --- a/frontend/test/unit/lib/expressions/parser.spec.js +++ b/frontend/test/unit/lib/expressions/parser.spec.js @@ -92,7 +92,7 @@ describe("lib/expressions/parser", () => { expect(cleanSuggestions(suggest("1 + ", aggregationOpts))).toEqual([ { type: 'aggregations', text: 'Count ' }, { type: 'aggregations', text: 'Sum(' }, - { type: 'metrics', text: 'FooBar()' }, + { type: 'metrics', text: '"foo bar"' }, { type: 'other', text: ' (' }, ]); }) @@ -119,13 +119,13 @@ describe("lib/expressions/parser", () => { describe("compile() in syntax mode", () => { it ("should parse source without whitespace into a recoverable syntax tree", () => { - const source = "1-Sum(A*2+\"Toucan Sam\")/Count()+FooBar()"; + const source = "1-Sum(A*2+\"Toucan Sam\")/Count()+\"foo bar\""; const tree = parse(source, aggregationOpts); expect(serialize(tree)).toEqual(source) }) xit ("should parse source with whitespace into a recoverable syntax tree", () => { // FIXME: not preserving whitespace - const source = "1 - Sum(A * 2 + \"Toucan Sam\") / Count + FooBar()"; + const source = "1 - Sum(A * 2 + \"Toucan Sam\") / Count + \"foo bar\""; const tree = parse(source, aggregationOpts); expect(serialize(tree)).toEqual(source) }) -- GitLab