DatalevinDatalevin
Part VII - Appendices

Appendix B: EDN Format

EDN, short for Extensible Data Notation, is the data notation used throughout this guide. Datalevin is written in Clojure, so its native examples use EDN directly: schemas are maps, attributes are keywords, transactions are maps or vectors, Datalog queries are vectors, rules are vectors of lists and vectors, and many examples use tagged literals such as #inst and #uuid.

Readers coming from JSON, SQL, Java, Python, or JavaScript do not need to learn Clojure as a programming language in order to read the examples. EDN is data, not code. It is closer to JSON than to a programming syntax, but with a more regular syntax, richer scalar types, and symbolic names. This appendix gives the EDN subset and Datalevin conventions needed to read and write the examples in the rest of this guide. The official EDN specification is maintained by the edn-format project [1].

1. EDN at a Glance

An EDN value is one readable value. It may be a scalar:

nil
true
false
42
3.14
"hello"
:user/email
?name
#inst "2026-06-09T12:00:00Z"
#uuid "00000000-0000-0000-0000-000000000001"

Or it may be a collection:

[:find ?name :where [?e :user/name ?name]]

{:user/email "ada@example.com"
 :user/name  "Ada"}

#{:admin :editor}

[(parent ?child ?parent)
 [?child :person/parent ?parent]]

Commas are optional whitespace in EDN. The following maps are equivalent:

{:user/name "Ada" :user/age 37}

{:user/name "Ada", :user/age 37}

Datalevin examples usually omit commas because aligned whitespace makes larger schemas and transactions easier to scan.

2. Common Literal Forms

EDN form Meaning Datalevin use
nil Absence of a value Query results, optional arguments, API options.
true, false Booleans Schema flags such as :db/fulltext true.
42, -7 Integers Entity ids, counters, ranks, limits.
3.14 Floating-point number Scores, measurements, vector coordinates.
"Ada" String Text fields and full-text content.
:user/email Keyword Attribute names, enum values, options.
?name Symbol Datalog variables.
[a b c] Vector Queries, transaction datoms, lookup refs, paths.
(f x y) List Rule heads and predicate/function clauses in queries.
{:a 1} Map Schemas, entity maps, idoc documents, options.
#{:a :b} Set Unordered collections and set-valued data.
#inst "..." Instant Time values.
#uuid "..." UUID Stable external identifiers.

Datalevin-specific caveat: normal datom transactions do not store nil as an attribute value. In transaction maps and [:db/add ...] forms, omit an unknown or inapplicable attribute, or retract the existing datom when a value should disappear. nil is still fine for optional API arguments, missing query results, and ordinary EDN data. idoc documents are the exception: inside a :db.type/idoc value, nil is normalized to :json/null.

The table above shows common EDN literals, not the full Datalevin schema value type reference. Appendix C lists every :db/valueType. A few Datalevin value types deserve special attention when writing portable data:

Datalevin value type EDN / portable representation
:db.type/bigint Clojure EDN can carry arbitrary-precision integers, such as 42N, but non-Clojure clients should use the binding's helper or native big-integer type where available.
:db.type/bigdec Clojure EDN can carry arbitrary-precision decimals, such as 3.14M, but portable clients should use binding helpers or native decimal types where available.
:db.type/bytes EDN has no simple portable byte-array literal; use the host binding's bytes value.
:db.type/tuple Use vectors as the value shape, with :db/tupleType or :db/tupleTypes in schema. Composite tuple attributes with :db/tupleAttrs are maintained from component attributes; application code does not transact those tuple values directly.
:db.type/vec Use a numeric vector/array with the client helper expected by the binding; configure dimensions and metric in vector options.
:db.type/idoc Use a nested map value, or a string payload for configured JSON, Markdown, or EDN idoc formats.

EDN has no object syntax and no assignment syntax. A map is just a collection of key-value pairs. A vector is just an ordered collection. A list is just another ordered collection, though Datalevin often gives lists a special meaning inside query and rule forms.

3. Keywords

