From 79337de1c73796874259e1e519bbbf89dad6dc14 Mon Sep 17 00:00:00 2001
From: Ariya Hidayat <ariya@metabase.com>
Date: Fri, 24 Jun 2022 07:47:54 -0700
Subject: [PATCH] Refactor and tests template tags extractions (#23524)

---
 .../metabase-lib/lib/queries/NativeQuery.ts   | 42 +++++++++----------
 .../metabase-lib/lib/queries/TemplateTag.ts   | 12 ++++++
 .../lib/queries/TemplateTag.unit.spec.ts      | 11 +++++
 .../lib/queries/NativeQuery.unit.spec.js      | 24 ++++++++++-
 4 files changed, 67 insertions(+), 22 deletions(-)
 create mode 100644 frontend/src/metabase-lib/lib/queries/TemplateTag.ts
 create mode 100644 frontend/src/metabase-lib/lib/queries/TemplateTag.unit.spec.ts

diff --git a/frontend/src/metabase-lib/lib/queries/NativeQuery.ts b/frontend/src/metabase-lib/lib/queries/NativeQuery.ts
index c48208aded3..6e6c7d73d7f 100644
--- a/frontend/src/metabase-lib/lib/queries/NativeQuery.ts
+++ b/frontend/src/metabase-lib/lib/queries/NativeQuery.ts
@@ -25,6 +25,7 @@ import { DatabaseEngine, DatabaseId } from "metabase-types/types/Database";
 import AtomicQuery from "metabase-lib/lib/queries/AtomicQuery";
 import Dimension, { TemplateTagDimension, FieldDimension } from "../Dimension";
 import Variable, { TemplateTagVariable } from "../Variable";
+import { createTemplateTag } from "metabase-lib/lib/queries/TemplateTag";
 import DimensionOptions from "../DimensionOptions";
 import { ValidationError } from "metabase-lib/lib/ValidationError";
 
@@ -422,21 +423,7 @@ export default class NativeQuery extends AtomicQuery {
    */
   _getUpdatedTemplateTags(queryText: string): TemplateTags {
     if (queryText && this.supportsNativeParameters()) {
-      let tags = [];
-      // look for variable usage in the query (like '{{varname}}').  we only allow alphanumeric characters for the variable name
-      // a variable name can optionally end with :start or :end which is not considered part of the actual variable name
-      // expected pattern is like mustache templates, so we are looking for something like {{category}} or {{date:start}}
-      // anything that doesn't match our rule is ignored, so {{&foo!}} would simply be ignored
-      // variables referencing other questions, by their card ID, are also supported: {{#123}} references question with ID 123
-      let match;
-      const re = /\{\{\s*((snippet:\s*[^}]+)|[A-Za-z0-9_\.]+?|#[0-9]*)\s*\}\}/g;
-
-      while ((match = re.exec(queryText)) != null) {
-        tags.push(match[1]);
-      }
-
-      // eliminate any duplicates since it's allowed for a user to reference the same variable multiple times
-      tags = _.uniq(tags);
+      const tags = recognizeTemplateTags(queryText);
       const existingTemplateTags = this.templateTagsMap();
       const existingTags = Object.keys(existingTemplateTags);
 
@@ -476,12 +463,7 @@ export default class NativeQuery extends AtomicQuery {
 
           // create new vars
           for (const tagName of newTags) {
-            templateTags[tagName] = {
-              id: Utils.uuid(),
-              name: tagName,
-              "display-name": humanize(tagName),
-              type: "text",
-            };
+            templateTags[tagName] = createTemplateTag(tagName);
 
             // parse card ID from tag name for card query template tags
             if (isCardQueryName(tagName)) {
@@ -533,3 +515,21 @@ export default class NativeQuery extends AtomicQuery {
       });
   }
 }
+
+// look for variable usage in the query (like '{{varname}}').  we only allow alphanumeric characters for the variable name
+// a variable name can optionally end with :start or :end which is not considered part of the actual variable name
+// expected pattern is like mustache templates, so we are looking for something like {{category}} or {{date:start}}
+// anything that doesn't match our rule is ignored, so {{&foo!}} would simply be ignored
+// variables referencing other questions, by their card ID, are also supported: {{#123}} references question with ID 123
+export function recognizeTemplateTags(queryText: string): string[] {
+  const tagNames = [];
+  let match;
+  const re = /\{\{\s*((snippet:\s*[^}]+)|[A-Za-z0-9_\.]+?|#[0-9]*)\s*\}\}/g;
+
+  while ((match = re.exec(queryText)) != null) {
+    tagNames.push(match[1]);
+  }
+
+  // eliminate any duplicates since it's allowed for a user to reference the same variable multiple times
+  return _.uniq(tagNames);
+}
diff --git a/frontend/src/metabase-lib/lib/queries/TemplateTag.ts b/frontend/src/metabase-lib/lib/queries/TemplateTag.ts
new file mode 100644
index 00000000000..eae0aeeecc7
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/queries/TemplateTag.ts
@@ -0,0 +1,12 @@
+import Utils from "metabase/lib/utils";
+import { humanize } from "metabase/lib/formatting";
+import { TemplateTag } from "metabase-types/types/Query";
+
+export const createTemplateTag = (tagName: string): TemplateTag => {
+  return {
+    id: Utils.uuid(),
+    name: tagName,
+    "display-name": humanize(tagName),
+    type: "text",
+  };
+};
diff --git a/frontend/src/metabase-lib/lib/queries/TemplateTag.unit.spec.ts b/frontend/src/metabase-lib/lib/queries/TemplateTag.unit.spec.ts
new file mode 100644
index 00000000000..b516139fc6f
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/queries/TemplateTag.unit.spec.ts
@@ -0,0 +1,11 @@
+import { createTemplateTag } from "metabase-lib/lib/queries/TemplateTag";
+
+describe("createTemplateTag", () => {
+  it("should create a proper template tag", () => {
+    const tag = createTemplateTag("stars");
+    expect(tag.name).toEqual("stars");
+    expect(tag.type).toEqual("text");
+    expect(typeof tag.id).toEqual("string");
+    expect(tag["display-name"]).toEqual("Stars");
+  });
+});
diff --git a/frontend/test/metabase-lib/lib/queries/NativeQuery.unit.spec.js b/frontend/test/metabase-lib/lib/queries/NativeQuery.unit.spec.js
index 3ed07478ba6..187470ef19e 100644
--- a/frontend/test/metabase-lib/lib/queries/NativeQuery.unit.spec.js
+++ b/frontend/test/metabase-lib/lib/queries/NativeQuery.unit.spec.js
@@ -6,7 +6,9 @@ import {
   MONGO_DATABASE,
 } from "__support__/sample_database_fixture";
 
-import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
+import NativeQuery, {
+  recognizeTemplateTags,
+} from "metabase-lib/lib/queries/NativeQuery";
 
 function makeDatasetQuery(queryText, templateTags, databaseId) {
   return {
@@ -339,4 +341,24 @@ describe("NativeQuery", () => {
       ]);
     });
   });
+
+  describe("recognizeTemplateTags", () => {
+    it("should handle standard variable names", () => {
+      expect(recognizeTemplateTags("SELECT * from {{products}}")).toEqual([
+        "products",
+      ]);
+    });
+
+    it("should allow duplicated variables", () => {
+      expect(
+        recognizeTemplateTags("SELECT {{col}} FROM {{t}} ORDER BY {{col}} "),
+      ).toEqual(["col", "t"]);
+    });
+
+    it("should ignore non-alphanumeric markers", () => {
+      expect(recognizeTemplateTags("SELECT * from X -- {{&universe}}")).toEqual(
+        [],
+      );
+    });
+  });
 });
-- 
GitLab