Repository: metabase/metabase
Pull request #55483: feat: prepare fe part of split-part
Author: login: uladzimirdev
Labels: name: .Team/Querying
name: PR-Env
description: See https://www.notion.so/metabase/Ephemeral-PR-Environments-c74a3710c87a460dbf4fc0007a001458
Created: 2025-03-20T17:47:52Z | Merged: 2025-03-21T17:11:57Z

adds split-part function to the frontend


Diff:
diff --git a/e2e/test/scenarios/custom-column/custom-column.cy.spec.js b/e2e/test/scenarios/custom-column/custom-column.cy.spec.js
index 4865bf7e31b7..3972e43cf7ec 100644
--- a/e2e/test/scenarios/custom-column/custom-column.cy.spec.js
+++ b/e2e/test/scenarios/custom-column/custom-column.cy.spec.js
@@ -1592,3 +1592,60 @@ describe("scenarios > question > custom column > function browser", () => {
     H.CustomExpressionEditor.value().should("equal", "now");
   });
 });
+
+describe("scenarios > question > custom column > splitPart", () => {
+  beforeEach(() => {
+    H.restore("postgres-12");
+    cy.signInAsAdmin();
+
+    H.startNewQuestion();
+    H.entityPickerModal().within(() => {
+      H.entityPickerModalTab("Tables").click();
+      cy.findByText("QA Postgres12").click();
+      cy.findByText("People").click();
+    });
+
+    cy.findByLabelText("Custom column").click();
+  });
+
+  function assertTableData({ title, value }) {
+    // eslint-disable-next-line no-unsafe-element-filtering
+    H.tableInteractive()
+      .findAllByTestId("header-cell")
+      .last()
+      .should("have.text", title);
+
+    // eslint-disable-next-line no-unsafe-element-filtering
+    H.tableInteractiveBody()
+      .findAllByTestId("cell-data")
+      .last()
+      .should("have.text", value);
+  }
+
+  it("should be possible to split a custom column", () => {
+    const CC_NAME = "Split Title";
+
+    H.enterCustomColumnDetails({
+      formula: "splitPart([Name], ' ', 1)",
+      name: CC_NAME,
+    });
+    H.popover().button("Done").click();
+
+    cy.findByLabelText("Row limit").click();
+    cy.findByPlaceholderText("Enter a limit").type(1).blur();
+
+    H.visualize();
+
+    H.tableInteractiveScrollContainer().scrollTo("right");
+    assertTableData({ title: CC_NAME, value: "Hudson" });
+  });
+
+  it("should show a message when index is below 1", () => {
+    H.enterCustomColumnDetails({
+      formula: "splitPart([Name], ' ', 0)",
+    });
+
+    H.popover().button("Done").should("be.disabled");
+    H.popover().should("contain", "Expected positive integer but found 0");
+  });
+});
diff --git a/frontend/src/metabase-lib/v1/expressions/config.ts b/frontend/src/metabase-lib/v1/expressions/config.ts
index 7f8ecce8ef3f..019e9a191ebe 100644
--- a/frontend/src/metabase-lib/v1/expressions/config.ts
+++ b/frontend/src/metabase-lib/v1/expressions/config.ts
@@ -110,6 +110,17 @@ export const MBQL_CLAUSES: MBQLClauseMap = {
       }
     },
   },