Keywords are symbolic values that begin with :. In Datalevin, they are used for attribute names, schema properties, enum-like values, index names, options, and many API flags:

:user/email
:db/valueType
:db.type/string
:order.status/paid
:eav
:closed
:string
:long

A keyword may be unqualified, such as :eav, or qualified with a slash, such as :user/email. The part before the slash is a namespace-like prefix. It does not have to name a Clojure namespace, file, table, or class. It is simply a naming convention that keeps related names together and avoids collisions.

Use qualified keywords for application attributes:

:account/email
:account/created-at
:invoice/total
:invoice/customer

Use unqualified keywords when the surrounding API defines them as local operators, modes, flags, or type descriptors. Datalevin's key-value API uses unqualified keywords for KV encoding types and range operators in transactions and queries:

[:find ?e :where [?e :user/email]]
[:closed "a" "m"]
[:put "users" "u1" {:name "Ada"} :string :data]
[:closed 10 20]
[:string :instant]

Here :string, :data, and :instant are KV type descriptors, while :closed is a range operator. These are intentionally separate from Datalog schema value types such as :db.type/string, which appear under :db/valueType.

Do not quote keywords as strings in Clojure examples. These are different values:

:user/email     ;; keyword
"user/email"    ;; string

Datalevin's JSON and non-Clojure APIs sometimes encode keywords as strings at the language boundary, but the logical value is still a keyword.

4. Symbols and Datalog Variables

Symbols are names without a leading colon:

?e
?name
parent
fulltext

In Datalog queries, symbols beginning with ? are variables. Datalevin binds them by matching clauses:

[:find ?name
 :where [?e :user/email "ada@example.com"]
        [?e :user/name ?name]]

The same symbol means the same logical variable within a query. In the example above, both clauses use ?e, so they must match the same entity.

Symbols without ? often name rules or functions:

[(active? ?e)]
[(> ?age 18)]
[(fulltext $ "error budget") [[?e ?a ?v]]]

In Clojure source code, query forms are commonly quoted:

(d/q '[:find ?name
       :where [?e :user/name ?name]]
     db)

The leading quote tells Clojure to pass the vector as data instead of trying to evaluate symbols such as ?name. In Java, Python, JavaScript, and other client languages, the same query is commonly passed as an EDN string or constructed with an interop helper.

5. Vectors, Lists, Maps, and Sets

Datalevin uses EDN collection forms consistently.

5.1 Vectors

Vectors preserve order and are written with square brackets. Datalevin uses vectors for Datalog queries, datom transactions, lookup refs, idoc paths, tuple values, and range specifications:

[:find ?e :where [?e :user/email]]

[:db/add 42 :user/name "Ada"]

[:user/email "ada@example.com"]

[:profile :address :city]

