Arithmetic expressions like | (ns metabase.lib.schema.expression.arithmetic (:require [medley.core :as m] [metabase.lib.hierarchy :as lib.hierarchy] [metabase.lib.schema.common :as common] [metabase.lib.schema.expression :as expression] [metabase.lib.schema.mbql-clause :as mbql-clause] [metabase.lib.schema.temporal-bucketing :as temporal-bucketing] [metabase.types :as types] [metabase.util.malli.registry :as mr])) |
(defn- valid-interval-for-type? [[_tag _opts _n unit :as _interval] expr-type]
(let [unit-schema (cond
(isa? expr-type :type/Date) ::temporal-bucketing/unit.date.interval
(isa? expr-type :type/Time) ::temporal-bucketing/unit.time.interval
(isa? expr-type :type/DateTime) ::temporal-bucketing/unit.date-time.interval)]
(if unit-schema
(mr/validate unit-schema unit)
true))) | |
(mr/def ::args.numbers
[:repeat {:min 2} [:schema [:ref ::expression/number]]]) | |
Validate a | (defn- validate-plus-minus-temporal-arithmetic-expression
[[_tag _opts & exprs]]
(let [{non-intervals false, intervals true} (group-by #(isa? (expression/type-of %) :type/Interval) exprs)]
(cond
(not= (count non-intervals) 1)
"Temporal arithmetic expression must contain exactly one non-interval value"
(< (count intervals) 1)
"Temporal arithmetic expression must contain at least one :interval"
:else
(let [expr-type (expression/type-of (first non-intervals))]
(some (fn [[_tag _opts _n unit :as interval]]
(when-not (valid-interval-for-type? interval expr-type)
(str "Cannot add a " unit " interval to a " expr-type " expression")))
intervals))))) |
TODO -- doesn't really make sense to say something like | (mr/def ::plus-minus-temporal-interval-schema
[:and
{:error/message ":+ or :- clause with a temporal expression and one or more :interval clauses"}
[:cat
{:min 4}
[:enum :+ :-]
[:schema [:ref ::common/options]]
[:repeat [:schema [:ref :mbql.clause/interval]]]
[:schema [:ref ::expression/temporal]]
[:repeat [:schema [:ref :mbql.clause/interval]]]]
[:fn
{:error/fn (fn [{:keys [value]} _]
(str "Invalid :+ or :- clause: " (validate-plus-minus-temporal-arithmetic-expression value)))}
(complement validate-plus-minus-temporal-arithmetic-expression)]]) |
(mr/def ::plus-minus-numeric-schema
[:cat
{:error/message ":+ or :- clause with numeric args"}
:keyword
[:schema [:ref ::common/options]]
[:repeat {:min 2} [:schema [:ref ::expression/number]]]]) | |
(defn- type-of-numeric-arithmetic-arg [expr]
(let [expr-type (expression/type-of expr)]
(if (and (isa? expr-type ::expression/type.unknown)
(mr/validate :metabase.lib.schema.ref/ref expr))
:type/Number
expr-type))) | |
Given a sequence of args to a numeric arithmetic expression like | (defn- type-of-numeric-arithmetic-args
[args]
(transduce
(map type-of-numeric-arithmetic-arg)
(completing (fn [x y]
(if (nil? x)
y
(types/most-specific-common-ancestor x y))))
nil
args)) |
Given a temporal value plus one or more intervals | (defn- type-of-temporal-arithmetic-args
[args]
(let [first-non-interval-arg-type (m/find-first #(not (isa? % :type/Interval))
(map expression/type-of args))]
(if (isa? first-non-interval-arg-type ::expression/type.unknown)
:type/Temporal
first-non-interval-arg-type))) |
Given a sequence of
| (defn- type-of-arithmetic-args
[tag args]
(cond
;; temporal value + intervals
(some #(isa? (expression/type-of %) :type/Interval) args)
(type-of-temporal-arithmetic-args args)
;; the difference of exactly two temporal values
(and (= tag :-)
(= (count args) 2)
(or (every? #(isa? (expression/type-of %) :type/Date) args)
(every? #(isa? (expression/type-of %) :type/DateTime) args)))
:type/Interval
;; fall back to numeric args
:else (type-of-numeric-arithmetic-args args))) |
(mr/def ::temporal-difference-schema
[:cat
{:error/message ":- clause taking the difference of two temporal expressions"}
[:= {:decode/normalize common/normalize-keyword} :-]
[:schema [:ref ::common/options]]
[:schema [:ref ::expression/temporal]]
[:schema [:ref ::expression/temporal]]]) | |
(mbql-clause/define-mbql-clause :+
[:and
{:error/message "valid :+ clause"}
[:cat
[:= {:decode/normalize common/normalize-keyword} :+]
[:schema [:ref ::common/options]]
[:+ {:min 2} :any]]
[:multi
{:dispatch (fn [[_tag _opts & args]]
(if (some #(common/is-clause? :interval %)
args)
:temporal
:numeric))}
[:temporal [:ref ::plus-minus-temporal-interval-schema]]
[:numeric [:ref ::plus-minus-numeric-schema]]]]) | |
TODO -- should | (mbql-clause/define-mbql-clause :-
[:and
[:cat
[:= {:decode/normalize common/normalize-keyword} :-]
[:schema [:ref ::common/options]]
[:+ {:min 2} :any]]
[:multi
{:dispatch (fn [[_tag _opts & args]]
(cond
(some #(common/is-clause? :interval %) args) :temporal
(> (count args) 2) :numeric
:else :numeric-or-temporal-difference))}
[:temporal [:ref ::plus-minus-temporal-interval-schema]]
[:numeric [:ref ::plus-minus-numeric-schema]]
;; TODO -- figure out a way to know definitively what type of `:-` this should be so we don't need to use `:or`
[:numeric-or-temporal-difference
[:or
[:ref ::plus-minus-numeric-schema]
[:ref ::temporal-difference-schema]]]]]) |
(mbql-clause/define-catn-mbql-clause :* [:args ::args.numbers]) | |
we always do non-integer real division even if all the expressions are integers, e.g. [:/ so the results are 0.5 as opposed to 0. This is what people expect division to do | (mbql-clause/define-catn-mbql-clause :/ :- :type/Float [:args ::args.numbers]) |
(doseq [tag [:+ :- :*]] (lib.hierarchy/derive tag :lib.type-of/type-is-type-of-arithmetic-args)) | |
| (defmethod expression/type-of-method :lib.type-of/type-is-type-of-arithmetic-args [[tag _opts & args]] (type-of-arithmetic-args tag args)) |
(mbql-clause/define-tuple-mbql-clause :abs [:schema [:ref ::expression/number]]) | |
(lib.hierarchy/derive :abs :lib.type-of/type-is-type-of-first-arg) | |
(doseq [op [:log :exp :sqrt]]
(mbql-clause/define-tuple-mbql-clause op :- :type/Float
[:schema [:ref ::expression/number]])) | |
(doseq [op [:ceil :floor :round]]
(mbql-clause/define-tuple-mbql-clause op :- :type/Integer
[:schema [:ref ::expression/number]])) | |
(mbql-clause/define-tuple-mbql-clause :power #_num [:schema [:ref ::expression/number]] #_exp [:schema [:ref ::expression/number]]) | |
(defmethod expression/type-of-method :power
[[_tag _opts expr exponent]]
;; if both expr and exponent are integers, this will return an integer.
(if (and (isa? (expression/type-of expr) :type/Integer)
(isa? (expression/type-of exponent) :type/Integer))
:type/Integer
;; otherwise this will return some sort of number with a decimal place. e.g.
;;
;; (Math/pow 2 2.1) => 4.2870938501451725
;;
;; If we don't know the type of `expr` or `exponent` it's safe to assume `:type/Float` anyway, maybe not as
;; specific as `:type/Integer` but better than `:type/*` or `::expression/type.unknown`.
:type/Float)) | |