defmodule DatomicGenServer.Db do @moduledoc """ DatomicGenServer.Db is a module intended to facilitate the use of Elixir data structures instead of edn strings for communicating with Datomic. This module maps the DatomicGenServer interface functions in wrappers that accept and return Elixir data structures, and also provides slightly more syntactically pleasant equivalents for Datomic keys and structures that would otherwise need to be represented using a lot of punctuation that isn't required in Clojure. The hexdoc organizes the functions in this module alphabetically; here is a list by topic: ## Interface functions q(server_identifier, exdn, options \\ []) transact(server_identifier, exdn, options \\ []) entity(server_identifier, exdn, attr_names \\ :all, options \\ []) load(server_identifier, data_path, options \\ []) ## Datomic Shortcuts ### Id/ident dbid(db_part) id ident ### Transaction creation add retract install_attribute alter_attribute tx_instant ### Value types value_type type_long type_keyword type_string type_boolean type_bigint type_float type_double type_bigdec type_ref type_instant type_uuid type_uri type_bytes ### Cardinalities cardinality cardinality_one cardinality_many ### Optional Schema Attributes doc unique unique_value unique_identity index fulltext is_component no_history ### Functions _fn fn_retract_entity fn_cas ### Common partions schema_partition transaction_partition user_partition ### Query placeholders q?(atom) ### Data sources implicit inS(placeholder_atom) db as_of(tx_id) since(tx_id) history ### Bindings and find specifications single_scalar blank collection_binding(placeholder_atom) ### Clauses _not(inner_clause) _not_join(binding_list, inner_clause_list) _or(inner_clauses) _or_join(binding_list, inner_clause_list) _and(inner_clauses) _pull({:symbol, entity_var}, pattern_clauses) _expr(function_symbol, remaining_expressions, bindings) ## Examples DatomicGenServer.start( "datomic:mem://test", true, [{:timeout, 20_000}, {:default_message_timeout, 20_000}, {:name, DatomicGenServer}] ) data_to_add = [%{ Db.id => Db.dbid(Db.schema_partition), Db.ident => :"person/name", Db.value_type => Db.type_string, Db.cardinality => Db.cardinality_one, Db.doc => "A person's name", Db.install_attribute => Db.schema_partition }] Db.transact(DatomicGenServer, data_to_add) # => {:ok, %DatomicGenServer.Db.DatomicTransaction{ basis_t_before: 1001, basis_t_after: 1002, retracted_datoms: [], added_datoms: [ %DatomicGenServer.Db.Datom{a: 50, added: true, e: 13194139534314, tx: 13194139534314, v: %Calendar.DateTime{abbr: "UTC", day: 15, hour: 3, min: 20, month: 2, sec: 1, std_off: 0, timezone: "Etc/UTC", usec: 746000, utc_off: 0, year: 2016}}, %DatomicGenServer.Db.Datom{a: 41, added: true, e: 65, tx: 13194139534314, v: 35}, %DatomicGenServer.Db.Datom{a: 62, added: true, e: 65, tx: 13194139534314, v: "A person's name"}, %DatomicGenServer.Db.Datom{a: 10, added: true, e: 65, tx: 13194139534314, v: :"person/name"}, %DatomicGenServer.Db.Datom{a: 40, added: true, e: 65, tx: 13194139534314, v: 23}, %DatomicGenServer.Db.Datom{a: 13, added: true, e: 0, tx: 13194139534314, v: 65}], tempids: %{-9223367638809264706 => 65}}} query = [:find, Db.q?(:c), :where, [Db.q?(:c), Db.doc, "A person's name"]] Db.q(DatomicGenServer, query) #=> {:ok, #MapSet<['A']>} # ASCII representation of ID 65 """ #TODO Use type Exdn.converter in signature instead of (Exdn.exdn -> term) @type query_option :: DatomicGenServer.send_option | {:response_converter, (Exdn.exdn -> term)} | {:edn_tag_handlers, [{atom, Exdn.handler}, ...]} @type datom_map :: %{:e => integer, :a => atom, :v => term, :tx => integer, :added => boolean} @type transaction_result :: %{:"db-before" => %{:"basis-t" => integer}, :"db-after" => %{:"basis-t" => integer}, :"tx-data" => [datom_map], :tempids => %{integer => integer}} ############################# INTERFACE FUNCTIONS ############################ # TODO Rest API allows for limit and offset on query; this is implemented as # a call to take and drop on the result set, but we would probably prefer to # do this in Clojure before returning it back to Elixir. @doc """ Queries a DatomicGenServer using a query formulated as an Elixir list. This query is translated to an edn string which is then passed to the Datomic `q` API function. The first parameter to this function is the pid or alias of the GenServer process; the second is the query. The optional third parameter is a list of bindings for the data sources in the query, passed to the `inputs` argument of the Datomic `q` function. **IMPORTANT:** These bindings are converted to edn strings which are read back in the Clojure peer and then passed to Clojure `eval`. Since any arbitrary Clojure forms that are passed in are evaluated, **you must be particularly careful that the bindings are sanitized** and that you are not passing anything in `{:list, [...]}` expressions that you don't control. Bindings may include `datomic_gen_server.peer/*db*` for the current database (or the `db` shortcut below), as well as the forms produced by `as_of` and `since` below. These accept transaction times or transaction IDs. The options keyword list may include a `:client_timeout` option that specifies the milliseconds timeout passed to `GenServer.call`, and a `:message_timeout` option that specifies how long the GenServer should wait for a response before crashing (overriding the default value set in `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the `:client_timeout` is shorter than the `:message_timeout` value, the call will return an error but the server will not crash even if the response message is never returned from the Clojure peer. ## Example query = [:find, Db.q?(:c), :where, [Db.q?(:c), Db.doc, "A person's name"]] Db.q(DatomicGenServer, query) #=> {:ok, #MapSet<['A']>} # ASCII representation of ID 65 """ @spec q(GenServer.server, [Exdn.exdn], [Exdn.exdn], [query_option]) :: {:ok, term} | {:error, term} def q(server_identifier, exdn, exdn_bindings \\ [], options \\ []) do {valid, invalid} = exdn_bindings |> Enum.map(&Exdn.from_elixir/1) |> Enum.partition(&is_ok/1) # TODO Get yourself a monad! if Enum.empty?(invalid) do bindings = Enum.map(valid, fn({:ok, binding}) -> binding end) case Exdn.from_elixir(exdn) do {:ok, edn_str} -> case DatomicGenServer.q(server_identifier, edn_str, bindings, options) do {:ok, reply_str} -> convert_query_response(reply_str, options) error -> error end parse_error -> parse_error end else {:error, invalid} end end defp is_ok({ :ok, _ }), do: true defp is_ok(_), do: false @doc """ Issues a transaction against a DatomicGenServer using a transaction formulated as an Elixir list of maps. This transaction is translated to an edn string which is then passed to the Datomic `transact` API function. The first parameter to this function is the pid or alias of the GenServer process; the second is the transaction data. The options keyword list may include a `:client_timeout` option that specifies the milliseconds timeout passed to GenServer.call, and a `:message_timeout` option that specifies how long the GenServer should wait for a response before crashing (overriding the default value set in `DatomicGenServer.start` or `DatomicGenServer.start_link`). Note that if the `:client_timeout` is shorter than the `:message_timeout` value, the call will return an error but the server will not crash even if the response message is never returned from the Clojure peer. ## Example data_to_add = [%{ Db.id => Db.dbid(Db.schema_partition), Db.ident => :"person/name", Db.value_type => Db.type_string, Db.cardinality => Db.cardinality_one, Db.doc => "A person's name", Db.install_attribute => Db.schema_partition }] Db.transact(DatomicGenServer, data_to_add) # => {:ok, %DatomicGenServer.Db.DatomicTransaction{ basis_t_before: 1001, basis_t_after: 1002, retracted_datoms: [], added_datoms: [ %DatomicGenServer.Db.Datom{a: 50, added: true, e: 13194139534314, tx: 13194139534314, v: %Calendar.DateTime{abbr: "UTC", day: 15, hour: 3, min: 20, month: 2, sec: 1, std_off: 0, timezone: "Etc/UTC", usec: 746000, utc_off: 0, year: 2016}}, %DatomicGenServer.Db.Datom{a: 41, added: true, e: 65, tx: 13194139534314, v: 35}, %DatomicGenServer.Db.Datom{a: 62, added: true, e: 65, tx: 13194139534314, v: "A person's name"}, %DatomicGenServer.Db.Datom{a: 10, added: true, e: 65, tx: 13194139534314, v: :"person/name"}, %DatomicGenServer.Db.Datom{a: 40, added: true, e: 65, tx: 13194139534314, v: 23}, %DatomicGenServer.Db.Datom{a: 13, added: true, e: 0, tx: 13194139534314, v: 65}], tempids: %{-9223367638809264706 => 65}}} """ @spec transact(GenServer.server, [Exdn.exdn], [DatomicGenServer.send_option]) :: {:ok, DatomicTransaction.t} | {:error, term} def transact(server_identifier, exdn, options \\ []) do case Exdn.from_elixir(exdn) do {:ok, edn_str} -> case DatomicGenServer.transact(server_identifier, edn_str, options) do {:ok, reply_str} -> case Exdn.to_elixir(reply_str) do {:ok, exdn_result} -> transaction(exdn_result) error -> error end error -> error end parse_error -> parse_error end end @doc """ Issues an `entity` call to that is passed to the Datomic `entity` API function. The first parameter to this function is the pid or alias of the GenServer process; the second is an edn string representing the parameter that is to be passed to `entity`: either an entity id, an ident, or a lookup ref. The third parameter is a list of atoms that represent the keys of the attributes you wish to fetch, or `:all` if you want all the entity's attributes. The options keyword list may include a `:client_timeout` option that specifies the milliseconds timeout passed to GenServer.call, and a `:message_timeout` option that specifies how long the GenServer should wait for a response before crashing (overriding the default value set in `start` or `start_link`). Note that if the `:client_timeout` is shorter than the `:message_timeout` value, the call will return an error but the server will not crash even if the message is never returned from the Clojure peer. ## Example Db.entity(DatomicGenServer, :"person/email") # => {ok, %{ Db.ident => :"person/email", Db.value_type => Db.type_string, Db.cardinality => Db.cardinality_one, Db.doc => "A person's email"}} """ @spec entity(GenServer.server, [Exdn.exdn], [atom] | :all, [query_option]) :: {:ok, term} | {:error, term} def entity(server_identifier, exdn, attr_names \\ :all, options \\ []) do case Exdn.from_elixir(exdn) do {:ok, edn_str} -> case DatomicGenServer.entity(server_identifier, edn_str, attr_names, options) do {:ok, reply_str} -> convert_query_response(reply_str, options) error -> error end parse_error -> parse_error end end @doc """ Issues a call to the Clojure net.phobot.datomic/seed library to load data into a database using data files in edn format. The database is not dropped, recreated, or migrated before loading. The first parameter to this function is the pid or alias of the GenServer process; the second is the path to the directory containing the data files. The data files will be processed in the sort order of their directory. Data is loaded in a single transaction. The return value of the function is the result of the Datomic `transact` API function call that executed the transaction, wrapped in a `DatomicTransaction` struct. Loading data does not use the Clojure Conformity library and is not idempotent. The options keyword list may include a `:client_timeout` option that specifies the milliseconds timeout passed to GenServer.call, and a `:message_timeout` option that specifies how long the GenServer should wait for a response before crashing (overriding the default value set in `start` or `start_link`). Note that if the `:client_timeout` is shorter than the `:message_timeout` value, the call will return an error but the server will not crash even if the message is never returned from the Clojure peer. ## Example data_dir = Path.join [System.cwd(), "seed-data"] DatomicGenServer.load(DatomicGenServer, data_dir) => {:ok, "{:db-before {:basis-t 1000}, :db-after {:basis-t 1000}, ... """ @spec load(GenServer.server, String.t, [DatomicGenServer.send_option]) :: {:ok, DatomicTransaction.t} | {:error, term} def load(server_identifier, data_path, options \\ []) do case DatomicGenServer.load(server_identifier, data_path, options) do {:ok, reply_str} -> case Exdn.to_elixir(reply_str) do {:ok, exdn_result} -> transaction(exdn_result) error -> error end error -> error end end @spec convert_query_response(String.t, [query_option]) :: {:ok, term} | {:error, term} defp convert_query_response(response_str, options) do converter = Keyword.get(options, :response_converter) || (fn x -> x end) handlers = Keyword.get(options, :edn_tag_handlers) || Exdn.standard_handlers Exdn.to_elixir(response_str, converter, handlers) end ############################# DATOMIC SHORTCUTS ############################ # Id/ident @doc "Convenience function that generates `#db/id[ ]`" @spec dbid(atom) :: {:tag, :"db/id", [atom]} def dbid(db_part) do {:tag, :"db/id", [db_part]} end @doc "Convenience shortcut for `:\"db/id\"`" @spec id :: :"db/id" def id, do: :"db/id" @doc "Convenience shortcut for `:\"db/ident\"`" @spec ident :: :"db/ident" def ident, do: :"db/ident" # Transaction creation @doc "Convenience shortcut for `:\"db/add\"`" @spec add :: :"db/add" def add, do: :"db/add" @doc "Convenience shortcut for `:\"db/retract\"`" @spec retract :: :"db/retract" def retract, do: :"db/retract" @doc "Convenience shortcut for `:\"db.install/_attribute\"`" @spec install_attribute :: :"db.install/_attribute" def install_attribute, do: :"db.install/_attribute" @doc "Convenience shortcut for `:\"db.alter/attribute\"`" @spec alter_attribute :: :"db.alter/attribute" def alter_attribute, do: :"db.alter/attribute" @doc "Convenience shortcut for `:\"db/txInstant\"`" @spec tx_instant :: :"db/txInstant" def tx_instant, do: :"db/txInstant" # Value types @doc "Convenience shortcut for `:\"db/valueType\"`" @spec value_type :: :"db/valueType" def value_type, do: :"db/valueType" @doc "Convenience shortcut for `:\"db.type/long\"`" @spec type_long :: :"db.type/long" def type_long, do: :"db.type/long" @doc "Convenience shortcut for `:\"db.type/keyword\"`" @spec type_keyword :: :"db.type/keyword" def type_keyword, do: :"db.type/keyword" @doc "Convenience shortcut for `:\"db.type/string\"`" @spec type_string :: :"db.type/string" def type_string, do: :"db.type/string" @doc "Convenience shortcut for `:\"db.type/boolean\"`" @spec type_boolean :: :"db.type/boolean" def type_boolean, do: :"db.type/boolean" @doc "Convenience shortcut for `:\"db.type/bigint\"`" @spec type_bigint :: :"db.type/bigint" def type_bigint, do: :"db.type/bigint" @doc "Convenience shortcut for `:\"db.type/float\"`" @spec type_float :: :"db.type/float" def type_float, do: :"db.type/float" @doc "Convenience shortcut for `:\"db.type/double\"`" @spec type_double :: :"db.type/double" def type_double, do: :"db.type/double" @doc "Convenience shortcut for `:\"db.type/bigdec\"`" @spec type_bigdec :: :"db.type/bigdec" def type_bigdec, do: :"db.type/bigdec" @doc "Convenience shortcut for `:\"db.type/ref\"`" @spec type_ref :: :"db.type/ref" def type_ref, do: :"db.type/ref" @doc "Convenience shortcut for `:\"db.type/instant\"`" @spec type_instant :: :"db.type/instant" def type_instant, do: :"db.type/instant" @doc "Convenience shortcut for `:\"db.type/uuid\"`" @spec type_uuid :: :"db.type/uuid" def type_uuid, do: :"db.type/uuid" @doc "Convenience shortcut for `:\"db.type/uri\"`" @spec type_uri :: :"db.type/uri" def type_uri, do: :"db.type/uri" @doc "Convenience shortcut for `:\"db.type/bytes\"`" @spec type_bytes :: :"db.type/bytes" def type_bytes, do: :"db.type/bytes" # Cardinalities @doc "Convenience shortcut for `:\"db/cardinality\"`" @spec cardinality :: :"db/cardinality" def cardinality, do: :"db/cardinality" @doc "Convenience shortcut for `:\"db.cardinality/one\"`" @spec cardinality_one :: :"db.cardinality/one" def cardinality_one, do: :"db.cardinality/one" @doc "Convenience shortcut for `:\"db.cardinality/many\"`" @spec cardinality_many :: :"db.cardinality/many" def cardinality_many, do: :"db.cardinality/many" # Optional Schema Attributes @doc "Convenience shortcut for `:\"db/doc\"`" @spec doc :: :"db/doc" def doc, do: :"db/doc" @doc "Convenience shortcut for `:\"db/unique\"`" @spec unique :: :"db/unique" def unique, do: :"db/unique" @doc "Convenience shortcut for `:\"db.unique/value\"`" @spec unique_value :: :"db.unique/value" def unique_value, do: :"db.unique/value" @doc "Convenience shortcut for `:\"db.unique/identity\"`" @spec unique_identity :: :"db.unique/identity" def unique_identity, do: :"db.unique/identity" @doc "Convenience shortcut for `:\"db/index\"`" @spec index :: :"db/index" def index, do: :"db/index" @doc "Convenience shortcut for `:\"db/fulltext\"`" @spec fulltext :: :"db/fulltext" def fulltext, do: :"db/fulltext" @doc "Convenience shortcut for `:\"db/isComponent\"`" @spec is_component :: :"db/isComponent" def is_component, do: :"db/isComponent" @doc "Convenience shortcut for `:\"db/noHistory\"`" @spec no_history :: :"db/noHistory" def no_history, do: :"db/noHistory" # Functions @doc "Convenience shortcut for `:\"db/fn\"`" @spec _fn :: :"db/fn" def _fn, do: :"db/fn" @doc "Convenience shortcut for `:\"db.fn/retractEntity\"`" @spec fn_retract_entity :: :"db.fn/retractEntity" def fn_retract_entity, do: :"db.fn/retractEntity" @doc "Convenience shortcut for `:\"db.fn/cas\"`" @spec fn_cas :: :"db.fn/cas" def fn_cas, do: :"db.fn/cas" # Common partions @doc "Convenience shortcut for `:\"db.part/db\"`" @spec schema_partition :: :"db.part/db" def schema_partition, do: :"db.part/db" @doc "Convenience shortcut for `:\"db.part/tx\"`" @spec transaction_partition :: :"db.part/tx" def transaction_partition, do: :"db.part/tx" @doc "Convenience shortcut for `:\"db.part/user\"`" @spec user_partition :: :"db.part/user" def user_partition, do: :"db.part/user" # Query placeholders @doc """ Convenience function to generate Datomic query placeholders--i.e., symbols prefixed by a question mark. Accepts an atom as its argument, representing the symbol to which the question mark is to be prepended. """ @spec q?(atom) :: {:symbol, atom } def q?(placeholder_atom) do variable_symbol = placeholder_atom |> to_string with_question_mark = "?" <> variable_symbol |> String.to_atom {:symbol, with_question_mark } end # Data sources @doc "Convenience shortcut for the implicit data source `$`" @spec implicit :: {:symbol, :"$"} def implicit, do: {:symbol, :"$"} @doc """ Convenience function to generate Datomic data source specifications--i.e., symbols prefixed by a dollar sign. Accepts an atom as its argument, representing the symbol to which the dollar sign is to be prepended. """ @spec inS(atom) :: {:symbol, atom} def inS(placeholder_atom) do placeholder = placeholder_atom |> to_string with_dollar_sign = "$" <> placeholder |> String.to_atom {:symbol, with_dollar_sign } end @doc """ Convenience shortcut to allow you to pass the current database in the data source bindings to a query or transaction. This gets bound to the value of the Clojure dynamic variable `datomic_gen_server.peer/*db*` inside the peer. This value is also used inside functions such as `as_of` which take the database and return a different database value based on transaction time etc. ## Example Db.q(DatomicGenServer, [:find, Db.q?(:c), :in, Db.implicit, Db.q?(:docstring), :where, [Db.q?(:c), Db.doc, Db.q?(:docstring)]], [Db.db, "A person's address"] ) """ @spec db :: {:symbol, :"datomic_gen_server.peer/*db*"} def db, do: {:symbol, :"datomic_gen_server.peer/*db*"} # TODO Allow dates @doc """ Convenience function to allow passing a call to the Datomic `as-of` API function when creating data source bindings to a query or transaction. Accepts an integer as its argument, representing a transaction number or transaction ID. Dates are not yet supported. ## Example Db.q(DatomicGenServer, [:find, Db.q?(:c), :in, Db.implicit, Db.q?(:docstring), :where, [Db.q?(:c), Db.doc, Db.q?(:docstring)]], [Db.as_of(transaction.basis_t_after), "A person's address"] ) """ @spec as_of(integer) :: {:list, [Exdn.exdn] } def as_of(tx_id), do: clojure_expression(:"datomic.api/as-of", [db, tx_id]) # TODO Allow dates @doc """ Convenience function to allow passing a call to the Datomic `since` API function when creating data source bindings to a query or transaction. Accepts an integer as its argument, representing a transaction number or transaction ID. Dates are not yet supported. ## Example Db.q(DatomicGenServer, [:find, Db.q?(:c), :in, Db.implicit, Db.q?(:docstring), :where, [Db.q?(:c), Db.doc, Db.q?(:docstring)]], [Db.since(transaction.basis_t_after), "A person's address"] ) """ @spec since(integer) :: {:list, [Exdn.exdn] } def since(tx_id), do: clojure_expression(:"datomic.api/since", [db, tx_id]) @doc """ Convenience shortcut to allw passing a call to the Datomic `history` API function when creating data source bindings to a query or transaction. This will become of use when datoms and index-range calls and queries are supported. """ @spec history :: {:list, [Exdn.exdn] } def history, do: clojure_expression(:"datomic.api/history", [db]) # Bindings and find specifications @doc """ Convenience shortcut for the single scalar find specification `.` as used, for example, in: `[:find ?e . :where [?e age 42] ]` """ @spec single_scalar :: {:symbol, :"."} def single_scalar, do: {:symbol, :"."} @doc """ Convenience shortcut for the blank binding `_` as used, for example, in: `[:find ?x :where [_ :likes ?x]]` """ @spec blank :: {:symbol, :"_"} def blank, do: {:symbol, :"_"} @doc """ Convenience shortcut for collection binding find specification `...` as used, for example, in: `[:find ?e in $ [?a ...] :where [?e age ?a] ]` """ @spec collection_binding(atom) :: [{:symbol, atom},...] def collection_binding(placeholder_atom) do [ q?(placeholder_atom), {:symbol, :"..."} ] end # Clauses @doc """ Convenience shortcut for creating a `not` clause. In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this allows us not to have to sprinkle that syntax all over the place. ## Example Db._not([Db.q?(:eid), :"person/age" 13]) sends the following to Datomic: (not [?eid :person/age 13]) """ @spec _not([Exdn.exdn]) :: {:list, [Exdn.exdn]} def _not(inner_clause), do: clojure_expression(:not, [inner_clause]) @doc """ Convenience shortcut for creating a `not-join` clause. In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this function allows us not to have to sprinkle that syntax all over the place. ## Example Db._not_join( [ Db.q?(:employer) ], [ [Db.q?(:employer), :"business/employee" Db.q?(:person)], [Db.q?(:employer), :"business/nonprofit" true] ] ) sends the following to Datomic: (not-join [?employer] [?employer :business/employee ?person] [?employer :business/nonprofit true]) """ @spec _not_join([{:symbol, atom},...], [Exdn.exdn]) :: {:list, [Exdn.exdn]} def _not_join(binding_list, inner_clause_list) do clauses_including_bindings = [ binding_list | inner_clause_list ] clojure_expression(:"not-join", clauses_including_bindings) end @doc """ Convenience shortcut for creating an `or` clause. In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this function allows us not to have to sprinkle that syntax all over the place. ## Example Db._or([ [Db.q?(:org), :"business/nonprofit" true], [Db.q?(:org), :"organization/ngo" true] ]) sends the following to Datomic: (or [?org :business/nonprofit true] [?org :organization/ngo true]) """ @spec _or([Exdn.exdn]) :: {:list, [Exdn.exdn]} def _or(inner_clauses), do: clojure_expression(:or, inner_clauses) @doc """ Convenience shortcut for creating an `and` clause. Note that in Datomic, `and` clauses are only for use inside `or` clauses; `and` is the default otherwise. In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this function allows us not to have to sprinkle that syntax all over the place. ## Example Db._and([ [Db.q?(:org), :"organization/ngo" true], [Db.q?(:org), :"organization/country" :"country/france"] ]) sends the following to Datomic: (and [?org :organization/ngo true] [?org :organization/country :country/france]) """ @spec _and([Exdn.exdn]) :: {:list, [Exdn.exdn]} def _and(inner_clauses), do: clojure_expression(:and, inner_clauses) @doc """ Convenience shortcut for creating an `or-join` clause. The first parameter to this function should be a list of bindings; the second the list of clauses. In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this function allows us not to have to sprinkle that syntax all over the place. ## Example Db._or_join( [ Db.q?(:person) ], [ Db._and([ [Db.q?(:employer), :"business/employee", Db.q?(:person)], [Db.q?(:employer), :"business/nonprofit", true] ]), [Db.q?(:person), :"person/age", 65] ] ) sends the following to Datomic: (or-join [?person] (and [?employer :business/employee ?person] [?employer :business/nonprofit true]) [?person :person/age 65]) """ @spec _or_join([{:symbol, atom},...], [Exdn.exdn]) :: {:list, [Exdn.exdn]} def _or_join(binding_list, inner_clause_list) do clauses_including_bindings = [ binding_list | inner_clause_list ] clojure_expression(:"or-join", clauses_including_bindings) end @doc """ Convenience shortcut for creating a Datomic pull expression. In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this function allows us not to have to sprinkle that syntax all over the place. ## Example Db._pull(Db.q?(:e), [:"person/address"]) sends the following to Datomic: (pull ?e [:person/address]) """ @spec _pull({:symbol, atom}, [Exdn.exdn]) :: {:list, [Exdn.exdn]} def _pull({:symbol, entity_var}, pattern_clauses) do clojure_expression(:pull, [entity_var, pattern_clauses]) end @doc """ Convenience shortcut for creating a Datomic expression clause. Note that this is *not* the same as a simple Clojure expression inside parentheses. An expression clause allows arbitrary Java or Clojure functions to be used inside of Datalog queries; they are either of form `[(predicate ...)]` or `[(function ...) bindings]`. An expression clause is thus a Clojure list inside a vector. In Exdn, Clojure lists are represented as tuples with the tag `:list`, so this function allows us not to have to sprinkle that syntax all over the place. """ @spec _expr(atom, [Exdn.exdn], [Exdn.exdn]) :: [{:list, [Exdn.exdn]}] def _expr(function_symbol, remaining_expressions, bindings \\ []) do [ clojure_expression(function_symbol, remaining_expressions) | bindings ] end # A Clojure expression is a list starting with a symbol @spec clojure_expression(atom, [Exdn.exdn]) :: {:list, [Exdn.exdn]} defp clojure_expression(symbol_atom, remaining_expressions) do clause_list = [{:symbol, symbol_atom} | remaining_expressions ] {:list, clause_list} end ########## PRIVATE FUNCTIONS FOR STRUCTIFYING TRANSACTION RESPONSES ############# @spec transaction(transaction_result) :: {:ok, DatomicTransaction.t} | {:error, term} defp transaction(transaction_result) do try do {added_datoms, retracted_datoms} = tx_data(transaction_result) |> to_datoms transaction_struct = %DatomicTransaction{ tx_id: tx_data(transaction_result) |> transaction_id, basis_t_before: basis_t_before(transaction_result), basis_t_after: basis_t_after(transaction_result), added_datoms: added_datoms, retracted_datoms: retracted_datoms, tempids: tempids(transaction_result)} {:ok, transaction_struct} rescue e -> {:error, e} end end @spec basis_t_before(%{:"db-before" => %{:"basis-t" => integer}}) :: integer defp basis_t_before(%{:"db-before" => %{:"basis-t" => before_t}}) do before_t end @spec basis_t_after(%{:"db-after" => %{:"basis-t" => integer}}) :: integer defp basis_t_after(%{:"db-after" => %{:"basis-t" => after_t}}) do after_t end @spec tx_data(%{:"tx-data" => [datom_map]}) :: [datom_map] defp tx_data(%{:"tx-data" => tx_data}) do tx_data end @spec to_datoms([datom_map]) :: {[Datom.t], [Datom.t]} defp to_datoms(datom_maps) do datom_maps |> Enum.map(fn(datom_map) -> struct(Datom, datom_map) end) |> Enum.partition(fn(datom) -> datom.added end) end @spec transaction_id([datom_map]) :: integer defp transaction_id(datom_maps) do %{tx: id} = datom_maps |> hd id end @spec tempids(%{tempids: %{integer => integer}}) :: %{integer => integer} defp tempids(%{tempids: tempids}) do tempids end end