From 48a30f4b8ea64dd7770943de5c3b5cbe2267b74d Mon Sep 17 00:00:00 2001 From: metamben <103100869+metamben@users.noreply.github.com> Date: Mon, 15 May 2023 22:22:49 +0300 Subject: [PATCH] Migrate default temporal bucket logic from Field.ts to ML (#30745) Fixes #30703. --- .../metabase/shared/util/internal/time.clj | 7 ++ .../metabase/shared/util/internal/time.cljs | 7 ++ shared/src/metabase/shared/util/time.cljc | 1 + src/metabase/lib/field.cljc | 34 ++++++++-- test/metabase/lib/field_test.cljc | 8 ++- test/metabase/lib/metadata/jvm_test.clj | 31 +++++++++ test/metabase/lib/temporal_bucket_test.cljc | 64 +++++++++++++++++++ 7 files changed, 144 insertions(+), 8 deletions(-) diff --git a/shared/src/metabase/shared/util/internal/time.clj b/shared/src/metabase/shared/util/internal/time.clj index 8f2622c77c7..e1446f59428 100644 --- a/shared/src/metabase/shared/util/internal/time.clj +++ b/shared/src/metabase/shared/util/internal/time.clj @@ -196,3 +196,10 @@ "Parses a time string that has been stripped of any time zone." [value] (t/local-time value)) + +;;; ------------------------------------------------ arithmetic ------------------------------------------------------ + +(defn day-diff + "Returns the time elapsed between `before` and `after` in days (an integer)." + [before after] + (.toDays (t/duration before after))) diff --git a/shared/src/metabase/shared/util/internal/time.cljs b/shared/src/metabase/shared/util/internal/time.cljs index 853aa0763ea..c446c564d84 100644 --- a/shared/src/metabase/shared/util/internal/time.cljs +++ b/shared/src/metabase/shared/util/internal/time.cljs @@ -148,3 +148,10 @@ "Parses a time string that has been stripped of any time zone." [value] (moment value parse-time-formats)) + +;;; ------------------------------------------------ arithmetic ------------------------------------------------------ + +(defn day-diff + "Returns the time elapsed between `before` and `after` in days." + [^moment/Moment before ^moment/Moment after] + (.diff after before "day")) diff --git a/shared/src/metabase/shared/util/time.cljc b/shared/src/metabase/shared/util/time.cljc index b5f4f558392..be684ee6f0e 100644 --- a/shared/src/metabase/shared/util/time.cljc +++ b/shared/src/metabase/shared/util/time.cljc @@ -14,6 +14,7 @@ (shared.ns/import-fn internal/same-day?) (shared.ns/import-fn internal/same-month?) (shared.ns/import-fn internal/same-year?) +(shared.ns/import-fn internal/day-diff) (defn- prep-options [options] (merge internal/default-options (u/normalize-map options))) diff --git a/src/metabase/lib/field.cljc b/src/metabase/lib/field.cljc index a74c7981631..d26d6eb8475 100644 --- a/src/metabase/lib/field.cljc +++ b/src/metabase/lib/field.cljc @@ -20,6 +20,7 @@ [metabase.lib.temporal-bucket :as lib.temporal-bucket] [metabase.lib.util :as lib.util] [metabase.shared.util.i18n :as i18n] + [metabase.shared.util.time :as shared.ut] [metabase.util :as u] [metabase.util.humanization :as u.humanization] [metabase.util.log :as log] @@ -286,14 +287,35 @@ [query stage-number field-ref] (lib.temporal-bucket/available-temporal-buckets query stage-number (resolve-field-metadata query stage-number field-ref))) +(defn- fingerprint-based-default [fingerprint] + (u/ignore-exceptions + (when-let [{:keys [earliest latest]} (-> fingerprint :type :type/DateTime)] + (let [days (shared.ut/day-diff (shared.ut/coerce-to-timestamp earliest) + (shared.ut/coerce-to-timestamp latest))] + (when-not (NaN? days) + (condp > days + 1 :minute + 31 :day + 365 :week + :month)))))) + +(defn- mark-default-unit [options unit] + (cond->> options + (some #(= (:unit %) unit) options) + (mapv (fn [option] + (cond-> (dissoc option :default) + (= (:unit option) unit) (assoc :default true)))))) + (defmethod lib.temporal-bucket/available-temporal-buckets-method :metadata/field [_query _stage-number field-metadata] - (let [effective-type ((some-fn :effective-type :base-type) field-metadata)] - (cond - (isa? effective-type :type/DateTime) lib.temporal-bucket/datetime-bucket-options - (isa? effective-type :type/Date) lib.temporal-bucket/date-bucket-options - (isa? effective-type :type/Time) lib.temporal-bucket/time-bucket-options - :else []))) + (let [effective-type ((some-fn :effective-type :base-type) field-metadata) + fingerprint-default (some-> field-metadata :fingerprint fingerprint-based-default)] + (cond-> (cond + (isa? effective-type :type/DateTime) lib.temporal-bucket/datetime-bucket-options + (isa? effective-type :type/Date) lib.temporal-bucket/date-bucket-options + (isa? effective-type :type/Time) lib.temporal-bucket/time-bucket-options + :else []) + fingerprint-default (mark-default-unit fingerprint-default)))) ;;; ---------------------------------------- Binning --------------------------------------------- (defmethod lib.binning/binning-method :field diff --git a/test/metabase/lib/field_test.cljc b/test/metabase/lib/field_test.cljc index 2bea9769d84..4a44022fc46 100644 --- a/test/metabase/lib/field_test.cljc +++ b/test/metabase/lib/field_test.cljc @@ -210,9 +210,13 @@ (deftest ^:parallel available-temporal-buckets-test (doseq [{:keys [metadata expected-options]} [{:metadata (get-in temporal-bucketing-mock-metadata [:fields :date]) - :expected-options lib.temporal-bucket/date-bucket-options} + :expected-options (-> lib.temporal-bucket/date-bucket-options + (update 0 dissoc :default) + (assoc-in [2 :default] true))} {:metadata (get-in temporal-bucketing-mock-metadata [:fields :datetime]) - :expected-options lib.temporal-bucket/datetime-bucket-options} + :expected-options (-> lib.temporal-bucket/datetime-bucket-options + (update 2 dissoc :default) + (assoc-in [4 :default] true))} {:metadata (get-in temporal-bucketing-mock-metadata [:fields :time]) :expected-options lib.temporal-bucket/time-bucket-options}]] (testing (str (:base-type metadata) " Field") diff --git a/test/metabase/lib/metadata/jvm_test.clj b/test/metabase/lib/metadata/jvm_test.clj index 12029cc4d4d..cfff113f0ef 100644 --- a/test/metabase/lib/metadata/jvm_test.clj +++ b/test/metabase/lib/metadata/jvm_test.clj @@ -83,3 +83,34 @@ :display-name "Sum" :source_alias "Orders"}] (lib.metadata.calculation/metadata mlv2-query)))))) + +(deftest ^:parallel temporal-bucketing-options-test + (mt/dataset sample-dataset + (let [query {:lib/type :mbql/query + :stages [{:lib/type :mbql.stage/mbql + :fields [[:field + {:lib/uuid (str (random-uuid))} + (mt/id :products :created_at)]] + :source-table (mt/id :products)}] + :database (mt/id)} + query (lib/query (lib.metadata.jvm/application-database-metadata-provider (mt/id)) + query)] + (is (= [{:unit :minute} + {:unit :hour} + {:unit :day} + {:unit :week} + {:unit :month, :default true} + {:unit :quarter} + {:unit :year} + {:unit :minute-of-hour} + {:unit :hour-of-day} + {:unit :day-of-week} + {:unit :day-of-month} + {:unit :day-of-year} + {:unit :week-of-year} + {:unit :month-of-year} + {:unit :quarter-of-year}] + (->> (lib.metadata.calculation/metadata query) + first + (lib/available-temporal-buckets query) + (mapv #(select-keys % [:unit :default])))))))) diff --git a/test/metabase/lib/temporal_bucket_test.cljc b/test/metabase/lib/temporal_bucket_test.cljc index b7efd5a6fa4..937b71ba163 100644 --- a/test/metabase/lib/temporal_bucket_test.cljc +++ b/test/metabase/lib/temporal_bucket_test.cljc @@ -63,3 +63,67 @@ (is (= "Unknown unit" (lib.temporal-bucket/describe-temporal-unit :unknown-unit) (lib.temporal-bucket/describe-temporal-unit 2 :unknown-unit)))) + +(deftest ^:parallel available-temporal-buckets-test + (let [column {:description nil + :lib/type :metadata/field + :database-is-auto-increment false + :fingerprint-version 5 + :base-type :type/DateTimeWithLocalTZ + :semantic-type :type/CreationTimestamp + :database-required false + :table-id 806 + :name "CREATED_AT" + :coercion-strategy nil + :lib/source :source/fields + :lib/source-column-alias "CREATED_AT" + :settings nil + :caveats nil + :nfc-path nil + :database-type "TIMESTAMP WITH TIME ZONE" + :effective-type :type/DateTimeWithLocalTZ + :fk-target-field-id nil + :custom-position 0 + :active true + :id 3068 + :parent-id nil + :points-of-interest nil + :visibility-type :normal + :lib/desired-column-alias "CREATED_AT" + :display-name "Created At" + :position 7 + :has-field-values nil + :json-unfolding false + :preview-display true + :database-position 7 + :fingerprint + {:global {:distinct-count 200, :nil% 0.0}}} + expected-units #{:minute :hour + :day :week :month :quarter :year + :minute-of-hour :hour-of-day + :day-of-week :day-of-month :day-of-year + :week-of-year :month-of-year :quarter-of-year} + expected-defaults [{:lib/type :type/temporal-bucketing-option, :unit :day, :default true}]] + (testing "missing fingerprint" + (let [column (dissoc column :fingerprint) + options (lib.temporal-bucket/available-temporal-buckets-method nil -1 column)] + (is (= expected-units + (into #{} (map :unit) options))) + (is (= expected-defaults + (filter :default options))))) + (testing "existing fingerprint" + (doseq [[latest unit] {"2019-04-15T13:34:19.931Z" :month + "2017-04-15T13:34:19.931Z" :week + "2016-05-15T13:34:19.931Z" :day + "2016-04-27T13:34:19.931Z" :minute + nil :day + "garbage" :day}] + (testing latest + (let [bounds {:earliest "2016-04-26T19:29:55.147Z" + :latest latest} + column (assoc-in column [:fingerprint :type :type/DateTime] bounds) + options (lib.temporal-bucket/available-temporal-buckets-method nil -1 column)] + (is (= expected-units + (into #{} (map :unit) options))) + (is (= (assoc-in expected-defaults [0 :unit] unit) + (filter :default options))))))))) -- GitLab