(ns metabase.lib.schema.mbql-clause (:require [malli.core :as mc] [metabase.lib.schema.common :as common] [metabase.lib.schema.expression :as expression] [metabase.types] [metabase.util.malli :as mu] [metabase.util.malli.registry :as mr])) | |
(comment metabase.types/keep-me) | |
Set of all registered MBQL clause tags e.g. #{:starts-with} | (defonce ^:private tag-registry
(atom #{})) |
Given an MBQL clause tag like | (defn tag->registered-schema-name [tag] (keyword "mbql.clause" (name tag))) |
(def ^:private invalid-clause-schema
[:fn {:error/message "not a known MBQL clause"} (constantly false)]) | |
Build the schema for | (defn- clause-schema
[]
(into [:multi
{:dispatch common/mbql-clause-tag
:error/fn (fn [{:keys [value]} _]
(if-let [tag (common/mbql-clause-tag value)]
(str "Invalid " tag " clause: " (pr-str value))
"not an MBQL clause"))}
[::mc/default invalid-clause-schema]]
(map (fn [tag]
[tag [:ref (tag->registered-schema-name tag)]]))
@tag-registry)) |
(defn- update-clause-schema! []
(mr/def ::clause
(clause-schema))) | |
create an initial empty definition of | (update-clause-schema!) |
whenever [[tag-registry]] is updated, update the | (add-watch tag-registry
::update-schemas
(fn [_key _ref _old-state _new-state]
(update-clause-schema!))) |
Register the (define-mbql-clause :is-null :- :type/Boolean [:tuple [:= :is-null] ::common/options [:ref :metabase.lib.schema.expression/expression]]) | (mu/defn define-mbql-clause
([tag :- simple-keyword?
schema]
(let [schema-name (tag->registered-schema-name tag)]
(mr/def schema-name schema)
;; only need to update the registry and calculated schemas if this is the very first time we're defining this
;; clause. Otherwise since they're wrapped in `:ref` we don't need to recalculate them. This way we can avoid tons
;; of pointless recalculations every time we reload a namespace.
(when-not (contains? @tag-registry tag)
(swap! tag-registry conj tag)))
nil)
([tag :- simple-keyword?
_arrow :- [:= :-]
return-type :- ::expression/base-type
schema]
(define-mbql-clause tag schema)
(defmethod expression/type-of-method tag
[_clause]
return-type)
nil)) |
TODO: Support options more nicely - these don't allow for overriding the options, but we have a few cases where that
is necessary. See for example the inclusion of | |
Helper intended for use with [[define-mbql-clause]]. Create an MBQL clause schema with | (defn catn-clause-schema
[tag & args]
{:pre [(simple-keyword? tag)
(every? vector? args)
(every? keyword? (map first args))]}
[:schema
(into [:catn
{:error/message (str "Valid " tag " clause")}
[:tag [:= {:decode/normalize common/normalize-keyword} tag]]
[:options [:schema [:ref ::common/options]]]]
args)]) |
Helper intended for use with [[define-mbql-clause]]. Create a clause schema with | (defn tuple-clause-schema
[tag & args]
{:pre [(simple-keyword? tag)]}
(into [:tuple
{:error/message (str "Valid " tag " clause")}
[:= {:decode/normalize common/normalize-keyword} tag]
[:ref ::common/options]]
args)) |
Even more convenient functions! | |
Helper. Combines [[define-mbql-clause]] and the result of applying | (defn define-mbql-clause-with-schema-fn
[schema-fn tag & args]
(let [[return-type & args] (if (= (first args) :-)
(cons (second args) (drop 2 args))
(cons nil args))
schema (apply schema-fn tag args)]
(if return-type
(define-mbql-clause tag :- return-type schema)
(define-mbql-clause tag schema)))) |
Helper. Combines [[define-mbql-clause]] and [[tuple-clause-schema]]. | (defn define-tuple-mbql-clause [tag & args] (apply define-mbql-clause-with-schema-fn tuple-clause-schema tag args)) |
Helper. Combines [[define-mbql-clause]] and [[catn-clause-schema]]. | (defn define-catn-mbql-clause [tag & args] (apply define-mbql-clause-with-schema-fn catn-clause-schema tag args)) |
For REPL/test usage: get the definition of the schema associated with an MBQL clause tag. | (defn resolve-schema [tag] (mr/resolve-schema (tag->registered-schema-name tag))) |