Schemas for the various types of filter clauses that you'd pass to | (ns metabase.lib.schema.filter (:require [metabase.lib.schema.common :as common] [metabase.lib.schema.expression :as expression] [metabase.lib.schema.id :as id] [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])) |
Helper intended for use with [[define-mbql-clause]]. Create a clause schema with | (defn- tuple-clause-of-comparables-schema
[compared-position-pairs]
(fn [tag & args]
{:pre [(simple-keyword? tag)]}
[:and
(apply mbql-clause/tuple-clause-schema tag args)
[:fn
{:error/message "arguments should be comparable"}
(fn [[_tag _opts & args]]
(let [argv (vec args)]
(or expression/*suppress-expression-type-check?*
(every? true? (map (fn [[i j]]
(expression/comparable-expressions? (get argv i) (get argv j)))
compared-position-pairs)))))]])) |
(mr/def ::default-filter-operator "Filter operators that should be supported by any column type. Note that the FE allows only `:is-empty` and `:not-empty` for string columns." [:enum :is-null :not-null]) | |
(mr/def ::string-filter-operator "String filter operators supported by the FE. Note that the FE does not support `:is-null` and `:not-null` with string columns; `:is-empty` and `:not-empty` should be used instead." [:enum :is-empty :not-empty := :!= :contains :does-not-contain :starts-with :ends-with]) | |
(mr/def ::string-filter-options
"String filter operator options. Only set for `:contains`, `:does-not-contain`, `:starts-with`, `:ends-with`
operators."
[:map [:case-sensitive {:optional true} :boolean]]) ; default true | |
(mr/def ::number-filter-operator "Numeric filter operators supported by the FE." [:enum :is-null :not-null := :!= :> :>= :< :<= :between]) | |
(mr/def ::coordinate-filter-operator "Coordinate filter operators supported by the FE. Note that the FE does not support `:is-null` and `:not-null` for coordinate columns." [:enum := :!= :> :>= :< :<= :between :inside]) | |
(mr/def ::boolean-filter-operator "Boolean filter operators supported by the FE. Note that `:!=` is not supported." [:enum :is-null :not-null :=]) | |
(mr/def ::specific-date-filter-operator "Specific date filter operators supported by the FE." [:enum := :> :< :between]) | |
(mr/def ::exclude-date-filter-operator "Exclude date filter operators supported by the FE." [:enum :!= :is-null :not-null]) | |
(mr/def ::exclude-date-filter-unit "Temporal extraction units supported by exclude date filters." [:enum :hour-of-day :day-of-week :month-of-year :quarter-of-year]) | |
(mr/def ::time-filter-operator "Time filter operators supported by the FE." [:enum :is-null :not-null :> :< :between]) | |
(mr/def ::time-interval-options
"Options for `:time-interval` operator. Note that `:relative-time-interval` does not support these options."
[:map [:include-current {:optional true} :boolean]]) ; default false | |
(doseq [op [:and :or]]
(mbql-clause/define-catn-mbql-clause op :- :type/Boolean
[:args [:repeat {:min 2} [:schema [:ref ::expression/boolean]]]])) | |
(mbql-clause/define-tuple-mbql-clause :not :- :type/Boolean [:ref ::expression/boolean]) | |
(doseq [op [:= :!= :in :not-in]]
(mbql-clause/define-catn-mbql-clause op :- :type/Boolean
[:args [:repeat {:min 2} [:schema [:ref ::expression/equality-comparable]]]])) | |
(doseq [op [:< :<= :> :>=]]
(mbql-clause/define-mbql-clause-with-schema-fn (tuple-clause-of-comparables-schema #{[0 1]})
op :- :type/Boolean
#_x [:ref ::expression/orderable]
#_y [:ref ::expression/orderable])) | |
(mbql-clause/define-mbql-clause-with-schema-fn (tuple-clause-of-comparables-schema #{[0 1] [0 2]})
:between :- :type/Boolean
;; TODO -- should we enforce that min is <= max (for literal number values?)
#_expr [:ref ::expression/orderable]
#_min [:ref ::expression/orderable]
#_max [:ref ::expression/orderable]) | |
sugar: a pair of | (mbql-clause/define-mbql-clause-with-schema-fn (tuple-clause-of-comparables-schema #{[0 2] [0 4] [1 3] [1 5]})
:inside :- :type/Boolean
;; TODO -- should we enforce that lat-min <= lat-max and lon-min <= lon-max? Should we enforce that -90 <= lat 90
;; and -180 <= lon 180 ?? (for literal number values)
#_lat-expr [:ref ::expression/orderable]
#_lon-expr [:ref ::expression/orderable]
#_lat-max [:ref ::expression/orderable] ; north
#_lon-min [:ref ::expression/orderable] ; west
#_lat-min [:ref ::expression/orderable] ; south
#_lon-max [:ref ::expression/orderable]) ; east |
null checking expressions these are sugar for [:= ... nil] and [:!= ... nil] respectively | (doseq [op [:is-null :not-null]]
(mbql-clause/define-tuple-mbql-clause op :- :type/Boolean
[:ref ::expression/expression])) |
:is-empty is sugar for [:or [:= ... nil] [:= ... ""]] for emptyable arguments :not-empty is sugar for [:and [:!= ... nil] [:!= ... ""]] for emptyable arguments For non emptyable arguments expansion is same with :is-null and :not-null | (doseq [op [:is-empty :not-empty]]
(mbql-clause/define-tuple-mbql-clause op :- :type/Boolean
[:ref ::expression/expression])) |
N-ary [:ref ::expression/string] filter clauses. These also accept a
| (doseq [op [:starts-with :ends-with :contains :does-not-contain]]
(mbql-clause/define-mbql-clause op :- :type/Boolean
[:schema [:catn {:error/message (str "Valid " op " clause")}
[:tag [:= {:decode/normalize common/normalize-keyword} op]]
[:options [:merge ::common/options ::string-filter-options]]
[:args [:repeat {:min 2} [:schema [:ref ::expression/string]]]]]])) |
SUGAR: rewritten as a filter clause with a relative-datetime value | (mbql-clause/define-mbql-clause :time-interval :- :type/Boolean
;; TODO -- we should probably further constrain this so you can't do weird stuff like
;;
;; [:time-interval {} <time> :current :year]
;;
;; using units that don't agree with the expr type
[:tuple
[:= {:decode/normalize common/normalize-keyword} :time-interval]
[:merge ::common/options ::time-interval-options]
#_expr [:ref ::expression/temporal]
#_n [:multi
{:dispatch (some-fn keyword? string?)}
[true [:enum {:decode/normalize common/normalize-keyword} :current :last :next]]
;; I guess there's no reason you shouldn't be able to do something like 1 + 2 in here
[false [:ref ::expression/integer]]]
#_unit [:ref ::temporal-bucketing/unit.date-time.interval]]) |
(mbql-clause/define-mbql-clause :during :- :type/Boolean
[:tuple
[:= {:decode/normalize common/normalize-keyword} :during]
::common/options
#_expr [:ref ::expression/temporal]
#_value [:or [:ref ::literal/string.date] [:ref ::literal/string.datetime]]
#_unit [:ref ::temporal-bucketing/unit.date-time.interval]]) | |
(mbql-clause/define-mbql-clause :relative-time-interval :- :type/Boolean
[:tuple
[:= {:decode/normalize common/normalize-keyword} :relative-time-interval]
;; `relative-time-interval` does not support options to eg. include/exclude start or end point. Only int values
;; are allowed for intervals.
::common/options
#_col [:ref ::expression/temporal]
#_value :int
#_bucket [:ref ::temporal-bucketing/unit.date-time.interval]
#_offset-value :int
#_offset-bucket [:ref ::temporal-bucketing/unit.date-time.interval]]) | |
segments are guaranteed to return valid filter clauses and thus booleans, right? | (mbql-clause/define-mbql-clause :segment :- :type/Boolean
[:tuple
[:= {:decode/normalize common/normalize-keyword} :segment]
::common/options
[:multi
{:dispatch string?}
[true ::common/non-blank-string]
[false ::id/segment]]]) |
(mr/def ::operator
[:map
[:lib/type [:= :operator/filter]]
[:short [:enum
:= :!= :inside :between :< :> :<= :>= :is-null :not-null :is-empty :not-empty
:contains :does-not-contain :starts-with :ends-with :time-interval :relative-time-interval]]
;; this is used for display name and it depends on the arguments to the filter clause itself... e.g.
;;
;; number_a < number_b
;;
;; gets a display name of "less than" for the operator, while
;;
;; timestamp_a < timestamp_b
;;
;; gets a display name of "before" for the operator. We don't want to encode the display name in the `::operator`
;; definition itself, because it forces us to do i18n in the definition itself; it's nicer to have static
;; definitions and only add the display name when we call `display-name` or `display-info`.
[:display-name-variant :keyword]]) | |