+  "split-part": {
+    displayName: "splitPart",
+    type: "string",
+    args: ["string", "string", "number"],
+    validator: function (_arg: any, _delimeter: string, position: number) {
+      if (position < 1) {
+        return t`Expected positive integer but found ${position}`;
+      }
+    },
+    requiresFeature: "split-part",
+  },
   "regex-match-first": {
     displayName: `regexextract`,
     type: "string",
@@ -530,6 +541,7 @@ export const EXPRESSION_FUNCTIONS = new Set([
   "lower",
   "upper",
   "substring",
+  "split-part",
   "regex-match-first",
   "concat",
   "replace",
diff --git a/frontend/src/metabase-lib/v1/expressions/helper-text-strings.ts b/frontend/src/metabase-lib/v1/expressions/helper-text-strings.ts
index a8cd9e2274b7..54b47c2d1033 100644
--- a/frontend/src/metabase-lib/v1/expressions/helper-text-strings.ts
+++ b/frontend/src/metabase-lib/v1/expressions/helper-text-strings.ts
@@ -334,6 +334,30 @@ const HELPER_TEXT_STRINGS: HelpTextConfig[] = [
     ],
     docsPage: "substring",
   },
+  {
+    name: "split-part",
+    category: "string",
+    structure: "splitPart",
+    description: () =>
+      t`Splits a string on a specified delimiter and returns the nth substring.`,
+    args: [
+      {
+        name: t`text`,
+        description: t`The column or text to return a portion of.`,
+        example: ["dimension", t`Title`],
+      },
+      {
+        name: t`delimiter`,
+        description: t`The pattern describing where each split should occur.`,
+        example: ",",
+      },
+      {
+        name: t`position`,
+        description: t`Which substring to return after the split. Index starts at position 1.`,
+        example: 1,
+      },
+    ],
+  },
   {
     name: "regex-match-first",
     structure: "regexextract",
diff --git a/frontend/src/metabase-types/api/database.ts b/frontend/src/metabase-types/api/database.ts
index e468bcdad8e5..395fe2fcc774 100644
--- a/frontend/src/metabase-types/api/database.ts
+++ b/frontend/src/metabase-types/api/database.ts
@@ -41,7 +41,8 @@ export type DatabaseFeature =
   | "connection-impersonation-requires-role"
   | "native-requires-specified-collection"
   | "window-functions/offset"
-  | "distinct-where";
+  | "distinct-where"
+  | "split-part";
 
 export interface Database extends DatabaseData {
   id: DatabaseId;


Reviews:
user: login: uladzimirdev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:35:06Z
user: login: ranquild
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:37:37Z
user: login: ranquild
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:47:05Z
user: login: uladzimirdev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:48:38Z
user: login: ranquild
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:11:30Z
user: login: ranquild
body: Please do not forget to remove `docsPage`
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:11:59Z

Review comments:
diff_hunk: @@ -334,6 +334,31 @@ const HELPER_TEXT_STRINGS: HelpTextConfig[] = [
     ],
     docsPage: "substring",
   },
+  {
+    name: "split-part",
+    category: "string",
+    structure: "splitPart",
+    description: () =>
+      t`Splits a string on a specified delimiter and returns the nth substring.`,
+    args: [
+      {
+        name: t`text`,
+        description: t`The column or text to return a portion of.`,
+        example: ["dimension", t`Title`],
+      },
+      {
+        name: t`delimiter`,
+        description: t`The pattern describing where each split should occur.`,
+        example: ",",
+      },
+      {
+        name: t`position`,
+        description: t`Which array element to return after the split. Index starts at position 1.`,
+        example: 1,
+      },
+    ],
+    docsPage: "split-part",
path: frontend/src/metabase-lib/v1/expressions/helper-text-strings.ts
user: login: uladzimirdev
body: @ranquild this feature doesn't work in sampledb, do you think we need to create a new feature flag?
created_at: 2025-03-21T14:35:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -334,6 +334,31 @@ const HELPER_TEXT_STRINGS: HelpTextConfig[] = [
     ],
     docsPage: "substring",
   },
+  {
+    name: "split-part",
+    category: "string",
+    structure: "splitPart",
+    description: () =>
+      t`Splits a string on a specified delimiter and returns the nth substring.`,
+    args: [
+      {
+        name: t`text`,
+        description: t`The column or text to return a portion of.`,
+        example: ["dimension", t`Title`],
+      },
+      {
+        name: t`delimiter`,
+        description: t`The pattern describing where each split should occur.`,
+        example: ",",
+      },
+      {
+        name: t`position`,
+        description: t`Which array element to return after the split. Index starts at position 1.`,
+        example: 1,
+      },
+    ],
+    docsPage: "split-part",
path: frontend/src/metabase-lib/v1/expressions/helper-text-strings.ts
user: login: ranquild
body: if this doesn't work for some databases, there should be a database feature flag
created_at: 2025-03-21T14:37:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,6 +110,16 @@ export const MBQL_CLAUSES: MBQLClauseMap = {
       }
     },
   },
+  "split-part": {
path: frontend/src/metabase-lib/v1/expressions/config.ts
user: login: ranquild
body: @uladzimirdev please specify `requiresFeature` here
created_at: 2025-03-21T14:47:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,6 +110,16 @@ export const MBQL_CLAUSES: MBQLClauseMap = {
       }
     },
   },
+  "split-part": {
path: frontend/src/metabase-lib/v1/expressions/config.ts
user: login: uladzimirdev
body: done
created_at: 2025-03-21T14:48:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -334,6 +334,31 @@ const HELPER_TEXT_STRINGS: HelpTextConfig[] = [
     ],
     docsPage: "substring",
   },
+  {
+    name: "split-part",
+    category: "string",
+    structure: "splitPart",
+    description: () =>
+      t`Splits a string on a specified delimiter and returns the nth substring.`,
+    args: [
+      {
+        name: t`text`,
+        description: t`The column or text to return a portion of.`,
+        example: ["dimension", t`Title`],
+      },
+      {
+        name: t`delimiter`,
+        description: t`The pattern describing where each split should occur.`,
+        example: ",",
+      },
+      {
+        name: t`position`,
+        description: t`Which array element to return after the split. Index starts at position 1.`,
+        example: 1,
+      },
+    ],
+    docsPage: "split-part",
path: frontend/src/metabase-lib/v1/expressions/helper-text-strings.ts
user: login: ranquild
body: @uladzimirdev please remove `docsPage` since it doesn't exist yet
created_at: 2025-03-21T15:11:29Z
author_association: CONTRIBUTOR

PR comments:
user: login: trunk-io[bot]
created_at: 2025-03-20T19:14:54Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://img.shields.io/badge/4384_tests-b4cd1eeb-purple)](https://app.trunk.io/metabase/flaky-tests/pr/55483?repo=metabase/metabase&commitHash=b4cd1eeb602649a25ac799ee82c02687bf5379e7&utm_source=gh-comment) &ensp; [![Static Badge](https://img.shields.io/badge/0-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/55483?repo=metabase/metabase&commitHash=b4cd1eeb602649a25ac799ee82c02687bf5379e7&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/0-flaky-red)](https://app.trunk.io/metabase/flaky-tests/pr/55483?repo=metabase/metabase&commitHash=b4cd1eeb602649a25ac799ee82c02687bf5379e7&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/metabase/flaky-tests/pr/55483?repo=metabase/metabase&commitHash=b4cd1eeb602649a25ac799ee82c02687bf5379e7&utm_source=gh-comment&runConclusion=QUARANTINED)


<sub>

[View Full Report ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/55483?repo=metabase/metabase&commitHash=b4cd1eeb602649a25ac799ee82c02687bf5379e7&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE