From 72c05c40bc8d4c490f2bcdf578249e1e18c1ad90 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 25 Oct 2023 15:26:22 +0000 Subject: [PATCH] Update backend docs on Wed Oct 25 15:26:22 UTC 2023 --- index.html | 1938 ++++++++++++++++++++++++++-------------------------- 1 file changed, 982 insertions(+), 956 deletions(-) diff --git a/index.html b/index.html index 99f2d29..0e5ee08 100644 --- a/index.html +++ b/index.html @@ -2866,7 +2866,7 @@ }; })(SyntaxHighlighter); Metabase -- Marginalia

Metabase


The simplest, fastest way to get business intelligence and analytics to everyone in your company 😋

-



(this space intentionally left almost blank)

namespaces

 

Metabase Backend Developer Documentation

+



(this space intentionally left almost blank)

namespaces

 

Metabase Backend Developer Documentation

Welcome to Metabase! Here are links to useful resources.

@@ -3994,8 +3994,7 @@

Important Libraries

(testing "changes not should be tracked" (is (empty? (model-tracking/changes)))) (testing "should take affects" - (is (nil? (t2/select-one Collection :id coll-id)))))))))
 
-
(ns dev.nocommit.macros)
 
+ (is (nil? (t2/select-one Collection :id coll-id)))))))))
 
(ns dev.portal
   (:require [portal.api :as p]))

The handle to portal. Can be used as @p to get the selected item.

