├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── bin ├── deploy-jar.sh └── jar.sh ├── deps.edn ├── pom.xml ├── src └── compute │ └── datomic_client_memdb │ ├── async.clj │ └── core.clj └── test └── compute └── datomic_client_memdb ├── async_test.clj └── core_test.clj /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | working_directory: ~/repo 5 | docker: 6 | - image: circleci/clojure:tools-deps-1.9.0.381 7 | 8 | jobs: 9 | test: 10 | <<: *defaults 11 | 12 | steps: 13 | - checkout 14 | 15 | - restore_cache: 16 | keys: 17 | - project-{{ checksum "deps.edn" }} 18 | - project 19 | 20 | - run: clojure -A:test:run-tests 21 | 22 | - save_cache: 23 | paths: 24 | - ~/.m2 25 | - ~/.gitlibs 26 | - ./.cpcache 27 | key: project-{{ checksum "deps.edn" }} 28 | 29 | workflows: 30 | version: 2 31 | ci-workflow: 32 | jobs: 33 | - test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml.asc 5 | *.jar 6 | *.class 7 | /.lein-* 8 | /.nrepl-port 9 | .nrepl-history 10 | .hgignore 11 | .hg/ 12 | *.impl 13 | .idea 14 | .envrc 15 | project.clj 16 | *.iml 17 | .nrepl-history 18 | .nrepl-port 19 | out 20 | .tags 21 | .cpcache 22 | extra -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF 5 | THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and 12 | documentation distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | 16 | i) changes to the Program, and 17 | 18 | ii) additions to the Program; 19 | 20 | where such changes and/or additions to the Program originate from and 21 | are distributed by that particular Contributor. A Contribution 22 | 'originates' from a Contributor if it was added to the Program by such 23 | Contributor itself or anyone acting on such Contributor's 24 | behalf. Contributions do not include additions to the Program which: 25 | (i) are separate modules of software distributed in conjunction with 26 | the Program under their own license agreement, and (ii) are not 27 | derivative works of the Program. 28 | 29 | "Contributor" means any person or entity that distributes the Program. 30 | 31 | "Licensed Patents" mean patent claims licensable by a Contributor 32 | which are necessarily infringed by the use or sale of its Contribution 33 | alone or when combined with the Program. 34 | 35 | "Program" means the Contributions distributed in accordance with this 36 | Agreement. 37 | 38 | "Recipient" means anyone who receives the Program under this 39 | Agreement, including all Contributors. 40 | 41 | 2. GRANT OF RIGHTS 42 | 43 | a) Subject to the terms of this Agreement, each Contributor hereby 44 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 45 | license to reproduce, prepare derivative works of, publicly display, 46 | publicly perform, distribute and sublicense the Contribution of such 47 | Contributor, if any, and such derivative works, in source code and 48 | object code form. 49 | 50 | b) Subject to the terms of this Agreement, each Contributor hereby 51 | grants Recipient a non-exclusive, worldwide, royalty-free patent 52 | license under Licensed Patents to make, use, sell, offer to sell, 53 | import and otherwise transfer the Contribution of such Contributor, if 54 | any, in source code and object code form. This patent license shall 55 | apply to the combination of the Contribution and the Program if, at 56 | the time the Contribution is added by the Contributor, such addition 57 | of the Contribution causes such combination to be covered by the 58 | Licensed Patents. The patent license shall not apply to any other 59 | combinations which include the Contribution. No hardware per se is 60 | licensed hereunder. 61 | 62 | c) Recipient understands that although each Contributor grants the 63 | licenses to its Contributions set forth herein, no assurances are 64 | provided by any Contributor that the Program does not infringe the 65 | patent or other intellectual property rights of any other entity. Each 66 | Contributor disclaims any liability to Recipient for claims brought by 67 | any other entity based on infringement of intellectual property rights 68 | or otherwise. As a condition to exercising the rights and licenses 69 | granted hereunder, each Recipient hereby assumes sole responsibility 70 | to secure any other intellectual property rights needed, if any. For 71 | example, if a third party patent license is required to allow 72 | Recipient to distribute the Program, it is Recipient's responsibility 73 | to acquire that license before distributing the Program. 74 | 75 | d) Each Contributor represents that to its knowledge it has sufficient 76 | copyright rights in its Contribution, if any, to grant the copyright 77 | license set forth in this Agreement. 78 | 79 | 3. REQUIREMENTS 80 | 81 | A Contributor may choose to distribute the Program in object code form 82 | under its own license agreement, provided that: 83 | 84 | a) it complies with the terms and conditions of this Agreement; and 85 | 86 | b) its license agreement: 87 | 88 | i) effectively disclaims on behalf of all Contributors all warranties 89 | and conditions, express and implied, including warranties or 90 | conditions of title and non-infringement, and implied warranties or 91 | conditions of merchantability and fitness for a particular purpose; 92 | 93 | ii) effectively excludes on behalf of all Contributors all liability 94 | for damages, including direct, indirect, special, incidental and 95 | consequential damages, such as lost profits; 96 | 97 | iii) states that any provisions which differ from this Agreement are 98 | offered by that Contributor alone and not by any other party; and 99 | 100 | iv) states that source code for the Program is available from such 101 | Contributor, and informs licensees how to obtain it in a reasonable 102 | manner on or through a medium customarily used for software exchange. 103 | 104 | When the Program is made available in source code form: 105 | 106 | a) it must be made available under this Agreement; and 107 | 108 | b) a copy of this Agreement must be included with each copy of the Program. 109 | 110 | Contributors may not remove or alter any copyright notices contained 111 | within the Program. 112 | 113 | Each Contributor must identify itself as the originator of its 114 | Contribution, if any, in a manner that reasonably allows subsequent 115 | Recipients to identify the originator of the Contribution. 116 | 117 | 4. COMMERCIAL DISTRIBUTION 118 | 119 | Commercial distributors of software may accept certain 120 | responsibilities with respect to end users, business partners and the 121 | like. While this license is intended to facilitate the commercial use 122 | of the Program, the Contributor who includes the Program in a 123 | commercial product offering should do so in a manner which does not 124 | create potential liability for other Contributors. Therefore, if a 125 | Contributor includes the Program in a commercial product offering, 126 | such Contributor ("Commercial Contributor") hereby agrees to defend 127 | and indemnify every other Contributor ("Indemnified Contributor") 128 | against any losses, damages and costs (collectively "Losses") arising 129 | from claims, lawsuits and other legal actions brought by a third party 130 | against the Indemnified Contributor to the extent caused by the acts 131 | or omissions of such Commercial Contributor in connection with its 132 | distribution of the Program in a commercial product offering. The 133 | obligations in this section do not apply to any claims or Losses 134 | relating to any actual or alleged intellectual property 135 | infringement. In order to qualify, an Indemnified Contributor must: a) 136 | promptly notify the Commercial Contributor in writing of such claim, 137 | and b) allow the Commercial Contributor tocontrol, and cooperate with 138 | the Commercial Contributor in, the defense and any related settlement 139 | negotiations. The Indemnified Contributor may participate in any such 140 | claim at its own expense. 141 | 142 | For example, a Contributor might include the Program in a commercial 143 | product offering, Product X. That Contributor is then a Commercial 144 | Contributor. If that Commercial Contributor then makes performance 145 | claims, or offers warranties related to Product X, those performance 146 | claims and warranties are such Commercial Contributor's responsibility 147 | alone. Under this section, the Commercial Contributor would have to 148 | defend claims against the other Contributors related to those 149 | performance claims and warranties, and if a court requires any other 150 | Contributor to pay any damages as a result, the Commercial Contributor 151 | must pay those damages. 152 | 153 | 5. NO WARRANTY 154 | 155 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS 156 | PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 157 | KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY 158 | WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY 159 | OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely 160 | responsible for determining the appropriateness of using and 161 | distributing the Program and assumes all risks associated with its 162 | exercise of rights under this Agreement , including but not limited to 163 | the risks and costs of program errors, compliance with applicable 164 | laws, damage to or loss of data, programs or equipment, and 165 | unavailability or interruption of operations. 166 | 167 | 6. DISCLAIMER OF LIABILITY 168 | 169 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR 170 | ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, 171 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING 172 | WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF 173 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 174 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR 175 | DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED 176 | HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 177 | 178 | 7. GENERAL 179 | 180 | If any provision of this Agreement is invalid or unenforceable under 181 | applicable law, it shall not affect the validity or enforceability of 182 | the remainder of the terms of this Agreement, and without further 183 | action by the parties hereto, such provision shall be reformed to the 184 | minimum extent necessary to make such provision valid and enforceable. 185 | 186 | If Recipient institutes patent litigation against any entity 187 | (including a cross-claim or counterclaim in a lawsuit) alleging that 188 | the Program itself (excluding combinations of the Program with other 189 | software or hardware) infringes such Recipient's patent(s), then such 190 | Recipient's rights granted under Section 2(b) shall terminate as of 191 | the date such litigation is filed. 192 | 193 | All Recipient's rights under this Agreement shall terminate if it 194 | fails to comply with any of the material terms or conditions of this 195 | Agreement and does not cure such failure in a reasonable period of 196 | time after becoming aware of such noncompliance. If all Recipient's 197 | rights under this Agreement terminate, Recipient agrees to cease use 198 | and distribution of the Program as soon as reasonably 199 | practicable. However, Recipient's obligations under this Agreement and 200 | any licenses granted by Recipient relating to the Program shall 201 | continue and survive. 202 | 203 | Everyone is permitted to copy and distribute copies of this Agreement, 204 | but in order to avoid inconsistency the Agreement is copyrighted and 205 | may only be modified in the following manner. The Agreement Steward 206 | reserves the right to publish new versions (including revisions) of 207 | this Agreement from time to time. No one other than the Agreement 208 | Steward has the right to modify this Agreement. The Eclipse Foundation 209 | is the initial Agreement Steward. The Eclipse Foundation may assign 210 | the responsibility to serve as the Agreement Steward to a suitable 211 | separate entity. Each new version of the Agreement will be given a 212 | distinguishing version number. The Program (including Contributions) 213 | may always be distributed subject to the version of the Agreement 214 | under which it was received. In addition, after a new version of the 215 | Agreement is published, Contributor may elect to distribute the 216 | Program (including its Contributions) under the new version. Except as 217 | expressly stated in Sections 2(a) and 2(b) above, Recipient receives 218 | no rights or licenses to the intellectual property of any Contributor 219 | under this Agreement, whether expressly, by implication, estoppel or 220 | otherwise. All rights in the Program not expressly granted under this 221 | Agreement are reserved. 222 | 223 | This Agreement is governed by the laws of the State of Washington and 224 | the intellectual property laws of the United States of America. No 225 | party to this Agreement will bring a legal action under this Agreement 226 | more than one year after the cause of action arose. Each party waives 227 | its rights to a jury trial in any resulting litigation. 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Library is Deprecated 2 | 3 | The Datomic team released [dev-local](https://docs.datomic.com/cloud/dev-local.html) on July 10, 2020. This library has always been a temporary workaround for offline testing and easy CI support. Dev-local supersedes this library. I recommend switching over to dev-local. 4 | 5 | # datomic-client-memdb 6 | 7 | [![CircleCI](https://circleci.com/gh/ComputeSoftware/datomic-client-memdb.svg?style=svg)](https://circleci.com/gh/ComputeSoftware/datomic-client-memdb) 8 | 9 | Datomic Client protocols for Datomic Peer databases. 10 | 11 | ## Installation 12 | 13 | ```clojure 14 | datomic-client-memdb {:mvn/version "1.1.1"} 15 | ``` 16 | 17 | ## Usage 18 | 19 | Create a Client like you normally would with the Client API: 20 | 21 | ```clojure 22 | (require '[compute.datomic-client-memdb.core :as memdb]) 23 | (def c (memdb/client {})) 24 | ``` 25 | 26 | `memdb/client` returns `compute.datomic-client-memdb.core/Client` type which implements 27 | `datomic.client.api/Client`. Calling `memdb/client` multiple times with the same 28 | arg-map will result in the same instance of the client (i.e. the function is memoized). 29 | 30 | This is technically the only function that this library exposes. All other operations 31 | are done using the Datomic Client API. See [here](https://docs.datomic.com/client-api/datomic.client.api.html) 32 | for a list of functions that you can call. 33 | 34 | ### Suggested usage pattern 35 | 36 | Our company has a database util library, and I imagine most companies have something 37 | similar, that contains a number of functions associated with database operations. 38 | Any time you use Datomic, that util library is probably on the classpath. I 39 | suggest adding a `client` function that returns a Datomic Client dependent on 40 | some values in the Datomic Client arg-map (the args passed to the `client` function). 41 | For example, 42 | 43 | ```clojure 44 | (defn client 45 | [datomic-config] 46 | (if (datomic-local? datomic-config) 47 | (do 48 | (require 'compute.datomic-client-memdb.core) 49 | (if-let [v (resolve 'compute.datomic-client-memdb.core/client)] 50 | (@v datomic-config) 51 | (throw (ex-info "compute.datomic-client-memdb.core is not on the classpath." {})))) 52 | (datomic.client.api/client datomic-config))) 53 | ``` 54 | 55 | The `datomic-local?` function returns true if `datomic-config` (the client arg-map) 56 | has some particular value set. Perhaps `:local? true` or `:endpoint "localhost"`. 57 | 58 | ### Caveats 59 | 60 | Datomic Cloud supports transaction functions through Datomic Ions. This library 61 | does not have support for Ions. 62 | 63 | This API wraps the Datomic Peer (On-Prem). There are features in Datomic Cloud's 64 | query that are not present in Datomic On-Prem, and vice versa. 65 | 66 | This library does not try to re-implement features from Datomic Cloud, 67 | but makes some effort to throw exceptions when the user tries to use 68 | features exclusive to on-prem, notably find spec for collections and scalars. 69 | 70 | Because this is not exhaustive, it is recommended to also run integration tests 71 | against an actual instance of Datomic Cloud. 72 | 73 | ### Cleanup 74 | 75 | If you would like to cleanup the in-memory DBs, you can use the `close` function: 76 | 77 | ```clojure 78 | (memdb/close c) 79 | ``` 80 | 81 | This will delete all the DBs associated with the client and purge them from the 82 | client's DB store. Our Client also implements the `Closeable` interface so you can 83 | use the Client with `with-open`, as seen [here](https://github.com/ComputeSoftware/datomic-client-memdb/blob/aa52ef9c125aef9c48777e0e3b024eb821f387a7/test/compute/datomic_client_memdb/core_test.clj#L10-L14). 84 | 85 | Because this library is only intended to be used during the development and 86 | testing stages, you can probably just ignore the `close` function and only address 87 | cleanup if it becomes a problem. 88 | 89 | ## Implementation 90 | 91 | This library is implemented by creating custom types (via `deftype`) that extend 92 | the Datomic Client protocols located in the `datomic.client.api` namespace. I 93 | tried implementing this using `extend-type` on the Datomic peer classes (i.e. `datomic.db.Db`), 94 | but you cannot add an implementation of `clojure.lang.ILookup` via `deftype` because 95 | `ILookup` is an interface, not a protocol. We need to provide an implementation of 96 | `ILookup` because that is how the Datomic Client API exposes `t` and `next-t`. 97 | 98 | ## License 99 | 100 | Copyright © 2020 Compute Software 101 | 102 | Distributed under the Eclipse Public License either version 1.0 or (at 103 | your option) any later version. 104 | -------------------------------------------------------------------------------- /bin/deploy-jar.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mvn deploy:deploy-file -Dfile=datomic-client-memdb.jar -DrepositoryId=clojars -Durl=https://clojars.org/repo -DpomFile=pom.xml 4 | rm datomic-client-memdb.jar -------------------------------------------------------------------------------- /bin/jar.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | clojure -Spom 6 | clojure -A:jar -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/core.async {:mvn/version "1.1.587"} 2 | com.datomic/client {:mvn/version "0.8.89"} 3 | com.datomic/datomic-free {:mvn/version "0.9.5697"}} 4 | :paths ["src"] 5 | :aliases {:test {:extra-paths ["test"]} 6 | :run-tests {:extra-deps {com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git" 7 | :sha "f7ef16dc3b8332b0d77bc0274578ad5270fbfedd"}} 8 | :main-opts ["-m" "cognitect.test-runner"]} 9 | :jar {:extra-deps {seancorfield/depstar {:mvn/version "0.5.2"}} 10 | :main-opts ["-m" "hf.depstar.jar" "datomic-client-memdb.jar"]}}} 11 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | datomic-client-memdb 5 | datomic-client-memdb 6 | 1.1.1 7 | datomic-client-memdb 8 | 9 | 10 | org.clojure 11 | clojure 12 | 1.10.1 13 | 14 | 15 | com.datomic 16 | datomic-free 17 | 0.9.5697 18 | 19 | 20 | com.datomic 21 | client 22 | 0.8.89 23 | 24 | 25 | org.clojure 26 | core.async 27 | 1.1.587 28 | 29 | 30 | 31 | src 32 | 33 | 34 | 35 | clojars 36 | https://repo.clojars.org/ 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/compute/datomic_client_memdb/async.clj: -------------------------------------------------------------------------------- 1 | (ns compute.datomic-client-memdb.async 2 | (:require 3 | [clojure.core.async :as async] 4 | [datomic.client.api :as d] 5 | [datomic.client.impl.shared.protocols :as async-proto] 6 | [compute.datomic-client-memdb.core :as memdb]) 7 | (:import (java.io Closeable) 8 | (clojure.lang ExceptionInfo))) 9 | 10 | (defn- wrap-async 11 | [f] 12 | (async/thread 13 | (try 14 | (f) 15 | (catch Throwable ex 16 | (if (and (instance? ExceptionInfo ex) 17 | (:cognitect.anomalies/category (ex-data ex))) 18 | (ex-data ex) 19 | {:cognitect.anomalies/category :cognitect.anomalies/fault 20 | :cognitect.anomalies/message (.getMessage ex) 21 | :ex ex}))))) 22 | 23 | (deftype LocalDb [conn db] 24 | async-proto/Db 25 | (as-of [_ time-point] 26 | (d/as-of db time-point)) 27 | (datoms [_ arg-map] 28 | (wrap-async #(d/datoms db arg-map))) 29 | (db-stats [_] 30 | (doto (async/promise-chan) 31 | (async/put! {:cognitect.anomalies/category :cognitect.anomalies/unsupported 32 | :cognitect.anomalies/message "db-stats is not implemented for datomic-client-memdb"}))) 33 | (history [_] 34 | (d/history db)) 35 | (index-range [_ arg-map] 36 | (wrap-async #(d/index-range db arg-map))) 37 | (pull [_ arg-map] 38 | (wrap-async #(d/pull db arg-map))) 39 | (since [_ t] 40 | (d/since db t)) 41 | (with [_ arg-map] 42 | (wrap-async #(d/with db arg-map))) 43 | 44 | async-proto/ParentConnection 45 | (-conn [_] conn) 46 | 47 | clojure.lang.ILookup 48 | (valAt [_ k] 49 | (.valAt db k)) 50 | (valAt [_ k not-found] 51 | (.valAt db k not-found))) 52 | 53 | (deftype LocalConnection [conn] 54 | async-proto/Connection 55 | (db [this] 56 | (wrap-async #(LocalDb. this (d/db conn)))) 57 | (log [_] 58 | (doto (async/promise-chan) 59 | (async/put! {:cognitect.anomalies/category :cognitect.anomalies/unsupported 60 | :cognitect.anomalies/message "log is not implemented for datomic-client-memdb"}))) 61 | (q [_ arg-map] 62 | (wrap-async 63 | #(d/q (update arg-map :args (fn [args] 64 | (map (fn [x] 65 | (if (async-proto/-conn x) 66 | (.-db x) 67 | x)) 68 | args)))))) 69 | (tx-range [_ arg-map] 70 | (wrap-async #(d/tx-range conn arg-map))) 71 | (transact [_ arg-map] 72 | (wrap-async #(d/transact conn arg-map))) 73 | (recent-db [this] 74 | (async-proto/db this)) 75 | (sync [_ t] 76 | (doto (async/promise-chan) 77 | (async/put! {:cognitect.anomalies/category :cognitect.anomalies/unsupported 78 | :cognitect.anomalies/message "recent-db is not implemented for datomic-client-memdb"}))) 79 | (with-db [_] 80 | (wrap-async #(d/with-db conn))) 81 | 82 | clojure.lang.ILookup 83 | (valAt [_ k] 84 | (.valAt conn k)) 85 | (valAt [_ k not-found] 86 | (.valAt conn k not-found))) 87 | 88 | (defrecord Client [client] 89 | async-proto/Client 90 | (list-databases [_ argm] 91 | (wrap-async #(d/list-databases client argm))) 92 | (connect [_ arg-map] 93 | (wrap-async #(LocalConnection. (d/connect client arg-map)))) 94 | (create-database [_ arg-map] 95 | (wrap-async #(d/create-database client arg-map))) 96 | (delete-database [_ arg-map] 97 | (wrap-async #(d/delete-database client arg-map))) 98 | Closeable 99 | (close [_] 100 | (.close client))) 101 | 102 | (defn client 103 | "Returns a Client record that implements the Datomic Client API Protocol. Optionally 104 | takes :db-name-as-uri-fn which is a function that is passed a db-name and is 105 | expected to return a Datomic Peer database URI. Note that this function is passed 106 | a '*' when list-databases is called." 107 | [arg-map] 108 | (map->Client {:client (memdb/client arg-map)})) -------------------------------------------------------------------------------- /src/compute/datomic_client_memdb/core.clj: -------------------------------------------------------------------------------- 1 | (ns compute.datomic-client-memdb.core 2 | (:require 3 | [datomic.client.api :as client] 4 | [datomic.client.api.protocols :as client-proto] 5 | [datomic.client.api.impl :as client-impl] 6 | [datomic.query.support :as q-support] 7 | [datomic.api :as peer]) 8 | (:import (java.io Closeable))) 9 | 10 | (defn- update-vals [m ks f] 11 | (reduce #(update-in % [%2] f) m ks)) 12 | 13 | (defn- throw-unsupported 14 | [data] 15 | (throw (ex-info "Unsupported operation." 16 | (merge {:cognitect.anomalies/category :cognitect.anomalies/unsupported} 17 | data)))) 18 | 19 | (defn memdb-uri 20 | "Returns a Datomic mem database URI for `db-name`." 21 | [db-name] 22 | (str "datomic:mem://" db-name)) 23 | 24 | (defn free-uri 25 | "Returns the default Datomic free database URI for `db-name`." 26 | [db-name] 27 | (str "datomic:free://localhost:4334/" db-name)) 28 | 29 | (defn collection-query? [query] 30 | (let [[{fnd :find}] (q-support/parse-as query)] 31 | (or (and (= 2 (count fnd)) 32 | (= '. (second fnd))) 33 | (and (vector? (first fnd)) 34 | (= 2 (count (first fnd))) 35 | (= '... (second (first fnd))))))) 36 | 37 | (deftype LocalDb [db db-name] 38 | client-proto/Db 39 | (as-of [_ time-point] 40 | (LocalDb. (peer/as-of db time-point) db-name)) 41 | (datoms [_ arg-map] 42 | (apply peer/datoms db (:index arg-map) (:components arg-map))) 43 | (db-stats [_] 44 | (throw-unsupported {})) 45 | (history [_] 46 | (LocalDb. (peer/history db) db-name)) 47 | (index-range [_ arg-map] 48 | (peer/index-range db (:attrid arg-map) (:start arg-map) (:end arg-map))) 49 | (pull [this arg-map] 50 | (client/pull this (:selector arg-map) (:eid arg-map))) 51 | (pull [_ selector eid] 52 | ;; Datomic free will return nil when pull'ing for a single attribute that does 53 | ;; not exist on an entity. Datomic Cloud returns {} in this case. Since there 54 | ;; isn't any cases known where Datomic Cloud will return nil for a pull, we 55 | ;; simply return an empty map if nil is returned from the Peer API. 56 | (if eid 57 | (or (peer/pull db selector eid) {}) 58 | (throw (ex-info "Expected value for :eid" 59 | {:cognitect.anomalies/category :cognitect.anomalies/incorrect 60 | :cognitect.anomalies/message "Expected value for :eid" 61 | :datomic.client.impl.shared.validator/got {:selector selector :eid eid} 62 | :datomic.client.impl.shared.validator/op :pull 63 | :datomic.client.impl.shared.validator/requirements '{:eid value :selector value}})))) 64 | (since [_ t] 65 | (LocalDb. (peer/since db t) db-name)) 66 | (with [_ arg-map] 67 | (-> (peer/with db (:tx-data arg-map)) 68 | (update :db-before #(LocalDb. % db-name)) 69 | (update :db-after #(LocalDb. % db-name)))) 70 | 71 | client-impl/Queryable 72 | (q [_ arg-map] 73 | (let [{:keys [query args]} arg-map] 74 | (when (collection-query? query) 75 | (throw (ex-info "Only find-rel elements are allowed in client find-spec, see http://docs.datomic.com/query.html#grammar" 76 | {:cognitect.anomalies/category :cognitect.anomalies/incorrect, 77 | :cognitect.anomalies/message "Only find-rel elements are allowed in client find-spec, see http://docs.datomic.com/query.html#grammar" 78 | :dbs (filterv #(satisfies? client-proto/Db %) args)}))) 79 | (apply peer/q query (map (fn [x] 80 | (if (instance? LocalDb x) 81 | (.-db x) 82 | x)) 83 | args)))) 84 | 85 | clojure.lang.ILookup 86 | (valAt [this k] 87 | (.valAt this k nil)) 88 | (valAt [this k not-found] 89 | (case k 90 | :t (peer/basis-t db) 91 | :next-t (inc (:t this)) 92 | :db-name db-name 93 | not-found))) 94 | 95 | 96 | (deftype LocalConnection [conn db-name] 97 | client-proto/Connection 98 | (db [_] 99 | (LocalDb. (peer/db conn) db-name)) 100 | 101 | (transact [_ arg-map] 102 | (-> @(peer/transact conn (:tx-data arg-map)) 103 | (update-vals #{:db-before :db-after} #(LocalDb. % db-name)))) 104 | 105 | (tx-range [_ arg-map] 106 | (peer/tx-range (peer/log conn) (:start arg-map) (:end arg-map))) 107 | 108 | (with-db [this] 109 | (client/db this)) 110 | 111 | clojure.lang.ILookup 112 | (valAt [this k] 113 | (.valAt this k nil)) 114 | (valAt [this k not-found] 115 | (get (client/db this) k not-found))) 116 | 117 | 118 | (defrecord Client [db-name-as-uri-fn] 119 | client-proto/Client 120 | (administer-system [_ arg-map] (throw-unsupported {})) 121 | (list-databases [_ _] 122 | (or (peer/get-database-names (db-name-as-uri-fn "*")) (list))) 123 | 124 | (connect [client arg-map] 125 | (let [db-name (:db-name arg-map)] 126 | (if (contains? (set (client/list-databases client {})) db-name) 127 | (LocalConnection. (peer/connect (db-name-as-uri-fn db-name)) db-name) 128 | (let [msg (format "Unable to find keyfile %s. Make sure that your endpoint and db-name are correct." db-name)] 129 | (throw (ex-info msg {:cognitect.anomalies/category :cognitect.anomalies/not-found 130 | :cognitect.anomalies/message msg})))))) 131 | 132 | (create-database [_ arg-map] 133 | (let [db-name (:db-name arg-map)] 134 | (peer/create-database (db-name-as-uri-fn db-name))) 135 | true) 136 | 137 | (delete-database [_ arg-map] 138 | (peer/delete-database (db-name-as-uri-fn (:db-name arg-map))) 139 | true) 140 | 141 | Closeable 142 | (close [client] 143 | (doseq [db (client/list-databases client {})] 144 | (client/delete-database client {:db-name db})) 145 | nil)) 146 | 147 | (defn close 148 | "Cleans up the Datomic Peer Client by deleting all databases." 149 | [client] 150 | (.close client)) 151 | 152 | (defn client 153 | "Returns a Client record that implements the Datomic Client API Protocol. Optionally 154 | takes :db-name-as-uri-fn which is a function that is passed a db-name and is 155 | expected to return a Datomic Peer database URI. Note that this function is passed 156 | a '*' when list-databases is called." 157 | [arg-map] 158 | (map->Client {:db-name-as-uri-fn (or (:db-name-as-uri-fn arg-map) memdb-uri)})) 159 | -------------------------------------------------------------------------------- /test/compute/datomic_client_memdb/async_test.clj: -------------------------------------------------------------------------------- 1 | (ns compute.datomic-client-memdb.async-test 2 | (:require 3 | [clojure.test :refer :all] 4 | [clojure.core.async :as async] 5 | [datomic.client.api.async :as d.async] 6 | [datomic.client.api.protocols :as client-proto] 7 | [datomic.client.impl.shared.protocols :as async-proto] 8 | [compute.datomic-client-memdb.core :as memdb] 9 | [compute.datomic-client-memdb.async :as memdb.async]) 10 | (:import (clojure.lang ExceptionInfo))) 11 | 12 | (def ^:dynamic *client* nil) 13 | 14 | (defn client-fixture 15 | [f] 16 | (with-open [c (memdb.async/client {})] 17 | (binding [*client* c] 18 | (f)))) 19 | 20 | (defn anom->ex 21 | [anom] 22 | (let [cat (:cognitect.anomalies/category anom) 23 | default-msg (str "A " cat " anomaly occurred.") 24 | ex-msg (or (:cognitect.anomalies/message anom) default-msg)] 25 | (if-let [ex (:ex anom)] 26 | (ex-info ex-msg anom ex) 27 | (ex-info ex-msg anom)))) 28 | 29 | (defn ex v)) 36 | v) 37 | ::timeout)))) 38 | 39 | (use-fixtures :each client-fixture) 40 | 41 | (deftest client-test 42 | (is (= true ( conn (d.async/tx-range {}) ( conn (d/tx-range {}) (first) (keys) (set))))) 50 | (testing "with-db is a db" 51 | (is (satisfies? client-proto/Db (d/with-db conn)))) 52 | (testing "conn info works" 53 | (is (every? some? (map #(get conn %) [:t :next-t :db-name])))))) 54 | 55 | (deftest db-test 56 | (d/create-database *client* {:db-name "test"}) 57 | (let [conn (d/connect *client* {:db-name "test"}) 58 | _ (d/transact conn {:tx-data test-schema}) 59 | tx-report (d/transact conn {:tx-data [{:db/id "bob" 60 | :user/name "bob"}]}) 61 | bob-id (get-in tx-report [:tempids "bob"]) 62 | db (d/db conn)] 63 | (testing "query works" 64 | (is (= #{[bob-id]} 65 | (d/q '[:find ?e 66 | :where 67 | [?e :user/name "bob"]] db)))) 68 | (testing "collection queries throw" 69 | (is (thrown? ExceptionInfo (d/q '[:find [(pull ?e [*]) ...] 70 | :where [?e :user/name]] 71 | db))) 72 | (is (thrown? ExceptionInfo (d/q '[:find ?e . 73 | :where [?e :user/name "bob"]] 74 | db)))) 75 | (testing "pull works" 76 | (is (= {:user/name "bob"} 77 | (d/pull db '[:user/name] bob-id))) 78 | (is (= {} 79 | (d/pull db '[:user/i-dont-exist] bob-id)) 80 | "nonexistent attribute results in empty map, not nil") 81 | (is (thrown? ExceptionInfo (d/pull db [:user/name] nil)) 82 | "exception thrown when pulling nil eid")) 83 | (testing "db info works" 84 | (is (every? some? (map #(get db %) [:t :next-t :db-name]))) 85 | (is (:t (last (d/tx-range conn {}))) 86 | (:t conn))) 87 | (testing "db with works" 88 | (let [tx-report (d/with db {:tx-data [{:user/name "bob5"}]})] 89 | (is (:db-before tx-report)) 90 | (is (:db-after tx-report)) 91 | (is (:tempids tx-report)) 92 | (is (:tx-data tx-report)))) 93 | (testing "index-range works" 94 | (let [[_ _ value] (first (d/index-range db {:attrid [:db/ident :user/name]}))] 95 | (is (= value "bob")))))) 96 | 97 | --------------------------------------------------------------------------------