Conj in a rule fails on the transactor


#1

Hi everyone,

I have a hard time understanding why the following rule sometimes fail with a ClassCastException on the conj. Apparently ?parent-path is a Java array and not a IPersistentCollection.

This only happens in the transactor and not with d/with.

We would like to refer to nodes in a tree using their path from the root. I was hoping that a recursive rule like this would work:

(def node-path
  '[;; node without a parent
    [(node-path ?n ?p)
     [?n :node/name ?name]
     [(missing? $ ?n :node/parent)]
     [(vector ?name) ?p]]
    ;; node with a parent
    [(node-path ?n ?p)
     [?n :node/name ?name]
     [?n :node/parent ?parent]
     (node-path ?parent ?parent-path)
     [(conj ?parent-path ?name) ?p]]])

I can send a minimal schema and query to reproduce my use-case if needed.

My expectation was that a vector would always be built at the bottom of the recursion. ?p would be bound to the result of (vector ?name). And then we conj this vector until we reach the leaf. Was my expectation correct? If not, where is it wrong?

Thanks a lot for your help.


#2

To repro on the lastest Datomic free transactor.

(ns sandbox
  (:require [datomic.api :as d]))

(def db-uri "datomic:free://localhost:4334/sandbox")

(def schema
  [#:db{:ident :node/name
        :valueType :db.type/string
        :cardinality :db.cardinality/one}
   #:db{:ident :node/parent
        :valueType :db.type/ref
        :cardinality :db.cardinality/one}
   #:db{:ident :node/path->eid
        :fn #db/fn
        {:lang "clojure"
         :params [db path]
         :requires [[datomic.api :as d]]
         :code (ffirst
                (d/q '[:find ?e
                       :in $ % ?path
                       :where (node-path ?e ?path)]
                     db
                     '[[(node-path ?n ?p)
                        [?n :node/name ?name]
                        [(missing? $ ?n :node/parent)]
                        [(vector ?name) ?p]]
                       [(node-path ?n ?p)
                        [?n :node/name ?name]
                        [?n :node/parent ?parent]
                        (node-path ?parent ?parent-path)
                        [(conj ?parent-path ?name) ?p]]]
                     path))}}
   #:db{:ident :node/ensure-path
        :fn #db/fn
        {:lang "clojure"
         :params [db path parent?]
         :requires [[datomic.api :as d]]
         :code (if-let [eid (d/invoke db :node/path->eid db path)]
                 (when parent?
                   eid)
                 (let [name (last path)
                       parent-path (butlast path)]
                   [(cond-> {:db/id (d/tempid :db.part/user)
                             :node/name name}
                      (seq parent-path)
                      (assoc :node/parent
                             (d/invoke db :node/ensure-path
                                       db parent-path true)))]))}}])

(defn connect
  []
  (d/create-database db-uri)
  (let [conn (d/connect db-uri)]
    @(d/transact conn schema)
    conn))

(def test-data
  [["1"
    ["1A"]
    ["1B"
     ["1B1"]
     ["1B2"]]]
   ["2"]])

(defn plex-tx
  ([data]
   (plex-tx data nil))
  ([data parent]
   (mapcat (fn [[name & children]]
             (let [tempid (d/tempid :db.part/user)
                   node (cond-> {:db/id tempid
                                 :node/name name}
                          parent (assoc :node/parent parent))]
               (into [node] (plex-tx children tempid))))
           data)))

(comment

  (def conn (connect))
  @(d/transact conn (plex-tx test-data))

  (def db (d/db conn))
  (d/invoke db :node/path->eid db ["1" "1B" "1B1"])

  @(d/transact conn [[:node/ensure-path ["1" "1B" "1B3"] false]])
  @(d/transact conn [[:node/ensure-path ["1" "1B"] false]])

  ;; This tx throws
  @(d/transact conn [[:node/ensure-path ["1"] false]])

  )

#3

Bump!


#4

This is a consequence of a couple factors:

  • array lists and vectors compare equal
  • you can’t force something to be a vector on the wire between peer and transactor
  • if you need a vector, you have to make it a vector after the wire

The encoding between peer and transactor doesn’t preserve Clojure vectors, which is why your example works using with (no wire) but doesn’t with a remote transactor (the vector becomes an array list).

When you pass in ?path in your query, it is bound to ?p in your rule, where the call to vector evaluates true, when the passed in path is an array list or a vector.

You would need to use an intermediate variable that you force into a new vector if you need the bottom of the rule to return a vector. I have provided a very small reproduction below (using an explicit array list):

(def uri "datomic:mem://testdb")
(d/create-database uri)
(def conn (d/connect uri))

(def a-list (java.util.ArrayList.))
(.add a-list 2)
(.add a-list 3)
a-list
;; [2 3]

(= a-list (vector 2 3))
;; true

(d/q '[:find ?a .
       :in $ ?a-list
       :where
       [(vector ?a-list) ?a]]
     (d/db conn) a-list)

;; [[2 3]]

(type (first *1))

;; java.util.ArrayList

(d/q '[:find ?a .
       :in $ ?a-list
       :where
       [(vector ?b) ?a]
       [(into [] ?a-list) ?b]]
     (d/db conn) a-list)

;; [[2 3]]

(type (first *1))

;; clojure.lang.PersistentVector