From 3db6407f637ad986fa9a1cd8f9f1831595e278b4 Mon Sep 17 00:00:00 2001 From: Gustavo Saiani <gus@metabase.com> Date: Thu, 1 Dec 2022 13:06:05 -0300 Subject: [PATCH] Build datetime filters as datetime before question is saved (#26679) --- frontend/src/metabase-lib/Dimension.ts | 4 ++ .../src/metabase-lib/expressions/config.js | 32 +++++----- .../src/metabase-lib/expressions/resolver.js | 15 ++++- .../expressions/typeinferencer.js | 6 +- .../lib/expressions/resolver.unit.spec.js | 60 +++++++++++++++++++ .../expressions/typeinferencer.unit.spec.js | 15 +++-- 6 files changed, 108 insertions(+), 24 deletions(-) diff --git a/frontend/src/metabase-lib/Dimension.ts b/frontend/src/metabase-lib/Dimension.ts index d481a677cef..d49fda38657 100644 --- a/frontend/src/metabase-lib/Dimension.ts +++ b/frontend/src/metabase-lib/Dimension.ts @@ -1264,6 +1264,10 @@ export class ExpressionDimension extends Dimension { base_type = "type/Boolean"; break; + case MONOTYPE.DateTime: + base_type = "type/DateTime"; + break; + // fallback default: base_type = "type/Float"; diff --git a/frontend/src/metabase-lib/expressions/config.js b/frontend/src/metabase-lib/expressions/config.js index f3e40c7cff3..1d6cab352f2 100644 --- a/frontend/src/metabase-lib/expressions/config.js +++ b/frontend/src/metabase-lib/expressions/config.js @@ -324,69 +324,69 @@ export const MBQL_CLAUSES = { "get-year": { displayName: `year`, type: "number", - args: ["expression"], + args: ["datetime"], }, "get-quarter": { displayName: `quarter`, type: "number", - args: ["expression"], + args: ["datetime"], }, "get-month": { displayName: `month`, type: "number", - args: ["expression"], + args: ["datetime"], }, "get-week": { displayName: `week`, type: "number", - args: ["expression"], + args: ["datetime"], hasOptions: true, // optional mode parameter }, "get-day": { displayName: `day`, type: "number", - args: ["expression"], + args: ["datetime"], }, "get-day-of-week": { displayName: `weekday`, type: "number", - args: ["expression"], + args: ["datetime"], }, "get-hour": { displayName: `hour`, type: "number", - args: ["expression"], + args: ["datetime"], }, "get-minute": { displayName: `minute`, type: "number", - args: ["expression"], + args: ["datetime"], }, "get-second": { displayName: `second`, type: "number", - args: ["expression"], + args: ["datetime"], }, "datetime-diff": { displayName: `datetimeDiff`, type: "number", - args: ["expression", "expression", "string"], + args: ["datetime", "datetime", "string"], requiresFeature: "datetime-diff", }, "datetime-add": { displayName: `datetimeAdd`, - type: "expression", - args: ["expression", "number", "string"], + type: "datetime", + args: ["datetime", "number", "string"], }, "datetime-subtract": { displayName: `datetimeSubtract`, - type: "expression", - args: ["expression", "number", "string"], + type: "datetime", + args: ["datetime", "number", "string"], }, "convert-timezone": { displayName: `convertTimezone`, - type: "expression", - args: ["expression", "string"], + type: "datetime", + args: ["datetime", "string"], hasOptions: true, requiresFeature: "convert-timezone", }, diff --git a/frontend/src/metabase-lib/expressions/resolver.js b/frontend/src/metabase-lib/expressions/resolver.js index 52c9ac8d780..2612db2c76d 100644 --- a/frontend/src/metabase-lib/expressions/resolver.js +++ b/frontend/src/metabase-lib/expressions/resolver.js @@ -38,11 +38,24 @@ function findMBQL(op) { return clause; } +// a is the type of the argument expected, +// as defined in MBQL_CLAUSES, +// and b is the inferred type of the argument const isCompatible = (a, b) => { if (a === b) { return true; } - if (a === "expression" && (b === "number" || b === "string")) { + + // if b is a string, then it can be an arg to a function that expects a datetime argument. + // This allows datetime string literals to work as args for functions that expect datetime types. + // FIXME: By doing this we are allowing string columns to be arguments to functions, which isn’t valid MBQL. + if (a === "datetime" && b === "string") { + return true; + } + if ( + a === "expression" && + (b === "datetime" || b === "number" || b === "string") + ) { return true; } if (a === "aggregation" && b === "number") { diff --git a/frontend/src/metabase-lib/expressions/typeinferencer.js b/frontend/src/metabase-lib/expressions/typeinferencer.js index 249c142b94b..5c0cb61caf5 100644 --- a/frontend/src/metabase-lib/expressions/typeinferencer.js +++ b/frontend/src/metabase-lib/expressions/typeinferencer.js @@ -6,6 +6,7 @@ export const MONOTYPE = { Number: "number", String: "string", Boolean: "boolean", + DateTime: "datetime", }; export function infer(mbql, env) { @@ -37,9 +38,6 @@ export function infer(mbql, env) { case "case": return infer(mbql[1][0][1], env); case "coalesce": - case "convert-timezone": - case "datetime-add": - case "datetime-subtract": return infer(mbql[1], env); } @@ -47,6 +45,8 @@ export function infer(mbql, env) { if (func) { const returnType = func.type; switch (returnType) { + case "datetime": + return MONOTYPE.DateTime; case "object": return MONOTYPE.Undefined; case "aggregation": diff --git a/frontend/test/metabase/lib/expressions/resolver.unit.spec.js b/frontend/test/metabase/lib/expressions/resolver.unit.spec.js index 74a6c14adc3..85ff4352508 100644 --- a/frontend/test/metabase/lib/expressions/resolver.unit.spec.js +++ b/frontend/test/metabase/lib/expressions/resolver.unit.spec.js @@ -159,6 +159,66 @@ describe("metabase-lib/expressions/resolve", () => { it("should honor CONCAT's implicit casting", () => { expect(() => expr(["concat", ["coalesce", "B", 1]])).not.toThrow(); }); + + describe("datetime functions", () => { + it("should resolve unchained functions", () => { + expect(() => expr(["get-week", "2022-01-01"])).not.toThrow(); + expect(() => + expr(["datetime-add", "2022-01-01", 1, "month"]), + ).not.toThrow(); + + // TODO: Implementation should be fine-tuned so that these throw + // as they are not really datetime + expect(() => expr(["get-day", A])).not.toThrow(); + expect(() => expr(["get-day", "a"])).not.toThrow(); + expect(() => expr(["get-day-of-week", A])).not.toThrow(); + expect(() => expr(["get-day-of-week", "a"])).not.toThrow(); + expect(() => expr(["get-week", A])).not.toThrow(); + expect(() => expr(["get-week", "a"])).not.toThrow(); + expect(() => expr(["get-month", A])).not.toThrow(); + expect(() => expr(["get-month", "a"])).not.toThrow(); + expect(() => expr(["get-quarter", A])).not.toThrow(); + expect(() => expr(["get-quarter", "a"])).not.toThrow(); + expect(() => expr(["get-year", A])).not.toThrow(); + expect(() => expr(["get-year", "a"])).not.toThrow(); + }); + + it("should resolve chained commmands", () => { + expect(() => + expr([ + "datetime-subtract", + ["datetime-add", "2022-01-01", 1, "month"], + 2, + "minute", + ]), + ).not.toThrow(); + }); + + it("should chain datetime functions onto functions of compatible types", () => { + expect(() => + expr([ + "concat", + ["datetime-add", "2022-01-01", 1, "month"], + "a string", + ]), + ).not.toThrow(); + }); + + it("should throw if chaining datetime functions onto functions of incompatible types", () => { + expect(() => + expr(["trim", ["datetime-add", "2022-01-01", 1, "month"]]), + ).toThrow(); + }); + + it("should throw if passing numbers as arguments expected to be datetime", () => { + expect(() => expr(["get-day", 15])).toThrow(); + expect(() => expr(["get-day-of-week", 6])).toThrow(); + expect(() => expr(["get-week", 52])).toThrow(); + expect(() => expr(["get-month", 12])).toThrow(); + expect(() => expr(["get-quarter", 3])).toThrow(); + expect(() => expr(["get-year", 2025])).toThrow(); + }); + }); }); describe("for aggregations", () => { diff --git a/frontend/test/metabase/lib/expressions/typeinferencer.unit.spec.js b/frontend/test/metabase/lib/expressions/typeinferencer.unit.spec.js index ba145846e9c..a37a2c5fc03 100644 --- a/frontend/test/metabase/lib/expressions/typeinferencer.unit.spec.js +++ b/frontend/test/metabase/lib/expressions/typeinferencer.unit.spec.js @@ -116,12 +116,19 @@ describe("metabase-lib/expressions/typeinferencer", () => { }); it("should infer the result of datetimeAdd, datetimeSubtract", () => { - expect(type('datetimeAdd([CreatedAt], 2, "month")')).toEqual( - "type/Datetime", - ); + expect(type('datetimeAdd([CreatedAt], 2, "month")')).toEqual("datetime"); + expect(type('datetimeAdd("2022-01-01", 2, "month")')).toEqual("datetime"); expect(type('datetimeSubtract([CreatedAt], 2, "month")')).toEqual( - "type/Datetime", + "datetime", + ); + expect(type('datetimeSubtract("2022-01-01", 2, "month")')).toEqual( + "datetime", ); + expect( + type( + 'datetimeSubtract(datetimeAdd("2022-01-01", 2, "month"), 4, "minute")', + ), + ).toEqual("datetime"); }); it("should infer the result of datetimeExtract functions", () => { -- GitLab