├── .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 | 4 | 5 | 6 | 7 | 8 | 404 – datomic_gen_server v2.2.5 9 | 10 | 11 | 12 | 13 | 14 |
15 | 18 | 56 | 57 |
58 |
59 | 60 | 61 |

Page not found

62 | 63 |

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.

67 | 68 | 81 |
82 |
83 |
84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /doc/Datom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Datom – datomic_gen_server v2.2.5 9 | 10 | 11 | 12 | 13 | 14 |
15 | 18 | 56 | 57 |
58 |
59 | 60 | 61 |

62 | datomic_gen_server v2.2.5 63 | Datom 64 | 65 | 66 |

67 | 68 | 69 |
70 |

A data structure for representing a Datomic datom.

71 | 72 |
73 | 74 | 75 | 76 |
77 |

78 | 79 | 80 | 81 | Summary 82 |

83 | 84 |
85 |

86 | Types 87 |

88 |
89 |
90 | t() 91 |
92 | 93 |
94 | 95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
105 | 106 | 107 | 108 |
109 |

110 | 111 | 112 | 113 | Types 114 |

115 |
116 |
117 |
t :: %Datom{a: atom, added: boolean, e: integer, tx: integer, v: term}
118 | 119 |
120 | 121 |
122 |
123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 143 |
144 |
145 |
146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /doc/DatomicGenServer.EntityMap.DataTuple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | DatomicGenServer.EntityMap.DataTuple – datomic_gen_server v2.2.5 9 | 10 | 11 | 12 | 13 | 14 |
15 | 18 | 56 | 57 |
58 |
59 | 60 | 61 |

62 | datomic_gen_server v2.2.5 63 | DatomicGenServer.EntityMap.DataTuple 64 | 65 | 66 |

67 | 68 | 69 | 70 | 71 |
72 |

73 | 74 | 75 | 76 | Summary 77 |

78 | 79 |
80 |

81 | Types 82 |

83 |
84 |
85 | t() 86 |
87 | 88 |
89 | 90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
100 | 101 | 102 | 103 |
104 |

105 | 106 | 107 | 108 | Types 109 |

110 |
111 |
112 |
t :: %DatomicGenServer.EntityMap.DataTuple{a: term, added: boolean, e: term, v: term}
113 | 114 |
115 | 116 |
117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 138 |
139 |
140 |
141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /doc/DatomicGenServer.ProcessState.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | DatomicGenServer.ProcessState – datomic_gen_server v2.2.5 9 | 10 | 11 | 12 | 13 | 14 |
15 | 18 | 56 | 57 |
58 |
59 | 60 | 61 |

62 | datomic_gen_server v2.2.5 63 | DatomicGenServer.ProcessState 64 | 65 | 66 |

67 | 68 | 69 | 70 | 71 |
72 |

73 | 74 | 75 | 76 | Summary 77 |

78 | 79 |
80 |

81 | Types 82 |

83 |
84 |
85 | t() 86 |
87 | 88 |
89 | 90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
100 | 101 | 102 | 103 |
104 |

105 | 106 | 107 | 108 | Types 109 |

110 |
111 |
112 |
t :: %DatomicGenServer.ProcessState{message_wait_until_crash: non_neg_integer, port: port}
113 | 114 |
115 | 116 |
117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 138 |
139 |
140 |
141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /doc/DatomicTransaction.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | DatomicTransaction – datomic_gen_server v2.2.5 9 | 10 | 11 | 12 | 13 | 14 |
15 | 18 | 56 | 57 |
58 |
59 | 60 | 61 |

62 | datomic_gen_server v2.2.5 63 | DatomicTransaction 64 | 65 | 66 |

67 | 68 | 69 |
70 |

A data structure for representing a Datomic transaction.

71 | 72 |
73 | 74 | 75 | 76 |
77 |

78 | 79 | 80 | 81 | Summary 82 |

83 | 84 |
85 |

86 | Types 87 |

88 |
89 |
90 | t() 91 |
92 | 93 |
94 | 95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
105 | 106 | 107 | 108 |
109 |

110 | 111 | 112 | 113 | Types 114 |

115 |
116 |
117 |
t :: %DatomicTransaction{added_datoms: [Datom.t], basis_t_after: integer, basis_t_before: integer, retracted_datoms: [Datom.t], tempids: %{integer => integer}, tx_id: integer}
118 | 119 |
120 | 121 |
122 |
123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 143 |
144 |
145 |
146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /doc/api-reference.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | API Reference – datomic_gen_server v2.2.5 9 | 10 | 11 | 12 | 13 | 14 |
15 | 18 | 56 | 57 |
58 |
59 | 60 |

61 | datomic_gen_server v2.2.5 62 | API Reference 63 |

64 | 65 | 72 | 73 | 74 |
75 |

Modules

76 |
77 |
78 | 79 | 80 |

A data structure for representing a Datomic datom

81 |
82 | 83 |
84 |
85 | 86 | 87 |

DatomicGenServer is an Elixir GenServer that communicates with a Clojure 88 | Datomic peer running in the JVM, using clojure-erlastic

89 |
90 | 91 |
92 |
93 | 94 | 95 |

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

101 |
102 | 103 |
104 |
105 | 106 | 107 |

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

111 |
112 | 113 |
114 | 118 |
119 | 120 | 121 |
122 |
123 | 124 | 125 |

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 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------