Internal implementation of the MBQL | (ns metabase.lib.util.match (:refer-clojure :exclude [replace]) (:require [clojure.core.match] [clojure.walk :as walk] [metabase.lib.util.match.impl] [net.cgrand.macrovich :as macros])) |
Generate a single approprate pattern for use with core.match based on the | (defn- generate-pattern
[pattern]
(cond
(keyword? pattern)
[[pattern '& '_]]
(and (set? pattern) (every? keyword? pattern))
[[`(:or ~@pattern) '& '_]]
;; special case for `_`, we'll let you match anything with that
(= pattern '_)
[pattern]
(symbol? pattern)
`[(~'_ :guard (metabase.lib.util.match.impl/match-with-pred-or-class ~pattern))]
:else
[pattern])) |
(defn- recur-form? [form]
(and (seq? form)
(= 'recur (first form)))) | |
Replace any | (defn- rewrite-recurs
[fn-name result-form]
(walk/postwalk
(fn [form]
(if (recur-form? form)
;; we *could* use plain `recur` here, but `core.match` cannot apply code size optimizations if a `recur` form
;; is present. Instead, just do a non-tail-call-optimized call to the pattern fn so `core.match` can generate
;; efficient code.
;;
;; (recur [:new-clause ...]) ; -> (match-123456 &parents [:new-clause ...])
`(~fn-name ~'&parents ~@(rest form))
form))
result-form)) |
Generate the
| (defn- generate-patterns-and-results
[fn-name patterns-and-results & {:keys [wrap-result-forms?]}]
(mapcat (fn [[pattern result]]
[(generate-pattern pattern) (let [result (rewrite-recurs fn-name result)]
(if (or (not wrap-result-forms?)
(and (seq? result)
(= fn-name (first result))))
result
[result]))])
(partition 2 2 ['&match] patterns-and-results))) |
If the last pattern passed in was | (defn- skip-else-clause? ;; TODO - why don't we just let people pass their own `:else` clause instead? [patterns-and-results] (= '_ (second (reverse patterns-and-results)))) |
(defmethod clojure.core.match/emit-pattern-for-syntax [:isa? :default]
[[_ parent]] {:clojure.core.match/tag ::isa? :parent parent}) | |
(defmethod clojure.core.match/to-source ::isa?
[{parent :parent} ocr]
`(isa? ~ocr ~parent)) | |
Internal impl for | (defmacro match**
[& args]
(macros/case
:clj `(clojure.core.match/match ~@args)
:cljs `(cljs.core.match/match ~@args))) |
Internal impl for | (defmacro match*
[form patterns-and-results]
(let [match-fn-symb (gensym "match-")]
`(seq
(filter
some?
((fn ~match-fn-symb [~'&parents ~'&match]
(match** [~'&match]
~@(generate-patterns-and-results match-fn-symb patterns-and-results, :wrap-result-forms? true)
~@(when-not (skip-else-clause? patterns-and-results)
[:else `(metabase.lib.util.match.impl/match-in-collection ~match-fn-symb ~'&parents ~'&match)])))
[]
~form))))) |
Return a sequence of things that match a
Examples: ;; keyword pattern (match {:fields [[:field 10 nil]]} :field) ; -> [[:field 10 nil]] ;; set of keywords (match some-query #{:field :expression}) ; -> [[:field 10 nil], [:expression "wow"], ...] ;; ;; symbol naming a Class ;; match anything that is an instance of that class (match some-query java.util.Date) ; -> [[#inst "2018-10-08", ...] ;; symbol naming a predicate function ;; match anything that satisfies that predicate (match some-query (every-pred integer? even?)) ; -> [2 4 6 8] ;; match anything with Using `core.match` patternsSee Pattern-matching works almost exactly the way it does when using
Returing something other than the exact match with result bodyBy default, ;; just return the IDs of Field ID clauses (match some-query [:field (id :guard integer?) _] id) ; -> [1 2 3] You can also use result body to filter results; any (match some-query [:field (id :guard integer?) _] (when (even? id) id)) ;; -> [2 4 6 8] Of course, it's more efficient to let You can also call `&match` and `&parents` anaphorsFor more advanced matches, like finding a (lib.util.match/match {:filter [:time-interval [:field 1 nil] :current :month]} :field ;; &parents will be [:filter :time-interval] (when (contains? (set &parents) :time-interval) &match)) ;; -> [[:field 1 nil]] | (defmacro match
{:style/indent :defn}
[x & patterns-and-results]
;; Actual implementation of these macros is in `mbql.util.match`. They're in a seperate namespace because they have
;; lots of other functions and macros they use for their implementation (which means they have to be public) that we
;; would like to discourage you from using directly.
`(match* ~x ~patterns-and-results)) |
Like | (defmacro match-one
{:style/indent :defn}
[x & patterns-and-results]
`(first (match* ~x ~patterns-and-results))) |
TODO - it would be ultra handy to have a {:query {:source-table 1, :joins [{:source-table 2, ...}]}} it would be useful to be able to do ;; get all the source tables (lib.util.match/match-all query (&match :guard (every-pred map? :source-table)) (:source-table &match)) | |
Internal implementation for | (defmacro replace*
[form patterns-and-results]
(let [replace-fn-symb (gensym "replace-")]
`((fn ~replace-fn-symb [~'&parents ~'&match]
(match** [~'&match]
~@(generate-patterns-and-results replace-fn-symb patterns-and-results, :wrap-result-forms? false)
~@(when-not (skip-else-clause? patterns-and-results)
[:else `(metabase.lib.util.match.impl/replace-in-collection ~replace-fn-symb ~'&parents ~'&match)])))
[]
~form))) |
Like | (defmacro replace
{:style/indent :defn}
[x & patterns-and-results]
;; as with `match` actual impl is in `match` namespace to discourage you from using the constituent functions and
;; macros that power this macro directly
`(replace* ~x ~patterns-and-results)) |
Like | (defmacro replace-in
{:style/indent :defn}
[x ks & patterns-and-results]
`(metabase.lib.util.match.impl/update-in-unless-empty ~x ~ks (fn [x#] (replace* x# ~patterns-and-results)))) |
TODO - it would be useful to have something like a | |