# 12: what about cycles?

(NOTE: this section digs into some intricate details of how Specmonstah works. It might be confusing and it could use improvement.)

Let's say that in your forum all posts reference a parent topic, and that topics reference their first post. That means that for every topic, its `:first-post-id` that should be set to a post's `:id`, and that's post's `:topic-id` must reference the topic's `:id`. What we got here is a good ol' fashioned cycle:

```scheme
(ns reifyhealth.specmonstah-tutorial.12
  (:require [clojure.spec.alpha :as s]
            [clojure.spec.gen.alpha :as gen]
            [reifyhealth.specmonstah.core :as sm]
            [reifyhealth.specmonstah.spec-gen :as sg]))

(def id-seq (atom 0))
;; This spec uses a generated such that each time a value is generated
;; it returns the incremented value of the atom id-seq
(s/def ::id (s/with-gen pos-int?
              #(gen/fmap (fn [_] (swap! id-seq inc))
                         (gen/return nil))))
(s/def ::topic-id ::id)
(s/def ::first-post-id ::id)

(s/def ::post (s/keys :req-un [::id ::topic-id]))
(s/def ::topic (s/keys :req-un [::id ::first-post-id]))

(def schema
  {:topic {:prefix    :t
           :spec      ::topic
           :relations {:first-post-id [:post :id]}
           :conform   {:cycle-keys #{:first-post-id}}}
   :post  {:prefix      :p
           :relations   {:topic-id [:topic :id]}
           :constraints {:topic-id #{:required}}
           :spec        ::post}})

(defn ex-01
  []
  (sm/view (sm/add-ents {:schema schema} {:post [[3]]})))
  
(ex-01)
```

![behold! a cycle!](https://4068685904-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoN3fu4ydp8osDZFQch%2F-LpG13D2I87fSP0N4P1P%2F-LpG3ijoF5Dj3t_4TgNV%2Fcycle.png?alt=media\&token=79f67de7-2731-4257-a8f1-3b6eb9c8e24f)

This graph shows how the ents are related: `:p0`, `:p1`, and `:p2` all reference the topic `:t0`, and `:t0` references `:p0`.

One problem that could arise in this situation is that your database might have a foreign key constraint on the `post` table requiring that a post's `:topic-id` be NOT NULL and that it reference a topic that actually exists. The topic's `:first-post-id` might allow NULL values, but have the constraint that the field reference a post that actually exist. However, when cycles exist in a graph there's no way to know which of the two nodes in the cycle should come first. How do we make sure that Specmonstah does the following?

1. Inserts a `:topic` without a value for `:first-post-id`&#x20;
2. Inserts a `:post` that references the `:topic`&#x20;
3. Performs an `update` on the `:topic` to set it's `:first-post-id` to the `:post`'s `:id`

There are two parts to solving this problem. The first is to ensure that ents are sorted correctly for visiting functions. Since you can't topologically sort a cycle, we need some way to tell Specmonstah how to get from a graph with a cycle to a graph without one. We do that with the `:required` constraint. Have another look at the schema:

```scheme
(def schema
  {:topic {:prefix    :t
           :spec      ::topic
           :relations {:first-post-id [:post :id]}
           :conform   {:cycle-keys #{:first-post-id}}}
   :post  {:prefix      :p
           :relations   {:topic-id [:topic :id]}
           :constraints {:topic-id #{:required}}
           :spec        ::post}})
```

You can see that the the `:post` definition includes `:constraints {:topic-id #{:required}}`. This is how you tell specmonstah, "Make sure that the `:topic` that this `:topic-id` refers to gets visited before this `:post`". Internally, this instructs Specmonstah to create a temporary graph where the directed edges from topics to posts are removed, and then topologically sort that graph. You can see it at work:

```scheme
(defn ex-02
  "Shows that `:required` results in correct order"
  []
  (-> (sm/add-ents {:schema schema} {:post [[3]]})
      (sm/visit-ents :print (fn [_ {:keys [ent-name]}] (prn ent-name))))
  nil)
(ex-02)
=>
:t0
:p2
:p1
:p0
```

The second part to solving this problem is to break your visiting function up into multiple functions:

```scheme
(def database (atom []))

(defn insert
  "When inserting records, remove any `:cycle-keys` because those keys
  will reference records that haven't been inserted yet."
  [db {:keys [ent-name ent-type visit-query-opts spec-gen schema-opts]}]
  (let [cycle-keys (into (:cycle-keys schema-opts) (:cycle-keys visit-query-opts))
        record     (apply dissoc spec-gen cycle-keys)]
    (swap! database conj [:insert ent-name record])))

(defn update-keys
  "Perform an 'update', setting all cycle keys to the correct value now
  that the referenced record exists"
  [db {:keys [ent-name ent-type visit-query-opts spec-gen schema-opts]}]
  (let [cycle-keys (into (:cycle-keys schema-opts) (:cycle-keys visit-query-opts))]
    (when (seq cycle-keys)
      (swap! database conj [:update ent-name (select-keys spec-gen cycle-keys)]))))

(def conform [insert update-keys])

(defn ex-03
  []
  (reset! database [])
  (reset! id-seq 0)
  (-> (sg/ent-db-spec-gen {:schema schema} {:post [[3]]})
      (sm/visit-ents :conform conform))
  @database)

(ex-03) ; =>
[[:insert :t0 {:id 1}]
 [:insert :p2 {:id 3, :topic-id 1}]
 [:insert :p1 {:id 5, :topic-id 1}]
 [:insert :p0 {:id 7, :topic-id 1}]
 [:update :t0 {:first-post-id 7}]]
```

Here we're creating a visiting "function" named `conform` which is actually a vector of functions. The `insert` function is applied to all ents in topsort order, then the `update-keys` function is applied to ents in topsort order. You can see that `:t0` is "inserted" without a `:first-post-id`, and then after all records have been inserted `:t0` is "updated", setting its `:first-post-id`.

This introduces one more question: How does the visiting function know to leave out `:first-post-id` when the `insert` function is applied, and add it when the `update-keys` function is applied?

The answer has multiple parts. First, let's look at the schema again:

```scheme
(def schema
  {:topic {:prefix    :t
           :spec      ::topic
           :relations {:first-post-id [:post :id]}
           :conform   {:cycle-keys #{:first-post-id}}}
   :post  {:prefix      :p
           :relations   {:topic-id [:topic :id]}
           :constraints {:topic-id #{:required}}
           :spec        ::post}})
```

Notice the `:conform` key in `:topic` schema. The `:conform` key doesn't have any special meaning; we name it to match the visiting key for the `conform` visiting function. When the conform visiting function is applied, the value of the `:conform` key is passed in under the `:schema-opts` key. You can see that the `insert` function uses this:

```scheme
(defn insert
  "When inserting records, remove any `:cycle-keys` because those keys
  will reference records that haven't been inserted yet."
  [db {:keys [ent-name ent-type visit-query-opts spec-gen schema-opts]}]
  (let [cycle-keys (into (:cycle-keys schema-opts) (:cycle-keys visit-query-opts))
        record     (apply dissoc spec-gen cycle-keys)]
    (swap! database conj [:insert ent-name record])))
```

The insert function creates the `record` to be inserted by `dissoc`ing any keys found in `:cycle-keys` of `schema-opts`.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://sweet-tooth.gitbook.io/specmonstah/tutorial/12-what-about-cycles.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
