Skip to content

Commit 4d5d93d

Browse files
authored
Namespaced keyword support (#21)
* Namespaces should work with transforms * Add alias based on namespace so keywords work as well * Namespaced insert support * Support namespaces inside `update!` * Support namespaced keywords in delete * Lint fix
1 parent 11209af commit 4d5d93d

File tree

17 files changed

+369
-64
lines changed

17 files changed

+369
-64
lines changed

src/toucan2/delete.clj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
"Implementation of [[delete!]]."
33
(:require
44
[methodical.core :as m]
5-
[toucan2.model :as model]
65
[toucan2.pipeline :as pipeline]
76
[toucan2.query :as query]))
87

@@ -11,7 +10,7 @@
1110
#_query clojure.lang.IPersistentMap]
1211
[query-type model parsed-args]
1312
(let [parsed-args (update parsed-args :query (fn [query]
14-
(merge {:delete-from [(keyword (model/table-name model))]}
13+
(merge {:delete-from (query/honeysql-table-and-alias model)}
1514
query)))]
1615
(next-method query-type model parsed-args)))
1716

src/toucan2/jdbc/result_set.clj

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
[pretty.core :as pretty]
66
[toucan2.instance :as instance]
77
[toucan2.jdbc.row :as jdbc.row]
8+
[toucan2.magic-map :as magic-map]
9+
[toucan2.model :as model]
810
[toucan2.protocols :as protocols]
911
[toucan2.util :as u])
1012
(:import
@@ -53,7 +55,7 @@
5355
[^ResultSetMetaData rsmeta]
5456
(range 1 (inc (.getColumnCount rsmeta))))
5557

