Skip to content

Commit

Permalink
Add insert-returning-pk! and insert-returning-instance! (#139)
Browse files Browse the repository at this point in the history
(Singular versions of -pks! and -instances!)
  • Loading branch information
tsmacdonald authored Mar 27, 2023
1 parent a104567 commit 651dd55
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 78 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,6 @@ Let's define another after-select method, `::without-created-at`, to remove the
```clj
(t2/define-after-select ::without-created-at
[row]
(println "row:" row) ; NOCOMMIT
(dissoc row :created-at))

(derive :models/people.cool.without-created-at :models/people.cool)
Expand Down
30 changes: 25 additions & 5 deletions src/toucan2/insert.clj
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,21 @@
[& unparsed-args]
(pipeline/transduce-unparsed-with-default-rf :toucan.query-type/insert.pks unparsed-args))

(defn insert-returning-pk!
"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\"]`)."
{: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)))

(defn insert-returning-instances!
"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\"]]`."
"Like [[insert!]], but returns a vector of maps representing the inserted objects."
{:arglists '([modelable-columns row-or-rows-or-queryable]
[modelable-columns k v & more]
[modelable-columns columns row-vectors]
Expand All @@ -152,3 +161,14 @@
[:conn connectable modelable-columns columns row-vectors])}
[& unparsed-args]
(pipeline/transduce-unparsed-with-default-rf :toucan.query-type/insert.instances unparsed-args))

(defn insert-returning-instance!
"Like [[insert-returning-instances!]], but for one-row insertions. Returns the inserted object as a map."
{: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)))
151 changes: 79 additions & 72 deletions test/toucan2/insert_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -94,41 +94,47 @@

(deftest ^:synchronized include-pk-test
(testing "If a value for the PK is explicitly specified, insert! and friends should still work correctly"
(doseq [[insert! expected] {#'insert/insert! 1
#'insert/insert-returning-pks! [4]
#'insert/insert-returning-instances! [(instance/instance
::test/venues
{:id 4
:name "Grant & Green"
:category "bar"
:created-at (LocalDateTime/parse "2017-01-01T00:00")
:updated-at (LocalDateTime/parse "2017-01-01T00:00")})]}]
(test/with-discarded-table-changes :venues
(testing insert!
(is (= expected
(insert! ::test/venues {:id 4, :name "Grant & Green", :category "bar"})))
(testing "Venue 4 should exist now"
(is (= (instance/instance ::test/venues {:id 4
:name "Grant & Green"
:category "bar"
:created-at (LocalDateTime/parse "2017-01-01T00:00")
:updated-at (LocalDateTime/parse "2017-01-01T00:00")})
(select/select-one ::test/venues 4)))))))))
(let [new-venue (instance/instance
::test/venues
{:id 4
:name "Grant & Green"
:category "bar"
:created-at (LocalDateTime/parse "2017-01-01T00:00")
:updated-at (LocalDateTime/parse "2017-01-01T00:00")})]
(doseq [[insert! expected] {#'insert/insert! 1
#'insert/insert-returning-pk! 4
#'insert/insert-returning-pks! [4]
#'insert/insert-returning-instance! new-venue
#'insert/insert-returning-instances! [new-venue]}]
(test/with-discarded-table-changes :venues
(testing insert!
(is (= expected
(insert! ::test/venues {:id 4, :name "Grant & Green", :category "bar"})))
(testing "Venue 4 should exist now"
(is (= (instance/instance ::test/venues {:id 4
:name "Grant & Green"
:category "bar"
:created-at (LocalDateTime/parse "2017-01-01T00:00")
:updated-at (LocalDateTime/parse "2017-01-01T00:00")})
(select/select-one ::test/venues 4))))))))))

(deftest ^:synchronized include-pk-non-integer-test
(testing "If a value for a *non-integer* PK is explicitly specified, insert! and friends should still work correctly"
(doseq [[insert! expected] {#'insert/insert! 1
#'insert/insert-returning-pks! ["012345678"]
#'insert/insert-returning-instances! [(instance/instance
::test/phone-number
{:number "012345678", :country-code "US"})]}]
(test/with-discarded-table-changes :phone_number
(testing insert!
(is (= expected
(insert! ::test/phone-number {:number "012345678", :country-code "US"})))
(testing "Phone Number 1 should exist now"
(is (= (instance/instance ::test/phone-number {:number "012345678", :country-code "US"})
(select/select-one ::test/phone-number :toucan/pk "012345678")))))))))
(let [new-phone-number (instance/instance
::test/phone-number
{:number "012345678", :country-code "US"})]
(doseq [[insert! expected] {#'insert/insert! 1
#'insert/insert-returning-pk! "012345678"
#'insert/insert-returning-pks! ["012345678"]
#'insert/insert-returning-instance! new-phone-number
#'insert/insert-returning-instances! [new-phone-number]}]
(test/with-discarded-table-changes :phone_number
(testing insert!
(is (= expected
(insert! ::test/phone-number {:number "012345678", :country-code "US"})))
(testing "Phone Number 1 should exist now"
(is (= (instance/instance ::test/phone-number {:number "012345678", :country-code "US"})
(select/select-one ::test/phone-number :toucan/pk "012345678"))))))))))

