Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JESI-2566 #7

Merged
merged 2 commits into from
Jan 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ jobs:
command: lein check-namespace-decls
no_output_timeout: 5m

snapshot:
working_directory: /home/circleci/metabase/toucan
docker:
- image: circleci/clojure:lein-2.8.1
steps:
- attach_workspace:
at: /home/circleci
- run:
name: Append -SNAPSHOT to version
command: lein change version str "\"-SNAPSHOT\""
- run:
name: Deploy to clojars
command: lein deploy clojars

deploy:
working_directory: /home/circleci/metabase/toucan
docker:
Expand Down Expand Up @@ -130,6 +144,16 @@ workflows:
- lint-namespace-decls:
requires:
- checkout
- snapshot:
requires:
- test
- lint-bikeshed
- lint-eastwood
- lint-docstrings
- lint-namespace-decls
filters:
branches:
ignore: master
- deploy:
requires:
- test
Expand Down
7 changes: 4 additions & 3 deletions project.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(defproject toucan "1.11.0-SNAPSHOT"
(defproject io.jesi/toucan "1.11.0"
:description "Functionality for defining your application's models and querying the database."
:url "https://github.com/metabase/toucan"
:license {:name "Eclipse Public License"
Expand All @@ -11,7 +11,7 @@
[honeysql "0.9.4"]]
:javac-options ["-target" "1.7", "-source" "1.7"]
:aliases {"bikeshed" ["bikeshed" "--max-line-length" "118" "--var-redefs" "false"]
"lint" ["do" ["eastwood"] "bikeshed" "docstring-checker" "check-namespace-decls"]
"lint" ["do" ["eastwood"] ["bikeshed"] ["docstring-checker"] ["check-namespace-decls"]]
"test" ["expectations"]
"start-db" ["shell" "./start-db"] ; `lein start-db` and stop-db are conveniences for running a test database via Docker
"stop-db" ["shell" "docker" "stop" "toucan_test"]}
Expand All @@ -23,7 +23,8 @@
:exclusions [org.clojure/clojure]]
[lein-bikeshed "0.5.1"]
[lein-check-namespace-decls "1.0.1"]
[lein-expectations "0.0.8"]]
[lein-expectations "0.0.8"]
[lein-shell "0.5.0"]]
:jvm-opts ["-Xverify:none"]
:eastwood {:add-linters [:unused-locals
:unused-private-vars]
Expand Down
74 changes: 35 additions & 39 deletions src/toucan/db.clj
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
[helpers :as h]]
[toucan
[models :as models]
[util :as u]])
(:import clojure.lang.Keyword))
[util :as u]]))