56-
(defn- row-instance [model #_key-xform col-name->thunk]
58+
(defn- row-instance [model col-name->thunk]
5759
(let [row (jdbc.row/row col-name->thunk)]
5860
(u/with-debug-result ["Creating new instance of %s, which has key transform fn %s"
5961
model
@@ -63,21 +65,29 @@
6365
(defn row-thunk
6466
"Return a thunk that when called fetched the current row from the cursor and returns it as a [[row-instance]]."
6567
[^Connection conn model ^ResultSet rset]
66-
(let [rsmeta (.getMetaData rset)
67-
key-xform (instance/key-transform-fn model)
68+
(let [rsmeta (.getMetaData rset)
69+
;; do case-insensitive lookup.
70+
table->namespace (some-> (model/table-name->namespace model) (magic-map/magic-map u/lower-case-en))
71+
key-xform (instance/key-transform-fn model)
6872
;; create a set of thunks to read each column. These thunks will call `read-column-thunk` to determine the
6973
;; appropriate column-reading thunk the first time they are used.
70-
col-name->thunk (into {} (for [^Long i (index-range rsmeta)
71-
:let [col-name (key-xform (keyword (.getColumnName rsmeta i)))
72-
;; TODO -- add test to ensure we only resolve the read-column-thunk
73-
;; once even with multiple rows.
74-
read-thunk (delay (read-column-thunk conn model rset rsmeta i))
75-
result-thunk (fn []
76-
(u/with-debug-result ["Realize column %s %s" i col-name]
77-
(next.jdbc.rs/read-column-by-index (@read-thunk) rsmeta i)))]]
78-
[col-name result-thunk]))]
74+
col-name->thunk (into {}
75+
(map (fn [^Long i]
76+
(let [table-name (.getTableName rsmeta i)
77+
col-name (.getColumnName rsmeta i)
78+
table-ns-name (some-> (get table->namespace table-name) name)
79+
col-key (key-xform (keyword table-ns-name col-name))
80+
;; TODO -- add test to ensure we only resolve the read-column-thunk
81+
;; once even with multiple rows.
82+
read-thunk (delay (read-column-thunk conn model rset rsmeta i))
83+
result-thunk (fn []
84+
(u/with-debug-result ["Realize column %s %s.%s as %s"
85+
i table-name col-name col-key]
86+
(next.jdbc.rs/read-column-by-index (@read-thunk) rsmeta i)))]
87+
[col-key result-thunk])))
88+
(index-range rsmeta))]
7989
(fn row-instance-thunk []
80-
(row-instance model #_key-xform col-name->thunk))))
90+
(row-instance model col-name->thunk))))
8191

8292
(deftype ^:no-doc ReducibleResultSet [^Connection conn model ^ResultSet rset]
8393
clojure.lang.IReduceInit

src/toucan2/model.clj

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
(ns toucan2.model
2+
(:refer-clojure :exclude [namespace])
23
(:require
34
[clojure.spec.alpha :as s]
45
[methodical.core :as m]
@@ -78,7 +79,7 @@
7879
If an implementation returns a single keyword, the default `:around` method will automatically wrap it in a vector. It
7980
also validates that the ultimate result is a sequence of keywords, so it is safe to assume that calls to this will
8081
always return a sequence of keywords."
81-
{:arglists '([model])}
82+
{:arglists '([model])}
8283
u/dispatch-on-first-arg)
8384

8485
;;; if the PK comes back unwrapped, wrap it.
@@ -96,10 +97,6 @@
9697
{:model model, :result pk-or-pks})))
9798
pks))
9899

99-
(m/defmethod primary-keys :default
100-
[_model]
101-
[:id])
102-
103100
;;; TODO -- rename to `primary-key-values-map`
104101
(defn primary-key-values
105102
"Return a map of primary key values for a Toucan 2 `instance`."
@@ -121,3 +118,34 @@
121118
(if (= (count pk-keys) 1)
122119
(first pk-keys)
123120
(apply juxt pk-keys)))))
121+
122+
(m/defmulti model->namespace
123+
{:arglists '([model₁])}
124+
u/dispatch-on-first-arg)
125+
126+
(m/defmethod model->namespace :default
127+
[_model]
128+
nil)
129+
130+
(defn table-name->namespace [model]
131+
(not-empty
132+
(into {}
133+
(comp (filter (fn [[model _a-namespace]]
134+
(not= (m/effective-primary-method table-name model)
135+
(m/default-effective-method table-name))))
136+
(map (fn [[model a-namespace]]
137+
[(table-name model) a-namespace])))
138+
(model->namespace model))))
139+
140+
(defn namespace [model]
141+
(some
142+
(fn [[a-model a-namespace]]
143+
(when (isa? model a-model)
144+
a-namespace))
145+
(model->namespace model)))
146+
147+
(m/defmethod primary-keys :default
148+
[model]
149+
(if-let [model-namespace (namespace model)]
150+
[(keyword (name model-namespace) "id")]
151+
[:id]))

src/toucan2/query.clj

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
2626
Compiled query is executed with connection
2727
```"
28-
2928
(:require
29+
[better-cond.core :as b]
3030
[clojure.spec.alpha :as s]
3131
[honey.sql.helpers :as hsql.helpers]
3232
[methodical.core :as m]
@@ -418,3 +418,23 @@
418418
(m/defmethod build [:default :default clojure.lang.IPersistentMap]
419419
[_query-type model {:keys [kv-args query], :as _args}]
420420
(apply-kv-args model query kv-args))
421+
422+
(defn honeysql-table-and-alias
423+
"Build an Honey SQL `[table]` or `[table alias]` (if the model has a [[toucan2.model/namespace]] form) for `model` for
424+
use in something like a `:select` clause."
425+
[model]
426+
(b/cond
427+
:let [table-id (keyword (model/table-name model))
428+
alias-id (model/namespace model)
429+
alias-id (when alias-id
430+
(keyword alias-id))]
431+
alias-id
432+
[table-id alias-id]
433+
434+
:else
435+
[table-id]))
436+
437+
;; (defn- format-identifier [_ parts]
438+
;; [(str/join \. (map hsql/format-entity parts))])
439+
440+
;; (hsql/register-fn! ::identifier #'format-identifier)

src/toucan2/select.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
(merge {:select (or (not-empty columns)
1818
[:*])}
1919
(when model
20-
{:from [[(keyword (model/table-name model))]]})
20+
{:from [(query/honeysql-table-and-alias model)]})
2121
query)))
2222
(dissoc :columns))]
2323
(next-method query-type model parsed-args)))

src/toucan2/update.clj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
(:require
44
[clojure.spec.alpha :as s]
55
[methodical.core :as m]
6-
[toucan2.model :as model]
76
[toucan2.pipeline :as pipeline]
87
[toucan2.query :as query]
98
[toucan2.util :as u]))
@@ -40,7 +39,7 @@
4039
{:query-type query-type, :model model, :parsed-args parsed-args})))
4140
(let [parsed-args (assoc parsed-args
4241
:kv-args (merge kv-args query)
43-
:query {:update [(keyword (model/table-name model))]
42+
:query {:update (query/honeysql-table-and-alias model)
4443
:set changes})]
4544
(next-method query-type model parsed-args)))
4645

test/toucan2/delete_test.clj

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,16 @@
8181
(test/with-discarded-table-changes :venues
8282
(is (= 0
8383
(delete/delete! ::test/venues nil))))))
84+
85+
(derive ::venues.namespaced ::test/venues)
86+
87+
(m/defmethod model/model->namespace ::venues.namespaced
88+
[_model]
89+
{::test/venues :venue})
90+
91+
(deftest namespaced-test
92+
(test/with-discarded-table-changes :venues
93+
(is (= 1
94+
(delete/delete! ::venues.namespaced :venue/id 3)))
95+
(is (= nil
96+
(select/select-one [::test/venues :id :name :category] :id 3)))))

test/toucan2/insert_test.clj

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,31 @@
263263
:created-at (LocalDateTime/parse "2017-01-01T00:00")
264264
:updated-at (LocalDateTime/parse "2017-01-01T00:00")})])
265265
(insert! ::test/venues ::named-rows)))))))
266+
267+
(derive ::venues.namespaced ::test/venues)
268+
269+
(m/defmethod model/model->namespace ::venues.namespaced
270+
[_model]
271+
{::test/venues :venue})
272+
273+
(deftest namespaced-test
274+
(doseq [insert! [#'insert/insert!
275+
#'insert/insert-returning-pks!
276+
#'insert/insert-returning-instances!]]
277+
(test/with-discarded-table-changes :venues
278+
(testing insert!
279+
(is (= (condp = insert!
280+
#'insert/insert! 1
281+
#'insert/insert-returning-pks! [4]
282+
#'insert/insert-returning-instances! [(instance/instance
283+
::venues.namespaced
284+
{:venue/name "Grant & Green"
285+
:venue/category "bar"})])
286+
(insert! [::venues.namespaced :venue/name :venue/category]
287+
{:venue/name "Grant & Green", :venue/category "bar"})))
288+
(is (= (instance/instance
289+
::test/venues
290+
{:id 4
291+
:name "Grant & Green"
292+
:category "bar"})
293+
(select/select-one [::test/venues :id :name :category] :id 4)))))))

test/toucan2/model_test.clj

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,38 @@
6363

6464
;;; [[model/default-connectable]] gets tested basically everywhere, because we define it for the models in
6565
;;; [[toucan2.test]] and use it in almost every test namespace
66+
67+
(derive ::venues.namespaced ::test/venues)
68+
69+
(m/defmethod model/model->namespace ::venues.namespaced
70+
[_model]
71+
{::venues.namespaced :venue
72+
::test/categories :category})
73+
74+
(deftest model->namespace-test
75+
(are [model expected] (= expected
76+
(model/model->namespace model))
77+
::venues.namespaced {::venues.namespaced :venue, ::test/categories :category}
78+
:venues nil
79+
nil nil))
80+
81+
(deftest table-name->namespace-test
82+
(are [model expected] (= expected
83+
(model/table-name->namespace model))
84+
::venues.namespaced {"venues" :venue, "category" :category}
85+
:venues nil
86+
nil nil))
87+
88+
(derive ::venues.namespaced.child ::venues.namespaced)
89+
90+
(deftest namespace-test
91+
(are [model expected] (= expected
92+
(model/namespace model))
93+
::venues.namespaced :venue
94+
::venues.namespaced.child :venue
95+
:venues nil
96+
nil nil))
97+
98+
(deftest namespaced-default-primary-keys-test
99+
(is (= [:venue/id]
100+
(model/primary-keys ::venues.namespaced))))

test/toucan2/query_test.clj

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,34 @@
100100
::venues.compound-pk [:in [[4 "BevMo"]]] [:and [:in :id [4]] [:in :name ["BevMo"]]]
101101
::venues.compound-pk [:in [[4 "BevMo"] [5 "BevLess"]]] [:and [:in :id [4 5]] [:in :name ["BevMo" "BevLess"]]]
102102
::venues.compound-pk [:between [4 "BevMo"] [5 "BevLess"]] [:and [:between :id 4 5] [:between :name "BevMo" "BevLess"]])))
103+
104+
(derive ::venues.namespaced ::test/venues)
105+
106+
(m/defmethod model/model->namespace ::venues.namespaced
107+
[_model]
108+
{::test/venues :venue})
109+
110+
(deftest namespaced-toucan-pk-test
111+
(is (= {:select [:*]
112+
:from [[:venues :venue]]
113+
:where [:= :venue/id 1]}
114+
(query/build :toucan.query-type/select.instances
115+
::venues.namespaced
116+
{:kv-args {:toucan/pk 1}, :query {}}))))
117+
118+
(deftest honeysql-table-and-alias-test
119+
(are [model expected] (= expected
120+
(query/honeysql-table-and-alias model))
121+
::test/venues [:venues]
122+
::venues.namespaced [:venues :venue]
123+
"venues" [:venues]))
124+
125+
;; (deftest identitfier-test
126+
;; (testing "Custom Honey SQL identifier clause"
127+
;; (are [identifier quoted? expected] (= expected
128+
;; (hsql/format {:select [:*], :from [[identifier]]}
129+
;; {:quoted quoted?}))
130+
;; [::query/identifier :wow] false ["SELECT * FROM wow"]
131+
;; [::query/identifier :wow] true ["SELECT * FROM \"wow\""]
132+
;; [::query/identifier :table :field] false ["SELECT * FROM table.field"]
133+
;; [::query/identifier :table :field] true ["SELECT * FROM \"table\".\"field\""])))

0 commit comments

Comments
 (0)