such.relational

This namespace provides two things: better documentation for relational functions in clojure.set, and an experimental set of functions for “pre-joining” relational tables for a more tree-structured or path-based lookup. See the wiki for more about the latter.

The API for the experimental functions may change without triggering a semver major number change.

combined-index-on

(combined-index-on starting-index foreign-key next-index ...)

Create an index that maps directly from values in the starting index to values in the last of the list of indexes, following keys to move from index to index. Example:

 (let [index:countries-by-person-id (subject/combined-index-on index:rulership-by-person-id
                                                               :country_code
                                                               index:country-by-country-code)]
   (subject/index-select 1 :using index:countries-by-person-id :keys [:gdp])
   => [{:gdp 1690}])

(See the wiki for details.)

extend-map

(extend-map kvs options)(extend-map kvs k v & rest)

Add more key/value pairs to kvs. They are found by looking up values in a one-to-one-index-on or one-to-many-index-on index.

See the wiki for examples.

The options control what maps are returned and how they’re merged into the original kvs. They may be given as N keys and values following the kvs argument (Smalltalk style) or as a single map. They are:

:using <index>
  (required) The index to use.
:via <key>
  (required) A single foreign key or a sequence of them that is used to
  look up a map in the <index>.
:into <key>
  (optional, relevant only to a one-to-many map). Since a one-to-many map
  can't be merged into the `kvs`, it has to be added "under" (as the
  value of) a particular `key`.
:keys [key1 key2 key3 ...]
  (optional) Keys you're interested in (default is all of them)
:prefix <prefix>
  (optional) Prepend the given prefix to all the keys in the selected map.
  The prefix may be either a string or keyword. The resulting key will be
  of the same type (string or keyword) as the original.

index

added in 1.0

(index xrel ks)

xrel is a collection of maps; consider it the result of an SQL SELECT. ks is a collection of values assumed to be keys of the maps (think table columns). The result maps from particular key-value pairs to a set of all the maps in xrel that contain them.

Consider this xrel:

(def xrel [ {:first "Brian" :order 1 :count 4}
            {:first "Dawn" :order 1 :count 6}
            {:first "Paul" :order 1 :count 5}
            {:first "Sophie" :order 2 :count 9} ])

Then (index xrel [:order]) is:

{{:order 1}
  #{{:first "Paul", :order 1, :count 5}
    {:first "Dawn", :order 1, :count 6}
    {:first "Brian", :order 1, :count 4}},
  {:order 2}
    #{{:first "Sophie", :order 2, :count 9}}}

… and (index xrel [:order :count]) is:

{{:order 1, :count 4}   #{ {:first "Brian", :order 1, :count 4} },
 {:order 1, :count 6}   #{ {:first "Dawn", :order 1, :count 6} },
 {:order 1, :count 5}   #{ {:first "Paul", :order 1, :count 5} },
 {:order 2, :count 9}   #{ {:first "Sophie", :order 2, :count 9} }}

If one of the xrel maps doesn’t have an key, it is assigned to an index without that key. Consider this xrel:

(def xrel [ {:a 1, :b 1} {:a 1} {:b 1} {:c 1}])

Then (index xrel [:a b]) is:

{  {:a 1, :b 1}    #{ {:a 1 :b 1} }
   {:a 1      }    #{ {:a 1} }
   {      :b 1}    #{ {:b 1} }
   {          }    #{ {:c 1} }})

index-select

(index-select key options)(index-select key k v & rest)

Produce a map by looking a key up in an index.

See the wiki for examples.

key is a unique or compound key that’s been indexed with one-to-one-index-on or one-to-many-index-on. The options may be given as N keys and values following key (Smalltalk style) or as a single map. They are:

:using <index>
  (required) The index to use.
:keys <[keys...]>
  (optional) Keys you're interested in (default is all of them)
:prefix <prefix>
  (optional) Prepend the given prefix to all the keys in the selected map.
  The prefix may be either a string or keyword. The resulting key will be
  of the same type (string or keyword) as the original.

The return value depends on the index. If it is one-to-one, a map is returned. If it is one-to-many, a vector of maps is returned.

join

added in 1.0

(join xrel yrel)(join xrel yrel km)

xrel and yrel are collections of maps (think SQL SELECT). In the first form, produces the natural join. That is, it joins on the shared keys. In the following, :b is shared:

  (def has-a-and-b [{:a 1, :b 2} {:a 2, :b 1} {:a 2, :b 2}])
  (def has-b-and-c [{:b 1, :c 2} {:b 2, :c 1} {:b 2, :c 2}])
  (join has-a-and-b has-b-and-c) => #{{:a 1, :b 2, :c 1}
                                      {:a 1, :b 2, :c 2}

                                      {:a 2, :b 1, :c 2}

                                      {:a 2, :b 2, :c 1}
                                      {:a 2, :b 2, :c 2}}}