;;; CONFIGURATION
;;; ==================================================================================================================
Expand Down Expand Up @@ -247,18 +246,18 @@
[& body]
`(-do-with-debug-print-queries (fn [] ~@body)))


(defn- honeysql->sql
(defn honeysql->sql
"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."
[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?))))]
(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)))
Expand Down Expand Up @@ -402,7 +401,7 @@
([model]
(simple-select-one model {}))
([model honeysql-form]
(first (simple-select model (h/limit honeysql-form 1)))))
(first (simple-select model (h/limit honeysql-form (hsql/inline 1))))))

(defn execute!
"Compile `honeysql-form` and call `jdbc/execute!` against the application DB.
Expand Down Expand Up @@ -448,7 +447,6 @@
first-arg (recur (merge honeysql-form first-arg) butfirst)
:else honeysql-form)))


;;; ### UPDATE!

(defn- method-implemented? [^clojure.lang.Keyword methodk model]
Expand All @@ -470,11 +468,12 @@

(^Boolean [model id kvs]
{:pre [(some? id) (map? kvs) (every? keyword? (keys kvs))]}
(let [model (resolve-model model)
kvs (-> (models/do-pre-update model (assoc kvs :id id))
(dissoc :id))
updated? (update! model (-> (h/sset {} kvs)
(where :id id)))]
(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)))
Expand Down Expand Up @@ -511,7 +510,7 @@

(def ^:private inserted-id-keys
"Different possible keys that might come back for the ID of a newly inserted row. Differs by DB."
[ ;; Postgres, newer H2, and most others return :id
[;; Postgres, newer H2, and most others return :id
:id
;; :generated_key is returned by MySQL
:generated_key
Expand All @@ -524,9 +523,9 @@

(defn get-inserted-id
"Get the ID of a row inserted by `jdbc/db-do-prepared-return-keys`."
[insert-result]
[primary-key insert-result]
(when insert-result
(some insert-result inserted-id-keys)))
(some insert-result (cons primary-key inserted-id-keys))))

(defn simple-insert-many!
"Do a simple JDBC `insert!` of multiple objects into the database.
Expand All @@ -540,12 +539,13 @@
[model row-maps]
{:pre [(sequential? row-maps) (every? map? row-maps)]}
(when (seq row-maps)
(let [model (resolve-model model)]
(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))))))
(->> (jdbc/db-do-prepared-return-keys (connection) false sql {}) ; false = don't use a transaction
(get-inserted-id primary-key)))))))

(defn insert-many!
"Insert several new rows into the Database. Resolves `entity`, and calls `pre-insert` on each of the `row-maps`.
Expand Down Expand Up @@ -596,7 +596,6 @@
([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
Expand Down Expand Up @@ -627,7 +626,7 @@
{:style/indent 1}
[model & options]
(let [model (resolve-model model)]
(apply select-one-field :id model options)))
(apply select-one-field (models/primary-key model) model options)))

(defn count
"Select the count of objects matching some condition.
Expand Down Expand Up @@ -676,7 +675,8 @@
(select-ids 'Table :db_id 1) -> #{1 2 3 4}"
{:style/indent 1}
[model & options]
(apply select-field :id model options))
(let [model (resolve-model model)]
(apply select-field (models/primary-key model) model options)))

(defn select-field->field
"Select fields `k` and `v` from objects in the database, and return them as a map from `k` to `v`.
Expand All @@ -694,29 +694,28 @@
(select-field->id :name 'Database) -> {\"Sample Dataset\" 1, \"test-data\" 2}"
{:style/indent 2}
[field model & options]
(apply select-field->field field :id model options))
(let [model (resolve-model model)]
(apply select-field->field field (models/primary-key model) model options)))

(defn select-id->field
"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\"}"
{:style/indent 2}
[field model & options]
(apply select-field->field :id field model options))

(let [model (resolve-model model)]
(apply select-field->field (models/primary-key model) field model options)))

;;; ### EXISTS?

(defn exists?
"Easy way to see if something exists in the DB.

(db/exists? User :id 100)

NOTE: This only works for objects that have an `:id` field."
(db/exists? User :id 100)"
{:style/indent 1}
^Boolean [model & kvs]
(boolean (select-one model (apply where (h/select {} :id) kvs))))

(let [model (resolve-model model)]
(boolean (select-one-id model (apply where (h/select {} (models/primary-key model)) kvs)))))

;;; ### DELETE!

Expand Down Expand Up @@ -746,14 +745,11 @@
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)

NOTE: This function assumes objects have an `:id` column. There's an [open
issue](https://github.com/metabase/toucan/issues/3) to support objects that don't have one; until that is resolved,
you'll have to use `simple-delete!` instead when deleting objects with no `:id`."
(delete! Database :id 1)"
{:style/indent 1}
[model & conditions]
(let [model (resolve-model model)]
(let [model (resolve-model model)
primary-key (models/primary-key model)]
(doseq [object (apply select model conditions)]
(models/pre-delete object)
(simple-delete! model :id (:id object)))))
(simple-delete! model primary-key (primary-key object)))))
5 changes: 3 additions & 2 deletions src/toucan/hydrate.clj
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,10 @@
: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, :id [:in ids])]
{(:id item) item}))
(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)]]
Expand Down
17 changes: 13 additions & 4 deletions src/toucan/models.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
(:require [clojure.walk :refer [postwalk]]
[honeysql.format :as hformat]
[toucan.util :as u])
(:import clojure.lang.IFn
honeysql.format.ToSql))
(:import honeysql.format.ToSql))

;;; Configuration
;;; ==================================================================================================================
Expand Down Expand Up @@ -188,6 +187,7 @@
(extend (class MyModel)
IModel
(merge IModelDefaults {...}))"

(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.
Expand All @@ -196,6 +196,14 @@
(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
Expand Down Expand Up @@ -345,6 +353,7 @@
(def IModelDefaults
"Default implementations for `IModel` methods."
{:default-fields (constantly nil)
:primary-key (constantly :id)
:types (constantly nil)
:properties (constantly nil)
:pre-insert identity
Expand All @@ -365,7 +374,7 @@
((resolve 'toucan.db/select) model))
([model id]
(when id
(invoke-model model :id id)))
(invoke-model model (primary-key model) id)))
([model k v & more]
(apply (resolve 'toucan.db/select-one) model k v more)))

Expand Down Expand Up @@ -484,7 +493,7 @@

clojure.lang.IFn
~@(ifn-invoke-forms)
(~'applyTo [~'this ^ISeq ~'args]
(~'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
Expand Down
16 changes: 15 additions & 1 deletion test/toucan/db_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
[toucan.test-models
[address :refer [Address]]
[category :refer [Category]]
[phone-number :refer [PhoneNumber]]
[user :refer [User]]
[venue :refer [Venue]]]))

Expand Down Expand Up @@ -217,6 +218,14 @@
(db/update! User 1 :last-name "Era")
(db/select-one User :id 1)))

(expect
#toucan.test_models.phone_number.PhoneNumberInstance{:number "012345678", :country_code "AU"}
(test/with-clean-db
(let [id "012345678"]
(db/simple-insert! PhoneNumber {:number id, :country_code "US"})
(db/update! PhoneNumber id :country_code "AU")
(db/select-one PhoneNumber :number id))))

;; Test update-where!
(expect
[#toucan.test_models.user.UserInstance{:id 1, :first-name "Cam", :last-name "Saul"}
Expand Down Expand Up @@ -295,6 +304,11 @@
(test/with-clean-db
(db/insert! User {:first-name "Trash", :last-name "Bird"})))

(expect
#toucan.test_models.phone_number.PhoneNumberInstance{:number "012345678", :country_code "AU"}
(test/with-clean-db
(db/insert! PhoneNumber {:number "012345678", :country_code "AU"})))

;; The returned data must match what's been inserted in the table
(expect
#toucan.test_models.user.UserInstance{:id 4, :first-name "Grass", :last-name "HOPPER"}
Expand All @@ -304,7 +318,7 @@
;; get-inserted-id shouldn't fail if nothing is returned for some reason
(expect
nil
(db/get-inserted-id nil))
(db/get-inserted-id :id nil))

;; Test select-one
(expect
Expand Down
2 changes: 2 additions & 0 deletions test/toucan/hydrate_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -559,3 +559,5 @@
(hydrate [{:type :toucan}
{:type :pigeon}]
:is-bird?))

;TODO add test for selecting hydration for where (not= pk :id)
6 changes: 6 additions & 0 deletions test/toucan/test_models/phone_number.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
(ns toucan.test-models.phone-number
(:require [toucan.models :as models]))

(models/defmodel PhoneNumber :phone_numbers
models/IModel
(primary-key [_] :number))
8 changes: 7 additions & 1 deletion test/toucan/test_setup.clj
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@
id SERIAL PRIMARY KEY,
street_name text NOT NULL
);"
"TRUNCATE TABLE address RESTART IDENTITY CASCADE;"))
"TRUNCATE TABLE address RESTART IDENTITY CASCADE;"
;; Phone Number
"CREATE TABLE IF NOT EXISTS phone_numbers (
number TEXT PRIMARY KEY,
country_code VARCHAR(3) NOT NULL
);"
"TRUNCATE TABLE phone_numbers;"))


(def ^java.sql.Timestamp jan-first-2017 (Timestamp/valueOf "2017-01-01 00:00:00"))
Expand Down