Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to write reusable canonical queries for datomic

I'm having trouble writing general datomic queries that I consider reusable.

For instance, following up from this post is there a canonical way to grab all idents from a particular datomic partition?, I have the following schema installed

{[63 :account/password] 
 [64 :account/firstName] 
 [65 :account/lastName] 
 [62 :account/username]
 [69 :email/prority]
 [68 :email/address]}

I want to have a function that shows only the attributes having a given namespace.

This function shows all the attributes in the ":account" namespace

(d/q '[:find ?e ?ident :where
        [?e :db/ident ?ident]
        [_ :db.install/attribute ?e]
        [(.toString ?ident) ?val]
        [(.startsWith ?val ":account")]] (d/db *conn*))

;; => [62 :account/username] [63 :account/password]  
;;    [64 :account/firstName]  [65 :account/lastName]

however, when I want to write a function that can take an input, I have to put quotes everywhere in order to make it work.

(defn get-ns-attrs [?ns db]
  (d/q [':find '?e '?ident ':where
         ['?e ':db/ident '?ident]
         ['_ ':db.install/attribute '?e]
         [(list '.toString '?ident) '?val]
         [(list '.startsWith '?val (str ":" ?ns))]] db))

(get-ns-attrs "account" (d/db *conn*))
;; => [62 :account/username] [63 :account/password]  
;;    [64 :account/firstName]  [65 :account/lastName]

(get-ns-attrs "email" (d/db *conn*))
;; => [69 :email/prority] [68 :email/address]

Is there a better way to do this?

------ update -------

The full code for this for people to try is here:

(ns schema.start
  (:require [datomic.api :as d])
  (:use [clojure.pprint :only [pprint]]))

(def *uri* "datomic:mem://login-profile")
(d/create-database *uri*)
(def *conn* (d/connect *uri*))

(defn boolean? [x]
  (instance? java.lang.Boolean x))

(defn db-pair [attr kns val f]
  (list (keyword (str "db/" (name attr)))
        (f val kns)))

(defn db-enum [val kns]
  (keyword (str "db." (name kns) "/" (name val))))

(def DB-KNS
  {:ident        {:required true
                  :check keyword?}
   :type         {:required true
                  :check #{:keyword :string :boolean :long :bigint :float
                           :double :bigdec :ref :instant :uuid :uri :bytes}
                  :attr :valueType
                  :fn db-enum}
   :cardinality  {:required true
                  :check #{:one :many}
                  :fn db-enum}
   :unique       {:check #{:value :identity}
                  :fn db-enum}
   :doc          {:check string?}
   :index        {:check boolean?}
   :fulltext     {:check boolean?}
   :component?   {:check keyword?}
   :no-history   {:check boolean?}})

(defn process-kns [m kns params res]
  (let [val (m kns)]
    (cond (nil? val)
          (if (:required params)
            (throw (Exception. (str "key " kns " is a required key")))
            res)

          :else
          (let [chk  (or (:check params) (constantly true))
                f    (or (:fn params) (fn [x & xs] x))
                attr (or (:attr params) kns)]
            (if (chk val)
              (apply assoc res (db-pair attr kns val f))
              (throw (Exception. (str "value " val " failed check"))))))))

(defn schema [m]
  (loop [db-kns# DB-KNS
         output  {}]
    (if-let [entry (first db-kns#)]
      (recur (rest db-kns#)
             (process-kns m (first entry) (second entry) output))
      (assoc output
        :db.install/_attribute :db.part/db
        :db/id (d/tempid :db.part/db)))))

(def account-schema
  [(schema {:ident       :account/username
            :type        :string
            :cardinality :one
            :unique      :value
            :doc         "The username associated with the account"})
   (schema {:ident       :account/password
            :type        :string
            :cardinality :one
            :doc         "The password associated with the account"})
   (schema {:ident       :account/firstName
            :type        :string
            :cardinality :one
            :doc         "The first name of the user"})
   (schema {:ident       :account/lastName
            :type        :string
            :cardinality :one
            :doc         "The first name of the user"})
   (schema {:ident       :account/otherEmails
            :type        :ref
            :cardinality :many
            :doc         "Other email address of the user"})
   (schema {:ident       :account/primaryEmail
            :type        :ref
            :cardinality :one
            :doc         "The primary email address of the user"})])

(def email-schema
  [(schema {:ident       :email/address
            :type        :string
            :cardinality :one
            :unique      :value
            :doc         "An email address"})
   (schema {:ident       :email/priority
            :type        :long
            :cardinality :one
            :doc         "An email address's priority"})])

(d/transact *conn* account-schema)
(d/transact *conn* email-schema)

(defn get-ns-attrs1 [?ns db]
  (d/q [':find '?e '?ident ':where
        ['?e ':db/ident '?ident]
        ['_ ':db.install/attribute '?e]
        [(list '.toString '?ident) '?val]
        [(list '.startsWith '?val (str ":" ?ns))]] db))

(defn get-ns-attrs2 [?ns db]
  (d/q '[:find ?e ?ident :where
        [?e :db/ident ?ident]
        [_ :db.install/attribute ?e]
        [(.toString ?ident) ?val]
        [(.startsWith ?val ~(str ":" ?ns))]] db))


(get-ns-attrs1 "account" (d/db *conn*))
(get-ns-attrs1 "email" (d/db *conn*))

(get-ns-attrs2 "account" (d/db *conn*))
(get-ns-attrs2 "email" (d/db *conn*))
like image 669
zcaudate Avatar asked Dec 27 '22 09:12

zcaudate


2 Answers

After a bit more reading, I've figured out that the :in keyword is the key to all of this. Examples are given in the 'Advanced Queries' section of the Tutorial - http://docs.datomic.com/tutorial.html.

This is the equivalent query listing all attributes in the :account namespace

(d/q '[:find ?e ?ident ?ns :in $ ?ns :where
        [?e :db/ident ?ident]
        [_ :db.install/attribute ?e]
        [(.toString ?ident) ?val]
        [(.startsWith ?val ?ns)]] 
     (d/db *conn*)
     "account")
;; => #<HashSet [[68 :account/firstName], [67 :account/password], [71 :account/primaryEmail], [66 :account/username], [69 :account/lastName], [70 :account/otherEmails]]>

This is the equivalent in a function

(defn get-ns-attrs [_ns db]
  (d/q '[:find ?e ?ident :in $ ?ns :where
         [?e :db/ident ?ident]
         [_ :db.install/attribute ?e]
         [(.toString ?ident) ?val]
         [(.startsWith ?val ?ns) ]] db (str _ns)))

(get-ns-attrs :account (d/db *conn*))

;; => #<HashSet [[68 :account/firstName], [67 :account/password], [71 :account/primaryEmail], [66 :account/username], [69 :account/lastName], [70 :account/otherEmails]]>

If you require more modularity, the function can be further broken down using % to pass in a set of rules:

(def rule-nsAttrs
  '[[nsAttrs ?e ?ident ?ns]
    [?e :db/ident ?ident]
    [_ :db.install/attribute ?e]
    [(.toString ?ident) ?val]
    [(.startsWith ?val ?ns)]])

(defn get-ns-attrs [_ns db]
  (d/q '[:find ?e ?ident :in $ % ?ns :where
         (nsAttrs ?e ?ident ?ns)]
       (d/db *conn*)
       [rule-nsAttrs]
       (str _ns)))

(get-ns-attrs :account (d/db *conn*))
;; => #<HashSet [[68 :account/firstName], [67 :account/password], [71 :account/primaryEmail], [66 :account/username], [69 :account/lastName], [70 :account/otherEmails]]>
like image 109
zcaudate Avatar answered Jan 10 '23 08:01

zcaudate


can be solved a bit simpler with Clojure's namespace:

(d/q '[:find ?name :in $ ?ns 
       :where [_ :db.install/attribute ?a] 
              [?a :db/ident ?name] 
              [(namespace ?name) ?attr-ns] 
              [(= ?attr-ns ?ns)]] (d/db *conn*) "account")

which returns:

#{[:account/password]
  [:account/firstName]
  [:account/lastName]
  [:account/username]}
like image 26
tolitius Avatar answered Jan 10 '23 08:01

tolitius