T2: Toucan 2

0.0.1-SNAPSHOT


A classy high-level Clojure library for defining application models and retrieving them from a DB




(this space intentionally left almost blank)
 

Connection Resolution

The rules for determining which connection to use are as follows. These are tried in order until one returns non-nil:

  1. The connectable specified in the function arguments.

  2. The [[toucan2.connection/current-connectable]], if bound. This is bound automatically when using [[with-connection]] or [[with-transaction]]

  3. The [[toucan2.model/default-connectable]] for the model resolved from the modelable in the function arguments;

  4. The :default implementation of [[toucan2.connection/do-with-connection]]

You can define a 'named' connectable such as ::db by adding an implementation of [[toucan2.connection/do-with-connection]], or use things like JDBC URL connection strings or [[clojure.java.jdbc]] connection properties maps directly.

IMPORTANT CAVEAT! Positional connectables will be used in preference to [[current-connectable]], even when it was bound by [[with-transaction]] -- this means your query will run OUTSIDE of the current transaction! Sometimes, this is what you want, because maybe a certain query is meant to run against a different database! Usually, however, it is not! So in that case you can either do something like

```clj (t2/query (or conn/current-connectable ::my-db) ...) ```

to use the current connection if it exists, or define your named connectable method like

```clj (m/defmethod conn/do-with-connection ::my-db [_connectable f] (conn/do-with-connection (if (and conn/current-connectable (not= conn/current-connectable ::my-db)) conn/current-connectable "jdbc:postgresql://...") f)) ```

This, however, is super annoying! So I might reconsider this behavior in the future.

For reducible queries, the connection is not resolved until the query is executed, so you may create a reducible query with no default connection available and execute it later with one bound. (This also means that [[toucan2.execute/reducible-query]] does not capture dynamic bindings such as [[toucan2.connection/current-connectable]] -- you probably wouldn't want it to, anyway, since we have no guarantees and open connection will be around when we go to use the reducible query later.

The default JDBC implementations for methods here live in [[toucan2.jdbc.connection]].

(ns toucan2.connection
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [pretty.core :as pretty]
   [toucan2.log :as log]
   [toucan2.protocols :as protocols]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(set! *warn-on-reflection* true)
(comment types/keep-me)

The current connectable or connection. If you get a connection with [[with-connection]] or [[with-transaction]], it will be bound here. You can also bind this yourself to a connectable or connection, and Toucan methods called without an explicit will connectable will use it rather than the :default connection.

(def ^:dynamic *current-connectable*
  nil)

Take a connectable, get a connection of some sort from it, and execute (f connection) with an open connection. A normal implementation might look something like:

```clj (m/defmethod t2.conn/do-with-connection ::my-connectable [_connectable f] (with-open [conn (get-connection)] (f conn))) ```

Another common use case is to define a 'named' connectable that acts as an alias for another more complicated connectable, such as a JDBC connection string URL. You can do that like this:

```clj (m/defmethod t2.conn/do-with-connection ::a-connectable [_connectable f] (t2.conn/do-with-connection "jdbc:postgresql://localhost:5432/toucan2?user=cam&password=cam" f)) ```

(m/defmulti do-with-connection
  {:arglists            '([connectable₁ f])
   :defmethod-arities   #{2}
   :dispatch-value-spec ::types/dispatch-value.keyword-or-class}
  u/dispatch-on-first-arg
  :default-value ::default)

Wrap functions as passed to [[do-with-connection]] or [[do-with-transaction]] in a way that binds [[current-connectable]].

(defn- bind-current-connectable-fn
  [f]
  {:pre [(fn? f)]}
  (^:once fn* [conn]
   (binding [*current-connectable* conn]
     (f conn))))
(m/defmethod do-with-connection :around ::default
  "Do some debug logging/context capture. Bind [[*current-connectable*]] to the connection `f` is called with inside of
  `f`."
  [connectable f]
  (assert (fn? f))
  ;; add the connection class or pretty representation rather than the connection type itself to avoid leaking sensitive
  ;; creds
  (let [connectable-class (if (instance? pretty.core.PrettyPrintable connectable)
                            (pretty/pretty connectable)
                            (protocols/dispatch-value connectable))]
    (log/debugf "Resolve connection %s" connectable-class)
    (u/try-with-error-context ["resolve connection" {::connectable connectable-class}]
      (next-method connectable (bind-current-connectable-fn f)))))

Execute body with an open connection. There are three ways to use this.

With no args in the bindings vector, with-connection will use the current connection -- [[current-connectable]] if one is bound, or the default connectable if not. See docstring for [[toucan2.connection]] for more information.

```clj (t2/with-connection [] ...) ```

With one arg, with-connection still uses the current connection, but binds it to something (conn in the example below):

```clj (t2/with-connection [conn] ...) ```

If you're using the default JDBC backend, conn will be an instance of java.sql.Connection. Since Toucan 2 is also written to work with other backend besides JDBC, conn does not include java.sql.Connection :tag metadata! If you're doing Java interop with conn, make sure to tag it yourself:

```clj (t2/with-connection [^java.sql.Connection conn] (let [metadata (.getMetaData conn)] ...)) ```

With a connection binding and a connectable:

```clj (t2/with-connection [conn ::my-connectable] ...) ```

This example gets a connection by calling [[do-with-connection]] with ::my-connectable, ignoring the *current connection*.

(defmacro with-connection
  {:arglists '([[connection-binding]             & body]
               [[connection-binding connectable] & body])}
  [[connection-binding connectable] & body]
  `(do-with-connection
    ~connectable
    (^:once fn* with-connection* [~(or connection-binding '_)] ~@body)))
(s/fdef with-connection
  :args (s/cat :bindings (s/spec (s/cat :connection-binding (s/? symbol?)
                                        :connectable        (s/? any?)))
               :body (s/+ any?))
  :ret  any?)

method if this is called with something we don't know how to handle or if no default connection is defined. This is separate from :default so if you implement :default you don't accidentally have that get called for unknown connectables

(m/defmethod do-with-connection ::default
  [connectable _f]
  (throw (ex-info (format "Don't know how to get a connection from ^%s %s. Do you need to implement %s for %s?"
                          (some-> connectable class .getCanonicalName)
                          (pr-str connectable)
                          `do-with-connection
                          (protocols/dispatch-value connectable))
                  {:connectable connectable})))

method called if there is no current connection.

(m/defmethod do-with-connection :default
  [_connectable _f]
  (throw (ex-info (str "No default Toucan connection defined. "
                       (format "You can define one by implementing %s for :default. "
                               `do-with-connection)
                       (format "You can also implement %s for a model, or bind %s."
                               'toucan2.model/default-connectable
                               `*current-connectable*))
                  {})))
(m/defmethod do-with-connection nil
  "`nil` means use the current connection.
  The difference between `nil` and using [[*current-connectable*]] directly is that this waits until it gets resolved
  by [[do-with-connection]] to get the value for [[*current-connectable*]]. For a reducible query this means you'll get
  the value at the time you reduce the query rather than at the time you build the reducible query."
  [_connectable f]
  (let [current-connectable (if (nil? *current-connectable*)
                              :default
                              *current-connectable*)]
    (do-with-connection current-connectable f)))

connection string support

Extract the protocol part of a connection-string.

```clj (connection-string-protocol "jdbc:postgresql:...") => "jdbc" ```