(defonce
@@ -6989,7 +6988,9 @@ 

Important Libraries

in the last 24 hours." [] (if-let [dashboard-id (view-log/most-recently-viewed-dashboard)] - (let [dashboard (api/check-404 (t2/select-one Dashboard :id dashboard-id))] + (let [dashboard (-> (t2/select-one Dashboard :id dashboard-id) + api/check-404 + (t2/hydrate [:collection :is_personal]))] (if (mi/can-read? dashboard) dashboard api/generic-204-no-content)) @@ -7845,13 +7846,13 @@

Important Libraries

(let [raw-card (t2/select-one Card :id id) card (-> raw-card (t2/hydrate :creator - :dashboard_count - :parameter_usage_count - :can_write - :average_query_time - :last_query_start - :collection - [:moderation_reviews :moderator_details]) + :dashboard_count + :parameter_usage_count + :can_write + :average_query_time + :last_query_start + [:collection :is_personal] + [:moderation_reviews :moderator_details]) collection.root/hydrate-root-collection (cond-> ;; card (:dataset raw-card) (t2/hydrate :persisted)) @@ -8152,11 +8153,12 @@

Important Libraries

;; returned one -- See #4283 (u/prog1 (-> card (t2/hydrate :creator - :dashboard_count - :can_write - :average_query_time - :last_query_start - :collection [:moderation_reviews :moderator_details]) + :dashboard_count + :can_write + :average_query_time + :last_query_start + [:collection :is_personal] + [:moderation_reviews :moderator_details]) (assoc :last-edit-info (last-edit/edit-information-for-user creator))) (when timed-out? (schedule-metadata-saving result-metadata-chan <>))))))

/

@@ -8342,7 +8344,8 @@

Important Libraries

:can_write :average_query_time :last_query_start - :collection [:moderation_reviews :moderator_details]) + [:collection :is_personal] + [:moderation_reviews :moderator_details]) (cond-> ;; card (:dataset card) (t2/hydrate :persisted)) (assoc :last-edit-info (last-edit/edit-information-for-user @api/*current-user*)))))

/:id

@@ -8867,7 +8870,7 @@

Important Libraries

(cond->> collections (mi/can-read? root) (cons root)))) - (t2/hydrate collections :can_write) + (t2/hydrate collections :can_write :is_personal) ;; remove the :metabase.models.collection.root/is-root? tag since FE doesn't need it ;; and for personal collections we translate the name to user's locale (for [collection collections] @@ -9390,7 +9393,7 @@

Important Libraries

[collection :- collection/CollectionWithLocationAndIDOrRoot] (-> collection collection/personal-collection-with-ui-details - (t2/hydrate :parent_id :effective_location [:effective_ancestors :can_write] :can_write)))

/:id

+ (t2/hydrate :parent_id :effective_location [:effective_ancestors :can_write] :can_write :is_personal)))

/:id

(api/defendpoint GET 
   "Fetch a specific Collection with standard details added"
   [id]
@@ -10681,7 +10684,7 @@ 

Important Libraries

:can_write :param_fields :param_values - :collection) + [:collection :is_personal]) collection.root/hydrate-root-collection api/read-check api/check-not-archived @@ -10751,9 +10754,9 @@

Important Libraries

{:user-id api/*current-user-id* :dashboard-id dashboard-id}))) (let [dashcards (if (seq id->new-tab-id) - (map #(assoc % :dashboard_tab_id (id->new-tab-id (:dashboard_tab_id %))) - dashcards) - dashcards)] + (map #(assoc % :dashboard_tab_id (id->new-tab-id (:dashboard_tab_id %))) + dashcards) + dashcards)] (if-not deep? dashcards (keep (fn [dashboard-card] @@ -10882,7 +10885,7 @@

Important Libraries

:embedding_params :archived :auto_apply_filters}))] (t2/update! Dashboard id updates)))) ;; now publish an event and return the updated Dashboard - (let [dashboard (t2/select-one :model/Dashboard :id id)] + (let [dashboard (t2/hydrate (t2/select-one :model/Dashboard :id id) [:collection :is_personal])] (events/publish-event! :event/dashboard-update (assoc dashboard :actor_id api/*current-user-id*)) (assoc dashboard :last-edit-info (last-edit/edit-information-for-user @api/*current-user*))))

/:id

@@ -26505,26 +26508,6 @@

3295.

[tables] (for [table tables] (assoc table :domain_entity (domain-entity-for-table table))))
 
-
(ns metabase.domain-entities.malli
-  (:require
-    [malli.core :as mc]
-    [malli.util :as mut]
-    [metabase.domain-entities.converters])
-  (:require-macros [metabase.domain-entities.malli]))

Given a schema and a value path (as opposed to a schema path), finds the schema for that -path. Throws if there are multiple such paths and those paths have different schemas.

-
(clojure.core/defn schema-for-path
-  [schema path]
-  (let [paths (-> schema mc/schema (mut/in->paths path))]
-    (cond
-      (empty? paths)      (throw (ex-info "Path does not match schema" {:schema schema :path path}))
-      (= (count paths) 1) (mut/get-in schema (first paths))
-      :else (let [child-schemas (map #(mut/get-in schema %) paths)]
-              (if (apply = child-schemas)
-                (first child-schemas)
-                (throw (ex-info "Value path has multiple schema paths, with different schemas"
-                                {:schema        schema
-                                 :paths         paths
-                                 :child-schemas child-schemas})))))))
 
(ns metabase.domain-entities.malli
   (:refer-clojure :exclude [defn])
   (:require
@@ -26637,7 +26620,27 @@ 

3295.

`(do (-define-getter-and-setter ~schema ~sym ~path) ~(when (seq more) - `(define-getters-and-setters ~schema ~@more))))
 
+ `(define-getters-and-setters ~schema ~@more))))
 
+
(ns metabase.domain-entities.malli
+  (:require
+    [malli.core :as mc]
+    [malli.util :as mut]
+    [metabase.domain-entities.converters])
+  (:require-macros [metabase.domain-entities.malli]))

Given a schema and a value path (as opposed to a schema path), finds the schema for that +path. Throws if there are multiple such paths and those paths have different schemas.

+
(clojure.core/defn schema-for-path
+  [schema path]
+  (let [paths (-> schema mc/schema (mut/in->paths path))]
+    (cond
+      (empty? paths)      (throw (ex-info "Path does not match schema" {:schema schema :path path}))
+      (= (count paths) 1) (mut/get-in schema (first paths))
+      :else (let [child-schemas (map #(mut/get-in schema %) paths)]
+              (if (apply = child-schemas)
+                (first child-schemas)
+                (throw (ex-info "Value path has multiple schema paths, with different schemas"
+                                {:schema        schema
+                                 :paths         paths
+                                 :child-schemas child-schemas})))))))
 
(ns metabase.domain-entities.specs
   (:require
    [medley.core :as m]
@@ -52732,8 +52735,8 @@ 

`:allowed-for`

;; Then see if the root-level ancestor is a Personal Collection (Personal Collections can only got in the Root ;; Collection.) (t2/exists? Collection - :id (first (location-path->ids (:location collection))) - :personal_owner_id [:not= nil]))))

----------------------------------------------------- INSERT -----------------------------------------------------

+ :id (first (location-path->ids (:location collection))) + :personal_owner_id [:not= nil]))))

----------------------------------------------------- INSERT -----------------------------------------------------

(t2/define-before-insert :model/Collection
   [{collection-name :name, :as collection}]
@@ -53135,7 +53138,29 @@ 

`:allowed-for`

;; as a side-effect. This will ensure this property never comes back as `nil` (for [user users] (assoc user :personal_collection_id (or (user-id->collection-id (u/the-id user)) - (user->personal-collection-id (u/the-id user))))))))

Set of Collection namespaces (as keywords) that instances of this model are allowed to go in. By default, only the + (user->personal-collection-id (u/the-id user))))))))

+
(mi/define-batched-hydration-method collection-is-personal
+  :is_personal
+  "Efficiently hydrate the `:is_personal` property of a sequence of Collections.
+  `true` means the collection is or nested in a personal collection."
+  [collections]
+  (if (= 1 (count collections))
+    (let [collection (first collections)]
+      (if (some? collection)
+        [(assoc collection :is_personal (is-personal-collection-or-descendant-of-one? collection))]
+        ;; root collection is nil
+        [collection]))
+    (let [personal-collection-ids (t2/select-pks-set :model/collection :personal_owner_id [:not= nil])
+          location-is-personal    (fn [location]
+                                    (boolean
+                                     (and (string? location)
+                                          (some #(str/starts-with? location (format "/%d/" %)) personal-collection-ids))))]
+      (map (fn [{:keys [location personal_owner_id] :as coll}]
+             (if (some? coll)
+               (assoc coll :is_personal (or (some? personal_owner_id)
+                                            (location-is-personal location)))
+               nil))
+           collections))))

Set of Collection namespaces (as keywords) that instances of this model are allowed to go in. By default, only the default namespace (namespace = nil).

(defmulti allowed-namespaces
   {:arglists '([model])}
@@ -53446,6 +53471,7 @@ 

`:allowed-for`

:snippets (tru "Top folder") (tru "Our analytics")) :namespace collection-namespace + :is_personal false :id "root"))
(defn- hydrated-root-collection
   []
@@ -90806,32 +90832,7 @@ 

Quartz JavaDoc

(throw (.getCause e)))))

Run body in a future and throw an exception if it fails to complete after timeout-ms.

(defmacro with-timeout
   [timeout-ms & body]
-  `(do-with-timeout ~timeout-ms (fn [] ~@body)))
 
-
(ns metabase.util.log
-  (:require
-   [goog.log :as glog]
-   [goog.string :as gstring]
-   [goog.string.format :as gstring.format]
-   [lambdaisland.glogi :as log]
-   [lambdaisland.glogi.console :as glogi-console])
-  (:require-macros
-   [metabase.util.log]))

The formatting functionality is only loaded if you depend on goog.string.format.

-
(comment gstring.format/keep-me)
-
(glogi-console/install!)
-(log/set-levels {:glogi/root :info})

Part of the internals of [[glogi-logp]] etc.

-
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
-(defn is-loggable?
-  [logger-name level]
-  (glog/isLoggable (log/logger logger-name) (log/level level)))

Part of the internals of [[logf]].

-
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
-(defn format-msg
-  [fmt & args]
-  (apply gstring/format fmt args))

Converts our standard metabase.util.log levels to those understood by glogi.

-
(defn glogi-level
-  [level]
-  (if (= level :fatal)
-    :shout
-    level))
 

Common logging interface that wraps clojure.tools.logging in JVM Clojure and Glogi in CLJS.

+ `(do-with-timeout ~timeout-ms (fn [] ~@body)))
 

Common logging interface that wraps clojure.tools.logging in JVM Clojure and Glogi in CLJS.

The interface is the same as [[clojure.tools.logging]].

(ns metabase.util.log
@@ -90988,7 +90989,32 @@ 

Quartz JavaDoc

(defmacro with-no-logs
   [& body]
   `(binding [clojure.tools.logging/*logger-factory* clojure.tools.logging.impl/disabled-logger-factory]
-     ~@body))
 
+ ~@body))
 
+
(ns metabase.util.log
+  (:require
+   [goog.log :as glog]
+   [goog.string :as gstring]
+   [goog.string.format :as gstring.format]
+   [lambdaisland.glogi :as log]
+   [lambdaisland.glogi.console :as glogi-console])
+  (:require-macros
+   [metabase.util.log]))

The formatting functionality is only loaded if you depend on goog.string.format.

+
(comment gstring.format/keep-me)
+
(glogi-console/install!)
+(log/set-levels {:glogi/root :info})

Part of the internals of [[glogi-logp]] etc.

+
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
+(defn is-loggable?
+  [logger-name level]
+  (glog/isLoggable (log/logger logger-name) (log/level level)))

Part of the internals of [[logf]].

+
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
+(defn format-msg
+  [fmt & args]
+  (apply gstring/format fmt args))

Converts our standard metabase.util.log levels to those understood by glogi.

+
(defn glogi-level
+  [level]
+  (if (= level :fatal)
+    :shout
+    level))
 
(ns metabase.util.malli.defn
   (:refer-clojure :exclude [defn])
   (:require
@@ -91699,17 +91725,7 @@ 

Quartz JavaDoc

(def UUIDString
   (mu/with-api-error-message
    [:re u/uuid-regex]
-   (deferred-tru "value must be a valid UUID.")))
 
-
(ns metabase.util.memoize
-  (:require
-   [clojure.core.memoize :as memoize]
-   [metabase.shared.util.namespaces :as shared.ns]))
-
(comment
-  memoize/keep-me)
-
(shared.ns/import-fns
- [memoize
-  lru
-  memoizer])
 

Copied from clojure.core.memoize.

+ (deferred-tru "value must be a valid UUID.")))
 

Copied from clojure.core.memoize.

(ns metabase.util.memoize
   (:require [cljs.cache :as cache]))

Similar to clojure.lang.Delay, but will not memoize an exception and will instead retry. @@ -91821,7 +91837,17 @@

Quartz JavaDoc

([f tkey threshold] (lru f {} tkey threshold)) ([f base key threshold] (assert (= key :lru/threshold) (str "wrong parameter key " key)) - (memoizer f (cache/lru-cache-factory {} :threshold threshold) base)))
 
+ (memoizer f (cache/lru-cache-factory {} :threshold threshold) base)))
 
+
(ns metabase.util.memoize
+  (:require
+   [clojure.core.memoize :as memoize]
+   [metabase.shared.util.namespaces :as shared.ns]))
+
(comment
+  memoize/keep-me)
+
(shared.ns/import-fns
+ [memoize
+  lru
+  memoizer])
 
(ns metabase.util.methodical.null-cache
   (:require
    [methodical.interface])
@@ -92928,549 +92954,84 @@ 

Quartz JavaDoc

(mbql-clause/define-catn-mbql-clause :temporal-extract :- :type/Integer
   [:datetime [:schema [:ref ::expression/temporal]]]
   [:unit     [:schema [:ref ::temporal-extract.unit]]]
-  [:mode     [:? [:schema [:ref ::temporal-extract.week-mode]]]])
 

Utility functions used by the Queries in metabase-lib.

-
(ns metabase.domain-entities.queries.util
-  (:require
-   [metabase.util.malli :as mu]
-   #?@(:cljs ([metabase.domain-entities.converters :as converters]))))

Schema for an Expression that's part of a query filter.

-
(def Expression
-  :any)

Malli schema for a map of expressions by name.

-
(def ExpressionMap
-  [:map-of string? Expression])

Malli schema for a list of {:name :expression} maps.

-
(def ExpressionList
-  [:vector [:map [:name string?] [:expression Expression]]])
-
(def ^:private ->expression-map
-  #?(:cljs (converters/incoming ExpressionMap)
-     :clj  identity))
-
(def ^:private expression-list->
-  #?(:cljs (converters/outgoing ExpressionList)
-     :clj  identity))
-
(mu/defn ^:export expressions-list :- ExpressionList
-  "Turns a map of expressions by name into a list of `{:name name :expression expression}` objects."
-  [expressions :- ExpressionMap]
-  (->> expressions
-       ->expression-map
-       (mapv (fn [[name expr]] {:name name :expression expr}))
-       expression-list->))
-
(defn- unique-name [names original-name index]
-  (let [indexed-name (str original-name " (" index ")")]
-    (if (names indexed-name)
-      (recur names original-name (inc index))
-      indexed-name)))
-
(mu/defn ^:export unique-expression-name :- string?
-  "Generates an expression name that's unique in the given map of expressions."
-  [expressions   :- ExpressionMap
-   original-name :- string?]
-  (let [expression-names (-> expressions ->expression-map keys set)]
-    (if (not (expression-names original-name))
-      original-name
-      (let [re-duplicates (re-pattern (str "^" original-name " \\([0-9]+\\)$"))
-            duplicates    (set (filter #(or (= % original-name)
-                                            (re-matches re-duplicates %))
-                                       expression-names))]
-        (unique-name duplicates original-name (count duplicates))))))
 
-
(ns metabase.lib.util
-  (:refer-clojure :exclude [format])
+  [:mode     [:? [:schema [:ref ::temporal-extract.week-mode]]]])
 

Common utility functions useful throughout the codebase.

+
(ns metabase.util
   (:require
-   #?@(:clj
-       ([potemkin :as p]))
-   #?@(:cljs
-       (["crc-32" :as CRC32]
-        [goog.string :as gstring]
-        [goog.string.format :as gstring.format]))
+   [camel-snake-kebab.internals.macros :as csk.macros]
+   [clojure.data :refer [diff]]
+   [clojure.pprint :as pprint]
    [clojure.set :as set]
    [clojure.string :as str]
+   [clojure.walk :as walk]
+   [flatland.ordered.map :refer [ordered-map]]
    [medley.core :as m]
-   [metabase.lib.common :as lib.common]
-   [metabase.lib.hierarchy :as lib.hierarchy]
-   [metabase.lib.options :as lib.options]
-   [metabase.lib.schema :as lib.schema]
-   [metabase.lib.schema.common :as lib.schema.common]
-   [metabase.lib.schema.expression :as lib.schema.expression]
-   [metabase.lib.schema.id :as lib.schema.id]
-   [metabase.lib.schema.ref :as lib.schema.ref]
-   [metabase.mbql.util :as mbql.u]
-   [metabase.shared.util.i18n :as i18n]
-   [metabase.util :as u]
-   [metabase.util.malli :as mu]))
-
#?(:clj
-   (set! *warn-on-reflection* true))

The formatting functionality is only loaded if you depend on goog.string.format.

-
#?(:cljs (comment gstring.format/keep-me))
+   [metabase.shared.util.i18n :refer [tru] :as i18n]
+   [metabase.shared.util.namespaces :as u.ns]
+   [metabase.util.format :as u.format]
+   [metabase.util.log :as log]
+   [metabase.util.memoize :as memoize]
+   [net.cgrand.macrovich :as macros]
+   [weavejester.dependency :as dep]
+   #?@(:clj  ([clojure.math.numeric-tower :as math]
+              [metabase.config :as config]
+              #_{:clj-kondo/ignore [:discouraged-namespace]}
+              [metabase.util.jvm :as u.jvm]
+              [metabase.util.string :as u.str]
+              [potemkin :as p]
+              [ring.util.codec :as codec])))
+  #?(:clj (:import
+           (java.text Normalizer Normalizer$Form)
+           (java.util Locale)
+           (org.apache.commons.validator.routines RegexValidator UrlValidator)))
+  #?(:cljs (:require-macros [camel-snake-kebab.internals.macros :as csk.macros]
+                            [metabase.util])))
+
(u.ns/import-fns
+  [u.format colorize format-bytes format-color format-milliseconds format-nanoseconds format-seconds])
+
#?(:clj (p/import-vars [u.jvm
+                        all-ex-data
+                        auto-retry
+                        decode-base64
+                        decode-base64-to-bytes
+                        deref-with-timeout
+                        encode-base64
+                        filtered-stacktrace
+                        full-exception-chain
+                        generate-nano-id
+                        host-port-up?
+                        host-up?
+                        ip-address?
+                        metabase-namespace-symbols
+                        sorted-take
+                        varargs
+                        with-timeout
+                        with-us-locale]
+                       [u.str
+                        build-sentence]))

Like or, but determines truthiness with pred.

+
(defmacro or-with
+  {:style/indent 1}
+  [pred & more]
+  (reduce (fn [inner value]
+            `(let [value# ~value]
+               (if (~pred value#)
+                 value#
+                 ~inner)))
+          nil
+          (reverse more)))

Simple macro which wraps the given expression in a try/catch block and ignores the exception if caught.

+
(defmacro ignore-exceptions
+  {:style/indent 0}
+  [& body]
+  `(try ~@body (catch ~(macros/case
+                         :cljs 'js/Error
+                         :clj  'Throwable)
+                      ~'_)))

Execute first-form, then any other expressions in body, presumably for side-effects; return the result of +first-form.

-;;; For convenience: [[metabase.lib.util/format]] maps to [[clojure.core/format]] in Clj and [[goog.string/format]] in -;;; Cljs. They both work like [[clojure.core/format]], but since that doesn't exist in Cljs, you can use this instead. -#?(:clj - (p/import-vars [clojure.core format]) +

(def numbers (atom []))

- :cljs - (def format "Exactly like [[clojure.core/format]] but ClojureScript-friendly." gstring/format))

Returns true if this is a clause.

-
(defn clause?
-  [clause]
-  (and (vector? clause)
-       (> (count clause) 1)
-       (keyword? (first clause))
-       (map? (second clause))
-       (contains? (second clause) :lib/uuid)))

Returns true if this is a clause.

-
(defn clause-of-type?
-  [clause clause-type]
-  (and (clause? clause)
-       (= (first clause) clause-type)))

Returns true if this is a field clause.

-
(defn field-clause?
-  [clause]
-  (clause-of-type? clause :field))

Returns true if this is any sort of reference clause

-
(defn ref-clause?
-  [clause]
-  (and (clause? clause)
-       (lib.hierarchy/isa? (first clause) ::lib.schema.ref/ref)))

Returns whether the type of expression isa? typ. - If the expression has an original-effective-type due to bucketing, check that.

-
(defn original-isa?
-  [expression typ]
-  (isa?
-    (or (and (clause? expression)
-             (:metabase.lib.field/original-effective-type (second expression)))
-        (lib.schema.expression/type-of expression))
-    typ))

Returns the :lib/expression-name of clause. Returns nil if clause is not a clause.

-
(defn expression-name
-  [clause]
-  (when (clause? clause)
-    (get-in clause [1 :lib/expression-name])))

Top level expressions must be clauses with :lib/expression-name, so if we get a literal, wrap it in :value.

-
(defn named-expression-clause
-  [clause a-name]
-  (assoc-in
-    (if (clause? clause)
-      clause
-      [:value {:lib/uuid (str (random-uuid))
-               :effective-type (lib.schema.expression/type-of clause)}
-       clause])
-    [1 :lib/expression-name] a-name))

Replace the target-clause in stage location with new-clause. - If a clause has :lib/uuid equal to the target-clause it is swapped with new-clause. - If location contains no clause with target-clause no replacement happens.

-
(defn replace-clause
-  [stage location target-clause new-clause]
-  {:pre [((some-fn clause? #(= (:lib/type %) :mbql/join)) target-clause)]}
-  (let [new-clause (if (= :expressions (first location))
-                     (named-expression-clause new-clause (expression-name target-clause))
-                     new-clause)]
-    (m/update-existing-in
-      stage
-      location
-      (fn [clause-or-clauses]
-        (->> (for [clause clause-or-clauses]
-               (if (= (lib.options/uuid clause) (lib.options/uuid target-clause))
-                 new-clause
-                 clause))
-             vec)))))

Remove the target-clause in stage location. - If a clause has :lib/uuid equal to the target-clause it is removed. - If location contains no clause with target-clause no removal happens. - If the the location is empty, dissoc it from stage. - For the [:fields] location if only expressions remain, dissoc from stage.

-
(defn remove-clause
-  [stage location target-clause]
-  {:pre [(clause? target-clause)]}
-  (if-let [target (get-in stage location)]
-    (let [target-uuid (lib.options/uuid target-clause)
-          [first-loc last-loc] [(first location) (last location)]
-          result (into [] (remove (comp #{target-uuid} lib.options/uuid)) target)
-          result (when-not (and (= location [:fields])
-                                (every? #(clause-of-type? % :expression) result))
-                   result)]
-      (cond
-        (seq result)
-        (assoc-in stage location result)
-        (= [:joins :conditions] [first-loc last-loc])
-        (throw (ex-info (i18n/tru "Cannot remove the final join condition")
-                        {:error ::cannot-remove-final-join-condition
-                         :conditions (get-in stage location)
-                         :join (get-in stage (pop location))
-                         :stage stage}))
-        (= [:joins :fields] [first-loc last-loc])
-        (update-in stage (pop location) dissoc last-loc)
-        :else
-        (m/dissoc-in stage location)))
-    stage))

TODO -- all of this ->pipeline stuff should probably be merged into [[metabase.lib.convert]] at some point in -the near future.

-

Convert a :type :native QP MBQL query to a pMBQL query. See docstring for [[mbql-query->pipeline]] for an -explanation of what this means.

-
(defn- native-query->pipeline
-  [query]
-  (merge {:lib/type :mbql/query
-          ;; we're using `merge` here instead of threading stuff so the `:lib/` keys are the first part of the map for
-          ;; readability in the REPL.
-          :stages   [(merge {:lib/type :mbql.stage/native}
-                            (set/rename-keys (:native query) {:query :native}))]}
-         (dissoc query :type :native)))
-
(declare inner-query->stages)

Updates m with a legacy boolean expression at legacy-key into a list with an implied and for pMBQL at pMBQL-key

-
(defn- update-legacy-boolean-expression->list
-  [m legacy-key pMBQL-key]
-  (cond-> m
-    (contains? m legacy-key) (update legacy-key #(if (and (vector? %)
-                                                       (= (first %) :and))
-                                                   (vec (drop 1 %))
-                                                   [%]))
-    (contains? m legacy-key) (set/rename-keys {legacy-key pMBQL-key})))
-
(defn- join->pipeline [join]
-  (let [source (select-keys join [:source-table :source-query])
-        stages (inner-query->stages source)]
-    (-> join
-        (dissoc :source-table :source-query)
-        (update-legacy-boolean-expression->list :condition :conditions)
-        (assoc :lib/type :mbql/join
-               :stages stages)
-        lib.options/ensure-uuid)))
-
(defn- joins->pipeline [joins]
-  (mapv join->pipeline joins))

Convert legacy :source-metadata to [[metabase.lib.metadata/StageMetadata]].

-
(defn ->stage-metadata
-  [source-metadata]
-  (when source-metadata
-    (-> (if (seqable? source-metadata)
-          {:columns source-metadata}
-          source-metadata)
-        (update :columns (fn [columns]
-                           (mapv (fn [column]
-                                   (-> column
-                                       (update-keys u/->kebab-case-en)
-                                       (assoc :lib/type :metadata/column)))
-                                 columns)))
-        (assoc :lib/type :metadata/results))))
-
(defn- inner-query->stages [{:keys [source-query source-metadata], :as inner-query}]
-  (let [previous-stages (if source-query
-                          (inner-query->stages source-query)
-                          [])
-        source-metadata (->stage-metadata source-metadata)
-        previous-stage  (dec (count previous-stages))
-        previous-stages (cond-> previous-stages
-                          (and source-metadata
-                               (not (neg? previous-stage))) (assoc-in [previous-stage :lib/stage-metadata] source-metadata))
-        stage-type      (if (:native inner-query)
-                          :mbql.stage/native
-                          :mbql.stage/mbql)
-        ;; we're using `merge` here instead of threading stuff so the `:lib/` keys are the first part of the map for
-        ;; readability in the REPL.
-        this-stage      (merge {:lib/type stage-type}
-                               (dissoc inner-query :source-query :source-metadata))
-        this-stage      (cond-> this-stage
-                          (seq (:joins this-stage)) (update :joins joins->pipeline)
-                          :always (update-legacy-boolean-expression->list :filter :filters))]
-    (conj previous-stages this-stage)))

Convert a :type :query QP MBQL (i.e., MBQL as currently understood by the Query Processor, or the JS MLv1) to a -pMBQL query. The key difference is that instead of having a :query with a :source-query with a :source-query -and so forth, you have a vector of :stages where each stage serves as the source query for the next stage. -Initially this was an implementation detail of a few functions, but it's easier to visualize and manipulate, so now -all of MLv2 deals with pMBQL. See this Slack thread -https://metaboat.slack.com/archives/C04DN5VRQM6/p1677118410961169?thread_ts=1677112778.742589&cid=C04DN5VRQM6 for -more information.

-
(defn- mbql-query->pipeline
-  [query]
-  (merge {:lib/type :mbql/query
-          :stages   (inner-query->stages (:query query))}
-         (dissoc query :type :query)))

Schema for a map that is either a legacy query OR a pMBQL query.

-
(def LegacyOrPMBQLQuery
-  [:or
-   [:map
-    {:error/message "legacy query"}
-    [:type [:enum :native :query]]]
-   [:map
-    {:error/message "pMBQL query"}
-    [:lib/type [:= :mbql/query]]]])

Ensure that a query is in the general shape of a pMBQL query. This doesn't walk the query and fix everything! The -goal here is just to make sure we have :stages in the correct place and the like. See [[metabase.lib.convert]] for -functions that actually ensure all parts of the query match the pMBQL schema (they use this function as part of that -process.)

-
(mu/defn pipeline
-  [query :- LegacyOrPMBQLQuery]
-  (if (= (:lib/type query) :mbql/query)
-    query
-    (case (:type query)
-      :native (native-query->pipeline query)
-      :query  (mbql-query->pipeline query))))
-
(mu/defn canonical-stage-index :- [:int {:min 0}]
-  "If `stage-number` index is a negative number e.g. `-1` convert it to a positive index so we can use `nth` on
-  `stages`. `-1` = the last stage, `-2` = the penultimate stage, etc."
-  [{:keys [stages], :as _query} :- :map
-   stage-number                 :- :int]
-  (let [stage-number' (if (neg? stage-number)
-                        (+ (count stages) stage-number)
-                        stage-number)]
-    (when (or (>= stage-number' (count stages))
-              (neg? stage-number'))
-      (throw (ex-info (i18n/tru "Stage {0} does not exist" stage-number)
-                      {:num-stages (count stages)})))
-    stage-number'))
-
(mu/defn previous-stage-number :- [:maybe [:int {:min 0}]]
-  "The index of the previous stage, if there is one. `nil` if there is no previous stage."
-  [query        :- :map
-   stage-number :- :int]
-  (let [stage-number (canonical-stage-index query stage-number)]
-    (when (pos? stage-number)
-      (dec stage-number))))

Whether a stage-number is referring to the first stage of a query or not.

-
(defn first-stage?
-  [query stage-number]
-  (not (previous-stage-number query stage-number)))

The index of the next stage, if there is one. nil if there is no next stage.

-
(defn next-stage-number
-  [{:keys [stages], :as _query} stage-number]
-  (let [stage-number (if (neg? stage-number)
-                       (+ (count stages) stage-number)
-                       stage-number)]
-    (when (< (inc stage-number) (count stages))
-      (inc stage-number))))
-
(mu/defn query-stage :- ::lib.schema/stage
-  "Fetch a specific `stage` of a query. This handles negative indices as well, e.g. `-1` will return the last stage of
-  the query."
-  [query        :- LegacyOrPMBQLQuery
-   stage-number :- :int]
-  (let [{:keys [stages], :as query} (pipeline query)]
-    (get (vec stages) (canonical-stage-index query stage-number))))
-
(mu/defn previous-stage :- [:maybe ::lib.schema/stage]
-  "Return the previous stage of the query, if there is one; otherwise return `nil`."
-  [query stage-number :- :int]
-  (when-let [stage-num (previous-stage-number query stage-number)]
-    (query-stage query stage-num)))
-
(mu/defn update-query-stage :- ::lib.schema/query
-  "Update a specific `stage-number` of a `query` by doing
-    (apply f stage args)
-  `stage-number` can be a negative index, e.g. `-1` will update the last stage of the query."
-  [query        :- LegacyOrPMBQLQuery
-   stage-number :- :int
-   f & args]
-  (let [{:keys [stages], :as query} (pipeline query)
-        stage-number'               (canonical-stage-index query stage-number)
-        stages'                     (apply update (vec stages) stage-number' f args)]
-    (assoc query :stages stages')))
-
(mu/defn ensure-mbql-final-stage :- ::lib.schema/query
-  "Convert query to a pMBQL (pipeline) query, and make sure the final stage is an `:mbql` one."
-  [query]
-  (let [query (pipeline query)]
-    (cond-> query
-      (= (:lib/type (query-stage query -1)) :mbql.stage/native)
-      (update :stages conj {:lib/type :mbql.stage/mbql}))))

This is basically [[clojure.string/join]] but uses commas to join everything but the last two args, which are joined -by a string conjunction. Uses Oxford commas for > 2 args.

- -

(join-strings-with-conjunction "and" ["X" "Y" "Z"]) -;; => "X, Y, and Z"

-
(defn join-strings-with-conjunction
-  [conjunction coll]
-  (when (seq coll)
-    (if (= (count coll) 1)
-      (first coll)
-      (let [conjunction (str \space (str/trim conjunction) \space)]
-        (if (= (count coll) 2)
-          ;; exactly 2 args: X and Y
-          (str (first coll) conjunction (second coll))
-          ;; > 2 args: X, Y, and Z
-          (str
-           (str/join ", " (butlast coll))
-           ","
-           conjunction
-           (last coll)))))))
-
(mu/defn ^:private string-byte-count :- [:int {:min 0}]
-  "Number of bytes in a string using UTF-8 encoding."
-  [s :- :string]
-  #?(:clj (count (.getBytes (str s) "UTF-8"))
-     :cljs (.. (js/TextEncoder.) (encode s) -length)))
-
#?(:clj
-   (mu/defn ^:private string-character-at :- [:string {:min 0, :max 1}]
-     [s :- :string
-      i :-[:int {:min 0}]]
-     (str (.charAt ^String s i))))
-
(mu/defn ^:private truncate-string-to-byte-count :- :string
-  "Truncate string `s` to `max-length-bytes` UTF-8 bytes (as opposed to truncating to some number of
-  *characters*)."
-  [s                :- :string
-   max-length-bytes :- [:int {:min 1}]]
-  #?(:clj
-     (loop [i 0, cumulative-byte-count 0]
-       (cond
-         (= cumulative-byte-count max-length-bytes) (subs s 0 i)
-         (> cumulative-byte-count max-length-bytes) (subs s 0 (dec i))
-         (>= i (count s))                           s
-         :else                                      (recur (inc i)
-                                                           (long (+
-                                                                  cumulative-byte-count
-                                                                  (string-byte-count (string-character-at s i)))))))
-     :cljs
-     (let [buf (js/Uint8Array. max-length-bytes)
-           result (.encodeInto (js/TextEncoder.) s buf)] ;; JS obj {read: chars_converted, write: bytes_written}
-       (subs s 0 (.-read result)))))

Length to truncate column and table identifiers to. See [[metabase.driver.impl/default-alias-max-length-bytes]] for -reasoning.

-
(def ^:private truncate-alias-max-length-bytes
-  60)

Length of the hash suffixed to truncated strings by [[truncate-alias]].

-
(def ^:private truncated-alias-hash-suffix-length
-  ;; 8 bytes for the CRC32 plus one for the underscore
-  9)
-
(mu/defn ^:private crc32-checksum :- [:string {:min 8, :max 8}]
-  "Return a 4-byte CRC-32 checksum of string `s`, encoded as an 8-character hex string."
-  [s :- :string]
-  (let [s #?(:clj (Long/toHexString (.getValue (doto (java.util.zip.CRC32.)
-                                                 (.update (.getBytes ^String s "UTF-8")))))
-             :cljs (-> (CRC32/str s 0)
-                       (unsigned-bit-shift-right 0) ; see https://github.com/SheetJS/js-crc32#signed-integers
-                       (.toString 16)))]
-    ;; pad to 8 characters if needed. Might come out as less than 8 if the first byte is `00` or `0x` or something.
-    (loop [s s]
-      (if (< (count s) 8)
-        (recur (str \0 s))
-        s))))
-
(mu/defn truncate-alias :- [:string {:min 1, :max 60}]
-  "Truncate string `s` if it is longer than [[truncate-alias-max-length-bytes]] and append a hex-encoded CRC-32
-  checksum of the original string. Truncated string is truncated to [[truncate-alias-max-length-bytes]]
-  minus [[truncated-alias-hash-suffix-length]] characters so the resulting string is
-  exactly [[truncate-alias-max-length-bytes]]. The goal here is that two really long strings that only differ at the
-  end will still have different resulting values.
-    (truncate-alias \"some_really_long_string\" 15) ;   -> \"some_r_8e0f9bc2\"
-    (truncate-alias \"some_really_long_string_2\" 15) ; -> \"some_r_2a3c73eb\
-  ([s]
-   (truncate-alias s truncate-alias-max-length-bytes))
-  ([s         :- ::lib.schema.common/non-blank-string
-    max-bytes :- [:int {:min 0}]]
-   (if (<= (string-byte-count s) max-bytes)
-     s
-     (let [checksum  (crc32-checksum s)
-           truncated (truncate-string-to-byte-count s (- max-bytes truncated-alias-hash-suffix-length))]
-       (str truncated \_ checksum)))))
-
(mu/defn legacy-string-table-id->card-id :- [:maybe ::lib.schema.id/card]
-  "If `table-id` is a legacy `card__<id>`-style string, parse the `<id>` part to an integer Card ID. Only for legacy
-  queries! You don't need to use this in pMBQL since this is converted automatically by [[metabase.lib.convert]] to
-  `:source-card`."
-  [table-id]
-  (when (string? table-id)
-    (when-let [[_match card-id-str] (re-find #"^card__(\d+)$" table-id)]
-      (parse-long card-id-str))))
-
(mu/defn source-table-id :- [:maybe ::lib.schema.id/table]
-  "If this query has a `:source-table` ID, return it."
-  [query]
-  (-> query :stages first :source-table))
-
(mu/defn source-card-id :- [:maybe ::lib.schema.id/card]
-  "If this query has a `:source-card` ID, return it."
-  [query]
-  (-> query :stages first :source-card))
-
(mu/defn unique-name-generator :- [:=>
-                                   [:cat ::lib.schema.common/non-blank-string]
-                                   ::lib.schema.common/non-blank-string]
-  "Create a new function with the signature
-    (f str) => str
-  That takes any sort of string identifier (e.g. a column alias or table/join alias) and returns a guaranteed-unique
-  name truncated to 60 characters (actually 51 characters plus a hash)."
-  []
-  (comp truncate-alias
-        (mbql.u/unique-name-generator
-         ;; unique by lower-case name, e.g. `NAME` and `name` => `NAME` and `name_2`
-         :name-key-fn     u/lower-case-en
-         ;; truncate alias to 60 characters (actually 51 characters plus a hash).
-         :unique-alias-fn (fn [original suffix]
-                            (truncate-alias (str original \_ suffix))))))
-
(def ^:private strip-id-regex
-  #?(:cljs (js/RegExp. " id$" "i")
-     ;; `(?i)` is JVM-specific magic to turn on the `i` case-insensitive flag.
-     :clj  #"(?i) id$"))
-
(mu/defn strip-id :- :string
-  "Given a display name string like \"Product ID\", this will drop the trailing \"ID\" and trim whitespace.
-  Used to turn a FK field's name into a pseudo table name when implicitly joining."
-  [display-name :- :string]
-  (-> display-name
-      (str/replace strip-id-regex )
-      str/trim))
-
(mu/defn add-summary-clause :- ::lib.schema/query
-  "If the given stage has no summary, it will drop :fields, :order-by, and :join :fields from it,
-   as well as any subsequent stages."
-  [query :- ::lib.schema/query
-   stage-number :- :int
-   location :- [:enum :breakout :aggregation]
-   a-summary-clause]
-  (let [query (pipeline query)
-        stage-number (or stage-number -1)
-        stage (query-stage query stage-number)
-        new-summary? (not (or (seq (:aggregation stage)) (seq (:breakout stage))))
-        new-query (update-query-stage
-                    query stage-number
-                    update location
-                    (fn [summary-clauses]
-                      (conj (vec summary-clauses) (lib.common/->op-arg a-summary-clause))))]
-    (if new-summary?
-      (-> new-query
-          (update-query-stage
-            stage-number
-            (fn [stage]
-              (-> stage
-                  (dissoc :order-by :fields)
-                  (m/update-existing :joins (fn [joins] (mapv #(dissoc % :fields) joins))))))
-          ;; subvec holds onto references, so create a new vector
-          (update :stages (comp #(into [] %) subvec) 0 (inc (canonical-stage-index query stage-number))))
-      new-query)))
 

Common utility functions useful throughout the codebase.

-
(ns metabase.util
-  (:require
-   [camel-snake-kebab.internals.macros :as csk.macros]
-   [clojure.data :refer [diff]]
-   [clojure.pprint :as pprint]
-   [clojure.set :as set]
-   [clojure.string :as str]
-   [clojure.walk :as walk]
-   [flatland.ordered.map :refer [ordered-map]]
-   [medley.core :as m]
-   [metabase.shared.util.i18n :refer [tru] :as i18n]
-   [metabase.shared.util.namespaces :as u.ns]
-   [metabase.util.format :as u.format]
-   [metabase.util.log :as log]
-   [metabase.util.memoize :as memoize]
-   [net.cgrand.macrovich :as macros]
-   [weavejester.dependency :as dep]
-   #?@(:clj  ([clojure.math.numeric-tower :as math]
-              [metabase.config :as config]
-              #_{:clj-kondo/ignore [:discouraged-namespace]}
-              [metabase.util.jvm :as u.jvm]
-              [metabase.util.string :as u.str]
-              [potemkin :as p]
-              [ring.util.codec :as codec])))
-  #?(:clj (:import
-           (java.text Normalizer Normalizer$Form)
-           (java.util Locale)
-           (org.apache.commons.validator.routines RegexValidator UrlValidator)))
-  #?(:cljs (:require-macros [camel-snake-kebab.internals.macros :as csk.macros]
-                            [metabase.util])))
-
(u.ns/import-fns
-  [u.format colorize format-bytes format-color format-milliseconds format-nanoseconds format-seconds])
-
#?(:clj (p/import-vars [u.jvm
-                        all-ex-data
-                        auto-retry
-                        decode-base64
-                        decode-base64-to-bytes
-                        deref-with-timeout
-                        encode-base64
-                        filtered-stacktrace
-                        full-exception-chain
-                        generate-nano-id
-                        host-port-up?
-                        host-up?
-                        ip-address?
-                        metabase-namespace-symbols
-                        sorted-take
-                        varargs
-                        with-timeout
-                        with-us-locale]
-                       [u.str
-                        build-sentence]))

Like or, but determines truthiness with pred.

-
(defmacro or-with
-  {:style/indent 1}
-  [pred & more]
-  (reduce (fn [inner value]
-            `(let [value# ~value]
-               (if (~pred value#)
-                 value#
-                 ~inner)))
-          nil
-          (reverse more)))

Simple macro which wraps the given expression in a try/catch block and ignores the exception if caught.

-
(defmacro ignore-exceptions
-  {:style/indent 0}
-  [& body]
-  `(try ~@body (catch ~(macros/case
-                         :cljs 'js/Error
-                         :clj  'Throwable)
-                      ~'_)))

Execute first-form, then any other expressions in body, presumably for side-effects; return the result of -first-form.

- -

(def numbers (atom []))

- -

(defn find-or-add [n] - (or (first-index-satisfying (partial = n) @numbers) - (prog1 (count @numbers) - (swap! numbers conj n))))

+

(defn find-or-add [n] + (or (first-index-satisfying (partial = n) @numbers) + (prog1 (count @numbers) + (swap! numbers conj n))))

(find-or-add 100) -> 0 (find-or-add 200) -> 1 @@ -94113,7 +93674,472 @@

Quartz JavaDoc

(defn empty-or-distinct?
   [xs]
   (or (empty? xs)
-      (apply distinct? xs)))
 
+ (apply distinct? xs)))
 
+
(ns metabase.lib.util
+  (:refer-clojure :exclude [format])
+  (:require
+   #?@(:clj
+       ([potemkin :as p]))
+   #?@(:cljs
+       (["crc-32" :as CRC32]
+        [goog.string :as gstring]
+        [goog.string.format :as gstring.format]))
+   [clojure.set :as set]
+   [clojure.string :as str]
+   [medley.core :as m]
+   [metabase.lib.common :as lib.common]
+   [metabase.lib.hierarchy :as lib.hierarchy]
+   [metabase.lib.options :as lib.options]
+   [metabase.lib.schema :as lib.schema]
+   [metabase.lib.schema.common :as lib.schema.common]
+   [metabase.lib.schema.expression :as lib.schema.expression]
+   [metabase.lib.schema.id :as lib.schema.id]
+   [metabase.lib.schema.ref :as lib.schema.ref]
+   [metabase.mbql.util :as mbql.u]
+   [metabase.shared.util.i18n :as i18n]
+   [metabase.util :as u]
+   [metabase.util.malli :as mu]))
+
#?(:clj
+   (set! *warn-on-reflection* true))

The formatting functionality is only loaded if you depend on goog.string.format.

+
#?(:cljs (comment gstring.format/keep-me))
+
+;;; For convenience: [[metabase.lib.util/format]] maps to [[clojure.core/format]] in Clj and [[goog.string/format]] in
+;;; Cljs. They both work like [[clojure.core/format]], but since that doesn't exist in Cljs, you can use this instead.
+#?(:clj
+   (p/import-vars [clojure.core format])
+
+   :cljs
+   (def format "Exactly like [[clojure.core/format]] but ClojureScript-friendly." gstring/format))

Returns true if this is a clause.

+
(defn clause?
+  [clause]
+  (and (vector? clause)
+       (> (count clause) 1)
+       (keyword? (first clause))
+       (map? (second clause))
+       (contains? (second clause) :lib/uuid)))

Returns true if this is a clause.

+
(defn clause-of-type?
+  [clause clause-type]
+  (and (clause? clause)
+       (= (first clause) clause-type)))

Returns true if this is a field clause.

+
(defn field-clause?
+  [clause]
+  (clause-of-type? clause :field))

Returns true if this is any sort of reference clause

+
(defn ref-clause?
+  [clause]
+  (and (clause? clause)
+       (lib.hierarchy/isa? (first clause) ::lib.schema.ref/ref)))

Returns whether the type of expression isa? typ. + If the expression has an original-effective-type due to bucketing, check that.

+
(defn original-isa?
+  [expression typ]
+  (isa?
+    (or (and (clause? expression)
+             (:metabase.lib.field/original-effective-type (second expression)))
+        (lib.schema.expression/type-of expression))
+    typ))

Returns the :lib/expression-name of clause. Returns nil if clause is not a clause.

+
(defn expression-name
+  [clause]
+  (when (clause? clause)
+    (get-in clause [1 :lib/expression-name])))

Top level expressions must be clauses with :lib/expression-name, so if we get a literal, wrap it in :value.

+
(defn named-expression-clause
+  [clause a-name]
+  (assoc-in
+    (if (clause? clause)
+      clause
+      [:value {:lib/uuid (str (random-uuid))
+               :effective-type (lib.schema.expression/type-of clause)}
+       clause])
+    [1 :lib/expression-name] a-name))

Replace the target-clause in stage location with new-clause. + If a clause has :lib/uuid equal to the target-clause it is swapped with new-clause. + If location contains no clause with target-clause no replacement happens.

+
(defn replace-clause
+  [stage location target-clause new-clause]
+  {:pre [((some-fn clause? #(= (:lib/type %) :mbql/join)) target-clause)]}
+  (let [new-clause (if (= :expressions (first location))
+                     (named-expression-clause new-clause (expression-name target-clause))
+                     new-clause)]
+    (m/update-existing-in
+      stage
+      location
+      (fn [clause-or-clauses]
+        (->> (for [clause clause-or-clauses]
+               (if (= (lib.options/uuid clause) (lib.options/uuid target-clause))
+                 new-clause
+                 clause))
+             vec)))))

Remove the target-clause in stage location. + If a clause has :lib/uuid equal to the target-clause it is removed. + If location contains no clause with target-clause no removal happens. + If the the location is empty, dissoc it from stage. + For the [:fields] location if only expressions remain, dissoc from stage.

+
(defn remove-clause
+  [stage location target-clause]
+  {:pre [(clause? target-clause)]}
+  (if-let [target (get-in stage location)]
+    (let [target-uuid (lib.options/uuid target-clause)
+          [first-loc last-loc] [(first location) (last location)]
+          result (into [] (remove (comp #{target-uuid} lib.options/uuid)) target)
+          result (when-not (and (= location [:fields])
+                                (every? #(clause-of-type? % :expression) result))
+                   result)]
+      (cond
+        (seq result)
+        (assoc-in stage location result)
+        (= [:joins :conditions] [first-loc last-loc])
+        (throw (ex-info (i18n/tru "Cannot remove the final join condition")
+                        {:error ::cannot-remove-final-join-condition
+                         :conditions (get-in stage location)
+                         :join (get-in stage (pop location))
+                         :stage stage}))
+        (= [:joins :fields] [first-loc last-loc])
+        (update-in stage (pop location) dissoc last-loc)
+        :else
+        (m/dissoc-in stage location)))
+    stage))

TODO -- all of this ->pipeline stuff should probably be merged into [[metabase.lib.convert]] at some point in +the near future.

+

Convert a :type :native QP MBQL query to a pMBQL query. See docstring for [[mbql-query->pipeline]] for an +explanation of what this means.

+
(defn- native-query->pipeline
+  [query]
+  (merge {:lib/type :mbql/query
+          ;; we're using `merge` here instead of threading stuff so the `:lib/` keys are the first part of the map for
+          ;; readability in the REPL.
+          :stages   [(merge {:lib/type :mbql.stage/native}
+                            (set/rename-keys (:native query) {:query :native}))]}
+         (dissoc query :type :native)))
+
(declare inner-query->stages)

Updates m with a legacy boolean expression at legacy-key into a list with an implied and for pMBQL at pMBQL-key

+
(defn- update-legacy-boolean-expression->list
+  [m legacy-key pMBQL-key]
+  (cond-> m
+    (contains? m legacy-key) (update legacy-key #(if (and (vector? %)
+                                                       (= (first %) :and))
+                                                   (vec (drop 1 %))
+                                                   [%]))
+    (contains? m legacy-key) (set/rename-keys {legacy-key pMBQL-key})))
+
(defn- join->pipeline [join]
+  (let [source (select-keys join [:source-table :source-query])
+        stages (inner-query->stages source)]
+    (-> join
+        (dissoc :source-table :source-query)
+        (update-legacy-boolean-expression->list :condition :conditions)
+        (assoc :lib/type :mbql/join
+               :stages stages)
+        lib.options/ensure-uuid)))
+
(defn- joins->pipeline [joins]
+  (mapv join->pipeline joins))

Convert legacy :source-metadata to [[metabase.lib.metadata/StageMetadata]].

+
(defn ->stage-metadata
+  [source-metadata]
+  (when source-metadata
+    (-> (if (seqable? source-metadata)
+          {:columns source-metadata}
+          source-metadata)
+        (update :columns (fn [columns]
+                           (mapv (fn [column]
+                                   (-> column
+                                       (update-keys u/->kebab-case-en)
+                                       (assoc :lib/type :metadata/column)))
+                                 columns)))
+        (assoc :lib/type :metadata/results))))
+
(defn- inner-query->stages [{:keys [source-query source-metadata], :as inner-query}]
+  (let [previous-stages (if source-query
+                          (inner-query->stages source-query)
+                          [])
+        source-metadata (->stage-metadata source-metadata)
+        previous-stage  (dec (count previous-stages))
+        previous-stages (cond-> previous-stages
+                          (and source-metadata
+                               (not (neg? previous-stage))) (assoc-in [previous-stage :lib/stage-metadata] source-metadata))
+        stage-type      (if (:native inner-query)
+                          :mbql.stage/native
+                          :mbql.stage/mbql)
+        ;; we're using `merge` here instead of threading stuff so the `:lib/` keys are the first part of the map for
+        ;; readability in the REPL.
+        this-stage      (merge {:lib/type stage-type}
+                               (dissoc inner-query :source-query :source-metadata))
+        this-stage      (cond-> this-stage
+                          (seq (:joins this-stage)) (update :joins joins->pipeline)
+                          :always (update-legacy-boolean-expression->list :filter :filters))]
+    (conj previous-stages this-stage)))

Convert a :type :query QP MBQL (i.e., MBQL as currently understood by the Query Processor, or the JS MLv1) to a +pMBQL query. The key difference is that instead of having a :query with a :source-query with a :source-query +and so forth, you have a vector of :stages where each stage serves as the source query for the next stage. +Initially this was an implementation detail of a few functions, but it's easier to visualize and manipulate, so now +all of MLv2 deals with pMBQL. See this Slack thread +https://metaboat.slack.com/archives/C04DN5VRQM6/p1677118410961169?thread_ts=1677112778.742589&cid=C04DN5VRQM6 for +more information.

+
(defn- mbql-query->pipeline
+  [query]
+  (merge {:lib/type :mbql/query
+          :stages   (inner-query->stages (:query query))}
+         (dissoc query :type :query)))

Schema for a map that is either a legacy query OR a pMBQL query.

+
(def LegacyOrPMBQLQuery
+  [:or
+   [:map
+    {:error/message "legacy query"}
+    [:type [:enum :native :query]]]
+   [:map
+    {:error/message "pMBQL query"}
+    [:lib/type [:= :mbql/query]]]])

Ensure that a query is in the general shape of a pMBQL query. This doesn't walk the query and fix everything! The +goal here is just to make sure we have :stages in the correct place and the like. See [[metabase.lib.convert]] for +functions that actually ensure all parts of the query match the pMBQL schema (they use this function as part of that +process.)

+
(mu/defn pipeline
+  [query :- LegacyOrPMBQLQuery]
+  (if (= (:lib/type query) :mbql/query)
+    query
+    (case (:type query)
+      :native (native-query->pipeline query)
+      :query  (mbql-query->pipeline query))))
+
(mu/defn canonical-stage-index :- [:int {:min 0}]
+  "If `stage-number` index is a negative number e.g. `-1` convert it to a positive index so we can use `nth` on
+  `stages`. `-1` = the last stage, `-2` = the penultimate stage, etc."
+  [{:keys [stages], :as _query} :- :map
+   stage-number                 :- :int]
+  (let [stage-number' (if (neg? stage-number)
+                        (+ (count stages) stage-number)
+                        stage-number)]
+    (when (or (>= stage-number' (count stages))
+              (neg? stage-number'))
+      (throw (ex-info (i18n/tru "Stage {0} does not exist" stage-number)
+                      {:num-stages (count stages)})))
+    stage-number'))
+
(mu/defn previous-stage-number :- [:maybe [:int {:min 0}]]
+  "The index of the previous stage, if there is one. `nil` if there is no previous stage."
+  [query        :- :map
+   stage-number :- :int]
+  (let [stage-number (canonical-stage-index query stage-number)]
+    (when (pos? stage-number)
+      (dec stage-number))))

Whether a stage-number is referring to the first stage of a query or not.

+
(defn first-stage?
+  [query stage-number]
+  (not (previous-stage-number query stage-number)))

The index of the next stage, if there is one. nil if there is no next stage.

+
(defn next-stage-number
+  [{:keys [stages], :as _query} stage-number]
+  (let [stage-number (if (neg? stage-number)
+                       (+ (count stages) stage-number)
+                       stage-number)]
+    (when (< (inc stage-number) (count stages))
+      (inc stage-number))))
+
(mu/defn query-stage :- ::lib.schema/stage
+  "Fetch a specific `stage` of a query. This handles negative indices as well, e.g. `-1` will return the last stage of
+  the query."
+  [query        :- LegacyOrPMBQLQuery
+   stage-number :- :int]
+  (let [{:keys [stages], :as query} (pipeline query)]
+    (get (vec stages) (canonical-stage-index query stage-number))))
+
(mu/defn previous-stage :- [:maybe ::lib.schema/stage]
+  "Return the previous stage of the query, if there is one; otherwise return `nil`."
+  [query stage-number :- :int]
+  (when-let [stage-num (previous-stage-number query stage-number)]
+    (query-stage query stage-num)))
+
(mu/defn update-query-stage :- ::lib.schema/query
+  "Update a specific `stage-number` of a `query` by doing
+    (apply f stage args)
+  `stage-number` can be a negative index, e.g. `-1` will update the last stage of the query."
+  [query        :- LegacyOrPMBQLQuery
+   stage-number :- :int
+   f & args]
+  (let [{:keys [stages], :as query} (pipeline query)
+        stage-number'               (canonical-stage-index query stage-number)
+        stages'                     (apply update (vec stages) stage-number' f args)]
+    (assoc query :stages stages')))
+
(mu/defn ensure-mbql-final-stage :- ::lib.schema/query
+  "Convert query to a pMBQL (pipeline) query, and make sure the final stage is an `:mbql` one."
+  [query]
+  (let [query (pipeline query)]
+    (cond-> query
+      (= (:lib/type (query-stage query -1)) :mbql.stage/native)
+      (update :stages conj {:lib/type :mbql.stage/mbql}))))

This is basically [[clojure.string/join]] but uses commas to join everything but the last two args, which are joined +by a string conjunction. Uses Oxford commas for > 2 args.

+ +

(join-strings-with-conjunction "and" ["X" "Y" "Z"]) +;; => "X, Y, and Z"

+
(defn join-strings-with-conjunction
+  [conjunction coll]
+  (when (seq coll)
+    (if (= (count coll) 1)
+      (first coll)
+      (let [conjunction (str \space (str/trim conjunction) \space)]
+        (if (= (count coll) 2)
+          ;; exactly 2 args: X and Y
+          (str (first coll) conjunction (second coll))
+          ;; > 2 args: X, Y, and Z
+          (str
+           (str/join ", " (butlast coll))
+           ","
+           conjunction
+           (last coll)))))))
+
(mu/defn ^:private string-byte-count :- [:int {:min 0}]
+  "Number of bytes in a string using UTF-8 encoding."
+  [s :- :string]
+  #?(:clj (count (.getBytes (str s) "UTF-8"))
+     :cljs (.. (js/TextEncoder.) (encode s) -length)))
+
#?(:clj
+   (mu/defn ^:private string-character-at :- [:string {:min 0, :max 1}]
+     [s :- :string
+      i :-[:int {:min 0}]]
+     (str (.charAt ^String s i))))
+
(mu/defn ^:private truncate-string-to-byte-count :- :string
+  "Truncate string `s` to `max-length-bytes` UTF-8 bytes (as opposed to truncating to some number of
+  *characters*)."
+  [s                :- :string
+   max-length-bytes :- [:int {:min 1}]]
+  #?(:clj
+     (loop [i 0, cumulative-byte-count 0]
+       (cond
+         (= cumulative-byte-count max-length-bytes) (subs s 0 i)
+         (> cumulative-byte-count max-length-bytes) (subs s 0 (dec i))
+         (>= i (count s))                           s
+         :else                                      (recur (inc i)
+                                                           (long (+
+                                                                  cumulative-byte-count
+                                                                  (string-byte-count (string-character-at s i)))))))
+     :cljs
+     (let [buf (js/Uint8Array. max-length-bytes)
+           result (.encodeInto (js/TextEncoder.) s buf)] ;; JS obj {read: chars_converted, write: bytes_written}
+       (subs s 0 (.-read result)))))

Length to truncate column and table identifiers to. See [[metabase.driver.impl/default-alias-max-length-bytes]] for +reasoning.

+
(def ^:private truncate-alias-max-length-bytes
+  60)

Length of the hash suffixed to truncated strings by [[truncate-alias]].

+
(def ^:private truncated-alias-hash-suffix-length
+  ;; 8 bytes for the CRC32 plus one for the underscore
+  9)
+
(mu/defn ^:private crc32-checksum :- [:string {:min 8, :max 8}]
+  "Return a 4-byte CRC-32 checksum of string `s`, encoded as an 8-character hex string."
+  [s :- :string]
+  (let [s #?(:clj (Long/toHexString (.getValue (doto (java.util.zip.CRC32.)
+                                                 (.update (.getBytes ^String s "UTF-8")))))
+             :cljs (-> (CRC32/str s 0)
+                       (unsigned-bit-shift-right 0) ; see https://github.com/SheetJS/js-crc32#signed-integers
+                       (.toString 16)))]
+    ;; pad to 8 characters if needed. Might come out as less than 8 if the first byte is `00` or `0x` or something.
+    (loop [s s]
+      (if (< (count s) 8)
+        (recur (str \0 s))
+        s))))
+
(mu/defn truncate-alias :- [:string {:min 1, :max 60}]
+  "Truncate string `s` if it is longer than [[truncate-alias-max-length-bytes]] and append a hex-encoded CRC-32
+  checksum of the original string. Truncated string is truncated to [[truncate-alias-max-length-bytes]]
+  minus [[truncated-alias-hash-suffix-length]] characters so the resulting string is
+  exactly [[truncate-alias-max-length-bytes]]. The goal here is that two really long strings that only differ at the
+  end will still have different resulting values.
+    (truncate-alias \"some_really_long_string\" 15) ;   -> \"some_r_8e0f9bc2\"
+    (truncate-alias \"some_really_long_string_2\" 15) ; -> \"some_r_2a3c73eb\
+  ([s]
+   (truncate-alias s truncate-alias-max-length-bytes))
+  ([s         :- ::lib.schema.common/non-blank-string
+    max-bytes :- [:int {:min 0}]]
+   (if (<= (string-byte-count s) max-bytes)
+     s
+     (let [checksum  (crc32-checksum s)
+           truncated (truncate-string-to-byte-count s (- max-bytes truncated-alias-hash-suffix-length))]
+       (str truncated \_ checksum)))))
+
(mu/defn legacy-string-table-id->card-id :- [:maybe ::lib.schema.id/card]
+  "If `table-id` is a legacy `card__<id>`-style string, parse the `<id>` part to an integer Card ID. Only for legacy
+  queries! You don't need to use this in pMBQL since this is converted automatically by [[metabase.lib.convert]] to
+  `:source-card`."
+  [table-id]
+  (when (string? table-id)
+    (when-let [[_match card-id-str] (re-find #"^card__(\d+)$" table-id)]
+      (parse-long card-id-str))))
+
(mu/defn source-table-id :- [:maybe ::lib.schema.id/table]
+  "If this query has a `:source-table` ID, return it."
+  [query]
+  (-> query :stages first :source-table))
+
(mu/defn source-card-id :- [:maybe ::lib.schema.id/card]
+  "If this query has a `:source-card` ID, return it."
+  [query]
+  (-> query :stages first :source-card))
+
(mu/defn unique-name-generator :- [:=>
+                                   [:cat ::lib.schema.common/non-blank-string]
+                                   ::lib.schema.common/non-blank-string]
+  "Create a new function with the signature
+    (f str) => str
+  That takes any sort of string identifier (e.g. a column alias or table/join alias) and returns a guaranteed-unique
+  name truncated to 60 characters (actually 51 characters plus a hash)."
+  []
+  (comp truncate-alias
+        (mbql.u/unique-name-generator
+         ;; unique by lower-case name, e.g. `NAME` and `name` => `NAME` and `name_2`
+         :name-key-fn     u/lower-case-en
+         ;; truncate alias to 60 characters (actually 51 characters plus a hash).
+         :unique-alias-fn (fn [original suffix]
+                            (truncate-alias (str original \_ suffix))))))
+
(def ^:private strip-id-regex
+  #?(:cljs (js/RegExp. " id$" "i")
+     ;; `(?i)` is JVM-specific magic to turn on the `i` case-insensitive flag.
+     :clj  #"(?i) id$"))
+
(mu/defn strip-id :- :string
+  "Given a display name string like \"Product ID\", this will drop the trailing \"ID\" and trim whitespace.
+  Used to turn a FK field's name into a pseudo table name when implicitly joining."
+  [display-name :- :string]
+  (-> display-name
+      (str/replace strip-id-regex )
+      str/trim))
+
(mu/defn add-summary-clause :- ::lib.schema/query
+  "If the given stage has no summary, it will drop :fields, :order-by, and :join :fields from it,
+   as well as any subsequent stages."
+  [query :- ::lib.schema/query
+   stage-number :- :int
+   location :- [:enum :breakout :aggregation]
+   a-summary-clause]
+  (let [query (pipeline query)
+        stage-number (or stage-number -1)
+        stage (query-stage query stage-number)
+        new-summary? (not (or (seq (:aggregation stage)) (seq (:breakout stage))))
+        new-query (update-query-stage
+                    query stage-number
+                    update location
+                    (fn [summary-clauses]
+                      (conj (vec summary-clauses) (lib.common/->op-arg a-summary-clause))))]
+    (if new-summary?
+      (-> new-query
+          (update-query-stage
+            stage-number
+            (fn [stage]
+              (-> stage
+                  (dissoc :order-by :fields)
+                  (m/update-existing :joins (fn [joins] (mapv #(dissoc % :fields) joins))))))
+          ;; subvec holds onto references, so create a new vector
+          (update :stages (comp #(into [] %) subvec) 0 (inc (canonical-stage-index query stage-number))))
+      new-query)))
 

Utility functions used by the Queries in metabase-lib.

+
(ns metabase.domain-entities.queries.util
+  (:require
+   [metabase.util.malli :as mu]
+   #?@(:cljs ([metabase.domain-entities.converters :as converters]))))

Schema for an Expression that's part of a query filter.

+
(def Expression
+  :any)

Malli schema for a map of expressions by name.

+
(def ExpressionMap
+  [:map-of string? Expression])

Malli schema for a list of {:name :expression} maps.

+
(def ExpressionList
+  [:vector [:map [:name string?] [:expression Expression]]])
+
(def ^:private ->expression-map
+  #?(:cljs (converters/incoming ExpressionMap)
+     :clj  identity))
+
(def ^:private expression-list->
+  #?(:cljs (converters/outgoing ExpressionList)
+     :clj  identity))
+
(mu/defn ^:export expressions-list :- ExpressionList
+  "Turns a map of expressions by name into a list of `{:name name :expression expression}` objects."
+  [expressions :- ExpressionMap]
+  (->> expressions
+       ->expression-map
+       (mapv (fn [[name expr]] {:name name :expression expr}))
+       expression-list->))
+
(defn- unique-name [names original-name index]
+  (let [indexed-name (str original-name " (" index ")")]
+    (if (names indexed-name)
+      (recur names original-name (inc index))
+      indexed-name)))
+
(mu/defn ^:export unique-expression-name :- string?
+  "Generates an expression name that's unique in the given map of expressions."
+  [expressions   :- ExpressionMap
+   original-name :- string?]
+  (let [expression-names (-> expressions ->expression-map keys set)]
+    (if (not (expression-names original-name))
+      original-name
+      (let [re-duplicates (re-pattern (str "^" original-name " \\([0-9]+\\)$"))
+            duplicates    (set (filter #(or (= % original-name)
+                                            (re-matches re-duplicates %))
+                                       expression-names))]
+        (unique-name duplicates original-name (count duplicates))))))
 
(ns metabase.shared.formatting.constants
   #?(:cljs (:require
             [metabase.shared.formatting.internal.date-builder :as builder])))

Months and weekdays should be abbreviated for compact output.

@@ -96613,10 +96639,6 @@

`:allowed-for`

(defn distinct
   [schema]
   [:and schema [:ref ::distinct]])
 
-
(ns metabase.mbql.schema.macros
-  (:require-macros
-   [metabase.mbql.schema.macros]))
-
(comment metabase.mbql.schema.macros/keep-me)
 
(ns metabase.mbql.schema.macros
   (:require
    [metabase.mbql.schema.helpers :as metabase.mbql.schema.helpers]
@@ -96669,7 +96691,11 @@ 

`:allowed-for`

~@(for [clause clauses] [`(or (:clause-name (meta (resolve '~clause))) '~clause) - clause])))
 

Internal implementation of the MBQL match and replace macros. Don't use these directly.

+ clause])))
 
+
(ns metabase.mbql.schema.macros
+  (:require-macros
+   [metabase.mbql.schema.macros]))
+
(comment metabase.mbql.schema.macros/keep-me)
 

Internal implementation of the MBQL match and replace macros. Don't use these directly.

(ns metabase.mbql.util.match
   (:refer-clojure :exclude [replace])
   (:require
@@ -97228,106 +97254,7 @@ 

`&match` and `&parents` anaphors

(time-only? unit) (assoc :date-enabled false) (= type "tooltip") (assoc :output-density "condensed") (or compact date-abbreviate) (assoc :output-density "compact") - (not (units-with-day unit)) (dissoc :weekday-enabled))))
 

ClojureScript implementation of number formatting. -Implements the [[NumberFormatter]] protocol from numbers_core, plus some helpers.

-
(ns metabase.shared.formatting.internal.numbers
-  (:require
-   [clojure.string :as str]
-   [metabase.shared.formatting.internal.numbers-core :as core]
-   [metabase.shared.util.currency :as currency]
-   [metabase.util :as u]))
-
(def ^:private default-number-separators ".,")
-
(defn- adjust-number-separators [text separators]
-  (if (and separators
-           (not= separators default-number-separators))
-    (let [decimal    (first separators)
-          grouping   (or (second separators) ) ; grouping separators are optional
-          transform  {"," grouping "." decimal}]
-      (str/replace text #"[\.,]" transform))
-    text))
-
(defn- fix-currency-symbols [text currency]
-  (let [sym (currency/currency-symbol currency)]
-    (-> text
-        ;; Some have spaces and some don't - remove the space if it's there.
-        (str/replace (str (name currency) core/non-breaking-space) sym)
-        (str/replace (name currency) sym))))
-
(defn- base-format-scientific [nf number]
-  (letfn [(transform [{:keys [type value]}]
-            (case type
-              "exponentSeparator" "e"
-              value))]
-    (let [parts  (js->clj (.formatToParts nf number) {:keywordize-keys true})
-          ;; If there's no exponent minus sign, add a plus sign.
-          parts  (if (some #(= (:type %) "exponentMinusSign") parts)
-                   parts
-                   (let [[pre post] (split-with #(not= (:type %) "exponentInteger") parts)]
-                     (concat pre [{:type "exponentPlusSign" :value "+"}] post)))]
-      (apply str (map transform parts)))))

Core internals ================================================================================================= -TODO(braden) We could get more nicely localized currency values by using the user's locale. -The problem is that then we don't know what the number separators are. We could determine it -with a simple test like formatting 12345.67, though. -Using "en" here means, among other things, that currency values are not localized as well -as they could be. Many European languages put currency signs as suffixes, eg. 123 euros is: -- "€123.00" in "en" -- "€123,00" with "en" but fixing up the separators for a German locale -- "123,00 €" in actual German convention, which is what we would get with a native "de" locale here.

-
(defn- number-formatter-for-options-baseline [options]
-  (let [default-fraction-digits (when (= (:number-style options) "currency")
-                                  2)]
-    (js/Intl.NumberFormat.
-      "en"
-      (clj->js (u/remove-nils
-                 {:style    (when-not (= (:number-style options) "scientific")
-                              (:number-style options "decimal"))
-                  :notation (when (= (:number-style options) "scientific")
-                              "scientific")
-                  :currency (:currency options)
-                  :currencyDisplay (:currency-style options)
-                  ;; Always use grouping separators, but we may remove them per number_separators.
-                  :useGrouping              true
-                  :minimumIntegerDigits     (:minimum-integer-digits     options)
-                  :minimumFractionDigits    (:minimum-fraction-digits    options default-fraction-digits)
-                  :maximumFractionDigits    (:maximum-fraction-digits    options default-fraction-digits)
-                  :minimumSignificantDigits (:minimum-significant-digits options)
-                  :maximumSignificantDigits (:maximum-significant-digits options)})))))
-
(defn- currency-symbols? [options]
-  (let [style (:currency-style options)]
-    (and (:currency options)
-         (or (nil? style)
-             (= style "symbol")))))
-
(defn- formatter-fn [nf options]
-  (case (:number-style options)
-    "scientific" #(base-format-scientific nf %)
-    #(.format nf %)))

The key function implemented for each language, and called by the top-level number formatting. -Returns a [[core/NumberFormatter]] instance for each set of options. -These formatters are reusable, but this does no caching.

-
(defn number-formatter-for-options
-  [options]
-  (let [nf        (number-formatter-for-options-baseline options)
-        symbols?  (currency-symbols? options)
-        formatter (formatter-fn nf options)]
-    (reify
-      core/NumberFormatter
-      (format-number-basic [_ number]
-        (cond-> (formatter number)
-          true     (adjust-number-separators (:number-separators options))
-          symbols? (fix-currency-symbols (:currency options))))
-      (wrap-currency [_ text]
-        ;; Intl.NumberFormat.formatToParts(1) returns, eg. [currency, integer, decimal, fraction]
-        ;; Keep only currency and integer, and replace integer's :value with our provided text.
-        (apply str (for [{:keys [type value]} (js->clj (.formatToParts nf 1) :keywordize-keys true)
-                         :when (#{"currency" "integer"} type)]
-                     (if (= type "integer")
-                       text
-                       value))))
-      (split-exponent [_ formatted] (throw (ex-info "split-exponent not implemented" {:text formatted}))))))

Formats a number in scientific notation. The wrangling required differs by platform.

- -

Scientific notation ============================================================================================

-
(defn format-number-scientific
-  [number options]
-  (-> (core/prep-options options)
-      number-formatter-for-options
-      (core/format-number-basic number)))
 

JVM Clojure implementation of the [[core/NumberFormatter]] abstaction.

+ (not (units-with-day unit)) (dissoc :weekday-enabled))))
 

JVM Clojure implementation of the [[core/NumberFormatter]] abstaction.

(ns metabase.shared.formatting.internal.numbers
   (:require
    [clojure.string :as str]
@@ -97456,7 +97383,106 @@ 

`&match` and `&parents` anaphors

base (core/format-number-basic nf number) {:keys [mantissa exponent]} (core/split-exponent nf base) ?plus (when-not (str/starts-with? exponent "-") "+")] - (str mantissa "e" ?plus exponent)))
 

Cross-platform foundation for the number formatters.

+ (str mantissa "e" ?plus exponent)))
 

ClojureScript implementation of number formatting. +Implements the [[NumberFormatter]] protocol from numbers_core, plus some helpers.

+
(ns metabase.shared.formatting.internal.numbers
+  (:require
+   [clojure.string :as str]
+   [metabase.shared.formatting.internal.numbers-core :as core]
+   [metabase.shared.util.currency :as currency]
+   [metabase.util :as u]))
+
(def ^:private default-number-separators ".,")
+
(defn- adjust-number-separators [text separators]
+  (if (and separators
+           (not= separators default-number-separators))
+    (let [decimal    (first separators)
+          grouping   (or (second separators) ) ; grouping separators are optional
+          transform  {"," grouping "." decimal}]
+      (str/replace text #"[\.,]" transform))
+    text))
+
(defn- fix-currency-symbols [text currency]
+  (let [sym (currency/currency-symbol currency)]
+    (-> text
+        ;; Some have spaces and some don't - remove the space if it's there.
+        (str/replace (str (name currency) core/non-breaking-space) sym)
+        (str/replace (name currency) sym))))
+
(defn- base-format-scientific [nf number]
+  (letfn [(transform [{:keys [type value]}]
+            (case type
+              "exponentSeparator" "e"
+              value))]
+    (let [parts  (js->clj (.formatToParts nf number) {:keywordize-keys true})
+          ;; If there's no exponent minus sign, add a plus sign.
+          parts  (if (some #(= (:type %) "exponentMinusSign") parts)
+                   parts
+                   (let [[pre post] (split-with #(not= (:type %) "exponentInteger") parts)]
+                     (concat pre [{:type "exponentPlusSign" :value "+"}] post)))]
+      (apply str (map transform parts)))))

Core internals ================================================================================================= +TODO(braden) We could get more nicely localized currency values by using the user's locale. +The problem is that then we don't know what the number separators are. We could determine it +with a simple test like formatting 12345.67, though. +Using "en" here means, among other things, that currency values are not localized as well +as they could be. Many European languages put currency signs as suffixes, eg. 123 euros is: +- "€123.00" in "en" +- "€123,00" with "en" but fixing up the separators for a German locale +- "123,00 €" in actual German convention, which is what we would get with a native "de" locale here.

+
(defn- number-formatter-for-options-baseline [options]
+  (let [default-fraction-digits (when (= (:number-style options) "currency")
+                                  2)]
+    (js/Intl.NumberFormat.
+      "en"
+      (clj->js (u/remove-nils
+                 {:style    (when-not (= (:number-style options) "scientific")
+                              (:number-style options "decimal"))
+                  :notation (when (= (:number-style options) "scientific")
+                              "scientific")
+                  :currency (:currency options)
+                  :currencyDisplay (:currency-style options)
+                  ;; Always use grouping separators, but we may remove them per number_separators.
+                  :useGrouping              true
+                  :minimumIntegerDigits     (:minimum-integer-digits     options)
+                  :minimumFractionDigits    (:minimum-fraction-digits    options default-fraction-digits)
+                  :maximumFractionDigits    (:maximum-fraction-digits    options default-fraction-digits)
+                  :minimumSignificantDigits (:minimum-significant-digits options)
+                  :maximumSignificantDigits (:maximum-significant-digits options)})))))
+
(defn- currency-symbols? [options]
+  (let [style (:currency-style options)]
+    (and (:currency options)
+         (or (nil? style)
+             (= style "symbol")))))
+
(defn- formatter-fn [nf options]
+  (case (:number-style options)
+    "scientific" #(base-format-scientific nf %)
+    #(.format nf %)))

The key function implemented for each language, and called by the top-level number formatting. +Returns a [[core/NumberFormatter]] instance for each set of options. +These formatters are reusable, but this does no caching.

+
(defn number-formatter-for-options
+  [options]
+  (let [nf        (number-formatter-for-options-baseline options)
+        symbols?  (currency-symbols? options)
+        formatter (formatter-fn nf options)]
+    (reify
+      core/NumberFormatter
+      (format-number-basic [_ number]
+        (cond-> (formatter number)
+          true     (adjust-number-separators (:number-separators options))
+          symbols? (fix-currency-symbols (:currency options))))
+      (wrap-currency [_ text]
+        ;; Intl.NumberFormat.formatToParts(1) returns, eg. [currency, integer, decimal, fraction]
+        ;; Keep only currency and integer, and replace integer's :value with our provided text.
+        (apply str (for [{:keys [type value]} (js->clj (.formatToParts nf 1) :keywordize-keys true)
+                         :when (#{"currency" "integer"} type)]
+                     (if (= type "integer")
+                       text
+                       value))))
+      (split-exponent [_ formatted] (throw (ex-info "split-exponent not implemented" {:text formatted}))))))

Formats a number in scientific notation. The wrangling required differs by platform.

+ +

Scientific notation ============================================================================================

+
(defn format-number-scientific
+  [number options]
+  (-> (core/prep-options options)
+      number-formatter-for-options
+      (core/format-number-basic number)))
 

Cross-platform foundation for the number formatters.

(ns metabase.shared.formatting.internal.numbers-core
   (:require
    [metabase.shared.util.currency :as currency]))

Options ========================================================================================================

@@ -98558,230 +98584,7 @@

`&match` and `&parents` anaphors

(-> format-string-pl escape-format-string (str/replace re-param-zero (str n))) - n)))
 

CLJS implementation of the time utilities on top of Moment.js. -See [[metabase.shared.util.time]] for the public interface.

-
(ns metabase.shared.util.internal.time
-  (:require
-   ["moment" :as moment]
-   [metabase.shared.util.internal.time-common :as common]))
-
(defn- now [] (moment))

Given any value, check if it's a (possibly invalid) Moment.

- -

----------------------------------------------- predicates -------------------------------------------------------

-
(defn datetime?
-  [value]
-  (and value (moment/isMoment value)))

checks if the provided value is a local time value.

-
(defn time?
-  [value]
-  (moment/isMoment value))

Given a Moment, check that it's valid.

-
(defn valid?
-  [value]
-  (and (datetime? value) (.isValid ^moment/Moment value)))

Does nothing. Just a placeholder in CLJS; the JVM implementation does some real work.

-
(defn normalize
-  [value]
-  value)

Given two platform-specific datetimes, checks if they fall within the same day.

-
(defn same-day?
-  [^moment/Moment d1 ^moment/Moment d2]
-  (.isSame d1 d2 "day"))

True if these two datetimes fall in the same (year and) month.

-
(defn same-month?
-  [^moment/Moment d1 ^moment/Moment d2]
-  (.isSame d1 d2 "month"))

True if these two datetimes fall in the same year.

-
(defn same-year?
-  [^moment/Moment d1 ^moment/Moment d2]
-  (.isSame d1 d2 "year"))

The first day of the week varies by locale, but Metabase has a setting that overrides it. -In CLJS, Moment is already configured with that setting.

- -

---------------------------------------------- information -------------------------------------------------------

-
(defn first-day-of-week
-  []
-  (-> (moment/weekdays 0)
-      (.toLowerCase)
-      keyword))

The default map of options - empty in CLJS.

-
(def default-options
-  {})

------------------------------------------------ to-range --------------------------------------------------------

-
(defn- apply-offset
-    [^moment/Moment value offset-n offset-unit]
-      (.add
-            (moment value)
-                offset-n
-                    (name offset-unit)))
-
(defmethod common/to-range :default [^moment/Moment value {:keys [n unit]}]
-  (let [^moment/Moment c1 (.clone value)
-        ^moment/Moment c2 (.clone value)]
-    [(.startOf c1 (name unit))
-     (cond-> c2
-       (> n 1) (.add (dec n) (name unit))
-       :always ^moment/Moment (.endOf (name unit)))]))

NB: Only the :default for to-range is needed in CLJS, since Moment's startOf and endOf methods are doing the work.

-

-------------------------------------------- string->timestamp ---------------------------------------------------

-
(defmethod common/string->timestamp :default [value _]
-  ;; Best effort to parse this unknown string format, as a local zoneless datetime, then treating it as UTC.
-  (moment/utc value moment/ISO_8601))
-
(defmethod common/string->timestamp :day-of-week [value options]
-  ;; Try to parse as a regular timestamp; if that fails then try to treat it as a weekday name and adjust from
-  ;; the current time.
-  (let [as-default (try ((get-method common/string->timestamp :default) value options)
-                        (catch js/Error _ nil))]
-    (if (valid? as-default)
-      as-default
-      (-> (now)
-          (.isoWeekday value)
-          (.startOf "day")))))

Some of the date coercions are relative, and not directly involved with any particular month. -To avoid errors we need to use a reference date that is (a) in a month with 31 days,(b) in a leap year. -This uses 2016-01-01 for the purpose. -This is a function that returns fresh values, since Moments are mutable.

- -

-------------------------------------------- number->timestamp ---------------------------------------------------

-
(defn- magic-base-date
-  []
-  (moment "2016-01-01"))
-
(defmethod common/number->timestamp :default [value _]
-  ;; If no unit is given, or the unit is not recognized, try to parse the number as year number, returning the timestamp
-  ;; for midnight UTC on January 1.
-  (moment/utc value moment/ISO_8601))
-
(defmethod common/number->timestamp :minute-of-hour [value _]
-  (.. (now) (minute value) (startOf "minute")))
-
(defmethod common/number->timestamp :hour-of-day [value _]
-  (.. (now) (hour value) (startOf "hour")))
-
(defmethod common/number->timestamp :day-of-week [value _]
-  ;; Metabase uses 1 to mean the start of the week, based on the Metabase setting for the first day of the week.
-  ;; Moment uses 0 as the first day of the week in its configured locale.
-  (.. (now) (weekday (dec value)) (startOf "day")))
-
(defmethod common/number->timestamp :day-of-month [value _]
-  ;; We force the initial date to be in a month with 31 days.
-  (.. (magic-base-date) (date value) (startOf "day")))
-
(defmethod common/number->timestamp :day-of-year [value _]
-  ;; We force the initial date to be in a leap year (2016).
-  (.. (magic-base-date) (dayOfYear value) (startOf "day")))
-
(defmethod common/number->timestamp :week-of-year [value _]
-  (.. (now) (week value) (startOf "week")))
-
(defmethod common/number->timestamp :month-of-year [value _]
-  (.. (now) (month (dec value)) (startOf "month")))
-
(defmethod common/number->timestamp :quarter-of-year [value _]
-  (.. (now) (quarter value) (startOf "quarter")))
-
(defmethod common/number->timestamp :year [value _]
-  (.. (now) (year value) (startOf "year")))

Parses a timestamp with Z or a timezone offset at the end. -This requires a different API call from timestamps without time zones in CLJS.

- -

---------------------------------------------- parsing helpers ---------------------------------------------------

-
(defn parse-with-zone
-  [value]
-  (moment/parseZone value))

Given a freshly parsed absolute Moment, convert it to a local one.

-
(defn localize
-  [value]
-  (.local value))
-
(def ^:private parse-time-formats
-  #js ["HH:mm:ss.SSS[Z]"
-       "HH:mm:ss.SSS"
-       "HH:mm:ss"
-       "HH:mm"])

Parses a time string that has been stripped of any time zone.

-
(defn parse-time-string
-  [value]
-  (moment value parse-time-formats))

------------------------------------------------ arithmetic ------------------------------------------------------

-

Returns the time elapsed between before and after in days.

-
(defn day-diff
-  [^moment/Moment before ^moment/Moment after]
-  (.diff after before "day"))
-
(defn- matches-time? [input]
-  (re-matches #"\d\d:\d\d(?::\d\d(?:\.\d+)?)?" input))
-
(defn- matches-date? [input]
-  (re-matches #"\d\d\d\d-\d\d-\d\d" input))
-
(defn- matches-date-time? [input]
-  (re-matches #"\d\d\d\d-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d+)?)?" input))

Formats a temporal-value (iso date/time string, int for hour/minute) given the temporal-bucketing unit. - If unit is nil, formats the full date/time. - Time input formatting is only defined with time units.

-
(defn format-unit
-  [input unit]
-  (if (string? input)
-    (let [time? (matches-time? input)
-          date? (matches-date? input)
-          date-time? (matches-date-time? input)
-          t (if time?
-              ;; Anchor to an arbitrary date since time inputs are only defined for
-              ;; :hour-of-day and :minute-of-hour.
-              (moment/utc (str "2023-01-01T" input) moment/ISO_8601)
-              (moment/utc input moment/ISO_8601))]
-      (if (and t (.isValid t))
-        (case unit
-          :day-of-week (.format t "dddd")
-          :month-of-year (.format t "MMM")
-          :minute-of-hour (.format t "m")
-          :hour-of-day (.format t "h A")
-          :day-of-month (.format t "D")
-          :day-of-year (.format t "DDD")
-          :week-of-year (.format t "w")
-          :quarter-of-year (.format t "[Q]Q")
-          (cond
-            time? (.format t "h:mm A")
-            date? (.format t "MMM D, YYYY")
-            date-time? (.format t "MMM D, YYYY, h:mm A")))
-        input))
-    (if (= unit :hour-of-day)
-      (str (cond (zero? input) "12" (<= input 12) input :else (- input 12)) " " (if (<= input 11) "AM" "PM"))
-      (str input))))

Formats a time difference between two temporal values. - Drops redundant information.

-
(defn format-diff
-  [temporal-value-1 temporal-value-2]
-  (let [default-format #(str (format-unit temporal-value-1 nil)
-                             " – "
-                             (format-unit temporal-value-2 nil))]
-    (cond
-      (some (complement string?) [temporal-value-1 temporal-value-2])
-      (default-format)
-      (= temporal-value-1 temporal-value-2)
-      (format-unit temporal-value-1 nil)
-      (and (matches-time? temporal-value-1)
-           (matches-time? temporal-value-2))
-      (default-format)
-      (and (matches-date-time? temporal-value-1)
-           (matches-date-time? temporal-value-2))
-      (let [lhs (moment/utc temporal-value-1 moment/ISO_8601)
-            rhs (moment/utc temporal-value-2 moment/ISO_8601)
-            year-matches? (= (.format lhs "YYYY") (.format rhs "YYYY"))
-            month-matches? (= (.format lhs "MMM") (.format rhs "MMM"))
-            day-matches? (= (.format lhs "D") (.format rhs "D"))
-            hour-matches? (= (.format lhs "HH") (.format rhs "HH"))
-            [lhs-fmt rhs-fmt] (cond
-                                (and year-matches? month-matches? day-matches? hour-matches?)
-                                ["MMM D, YYYY, h:mm A " " h:mm A"]
-                                (and year-matches? month-matches? day-matches?)
-                                ["MMM D, YYYY, h:mm A " " h:mm A"]
-                                year-matches?
-                                ["MMM D, h:mm A " " MMM D, YYYY, h:mm A"])]
-        (if lhs-fmt
-          (str (.format lhs lhs-fmt) "–" (.format rhs rhs-fmt))
-          (default-format)))
-      (and (matches-date? temporal-value-1)
-           (matches-date? temporal-value-2))
-      (let [lhs (moment/utc temporal-value-1 moment/ISO_8601)
-            rhs (moment/utc temporal-value-2 moment/ISO_8601)
-            year-matches? (= (.format lhs "YYYY") (.format rhs "YYYY"))
-            month-matches? (= (.format lhs "MMM") (.format rhs "MMM"))
-            [lhs-fmt rhs-fmt] (cond
-                                (and year-matches? month-matches?)
-                                ["MMM D" "D, YYYY"]
-                                year-matches?
-                                ["MMM D " " MMM D, YYYY"])]
-        (if lhs-fmt
-          (str (.format lhs lhs-fmt) "–" (.format rhs rhs-fmt))
-          (default-format)))
-      :else
-      (default-format))))

Given a n unit time interval and the current date, return a string representing the date-time range. - Provide an offset-n and offset-unit time interval to change the date used relative to the current date. - options is a map and supports :include-current to include the current given unit of time in the range.

-
(defn format-relative-date-range
-  [n unit offset-n offset-unit {:keys [include-current]}]
-  (let [offset-now (cond-> (now)
-                     (neg? n) (apply-offset n unit)
-                     (and (pos? n) (not include-current)) (apply-offset 1 unit)
-                     (and offset-n offset-unit) (apply-offset offset-n offset-unit))
-        pos-n (cond-> (abs n)
-                include-current inc)
-        date-ranges (map #(.format % (if (#{:hour :minute} unit) "YYYY-MM-DDTHH:mm" "YYYY-MM-DD"))
-                         (common/to-range offset-now
-                                          {:unit unit
-                                           :n pos-n
-                                           :offset-n offset-n
-                                           :offset-unit offset-unit}))]
-    (apply format-diff date-ranges)))
 
+ n)))
 
(ns metabase.shared.util.internal.time
   (:require
    [java-time.api :as t]
@@ -99056,6 +98859,229 @@ 

`&match` and `&parents` anaphors

:n pos-n :offset-n offset-n :offset-unit offset-unit}))] + (apply format-diff date-ranges)))
 

CLJS implementation of the time utilities on top of Moment.js. +See [[metabase.shared.util.time]] for the public interface.

+
(ns metabase.shared.util.internal.time
+  (:require
+   ["moment" :as moment]
+   [metabase.shared.util.internal.time-common :as common]))
+
(defn- now [] (moment))

Given any value, check if it's a (possibly invalid) Moment.

+ +

----------------------------------------------- predicates -------------------------------------------------------

+
(defn datetime?
+  [value]
+  (and value (moment/isMoment value)))

checks if the provided value is a local time value.

+
(defn time?
+  [value]
+  (moment/isMoment value))

Given a Moment, check that it's valid.

+
(defn valid?
+  [value]
+  (and (datetime? value) (.isValid ^moment/Moment value)))

Does nothing. Just a placeholder in CLJS; the JVM implementation does some real work.

+
(defn normalize
+  [value]
+  value)

Given two platform-specific datetimes, checks if they fall within the same day.

+
(defn same-day?
+  [^moment/Moment d1 ^moment/Moment d2]
+  (.isSame d1 d2 "day"))

True if these two datetimes fall in the same (year and) month.

+
(defn same-month?
+  [^moment/Moment d1 ^moment/Moment d2]
+  (.isSame d1 d2 "month"))

True if these two datetimes fall in the same year.

+
(defn same-year?
+  [^moment/Moment d1 ^moment/Moment d2]
+  (.isSame d1 d2 "year"))

The first day of the week varies by locale, but Metabase has a setting that overrides it. +In CLJS, Moment is already configured with that setting.

+ +

---------------------------------------------- information -------------------------------------------------------

+
(defn first-day-of-week
+  []
+  (-> (moment/weekdays 0)
+      (.toLowerCase)
+      keyword))

The default map of options - empty in CLJS.

+
(def default-options
+  {})

------------------------------------------------ to-range --------------------------------------------------------

+
(defn- apply-offset
+    [^moment/Moment value offset-n offset-unit]
+      (.add
+            (moment value)
+                offset-n
+                    (name offset-unit)))
+
(defmethod common/to-range :default [^moment/Moment value {:keys [n unit]}]
+  (let [^moment/Moment c1 (.clone value)
+        ^moment/Moment c2 (.clone value)]
+    [(.startOf c1 (name unit))
+     (cond-> c2
+       (> n 1) (.add (dec n) (name unit))
+       :always ^moment/Moment (.endOf (name unit)))]))

NB: Only the :default for to-range is needed in CLJS, since Moment's startOf and endOf methods are doing the work.

+

-------------------------------------------- string->timestamp ---------------------------------------------------

+
(defmethod common/string->timestamp :default [value _]
+  ;; Best effort to parse this unknown string format, as a local zoneless datetime, then treating it as UTC.
+  (moment/utc value moment/ISO_8601))
+
(defmethod common/string->timestamp :day-of-week [value options]
+  ;; Try to parse as a regular timestamp; if that fails then try to treat it as a weekday name and adjust from
+  ;; the current time.
+  (let [as-default (try ((get-method common/string->timestamp :default) value options)
+                        (catch js/Error _ nil))]
+    (if (valid? as-default)
+      as-default
+      (-> (now)
+          (.isoWeekday value)
+          (.startOf "day")))))

Some of the date coercions are relative, and not directly involved with any particular month. +To avoid errors we need to use a reference date that is (a) in a month with 31 days,(b) in a leap year. +This uses 2016-01-01 for the purpose. +This is a function that returns fresh values, since Moments are mutable.

+ +

-------------------------------------------- number->timestamp ---------------------------------------------------

+
(defn- magic-base-date
+  []
+  (moment "2016-01-01"))
+
(defmethod common/number->timestamp :default [value _]
+  ;; If no unit is given, or the unit is not recognized, try to parse the number as year number, returning the timestamp
+  ;; for midnight UTC on January 1.
+  (moment/utc value moment/ISO_8601))
+
(defmethod common/number->timestamp :minute-of-hour [value _]
+  (.. (now) (minute value) (startOf "minute")))
+
(defmethod common/number->timestamp :hour-of-day [value _]
+  (.. (now) (hour value) (startOf "hour")))
+
(defmethod common/number->timestamp :day-of-week [value _]
+  ;; Metabase uses 1 to mean the start of the week, based on the Metabase setting for the first day of the week.
+  ;; Moment uses 0 as the first day of the week in its configured locale.
+  (.. (now) (weekday (dec value)) (startOf "day")))
+
(defmethod common/number->timestamp :day-of-month [value _]
+  ;; We force the initial date to be in a month with 31 days.
+  (.. (magic-base-date) (date value) (startOf "day")))
+
(defmethod common/number->timestamp :day-of-year [value _]
+  ;; We force the initial date to be in a leap year (2016).
+  (.. (magic-base-date) (dayOfYear value) (startOf "day")))
+
(defmethod common/number->timestamp :week-of-year [value _]
+  (.. (now) (week value) (startOf "week")))
+
(defmethod common/number->timestamp :month-of-year [value _]
+  (.. (now) (month (dec value)) (startOf "month")))
+
(defmethod common/number->timestamp :quarter-of-year [value _]
+  (.. (now) (quarter value) (startOf "quarter")))
+
(defmethod common/number->timestamp :year [value _]
+  (.. (now) (year value) (startOf "year")))

Parses a timestamp with Z or a timezone offset at the end. +This requires a different API call from timestamps without time zones in CLJS.

+ +

---------------------------------------------- parsing helpers ---------------------------------------------------

+
(defn parse-with-zone
+  [value]
+  (moment/parseZone value))

Given a freshly parsed absolute Moment, convert it to a local one.

+
(defn localize
+  [value]
+  (.local value))
+
(def ^:private parse-time-formats
+  #js ["HH:mm:ss.SSS[Z]"
+       "HH:mm:ss.SSS"
+       "HH:mm:ss"
+       "HH:mm"])

Parses a time string that has been stripped of any time zone.

+
(defn parse-time-string
+  [value]
+  (moment value parse-time-formats))

------------------------------------------------ arithmetic ------------------------------------------------------

+

Returns the time elapsed between before and after in days.

+
(defn day-diff
+  [^moment/Moment before ^moment/Moment after]
+  (.diff after before "day"))
+
(defn- matches-time? [input]
+  (re-matches #"\d\d:\d\d(?::\d\d(?:\.\d+)?)?" input))
+
(defn- matches-date? [input]
+  (re-matches #"\d\d\d\d-\d\d-\d\d" input))
+
(defn- matches-date-time? [input]
+  (re-matches #"\d\d\d\d-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d+)?)?" input))

Formats a temporal-value (iso date/time string, int for hour/minute) given the temporal-bucketing unit. + If unit is nil, formats the full date/time. + Time input formatting is only defined with time units.

+
(defn format-unit
+  [input unit]
+  (if (string? input)
+    (let [time? (matches-time? input)
+          date? (matches-date? input)
+          date-time? (matches-date-time? input)
+          t (if time?
+              ;; Anchor to an arbitrary date since time inputs are only defined for
+              ;; :hour-of-day and :minute-of-hour.
+              (moment/utc (str "2023-01-01T" input) moment/ISO_8601)
+              (moment/utc input moment/ISO_8601))]
+      (if (and t (.isValid t))
+        (case unit
+          :day-of-week (.format t "dddd")
+          :month-of-year (.format t "MMM")
+          :minute-of-hour (.format t "m")
+          :hour-of-day (.format t "h A")
+          :day-of-month (.format t "D")
+          :day-of-year (.format t "DDD")
+          :week-of-year (.format t "w")
+          :quarter-of-year (.format t "[Q]Q")
+          (cond
+            time? (.format t "h:mm A")
+            date? (.format t "MMM D, YYYY")
+            date-time? (.format t "MMM D, YYYY, h:mm A")))
+        input))
+    (if (= unit :hour-of-day)
+      (str (cond (zero? input) "12" (<= input 12) input :else (- input 12)) " " (if (<= input 11) "AM" "PM"))
+      (str input))))

Formats a time difference between two temporal values. + Drops redundant information.

+
(defn format-diff
+  [temporal-value-1 temporal-value-2]
+  (let [default-format #(str (format-unit temporal-value-1 nil)
+                             " – "
+                             (format-unit temporal-value-2 nil))]
+    (cond
+      (some (complement string?) [temporal-value-1 temporal-value-2])
+      (default-format)
+      (= temporal-value-1 temporal-value-2)
+      (format-unit temporal-value-1 nil)
+      (and (matches-time? temporal-value-1)
+           (matches-time? temporal-value-2))
+      (default-format)
+      (and (matches-date-time? temporal-value-1)
+           (matches-date-time? temporal-value-2))
+      (let [lhs (moment/utc temporal-value-1 moment/ISO_8601)
+            rhs (moment/utc temporal-value-2 moment/ISO_8601)
+            year-matches? (= (.format lhs "YYYY") (.format rhs "YYYY"))
+            month-matches? (= (.format lhs "MMM") (.format rhs "MMM"))
+            day-matches? (= (.format lhs "D") (.format rhs "D"))
+            hour-matches? (= (.format lhs "HH") (.format rhs "HH"))
+            [lhs-fmt rhs-fmt] (cond
+                                (and year-matches? month-matches? day-matches? hour-matches?)
+                                ["MMM D, YYYY, h:mm A " " h:mm A"]
+                                (and year-matches? month-matches? day-matches?)
+                                ["MMM D, YYYY, h:mm A " " h:mm A"]
+                                year-matches?
+                                ["MMM D, h:mm A " " MMM D, YYYY, h:mm A"])]
+        (if lhs-fmt
+          (str (.format lhs lhs-fmt) "–" (.format rhs rhs-fmt))
+          (default-format)))
+      (and (matches-date? temporal-value-1)
+           (matches-date? temporal-value-2))
+      (let [lhs (moment/utc temporal-value-1 moment/ISO_8601)
+            rhs (moment/utc temporal-value-2 moment/ISO_8601)
+            year-matches? (= (.format lhs "YYYY") (.format rhs "YYYY"))
+            month-matches? (= (.format lhs "MMM") (.format rhs "MMM"))
+            [lhs-fmt rhs-fmt] (cond
+                                (and year-matches? month-matches?)
+                                ["MMM D" "D, YYYY"]
+                                year-matches?
+                                ["MMM D " " MMM D, YYYY"])]
+        (if lhs-fmt
+          (str (.format lhs lhs-fmt) "–" (.format rhs rhs-fmt))
+          (default-format)))
+      :else
+      (default-format))))

Given a n unit time interval and the current date, return a string representing the date-time range. + Provide an offset-n and offset-unit time interval to change the date used relative to the current date. + options is a map and supports :include-current to include the current given unit of time in the range.

+
(defn format-relative-date-range
+  [n unit offset-n offset-unit {:keys [include-current]}]
+  (let [offset-now (cond-> (now)
+                     (neg? n) (apply-offset n unit)
+                     (and (pos? n) (not include-current)) (apply-offset 1 unit)
+                     (and offset-n offset-unit) (apply-offset offset-n offset-unit))
+        pos-n (cond-> (abs n)
+                include-current inc)
+        date-ranges (map #(.format % (if (#{:hour :minute} unit) "YYYY-MM-DDTHH:mm" "YYYY-MM-DD"))
+                         (common/to-range offset-now
+                                          {:unit unit
+                                           :n pos-n
+                                           :offset-n offset-n
+                                           :offset-unit offset-unit}))]
     (apply format-diff date-ranges)))
 

Shared core of time utils used by the internal CLJ and CLJS implementations. See [[metabase.shared.util.time]] for the public interface.

(ns metabase.shared.util.internal.time-common)
@@ -109047,7 +109073,7 @@

Java system property user.dir

min-retention-days) (do (truncate-audit-log.i/log-minimum-value-warning env-var-value) min-retention-days) - :else env-var-value))))