toucan

1.15.0


Functionality for defining your application's models and querying the database.

dependencies

org.clojure/java.classpath
0.3.0
org.clojure/java.jdbc
0.7.10
org.clojure/tools.logging
0.5.0
org.clojure/tools.namespace
0.3.1
honeysql
0.9.8
potemkin
0.4.5



(this space intentionally left almost blank)
 

Helper functions for querying the DB and inserting or updating records using Toucan models.

(ns toucan.db
  (:refer-clojure :exclude [count])
  (:require [clojure
             [pprint :refer [pprint]]
             [string :as s]
             [walk :as walk]]
            [clojure.java.jdbc :as jdbc]
            [clojure.tools.logging :as log]
            [honeysql
             [core :as hsql]
             [format :as hformat]
             [helpers :as h]]
            [toucan
             [models :as models]
             [util :as u]]))

CONFIGURATION

Quoting Style

The quoting style is the HoneySQL quoting style that should be used to quote identifiers. By default, this is :ansi, which wraps identifers in double-quotes. Alternatively, you can specify :mysql (backticks), or :sqlserver (square brackets)

(defonce ^:private default-quoting-style (atom :ansi))

Bind this to override the identifier quoting style. Provided for cases where you want to override the quoting style (such as when connecting to a different DB) without changing the default value.

(def ^:dynamic *quoting-style*
  nil)

Set the default quoting style that should be used to quote identifiers. Defaults to :ansi, but you can instead set it to :mysql or :sqlserver.

(defn set-default-quoting-style!
  [new-quoting-style]
  (reset! default-quoting-style new-quoting-style))

Fetch the HoneySQL quoting style that should be used to quote identifiers. One of :ansi, :mysql, or :sqlserver.

Returns the value of *quoting-style* if it is bound, otherwise returns the default quoting style, which is normally :ansi; this can be changed by calling set-default-quoting-style!.

(defn quoting-style
  ^clojure.lang.Keyword []
  (or *quoting-style*
      @default-quoting-style))

Additional HoneySQL options

Automatically Convert Dashes & Underscores

Convert dashes to underscores in queries going into the DB, and underscores in results back to dashes coming out of the DB. By default, this is disabled. See the documentation in setup.md for more details.

(defonce ^:private default-automatically-convert-dashes-and-underscores (atom false))

Bind this to enable automatic conversion between dashes and underscores for indentifiers. Provided for cases where you want to override the behavior (such as when connecting to a different DB) without changing the default value.

(def ^:dynamic *automatically-convert-dashes-and-underscores*
  nil)

Set the default value for allowing dashes in field names. Defaults to true.

(defn set-default-automatically-convert-dashes-and-underscores!
  [^Boolean new-automatically-convert-dashes-and-underscores]
  (reset! default-automatically-convert-dashes-and-underscores new-automatically-convert-dashes-and-underscores))

Deterimine whether we should automatically convert dashes and underscores in identifiers.

Returns the value of *automatically-convert-dashes-and-underscores* if it is bound, otherwise returns the default-automatically-convert-dashes-and-underscores, which is normally false; this can be changed by calling set-default-automatically-convert-dashes-and-underscores!.

(defn automatically-convert-dashes-and-underscores?
  ^Boolean []
  (if (nil? *automatically-convert-dashes-and-underscores*)
    @default-automatically-convert-dashes-and-underscores
    *automatically-convert-dashes-and-underscores*))

DB Connection

The default DB connection is used automatically when accessing the DB; it can be anything that would normally be passed to clojure.java.jdbc functions -- normally a connection details map, but alternatively something like a C3PO connection pool.

(defonce ^:private default-db-connection (atom nil))

Bind this to override the default DB connection used by toucan.db functions. Provided for situations where you'd like to connect to a DB other than the primary application DB, or connect to it with different connection options.

(def ^:dynamic *db-connection*
  nil)

Set the JDBC connecton details map for the default application DB connection. This connection is used by default by the various toucan.db functions.

db-connection-map is passed directly to clojure.java.jdbc; it can be anything that is accepted by it.

 (db/set-default-db-connection!
   {:classname   "org.postgresql.Driver"
    :subprotocol "postgresql"
    :subname     "//localhost:5432/my_db"
    :user        "cam"})
(defn set-default-db-connection!
  {:style/indent 0}
  [db-connection-map]
  (reset! default-db-connection db-connection-map))
(defonce ^:private default-jdbc-options
  ;; FIXME: This has already been fixed in `clojure.java.jdbc`, so
  ;;        this option can be removed when using >= 0.7.10.
  (atom {:identifiers u/lower-case}))

Set the default options to be used for all calls to clojure.java.jdbc/query or execute!.

(defn set-default-jdbc-options!
  [jdbc-options]
  (reset! default-jdbc-options jdbc-options))

TRANSACTION & CONNECTION UTIL FNS

Transaction connection to the application DB. Used internally by transaction.

(def ^:dynamic *transaction-connection*
  nil)

Fetch the JDBC connection details for passing to clojure.java.jdbc. Returns *db-connection*, if it is set; otherwise *transaction-connection*, if we're inside a transaction (this is bound automatically); otherwise the default DB connection, set by set-default-db-connection!.

If no DB connection has been set this function will throw an exception.

(defn connection
  []
  (or *db-connection*
      *transaction-connection*
      @default-db-connection
      (throw (Exception. "DB is not set up. Make sure to call set-default-db-connection! or bind *db-connection*."))))

Execute F inside a DB transaction. Prefer macro form transaction to using this directly.

(defn do-in-transaction
  [f]
  (jdbc/with-db-transaction [conn (connection)]
    (binding [*transaction-connection* conn]
      (f))))

Execute all queries within the body in a single transaction.

