oakes / odoyle-rules

A rules engine for Clojure(Script)
The Unlicense
530 stars 20 forks source link

Relationships? #3

Open den1k opened 3 years ago

den1k commented 3 years ago

How would one query relationships? For example what when todos have sub-todos:

{:id        :todo1
 :text      "do this"
 :sub-todos [{:id   :todo2
              :text "but first do this!"}]}

This can be normalized and added to the session one by one:

[{:id        :todo1
  :text      "do this"
  :sub-todos [[:id :todo2]]}
 {:id   :todo2
  :text "but first do this!"}]

But how does one query it??

oakes commented 3 years ago

I would do it like this:

(require '[odoyle.rules :as o])

(def rules
  (o/ruleset
    {::todo
     [:what
      [id ::text text]
      :then-finally
      (->> (o/query-all o/*session* ::todo)
           (reduce #(assoc %1 (:id %2) %2) {})
           (o/insert! ::todos ::by-id))]

     ::todo-with-sub-todos
     [:what
      [id ::text text]
      [id ::sub-todos sub-todos]]

     ::update-sub-todos
     [:what
      [id ::sub-todo-ids sub-todo-ids]
      [::todos ::by-id id->todo]
      :then
      (->> (mapv id->todo sub-todo-ids)
           (o/insert! id ::sub-todos))]}))

(def initial-session
  (reduce o/add-rule (o/->session) rules))

(-> initial-session
    (o/insert 1 {::text "do this"
                 ::sub-todo-ids [2]})
    (o/insert 2 ::text "but first do this!")
    o/fire-rules
    (o/query-all ::todo-with-sub-todos)
    println)

;; [{:id 1, :text "do this", :sub-todos [{:id 2, :text "but first do this!"}]}]

I'm saving all the todos as a derived fact (i added :then-finally recently, see the README for explanation). The ::update-sub-todos rule will ensure that the sub todos always match the sub-todo-ids that are inserted.

With rules it kind of turns "querying" inside out but it accomplishes the same thing :D

den1k commented 3 years ago

Very interesting! This is probably too granular for users who want to express relationships all over their programs. Do you think it would make sense performance-wise to build an in-memory graph-db on top of odoyle-rules?

Having performant rules on top of a graph-db that can derive facts on itself and cause side effects is highly exciting!

oakes commented 3 years ago

I've been thinking about it recently actually. The ::by-id derived fact is a simple example of creating an "index" (in the case, a map of ids -> todos) that is constantly kept up to date. But it is completely recomputed when new todos are inserted, so if you have thousands of todos it could become slow.

I'd like to figure out how to update that map incrementally, but i haven't come up with a good approach yet. That would allow it to possibly behave like a normal database, maintaining all sorts of indexes of facts and only recomputing when necessary. That would be really neat.

oakes commented 3 years ago

FYI for 0.6.0 i wrote a longer write-up about this topic, which shows how to do this recursively (sub-todos with their own sub-todos).

jtrunick commented 1 year ago

From looking at Datomic it appears you can have a one-to-many relationship by specifying db.cardinality/many in which case the id's will be repeated for each entry of the "many" attribute. I was expecting something similar with O'Doyle, and was surprised it appears different in that regard. I was hoping to stay more in "triplet-land" and not need to resort to Clojure as much. (Newb at this stuff, so my understanding may be incorrect.)

oakes commented 1 year ago

Right, in odoyle the triplets must have a unique id+attribute combo; that is a strict requirement. You are technically still in triplet land; you're just inserting data structures derived from the original triplets as new triplets. You won't be able to avoid resorting to Clojure; in fact, using it more often is one of the goals. See the very end of the writeup I mentioned above.

flow-danny commented 1 year ago

If o/insert and o/insert! accepted functions as values (similar to swap!) then incremental updates would be possible by conj and disj the relationship, without first having to query-all