(ns metabase.lib.schema.expression.temporal (:require [clojure.set :as set] [metabase.lib.hierarchy :as lib.hierarchy] [metabase.lib.schema.common :as common] [metabase.lib.schema.expression :as expression] [metabase.lib.schema.literal :as literal] [metabase.lib.schema.mbql-clause :as mbql-clause] [metabase.lib.schema.temporal-bucketing :as temporal-bucketing] [metabase.util.malli.registry :as mr] [metabase.util.time.impl-common :as u.time.impl-common]) #?@ (:clj [(:import (java.time ZoneId))] :cljs [(:require ["moment" :as moment] ["moment-timezone" :as mtz])])) | |
#?(:cljs ;; so the moment-timezone stuff gets loaded (comment mtz/keep-me)) (mbql-clause/define-tuple-mbql-clause :interval :- :type/Interval :int ::temporal-bucketing/unit.date-time.interval) | |
(defmethod expression/type-of-method :lib.type-of/type-is-temporal-type-of-first-arg [[_tag _opts temporal]] ;; For datetime-add, datetime-subtract, etc. the first arg is a temporal value. However, some valid values are ;; formatted strings for which type-of returns eg. #{:type/String :type/DateTime}. Since we're doing date arithmetic, ;; we know for sure it's the temporal type. (let [inner-type (expression/type-of temporal)] (if (set? inner-type) (let [temporal-set (set/intersection inner-type #{:type/Date :type/DateTime})] (if (= (count temporal-set) 1) (first temporal-set) temporal-set)) inner-type))) | |
For most purposes, | (lib.hierarchy/derive :lib.type-of/type-is-temporal-type-of-first-arg :lib.type-of/type-is-type-of-first-arg) |
TODO -- we should constrain this so that you can only use a Date unit if expr is a date, etc. | (doseq [op [:datetime-add :datetime-subtract]] (mbql-clause/define-tuple-mbql-clause op #_expr [:ref ::expression/temporal] #_amount :int #_unit [:ref ::temporal-bucketing/unit.date-time.interval]) (lib.hierarchy/derive op :lib.type-of/type-is-temporal-type-of-first-arg)) |
(doseq [op [:get-year :get-month :get-day :get-hour :get-minute :get-second :get-quarter]] (mbql-clause/define-tuple-mbql-clause op :- :type/Integer [:schema [:ref ::expression/temporal]])) | |
(mbql-clause/define-tuple-mbql-clause :datetime-diff :- :type/Integer #_:datetime1 [:schema [:ref ::expression/temporal]] #_:datetime2 [:schema [:ref ::expression/temporal]] #_:unit [:ref ::temporal-bucketing/unit.date-time.truncate]) | |
(doseq [temporal-extract-op #{:get-second :get-minute :get-hour :get-day :get-month :get-quarter :get-year}] (mbql-clause/define-tuple-mbql-clause temporal-extract-op :- :type/Integer #_:datetime [:schema [:ref ::expression/temporal]])) | |
(mr/def ::week-mode [:enum {:decode/normalize common/normalize-keyword} :iso :us :instance]) | |
(mbql-clause/define-catn-mbql-clause :get-week :- :type/Integer [:datetime [:schema [:ref ::expression/temporal]]] ;; TODO : the mode should probably go in the options map in modern MBQL rather than have it be a separate positional ;; argument. But we can't refactor everything in one go, so that will have to be a future refactor. [:mode [:? [:schema [:ref ::week-mode]]]]) | |
(mbql-clause/define-catn-mbql-clause :get-day-of-week :- :type/Integer [:datetime [:schema [:ref ::expression/temporal]]] ;; TODO : the mode should probably go in the options map in modern MBQL rather than have it be a separate positional ;; argument. But we can't refactor everything in one go, so that will have to be a future refactor. [:mode [:? [:schema [:ref ::week-mode]]]]) | |
(mr/def ::timezone-id [:and ::common/non-blank-string [:or (into [:enum {:error/message "valid timezone ID" :error/fn (fn [{:keys [value]} _] (str "invalid timezone ID: " (pr-str value)))}] (sort #?(;; 600 timezones on java 17 :clj (ZoneId/getAvailableZoneIds) ;; 596 timezones on moment-timezone 0.5.38 :cljs (.names (.-tz moment))))) ::literal/string.zone-offset]]) | |
(mbql-clause/define-catn-mbql-clause :convert-timezone [:datetime [:schema [:ref ::expression/temporal]]] [:target [:schema [:ref ::timezone-id]]] [:source [:? [:schema [:ref ::timezone-id]]]]) | |
(lib.hierarchy/derive :convert-timezone :lib.type-of/type-is-temporal-type-of-first-arg) | |
(mbql-clause/define-tuple-mbql-clause :now :- :type/DateTimeWithTZ) | |
if | (mr/def ::absolute-datetime.base-type [:and [:ref ::common/base-type] [:fn {:error/message ":absolute-datetime base-type must derive from :type/Date or :type/DateTime"} (fn [base-type] (some #(isa? base-type %) [:type/Date :type/DateTime]))]]) |
(mr/def ::absolute-datetime.options [:merge [:ref ::common/options] [:map [:base-type {:optional true} [:ref ::absolute-datetime.base-type]]]]) | |
(mbql-clause/define-mbql-clause :absolute-datetime [:cat {:error/message "valid :absolute-datetime clause"} [:= {:decode/normalize common/normalize-keyword} :absolute-datetime] [:schema [:ref ::absolute-datetime.options]] [:alt [:cat {:error/message ":absolute-datetime literal and unit for :type/Date"} [:schema [:or [:ref ::literal/date] ;; absolute datetime also allows `year-month` and `year` literals. [:ref ::literal/string.year-month] [:ref ::literal/string.year]]] [:schema [:or [:= {:decode/normalize common/normalize-keyword} :default] [:ref ::temporal-bucketing/unit.date]]]] [:cat {:error/message ":absolute-datetime literal and unit for :type/DateTime"} [:schema [:or [:= {:decode/normalize common/normalize-keyword} :current] [:ref ::literal/datetime]]] [:schema [:or [:= {:decode/normalize common/normalize-keyword} :default] [:ref ::temporal-bucketing/unit.date-time]]]]]]) | |
(defmethod expression/type-of-method :absolute-datetime [[_tag _opts value unit]] (or ;; if value is `:current`, then infer the type based on the unit. Date unit = `:type/Date`. Anything else = ;; `:type/DateTime`. (when (= value :current) (cond (= unit :default) :type/DateTime (mr/validate ::temporal-bucketing/unit.date unit) :type/Date :else :type/DateTime)) ;; handle year-month and year string regexes, which are not allowed as date literals unless wrapped in ;; `:absolute-datetime`. (when (string? value) (cond (re-matches u.time.impl-common/year-month-regex value) :type/Date (re-matches u.time.impl-common/year-regex value) :type/Date)) ;; for things that return a union of types like string literals, only the temporal types make sense, so filter out ;; everything else. (let [value-type (expression/type-of value) value-type (if (set? value-type) (into #{} (filter #(isa? % :type/Temporal)) value-type) value-type)] (if (and (set? value-type) (= (count value-type) 1)) (first value-type) value-type)))) | |
(mr/def ::relative-datetime.amount [:multi {:dispatch (some-fn keyword? string?)} [true [:= {:decode/normalize common/normalize-keyword} :current]] [false :int]]) | |
(mbql-clause/define-catn-mbql-clause :relative-datetime :- :type/DateTime [:n [:schema [:ref ::relative-datetime.amount]]] [:unit [:? [:schema [:ref ::temporal-bucketing/unit.date-time.interval]]]]) | |
(mbql-clause/define-tuple-mbql-clause :time :- :type/Time #_:timestr [:schema [:ref ::expression/string]] #_:unit [:ref ::temporal-bucketing/unit.time.interval]) | |
this has some stuff that's missing from [[::temporal-bucketing/unit.date-time.extract]], like | (mr/def ::temporal-extract.unit [:enum {:decode/normalize common/normalize-keyword} :year-of-era :quarter-of-year :month-of-year :week-of-year-iso :week-of-year-us :week-of-year-instance :day-of-month :day-of-week :day-of-week-iso :hour-of-day :minute-of-hour :second-of-minute]) |
TODO -- this should make sure unit agrees with the type of expression we're extracting from. | (mbql-clause/define-catn-mbql-clause :temporal-extract :- :type/Integer [:datetime [:schema [:ref ::expression/temporal]]] [:unit [:schema [:ref ::temporal-extract.unit]]] [:mode [:? [:schema [:ref ::week-mode]]]]) |