From 651dd55c18b15d3e7a5c8957e93f4f56eb3b7e50 Mon Sep 17 00:00:00 2001 From: Tim Macdonald Date: Mon, 27 Mar 2023 18:45:32 +0100 Subject: [PATCH] Add insert-returning-pk! and insert-returning-instance! (#139) (Singular versions of -pks! and -instances!) --- README.md | 1 - src/toucan2/insert.clj | 30 +++++-- test/toucan2/insert_test.clj | 151 ++++++++++++++++++----------------- 3 files changed, 104 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index c961cca..62759fe 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/src/toucan2/insert.clj b/src/toucan2/insert.clj index 45ad895..9ef0ba1 100644 --- a/src/toucan2/insert.clj +++ b/src/toucan2/insert.clj @@ -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] @@ -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))) diff --git a/test/toucan2/insert_test.clj b/test/toucan2/insert_test.clj index 154d84f..be5f9f9 100644 --- a/test/toucan2/insert_test.clj +++ b/test/toucan2/insert_test.clj @@ -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" @@ -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 @@ -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" @@ -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))))))))