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 | |