(defmacro transaction
  {:arglists '([body] [options & body]), :style/indent 0}
  [& body]
  `(do-in-transaction (fn [] ~@body)))

QUERY UTIL FNS

Should we disable logging for database queries? Normally false, but bind this to true to keep logging from getting too noisy during operations that require a lot of DB access, like the sync process.

(def ^:dynamic ^Boolean *disable-db-logging*
  false)

Return the namespace symbol where we'd expect to find an model symbol.

 (model-symb->ns 'CardFavorite) -> 'my-project.models.card-favorite
(defn- model-symb->ns
  [symb]
  {:pre [(symbol? symb)]}
  (symbol (str (models/root-namespace) \. (u/lower-case (s/replace (name symb) #"([a-z])([A-Z])" "$1-$2")))))

Resolve the model associated with SYMB, calling require on its namespace if needed.

 (resolve-model-from-symbol 'CardFavorite) -> my-project.models.card-favorite/CardFavorite
(defn- resolve-model-from-symbol
  [symb]
  (let [model-ns (model-symb->ns symb)]
    @(try (ns-resolve model-ns symb)
          (catch Throwable _
            (require model-ns)
            (ns-resolve model-ns symb)))))

Resolve a model if it's quoted. This also unwraps entities when they're inside vectores.

 (resolve-model Database)         -> #'my-project.models.database/Database
 (resolve-model [Database :name]) -> #'my-project.models.database/Database
 (resolve-model 'Database)        -> #'my-project.models.database/Database
(defn resolve-model
  [model]
  {:post [(:toucan.models/model %)]}
  (cond
    (:toucan.models/model model) model
    (vector? model)              (resolve-model (first model))
    (symbol? model)              (resolve-model-from-symbol model)
    :else                        (throw (Exception. (str "Invalid model: " model)))))

The function that JDBC should use to quote identifiers for our database. This is passed as the :entities option to functions like jdbc/insert!.

(defn quote-fn
  []
  ((quoting-style) @(resolve 'honeysql.format/quote-fns))) ; have to call resolve because it's not public

Atom used as a counter for DB calls when enabled. This number isn't perfectly accurate, only mostly; DB calls made directly to JDBC won't be logged.

(def ^:private ^:dynamic *call-count*
  nil)

Execute F with DB call counting enabled. F is passed a single argument, a function that can be used to retrieve the current call count. (It's probably more useful to use the macro form of this function, with-call-counting, instead.)

(defn -do-with-call-counting
  {:style/indent 0}
  [f]
  (binding [*call-count* (atom 0)]
    (f (partial deref *call-count*))))

Execute body and track the number of DB calls made inside it. call-count-fn-binding is bound to a zero-arity function that can be used to fetch the current DB call count.

 (db/with-call-counting [call-count] ...
   (call-count))
(defmacro with-call-counting
  {:style/indent 1}
  [[call-count-fn-binding] & body]
  `(-do-with-call-counting (fn [~call-count-fn-binding] ~@body)))

Print the number of DB calls executed inside body to stdout. Intended for use during REPL development.

(defmacro debug-count-calls
  {:style/indent 0}
  [& body]
  `(with-call-counting [call-count#]
     (let [results# (do ~@body)]
       (println "DB Calls:" (call-count#))
       results#)))
(defn- format-sql [sql]
  (when sql
    (loop [sql sql, [k & more] ["FROM" "LEFT JOIN" "INNER JOIN" "WHERE" "GROUP BY" "HAVING" "ORDER BY" "OFFSET"
                                "LIMIT"]]
      (if-not k
        sql
        (recur (s/replace sql (re-pattern (format "\\s+%s\\s+" k)) (format "\n%s " k))
               more)))))
(def ^:dynamic ^:private *debug-print-queries* false)

Execute f with debug query logging enabled. Don't use this directly; prefer the debug-print-queries macro form instead.

(defn -do-with-debug-print-queries
  [f]
  (binding [*debug-print-queries* true]
    (f)))

Print the HoneySQL and SQL forms of any queries executed inside body to stdout. Intended for use during REPL development.

(defmacro debug-print-queries
  {:style/indent 0}
  [& body]
  `(-do-with-debug-print-queries (fn [] ~@body)))

Compile honeysql-form to SQL. This returns a vector with the SQL string as its first item and prepared statement params as the remaining items.

(defn honeysql->sql
  [honeysql-form]
  {:pre [(map? honeysql-form)]}
  ;; Not sure *why* but without setting this binding on *rare* occasion HoneySQL will unwantedly
  ;; generate SQL for a subquery and wrap the query in parens like "(UPDATE ...)" which is invalid
  (let [[sql & args :as sql+args]
        (binding [hformat/*subquery?* false]
          (hsql/format honeysql-form
                       :quoting             (quoting-style)
                       :allow-dashed-names? (not (automatically-convert-dashes-and-underscores?))))]
    (when *debug-print-queries*
      (println (pprint honeysql-form)
               (format "\n%s\n%s" (format-sql sql) args)))
    (when-not *disable-db-logging*
      (log/debug (str "DB Call: " sql))
      (when *call-count*
        (swap! *call-count* inc)))
    sql+args))

Compile honeysql-from and call jdbc/query against the application database. Options are passed along to jdbc/query.

(defn query
  [honeysql-form & {:as options}]
  (jdbc/query (connection)
              (honeysql->sql honeysql-form)
              (merge @default-jdbc-options options)))

Compile honeysql-from and call jdbc/reducible-query against the application database. Options are passed along to jdbc/reducible-query. Note that the query won't actually be executed until it's reduced.

(defn reducible-query
  [honeysql-form & {:as options}]
  (jdbc/reducible-query (connection) (honeysql->sql honeysql-form) options))

Qualify a field-name name with the name its entity. This is necessary for disambiguating fields for HoneySQL queries that contain joins.

 (db/qualify 'CardFavorite :id) -> :report_cardfavorite.id
(defn qualify
  ^clojure.lang.Keyword [model field-name]
  (if (vector? field-name)
    [(qualify model (first field-name)) (second field-name)]
    (hsql/qualify (:table (resolve-model model)) field-name)))

Is field-name qualified (e.g. with its table name)?

(defn qualified?
  ^Boolean [field-name]
  (if (vector? field-name)
    (qualified? (first field-name))
    (boolean (re-find #"\." (name field-name)))))

Qualify field-name with its table name if it's not already qualified.

(defn- maybe-qualify
  ^clojure.lang.Keyword [model field-name]
  (if (qualified? field-name)
    field-name
    (qualify model field-name)))

Get the fields that should be used in a query, destructuring entity if it's wrapped in a vector, otherwise calling default-fields. This will return nil if the model isn't wrapped in a vector and uses the default implementation of default-fields.

 (model->fields 'User) -> [:id :email :date_joined :first-name :last-name :last_login :is_superuser :is_qbnewb]
 (model->fields ['User :first-name :last-name]) -> [:first-name :last-name]
 (model->fields 'Database) -> nil
(defn- model->fields
  [model]
  (if (vector? model)
    (let [[model & fields] model]
      (for [field fields]
        (maybe-qualify model field)))
    (models/default-fields (resolve-model model))))

Replace underscores in k with dashes. In other words, converts a keyword from :snake_case to :lisp-case.

 (replace-underscores :2_cans) ; -> :2-cans
(defn- replace-underscores
  ^clojure.lang.Keyword [k]
  ;; if k is not a string or keyword don't transform it
  (if-not ((some-fn string? keyword?) k)
    k
    (let [k-str (u/keyword->qualified-name k)]
      (if (s/index-of k-str \_)
        (keyword (s/replace k-str \_ \-))
        k))))

Replace the keys in any maps in x with the result of (f key). Recursively walks x using clojure.walk.

(defn- transform-keys
  [f x]
  (walk/postwalk
   (fn [y]
     (if-not (map? y)
       y
       (into {} (for [[k v] y]
                  [(f k) v]))))
   x))

Perform post-processing for objects fetched from the DB. Convert results objects to entity record types and call the model's post-select method on them.

(defn do-post-select
  {:style/indent 1}
  [model objects]
  (let [model            (resolve-model model)
        key-transform-fn (if-not (automatically-convert-dashes-and-underscores?)
                           identity
                           (partial transform-keys replace-underscores))]
    (vec (for [object objects]
           (models/do-post-select model (key-transform-fn object))))))

Includes projected fields and a from clause for honeysql-form. Will not override if already present.

(defn- merge-select-and-from
  [resolved-model honeysql-form]
  (merge {:select (or (models/default-fields resolved-model)
                      [:*])
          :from   [resolved-model]}
         honeysql-form))

Select objects from the database.

Like select, but doesn't offer as many conveniences, so prefer that instead; like select, simple-select callts post-select on the results, but unlike select, only accepts a single raw HoneySQL form as an argument.

(db/simple-select 'User {:where [:= :id 1]})
(defn simple-select
  {:style/indent 1}
  [model honeysql-form]
  (let [model (resolve-model model)]
    (do-post-select model (query (merge-select-and-from model honeysql-form)))))

Select objects from the database.

Same as simple-select, but returns something reducible instead of a result set. Like simple-select, will call post-select on the results, but will do so lazily.

(transduce (filter can-read?) conj [] (simple-select-reducible 'User {:where [:= :id 1]}))
(defn simple-select-reducible
  {:style/indent 1}
  [model honeysql-form]
  (let [model (resolve-model model)]
    (eduction (map #(models/do-post-select model %))
              (reducible-query (merge-select-and-from model honeysql-form)))))

Select a single object from the database.

Like select-one, but doesn't offer as many conveniences, so prefer that instead; like select-one, simple-select-one callts post-select on the results, but unlike select-one, only accepts a single raw HoneySQL form as an argument.

(db/simple-select-one 'User (h/where [:= :first-name "Cam"]))
(defn simple-select-one
  ([model]
   (simple-select-one model {}))
  ([model honeysql-form]
   (first (simple-select model (h/limit honeysql-form (hsql/inline 1))))))

Compile honeysql-form and call jdbc/execute! against the application DB. options are passed directly to jdbc/execute! and can be things like :multi? (default false) or :transaction? (default true).

(defn execute!
  [honeysql-form & {:as options}]
  (jdbc/execute! (connection) (honeysql->sql honeysql-form) (merge @default-jdbc-options options)))

Generate a HoneySQL where form using key-value args.

 (where {} :a :b)        -> (h/merge-where {} [:= :a :b])
 (where {} :a [:!= b])   -> (h/merge-where {} [:!= :a :b])
 (where {} {:a [:!= b]}) -> (h/merge-where {} [:!= :a :b])
(defn- where
  {:style/indent 1}
  ([honeysql-form]
   honeysql-form) ; no-op
  ([honeysql-form m]
   (apply where honeysql-form (apply concat m)))
  ([honeysql-form k v]
   (h/merge-where honeysql-form (if (vector? v)
                                  (let [[f & args] v] ; e.g. :id [:!= 1] -> [:!= :id 1]
                                    (assert (keyword? f))
                                    (vec (cons f (cons k args))))
                                  [:= k v])))
  ([honeysql-form k v & more]
   (apply where (where honeysql-form k v) more)))

Generate a HoneySQL form, converting pairs of arguments with keywords into a where clause, and merging other HoneySQL clauses in as-is. Meant for internal use by functions like select. (So-called because it handles where plus other clauses).

 (where+ {} [:id 1 {:limit 10}]) -> {:where [:= :id 1], :limit 10}
(defn- where+
  [honeysql-form args]
  (loop [honeysql-form honeysql-form, [first-arg & [second-arg & more, :as butfirst]] args]
    (cond
      (keyword? first-arg) (recur (where honeysql-form first-arg second-arg) more)
      first-arg            (recur (merge honeysql-form first-arg)            butfirst)
      :else                honeysql-form)))

UPDATE!

(defn- method-implemented? [^clojure.lang.Keyword methodk model]
  (not (nil? (find-protocol-method models/IModel methodk model))))

Update a single row in the database. Returns true if a row was affected, false otherwise. Accepts either a single map of updates to make or kwargs. entity is automatically resolved, and pre-update is called on kvs before the object is inserted into the database.

 (db/update! 'Label 11 :name "ToucanFriendly")
 (db/update! 'Label 11 {:name "ToucanFriendly"})
(defn update!
  {:style/indent 2}
  (^Boolean [model honeysql-form]
   (let [model (resolve-model model)]
     (not= [0] (execute! (merge (h/update model)
                                honeysql-form)))))
  (^Boolean [model id kvs]
   {:pre [(some? id) (map? kvs) (every? keyword? (keys kvs))]}
   (let [model       (resolve-model model)
         primary-key (models/primary-key model)
         kvs         (-> (models/do-pre-update model (assoc kvs primary-key id))
                         (dissoc primary-key))
         updated?    (update! model (-> (h/sset {} kvs)
                                        (where primary-key id)))]
        (when (and updated?
                   (method-implemented? :post-update model))
          (models/post-update (model id)))
     updated?))
  (^Boolean [model id k v & more]
   (update! model id (apply array-map k v more))))

Convenience for updating several objects matching conditions-map. Returns true if any objects were affected. For updating a single object, prefer using update!, which calls entity's pre-update method first.

 (db/update-where! Table {:name  table-name
                          :db_id (:id database)}
   :active false)
(defn update-where!
  {:style/indent 2}
  ^Boolean [model conditions-map & {:as values}]
  {:pre [(map? conditions-map) (every? keyword? (keys values))]}
  (update! model (where {:set values} conditions-map)))

Like update!, but filters out KVS with nil values.

(defn update-non-nil-keys!
  {:style/indent 2}
  ([model id kvs]
   (update! model id (into {} (for [[k v] kvs
                                     :when (not (nil? v))]
                                 [k v]))))
  ([model id k v & more]
   (update-non-nil-keys! model id (apply array-map k v more))))

INSERT!

Different possible keys that might come back for the ID of a newly inserted row. Differs by DB.

(def ^:private inserted-id-keys
  [;; Postgres, newer H2, and most others return :id
   :id
   ;; :generated_key is returned by MySQL
   :generated_key
   ;; MariaDB returns :insert_id
   :insert_id
   ;; scope_identity() returned by older versions of H2
   (keyword "scope_identity()")
   ;; last_insert_rowid() returned by SQLite3
   (keyword "last_insert_rowid()")])

Get the ID of a row inserted by jdbc/db-do-prepared-return-keys.

(defn get-inserted-id
  [primary-key insert-result]
  (when insert-result
    (some insert-result (cons primary-key inserted-id-keys))))

Do a simple JDBC insert! of multiple objects into the database. Normally you should use insert-many! instead, which calls the model's pre-insert method on the row-maps; simple-insert-many! is offered for cases where you'd like to specifically avoid this behavior. Returns a sequences of IDs of newly inserted objects.

 (db/simple-insert-many! 'Label [{:name "Toucan Friendly"}
                                 {:name "Bird Approved"}]) ;;=> (38 39)
(defn simple-insert-many!
  {:style/indent 1}
  [model row-maps]
  {:pre [(sequential? row-maps) (every? map? row-maps)]}
  (when (seq row-maps)
    (let [model       (resolve-model model)
          primary-key (models/primary-key model)]
      (doall
       (for [row-map row-maps
             :let    [sql (honeysql->sql {:insert-into model, :values [row-map]})]]
         (->> (jdbc/db-do-prepared-return-keys (connection) false sql {}) ; false = don't use a transaction
              (get-inserted-id primary-key)))))))

Insert several new rows into the Database. Resolves entity, and calls pre-insert on each of the row-maps. Returns a sequence of the IDs of the newly created objects.

Note: this does not call post-insert on newly created objects. If you need post-insert behavior, use insert! instead. (This might change in the future: there is an open issue to consider this).

 (db/insert-many! 'Label [{:name "Toucan Friendly"}
                          {:name "Bird Approved"}]) -> [38 39]
(defn insert-many!
  {:style/indent 1}
  [model row-maps]
  (let [model (resolve-model model)]
    (simple-insert-many! model (for [row-map row-maps]
                                  (models/do-pre-insert model row-map)))))

Do a simple JDBC insert of a single object. This is similar to insert! but returns the ID of the newly created object rather than the object itself, and does not call pre-insert or post-insert.

 (db/simple-insert! 'Label :name "Toucan Friendly") -> 1

Like insert!, simple-insert! can be called with either a single row-map or kv-style arguments.

(defn simple-insert!
  {:style/indent 1}
  ([model row-map]
   {:pre [(map? row-map) (every? keyword? (keys row-map))]}
   (first (simple-insert-many! model [row-map])))
  ([model k v & more]
   (simple-insert! model (apply array-map k v more))))

Insert a new object into the Database. Resolves entity, calls its pre-insert method on row-map to prepare it before insertion; after insert, it fetches and the newly created object, passes it to post-insert, and returns the results.

For flexibility, insert! can handle either a single map or individual kwargs:

 (db/insert! Label {:name "Toucan Unfriendly"})
 (db/insert! 'Label :name "Toucan Friendly")
(defn insert!
  {:style/indent 1}
  ([model row-map]
   {:pre [(map? row-map) (every? keyword? (keys row-map))]}
   (let [model (resolve-model model)]
     (when-let [id (simple-insert! model (models/do-pre-insert model row-map))]
       (models/post-insert (model id)))))
  ([model k v & more]
   (insert! model (apply array-map k v more))))

SELECT

All of the following functions are based off of the old sel macro and can do things like select certain fields by wrapping ENTITY in a vector and automatically convert kv-args to a where clause

Select a single object from the database.

 (select-one ['Database :name] :id 1) -> {:name "Sample Dataset"}
(defn select-one
  {:style/indent 1}
  [model & options]
  (let [fields (model->fields model)]
    (simple-select-one model (where+ {:select (or fields [:*])} options))))

Select a single field of a single object from the database.

 (select-one-field :name 'Database :id 1) -> "Sample Dataset"
(defn select-one-field
  {:style/indent 2}
  [field model & options]
  {:pre [(keyword? field)]}
  (field (apply select-one [model field] options)))

Select the :id of a single object from the database.

 (select-one-id 'Database :name "Sample Dataset") -> 1
(defn select-one-id
  {:style/indent 1}
  [model & options]
  (let [model (resolve-model model)]
    (apply select-one-field (models/primary-key model) model options)))

Select the count of objects matching some condition.

 ;; Get all Users whose email is non-nil
 (count 'User :email [:not= nil]) -> 12
(defn count
  {:style/indent 1}
  [model & options]
  (:count (apply select-one [model [:%count.* :count]] options)))

Select objects from the database.

 (select 'Database :name [:not= nil] {:limit 2}) -> [...]
(defn select
  {:style/indent 1}
  [model & options]
  (simple-select model (where+ {:select (or (model->fields model)
                                            [:*])}
                               options)))

Select objects from the database, returns a reducible.

 (transduce (map :name) conj [] (select 'Database :name [:not= nil] {:limit 2}))
    -> ["name1", "name2"]
(defn select-reducible
  {:style/indent 1}
  [model & options]
  (simple-select-reducible model (where+ {:select (or (model->fields model)
                                                      [:*])}
                                         options)))

Select values of a single field for multiple objects. These are returned as a set if any matching fields were returned, otherwise nil.

 (select-field :name 'Database) -> #{"Sample Dataset", "test-data"}
(defn select-field
  {:style/indent 2}
  [field model & options]
  {:pre [(keyword? field)]}
  (when-let [results (seq (map field (apply select [model field] options)))]
    (set results)))

Select IDs for multiple objects. These are returned as a set if any matching IDs were returned, otherwise nil.

 (select-ids 'Table :db_id 1) -> #{1 2 3 4}
(defn select-ids
  {:style/indent 1}
  [model & options]
  (let [model (resolve-model model)]
    (apply select-field (models/primary-key model) model options)))

Select fields k and v from objects in the database, and return them as a map from k to v.

 (select-field->field :id :name 'Database) -> {1 "Sample Dataset", 2 "test-data"}
(defn select-field->field
  {:style/indent 3}
  [k v model & options]
  {:pre [(keyword? k) (keyword? v)]}
  (into {} (for [result (apply select [model k v] options)]
             {(k result) (v result)})))

Select FIELD and :id from objects in the database, and return them as a map from field to :id.

 (select-field->id :name 'Database) -> {"Sample Dataset" 1, "test-data" 2}
(defn select-field->id
  {:style/indent 2}
  [field model & options]
  (let [model (resolve-model model)]
    (apply select-field->field field (models/primary-key model) model options)))

Select field and :id from objects in the database, and return them as a map from :id to field.

 (select-id->field :name 'Database) -> {1 "Sample Dataset", 2 "test-data"}
(defn select-id->field
  {:style/indent 2}
  [field model & options]
  (let [model (resolve-model model)]
    (apply select-field->field (models/primary-key model) field model options)))

EXISTS?

Easy way to see if something exists in the DB.

(db/exists? User :id 100)
(defn exists?
  {:style/indent 1}
  ^Boolean [model & kvs]
  (let [model (resolve-model model)]
    (boolean (select-one-id model (apply where (h/select {} (models/primary-key model)) kvs)))))

DELETE!

Delete an object or objects from the application DB matching certain constraints. Returns true if something was deleted, false otherwise.

 (db/simple-delete! 'Label)                ; delete all Labels
 (db/simple-delete! Label :name "Cam")   ; delete labels where :name == "Cam"
 (db/simple-delete! Label {:name "Cam"}) ; for flexibility either a single map or kwargs are accepted

Unlike delete!, this does not call pre-delete on the object about to be deleted.

(defn simple-delete!
  {:style/indent 1}
  ([model]
   (simple-delete! model {}))
  ([model conditions]
   {:pre [(map? conditions) (every? keyword? (keys conditions))]}
   (let [model (resolve-model model)]
     (not= [0] (execute! (-> (h/delete-from model)
                             (where conditions))))))
  ([model k v & more]
   (simple-delete! model (apply array-map k v more))))

Delete of object(s). For each matching object, the pre-delete multimethod is called, which should do any cleanup needed before deleting the object, (such as deleting objects related to the object about to be deleted), or otherwise enforce preconditions before deleting (such as refusing to delete the object if something else depends on it).

 (delete! Database :id 1)
(defn delete!
  {:style/indent 1}
  [model & conditions]
  (let [model       (resolve-model model)
        primary-key (models/primary-key model)]
    (doseq [object (apply select model conditions)]
      (models/pre-delete object)
      (simple-delete! model primary-key (primary-key object)))))
 

Functions for deserializing and hydrating fields in objects fetched from the DB.

(ns toucan.hydrate
  (:require [toucan
             [db :as db]
             [models :as models]]))

Counts Destructuring & Restructuring

*DISCLAIMER*

This I wrote this code at 4 AM nearly 2 years ago and don't remember exactly what it is supposed to accomplish, or why. It generates a sort of path that records the wacky ways in which objects in a collection are nested, and how they fit into sequences; it then returns a flattened sequence of desired objects for easy modification. Afterwards the modified objects can be put in place of the originals by passing in the sequence of modified objects and the path.

Nonetheless, it still works (somehow) and is well-tested. But it's definitely overengineered and crying out to be replaced with a simpler implementation (clojure.walk would probably work here). PRs welcome!

Original Overview

At a high level, these functions let you aggressively flatten a sequence of maps by a key so you can apply some function across it, and then unflatten that sequence.

     +-------------------------------------------------------------------------+
     |                                                                         +--> (map merge) --> new seq
seq -+--> counts-of ------------------------------------+                      |
     |                                                  +--> counts-unflatten -+
     +--> counts-flatten -> (modify the flattened seq) -+
  1. Get a value that can be used to unflatten a sequence later with counts-of.
  2. Flatten the sequence with counts-flatten
  3. Modify the flattened sequence as needed
  4. Unflatten the sequence by calling counts-unflatten with the modified sequence and value from step 1
  5. map merge the original sequence and the unflattened sequence.

For your convenience counts-apply combines these steps for you.

Return a sequence of counts / keywords that can be used to unflatten COLL later.

(counts-of [{:a [{:b 1} {:b 2}], :c 2}
            {:a {:b 3}, :c 4}] :a)
  -> [2 :atom]

For each x in COLL, return:

  • (count (k x)) if (k x) is sequential
  • :atom if (k x) is otherwise non-nil
  • :nil if x has key k but the value is nil
  • nil if x is nil.
(defn- counts-of
  [coll k]
  (map (fn [x]
         (cond
           (sequential? (k x)) (count (k x))
           (k x)               :atom
           (contains? x k)     :nil
           :else               nil))
       coll))

Flatten COLL by K.

(counts-flatten [{:a [{:b 1} {:b 2}], :c 2}
                 {:a {:b 3}, :c 4}] :a)
  -> [{:b 1} {:b 2} {:b 3}]
(defn- counts-flatten
  [coll k]
  {:pre [(sequential? coll)
         (keyword? k)]}
  (->> coll
       (map k)
       (mapcat (fn [x]
                 (if (sequential? x)  x
                     [x])))))

Unflatten COLL by K using COUNTS from counts-of.

(counts-unflatten [{:b 2} {:b 4} {:b 6}] :a [2 :atom])
  -> [{:a [{:b 2} {:b 4}]}
      {:a {:b 6}}]
(defn- counts-unflatten
  ([coll k counts]
   (counts-unflatten [] coll k counts))
  ([acc coll k [count & more]]
   (let [[unflattend coll] (condp = count
                             nil   [nil (rest coll)]
                             :atom [(first coll) (rest coll)]
                             :nil  [:nil (rest coll)]
                             (split-at count coll))
         acc (conj acc unflattend)]
     (if-not (seq more) (map (fn [x]
                               (when x
                                 {k (when-not (= x :nil) x)}))
                             acc)
             (recur acc coll k more)))))

Apply F to values of COLL flattened by K, then return unflattened/updated results.

(counts-apply [{:a [{:b 1} {:b 2}], :c 2}
               {:a {:b 3}, :c 4}]
  :a #(update-in % [:b] (partial * 2)))

  -> [{:a [{:b 2} {:b 4}], :c 2}
      {:a {:b 3}, :c 4}]
(defn- counts-apply
  [coll k f]
  (let [counts (counts-of coll k)
        new-vals (-> coll
                     (counts-flatten k)
                     f
                     (counts-unflatten k counts))]
    (map merge coll new-vals)))

Util Fns

Is this a valid argument to hydrate?

(defn- valid-hydration-form?
  [k]
  (or (keyword? k)
      (and (sequential? k)
           (keyword? (first k))
           (every? valid-hydration-form? (rest k)))))

Append to a keyword.

 (kw-append :user "_id") -> :user_id
(defn- kw-append
  [k suffix]
  (keyword (str (name k) suffix)))

Return a map of hydration keywords to functions that should be used to hydrate them, e.g.

 {:fields #'my-project.models.table/fields
  :tables #'my-project.models.database/tables
  ...}

These functions are ones that are marked METADATA-KEY, e.g. ^:hydrate or ^:batched-hydrate.

(defn- lookup-functions-with-metadata-key
  [metadata-key]
  (loop [m {}, [[k f] & more] (for [ns          (all-ns)
                                    [symb varr] (ns-interns ns)
                                    :let        [hydration-key (metadata-key (meta varr))]
                                    :when       hydration-key]
                                [(if (true? hydration-key)
                                   (keyword (name symb))
                                   hydration-key)
                                 varr])]
    (cond
      (not k) m
      (m k)   (throw (Exception.
                      (format "Duplicate `^%s` functions for key '%s': %s and %s." metadata-key k (m k) f)))
      :else   (recur (assoc m k f) more))))

Automagic Batched Hydration (via :model-keys)

Return map of hydration-key -> model e.g. :user -> User.

This is built pulling the hydration-keys set from all of our entities.

(defn- require-model-namespaces-and-find-hydration-fns
  []
  (into {} (for [ns       (all-ns)
                 [_ varr] (ns-publics ns)
                 :let     [model (var-get varr)]
                 :when    (models/model? model)
                 :let     [hydration-keys (models/hydration-keys model)]
                 k        hydration-keys]
             {k model})))
(def ^:private automagic-batched-hydration-key->model* (atom nil))

Get a map of hydration keys to corresponding models.

(defn- automagic-batched-hydration-key->model
  []
  (or @automagic-batched-hydration-key->model*
      (reset! automagic-batched-hydration-key->model* (require-model-namespaces-and-find-hydration-fns))))

Can we do a batched hydration of RESULTS with key K?

(defn- can-automagically-batched-hydrate?
  [results k]
  (let [k-id-u (kw-append k "_id")
        k-id-d (kw-append k "-id")
        contains-k-id? (fn [obj]
                         (or (contains? obj k-id-u)
                             (contains? obj k-id-d)))]
    (and (contains? (automagic-batched-hydration-key->model) k)
         (every? contains-k-id? results))))

Hydrate keyword DEST-KEY across all RESULTS by aggregating corresponding source keys (DEST-KEY_id), doing a single db/select, and mapping corresponding objects to DEST-KEY.

(defn- automagically-batched-hydrate
  [results dest-key]
  {:pre [(keyword? dest-key)]}
  (let [model       ((automagic-batched-hydration-key->model) dest-key)
        source-keys #{(kw-append dest-key "_id") (kw-append dest-key "-id")}
        ids         (set (for [result results
                               :when  (not (get result dest-key))
                               :let   [k (some result source-keys)]
                               :when  k]
                           k))
        primary-key (models/primary-key model)
        objs        (if (seq ids)
                      (into {} (for [item (db/select model, primary-key [:in ids])]
                                 {(primary-key item) item}))
                      (constantly nil))]
    (for [result results
          :let [source-id (some result source-keys)]]
      (if (get result dest-key)
        result
        (assoc result dest-key (objs source-id))))))

Function-Based Batched Hydration (fns marked ^:batched-hydrate)

(def ^:private hydration-key->batched-f*
  (atom nil))

Map of keys to functions marked ^:batched-hydrate for them.

(defn- hydration-key->batched-f
  []
  (or @hydration-key->batched-f*
      (reset! hydration-key->batched-f* (lookup-functions-with-metadata-key :batched-hydrate))))
(defn- can-fn-based-batched-hydrate? [_ k]
  (contains? (hydration-key->batched-f) k))
(defn- fn-based-batched-hydrate
  [results k]
  {:pre [(keyword? k)]}
  (((hydration-key->batched-f) k) results))

Function-Based Simple Hydration (fns marked ^:hydrate)

(def ^:private hydration-key->f*
  (atom nil))

Fetch a map of keys to functions marked ^:hydrate for them.

(defn- hydration-key->f
  []
  (or @hydration-key->f*
      (reset! hydration-key->f* (lookup-functions-with-metadata-key :hydrate))))

Hydrate keyword K in results by calling corresponding functions when applicable.

(defn- simple-hydrate
  [results k]
  {:pre [(keyword? k)]}
  (for [result results]
    ;; don't try to hydrate if they key is already present. If we find a matching fn, hydrate with it
    (when result
      (or (when-not (k result)
            (when-let [f ((hydration-key->f) k)]
              (assoc result k (f result))))
          result))))

Resetting Hydration keys (for REPL usage)

Clear out the cached hydration keys. Useful when doing interactive development and defining new hydration functions.

(defn flush-hydration-key-caches!
  []
  (reset! automagic-batched-hydration-key->model* nil)
  (reset! hydration-key->batched-f*               nil)
  (reset! hydration-key->f*                       nil))

Primary Hydration Fns

(declare hydrate)

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

(defn- hydrate-vector
  [results [k & more :as vect]]
  (assert (> (count vect) 1)
    (format (str "Replace '%s' with '%s'. Vectors are for nested hydration. "
                 "There's no need to use one when you only have a single key.")
            vect (first vect)))
  (let [results (hydrate results k)]
    (if-not (seq more)
      results
      (counts-apply results k #(apply hydrate % more)))))

Hydrate a single keyword.

(defn- hydrate-kw
  [results k]
  (cond
    (can-automagically-batched-hydrate? results k) (automagically-batched-hydrate results k)
    (can-fn-based-batched-hydrate? results k)      (fn-based-batched-hydrate results k)
    :else                                          (simple-hydrate results k)))

Hydrate a single hydration form.

(defn- hydrate-1
  [results k]
  (if (keyword? k)
    (hydrate-kw results k)
    (hydrate-vector results k)))

Hydrate many hydration forms across a sequence of RESULTS by recursively calling hydrate-1.

(defn- hydrate-many
  [results k & more]
  (let [results (hydrate-1 results k)]
    (if-not (seq more)
      results
      (recur results (first more) (rest more)))))

Public Interface

                         hydrate <-------------+
                           |                   |
                       hydrate-many            |
                           | (for each form)   |
                       hydrate-1               | (recursively)
                           |                   |
                keyword? --+-- vector?         |
                   |             |             |
              hydrate-kw    hydrate-vector ----+
                   |
          can-automagically-batched-hydrate?
                         |
        true ------------+----------------- false
         |                                    |
automagically-batched-hydrate    can-fn-based-batched-hydrate?
                                             |
                           true -------------+------------- false
                            |                                 |
                 fn-based-batched-hydrate              simple-hydrate

Hydrate a single object or sequence of objects.

Automagic Batched Hydration (via hydration-keys)

hydrate attempts to do a batched hydration where possible. If the key being hydrated is defined as one of some model's hydration-keys, hydrate will do a batched db/select if a corresponding key ending with _id is found in the objects being batch hydrated.

(hydrate [{:user_id 100}, {:user_id 101}] :user)

Since :user is a hydration key for User, a single db/select will used to fetch Users:

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

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

Function-Based Batched Hydration (via functions marked ^:batched-hydrate)

If the key can't be hydrated auto-magically with the appropriate :hydration-keys, hydrate will look for a function tagged with :batched-hydrate in its metadata, and use that instead. If a matching function is found, it is called with a collection of objects, e.g.

(defn with-fields
  "Efficiently add `Fields` to a collection of TABLES."
  {:batched-hydrate :fields}
  [tables]
  ...)

(let [tables (get-some-tables)]
  (hydrate tables :fields))     ; uses with-fields

By default, the function will be used to hydrate keys that match its name; as in the example above, you can specify a different key to hydrate for in the metadata instead.

Simple Hydration (via functions marked ^:hydrate)

If the key is not eligible for batched hydration, hydrate will look for a function or method tagged with :hydrate in its metadata, and use that instead; if a matching function is found, it is called on the object being hydrated and the result is assoced:

(defn ^:hydrate dashboard [{:keys [dashboard_id]}]
  (Dashboard dashboard_id))

(let [dc (DashboardCard ...)]
  (hydrate dc :dashboard))    ; roughly equivalent to (assoc dc :dashboard (dashboard dc))

As with :batched-hydrate functions, by default, the function will be used to hydrate keys that match its name; you can specify a different key to hydrate instead as the metadata value of :hydrate:

(defn ^{:hydrate :pk_field} pk-field-id [obj] ...) ; hydrate :pk_field with pk-field-id

Keep in mind that you can only define a single function/method to hydrate each key; move functions into the IModel interface as needed.

Hydrating Multiple Keys

You can hydrate several keys at one time:

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

Nested Hydration

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

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

(hydrate {...}
         [:a [:b :c] :e])
  -> {:a {:b {:c 1} :e 2}}
(defn hydrate
  [results k & ks]
  {:pre [(valid-hydration-form? k)
         (every? valid-hydration-form? ks)]}
  (when results
    (if (sequential? results)
      (if (empty? results)
        results
        (apply hydrate-many results k ks))
      (first (apply hydrate-many [results] k ks)))))
 

The defmodel macro, used to define Toucan models, and the IModel protocol and default implementations, which implement Toucan model functionality.

(ns toucan.models
  (:require [clojure.walk :refer [postwalk]]
            [honeysql.format :as hformat]
            [potemkin.types :as p.types]
            [toucan.util :as u])
  (:import honeysql.format.ToSql))

Configuration

Root Model Namespace

The root model namespace is the parent namespace of all Toucan models. Toucan knows how to automatically load namespaces where models live, which is handy for avoiding circular references; to facilitate this, Toucan models need to live in places that match an expected pattern.

For example, a model named UserFollow must live in the namespace <root-model-namespace>.user-follow.

The root model namespace defaults to models; in the example above, UserFollow would live in models.user-follow.

This is almost certainly not what you want; set your own value by calling set-root-namespace!:

(models/set-root-namespace! 'my-project.models)

After setting the default model root namespace as in the example above, Toucan will look for UserFollow in my-project.models.user-follow.

(defonce ^:private -root-namespace (atom 'models))

Set the root namespace where all models are expected to live.

 (set-root-namespace! 'my-project.models)

In this example, Toucan would look for a model named UserFollow in the namespace my-project.models.user-follow.

(defn set-root-namespace!
  [new-root-namespace]
  {:pre [(symbol? new-root-namespace)]}
  (reset! -root-namespace new-root-namespace))

Fetch the parent namespace for all Toucan models.

(defn root-namespace
  []
  @-root-namespace)

Types

Model types are a easy way to define functions that should be used to transform values of a certain column when they come out from or go into the database.

For example, suppose you had a Venue model, and wanted the value of its :category column to automatically be converted to a Keyword when it comes out of the DB, and back into a string when put in. You could let Toucan know to take care of this by defining the model as follows:

(defmodel Venue :my_venue_table)

(extend (class Venue)
  models/IModel
  (merge models/IModelDefaults
         {:types (constantly {:category :keyword})}))

Whenever you fetch a Venue, Toucan will automatically apply the appropriate :out function for values of :category:

(db/select-one Venue) ; -> {:id 1, :category :bar, ...}

In the other direction, insert! and update! will automatically do the reverse, and call the appropriate :in function.

:keyword is the only Toucan type defined by default, but adding more is simple.

You can add a new type by calling add-type!:

;; add a :json type (using Cheshire) will serialize objects as JSON
;; going into the DB, and deserialize JSON coming out from the DB
(add-type! :json
  :in  json/generate-string
  :out #(json/parse-string % keyword))

In the example above, values of any columns marked as :json would be serialized as JSON before going into the DB, and deserialized from JSON when coming out of the DB.

(defonce ^:private type-fns
  (atom {:keyword {:in  u/keyword->qualified-name
                   :out keyword}}))

Add a new type mapping for type named by key K. Supply mappings for the functions that should prepare value when it goes :in to the database, and for when it comes :out.

 ;; add a :json type (using Cheshire) will serialize objects as JSON
 ;; going into the DB, and deserialize JSON coming out from the DB
 (add-type! :json
   :in  json/generate-string
   :out #(json/parse-string % keyword))
(defn add-type!
  {:style/indent 1}
  [k & {:keys [in out]}]
  {:pre [(fn? in) (fn? out)]}
  (swap! type-fns assoc k {:in in, :out out}))

Properties

Model properties are a powerful way to extend the functionality of Toucan models.

With properties, you can define custom functions that can modify the values (or even add new ones) of an object before it is saved (via the insert! and update! family of functions) or when it comes out of the DB (via the select family of functions).

Properties are global, which lets you define a single set of functions that can be applied to multiple models that have the same property, without having to define repetitive code in model methods such as pre-insert!.

For example, suppose you have several models with :created-at and :updated-at columns. Whenever a new instance of these models is inserted, you want to set :created-at and :updated-at to be the current time; whenever an instance is updated, you want to update :updated-at.

You could handle this behavior by defining custom implementations for pre-insert and pre-update for each of these models, but that gets repetitive quickly. Instead, you can simplfy this behavior by defining a new property that can be shared by multiple models:

(add-property! :timestamped?
  :insert (fn [obj _]
            (let [now (java.sql.Timestamp. (System/currentTimeMillis))]
              (assoc obj :created-at now, :updated-at now)))
  :update (fn [obj _]
            (assoc obj :updated-at (java.sql.Timestamp. (System/currentTimeMillis)))))

(defmodel Venue :my_venue_table)

(extend (class Venue)
  models/IModel
  (merge models/IModelDefaults
         {:properties (constantly {:timestamped? true})}))

In this example, before a Venue is inserted, a new value for :created-at and :updated-at will be added; before one is updated, a new value for :updated-at will be added.

Property functions can be defined for any combination of :insert, :update, and :select. If these functions are defined, they will be called as such:

(fn [object property-value])

where property-value is the value for the key in question returned by the model's implementation of properties.

In the example above, :timestamped? is set to true for Venue; since we're not interested in the value in the example above we simply ignore it (by binding it to _).

You can set the value to any truthy value you'd like, which can be used to customize behavior for different models, making properties even more flexible.

(defonce ^:private property-fns (atom nil))

Define a new model property and set the functions used to implement its functionality. See documentation for more details.

 (add-property! :timestamped?
   :insert (fn [obj _]
             (let [now (java.sql.Timestamp. (System/currentTimeMillis))]
               (assoc obj :created-at now, :updated-at now)))
   :update (fn [obj _]
             (assoc obj :updated-at (java.sql.Timestamp. (System/currentTimeMillis)))))
(defn add-property!
  {:style/indent 1}
  [k & {:keys [insert update select]}]
  {:pre [(or (not insert) (fn? insert))
         (or (not update) (fn? update))
         (or (not select) (fn? select))]}
  (swap! property-fns assoc k {:insert insert, :update update, :select select}))

IModel Interface

The IModel protocol defines the various methods that are used to provide custom behavior for various models.

This protocol contains the various methods model classes can optionally implement. All methods have a default implementation provided by IModelDefaults; new models created with the defmodel macro automatically implement this protocol using those default implementations. To override one or more implementations, use extend and merge your custom implementations with IModelDefaults:

 (defmodel MyModel)

 (extend (class MyModel)
   IModel
   (merge IModelDefaults {...}))
(p.types/defprotocol+ IModel
  (pre-insert [this]
    "Gets called by `insert!` immediately before inserting a new object.
     This provides an opportunity to do things like encode JSON or provide default values for certain fields.
         (pre-insert [query]
           (let [defaults {:version 1}]
             (merge defaults query))) ; set some default values")
  ;TODO add support for composite keys
  (primary-key ^clojure.lang.Keyword [this]
    "Defines the primary key. Defaults to :id
        (primary-key [_] :id)
    NOTE: composite keys are currently not supported")
  (post-insert [this]
    "Gets called by `insert!` with an object that was newly inserted into the database.
     This provides an opportunity to trigger specific logic that should occur when an object is inserted or modify the
     object that is returned. The value returned by this method is returned to the caller of `insert!`. The default
     implementation is `identity`.
       (post-insert [user]
         (assoc user :newly-created true))
       (post-insert [user]
         (add-user-to-magic-perm-groups! user)
         user)")
  (pre-update [this]
    "Called by `update!` before DB operations happen. A good place to set updated values for fields like `updated-at`,
     or to check preconditions.")
  (post-update [this]
    "Gets called by `update!` with an object that was successfully updated in the database.
     This provides an opportunity to trigger specific logic that should occur when an object is updated.
     The value returned by this method is not returned to the caller of `update!`. The default
     implementation is `nil` (not invoked).
     Note: This method is *not* invoked when calling `update!` with a `honeysql-form` form.
       (post-update [user]
         (audit-user-updated! user)")
  (post-select [this]
    "Called on the results from a call to `select` and similar functions. Default implementation doesn't do anything,
     but you can provide custom implementations to do things like remove sensitive fields or add dynamic new ones.
  For example, let's say we want to add a `:name` field to Users that combines their `:first-name` and `:last-name`:
      (defn- post-select [user]
        (assoc user :name (str (:first-name user) \" \" (:last-name user))))
  Then, when we select a User:
      (User 1) ; -> {:id 1, :first-name \"Cam\", :last-name \"Saul\", :name \"Cam Saul\"}")
  (pre-delete [this]
    "Called by `delete!` for each matching object that is about to be deleted.
     Implementations can delete any objects related to this object by recursively calling `delete!`, or do
     any other cleanup needed, or check some preconditions that must be fulfilled before deleting an object.
  The output of this function is ignored.
        (pre-delete [{database-id :id :as database}]
          (delete! Card :database_id database-id)
          ...)")
  (default-fields ^clojure.lang.Sequential [this]
    "Return a sequence of keyword field names that should be fetched by default when calling
     `select` or invoking the model (e.g., `(Database 1)`).")
  (hydration-keys ^clojure.lang.Sequential [this]
    "The `hydration-keys` method can be overrode to specify the keyword field names that should be hydrated
     as instances of this model. For example, `User` might include `:creator`, which means `hydrate` will
     look for `:creator_id` or `:creator-id` in other objects to find the User ID, and fetch the `Users`
     corresponding to those values.")
  (types ^clojure.lang.IPersistentMap [this]
    "Return a map of keyword field names to their types for fields that should be serialized/deserialized in a special
     way. Values belonging to a type are sent through an input function before being inserted into the DB, and sent
     through an output function on their way out. `:keyword` is the only type enabled by default; you can add more by
     calling `add-type!`:
       (add-type! :json, :in json/generate-string, :out json/parse-string)
   Set the types for a model like so:
       ;; convert `:category` to a keyword when it comes out of the DB; convert back to a string before going in
       (types [_] {:category :keyword})")
  (properties ^clojure.lang.IPersistentMap [this]
    "Return a map of properties of this model. Properties can be used to implement advanced behavior across many
     different models; see the documentation for more details. Declare a model's properties as such:
       (properties [_] {:timestamped? true})
  Define functions to handle objects with those properties using `add-property!`:
      (add-property! :timestamped?
        :insert (fn [obj] (assoc obj :created-at (new-timestamp), :updated-at (new-timestamp)))
        :update (fn [obj] (assoc obj :updated-at (new-timestamp))))"))

INTERNAL IMPL

Apply the appropriate type-fns for OBJ.

(defn- apply-type-fns
  [obj direction]
  (into obj (for [[col type] (types obj)]
              (when-let [v (get obj col)]
                {col ((get-in @type-fns [type direction]) v)}))))
(defn- apply-property-fns
  [context obj]
  (loop [obj obj, [[k v] & more] (seq (properties obj))]
    (let [f (get-in @property-fns [k context])]
      (cond
        (not k) obj
        f       (recur (f obj v) more)
        :else   (recur obj       more)))))

Used by internal functions like do-post-select.

(p.types/defprotocol+ ICreateFromMap
  (^:private map-> [klass, ^clojure.lang.IPersistentMap m]
   "Convert map M to instance of record type KLASS."))

these functions call (map-> model ...) twice to make sure functions like pre-insert/post-select didn't do something that accidentally removed the typing

Don't call this directly! Apply functions like pre-insert before inserting an object into the DB.

(defn do-pre-insert
  [model obj]
  (as-> obj <>
    (map-> model <>)
    (pre-insert <>)
    (map-> model <>)
    (apply-type-fns <> :in)
    (apply-property-fns :insert <>)))

Don't call this directly! Apply internal functions like pre-update before updating an object in the DB.

(defn do-pre-update
  [model obj]
  (as-> obj <>
    (map-> model <>)
    (pre-update <>)
    (map-> model <>)
    (apply-type-fns <> :in)
    (apply-property-fns :update <>)))

Don't call this directly! Apply internal functions like post-select when an object is retrieved from the DB.

(defn do-post-select
  [model obj]
  (as-> obj <>
    (map-> model <>)
    (apply-type-fns <> :out)
    (post-select <>)
    (map-> model <>)
    (apply-property-fns :select <>)))

Default implementations for IModel methods.

(def IModelDefaults
  {:default-fields (constantly nil)
   :primary-key    (constantly :id)
   :types          (constantly nil)
   :properties     (constantly nil)
   :pre-insert     identity
   :post-insert    identity
   :pre-update     identity
   :post-update    nil
   :post-select    identity
   :pre-delete     (constantly nil)
   :hydration-keys (constantly nil)})

Fetch an object with a specific ID or all objects of type ENTITY from the DB.

 (invoke-model Database)           -> seq of all databases
 (invoke-model Database 1)         -> Database w/ ID 1
 (invoke-model Database :id 1 ...) -> A single Database matching some key-value args
(defn- invoke-model
  ([model]
   ((resolve 'toucan.db/select) model))
  ([model id]
   (when id
     (invoke-model model (primary-key model) id)))
  ([model k v & more]
   (apply (resolve 'toucan.db/select-one) model k v more)))

Is model a valid toucan model?

(defn model?
  [model]
  (and (record? model)
       (::model model)))

Check whether OBJ is an model (e.g. Database) or an object from the DB; if an model, call invoked-model; otherwise call get.

We use the same record type (e.g., DatabaseInstance) for both the "model" (e.g., Database) and objects fetched from the DB ("instances"). Model definitions have the key ::model assoced so we can differentiate. Invoking an instance calls get so you can do things like (db :name) as if it were a regular map.

(defn invoke-model-or-instance
  [obj & args]
  (apply (if (model? obj)
           invoke-model
           get)
         obj args))

DEFMODEL MACRO

Macro helper, generates

   (~'invoke [this#]
    (invoke-model-or-instance this#))
   (~'invoke [this# id#]
    (invoke-model-or-instance this# id#))
   (~'invoke [this# arg1# arg2#]
    (invoke-model-or-instance this# arg1# arg2#))
   ,,,
(defn- ifn-invoke-forms
  []
  (let [args (map #(symbol (str "arg" %)) (range 1 19))
        arg-lists (reductions conj ['this] args)]
    (for [l arg-lists]
      (list 'invoke l (concat `(invoke-model-or-instance) l)))))

Make a symbol fully qualified by resolving it as a var in the current namespace.

 conj        ;;=> clojure.core/conj
 str/join    ;;=> clojure.string/join
 foo.bar/baz ;;=> foo.bar/baz
(defn- fully-qualified-symbol
  [s]
  (let [{:keys [ns name]} (meta (resolve s))]
    (symbol (str ns "/" name))))

Take in forms as passed to defrecord or extend-type (protocol or interface name followed by method definitions), and return a map suitable for use with extend.

(IFn (invoke [this] this)) ;;=> {IFn {:invoke (fn [this] this)}}
(defn- method-forms-map
  [forms]
  (second
   (reduce (fn [[type acc] form]
             (if (symbol? form)
               [(fully-qualified-symbol form) acc]
               [type (assoc-in acc [type (keyword (first form))] `(fn ~@(drop 1 form)))])) [nil {}] forms)))

Define a new "model". Models encapsulate information and behaviors related to a specific table in the application DB, and have their own unique record type.

defmodel defines a backing record type following the format <model>Instance. For example, the class associated with User is <root-namespace>.user/UserInstance. (The root namespace defaults to models but can be configured via set-root-namespace!)

This class is used for both the titular model (e.g. User) and for objects that are fetched from the DB. This means they can share the IModel protocol and simplifies the interface somewhat; functions like types work on either the model or instances fetched from the DB.

 (defmodel User :user_table)  ; creates class `UserInstance` and DB model `User`

 (db/select User, ...)  ; use with `toucan.db` functions. All results are instances of `UserInstance`

The record type automatically extends IModel with IModelDefaults, but you can override specific methods, or implement other protocols, by passing them to defmodel, the same way you would with defrecord.

 (defmodel User :user_table
   IModel
   (hydration-keys [_]
     [:user])
   (properties [_]
     {:timestamped true})
   (pre-insert [user]
     user))

This is equivalent to:

 (extend (class User)             ; it's somewhat more readable to write `(class User)` instead of `UserInstance`
   IModel (merge IModelDefaults
                  {...}))

Finally, the model itself is invokable. Calling with no args returns all values of that object; calling with a single arg can be used to fetch a specific instance by its integer ID.

 (Database)                       ; return a seq of *all* Databases (as instances of `DatabaseInstance`)
 (Database 1)                     ; return Database 1
(defmacro defmodel
  {:arglists     '([model table-name] [model docstr? table-name])
   :style/indent [2 :form :form [1]]}
  [model & args]
  (let [[docstr table-name] (if (string? (first args))
                              args
                              (list nil (first args)))
        extend-forms        (if (string? (first args))
                              (drop 2 args)
                              (drop 1 args))
        instance            (symbol (str model "Instance"))
        map->instance       (symbol (str "map->" instance))
        defrecord-form      `(p.types/defrecord+ ~instance []
                               clojure.lang.Named
                               (~'getName [~'_] ~(name model))
                               (~'getNamespace [~'_] ~(name (ns-name *ns*)))
                               clojure.lang.IFn
                               ~@(ifn-invoke-forms)
                               (~'applyTo [~'this ^clojure.lang.ISeq ~'args]
                                (apply invoke-model-or-instance ~'this ~'args)))
        ;; Replace the implementation of `empty`. It's either this, or using the
        ;; lower level `deftype`, and re-implementing all of `defrecord`
        defrecord-form (postwalk (fn [f]
                                   (if (and (seq? f) (= (first f) 'clojure.core/empty))
                                     `(empty [_#] (~map->instance {}))
                                     f))
                                 (macroexpand defrecord-form))]
    `(do
       ~defrecord-form
       (extend ~instance
         ~@(mapcat identity (merge-with (fn [this that] `(merge ~this ~that))
                              `{toucan.models/IModel         toucan.models/IModelDefaults
                                toucan.models/ICreateFromMap {:map-> (fn [~'_ & args#] (apply ~map->instance args#))}
                                honeysql.format/ToSql        {:to-sql (comp hformat/to-sql keyword :table)}}
                              (method-forms-map extend-forms))))
       (def ~(vary-meta model assoc
                        :tag      (symbol (str (namespace-munge *ns*) \. instance))
                        :arglists ''([] [id] [& kvs])
                        :doc      (or docstr
                                      (format "Entity for '%s' table; instance of %s." (name table-name) instance)))
         (~map->instance {:table  ~table-name
                          :name   ~(name model)
                          ::model true})))))
 

Utility functions for writing tests with Toucan models.

(ns toucan.util.test
  (:require [potemkin.types :as p.types]
            [toucan.db :as db]))

TEMP OBJECTS

For your convenience Toucan makes testing easy with Temporary Objects. A temporary object is created and made available to some body of code, and then wiped from that database via a finally statement (i.e., whether the body completes successfully or not). This makes it easy to write tests that do not change your test database when they are ran.

Here's an example of a unit test using a temporary object created via with-temp:

;; Make sure newly created users aren't admins
(expect false
  (with-temp User [user {:first-name "Cam", :last-name "Saul"}]
    (is-admin? user)))

In this example, a new instance of User is created (via the normal insert! pathway), and bound to user; the body of with-temp (the test-something fncall) is executed. Immediately after, the user is removed from the Database, but the entire statement returns the results of the body (hopefully false).

Often a Model will require that many fields be NOT NULL, and specifying all of them in every test can get tedious. In the example above, we don't care about the :first-name or :last-name of the user. We can provide default values for temporary objects by implementing the WithTempDefaults protocol:

(defn- random-name
  "Generate a random name of 10 uppercase characters"
  []
  (apply str (map char (repeatedly 10 #(rand-nth (range (int \A) (inc (int \Z))))))))

(extend-protocol WithTempDefaults
  (class User)
  (with-temp-defaults [_] {:first-name (random-name), :last-name (random-name)}))

Now whenever we use with-temp to create a temporary User, a random :first-name and :last-name will be provided.

(with-temp User [user]
  user)
;; -> {:first-name "RIQGVIDTZN", :last-name "GMYROFEZYO", ...}

You can still override any of the defaults, however:

(with-temp User [user {:first-name "Cam"}]
  user)
;; -> {:first-name "Cam", :last-name "OVTAAJBVOF"}

Finally, Toucan provides a couple more advanced versions of with-temp. The first, with-temp*, can be used to create multiple objects at once:

(with-temp* [User         [user]
             Conversation [convo {:user_id (:id user)}]]
  ...)

Each successive object can reference the temp object before it; the form is equivalent to writing multiple with-temp forms.

The last helper macro is available if you use the expectations unit test framework:

;; Make sure our get-id function works on users
(expect-with-temp [User [user {:first-name "Cam"}]]
  (:id user)
  (get-id user))

This macro makes the temporary object available to both the "expected" and "actual" parts of the test. (PRs for similar macros for other unit test frameworks are welcome!)

Protocol defining the with-temp-defaults method, which provides default values for new temporary objects.

(p.types/defprotocol+ WithTempDefaults
  (with-temp-defaults ^clojure.lang.IPersistentMap [this]
    "Return a map of default values that should be used when creating a new temporary object of this model.
       ;; Use a random first and last name for new temporary Users unless otherwise specified
       (extend-protocol WithTempDefaults
         (class User)
         (with-temp-defaults [_] {:first-name (random-name), :last-name (random-name)}))"))

default impl

(extend Object
  WithTempDefaults
  {:with-temp-defaults (constantly {})})

Internal implementation of with-temp (don't call this directly).

(defn do-with-temp
  [model attributes f]
  (let [temp-object (db/insert! model (merge (when (satisfies? WithTempDefaults model)
                                               (with-temp-defaults model))
                                             attributes))]
    (try
      (f temp-object)
      (finally
        (db/delete! model :id (:id temp-object))))))

Create a temporary instance of ENTITY bound to BINDING-FORM, execute BODY, then deletes it via delete!.

Our unit tests rely a heavily on the test data and make some assumptions about the DB staying in the same clean state. This allows us to write very concise tests. Generally this means tests should "clean up after themselves" and leave things the way they found them.

with-temp should be preferrable going forward over creating random objects without deleting them afterward.

(with-temp EmailReport [report {:creator_id (user->id :rasta)
                                :name       (random-name)}]
  ...)
(defmacro with-temp
  [model [binding-form & [options-map]] & body]
  `(do-with-temp ~model ~options-map (fn [~binding-form]
                                       ~@body)))

Like with-temp but establishes multiple temporary objects at the same time.

 (with-temp* [Database [{database-id :id}]
              Table    [table {:db_id database-id}]]
   ...)
(defmacro with-temp*
  [model-bindings & body]
  (loop [[pair & more] (reverse (partition 2 model-bindings)), body `(do ~@body)]
    (let [body `(with-temp ~@pair
                  ~body)]
      (if (seq more)
        (recur more body)
        body))))

EXPECTATIONS HELPER MACROS

(defn- has-expectations-dependency? []
  (try (require 'expectations)
       true
       (catch Throwable _
         false)))

Combines expect with a with-temp* form. The temporary objects established by with-temp* are available to both EXPECTED and ACTUAL.

 (expect-with-temp [Database [{database-id :id}]]
    database-id
    (get-most-recent-database-id))
(when (has-expectations-dependency?)
  (defmacro expect-with-temp
    {:style/indent 1}
    [with-temp*-form expected actual]
    ;; use `gensym` instead of auto gensym here so we can be sure it's a unique symbol every time. Otherwise since
    ;; expectations hashes its body to generate function names it will treat every usage of `expect-with-temp` as
    ;; the same test and only a single one will end up being ran
    (let [with-temp-form (gensym "with-temp-")]
      `(let [~with-temp-form (delay (with-temp* ~with-temp*-form
                                      [~expected ~actual]))]
         (expectations/expect
           ;; if dereferencing with-temp-form throws an exception then expect Exception <-> Exception will pass;
           ;; we don't want that, so make sure the expected is nil
           (try
             (first @~with-temp-form)
             (catch Throwable ~'_))
           (second @~with-temp-form))))))
 

Utility functions used by other Toucan modules.

(ns toucan.util
  (:require [clojure.string :as s])
  (:import java.util.Locale))

Return keyword K as a string, including its namespace, if any (unlike name).

 (keyword->qualified-name :type/FK) -> "type/FK"
(defn keyword->qualified-name
  [k]
  (when k
    (s/replace (str k) #"^:" "")))

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
  [^CharSequence s]
  (.. s toString (toLowerCase (Locale/US))))