From 4a76c4b05395dd0033491b4b48ea41b42c913bb0 Mon Sep 17 00:00:00 2001
From: Ariya Hidayat <ariya@metabase.com>
Date: Fri, 11 Feb 2022 08:49:15 -0800
Subject: [PATCH] Fix parsing of some aggregation functions with no args
 (#20432)

In an expression `COUNT/2`, `COUNT` there should be treated as a
function (i.e. `COUNT()`) instead of a field.
Another function is the same category is `CUMULATIVECOUNT`.
Both don't require any arguments, hence the parentheses are optional.
---
 .../metabase/lib/expressions/diagnostics.js   |  7 ++++++-
 .../lib/expressions/recursive-parser.js       | 19 +++++++++++++++++++
 .../src/metabase/lib/expressions/resolver.js  |  3 +++
 .../expressions/recursive-parser.unit.spec.js |  5 +++++
 4 files changed, 33 insertions(+), 1 deletion(-)

diff --git a/frontend/src/metabase/lib/expressions/diagnostics.js b/frontend/src/metabase/lib/expressions/diagnostics.js
index edc6801c1ff..7e7627b74fe 100644
--- a/frontend/src/metabase/lib/expressions/diagnostics.js
+++ b/frontend/src/metabase/lib/expressions/diagnostics.js
@@ -1,6 +1,7 @@
 import { t } from "ttag";
 
 import {
+  MBQL_CLAUSES,
   getMBQLName,
   parseDimension,
   parseMetric,
@@ -17,6 +18,7 @@ import {
   useShorthands,
   adjustCase,
   adjustOptions,
+  transformNoArgFunction,
 } from "metabase/lib/expressions/recursive-parser";
 import { tokenize, TOKEN, OPERATOR } from "metabase/lib/expressions/tokenizer";
 
@@ -43,7 +45,9 @@ export function diagnose(source, startRule, query) {
     const token = tokens[i];
     if (token.type === TOKEN.Identifier && source[token.start] !== "[") {
       const functionName = source.slice(token.start, token.end);
-      if (getMBQLName(functionName)) {
+      const fn = getMBQLName(functionName);
+      const clause = fn ? MBQL_CLAUSES[fn] : null;
+      if (clause && clause.args.length > 0) {
         const next = tokens[i + 1];
         if (next.op !== OPERATOR.OpenParenthesis) {
           return {
@@ -121,6 +125,7 @@ function prattCompiler(source, startRule, query) {
       passes: [
         adjustOptions,
         useShorthands,
+        transformNoArgFunction,
         adjustCase,
         expr => resolve(expr, startRule, resolveMBQLField),
       ],
diff --git a/frontend/src/metabase/lib/expressions/recursive-parser.js b/frontend/src/metabase/lib/expressions/recursive-parser.js
index 0f6328861c4..d7287b6febe 100644
--- a/frontend/src/metabase/lib/expressions/recursive-parser.js
+++ b/frontend/src/metabase/lib/expressions/recursive-parser.js
@@ -303,11 +303,30 @@ export const adjustCase = tree =>
     return node;
   });
 
+// [ "dimension", "count "] becomes ["count"], since Count is a valid no-arg function
+export const transformNoArgFunction = tree =>
+  modify(tree, node => {
+    if (Array.isArray(node)) {
+      const [operator, ...operands] = node;
+      if (operands.length === 1 && operator === "dimension") {
+        const text = operands[0];
+        const fn = getMBQLName(text.trim().toLowerCase());
+
+        // refering a function with no args?
+        if (fn && MBQL_CLAUSES[fn].args.length === 0) {
+          return [fn];
+        }
+      }
+    }
+    return node;
+  });
+
 const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
 
 export const parse = pipe(
   recursiveParse,
   adjustOptions,
   useShorthands,
+  transformNoArgFunction,
   adjustCase,
 );
diff --git a/frontend/src/metabase/lib/expressions/resolver.js b/frontend/src/metabase/lib/expressions/resolver.js
index 5fed295a3a7..fa9b3fc75c1 100644
--- a/frontend/src/metabase/lib/expressions/resolver.js
+++ b/frontend/src/metabase/lib/expressions/resolver.js
@@ -48,6 +48,9 @@ const isCompatible = (a, b) => {
   if (a === "aggregation" && b === "number") {
     return true;
   }
+  if (a === "number" && b === "aggregation") {
+    return true;
+  }
   return false;
 };
 
diff --git a/frontend/test/metabase/lib/expressions/recursive-parser.unit.spec.js b/frontend/test/metabase/lib/expressions/recursive-parser.unit.spec.js
index 39b44da6851..1504b3c3521 100644
--- a/frontend/test/metabase/lib/expressions/recursive-parser.unit.spec.js
+++ b/frontend/test/metabase/lib/expressions/recursive-parser.unit.spec.js
@@ -149,6 +149,11 @@ describe("metabase/lib/expressions/recursive-parser", () => {
     expect(process("A and not X")).toEqual(["and", A, ["not", X]]);
   });
 
+  it("should detect aggregation functions with no argument", () => {
+    expect(process("COUNT/2")).toEqual(["/", ["count"], 2]);
+    expect(process("1+CumulativeCount")).toEqual(["+", 1, ["cum-count"]]);
+  });
+
   it("should resolve segments", () => {
     expect(process("Expensive", "boolean")).toEqual(["segment", "Expensive"]);
     expect(process("NOT LowVolume")).toEqual(["not", ["segment", "LowVolume"]]);
-- 
GitLab