A basic in-memory database for storing linked maps in Clojure and ClojureScript
Leiningen dependency information:
[com.stuartsierra/mapgraph "0.2.1"]
Maven dependency information:
<dependency>
<groupId>com.stuartsierra</groupId>
<artifactId>mapgraph</artifactId>
<version>0.2.1</version>
</dependency>
Gradle dependency information:
compile "com.stuartsierra:mapgraph:0.2.1"
MapGraph is written in .cljc
and depends on Clojure or ClojureScript
version 1.7.0 or higher.
To run the tests you will need clojure.spec, available in Clojure 1.9.0-alpha5 or higher.
Please post questions on the Clojure Mailing List
(ns examples
(:require [com.stuartsierra.mapgraph :as mg]))
Create a new MapGraph database with new-db
. You will probably want
to store it in a mutable reference such as an Atom.
(def db (atom (mg/new-db)))
Add the unique identity attributes that define your schema.
(swap! db mg/add-id-attr :user/id :color/hex)
Add entities to your database with add
. You can add multiple
entities at once, and they may be nested.
(swap! db mg/add
{:user/id 1
:user/name "Pat"
:user/favorite-color {:color/hex "9C27B0"
:color/name "Purple"}}
; ^-- nested entity
{:user/id 2
:user/name "Reese"
:user/favorite-color {:color/hex "D50000"
:color/name "Red"}})
Entities in the database are stored normalized: all nested entities
are replaced with lookup refs. You can see this if you get
an entity
by its lookup ref.
(get @db [:user/id 2])
;;=> {:user/id 2,
;; :user/name "Reese",
;; :user/favorite-color [:color/hex "D50000"]}
; ^-- lookup ref
To get back nested entities, use pull
, which takes a pattern
describing which attributes and entities you want to get back.
It is similar to Datomic Pull.
(mg/pull @db
[:user/name {:user/favorite-color [:color/name]}]
[:user/id 2])
;;=> {:user/name "Reese",
;; :user/favorite-color {:color/name "Red"}}
Entities with the same unique identity are merged.
(swap! db
mg/add
{:user/id 1 ; "Pat"
:user/profession "Programmer"})
(mg/pull @db
[:user/id :user/name :user/profession]
[:user/id 1])
;; {:user/id 1,
;; :user/name "Pat",
;; :user/profession "Programmer"}
Entities can refer to other entities, forming a graph. The graph may have cycles.
(swap! db
mg/add
{:user/id 1
:user/friends #{{:user/id 2}}}
{:user/id 2
:user/friends #{{:user/id 1}}})
(mg/pull @db
[:user/name
{:user/friends [:user/name
{:user/friends [:user/name]}]}]
[:user/id 1])
;;=> {:user/name "Pat",
;; :user/friends #{{:user/name "Reese",
;; :user/friends #{{:user/name "Pat"}}}}}
To remove an entity, dissoc
its lookup ref. Dangling lookup refs
will be ignored on subsequent pull
.
(swap! db dissoc [:user/id 2]) ; Reese
(mg/pull @db '[*] [:user/id 2])
;;=> nil
(mg/pull @db
[:user/name
{:user/friends [:user/name]}]
[:user/id 1])
;;=> {:user/name "Pat",
;; :user/friends #{}}
; ^-- Reese is gone
Attribute values can be any Clojure collection type.
(swap! db mg/add
{:user/id 1
:user/favorite-sports '(hockey tennis golf)})
(mg/pull @db
[:user/name :user/favorite-sports]
[:user/id 1])
;;=> {:user/name "Pat", :user/favorite-sports (hockey tennis golf)}
Merging a new collection value completely replaces the previous value.
(swap! db mg/add
{:user/id 1
:user/favorite-sports '(tennis polo)})
(mg/pull @db
[:user/name :user/favorite-sports]
[:user/id 1])
;;=> {:user/name "Pat", :user/favorite-sports (tennis polo)}
A collection of nested entities may be a list, vector, set, or map in which the vals are entities.
(def sample-host
{;; identifier
:host/ip "10.10.1.1"
;; non-entity value
:host/name "web1"
;; collections (list, vector, set, map) of non-entity values
:host/aliases ["host1" "www"]
:host/rules {"input" {"block" "*", "allow" 80}
"output" {"allow" 80}}
;; single entity value
:host/gateway {:host/ip "10.10.10.1"}
;; collection of entities (list, vector, set)
:host/peers #{{:host/ip "10.10.1.2", :host/name "web2"}
{:host/ip "10.10.1.3"}}
;; map of non-entity keys to entity vals
:host/connections {"database" {:host/ip "10.10.1.4", :host/name "db"}
["cache" "level2"] {:host/ip "10.10.1.5", :host/name "cache"}}})
pull
works the same way on single entities and collections of entities.
(def hosts
(atom (-> (mg/new-db)
(mg/add-id-attr :host/ip)
(mg/add sample-host))))
(mg/pull @hosts
[:host/ip
:host/rules
{:host/gateway [:host/ip]
:host/peers [:host/ip]
:host/connections [:host/name]}]
[:host/ip "10.10.1.1"])
;;=> {:host/ip "10.10.1.1",
;; :host/rules {"input" {"block" "*", "allow" 80},
;; "output" {"allow" 80}},
;; :host/gateway {:host/ip "10.10.10.1"},
;; :host/peers #{{:host/ip "10.10.1.3"}
;; {:host/ip "10.10.1.2"}},
;; :host/connections {"database" {:host/name "db"},
;; ["cache" "level2"] {:host/name "cache"}}}
Collections may not mix entities and non-entities.
(try (swap! db mg/add {:user/id 3 :user/friends [{:user/id 1} "Bob"]})
(catch Throwable t t))
;; #error {:data {:reason ::mg/mixed-collection,
;; ::mg/attribute :user/friends,
;; ::mg/value [{:user/id 1} "Bob"]}}
MapGraph is designed to be used as a temporary store for data kept in Datomic or Datascript.
MapGraph is different from Datomic/Datascript in the following ways:
-
Schema only specifies unique identity attributes
-
Non-identity attributes do not need to be declared before they are used
-
An entity must not have more than one unique identity attribute
-
Values may include collections of any type
-
Empty collections will be stored rather than ignored
-
Updating the value of an attribute with a collection always replaces the entire previous value
-
No reverse attribute references (like
:user/_friends
) -
No component attributes
-
pull
does not support recursion, default values, limits, or reverse lookup -
No indexes
-
No queries, only lookup by unique identity attribute
-
No database entity IDs, only lookup refs
Please file issues on GitHub with minimal sample code that demonstrates the problem.
Please do not send pull requests without prior discussion. Please contact me via email first. Thank you.
Jeb Beich for discussion, early testing, and contributions.
Cognitect for providing me with time to work on open-source projects. This library is my personal work and is not officially supported by Cognitect, Inc.
The MIT License (MIT)
Copyright (c) 2016 Stuart Sierra
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.