(defn connection-string-protocol
  [connection-string]
  (when (string? connection-string)
    (second (re-find #"^(?:([^:]+):)" connection-string))))

Implementation of [[do-with-connection]] for strings. Dispatches on the [[connection-string-protocol]] of the string, e.g. "jdbc" for "jdbc:postgresql://localhost:3000/toucan".

(m/defmulti do-with-connection-string
  {:arglists            '([^java.lang.String connection-string f])
   :defmethod-arities   #{2}
   :dispatch-value-spec string?}
  (fn [connection-string _f]
    (connection-string-protocol connection-string)))
(m/defmethod do-with-connection String
  "Implementation for Strings. Hands off to [[do-with-connection-string]]."
  [connection-string f]
  (do-with-connection-string connection-string f))

JDBC implementations live in [[toucan2.jdbc.connection]]

options are options for determining what type of transaction we'll get. See dox for [[with-transaction]] for more information.

(m/defmulti do-with-transaction
  {:arglists            '([connection₁ options f])
   :defmethod-arities   #{3}
   :dispatch-value-spec ::types/dispatch-value.keyword-or-class}
  u/dispatch-on-first-arg
  :default-value ::default)
(m/defmethod do-with-transaction :around ::default
  "Bind [[*current-connectable*]] to the connection `f` is called with inside of `f`."
  [connection options f]
  (log/debugf "do with transaction %s %s" options (some-> connection class .getCanonicalName symbol))
  (next-method connection options (bind-current-connectable-fn f)))

Gets a connection with [[with-connection]], and executes body within that transaction.

An options map, if specified, determine what sort of transaction we're asking for (stuff like the read isolation level and what not). One key, :nested-transaction-rule, is handled directly in Toucan 2; other options are passed directly to the underlying implementation, such as [[next.jdbc.transaction]].

:nested-transaction-rule must be one of #{:allow :ignore :prohibit}, a set of possibilities borrowed from next.jdbc. For non-JDBC implementations, you should treat :allow as the default behavior if unspecified.

(defmacro with-transaction
  {:style/indent 1, :arglists '([[conn-binding connectable options?] & body])}
  [[conn-binding connectable options] & body]
  `(with-connection [conn# ~connectable]
     (do-with-transaction conn# ~options (^:once fn* with-transaction* [~(or conn-binding '_)] ~@body))))
(s/def :toucan2.with-transaction-options/nested-transaction-rule
  (s/nilable #{:allow :ignore :prohibit}))
(s/def ::with-transaction-options
  (s/keys :opt-un [:toucan2.with-transaction-options/nested-transaction-rule]))
(s/fdef with-transaction
  :args (s/cat :bindings (s/spec (s/cat :connection-binding (s/? symbol?)
                                        :connectable        (s/? any?)
                                        :options            (s/? ::with-transaction-options)))
               :body (s/+ any?))
  :ret  any?)

JDBC implementation lives in [[toucan2.jdbc.connection]]

 

Convenience namespace exposing the most common parts of the library's public API for day-to-day usage (i.e., not implementing anything advanced)

(ns ^:no-doc toucan2.core
  (:refer-clojure :exclude [compile count instance?])
  (:require
   [potemkin :as p]
   [toucan2.connection]
   [toucan2.delete]
   [toucan2.execute]
   [toucan2.honeysql2]
   [toucan2.insert]
   [toucan2.instance]
   [toucan2.jdbc]
   [toucan2.model]
   [toucan2.protocols]
   [toucan2.save]
   [toucan2.select]
   [toucan2.tools.after-insert]
   [toucan2.tools.after-select]
   [toucan2.tools.after-update]
   [toucan2.tools.before-delete]
   [toucan2.tools.before-insert]
   [toucan2.tools.before-select]
   [toucan2.tools.before-update]
   [toucan2.tools.compile]
   [toucan2.tools.debug]
   [toucan2.tools.default-fields]
   [toucan2.tools.hydrate]
   [toucan2.tools.named-query]
   [toucan2.tools.transformed]
   [toucan2.update]
   [toucan2.util]))

this is so no one gets confused and thinks these namespaces are unused.

(comment
  toucan2.connection/keep-me
  toucan2.delete/keep-me
  toucan2.execute/keep-me
  toucan2.honeysql2/keep-me
  toucan2.insert/keep-me
  toucan2.instance/keep-me
  toucan2.jdbc/keep-me
  toucan2.model/keep-me
  toucan2.protocols/keep-me
  toucan2.save/keep-me
  toucan2.select/keep-me
  toucan2.tools.after-insert/keep-me
  toucan2.tools.after-select/keep-me
  toucan2.tools.after-update/keep-me
  toucan2.tools.before-delete/keep-me
  toucan2.tools.before-insert/keep-me
  toucan2.tools.before-select/keep-me
  toucan2.tools.before-update/keep-me
  toucan2.tools.compile/keep-me
  toucan2.tools.debug/keep-me
  toucan2.tools.default-fields/keep-me
  toucan2.tools.hydrate/keep-me
  toucan2.tools.named-query/keep-me
  toucan2.tools.transformed/keep-me
  toucan2.update/keep-me
  toucan2.util/keep-me)
(p/import-vars
 [toucan2.connection
  do-with-connection
  with-connection
  with-transaction]

 [toucan2.delete
  delete!]

 [toucan2.execute
  query
  query-one
  reducible-query
  with-call-count]

 [toucan2.insert
  insert!
  insert-returning-instance!
  insert-returning-pk!
  insert-returning-instances!
  insert-returning-pks!]

 [toucan2.instance
  instance
  instance-of?
  instance?]

 [toucan2.model
  default-connectable
  primary-key-values-map
  primary-keys
  resolve-model
  select-pks-fn
  table-name]

 [toucan2.protocols
  changes
  current
  model
  original]

 [toucan2.save
  save!]

 [toucan2.select
  count
  exists?
  reducible-select
  select
  select-fn->fn
  select-fn->pk
  select-fn-reducible
  select-fn-set
  select-fn-vec
  select-one
  select-one-fn
  select-one-pk
  select-pk->fn
  select-pks-set
  select-pks-vec]

 [toucan2.tools.after-insert
  define-after-insert]

 [toucan2.tools.after-select
  define-after-select]

 [toucan2.tools.after-update
  define-after-update]

 [toucan2.tools.before-delete
  define-before-delete]

 [toucan2.tools.before-insert
  define-before-insert]

 [toucan2.tools.before-select
  define-before-select]

 [toucan2.tools.before-update
  define-before-update]

 [toucan2.tools.compile
  build
  compile]

 [toucan2.tools.debug
  debug]

 [toucan2.tools.default-fields
  define-default-fields]

 [toucan2.tools.hydrate
  batched-hydrate
  hydrate
  model-for-automagic-hydration
  simple-hydrate]

 [toucan2.tools.named-query
  define-named-query]

 [toucan2.tools.transformed
  deftransforms
  transforms]

 [toucan2.update
  reducible-update
  reducible-update-returning-pks
  update!
  update-returning-pks!])
 

Implementation of [[delete!]].

Code for building Honey SQL for DELETE lives in [[toucan2.honeysql2]]

(ns toucan2.delete
  (:require
   [toucan2.pipeline :as pipeline]))

Delete instances of a model that match conditions or a queryable. Returns the number of rows deleted.

```clj ;; delete a row by primary key (t2/delete! :models/venues 1)

;; Delete all rows matching a condition (t2/delete! :models/venues :name "Bird Store") ```

Allowed syntax is identical to [[toucan2.select/select]], including optional positional parameters like :conn; see [[toucan2.query/parse-args]] and the :toucan2.query/default-args spec.

(defn delete!
  {:arglists '([modelable & conditions? queryable?]
               [:conn connectable modelable & conditions? queryable?])}
  [& unparsed-args]
  (pipeline/transduce-unparsed-with-default-rf :toucan.query-type/delete.update-count unparsed-args))
 

Code for executing queries and statements, and reducing their results.

The functions here meant for use on a regular basis are:

  • [[query]] -- resolve and compile a connectable, execute it using a connection from a connectable, and immediately fully realize the results.

  • [[query-one]] -- like [[query]], but only realizes the first result.

  • [[reducible-query]] -- like [[query]], but returns a [[clojure.lang.IReduceInit]] that can be reduced later rather than immediately realizing the results.

  • [[with-call-count]] -- helper macro to count the number of queries executed within a body.

(ns toucan2.execute
  (:require
   [clojure.spec.alpha :as s]
   [toucan2.pipeline :as pipeline]))
(set! *warn-on-reflection* true)
(defn- query* [f]
  (fn query**
    ([queryable]
     ;; `nil` connectable = use the current connection or `:default` if none is specified.
     (query** nil queryable))
    ([connectable queryable]
     (query** connectable nil queryable))
    ([connectable modelable queryable]
     ;; by passing `result-type/*` we'll basically get whatever the default is -- instances for `SELECT` or update counts
     ;; for `DML` stuff.
     (query** connectable :toucan.result-type/* modelable queryable))
    ([connectable query-type modelable queryable]
     (let [parsed-args {:connectable connectable
                        :modelable   modelable
                        :queryable   queryable}]
       (f query-type parsed-args)))))

Create a reducible query that when reduced with resolve and compile queryable, open a connection using connectable and [[toucan2.connection/with-connection]], execute the query, and reduce the results.

Note that this query can be something like a SELECT query, or something that doesn't normally return results, like an UPDATE; this works either way. Normally something like an UPDATE will reduce to the number of rows updated/inserted/deleted, e.g. [5].

You can specify modelable to execute the query in the context of a specific model; for queries returning rows, the rows will be returned as an [[toucan2.instance/instance]] of the resolved model.

See [[toucan2.connection]] for Connection resolution rules.

(def ^{:arglists '([queryable]
                   [connectable queryable]
                   [connectable modelable queryable]
                   [connectable query-type modelable queryable])}
  reducible-query
  (query* pipeline/reducible-parsed-args))

Util functions for running queries and immediately realizing the results.

Like [[reducible-query]], but immediately executes and fully reduces the query.

```clj (query ::my-connectable ["SELECT * FROM venues;"]) ;; => [{:id 1, :name "Tempest"} {:id 2, :name "BevMo"}] ```

Like [[reducible-query]], this may be used with either SELECT queries that return rows or things like UPDATE that normally return the count of rows updated.

(def ^{:arglists '([queryable]
                   [connectable queryable]
                   [connectable modelable queryable]
                   [connectable query-type modelable queryable])}
  query
  (query*
   (fn [query-type parsed-args]
     (let [rf (pipeline/default-rf query-type)]
       (pipeline/transduce-parsed rf query-type parsed-args)))))

Like [[query]], and immediately executes the query, but realizes and returns only the first result.

```clj (query-one ::my-connectable ["SELECT * FROM venues;"]) ;; => {:id 1, :name "Tempest"} ```

Like [[reducible-query]], this may be used with either SELECT queries that return rows or things like UPDATE that normally return the count of rows updated.

(def ^{:arglists '([queryable]
                   [connectable queryable]
                   [connectable modelable queryable]
                   [connectable query-type modelable queryable])}
  query-one
  (query*
   (fn [query-type parsed-args]
     (let [rf    (pipeline/default-rf query-type)
           xform (pipeline/first-result-xform-fn query-type)]
       (pipeline/transduce-parsed (xform rf) query-type parsed-args)))))

[[with-call-count]]

Impl for [[with-call-count]] macro; don't call this directly.

(defn ^:no-doc do-with-call-counts
  [f]
  (let [call-count (atom 0)
        old-thunk  pipeline/*call-count-thunk*]
    (binding [pipeline/*call-count-thunk* (fn []
                                            (when old-thunk
                                              (old-thunk))
                                            (swap! call-count inc))]
      (f (fn [] @call-count)))))

Execute body, trackingthe number of database queries and statements executed. This number can be fetched at any time within body by calling function bound to call-count-fn-binding:

```clj (with-call-count [call-count] (select ...) (println "CALLS:" (call-count)) (insert! ...) (println "CALLS:" (call-count))) ;; -> CALLS: 1 ;; -> CALLS: 2 ```

(defmacro with-call-count
  [[call-count-fn-binding] & body]
  `(do-with-call-counts (^:once fn* [~call-count-fn-binding] ~@body)))
(s/fdef with-call-count
  :args (s/cat :bindings (s/spec (s/cat :call-count-binding symbol?))
               :body     (s/+ any?))
  :ret any?)
 
(ns toucan2.honeysql2
  (:require
   [better-cond.core :as b]
   [clojure.string :as str]
   [honey.sql :as hsql]
   [honey.sql.helpers :as hsql.helpers]
   [methodical.core :as m]
   [toucan2.instance :as instance]
   [toucan2.log :as log]
   [toucan2.model :as model]
   [toucan2.pipeline :as pipeline]
   [toucan2.query :as query]
   [toucan2.util :as u]))

Default global options to pass to [[honey.sql/format]].

(defonce  global-options
  (atom {:quoted true, :dialect :ansi, :quoted-snake true}))

Option override when to pass to [[honey.sql/format]].

(def ^:dynamic *options*
  nil)

Get combined Honey SQL options for building and compiling queries by merging [[global-options]] and [[options]].

(defn options
  []
  (merge @global-options
         *options*))

Building queries

(defn- fn-condition->honeysql-where-clause
  [k [f & args]]
  {:pre [(keyword? f) (seq args)]}
  (into [f k] args))

Something sequential like :id [:> 5] becomes [:> :id 5]. Other stuff like :id 5 just becomes [:= :id 5].

(defn condition->honeysql-where-clause
  [k v]
  ;; don't think there's any situation where `nil` on the LHS is on purpose and not a bug.
  {:pre [(some? k)]}
  (if (sequential? v)
    (fn-condition->honeysql-where-clause k v)
    [:= k v]))
(m/defmethod query/apply-kv-arg [#_model :default #_query clojure.lang.IPersistentMap #_k :default]
  "Apply key-value args to a Honey SQL 2 query map."
  [_model honeysql k v]
  (log/debugf "apply kv-arg %s %s" k v)
  (let [result (update honeysql :where (fn [existing-where]
                                         (:where (hsql.helpers/where existing-where
                                                                     (condition->honeysql-where-clause k v)))))]
    (log/tracef "=> %s" result)
    result))

Build an Honey SQL [table] or [table alias] (if the model has a [[toucan2.model/namespace]] form) for model for use in something like a :select clause.

(defn table-and-alias
  [model]
  (b/cond
    :let [table-id (keyword (model/table-name model))
          alias-id (model/namespace model)
          alias-id (when alias-id
                     (keyword alias-id))]
    alias-id
    [table-id alias-id]
    :else
    [table-id]))

Qualify the (plain keyword) columns in [model & columns] forms with the model table name, unless they are already qualified. This apparently doesn't hurt anything and prevents ambiguous column errors if you're joining another column or something like that. I wasn't going to put this in at first since I forgot it existed, but apparently Toucan 1 did it (despite not being adequately tested) so I decided to preserve this behavior going forward since I can't see any downsides to it.

In Honey SQL 2 I think using keyword namespaces is the preferred way to qualify stuff, so we'll go that route instead of using :a.b style qualification like we generated in Toucan 1.

(defn- qualified? [column]
  (or (namespace column)
      (str/includes? (name column) ".")))
(defn- maybe-qualify [column table]
  (cond
    (not (keyword? column)) column
    (qualified? column)     column
    :else                   (keyword (name table) (name column))))
(defn- maybe-qualify-columns [columns [table-id alias-id]]
  (let [table (or alias-id table-id)]
    (assert (keyword? table))
    (mapv #(maybe-qualify % table)
          columns)))

Should we splice in the default :select clause for this honeysql-query? Only splice in the default :select if we don't have :union, :union-all, or :select-distinct in the resolved query.

(defn include-default-select?
  [honeysql-query]
  (every? (fn [k]
            (not (contains? honeysql-query k)))
          [:union :union-all :select :select-distinct]))

Should we splice in the default :from clause for this honeysql-query? Only splice in the default :from if we don't have :union or :union-all in the resolved query. It doesn't make sense to do a x UNION y query and then include FROM as well.

(defn- include-default-from?
  [honeysql-query]
  (every? (fn [k]
            (not (contains? honeysql-query k)))
          [:union :union-all]))
(m/defmethod pipeline/build [#_query-type     :default
                             #_model          :default
                             #_resolved-query clojure.lang.IPersistentMap]
  "Base map backend implementation. Applies the `:kv-args` in `parsed-args` using [[query/apply-kv-args]], and ignores
  other parsed args."
  [query-type model {:keys [kv-args], :as parsed-args} m]
  (let [m (query/apply-kv-args model m kv-args)]
    (next-method query-type model (dissoc parsed-args :kv-args) m)))
(m/defmethod pipeline/build [#_query-type :toucan.query-type/select.*
                             #_model      :default
                             #_query      clojure.lang.IPersistentMap]
  "Build a Honey SQL 2 SELECT query."
  [query-type model {:keys [columns], :as parsed-args} resolved-query]
  (log/debugf "Building SELECT query for %s with columns %s" model columns)
  (let [parsed-args    (dissoc parsed-args :columns)
        table+alias    (table-and-alias model)
        resolved-query (-> (merge
                            (when (include-default-select? resolved-query)
                              {:select (or (some-> (not-empty columns) (maybe-qualify-columns table+alias))
                                           [:*])})
                            (when (and model
                                       (include-default-from? resolved-query))
                              {:from [table+alias]})
                            resolved-query)
                           (with-meta (meta resolved-query)))]
    (log/debugf "=> %s" resolved-query)
    (next-method query-type model parsed-args resolved-query)))
(m/defmethod pipeline/build [#_query-type :toucan.query-type/select.count
                             #_model      :default
                             #_query      clojure.lang.IPersistentMap]
  "Build an efficient `count(*)` query to power [[toucan2.select/count]]."
  [query-type model parsed-args resolved-query]
  (let [parsed-args (assoc parsed-args :columns [[:%count.* :count]])]
    (next-method query-type model parsed-args resolved-query)))
(m/defmethod pipeline/build [#_query-type :toucan.query-type/select.exists
                             #_model      :default
                             #_query      clojure.lang.IPersistentMap]
  "Build an efficient query like `SELECT exists(SELECT 1 FROM ...)` query to power [[toucan2.select/exists?]]."
  [query-type model parsed-args resolved-query]
  (let [parsed-args (assoc parsed-args :columns [[[:inline 1]]])
        subselect   (next-method query-type model parsed-args resolved-query)]
    {:select [[[:exists subselect] :exists]]}))
(defn- empty-insert [_model dialect]
  (if (#{:mysql :mariadb} dialect)
    {:columns []
     :values  [[]]}
    {:values :default}))
(m/defmethod pipeline/build [#_query-type :toucan.query-type/insert.*
                             #_model      :default
                             #_query      clojure.lang.IPersistentMap]
  "Build a Honey SQL 2 INSERT query.
  if `rows` is just a single empty row then insert it with
  ```sql
  INSERT INTO table DEFAULT VALUES
  ```
  (Postgres/H2/etc.)
  or
  ```sql
  INSERT INTO table () VALUES ()
  ```
  (MySQL/MariaDB)"
  [query-type model parsed-args resolved-query]
  (log/debugf "Building INSERT query for %s" model)
  (let [rows        (some (comp not-empty :rows) [parsed-args resolved-query])
        built-query (-> (merge {:insert-into [(keyword (model/table-name model))]}
                               (if (= rows [{}])
                                 (empty-insert model (:dialect (options)))
                                 {:values (map (partial instance/instance model)
                                               rows)}))
                        (with-meta (meta resolved-query)))]
    (log/debugf "=> %s" built-query)
    ;; rows is only added so we can get the default methods' no-op logic if there are no rows at all.
    (next-method query-type model (assoc parsed-args :rows rows) built-query)))
(m/defmethod pipeline/build [#_query-type :toucan.query-type/update.*
                             #_model      :default
                             #_query      clojure.lang.IPersistentMap]
  "Build a Honey SQL 2 UPDATE query."
  [query-type model {:keys [kv-args changes], :as parsed-args} conditions-map]
  (log/debugf "Building UPDATE query for %s" model)
  (let [parsed-args (assoc parsed-args :kv-args (merge kv-args conditions-map))
        built-query (-> {:update (table-and-alias model)
                         :set    changes}
                        (with-meta (meta conditions-map)))]
    (log/debugf "=> %s" built-query)
    ;; `:changes` are added to `parsed-args` so we can get the no-op behavior in the default method.
    (next-method query-type model (assoc parsed-args :changes changes) built-query)))

For building a SELECT query using the args passed to something like [[toucan2.update/update!]]. This is needed to implement [[toucan2.tools.before-update]]. The main syntax difference is a map 'resolved-query' is supposed to be treated as a conditions map for update instead of as a raw Honey SQL query.

TODO -- a conditions map should probably not be given a type of clojure.lang.IPersistentMap -- conditions maps should be a separate map backend I think.

(m/defmethod pipeline/build [#_query-type :toucan.query-type/select.instances.from-update
                             #_model      :default
                             #_query      clojure.lang.IPersistentMap]
  "Treat the resolved query as a conditions map but otherwise behave the same as the `:toucan.query-type/select.instances`
  impl."
  [query-type model parsed-args conditions-map]
  (next-method query-type model (update parsed-args :kv-args #(merge % conditions-map)) {}))

Build the correct DELETE ... FROM ... or DELETE FROM ... Honey SQL for the current dialect (see docstring for build method below).

(defn- delete-from
  [table+alias dialect]
  (if (= (count table+alias) 1)
    {:delete-from table+alias}
    (let [[_table table-alias] table+alias]
      (if (#{:mysql :mariadb} dialect)
        {:delete table-alias
         :from   [table+alias]}
        {:delete-from table+alias}))))
(m/defmethod pipeline/build [#_query-type :toucan.query-type/delete.*
                             #_model      :default
                             #_query      clojure.lang.IPersistentMap]
  "Build a Honey SQL 2 DELETE query.
  If the table for `model` should not be aliased (i.e., [[toucan2.model/namespace]] returns `nil`), builds a query that
  compiles to something like:
  ```sql
  DELETE FROM my_table
  WHERE ...
  ```
  If the table is aliased, this looks like
  ```sql
  DELETE FROM my_table AS t1
  WHERE ...
  ```
  for Postgres/H2/etc., or like
  ```sql
  DELETE t1
  FROM my_table AS t1
  WHERE ...
  ```
  for MySQL/MariaDB. MySQL/MariaDB does not seem to support aliases in `DELETE FROM`, so we need to use this alternative
  syntax; H2 doesn't support it however. So it has to be compiled differently based on the DB."
  [query-type model parsed-args resolved-query]
  (log/debugf "Building DELETE query for %s" model)
  (let [built-query (-> (merge (delete-from (table-and-alias model) (:dialect (options)))
                               resolved-query)
                        (with-meta (meta resolved-query)))]
    (log/debugf "=> %s" built-query)
    (next-method query-type model parsed-args built-query)))

Query compilation

(m/defmethod pipeline/compile [#_query-type :default
                               #_model      :default
                               #_query      clojure.lang.IPersistentMap]
  "Compile a Honey SQL 2 map to [sql & args]."
  [query-type model honeysql]
  (let [options-map (options)
        _           (log/debugf "Compiling Honey SQL 2 with options %s" options-map)
        sql-args    (u/try-with-error-context ["compile Honey SQL to SQL" {::honeysql honeysql, ::options-map options-map}]
                      (hsql/format honeysql options-map))]
    (log/debugf "=> %s" sql-args)
    (pipeline/compile query-type model sql-args)))
 

Implementation of [[insert!]].

The code for building an INSERT query as Honey SQL lives in [[toucan2.honeysql2]]

(ns toucan2.insert
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.log :as log]
   [toucan2.pipeline :as pipeline]
   [toucan2.query :as query]))
(s/def ::kv-args
  (s/+ (s/cat
        :k keyword?
        :v any?)))
(s/def ::args.rows
  (s/alt :nil               nil?
         :single-row-map    map?
         :multiple-row-maps (s/spec (s/* map?))
         :kv-pairs          ::kv-args
         :columns-rows      (s/cat :columns (s/spec (s/+ keyword?))
                                   :rows    (s/spec (s/+ sequential?)))))
(s/def ::args
  (s/cat
   :connectable       ::query/default-args.connectable
   :modelable         ::query/default-args.modelable
   :rows-or-queryable (s/alt :rows      ::args.rows
                             :queryable some?)))
(m/defmethod query/parse-args :toucan.query-type/insert.*
  "Default args parsing method for [[toucan2.insert/insert!]]. Uses the spec `:toucan2.insert/args`."
  [query-type unparsed-args]
  (let [parsed                               (query/parse-args-with-spec query-type ::args unparsed-args)
        [rows-queryable-type rows-queryable] (:rows-or-queryable parsed)
        parsed                               (select-keys parsed [:modelable :columns :connectable])]
    (case rows-queryable-type
      :queryable
      (assoc parsed :queryable rows-queryable)
      :rows
      (assoc parsed :rows (let [[rows-type x] rows-queryable]
                            (condp = rows-type
                              :nil               nil
                              :single-row-map    [x]
                              :multiple-row-maps x
                              :kv-pairs          [(into {} (map (juxt :k :v)) x)]
                              :columns-rows      (let [{:keys [columns rows]} x]
                                                   (map (partial zipmap columns)
                                                        rows))))))))
(defn- can-skip-insert? [parsed-args resolved-query]
  (and (empty? (:rows parsed-args))
       ;; don't try to optimize out stuff like identity query.
       (or (and (map? resolved-query)
                (empty? (:rows resolved-query)))
           (nil? resolved-query))))
(m/defmethod pipeline/build [#_query-type     :toucan.query-type/insert.*
                             #_model          :default
                             #_resolved-query :default]
  "Default INSERT query method. No-ops if there are no `:rows` to insert in either `parsed-args` or `resolved-query`."
  [query-type model parsed-args resolved-query]
  (let [rows (some (comp not-empty :rows) [parsed-args resolved-query])]
    (if (can-skip-insert? parsed-args resolved-query)
      (do
        (log/debugf "Query has no changes, skipping update")
        ::pipeline/no-op)
      (do
        (log/debugf "Inserting %s rows into %s" (if (seq rows) (count rows) "?") model)
        (next-method query-type model parsed-args resolved-query)))))

Insert a row or rows into the database. Returns the number of rows inserted.

This function is pretty flexible in what it accepts:

Insert a single row with key-value args:

```clj (t2/insert! :models/venues :name "Grant & Green", :category "bar") ```

Insert a single row as a map:

```clj (t2/insert! :models/venues {:name "Grant & Green", :category "bar"}) ```

Insert multiple row maps:

```clj (t2/insert! :models/venues [{:name "Grant & Green", :category "bar"} {:name "Savoy Tivoli", :category "bar"}]) ```

Insert rows with a vector of column names and a vector of value maps:

```clj (t2/insert! :models/venues [:name :category] [["Grant & Green" "bar"] ["Savoy Tivoli" "bar"]]) ```

As with other Toucan 2 functions, you can optionally pass a connectable if you pass :conn as the first arg. Refer to the :toucan2.insert/args spec for the complete syntax.

Named connectables can also be used to define the rows:

```clj (t2/define-named-query ::named-rows {:rows [{:name "Grant & Green", :category "bar"} {:name "North Beach Cantina", :category "restaurant"}]})

(t2/insert! :models/venues ::named-rows) ```

(defn insert!
  {:arglists '([modelable row-or-rows-or-queryable]
               [modelable k v & more]
               [modelable columns row-vectors]
               [:conn connectable modelable row-or-rows]
               [:conn connectable modelable k v & more]
               [:conn connectable modelable columns row-vectors])}
  [& unparsed-args]
  (pipeline/transduce-unparsed-with-default-rf :toucan.query-type/insert.update-count unparsed-args))

Like [[insert!]], but returns a vector of the primary keys of the newly inserted rows rather than the number of rows inserted. The primary keys are determined by [[model/primary-keys]]. For models with a single primary key, this returns a vector of single values, e.g. [1 2] if the primary key is :id and you've inserted rows 1 and 2; for composite primary keys this returns a vector of tuples where each tuple has the value of corresponding primary key as returned by [[model/primary-keys]], e.g. for composite PK [:id :name] you might get [[1 "Cam"] [2 "Sam"]].

(defn insert-returning-pks!
  {:arglists '([modelable row-or-rows-or-queryable]
               [modelable k v & more]
               [modelable columns row-vectors]
               [:conn connectable modelable row-or-rows]
               [:conn connectable modelable k v & more]
               [:conn connectable modelable columns row-vectors])}
  [& unparsed-args]
  (pipeline/transduce-unparsed-with-default-rf :toucan.query-type/insert.pks unparsed-args))

Like [[insert-returning-pks!]], but for one-row insertions. For models with a single primary key, this returns just the new primary key as a scalar value (e.g. 1). For models with a composite primary key, it will return a single tuple as determined by [[model/primary-keys]] (e.g. [1 "Cam"]).

(defn insert-returning-pk!
  {:arglists '([modelable row-or-rows-or-queryable]
               [modelable k v & more]
               [modelable columns row-vectors]
               [:conn connectable modelable row-or-rows]
               [:conn connectable modelable k v & more]
               [:conn connectable modelable columns row-vectors])}
  [& unparsed-args]
  (first (apply insert-returning-pks! unparsed-args)))

Like [[insert!]], but returns a vector of maps representing the inserted objects.

(defn insert-returning-instances!
  {:arglists '([modelable-columns row-or-rows-or-queryable]
               [modelable-columns k v & more]
               [modelable-columns columns row-vectors]
               [:conn connectable modelable-columns row-or-rows]
               [:conn connectable modelable-columns k v & more]
               [:conn connectable modelable-columns columns row-vectors])}
  [& unparsed-args]
  (pipeline/transduce-unparsed-with-default-rf :toucan.query-type/insert.instances unparsed-args))

Like [[insert-returning-instances!]], but for one-row insertions. Returns the inserted object as a map.

(defn insert-returning-instance!
  {:arglists '([modelable row-or-rows-or-queryable]
               [modelable k v & more]
               [modelable columns row-vectors]
               [:conn connectable modelable row-or-rows]
               [:conn connectable modelable k v & more]
               [:conn connectable modelable columns row-vectors])}
  [& unparsed-args]
  (first (apply insert-returning-instances! unparsed-args)))
 

Toucan 2 instances are a custom map type that does two things regular maps do not do:

  1. They are associated with a particular model; [[toucan2.protocols/model]] can be used to get it. This is usually set when the instance comes out of that database.

  2. They track their [[toucan2.protocols/original]] version when they come out of the application database. This can in turn be used to calculate the [[toucan2.protocols/changes]] that have been made, which powers features like [[toucan2.save/save!]].

Normally a Toucan instance is considered equal to a plain map with the same current (via [[toucan2.protocols/current]]) value. It is considered equal to other instances if they have the same current value and their model is the same.

(ns toucan2.instance
  (:refer-clojure :exclude [instance?])
  (:require
   clojure.core.protocols
   [potemkin :as p]
   [pretty.core :as pretty]
   [toucan2.protocols :as protocols]
   [toucan2.realize :as realize]
   [toucan2.util :as u]))
(set! *warn-on-reflection* true)

For debugging purposes: whether to print the original version of an instance, in addition to the current version, when printing an [[instance]].

(def ^:dynamic *print-original*
  false)

True if x is a Toucan2 instance, i.e. a toucan2.instance.Instance or some other class that satisfies the correct interfaces.

Toucan instances need to implement [[protocols/IModel]], [[protocols/IWithModel]], and [[protocols/IRecordChanges]].

(defn instance?
  [x]
  (and (clojure.core/instance? toucan2.protocols.IModel x)
       (clojure.core/instance? toucan2.protocols.IWithModel x)
       (clojure.core/instance? toucan2.protocols.IRecordChanges x)))

True if x is a Toucan2 instance, and its [[protocols/model]] isa? model.

```clj (instance-of? ::bird (instance ::toucan {})) ; -> true (instance-of? ::toucan (instance ::bird {})) ; -> false ```

(defn instance-of?
  [model x]
  (and (instance? x)
       (isa? (protocols/model x) model)))

Changes between orig and m. Unlike [[clojure.data/diff]], this is a shallow diff. Only columns that are added or modified should be returned.

(defn- changes*
  [original current]
  (if (or (not (map? original))
          (not (map? current)))
    current
    (not-empty
     (into
      (empty original)
      (map (fn [k]
             (let [original-value (get original k ::not-found)
                   current-value  (get current k)]
               (when-not (= original-value current-value)
                 [k current-value]))))
      (keys current)))))
(declare ->TransientInstance)
(p/def-map-type Instance [model
                          ^clojure.lang.IPersistentMap orig
                          ^clojure.lang.IPersistentMap m
                          mta]
  (get [_ k default-value]
    (get m k default-value))
  (assoc [this k v]
    (let [m' (assoc m k v)]
      (if (identical? m m')
        this
        (Instance. model orig m' mta))))
  (dissoc [this k]
    (let [m' (dissoc m k)]
      (if (identical? m m')
        this
        (Instance. model orig m' mta))))
  (keys [_this]
    (keys m))
  (meta [_this]
    mta)
  (with-meta [this new-meta]
    (if (identical? mta new-meta)
      this
      (Instance. model orig m new-meta)))
  clojure.lang.IPersistentCollection
  (cons [this o]
    (cond
      (map? o)
      (reduce #(apply assoc %1 %2) this o)
      (clojure.core/instance? java.util.Map o)
      (reduce
       #(apply assoc %1 %2)
       this
       (into {} o))
      :else
      (if-let [[k v] (seq o)]
        (assoc this k v)
        this)))
  (equiv [_this another]
    (cond
      (clojure.core/instance? toucan2.protocols.IModel another)
      (and (= model (protocols/model another))
           (= m another))
      (map? another)
      (= m another)
      :else
      false))
  (empty [_this]
    (Instance. model (empty orig) (empty m) mta))
  java.util.Map
  (containsKey [_this k]
    (.containsKey m k))
  clojure.lang.IEditableCollection
  (asTransient [_this]
    (->TransientInstance model (transient m) mta))
  clojure.lang.IKVReduce
  (kvreduce [_this f init]
    (clojure.core.protocols/kv-reduce m f init))
  protocols/IModel
  (protocols/model [_this]
    model)
  protocols/IWithModel
  (with-model [this new-model]
    (if (= model new-model)
      this
      (Instance. new-model orig m mta)))
  protocols/IRecordChanges
  (original [_this]
    orig)
  (with-original [this new-original]
    (if (identical? orig new-original)
      this
      (let [new-original (if (nil? new-original)
                           {}
                           new-original)]
        (assert (map? new-original))
        (Instance. model new-original m mta))))
  (current [_this]
    m)
  (with-current [this new-current]
    (if (identical? m new-current)
      this
      (let [new-current (if (nil? new-current)
                          {}
                          new-current)]
        (assert (map? new-current))
        (Instance. model orig new-current mta))))
  (changes [_this]
    (not-empty (changes* orig m)))
  protocols/IDispatchValue
  (dispatch-value [_this]
    (protocols/dispatch-value model))
  realize/Realize
  (realize [_this]
    (if (identical? orig m)
      (let [m (realize/realize m)]
        (Instance. model m m mta))
      (Instance. model (realize/realize orig) (realize/realize m) mta)))
  pretty/PrettyPrintable
  (pretty [_this]
    (if *print-original*
      (list `instance model (symbol "#_") orig m)
      (list `instance model m))))
(deftype ^:no-doc TransientInstance [model ^clojure.lang.ITransientMap m mta]
  clojure.lang.ITransientMap
  (conj [this v]
    (let [m' (conj! m v)]
      (if (identical? m m')
        this
        (TransientInstance. model m' mta))))
  (persistent [_this]
    (let [m (persistent! m)]
      (Instance. model m m mta)))
  (assoc [this k v]
    (let [m' (assoc! m k v)]
      (if (identical? m m')
        this
        (TransientInstance. model m' mta))))
  (without [this k]
    (let [m' (dissoc! m k)]
      (if (identical? m m')
        this
        (TransientInstance. model m' mta))))
  (valAt [_this k]
    (.valAt m k))
  (valAt [_this k not-found]
    (.valAt m k not-found))
  (count [_this]
    (count m))
  clojure.lang.Associative
  (containsKey [_this k]
    (contains? m k))
  pretty/PrettyPrintable
  (pretty [_this]
    (list `->TransientInstance model m mta)))

Create a new Toucan 2 instance. See the namespace docstring for [[toucan2.instance]] for more information about what a Toucan 2 instance is.

This function has several arities:

  • With no args, creates an empty instance with its model set to nil

  • With one arg, creates an empty instance of a model.

  • With two args, creates an instance of a model from an existing map. This is optimized: if the map is already an instance of the model, returns the map as-is.

  • With three or more args, creates an instance of a model with key-value args.

(defn instance
  (^toucan2.instance.Instance []
   (instance nil))
  (^toucan2.instance.Instance [model]
   (instance model {}))
  (^toucan2.instance.Instance [model m]
   (assert ((some-fn map? nil?) m)
           (format "Expected a map or nil, got ^%s %s" (.getCanonicalName (class m)) (pr-str m)))
   (cond
     ;; optimization: if `m` is already an instance with `model` return it as-is.
     (and (instance? m)
          (= (protocols/model m) model))
     m
     ;; DISABLED FOR NOW BECAUSE MAYBE THE OTHER MODEL HAS A DIFFERENT UNDERLYING EMPTY MAP OR KEY TRANSFORM
     ;;
     ;; ;; optimization 2: if `m` is an instance of something else use [[protocols/with-model]]
     ;; (instance? m)
     ;; (protocols/with-model m model)
     ;; Strip any customizations, e.g. a different underlying empty map or key transform.
     (u/custom-map? m)
     (let [m* (into {} m)]
       (->Instance model m* m* (meta m)))
     :else
     (->Instance model m m (meta m))))
  (^toucan2.instance.Instance [model k v & more]
   (let [m (into {} (partition-all 2) (list* k v more))]
     (instance model m))))
(extend-protocol protocols/IWithModel
  nil
  (with-model [_this model]
    (instance model))

  clojure.lang.IPersistentMap
  (with-model [m model]
    (instance model m)))

Return a copy of an-instance with its original value set to its current value, discarding the previous original value. No-ops if an-instance is not a Toucan 2 instance.

(defn reset-original
  [an-instance]
  (if (instance? an-instance)
    (protocols/with-original an-instance (protocols/current an-instance))
    an-instance))

TODO -- should we have a revert-changes helper function as well?

Applies f directly to the underlying original map of an-instance. No-ops if an-instance is not an [[Instance]].

(defn update-original
  ([an-instance f]
   (if (instance? an-instance)
     (protocols/with-original an-instance (f (protocols/original an-instance)))
     an-instance))
  ([an-instance f & args]
   (if (instance? an-instance)
     (protocols/with-original an-instance (apply f (protocols/original an-instance) args))
     an-instance)))

Applies f directly to the underlying current map of an-instance; useful if you need to operate on it directly. Acts like regular (apply f instance args) if an-instance is not an [[Instance]].

(defn update-current
  ([an-instance f]
   (protocols/with-current an-instance (f (protocols/current an-instance))))
  ([an-instance f & args]
   (protocols/with-current an-instance (apply f (protocols/current an-instance) args))))

Like (apply f instance args), but affects both the original map and current map of an-instance rather than just the current map. Acts like regular (apply f instance args) if instance is not an Instance.

f is applied directly to the underlying original and current maps of instance itself. f is only applied once if original and current are currently the same object (i.e., the new original and current will also be the same object). If current and original are not the same object, f is applied twice.

(defn update-original-and-current
  ([an-instance f]
   (if (identical? (protocols/original an-instance) (protocols/current an-instance))
     (reset-original (update-current an-instance f))
     (as-> an-instance an-instance
       (update-original an-instance f)
       (update-current  an-instance f))))
  ([an-instance f & args]
   (if (identical? (protocols/original an-instance) (protocols/current an-instance))
     (reset-original (apply update-current an-instance f args))
     (as-> an-instance an-instance
       (apply update-original an-instance f args)
       (apply update-current  an-instance f args)))))
 
(ns toucan2.jdbc
  (:require
   [toucan2.jdbc.connection :as jdbc.conn]
   [toucan2.jdbc.pipeline :as jdbc.pipeline]
   [toucan2.protocols :as protocols]))
(comment jdbc.conn/keep-me
         jdbc.pipeline/keep-me)
(set! *warn-on-reflection* true)

load the miscellaneous integrations

(defn- class-for-name ^Class [^String class-name]
  (try
    (Class/forName class-name)
    (catch Throwable _)))
(when (class-for-name "org.postgresql.jdbc.PgConnection")
  (require 'toucan2.jdbc.postgres))
(when (some class-for-name ["org.mariadb.jdbc.Connection"
                           "org.mariadb.jdbc.MariaDbConnection"
                           "com.mysql.cj.MysqlConnection"])
  (require 'toucan2.jdbc.mysql-mariadb))

c3p0 and Hikari integration: when we encounter a wrapped connection pool connection, dispatch off of the class of connection it wraps

(doseq [pool-connection-class-name ["com.mchange.v2.c3p0.impl.NewProxyConnection"
                                    "com.zaxxer.hikari.pool.HikariProxyConnection"]]
  (when-let [pool-connection-class (class-for-name pool-connection-class-name)]
    (extend pool-connection-class
      protocols/IDispatchValue
      {:dispatch-value (fn [^java.sql.Wrapper conn]
                         (try
                           (protocols/dispatch-value (.unwrap conn java.sql.Connection))
                           (catch Throwable _
                             pool-connection-class)))})))
 
(ns toucan2.jdbc.connection
  (:require
   [methodical.core :as m]
   [next.jdbc]
   [next.jdbc.transaction]
   [toucan2.connection :as conn]
   [toucan2.log :as log]))
(set! *warn-on-reflection* true)
(m/defmethod conn/do-with-connection java.sql.Connection
  [conn f]
  (f conn))
(m/defmethod conn/do-with-connection javax.sql.DataSource
  [^javax.sql.DataSource data-source f]
  (with-open [conn (.getConnection data-source)]
    (f conn)))
(m/defmethod conn/do-with-connection clojure.lang.IPersistentMap
  "Implementation for map connectables. Treats them as a `clojure.java.jdbc`-style connection spec map, converting them to
  a `java.sql.DataSource` with [[next.jdbc/get-datasource]]."
  [m f]
  (conn/do-with-connection (next.jdbc/get-datasource m) f))

for record types that implement DataSource, prefer the DataSource impl over the map impl.

(m/prefer-method! #'conn/do-with-connection javax.sql.DataSource clojure.lang.IPersistentMap)

jdbc

(m/defmethod conn/do-with-connection-string 
  "Implementation of `do-with-connection-string` (and thus [[do-with-connection]]) for all strings starting with `jdbc:`.
  Calls `java.sql.DriverManager/getConnection` on the connection string."
  [^String connection-string f]
  (with-open [conn (java.sql.DriverManager/getConnection connection-string)]
    (f conn)))
(m/defmethod conn/do-with-transaction java.sql.Connection
  [^java.sql.Connection conn options f]
  (let [nested-tx-rule (get options :nested-transaction-rule next.jdbc.transaction/*nested-tx*)
        options        (dissoc options :nested-transaction-rule)]
    (log/debugf "do with JDBC transaction (nested rule: %s) with options %s" nested-tx-rule options)
    (binding [next.jdbc.transaction/*nested-tx* nested-tx-rule]
      (next.jdbc/with-transaction [t-conn conn options]
        (f t-conn)))))
 

MySQL and MariaDB integration (mostly workarounds for broken stuff).

(ns toucan2.jdbc.mysql-mariadb
  (:require
   [methodical.core :as m]
   [toucan2.jdbc.options :as jdbc.options]
   [toucan2.jdbc.read :as jdbc.read]
   [toucan2.jdbc.result-set :as jdbc.rs]
   [toucan2.log :as log]
   [toucan2.model :as model]
   [toucan2.pipeline :as pipeline]
   [toucan2.util :as u])
  (:import
   (java.sql ResultSet ResultSetMetaData Types)))
(set! *warn-on-reflection* true)

TODO -- need the MySQL class here too.

(doseq [^String connection-class-name ["org.mariadb.jdbc.Connection"
                                       "org.mariadb.jdbc.MariaDbConnection"
                                       "com.mysql.cj.MysqlConnection"]]
  (when-let [connection-class (try
                                (Class/forName connection-class-name)
                                (catch Throwable _))]
    (derive connection-class ::connection)))
(m/defmethod jdbc.read/read-column-thunk [#_conn  ::connection
                                          #_model :default
                                          #_type  Types/TIMESTAMP]
  "MySQL/MariaDB `timestamp` is normalized to UTC, so return it as an `OffsetDateTime` rather than a `LocalDateTime`.
  `datetime` columns should be returned as `LocalDateTime`. Both `timestamp` and `datetime` seem to come back as
  `java.sql.Types/TIMESTAMP`, so check the actual database column type name so we can fetch objects as the correct
  class."
  [_conn _model ^ResultSet rset ^ResultSetMetaData rsmeta ^Long i]
  (let [^Class klass (if (= (u/lower-case-en (.getColumnTypeName rsmeta i)) "timestamp")
                       java.time.OffsetDateTime
                       java.time.LocalDateTime)]
    (jdbc.read/get-object-of-class-thunk rset i klass)))
(m/prefer-method! #'jdbc.read/read-column-thunk
                  [::connection :default Types/TIMESTAMP]
                  [java.sql.Connection :default Types/TIMESTAMP])

INSERT RETURNGENERATEDKEYS with explicit non-integer PK value workaround

(m/defmethod pipeline/transduce-execute-with-connection [#_conn       ::connection
                                                         #_query-type :toucan.query-type/insert.pks
                                                         #_model      :default]
  "Apparently `RETURN_GENERATED_KEYS` doesn't work for MySQL/MariaDB if:
  1. Values for the primary key are specified in the INSERT itself, *and*
  2. The primary key is not an integer.
  So to work around this we will look at the rows we're inserting: if every rows specifies the primary key
  column(s) (including `nil` values), we'll transduce those specified values rather than what JDBC returns.
  This seems like it won't work if these values were arbitrary Honey SQL expressions. I suppose we could work around
  THAT problem by running the primary key values thru another SELECT query... but that just seems like too much. I guess
  we can cross that bridge when we get there."
  [rf conn query-type model compiled-query]
  (let [rows                 (:rows pipeline/*parsed-args*)
        pks                  (model/primary-keys model)
        return-pks-directly? (and (seq rows)
                                  (every? (fn [row]
                                            (every? (fn [k]
                                                      (contains? row k))
                                                    pks))
                                          rows))]
    (if return-pks-directly?
      (do
        (pipeline/transduce-execute-with-connection (pipeline/default-rf :toucan.query-type/insert.update-count)
                                                    conn
                                                    :toucan.query-type/insert.update-count
                                                    model
                                                    compiled-query)
        (transduce
         (map (model/select-pks-fn model))
         rf
         rows))
      (next-method rf conn query-type model compiled-query))))
(m/prefer-method! #'pipeline/transduce-execute-with-connection
                  [::connection :toucan.query-type/insert.pks :default]
                  [java.sql.Connection :toucan.result-type/pks :default])

UPDATE returning PKs workaround

MySQL and MariaDB don't support returning PKs for UPDATE, so we'll have to hack it as follows:

  1. Rework the original query to be a SELECT, run it, and record the matching PKs somewhere. Currently only supported for queries we can manipulate e.g. Honey SQL

  2. Run the original UPDATE query

  3. Return the PKs from the rewritten SELECT query

(m/defmethod pipeline/transduce-execute-with-connection [#_connection ::connection
                                                         #_query-type :toucan.query-type/update.pks
                                                         #_model      :default]
  "MySQL and MariaDB don't support returning PKs for UPDATE. Execute a SELECT query to capture the PKs of the rows that
  will be affected BEFORE performing the UPDATE. We need to capture PKs for both `:toucan.query-type/update.pks` and for
  `:toucan.query-type/update.instances`, since ultimately the latter is implemented on top of the former."
  [original-rf conn _query-type model sql-args]
  ;; if for some reason we've already captured PKs, don't do it again.
  (let [conditions-map pipeline/*resolved-query*
        _              (log/debugf "update-returning-pks workaround: doing SELECT with conditions %s"
                                   conditions-map)
        parsed-args    (update pipeline/*parsed-args* :kv-args merge conditions-map)
        select-rf      (pipeline/with-init conj [])
        xform          (map (model/select-pks-fn model))
        pks            (pipeline/transduce-query (xform select-rf)
                                                 :toucan.query-type/select.instances.fns
                                                 model
                                                 parsed-args
                                                 {})]
    (log/debugf "update-returning-pks workaround: got PKs %s" pks)
    (let [update-rf (pipeline/default-rf :toucan.query-type/update.update-count)]
      (log/debugf "update-returning-pks workaround: performing original UPDATE")
      (pipeline/transduce-execute-with-connection update-rf conn :toucan.query-type/update.update-count model sql-args))
    (log/debugf "update-returning-pks workaround: transducing PKs with original reducing function")
    (transduce
     identity
     original-rf
     pks)))
(m/prefer-method! #'pipeline/transduce-execute-with-connection
                  [::connection :toucan.query-type/update.pks :default]
                  [java.sql.Connection :toucan.result-type/pks :default])

Builder function

(m/defmethod jdbc.rs/builder-fn [::connection :default]
  "This is an icky hack for MariaDB/MySQL. Inserted rows come back with the newly inserted ID as `:insert-id` rather than
  the actual name of the primary key column. So tweak the `:label-fn` we pass to `next.jdbc` to rename `:insert-id` to
  the actual PK name we'd expect. This only works for tables with a single-column PK."
  [conn model rset opts]
  (let [opts               (jdbc.options/merge-options opts)
        label-fn           (get opts :label-fn name)
        model-pks          (model/primary-keys model)
        insert-id-label-fn (if (= (count model-pks) 1)
                             (fn [label]
                               (if (= label "insert_id")
                                 (let [pk (first model-pks)
                                       ;; there is some weirdness afoot. If we return a keyword without a namespace
                                       ;; then `next.jdbc` seems to qualify it regardless of whether the
                                       ;; `:qualifier-fn` returns `nil` or not -- so a PK like `:id` gets returned
                                       ;; as `(keyword  "id")`. But that doesn't happen if the label function
                                       ;; returns a String.
                                       ;;
                                       ;; It seems like returning a string is the preferred thing to do, but in some
                                       ;; cases [[model/primary-keys]] returns a namespaced keyword, and we want to
                                       ;; preserve that namespace; `next.jdbc` does not try to change keywords that
                                       ;; already have namespaces.
                                       ;;
                                       ;; So return the PK name as a keyword if the PK keyword is namespaced;
                                       ;; otherwise return a string.
                                       pk (if (namespace pk)
                                            pk
                                            (name pk))]
                                   (log/debugf "MySQL/MariaDB inserted ID workaround: fetching insert_id as %s" pk)
                                   pk)
                                 label))
                             identity)
        label-fn'          (comp label-fn insert-id-label-fn)]
    (next-method conn model rset (assoc opts :label-fn label-fn'))))
 
(ns toucan2.jdbc.options
  (:require
   [toucan2.util :as u]))

Default options automatically passed to all next.jdbc queries and builder functions. This is stored as an atom; reset! or swap! it to define other default options.

(defonce  global-options
  (atom {:label-fn u/lower-case-en}))

Options to pass to next.jdbc when executing queries or statements. Overrides the [[global-options]].

(def ^:dynamic *options*
  nil)

Merge maps of next.jdbc options together. extra-options are ones passed in as part of the query execution pipeline and override [[options]], which in turn override the default [[global-options]].

(defn merge-options
  [extra-options]
  (merge @global-options
         *options*
         extra-options))
 
(ns toucan2.jdbc.pipeline
  (:require
   [methodical.core :as m]
   [toucan2.jdbc.query :as jdbc.query]
   [toucan2.model :as model]
   [toucan2.pipeline :as pipeline]
   [toucan2.types :as types]))
(m/defmethod pipeline/transduce-execute-with-connection [#_connection java.sql.Connection
                                                         #_query-type :default
                                                         #_model      :default]
  "Default impl for the JDBC query execution backend."
  [rf conn query-type model sql-args]
  {:pre [(sequential? sql-args) (string? (first sql-args))]}
  ;; `:return-keys` is passed in this way instead of binding a dynamic var because we don't want any additional queries
  ;; happening inside of the `rf` to return keys or whatever.
  (let [extra-options (when (isa? query-type :toucan.result-type/pks)
                        {:return-keys true})
        result        (jdbc.query/reduce-jdbc-query rf (rf) conn model sql-args extra-options)]
    (rf result)))
(m/defmethod pipeline/transduce-execute-with-connection [#_connection java.sql.Connection
                                                         #_query-type :toucan.result-type/pks
                                                         #_model      :default]
  "JDBC query execution backend for executing queries that return PKs (`:toucan.result-type/pks`).
  Applies transducer to call [[toucan2.model/select-pks-fn]] on each result row."
  [rf conn query-type model sql-args]
  (let [xform (map (model/select-pks-fn model))]
    (next-method (xform rf) conn query-type model sql-args)))
(defn- transduce-instances-from-pks
  [rf model columns pks]
  ;; make sure [[toucan2.select]] is loaded so we get the impls for `:toucan.query-type/select.instances`
  (when-not (contains? (loaded-libs) 'toucan2.select)
    (locking clojure.lang.RT/REQUIRE_LOCK
      (require 'toucan2.select)))
  (if (empty? pks)
    []
    (let [kv-args     {:toucan/pk [:in pks]}
          parsed-args {:columns columns
                       :kv-args kv-args}]
      (pipeline/transduce-query rf :toucan.query-type/select.instances-from-pks model parsed-args {}))))
(derive ::DML-queries-returning-instances :toucan.result-type/instances)
(doseq [query-type [:toucan.query-type/delete.instances
                    :toucan.query-type/update.instances
                    :toucan.query-type/insert.instances]]
  (derive query-type ::DML-queries-returning-instances))
(m/defmethod pipeline/transduce-execute-with-connection [#_connection java.sql.Connection
                                                         #_query-type ::DML-queries-returning-instances
                                                         #_model      :default]
  "DML queries like `UPDATE` or `INSERT` don't usually support returning instances, at least not with JDBC. So for these
  situations we'll fake it by first running an equivalent query returning inserted/affected PKs, and then do a
  subsequent SELECT to get those rows. Then we'll reduce the rows with the original reducing function."
  [rf conn query-type model sql-args]
  ;; We're using `conj` here instead of `rf` so no row-transform nonsense or whatever is done. We will pass the
  ;; actual instances to the original `rf` once we get them.
  (let [pk-query-type (types/similar-query-type-returning query-type :toucan.result-type/pks)
        pks           (pipeline/transduce-execute-with-connection conj conn pk-query-type model sql-args)
        ;; this is sort of a hack but I don't know of any other way to pass along `:columns` information with the
        ;; original parsed args
        columns       (:columns pipeline/*parsed-args*)]
    ;; once we have a sequence of PKs then get instances as with `select` and do our magic on them using the
    ;; ORIGINAL `rf`.
    (transduce-instances-from-pks rf model columns pks)))
 

PostgreSQL integration.

(ns toucan2.jdbc.postgres
  (:require
   [methodical.core :as m]
   [toucan2.jdbc.read :as jdbc.read]
   [toucan2.util :as u])
  (:import
   (java.sql ResultSet ResultSetMetaData Types)))
(set! *warn-on-reflection* true)
(when-let [pg-connection-class (try
                                 (Class/forName "org.postgresql.jdbc.PgConnection")
                                 (catch Throwable _
                                   nil))]
  (derive pg-connection-class ::connection))
(m/defmethod jdbc.read/read-column-thunk [#_conn  ::connection
                                          #_model :default
                                          #_type  Types/TIMESTAMP]
  "Both Postgres `timestamp` and `timestamp with time zone` come back as `java.sql.Types/TIMESTAMP`; check the actual
  database column type name so we can fetch objects as the correct class."
  [_conn _model ^ResultSet rset ^ResultSetMetaData rsmeta ^Long i]
  (let [^Class klass (if (= (u/lower-case-en (.getColumnTypeName rsmeta i)) "timestamptz")
                       java.time.OffsetDateTime
                       java.time.LocalDateTime)]
    (jdbc.read/get-object-of-class-thunk rset i klass)))
(m/prefer-method! #'jdbc.read/read-column-thunk
                  [::connection :default Types/TIMESTAMP]
                  [java.sql.Connection :default Types/TIMESTAMP])
 
(ns ^:no-doc toucan2.jdbc.query
  (:require
   [next.jdbc]
   [toucan2.jdbc.options :as jdbc.options]
   [toucan2.jdbc.result-set :as jdbc.rs]
   [toucan2.log :as log]
   [toucan2.util :as u])
  (:import java.sql.ResultSet))
(set! *warn-on-reflection* true)

TODO: it's a little silly having a one-function namespace. Maybe we should move this into one other ones

We normally only read in a forward direction, and treat result sets as read-only. So make sure the JDBC can optimize things when possible. Note that you're apparently not allowed to do this when :return-keys is set. So this only is merged in otherwise.

(def ^:private read-forward-options
  {:concurrency :read-only
   :cursors     :close
   :result-type :forward-only})

Execute sql-args against a JDBC connection conn, and reduce results with reducing function rf and initial value init. Part of the implementation of the JDBC backend; you shouldn't need to call this directly.

(defn ^:no-doc reduce-jdbc-query
  [rf init ^java.sql.Connection conn model sql-args extra-options]
  {:pre [(instance? java.sql.Connection conn) (sequential? sql-args) (string? (first sql-args)) (ifn? rf)]}
  (let [opts (jdbc.options/merge-options extra-options)
        opts (merge (when-not (:return-keys opts)
                      read-forward-options)
                    opts)]
    (log/debugf "Preparing JDBC query with next.jdbc options %s" opts)
    (u/try-with-error-context [(format "execute SQL with %s" (class conn)) {::sql-args sql-args}]
      (with-open [stmt (next.jdbc/prepare conn sql-args opts)]
        (when-not (= (.getFetchDirection stmt) ResultSet/FETCH_FORWARD)
          (try
            (.setFetchDirection stmt ResultSet/FETCH_FORWARD)
            (catch Throwable e
              (log/debugf e "Error setting fetch direction"))))
        (log/tracef "Executing statement with %s" (class conn))
        (let [result-set? (.execute stmt)]
          (cond
            (:return-keys opts)
            (do
              (log/debugf "Query was executed with %s; returning generated keys" :return-keys)
              (with-open [rset (.getGeneratedKeys stmt)]
                (jdbc.rs/reduce-result-set rf init conn model rset opts)))
            result-set?
            (with-open [rset (.getResultSet stmt)]
              (log/debugf "Query returned normal result set")
              (jdbc.rs/reduce-result-set rf init conn model rset opts))
            :else
            (do
              (log/debugf "Query did not return a ResultSet; nothing to reduce. Returning update count.")
              (reduce rf init [(.getUpdateCount stmt)]))))))))
 

[[read-column-thunk]] method, which is used to determine how to read values of columns in results, and default implementations

(ns toucan2.jdbc.read
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [next.jdbc.result-set :as next.jdbc.rs]
   [toucan2.log :as log]
   [toucan2.protocols :as protocols]
   [toucan2.types :as types]
   [toucan2.util :as u])
  (:import
   (java.sql Connection ResultSet ResultSetMetaData Types)))
(comment s/keep-me
         types/keep-me)
(set! *warn-on-reflection* true)

Map of java.sql.Types enum integers (e.g. java.sql.Types/FLOAT, whose value is 6) to the string type name e.g. FLOAT.

```clj (type-name java.sql.Types/FLOAT) -> (type-name 6) -> "FLOAT" ```

arglists metadata is mostly so (theoretically) Kondo can catch if you try to call this with the wrong type or wrong number of args.

(def ^{:arglists '(^String [^Integer i] ^String [^Integer i not-found])} type-name
  (into {} (for [^java.lang.reflect.Field field (.getDeclaredFields Types)]
             [(.getLong field Types) (.getName field)])))

Return a zero-arg function that, when called, will fetch the value of the column from the current row.

Dispatches on java.sql.Connection class, model, and the java.sql.Types mapping for the column, e.g. java.sql.Types/TIMESTAMP.

TODO -- dispatch for this method is busted.

  1. The java.sql.Types column type should be an explicit parameter. A little weird to dispatch off of something that's not even one of the parameters

  2. Should this also dispatch off of the actual underlying column name? So you can read a column in different ways for different models.

  3. Should this dispatch off of the underlying database column type name string, e.g. timestamp or timestamptz? It seems like a lot of the time we need to do different things based on that type name.

(m/defmulti read-column-thunk
  {:arglists            '([^Connection conn₁ model₂ ^ResultSet rset ^ResultSetMetaData rsmeta₍₃₎ ^Long i])
   :defmethod-arities   #{5}
   :dispatch-value-spec (types/or-default-spec
                         (s/cat :connection-class ::types/dispatch-value.keyword-or-class
                                :model            ::types/dispatch-value.model
                                :column-type      any?))}
  (fn [^Connection conn model _rset ^ResultSetMetaData rsmeta ^Long i]
    (let [col-type (.getColumnType rsmeta i)]
      (log/debugf
       "Column %s %s is of JDBC type %s, native type %s"
       i
       (let [table-name  (some->> (.getTableName rsmeta i) not-empty)
             column-name (.getColumnLabel rsmeta i)]
         (if table-name
           (str table-name \. column-name)
           column-name))
       (symbol "java.sql.Types" (type-name col-type))
       (.getColumnTypeName rsmeta i))
      [(protocols/dispatch-value conn) (protocols/dispatch-value model) col-type])))
(m/defmethod read-column-thunk :default
  [_conn _model ^ResultSet rset _rsmeta ^Long i]
  (log/debugf "Fetching values in column %s with %s" i (list '.getObject 'rs i))
  (fn default-read-column-thunk []
    ;; (log/tracef "col %s => %s" i (list '.getObject 'rset i))
    (.getObject rset i)))
(m/defmethod read-column-thunk :after :default
  [_conn model _rset _rsmeta thunk]
  (fn []
    (u/try-with-error-context ["read column" {:thunk thunk, :model model}]
      (thunk))))

Return a thunk that will be used to fetch values at column index i from ResultSet rset as a given class. Calling this thunk is equivalent to

```clj (.getObject rset i klass) ```

but includes extra logging.

(defn get-object-of-class-thunk
  [^ResultSet rset ^Long i ^Class klass]
  (log/debugf
              "Fetching values in column %s with %s"
              i
              (list '.getObject 'rs i klass))
  (fn get-object-of-class-thunk []
    ;; what's the overhead of this? A million rows with 10 columns each = 10 million calls =(
    ;;
    ;; from Criterium: a no-op call takes about 20ns locally. So 10m rows => 200ms from this no-op call. That's a little
    ;; expensive, but probably not as bad as the overhead we get from other nonsense here in Toucan 2. We'll have to do
    ;; some general performance tuning in the future.
    ;; (log/tracef "col %s => %s" i (list '.getObject 'rset i klass))
    (.getObject rset i klass)))

Default column read methods

(m/defmethod read-column-thunk [:default :default Types/CLOB]
  [_conn _model ^ResultSet rset _ ^Long i]
  (fn get-string-thunk []
    (.getString rset i)))
(m/defmethod read-column-thunk [:default :default Types/TIMESTAMP]
  [_conn _model rset _rsmeta i]
  (get-object-of-class-thunk rset i java.time.LocalDateTime))
(m/defmethod read-column-thunk [:default :default Types/TIMESTAMP_WITH_TIMEZONE]
  [_conn _model rset _rsmeta i]
  (get-object-of-class-thunk rset i java.time.OffsetDateTime))
(m/defmethod read-column-thunk [:default :default Types/DATE]
  [_conn _model rset _rsmeta i]
  (get-object-of-class-thunk rset i java.time.LocalDate))
(m/defmethod read-column-thunk [:default :default Types/TIME]
  [_conn _model rset _rsmeta i]
  (get-object-of-class-thunk rset i java.time.LocalTime))
(m/defmethod read-column-thunk [:default :default Types/TIME_WITH_TIMEZONE]
  [_conn _model rset _rsmeta i]
  (get-object-of-class-thunk rset i java.time.OffsetTime))
(defn- make-column-thunk [conn model ^ResultSet rset i]
  (log/tracef "Building thunk to read column %s" i)
  (let [rsmeta (.getMetaData rset)
        thunk (read-column-thunk conn model rset rsmeta i)]
    (fn column-thunk []
      (next.jdbc.rs/read-column-by-index (thunk) rsmeta i))))

Given a connection conn, a model and a result set rset, return a function which given a column number i returns a thunk that retrieves the column value of the current row from the result set.

(defn ^:no-doc make-i->thunk
  [conn model ^ResultSet rset]
  (let [n (.getColumnCount (.getMetaData rset))
        thunks (mapv (fn [i]
                       (delay (make-column-thunk conn model rset (inc i))))
                     (range n))]
    (fn [i]
      @(nth thunks (dec i)))))

Given a i->thunk function, return a function that can be used with [[next.jdbc.result-set/builder-adapter]]. The function has the signature

```clj (f builder rset i) => result ```

When this function is called with a next.jdbc result set builder, a java.sql.ResultSet rset, and column index i, it will return the value of that column for the current row.

The function used to fetch the column is built by combining [[read-column-thunk]] and [[next.jdbc.result-set/read-column-by-index]]; the function is built once and used repeatedly for each new row.

(defn ^:no-doc read-column-by-index-fn
  [i->thunk]
  (fn read-column-by-index-fn* [_builder ^ResultSet rset ^Integer i]
    (assert (not (.isClosed ^ResultSet rset))
            "ResultSet is already closed. Do you need call toucan2.realize/realize on the row before the Connection is closed?")
    (let [thunk    (i->thunk i)
          result   (thunk)]
      (log/tracef "col %s => %s" i result)
      result)))
 

Implementation of a custom next.jdbc result set builder function, [[builder-fn]], and the default implementation; [[reduce-result-set]] which is used to reduce results from JDBC databases.

(ns toucan2.jdbc.result-set
  (:require
   [better-cond.core :as b]
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [next.jdbc.result-set :as next.jdbc.rs]
   [toucan2.instance :as instance]
   [toucan2.jdbc.options :as jdbc.options]
   [toucan2.jdbc.read :as jdbc.read]
   [toucan2.jdbc.row :as jdbc.row]
   [toucan2.log :as log]
   [toucan2.model :as model]
   [toucan2.types :as types]
   [toucan2.util :as u])
  (:import
   (java.sql ResultSet ResultSetMetaData)))
(set! *warn-on-reflection* true)
(comment s/keep-me
         types/keep-me)

Return the next.jdbc builder function to use to create the results when querying a model. By default, this uses [[instance-builder-fn]], and returns Toucan 2 instances; but if you want to use plain maps you can use one of the other builder functions that ships with next.jdbc, or write your own custom builder function.

(m/defmulti builder-fn
  {:arglists            '([^java.sql.Connection conn₁ model₂ ^java.sql.ResultSet rset opts])
   :defmethod-arities   #{4}
   :dispatch-value-spec (types/or-default-spec
                         (s/cat :conn  ::types/dispatch-value.keyword-or-class
                                :model ::types/dispatch-value.model))}
  u/dispatch-on-first-two-args)
(defrecord ^:no-doc InstanceBuilder [model ^ResultSet rset ^ResultSetMetaData rsmeta cols]
  next.jdbc.rs/RowBuilder
  (->row [_this]
    ;; (log/tracef "Fetching row %s" (.getRow rset))
    (transient (instance/instance model)))
  (column-count [_this]
    (count cols))
  ;; this is purposefully not implemented because we should never get here; if we do it is an error and we want an
  ;; Exception thrown.
  #_(with-column [this row i]
      (println (pr-str (list 'with-column 'this 'row i)))
      (next.jdbc.rs/with-column-value this row (nth cols (dec i))
        (next.jdbc.rs/read-column-by-index (.getObject rset ^Integer i) rsmeta i)))
  (with-column-value [_this row col v]
    (assert (some? col) "Invalid col")
    (assoc! row col v))
  (row! [_this row]
    ;; (log/tracef "Converting transient row to persistent row")
    (persistent! row))
  next.jdbc.rs/ResultSetBuilder
  (->rs [_this]
    (transient []))
  (with-row [_this acc row]
    (conj! acc row))
  (rs! [_this acc]
    (persistent! acc)))
(defn- make-column-name->index [cols label-fn]
  {:pre [(fn? label-fn)]}
  (if (empty? cols)
    (constantly nil)
    (memoize
     (fn [column-name]
       (when (or (string? column-name)
                 (instance? clojure.lang.Named column-name))
         ;; TODO FIXME -- it seems like the column name we get here has already went thru the label fn/qualifying
         ;; functions. The `(originally ...)` in the log message is wrong. Are we applying label function twice?!
         (let [column-name' (keyword
                             (when (instance? clojure.lang.Named column-name)
                               (when-let [col-ns (namespace column-name)]
                                 (label-fn (name col-ns))))
                             (label-fn (name column-name)))
               i            (when column-name'
                              (first (keep-indexed
                                      (fn [i col]
                                        (when (= col column-name')
                                          (inc i)))
                                      cols)))]
           (log/tracef "Index of column named %s (originally %s) is %s" column-name' column-name i)
           (when-not i
             (log/debugf "Could not determine index of column name %s. Found: %s" column-name cols))
           i))))))

Create a result set map builder function appropriate for passing as the :builder-fn option to next.jdbc that will create [[toucan2.instance]]s of model using namespaces determined by [[toucan2.model/table-name->namespace]].

(defn instance-builder-fn
  [model ^ResultSet rset opts]
  (let [table-name->ns (model/table-name->namespace model)
        label-fn       (get opts :label-fn name)
        qualifier-fn   (memoize
                        (fn [table]
                          (let [table    (some-> table not-empty name label-fn)
                                table-ns (some-> (get table-name->ns table) name)]
                            (log/tracef "Using namespace %s for columns in table %s" table-ns table)
                            table-ns)))
        opts           (merge {:label-fn     label-fn
                               :qualifier-fn qualifier-fn}
                              opts)
        rsmeta         (.getMetaData rset)
        _              (log/debugf "Getting modified column names with next.jdbc options %s" opts)
        col-names      (next.jdbc.rs/get-modified-column-names rsmeta opts)]
    (log/debugf "Column names: %s" col-names)
    (constantly
     (assoc (->InstanceBuilder model rset rsmeta col-names) :opts opts))))
(m/defmethod builder-fn :default
  "Default `next.jdbc` builder function. Uses [[instance-builder-fn]] to return Toucan 2 instances."
  [_conn model rset opts]
  (let [merged-opts (jdbc.options/merge-options opts)]
    (instance-builder-fn model rset merged-opts)))

Reduce a java.sql.ResultSet using reducing function rf and initial value init. conn is an instance of java.sql.Connection. conn and model are used mostly for dispatch value purposes for things like [[builder-fn]], and for creating instances with the correct model.

Part of the low-level implementation of the JDBC query execution backend -- you probably shouldn't be using this directly.

(defn ^:no-doc reduce-result-set
  [rf init conn model ^ResultSet rset opts]
  (log/debugf "Reduce JDBC result set for model %s with rf %s and init %s" model rf init)
  (let [i->thunk          (jdbc.read/make-i->thunk conn model rset)
        builder-fn*       (next.jdbc.rs/builder-adapter
                           (builder-fn conn model rset opts)
                           (jdbc.read/read-column-by-index-fn i->thunk))
        builder           (builder-fn* rset opts)
        combined-opts     (jdbc.options/merge-options (merge (:opts builder) opts))
        label-fn          (get combined-opts :label-fn)
        _                 (assert (fn? label-fn) "Options must include :label-fn")
        col-names         (get builder :cols (next.jdbc.rs/get-modified-column-names
                                              (.getMetaData rset)
                                              combined-opts))
        col-name->index   (make-column-name->index col-names label-fn)]
    (log/tracef "column name -> index = %s" col-name->index)
    (loop [acc init]
      (b/cond
        (not (.next rset))
        (do
          (log/tracef "Result set has no more rows.")
          acc)
        :let [;; _     (log/tracef "Fetch row %s" (.getRow rset))
              row      (jdbc.row/row model rset builder i->thunk col-name->index)
              acc'     (rf acc row)]
        (reduced? acc')
        @acc'
        :else
        (recur acc')))))
 

Custom [[TransientRow]] type. This is mostly in a separate namespace so I don't have to look at it when working on unrelated [[toucan2.jdbc.result-set]] stuff.

This is roughly adapted from [[next.jdbc.result-set/mapify-result-set]] in a somewhat-successful attempt to make Toucan 2 be next.jdbc-compatible.

(ns ^:no-doc toucan2.jdbc.row
  (:require
   [better-cond.core :as b]
   [clojure.core.protocols :as core-p]
   [clojure.datafy :as d]
   [clojure.pprint :as pprint]
   [clojure.string :as str]
   [next.jdbc.result-set :as next.jdbc.rs]
   [puget.printer :as puget]
   [toucan2.instance :as instance]
   [toucan2.log :as log]
   [toucan2.protocols :as protocols]
   [toucan2.realize :as realize]
   [toucan2.util :as u])
  (:import
   (java.sql ResultSet)))
(set! *warn-on-reflection* true)
(declare print-representation-parts)

Fetch the column with column-name. Returns not-found if no such column exists.

(defn- fetch-column-with-name
  [column-name->index i->thunk column-name not-found]
  ;; this might get called with some other non-string or non-keyword key, in that case just return `not-found`
  ;; immediately since we're not going to find it by hitting the database.
  (let [i      (column-name->index column-name)
        result (b/cond
                 (not i)     not-found
                 :let        [thunk (i->thunk i)]
                 (not thunk) not-found
                 :else       (thunk))]
    (log/tracef "=> %s" result)
    result))
(def ^:private ^:dynamic *fetch-all-columns* true)

One of these is built for every row in the results.

TODO -- maybe we can combine the

(deftype ^:no-doc TransientRow [model
                                ^ResultSet rset
                                ;; `next.jdbc` result set builder, usually an instance
                                ;; of [[toucan2.jdbc.result_set.InstanceBuilder]] or
                                ;; whatever [[toucan2.jdbc.result-set/builder-fn]] returns. Should have the key `:cols`
                                builder
                                ;; a function that given a column name key will normalize it and return the
                                ;; corresponding JDBC index. This should probably be memoized for the whole result set.
                                column-name->index
                                ;; an atom with a set of realized column name keywords.
                                realized-keys
                                ;; ATOM with map. Given a JDBC column index (starting at 1) return a thunk that can be
                                ;; used to fetch the column. This usually comes
                                ;; from [[toucan2.jdbc.read/make-cached-i->thunk]].
                                i->thunk
                                ;; a Volatile that contains the underlying transient map representing this row.
                                ^clojure.lang.Volatile volatile-transient-row
                                ;; a delay that should return a persistent map for the current row. Once this is called
                                ;; we should return the realized row directly and work with that going forward.
                                realized-row]
  next.jdbc.result_set.InspectableMapifiedResultSet
  (row-number   [_this] (.getRow rset))
  (column-names [_this] (:cols builder))
  (metadata     [_this] (d/datafy (.getMetaData rset)))
  clojure.lang.IPersistentMap
  (assoc [this k v]
    (log/tracef ".assoc %s %s" k v)
    (if (realized? realized-row)
      (assoc @realized-row k v)
      (do
        (vswap! volatile-transient-row (fn [^clojure.lang.ITransientMap transient-row]
                                         (let [^clojure.lang.ITransientMap transient-row' (assoc! transient-row k v)]
                                           (assert (= (.valAt transient-row' k) v)
                                                   (format "assoc! did not do what we expected. k = %s v = %s row = %s .valAt = %s"
                                                           (pr-str k)
                                                           (pr-str v)
                                                           (pr-str transient-row')
                                                           (pr-str (.valAt transient-row' k))))
                                           transient-row')))
        (swap! realized-keys conj k)
        this)))
  ;; TODO -- can we `assocEx` the transient row?
  (assocEx [_this k v]
    (log/tracef ".assocEx %s %s" k v)
    (.assocEx ^clojure.lang.IPersistentMap @realized-row k v))
  (without [this k]
    (log/tracef ".without %s" k)
    (if (realized? realized-row)
      (dissoc @realized-row k)
      (do
        (vswap! volatile-transient-row dissoc! k)
        (let [k-index             (column-name->index k)
              column-name->index' (if-not k-index
                                    column-name->index
                                    (fn [column-name]
                                      (let [index (column-name->index column-name)]
                                        (when-not (= index k-index)
                                          index))))]
          (when k-index
            (swap! i->thunk (fn [i->thunk]
                              (fn [index]
                                (if (= index k-index)
                                  (constantly ::not-found)
                                  (i->thunk index))))))
          (swap! realized-keys disj k)
          (if (identical? column-name->index column-name->index')
            this
            (TransientRow. model
                           rset
                           builder
                           column-name->index'
                           realized-keys
                           i->thunk
                           volatile-transient-row
                           realized-row))))))
  ;; Java 7 compatible: no forEach / spliterator
  ;;
  ;; TODO -- not sure if we need/want this
  java.lang.Iterable
  (iterator [_this]
    (log/tracef ".iterator")
    (.iterator ^java.lang.Iterable @realized-row))
  clojure.lang.Associative
  (containsKey [_this k]
    (log/tracef ".containsKey %s" k)
    (if (realized? realized-row)
      (contains? @realized-row k)
      (or (contains? @volatile-transient-row k)
          (boolean (column-name->index k)))))
  (entryAt [this k]
    (log/tracef ".entryAt %s" k)
    (let [v (.valAt this k ::not-found)]
      (when-not (= v ::not-found)
        (clojure.lang.MapEntry. k v))))
  ;; TODO -- this should probably also include any extra keys added with `assoc` or whatever
  clojure.lang.Counted
  (count [_this]
    (log/tracef ".count")
    (let [cols (:cols builder)]
      (assert (seq cols))
      (count cols)))
  clojure.lang.IPersistentCollection
  (cons [this o]
    (log/tracef ".cons %s" o)
    (cond
      (map? o)
      (reduce #(apply assoc %1 %2) this o)
      (instance? java.util.Map o)
      (reduce #(apply assoc %1 %2) this (into {} o))
      :else
      (if-let [[k v] (seq o)]
        (assoc this k v)
        this)))
  (empty [_this]
    (log/tracef ".empty")
    (instance/instance model))
  (equiv [_this obj]
    (log/tracef ".equiv %s" obj)
    (.equiv ^clojure.lang.IPersistentCollection @realized-row obj))
  ;; we support get with a numeric key for array-based builders:
  clojure.lang.ILookup
  (valAt [this k]
    (log/tracef ".valAt %s" k)
    (.valAt this k nil))
  (valAt [this k not-found]
    (log/tracef ".valAt %s %s" k not-found)
    (cond
      (realized? realized-row)
      (get @realized-row k not-found)
      (number? k)
      (let [i (inc k)]
        (if-let [thunk (@i->thunk i)]
          (thunk)
          not-found))
      ;; non-number column name
      :else
      (let [existing-value (.valAt ^clojure.lang.ITransientMap @volatile-transient-row k ::not-found)]
        (if-not (= existing-value ::not-found)
          existing-value
          (let [fetched-value (fetch-column-with-name column-name->index @i->thunk k ::not-found)]
            (if (= fetched-value ::not-found)
              not-found
              (do
                (.assoc this k fetched-value)
                fetched-value)))))))
  clojure.lang.Seqable
  (seq [_this]
    (log/tracef ".seq")
    (seq @realized-row))
  ;; calling [[persistent!]] on a transient row will convert it to a persistent object WITHOUT realizing all the columns.
  clojure.lang.ITransientCollection
  (persistent [_this]
    (log/tracef ".persistent")
    (binding [*fetch-all-columns* false]
      @realized-row))
  next.jdbc.rs/DatafiableRow
  (datafiable-row [_this connectable opts]
    ;; since we have to call these eagerly, we trap any exceptions so
    ;; that they can be thrown when the actual functions are called
    (let [row   (try (.getRow rset)  (catch Throwable t t))
          cols  (try (:cols builder) (catch Throwable t t))
          metta (try (d/datafy (.getMetaData rset)) (catch Throwable t t))]
      (vary-meta
       @realized-row
       assoc
       `core-p/datafy (#'next.jdbc.rs/navize-row connectable opts)
       `core-p/nav    (#'next.jdbc.rs/navable-row connectable opts)
       `row-number    (fn [_this] (if (instance? Throwable row) (throw row) row))
       `column-names  (fn [_this] (if (instance? Throwable cols) (throw cols) cols))
       `metadata      (fn [_this] (if (instance? Throwable metta) (throw metta) metta)))))
  protocols/IModel
  (model [_this]
    model)
  protocols/IDispatchValue
  (dispatch-value [_this]
    (protocols/dispatch-value model))
  protocols/IDeferrableUpdate
  (deferrable-update [this k f]
    (log/tracef "Doing deferrable update of %s with %s" k f)
    (b/cond
      (realized? realized-row)
      (update @realized-row k f)
      :let [existing-value (.valAt ^clojure.lang.ITransientMap @volatile-transient-row k ::not-found)]
      ;; value already exists: update the value in the transient row and call it a day
      (not= existing-value ::not-found)
      (assoc this k (f existing-value))
      ;; otherwise compose the column thunk with `f`
      :else
      (let [col-index (column-name->index k)]
        (assert col-index (format "No column named %s in results. Got: %s" (pr-str k) (pr-str (:cols builder))))
        (swap! i->thunk (fn [i->thunk]
                          (fn [i]
                            (let [thunk (i->thunk i)]
                              (if (= i col-index)
                                (comp f thunk)
                                thunk)))))
        this)))
  realize/Realize
  (realize [_this]
    @realized-row)
  u/IsCustomMap
  (custom-map? [_] true)
  (toString [this]
    (str/join \space (map str (print-representation-parts this)))))

We don't use [[pretty.core/PrettyPrintable]] for this like we do for everything else because we want to print TWO things, the [[print-symbol]] and a map.

Returns a sequence of things to print to represent a [[TransientRow]]. Avoids realizing the entire row if we're still in 'transient' mode.

(defn- print-representation-parts
  [^toucan2.jdbc.row.TransientRow row]
  (try
    (let [transient-row @(.volatile_transient_row row)
          realized-keys (.realized_keys row)]
      [(symbol (format "^%s " `TransientRow))
       ;; (instance? pretty.core.PrettyPrintable transient-row) (pretty/pretty transient-row)
       (zipmap @realized-keys
               (map #(get transient-row %) @realized-keys))])
    (catch Exception _
      ["unrealized result set {row} -- do you need to call toucan2.realize/realize ?"])))
(defmethod print-method toucan2.jdbc.row.TransientRow
  [row writer]
  (doseq [part (print-representation-parts row)]
    (print-method part writer)))
(defmethod pprint/simple-dispatch toucan2.jdbc.row.TransientRow
  [row]
  (doseq [part (print-representation-parts row)]
    (pprint/simple-dispatch part)))
(defmethod log/print-handler toucan2.jdbc.row.TransientRow
  [_klass]
  (fn [printer row]
    (for [part (print-representation-parts row)]
      (puget/format-doc printer part))))
(doseq [methodd [print-method
                 pprint/simple-dispatch]]
  (prefer-method methodd toucan2.jdbc.row.TransientRow clojure.lang.IPersistentMap))

A lot of the stuff below is an adapted/custom version of the code in [[next.jdbc.result-set]] -- I would have preferred to not have to do this but a lot of it was necessary to make things work in the Toucan 2 work. See this Slack thread for more information: https://clojurians.slack.com/archives/C1Q164V29/p1662494291800529

(defn- fetch-column! [builder i->thunk ^clojure.lang.ITransientMap transient-row i]
  ;; make sure the key is not already present. If it is we don't want to stomp over existing values.
  (let [col-name (nth (:cols builder) (dec i))]
    (if (= (.valAt transient-row col-name ::not-found) ::not-found)
      (let [thunk (@i->thunk i)]
        (assert (fn? thunk))
        (let [value (thunk)]
          (if (= value ::not-found)
            transient-row
            (next.jdbc.rs/with-column-value builder transient-row col-name value))))
      transient-row)))
(defn- fetch-all-columns! [builder i->thunk transient-row]
  ;; (log/tracef "Fetching all columns")
  (let [n (next.jdbc.rs/column-count builder)]
    (loop [i 1
           transient-row transient-row]
      (if (<= i n)
        (recur (inc i) (fetch-column! builder i->thunk transient-row i))
        transient-row))))
(defn- make-realized-row-delay [builder i->thunk ^clojure.lang.Volatile volatile-transient-row]
  (delay
    ;; (log/tracef "Fully realizing row. *fetch-all-columns* = %s" *fetch-all-columns*)
    (let [row (cond->> @volatile-transient-row
                *fetch-all-columns* (fetch-all-columns! builder i->thunk))]
      (next.jdbc.rs/row! builder row))))

Create a new TransientRow. Part of the low-level implementation of the JDBC query execution backend. You probably shouldn't be using this directly!

(defn ^:no-doc row
  [model ^ResultSet rset builder i->thunk col-name->index]
  (assert (not (.isClosed rset)) "ResultSet is already closed")
  (let [volatile-transient-row (volatile! (next.jdbc.rs/->row builder))
        i->thunk               (atom i->thunk)
        realized-row-delay     (make-realized-row-delay builder i->thunk volatile-transient-row)
        realized-keys          (atom #{})]
    ;; this is a gross amount of positional args. But using `reify` makes debugging things too hard IMO.
    (->TransientRow model
                    rset
                    builder
                    col-name->index
                    realized-keys
                    i->thunk
                    volatile-transient-row
                    realized-row-delay)))
 

Toucan 2 logging utilities. This is basically just a fancy wrapper around clojure.tools.logging that supports some additional debugging facilities, such as dynamically enabling logging for different topics or levels from the REPL.

(ns toucan2.log
  (:require
   [clojure.spec.alpha :as s]
   [clojure.string :as str]
   [clojure.tools.logging :as tools.log]
   [clojure.tools.logging.impl :as tools.log.impl]
   [environ.core :as env]
   [pretty.core :as pretty]
   [puget.color]
   [puget.printer :as puget]))
(set! *warn-on-reflection* true)

The current log level (as a keyword) to log messages directly to stdout with. By default this is nil, which means don't log any messages to stdout regardless of their level. (They are still logged via clojure.tools.logging.)

You can dynamically bind this to enable logging at a higher level in the REPL for debugging purposes. You can also set a default value for this by setting the atom [[level]].

(def ^:dynamic *level*
  nil)

The default log level to log messages directly to stdout with. Takes the value of the env var TOUCAN_DEBUG_LEVEL, if set. Can be overridden with [[level]]. This is stored as an atom, so you can swap! or reset! it.

(defonce  level
  (atom (some-> (env/env :toucan-debug-level) keyword)))

Whether or not to print the trace in color. True by default, unless the env var NO_COLOR is true.

(def ^:private ^:dynamic *color*
  (if-let [env-var-value (env/env :no-color)]
    (complement (Boolean/parseBoolean env-var-value))
    true))

Puget printer method used when logging. Dispatches on class.

(defmulti ^:no-doc print-handler
  {:arglists '([klass])}
  (fn [klass]
    klass))
(defmethod print-handler :default
  [_klass]
  nil)
(defmethod print-handler pretty.core.PrettyPrintable
  [_klass]
  (fn [printer x]
    (puget/format-doc printer (pretty/pretty x))))
(defmethod print-handler clojure.core.Eduction
  [_klass]
  (fn [printer ^clojure.core.Eduction ed]
    (puget/format-doc printer (list 'eduction (.xform ed) (.coll ed)))))
(defmethod print-handler clojure.lang.IRecord
  [klass]
  (when (isa? klass clojure.lang.IReduceInit)
    (fn [_printer x]
      [:text (str x)])))
(defrecord ^:no-doc Doc [forms])
(defmethod print-handler Doc
  [_klass]
  (fn [printer {:keys [forms ns-symb]}]
    (let [ns-symb (when ns-symb
                    (symbol (last (str/split (name ns-symb) #"\."))))]
      [:group
       (when ns-symb
         [:span
          (puget.color/document printer :number (name ns-symb))
          [:text " "]])
       [:align
        (for [form forms]
          (puget/format-doc printer form))]])))
(defmethod print-handler Class
  [_klass]
  (fn [printer ^Class klass]
    (puget.color/document printer :class-name (.getCanonicalName klass))))
(defrecord ^:no-doc Text [s])
(defmethod print-handler Text
  [_klass]
  (fn [_printer {:keys [s]}]
    [:span
     [:text s]
     [:line]]))
(prefer-method print-handler pretty.core.PrettyPrintable clojure.lang.IRecord)
(def ^:private printer-options
  {:print-handlers print-handler
   :width          120
   :print-meta     true
   :coll-limit     5})
(defn- default-color-printer [x]
  ;; don't print in black. I can't see it
  (puget/cprint x (assoc printer-options :color-scheme {:nil [:green]})))
(defn- default-boring-printer [x]
  (puget/pprint x printer-options))
(defn- pretty-printer []
  (if *color*
    default-color-printer
    default-boring-printer))

Pretty print a doc.

(defn ^:no-doc -pprint-doc
  ([doc]
   (-pprint-doc nil doc))
  ([ns-symb doc]
   (try
     ((pretty-printer) (assoc doc :ns-symb ns-symb))
     (catch Throwable e
       (throw (ex-info (format "Error pretty printing doc: %s" (ex-message e))
                       {:doc doc}
                       e))))))

Implementation of the log macros. You shouldn't need to use this directly.

(defn ^:no-doc -pprint-doc-to-str
  [doc]
  (str/trim (with-out-str (-pprint-doc doc))))

Exactly like [[interleave]] but includes the entirety of both collections even if the other collection is shorter. If one collection 'runs out', the remaining elements of the other collection are appended directly to the end of the resulting collection.

(defn- interleave-all
  [x y]
  (loop [acc [], x x, y y]
    (let [acc (cond-> acc
                (seq x) (conj (first x))
                (seq y) (conj (first y)))
          x   (next x)
          y   (next y)]
      (if (and (empty? x) (empty? y))
        acc
        (recur acc x y)))))

Convert format-string and args into something that can be pretty-printed by puget.

(defn- build-doc
  [format-string & args]
  (let [->Text* (fn [s]
                  (list `->Text (str/trimr s)))
        texts   (map ->Text* (str/split format-string #"%s"))]
    `(->Doc ~(vec (interleave-all texts args)))))
(defn- level->int ^long [a-level ^long default]
  (case a-level
    :disabled 5
    :error    4
    :warn     3
    :info     2
    :debug    1
    :trace    0
    default))

Current log level, as an integer.

TODO -- better idea, why don't we just change [[level]] and [[level]] to store ints so we don't have to convert them over and over again. We could introduce a with-level macro or something to make changing the level convenient.

(defn ^:no-doc -current-level-int
  ^long []
  (level->int (or *level* @level) Integer/MAX_VALUE))

Whether to enable logging for a-level. This is a macro for performance reasons, so we can do the [[level->int]] conversion at compile time rather than on every call.

(defmacro ^:no-doc -enable-level?
  [a-level]
  (let [a-level-int (level->int a-level 0)]
    `(>= ~a-level-int (-current-level-int))))

Get a logger factor for the namespace named by symbol ns-symb at a-level, iff logging is enabled for that namespace and level. The logger returned is something that satisfies the clojure.tools.logging.impl.LoggerFactory protocol.

(defn ^:no-doc -enabled-logger
  [ns-symb a-level]
  (let [logger (tools.log.impl/get-logger tools.log/*logger-factory* ns-symb)]
    (when (tools.log.impl/enabled? logger a-level)
      logger)))

Implementation of various log macros. Don't use this directly.

(defmacro ^:no-doc -log
  [a-level e doc]
  `(let [doc# (delay ~doc)]
     (when (-enable-level? ~a-level)
       (-pprint-doc '~(ns-name *ns*) @doc#))
     (when-let [logger# (-enabled-logger '~(ns-name *ns*) ~a-level)]
       (tools.log/log* logger# ~a-level ~e (-pprint-doc-to-str @doc#)))))

Implementation of various log macros. Don't use this directly.

(defmacro ^:no-doc logf
  [a-level & args]
  (let [[e format-string & args] (if (string? (first args))
                                   (cons nil args)
                                   args)]
    (assert (string? format-string))
    (let [doc (apply build-doc format-string args)]
      `(-log ~a-level ~e ~doc))))

The log macros only work with %s for now.

(defn- correct-number-of-args-for-format-string? [{:keys [msg args]}]
  (let [matcher                 (re-matcher #"%s" msg)
        format-string-arg-count (count (take-while some? (repeatedly #(re-find matcher))))]
    (= (count args) format-string-arg-count)))
(defn- pr-str-form? [form]
  (and (seq? form)
       (= (first form) 'pr-str)))
(s/def ::args
  (s/& (s/cat :e    (s/? (complement string?))
              :msg  string?
              :args (s/* (complement pr-str-form?)))
       correct-number-of-args-for-format-string?))

Log an error message for a topic. Optionally include a throwable. Only things that are actually serious errors should be logged at this level.

(defmacro errorf
  {:arglists '([throwable? s] [throwable? format-string & args])}
  [& args]
  `(logf :error ~@args))
(s/fdef errorf
  :args ::args
  :ret  any?)

Log a warning for a topic. Optionally include a throwable. Bad things that can be worked around should be logged at this level.

(defmacro warnf
  {:arglists '([throwable? s] [throwable? format-string & args])}
  [& args]
  `(logf :warn ~@args))
(s/fdef warnf
  :args ::args
  :ret  any?)

Only things that all users should see by default without configuring a logger should be this level.

(defmacro infof
  {:arglists '([throwable? s] [throwable? format-string & args])}
  [& args]
  `(logf :info ~@args))
(s/fdef infof
  :args ::args
  :ret  any?)

Most log messages should be this level.

(defmacro debugf
  {:arglists '([throwable? s] [throwable? format-string & args])}
  [& args]
  `(logf :debug ~@args))
(s/fdef debugf
  :args ::args
  :ret  any?)

Log messages that are done once-per-row should be this level.

(defmacro tracef
  {:arglists '([throwable? s] [throwable? format-string & args])}
  [& args]
  `(logf :trace ~@args))
(s/fdef tracef
  :args ::args
  :ret  any?)
(comment
  (reset! level :debug)
  (tracef "VERY NICE MESSAGE %s 1000" :abc)
  (debugf "VERY NICE MESSAGE %s 1000" :abc))
 

Methods related to resolving Toucan 2 models, appropriate table names to use when building queries for them, and namespaces to use for columns in query results.

(ns toucan2.model
  (:refer-clojure :exclude [namespace])
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.log :as log]
   [toucan2.protocols :as protocols]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(set! *warn-on-reflection* true)
(comment s/keep-me
         types/keep-me)

Resolve a modelable to an actual Toucan model (usually a keyword). A modelable is anything that can be resolved to a model via this method. You can implement this method to define special model resolution behavior, for example toucan2-toucan1 defines a method for clojure.lang.Symbol that does namespace resolution to return an appropriate model keyword.

You can also implement this method to do define behaviors when a model is used, for example making sure some namespace with method implementation for the model is loaded, logging some information, etc.

(m/defmulti resolve-model
  {:arglists '([modelable₁]), :defmethod-arities #{1}}
  u/dispatch-on-first-arg)
(m/defmethod resolve-model :default
  "Default implementation. Return `modelable` as is, i.e., there is nothing to resolve, and we can use it directly as a
  model."
  [modelable]
  modelable)
(m/defmethod resolve-model :around :default
  "Log model resolution as it happens for debugging purposes."
  [modelable]
  (let [model (next-method modelable)]
    (log/debugf "Resolved modelable %s => model %s" modelable model)
    model))

The default connectable that should be used when executing queries for model if no [[toucan2.connection/current-connectable]] is currently bound. By default, this just returns the global default connectable, :default, but you can tell Toucan to use a different default connectable for a model by implementing this method.

(m/defmulti default-connectable
  {:arglists '([model₁]), :defmethod-arities #{1}}
  u/dispatch-on-first-arg)
(m/defmethod default-connectable :default
  "Return `nil`, so we can fall thru to something else (presumably `:default` anyway)?"
  [_model]
  nil)

Return the actual underlying table name that should be used to query a model.

By default for things that implement name, the table name is just (keyword (name x)).

```clj (t2/table-name :models/user) ;; => :user ```

You can write your own implementations for this for models whose table names do not match their name.

This is guaranteed to return a keyword, so it can easily be used directly in Honey SQL queries and the like; if you return something else, the default :after method will convert it to a keyword for you.

(m/defmulti table-name
  {:arglists            '([model₁])
   :defmethod-arities   #{1}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.model)}
  u/dispatch-on-first-arg)
(m/defmethod table-name :default
  "Fallback implementation. Redirects keywords to the implementation for `clojure.lang.Named` (use the `name` of the
  keyword). For everything else, throws an error, since we don't know how to get a table name from it."
  [model]
  (if (instance? clojure.lang.Named model)
    ((m/effective-method table-name clojure.lang.Named) model)
    (throw (ex-info (format "Invalid model %s: don't know how to get its table name." (pr-str model))
                    {:model model}))))
(m/defmethod table-name :after :default
  "Always return table names as keywords. This will facilitate using them directly inside Honey SQL, e.g.
    {:select [:*], :from [(t2/table-name MyModel)]}"
  [a-table-name]
  (keyword a-table-name))
(m/defmethod table-name clojure.lang.Named
  "Default implementation for anything that is a `clojure.lang.Named`, such as a keywords or symbols. Use the `name` as
  the table name.
  ```clj
  (t2/table-name :models/user) => :user
  ```"
  [model]
  (name model))
(m/defmethod table-name String
  "Implementation for strings. Use the string name as-is."
  [table]
  table)

Return a sequence of the primary key columns names, as keywords, for a model. The default primary keys for a model are [:id]; implement this method if your model has different primary keys.

```clj ;; tell Toucan that :model/bird has a composite primary key consisting of the columns :id and :name (m/defmethod primary-keys :model/bird [_model] [:id :name]) ```

If an implementation returns a single keyword, the default :around method will automatically wrap it in a vector. It also validates that the ultimate result is a sequence of keywords, so it is safe to assume that calls to this will always return a sequence of keywords.

(m/defmulti primary-keys
  {:arglists            '([model₁])
   :defmethod-arities   #{1}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.model)}
  u/dispatch-on-first-arg)
(m/defmethod primary-keys :around :default
  "If the PK comes back unwrapped, wrap it -- make sure results are always returned as a vector of keywords. Throw an
  error if results are in the incorrect format."
  [model]
  (let [pk-or-pks (next-method model)
        pks       (if (sequential? pk-or-pks)
                    pk-or-pks
                    [pk-or-pks])]
    (when-not (every? keyword? pks)
      (throw (ex-info (format "Bad %s for model %s: should return keyword or sequence of keywords, got %s"
                              `primary-keys
                              (pr-str model)
                              (pr-str pk-or-pks))
                      {:model model, :result pk-or-pks})))
    pks))

Return a map of primary key values for a Toucan 2 instance.

(defn primary-key-values-map
  ([instance]
   (primary-key-values-map (protocols/model instance) instance))
  ([model m]
   (select-keys m (primary-keys model))))

Return a function to get the value(s) of the primary key(s) from a row, as a single value or vector of values. Used by [[toucan2.select/select-pks-reducible]] and thus by [[toucan2.select/select-pks-set]], [[toucan2.select/select-pks-vec]], etc.

The primary keys are determined by [[primary-keys]]. By default this is simply the keyword :id.

TODO -- consider renaming this to something better. What?

(defn select-pks-fn
  [modelable]
  (let [model   (resolve-model modelable)
        pk-keys (primary-keys model)]
    (if (= (count pk-keys) 1)
      (first pk-keys)
      (apply juxt pk-keys))))

Return a map of namespaces to use when fetching results with this model.

```clj (m/defmethod model->namespace ::my-model [_model] {::my-model "x" ::another-model "y"}) ```

(m/defmulti model->namespace
  {:arglists            '([model₁])
   :defmethod-arities   #{1}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.model)}
  u/dispatch-on-first-arg)
(m/defmethod model->namespace :default
  "By default, don't namespace column names when fetching rows."
  [_model]
  nil)
(m/defmethod model->namespace :after :default
  "Validate the results."
  [namespace-map]
  (when (some? namespace-map)
    (assert (map? namespace-map)
            (format "model->namespace should return a map. Got: ^%s %s"
                    (some-> namespace-map class .getCanonicalName)
                    (pr-str namespace-map))))
  namespace-map)

Take the [[model->namespace]] map for a model and return a map of string table name -> namespace. This is used to determine how to prefix columns in results based on their table name; see [[toucan2.jdbc.result-set/instance-builder-fn]] for an example of this usage.

(defn table-name->namespace
  [model]
  (not-empty
   (into {}
         (comp (filter (fn [[model _a-namespace]]
                         (not= (m/effective-primary-method table-name model)
                               (m/default-effective-method table-name))))
               (map (fn [[model a-namespace]]
                      [(name (table-name model)) a-namespace])))
         (model->namespace model))))

Get the namespace that should be used to prefix keys associated with a model in query results. This is taken from the model's implementation of [[model->namespace]].

(defn namespace
  [model]
  (some
   (fn [[a-model a-namespace]]
     (when (isa? model a-model)
       a-namespace))
   (model->namespace model)))
(m/defmethod primary-keys :default
  "By default the primary key for a model is the column `:id`; or `:some-namespace/id` if the model defines a namespace
  for itself with [[model->namespace]]."
  [model]
  (if-let [model-namespace (namespace model)]
    [(keyword (name model-namespace) "id")]
    [:id]))
 

This is a low-level namespace implementing our query execution pipeline. Most of the stuff you'd use on a regular basis are implemented on top of stuff here.

Pipeline order is

  1. [[toucan2.query/parse-args]] (entrypoint fn: [[transduce-unparsed]])
  2. [[toucan2.model/resolve-model]] (entrypoint fn: [[transduce-parsed]])
  3. [[resolve]]
  4. [[transduce-query]]
  5. [[build]]
  6. [[compile]]
  7. [[results-transform]]
  8. [[transduce-execute-with-connection]]

The main pipeline entrypoint is [[transduce-unparsed]].

(ns toucan2.pipeline
  (:refer-clojure :exclude [compile resolve])
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [pretty.core :as pretty]
   [toucan2.connection :as conn]
   [toucan2.model :as model]
   [toucan2.query :as query]
   [toucan2.realize :as realize]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(set! *warn-on-reflection* true)
(comment s/keep-me)

pipeline

Thunk function to call every time a query is executed if [[toucan2.execute/with-call-count]] is in use. Implementees of [[transduce-execute-with-connection]] should invoke this every time a query gets executed. You can use [[increment-call-count!]] to simplify the chore of making sure it's non-nil before invoking it.

(def ^:dynamic ^:no-doc *call-count-thunk*
  nil)

The final stage of the Toucan 2 query execution pipeline. Execute a compiled query (as returned by [[compile]]) with a database connection, e.g. a java.sql.Connection, and transduce results with reducing function rf.

The only reason you should need to implement this method is if you are writing a new query execution backend.

TODO -- This name is a little long, maybe this should be called transduce-execute and the function should get called something else

(m/defmulti transduce-execute-with-connection
  {:arglists            '([rf conn₁ query-type₂ model₃ compiled-query])
   :defmethod-arities   #{5}
   :dispatch-value-spec (types/or-default-spec
                         (s/cat :conn       ::types/dispatch-value.keyword-or-class
                                :query-type ::types/dispatch-value.query-type
                                :model      ::types/dispatch-value.model))}
  (fn [_rf conn query-type model _compiled-query]
    (u/dispatch-on-first-three-args conn query-type model)))
(m/defmethod transduce-execute-with-connection :before :default
  "Count all queries that are executed by calling [[*call-count-thunk*]] if bound."
  [_rf _conn _query-type _model query]
  (when *call-count-thunk*
    (*call-count-thunk*))
  query)

Get a connection from the current connection and call [[transduce-execute-with-connection]]. For DML queries, this uses [[conn/with-transaction]] to get a connection and ensure we are in a transaction, if we're not already in one. For non-DML queries this uses the usual [[conn/with-connection]].

(defn- transduce-execute
  [rf query-type model compiled-query]
  (u/try-with-error-context {::rf rf}
    (if (isa? query-type :toucan.statement-type/DML)
      ;; For DML stuff we will run the whole thing in a transaction if we're not already in one. Not 100% sure this is
      ;; necessary since we would probably already be in one if we needed to be because stuff
      ;; like [[toucan2.tools.before-delete]] have to put us in one much earlier.
      (conn/with-transaction [conn nil {:nested-transaction-rule :ignore}]
        (transduce-execute-with-connection rf conn query-type model compiled-query))
      ;; otherwise we can just execute with a normal non-transaction query.
      (conn/with-connection [conn]
        (transduce-execute-with-connection rf conn query-type model compiled-query)))))

The transducer that should be applied to the reducing function executed when running a query of query-type (see [[toucan2.types]]) for model (nil if the query is ran without a model, e.g. with [[toucan2.execute/query]]). The default implementation returns identity; add your own implementations as desired to apply additional results transforms.

Be sure to comp the transform from next-method:

```clj (m/defmethod t2/results-transform [:toucan.query-type/select.* :my-model] [query-type model] (comp (next-method query-type model) (map (fn [instance] (assoc instance :num-cans 2))))) ```

It's probably better to put the transducer returned by next-method first in the call to comp, because cond works like -> when composing transducers, and since next-method is by definition the less-specific method, it makes sense to call that transform before we apply our own. This means our own transforms will get to see the results of the previous stage, rather than vice-versa.

(m/defmulti results-transform
  {:arglists            '([query-type₁ model₂])
   :defmethod-arities   #{2}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.query-type-model)}
  u/dispatch-on-first-two-args)
(m/defmethod results-transform :default
  [_query-type _model]
  identity)

Compile a built-query to something that can be executed natively by the query execution backend, e.g. compile a Honey SQL map to a [sql & args] vector.

You can implement this method to write a custom query compilation backend, for example to compile some certain record type in a special way. See [[toucan2.honeysql2]] for an example implementation.

In addition to dispatching on query-type and model, this dispatches on the type of built-query, in a special way: for plain maps this will dispatch on the current [[map/backend]].

(m/defmulti compile
  {:arglists            '([query-type₁ model₂ built-query₃])
   :defmethod-arities   #{3}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.query-type-model-query)}
  u/dispatch-on-first-three-args)
(m/defmethod compile :default
  "Default implementation: return query as-is (i.e., consider it to already be compiled). Check that the query is non-nil
  and, if it is a collection, non-empty. Everything else is fair game."
  [_query-type _model query]
  (assert (and (some? query)
               (or (not (coll? query))
                   (seq query)))
          (format "Compiled query should not be nil/empty. Got: %s" (pr-str query)))
  query)

TODO -- this is a little JDBC-specific. What if some other query engine wants to run plain string queries without us wrapping them in a vector? Maybe this is something that should be handled at the query execution level in [[transduce-execute-with-connection]] instead. I guess that wouldn't actually work because we need to attach metadata to compiled queries

(m/defmethod compile [#_query-type :default #_model :default #_built-query String]
  "Compile a string query. Default impl wraps the string in a vector and recursively calls [[compile]]."
  [query-type model sql]
  (compile query-type model [sql]))

default implementation of [[compile]] for maps lives in [[toucan2.honeysql2]]

Build a query by applying parsed-args to resolved-query into something that can be compiled by [[compile]], e.g. build a Honey SQL query by applying parsed-args to an initial resolved-query map.

You can implement this method to write a custom query compilation backend, for example to compile some certain record type in a special way. See [[toucan2.honeysql2]] for an example implementation.

In addition to dispatching on query-type and model, this dispatches on the type of resolved-query, in a special way: for plain maps this will dispatch on the current [[map/backend]].

TODO -- does this belong here, or in [[toucan2.query]]?

(m/defmulti build
  {:arglists            '([query-type₁ model₂ parsed-args resolved-query₃])
   :defmethod-arities   #{4}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.query-type-model-query)}
  (fn [query-type model _parsed-args resolved-query]
    (u/dispatch-on-first-three-args query-type model resolved-query)))
(m/defmethod build :default
  [_query-type _model _parsed-args resolved-query]
  resolved-query)
(m/defmethod build [#_query-type :default #_model :default #_resolved-query nil]
  "Something like (select my-model nil) should basically mean SELECT * FROM my_model WHERE id IS NULL"
  [query-type model parsed-args _nil]
  ;; if `:query` is present but equal to `nil`, treat that as if the pk value IS NULL
  (let [parsed-args (assoc-in parsed-args [:kv-args :toucan/pk] nil)]
    (build query-type model parsed-args {})))
(m/defmethod build [#_query-type :default #_model :default #_resolved-query Integer]
  "Treat lone integers as queries to select an integer primary key."
  [query-type model parsed-args n]
  (build query-type model parsed-args (long n)))
(m/defmethod build [#_query-type :default #_model :default #_resolved-query Long]
  "Treat lone integers as queries to select an integer primary key."
  [query-type model parsed-args pk]
  (build query-type model (update parsed-args :kv-args assoc :toucan/pk pk) {}))
(m/defmethod build [#_query-type :default #_model :default #_query String]
  "Default implementation for plain strings. Wrap the string in a vector and recurse."
  [query-type model parsed-args sql]
  (build query-type model parsed-args [sql]))
(m/defmethod build [#_query-type :default #_model :default #_query clojure.lang.Sequential]
  "Default implementation of vector [query & args] queries."
  [query-type model {:keys [kv-args], :as parsed-args} sql-args]
  (when (seq kv-args)
    (throw (ex-info "key-value args are not supported for [query & args]."
                    {:query-type     query-type
                     :model          model
                     :parsed-args    parsed-args
                     :method         #'build
                     :dispatch-value (m/dispatch-value build query-type model parsed-args sql-args)})))
  (next-method query-type model parsed-args sql-args))

default implementation of [[build]] for maps lives in [[toucan2.honeysql2]]

Resolve a queryable to an actual query, e.g. resolve a named query defined by [[toucan2.tools.named-query]] to an actual Honey SQL map.

(m/defmulti resolve
  {:arglists            '([query-type₁ model₂ queryable₃])
   :defmethod-arities   #{3}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.query-type-model-query)}
  u/dispatch-on-first-three-args)
(m/defmethod resolve :default
  "The default implementation considers a query to already be resolved, and returns it as-is."
  [_query-type _model queryable]
  queryable)

The function to use when building a query. Normally [[build]], but you can bind this to intercept build behavior to do something different.

(def ^:dynamic ^{:arglists '([query-type model parsed-args resolved-query])} *build*
  #'build)

The function to use when compiling a query. Normally [[compile]], but you can bind this to intercept normal compilation behavior to do something different.

(def ^:dynamic ^{:arglists '([query-type model built-query])} *compile*
  #'compile)

The function to use to open a connection, execute, and transduce a query. Normally [[transduce-execute]]. The primary use case for binding this is to intercept query execution and return some results without opening any connections.

(def ^:no-doc ^:dynamic ^{:arglists '([rf query-type model compiled-query])}
  *transduce-execute*
  #'transduce-execute)

The parsed args seen at the beginning of the pipeline. This is bound in case methods in later stages of the pipeline, such as [[results-transform]], need it for one reason or another. (See for example [[toucan2.tools.default-fields]], which applies different behavior if a query was initiated with [model & columns] syntax vs. if it was not.)

(def ^:dynamic *parsed-args*
  nil)

The query after it has been resolved. This is bound in case methods in the later stages of the pipeline need it for one reason or another.

(def ^:dynamic *resolved-query*
  nil)
(defn- transduce-compiled-query [rf query-type model compiled-query]
  (u/try-with-error-context ["with compiled query" {::compiled-query compiled-query}]
    (let [xform (results-transform query-type model)
          rf    (xform rf)]
      (*transduce-execute* rf query-type model compiled-query))))
(defn- transduce-built-query [rf query-type model built-query]
  (u/try-with-error-context ["with built query" {::built-query built-query}]
    (if (isa? built-query ::no-op)
      (let [init (rf)]
        (rf init))
      (let [compiled-query (*compile* query-type model built-query)]
        (transduce-compiled-query rf query-type model compiled-query)))))

One of the primary customization points for the Toucan 2 query execution pipeline. [[build]] and [[compile]] a resolved-query, then open a connection, execute the query, and transduce the results with [[transduce-execute-with-connection]] (using the [[results-transform]]).

You can implement this method to introduce custom behavior that should happen before a query is built or compiled, e.g. transformations to the parsed-args or other shenanigans like changing underlying query type being executed (e.g. [[toucan2.tools.after]], which 'upgrades' queries returning update counts or PKs to ones returning instances so [[toucan2.tools.after-update]] and [[toucan2.tools.after-insert]] can be applied to affected rows).

(m/defmulti transduce-query
  {:arglists            '([rf query-type₁ model₂ parsed-args resolved-query₃])
   :defmethod-arities   #{5}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.query-type-model-query)}
  (fn [_rf query-type model _parsed-args resolved-query]
    (u/dispatch-on-first-three-args query-type model resolved-query)))
(m/defmethod transduce-query :default
  [rf query-type model parsed-args resolved-query]
  (let [built-query (*build* query-type model parsed-args resolved-query)]
    (transduce-built-query rf query-type model built-query)))
(defn- transduce-query* [rf query-type model parsed-args resolved-query]
  (let [parsed-args (dissoc parsed-args :queryable)]
    (binding [*resolved-query* resolved-query]
      (u/try-with-error-context ["with resolved query" {::resolved-query resolved-query}]
        (transduce-query rf query-type model parsed-args resolved-query)))))
(defn- transduce-with-model
  [rf query-type model {:keys [queryable], :as parsed-args}]
  ;; if `*current-connectable*` is unbound but `model` has a default connectable, bind `*current-connectable*` and recur
  (if-let [model-connectable (when-not conn/*current-connectable*
                               (model/default-connectable model))]
    (binding [conn/*current-connectable* model-connectable]
      (transduce-with-model rf query-type model parsed-args))
    (binding [*parsed-args* parsed-args]
      (u/try-with-error-context ["with parsed args" {::query-type query-type, ::parsed-args parsed-args}]
        (let [queryable      (if (contains? parsed-args :queryable)
                               queryable
                               (or queryable {}))
              resolved-query (resolve query-type model queryable)]
          (transduce-query* rf query-type model parsed-args resolved-query))))))

Like [[transduce-unparsed]], but called with already-parsed args rather than unparsed args.

(defn ^:no-doc transduce-parsed
  [rf query-type {:keys [modelable connectable], :as parsed-args}]
  ;; if `:connectable` was specified, bind it to [[conn/*current-connectable*]]; it should always override the current
  ;; connection (if one is bound). See docstring for [[toucan2.query/reducible-query]] for more info.
  ;;
  ;; TODO -- I'm not 100% sure this makes sense -- if we specify `:conn ::my-connection` and then want to do something
  ;; in a transaction for `::my-connection`? Shouldn't it still be done in a transaction?
  (if connectable
    (binding [conn/*current-connectable* connectable]
      (transduce-parsed rf query-type (dissoc parsed-args :connectable)))
    ;; if [[conn/*current-connectable*]] is not yet bound, then get the default connectable for the model and recur.
    (let [model (model/resolve-model modelable)]
      (u/try-with-error-context ["with model" {::model model}]
        (transduce-with-model rf query-type model (dissoc parsed-args :modelable))))))

Entrypoint to the Toucan 2 query execution pipeline. Parse unparsed-args for a query-type, then resolve model and query, build and compile query, then open a connection, execute the query, and transduce the results.

(defn ^:no-doc transduce-unparsed
  [rf query-type unparsed-args]
  (let [parsed-args (query/parse-args query-type unparsed-args)]
    (u/try-with-error-context ["with unparsed args" {::query-type query-type, ::unparsed-args unparsed-args}]
      (transduce-parsed rf query-type parsed-args))))

rf helper functions

Returns a version of reducing function rf with a zero-arity (initial value arity) that returns init.

(defn ^:no-doc with-init
  [rf init]
  (fn
    ([]    init)
    ([x]   (rf x))
    ([x y] (rf x y))))

The default reducing function for queries of query-type. Used for non-reducible operations like [[toucan2.select/select]] or [[toucan2.execute/query]].

(m/defmulti default-rf
  {:arglists            '([query-type])
   :defmethod-arities   #{1}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.query-type)}
  keyword)
(m/defmethod default-rf :toucan.result-type/update-count
  "The reducing function for queries returning an update count. Sums all numbers passed in."
  [_query-type]
  (-> (fnil + 0 0)
      (with-init 0)
      completing))
(m/defmethod default-rf :toucan.result-type/pks
  "The reducing function for queries returning PKs. Presumably these will come back as a map, but that map doesn't need to
  be realized. This needs to be combined with a transducer like `map` [[toucan2.model/select-pks-fn]] to get the PKs
  themselves."
  [_query-type]
  conj)
(m/defmethod default-rf :toucan.result-type/*
  "The default reducing function for all query types unless otherwise specified. Returns realized maps (by default, Toucan
  2 instances)."
  [_query-type]
  ((map realize/realize) conj))

Return a transducer that transforms a reducing function rf so it always takes at most one value and returns the first value from the results. This doesn't work for things that return update counts!

(defn ^:no-doc first-result-xform-fn
  [query-type]
  (if (isa? query-type :toucan.result-type/update-count)
    identity
    (fn [rf]
      (completing ((take 1) rf) first))))

Helper functions for implementing stuff like [[toucan2.select/select]]

Helper for implementing things like [[toucan2.select/select]]. Transduce unparsed-args using the [[default-rf]] for this query-type.

(defn ^:no-doc transduce-unparsed-with-default-rf
  [query-type unparsed-args]
  (assert (types/query-type? query-type))
  (let [rf (default-rf query-type)]
    (transduce-unparsed rf query-type unparsed-args)))

reducible versions for implementing stuff like [[toucan2.select/reducible-select]]

Create a reducible with one of the functions in this namespace.

(defn- reducible-fn
  [f & args]
  (reify
    clojure.lang.IReduceInit
    (reduce [_this rf init]
      ;; wrap the rf in `completing` so we don't end up doing any special one-arity TRANSDUCE stuff inside of REDUCE
      (apply f (completing (with-init rf init)) args))
    pretty/PrettyPrintable
    (pretty [_this]
      (list* `reducible-fn f args))))

Helper for implementing things like [[toucan2.select/reducible-select]]. A reducible version of [[transduce-unparsed]].

(defn ^:no-doc reducible-unparsed
  [query-type unparsed]
  (reducible-fn transduce-unparsed query-type unparsed))

Helper for implementing things like [[toucan2.execute/reducible-query]] that don't need arg parsing. A reducible version of [[transduce-parsed]].

(defn ^:no-doc reducible-parsed-args
  [query-type parsed-args]
  (reducible-fn transduce-parsed query-type parsed-args))

Misc util functions. TODO -- I don't think this belongs here; hopefully this can live somewhere where we can call it compile instead.

Helper for compiling a built-query to something that can be executed natively.

(defn compile*
  ([built-query]
   (compile* nil built-query))
  ([query-type built-query]
   (compile* query-type nil built-query))
  ([query-type model built-query]
   (compile query-type model built-query)))
 
(ns toucan2.protocols
  (:require
   [potemkin :as p]))
(set! *warn-on-reflection* true)
(p/defprotocol+ IModel
  :extend-via-metadata true
  "Protocol for something that is-a or has-a model."
  (model [this]
    "Get the Toucan model associated with `this`."))
(extend-protocol IModel
  nil
  (model [_this]
    nil)

  Object
  (model [_this]
    nil))
(p/defprotocol+ IWithModel
  :extend-via-metadata true
  "Protocol for something that has-a model that supports creating a copy with a different model."
  (^{:style/indent nil} with-model [this new-model]
    "Return a copy of `instance` with its model set to `new-model.`"))

there are some default impls of [[with-model]] in [[toucan2.instance]]

Protocol for something that records the changes made to it, e.g. a Toucan instance.

(p/defprotocol+ IRecordChanges
  (original [instance]
    "Get the original version of `instance` as it appeared when it first came out of the DB.")
  (^{:style/indent nil} with-original [instance new-original]
   "Return a copy of `instance` with its `original` map set to `new-original`.")
  (current [instance]
    "Return the underlying map representing the current state of an `instance`.")
  (^{:style/indent nil} with-current [instance new-current]
   "Return a copy of `instance` with its underlying `current` map set to `new-current`.")
  (changes [instance]
    "Get a map with any changes made to `instance` since it came out of the DB. Only includes keys that have been
    added or given different values; keys that were removed are not counted. Returns `nil` if there are no changes."))

nil and IPersistentMap can implement the methods that make sense for them: nil or a plain map doesn't have any changes, so [[changes]] can return nil. I don't know what sort of implementation for stuff like [[with-original]] or [[with-current]] makes sense so I'm not implementing those for now.

(extend-protocol IRecordChanges
  nil
  (original [_this]
    nil)
  ;; (with-original [this])
  (current [_this]
    nil)
  ;; (with-current [this])
  (changes [_this]
    nil)

  ;; generally just treat a plain map like an instance with nil model/and original = nil,
  ;; and no-op for anything that would require "upgrading" the map to an actual instance in such a way that if
  ;;
  ;;    (= plain-map instance)
  ;;
  ;; then
  ;;
  ;;    (= (f plain-map) (f instance))
  clojure.lang.IPersistentMap
  (original [_this]
    nil)
  (with-original [this _m]
    this)
  (current [this]
    this)
  (with-current [_this new-current]
    new-current)

  ;; treat the entire map as `changes` -- that way if you accidentally do something like
  ;;
  ;; (merge plain-map instance)
  ;;
  ;; in a `before-update` method, you don't accidentally shoot yourself in the foot and break `update!` or the like.
  (changes [this]
    this))
(p/defprotocol+ IDispatchValue
  :extend-via-metadata true
  "Protocol to get the value to use for multimethod dispatch in Toucan from something."
  (dispatch-value
   [this]
   "Get the value that we should dispatch off of in multimethods for `this`. By default, the dispatch of a keyword is
    itself while the dispatch value of everything else is its [[type]]."))
(extend-protocol IDispatchValue
  Object
  (dispatch-value [x]
    (type x))

  nil
  (dispatch-value [_nil]
    nil)

  clojure.lang.Keyword
  (dispatch-value [k]
    k))
(p/defprotocol+ IDeferrableUpdate
  (deferrable-update [this k f]
    "Like [[clojure.core/update]], but this update can be deferred until later. For things like transient rows where you
  might want to apply transforms to values that ultimately get realized, but not *cause* them to be realized. Unlike
  [[clojure.core/update]], does not support additional args to pass to `f`."))
(extend-protocol IDeferrableUpdate
  nil
  (deferrable-update [_this k f]
    (update nil k f))

  clojure.lang.IPersistentMap
  (deferrable-update [m k f]
    (update m k f)))
 
(ns toucan2.query
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.log :as log]
   [toucan2.model :as model]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(comment types/keep-me)

[[parse-args]]

(s/def ::default-args.connectable
  (s/? (s/cat :key         (partial = :conn)
              :connectable any?)))
(s/def ::default-args.modelable.column
  (s/or :column      keyword?
        :expr-column (s/cat :expr   any?
                            :column (s/? keyword?))))
(s/def ::default-args.modelable
  (s/or
   :modelable         (complement sequential?)
   :modelable-columns (s/cat :modelable some? ; can't have a nil model. Or can you?
                             :columns   (s/* (s/nonconforming ::default-args.modelable.column)))))

TODO -- can we use [[s/every-kv]] for this stuff?

(s/def ::default-args.kv-args
  (s/* (s/cat
        :k any?
        :v any?)))
(s/def ::default-args.queryable
  (s/? any?))
(s/def ::default-args
  (s/cat
   :connectable ::default-args.connectable
   :modelable   ::default-args.modelable
   :kv-args     ::default-args.kv-args
   :queryable   ::default-args.queryable))
(s/def :toucan2.query.parsed-args/modelable
  some?)
(s/def :toucan2.query.parsed-args/kv-args
  (some-fn nil? map?))
(s/def :toucan2.query.parsed-args/columns
  (some-fn nil? sequential?))
(s/def ::parsed-args
  (s/keys :req-un [:toucan2.query.parsed-args/modelable]
          :opt-un [:toucan2.query.parsed-args/kv-args
                   :toucan2.query.parsed-args/columns]))
(defn- validate-parsed-args [parsed-args]
  (u/try-with-error-context ["validate parsed args" {::parsed-args parsed-args}]
    (let [result (s/conform ::parsed-args parsed-args)]
      (when (s/invalid? result)
        (throw (ex-info (format "Invalid parsed args: %s" (s/explain-str ::parsed-args parsed-args))
                        (s/explain-data ::parsed-args parsed-args)))))))

Parse unparsed-args for query-type with the given spec. See documentation for [[parse-args]] for more details.

(defn parse-args-with-spec
  [query-type spec unparsed-args]
  (u/try-with-error-context ["parse args" {::query-type query-type, ::unparsed-args unparsed-args}]
    (log/debugf "Parse args for query type %s %s" query-type unparsed-args)
    (let [parsed (s/conform spec unparsed-args)]
      (when (s/invalid? parsed)
        (throw (ex-info (format "Don't know how to interpret %s args: %s"
                                (pr-str query-type)
                                (s/explain-str spec unparsed-args))
                        (s/explain-data spec unparsed-args))))
      (log/tracef "Conformed args: %s" parsed)
      (let [parsed (cond-> parsed
                     (:modelable parsed)                 (merge (let [[modelable-type x] (:modelable parsed)]
                                                                  (case modelable-type
                                                                    :modelable         {:modelable x}
                                                                    :modelable-columns x)))
                     (:connectable parsed)               (update :connectable :connectable)
                     (not (contains? parsed :queryable)) (assoc :queryable {})
                     (seq (:kv-args parsed))             (update :kv-args (fn [kv-args]
                                                                            (into {} (map (juxt :k :v)) kv-args))))]
        (log/debugf "Parsed => %s" parsed)
        (validate-parsed-args parsed)
        parsed))))

parse-args takes a sequence of unparsed args passed to something like [[toucan2.select/select]] and parses them into a parsed args map. The default implementation uses [[clojure.spec.alpha]] to parse the args according to args-spec.

These keys are commonly returned by several of the different implementations parse-args, and other tooling is build to leverage them:

  • :modelable -- usually the first of the unparsed-args, this is the thing that should get resolved to a model with [[toucan2.model/resolve-model]].

  • :queryable -- something that can be resolved to a query, for example a map or integer or 'named query' keyword. The resolved query is ultimately combined with other parsed args and built into something like a Honey SQL map, then compiled to something like SQL.

  • :kv-args -- map of key-value pairs. When [[build]] builds a query, it calls [[apply-kv-arg]] for each of the key-value pairs. The default behavior is to append a Honey SQL :where clause based on the pair; but you can customize the behavior for specific keywords to do other things -- :toucan/pk is one such example.

  • :columns -- for things that return instances, :columns is a sequence of columns to return. These are commonly specified by wrapping the modelable in a [modelable & columns] vector.

(m/defmulti parse-args
  {:arglists            '([query-type₁ unparsed-args])
   :defmethod-arities   #{2}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.query-type)}
  u/dispatch-on-first-arg)
(m/defmethod parse-args :default
  "The default implementation calls [[parse-args-with-spec]] with the `:toucan2.query/default-args` spec."
  [query-type unparsed-args]
  (parse-args-with-spec query-type ::default-args unparsed-args))

Part of the default [[pipeline/build]] for maps: applying key-value args

Merge a key-value pair into a query, presumably a map. What this means depends on the [[toucan2.protocols/dispatch-value]] of query -- for a plain map, the default Honey SQL backend applies k and v as a :where condition:

```clj (apply-kv-arg :default {} :k :v) ;; => {:where [:= :k :v]} ```

You can add new implementations of this method to special behaviors for support arbitrary keys, or to support new query backends. :toucan/pk support is implemented this way.

(m/defmulti apply-kv-arg
  {:arglists            '([model₁ resolved-query₂ k₃ v])
   :defmethod-arities   #{4}
   :dispatch-value-spec (types/or-default-spec
                         (s/cat :model          ::types/dispatch-value.model
                                :resolved-query ::types/dispatch-value.query
                                :k              keyword?))}
  u/dispatch-on-first-three-args)
(comment
  ;; with a composite PK like
  [:id :name]
  ;; we need to be able to handle either
  [:in [["BevMo" 4] ["BevLess" 5]]]
  ;; or
  [:between ["BevMo" 4] ["BevLess" 5]])
(defn- toucan-pk-composite-values** [pk-columns tuple]
  {:pre [(= (count pk-columns) (count tuple))]}
  (map-indexed (fn [i col]
                 {:col col, :v (nth tuple i)})
               pk-columns))
(defn- toucan-pk-nested-composite-values [pk-columns tuples]
  (->> (mapcat (fn [tuple]
                 (toucan-pk-composite-values** pk-columns tuple))
               tuples)
       (group-by :col)
       (map (fn [[col ms]]
              {:col col, :v (mapv :v ms)}))))
(defn- toucan-pk-composite-values* [pk-columns tuple]
  (if (some sequential? tuple)
    (toucan-pk-nested-composite-values pk-columns tuple)
    (toucan-pk-composite-values** pk-columns tuple)))
(defn- toucan-pk-fn-values [pk-columns fn-name tuples]
  (->> (mapcat (fn [tuple]
                 (toucan-pk-composite-values* pk-columns tuple))
               tuples)
       (group-by :col)
       (map (fn [[col ms]]
              {:col col, :v (into [fn-name] (map :v) ms)}))))
(defn- toucan-pk-composite-values [pk-columns tuple]
  {:pre [(sequential? tuple)], :post [(sequential? %) (every? map? %) (every? :col %)]}
  (if (keyword? (first tuple))
    (toucan-pk-fn-values pk-columns (first tuple) (rest tuple))
    (toucan-pk-composite-values* pk-columns tuple)))
(defn- apply-non-composite-toucan-pk [model m pk-column v]
  ;; unwrap the value if we got something like `:toucan/pk [1]`
  (let [v (if (and (sequential? v)
                   (not (keyword? (first v)))
                   (= (count v) 1))
            (first v)
            v)]
    (apply-kv-arg model m pk-column v)))
(defn- apply-composite-toucan-pk [model m pk-columns v]
  (reduce
   (fn [m {:keys [col v]}]
     (apply-kv-arg model m col v))
   m
   (toucan-pk-composite-values pk-columns v)))
(m/defmethod apply-kv-arg :around [#_model :default #_query :default #_k :toucan/pk]
  "Implementation for handling key-value args for `:toucan/pk`.
  This is an `:around` so we can intercept the normal handler. This 'unpacks' the PK and ultimately uses the normal
  calls to [[apply-kv-arg]]."
  [model honeysql _k v]
  ;; `fn-name` here would be if you passed something like `:toucan/pk [:in 1 2]` -- the fn name would be `:in` -- and we
  ;; pass that to [[condition->honeysql-where-clause]]
  (let [pk-columns (model/primary-keys model)]
    (log/debugf "apply :toucan/pk %s for primary keys" v)
    (if (= (count pk-columns) 1)
      (apply-non-composite-toucan-pk model honeysql (first pk-columns) v)
      (apply-composite-toucan-pk model honeysql pk-columns v))))

Convenience. Merge a map of kv-args into a resolved query with repeated calls to [[apply-kv-arg]].

(defn apply-kv-args
  [model query kv-args]
  (log/debugf "Apply kv-args %s" kv-args)
  (reduce
   (fn [query [k v]]
     (apply-kv-arg model query k v))
   query
   kv-args))
 
(ns toucan2.realize
  (:require
   [potemkin :as p]
   [toucan2.log :as log]
   [toucan2.util :as u]))
(set! *warn-on-reflection* true)

TODO -- this should probably be moved to [[toucan2.protocols]], and be renamed IRealize for consistency.

(p/defprotocol+ Realize
  (realize [x]
    "Fully realize either a reducible query, or a result row from that query."))
(defn- realize-IReduceInit [this]
  (log/tracef "realize IReduceInit %s" (symbol (.getCanonicalName (class this))))
  (u/try-with-error-context ["realize IReduceInit" {::reducible this}]
    (into []
          (map (fn [row]
                 (log/tracef "realize row ^%s %s" (some-> row class .getCanonicalName symbol) row)
                 (u/try-with-error-context ["realize row" {::row row}]
                   (realize row))))
          this)))
(extend-protocol Realize
  Object
  (realize [this]
    (log/tracef "Already realized: %s" (class this))
    this)

  ;; Eduction is assumed to be for query results.
  ;; TODO -- isn't an Eduction an IReduceInit??
  clojure.core.Eduction
  (realize [this]
    (into [] (map realize) this))

  clojure.lang.IReduceInit
  (realize [this]
    (realize-IReduceInit this))

  nil
  (realize [_]
    nil))
 
(ns toucan2.save
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.connection :as conn]
   [toucan2.instance :as instance]
   [toucan2.log :as log]
   [toucan2.model :as model]
   [toucan2.protocols :as protocols]
   [toucan2.types :as types]
   [toucan2.update :as update]
   [toucan2.util :as u]))
(comment s/keep-me
         types/keep-me)
(set! *warn-on-reflection* true)
(m/defmulti save!*
  {:arglists            '([object])
   :defmethod-arities   #{1}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.model)}
  (fn [object]
    (protocols/dispatch-value (protocols/model object))))
(m/defmethod save!* :around :default
  [object]
  (u/try-with-error-context ["save changes" {::model   (protocols/model object)
                                             ::object  object
                                             ::changes (protocols/changes object)}]
    (log/debugf "Save %s %s changes %s" (protocols/model object) object (protocols/changes object))
    (next-method object)))
(m/defmethod save!* :default
  [object]
  (assert (instance/instance? object)
          (format "Don't know how to save something that's not a Toucan instance. Got: ^%s %s"
                  (some-> object class .getCanonicalName)
                  (pr-str object)))
  (if-let [changes (not-empty (protocols/changes object))]
    (let [model         (protocols/model object)
          pk-values     (select-keys object (model/primary-keys (protocols/model object)))
          rows-affected (update/update! model pk-values changes)]
      (when-not (pos? rows-affected)
        (throw (ex-info (format "Unable to save object: %s with primary key %s does not exist."
                                (pr-str model)
                                (pr-str pk-values))
                        {:object object
                         :pk     pk-values})))
      (when (> rows-affected 1)
        (log/warnf "Warning: more than 1 row affected when saving %s with primary key %s" model pk-values))
      (instance/reset-original object))
    object))
(defn save!
  ([object]
   (save!* object))
  ([connectable object]
   (if connectable
     (binding [conn/*current-connectable* connectable]
       (save!* object))
     (save!* object))))
 

Implementation of [[select]] and variations.

The args spec used by [[select]] lives in [[toucan2.query]], specifically :toucan2.query/default-args.

Code for building Honey SQL for a SELECT lives in [[toucan2.honeysql2]].

Functions that return primary keys

Functions that return primary keys such as [[select-pks-set]] determine which primary keys to return by calling [[toucan2.model/select-pks-fn]], which is based on the model's implementation of [[toucan2.model/primary-keys]]. Models with just a single primary key column will return primary keys 'unwrapped', i.e., the values of that column will be returned directly. Models with compound primary keys (i.e., primary keys consisting of more than one column) will be returned in vectors as if by calling juxt.

```clj ;; A model with a one-column primary key, :id (t2/select-pks-vec :models/venues :category "bar") ;; => [1 2]

;; A model with a compound primary key, [:id :name] (t2/select-pks-vec :models/venues.compound-key :category "bar") ;; => [[1 "Tempest"] [2 "Ho's Tavern"]] ```

(ns toucan2.select
  (:refer-clojure :exclude [count])
  (:require
   [clojure.spec.alpha :as s]
   [toucan2.log :as log]
   [toucan2.model :as model]
   [toucan2.pipeline :as pipeline]
   [toucan2.realize :as realize]
   [toucan2.types :as types]))
(comment s/keep-me
         types/keep-me)

Like [[select]], but returns an IReduceInit.

(defn reducible-select
  {:arglists '([modelable-columns & kv-args? query?]
               [:conn connectable modelable-columns & kv-args? query?])}
  [& unparsed-args]
  (pipeline/reducible-unparsed :toucan.query-type/select.instances unparsed-args))
(defn select
  {:arglists '([modelable-columns & kv-args? query?]
               [:conn connectable modelable-columns & kv-args? query?])}
  [& unparsed-args]
  (pipeline/transduce-unparsed-with-default-rf :toucan.query-type/select.instances unparsed-args))

Like [[select]], but only fetches a single row, and returns only that row.

(defn select-one
  {:arglists '([modelable-columns & kv-args? query?]
               [:conn connectable modelable-columns & kv-args? query?])}
  [& unparsed-args]
  (let [query-type :toucan.query-type/select.instances
        rf         (pipeline/default-rf query-type)
        xform      (pipeline/first-result-xform-fn query-type)]
    (pipeline/transduce-unparsed (xform rf) query-type unparsed-args)))

Like [[reducible-select]], but returns a reducible sequence of results of (f row).

(defn select-fn-reducible
  {:arglists '([f modelable-columns & kv-args? query?]
               [f :conn connectable modelable-columns & kv-args? query?])}
  [f & unparsed-args]
  (eduction
   (map f)
   (pipeline/reducible-unparsed :toucan.query-type/select.instances.fns unparsed-args)))

Like [[select]], but returns a set of values of (f instance) for the results. Returns nil if the set is empty.

```clj (t2/select-fn-set (comp str/upper-case :category) :models/venues :category "bar") ;; =>

{"BAR"}

```

(defn select-fn-set
  {:arglists '([f modelable-columns & kv-args? query?]
               [f :conn connectable modelable-columns & kv-args? query?])}
  [f & unparsed-args]
  (let [f     (comp realize/realize f)
        rf    (pipeline/with-init conj #{})
        xform (map f)]
    (not-empty (pipeline/transduce-unparsed (xform rf) :toucan.query-type/select.instances.fns unparsed-args))))

Like [[select]], but returns a vector of values of (f instance) for the results. Returns nil if the vector is empty.

```clj (t2/select-fn-vec (comp str/upper-case :category) :models/venues :category "bar") ;; => ["BAR" "BAR"] ```

NOTE: If your query does not specify an :order-by clause (or equivalent), the results are like indeterminate. Keep this in mind!

(defn select-fn-vec
  {:arglists '([f modelable-columns & kv-args? query?]
               [f :conn connectable modelable-columns & kv-args? query?])}
  [f & unparsed-args]
  (let [f     (comp realize/realize f)
        rf    (pipeline/with-init conj [])
        xform (map f)]
    (not-empty (pipeline/transduce-unparsed (xform rf) :toucan.query-type/select.instances.fns unparsed-args))))

Like [[select-one]], but applies f to the result.

```clj (t2/select-one-fn :id :models/people :name "Cam") ;; => 1 ```

(defn select-one-fn
  {:arglists '([f modelable-columns & kv-args? query?]
               [f :conn connectable modelable-columns & kv-args? query?])}
  [f & unparsed-args]
  (let [query-type :toucan.query-type/select.instances.fns
        f          (comp realize/realize f)
        rf         (pipeline/with-init conj [])
        xform      (comp (map f)
                         (pipeline/first-result-xform-fn query-type))]
    (pipeline/transduce-unparsed (xform rf) query-type unparsed-args)))

Returns a reducible sequence of all primary keys

```clj (into [] (t2/select-pks-reducible :models/venues :category "bar")) ;; => [1 2] ```

(defn select-pks-reducible
  {:arglists '([modelable-columns & kv-args? query?]
               [:conn connectable modelable-columns & kv-args? query?])}
  [modelable & unparsed-args]
  (apply select-fn-reducible (model/select-pks-fn modelable) modelable unparsed-args))

Returns a set of all primary keys (as determined by [[toucan2.model/primary-keys]] and [[toucan2.model/select-pks-fn]]) of instances matching the query. Models with just a single primary key columns will be 'unwrapped' (i.e., the values of that column will be returned); models with compound primary keys (i.e., more than one column) will be returned in vectors as if by calling juxt.

```clj (t2/select-pks-set :models/venues :category "bar") ;; => #{1 2} ```

(defn select-pks-set
  {:arglists '([modelable-columns & kv-args? query?]
               [:conn connectable modelable-columns & kv-args? query?])}
  [modelable & unparsed-args]
  (apply select-fn-set (model/select-pks-fn modelable) modelable unparsed-args))

Returns a vector of all primary keys (as determined by [[toucan2.model/primary-keys]] and [[toucan2.model/select-pks-fn]]) of instances matching the query. Models with just a single primary key columns will be 'unwrapped' (i.e., the values of that column will be returned); models with compound primary keys (i.e., more than one column) will be returned in vectors as if by calling juxt.

```clj (t2/select-pks-vec :models/venues :category "bar") ;; => [1 2] ```

NOTE: If your query does not specify an :order-by clause (or equivalent), the results are like indeterminate. Keep this in mind!

(defn select-pks-vec
  {:arglists '([modelable-columns & kv-args? query?]
               [:conn connectable modelable-columns & kv-args? query?])}
  [modelable & unparsed-args]
  (apply select-fn-vec (model/select-pks-fn modelable) modelable unparsed-args))

Return the primary key of the first row matching the query. Models with just a single primary key columns will be 'unwrapped' (i.e., the values of that column will be returned); models with compound primary keys (i.e., more than one column) will be returned in vectors as if by calling juxt.

```clj (t2/select-one-pk :models/people :name "Cam") ;; => 1 ```

(defn select-one-pk
  {:arglists '([modelable-columns & kv-args? query?]
               [:conn connectable modelable-columns & kv-args? query?])}
  [modelable & unparsed-args]
  (apply select-one-fn (model/select-pks-fn modelable) modelable unparsed-args))

Return a map of (f1 instance) -> (f2 instance) for instances matching the query.

```clj (t2/select-fn->fn :id (comp str/upper-case :name) :models/people) ;; => {1 "CAM", 2 "SAM", 3 "PAM", 4 "TAM"} ```

(defn select-fn->fn
  {:arglists '([f1 f2 modelable-columns & kv-args? query?]
               [f1 f2 :conn connectable modelable-columns & kv-args? query?])}
  [f1 f2 & unparsed-args]
  (let [f1    (comp realize/realize f1)
        f2    (comp realize/realize f2)
        rf    (pipeline/with-init conj {})
        xform (map (juxt f1 f2))]
    (pipeline/transduce-unparsed (xform rf) :toucan.query-type/select.instances unparsed-args)))

The inverse of [[select-pk->fn]]. Return a map of (f instance) -> primary key for instances matching the query.

```clj (t2/select-fn->pk (comp str/upper-case :name) :models/people) ;; => {"CAM" 1, "SAM" 2, "PAM" 3, "TAM" 4} ```

(defn select-fn->pk
  {:arglists '([f modelable-columns & kv-args? query?]
               [f :conn connectable modelable-columns & kv-args? query?])}
  [f modelable & args]
  (let [pks-fn (model/select-pks-fn modelable)]
    (apply select-fn->fn f pks-fn modelable args)))

The inverse of [[select-fn->pk]]. Return a map of primary key -> (f instance) for instances matching the query.

```clj (t2/select-pk->fn (comp str/upper-case :name) :models/people) ;; => {1 "CAM", 2 "SAM", 3 "PAM", 4 "TAM"} ```

(defn select-pk->fn
  {:arglists '([f modelable-columns & kv-args? query?]
               [f :conn connectable modelable-columns & kv-args? query?])}
  [f modelable & args]
  (let [pks-fn (model/select-pks-fn modelable)]
    (apply select-fn->fn pks-fn f modelable args)))
(defn- count-rf []
  (let [logged-warning? (atom false)
        log-warning     (fn []
                          (when-not @logged-warning?
                            (log/warnf "Warning: inefficient count query. See documentation for toucan2.select/count.")
                            (reset! logged-warning? true)))]
    (fn count-rf*
      ([] 0)
      ([acc] acc)
      ([acc row]
       (if (:count row)
         (+ acc (:count row))
         (do (log-warning)
             (inc acc)))))))

Like [[select]], but returns the number of rows that match in an efficient way.

Implementation note:

The default Honey SQL 2 map query compilation backend builds an efficient

```sql SELECT count(*) AS "count" FROM ... ```

query. Custom query compilation backends should do the equivalent by implementing [[toucan2.pipeline/build]] for the query type :toucan.query-type/select.count and build a query that returns the key :count, If an efficient implementation does not exist, this will fall back to simply counting all matching rows.

(defn count
  {:arglists '([modelable-columns & kv-args? query?]
               [:conn connectable modelable-columns & kv-args? query?])}
  [& unparsed-args]
  (pipeline/transduce-unparsed (count-rf) :toucan.query-type/select.count unparsed-args))
(defn- exists?-rf
  ([] false)
  ([acc] acc)
  ([_acc row]
   (if (contains? row :exists)
     (let [exists (:exists row)
           result (if (integer? exists)
                    (pos? exists)
                    (boolean exists))]
       (if (true? result)
         (reduced true)
         false))
     (do
       (log/warnf "Warning: inefficient exists? query. See documentation for toucan2.select/exists?.")
       (reduced true)))))

Like [[select]], but returns whether or not any rows match in an efficient way.

Implementation note:

The default Honey SQL 2 map query compilation backend builds an efficient

```sql SELECT exists(SELECT 1 FROM ... WHERE ...) AS exists ```

(defn exists?
  {:arglists '([modelable-columns & kv-args? query?]
               [:conn connectable modelable-columns & kv-args? query?])}
  [& unparsed-args]
  (pipeline/transduce-unparsed exists?-rf :toucan.query-type/select.exists unparsed-args))
 

Common code shared by various after- methods. Since the after methods operate over instances, we need to upgrade result-type/pks and result-type/update-count queries to result-type/instances, run them with the 'upgraded' result type, run our after stuff on each row, and then return the original results.

(ns toucan2.tools.after
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.log :as log]
   [toucan2.model :as model]
   [toucan2.pipeline :as pipeline]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(set! *warn-on-reflection* true)

Should return a function with the signature

```clj (f instance) ```

This function is only done for side-effects for query types that return update counts or PKs.

(m/defmulti each-row-fn
  {:arglists '([query-type₁ model₂])
   :defmethod-arities #{2}
   :dispatch-value-spec ::types/dispatch-value.query-type-model}
  u/dispatch-on-first-two-args)
(m/defmethod each-row-fn :after :default
  [query-type f]
  (assert (fn? f)
          (format "Expected each-row-fn for query type %s to return a function, got ^%s %s"
                  (pr-str query-type)
                  (some-> f class .getCanonicalName)
                  (pr-str f)))
  f)
(m/defmethod pipeline/results-transform [#_query-type :toucan.result-type/instances #_model ::model]
  [query-type model]
  ;; if there's no [[each-row-fn]] for this `query-type` then we don't need to apply any transforms. Example: maybe a
  ;; model has an [[each-row-fn]] for `INSERT` queries, but not for `UPDATE`. Since the model derives from `::model`, we
  ;; end up here either way. But if `query-type` is `UPDATE` we shouldn't touch the query.
  (if (m/is-default-primary-method? each-row-fn [query-type model])
    (next-method query-type model)
    (let [row-fn (each-row-fn query-type model)
          row-fn (fn [row]
                   (u/try-with-error-context ["Apply after row fn" {::query-type query-type, ::model model}]
                     (log/debugf "Apply after %s for %s" query-type model)
                     (let [result (row-fn row)]
                       ;; if the row fn didn't return something (not generally necessary for something like
                       ;; `after-update` which is always done for side effects) then return the original row. We still
                       ;; need it for stuff like getting the PKs back out.
                       (if (some? result)
                         result
                         row))))
          xform  (map row-fn)]
      (comp xform
            (next-method query-type model)))))
(m/defmulti ^:private result-type-rf
  {:arglists            '([original-query-type₁ model rf])
   :defmethod-arities   #{3}
   :dispatch-value-spec ::types/dispatch-value.query-type}
  u/dispatch-on-first-arg)
(m/defmethod result-type-rf :toucan.result-type/update-count
  "Reducing function transform that will return the count of rows."
  [_original-query-type _model rf]
  ((map (constantly 1))
   rf))
(m/defmethod result-type-rf :toucan.result-type/pks
  "Reducing function transform that will return just the PKs (as single values or vectors of values) by getting them from
  row maps (instances)."
  [_original-query-type model rf]
  (let [pks-fn (model/select-pks-fn model)]
    ((map (fn [row]
            (assert (map? row))
            (pks-fn row)))
     rf)))
(derive ::query-type :toucan.query-type/abstract)
(m/defmethod pipeline/transduce-query [#_query-type     ::query-type
                                       #_model          ::model
                                       #_resolved-query :default]
  "'Upgrade' a query so that it returns instances, and run the upgraded query so that we can apply [[each-row-fn]] to the
  results. Then apply [[result-type-rf]] to the results of the original expected type are ultimately returned."
  [rf query-type model parsed-args resolved-query]
  (if (or
       ;; only "upgrade" the query if there's an applicable [[each-row-fn]] to apply.
       (m/is-default-primary-method? each-row-fn [query-type model])
       ;; there's no need to "upgrade" the query if it's already returning instances.
       (isa? query-type :toucan.result-type/instances))
    (next-method rf query-type model parsed-args resolved-query)
    ;; otherwise we need to run an upgraded query but then transform the results back to the originals
    ;; with [[result-type-rf]]
    (let [upgraded-type (types/similar-query-type-returning query-type :toucan.result-type/instances)
          _             (assert upgraded-type (format "Don't know how to upgrade a %s query to one returning instances"
                                                      query-type))
          rf*           (result-type-rf query-type model rf)]
      (pipeline/transduce-query rf* upgraded-type model parsed-args resolved-query))))
(defn ^:no-doc ^{:style/indent [:form]} define-after-impl
  [next-method query-type model row-fn]
  (let [f      (fn [row]
                 (or (row-fn row)
                     row))
        next-f (when next-method
                 (next-method query-type model))]
    (if next-f
      (comp next-f f)
      f)))
(defmacro define-after
  [query-type model [instance-binding] & body]
  `(do
     (u/maybe-derive ~model ::model)
     (m/defmethod each-row-fn [~query-type ~model]
       [~'&query-type ~'&model]
       (define-after-impl ~'next-method
                          ~'&query-type
                          ~'&model
                          (fn [~instance-binding]
                            ~@body)))))
(s/fdef define-after*
  :args (s/cat :query-type #(isa? % :toucan.query-type/*)
               :model      some?
               :bindings   (s/spec (s/cat :instance :clojure.core.specs.alpha/binding-form))
               :body       (s/+ any?))
  :ret any?)
 
(ns toucan2.tools.after-insert
  (:require
   [clojure.spec.alpha :as s]
   [toucan2.tools.after :as tools.after]))
(derive :toucan.query-type/insert.* ::tools.after/query-type)
(defmacro define-after-insert
  {:style/indent :defn}
  [model [instance-binding] & body]
  `(tools.after/define-after :toucan.query-type/insert.*
     ~model
     [~instance-binding]
     ~@body))
(s/fdef define-after-insert
  :args (s/cat :model    some?
               :bindings (s/spec (s/cat :instance :clojure.core.specs.alpha/binding-form))
               :body     (s/+ any?))
  :ret any?)
 
(ns toucan2.tools.after-select
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.pipeline :as pipeline]
   [toucan2.tools.simple-out-transform :as tools.simple-out-transform]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(comment types/keep-me)
(m/defmulti after-select
  {:arglists            '([instance])
   :defmethod-arities   #{1}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.model)}
  u/dispatch-on-first-arg)

Do after-select for anything returning instances, not just SELECT. [[toucan2.insert/insert-returning-instances!]] should do after-select as well.

(tools.simple-out-transform/define-out-transform [:toucan.result-type/instances ::after-select]
  [instance]
  (if (isa? &query-type :toucan.query-type/select.instances-from-pks)
    instance
    (after-select instance)))
(defmacro define-after-select
  {:style/indent :defn}
  [model [instance-binding] & body]
  `(do
     (u/maybe-derive ~model ::after-select)
     (m/defmethod after-select ~model
       [instance#]
       (let [~instance-binding (cond-> instance#
                                 ~'next-method ~'next-method)]
         ~@body))))
(s/fdef define-after-select
  :args (s/cat :model    some?
               :bindings (s/spec (s/cat :instance :clojure.core.specs.alpha/binding-form))
               :body     (s/+ any?))
  :ret any?)

after-select should be done before [[toucan2.tools.after-update]] and [[toucan2.tools.after-insert]]

(m/prefer-method! #'pipeline/results-transform
                  [:toucan.result-type/instances ::after-select]
                  [:toucan.result-type/instances :toucan2.tools.after/model])
 
(ns toucan2.tools.after-update
  (:require
   [clojure.spec.alpha :as s]
   [toucan2.tools.after :as tools.after]))
(derive :toucan.query-type/update.* ::tools.after/query-type)

The value of this is ultimately ignored, but when composing multiple after-update methods the values after each method are threaded thru, so this should return the updated row

(defmacro define-after-update
  {:style/indent :defn}
  [model [instance-binding] & body]
  `(tools.after/define-after :toucan.query-type/update.*
     ~model
     [~instance-binding]
     ~@body))
(s/fdef define-after-update
  :args (s/cat :model    some?
               :bindings (s/spec (s/cat :instance :clojure.core.specs.alpha/binding-form))
               :body     (s/+ any?))
  :ret any?)
 
(ns toucan2.tools.before-delete
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.connection :as conn]
   [toucan2.log :as log]
   [toucan2.pipeline :as pipeline]
   [toucan2.realize :as realize]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(set! *warn-on-reflection* true)
(comment types/keep-me)

Underlying method implemented when using [[define-before-delete]]. You probably shouldn't be adding implementations to this method directly, unless you know what you are doing!

(m/defmulti before-delete
  {:arglists            '([model₁ instance])
   :defmethod-arities   #{2}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.model)}
  u/dispatch-on-first-arg)
(m/defmethod before-delete :around :default
  [model instance]
  (log/tracef "Do before-delete for %s %s" model instance)
  (next-method model instance))

Select and transduce the matching rows and run their [[before-delete]] methods.

(defn- do-before-delete-for-matching-rows!
  [model parsed-args resolved-query]
  (pipeline/transduce-query
   ((map (fn [row]
           ;; this is another case where we don't NEED to fully realize the rows but it's a big hassle for people
           ;; to use this if we don't. Let's be nice and realize things for people.
           (before-delete model (realize/realize row))))
    (constantly nil))
   :toucan.query-type/select.instances
   model
   parsed-args
   resolved-query))
(m/defmethod pipeline/transduce-query [#_query-type     :toucan.query-type/delete.*
                                       #_model          ::before-delete
                                       #_resolved-query :default]
  "Do a recursive SELECT query with the args passed to `delete!`; apply [[before-delete]] to all matching rows. Then call
  the `next-method`. This is all done inside of a transaction."
  [rf query-type model parsed-args resolved-query]
  (conn/with-transaction [_conn nil {:nested-transaction-rule :ignore}]
    (do-before-delete-for-matching-rows! model parsed-args resolved-query)
    (next-method rf query-type model parsed-args resolved-query)))

Implementation of [[define-before-delete]]; don't call this directly.

(defn ^:no-doc -before-delete-impl
  [next-method model instance f]
  ;; if `f` didn't return anything, just use the original instance again.
  (let [result (or (f model instance)
                   instance)]
    (if next-method
      (next-method model result)
      result)))

Define a method that will be called for every instance that is about to be deleted. The results of this before-delete method are ultimately ignored, but the entire operation (both the original delete and the recursive select) are done in a transaction, so you can use before-delete to enforce preconditions and abort deletes when they fail, or do something for side effects.

Before-delete is implemented by first selecting all the rows matching the [[toucan2.delete/delete!]] conditions and then transducing those rows and calling the [[before-delete]] method this macro defines on each row. Because before-delete has to fetch every instance matching the condition, defining a before-delete method can be quite expensive! For example, a delete! operation that deletes a million rows would normally be a single database call; with before-delete in place, it would have to fetch and transduce all million rows and apply [[before-delete]] to each of them before even getting to the DELETE operation! So be sure you really need before-delete behavior before opting in to it.

To skip before-delete behavior, you can always use the model's raw table name directly, e.g.

```clj (t2/delete! (t2/table-name :models/user) ...) ```

This might be wise when deleting a large number of rows.

(defmacro define-before-delete
  {:style/indent :defn}
  [model [instance-binding] & body]
  `(do
     (u/maybe-derive ~model ::before-delete)
     (m/defmethod before-delete ~model
       [model# instance#]
       (-before-delete-impl ~'next-method
                            model#
                            instance#
                            (fn [~'&model ~instance-binding]
                              ~@body)))))
(s/fdef define-before-delete
  :args (s/cat :model    some?
               :bindings (s/spec (s/cat :instance :clojure.core.specs.alpha/binding-form))
               :body     (s/+ any?))
  :ret any?)
 
(ns toucan2.tools.before-insert
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.connection :as conn]
   [toucan2.log :as log]
   [toucan2.pipeline :as pipeline]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(comment types/keep-me)
(m/defmulti before-insert
  {:arglists            '([model₁ row])
   :defmethod-arities   #{2}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.model)}
  u/dispatch-on-first-arg)
(defn- do-before-insert-to-rows [rows model]
  (mapv
   (fn [row]
     (u/try-with-error-context [`before-insert {::model model, ::row row}]
       (log/tracef "Do before-insert for %s %s" model row)
       (let [result (before-insert model row)]
         (log/tracef "[before insert] => %s" row)
         result)))
   rows))

make sure we transform rows whether it's in the parsed args or in the resolved query.

(m/defmethod pipeline/transduce-query :around [#_query-type     :toucan.query-type/insert.*
                                               #_model          ::before-insert
                                               #_resolved-query :default]
  "Execute [[before-insert]] methods and the INSERT query inside a transaction."
  [rf query-type model parsed-args resolved-query]
  (conn/with-transaction [_conn nil {:nested-transaction-rule :ignore}]
    (next-method rf query-type model parsed-args resolved-query)))
(m/defmethod pipeline/build [#_query-type     :toucan.query-type/insert.*
                             #_model          ::before-insert
                             #_resolved-query clojure.lang.IPersistentMap]
  "Apply [[before-insert]] to `:rows` in the `resolved-query` or `parsed-args` for Honey SQL queries."
  [query-type model parsed-args resolved-query]
  ;; not 100% sure why either `parsed-args` OR `resolved-query` can have `:rows` but I guess we have to update either
  ;; one
  (let [parsed-args    (cond-> parsed-args
                         (:rows parsed-args) (update :rows do-before-insert-to-rows model))
        resolved-query (cond-> resolved-query
                         (:rows resolved-query) (update :rows do-before-insert-to-rows model))]
    (next-method query-type model parsed-args resolved-query)))

Important! before-insert should be done BEFORE any [[toucan2.tools.transformed/transforms]]. Transforms are often for serializing and deserializing values; we don't want before insert methods to have to work with already-serialized values.

By marking ::before-insert as preferred over :toucan2.tools.transformed/transformed it will be done first (see https://github.com/camsaul/methodical#before-methods)

#_(m/prefer-method! #'pipeline/transduce-with-model
                  [:toucan.query-type/insert.* ::before-insert]
                  [:toucan.query-type/insert.* :toucan2.tools.transformed/transformed.model])
(defmacro define-before-insert
  {:style/indent :defn}
  [model [instance-binding] & body]
  `(do
     (u/maybe-derive ~model ::before-insert)
     (m/defmethod before-insert ~model
       [~'&model ~instance-binding]
       (cond->> (do ~@body)
         ~'next-method (~'next-method ~'&model)))))
(s/fdef define-before-insert
  :args (s/cat :model    some?
               :bindings (s/spec (s/cat :instance :clojure.core.specs.alpha/binding-form))
               :body     (s/+ any?))
  :ret any?)
 
(ns toucan2.tools.before-select
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.log :as log]
   [toucan2.pipeline :as pipeline]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(set! *warn-on-reflection* true)
(comment types/keep-me)

Impl for [[define-before-select]].

(m/defmulti before-select
  {:arglists            '([model₁ parsed-args])
   :defmethod-arities   #{2}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.model)}
  u/dispatch-on-first-arg)
(m/defmethod before-select :around :default
  [model parsed-args]
  (u/try-with-error-context ["before select" {::model model}]
    (log/debugf "do before-select for %s" model)
    (let [result (next-method model parsed-args)]
      (log/debugf "[before select] => %s" result)
      result)))
(m/defmethod pipeline/build [#_query-type     :toucan.query-type/select.*
                             #_model          ::model
                             #_resolved-query :default]
  [query-type model parsed-args resolved-query]
  (let [parsed-args (before-select model parsed-args)]
    (next-method query-type model parsed-args resolved-query)))
(defmacro define-before-select
  {:style/indent :defn}
  [model [args-binding] & body]
  `(do
     (u/maybe-derive ~model ::model)
     (m/defmethod before-select ~model
       [~'&model ~args-binding]
       (cond->> (do ~@body)
         ~'next-method
         (~'next-method ~'&model)))))
(s/fdef define-before-select
  :args (s/cat :dispatch-value some?
               :bindings       (s/spec (s/cat :args :clojure.core.specs.alpha/binding-form))
               :body           (s/+ any?))
  :ret any?)
 
(ns toucan2.tools.before-update
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.connection :as conn]
   [toucan2.instance :as instance]
   [toucan2.log :as log]
   [toucan2.model :as model]
   [toucan2.pipeline :as pipeline]
   [toucan2.protocols :as protocols]
   [toucan2.realize :as realize]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(set! *warn-on-reflection* true)
(comment types/keep-me)
(derive ::select-for-before-update :toucan.query-type/select.instances.from-update)

Do before-update operations for side effects and transformations to a row (presumably a Toucan instance?) before applying an UPDATE operation.

(m/defmulti before-update
  {:arglists            '([model₁ row])
   :defmethod-arities   #{2}
   ;; work around https://github.com/camsaul/methodical/issues/142
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.model)}
  u/dispatch-on-first-arg)
(m/defmethod before-update :around :default
  [model row]
  (assert (map? row) (format "Expected a map row, got ^%s %s" (some-> row class .getCanonicalName) (pr-str row)))
  (log/debugf "before-update %s %s" model row)
  (let [result (next-method model row)]
    (assert (map? result) (format "%s for %s should return a map, got %s" `before-update model (pr-str result)))
    (log/debugf "[before-update] => %s" result)
    result))
(defn- changes->affected-pk-maps-rf [model changes]
  (assert (map? changes) (format "Expected changes to be a map, got %s" (pr-str changes)))
  (fn
    ([] {})
    ([m]
     (assert (map? m) (format "changes->affected-pk-maps-rf should have returned a map, got %s" (pr-str m)))
     m)
    ([changes->pks row]
     (assert (map? changes->pks))
     (assert (map? row) (format "%s expected a map row, got %s" `changes->affected-pk-maps (pr-str row)))
     ;; After going back and forth on this I've concluded that it's probably best to just realize the entire row here.
     ;; There are a lot of situations where we don't need to do this, but it means we have to step on eggshells
     ;; everywhere else in order to make things work nicely. Maybe we can revisit this in the future.
     (let [row         (realize/realize row)
           row         (merge row changes)
           row         (before-update model row)
           ;; if the `before-update` method returned a plain map then consider that to be the changes.
           ;; `protocols/changes` will return `nil` for non-instances. TODO -- does that behavior make sense? Clearly,
           ;; it's easy to use wrong -- it took me hours to figure out why something was working and that I needed to
           ;; make this change :sad:
           row-changes (if (instance/instance? row)
                         (protocols/changes row)
                         row)]
       (log/tracef "The following values have changed: %s" changes)
       (cond-> changes->pks
         (seq row-changes) (update row-changes (fn [pks]
                                                 (conj (set pks) (model/primary-key-values-map model row)))))))))
(defn- fetch-changes->pk-maps [model {:keys [changes], :as parsed-args} resolved-query]
  (not-empty
   (pipeline/transduce-query
    (changes->affected-pk-maps-rf model changes)
    ::select-for-before-update
    model
    parsed-args
    resolved-query)))

Fetch the matching rows based on original parsed-args; apply [[before-update]] to each. Return a new sequence of parsed args maps that should be used to perform 'replacement' update operations.

TODO -- this is sort of problematic since it breaks [[toucan2.tools.compile]]

(defn- apply-before-update-to-matching-rows
  [model {:keys [changes], :as parsed-args} resolved-query]
  (u/try-with-error-context ["apply before-update to matching rows" {::model model, ::changes changes}]
    (log/debugf "apply before-update to matching rows for %s" model)
    (when-let [changes->pk-maps (fetch-changes->pk-maps model parsed-args resolved-query)]
      (log/tracef "changes->pk-maps = %s" changes->pk-maps)
      (if (= (count changes->pk-maps) 1)
        ;; every row has the same exact changes: we only need to perform a single update, using the original
        ;; conditions.
        [(assoc parsed-args :changes (first (keys changes->pk-maps)))]
        ;; more than one set of changes: need to do multiple updates.
        (for [[changes pk-maps] changes->pk-maps
              pk-map            pk-maps]
          (assoc parsed-args :changes changes, :kv-args pk-map))))))
(m/defmethod pipeline/transduce-query [#_query-type     :toucan.query-type/update.*
                                       #_model          ::before-update
                                       #_resolved-query :default]
  "Apply [[toucan2.tools.before-update/before-update]] to matching rows. If multiple versions of `:changes` are produced
  as a result, recursively does an update for each version."
  [rf query-type model {::keys [doing-before-update?], :keys [changes], :as parsed-args} resolved-query]
  (cond
    doing-before-update?
    (next-method rf query-type model parsed-args resolved-query)
    (empty? changes)
    (next-method rf query-type model parsed-args resolved-query)
    :else
    (let [new-args-maps (apply-before-update-to-matching-rows model
                                                              (assoc parsed-args ::doing-before-update? true)
                                                              resolved-query)]
      (log/debugf "Doing recursive updates with new args maps %s" new-args-maps)
      (conn/with-transaction [_conn nil {:nested-transaction-rule :ignore}]
        (transduce
         (comp (map (fn [args-map]
                      (next-method rf query-type model args-map resolved-query)))
               (if (isa? query-type :toucan.result-type/pks)
                 cat
                 identity))
         rf
         new-args-maps)))))
(defmacro define-before-update [model [instance-binding] & body]
  `(do
     (u/maybe-derive ~model ::before-update)
     (m/defmethod before-update ~model
       [~'&model ~instance-binding]
       (cond->> (do ~@body)
         ~'next-method
         (~'next-method ~'&model)))))
(s/fdef define-before-update
  :args (s/cat :model    some?
               :bindings (s/spec (s/cat :instance :clojure.core.specs.alpha/binding-form))
               :body     (s/+ any?))
  :ret any?)

::before-update should intercept the query before after-update tries to upgrade the query for results transforms.

(m/prefer-method! #'pipeline/transduce-query
                  [:toucan.query-type/update.* ::before-update :default]
                  [:toucan2.tools.after/query-type :toucan2.tools.after/model :default])
 

Macros that can wrap a form and return the built query, compiled query, etc. without executing it.

(ns toucan2.tools.compile
  (:refer-clojure :exclude [compile])
  (:require
   [toucan2.pipeline :as pipeline]))

Impl for the [[compile]] macro. Do not use this directly.

(defn ^:no-doc -compile
  [thunk]
  (binding [pipeline/*transduce-execute* (fn [_rf _query-type _model compiled-query]
                                           compiled-query)]
    (thunk)))

Return the compiled query that would be executed by a form, rather than executing that form itself.

```clj (compile (delete/delete :table :id 1)) => ["DELETE FROM table WHERE ID = ?" 1] ```

(defmacro compile
  {:style/indent 0}
  [& body]
  `(-compile (^:once fn* [] ~@body)))

Impl for the [[build]] macro. Do not use this directly.

(defn ^:no-doc -build
  [thunk]
  (binding [pipeline/*compile* (fn [_query-type _model built-query]
                                 built-query)]
    (-compile thunk)))

Return the built query before compilation that would have been executed by body without compiling or executing it.

(defmacro build
  {:style/indent 0}
  [& body]
  `(-build (^:once fn* [] ~@body)))
(defn -resolved [thunk]
  (binding [pipeline/*build* (fn [_query-type _model _parsed-args resolved-query]
                               resolved-query)]
    (-build thunk)))

Return the resolved query and parsed args before building a query (e.g. before creating a Honey SQL query from the args passed to [[toucan2.select/select]] created by body without building a query, compiling it, or executing it.

(defmacro resolved
  {:style/indent 0}
  [& body]
  `(-resolved (^:once fn* [] ~@body)))

(defmacro parsed-args [& body] )

 
(ns toucan2.tools.debug
  (:require
   [toucan2.log :as log]
   [toucan2.pipeline :as pipeline]))
(defn- print-result [message result]
  (log/-pprint-doc (log/->Doc [(log/->Text message) result]))
  result)
(defn -debug [thunk]
  (binding [pipeline/*build*   (comp (partial print-result "\nBuilt:")
                                     (let [build* pipeline/*build*]
                                       (fn [query-type model parsed-args resolved-query]
                                         (print-result "\nParsed args:" parsed-args)
                                         (print-result "\nResolved query:" resolved-query)
                                         (build* query-type model parsed-args resolved-query))))
            pipeline/*compile* (comp (partial print-result "\nCompiled:") pipeline/*compile*)]
    (thunk)))

Simple debug macro. This is a placeholder until I come up with a more sophisticated version.

(defmacro debug
  {:style/indent 0}
  [& body]
  `(-debug (^:once fn* [] ~@body)))
 
(ns toucan2.tools.default-fields
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.honeysql2 :as t2.honeysql]
   [toucan2.log :as log]
   [toucan2.pipeline :as pipeline]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(comment types/keep-me)
(set! *warn-on-reflection* true)
(s/def ::default-field
  (s/or :keyword      keyword?
        :fn-and-alias (s/spec (s/cat :fn      ifn?
                                     :keyword keyword?))))
(s/def ::default-fields
  (s/coll-of ::default-field))

The default fields to return for a model model that derives from :toucan2.tools.default-fields/default-fields. You probably don't need to use this directly; use [[toucan2.tools.default-fields/define-default-fields]] instead.

(m/defmulti default-fields
  {:arglists            '([model])
   :defmethod-arities   #{1}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.model)}
  u/dispatch-on-first-arg)
(m/defmethod default-fields :around :default
  [model]
  (let [fields (next-method model)]
    (when (s/invalid? (s/conform ::default-fields fields))
      (throw (ex-info (format "Invalid default fields for %s: %s" (pr-str model) (s/explain-str ::default-fields fields))
                      (s/explain-data ::default-fields fields))))
    (log/debugf "Default fields for %s: %s" model fields)
    fields))
(defn- default-fields-xform [model]
  (let [field-fns (mapv (fn [[field-type v]]
                          (case field-type
                            :keyword      (fn [instance]
                                            [v (get instance v)])
                            :fn-and-alias (let [{k :keyword, f :fn} v]
                                            (fn [instance]
                                              [k (f instance)]))))
                        (s/conform ::default-fields (default-fields model)))]
    (map (fn [instance]
           (log/tracef "Selecting default-fields from instance")
           (into (empty instance) (map (fn [field-fn]
                                         (field-fn instance))
                                       field-fns))))))

Whether to skip applying default Fields because the query already includes explicit fields, e.g. :select for a Honey SQL query.

(def ^:dynamic *skip-default-fields*
  false)

TODO -- should we skip default fields for a Query that has top-level :union or :union-all?

(m/defmethod pipeline/transduce-query [#_query-type          :toucan.result-type/instances
                                       #_model               ::default-fields
                                       #_resolved-query-type clojure.lang.IPersistentMap]
  "Skip default fields behavior for Honey SQL queries that contain `:select`. Bind [[*skip-default-fields*]] to `true`."
  [rf query-type model parsed-args honeysql]
  (if (t2.honeysql/include-default-select? honeysql)
    (next-method rf query-type model parsed-args honeysql)
    (binding [*skip-default-fields* true]
      (log/debugf "Not adding default fields because query already contains `:select` or `:select-distinct`")
      (next-method rf query-type model parsed-args honeysql))))
(m/defmethod pipeline/results-transform [#_query-type :toucan.result-type/instances #_model ::default-fields]
  [query-type model]
  (log/debugf "Model %s has default fields" model)
  (cond
    *skip-default-fields*
    (next-method query-type model)
    ;; don't apply default fields for queries that specify other columns e.g. `(select [SomeModel :col])`
    (seq (:columns pipeline/*parsed-args*))
    (do
      (log/debugf "Not adding default fields transducer since query already has `:columns`")
      (next-method query-type model))
    ;; don't apply default fields for queries like [[toucan2.select/select-fn-set]] since they are already doing their
    ;; own transforms
    (isa? query-type :toucan.query-type/select.instances.fns)
    (do
      (log/debugf "Not adding default fields transducer since query type derives from :toucan.query-type/select.instances.fns")
      (next-method query-type model))
    ;; don't apply default fields for the recursive select done by before-update, because it busts things when we want
    ;; to update non-default fields =(
    ;;
    ;; See [[toucan2.tools.before-update-test/before-update-with-default-fields-test]]
    (isa? query-type :toucan2.tools.before-update/select-for-before-update)
    (do
      (log/debugf "Not adding default fields transducer since query type is done for the purposes of before-update")
      (next-method query-type model))
    :else
    (do
      (log/debugf "adding transducer to return default fields for %s" model)
      (let [xform (default-fields-xform model)]
        (comp xform
              (next-method query-type model))))))

default-fields should be done before [[toucan2.tools.after-select]] so that fields added by after select get preserved.

(m/prefer-method! #'pipeline/results-transform
                  [:toucan.result-type/instances ::default-fields]
                  [:toucan.result-type/instances :toucan2.tools.after-select/after-select])

default-fields should be done before [[toucan2.tools.after-update]] and [[toucan2.tools.after-insert]] so that fields added by those methods get preserved.

(m/prefer-method! #'pipeline/results-transform
                  [:toucan.result-type/instances ::default-fields]
                  [:toucan.result-type/instances :toucan2.tools.after/model])
(defmacro define-default-fields {:style/indent :defn} [model & body]
  `(do
     (u/maybe-derive ~model ::default-fields)
     (m/defmethod default-fields ~model [~'&model] ~@body)))
(s/fdef define-default-fields
  :args (s/cat :model some?
               :body  (s/+ any?))
  :ret any?)
 
(ns toucan2.tools.disallow
  (:require
   [methodical.core :as m]
   [toucan2.pipeline :as pipeline]))
(m/defmethod pipeline/build [#_query-type     :toucan.query-type/select.*
                             #_model          ::select
                             #_resolved-query :default]
  "Throw an Exception when trying to build a SELECT query for models deriving from `:toucan2.tools.disallow/select`."
  [_query-type model _parsed-args _resolved-query]
  (throw (UnsupportedOperationException. (format "You cannot select %s." model))))
(m/defmethod pipeline/build [#_query-type     :toucan.query-type/delete.*
                             #_model          ::delete
                             #_resolved-query :default]
  "Throw an Exception when trying to build a DELETE query for models deriving from `:toucan2.tools.disallow/delete`."
  [_query-type model _parsed-args _resolved-query]
  (throw (UnsupportedOperationException. (format "You cannot delete instances of %s." model))))
(m/defmethod pipeline/build [#_query-type     :toucan.query-type/insert.*
                             #_model          ::insert
                             #_resolved-query :default]
  "Throw an Exception when trying to build a INSERT query for models deriving from `:toucan2.tools.disallow/insert`."
  [_query-type model _parsed-args _resolved-query]
  (throw (UnsupportedOperationException. (format "You cannot create new instances of %s." model))))
(m/defmethod pipeline/build [#_query-type     :toucan.query-type/update.*
                             #_model          ::update
                             #_resolved-query :default]
  "Throw an Exception when trying to build a UPDATE query for models deriving from `:toucan2.tools.disallow/update`."
  [_query-type model _parsed-args _resolved-query]
  (throw (UnsupportedOperationException. (format "You cannot update a %s after it has been created." model))))
 

[[hydrate]] adds one or more keys to an instance or instances using various hydration strategies, usually using one of the existing keys in those instances. A typical use case would be to take a sequence of orders and add :user keys to them based on their values of the foreign key :user-id.

[[hydrate]] is how you use the hydration facilities; everything else in this namespace is only for extending hydration to support your models.

Toucan 2 ships with several hydration strategies out of the box:

Automagic Batched Hydration (via [[model-for-automagic-hydration]])

[[hydrate]] attempts to do a batched hydration where possible. If the key being hydrated is defined as one of some table's [[model-for-automagic-hydration]], hydrate will do a batched [[toucan2.select/select]] if a corresponding key (by default, the same key suffixed by -id) is found in the objects being batch hydrated. The corresponding key can be customized by implementing [[fk-keys-for-automagic-hydration]].

```clj (hydrate [{:userid 100}, {:userid 101}] :user) ```

Since :user is a hydration key for :models/User, a single [[toucan2.select/select]] will used to fetch Users:

```clj (db/select :models/User :id [:in #{100 101}]) ```

The corresponding Users are then added under the key :user.

Function-Based Batched Hydration (via [[batched-hydrate]] methods)

If the key can't be hydrated auto-magically with the appropriate [[model-for-automagic-hydration]], [[hydrate]] will attempt to do batched hydration if it can find a matching method for [[batched-hydrate]]. If a matching function is found, it is called with a collection of objects, e.g.

```clj (m/defmethod hydrate/batched-hydrate [:default :fields] [_model _k instances] (let [id->fields (get-some-fields instances)] (for [instance instances] (assoc instance :fields (get id->fields (:id instance)))))) ```

Simple Hydration (via [[simple-hydrate]] methods)

If the key is not eligible for batched hydration, [[hydrate]] will look for a matching [[simple-hydrate]] method. simple-hydrate is called with a single instance.

```clj (m/defmethod simple-hydrate [:default :dashboard] [_model _k {:keys [dashboard-id], :as instance}] (assoc instance :dashboard (select/select-one :models/Dashboard :toucan/pk dashboard-id))) ```

Hydrating Multiple Keys

You can hydrate several keys at one time:

```clj (hydrate {...} :a :b) -> {:a 1, :b 2} ```

Nested Hydration

You can do recursive hydration by listing keys inside a vector:

```clj (hydrate {...} [:a :b]) -> {:a {:b 1}} ```

The first key in a vector will be hydrated normally, and any subsequent keys will be hydrated inside the corresponding values for that key.

```clj (hydrate {...} [:a [:b :c] :e]) -> {:a {:b {:c 1} :e 2}} ```

Forcing Hydration

Normally, hydration is skipped if an instance already has a non-nil value for the key being hydrated, but you can override this behavior by implementing [[needs-hydration?]].

Flowchart

If you're digging in to the details, this is a flowchart of how hydration works:

``` hydrate ◄─────────────┐ │ │ ▼ │ hydrate-forms │ │ │ ▼ │ (recursively) hydrate-one-form │ │ │ keyword? ◄─┴─► sequence? │ │ │ │ ▼ ▼ │ hydrate-key hydrate-key-seq ─┘ │ ▼ (for each strategy) ◄────────┐ ::automagic-batched │ ::multimethod-batched │ ::multimethod-simple │ │ │ (try next strategy) ▼ │ can-hydrate-with-strategy? │ │ │ yes ◄──┴──► no ────────────┘ │ ▼ hydrate-with-strategy ```

(ns toucan2.tools.hydrate
  (:require
   [camel-snake-kebab.core :as csk]
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.log :as log]
   [toucan2.model :as model]
   [toucan2.protocols :as protocols]
   [toucan2.realize :as realize]
   [toucan2.select :as select]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(derive ::automagic-batched   ::strategy)
(derive ::multimethod-batched ::strategy)
(derive ::multimethod-simple  ::strategy)
(s/def ::dispatch-value.strategy
  (s/or
   :default  ::types/dispatch-value.default
   :strategy #(isa? % ::strategy)))
(s/def ::dispatch-value.hydration-key
  (some-fn keyword? symbol?))
(s/def ::dispatch-value.model-k
  (s/or :default ::types/dispatch-value.default
        :model-k (s/cat
                  :model ::types/dispatch-value.model
                  :k     ::dispatch-value.hydration-key)))
(s/def ::dispatch-value.model-k-model
  (s/or :default ::types/dispatch-value.default
        :model-k (s/cat
                  :model ::types/dispatch-value.model
                  :k     ::dispatch-value.hydration-key
                  :model ::types/dispatch-value.model)))
(s/def ::dispatch-value.model-strategy-k
  (s/or :default          ::types/dispatch-value.default
        :model-strategy-k (s/cat :model    ::types/dispatch-value.model
                                 :strategy ::dispatch-value.strategy
                                 :k        ::dispatch-value.hydration-key)))

Can we hydrate the key k in instances of model using a specific hydration strategy?

Normally you should never need to call this yourself. The only reason you would implement it is if you are implementing a custom hydration strategy.

(m/defmulti can-hydrate-with-strategy?
  {:arglists            '([model₁ strategy₂ k₃])
   :defmethod-arities   #{3}
   :dispatch-value-spec (s/nonconforming ::dispatch-value.model-strategy-k)}
  u/dispatch-on-first-three-args)

Hydrate the key k in instances of model using a specific hydration strategy.

Normally you should not call this yourself. The only reason you would implement this method is if you are implementing a custom hydration strategy.

(m/defmulti hydrate-with-strategy
  {:arglists            '([model strategy₁ k instances])
   :defmethod-arities   #{4}
   :dispatch-value-spec (s/nonconforming ::dispatch-value.strategy)}
  (fn [_model strategy _k _instances]
    strategy))

Whether an instance of model needs the key k to be hydrated. By default, this is true if (get instance k) is nil. You can override this if you want to force a key to always be re-hydrated even if it is already present.

(m/defmulti needs-hydration?
  {:arglists            '([model₁ k₂ instance])
   :defmethod-arities   #{3}
   :dispatch-value-spec (s/nonconforming ::dispatch-value.model-k)}
  u/dispatch-on-first-two-args)
(m/defmethod needs-hydration? :default
  [_model k instance]
  (nil? (get instance k)))

Automagic Batched Hydration (via :table-keys)

The model that should be used to automagically hydrate the key k in instances of original-model.

```clj (model-for-automagic-hydration :some-table :user) :-> :myapp.models/user ```

Dispatches off of the [[toucan2.protocols/dispatch-value]] (normally [[toucan2.protocols/model]]) of the instance being hydrated and the key k that we are attempting to hydrate. To hydrate the key k for any model, you can use :default in your defmethod dispatch value. Example implementation:

```clj ;; when hydrating the :user key for any model, hydrate with instances of the model :models/user ;; ;; By default, this will look for values of :user-id in the instances being hydrated and then fetch the instances of ;; :models/user with a matching :id (m/defmethod hydrate/model-for-automagic-hydration [:default :user] [_original-model _k] :models/user)

;; when hydrating the :user key for instances of :models/orders, hydrate with instances of :models/user (m/defmethod hydrate/model-for-automagic-hydration [:models/orders :user] [_original-model _k] :models/user) ```

Automagic hydration looks for the [[fk-keys-for-automagic-hydration]] in the instance being hydrated, and if they're all non-nil, fetches instances of [[model-for-automagic-hydration]] with the corresponding [[toucan2.model/primary-keys]]. Thus you might also want to implement

  • [[fk-keys-for-automagic-hydration]] for the model whose instances hydrating and the key being hydrated (e.g. :models/orders and :user)

  • [[toucan2.model/primary-keys]] for the model you want to hydrate with (e.g. :models/user)

Tips

  • You probably don't want to write an implementation for [<some-model> :default] or [:default :default], unless you want every key that we attempt to hydrate to be hydrated by that method.

TODO -- should this get called with instance rather than the instance's model? Should we support multiple possible models automagically hydrating the same key? For example an Emitter in Metabase can be a CardEmitter, that points to a Card, or a DashboardEmitter, that points to a Dashboard -- would it be possible to hydrate the same key in a sequence of mixed-type emitters?

(m/defmulti model-for-automagic-hydration
  {:arglists            '([original-model₁ k₂])
   :defmethod-arities   #{2}
   :dispatch-value-spec (s/nonconforming ::dispatch-value.model-k)}
  u/dispatch-on-first-two-args)
(m/defmethod model-for-automagic-hydration :default
  [_model _k]
  nil)
(m/defmethod can-hydrate-with-strategy? [#_model :default #_strategy ::automagic-batched #_k :default]
  [model _strategy dest-key]
  (boolean (model-for-automagic-hydration model dest-key)))

The keys in we should use when automagically hydrating the key dest-key in instances of original-model with instances hydrating-model.

```clj ;; when hydrating :user in an order with a user, fetch the user based on the value of the :user-id key ;; ;; This means we take the value of :user-id from our order and then fetch the user with the matching primary key ;; (by default, :id) (fk-keys-for-automagic-hydration :models/orders :user :models/user) => [:user-id] ```

The model that we are hydrating with (e.g. :models/user) is determined by [[model-for-automagic-hydration]].

By default [[fk-keys-for-automagic-hydration]] is just the key we're hydrating, with -id appended to it; e.g. if we're hydrating :user the default FK key to use is :user-id. *If this convention is fine, you do not need to implement this method.* If you want to do something that does not follow this convention, like hydrate a :user key based on values of :creator-id, you should implement this method.

Example implementation:

```clj ;; hydrate orders :user key using values of :creator-id (m/defmethod hydrate/fk-keys-for-automagic-hydration [:model/orders :user :default] [_original-model _dest-key _hydrating-model] [:creator-id]) ```

Tips

When implementing this method, you probably do not want to specialize on the hydrating-model (the third part of the dispatch value) -- the model to use is normally determined by [[model-for-automagic-hydration]]. The only time you might want to specialize on hydrating-model is you wanted to do something like

```clj ;; by default when we hydrate the :user key with a :models/user, use :creator-id as the source FK (m/defmethod hydrate/fk-keys-for-automagic-hydration [:default :model/orders :user :models/user] [_original-model _dest-key _hydrating-model] [:creator-id]) ```

(m/defmulti fk-keys-for-automagic-hydration
  {:arglists            '([original-model₁ dest-key₂ hydrating-model₃])
   :defmethod-arities   #{3}
   :dispatch-value-spec (s/nonconforming ::dispatch-value.model-k-model)}
  u/dispatch-on-first-three-args)
(m/defmethod fk-keys-for-automagic-hydration :default
  [_original-model dest-key _hydrated-key]
  ;; TODO -- this should probably use the key transform associated with the `original-model` -- if it's not using magic
  ;; maps this wouldn't work
  [(csk/->kebab-case (keyword (str (name dest-key) "-id")))])
(m/defmethod fk-keys-for-automagic-hydration :around :default
  [original-model dest-key hydrating-model]
  (assert (keyword? dest-key) "dest-key should be a keyword")
  (let [result (next-method original-model dest-key hydrating-model)]
    (when-not (and (sequential? result)
                   (seq result)
                   (every? keyword? result))
      (throw (ex-info (format "fk-keys-for-automagic-hydration should return a non-empty sequence of keywords. Got: %s"
                              (pr-str result))
                      {:original-model original-model
                       :dest-key       dest-key
                       :hydrating-model hydrating-model
                       :result         result})))
    result))
(defn- automagic-batched-hydration-add-fks [model dest-key instances fk-keys]
  (assert (seq fk-keys) "fk-keys cannot be empty")
  (let [get-fk-values (apply juxt fk-keys)]
    (for [instance instances]
      (if-not (needs-hydration? model dest-key instance)
        (do
          (log/tracef "Don't need to hydrate %s: %s does not need hydration" instance dest-key)
          instance)
        (do
          (log/tracef "Getting values of %s for instance" fk-keys)
          (let [fk-vals (get-fk-values instance)]
            (if (every? some? fk-vals)
              (do
                (log/tracef "Attempting to hydrate %s with values of %s %s" instance fk-keys fk-vals)
                (assoc instance ::fk fk-vals))
              (do
                (log/tracef "Skipping %s: values of %s are %s" instance fk-keys fk-vals)
                instance))))))))
(defn- automagic-batched-hydration-fetch-pk->instance [hydrating-model instances]
  (let [pk-keys (model/primary-keys hydrating-model)]
    (assert (pos? (count pk-keys)))
    (if-let [fk-values-set (not-empty (set (filter some? (map ::fk instances))))]
      (let [fk-values-set (if (= (count pk-keys) 1)
                            (into #{} (map first) fk-values-set)
                            fk-values-set)]
        (log/debugf "Fetching %s with PK columns %s values %s" hydrating-model pk-keys fk-values-set)
        ;; TODO -- not sure if we need to be realizing stuff here?
        (select/select-pk->fn realize/realize hydrating-model :toucan/pk [:in fk-values-set]))
      (log/debugf "Not hydrating %s because no instances have non-nil FK values" hydrating-model))))
(defn- do-automagic-batched-hydration [dest-key instances pk->fetched-instance]
  (log/debugf "Attempting to hydrate %s instances out of %s" (count (filter ::fk instances)) (count instances))
  (for [instance instances]
    (if-not (::fk instance)
      ;; If `::fk` doesn't exist for this instance...
      (if (contains? instance dest-key)
        ;; don't stomp on any existing values of `dest-key` if one is already present.
        instance
        ;; if no value is present, `assoc` `nil`.
        (assoc instance dest-key nil))
      ;; otherwise if we DO have an `::fk`, remove it and add the hydrated key in its place
      (let [fk-vals          (::fk instance)
            ;; convert fk to from [id] to id if it only has one key. This is what [[toucan2.select/select-pk->fn]]
            ;; returns.
            fk-vals          (if (= (count fk-vals) 1)
                               (first fk-vals)
                               fk-vals)
            fetched-instance (get pk->fetched-instance fk-vals)]
        (log/tracef "Hydrate %s %s with %s" dest-key [instance] (or fetched-instance
                                                                    "nil (no matching fetched instance)"))
        (-> (dissoc instance ::fk)
            (assoc dest-key fetched-instance))))))
(m/defmethod hydrate-with-strategy ::automagic-batched
  [model _strategy dest-key instances]
  (u/try-with-error-context ["automagic batched hydration" {::model model, ::dest-key dest-key}]
                            (let [hydrating-model      (model-for-automagic-hydration model dest-key)
                                  _                    (log/debugf "Hydrating %s key %s with instances from %s" (or model "map") dest-key hydrating-model)
                                  fk-keys              (fk-keys-for-automagic-hydration model dest-key hydrating-model)
                                  _                    (log/debugf "Hydrating with FKs %s" fk-keys)
                                  instances                 (automagic-batched-hydration-add-fks model dest-key instances fk-keys)
                                  pk->fetched-instance (automagic-batched-hydration-fetch-pk->instance hydrating-model instances)]
                              (log/debugf "Fetched %s instances of %s" (count pk->fetched-instance) hydrating-model)
                              (do-automagic-batched-hydration dest-key instances pk->fetched-instance))))

Method-Based Batched Hydration (using impls of [[batched-hydrate]])

Hydrate the key k in one or more instances of model. Implement this method to support batched hydration for the key k. Example implementation:

```clj ;; This method defines a batched hydration strategy for the :bird-type key for all models. (m/defmethod hydrate/batched-hydrate [:default :is-bird?] [_model _k instances] ;; fetch a set of all the non-nil bird IDs in instances. (let [bird-ids (into #{} (comp (map :bird-id) (filter some?)) instances) ;; if bird-ids is non-empty, fetch a map of bird ID => bird type bird-id->bird-type (when (seq bird-ids) (select/select-pk->fn :bird-type :models/bird :id [:in bird-ids]))] ;; for each instance add a :bird-type key. (for [instance instances] (assoc instance :bird-type (get bird-id->bird-type (:bird-id instance)))))) ```

Batched hydration implementations should try to be efficient, e.g. minimizing the number of database calls made rather than doing one call per instance. If you just want to hydrate each instance independently, implement [[simple-hydrate]] instead. If you are hydrating entire instances of some other model, consider setting up automagic batched hydration using [[model-for-automagic-hydration]] and possibly [[fk-keys-for-automagic-hydration]].

(m/defmulti batched-hydrate
  {:arglists            '([model₁ k₂ instances])
   :defmethod-arities   #{3}
   :dispatch-value-spec (s/nonconforming ::dispatch-value.model-k)}
  u/dispatch-on-first-two-args)
(m/defmethod can-hydrate-with-strategy? [#_model :default #_strategy ::multimethod-batched #_k :default]
  [model _strategy k]
  (some? (m/effective-method batched-hydrate (m/dispatch-value batched-hydrate model k))))

The basic strategy behind batched hydration is thus:

  1. Take the sequence of instances and "annotate" them, recording whether they :needs-hydration?

  2. Take all the instances that :needs-hydration? and do [[batched-hydrate]] for them

  3. Go back thru the sequence of annotated instances, and each time we encounter an instance marked :needs-hydration?, replace it with the first hydrated instance; repeat this process until we have spliced all the hydrated instances back in.

Merge the annotated instances as returned by [[annotate-instances]] and the hydrated instances as returned by [[hydrate-annotated-instances]] back into a single un-annotated sequence.

(merge-hydrated-instances [{:needs-hydration? false, :instance {:x 1, :y 1}} {:needs-hydration? false, :instance {:x 2, :y 2}} {:needs-hydration? true, :instance {:x 3}} {:needs-hydration? true, :instance {:x 4}} {:needs-hydration? false, :instance {:x 5, :y 5}} {:needs-hydration? true, :instance {:x 6}}] [{:x 3, :y 3} {:x 4, :y 4} {:x 6, :y 6}]) => [{:x 1, :y 1} {:x 2, :y 2} {:x 3, :y 3} {:x 4, :y 4} {:x 5, :y 5} {:x 6, :y 6}]

(defn- merge-hydrated-instances
  [annotated-instances hydrated-instances]
  (loop [acc [], annotated-instances annotated-instances, hydrated-instances hydrated-instances]
    (if (empty? hydrated-instances)
      (concat acc (map :instance annotated-instances))
      (let [[not-hydrated [_needed-hydration & more]] (split-with (complement :needs-hydration?) annotated-instances)]
        (recur (vec (concat acc (map :instance not-hydrated) [(first hydrated-instances)]))
               more
               (rest hydrated-instances))))))
(defn- annotate-instances [model k instances]
  (for [instance instances]
    {:needs-hydration? (needs-hydration? model k instance)
     :instance         instance}))
(defn- hydrate-annotated-instances [model k annotated-instances]
  (when-let [instances-that-need-hydration (not-empty (map :instance (filter :needs-hydration? annotated-instances)))]
    (batched-hydrate model k instances-that-need-hydration)))
(m/defmethod hydrate-with-strategy ::multimethod-batched
  [model _strategy k instances]
  (let [annotated-instances (annotate-instances model k instances)
        hydrated-instances  (hydrate-annotated-instances model k annotated-instances)]
    (merge-hydrated-instances annotated-instances hydrated-instances)))

Method-Based Simple Hydration (using impls of [[simple-hydrate]])

Implementations should return a version of map instance with the key k added.

TODO -- better dox

(m/defmulti simple-hydrate
  {:arglists            '([model₁ k₂ instance])
   :defmethod-arities   #{3}
   :dispatch-value-spec (s/nonconforming ::dispatch-value.model-k)}
  u/dispatch-on-first-two-args)
(defn- simple-hydrate* [model k instance]
  (u/try-with-error-context ["simple hydrate" {:model model, :k k, :instance instance}]
    (simple-hydrate model k instance)))
(m/defmethod can-hydrate-with-strategy? [#_model :default #_strategy ::multimethod-simple #_k :default]
  [model _strategy k]
  (some? (m/effective-method simple-hydrate (m/dispatch-value simple-hydrate model k))))
(m/defmethod hydrate-with-strategy ::multimethod-simple
  [model _strategy k instances]
  ;; TODO -- consider whether we should optimize this a bit and cache the methods we use so we don't have to go thru
  ;; multimethod dispatch on every instance.
  (for [instance instances]
    (when instance
      ;; only hydrate the key if it's not non-nil.
      (cond->> instance
        (needs-hydration? model k instance)
        (simple-hydrate* model k)))))

Hydration Using All Strategies

(defn- strategies []
  (keys (m/primary-methods hydrate-with-strategy)))

Determine the appropriate hydration strategy to hydrate the key k in instances of model.

(defn ^:no-doc hydration-strategy
  [model k]
  (some
   (fn [strategy]
     (when (can-hydrate-with-strategy? model strategy k)
       strategy))
   (strategies)))

Primary Hydration Fns

(declare hydrate)

TODO -- consider renaming this to *error-on-unknown-key-override*

(def ^:dynamic *error-on-unknown-key* nil)

Whether hydration should error when it encounters a key that has no hydration methods associated with it (default: false). Global default value. You can bind [[error-on-unknown-key]] to temporarily override this value.

(defonce 
  global-error-on-unknown-key
  (atom false))
(defn ^:no-doc error-on-unknown-key? []
  (if (some? *error-on-unknown-key*)
    *error-on-unknown-key*
    @global-error-on-unknown-key))
(defn- hydrate-key
  [model instances k]
  (if-let [strategy (hydration-strategy model k)]
    (u/try-with-error-context ["hydrate key" {:model model, :key k, :strategy strategy}]
                              (log/debugf "Hydrating %s %s with strategy %s" (or model "map") k strategy)
                              (hydrate-with-strategy model strategy k instances))
    (do
      (log/warnf "Don't know how to hydrate %s for model %s instances %s" k model (take 1 instances))
      (when (error-on-unknown-key?)
        (throw (ex-info (format "Don't know how to hydrate %s" (pr-str k))
                        {:model model, :instances instances, :k k})))
      instances)))

Hydrate a nested hydration form (vector) by recursively calling [[hydrate]].

(defn- hydrate-key-seq
  [results [k & nested-keys :as coll]]
  (when-not (seq nested-keys)
    (throw (ex-info (str (format "Invalid hydration form: replace %s with %s. Vectors are for nested hydration." coll k)
                         " There's no need to use one when you only have a single key.")
                    {:invalid-form coll})))
  (let [results                     (hydrate results k)
        newly-hydrated-values       (map k results)
        recursively-hydrated-values (apply hydrate newly-hydrated-values nested-keys)]
    (map
     (fn [result v]
       (if (and result (some? v))
         (assoc result k v)
         result))
     results
     recursively-hydrated-values)))
(declare hydrate-one-form)

Convert a collection coll into a flattened sequence of maps with :item and :path.

```clj (flatten-collection [[{:a 1}]]) => [{:path [], :item []} {:path [0], :item []} {:path [0 0], :item {:a 1}}] ```

(defn- flatten-collection
  ([coll]
   (flatten-collection [] coll))
  ([path coll]
   (if (sequential? coll)
     (into [{:path path, :item []}]
           (comp (map-indexed (fn [i x]
                                (flatten-collection (conj path i) x)))
                 cat)
           coll)
     [{:path path, :item coll}])))

Take a sequence of items flattened by [[flatten-collection]] and restore them to their original shape.

(defn- unflatten-collection
  [flattened]
  (reduce
   (fn [acc {:keys [path item]}]
     (if (= path [])
       item
       (assoc-in acc path item)))
   []
   flattened))
(defn- hydrate-sequence-of-sequences [model coll k]
  (let [flattened (flatten-collection coll)
        items     (for [{:keys [item path]} flattened
                        :when (map? item)]
                    (vary-meta item assoc ::path path))
        hydrated (hydrate-one-form model items k)]
    (unflatten-collection (concat flattened
                                  (for [item hydrated]
                                    (do
                                      (assert (::path (meta item)))
                                      {:item item, :path (::path (meta item))}))))))

Hydrate for a single hydration key or form k.

(defn- hydrate-one-form
  [model results k]
  (log/debugf "hydrate %s for model %s instances %s" k model (take 1 results))
  (cond
    (and (sequential? results)
         (empty? results))
    results
    ;; check whether the first non-nil result is a sequence. If it is, then hydrate sequences-of-sequences
    (sequential? (some (fn [result]
                         (when (some? result)
                           result))
                       results))
    (hydrate-sequence-of-sequences model results k)
    (keyword? k)
    (hydrate-key model results k)
    (sequential? k)
    (hydrate-key-seq results k)
    :else
    (throw (ex-info (format "Invalid hydration form: %s. Expected keyword or sequence." k)
                    {:invalid-form k}))))

Hydrate many hydration forms across a sequence of results by recursively calling [[hydrate-one-form]].

(defn- hydrate-forms
  [model results & forms]
  (reduce (partial hydrate-one-form model) results forms))

Given an arbitrarily nested sequence coll, continue unnesting the sequence until we get a non-sequential first item.

```clj (unnest-first-result [:a :b]) => :a (unnest-first-result [[:a]]) => :a (unnest-first-result [[[:a]]]) => :a ```

(defn- unnest-first-result
  [coll]
  (->> (iterate first coll)
       (take-while sequential?)
       last
       first))

Public Interface

Hydrate the keys ks in a single instance or sequence of instances. See [[toucan2.tools.hydrate]] for more information.

(defn hydrate
  ;; no keys -- no-op
  ([instance-or-instances]
   instance-or-instances)
  ([instance-or-instances & ks]
   (u/try-with-error-context ["hydrate" {:what instance-or-instances, :keys ks}]
     (cond
       (not instance-or-instances)
       nil
       (and (sequential? instance-or-instances)
            (empty? instance-or-instances))
       instance-or-instances
       ;; sequence of instances
       (sequential? instance-or-instances)
       (let [model (protocols/model (unnest-first-result instance-or-instances))]
         (apply hydrate-forms model instance-or-instances ks))
       ;; not sequential
       :else
       (first (apply hydrate-forms (protocols/model instance-or-instances) [instance-or-instances] ks))))))
 
(ns toucan2.tools.identity-query
  (:require
   [methodical.core :as m]
   [pretty.core :as pretty]
   [toucan2.connection :as conn]
   [toucan2.instance :as instance]
   [toucan2.log :as log]
   [toucan2.pipeline :as pipeline]
   [toucan2.realize :as realize]))
(set! *warn-on-reflection* true)
(defrecord ^:no-doc IdentityQuery [rows]
  pretty/PrettyPrintable
  (pretty [_this]
    (list `identity-query rows))
  realize/Realize
  (realize [_this]
    (realize/realize rows))
  clojure.lang.IReduceInit
  (reduce [_this rf init]
    (log/debugf "reduce IdentityQuery rows")
    (reduce rf init rows)))

A queryable that returns reducible-rows as-is without compiling anything or running anything against a database. Good for mocking stuff.

```clj (def parrot-query (identity-query [{:id 1, :name "Parroty"} {:id 2, :name "Green Friend"}]))

(select/select ::parrot parrot-query) => [(instance ::parrot {:id 1, :name "Parroty"}) (instance ::parrot {:id 2, :name "Green Friend"})] ```

(defn identity-query
  [reducible-rows]
  (->IdentityQuery reducible-rows))

(m/defmethod pipeline/transduce-execute [#query-type :default #model :default #_query IdentityQuery] [rf _query-type model {:keys [rows], :as _query}] (log/debugf "transduce IdentityQuery rows %s" rows) (transduce (if model (map (fn [result-row] (instance/instance model result-row))) identity) rf rows))

Not sure I understand how you're supposed to get to these points anyway

(m/defmethod pipeline/compile [#_query-type :default #_model :default #_query IdentityQuery]
  [_query-type _model query]
  query)
(m/defmethod pipeline/build :around [#_query-type :default #_model :default #_query IdentityQuery]
  "This is an around method so we can intercept anything else that might normally be considered a more specific method
  when it dispatches off of more-specific values of `query-type`."
  [_query-type _model _parsed-args resolved-query]
  resolved-query)
(m/defmethod pipeline/transduce-query [#_query-type     :default
                                       #_model          IdentityQuery
                                       #_resolved-query :default]
  "Allow using an identity query as an 'identity model'."
  [rf _query-type model _parsed-args _resolved-query]
  (transduce identity rf model))

Identity connection

(deftype ^:no-doc IdentityConnection []
  pretty/PrettyPrintable
  (pretty [_this]
    (list `->IdentityConnection)))
(m/defmethod conn/do-with-connection IdentityConnection
  [connectable f]
  {:pre [(ifn? f)]}
  (f connectable))
(m/defmethod conn/do-with-transaction IdentityConnection
  [connectable _options f]
  {:pre [(ifn? f)]}
  (f connectable))
(m/defmethod pipeline/transduce-execute-with-connection [#_conn IdentityConnection #_query-type :default #_model :default]
  [rf _conn _query-type model id-query]
  (assert (instance? IdentityQuery id-query))
  (binding [conn/*current-connectable* nil]
    (transduce
     (map (fn [row]
            (if (map? row)
              (instance/instance model row)
              row)))
     rf
     (:rows id-query))))
(m/defmethod pipeline/transduce-query [#_query-type     :default
                                       #_model          :default
                                       #_resolved-query IdentityQuery]
  [rf query-type model parsed-args resolved-query]
  (binding [conn/*current-connectable* (->IdentityConnection)]
    (next-method rf query-type model parsed-args resolved-query)))
 
(ns toucan2.tools.named-query
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.pipeline :as pipeline]
   [toucan2.types :as types]))

Helper for defining 'named' queries.

```clj ;; define a custom query ::my-count that you can then use with select and the like (define-named-query ::my-count {:select [:%count.*], :from [(keyword (model/table-name model))]})

(select :model/user ::my-count) ```

This doesn't NEED to be a macro but having the definition live in the namespace it was defined in is useful for stack trace purposes. Also it lets us do validation with the spec below.

(defmacro define-named-query
  {:style/indent 1}
  ([query-name resolved-query]
   `(define-named-query ~query-name :default :default ~resolved-query))
  ([query-name query-type model resolved-query]
   `(m/defmethod pipeline/resolve [~query-type ~model ~query-name]
      "Created by [[toucan2.tools.named-query/define-named-query]]."
      [~'&query-type ~'&model ~'&unresolved-query]
      ~resolved-query)))
(s/fdef define-named-query
  :args (s/alt :2-arity (s/cat :query-name     (every-pred keyword? namespace)
                               :resolved-query some?)
               :4-arity (s/cat :query-name     (every-pred keyword? namespace)
                               :query-type     (s/alt :query-type types/query-type?
                                                      :default    #{:default})
                               :model          some?
                               :resolved-query some?))
  :ret any?)
 
(ns toucan2.tools.simple-out-transform
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.instance :as instance]
   [toucan2.pipeline :as pipeline]))

TODO -- I'm not really convinced this is worth it at all. It's used in exactly one place =(

(defn -xform [f]
  (map (fn [instance]
         (let [instance (f instance)]
           (cond-> instance
             (instance/instance? instance)
             instance/reset-original)))))
(defmacro define-out-transform
  {:style/indent :defn}
  [[query-type model-type] [instance-binding] & body]
  `(m/defmethod pipeline/results-transform [~query-type ~model-type]
     [~'&query-type ~'&model]
     (let [xform# (-xform (fn [~instance-binding] ~@body))]
       (comp xform#
             (~'next-method ~'&query-type ~'&model)))))
(s/fdef define-out-transform
  :args (s/cat :dispatch-value (s/spec (s/cat :query-type keyword?
                                              :model-type any?))
               :bindings (s/spec (s/cat :row :clojure.core.specs.alpha/binding-form))
               :body     (s/+ any?))
  :ret any?)
(comment
  (define-out-transform [:toucan.query-type/select.instances ::venues]
    [row]
    (assoc row :neat true)))
 
(ns toucan2.tools.transformed
  (:require
   [better-cond.core :as b]
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [methodical.impl.combo.operator :as m.combo.operator]
   [toucan2.instance :as instance]
   [toucan2.log :as log]
   [toucan2.model :as model]
   [toucan2.pipeline :as pipeline]
   [toucan2.protocols :as protocols]
   [toucan2.query :as query]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(set! *warn-on-reflection* true)
(s/def ::transforms-map.direction->fn
  (s/map-of #{:in :out}
            ifn?))
(s/def ::transforms-map.column->direction
  (s/map-of keyword?
            ::transforms-map.direction->fn))
(defn- validate-transforms-map [transforms-map]
  (when (and transforms-map
             (s/invalid? (s/conform ::transforms-map.column->direction transforms-map)))
    (throw (ex-info (format "Invalid deftransforms map: %s"
                            (s/explain-str ::transforms-map.column->direction transforms-map))
                    (s/explain-data ::transforms-map.column->direction transforms-map)))))

combine the results of all matching methods into one map.

(m.combo.operator/defoperator ::merge-transforms
  [method-fns invoke]
  (transduce
   (map invoke)
   (fn
     ([]
      {})
     ([m]
      (validate-transforms-map m)
      m)
     ([m1 m2]
      ;; for the time being keep the values from map returned by the more-specific method in preference to the ones
      ;; returned by the less-specific methods.
      ;;
      ;; TODO -- we should probably throw an error if one of the transforms is stomping on the other.
      (merge-with merge m2 m1)))
   method-fns))

Return a map of

```clj {column-name {:in , :out }} ```

For a given model, all matching transforms are combined with merge-with merge in an indeterminate order, so don't try to specify multiple transforms for the same column in the same direction for a given model -- compose your transform functions instead if you want to do that. See [[toucan2.tools.transformed/deftransforms]] for more info.

(defonce ^{:doc 
           :arglists '([model₁])}
  transforms
  ;; TODO -- this has to be uncached for now because of https://github.com/camsaul/methodical/issues/98
  (m/uncached-multifn
   (m/standard-multifn-impl
    (m.combo.operator/operator-method-combination ::merge-transforms)
    ;; TODO -- once https://github.com/camsaul/methodical/issues/97 is implemented, use that.
    (m/standard-dispatcher u/dispatch-on-first-arg)
    (m/standard-method-table))
   {:ns                  *ns*
    :name                `transforms
    :defmethod-arities   #{1}
    :dispatch-value-spec (s/nonconforming ::types/dispatch-value.model)}))

I originally considered walking and transforming the HoneySQL, but decided against it because it's too ambiguous. It's too hard to tell if

[:= :id :col]

means

A) :id is a column identifier key, and :col is a value for it that we should transform B) :col is a column identifier key, and :id is a value for it that we should transform C) :id is a column identifier key, but :col is just a reference to another column, and we shouldn't transform it D) :col is a column identifier key, but :id is just a reference to another column, and we shouldn't transform it

It's also hard to know what are the "values" of every different type of filter clause (including custom ones we don't know about). I think leaving HoneySQL as an outlet to bypass type transforms makes sense for now. This also avoids locking us in to HoneySQL too much

(defn- transform-condition-value [xform v]
  (cond
    (sequential? v)
    (into [(first v)]
          (map (fn xform* [v]
                 (if (or (sequential? v)
                         (set? v))
                   (mapv xform* v)
                   (xform v))))
          (rest v))
    ;; only apply xform if the value is non-nil.
    (some? v)
    (xform v)
    :else
    nil))

Get the [[transforms]] functions for a model in a either the :in or :out direction; wrap the functions in try-catch forms so we can meaningful error messages if they fail, and so that the transform is skipped for nil values.

(defn- wrapped-transforms
  [model direction]
  (u/try-with-error-context ["calculate transforms" {::model model, ::direction direction}]
    (when-let [k->direction->transform (not-empty (transforms model))]
      ;; make the transforms map an instance so we can get appropriate magic map behavior when looking for the
      ;; appropriate transform for a given key.
      (instance/instance
       model
       (into {} (for [[k direction->xform] k->direction->transform
                      :let                 [xform (get direction->xform direction)]
                      :when                xform]
                  [k (fn xform-fn [v]
                       (if-not (some? v)
                         v
                         (u/try-with-error-context ["apply transform" {::transforms k->direction->transform
                                                                       ::model      model
                                                                       ::k          k
                                                                       ::v          v
                                                                       ::xform      xform}]
                           (xform v))))]))))))
(defn- in-transforms [model]
  (wrapped-transforms model :in))

this is just here so we can intercept low-level calls to [[query/apply-kv-arg]] instead of needing to know how to handle stuff like :toucan/pk ourselves

TODO -- this whole thing is still busted a little bit I think because it's assuming that everything in kv-args is a CONDITION (goes in a WHERE clause) but I guess if we're intercepting the bottom-level calls to [[query/apply-kv-arg]] at that point it IS a condition. We need to verify this tho and test it with some sort of thing like the custom ::limit thing

TODO This name is WACK

(defrecord ^:no-doc RecordTypeForInterceptingApplyKVArgCalls [])
(m/defmethod query/apply-kv-arg [#_model :default
                                 #_query RecordTypeForInterceptingApplyKVArgCalls
                                 #_k     :default]
  [model _query k v]
  (let [v (if-let [xform (get (in-transforms model) k)]
            (transform-condition-value xform v)
            v)]
    [k v]))
(def ^:private ^:dynamic *skip-in-transforms* false)
(m/defmethod query/apply-kv-arg [#_model ::transformed.model
                                 #_query :default
                                 #_k     :default]
  [model query k v]
  (if (or (instance? RecordTypeForInterceptingApplyKVArgCalls query)
          *skip-in-transforms*
          (nil? v))
    (next-method model query k v)
    (let [[k v*] (query/apply-kv-arg model (->RecordTypeForInterceptingApplyKVArgCalls) k v)]
      #_(printf "Intercepted apply-kv-arg %s %s => %s\n" k (pr-str v) (pr-str v*))
      (binding [*skip-in-transforms* true]
        (next-method model query k v*)))))

after select (or other things returning instances)

(defn- apply-result-row-transform [instance k xform]
  (assert (map? instance)
          (format "%s expected map rows, got %s" `apply-result-row-transform (pr-str instance)))
  ;; The "Special Optimizations" below *should* be the default case, but if some other aux methods are in place or
  ;; custom impls it might not be; things should still work normally either way.
  ;;
  ;; Special Optimization 1: if `instance` is an `IInstance`, and original and current are the same object, this only
  ;; applies `xform` once.
  (instance/update-original-and-current
   instance
   (fn [row]
     ;; Special Optimization 2: if the underlying original/current maps of `instance` are instances of something
     ;; like [[toucan2.jdbc.row/->TransientRow]] we can do a 'deferred update' that only applies the transform if and
     ;; when the value is realized.
     (log/tracef "Transform %s %s" k (get row k))
     (u/try-with-error-context ["Transform result column" {::k k, ::xform xform, ::row row}]
       (protocols/deferrable-update row k xform)))))
(defn- out-transforms [model]
  (wrapped-transforms model :out))

Given a column and a transform function xform, return a function with the signature

```clj (f row) ```

that will apply that transform to that column if the row contains that column.

(defn- apply-result-row-transform-fn
  [column xform]
  (fn [instance]
    (cond-> instance
      (contains? instance column) (apply-result-row-transform column xform))))

Given a map of column key -> transform function, return a function with the signature

```clj (f row) ```

That can be called on each instance returned in the results.

(defn- result-row-transform-fn
  [k->transform]
  {:pre [(map? k->transform) (seq k->transform)]}
  (reduce
   (fn [f [k xform]]
     (comp (apply-result-row-transform-fn k xform)
           f))
   identity
   k->transform))

Return a transducer to transform rows of model using its [[out-transforms]].

(defn- transform-result-rows-transducer
  [model]
  (if-let [k->transform (not-empty (out-transforms model))]
    (map (let [f (result-row-transform-fn k->transform)]
           (fn [row]
             (assert (map? row) (format "Expected map row, got ^%s %s" (some-> row class .getCanonicalName) (pr-str row)))
             (log/tracef "Transform %s row %s" model row)
             (let [result (u/try-with-error-context ["transform result row" {::model model, ::row row}]
                            (f row))]
               (log/tracef "[transform] => %s" result)
               result))))
    identity))
(m/defmethod pipeline/build [#_query-type     :toucan.query-type/select.instances-from-pks
                             #_model          ::transformed.model
                             #_resolved-query :default]
  "Don't try to transform stuff when we're doing SELECT directly with PKs (e.g. to fake INSERT returning instances), We're
  not doing transforms on the way out so we don't need to do them on the way in."
  [query-type model parsed-args resolved-query]
  (binding [*skip-in-transforms* true]
    (next-method query-type model parsed-args resolved-query)))
(m/defmethod pipeline/results-transform [#_query-type :toucan.result-type/instances #_model ::transformed.model]
  [query-type model]
  (if (isa? query-type :toucan.query-type/select.instances-from-pks)
    (next-method query-type model)
    (let [xform (transform-result-rows-transducer model)]
      (comp xform
            (next-method query-type model)))))

before update

(defn- transform-update-changes [m k->transform]
  {:pre [(map? k->transform) (seq k->transform)]}
  (into {} (for [[k v] m]
             [k (when (some? v)
                  (if-let [xform (get k->transform k)]
                    (xform v)
                    v))])))
(m/defmethod pipeline/build [#_query-type     :toucan.query-type/update.*
                             #_model          ::transformed.model
                             #_resolved-query :default]
  "Apply transformations to the `changes` map in an UPDATE query."
  [query-type model {:keys [changes], :as parsed-args} resolved-query]
  (b/cond
    (not (map? changes))
    (next-method query-type model parsed-args resolved-query)
    :let [k->transform (not-empty (in-transforms model))]
    (not k->transform)
    (next-method query-type model parsed-args resolved-query)
    (let [parsed-args (update parsed-args :changes transform-update-changes k->transform)]
      (next-method query-type model parsed-args resolved-query))))

before insert

(defn- transform-insert-rows [[first-row :as rows] k->transform]
  {:pre [(map? first-row) (map? k->transform)]}
  (let [x-forms (for [[k transform] k->transform]
                  (fn [row]
                    (if (some? (get row k))
                      (update row k transform)
                      row)))
        x-form  (apply comp x-forms)]
   (map x-form rows)))
(m/defmethod pipeline/build [#_query-type     :toucan.query-type/insert.*
                             #_model          ::transformed.model
                             #_resolved-query :default]
  [query-type model parsed-args resolved-query]
  (assert (isa? model ::transformed.model))
  (b/cond
    (::already-transformed? parsed-args)
    (next-method query-type model parsed-args resolved-query)
    :let [k->transform (in-transforms model)]
    (empty? k->transform)
    (next-method query-type model parsed-args resolved-query)
    :else
    (u/try-with-error-context ["apply in transforms before inserting rows" {::query-type  query-type
                                                                            ::model       model
                                                                            ::parsed-args parsed-args
                                                                            ::transforms  k->transform}]
      (log/debugf "Apply %s transforms to %s" k->transform parsed-args)
      (let [parsed-args    (cond-> parsed-args
                             (seq (:rows parsed-args))
                             (update :rows transform-insert-rows k->transform)
                             true
                             (assoc ::already-transformed? true))
            resolved-query (cond-> resolved-query
                             (seq (:rows resolved-query))
                             (update :rows transform-insert-rows k->transform))]
        (next-method query-type model parsed-args resolved-query)))))
(m/defmethod pipeline/results-transform [#_query-type :toucan.query-type/insert.pks #_model ::transformed.model]
  "Transform results of `insert!` returning PKs."
  [query-type model]
  (let [pk-keys (model/primary-keys model)
        xform   (comp
                 ;; 1. convert result PKs to a map of PK key -> value
                 (map (fn [pk-or-pks]
                        (log/tracef "convert result PKs %s to map of PK key -> value" pk-or-pks)
                        (let [m (zipmap pk-keys (if (sequential? pk-or-pks)
                                                  pk-or-pks
                                                  [pk-or-pks]))]
                          (log/tracef "=> %s" pk-or-pks)
                          m)))
                 ;; 2. transform the PK results using the model's [[out-transforms]]
                 (transform-result-rows-transducer model)
                 ;; 3. Now flatten the maps of PK key -> value back into plain PK values or vectors of plain PK values
                 ;; (if the model has a composite primary key)
                 (map (let [f (if (= (count pk-keys) 1)
                                (first pk-keys)
                                (juxt pk-keys))]
                        (fn [row]
                          (log/tracef "convert PKs map back to flat PKs")
                          (let [row (f row)]
                            (log/tracef "=> %s" row)
                            row)))))]
    (comp xform
          (next-method query-type model))))

[[deftransforms]]

Define type transforms to use for a specific model. transforms should be a map of

```clj {:column-name {:in , :out }} ```

:in transforms are applied to values going over the wire to the database; these generally only applied to values passed at or near the top level to various functions; don't expect Toucan 2 to parse your SQL to find out which parameter corresponds to what in order to apply transforms or to apply transforms inside JOINS in hand-written HoneySQL. That said, unless you're doing something weird your transforms should generally get applied.

:out transforms are applied to values coming out of the database; since nothing weird really happens there this is done consistently.

Transform functions for either case are skipped for nil values.

Example:

```clj (deftransforms :models/user {:type {:in name, :out keyword}}) ```

You can also define transforms independently, and derive a model from them:

```clj (deftransforms ::type-keyword {:type {:in name, :out keyword}})

(derive :models/user ::type-keyword) (derive :models/user ::some-other-transform) ```

Don't derive a model from multiple [[deftransforms]] for the same key in the same direction.

When multiple transforms match a given model they are combined into a single map of transforms with `merge-with merge`. If multiple transforms match a given column in a given direction, only one of them will be used; you should assume which one is used is indeterminate. (This may be made an error, or at least a warning, in the future.)

Until upstream issue https://github.com/camsaul/methodical/issues/97 is resolved, you will have to specify which method should be applied first in cases of ambiguity using [[methodical.core/prefer-method!]]:

```clj (m/prefer-method! transforms ::user-with-location ::user-with-password) ```

If you want to override transforms completely for a model, and ignore transforms from ancestors of a model, you can create an :around method:

```clj (m/defmethod toucan2.tools.transforms :around ::my-model [_model] {:field {:in name, :out keyword}}) ```

(defmacro deftransforms
  {:style/indent 1}
  [model column->direction->fn]
  `(do
     (u/maybe-derive ~model ::transformed.model)
     (m/defmethod transforms ~model
       [~'model]
       ~column->direction->fn)))
(s/fdef deftransforms
  :args (s/cat :model      some?
               :transforms any?)
  :ret any?)

apply results transforms before [[toucan2.tools.after-update]] or [[toucan2.tools.after-insert]]

(m/prefer-method! #'pipeline/results-transform
                  [:toucan.result-type/instances ::transformed.model]
                  [:toucan.result-type/instances :toucan2.tools.after/model])

apply results transforms before [[toucan2.tools.after-select]]

(m/prefer-method! #'pipeline/results-transform
                  [:toucan.result-type/instances ::transformed.model]
                  [:toucan.result-type/instances :toucan2.tools.after-select/after-select])

apply transforms before applying the [[toucan2.tools.default-fields]] functions

(m/prefer-method! #'pipeline/results-transform
                  [:toucan.result-type/instances ::transformed.model]
                  [:toucan.result-type/instances :toucan2.tools.default-fields/default-fields])
 
(ns toucan2.tools.with-temp
  (:require
   [clojure.pprint :as pprint]
   [clojure.spec.alpha :as s]
   [clojure.test :as t]
   [methodical.core :as m]
   [toucan2.delete :as delete]
   [toucan2.insert :as insert]
   [toucan2.log :as log]
   [toucan2.model :as model]
   [toucan2.types :as types]
   [toucan2.util :as u]))
(comment types/keep-me)
(m/defmulti with-temp-defaults
  {:arglists            '([model])
   :defmethod-arities   #{1}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.model)}
  u/dispatch-on-first-arg)
(m/defmethod with-temp-defaults :default
  [_model]
  nil)

Implementation of [[with-temp]]. You can implement this if you need to do some sort of special behavior for a particular model. But normally you would just implement [[with-temp-defaults]]. If you need to do special setup when using [[with-temp]], you can implement a :before method:

```clj (m/defmethod do-with-temp* :before :default [_model _explicit-attributes f] (set-up-db!) f) ```

explicit-attributes are the attributes specified in the with-temp form itself, any may be nil. The default implementation merges the attributes from [[with-temp-defaults]] like

```clj (merge {} (with-temp-defaults model) explict-attributes) ```

(m/defmulti do-with-temp*
  {:arglists            '([model₁ explicit-attributes f])
   :defmethod-arities   #{3}
   :dispatch-value-spec (s/nonconforming ::types/dispatch-value.model)}
  u/dispatch-on-first-arg)
(m/defmethod do-with-temp* :default
  [model explicit-attributes f]
  (assert (some? model) (format "%s model cannot be nil." `with-temp))
  (when (some? explicit-attributes)
    (assert (map? explicit-attributes) (format "attributes passed to %s must be a map." `with-temp)))
  (let [defaults          (with-temp-defaults model)
        merged-attributes (merge {} defaults explicit-attributes)]
    (u/try-with-error-context ["with temp" {::model               model
                                            ::explicit-attributes explicit-attributes
                                            ::default-attributes  defaults
                                            ::merged-attributes   merged-attributes}]
      (log/debugf "Create temporary %s with attributes %s" model merged-attributes)
      (let [temp-object (first (insert/insert-returning-instances! model merged-attributes))]
        (log/debugf "[with-temp] => %s" temp-object)
        (try
          (t/testing (format "\nwith temporary %s with attributes\n%s\n"
                             (pr-str model)
                             (with-out-str (pprint/pprint merged-attributes)))
            (f temp-object))
          (finally
            (delete/delete! model :toucan/pk ((model/select-pks-fn model) temp-object))))))))
(defn do-with-temp [modelable attributes f]
  (let [model (model/resolve-model modelable)]
    (do-with-temp* model attributes f)))

Define a temporary instance of a model and bind it to temp-object-binding. The object is inserted using [[insert/insert-returning-instances!]] using the values from [[with-temp-defaults]] merged with a map of attributes. At the conclusion of body, the object is deleted. This is primarily intended for usage in tests, so this adds a [[clojure.test/testing]] context around body as well.

[[with-temp]] can create multiple objects in one form if you pass additional bindings.

temp-object-binding and attributes are optional, and default to _ and nil, respectively. If you're creating multiple objects at once these must be explicitly specified.

Examples:

```clj ;;; use the with-temp-defaults for :models/bird (with-temp [:models/bird bird] (do-something bird))

;;; use the with-temp-defaults for :models/bird merged with {:name "Lucky Pigeon"} (with-temp [:models/bird bird {:name "Lucky Pigeon"}] (do-something bird))

;;; define multiple instances at the same time (with-temp [:models/bird bird-1 {:name "Parroty"} :models/bird bird-2 {:name "Green Friend", :best-friend-id (:id bird-1)}] (do-something bird)) ```

If you want to implement custom behavior for a model other than default values, you can implement [[do-with-temp*]].

(defmacro with-temp
  {:style/indent :defn}
  [[modelable temp-object-binding attributes & more] & body]
  `(do-with-temp ~modelable ~attributes
                 (^:once fn* [temp-object#]
                  (let [~(or temp-object-binding '_) temp-object#]
                    ~(if (seq more)
                       `(with-temp ~(vec more) ~@body)
                       `(do ~@body))))))
(s/fdef with-temp
  :args (s/cat :bindings (s/spec (s/cat
                                  :model+binding+attributes
                                  (s/* (s/cat :model      some?
                                              :binding    :clojure.core.specs.alpha/binding-form
                                              :attributes any?))

                                  :model+optional
                                  (s/cat :model    some?
                                         :optional (s/? (s/cat :binding    :clojure.core.specs.alpha/binding-form
                                                               :attributes (s/? any?))))))
               :body     (s/* any?))
  :ret  any?)
 

Toucan 2 query type hierarchy.

(ns toucan2.types
  (:require [clojure.spec.alpha :as s]))

the query type hierarchy below is used for pipeline methods and tooling to decide what sort of things they need to do -- for example you should not do row-map transformations to a query that returns an update count.

(derive :toucan.query-type/select.* :toucan.query-type/*)
(derive :toucan.query-type/insert.* :toucan.query-type/*)
(derive :toucan.query-type/update.* :toucan.query-type/*)
(derive :toucan.query-type/delete.* :toucan.query-type/*)

DML (Data manipulation language) here means things like UPDATE, DELETE, or INSERT. Some goofballs include SELECT in this category, but we are not! We are calling SELECT a DQL (Data Query Language) statement. There are other types of queries like DDL (Data Definition Language, e.g. CREATE TABLE), but Toucan 2 doesn't currently have any tooling around those. Stuff like [[toucan2.execute/query]] that could potentially execute those don't care what kind of query you're executing anyway.

(derive :toucan.statement-type/DML :toucan.statement-type/*)
(derive :toucan.statement-type/DQL :toucan.statement-type/*)
(derive :toucan.query-type/select.* :toucan.statement-type/DQL)
(derive :toucan.query-type/insert.* :toucan.statement-type/DML)
(derive :toucan.query-type/update.* :toucan.statement-type/DML)
(derive :toucan.query-type/delete.* :toucan.statement-type/DML)
(derive :toucan.result-type/instances    :toucan.result-type/*)
(derive :toucan.result-type/pks          :toucan.result-type/*)
(derive :toucan.result-type/update-count :toucan.result-type/*)
(doto :toucan.query-type/select.instances
  (derive :toucan.query-type/select.*)
  (derive :toucan.result-type/instances))

[[toucan2.select/select-fn-set]] and [[toucan2.select/select-fn-vec]] queries -- we are applying a specific function transform to the results, so we don't want to apply a default fields transform or other stuff like that.

(derive :toucan.query-type/select.instances.fns :toucan.query-type/select.instances)

A special query type that is just supposed to return the count of matching rows rather than the actual matching rows. This is used to implement [[toucan2.select/count]]. Query compilation backends should build something that returns a row with the key :count.

If the query does not return a row with the key :count, [[toucan2.select/count]] will count up all the rows returned.

(derive :toucan.query-type/select.count :toucan.query-type/select.instances.fns)

A special query type that should just return whether or not any rows matching the conditions exist. Used to implement [[toucan2.select/exists?]]. Similar to :toucan.query-type/select.count, but this should return a result row with the key :exists, which may be either a boolean or integer (positive = truthy).

If the query does not return a row with the key :exists, [[toucan2.select/exists?]] will simply check whether or not a row is returned.

(derive :toucan.query-type/select.exists :toucan.query-type/select.instances.fns)

A special subtype of a SELECT query that should use the syntax of update. Used to power [[toucan2.tools.before-update]].

The difference is that update is supposed to treat a resolved query map as a conditions map rather than a Honey SQL form.

(derive :toucan.query-type/select.instances.from-update :toucan.query-type/select.instances)
(doto :toucan.query-type/insert.update-count
  (derive :toucan.query-type/insert.*)
  (derive :toucan.result-type/update-count))
(doto :toucan.query-type/insert.pks
  (derive :toucan.query-type/insert.*)
  (derive :toucan.result-type/pks))
(doto :toucan.query-type/insert.instances
  (derive :toucan.query-type/insert.*)
  (derive :toucan.result-type/instances))
(doto :toucan.query-type/update.update-count
  (derive :toucan.query-type/update.*)
  (derive :toucan.result-type/update-count))
(doto :toucan.query-type/update.pks
  (derive :toucan.query-type/update.*)
  (derive :toucan.result-type/pks))
(doto :toucan.query-type/update.instances
  (derive :toucan.query-type/update.*)
  (derive :toucan.result-type/instances))
(doto :toucan.query-type/delete.update-count
  (derive :toucan.query-type/delete.*)
  (derive :toucan.result-type/update-count))
(doto :toucan.query-type/delete.pks
  (derive :toucan.query-type/delete.*)
  (derive :toucan.result-type/pks))
(doto :toucan.query-type/delete.instances
  (derive :toucan.query-type/delete.*)
  (derive :toucan.result-type/instances))

The following are 'special' types only used in SPECIAL situations.

A select query that is done with PKs fetched directly from that database. These don't need to be transformed.

(derive :toucan.query-type/select.instances-from-pks :toucan.query-type/select.instances)

True if query-type derives from one of the various abstract query keywords such as :toucan.result-type/* or :toucan.query-type/*. This does not guarantee that the query type is a 'concrete', just that it is something with some sort of query type information.

(defn query-type?
  [query-type]
  (some (fn [abstract-type]
          (isa? query-type abstract-type))
        [:toucan.result-type/*
         :toucan.query-type/*
         :toucan.statement-type/*]))

utils

(defn parent-query-type [query-type]
  (some (fn [k]
          (when (isa? k :toucan.query-type/*)
            k))
        (parents query-type)))

E.g. something like :toucan.query-type/insert.*. The immediate descendant of :toucan.query-type/*.

```clj (base-query-type :toucan.query-type/insert.instances) => :toucan.query-type/insert.* ```

(defn base-query-type
  [query-type]
  (when (isa? query-type :toucan.query-type/*)
    (loop [last-query-type nil, query-type query-type]
      (if (or (= query-type :toucan.query-type/*)
              (not query-type))
        last-query-type
        (recur query-type (parent-query-type query-type))))))

```clj (similar-query-type-returning :toucan.query-type/insert.instances :toucan.result-type/pks) => :toucan.query-type/insert.pks ```

(defn similar-query-type-returning
  [query-type result-type]
  (let [base-type (base-query-type query-type)]
    (some (fn [descendant]
            (when (and ((parents descendant) base-type)
                       (isa? descendant result-type))
              descendant))
          (descendants base-type))))
(s/def ::dispatch-value.default
  (partial = :default))

Helper for creating a spec that also accepts the :default keyword.

(defn or-default-spec
  [spec]
  (s/nonconforming
   (s/or :default     ::dispatch-value.default
         :non-default spec)))

:toucan.query-type/abstract exists for things that aren't actually supposed to go in the query type hierarchy, but we want to be able to derive other query types FROM them. See [[toucan2.tools.after]] and :toucan2.tools.after/query-type for example.

(s/def ::dispatch-value.query-type
  (or-default-spec
   (s/or :abstract-query-type #(isa? % :toucan.query-type/abstract)
         :query-type          query-type?)))

technically nil is valid and it's not read in as a Symbol

What about (Class/forName "...") forms? Those are valid classes...

(s/def ::dispatch-value.keyword-or-class
  (some-fn keyword? symbol? nil?))
(s/def ::dispatch-value.model
  ::dispatch-value.keyword-or-class)
(s/def ::dispatch-value.query
  ::dispatch-value.keyword-or-class)
(s/def ::dispatch-value.query-type-model
  (or-default-spec
   (s/cat :query-type ::dispatch-value.query-type
          :model      ::dispatch-value.model)))
(s/def ::dispatch-value.query-type-model-query
  (or-default-spec
   (s/cat :query-type ::dispatch-value.query-type
          :model      ::dispatch-value.model
          :query      ::dispatch-value.query)))
 

Implementation of [[update!]].

(ns toucan2.update
  (:require
   [clojure.spec.alpha :as s]
   [methodical.core :as m]
   [toucan2.log :as log]
   [toucan2.pipeline :as pipeline]
   [toucan2.query :as query]))

this is basically the same as the args for select and delete but the difference is that it has an additional optional arg, :pk, as the second arg, and one additional optional arg, the changes map at the end

(s/def ::args
  (s/cat
   :connectable ::query/default-args.connectable
   :modelable   ::query/default-args.modelable
   :pk          (s/? (complement (some-fn keyword? map?)))
   ;; these are treated as CONDITIONS
   :kv-args     ::query/default-args.kv-args
   ;; by default, assuming this resolves to a map query, is treated as a map of conditions.
   :queryable   ::query/default-args.queryable
   ;; TODO -- This should support named changes maps too. Also,
   :changes     map?))
(m/defmethod query/parse-args :toucan.query-type/update.*
  [query-type unparsed-args]
  (let [parsed (query/parse-args-with-spec query-type ::args unparsed-args)]
    (cond-> parsed
      (contains? parsed :pk) (-> (dissoc :pk)
                                 (update :kv-args assoc :toucan/pk (:pk parsed))))))
(m/defmethod pipeline/build [#_query-type :toucan.query-type/update.*
                             #_model      :default
                             #_query      :default]
  "Default method for building UPDATE queries. Code for building Honey SQL for UPDATE lives
  in [[toucan2.honeysql2]].
  This doesn't really do much, but if the query has no `:changes`, returns the special flag `:toucan2.pipeline/no-op`."
  [query-type model {:keys [changes], :as parsed-args} resolved-query]
  (if (empty? changes)
    (do
      (log/debugf "Query has no changes, skipping update")
      ::pipeline/no-op)
    (next-method query-type model parsed-args resolved-query)))
(defn reducible-update
  {:arglists '([modelable pk? conditions-map-or-query? & conditions-kv-args changes-map])}
  [& unparsed-args]
  (pipeline/reducible-unparsed :toucan.query-type/update.update-count unparsed-args))
(defn update!
  {:arglists '([modelable pk? conditions-map-or-query? & conditions-kv-args changes-map])}
  [& unparsed-args]
  (pipeline/transduce-unparsed-with-default-rf :toucan.query-type/update.update-count unparsed-args))
(defn reducible-update-returning-pks
  {:arglists '([modelable pk? conditions-map-or-query? & conditions-kv-args changes-map])}
  [& unparsed-args]
  (pipeline/reducible-unparsed :toucan.query-type/update.pks unparsed-args))
(defn update-returning-pks!
  {:arglists '([modelable pk? conditions-map-or-query? & conditions-kv-args changes-map])}
  [& unparsed-args]
  (pipeline/transduce-unparsed-with-default-rf :toucan.query-type/update.pks unparsed-args))

TODO -- add update-returning-instances!, similar to [[toucan2.update/insert-returning-instances!]]

 
(ns toucan2.util
  (:require
   [camel-snake-kebab.core :as csk]
   [clojure.spec.alpha :as s]
   [clojure.walk :as walk]
   [methodical.util.dispatch :as m.dispatch]
   [pretty.core :as pretty]
   [toucan2.protocols :as protocols])
  (:import
   (clojure.lang IPersistentMap)
   (potemkin.collections PotemkinMap)))

TODO -- there is a lot of repeated code in here to make sure we don't accidentally realize and print IReduceInit, and at least 3 places we turn an eduction into the same pretty form. Maybe we should try to consolidate some of that logic.

(set! *warn-on-reflection* true)

Dispatch on the first argument using [[dispatch-value]], and ignore all other args.

(def ^{:arglists '([x & _])} dispatch-on-first-arg
  (m.dispatch/dispatch-on-first-arg #'protocols/dispatch-value))

Dispatch on the two arguments using [[protocols/dispatch-value]], and ignore all other args.

(def ^{:arglists '([x y & _])} dispatch-on-first-two-args
  (m.dispatch/dispatch-on-first-two-args #'protocols/dispatch-value))

Dispatch on the three arguments using [[protocols/dispatch-value]], and ignore all other args.

(def ^{:arglists '([x y z & _])} dispatch-on-first-three-args
  (m.dispatch/dispatch-on-first-three-args #'protocols/dispatch-value))

Locale-agnostic version of [[clojure.string/lower-case]]. clojure.string/lower-case uses the default locale in conversions, turning ID into ıd, in the Turkish locale. This function always uses the Locale/US locale.

(defn lower-case-en
  [^CharSequence s]
  (.. s toString (toLowerCase java.util.Locale/US)))

Derive child from parent only if child is not already a descendant of parent.

(defn maybe-derive
  [child parent]
  (when-not (isa? child parent)
    (derive child parent)))

[[try-with-error-context]]

TODO -- I don't love this stuff anymore, need to rework it at some point.

(defprotocol ^:private AddContext
  (^:no-doc add-context ^Throwable [^Throwable e additional-context]))
(defn- add-context-to-ex-data [ex-data-map additional-context]
  (update ex-data-map
          :toucan2/context-trace
          #(conj (vec %) (walk/prewalk
                          (fn [form]
                            (cond
                              (instance? pretty.core.PrettyPrintable form)
                              (pretty/pretty form)
                              (instance? clojure.core.Eduction form)
                              (list 'eduction
                                    (.xform ^clojure.core.Eduction form)
                                    (.coll ^clojure.core.Eduction form))
                              (and (instance? clojure.lang.IReduceInit form)
                                   (not (coll? form)))
                              (class form)
                              :else
                              form))
                          additional-context))))
(extend-protocol AddContext
  clojure.lang.ExceptionInfo
  (add-context [^Throwable e additional-context]
    (if (empty? additional-context)
      e
      (doto ^Throwable (ex-info (ex-message e)
                                (add-context-to-ex-data (ex-data e) additional-context)
                                (ex-cause e))
        (.setStackTrace (.getStackTrace e)))))

  Throwable
  (add-context [^Throwable e additional-context]
    (if (empty? additional-context)
      e
      (doto ^Throwable (ex-info (ex-message e)
                                (add-context-to-ex-data {} additional-context)
                                e)
        (.setStackTrace (.getStackTrace e))))))
(defmacro try-with-error-context
  {:style/indent :defn}
  [additional-context & body]
  `(try
     ~@body
     (catch Exception e#
       (throw (add-context e# ~additional-context)))
     (catch AssertionError e#
       (throw (add-context e# ~additional-context)))))
(s/fdef try-with-error-context
  :args (s/cat :additional-context (s/alt :message+map (s/spec (s/cat :message string?
                                                                      :map     map?))
                                          ;; some sort of function call or something like that.
                                          :form        seqable?)
               :body               (s/+ any?))
  :ret  any?)

Like camel-snake-kebab.core/->kebab-case, but supports namespaced keywords.

(defn ->kebab-case
  [x]
  (if (and (keyword? x) (namespace x))
    (keyword (csk/->kebab-case (namespace x))
             (csk/->kebab-case (name x)))
    (csk/->kebab-case x)))

Is this a map a transient row, or created using p/def-map-type? This includes Instances.

(defprotocol IsCustomMap
  (custom-map? [m]))
(extend-protocol IsCustomMap
  IPersistentMap
  (custom-map? [_] false)

  PotemkinMap
  (custom-map? [_] true))