(deftest ^:synchronized string-model-test
(testing "insert! should work with string table names as the model"
Expand Down Expand Up @@ -275,19 +281,18 @@

(deftest ^:synchronized empty-rows-no-op-test
(testing "insert! empty rows should no-op"
(doseq [insert! [#'insert/insert!
#'insert/insert-returning-pks!
#'insert/insert-returning-instances!]
model [::test/venues
:venues]
row-or-rows [nil
[]]]
(doseq [[insert! expected] [[#'insert/insert! 0]
[#'insert/insert-returning-pk! nil]
[#'insert/insert-returning-pks! []]
[#'insert/insert-returning-instance! nil]
[#'insert/insert-returning-instances! []]]
model [::test/venues
:venues]
row-or-rows [nil
[]]]
(testing (pr-str (list insert! model row-or-rows))
(execute/with-call-count [call-count]
(is (= (condp = insert!
#'insert/insert! 0
#'insert/insert-returning-pks! []
#'insert/insert-returning-instances! [])
(is (= expected
(insert! model row-or-rows)))
(testing "\ncall count"
(is (= 0
Expand Down Expand Up @@ -329,26 +334,26 @@
{::test/venues :venue})

(deftest ^:synchronized namespaced-test
(doseq [insert! [#'insert/insert!
#'insert/insert-returning-pks!
#'insert/insert-returning-instances!]]
(test/with-discarded-table-changes :venues
(testing insert!
(is (= (condp = insert!
#'insert/insert! 1
#'insert/insert-returning-pks! [4]
#'insert/insert-returning-instances! [(instance/instance
::venues.namespaced
{:venue/name "Grant & Green"
:venue/category "bar"})])
(insert! [::venues.namespaced :venue/name :venue/category]
{:venue/name "Grant & Green", :venue/category "bar"})))
(is (= (instance/instance
::test/venues
{:id 4
:name "Grant & Green"
:category "bar"})
(select/select-one [::test/venues :id :name :category] :id 4)))))))
(let [new-venue (instance/instance
::venues.namespaced
{:venue/name "Grant & Green"
:venue/category "bar"})]
(doseq [[insert! expected] [[#'insert/insert! 1]
[#'insert/insert-returning-pk! 4]
[#'insert/insert-returning-pks! [4]]
[#'insert/insert-returning-instance! new-venue]
[#'insert/insert-returning-instances! [new-venue]]]]
(test/with-discarded-table-changes :venues
(testing insert!
(is (= expected
(insert! [::venues.namespaced :venue/name :venue/category]
{:venue/name "Grant & Green", :venue/category "bar"})))
(is (= (instance/instance
::test/venues
{:id 4
:name "Grant & Green"
:category "bar"})
(select/select-one [::test/venues :id :name :category] :id 4))))))))

(deftest ^:synchronized positional-connectable-test
(testing "Support :conn positional connectable arg"
Expand All @@ -374,12 +379,14 @@
(insert/insert! :conn :fake-db ::test/venues {:name "Grant & Green", :category "bar"})))))))

(deftest ^:synchronized insert-returning-pks-should-not-realize-all-columns-test
(testing "insert-returning-pks! should only fetch the PK column(s) when fetching results"
(test/with-discarded-table-changes :venues
(test.track-realized/with-realized-columns [realized-columns]
(is (= [4]
(insert/insert-returning-pks! ::test.track-realized/venues {:name "Walgreens", :category "store"})))
(is (= (case (test/current-db-type)
(:postgres :h2) #{:venues/id}
:mariadb #{:insert-id})
(realized-columns)))))))
(testing "insert-returning-pk(s)! should only fetch the PK column(s) when fetching results"
(doseq [[insert! expected] [[insert/insert-returning-pk! 4]
[insert/insert-returning-pks! [4]]]]
(test/with-discarded-table-changes :venues
(test.track-realized/with-realized-columns [realized-columns]
(is (= expected
(insert! ::test.track-realized/venues {:name "Walgreens", :category "store"})))
(is (= (case (test/current-db-type)
(:postgres :h2) #{:venues/id}
:mariadb #{:insert-id})
(realized-columns))))))))

0 comments on commit 651dd55

Please sign in to comment.