├── .gitignore
├── README.md
├── config
└── config.exs
├── doc
├── 404.html
├── Datom.html
├── DatomicGenServer.Db.html
├── DatomicGenServer.EntityMap.DataTuple.html
├── DatomicGenServer.EntityMap.html
├── DatomicGenServer.ProcessState.html
├── DatomicGenServer.html
├── DatomicTransaction.html
├── api-reference.html
├── dist
│ ├── app-1e374caa3d.css
│ ├── app-6d2e071366.js
│ └── sidebar_items.js
├── fonts
│ ├── icomoon.eot
│ ├── icomoon.svg
│ ├── icomoon.ttf
│ └── icomoon.woff
└── index.html
├── lib
├── datomic_gen_server.ex
└── datomic_gen_server
│ ├── datom.ex
│ ├── datomic_transaction.ex
│ ├── db.ex
│ └── entity_map.ex
├── mix.exs
├── mix.lock
├── priv
└── datomic_gen_server_peer
│ ├── .gitignore
│ ├── project.clj
│ ├── src
│ └── datomic_gen_server
│ │ └── peer.clj
│ └── test
│ ├── datomic_gen_server
│ └── peer_test.clj
│ └── resources
│ ├── migrations
│ └── 2016-02-18T16:30_create_test_schema.edn
│ └── seed
│ └── V001_categories_and_subcategories.edn
└── test
├── datomic_gen_server
├── db_load_data_test.exs
├── db_test.exs
└── entity_map_test.exs
├── datomic_gen_server_test.exs
├── load_data_test.exs
├── migration_test.exs
├── mocking_test.exs
├── registration_test.exs
└── test_helper.exs
/.gitignore:
--------------------------------------------------------------------------------
1 | /_build
2 | /cover
3 | /deps
4 | erl_crash.dump
5 | *.ez
6 | .DS_Store
7 | /target
8 | /classes
9 | /checkouts
10 | pom.xml
11 | pom.xml.asc
12 | *.jar
13 | *.class
14 | /.lein-*
15 | /.nrepl-port
16 | .hgignore
17 | .idea
18 | *.iml
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DatomicGenServer
2 |
3 | An Elixir GenServer that communicates with a Clojure Datomic peer running in the
4 | JVM, using clojure-erlastic.
5 |
6 | ## Example
7 |
8 | ```elixir
9 | data_to_add = [%{
10 | Db.id => Db.dbid(Db.schema_partition),
11 | Db.ident => :"person/name",
12 | Db.value_type => Db.type_string,
13 | Db.cardinality => Db.cardinality_one,
14 | Db.doc => "A person's name",
15 | Db.install_attribute => Db.schema_partition
16 | }]
17 |
18 | Db.transact(DatomicGenServer, data_to_add)
19 |
20 | # => {:ok, %DatomicGenServer.Db.DatomicTransaction{
21 | basis_t_after: 1001,
22 | basis_t_before: 1000,
23 | retracted_datoms: [],
24 | added_datoms: [%DatomicGenServer.Db.Datom{a: 50, added: true, e: 13194139534313, tx: 13194139534313,
25 | v: %Calendar.DateTime{abbr: "UTC", day: 14, hour: 5, min: 56, month: 2, sec: 53, std_off: 0,
26 | timezone: "Etc/UTC", usec: 400000, utc_off: 0, year: 2016}},
27 | %DatomicGenServer.Db.Datom{a: 41, added: true, e: 66, tx: 13194139534313, v: 35},
28 | %DatomicGenServer.Db.Datom{a: 62, added: true, e: 66, tx: 13194139534313, v: "A person's name"},
29 | %DatomicGenServer.Db.Datom{a: 10, added: true, e: 66, tx: 13194139534313, v: :"person/name"},
30 | %DatomicGenServer.Db.Datom{a: 40, added: true, e: 66, tx: 13194139534313, v: 23},
31 | %DatomicGenServer.Db.Datom{a: 13, added: true, e: 0, tx: 13194139534313, v: 64}],
32 | tempids: %{-9223367638809264705 => 66}}}
33 |
34 | query = [:find, Db.q?(:c), :where, [Db.q?(:c), :Db.doc, "A person's name"]]
35 |
36 | Db.q(DatomicGenServer, query)
37 |
38 | # => {:ok, #MapSet<['B']>} # ASCII representation of ID 66
39 | ```
40 |
41 | ## Installation
42 |
43 | If available in Hex, the package can be installed as follows:
44 |
45 | 1. You will need to install the Clojure leiningen build tool in order to build
46 | the Clojure jar with which the application communicates. You will also need
47 | to have the datomic-pro peer jar installed in a local repository, with a
48 | version matching the one specified in `priv/datomic_gen_server_peer/project.clj`.
49 |
50 | You can also specify your credentials for the my.datomic.com repo by
51 | creating a `~/.lein/credentials.clj.gpg` file. See the comments in
52 | `priv/datomic_gen_server_peer/project.clj` for more details.
53 |
54 | The `mix compile` task includes running `lein uberjar` in the
55 | `priv/datomic_gen_server_peer` directory, and `mix clean` will remove the
56 | `target` subdirectory of that directory.
57 |
58 | 2. Add datomic_gen_server to your list of dependencies in `mix.exs`:
59 |
60 | ```elixir
61 | def deps do
62 | [{:datomic_gen_server, "~> 2.2.5"}]
63 | end
64 | ```
65 |
66 | 3. You may want to create a config.exs file in your application that adds to
67 | your application environment default values to control the default amount of
68 | time the GenServer waits for the JVM to start before crashing, and the default
69 | amount of time it waits for a reply from the JVM peer before crashing. See
70 | the `config/config.exs` file for an example.
71 |
72 | 4. Ensure datomic_gen_server (as well as Logger and Calendar) is started before
73 | your application:
74 |
75 | ```elixir
76 | def application do
77 | [applications: [:logger, :calendar, :datomic_gen_server]]
78 | end
79 | ```
80 |
81 | ## Usage
82 |
83 | See the Hex docs at [http://hexdocs.pm/datomic_gen_server/](http://hexdocs.pm/datomic_gen_server/).
84 |
85 | Start the server by calling `DatomicGenServer.start` or `DatomicGenServer.start_link`.
86 | These functions accept the URL of the Datomic transactor to which to connect, a
87 | boolean parameter indicating whether or not to create the database if it does not
88 | yet exist, and a keyword list of options. The options may include the normal
89 | options accepted by `GenServer.start` and `GenServer.start_link`, as well as
90 | options to control the default wait times after which the server will crash.
91 |
92 | On start, the server will send itself an initial message to start the JVM, then
93 | register itself under any alias provided in the options. Any subsequent message
94 | sent to the server will arrive after the initialization message, and will need
95 | to wait until initialization is complete. Thus, it is important that the timeouts
96 | on messages sent to the server exceed the startup timeout value, at least for the
97 | messages sent during the startup phase.
98 |
99 | Two interfaces for interacting with Datomic are exposed. With one, you communicate
100 | back and forth with Datomic using edn strings; this API is exposed by
101 | `DatomicGenServer`. Currently there are three interface functions corresponding
102 | to the Datomic API's `q`, `transact`, and `entity` functions. There are also
103 | interface functions `migrate` and `seed` allowing you to migrate a database and
104 | to seed it with test data.
105 |
106 | A second API allows you to interact with Datomic using Elixir data structures as set
107 | out in the [Exdn project](http://github.com/psfblair/exdn) for translating between
108 | Elixir and edn; this API is exposed by `DatomicGenServer.Db`. The results of query
109 | functions such as `q` and `pull` are translated back to Elixir data structures
110 | using Exdn's "irreversible" data translators, which can also accept converter
111 | functions that will transform the data into your own structures or custom formats
112 | (see the tests for examples). The results of `transact` are returned in a
113 | `DatomicTransaction` struct; the datoms are returned in `Datom` structs. The
114 | results of `seed` are also encoded in a `DatomicTransaction` struct.
115 |
116 | The `entity` functions in both `DatomicGenServer` and `DatomicGenServer.Db` allow
117 | passing in a list of keys representing the attributes you wish to fetch, or `:all`
118 | if you want all of them.
119 |
120 | The `DatomicGenServer.Db` module also contains shortcuts for many common Datomic
121 | keys and values, which would otherwise require a lot of additional punctuation
122 | in Elixir.
123 |
124 | ## Entity Maps
125 |
126 | The `DatomicGenServer.EntityMap` module provides a data structure for holding
127 | entity data as a map of maps or structs. The keys of the map are Datomic
128 | entity IDs by default, but you can choose any attribute as the index. Entity
129 | data is by default stored as a map of Datomic attributes to values; however,
130 | you can choose to have the data placed into a struct of your choosing. You can
131 | also supply a translation map in case the field names of the struct do not
132 | match the names of the Datomic attributes they correspond with. (Make sure to
133 | read the Hex docs for important caveats about using this data structure.)
134 |
135 | ## Mocking Connections
136 |
137 | The `DatomicGenServer` module offers functions that allow you to create and use
138 | mock connections based on database snapshots. Mocking connections requires that
139 | the `:allow_datomic_mocking?` configuration parameter be set in the
140 | `:datomic_gen_server` application environment.
141 |
142 | The `mock` function saves a snapshot of the current database value using a key
143 | that you provide, and creates a new mock connection using that database as a
144 | starting point. Subsequent operations on the database will use that mock
145 | connection until you call either the `reset` or `unmock` functions. The `reset`
146 | function accepts a key and retrieves the database snapshot saved under that
147 | key; a new mock connection is created using that snapshot as the starting point.
148 | The `unmock` function reverts back to the real, live connection and database.
149 |
150 | ## Limitations
151 |
152 | This code has not been checked for possible injection vulnerabilities. Use at
153 | your own risk.
154 |
155 | Currently all interaction with Datomic is synchronous and there is no support for
156 | Datomic functions such as `transact-async`; furthermore, while the GenServer's
157 | `handle_cast` callback will send requests to the Clojure peer, the responses from
158 | the peer will be discarded. Implementing support for `transact-async` and `handle_cast`
159 | may be somewhat complicated owing to the way in which the GenServer waits for replies
160 | from the Clojure peer (see the comments on `DatomicGenServer.wait_for_reply`).
161 |
162 | Queries and transactions are passed to the Clojure peer as edn strings, and results
163 | come back as edn strings. Certain Datomic APIs return references to Java objects,
164 | which can't be manipulated from Elixir (e.g., a call to `entity` returns a
165 | dynamic map); where possible the related data is translated to edn to be
166 | returned to the GenServer.
167 |
168 | There may be ways to take better advantage of clojure-erlastic to serialize data
169 | structures directly; however, clojure-erlastic's serialization format is different
170 | from that of Exdn, which is more suited to Datomic operations. It isn't clear
171 | whether an additional translation layer would be worthwhile.
172 |
173 | ## License
174 |
175 | ```
176 | The MIT License (MIT)
177 |
178 | Copyright (c) 2016 Paul Blair
179 |
180 | Permission is hereby granted, free of charge, to any person obtaining a copy
181 | of this software and associated documentation files (the "Software"), to deal
182 | in the Software without restriction, including without limitation the rights
183 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
184 | copies of the Software, and to permit persons to whom the Software is
185 | furnished to do so, subject to the following conditions:
186 |
187 | The above copyright notice and this permission notice shall be included in all
188 | copies or substantial portions of the Software.
189 |
190 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
191 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
192 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
193 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
194 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
195 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
196 | SOFTWARE.
197 | ```
198 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | use Mix.Config
4 |
5 | # Default time to wait for the JVM to startup; crash if it is exceeded.
6 | # This can be overridden by passing a different value to start_link.
7 | config :datomic_gen_server, startup_wait_millis: 15_000
8 |
9 | # Default time to wait for a message to come back from the JVM before crashing.
10 | # This value can be overridden on a per-message basis by passing a timeout
11 | # to the client functions in the module.
12 | config :datomic_gen_server, message_wait_until_crash: 5_000
13 |
14 | # Default time for the client functions in the API to wait for a message to
15 | # come back from the JVM before failing. Note that if the client call fails
16 | # before the GenServer crashes (based on the message_wait_until_crash setting)
17 | # the GenServer will not crash due to the non-receipt of that message.
18 | # This value can be overridden on a per-message basis by passing a timeout_on_call
19 | # parameter to the client functions in the module. However, if this is set
20 | # to be longer than the message_wait_until_crash value, it will have no effect,
21 | # since the GenServer will crash first.
22 | config :datomic_gen_server, timeout_on_call: 20_000
23 |
24 | # Generally you'll want to allow Datomic mocking in test environments only.
25 | # config :datomic_gen_server, :allow_datomic_mocking? true
26 |
27 | # Set this if you want messages to be logged. This will log messages not only
28 | # The peer will also write messages
29 | # and exceptions to STDERR.
30 | # config :datomic_gen_server, :debug_messages? true
31 |
--------------------------------------------------------------------------------
/doc/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Sorry, but the page you were trying to get to, does not exist. You
64 | may want to try searching this site using the sidebar or using our
65 | API Reference page to find what
66 | you were looking for.
DatomicGenServer.Db is a module intended to facilitate the use of Elixir
96 | data structures instead of edn strings for communicating with Datomic. This
97 | module maps the DatomicGenServer interface functions in wrappers that accept
98 | and return Elixir data structures, and also provides slightly more syntactically
99 | pleasant equivalents for Datomic keys and structures that would otherwise
100 | need to be represented using a lot of punctuation that isn’t required in Clojure
DatomicGenServer.EntityMap is a data structure designed to store the results
108 | of Datomic queries and transactions in a map of maps or structs. The keys
109 | of the EntityMap are by default Datomic entity IDs, but you may also index
110 | the map using other attributes as keys
A data structure for representing a Datomic transaction
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/doc/dist/app-1e374caa3d.css:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Lato:400,300,700,900|Merriweather:300italic,300,700,700italic|Inconsolata:400,700);.hljs,article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}img,legend{border:0}.results ul,.sidebar ul{list-style:none}.sidebar a,.sidebar-toggle{transition:color .3s ease-in-out}.night-mode-toggle:focus,.sidebar .sidebar-search .sidebar-searchInput:focus,.sidebar .sidebar-search .sidebar-searchInput:hover,.sidebar-toggle:active,.sidebar-toggle:focus,.sidebar-toggle:hover,a:active,a:hover{outline:0}.hljs-comment{color:#8e908c}.css .hljs-class,.css .hljs-id,.css .hljs-pseudo,.hljs-attribute,.hljs-regexp,.hljs-tag,.hljs-variable,.html .hljs-doctype,.ruby .hljs-constant,.xml .hljs-doctype,.xml .hljs-pi,.xml .hljs-tag .hljs-title{color:#c82829}.hljs-built_in,.hljs-constant,.hljs-literal,.hljs-number,.hljs-params,.hljs-pragma,.hljs-preprocessor{color:#f5871f}.css .hljs-rule .hljs-attribute,.ruby .hljs-class .hljs-title{color:#eab700}.hljs-header,.hljs-inheritance,.hljs-name,.hljs-string,.hljs-value,.ruby .hljs-symbol,.xml .hljs-cdata{color:#718c00}.css .hljs-hexcolor,.hljs-title{color:#3e999f}.coffeescript .hljs-title,.hljs-function,.javascript .hljs-title,.perl .hljs-sub,.python .hljs-decorator,.python .hljs-title,.ruby .hljs-function .hljs-title,.ruby .hljs-title .hljs-keyword{color:#4271ae}.hljs-keyword,.javascript .hljs-function{color:#8959a8}.hljs{overflow-x:auto;background:#fff;color:#4d4d4c;padding:.5em;-webkit-text-size-adjust:none}legend,td,th{padding:0}.coffeescript .javascript,.javascript .xml,.tex .hljs-formula,.xml .css,.xml .hljs-cdata,.xml .javascript,.xml .vbscript{opacity:.5}/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}abbr[title]{border-bottom:1px dotted}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}.main,body,html{overflow:hidden}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}.content,.main,.sidebar,body,html{height:100%}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}table{border-collapse:collapse;border-spacing:0}@font-face{font-family:icomoon;src:url(../fonts/icomoon.eot?h5z89e);src:url(../fonts/icomoon.eot?#iefixh5z89e) format('embedded-opentype'),url(../fonts/icomoon.ttf?h5z89e) format('truetype'),url(../fonts/icomoon.woff?h5z89e) format('woff'),url(../fonts/icomoon.svg?h5z89e#icomoon) format('svg');font-weight:400;font-style:normal}.icon-elem,[class*=" icon-"],[class^=icon-]{font-family:icomoon;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.sidebar,body{font-family:Lato,sans-serif}.icon-link:before{content:"\e005"}.icon-search:before{content:"\e036"}.icon-cross:before{content:"\e117"}.icon-menu:before{content:"\e120"}.icon-angle-right:before{content:"\f105"}.icon-code:before{content:"\f121"}body,html{box-sizing:border-box;width:100%}body{margin:0;font-size:16px;line-height:1.6875em}*,:after,:before{box-sizing:inherit}.main{display:-webkit-flex;display:-ms-flexbox;display:-ms-flex;display:flex}.sidebar,body.sidebar-closed .sidebar{display:none}.sidebar{-webkit-flex:0 1 300px;-moz-flex:0 1 300px;-ms-flex:0 1 300px;flex:0 1 300px;-ms-flex-positive:0;-ms-flex-negative:1;-ms-flex-preferred-size:300px;-webkit-box-orient:vertical;-moz-box-orient:vertical;-webkit-box-direction:normal;-moz-box-direction:normal;min-height:0;-webkit-flex-direction:column;-moz-flex-direction:column;-ms-flex-direction:column;flex-direction:column;position:absolute;z-index:999}.content{-webkit-flex:1 1 .01%;-moz-flex:1 1 .01%;-ms-flex:1 1 .01%;flex:1 1 .01%;-ms-flex-positive:1;-ms-flex-negative:1;-ms-flex-preferred-size:.01%;overflow-y:auto;-webkit-overflow-scrolling:touch}.content-inner{max-width:949px;margin:0 auto;padding:3px 60px}@media screen and (max-width:768px){.content-inner{padding:27px 20px 27px 40px}}body.sidebar-closed .sidebar-toggle{display:block}.sidebar-toggle{position:fixed;z-index:99;left:18px;top:8px;background-color:transparent;border:none;padding:0;font-size:16px}.sidebar-toggle:hover{color:#e1e1e1}@media screen and (min-width:768px){.sidebar-toggle{display:none}}.sidebar{font-size:14px;line-height:18px;background:#373f52;color:#d5dae6;overflow:hidden}.sidebar .sidebar-toggle{display:block;left:275px;color:#e1e1e1}.sidebar .sidebar-toggle:hover{color:#fff}.sidebar ul li{margin:0;padding:0 10px}.sidebar a{color:#d5dae6;text-decoration:none}.sidebar a:hover{color:#fff}.sidebar .sidebar-projectLink{margin:23px 30px 0}.sidebar .sidebar-projectDetails{display:inline-block;text-align:right;vertical-align:top;margin-top:6px}.sidebar .sidebar-projectImage{display:inline-block;max-width:64px;max-height:64px;margin-left:15px;vertical-align:bottom}.sidebar .sidebar-projectName{font-weight:700;font-size:24px;line-height:30px;color:#fff;margin:0;padding:0;max-width:155px}.sidebar .sidebar-projectVersion{margin:0;padding:0;font-weight:300;font-size:16px;line-height:20px;color:#fff}.sidebar .sidebar-listNav{padding:0 30px}.sidebar .sidebar-listNav li,.sidebar .sidebar-listNav li a{text-transform:uppercase;font-weight:300;font-size:13px}.sidebar .sidebar-listNav li{padding-left:17px;border-left:3px solid transparent;transition:all .3s linear;line-height:27px}.sidebar .sidebar-listNav li.selected,.sidebar .sidebar-listNav li.selected a,.sidebar .sidebar-listNav li:hover,.sidebar .sidebar-listNav li:hover a{border-color:#9768d1;color:#fff}.sidebar .sidebar-search{margin:23px 30px 18px;display:-webkit-flex;display:-ms-flexbox;display:-ms-flex;display:flex}.sidebar .sidebar-search i.icon-search{font-size:14px;color:#d5dae6}.sidebar #full-list li.clicked>a,.sidebar #full-list ul li.active a{color:#fff}.sidebar .sidebar-search .sidebar-searchInput{background-color:transparent;border:none;border-radius:0;border-bottom:1px solid #959595;margin-left:5px}.sidebar #full-list{margin:4px 0 0 30px;padding:0 20px;overflow-y:auto;-webkit-overflow-scrolling:touch;-webkit-flex:1 1 .01%;-moz-flex:1 1 .01%;-ms-flex:1 1 .01%;flex:1 1 .01%;-ms-flex-positive:1;-ms-flex-negative:1;-ms-flex-preferred-size:.01%}.sidebar #full-list ul{margin:0 20px;padding:9px 0 18px}.sidebar #full-list ul li{font-weight:300;line-height:18px}.sidebar #full-list ul li ul{display:none;padding:9px 0}.sidebar #full-list ul li ul li{border-left:1px solid #959595;padding:0 10px}.sidebar #full-list ul li ul li.active:before{font-family:icomoon;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f105";margin-left:-10px;font-size:16px;margin-right:5px}.sidebar #full-list ul li.active{border-left:none}.sidebar #full-list ul li.active ul{display:block}.sidebar #full-list li{padding:0;line-height:27px}.sidebar #full-list li.collapsed ul{display:none}@media screen and (min-width:768px){.sidebar{position:relative;display:-webkit-flex;display:-ms-flexbox;display:-ms-flex;display:flex}}@media screen and (max-height:500px){.sidebar{overflow-y:auto}.sidebar #full-list{overflow:visible}}.content-inner{font-family:Merriweather,serif;font-size:1em;line-height:1.6875em;-webkit-font-feature-settings:"liga" 0;font-feature-settings:"liga" 0;-webkit-font-variant-ligatures:no-common-ligatures}.content-inner h1,.content-inner h2,.content-inner h3,.content-inner h4,.content-inner h5,.content-inner h6{font-family:Lato,sans-serif;font-weight:800;line-height:1.5em;word-wrap:break-word}.content-inner h1{font-size:2em;margin:1em 0 .5em}.content-inner h1.section-heading{margin:1.5em 0 .5em}.content-inner h1 small{font-weight:300}.content-inner h1 a.view-source{font-size:1.2rem}.content-inner h2{font-size:1.625em;margin:1em 0 .5em;font-weight:400}.content-inner h3{font-size:1.375em;margin:1em 0 .5em;font-weight:600}.content-inner a{color:#000;text-decoration:none;text-shadow:.03em 0 #fff,-.03em 0 #fff,0 .03em #fff,0 -.03em #fff,.06em 0 #fff,-.06em 0 #fff,.09em 0 #fff,-.09em 0 #fff,.12em 0 #fff,-.12em 0 #fff,.15em 0 #fff,-.15em 0 #fff;background-image:linear-gradient(#fff,#fff),linear-gradient(#fff,#fff),linear-gradient(#000,#000);background-size:.05em 1px,.05em 1px,1px 1px;background-repeat:no-repeat,no-repeat,repeat-x;background-position:0 90%,100% 90%,0 90%}.content-inner a:selection{text-shadow:.03em 0 #b4d5fe,-.03em 0 #b4d5fe,0 .03em #b4d5fe,0 -.03em #b4d5fe,.06em 0 #b4d5fe,-.06em 0 #b4d5fe,.09em 0 #b4d5fe,-.09em 0 #b4d5fe,.12em 0 #b4d5fe,-.12em 0 #b4d5fe,.15em 0 #b4d5fe,-.15em 0 #b4d5fe;background:#b4d5fe}.content-inner a:-moz-selection{text-shadow:.03em 0 #b4d5fe,-.03em 0 #b4d5fe,0 .03em #b4d5fe,0 -.03em #b4d5fe,.06em 0 #b4d5fe,-.06em 0 #b4d5fe,.09em 0 #b4d5fe,-.09em 0 #b4d5fe,.12em 0 #b4d5fe,-.12em 0 #b4d5fe,.15em 0 #b4d5fe,-.15em 0 #b4d5fe;background:#b4d5fe}.content-inner a *,.content-inner a :after,.content-inner a :before,.content-inner a:after,.content-inner a:before{text-shadow:none}.content-inner a:visited{color:#000}.content-inner ul li{line-height:1.5em}.content-inner a.view-source{float:right;color:#959595;background:0 0;border:none;text-shadow:none;transition:color .3s ease-in-out}.content-inner a.view-source:hover{color:#373f52}.content-inner blockquote{font-style:italic;margin:.5em 0;padding:.25em 1.5em;border-left:3px solid #e1e1e1;display:inline-block}.content-inner blockquote :first-child{padding-top:0;margin-top:0}.content-inner blockquote :last-child{padding-bottom:0;margin-bottom:0}.content-inner table{margin:2em 0}.content-inner th{text-align:left;font-family:Lato,sans-serif;text-transform:uppercase;font-weight:600;padding-bottom:.5em}.content-inner tr{border-bottom:1px solid #d5dae6;vertical-align:bottom;height:2.5em}.content-inner .summary .summary-row .summary-signature a,.content-inner .summary h2 a{background:0 0;border:none;text-shadow:none}.content-inner td,.content-inner th{padding-left:1em;line-height:2em}.content-inner h1.section-heading:hover a.hover-link{opacity:1;text-decoration:none}.content-inner h1.section-heading a.hover-link{transition:opacity .3s ease-in-out;display:inline-block;opacity:0;padding:.3em .6em .6em;line-height:1em;margin-left:-2.7em;background:0 0;border:none;text-shadow:none;font-size:16px;vertical-align:middle}.content-inner .visible-xs{display:none!important}@media screen and (max-width:767px){.content-inner .visible-xs{display:block!important}}.content-inner img{max-width:100%}.content-inner .summary h2{font-weight:600}.content-inner .summary .summary-row .summary-signature{font-family:Inconsolata,Menlo,Courier,monospace;font-weight:600}.content-inner .summary .summary-row .summary-synopsis{font-family:Merriweather,serif;font-style:italic;padding:0 .5em;margin:0 0 .5em}.content-inner .detail-header,.content-inner code{font-family:Inconsolata,Menlo,Courier,monospace}.content-inner .summary .summary-row .summary-synopsis p{margin:0;padding:0}.content-inner .detail-header{margin:1.5em 0 .5em;padding:.5em 1em;background:#f7f7f7;border-left:3px solid #9768d1;font-size:1em;position:relative}.content-inner .detail-header .signature{font-size:1rem;font-weight:600}.content-inner .detail-header:hover a.detail-link{opacity:1;text-decoration:none}.content-inner .detail-header a.detail-link{transition:opacity .3s ease-in-out;position:absolute;top:0;left:0;display:block;opacity:0;padding:.6em;line-height:1.5em;margin-left:-2.5em;background:0 0;border:none;text-shadow:none}.content-inner .footer .line,.search-results h1{display:inline-block}.content-inner .specs .specs-list pre code,.content-inner .types .types-list .type-detail pre code{padding:0 .5em;border:none}.content-inner .specs .specs-list{margin:0 0 2em}.content-inner .specs .specs-list pre{margin:.5em 0}.content-inner .docstring{margin-bottom:3.5em}.content-inner .docstring h2,.content-inner .docstring h3,.content-inner .docstring h4,.content-inner .docstring h5{font-weight:800}.content-inner .docstring h2{font-size:1em}.content-inner .docstring h3{font-size:.95em}.content-inner .docstring h4{font-size:.9em}.content-inner .docstring h5{font-size:.85em}.content-inner .types .types-list .type-detail{margin-bottom:2em}.content-inner .types .types-list .type-detail pre{margin:.5em 0}.content-inner .types .types-list .type-detail .typespec-doc{padding:0 1.5em}.content-inner a.no-underline,.content-inner code a{color:#9768d1;text-shadow:none;background-image:none}.content-inner a.no-underline:active,.content-inner a.no-underline:focus,.content-inner a.no-underline:hover,.content-inner a.no-underline:visited,.content-inner code a:active,.content-inner code a:focus,.content-inner code a:hover,.content-inner code a:visited{color:#9768d1}.content-inner code{font-size:15px;font-style:normal;line-height:24px;font-weight:400;background-color:#f7f9fc;border:1px solid #e1e1e1;vertical-align:middle;border-radius:2px;padding:0 .5em}.content-inner pre{margin:1.5em 0}.content-inner pre.spec{margin:0}.content-inner pre.spec code{padding:0}.content-inner pre code.hljs{white-space:inherit;padding:1em 1.5em;background-color:#f7f9fc}.content-inner .footer{margin:4em auto 1em;text-align:center;font-style:italic;font-size:14px;color:#959595}.content-inner .footer a{color:#959595;text-decoration:none;text-shadow:.03em 0 #fff,-.03em 0 #fff,0 .03em #fff,0 -.03em #fff,.06em 0 #fff,-.06em 0 #fff,.09em 0 #fff,-.09em 0 #fff,.12em 0 #fff,-.12em 0 #fff,.15em 0 #fff,-.15em 0 #fff;background-image:linear-gradient(#fff,#fff),linear-gradient(#fff,#fff),linear-gradient(#959595,#959595);background-size:.05em 1px,.05em 1px,1px 1px;background-repeat:no-repeat,no-repeat,repeat-x;background-position:0 90%,100% 90%,0 90%}.content-inner .footer a:selection{text-shadow:.03em 0 #b4d5fe,-.03em 0 #b4d5fe,0 .03em #b4d5fe,0 -.03em #b4d5fe,.06em 0 #b4d5fe,-.06em 0 #b4d5fe,.09em 0 #b4d5fe,-.09em 0 #b4d5fe,.12em 0 #b4d5fe,-.12em 0 #b4d5fe,.15em 0 #b4d5fe,-.15em 0 #b4d5fe;background:#b4d5fe}.content-inner .footer a:-moz-selection{text-shadow:.03em 0 #b4d5fe,-.03em 0 #b4d5fe,0 .03em #b4d5fe,0 -.03em #b4d5fe,.06em 0 #b4d5fe,-.06em 0 #b4d5fe,.09em 0 #b4d5fe,-.09em 0 #b4d5fe,.12em 0 #b4d5fe,-.12em 0 #b4d5fe,.15em 0 #b4d5fe,-.15em 0 #b4d5fe;background:#b4d5fe}.results .result-id a,.search-results a.close-search{background-image:none;transition:color .3s ease-in-out;text-shadow:none}.content-inner .footer a *,.content-inner .footer a :after,.content-inner .footer a :before,.content-inner .footer a:after,.content-inner .footer a:before{text-shadow:none}.content-inner .footer a:visited{color:#959595}.search-results a.close-search{display:inline-block;float:right}.search-results a.close-search:active,.search-results a.close-search:focus,.search-results a.close-search:visited{color:#000}.search-results a.close-search:hover{color:#9768d1}.results .result-id{font-size:1.2em}.results .result-id a:active,.results .result-id a:focus,.results .result-id a:visited{color:#000}.results .result-id a:hover{color:#9768d1}.results .result-elem em,.results .result-id em{font-style:normal;color:#9768d1}.results ul{margin:0;padding:0}.night-mode-toggle{background:0 0;border:none}.night-mode-toggle:after{font-size:12px;content:'Switch to night mode';text-decoration:underline}body.night-mode{background:#212127}body.night-mode .hljs-comment{color:#969896}body.night-mode .css .hljs-class,body.night-mode .css .hljs-id,body.night-mode .css .hljs-pseudo,body.night-mode .hljs-attribute,body.night-mode .hljs-regexp,body.night-mode .hljs-tag,body.night-mode .hljs-variable,body.night-mode .html .hljs-doctype,body.night-mode .ruby .hljs-constant,body.night-mode .xml .hljs-doctype,body.night-mode .xml .hljs-pi,body.night-mode .xml .hljs-tag .hljs-title{color:#c66}body.night-mode .hljs-built_in,body.night-mode .hljs-constant,body.night-mode .hljs-literal,body.night-mode .hljs-number,body.night-mode .hljs-params,body.night-mode .hljs-pragma,body.night-mode .hljs-preprocessor{color:#de935f}body.night-mode .css .hljs-rule .hljs-attribute,body.night-mode .ruby .hljs-class .hljs-title{color:#f0c674}body.night-mode .hljs-header,body.night-mode .hljs-inheritance,body.night-mode .hljs-name,body.night-mode .hljs-string,body.night-mode .hljs-value,body.night-mode .ruby .hljs-symbol,body.night-mode .xml .hljs-cdata{color:#b5bd68}body.night-mode .css .hljs-hexcolor,body.night-mode .hljs-title{color:#8abeb7}body.night-mode .coffeescript .hljs-title,body.night-mode .hljs-function,body.night-mode .javascript .hljs-title,body.night-mode .perl .hljs-sub,body.night-mode .python .hljs-decorator,body.night-mode .python .hljs-title,body.night-mode .ruby .hljs-function .hljs-title,body.night-mode .ruby .hljs-title .hljs-keyword{color:#81a2be}body.night-mode .hljs-keyword,body.night-mode .javascript .hljs-function{color:#b294bb}body.night-mode .hljs{display:block;overflow-x:auto;background:#1d1f21;color:#c5c8c6;padding:.5em;-webkit-text-size-adjust:none}body.night-mode .coffeescript .javascript,body.night-mode .javascript .xml,body.night-mode .tex .hljs-formula,body.night-mode .xml .css,body.night-mode .xml .hljs-cdata,body.night-mode .xml .javascript,body.night-mode .xml .vbscript{opacity:.5}body.night-mode .night-mode-toggle:after{color:#959595;content:'Switch to day mode';text-decoration:underline}body.night-mode .content-inner{color:#b4b4b4}body.night-mode .content-inner h1,body.night-mode .content-inner h2,body.night-mode .content-inner h3,body.night-mode .content-inner h4,body.night-mode .content-inner h5,body.night-mode .content-inner h6{color:#d2d2d2}body.night-mode .content-inner a{color:#d2d2d2;text-decoration:none;text-shadow:none;background-image:none}body.night-mode .content-inner .detail-header{background:#3a4152;color:#d2d2d2}body.night-mode .content-inner code,body.night-mode .content-inner pre code.hljs{background-color:#2c2c31;border:1px solid #38383d}body.night-mode .content-inner .footer{color:#959595}body.night-mode .content-inner .footer .line{display:inline-block}body.night-mode .content-inner .footer a{color:#959595;text-shadow:none;background-image:none;text-decoration:underline}.sidebar-toggle i{color:#d5dae6}@media print{#sidebar{display:none}}
--------------------------------------------------------------------------------
/doc/dist/sidebar_items.js:
--------------------------------------------------------------------------------
1 | sidebarNodes={"exceptions":[],"extras":[{"id":"api-reference","title":"API Reference","headers":[]}],"modules":[{"id":"Datom","title":"Datom"},{"id":"DatomicGenServer","title":"DatomicGenServer","functions":[{"id":"entity/4","anchor":"entity/4"},{"id":"load/3","anchor":"load/3"},{"id":"migrate/3","anchor":"migrate/3"},{"id":"mock/3","anchor":"mock/3"},{"id":"pull/4","anchor":"pull/4"},{"id":"pull_many/4","anchor":"pull_many/4"},{"id":"q/4","anchor":"q/4"},{"id":"reset/3","anchor":"reset/3"},{"id":"start/3","anchor":"start/3"},{"id":"start_link/3","anchor":"start_link/3"},{"id":"transact/3","anchor":"transact/3"},{"id":"unmock/2","anchor":"unmock/2"}],"types":[{"id":"datomic_call/0","anchor":"t:datomic_call/0"},{"id":"datomic_message/0","anchor":"t:datomic_message/0"},{"id":"datomic_result/0","anchor":"t:datomic_result/0"},{"id":"send_option/0","anchor":"t:send_option/0"},{"id":"start_option/0","anchor":"t:start_option/0"}]},{"id":"DatomicGenServer.Db","title":"DatomicGenServer.Db","functions":[{"id":"_and/1","anchor":"_and/1"},{"id":"_expr/3","anchor":"_expr/3"},{"id":"_fn/0","anchor":"_fn/0"},{"id":"_not/1","anchor":"_not/1"},{"id":"_not_join/2","anchor":"_not_join/2"},{"id":"_or/1","anchor":"_or/1"},{"id":"_or_join/2","anchor":"_or_join/2"},{"id":"_pull/2","anchor":"_pull/2"},{"id":"add/0","anchor":"add/0"},{"id":"alter_attribute/0","anchor":"alter_attribute/0"},{"id":"as_of/1","anchor":"as_of/1"},{"id":"blank/0","anchor":"blank/0"},{"id":"cardinality/0","anchor":"cardinality/0"},{"id":"cardinality_many/0","anchor":"cardinality_many/0"},{"id":"cardinality_one/0","anchor":"cardinality_one/0"},{"id":"collection_binding/1","anchor":"collection_binding/1"},{"id":"db/0","anchor":"db/0"},{"id":"dbid/1","anchor":"dbid/1"},{"id":"doc/0","anchor":"doc/0"},{"id":"entity/4","anchor":"entity/4"},{"id":"fn_cas/0","anchor":"fn_cas/0"},{"id":"fn_retract_entity/0","anchor":"fn_retract_entity/0"},{"id":"fulltext/0","anchor":"fulltext/0"},{"id":"history/0","anchor":"history/0"},{"id":"id/0","anchor":"id/0"},{"id":"ident/0","anchor":"ident/0"},{"id":"implicit/0","anchor":"implicit/0"},{"id":"inS/1","anchor":"inS/1"},{"id":"index/0","anchor":"index/0"},{"id":"install_attribute/0","anchor":"install_attribute/0"},{"id":"is_component/0","anchor":"is_component/0"},{"id":"load/3","anchor":"load/3"},{"id":"no_history/0","anchor":"no_history/0"},{"id":"pull/4","anchor":"pull/4"},{"id":"pull_many/4","anchor":"pull_many/4"},{"id":"q/4","anchor":"q/4"},{"id":"q?/1","anchor":"q?/1"},{"id":"retract/0","anchor":"retract/0"},{"id":"schema_partition/0","anchor":"schema_partition/0"},{"id":"since/1","anchor":"since/1"},{"id":"single_scalar/0","anchor":"single_scalar/0"},{"id":"star/0","anchor":"star/0"},{"id":"transact/3","anchor":"transact/3"},{"id":"transaction_partition/0","anchor":"transaction_partition/0"},{"id":"tx_instant/0","anchor":"tx_instant/0"},{"id":"type_bigdec/0","anchor":"type_bigdec/0"},{"id":"type_bigint/0","anchor":"type_bigint/0"},{"id":"type_boolean/0","anchor":"type_boolean/0"},{"id":"type_bytes/0","anchor":"type_bytes/0"},{"id":"type_double/0","anchor":"type_double/0"},{"id":"type_float/0","anchor":"type_float/0"},{"id":"type_instant/0","anchor":"type_instant/0"},{"id":"type_keyword/0","anchor":"type_keyword/0"},{"id":"type_long/0","anchor":"type_long/0"},{"id":"type_ref/0","anchor":"type_ref/0"},{"id":"type_string/0","anchor":"type_string/0"},{"id":"type_uri/0","anchor":"type_uri/0"},{"id":"type_uuid/0","anchor":"type_uuid/0"},{"id":"unique/0","anchor":"unique/0"},{"id":"unique_identity/0","anchor":"unique_identity/0"},{"id":"unique_value/0","anchor":"unique_value/0"},{"id":"user_partition/0","anchor":"user_partition/0"},{"id":"value_type/0","anchor":"value_type/0"}],"types":[{"id":"datom_map/0","anchor":"t:datom_map/0"},{"id":"query_option/0","anchor":"t:query_option/0"},{"id":"transaction_result/0","anchor":"t:transaction_result/0"}]},{"id":"DatomicGenServer.EntityMap","title":"DatomicGenServer.EntityMap","functions":[{"id":"aggregate_by/3","anchor":"aggregate_by/3"},{"id":"delete/2","anchor":"delete/2"},{"id":"e_key/0","anchor":"e_key/0"},{"id":"equal?/2","anchor":"equal?/2"},{"id":"from_records/3","anchor":"from_records/3"},{"id":"from_rows/4","anchor":"from_rows/4"},{"id":"from_transaction/2","anchor":"from_transaction/2"},{"id":"get/3","anchor":"get/3"},{"id":"get_attr/4","anchor":"get_attr/4"},{"id":"has_key?/2","anchor":"has_key?/2"},{"id":"index_by/2","anchor":"index_by/2"},{"id":"keys/1","anchor":"keys/1"},{"id":"new/2","anchor":"new/2"},{"id":"put/3","anchor":"put/3"},{"id":"put_attr/5","anchor":"put_attr/5"},{"id":"rename_keys/2","anchor":"rename_keys/2"},{"id":"set_defaults/1","anchor":"set_defaults/1"},{"id":"update/2","anchor":"update/2"},{"id":"update_from_records/3","anchor":"update_from_records/3"},{"id":"update_from_rows/4","anchor":"update_from_rows/4"},{"id":"update_from_transaction/2","anchor":"update_from_transaction/2"},{"id":"values/1","anchor":"values/1"}],"types":[{"id":"aggregate/0","anchor":"t:aggregate/0"},{"id":"entity_map_option/0","anchor":"t:entity_map_option/0"},{"id":"t/0","anchor":"t:t/0"}]},{"id":"DatomicGenServer.EntityMap.DataTuple","title":"DatomicGenServer.EntityMap.DataTuple"},{"id":"DatomicGenServer.ProcessState","title":"DatomicGenServer.ProcessState"},{"id":"DatomicTransaction","title":"DatomicTransaction"}],"protocols":[]}
--------------------------------------------------------------------------------
/doc/fonts/icomoon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psfblair/datomic_gen_server/a1df454a9b78034c4ac68820dd625128799fc2c1/doc/fonts/icomoon.eot
--------------------------------------------------------------------------------
/doc/fonts/icomoon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/doc/fonts/icomoon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psfblair/datomic_gen_server/a1df454a9b78034c4ac68820dd625128799fc2c1/doc/fonts/icomoon.ttf
--------------------------------------------------------------------------------
/doc/fonts/icomoon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/psfblair/datomic_gen_server/a1df454a9b78034c4ac68820dd625128799fc2c1/doc/fonts/icomoon.woff
--------------------------------------------------------------------------------
/doc/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | datomic_gen_server v2.2.5 – Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/lib/datomic_gen_server.ex:
--------------------------------------------------------------------------------
1 | defmodule DatomicGenServer do
2 | use GenServer
3 | require Logger
4 | @moduledoc """
5 | DatomicGenServer is an Elixir GenServer that communicates with a Clojure
6 | Datomic peer running in the JVM, using clojure-erlastic.
7 |
8 | The interface functions in this module communicate with Datomic using edn
9 | strings. To use Elixir data structures, see the accompanying `DatomicGenServer.db`
10 | module.
11 |
12 | ## Examples
13 |
14 | DatomicGenServer.start(
15 | "datomic:mem://test",
16 | true,
17 | [{:timeout, 20_000}, {:default_message_timeout, 20_000}, {:name, DatomicGenServer}]
18 | )
19 |
20 | query = "[:find ?c :where [?c :db/doc \\"Some docstring that isn't in the database\\"]]"
21 | DatomicGenServer.q(DatomicGenServer, query)
22 |
23 | # => {:ok, "\#{}\\n"}
24 |
25 | data_to_add = \"\"\"
26 | [ { :db/id #db/id[:db.part/db]
27 | :db/ident :person/name
28 | :db/valueType :db.type/string
29 | :db/cardinality :db.cardinality/one
30 | :db/doc \\"A person's name\\"
31 | :db.install/_attribute :db.part/db}]
32 | \"\"\"
33 |
34 | DatomicGenServer.transact(DatomicGenServer, data_to_add)
35 |
36 | # => {:ok, "{:db-before {:basis-t 1000}, :db-after {:basis-t 1000},
37 | :tx-data [{:a 50, :e 13194139534313, :v #inst \\"2016-02-14T02:10:54.580-00:00\\",
38 | :tx 13194139534313, :added true} {:a 10, :e 64, :v :person/name, :tx 13194139534313,
39 | :added true} {:a 40, :e 64, :v 23, :tx 13194139534313, :added true} {:a 41,
40 | :e 64, :v 35, :tx 13194139534313, :added true} {:a 62, :e 64,
41 | :v \\"A person's name\\", :tx 13194139534313, :added true} {:a 13,
42 | :e 0, :v 64, :tx 13194139534313, :added true}], :tempids {-9223367638809264705 64}}"}
43 | """
44 | @type datomic_message :: {:q, integer, String.t, [String.t]} |
45 | {:transact, integer, String.t} |
46 | {:pull, integer, String.t, String.t} |
47 | {:"pull-many", integer, String.t, String.t} |
48 | {:entity, integer, String.t, [atom] | :all} |
49 | {:migrate, integer, String.t} |
50 | {:load, integer, String.t} |
51 | {:mock, integer, atom} |
52 | {:reset, integer, atom} |
53 | {:unmock, integer}
54 | @type datomic_call :: {datomic_message, message_timeout :: non_neg_integer}
55 | @type datomic_result :: {:ok, String.t} | {:error, term}
56 | @type start_option :: GenServer.option | {:default_message_timeout, non_neg_integer}
57 | @type send_option :: {:message_timeout, non_neg_integer} | {:client_timeout, non_neg_integer}
58 |
59 | # These should be overridden either by application configs or by passed parameters
60 | @last_ditch_startup_timeout 5000
61 | @last_ditch_default_message_timeout 5000
62 |
63 | defmodule ProcessState do
64 | defstruct port: nil, message_wait_until_crash: 5_000
65 | @type t :: %ProcessState{port: port, message_wait_until_crash: non_neg_integer}
66 | end
67 |
68 | ############################# INTERFACE FUNCTIONS ############################
69 | @doc """
70 | Starts the GenServer.
71 |
72 | This function is basically a pass-through to `GenServer.start`, but with some
73 | additional parameters: The first is the URL of the Datomic transactor to which
74 | to connect, and the second a boolean parameter indicating whether or not to
75 | create the database if it does not yet exist.
76 |
77 | The options keyword list may include the normal options accepted by `GenServer.start`,
78 | as well as a `:default_message_timeout` option that controls the default time in
79 | milliseconds that the server will wait for a database response before crashing.
80 | Note that if the `:timeout` option is provided, the GenServer will crash if that
81 | timeout is exceeded.
82 |
83 | ## Example
84 |
85 | DatomicGenServer.start(
86 | "datomic:mem://test",
87 | true,
88 | [{:timeout, 20_000}, {:default_message_timeout, 20_000}, {:name, DatomicGenServer}]
89 | )
90 |
91 | """
92 | @spec start(String.t, boolean, [start_option]) :: GenServer.on_start
93 | def start(db_uri, create? \\ false, options \\ []) do
94 | {params, options_without_name} = startup_params(db_uri, create?, options)
95 | GenServer.start(__MODULE__, params, options_without_name)
96 | end
97 |
98 | @doc """
99 | Starts the GenServer in a linked process.
100 |
101 | This function is basically a pass-through to `GenServer.start_link`, but with
102 | some additional parameters: The first is the URL of the Datomic transactor to
103 | which to connect, and the second a boolean parameter indicating whether or not
104 | to create the database if it does not yet exist.
105 |
106 | The options keyword list may include the normal options accepted by `GenServer.start_link`,
107 | as well as a `:default_message_timeout` option that controls the default time in
108 | milliseconds that the server will wait for a database response before crashing.
109 | Note that if the `:timeout` option is provided, the GenServer will crash if that
110 | timeout is exceeded.
111 |
112 | ## Example
113 |
114 | DatomicGenServer.start_link(
115 | "datomic:mem://test",
116 | true,
117 | [{:timeout, 20_000}, {:default_message_timeout, 20_000}, {:name, DatomicGenServer}]
118 | )
119 |
120 | """
121 | @spec start_link(String.t, boolean, [start_option]) :: GenServer.on_start
122 | def start_link(db_uri, create? \\ false, options \\ []) do
123 | {params, options_without_name} = startup_params(db_uri, create?, options)
124 | GenServer.start_link(__MODULE__, params, options_without_name)
125 | end
126 |
127 | defp startup_params(db_uri, create?, options) do
128 | {startup_wait, options} = Keyword.pop(options, :timeout)
129 | startup_wait = startup_wait || Application.get_env(:datomic_gen_server, :startup_wait_millis) || @last_ditch_startup_timeout
130 |
131 | {default_message_timeout, options} = Keyword.pop(options, :default_message_timeout)
132 | default_message_timeout = default_message_timeout || Application.get_env(:datomic_gen_server, :message_wait_until_crash) || @last_ditch_default_message_timeout
133 |
134 | {maybe_process_identifier, options} = Keyword.pop(options, :name)
135 |
136 | params = {db_uri, create?, maybe_process_identifier, startup_wait, default_message_timeout}
137 | {params, options}
138 | end
139 |
140 | @doc """
141 | Queries a DatomicGenServer using a query formulated as an edn string.
142 | This query is passed to the Datomic `q` API function.
143 |
144 | The first parameter to this function is the pid or alias of the GenServer process;
145 | the second is the query.
146 |
147 | The optional third parameter is a list of bindings for the data sources in the
148 | query, passed to the `inputs` argument of the Datomic `q` function. **IMPORTANT:**
149 | These bindings are passed in the form of edn strings which are read back in the
150 | Clojure peer and then passed to Clojure `eval`. Since any arbitrary Clojure forms
151 | that are passed in are evaluated, **you must be particularly careful that these
152 | bindings are sanitized** and that you are not passing anything that you don't
153 | control. In general, you should prefer the `DatomicGenServer.Db.q/3` function
154 | which accepts data structures and converts them to edn.
155 |
156 | Bindings may include `datomic_gen_server.peer/*db*` for the current database.
157 |
158 | The options keyword list may also include a `:client_timeout` option that
159 | specifies the milliseconds timeout passed to `GenServer.call`, and a
160 | `:message_timeout` option that specifies how long the GenServer should wait
161 | for a response before crashing (overriding the default value set in
162 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
163 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
164 | return an error but the server will not crash even if the response message is
165 | never returned from the Clojure peer.
166 |
167 | If the client timeout is not supplied, the value is taken from the configured
168 | value of `:timeout_on_call` in the application environment; if that is not
169 | configured, the GenServer default of 5000 is used.
170 |
171 | If the message timeout is not supplied, the default value supplied at startup
172 | with the option `:default_message_timeout` is used; if this was not specified,
173 | the configured value of `:message_wait_until_crash` in the application
174 | environment is used. If this is also omitted, a value of 5000 is used.
175 |
176 | ## Example
177 |
178 | query = "[:find ?c :where [?c :db/doc \\"Some docstring that isn't in the database\\"]]"
179 | DatomicGenServer.q(DatomicGenServer, query)
180 |
181 | => {:ok, "\#{}\\n"}
182 |
183 | """
184 | @spec q(GenServer.server, String.t, [String.t], [send_option]) :: datomic_result
185 | def q(server_identifier, edn_str, bindings_edn \\ [], options \\ []) do
186 | # Note that clojure-erltastic sends empty lists to Clojure as `nil`. This
187 | # interferes with `wait_for_response` - if an error comes back it expects to
188 | # find the original message, but Clojure will return the original message as
189 | # having a `nil` where we are waiting for original message containing an
190 | # empty list. To protect against this situation we never send empty lists to
191 | # Clojure; only `nil`.
192 | bindings = if bindings_edn && ! Enum.empty?(bindings_edn) do bindings_edn else nil end
193 | msg_unique_id = :erlang.unique_integer([:monotonic])
194 | call_server(server_identifier, {:q, msg_unique_id, edn_str, bindings}, options)
195 | end
196 |
197 | @doc """
198 | Issues a transaction against a DatomicGenServer using a transaction
199 | formulated as an edn string. This transaction is passed to the Datomic `transact`
200 | API function.
201 |
202 | The first parameter to this function is the pid or alias of the GenServer process;
203 | the second is the transaction data in edn format.
204 |
205 | The options keyword list may also include a `:client_timeout` option that
206 | specifies the milliseconds timeout passed to `GenServer.call`, and a
207 | `:message_timeout` option that specifies how long the GenServer should wait
208 | for a response before crashing (overriding the default value set in
209 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
210 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
211 | return an error but the server will not crash even if the response message is
212 | never returned from the Clojure peer.
213 |
214 | If the client timeout is not supplied, the value is taken from the configured
215 | value of `:timeout_on_call` in the application environment; if that is not
216 | configured, the GenServer default of 5000 is used.
217 |
218 | If the message timeout is not supplied, the default value supplied at startup
219 | with the option `:default_message_timeout` is used; if this was not specified,
220 | the configured value of `:message_wait_until_crash` in the application
221 | environment is used. If this is also omitted, a value of 5000 is used.
222 |
223 | ## Example
224 |
225 | data_to_add = \"\"\"
226 | [ { :db/id #db/id[:db.part/db]
227 | :db/ident :person/name
228 | :db/valueType :db.type/string
229 | :db/cardinality :db.cardinality/one
230 | :db/doc \\"A person's name\\"
231 | :db.install/_attribute :db.part/db}]
232 | \"\"\"
233 |
234 | DatomicGenServer.transact(DatomicGenServer, data_to_add)
235 |
236 | => {:ok, "{:db-before {:basis-t 1000}, :db-after {:basis-t 1000},
237 | :tx-data [{:a 50, :e 13194139534313, :v #inst \\"2016-02-14T02:10:54.580-00:00\\",
238 | :tx 13194139534313, :added true} {:a 10, :e 64, :v :person/name, :tx 13194139534313,
239 | :added true} {:a 40, :e 64, :v 23, :tx 13194139534313, :added true} {:a 41,
240 | :e 64, :v 35, :tx 13194139534313, :added true} {:a 62, :e 64,
241 | :v \\"A person's name\\", :tx 13194139534313, :added true} {:a 13,
242 | :e 0, :v 64, :tx 13194139534313, :added true}], :tempids {-9223367638809264705 64}}"}
243 |
244 | """
245 | @spec transact(GenServer.server, String.t, [send_option]) :: datomic_result
246 | def transact(server_identifier, edn_str, options \\ []) do
247 | msg_unique_id = :erlang.unique_integer([:monotonic])
248 | call_server(server_identifier, {:transact, msg_unique_id, edn_str}, options)
249 | end
250 |
251 | @doc """
252 | Issues a `pull` call that is passed to the Datomic `pull` API function.
253 |
254 | The first parameter to this function is the pid or alias of the GenServer process;
255 | the second is an edn string representing the pattern that is to be passed as the
256 | first parameter to `pull` -- you shouldn't need to single-quote this. The third
257 | parameter is an entity identifier (entity id, ident, or lookup ref).
258 |
259 | The options keyword list may also include a `:client_timeout` option that
260 | specifies the milliseconds timeout passed to `GenServer.call`, and a
261 | `:message_timeout` option that specifies how long the GenServer should wait
262 | for a response before crashing (overriding the default value set in
263 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
264 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
265 | return an error but the server will not crash even if the response message is
266 | never returned from the Clojure peer.
267 |
268 | If the client timeout is not supplied, the value is taken from the configured
269 | value of `:timeout_on_call` in the application environment; if that is not
270 | configured, the GenServer default of 5000 is used.
271 |
272 | If the message timeout is not supplied, the default value supplied at startup
273 | with the option `:default_message_timeout` is used; if this was not specified,
274 | the configured value of `:message_wait_until_crash` in the application
275 | environment is used. If this is also omitted, a value of 5000 is used.
276 |
277 | ## Example
278 |
279 | DatomicGenServer.pull(DatomicGenServer, "[*]", "123242")
280 |
281 | => {:ok, "{:db/id 75, :db/ident :person/city, :db/valueType {:db/id 23},
282 | :db/cardinality {:db/id 35}, :db/doc \"A person's city\"}\n"}
283 |
284 | """
285 | @spec pull(GenServer.server, String.t, String.t, [send_option]) :: datomic_result
286 | def pull(server_identifier, pattern_str, identifier_str, options \\ []) do
287 | msg_unique_id = :erlang.unique_integer([:monotonic])
288 | call_server(server_identifier, {:pull, msg_unique_id, pattern_str, identifier_str}, options)
289 | end
290 |
291 | @doc """
292 | Issues a `pull-many` call that is passed to the Datomic `pull-many` API function.
293 |
294 | The first parameter to this function is the pid or alias of the GenServer process;
295 | the second is an edn string representing the pattern that is to be passed as the
296 | first parameter to `pull-many` -- you shouldn't need to single-quote this. The
297 | third parameter is a list of entity identifiers (entity id, ident, or lookup ref).
298 |
299 | The options keyword list may also include a `:client_timeout` option that
300 | specifies the milliseconds timeout passed to `GenServer.call`, and a
301 | `:message_timeout` option that specifies how long the GenServer should wait
302 | for a response before crashing (overriding the default value set in
303 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
304 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
305 | return an error but the server will not crash even if the response message is
306 | never returned from the Clojure peer.
307 |
308 | If the client timeout is not supplied, the value is taken from the configured
309 | value of `:timeout_on_call` in the application environment; if that is not
310 | configured, the GenServer default of 5000 is used.
311 |
312 | If the message timeout is not supplied, the default value supplied at startup
313 | with the option `:default_message_timeout` is used; if this was not specified,
314 | the configured value of `:message_wait_until_crash` in the application
315 | environment is used. If this is also omitted, a value of 5000 is used.
316 |
317 | ## Example
318 |
319 | DatomicGenServer.pull_many(DatomicGenServer, "[*]", "[12343 :person/zip]")
320 |
321 | => {:ok, "[{:db/id 67, :db/ident :person/state, :db/valueType {:db/id 23},
322 | :db/cardinality {:db/id 35}, :db/doc \"A person's state\"}
323 | {:db/id 78, :db/ident :person/zip, :db/valueType {:db/id 23},
324 | :db/cardinality {:db/id 35}, :db/doc \"A person's zip code\"}]\n"}
325 | """
326 | @spec pull_many(GenServer.server, String.t, String.t, [send_option]) :: datomic_result
327 | def pull_many(server_identifier, pattern_str, identifiers_str, options \\ []) do
328 | msg_unique_id = :erlang.unique_integer([:monotonic])
329 | call_server(server_identifier, {:"pull-many", msg_unique_id, pattern_str, identifiers_str}, options)
330 | end
331 |
332 |
333 | @doc """
334 | Issues an `entity` call that is passed to the Datomic `entity` API function.
335 |
336 | The first parameter to this function is the pid or alias of the GenServer process;
337 | the second is an edn string representing the parameter that is to be passed to
338 | `entity`: either an entity id, an ident, or a lookup ref. The third parameter
339 | is a list of atoms that represent the keys of the attributes you wish to fetch,
340 | or `:all` if you want all the entity's attributes.
341 |
342 | The options keyword list may also include a `:client_timeout` option that
343 | specifies the milliseconds timeout passed to `GenServer.call`, and a
344 | `:message_timeout` option that specifies how long the GenServer should wait
345 | for a response before crashing (overriding the default value set in
346 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
347 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
348 | return an error but the server will not crash even if the response message is
349 | never returned from the Clojure peer.
350 |
351 | If the client timeout is not supplied, the value is taken from the configured
352 | value of `:timeout_on_call` in the application environment; if that is not
353 | configured, the GenServer default of 5000 is used.
354 |
355 | If the message timeout is not supplied, the default value supplied at startup
356 | with the option `:default_message_timeout` is used; if this was not specified,
357 | the configured value of `:message_wait_until_crash` in the application
358 | environment is used. If this is also omitted, a value of 5000 is used.
359 |
360 | ## Example
361 |
362 | DatomicGenServer.entity(DatomicGenServer, ":person/email", [:"db/valueType", :"db/doc"])
363 |
364 | => {:ok, "{:db/valueType :db.type/string, :db/doc \\"A person's email\\"}\\n"}
365 |
366 | """
367 | @spec entity(GenServer.server, String.t, [atom] | :all, [send_option]) :: datomic_result
368 | def entity(server_identifier, edn_str, attr_names \\ :all, options \\ []) do
369 | msg_unique_id = :erlang.unique_integer([:monotonic])
370 | call_server(server_identifier, {:entity, msg_unique_id, edn_str, attr_names}, options)
371 | end
372 |
373 | @doc """
374 | Issues a call to net.phobot.datomic/migrator to migrate a database using
375 | database migration files in edn format.
376 |
377 | The first parameter to this function is the pid or alias of the GenServer process;
378 | the second is the path to the directory containing the migration files. Files
379 | will be processed in sort order. The Clojure Conformity library is used to keep
380 | the migrations idempotent.
381 |
382 | The options keyword list may also include a `:client_timeout` option that
383 | specifies the milliseconds timeout passed to `GenServer.call`, and a
384 | `:message_timeout` option that specifies how long the GenServer should wait
385 | for a response before crashing (overriding the default value set in
386 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
387 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
388 | return an error but the server will not crash even if the response message is
389 | never returned from the Clojure peer.
390 |
391 | If the client timeout is not supplied, the value is taken from the configured
392 | value of `:timeout_on_call` in the application environment; if that is not
393 | configured, the GenServer default of 5000 is used.
394 |
395 | If the message timeout is not supplied, the default value supplied at startup
396 | with the option `:default_message_timeout` is used; if this was not specified,
397 | the configured value of `:message_wait_until_crash` in the application
398 | environment is used. If this is also omitted, a value of 5000 is used.
399 |
400 | ## Example
401 |
402 | DatomicGenServer.migrate(DatomicGenServer, Path.join [System.cwd(), "migrations"])
403 |
404 | => {:ok, :migrated}
405 |
406 | """
407 | @spec migrate(GenServer.server, String.t, [send_option]) :: datomic_result
408 | def migrate(server_identifier, migration_path, options \\ []) do
409 | msg_unique_id = :erlang.unique_integer([:monotonic])
410 | call_server(server_identifier, {:migrate, msg_unique_id, migration_path}, options)
411 | end
412 |
413 | @doc """
414 | Issues a call to the Clojure net.phobot.datomic/seed library to load data into
415 | a database using data files in edn format. The database is not dropped,
416 | recreated, or migrated before loading.
417 |
418 | The first parameter to this function is the pid or alias of the GenServer process;
419 | the second is the path to the directory containing the data files. The data
420 | files will be processed in the sort order of their directory.
421 |
422 | Data is loaded in a single transaction. The return value of the function
423 | is the result of the Datomic `transact` API function call that executed the
424 | transaction. If you want this result in a struct, call the wrapper `load`
425 | function in the `DatomicGenServer.Db` module.
426 |
427 | Loading data does not use the Clojure Conformity library and is not idempotent.
428 |
429 | The options keyword list may also include a `:client_timeout` option that
430 | specifies the milliseconds timeout passed to `GenServer.call`, and a
431 | `:message_timeout` option that specifies how long the GenServer should wait
432 | for a response before crashing (overriding the default value set in
433 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
434 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
435 | return an error but the server will not crash even if the response message is
436 | never returned from the Clojure peer.
437 |
438 | If the client timeout is not supplied, the value is taken from the configured
439 | value of `:timeout_on_call` in the application environment; if that is not
440 | configured, the GenServer default of 5000 is used.
441 |
442 | If the message timeout is not supplied, the default value supplied at startup
443 | with the option `:default_message_timeout` is used; if this was not specified,
444 | the configured value of `:message_wait_until_crash` in the application
445 | environment is used. If this is also omitted, a value of 5000 is used.
446 |
447 | ## Example
448 | data_dir = Path.join [System.cwd(), "seed-data"]
449 | DatomicGenServer.load(DatomicGenServer, data_dir)
450 |
451 | => {:ok, "{:db-before {:basis-t 1000}, :db-after {:basis-t 1000}, ...
452 |
453 | """
454 | @spec load(GenServer.server, String.t, [send_option]) :: datomic_result
455 | def load(server_identifier, data_path, options \\ []) do
456 | msg_unique_id = :erlang.unique_integer([:monotonic])
457 | call_server(server_identifier, {:load, msg_unique_id, data_path}, options)
458 | end
459 |
460 | @doc """
461 | Saves a snapshot of the current database state using a supplied key, and
462 | creates a mock connection (using the datomock library) using that database
463 | as a starting point. Requires the `:allow_datomic_mocking?` configuration
464 | parameter to be set in the `:datomic_gen_server` application environment;
465 | otherwise the current connection and database continue to be active.
466 | Subsequent operations on the database will use the mock connection until
467 | you call either the `reset` or `unmock` functions.
468 |
469 | The first parameter to this function is the pid or alias of the GenServer process;
470 | the second is the key under which to store the database snapshot. If successful,
471 | the return value is a tuple of `:ok` and the key that was passed to the function.
472 |
473 | The active database can be reverted to the initial snapshot using the `reset`
474 | function, or can be switched back to use the real, live connection and database
475 | using the `unmock` function.
476 |
477 | The options keyword list may also include a `:client_timeout` option that
478 | specifies the milliseconds timeout passed to `GenServer.call`, and a
479 | `:message_timeout` option that specifies how long the GenServer should wait
480 | for a response before crashing (overriding the default value set in
481 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
482 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
483 | return an error but the server will not crash even if the response message is
484 | never returned from the Clojure peer.
485 |
486 | If the client timeout is not supplied, the value is taken from the configured
487 | value of `:timeout_on_call` in the application environment; if that is not
488 | configured, the GenServer default of 5000 is used.
489 |
490 | If the message timeout is not supplied, the default value supplied at startup
491 | with the option `:default_message_timeout` is used; if this was not specified,
492 | the configured value of `:message_wait_until_crash` in the application
493 | environment is used. If this is also omitted, a value of 5000 is used.
494 |
495 | ## Example
496 |
497 | DatomicGenServer.mock(DatomicGenServer, :"just-migrated")
498 |
499 | => {:ok, :"just-migrated"}
500 |
501 | """
502 | @spec mock(GenServer.server, atom, [send_option]) :: datomic_result
503 | def mock(server_identifier, db_key, options \\ []) do
504 | msg_unique_id = :erlang.unique_integer([:monotonic])
505 | call_server(server_identifier, {:mock, msg_unique_id, db_key}, options)
506 | end
507 |
508 | @doc """
509 | Generates a new mock connection using a database snapshot previously saved
510 | using the `mock` function. Requires the `:allow_datomic_mocking?` configuration
511 | parameter to be set in the `:datomic_gen_server` application enviroment;
512 | otherwise the current connection and database continue to be active.
513 |
514 | The first parameter to this function is the pid or alias of the GenServer process;
515 | the second is the key under which the database snapshot was saved. If successful,
516 | the return value is a tuple of `:ok` and the key that was passed to the function.
517 |
518 | The database can be switched back to a real connection using the `unmock`
519 | function. It is also possible to manipulate the mocked database and save that
520 | new database state in a snapshot using the `mock` function.
521 |
522 | The options keyword list may also include a `:client_timeout` option that
523 | specifies the milliseconds timeout passed to `GenServer.call`, and a
524 | `:message_timeout` option that specifies how long the GenServer should wait
525 | for a response before crashing (overriding the default value set in
526 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
527 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
528 | return an error but the server will not crash even if the response message is
529 | never returned from the Clojure peer.
530 |
531 | If the client timeout is not supplied, the value is taken from the configured
532 | value of `:timeout_on_call` in the application environment; if that is not
533 | configured, the GenServer default of 5000 is used.
534 |
535 | If the message timeout is not supplied, the default value supplied at startup
536 | with the option `:default_message_timeout` is used; if this was not specified,
537 | the configured value of `:message_wait_until_crash` in the application
538 | environment is used. If this is also omitted, a value of 5000 is used.
539 |
540 | ## Example
541 |
542 | DatomicGenServer.reset(DatomicGenServer, :"just-migrated")
543 |
544 | => {:ok, :"just-migrated"}
545 |
546 | """
547 | @spec reset(GenServer.server, atom, [send_option]) :: datomic_result
548 | def reset(server_identifier, db_key, options \\ []) do
549 | msg_unique_id = :erlang.unique_integer([:monotonic])
550 | call_server(server_identifier, {:reset, msg_unique_id, db_key}, options)
551 | end
552 |
553 |
554 | @doc """
555 | Reverts to using a database derived from the real database connection rather
556 | than a mocked connection. If no mock connection is active, this function is
557 | a no-op.
558 |
559 | If the call is successful, the return value is a tuple of `:ok` and `:unmocked`.
560 |
561 | The first parameter to this function is the pid or alias of the GenServer process.
562 |
563 | The options keyword list may also include a `:client_timeout` option that
564 | specifies the milliseconds timeout passed to `GenServer.call`, and a
565 | `:message_timeout` option that specifies how long the GenServer should wait
566 | for a response before crashing (overriding the default value set in
567 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
568 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
569 | return an error but the server will not crash even if the response message is
570 | never returned from the Clojure peer.
571 |
572 | If the client timeout is not supplied, the value is taken from the configured
573 | value of `:timeout_on_call` in the application environment; if that is not
574 | configured, the GenServer default of 5000 is used.
575 |
576 | If the message timeout is not supplied, the default value supplied at startup
577 | with the option `:default_message_timeout` is used; if this was not specified,
578 | the configured value of `:message_wait_until_crash` in the application
579 | environment is used. If this is also omitted, a value of 5000 is used.
580 |
581 | ## Example
582 |
583 | DatomicGenServer.unmock(DatomicGenServer)
584 |
585 | => {:ok, :unmocked}
586 |
587 | """
588 | @spec unmock(GenServer.server, [send_option]) :: datomic_result
589 | def unmock(server_identifier, options \\ []) do
590 | msg_unique_id = :erlang.unique_integer([:monotonic])
591 | call_server(server_identifier, {:unmock, msg_unique_id}, options)
592 | end
593 |
594 | @spec call_server(GenServer.server, datomic_message, [send_option]) :: datomic_result
595 | defp call_server(server_identifier, request, options) do
596 | {message_timeout, client_timeout} = message_wait_times(options)
597 | if client_timeout do
598 | GenServer.call(server_identifier, {request, message_timeout}, client_timeout)
599 | else
600 | GenServer.call(server_identifier, {request, message_timeout})
601 | end
602 | end
603 |
604 | defp message_wait_times(options) do
605 | # If it's nil, this value value is nil and we'll use the general default when handling the call.
606 | message_timeout = Keyword.get(options, :message_timeout)
607 |
608 | client_timeout = Keyword.get(options, :client_timeout)
609 | client_timeout = client_timeout || Application.get_env(:datomic_gen_server, :timeout_on_call)
610 |
611 | {message_timeout, client_timeout}
612 | end
613 |
614 | ############################# CALLBACK FUNCTIONS ##############################
615 |
616 | # Implements the GenServer `init` callback; clients should not call this function.
617 | #
618 | # On start, the server sends itself an initial message to start the JVM, then
619 | # registers itself under any alias provided. Any messages sent to the server by
620 | # clients at startup will arrive after the initialization message, and will need
621 | # to wait until the JVM starts and initialization is complete. Thus, it is important
622 | # that the timeouts on messages sent to the server exceed the startup timeout value,
623 | # at least for the messages sent during the startup phase.
624 | @spec init({String.t, boolean, GenServer.name | nil, non_neg_integer, non_neg_integer}) :: {:ok, ProcessState.t}
625 | def init({db_uri, create?, maybe_process_identifier, startup_wait_millis, default_message_timeout_millis}) do
626 | # Trapping exits actually does what we want here - i.e., allows us to exit
627 | # if the Clojure process crashes with an error on startup, using handle_info below.
628 | Process.flag(:trap_exit, true)
629 |
630 | send(self, {:initialize_jvm, db_uri, create?, startup_wait_millis})
631 | case maybe_process_identifier do
632 | nil -> nil
633 | {:global, identifier} -> :global.register_name(identifier, self)
634 | identifier -> Process.register(self, identifier)
635 | end
636 | # if maybe_process_identifier do
637 | # Process.register(self, maybe_process_identifier)
638 | # end
639 | {:ok, %ProcessState{port: nil, message_wait_until_crash: default_message_timeout_millis}}
640 | end
641 |
642 | defp start_jvm_command(db_uri, create?) do
643 | create_str = if create?, do: "true", else: ""
644 | working_directory = "#{:code.priv_dir(:datomic_gen_server)}/datomic_gen_server_peer"
645 | allow_mocking? = if Application.get_env(:datomic_gen_server, :allow_datomic_mocking?),
646 | do: "-Ddatomic.mocking=true", else: ""
647 | debug_peer_messages? = if Application.get_env(:datomic_gen_server, :debug_messages?),
648 | do: "-Ddebug.messages=true", else: ""
649 | command = "java -cp target/peer*standalone.jar #{allow_mocking?} #{debug_peer_messages?} " <>
650 | "datomic_gen_server.peer #{db_uri} #{create_str}"
651 | {working_directory, command}
652 | end
653 |
654 | defp my_name do
655 | Process.info(self) |> Keyword.get(:registered_name) || self |> inspect
656 | end
657 |
658 | # Implements the GenServer `handle_info` callback for the initial message that
659 | # starts the JVM. This is an internal message used for initialization; clients should not send this message to the GenServer.
660 | @spec handle_info({:initialize_jvm, String.t, boolean, non_neg_integer}, ProcessState.t) :: {:noreply, ProcessState.t}
661 | def handle_info({:initialize_jvm, db_uri, create?, startup_wait_millis}, state) do
662 | {working_directory, command} = start_jvm_command(db_uri, create?)
663 | port = Port.open({:spawn, '#{command}'}, [:binary, :exit_status, packet: 4, cd: working_directory])
664 |
665 | # Block until JVM starts up, or we're not ready
666 | send(port, {self, {:command, :erlang.term_to_binary({:ping})}})
667 | receive do
668 | # Make sure we're only listening for a message back from the port, not some
669 | # message from a caller that may have gotten in first.
670 | {^port, {:data, _}} -> {:noreply, %ProcessState{port: port, message_wait_until_crash: state.message_wait_until_crash}}
671 | {:EXIT, _, :normal} ->
672 | _ = Logger.info("DatomicGenServer #{my_name} port received :normal exit signal; exiting.")
673 | exit(:normal)
674 | {:EXIT, _, error} ->
675 | _ = Logger.error("DatomicGenServer #{my_name} port exited with error on startup: #{inspect(error)}")
676 | exit(:port_exited_with_error)
677 | after startup_wait_millis ->
678 | _ = Logger.error("DatomicGenServer #{my_name} port startup timed out after startup_wait_millis: [#{startup_wait_millis}]")
679 | exit(:port_start_timed_out)
680 | end
681 | end
682 |
683 | # Handle exit messages
684 | @spec handle_info({:EXIT, port, term}, ProcessState.t) :: no_return
685 | def handle_info({:EXIT, _, _}, _) do
686 | _ = Logger.warn("DatomicGenServer #{my_name} received exit message.")
687 | exit(:port_terminated)
688 | end
689 |
690 | # Do-nothing implementation of the GenServer `handle_info` callback as a catch-all case.
691 | # Not sure how to do spec for this catch-all case without Dialyzer telling me
692 | # I have overlapping domains.
693 | def handle_info(_, state) do
694 | {:noreply, state}
695 | end
696 |
697 | # Implements the GenServer `handle_call` callback to handle client messages.
698 | #
699 | # This function sends a message to the Clojure peer that is run on a port, and waits
700 | # for a response from the peer with the same message ID. Messages that are returned
701 | # from the port with different message IDs (for example, responses to earlier
702 | # requests that timed out) are discarded.
703 | @spec handle_call(datomic_call, term, ProcessState.t) :: {:reply, datomic_result, ProcessState.t}
704 | def handle_call(message, _, state) do
705 | port = state.port
706 | {datomic_operation, this_msg_timeout} = message
707 | message_timeout = this_msg_timeout || state.message_wait_until_crash
708 | send(port, {self, {:command, :erlang.term_to_binary(datomic_operation)}})
709 | response = wait_for_reply(port, datomic_operation, message_timeout, this_msg_timeout, state.message_wait_until_crash)
710 | {:reply, response, state}
711 | end
712 |
713 | # Need this in case an earlier call times out without the GenServer crashing, and
714 | # then the reply for that call from the Clojure port comes back before the
715 | # response to the message we're waiting for. Right now if there is such a
716 | # message we just clear it out of the mailbox and keep going. In the future,
717 | # if we're handling async messages we will need to do something more intelligent.
718 | # It would be nice if we could just use a pattern match in the receive clause
719 | # for this purpose, but we need to call :erlang.binary_to_term on the message
720 | # to see what's in it, and we can't do that without handling the message.
721 | defp wait_for_reply(port, sent_message, message_timeout, this_msg_timeout, message_wait_until_crash) do
722 | start_time = :os.system_time(:milli_seconds)
723 |
724 | if Application.get_env(:datomic_gen_server, :debug_messages?),
725 | do: Logger.info("Waiting for reply to message #{inspect(sent_message)}")
726 |
727 | response = receive do
728 | {^port, {:data, b}} -> :erlang.binary_to_term(b)
729 | after message_timeout ->
730 | _ = Logger.error("DatomicGenServer #{my_name} port unresponsive with message_wait_until_crash [#{message_wait_until_crash}] and this_msg_timeout [#{this_msg_timeout}]")
731 | exit(:port_unresponsive)
732 | end
733 |
734 | if Application.get_env(:datomic_gen_server, :debug_messages?),
735 | do: Logger.info("Received response from peer: [#{inspect(response)}]")
736 |
737 | elapsed = :os.system_time(:milli_seconds) - start_time
738 | sent_msg_id = message_unique_id(sent_message)
739 |
740 | # Determine if this is a response to the message we were waiting for. If not, recurse
741 | case response do
742 | {:ok, response_id, reply} when response_id == sent_msg_id -> {:ok, reply}
743 | {:error, echoed_msg, error} ->
744 | if message_unique_id(echoed_msg) == sent_msg_id do
745 | {:error, error}
746 | else
747 | wait_for_reply(port, sent_message, (message_timeout - elapsed), this_msg_timeout, message_wait_until_crash)
748 | end
749 | _ -> wait_for_reply(port, sent_message, (message_timeout - elapsed), this_msg_timeout, message_wait_until_crash)
750 | end
751 | end
752 |
753 | defp message_unique_id(message) do
754 | if is_tuple(message) && tuple_size(message) > 1 do
755 | elem(message, 1)
756 | else
757 | nil
758 | end
759 | end
760 |
761 | # Implements the GenServer `handle_cast` callback to handle client messages.
762 | #
763 | # This function sends a message to the Clojure peer that is run on a port, but does
764 | # not wait for the result. Responses from the peer will be discarded either in
765 | # the `wait_for_reply` loop called by `handle_call`, or in the `handle_info`
766 | # do-nothing catch-all function.
767 | @spec handle_cast(datomic_message, ProcessState.t) :: {:noreply, ProcessState.t}
768 | def handle_cast(message, state) do
769 | port = state.port
770 | send(port, {self, {:command, :erlang.term_to_binary(message)}})
771 | {:noreply, state}
772 | end
773 | end
774 |
--------------------------------------------------------------------------------
/lib/datomic_gen_server/datom.ex:
--------------------------------------------------------------------------------
1 | defmodule Datom do
2 | @moduledoc """
3 | A data structure for representing a Datomic datom.
4 | """
5 | defstruct e: 0, a: 0, v: [], tx: %{}, added: false
6 | @type t :: %Datom{e: integer, a: atom, v: term, tx: integer, added: boolean}
7 | end
8 |
--------------------------------------------------------------------------------
/lib/datomic_gen_server/datomic_transaction.ex:
--------------------------------------------------------------------------------
1 | defmodule DatomicTransaction do
2 | @moduledoc """
3 | A data structure for representing a Datomic transaction.
4 | """
5 | defstruct tx_id: 0,
6 | basis_t_before: 0,
7 | basis_t_after: 0,
8 | added_datoms: [],
9 | retracted_datoms: [],
10 | tempids: %{}
11 | @type t :: %DatomicTransaction{tx_id: integer,
12 | basis_t_before: integer,
13 | basis_t_after: integer,
14 | added_datoms: [Datom.t],
15 | retracted_datoms: [Datom.t],
16 | tempids: %{integer => integer}}
17 | end
18 |
--------------------------------------------------------------------------------
/lib/datomic_gen_server/db.ex:
--------------------------------------------------------------------------------
1 | defmodule DatomicGenServer.Db do
2 | @moduledoc """
3 | DatomicGenServer.Db is a module intended to facilitate the use of Elixir
4 | data structures instead of edn strings for communicating with Datomic. This
5 | module maps the DatomicGenServer interface functions in wrappers that accept
6 | and return Elixir data structures, and also provides slightly more syntactically
7 | pleasant equivalents for Datomic keys and structures that would otherwise
8 | need to be represented using a lot of punctuation that isn't required in Clojure.
9 |
10 | The hexdoc organizes the functions in this module alphabetically; here is a
11 | list by topic:
12 |
13 | ## Interface functions
14 |
15 | q(server_identifier, exdn, exdn_bindings, options \\ [])
16 | transact(server_identifier, exdn, options \\ [])
17 | pull(server_identifier, pattern_exdn, identifier_exdn, options \\ [])
18 | pull_many(server_identifier, pattern_exdn, identifiers_exdn, options \\ [])
19 | entity(server_identifier, exdn, attr_names \\ :all, options \\ [])
20 | load(server_identifier, data_path, options \\ [])
21 |
22 | ## Datomic Shortcuts
23 | ### Id/ident
24 |
25 | dbid(db_part)
26 | id
27 | ident
28 |
29 | ### Transaction creation
30 |
31 | add
32 | retract
33 | install_attribute
34 | alter_attribute
35 | tx_instant
36 |
37 | ### Value types
38 |
39 | value_type
40 | type_long
41 | type_keyword
42 | type_string
43 | type_boolean
44 | type_bigint
45 | type_float
46 | type_double
47 | type_bigdec
48 | type_ref
49 | type_instant
50 | type_uuid
51 | type_uri
52 | type_bytes
53 |
54 | ### Cardinalities
55 |
56 | cardinality
57 | cardinality_one
58 | cardinality_many
59 |
60 | ### Optional Schema Attributes
61 |
62 | doc
63 | unique
64 | unique_value
65 | unique_identity
66 | index
67 | fulltext
68 | is_component
69 | no_history
70 |
71 | ### Functions
72 |
73 | _fn
74 | fn_retract_entity
75 | fn_cas
76 |
77 | ### Common partions
78 |
79 | schema_partition
80 | transaction_partition
81 | user_partition
82 |
83 | ### Query placeholders
84 |
85 | q?(atom)
86 |
87 | ### Data sources
88 |
89 | implicit
90 | inS(placeholder_atom)
91 | db
92 | as_of(tx_id)
93 | since(tx_id)
94 | history
95 |
96 | ### Bindings and find specifications
97 |
98 | single_scalar
99 | blank
100 | collection_binding(placeholder_atom)
101 |
102 | ### Patterns for use in `pull`
103 |
104 | star
105 |
106 | ### Clauses
107 |
108 | _not(inner_clause)
109 | _not_join(binding_list, inner_clause_list)
110 | _or(inner_clauses)
111 | _or_join(binding_list, inner_clause_list)
112 | _and(inner_clauses)
113 | _pull({:symbol, entity_var}, pattern_clauses)
114 | _expr(function_symbol, remaining_expressions, bindings)
115 |
116 | ## Examples
117 |
118 | DatomicGenServer.start(
119 | "datomic:mem://test",
120 | true,
121 | [{:timeout, 20_000}, {:default_message_timeout, 20_000}, {:name, DatomicGenServer}]
122 | )
123 |
124 | data_to_add = [%{
125 | Db.id => Db.dbid(Db.schema_partition),
126 | Db.ident => :"person/name",
127 | Db.value_type => Db.type_string,
128 | Db.cardinality => Db.cardinality_one,
129 | Db.doc => "A person's name",
130 | Db.install_attribute => Db.schema_partition
131 | }]
132 | Db.transact(DatomicGenServer, data_to_add)
133 |
134 | # => {:ok, %DatomicGenServer.Db.DatomicTransaction{
135 | basis_t_before: 1001,
136 | basis_t_after: 1002,
137 | retracted_datoms: [],
138 | added_datoms: [
139 | %DatomicGenServer.Db.Datom{a: 50, added: true, e: 13194139534314, tx: 13194139534314,
140 | v: %Calendar.DateTime{abbr: "UTC", day: 15, hour: 3, min: 20, month: 2, sec: 1, std_off: 0,
141 | timezone: "Etc/UTC", usec: 746000, utc_off: 0, year: 2016}},
142 | %DatomicGenServer.Db.Datom{a: 41, added: true, e: 65, tx: 13194139534314, v: 35},
143 | %DatomicGenServer.Db.Datom{a: 62, added: true, e: 65, tx: 13194139534314, v: "A person's name"},
144 | %DatomicGenServer.Db.Datom{a: 10, added: true, e: 65, tx: 13194139534314, v: :"person/name"},
145 | %DatomicGenServer.Db.Datom{a: 40, added: true, e: 65, tx: 13194139534314, v: 23},
146 | %DatomicGenServer.Db.Datom{a: 13, added: true, e: 0, tx: 13194139534314, v: 65}],
147 | tempids: %{-9223367638809264706 => 65}}}
148 |
149 | query = [:find, Db.q?(:c), :where, [Db.q?(:c), Db.doc, "A person's name"]]
150 | Db.q(DatomicGenServer, query)
151 |
152 | #=> {:ok, #MapSet<['A']>} # ASCII representation of ID 65
153 |
154 | """
155 | @type query_option :: DatomicGenServer.send_option |
156 | {:response_converter, Exdn.converter} |
157 | {:edn_tag_handlers, [{atom, Exdn.handler}, ...]}
158 |
159 | @type datom_map :: %{:e => integer, :a => atom, :v => term, :tx => integer, :added => boolean}
160 | @type transaction_result :: %{:"db-before" => %{:"basis-t" => integer},
161 | :"db-after" => %{:"basis-t" => integer},
162 | :"tx-data" => [datom_map],
163 | :tempids => %{integer => integer}}
164 |
165 | ############################# INTERFACE FUNCTIONS ############################
166 | # TODO Rest API allows for limit and offset on query; this is implemented as
167 | # a call to take and drop on the result set, but we would probably prefer to
168 | # do this in Clojure before returning it back to Elixir.
169 |
170 | @doc """
171 | Queries a DatomicGenServer using a query formulated as an Elixir list.
172 | This query is translated to an edn string which is then passed to the Datomic
173 | `q` API function.
174 |
175 | The first parameter to this function is the pid or alias of the GenServer process;
176 | the second is the query.
177 |
178 | The optional third parameter is a list of bindings for the data sources in the
179 | query, passed to the `inputs` argument of the Datomic `q` function. **IMPORTANT:**
180 | These bindings are converted to edn strings which are read back in the Clojure
181 | peer and then passed to Clojure `eval`. Since any arbitrary Clojure forms that
182 | are passed in are evaluated, **you must be particularly careful that the bindings
183 | are sanitized** and that you are not passing anything in `{:list, [...]}`
184 | expressions that you don't control.
185 |
186 | Bindings may include `datomic_gen_server.peer/*db*` for the current database
187 | (or the `db` shortcut below), as well as the forms produced by `as_of` and
188 | `since` below. These accept transaction times or transaction IDs.
189 |
190 | The options keyword list for querying functions accepts as options
191 | `:response_converter` and :edn_tag_handlers, which are supplied to Exdn's
192 | `to_elixir` function. With `:response_converter` you may choose to supply a
193 | function to recursively walk down the edn data tree and convert the data to
194 | structs. Care must be taken when doing pattern matches that patterns don't
195 | accidentally match unexpected parts of the tree. (For example, if Datomic
196 | results are returned in a list of lists, a pattern that matches inner lists
197 | may also match outer ones.)
198 |
199 | Edn tag handlers allow you to customize what is done with edn tags in the
200 | response. The default handlers are generally sufficient for data returned
201 | from Datomic queries.
202 |
203 | The options keyword list may also include a `:client_timeout` option that
204 | specifies the milliseconds timeout passed to `GenServer.call`, and a
205 | `:message_timeout` option that specifies how long the GenServer should wait
206 | for a response before crashing (overriding the default value set in
207 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
208 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
209 | return an error but the server will not crash even if the response message is
210 | never returned from the Clojure peer.
211 |
212 | If the client timeout is not supplied, the value is taken from the configured
213 | value of `:timeout_on_call` in the application environment; if that is not
214 | configured, the GenServer default of 5000 is used.
215 |
216 | If the message timeout is not supplied, the default value supplied at startup
217 | with the option `:default_message_timeout` is used; if this was not specified,
218 | the configured value of `:message_wait_until_crash` in the application
219 | environment is used. If this is also omitted, a value of 5000 is used.
220 |
221 | ## Example
222 |
223 | query = [:find, Db.q?(:c), :where, [Db.q?(:c), Db.doc, "A person's name"]]
224 | Db.q(DatomicGenServer, query)
225 |
226 | #=> {:ok, #MapSet<['A']>} # ASCII representation of ID 65
227 |
228 | """
229 | @spec q(GenServer.server, [Exdn.exdn], [Exdn.exdn], [query_option]) :: {:ok, term} | {:error, term}
230 | def q(server_identifier, exdn, exdn_bindings \\ [], options \\ []) do
231 |
232 | {valid, invalid} = exdn_bindings |> Enum.map(&Exdn.from_elixir/1) |> Enum.partition(&is_ok/1)
233 | # TODO Get yourself a monad!
234 | if Enum.empty?(invalid) do
235 | bindings = Enum.map(valid, fn({:ok, binding}) -> binding end)
236 |
237 | case Exdn.from_elixir(exdn) do
238 | {:ok, edn_str} ->
239 | case DatomicGenServer.q(server_identifier, edn_str, bindings, options) do
240 | {:ok, reply_str} -> convert_query_response(reply_str, options)
241 | error -> error
242 | end
243 | parse_error -> parse_error
244 | end
245 | else
246 | {:error, invalid}
247 | end
248 | end
249 |
250 | defp is_ok({ :ok, _ }), do: true
251 | defp is_ok(_), do: false
252 |
253 | @doc """
254 | Issues a transaction against a DatomicGenServer using a transaction
255 | formulated as an Elixir list of maps. This transaction is translated to an edn
256 | string which is then passed to the Datomic `transact` API function.
257 |
258 | The first parameter to this function is the pid or alias of the GenServer process;
259 | the second is the transaction data.
260 |
261 | The options keyword list may also include a `:client_timeout` option that
262 | specifies the milliseconds timeout passed to `GenServer.call`, and a
263 | `:message_timeout` option that specifies how long the GenServer should wait
264 | for a response before crashing (overriding the default value set in
265 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
266 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
267 | return an error but the server will not crash even if the response message is
268 | never returned from the Clojure peer.
269 |
270 | If the client timeout is not supplied, the value is taken from the configured
271 | value of `:timeout_on_call` in the application environment; if that is not
272 | configured, the GenServer default of 5000 is used.
273 |
274 | If the message timeout is not supplied, the default value supplied at startup
275 | with the option `:default_message_timeout` is used; if this was not specified,
276 | the configured value of `:message_wait_until_crash` in the application
277 | environment is used. If this is also omitted, a value of 5000 is used.
278 |
279 | ## Example
280 |
281 | data_to_add = [%{
282 | Db.id => Db.dbid(Db.schema_partition),
283 | Db.ident => :"person/name",
284 | Db.value_type => Db.type_string,
285 | Db.cardinality => Db.cardinality_one,
286 | Db.doc => "A person's name",
287 | Db.install_attribute => Db.schema_partition
288 | }]
289 | Db.transact(DatomicGenServer, data_to_add)
290 |
291 | # => {:ok, %DatomicGenServer.Db.DatomicTransaction{
292 | basis_t_before: 1001,
293 | basis_t_after: 1002,
294 | retracted_datoms: [],
295 | added_datoms: [
296 | %DatomicGenServer.Db.Datom{a: 50, added: true, e: 13194139534314, tx: 13194139534314,
297 | v: %Calendar.DateTime{abbr: "UTC", day: 15, hour: 3, min: 20, month: 2, sec: 1, std_off: 0,
298 | timezone: "Etc/UTC", usec: 746000, utc_off: 0, year: 2016}},
299 | %DatomicGenServer.Db.Datom{a: 41, added: true, e: 65, tx: 13194139534314, v: 35},
300 | %DatomicGenServer.Db.Datom{a: 62, added: true, e: 65, tx: 13194139534314, v: "A person's name"},
301 | %DatomicGenServer.Db.Datom{a: 10, added: true, e: 65, tx: 13194139534314, v: :"person/name"},
302 | %DatomicGenServer.Db.Datom{a: 40, added: true, e: 65, tx: 13194139534314, v: 23},
303 | %DatomicGenServer.Db.Datom{a: 13, added: true, e: 0, tx: 13194139534314, v: 65}],
304 | tempids: %{-9223367638809264706 => 65}}}
305 |
306 | """
307 | @spec transact(GenServer.server, [Exdn.exdn], [DatomicGenServer.send_option]) :: {:ok, DatomicTransaction.t} | {:error, term}
308 | def transact(server_identifier, exdn, options \\ []) do
309 | case Exdn.from_elixir(exdn) do
310 | {:ok, edn_str} ->
311 | case DatomicGenServer.transact(server_identifier, edn_str, options) do
312 | {:ok, reply_str} ->
313 | case Exdn.to_elixir(reply_str) do
314 | {:ok, exdn_result} -> transaction(exdn_result)
315 | error -> error
316 | end
317 | error -> error
318 | end
319 | parse_error -> parse_error
320 | end
321 | end
322 |
323 | @doc """
324 | Issues a `pull` call to that is passed to the Datomic `pull` API function.
325 |
326 | The first parameter to this function is the pid or alias of the GenServer process;
327 | the second is a list representing the pattern that is to be passed to `pull`
328 | as its second parameter, and the third is an entity identifier: either an entity
329 | id, an ident, or a lookup ref.
330 |
331 | The options keyword list for querying functions accepts as options
332 | `:response_converter` and :edn_tag_handlers, which are supplied to Exdn's
333 | `to_elixir` function. With `:response_converter` you may choose to supply a
334 | function to recursively walk down the edn data tree and convert the data to
335 | structs. Care must be taken when doing pattern matches that patterns don't
336 | accidentally match unexpected parts of the tree. (For example, if Datomic
337 | results are returned in a list of lists, a pattern that matches inner lists
338 | may also match outer ones.)
339 |
340 | Edn tag handlers allow you to customize what is done with edn tags in the
341 | response. The default handlers are generally sufficient for data returned
342 | from Datomic queries.
343 |
344 | The options keyword list may also include a `:client_timeout` option that
345 | specifies the milliseconds timeout passed to `GenServer.call`, and a
346 | `:message_timeout` option that specifies how long the GenServer should wait
347 | for a response before crashing (overriding the default value set in
348 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
349 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
350 | return an error but the server will not crash even if the response message is
351 | never returned from the Clojure peer.
352 |
353 | If the client timeout is not supplied, the value is taken from the configured
354 | value of `:timeout_on_call` in the application environment; if that is not
355 | configured, the GenServer default of 5000 is used.
356 |
357 | If the message timeout is not supplied, the default value supplied at startup
358 | with the option `:default_message_timeout` is used; if this was not specified,
359 | the configured value of `:message_wait_until_crash` in the application
360 | environment is used. If this is also omitted, a value of 5000 is used.
361 |
362 | ## Example
363 |
364 | Db.pull(DatomicGenServer, Db.star, entity_id)
365 |
366 | # => {ok, %{ Db.ident => :"person/email",
367 | Db.value_type => Db.type_string,
368 | Db.cardinality => Db.cardinality_one,
369 | Db.doc => "A person's email"}}
370 |
371 | """
372 | @spec pull(GenServer.server, [Exdn.exdn], atom | integer, [query_option]) :: {:ok, term} | {:error, term}
373 | def pull(server_identifier, pattern_exdn, identifier_exdn, options \\ []) do
374 | case Exdn.from_elixir(pattern_exdn) do
375 | {:ok, pattern_str} ->
376 | case Exdn.from_elixir(identifier_exdn) do
377 | {:ok, identifier_str} ->
378 | case DatomicGenServer.pull(server_identifier, pattern_str, identifier_str, options) do
379 | {:ok, reply_str} -> convert_query_response(reply_str, options)
380 | error -> error
381 | end
382 | parse_error -> parse_error
383 | end
384 | parse_error -> parse_error
385 | end
386 | end
387 |
388 | @doc """
389 | Issues a `pull-many` call to that is passed to the Datomic `pull-many` API function.
390 |
391 | The first parameter to this function is the pid or alias of the GenServer process;
392 | the second is a list representing the pattern to be passed to `pull-many`
393 | as its second parameter, and the third is a list of entity identifiers, any
394 | of which may be either an entity id, an ident, or a lookup ref.
395 |
396 | The options keyword list for querying functions accepts as options
397 | `:response_converter` and :edn_tag_handlers, which are supplied to Exdn's
398 | `to_elixir` function. With `:response_converter` you may choose to supply a
399 | function to recursively walk down the edn data tree and convert the data to
400 | structs. Care must be taken when doing pattern matches that patterns don't
401 | accidentally match unexpected parts of the tree. (For example, if Datomic
402 | results are returned in a list of lists, a pattern that matches inner lists
403 | may also match outer ones.)
404 |
405 | Edn tag handlers allow you to customize what is done with edn tags in the
406 | response. The default handlers are generally sufficient for data returned
407 | from Datomic queries.
408 |
409 | The options keyword list may also include a `:client_timeout` option that
410 | specifies the milliseconds timeout passed to `GenServer.call`, and a
411 | `:message_timeout` option that specifies how long the GenServer should wait
412 | for a response before crashing (overriding the default value set in
413 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
414 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
415 | return an error but the server will not crash even if the response message is
416 | never returned from the Clojure peer.
417 |
418 | If the client timeout is not supplied, the value is taken from the configured
419 | value of `:timeout_on_call` in the application environment; if that is not
420 | configured, the GenServer default of 5000 is used.
421 |
422 | If the message timeout is not supplied, the default value supplied at startup
423 | with the option `:default_message_timeout` is used; if this was not specified,
424 | the configured value of `:message_wait_until_crash` in the application
425 | environment is used. If this is also omitted, a value of 5000 is used.
426 |
427 | ## Example
428 |
429 | Db.pull_many(DatomicGenServer, Db.star, [ id_1, id_2 ])
430 |
431 | # => {ok, [%{"db/cardinality": %{"db/id": 35}, "db/doc": "A person's state",
432 | "db/id": 63, "db/ident": :"person/state", "db/valueType": %{"db/id": 23}},
433 | %{"db/cardinality": %{"db/id": 35}, "db/doc": "A person's zip code",
434 | "db/id": 64, "db/ident": :"person/zip", "db/valueType": %{"db/id": 23}}]}
435 |
436 | """
437 | @spec pull_many(GenServer.server, [Exdn.exdn], [atom | integer], [query_option]) :: {:ok, term} | {:error, term}
438 | def pull_many(server_identifier, pattern_exdn, identifier_exdns, options \\ []) do
439 | case Exdn.from_elixir(pattern_exdn) do
440 | {:ok, pattern_str} ->
441 | case Exdn.from_elixir(identifier_exdns) do
442 | {:ok, identifiers_str} ->
443 | case DatomicGenServer.pull_many(server_identifier, pattern_str, identifiers_str, options) do
444 | {:ok, reply_str} -> convert_query_response(reply_str, options)
445 | error -> error
446 | end
447 | parse_error -> parse_error
448 | end
449 | parse_error -> parse_error
450 | end
451 | end
452 |
453 | @doc """
454 | Issues an `entity` call to that is passed to the Datomic `entity` API function.
455 |
456 | The first parameter to this function is the pid or alias of the GenServer process;
457 | the second is the data representing the parameter to be passed to `entity` as
458 | edn: either an entity id, an ident, or a lookup ref. The third parameter is
459 | a list of atoms that represent the keys of the attributes you wish to fetch,
460 | or `:all` if you want all the entity's attributes.
461 |
462 | The options keyword list for querying functions accepts as options
463 | `:response_converter` and :edn_tag_handlers, which are supplied to Exdn's
464 | `to_elixir` function. With `:response_converter` you may choose to supply a
465 | function to recursively walk down the edn data tree and convert the data to
466 | structs. Care must be taken when doing pattern matches that patterns don't
467 | accidentally match unexpected parts of the tree. (For example, if Datomic
468 | results are returned in a list of lists, a pattern that matches inner lists
469 | may also match outer ones.)
470 |
471 | Edn tag handlers allow you to customize what is done with edn tags in the
472 | response. The default handlers are generally sufficient for data returned
473 | from Datomic queries.
474 |
475 | The options keyword list may also include a `:client_timeout` option that
476 | specifies the milliseconds timeout passed to `GenServer.call`, and a
477 | `:message_timeout` option that specifies how long the GenServer should wait
478 | for a response before crashing (overriding the default value set in
479 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
480 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
481 | return an error but the server will not crash even if the response message is
482 | never returned from the Clojure peer.
483 |
484 | If the client timeout is not supplied, the value is taken from the configured
485 | value of `:timeout_on_call` in the application environment; if that is not
486 | configured, the GenServer default of 5000 is used.
487 |
488 | If the message timeout is not supplied, the default value supplied at startup
489 | with the option `:default_message_timeout` is used; if this was not specified,
490 | the configured value of `:message_wait_until_crash` in the application
491 | environment is used. If this is also omitted, a value of 5000 is used.
492 |
493 | ## Example
494 |
495 | Db.entity(DatomicGenServer, :"person/email")
496 |
497 | # => {ok, %{ Db.ident => :"person/email",
498 | Db.value_type => Db.type_string,
499 | Db.cardinality => Db.cardinality_one,
500 | Db.doc => "A person's email"}}
501 |
502 | """
503 | @spec entity(GenServer.server, [Exdn.exdn], [atom] | :all, [query_option]) :: {:ok, term} | {:error, term}
504 | def entity(server_identifier, exdn, attr_names \\ :all, options \\ []) do
505 | case Exdn.from_elixir(exdn) do
506 | {:ok, edn_str} ->
507 | case DatomicGenServer.entity(server_identifier, edn_str, attr_names, options) do
508 | {:ok, reply_str} -> convert_query_response(reply_str, options)
509 | error -> error
510 | end
511 | parse_error -> parse_error
512 | end
513 | end
514 |
515 | @doc """
516 | Issues a call to the Clojure net.phobot.datomic/seed library to load data into
517 | a database using data files in edn format. The database is not dropped,
518 | recreated, or migrated before loading.
519 |
520 | The first parameter to this function is the pid or alias of the GenServer process;
521 | the second is the path to the directory containing the data files. The data
522 | files will be processed in the sort order of their directory.
523 |
524 | Data is loaded in a single transaction. The return value of the function
525 | is the result of the Datomic `transact` API function call that executed the
526 | transaction, wrapped in a `DatomicTransaction` struct.
527 |
528 | Loading data does not use the Clojure Conformity library and is not idempotent.
529 |
530 | The options keyword list may also include a `:client_timeout` option that
531 | specifies the milliseconds timeout passed to `GenServer.call`, and a
532 | `:message_timeout` option that specifies how long the GenServer should wait
533 | for a response before crashing (overriding the default value set in
534 | `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the
535 | `:client_timeout` is shorter than the `:message_timeout` value, the call will
536 | return an error but the server will not crash even if the response message is
537 | never returned from the Clojure peer.
538 |
539 | If the client timeout is not supplied, the value is taken from the configured
540 | value of `:timeout_on_call` in the application environment; if that is not
541 | configured, the GenServer default of 5000 is used.
542 |
543 | If the message timeout is not supplied, the default value supplied at startup
544 | with the option `:default_message_timeout` is used; if this was not specified,
545 | the configured value of `:message_wait_until_crash` in the application
546 | environment is used. If this is also omitted, a value of 5000 is used.
547 |
548 | ## Example
549 | data_dir = Path.join [System.cwd(), "seed-data"]
550 | DatomicGenServer.load(DatomicGenServer, data_dir)
551 |
552 | => {:ok, "{:db-before {:basis-t 1000}, :db-after {:basis-t 1000}, ...
553 |
554 | """
555 | @spec load(GenServer.server, String.t, [DatomicGenServer.send_option]) :: {:ok, DatomicTransaction.t} | {:error, term}
556 | def load(server_identifier, data_path, options \\ []) do
557 | case DatomicGenServer.load(server_identifier, data_path, options) do
558 | {:ok, reply_str} ->
559 | case Exdn.to_elixir(reply_str) do
560 | {:ok, exdn_result} -> transaction(exdn_result)
561 | error -> error
562 | end
563 | error -> error
564 | end
565 | end
566 |
567 | @spec convert_query_response(String.t, [query_option]) :: {:ok, term} | {:error, term}
568 | defp convert_query_response(response_str, options) do
569 | converter = Keyword.get(options, :response_converter) || (fn x -> x end)
570 | handlers = Keyword.get(options, :edn_tag_handlers) || Exdn.standard_handlers
571 | Exdn.to_elixir(response_str, converter, handlers)
572 | end
573 |
574 | ############################# DATOMIC SHORTCUTS ############################
575 | # Id/ident
576 | @doc "Convenience function that generates `#db/id[ ]`"
577 | @spec dbid(atom) :: {:tag, :"db/id", [atom]}
578 | def dbid(db_part) do
579 | {:tag, :"db/id", [db_part]}
580 | end
581 |
582 | @doc "Convenience shortcut for `:\"db/id\"`"
583 | @spec id :: :"db/id"
584 | def id, do: :"db/id"
585 |
586 | @doc "Convenience shortcut for `:\"db/ident\"`"
587 | @spec ident :: :"db/ident"
588 | def ident, do: :"db/ident"
589 |
590 | # Transaction creation
591 | @doc "Convenience shortcut for `:\"db/add\"`"
592 | @spec add :: :"db/add"
593 | def add, do: :"db/add"
594 |
595 | @doc "Convenience shortcut for `:\"db/retract\"`"
596 | @spec retract :: :"db/retract"
597 | def retract, do: :"db/retract"
598 |
599 | @doc "Convenience shortcut for `:\"db.install/_attribute\"`"
600 | @spec install_attribute :: :"db.install/_attribute"
601 | def install_attribute, do: :"db.install/_attribute"
602 |
603 | @doc "Convenience shortcut for `:\"db.alter/attribute\"`"
604 | @spec alter_attribute :: :"db.alter/attribute"
605 | def alter_attribute, do: :"db.alter/attribute"
606 |
607 | @doc "Convenience shortcut for `:\"db/txInstant\"`"
608 | @spec tx_instant :: :"db/txInstant"
609 | def tx_instant, do: :"db/txInstant"
610 |
611 | # Value types
612 | @doc "Convenience shortcut for `:\"db/valueType\"`"
613 | @spec value_type :: :"db/valueType"
614 | def value_type, do: :"db/valueType"
615 |
616 | @doc "Convenience shortcut for `:\"db.type/long\"`"
617 | @spec type_long :: :"db.type/long"
618 | def type_long, do: :"db.type/long"
619 |
620 | @doc "Convenience shortcut for `:\"db.type/keyword\"`"
621 | @spec type_keyword :: :"db.type/keyword"
622 | def type_keyword, do: :"db.type/keyword"
623 |
624 | @doc "Convenience shortcut for `:\"db.type/string\"`"
625 | @spec type_string :: :"db.type/string"
626 | def type_string, do: :"db.type/string"
627 |
628 | @doc "Convenience shortcut for `:\"db.type/boolean\"`"
629 | @spec type_boolean :: :"db.type/boolean"
630 | def type_boolean, do: :"db.type/boolean"
631 |
632 | @doc "Convenience shortcut for `:\"db.type/bigint\"`"
633 | @spec type_bigint :: :"db.type/bigint"
634 | def type_bigint, do: :"db.type/bigint"
635 |
636 | @doc "Convenience shortcut for `:\"db.type/float\"`"
637 | @spec type_float :: :"db.type/float"
638 | def type_float, do: :"db.type/float"
639 |
640 | @doc "Convenience shortcut for `:\"db.type/double\"`"
641 | @spec type_double :: :"db.type/double"
642 | def type_double, do: :"db.type/double"
643 |
644 | @doc "Convenience shortcut for `:\"db.type/bigdec\"`"
645 | @spec type_bigdec :: :"db.type/bigdec"
646 | def type_bigdec, do: :"db.type/bigdec"
647 |
648 | @doc "Convenience shortcut for `:\"db.type/ref\"`"
649 | @spec type_ref :: :"db.type/ref"
650 | def type_ref, do: :"db.type/ref"
651 |
652 | @doc "Convenience shortcut for `:\"db.type/instant\"`"
653 | @spec type_instant :: :"db.type/instant"
654 | def type_instant, do: :"db.type/instant"
655 |
656 | @doc "Convenience shortcut for `:\"db.type/uuid\"`"
657 | @spec type_uuid :: :"db.type/uuid"
658 | def type_uuid, do: :"db.type/uuid"
659 |
660 | @doc "Convenience shortcut for `:\"db.type/uri\"`"
661 | @spec type_uri :: :"db.type/uri"
662 | def type_uri, do: :"db.type/uri"
663 |
664 | @doc "Convenience shortcut for `:\"db.type/bytes\"`"
665 | @spec type_bytes :: :"db.type/bytes"
666 | def type_bytes, do: :"db.type/bytes"
667 |
668 | # Cardinalities
669 | @doc "Convenience shortcut for `:\"db/cardinality\"`"
670 | @spec cardinality :: :"db/cardinality"
671 | def cardinality, do: :"db/cardinality"
672 |
673 | @doc "Convenience shortcut for `:\"db.cardinality/one\"`"
674 | @spec cardinality_one :: :"db.cardinality/one"
675 | def cardinality_one, do: :"db.cardinality/one"
676 |
677 | @doc "Convenience shortcut for `:\"db.cardinality/many\"`"
678 | @spec cardinality_many :: :"db.cardinality/many"
679 | def cardinality_many, do: :"db.cardinality/many"
680 |
681 | # Optional Schema Attributes
682 | @doc "Convenience shortcut for `:\"db/doc\"`"
683 | @spec doc :: :"db/doc"
684 | def doc, do: :"db/doc"
685 |
686 | @doc "Convenience shortcut for `:\"db/unique\"`"
687 | @spec unique :: :"db/unique"
688 | def unique, do: :"db/unique"
689 |
690 | @doc "Convenience shortcut for `:\"db.unique/value\"`"
691 | @spec unique_value :: :"db.unique/value"
692 | def unique_value, do: :"db.unique/value"
693 |
694 | @doc "Convenience shortcut for `:\"db.unique/identity\"`"
695 | @spec unique_identity :: :"db.unique/identity"
696 | def unique_identity, do: :"db.unique/identity"
697 |
698 | @doc "Convenience shortcut for `:\"db/index\"`"
699 | @spec index :: :"db/index"
700 | def index, do: :"db/index"
701 |
702 | @doc "Convenience shortcut for `:\"db/fulltext\"`"
703 | @spec fulltext :: :"db/fulltext"
704 | def fulltext, do: :"db/fulltext"
705 |
706 | @doc "Convenience shortcut for `:\"db/isComponent\"`"
707 | @spec is_component :: :"db/isComponent"
708 | def is_component, do: :"db/isComponent"
709 |
710 | @doc "Convenience shortcut for `:\"db/noHistory\"`"
711 | @spec no_history :: :"db/noHistory"
712 | def no_history, do: :"db/noHistory"
713 |
714 | # Functions
715 | @doc "Convenience shortcut for `:\"db/fn\"`"
716 | @spec _fn :: :"db/fn"
717 | def _fn, do: :"db/fn"
718 |
719 | @doc "Convenience shortcut for `:\"db.fn/retractEntity\"`"
720 | @spec fn_retract_entity :: :"db.fn/retractEntity"
721 | def fn_retract_entity, do: :"db.fn/retractEntity"
722 |
723 | @doc "Convenience shortcut for `:\"db.fn/cas\"`"
724 | @spec fn_cas :: :"db.fn/cas"
725 | def fn_cas, do: :"db.fn/cas"
726 |
727 | # Common partions
728 | @doc "Convenience shortcut for `:\"db.part/db\"`"
729 | @spec schema_partition :: :"db.part/db"
730 | def schema_partition, do: :"db.part/db"
731 |
732 | @doc "Convenience shortcut for `:\"db.part/tx\"`"
733 | @spec transaction_partition :: :"db.part/tx"
734 | def transaction_partition, do: :"db.part/tx"
735 |
736 | @doc "Convenience shortcut for `:\"db.part/user\"`"
737 | @spec user_partition :: :"db.part/user"
738 | def user_partition, do: :"db.part/user"
739 |
740 | # Query placeholders
741 | @doc """
742 | Convenience function to generate Datomic query placeholders--i.e.,
743 | symbols prefixed by a question mark.
744 |
745 | Accepts an atom as its argument, representing the symbol to which
746 | the question mark is to be prepended.
747 | """
748 | @spec q?(atom) :: {:symbol, atom }
749 | def q?(placeholder_atom) do
750 | variable_symbol = placeholder_atom |> to_string
751 | with_question_mark = "?" <> variable_symbol |> String.to_atom
752 | {:symbol, with_question_mark }
753 | end
754 |
755 | # Data sources
756 | @doc "Convenience shortcut for the implicit data source `$`"
757 | @spec implicit :: {:symbol, :"$"}
758 | def implicit, do: {:symbol, :"$"}
759 |
760 | @doc """
761 | Convenience function to generate Datomic data source specifications--i.e.,
762 | symbols prefixed by a dollar sign.
763 |
764 | Accepts an atom as its argument, representing the symbol to which the dollar
765 | sign is to be prepended.
766 | """
767 | @spec inS(atom) :: {:symbol, atom}
768 | def inS(placeholder_atom) do
769 | placeholder = placeholder_atom |> to_string
770 | with_dollar_sign = "$" <> placeholder |> String.to_atom
771 | {:symbol, with_dollar_sign }
772 | end
773 |
774 | @doc """
775 | Convenience shortcut to allow you to pass the current database in the data source
776 | bindings to a query or transaction.
777 |
778 | This gets bound to the value of the Clojure dynamic variable
779 | `datomic_gen_server.peer/*db*` inside the peer.
780 |
781 | This value is also used inside functions such as `as_of` which take the database
782 | and return a different database value based on transaction time etc.
783 |
784 | ## Example
785 |
786 | Db.q(DatomicGenServer,
787 | [:find, Db.q?(:c), :in, Db.implicit, Db.q?(:docstring),
788 | :where, [Db.q?(:c), Db.doc, Db.q?(:docstring)]],
789 | [Db.db, "A person's address"]
790 | )
791 |
792 | """
793 | @spec db :: {:symbol, :"datomic_gen_server.peer/*db*"}
794 | def db, do: {:symbol, :"datomic_gen_server.peer/*db*"}
795 |
796 | # TODO Allow dates
797 | @doc """
798 | Convenience function to allow passing a call to the Datomic `as-of` API function
799 | when creating data source bindings to a query or transaction.
800 |
801 | Accepts an integer as its argument, representing a transaction number or
802 | transaction ID. Dates are not yet supported.
803 |
804 | ## Example
805 |
806 | Db.q(DatomicGenServer,
807 | [:find, Db.q?(:c), :in, Db.implicit, Db.q?(:docstring),
808 | :where, [Db.q?(:c), Db.doc, Db.q?(:docstring)]],
809 | [Db.as_of(transaction.basis_t_after), "A person's address"]
810 | )
811 |
812 | """
813 | @spec as_of(integer) :: {:list, [Exdn.exdn] }
814 | def as_of(tx_id), do: clojure_expression(:"datomic.api/as-of", [db, tx_id])
815 |
816 | # TODO Allow dates
817 | @doc """
818 | Convenience function to allow passing a call to the Datomic `since` API function
819 | when creating data source bindings to a query or transaction.
820 |
821 | Accepts an integer as its argument, representing a transaction number or
822 | transaction ID. Dates are not yet supported.
823 |
824 | ## Example
825 |
826 | Db.q(DatomicGenServer,
827 | [:find, Db.q?(:c), :in, Db.implicit, Db.q?(:docstring),
828 | :where, [Db.q?(:c), Db.doc, Db.q?(:docstring)]],
829 | [Db.since(transaction.basis_t_after), "A person's address"]
830 | )
831 |
832 | """
833 | @spec since(integer) :: {:list, [Exdn.exdn] }
834 | def since(tx_id), do: clojure_expression(:"datomic.api/since", [db, tx_id])
835 |
836 | @doc """
837 | Convenience shortcut to allw passing a call to the Datomic `history` API function
838 | when creating data source bindings to a query or transaction.
839 |
840 | This will become of use when datoms and index-range calls and queries are
841 | supported.
842 | """
843 | @spec history :: {:list, [Exdn.exdn] }
844 | def history, do: clojure_expression(:"datomic.api/history", [db])
845 |
846 | # Bindings and find specifications
847 | @doc """
848 | Convenience shortcut for the single scalar find specification `.`
849 | as used, for example, in: `[:find ?e . :where [?e age 42] ]`
850 | """
851 | @spec single_scalar :: {:symbol, :"."}
852 | def single_scalar, do: {:symbol, :"."}
853 |
854 | @doc """
855 | Convenience shortcut for the blank binding `_` as used, for example, in:
856 | `[:find ?x :where [_ :likes ?x]]`
857 | """
858 | @spec blank :: {:symbol, :"_"}
859 | def blank, do: {:symbol, :"_"}
860 |
861 | @doc """
862 | Convenience shortcut for collection binding find specification `...`
863 | as used, for example, in: `[:find ?e in $ [?a ...] :where [?e age ?a] ]`
864 | """
865 | @spec collection_binding(atom) :: [{:symbol, atom},...]
866 | def collection_binding(placeholder_atom) do
867 | [ q?(placeholder_atom), {:symbol, :"..."} ]
868 | end
869 |
870 | # Patterns for use in `pull`
871 | @doc """
872 | Convenience shortcut for the star pattern used in `pull` (i.e., `[*]`).
873 | """
874 | @spec star :: [{:symbol, :"*"}, ...]
875 | def star do
876 | [ {:symbol, :"*"} ]
877 | end
878 |
879 | # Clauses
880 | @doc """
881 | Convenience shortcut for creating a `not` clause.
882 |
883 | In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this
884 | allows us not to have to sprinkle that syntax all over the place.
885 |
886 | ## Example
887 |
888 | Db._not([Db.q?(:eid), :"person/age" 13])
889 |
890 | sends the following to Datomic:
891 |
892 | (not [?eid :person/age 13])
893 |
894 | """
895 | @spec _not([Exdn.exdn]) :: {:list, [Exdn.exdn]}
896 | def _not(inner_clause), do: clojure_expression(:not, [inner_clause])
897 |
898 | @doc """
899 | Convenience shortcut for creating a `not-join` clause.
900 |
901 | In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this
902 | function allows us not to have to sprinkle that syntax all over the place.
903 |
904 | ## Example
905 |
906 | Db._not_join(
907 | [ Db.q?(:employer) ],
908 | [ [Db.q?(:employer), :"business/employee" Db.q?(:person)],
909 | [Db.q?(:employer), :"business/nonprofit" true]
910 | ]
911 | )
912 |
913 | sends the following to Datomic:
914 |
915 | (not-join [?employer]
916 | [?employer :business/employee ?person]
917 | [?employer :business/nonprofit true])
918 |
919 | """
920 | @spec _not_join([{:symbol, atom},...], [Exdn.exdn]) :: {:list, [Exdn.exdn]}
921 | def _not_join(binding_list, inner_clause_list) do
922 | clauses_including_bindings = [ binding_list | inner_clause_list ]
923 | clojure_expression(:"not-join", clauses_including_bindings)
924 | end
925 |
926 | @doc """
927 | Convenience shortcut for creating an `or` clause.
928 |
929 | In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this
930 | function allows us not to have to sprinkle that syntax all over the place.
931 |
932 | ## Example
933 |
934 | Db._or([
935 | [Db.q?(:org), :"business/nonprofit" true],
936 | [Db.q?(:org), :"organization/ngo" true]
937 | ])
938 |
939 | sends the following to Datomic:
940 |
941 | (or [?org :business/nonprofit true]
942 | [?org :organization/ngo true])
943 |
944 | """
945 | @spec _or([Exdn.exdn]) :: {:list, [Exdn.exdn]}
946 | def _or(inner_clauses), do: clojure_expression(:or, inner_clauses)
947 |
948 | @doc """
949 | Convenience shortcut for creating an `and` clause.
950 |
951 | Note that in Datomic, `and` clauses are only for use inside `or` clauses; `and`
952 | is the default otherwise.
953 |
954 | In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this
955 | function allows us not to have to sprinkle that syntax all over the place.
956 |
957 | ## Example
958 |
959 | Db._and([
960 | [Db.q?(:org), :"organization/ngo" true],
961 | [Db.q?(:org), :"organization/country" :"country/france"]
962 | ])
963 |
964 | sends the following to Datomic:
965 |
966 | (and [?org :organization/ngo true]
967 | [?org :organization/country :country/france])
968 |
969 | """
970 | @spec _and([Exdn.exdn]) :: {:list, [Exdn.exdn]}
971 | def _and(inner_clauses), do: clojure_expression(:and, inner_clauses)
972 |
973 | @doc """
974 | Convenience shortcut for creating an `or-join` clause.
975 |
976 | The first parameter to this function should be a list of bindings; the second
977 | the list of clauses.
978 |
979 | In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this
980 | function allows us not to have to sprinkle that syntax all over the place.
981 |
982 | ## Example
983 |
984 | Db._or_join(
985 | [ Db.q?(:person) ],
986 | [ Db._and([
987 | [Db.q?(:employer), :"business/employee", Db.q?(:person)],
988 | [Db.q?(:employer), :"business/nonprofit", true]
989 | ]),
990 | [Db.q?(:person), :"person/age", 65]
991 | ]
992 | )
993 |
994 | sends the following to Datomic:
995 |
996 | (or-join [?person]
997 | (and [?employer :business/employee ?person]
998 | [?employer :business/nonprofit true])
999 | [?person :person/age 65])
1000 |
1001 | """
1002 | @spec _or_join([{:symbol, atom},...], [Exdn.exdn]) :: {:list, [Exdn.exdn]}
1003 | def _or_join(binding_list, inner_clause_list) do
1004 | clauses_including_bindings = [ binding_list | inner_clause_list ]
1005 | clojure_expression(:"or-join", clauses_including_bindings)
1006 | end
1007 |
1008 | @doc """
1009 | Convenience shortcut for creating a Datomic pull expression for use in a :find
1010 | clause. Note that this is not the function to use if you want to call the
1011 | Datomic `pull` API function.
1012 |
1013 | In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this
1014 | function allows us not to have to sprinkle that syntax all over the place.
1015 |
1016 | ## Example
1017 |
1018 | Db._pull(Db.q?(:e), [:"person/address"])
1019 |
1020 | sends the following to Datomic:
1021 |
1022 | (pull ?e [:person/address])
1023 |
1024 | """
1025 | @spec _pull({:symbol, atom}, [Exdn.exdn]) :: {:list, [Exdn.exdn]}
1026 | def _pull({:symbol, entity_var}, pattern_clauses) do
1027 | clojure_expression(:pull, [entity_var, pattern_clauses])
1028 | end
1029 |
1030 | @doc """
1031 | Convenience shortcut for creating a Datomic expression clause. Note that this
1032 | is *not* the same as a simple Clojure expression inside parentheses.
1033 |
1034 | An expression clause allows arbitrary Java or Clojure functions to be used
1035 | inside of Datalog queries; they are either of form `[(predicate ...)]` or
1036 | `[(function ...) bindings]`. An expression clause is thus a Clojure list inside
1037 | a vector.
1038 |
1039 | In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this
1040 | function allows us not to have to sprinkle that syntax all over the place.
1041 | """
1042 | @spec _expr(atom, [Exdn.exdn], [Exdn.exdn]) :: [{:list, [Exdn.exdn]}]
1043 | def _expr(function_symbol, remaining_expressions, bindings \\ []) do
1044 | [ clojure_expression(function_symbol, remaining_expressions) | bindings ]
1045 | end
1046 |
1047 | # A Clojure expression is a list starting with a symbol
1048 | @spec clojure_expression(atom, [Exdn.exdn]) :: {:list, [Exdn.exdn]}
1049 | defp clojure_expression(symbol_atom, remaining_expressions) do
1050 | clause_list = [{:symbol, symbol_atom} | remaining_expressions ]
1051 | {:list, clause_list}
1052 | end
1053 |
1054 | ########## PRIVATE FUNCTIONS FOR STRUCTIFYING TRANSACTION RESPONSES #############
1055 | @spec transaction(transaction_result) :: {:ok, DatomicTransaction.t} | {:error, term}
1056 | defp transaction(transaction_result) do
1057 | try do
1058 | {added_datoms, retracted_datoms} = tx_data(transaction_result) |> to_datoms
1059 | transaction_struct = %DatomicTransaction{
1060 | tx_id: tx_data(transaction_result) |> transaction_id,
1061 | basis_t_before: basis_t_before(transaction_result),
1062 | basis_t_after: basis_t_after(transaction_result),
1063 | added_datoms: added_datoms,
1064 | retracted_datoms: retracted_datoms,
1065 | tempids: tempids(transaction_result)}
1066 | {:ok, transaction_struct}
1067 | rescue
1068 | e -> {:error, e}
1069 | end
1070 | end
1071 |
1072 | @spec basis_t_before(%{:"db-before" => %{:"basis-t" => integer}}) :: integer
1073 | defp basis_t_before(%{:"db-before" => %{:"basis-t" => before_t}}) do
1074 | before_t
1075 | end
1076 |
1077 | @spec basis_t_after(%{:"db-after" => %{:"basis-t" => integer}}) :: integer
1078 | defp basis_t_after(%{:"db-after" => %{:"basis-t" => after_t}}) do
1079 | after_t
1080 | end
1081 |
1082 | @spec tx_data(%{:"tx-data" => [datom_map]}) :: [datom_map]
1083 | defp tx_data(%{:"tx-data" => tx_data}) do
1084 | tx_data
1085 | end
1086 |
1087 | @spec to_datoms([datom_map]) :: {[Datom.t], [Datom.t]}
1088 | defp to_datoms(datom_maps) do
1089 | datom_maps
1090 | |> Enum.map(fn(datom_map) -> struct(Datom, datom_map) end)
1091 | |> Enum.partition(fn(datom) -> datom.added end)
1092 | end
1093 |
1094 | @spec transaction_id([datom_map]) :: integer
1095 | defp transaction_id(datom_maps) do
1096 | %{tx: id} = datom_maps |> hd
1097 | id
1098 | end
1099 |
1100 | @spec tempids(%{tempids: %{integer => integer}}) :: %{integer => integer}
1101 | defp tempids(%{tempids: tempids}) do
1102 | tempids
1103 | end
1104 | end
1105 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule DatomicGenServer.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :datomic_gen_server,
6 | # NOTE!! - This package explicitly lists the files to include - see below under package
7 | version: "2.2.5",
8 | elixir: "~> 1.2",
9 | description: """
10 | An Elixir GenServer that communicates with a Clojure Datomic peer running
11 | in the JVM, using clojure-erlastic.
12 | """,
13 | package: package,
14 | build_embedded: Mix.env == :prod,
15 | start_permanent: Mix.env == :prod,
16 | deps: deps,
17 | aliases: aliases]
18 | end
19 |
20 | # Configuration for the OTP application
21 | #
22 | # Type "mix help compile.app" for more information
23 | def application do
24 | [applications: [:logger, :calendar]] # Calendar needed for edn conversions of #inst
25 | end
26 |
27 | # Dependencies can be Hex packages:
28 | #
29 | # {:mydep, "~> 0.3.0"}
30 | #
31 | # Or git/path repositories:
32 | #
33 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
34 | #
35 | # Type "mix help deps" for more examples and options
36 | defp deps do
37 | [ {:exdn, "~> 2.1.2"},
38 | {:ex_doc, "~> 0.11", only: :dev},
39 | {:earmark, "~> 0.2", only: :dev},
40 | {:dialyxir, "~> 0.3", only: [:dev]}]
41 | end
42 |
43 | defp aliases do
44 | [ {:clean, [&clean_uberjars/1, "clean"]},
45 | {:compile, [&uberjar/1, "compile"]}
46 | ]
47 | end
48 |
49 | defp package do
50 | [maintainers: ["Paul Blair"],
51 | licenses: ["MIT"],
52 | links: %{github: "https://github.com/psfblair/datomic_gen_server"},
53 | files: [
54 | "lib/datomic_gen_server.ex",
55 | "lib/datomic_gen_server/db.ex",
56 | "lib/datomic_gen_server/datom.ex",
57 | "lib/datomic_gen_server/datomic_transaction.ex",
58 | "lib/datomic_gen_server/entity_map.ex",
59 | "priv/datomic_gen_server_peer/project.clj",
60 | "priv/datomic_gen_server_peer/src/datomic_gen_server/peer.clj",
61 | "mix.exs",
62 | "README.md"
63 | ]
64 | ]
65 | end
66 |
67 | defp clean_uberjars(_) do
68 | priv_dir = Path.join [System.cwd(), "priv"]
69 |
70 | uberjar_dir = Path.join [priv_dir, "datomic_gen_server_peer", "target" ]
71 | if File.exists?(uberjar_dir) do
72 | File.rm_rf uberjar_dir
73 | end
74 | end
75 |
76 | defp uberjar(_) do
77 | peer_dir = Path.join [System.cwd(), "priv", "datomic_gen_server_peer" ]
78 | if [peer_dir, "target", "peer*standalone.jar"] |> Path.join |> Path.wildcard |> Enum.empty? do
79 | pwd = System.cwd()
80 | File.cd(peer_dir)
81 | Mix.shell.cmd "lein uberjar"
82 | File.cd(pwd)
83 | end
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{"calendar": {:hex, :calendar, "0.12.4"},
2 | "certifi": {:hex, :certifi, "0.4.0"},
3 | "dialyxir": {:hex, :dialyxir, "0.3.3"},
4 | "earmark": {:hex, :earmark, "0.2.1"},
5 | "erldn": {:hex, :erldn, "1.0.5"},
6 | "ex_doc": {:hex, :ex_doc, "0.11.4"},
7 | "exdn": {:hex, :exdn, "2.1.2"},
8 | "hackney": {:hex, :hackney, "1.5.1"},
9 | "idna": {:hex, :idna, "1.1.0"},
10 | "metrics": {:hex, :metrics, "1.0.1"},
11 | "mimerl": {:hex, :mimerl, "1.0.2"},
12 | "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.6"},
13 | "tzdata": {:hex, :tzdata, "0.5.6"}}
14 |
--------------------------------------------------------------------------------
/priv/datomic_gen_server_peer/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | pom.xml
5 | pom.xml.asc
6 | *.jar
7 | *.class
8 | /.lein-*
9 | /.nrepl-port
10 | .hgignore
11 | .hg/
12 |
--------------------------------------------------------------------------------
/priv/datomic_gen_server_peer/project.clj:
--------------------------------------------------------------------------------
1 | (defproject datomic_gen_server/peer "2.2.5"
2 | :description "Datomic peer server in Clojure, accepting edn strings which will be sent by Elixir"
3 | :dependencies [[org.clojure/clojure "1.8.0"]
4 | [org.clojure/core.async "0.2.374"]
5 | [org.clojure/core.match "0.3.0-alpha4"]
6 | [clojure-erlastic "0.3.1"]
7 | [com.datomic/datomic-pro "0.9.5350"]
8 | [vvvvalvalval/datomock "0.1.0"]
9 | [net.phobot.datomic/seed "3.0.0"]
10 | ]
11 | :repositories {"my.datomic.com" {:url "https://my.datomic.com/repo"
12 | :creds :gpg}}
13 | :main datomic_gen_server.peer
14 | :aot :all)
15 |
16 | ; If you are using Datomic Pro, the repository above will be necessary and you'll
17 | ; need your credentials for it. You'll need to install gpg for this.
18 | ;
19 | ; First write your credentials map to ~/.lein/credentials.clj like so:
20 | ;
21 | ; {#"my\.datomic\.com" {:username "[USERNAME]"
22 | ; :password "[LICENSE_KEY]"}}
23 | ; Then encrypt it with gpg:
24 | ;
25 | ; $ gpg --default-recipient-self -e ~/.lein/credentials.clj > ~/.lein/credentials.clj.gpg
26 | ;
27 | ; Remember to delete the plaintext credentials.clj once you've encrypted it.
28 |
--------------------------------------------------------------------------------
/priv/datomic_gen_server_peer/src/datomic_gen_server/peer.clj:
--------------------------------------------------------------------------------
1 | (ns datomic_gen_server.peer
2 | (:gen-class)
3 | (:require [clojure.core.async :as async :refer [! edn-str (datomic/q database) prn-str)]
34 | result)
35 | (binding [*db* database]
36 | (let [result (->> binding-edn-list (map read-edn) (map eval) (apply datomic/q edn-str) prn-str)]
37 | result))))
38 |
39 | (defn- pull [database unquoted-pattern-edn-str identifier-edn-str]
40 | (let [pattern (read-edn unquoted-pattern-edn-str)
41 | identifier (read-edn identifier-edn-str)
42 | result (-> (datomic/pull database pattern identifier) prn-str)]
43 | result))
44 |
45 | (defn- pull-many [database unquoted-pattern-edn-str identifier-list-edn-str]
46 | (let [pattern (read-edn unquoted-pattern-edn-str)
47 | identifiers (read-edn identifier-list-edn-str)
48 | result (-> (datomic/pull-many database pattern identifiers) prn-str)]
49 | result))
50 |
51 | (defn- transact [connection edn-str]
52 | (let [completed-future (datomic/transact connection (read-edn edn-str))]
53 | @completed-future))
54 |
55 | (defn- with [database edn-str db-edn]
56 | (binding [*db* database]
57 | (let [as-of-db (-> db-edn read-edn eval)
58 | result (datomic/with as-of-db (read-edn edn-str))]
59 | result)))
60 |
61 | (defn- entity-attributes [attribute-names entity-map]
62 | (let [attrs (if (= :all attribute-names)
63 | (keys entity-map)
64 | attribute-names)
65 | selected (select-keys entity-map attrs)]
66 | (select-keys entity-map attrs)))
67 |
68 | (defn- entity [database edn-str attr-names]
69 | (->> (read-edn edn-str) (datomic/entity database) (entity-attributes attr-names) prn-str))
70 |
71 | (defn- serialize-datoms [datom]
72 | {:a (.a datom) :e (.e datom) :v (.v datom) :tx (.tx datom) :added (.added datom) })
73 |
74 | (defn- serialize-transaction-response [transaction-response]
75 | (let [db-before (transaction-response :db-before)
76 | before-basis-t (datomic/basis-t db-before)
77 | db-after (transaction-response :db-after)
78 | after-basis-t (datomic/basis-t db-after)
79 | tx-data (transaction-response :tx-data)]
80 | (prn-str
81 | { :db-before {:basis-t before-basis-t}
82 | :db-after {:basis-t after-basis-t}
83 | :tx-data (into [] (map serialize-datoms tx-data))
84 | :tempids (transaction-response :tempids)
85 | })))
86 |
87 | (defn- migrate [connection migration-path]
88 | ;; TODO Figure out a better way to handle logging
89 | (let [logger-fn (fn [& args] nil)]
90 | (run-migrations connection migration-path logger-fn)
91 | ; run-migrations calls doseq, which returns nil, so migrate does not supply a db-after.
92 | {:db-after (deref (datomic/sync connection) migration-timeout-ms nil)}))
93 |
94 | (defn- load-data [connection data-resource-path]
95 | ;; TODO Figure out a better way to handle logging
96 | (let [logger-fn (fn [& args] nil)
97 | completed-future (transact-seed-data connection data-resource-path logger-fn)]
98 | @completed-future))
99 |
100 | (defn- mock-connection [starting-point-db active-connection db-map db-key]
101 | (if (Boolean/getBoolean "datomic.mocking")
102 | (let [mocked-conn (datomock/mock-conn starting-point-db)
103 | updated-db-map (assoc db-map db-key starting-point-db)]
104 | {:db starting-point-db :connection mocked-conn :db-map updated-db-map})
105 | {:db starting-point-db :connection active-connection :db-map db-map}
106 | ))
107 |
108 | (defn- reset-to-mock [db-map db-key active-db active-connection]
109 | (if (Boolean/getBoolean "datomic.mocking")
110 | (let [starting-point-db (db-map db-key)
111 | mocked-conn (datomock/mock-conn starting-point-db)]
112 | {:db starting-point-db :connection mocked-conn :db-map db-map})
113 | {:db active-db :connection active-connection :db-map db-map}
114 | ))
115 |
116 | (defn- new-state [result active-db-value active-connection db-map]
117 | { :result result
118 | :active-db active-db-value
119 | :active-connection active-connection
120 | :db-snapshots db-map
121 | })
122 |
123 | ; Returns the result along with the state of the database, or nil if shut down.
124 | ; Results are vectors starting with :ok or :error so that they go back to Elixir
125 | ; as the corresponding tuples.
126 | (defn- process-message [message database connection db-map real-connection]
127 | (try
128 | (if (Boolean/getBoolean "debug.messages") (.println *err* (str "PEER RECEIVED: [" message "]")) :default)
129 | (match message
130 | ; IMPORTANT: RETURN MESSAGE ID IF IT IS AVAILABLE
131 | [:q message-id edn binding-edn]
132 | (let [response [:ok message-id (q database edn binding-edn)]]
133 | (new-state response database connection db-map))
134 | [:pull message-id unquoted-pattern-edn identifier-edn]
135 | (let [response [:ok message-id (pull database unquoted-pattern-edn identifier-edn)]]
136 | (new-state response database connection db-map))
137 | [:pull-many message-id unquoted-pattern-edn identifier-list-edn]
138 | (let [response [:ok message-id (pull-many database unquoted-pattern-edn identifier-list-edn)]]
139 | (new-state response database connection db-map))
140 | [:entity message-id edn attr-names]
141 | (let [response [:ok message-id (entity database edn attr-names)]]
142 | (new-state response database connection db-map))
143 | [:transact message-id edn]
144 | (let [transaction-result (transact connection edn)
145 | db-after (transaction-result :db-after)
146 | response [:ok message-id (serialize-transaction-response transaction-result)]]
147 | (new-state response db-after connection db-map))
148 | [:migrate message-id migration-path]
149 | (let [transaction-result (migrate connection migration-path)
150 | db-after (transaction-result :db-after)
151 | response [:ok message-id :migrated]]
152 | (new-state response db-after connection db-map))
153 | [:load message-id data-resource-path]
154 | (let [transaction-result (load-data connection data-resource-path)
155 | db-after (transaction-result :db-after)
156 | response [:ok message-id (serialize-transaction-response transaction-result)]]
157 | (new-state response db-after connection db-map))
158 | [:mock message-id db-key]
159 | (let [{new-db :db mock-connection :connection updated-db-map :db-map}
160 | (mock-connection database connection db-map db-key)
161 | response [:ok message-id db-key]]
162 | (new-state response new-db mock-connection updated-db-map))
163 | [:reset message-id db-key]
164 | (let [{new-db :db mock-connection :connection updated-db-map :db-map}
165 | (reset-to-mock db-map db-key database connection)
166 | response [:ok message-id db-key]]
167 | (new-state response new-db mock-connection updated-db-map))
168 | [:unmock message-id]
169 | (let [real-db (datomic/db real-connection)
170 | response [:ok message-id :unmocked]]
171 | (new-state response real-db real-connection db-map))
172 | [:ping]
173 | (let [response [:ok :ping]]
174 | (new-state response database connection db-map))
175 | [:stop] (do (datomic/shutdown false) nil) ; For testing from Clojure; does not release Clojure resources
176 | [:exit] (do (datomic/shutdown true) nil) ; Shuts down Clojure resources as part of JVM shutdown
177 | nil (do (datomic/shutdown true) nil)) ; Handle close of STDIN - parent is gone
178 | (catch Exception e
179 | (let [error-stack (string/join "\n" (map str (.getStackTrace e)))
180 | error-message (str "Error processing message:\n" message "\n" e "\n" error-stack)
181 | response [:error message error-message]]
182 | (if (Boolean/getBoolean "debug.messages") (.println *err* (str "PEER EXCEPTION: [" response "]")) :default)
183 | (new-state response database connection db-map)))))
184 |
185 | (defn- exit-loop [in out]
186 | (do
187 | (close! out)
188 | (close! in)
189 | :default))
190 |
191 | (defn start-server
192 | ([db-url in out] (start-server db-url in out false))
193 | ([db-url in out create?]
194 | (let [real-connection (connect db-url create?)]
195 | (! out (result :result))
205 | (recur (result :active-db)(result :active-connection)(result :db-snapshots)))
206 | (exit-loop in out))))))))) ; exit if we get a nil back from process-message.
207 |
208 | (defn -main [& args]
209 | (cond
210 | (empty? args) (System/exit 1)
211 | (> (count args) 2) (System/exit 1)
212 | :else
213 | (let [ port-config {:convention :elixir :str-detect :all}
214 | create-arg (second args)
215 | create? (and (some? create-arg) (.equalsIgnoreCase create-arg "true"))
216 | [in out] (clojure-erlastic.core/port-connection port-config)]
217 | (start-server (first args) in out create?))))
218 |
219 | ; TODO:
220 | ; 1. When we send the :mock message, we:
221 | ; a. Check system property to see if mocking is enabled #(Boolean/getBoolean "datomic.mocking")
222 | ; b. If mocking is not enabled, return
223 | ; the current db,
224 | ; the real connection as the "active" one
225 | ; the unchanged map of db values, and
226 | ; the real connection
227 | ; c. Otherwise, we create a new mock connection with the current db value
228 | ; d. Save the db value as a "starting-point" db in a map of dbs with the key passed by mock
229 | ; e. Return
230 | ; the "starting-point" db,
231 | ; the mocked connection as the "active" one
232 | ; the map of db values, and
233 | ; the real connection
234 | ;
235 | ; 2. When we send a new message we continue to use the current db and connection,
236 | ; but have to pass those same 4 things out of the loop.
237 | ;
238 | ; 3. When we send a :reset message, we
239 | ; a. Check system property to see if mocking is enabled #(Boolean/getBoolean "datomic.mocking")
240 | ; b. If mocking is not enabled, return the current db, the map, the real connection, and the real connection as the active one
241 | ; c. Use the key in the :reset message to get the db we are resetting to
242 | ; d. Create a new mocked connection from the db.
243 | ; e. Return
244 | ; the db that you reset to,
245 | ; the mocked connection as the "active" one
246 | ; the map of db values, and
247 | ; the real connection
248 | ;
249 | ; 4. When we send an :unmock message, we
250 | ; a. We use the real connection and get the db from it.
251 | ; b. Return
252 | ; the real db,
253 | ; the real connection as the "active" one
254 | ; the unchanged map of db values, and
255 | ; the real connection
256 |
--------------------------------------------------------------------------------
/priv/datomic_gen_server_peer/test/datomic_gen_server/peer_test.clj:
--------------------------------------------------------------------------------
1 | (ns datomic_gen_server.peer_test
2 | (:require [clojure.test :refer :all]
3 | [clojure.core.async :as async :refer [>!! !! in [:stop]) ; This does a datomic/shutdown but does not release clojure resources
22 | (!! in [:ping])
34 | (is (= [:ok :ping] (!! in [:q 1 "[:find ?c :where [?c :db/doc \"A person's name\"]]" '()])
40 | (is (= [:ok 1 "#{}\n"] (!! in [:transact 2 "[ {:db/id #db/id[:db.part/db]
43 | :db/ident :person/name
44 | :db/valueType :db.type/string
45 | :db/cardinality :db.cardinality/one
46 | :db/doc \"A person's name\"
47 | :db.install/_attribute :db.part/db}]"])
48 | (let [response ( ((edn-data :db-after) :basis-t) ((edn-data :db-before) :basis-t)))
59 |
60 | (is (= 6 (count (edn-data :tx-data))))
61 | (is (= java.lang.Long (type ((nth (edn-data :tx-data) 0) :e))))
62 | (is (= java.lang.Long (type ((nth (edn-data :tx-data) 0) :a))))
63 | (is (contains? (nth (edn-data :tx-data) 0) :v))
64 | (is (= java.lang.Long (type ((nth (edn-data :tx-data) 0) :tx))))
65 | (is (= true ((nth (edn-data :tx-data) 0) :added)))
66 | (is (= clojure.lang.PersistentArrayMap (type (edn-data :tempids))))
67 | )
68 |
69 | (>!! in [:q 3 "[:find ?c :where [?c :db/doc \"A person's name\"]]" '()])
70 | (let [query-result (!! in [:transact 4 "[ {:db/id #db/id[:db.part/db]
83 | :db/ident :person/email
84 | :db/valueType :db.type/string
85 | :db/cardinality :db.cardinality/one
86 | :db/doc \"A person's email\"
87 | :db.install/_attribute :db.part/db}]"])
88 | (let [edn-data (read-edn-response (!! in [:entity 5 (str entity-id) :all])
92 | (let [response (!! in [:entity 6 (str :person/email) [:db/valueType :db/doc]])
104 | (let [response-edn (read-edn-response (!! in [:entity 7 (str [:db/ident :person/email]) [:db/ident]])
109 | (let [response-edn (read-edn-response (!! in [:migrate 8 (.getPath migration-dir)]))
118 | (is (= [:ok 8 :migrated] (!! in [:q 9 "[:find ?c :where [?e :db/doc \"A category's name\"] [?e :db/ident ?c]]" '()])
120 | (let [query-result (!! in [:migrate 10 (.getPath migration-dir)]))
130 | (is (= [:ok 10 :migrated] (!! in [:load 11 (.getPath seed-dir)]))
134 | (let [transaction-result (!! in [:q 12 (str "[:find ?c :where "
139 | "[?e :category/name ?c] "
140 | "[?e :category/subcategories ?s] "
141 | "[?s :subcategory/name \"Soccer\"]]")
142 | '()])
143 | (let [query-result (!! in [:unknown 13 "[:find ?c :where [?c :db/doc \"A person's name\"]]"])
151 | (let [response (!! in [:q 14 "[:find ?c }" '()])
158 | (let [response (!! in [:transact 15 "[ {:db/id #db/id[:db.part/db]
165 | :db/ident :person/address
166 | :db/valueType :db.type/string
167 | :db/cardinality :db.cardinality/one
168 | :db/doc \"A person's address\"
169 | :db.install/_attribute :db.part/db}]"])
170 |
171 | (let [edn-data (read-edn-response (!! in [:q 16 query before-bindings])
181 |
182 | (let [query-result (read-edn-response (!! in [:q 17 query after-bindings])
186 |
187 | (let [query-result (read-edn-response (!! in [:migrate 18 (.getPath migration-dir)]))
195 | (!! in [:load 11 (.getPath seed-dir)]))
199 | ; (!! in [:mock 19 :freshly-migrated])
204 | (is (= [:ok 19 :freshly-migrated] (!! in [:q 20 "[:find ?c :where [?e :db/doc \"A category's name\"] [?e :db/ident ?c]]" '()])
207 | (is (= [:ok 20 "#{[:category/name]}\n"] (!! in [:q 21 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
210 | (is (= [:ok 21 "#{}\n"] (!! in [:transact 22 "[ { :db/id #db/id[:test/main]
213 | :category/name \"Sports\"}]"])
214 | (!! in [:q 23 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
217 | (let [query-result (read-edn-response (!! in [:reset 24 :freshly-migrated])
222 | (is (= [:ok 24 :freshly-migrated] (!! in [:q 25 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
225 | (is (= [:ok 25 "#{}\n"] (!! in [:transact 26 "[ { :db/id #db/id[:test/main]
228 | :category/name \"Sports\"}]"])
229 | (!! in [:q 27 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
232 | (let [query-result (read-edn-response (!! in [:unmock 28])
237 | (is (= [:ok 28 :unmocked] (!! in [:q 29 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
240 | (is (= [:ok 29 "#{}\n"] (!! in [:transact 30 "[ { :db/id #db/id[:test/main]
243 | :category/name \"Sports\"}]"])
244 | (!! in [:q 31 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
247 | (let [query-result (read-edn-response (!! in [:mock 32 :with-sports])
252 | (is (= [:ok 32 :with-sports] (!! in [:q 33 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
255 | (let [query-result (read-edn-response (!! in [:q 34 (str "[:find ?e :where [?e :category/name \"News\"]]") '()])
259 | (is (= [:ok 34 "#{}\n"] (!! in [:transact 35 "[ { :db/id #db/id[:test/main]
262 | :category/name \"News\"}]"])
263 | (!! in [:q 36 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
266 | (let [query-result (read-edn-response (!! in [:q 37 (str "[:find ?e :where [?e :category/name \"News\"]]") '()])
270 | (let [query-result (read-edn-response (!! in [:reset 38 :freshly-migrated])
275 | (is (= [:ok 38 :freshly-migrated] (!! in [:q 39 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
278 | (is (= [:ok 39 "#{}\n"] (!! in [:q 40 (str "[:find ?e :where [?e :category/name \"News\"]]") '()])
281 | (is (= [:ok 40 "#{}\n"] (!! in [:unmock 41])
285 | (is (= [:ok 41 :unmocked] (!! in [:q 42 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
288 | (let [query-result (read-edn-response (!! in [:q 43 (str "[:find ?e :where [?e :category/name \"News\"]]") '()])
292 | (is (= [:ok 43 "#{}\n"] (!! in [:migrate 44 (.getPath migration-dir)]))
302 | (!! in [:mock 45 :freshly-migrated])
305 | (is (= [:ok 45 :freshly-migrated] (!! in [:q 46 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
308 | (is (= [:ok 46 "#{}\n"] (!! in [:transact 47 "[ { :db/id #db/id[:test/main]
311 | :category/name \"Sports\"}]"])
312 | (!! in [:q 48 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
315 | (let [query-result (read-edn-response (!! in [:reset 49 :freshly-migrated])
319 | (is (= [:ok 49 :freshly-migrated] (!! in [:q 50 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
322 | (let [query-result (read-edn-response (!! in [:unmock 51])
326 | (is (= [:ok 51 :unmocked] (!! in [:q 52 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
329 | (let [query-result (read-edn-response (!! in [:migrate 53 (.getPath migration-dir)]))
338 | (!! in [:mock 54 :freshly-migrated])
342 | (is (= [:ok 54 :freshly-migrated] (!! in [:q 55 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
345 | (is (= [:ok 55 "#{}\n"] (!! in [:unmock 56])
348 | (is (= [:ok 56 :unmocked] (!! in [:load 57 (.getPath seed-dir)]))
352 | (!! in [:mock 58 :freshly-seeded])
355 | (is (= [:ok 58 :freshly-seeded] (!! in [:q 59 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
358 | (let [query-result (read-edn-response (!! in [:reset 60 :freshly-migrated])
362 | (is (= [:ok 60 :freshly-migrated] (!! in [:q 61 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
365 | (is (= [:ok 61 "#{}\n"] (!! in [:reset 62 :freshly-seeded])
368 | (is (= [:ok 62 :freshly-seeded] (!! in [:q 63 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
371 | (let [query-result (read-edn-response (!! in [:unmock 64])
375 | (is (= [:ok 64 :unmocked] (!! in [:q 65 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
378 | (let [query-result (read-edn-response (!! in [:transact 66 "[ { :db/id #db/id[:test/main]
382 | :category/name \"News\"}]"])
383 | (!! in [:q 67 (str "[:find ?e :where [?e :category/name \"News\"]]") '()])
386 | (let [query-result (read-edn-response (!! in [:reset 68 :freshly-migrated])
390 | (is (= [:ok 68 :freshly-migrated] (!! in [:q 69 (str "[:find ?e :where [?e :category/name \"News\"]]") '()])
393 | (is (= [:ok 69 "#{}\n"] (!! in [:q 70 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
396 | (is (= [:ok 70 "#{}\n"] (!! in [:reset 71 :freshly-seeded])
399 | (is (= [:ok 71 :freshly-seeded] (!! in [:q 72 (str "[:find ?e :where [?e :category/name \"News\"]]") '()])
402 | (is (= [:ok 72 "#{}\n"] (!! in [:q 73 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
405 | (let [query-result (read-edn-response (!! in [:unmock 74])
409 | (is (= [:ok 74 :unmocked] (!! in [:q 75 (str "[:find ?e :where [?e :category/name \"Sports\"]]") '()])
412 | (let [query-result (read-edn-response (!! in [:q 76 (str "[:find ?e :where [?e :category/name \"News\"]]") '()])
416 | (let [query-result (read-edn-response (!! in [:migrate 77 (.getPath migration-dir)])
427 | (is (= [:ok 77 :migrated] (!! in [:load 78 (.getPath seed-dir)]))
431 | (let [seed-result (!! in [:q 79 "[:find ?e :where [?e :category/name]]" '()])
435 | (let [query-result (> (read-edn-response query-result) concat flatten)]
437 | (is (= (query-result 0) :ok))
438 | (is (= (query-result 1) 79))
439 |
440 | (>!! in [:pull 80 "[*]" (str (first entity-ids))])
441 | (let [pull-result ( (-> (the-entity :category/subcategories) first :subcategory/name count) 0)))
448 |
449 | (>!! in [:pull-many 81 "[*]" (str (into () entity-ids))])
450 | (let [pull-result ( (-> (the-first-entity :category/subcategories) first :subcategory/name count) 0)))
457 | )
458 | ))
459 |
--------------------------------------------------------------------------------
/priv/datomic_gen_server_peer/test/resources/migrations/2016-02-18T16:30_create_test_schema.edn:
--------------------------------------------------------------------------------
1 | {:datomic-gen-server-peer/create-test-schema
2 | {:txes [
3 | [ ; Partition creation transaction
4 | { :db/id #db/id [:db.part/db]
5 | :db/ident :test/main
6 | :db.install/_partition :db.part/db}
7 | ]
8 | [ ; Category entity transaction
9 | { :db/id #db/id [:db.part/db]
10 | :db/ident :category/name
11 | :db/doc "A category's name"
12 | :db/valueType :db.type/string
13 | :db/cardinality :db.cardinality/one
14 | :db/unique :db.unique/identity
15 | :db.install/_attribute :db.part/db}
16 | ]
17 | [ ; Subcategory entity transaction
18 | { :db/id #db/id [:db.part/db]
19 | :db/ident :subcategory/name
20 | :db/doc "A subcategory's name"
21 | :db/valueType :db.type/string
22 | :db/cardinality :db.cardinality/one
23 | :db/unique :db.unique/identity
24 | :db.install/_attribute :db.part/db}
25 | ]
26 | [ ; Category/Subcategory relation transaction
27 | ; Datomic doesn't guarantee the target of the reference is of a specific type.
28 | ; Also, like all isComponent constraints, datomic will not enforce that a
29 | ; component entity only has one parent.
30 | { :db/id #db/id [:db.part/db]
31 | :db/ident :category/subcategories
32 | :db/doc "A category's subcategories"
33 | :db/valueType :db.type/ref
34 | :db/cardinality :db.cardinality/many
35 | :db/index true
36 | :db/isComponent true
37 | :db.install/_attribute :db.part/db}
38 | ]
39 | ]}
40 | }
41 |
--------------------------------------------------------------------------------
/priv/datomic_gen_server_peer/test/resources/seed/V001_categories_and_subcategories.edn:
--------------------------------------------------------------------------------
1 | [ ; Categories
2 | { :db/id #db/keyid[:test/main :category/sports]
3 | :category/name "Sports"}
4 |
5 | ; ; Subcategories
6 | { :db/id #db/keyid[:test/main :subcategory/baseball]
7 | :subcategory/name "Baseball"}
8 | { :db/id #db/keyid[:test/main :subcategory/football]
9 | :subcategory/name "Football"}
10 | { :db/id #db/keyid[:test/main :subcategory/soccer]
11 | :subcategory/name "Soccer"}
12 | { :db/id #db/keyid[:test/main :subcategory/basketball]
13 | :subcategory/name "Basketball"}
14 | { :db/id #db/keyid[:test/main :subcategory/hockey]
15 | :subcategory/name "Hockey"}
16 | { :db/id #db/keyid[:test/main :subcategory/golf]
17 | :subcategory/name "Golf"}
18 | { :db/id #db/keyid[:test/main :subcategory/tennis]
19 | :subcategory/name "Tennis"}
20 |
21 | ; Category/Subcategory relation
22 | { :db/id #db/lookupid :category/sports
23 | :category/subcategories #db/lookupids
24 | [ :subcategory/baseball
25 | :subcategory/football
26 | :subcategory/soccer
27 | :subcategory/basketball
28 | :subcategory/hockey
29 | :subcategory/golf
30 | :subcategory/tennis
31 | ]
32 | }
33 | ]
34 |
--------------------------------------------------------------------------------
/test/datomic_gen_server/db_load_data_test.exs:
--------------------------------------------------------------------------------
1 | defmodule DbSeedDataTest do
2 | use ExUnit.Case, async: false
3 | alias DatomicGenServer.Db, as: Db
4 |
5 | setup_all do
6 | # Need long timeouts to let the JVM start.
7 | DatomicGenServer.start_link(
8 | "datomic:mem://db-seed-data-test",
9 | true,
10 | [{:timeout, 20_000}, {:default_message_timeout, 20_000}, {:name, LoadDataServer}]
11 | )
12 | :ok
13 | end
14 |
15 | test "Can seed a database" do
16 | migration_dir = Path.join [System.cwd(), "priv", "datomic_gen_server_peer", "test", "resources", "migrations" ]
17 | {:ok, :migrated} = DatomicGenServer.migrate(LoadDataServer, migration_dir)
18 | data_dir = Path.join [System.cwd(), "priv", "datomic_gen_server_peer", "test", "resources", "seed" ]
19 | {:ok, transaction_result} = Db.load(LoadDataServer, data_dir)
20 |
21 | assert is_integer(transaction_result.basis_t_before)
22 | assert is_integer(transaction_result.basis_t_after)
23 | assert (transaction_result.basis_t_after - transaction_result.basis_t_before) > 0
24 |
25 | retracted_datoms = transaction_result.retracted_datoms
26 | assert 0 == Enum.count(retracted_datoms)
27 |
28 | added_datoms = transaction_result.added_datoms
29 | assert 16 == Enum.count(added_datoms)
30 |
31 | first_datom = hd(added_datoms)
32 | assert is_integer(first_datom.e)
33 | assert is_integer(first_datom.a)
34 | assert ! is_nil(first_datom.v)
35 | assert is_integer(first_datom.tx)
36 | assert first_datom.added
37 |
38 | tempids = transaction_result.tempids
39 | assert 0 == Enum.count(tempids)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/datomic_gen_server/db_test.exs:
--------------------------------------------------------------------------------
1 | defmodule DatomicGenServer.DbTest do
2 | use ExUnit.Case, async: false
3 | alias DatomicGenServer.Db, as: Db
4 |
5 | setup_all do
6 | # Need long timeouts to let the JVM start.
7 | DatomicGenServer.start_link(
8 | "datomic:mem://test",
9 | true,
10 | [{:timeout, 20_000}, {:default_message_timeout, 20_000}, {:name, DatomicGenServer}]
11 | )
12 | :ok
13 | end
14 |
15 | test "can issue multiple Datomic queries" do
16 | query = [
17 | :find, Db.q?(:c), :where,
18 | [Db.q?(:c), Db.doc, "Some docstring that shouldn't be in the database"]
19 | ]
20 | result = Db.q(DatomicGenServer, query)
21 |
22 | empty_set = MapSet.new()
23 | assert {:ok, empty_set} == result
24 |
25 | second_result = Db.q(DatomicGenServer, query)
26 | assert {:ok, empty_set} == second_result
27 | end
28 |
29 | test "can issue parameterized queries" do
30 | query = [
31 | :find, Db.q?(:c), :in, Db.implicit, Db.q?(:docstring), :where,
32 | [Db.q?(:c), Db.doc, Db.q?(:docstring)]
33 | ]
34 |
35 | result = Db.q(DatomicGenServer, query, [Db.db, "Some docstring that shouldn't be in the database"])
36 |
37 | empty_set = MapSet.new()
38 | assert {:ok, empty_set} == result
39 | end
40 |
41 | test "can execute Datomic transactions" do
42 | data_to_add = [%{
43 | Db.id => Db.dbid(Db.schema_partition),
44 | Db.ident => :"person/name",
45 | Db.value_type => Db.type_string,
46 | Db.cardinality => Db.cardinality_one,
47 | Db.doc => "A person's name",
48 | Db.install_attribute => Db.schema_partition
49 | }]
50 |
51 | {:ok, transaction_result} = Db.transact(DatomicGenServer, data_to_add)
52 | assert is_integer(transaction_result.basis_t_before)
53 | assert is_integer(transaction_result.basis_t_after)
54 | assert (transaction_result.basis_t_after - transaction_result.basis_t_before) > 0
55 |
56 | retracted_datoms = transaction_result.retracted_datoms
57 | assert 0 == Enum.count(retracted_datoms)
58 |
59 | added_datoms = transaction_result.added_datoms
60 | assert 6 == Enum.count(added_datoms)
61 |
62 | first_datom = hd(added_datoms)
63 | assert is_integer(first_datom.e)
64 | assert is_integer(first_datom.a)
65 | assert ! is_nil(first_datom.v)
66 | assert is_integer(first_datom.tx)
67 | assert first_datom.added
68 |
69 | tempids = transaction_result.tempids
70 | assert 1 == Enum.count(tempids)
71 | assert (Map.keys(tempids) |> hd |> is_integer)
72 | assert (Map.values(tempids) |> hd |> is_integer)
73 |
74 | query = [:find, Db.q?(:c), :where, [Db.q?(:c), Db.doc, "A person's name"]]
75 | {:ok, query_result} = Db.q(DatomicGenServer, query)
76 | assert 1 == Enum.count(query_result)
77 | end
78 |
79 | test "will evaluate Clojure forms passed as lists in bindings" do
80 | data_to_add = [%{
81 | Db.id => Db.dbid(Db.schema_partition),
82 | Db.ident => :"animal/species",
83 | Db.value_type => Db.type_string,
84 | Db.cardinality => Db.cardinality_one,
85 | Db.doc => "An animal's species",
86 | Db.install_attribute => Db.schema_partition
87 | }]
88 |
89 | {:ok, _} = Db.transact(DatomicGenServer, data_to_add)
90 |
91 | query = [:find, Db.q?(:e), :in, Db.implicit, Db.q?(:idmin), :where,
92 | [Db.q?(:e), Db.ident, :"animal/species"], Db._expr(:>, [Db.q?(:e), Db.q?(:idmin)]) ]
93 |
94 | {:ok, query_result} = Db.q(DatomicGenServer, query, [Db.db, {:list, [:-, 1, 1]} ])
95 |
96 | assert 1 == Enum.count(query_result)
97 | end
98 |
99 | test "does not evaluate escaped bindings" do
100 | data_to_add = [%{
101 | Db.id => Db.dbid(Db.schema_partition),
102 | Db.ident => :"animal/phylum",
103 | Db.value_type => Db.type_string,
104 | Db.cardinality => Db.cardinality_one,
105 | Db.doc => "An animal's phylum",
106 | Db.install_attribute => Db.schema_partition
107 | }]
108 |
109 | {:ok, _} = Db.transact(DatomicGenServer, data_to_add)
110 |
111 | query = [:find, Db.q?(:e), :in, Db.implicit, Db.q?(:idmin), :where,
112 | [Db.q?(:e), Db.ident, :"animal/phylum"], Db._expr(:>, [Db.q?(:e), Db.q?(:idmin)]) ]
113 |
114 | {:error, query_result} = Db.q(DatomicGenServer, query, [Db.db, "(- 1 1)"])
115 |
116 | assert Regex.match?(~r/java.lang.Long cannot be cast to java.lang.String/, query_result)
117 | end
118 |
119 | test "can issue as-of queries" do
120 | data_to_add = [%{
121 | Db.id => Db.dbid(Db.schema_partition),
122 | Db.ident => :"person/address",
123 | Db.value_type => Db.type_string,
124 | Db.cardinality => Db.cardinality_one,
125 | Db.doc => "A person's address",
126 | Db.install_attribute => Db.schema_partition
127 | }]
128 |
129 | {:ok, transaction_result} = Db.transact(DatomicGenServer, data_to_add)
130 |
131 | query = [
132 | :find, Db.q?(:c), :in, Db.implicit, Db.q?(:docstring), :where,
133 | [Db.q?(:c), Db.doc, Db.q?(:docstring)]
134 | ]
135 |
136 | {:ok, before_result} = Db.q(DatomicGenServer, query,
137 | [Db.as_of(transaction_result.basis_t_before), "A person's address"]
138 | )
139 | assert 0 == Enum.count(before_result)
140 |
141 | {:ok, after_result} = Db.q(DatomicGenServer, query,
142 | [Db.as_of(transaction_result.basis_t_after), "A person's address"]
143 | )
144 | assert 1 == Enum.count(after_result)
145 |
146 | tx_id_query = [
147 | :find, Db.q?(:tx), :where, [Db.blank, Db.doc, "A person's address", Db.q?(:tx)]
148 | ]
149 | {:ok, tx_id_response} = Db.q(DatomicGenServer, tx_id_query)
150 |
151 | # MapSet contains a list. When we do to_list it becomes a list of lists
152 | tx_id = tx_id_response |> MapSet.to_list |> hd |> hd
153 |
154 | {:ok, before_result2} = Db.q(DatomicGenServer, query,
155 | [Db.as_of(tx_id - 1), "A person's address"]
156 | )
157 | assert 0 == Enum.count(before_result2)
158 |
159 | {:ok, after_result2} = Db.q(DatomicGenServer, query,
160 | [Db.as_of(tx_id), "A person's address"]
161 | )
162 | assert 1 == Enum.count(after_result2)
163 | end
164 |
165 | test "can pull an entity" do
166 | data_to_add = [%{
167 | Db.id => Db.dbid(Db.schema_partition),
168 | Db.ident => :"person/city",
169 | Db.value_type => Db.type_string,
170 | Db.cardinality => Db.cardinality_one,
171 | Db.doc => "A person's city",
172 | Db.install_attribute => Db.schema_partition
173 | }]
174 | {:ok, _} = Db.transact(DatomicGenServer, data_to_add)
175 |
176 | entity_id_query = [
177 | :find, Db.q?(:e), :where, [Db.q?(:e), Db.ident, :"person/city"]
178 | ]
179 |
180 | {:ok, entity_id_result} = Db.q(DatomicGenServer, entity_id_query)
181 | #Set of 1 list of length 1, which contains a list of length 1 which is the entity ID
182 | assert Enum.count(entity_id_result) == 1
183 | entity_id = Enum.take(entity_id_result, 1) |> hd |> hd
184 |
185 | {:ok, pull_result} = Db.pull(DatomicGenServer, Db.star, entity_id)
186 | assert Map.get(pull_result, :"db/ident") == :"person/city"
187 | assert Map.get(pull_result, :"db/doc") == "A person's city"
188 | end
189 |
190 | test "can pull many entities" do
191 | data_to_add = [%{
192 | Db.id => Db.dbid(Db.schema_partition),
193 | Db.ident => :"person/state",
194 | Db.value_type => Db.type_string,
195 | Db.cardinality => Db.cardinality_one,
196 | Db.doc => "A person's state",
197 | Db.install_attribute => Db.schema_partition
198 | }]
199 | {:ok, _} = Db.transact(DatomicGenServer, data_to_add)
200 |
201 | data_to_add = [%{
202 | Db.id => Db.dbid(Db.schema_partition),
203 | Db.ident => :"person/zip",
204 | Db.value_type => Db.type_string,
205 | Db.cardinality => Db.cardinality_one,
206 | Db.doc => "A person's zip code",
207 | Db.install_attribute => Db.schema_partition
208 | }]
209 | {:ok, _} = Db.transact(DatomicGenServer, data_to_add)
210 |
211 | {:ok, [first_result, second_result]} = Db.pull_many(DatomicGenServer, Db.star, [ :"person/state", :"person/zip" ])
212 |
213 | if :"person/state" = Map.get(first_result, :"db/ident") do
214 | assert Map.get(first_result, :"db/doc") == "A person's state"
215 | assert Map.get(second_result, :"db/ident") == :"person/zip"
216 | assert Map.get(second_result, :"db/doc") == "A person's zip code"
217 | else
218 | assert Map.get(second_result, :"db/ident") == :"person/state"
219 | assert Map.get(second_result, :"db/doc") == "A person's state"
220 | assert Map.get(first_result, :"db/ident") == :"person/zip"
221 | assert Map.get(first_result, :"db/doc") == "A person's zip code"
222 | end
223 | end
224 |
225 | test "can ask for an entity" do
226 | data_to_add = [%{
227 | Db.id => Db.dbid(Db.schema_partition),
228 | Db.ident => :"person/email",
229 | Db.value_type => Db.type_string,
230 | Db.cardinality => Db.cardinality_one,
231 | Db.doc => "A person's email",
232 | Db.install_attribute => Db.schema_partition
233 | }]
234 | {:ok, _} = Db.transact(DatomicGenServer, data_to_add)
235 |
236 | all_attributes =
237 | %{ Db.ident => :"person/email",
238 | Db.value_type => Db.type_string,
239 | Db.cardinality => Db.cardinality_one,
240 | Db.doc => "A person's email"
241 | }
242 |
243 | {:ok, entity_result} = Db.entity(DatomicGenServer, :"person/email")
244 | assert all_attributes == entity_result
245 |
246 | {:ok, entity_result2} = Db.entity(DatomicGenServer, :"person/email", :all)
247 | assert all_attributes == entity_result2
248 |
249 | {:ok, entity_result3} = Db.entity(DatomicGenServer, :"person/email", [Db.value_type, Db.doc])
250 | assert %{Db.value_type => Db.type_string, Db.doc => "A person's email"} == entity_result3
251 |
252 | {:ok, entity_result4} = Db.entity(DatomicGenServer, [Db.ident, :"person/email"], [Db.cardinality])
253 | assert %{Db.cardinality => Db.cardinality_one} == entity_result4
254 | end
255 |
256 | defmodule TestQueryResponse do
257 | defstruct id: nil, identity: nil
258 | end
259 |
260 | test "Can convert a query response to a list of structs" do
261 | seed_data = [%{
262 | Db.id => Db.dbid(Db.schema_partition),
263 | Db.ident => :"business/name",
264 | Db.value_type => Db.type_string,
265 | Db.cardinality => Db.cardinality_one,
266 | Db.doc => "A business's name",
267 | Db.install_attribute => Db.schema_partition
268 | }]
269 | {:ok, _} = Db.transact(DatomicGenServer, seed_data)
270 |
271 | converter = fn(exdn) ->
272 | case exdn do
273 | [id, ident] -> %TestQueryResponse{id: id, identity: ident}
274 | _ -> exdn
275 | end
276 | end
277 |
278 | query = [:find, Db.q?(:e), Db.q?(:ident),
279 | :where, [Db.q?(:e), :"db/doc", "A business's name"],
280 | [Db.q?(:e), Db.ident, Db.q?(:ident)]]
281 | {:ok, query_result} = Db.q(DatomicGenServer, query, [], [{:response_converter, converter}])
282 | [%TestQueryResponse{id: entity_id, identity: :"business/name"}] = MapSet.to_list(query_result)
283 | assert is_integer(entity_id)
284 | end
285 |
286 | defmodule TestEntityResponse do
287 | defstruct "db/ident": nil, "db/valueType": nil, "db/cardinality": nil, "db/doc": nil
288 | end
289 |
290 | test "Can convert an entity response to a struct" do
291 | seed_data = [%{
292 | Db.id => Db.dbid(Db.schema_partition),
293 | Db.ident => :"business/email",
294 | Db.value_type => Db.type_string,
295 | Db.cardinality => Db.cardinality_one,
296 | Db.doc => "A business's email",
297 | Db.install_attribute => Db.schema_partition
298 | }]
299 |
300 | {:ok, _} = Db.transact(DatomicGenServer, seed_data)
301 |
302 | converter = fn(exdn) ->
303 | case exdn do
304 | %{"db/ident": _} -> struct(TestEntityResponse, exdn)
305 | _ -> exdn
306 | end
307 | end
308 |
309 | {:ok, entity_result} = Db.entity(DatomicGenServer, :"business/email", :all, [{:response_converter, converter}])
310 |
311 | %TestEntityResponse{
312 | "db/ident": :"business/email",
313 | "db/valueType": :"db.type/string",
314 | "db/cardinality": :"db.cardinality/one",
315 | "db/doc": "A business's email" } = entity_result
316 | end
317 |
318 | test "Handles garbled queries" do
319 | query = [:find, Db.q?(:c), :"wh?ere"]
320 | {:error, query_result} = Db.q(DatomicGenServer, query)
321 | assert Regex.match?(~r/Exception/, query_result)
322 | end
323 |
324 | test "Handles garbled transactions" do
325 | data_to_add = [%{ Db.id => :foobar, some: :other }]
326 | {:error, transaction_result} = Db.transact(DatomicGenServer, data_to_add)
327 | assert Regex.match?(~r/Exception/, transaction_result)
328 | end
329 |
330 | test "Creates an or-join clause" do
331 | clause = Db._or_join(
332 | [ Db.q?(:person) ],
333 | [ Db._and([
334 | [Db.q?(:employer), :"business/employee", Db.q?(:person)],
335 | [Db.q?(:employer), :"business/nonprofit", true]
336 | ]),
337 | [Db.q?(:person), :"person/age", 65]
338 | ])
339 |
340 | expected = {:list,
341 | [
342 | {:symbol, :"or-join"},
343 | [{:symbol, :"?person"}],
344 | {:list, [{:symbol, :"and"},
345 | [ {:symbol, :"?employer"}, :"business/employee", {:symbol, :"?person"}],
346 | [ {:symbol, :"?employer"}, :"business/nonprofit", true]]},
347 | [{:symbol, :"?person"}, :"person/age", 65]
348 | ]}
349 |
350 | assert expected == clause
351 | end
352 | test "Creates a Clojure expression inside a vector" do
353 | expression = Db._expr(:>, [Db.q?(:e), Db.q?(:idmin)])
354 | assert [{:list, [{:symbol, :>}, {:symbol, :"?e"}, {:symbol, :"?idmin"}]}] == expression
355 | end
356 |
357 | # TODO Add tests that use inS, history, bindings and find specifications,
358 | # and clauses.
359 |
360 | end
361 |
--------------------------------------------------------------------------------
/test/datomic_gen_server_test.exs:
--------------------------------------------------------------------------------
1 | defmodule DatomicGenServerTest do
2 | use ExUnit.Case, async: false
3 | alias DatomicGenServer.Db, as: Db
4 |
5 | setup_all do
6 | # Need long timeouts to let the JVM start.
7 | DatomicGenServer.start_link(
8 | "datomic:mem://test",
9 | true,
10 | [{:timeout, 20_000}, {:default_message_timeout, 20_000}, {:name, DatomicGenServer}]
11 | )
12 | :ok
13 | end
14 |
15 | test "can issue multiple Datomic queries" do
16 | query = "[:find ?c :where [?c :db/doc \"Some docstring that shouldn't be in the database\"]]"
17 | result = DatomicGenServer.q(DatomicGenServer, query)
18 | assert {:ok, "\#{}\n"} == result
19 | second_result = DatomicGenServer.q(DatomicGenServer, query)
20 | assert {:ok, "\#{}\n"} == second_result
21 | end
22 |
23 | test "can issue parameterized queries" do
24 | query = "[:find ?c :in $ ?docstring :where [?c :db/doc ?docstring]]"
25 | result = DatomicGenServer.q(DatomicGenServer, query,
26 | ["datomic_gen_server.peer/*db*", "\"Some docstring that shouldn't be in the database\""]
27 | )
28 | assert {:ok, "\#{}\n"} == result
29 | end
30 |
31 | test "can execute Datomic transactions" do
32 | data_to_add = """
33 | [ { :db/id #db/id[:db.part/db]
34 | :db/ident :person/name
35 | :db/valueType :db.type/string
36 | :db/cardinality :db.cardinality/one
37 | :db/doc \"A person's name\"
38 | :db.install/_attribute :db.part/db}]
39 | """
40 | {:ok, transaction_result} = DatomicGenServer.transact(DatomicGenServer, data_to_add)
41 | assert Regex.match?(~r/:db-before \{:basis-t \d+/, transaction_result)
42 | assert Regex.match?(~r/:db-after \{:basis-t \d+/, transaction_result)
43 | assert Regex.match?(~r/:tx-data \[\{:a \d+/, transaction_result)
44 | assert Regex.match?(~r/:tempids \{/, transaction_result)
45 |
46 | query = "[:find ?c :where [?c :db/doc \"A person's name\"]]"
47 | {:ok, result_str} = DatomicGenServer.q(DatomicGenServer, query)
48 | assert Regex.match?(~r/\#\{\[\d+\]\}\n/, result_str)
49 | end
50 |
51 | test "will evaluate unescaped bindings" do
52 | data_to_add = """
53 | [ { :db/id #db/id[:db.part/db]
54 | :db/ident :person/name
55 | :db/valueType :db.type/string
56 | :db/cardinality :db.cardinality/one
57 | :db/doc \"A person's name\"
58 | :db.install/_attribute :db.part/db}]
59 | """
60 | {:ok, _} = DatomicGenServer.transact(DatomicGenServer, data_to_add)
61 |
62 | query = "[:find ?e :in $ ?idmin :where [?e :db/ident :person/name][(> ?e ?idmin)]]"
63 | {:ok, result_str} = DatomicGenServer.q(DatomicGenServer, query,
64 | ["datomic_gen_server.peer/*db*", "(- 1 1)"])
65 |
66 | assert Regex.match?(~r/\#\{\[\d+\]\}\n/, result_str)
67 | end
68 |
69 | test "does not evaluate escaped bindings" do
70 | data_to_add = """
71 | [ { :db/id #db/id[:db.part/db]
72 | :db/ident :person/name
73 | :db/valueType :db.type/string
74 | :db/cardinality :db.cardinality/one
75 | :db/doc \"A person's name\"
76 | :db.install/_attribute :db.part/db}]
77 | """
78 | {:ok, _} = DatomicGenServer.transact(DatomicGenServer, data_to_add)
79 |
80 | query = "[:find ?e :in $ ?idmin :where [?e :db/ident :person/name][(> ?e ?idmin)]]"
81 | {:error, result_str} = DatomicGenServer.q(DatomicGenServer, query,
82 | ["datomic_gen_server.peer/*db*", "\"(- 1 1)\""])
83 |
84 | assert Regex.match?(~r/java.lang.Long cannot be cast to java.lang.String/, result_str)
85 | end
86 |
87 | test "can issue as-of queries" do
88 | data_to_add = [%{
89 | Db.id => Db.dbid(Db.schema_partition),
90 | Db.ident => :"person/address",
91 | Db.value_type => Db.type_string,
92 | Db.cardinality => Db.cardinality_one,
93 | Db.doc => "A person's address",
94 | Db.install_attribute => Db.schema_partition
95 | }]
96 |
97 | # Get an interpreted struct so we can use the tx time.
98 | {:ok, transaction_result} = Db.transact(DatomicGenServer, data_to_add)
99 |
100 | query = "[:find ?ident :in $ ?docstring :where [?e :db/doc ?docstring][?e :db/ident ?ident]]"
101 |
102 | before_result = DatomicGenServer.q(DatomicGenServer, query,
103 | ["(datomic.api/as-of datomic_gen_server.peer/*db* #{transaction_result.basis_t_before})", "\"A person's address\""]
104 | )
105 | assert {:ok, "\#{}\n"} == before_result
106 |
107 | after_result = DatomicGenServer.q(DatomicGenServer, query,
108 | ["(datomic.api/as-of datomic_gen_server.peer/*db* #{transaction_result.basis_t_after})", "\"A person's address\""]
109 | )
110 | assert {:ok, "\#{[:person/address]}\n"} == after_result
111 | end
112 |
113 | test "can handle multiple messages from different processes" do
114 | Enum.each(1..10, (fn(index) -> spawn (fn () ->
115 | data_to_add = """
116 | [ { :db/id #db/id[:db.part/db]
117 | :db/ident :some/field#{index}
118 | :db/valueType :db.type/string
119 | :db/cardinality :db.cardinality/one
120 | :db/doc \"Field #{index}\"
121 | :db.install/_attribute :db.part/db}]
122 | """
123 | :timer.sleep(:random.uniform(3)) # Mix up the order of sending the messages
124 | {:ok, transaction_result} = DatomicGenServer.transact(DatomicGenServer, data_to_add)
125 | assert Regex.match?(~r/:db-before \{:basis-t \d+/, transaction_result)
126 | assert Regex.match?(~r/Field #{index}/, transaction_result)
127 | end) end))
128 | end
129 |
130 | test "can pull an entity" do
131 | data_to_add = """
132 | [ {:db/id #db/id[:db.part/db]
133 | :db/ident :person/city
134 | :db/valueType :db.type/string
135 | :db/cardinality :db.cardinality/one
136 | :db/doc \"A person's city\"
137 | :db.install/_attribute :db.part/db} ]
138 | """
139 | {:ok, _} = DatomicGenServer.transact(DatomicGenServer, data_to_add)
140 |
141 | {:ok, entity_id_result} = DatomicGenServer.q(DatomicGenServer, "[:find ?e :where [?e :db/ident :person/city]]")
142 | assert Regex.match?(~r/\#\{\[\d+\]\}/, entity_id_result)
143 |
144 | entity_id = Regex.replace(~r/\#\{\[(\d+)\]\}/, entity_id_result, "\\1")
145 |
146 | {:ok, pull_result} = DatomicGenServer.pull(DatomicGenServer, "[*]", "#{entity_id}")
147 | assert Regex.match?(~r/:db\/ident :person\/city/, pull_result)
148 | assert Regex.match?(~r/:db\/doc "A person's city"/, pull_result)
149 | end
150 |
151 | test "can pull many entities" do
152 | data_to_add = """
153 | [ {:db/id #db/id[:db.part/db]
154 | :db/ident :person/state
155 | :db/valueType :db.type/string
156 | :db/cardinality :db.cardinality/one
157 | :db/doc \"A person's state\"
158 | :db.install/_attribute :db.part/db} ]
159 | """
160 | {:ok, _} = DatomicGenServer.transact(DatomicGenServer, data_to_add)
161 | {:ok, entity_id_result} = DatomicGenServer.q(DatomicGenServer, "[:find ?e :where [?e :db/ident :person/state]]")
162 | entity_id_1 = Regex.replace(~r/\#\{\[(\d+)\]\}/, entity_id_result, "\\1")
163 |
164 | data_to_add = """
165 | [ {:db/id #db/id[:db.part/db]
166 | :db/ident :person/zip
167 | :db/valueType :db.type/string
168 | :db/cardinality :db.cardinality/one
169 | :db/doc \"A person's zip code\"
170 | :db.install/_attribute :db.part/db} ]
171 | """
172 | {:ok, _} = DatomicGenServer.transact(DatomicGenServer, data_to_add)
173 |
174 | {:ok, pull_result} = DatomicGenServer.pull_many(DatomicGenServer, "[*]", "[#{entity_id_1} :person/zip]")
175 | assert Regex.match?(~r/:db\/ident :person\/state/, pull_result)
176 | assert Regex.match?(~r/:db\/doc "A person's state"/, pull_result)
177 | assert Regex.match?(~r/:db\/ident :person\/zip/, pull_result)
178 | assert Regex.match?(~r/:db\/doc "A person's zip code"/, pull_result)
179 | end
180 |
181 | test "can ask for an entity" do
182 | data_to_add = """
183 | [ {:db/id #db/id[:db.part/db]
184 | :db/ident :person/email
185 | :db/valueType :db.type/string
186 | :db/cardinality :db.cardinality/one
187 | :db/doc \"A person's email\"
188 | :db.install/_attribute :db.part/db} ]
189 | """
190 | {:ok, _} = DatomicGenServer.transact(DatomicGenServer, data_to_add)
191 |
192 | all_attributes = "{:db/ident :person/email, :db/valueType :db.type/string, :db/cardinality :db.cardinality/one, :db/doc \"A person's email\"}\n"
193 | {:ok, entity_result} = DatomicGenServer.entity(DatomicGenServer, ":person/email")
194 | assert all_attributes == entity_result
195 |
196 | {:ok, entity_result2} = DatomicGenServer.entity(DatomicGenServer, ":person/email", :all)
197 | assert all_attributes == entity_result2
198 |
199 | {:ok, entity_result3} = DatomicGenServer.entity(DatomicGenServer, ":person/email", [:"db/valueType", :"db/doc"])
200 | assert "{:db/valueType :db.type/string, :db/doc \"A person's email\"}\n" == entity_result3
201 |
202 | {:ok, entity_result4} = DatomicGenServer.entity(DatomicGenServer, "[:db/ident :person/email]", [:"db/cardinality"])
203 | assert "{:db/cardinality :db.cardinality/one}\n" == entity_result4
204 | end
205 |
206 | test "Handles garbled queries" do
207 | query = "[:find ?c :wh?ere]"
208 | {:error, query_result} = DatomicGenServer.q(DatomicGenServer, query)
209 | assert Regex.match?(~r/Exception/, query_result)
210 | end
211 |
212 | test "Handles garbled transactions" do
213 | data_to_add = """
214 | [ { :db/ii #db/foo[:db.part/db]
215 | """
216 | {:error, transaction_result} = DatomicGenServer.transact(DatomicGenServer, data_to_add)
217 | assert Regex.match?(~r/Exception/, transaction_result)
218 | end
219 | end
220 |
--------------------------------------------------------------------------------
/test/load_data_test.exs:
--------------------------------------------------------------------------------
1 | defmodule LoadDataTest do
2 | use ExUnit.Case, async: false
3 |
4 | setup_all do
5 | # Need long timeouts to let the JVM start.
6 | DatomicGenServer.start_link(
7 | "datomic:mem://seed-data-test",
8 | true,
9 | [{:timeout, 20_000}, {:default_message_timeout, 20_000}, {:name, LoadDataServer}]
10 | )
11 | :ok
12 | end
13 |
14 | test "Can load data files into a database" do
15 | migration_dir = Path.join [System.cwd(), "priv", "datomic_gen_server_peer", "test", "resources", "migrations" ]
16 | {:ok, :migrated} = DatomicGenServer.migrate(LoadDataServer, migration_dir)
17 | data_dir = Path.join [System.cwd(), "priv", "datomic_gen_server_peer", "test", "resources", "seed" ]
18 | {:ok, _} = DatomicGenServer.load(LoadDataServer, data_dir)
19 |
20 | query = "[:find ?c :where " <>
21 | "[?e :category/name ?c] " <>
22 | "[?e :category/subcategories ?s] " <>
23 | "[?s :subcategory/name \"Soccer\"]]"
24 | {:ok, result_str} = DatomicGenServer.q(LoadDataServer, query)
25 | assert "\#{[\"Sports\"]}\n" == result_str
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/test/migration_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MigrationTest do
2 | use ExUnit.Case, async: false
3 |
4 | setup_all do
5 | # Need long timeouts to let the JVM start.
6 | DatomicGenServer.start_link(
7 | "datomic:mem://migration-test",
8 | true,
9 | [{:timeout, 20_000}, {:default_message_timeout, 20_000}, {:name, MigrationTestServer}]
10 | )
11 | :ok
12 | end
13 |
14 | test "Can migrate a database" do
15 | migration_dir = Path.join [System.cwd(), "priv", "datomic_gen_server_peer", "test", "resources", "migrations" ]
16 | {:ok, :migrated} = DatomicGenServer.migrate(MigrationTestServer, migration_dir)
17 | query = "[:find ?c :where [?e :db/doc \"A category's name\"] [?e :db/ident ?c]]"
18 | {:ok, result_str} = DatomicGenServer.q(MigrationTestServer, query)
19 | assert "\#{[:category/name]}\n" == result_str
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test/mocking_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MockingTest do
2 | use ExUnit.Case, async: false
3 |
4 | setup_all do
5 | Application.put_env(:datomic_gen_server, :allow_datomic_mocking?, true)
6 | # Need long timeouts to let the JVM start.
7 | DatomicGenServer.start_link(
8 | "datomic:mem://mocking-test",
9 | true,
10 | [{:timeout, 20_000}, {:default_message_timeout, 20_000}, {:name, MockingTestServer}]
11 | )
12 | :ok
13 | end
14 |
15 | test "Can mock a database" do
16 | migration_dir = Path.join [System.cwd(), "priv", "datomic_gen_server_peer", "test", "resources", "migrations" ]
17 | {:ok, :migrated} = DatomicGenServer.migrate(MockingTestServer, migration_dir)
18 |
19 | query = "[:find ?c :where [?e :db/doc \"A category's name\"] [?e :db/ident ?c]]"
20 | {:ok, result_str} = DatomicGenServer.q(MockingTestServer, query)
21 | assert "\#{[:category/name]}\n" == result_str
22 |
23 | query = "[:find ?e :where [?e :category/name \"Sports\"]]"
24 | {:ok, result_str} = DatomicGenServer.q(MockingTestServer, query)
25 | assert "\#{}\n" == result_str
26 |
27 | {:ok, :"just-migrated"} = DatomicGenServer.mock(MockingTestServer, :"just-migrated")
28 |
29 | data_dir = Path.join [System.cwd(), "priv", "datomic_gen_server_peer", "test", "resources", "seed" ]
30 | {:ok, _} = DatomicGenServer.load(MockingTestServer, data_dir)
31 |
32 | query = "[:find ?e :where [?e :category/name \"Sports\"]]"
33 | {:ok, result_str} = DatomicGenServer.q(MockingTestServer, query)
34 | assert Regex.match?(~r/\#\{\[\d+\]\}\n/, result_str)
35 |
36 | {:ok, :seeded} = DatomicGenServer.mock(MockingTestServer, :seeded)
37 |
38 | {:ok, :"just-migrated"} = DatomicGenServer.reset(MockingTestServer, :"just-migrated")
39 |
40 | query = "[:find ?e :where [?e :category/name \"Sports\"]]"
41 | {:ok, result_str} = DatomicGenServer.q(MockingTestServer, query)
42 | assert "\#{}\n" == result_str
43 |
44 | {:ok, :unmocked} = DatomicGenServer.unmock(MockingTestServer)
45 |
46 | data_to_add = "[ { :db/id #db/id[:test/main] :category/name \"News\"} ]"
47 | {:ok, _} = DatomicGenServer.transact(MockingTestServer, data_to_add)
48 |
49 | query = "[:find ?e :where [?e :category/name \"Sports\"]]"
50 | {:ok, result_str} = DatomicGenServer.q(MockingTestServer, query)
51 | assert "\#{}\n" == result_str
52 |
53 | query = "[:find ?e :where [?e :category/name \"News\"]]"
54 | {:ok, result_str} = DatomicGenServer.q(MockingTestServer, query)
55 | assert Regex.match?(~r/\#\{\[\d+\]\}\n/, result_str)
56 |
57 | {:ok, :"just-migrated"} = DatomicGenServer.reset(MockingTestServer, :"just-migrated")
58 |
59 | query = "[:find ?e :where [?e :category/name \"Sports\"]]"
60 | {:ok, result_str} = DatomicGenServer.q(MockingTestServer, query)
61 | assert "\#{}\n" == result_str
62 |
63 | query = "[:find ?e :where [?e :category/name \"News\"]]"
64 | {:ok, result_str} = DatomicGenServer.q(MockingTestServer, query)
65 | assert "\#{}\n" == result_str
66 |
67 | {:ok, :seeded} = DatomicGenServer.reset(MockingTestServer, :seeded)
68 |
69 | query = "[:find ?e :where [?e :category/name \"Sports\"]]"
70 | {:ok, result_str} = DatomicGenServer.q(MockingTestServer, query)
71 | assert Regex.match?(~r/\#\{\[\d+\]\}\n/, result_str)
72 |
73 | query = "[:find ?e :where [?e :category/name \"News\"]]"
74 | {:ok, result_str} = DatomicGenServer.q(MockingTestServer, query)
75 | assert "\#{}\n" == result_str
76 |
77 | Application.put_env(:datomic_gen_server, :allow_datomic_mocking, false)
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/test/registration_test.exs:
--------------------------------------------------------------------------------
1 | defmodule RegistrationTest do
2 | use ExUnit.Case, async: false
3 |
4 | # Need long timeouts to let the JVM start.
5 | test "can register global name" do
6 | DatomicGenServer.start_link(
7 | "datomic:mem://test",
8 | true,
9 | [{:timeout, 20_000}, {:default_message_timeout, 20_000}, {:name, {:global, :foo}}]
10 | )
11 | :global.send(:foo, {:EXIT, self, :normal})
12 | end
13 |
14 | test "can register local name" do
15 | DatomicGenServer.start_link(
16 | "datomic:mem://test",
17 | true,
18 | [{:timeout, 20_000}, {:default_message_timeout, 20_000}, {:name, RegistrationTest}]
19 | )
20 | send(RegistrationTest, {:EXIT, self, :normal})
21 | end
22 |
23 | test "can start without registering a name" do
24 | {:ok, pid} =
25 | DatomicGenServer.start_link(
26 | "datomic:mem://test",
27 | true,
28 | [{:timeout, 20_000}, {:default_message_timeout, 20_000}]
29 | )
30 | send(pid, {:EXIT, self, :normal})
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------