Alternately, you can use a map to describe which left-hand-side keys should be considered the same as which right-hand-side keys. In the above case, the sharing could be made explicit with (join has-a-and-b has-b-and-c {:b :b}).

A more likely example is one where the two relations have slightly different “b” keys, like this:

  (def has-a-and-b [{:a 1, :b 2} {:a 2, :b 1} {:a 2, :b 2}])
  (def has-b-and-c [{:blike 1, :c 2} {:blike 2, :c 1} {:blike 2, :c 2}])

In such a case, the join would look like this:

  (join has-a-and-b has-b-and-c {:b :blike}) =>
                                    #{{:a 1, :b 2, :blike 2, :c 1}
                                      {:a 1, :b 2, :blike 2, :c 2}

                                      {:a 2, :b 1, :blike 1, :c 2}

                                      {:a 2, :b 2, :blike 2, :c 1}
                                      {:a 2, :b 2, :blike 2, :c 2}}

Notice that the :b and :blike keys are both included.

The join when there are no keys shared is the cross-product of the relations.

  (clojure.set/join [{:a 1} {:a 2}] [{:b 1} {:b 2}])
  => #{{:a 1, :b 2} {:a 2, :b 1} {:a 1, :b 1} {:a 2, :b 2}}

The behavior when maps are missing keys is probably not something you should depend on.

one-to-many-index-on

(one-to-many-index-on table keyseq)

table should be a sequence of maps. keyseq is either a single value (corresponding to a traditional :id or :pk entry) or a sequence of values (corresponding to a compound key).

The resulting index provides fast retrieval of vectors of matching maps.

(def index:traditional (one-to-many-index-on table :id))
(index-select 5 :using index:traditional :keys [:key-i-want]) ; a vector of maps

(def index:compound (one-to-many-index-on table ["intkey" "strkey")))
(index-select [4 "dawn"] :using index:compound) ; a vector of maps

Keys may be either Clojure keywords or strings.

one-to-one-index-on

(one-to-one-index-on table keyseq)

table should be a sequence of maps. keyseq is either a single value (corresponding to a traditional :id or :pk entry) or a sequence of values (corresponding to a compound key).

The resulting index provides fast access to individual maps.

(def index:traditional (one-to-one-index-on table :id))
(index-select 5 :using index:traditional :keys [:key1 :key2])

(def index:compound (one-to-one-index-on table ["intkey" "strkey")))
(index-select [4 "dawn"] :using index:compound)

Note that keys need not be Clojure keywords.

project

added in 1.0

(project xrel ks)

xrel is a collection of maps (think SQL SELECT *). This function produces a set of maps, each of which contains only the keys in ks.

(project [{:a 1, :b 1} {:a 2, :b 2}] [:b]) => #{{:b 1} {:b 2}}

project differs from (map #(select-keys % ks) ...) in two ways:

  1. It returns a set, rather than a lazy sequence.
  2. Any metadata on the original xrel is preserved. (It shares this behavior with rename but with no other relational functions.)

rename

added in 1.0

(rename xrel kmap)

xrel is a collection of maps. Transform each map according to the keys and values in kmap. Each map key that matches a kmap key is replaced with that kmap key’s value.

 (rename [{:a 1, :b 2}] {:b :replacement}) => #{{:a 1, :replacement 2}}

rename differs from (map #(set/rename-keys % kmap) ...) in two ways:

  1. It returns a set, rather than a lazy sequence.
  2. Any metadata on the original xrel is preserved. (It shares this behavior with project but with no other relational functions.)