["acct-42" #inst "2026-06-09T00:00:00Z"]

[:closed 10 20]

When a vector appears inside a Datalog query, position matters. In a data pattern such as [?e :user/name ?name], the positions are entity, attribute, and value.

5.2 Lists

Lists are written with parentheses. In Datalevin query and rule syntax, lists usually represent rule heads, rule calls, or function/predicate calls:

(parent ?child ?parent)

[(parent ?child ?parent)
 [?child :person/parent ?parent]]

[(> ?age 18)]

Outside quoted data, a Clojure list means a function call. Inside quoted query or rule data, it is only EDN data until Datalevin interprets it.

5.3 Maps

Maps are written with curly braces and contain alternating keys and values. Datalevin uses maps for schema definitions, entity transactions, option maps, and idoc documents:

{:user/email {:db/valueType :db.type/string
              :db/unique    :db.unique/identity}
 :user/name  {:db/valueType :db.type/string}}

{:user/email "ada@example.com"
 :user/name  "Ada Lovelace"}

{:limit 10
 :offset 20}

Map order is not part of the meaning. Examples often align map values vertically to make related fields easier to compare.

5.4 Sets

Sets are written with #{...} and represent unordered unique values:

#{:admin :editor}

A plain {...} form is a map, not a set. Use #{...} when the value is a collection of unique elements without associated values.

6. Tagged Literals

EDN can attach a tag to the next value. Datalevin examples most often use the built-in #inst and #uuid tags.

Do not use custom tagged literals unless the reader or API path you are using has installed support for those tags. In particular, Datalevin reads EDN idoc string payloads with the standard EDN reader path; #inst and #uuid are the safe built-ins to rely on there.

#inst represents an instant in time:

#inst "2026-06-09T12:30:00Z"

#uuid represents a UUID:

#uuid "00000000-0000-0000-0000-000000000001"

These are not strings, even though their textual form contains quoted strings. The tag tells the EDN reader to produce a time or UUID value. This matters for typed schema attributes:

{:event/id         {:db/valueType :db.type/uuid
                   :db/unique    :db.unique/identity}
 :event/created-at {:db/valueType :db.type/instant}}

When using a non-Clojure client, prefer the client library's EDN helper or typed wrapper for keywords, UUIDs, instants, big integers, big decimals, and byte values. If you send raw JSON strings where the database expects typed values, Datalevin may store strings instead of UUID, instant, or keyword values.

7. EDN in Datalevin

The same EDN forms recur across Datalevin's API surface.

7.1 Schemas

A schema is a map from attribute keywords to property maps:

{:user/email {:db/valueType :db.type/string
              :db/unique    :db.unique/identity}
 :user/age   {:db/valueType :db.type/long}
 :user/tags  {:db/valueType   :db.type/keyword
              :db/cardinality :db.cardinality/many}}

The outer keys are application attributes. The inner keys are Datalevin schema properties. Most schema property values are also keywords.

7.2 Entity Map Transactions

Entity transactions are maps whose keys are attributes:

{:user/email "ada@example.com"
 :user/name  "Ada"
 :user/tags  #{:admin :researcher}}

With a unique identity attribute, the same map shape can insert a new entity or update the existing entity with that identity.

7.3 Datom Transactions

Datom transactions use vectors:

[:db/add 42 :user/name "Ada"]
[:db/retract 42 :user/old-email "ada@old.example"]
[:db/retractEntity 42]

The vector shape is compact and positional, so it is useful when you need to be explicit about the entity id and transaction operation.

7.4 Lookup Refs

A lookup ref is a two-element vector containing a unique attribute and a value:

[:user/email "ada@example.com"]
[:task/id #uuid "00000000-0000-0000-0000-000000000501"]

Lookup refs let transactions and queries refer to entities by domain identity instead of by numeric entity id.

7.5 Datalog Queries

A Datalog query is usually written as an EDN vector containing clauses and keywords such as :find, :where, :in, :keys, and :limit:

[:find ?e ?name
 :where [?e :user/email ?email]
        [?e :user/name ?name]
 :in $ ?email]

Datalevin also accepts the equivalent map form, which is convenient when code or configuration wants named query parts:

{:find  [?e ?name]
 :in    [$ ?email]
 :where [[?e :user/email ?email]
         [?e :user/name ?name]]
 :limit 10}

In Clojure source code, the same map is commonly written with quoted vectors so Clojure passes the query variables as data, while ordinary Clojure expressions such as a local limit value are evaluated before Datalevin receives the map.

{:find  '[?e ?name]
 :in    '[$ ?email]
 :where '[[?e :user/email ?email]
          [?e :user/name ?name]]
 :limit limit}

The query is data. Datalevin interprets that data as a logical query.

7.6 Rules

Rules are EDN data too. A rule set is a vector of rule definitions:

[[(active? ?e)
  [?e :user/status :user.status/active]]

 [(can-login? ?e)
  (active? ?e)
  [?e :user/email]]]

Because rule syntax contains lists and symbols, Clojure code usually quotes the rule set, while non-Clojure clients usually pass it through an EDN reader helper.

7.7 Indexed Documents

For :db.type/idoc, EDN can be the document format:

{:doc/id      "profile-1"
 :doc/content {:profile {:name "Ada"
                         :age  37
                         :tags [:math :computing]}}}

When :db/idocFormat is :edn, string payloads are read as EDN and must produce a map. Important document rules:

Within those rules, EDN idocs can use keyword keys, string keys, vectors, numbers, strings, booleans, instants, UUIDs, sets, and nested maps.

7.8 Dumps and Configuration Files

Datalevin command-line dump/load workflows can use EDN files. EDN is useful for this because it preserves Datalevin-specific values such as keywords, UUIDs, instants, and sets without inventing JSON conventions for each type.

8. EDN and JSON

EDN and JSON are both textual data formats, but they make different trade-offs.

Feature EDN JSON
Map keys Any EDN value, commonly keywords Strings only
Symbolic names Keywords and symbols Usually strings
Sets Built in with #{...} Usually arrays by convention
Instants and UUIDs Tagged literals Usually strings by convention
Comments Semicolon line comments Not part of JSON
Commas Optional whitespace Required separators
Top-level value Any single EDN value Any JSON value

This is why this guide's Clojure examples can express Datalevin data directly:

{:task/id       #uuid "00000000-0000-0000-0000-000000000501"
 :task/status   :task.status/running
 :task/tags     #{:agent :memory}
 :task/started  #inst "2026-06-09T12:00:00Z"}

The equivalent JSON representation needs conventions for keyword, set, UUID, and instant values. Datalevin's non-Clojure clients provide helpers for those cases.

9. Reading and Printing EDN Safely

In Clojure, use clojure.edn/read-string to read EDN text:

(require '[clojure.edn :as edn])

(edn/read-string "{:user/name \"Ada\"}")

Do not use the general Clojure code reader for untrusted input. EDN is designed as a data reader; the general Clojure reader reads a larger language.

Use pr-str to print ordinary Clojure values in a readable form:

(pr-str {:user/name "Ada" :user/active? true})
;; => "{:user/name \"Ada\", :user/active? true}"

For Datalevin APIs, prefer passing native values when you are already in Clojure. Use EDN strings mainly at language boundaries, in configuration files, or when an API explicitly asks for an EDN form.

10. Common Mistakes

Do not write JSON object syntax when the example expects EDN:

;; EDN
{:user/email "ada@example.com"}

;; JSON, not EDN
{"user/email": "ada@example.com"}

Do not use strings when Datalevin expects keywords:

;; Keyword attribute
:user/email

;; String
"user/email"

Do not forget # when writing a set:

#{:read :write}  ;; set
{:read :write}   ;; map from :read to :write

Do not forget to quote Datalog queries in Clojure source code:

;; Correct: pass query data.
(d/q '[:find ?e :where [?e :user/email]] db)

;; Incorrect: Clojure tries to evaluate ?e and other symbols.
(d/q [:find ?e :where [?e :user/email]] db)

Do not use null; EDN uses nil.

For Datalevin attributes, do not transact nil as the value. Use omission for unknown values and retraction for values that should no longer be present. The exception is nested idoc content, where nil normalizes to :json/null.

Do not treat tagged literals as plain strings:

#uuid "00000000-0000-0000-0000-000000000001"  ;; UUID value
"00000000-0000-0000-0000-000000000001"        ;; string value

11. Quick Reference

Need Use
Attribute name :user/email
Enum value :order.status/paid
Datalog variable ?e, ?name, ?score
Entity transaction {:user/email "ada@example.com"}
Datom transaction [:db/add 42 :user/name "Ada"]
Lookup ref [:user/email "ada@example.com"]
Query [:find ?e :where [?e :user/email]]
Rule set [[(rule ?x) [?x :attr/value]]]
Set #{:a :b :c}
Instant #inst "2026-06-09T12:00:00Z"
UUID #uuid "00000000-0000-0000-0000-000000000001"
Missing value nil

EDN's main advantage in Datalevin is that the examples show the actual data structures the database consumes. Once the literal forms are familiar, schemas, transactions, queries, rules, and indexed documents all read as variations on the same small notation.

References

[1] Rich Hickey and contributors, "Extensible Data Notation," official edn-format specification. URL: https://github.com/edn-format/edn.

No examples for this chapter yet.