DatalevinDatalevin
Part VII - Appendices

Appendix C: Datalog Schema Reference

This appendix is a compact reference for Datalevin Datalog schema maps. Schema maps are EDN data; projects using any Datalevin language binding can keep them in checked-in EDN files. For design guidance, see Chapter 5 and Chapter 11. For specialized index behavior, see the full-text, vector, embedding, and idoc chapters.

1. Schema Shape

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

(def schema
  {:user/email {:db/valueType :db.type/string
                :db/unique    :db.unique/identity
                :db/doc       "Primary account email."}
   :user/roles {:db/valueType   :db.type/ref
                :db/cardinality :db.cardinality/many}
   :post/body  {:db/valueType            :db.type/string
                :db/fulltext             true
                :db.fulltext/autoDomain true}})

Pass the schema when opening or creating a connection:

(def conn (d/get-conn "/data/app" schema))

Read the current effective schema with d/schema:

(d/schema conn)

The result includes application attributes, built-in attributes, and assigned internal attribute ids such as :db/aid. Use this for inspection, debugging, and migration checks. In Java examples, Datalevin.schema() is a builder for constructing schema input; it is not the same operation as reading the current schema from a database.

Attribute ids are encoded as 32-bit signed non-negative integers in Datalevin's Datalog indexes. This means one database can have at most about 2.1 billion distinct attribute ids, including built-in attributes. Normal application schemas will not approach this, but unbounded dynamic key spaces should be modeled as data rather than as newly minted attributes.

Use update-schema to change schema:

(d/update-schema conn schema-update)
(d/update-schema conn schema-update #{:old/attr})
(d/update-schema conn nil nil {:old/name :new/name})

2. Defaults and Built-Ins

Some behavior that feels schema-related is controlled by connection or store options, not by entries inside the schema map:

Option Default Meaning
:validate-data? false When true, transaction values must already match declared :db/valueType runtime types. When false, Datalevin still uses declared types to coerce or canonicalize values where possible.
:closed-schema? false When true, transactions may mention only attributes already present in the schema. When false, Datalevin allows new attributes to be added as data is written.
:auto-entity-time? false When true, Datalevin maintains :db/created-at and :db/updated-at epoch-millisecond values for entities.

Pass these options when opening or creating a database:

4. Attribute Properties

The public schema properties that Datalevin interprets are the following:

Property Values Meaning
:db/valueType See value type table below Encoded value type. Required for refs, range-friendly ordering, tuples, vectors, idocs, and typed validation.
:db/cardinality :db.cardinality/one, :db.cardinality/many One value or a set of values per entity. Defaults to one.
:db/unique :db.unique/value, :db.unique/identity Enforces uniqueness. Both modes support lookup refs; identity also enables upsert.
:db/isComponent true, false Owned child relationship. Must be used with :db/valueType :db.type/ref.
:db/doc string Human-readable attribute documentation.
:db/fulltext true, false Maintains a full-text secondary index. Values are converted with str before indexing.
:db.fulltext/domains sequence of strings Adds a full-text attribute to explicit search domains.
:db.fulltext/autoDomain true, false Adds an attribute-specific full-text domain named by the attribute without the leading colon.
:db/embedding true, false Maintains an embedding index for string values. Requires :db/valueType :db.type/string.
:db.embedding/domains non-empty sequence of non-blank strings Adds an embedding attribute to explicit embedding domains.
:db.embedding/autoDomain true, false Adds an attribute-specific embedding domain using vector-domain naming.
:db.vec/domains sequence of strings Adds a :db.type/vec attribute to shared vector domains in addition to its attribute domain.
:db/idocFormat :edn, :json, :markdown Format hint for :db.type/idoc; defaults to :edn.
:db/domain string Explicit idoc domain for a :db.type/idoc attribute. If omitted, Datalevin derives the idoc domain from the attribute name.
:db.idoc/indexedPaths sequence of top-level keywords, top-level strings, or path vectors Include-list of idoc path prefixes to index for a :db.type/idoc attribute. If absent, all paths are included.
:db.idoc/excludedPaths sequence of top-level keywords, top-level strings, or path vectors Omit-list of idoc path prefixes for a :db.type/idoc attribute. Exclusions win over included paths.
:db.attr/preds qualified symbol, predicate UDF descriptor map, registered UDF keyword id, or non-empty sequence of those Additional checks for values written to this attribute. Each predicate receives the normalized attribute value and must return true.
:db/tupleAttrs non-empty sequence of attribute keywords Derived composite index entry maintained from other attributes.
:db/tupleType single supported tuple element type Homogeneous tuple element type.
:db/tupleTypes sequence of more than one supported tuple element type Heterogeneous tuple element types.

Use :db/ensure in transaction data for entity invariants over the would-be transaction result:

[:db/ensure 'my.app.payments/bank-account? entity]

The predicate may be a function, var, or qualified symbol. It receives the would-be database after applying the transaction followed by the resolved ensure arguments, so it can check required attributes, entity-shape rules, and cross-attribute invariants after all transaction data has been expanded to datoms. :db/ensure forms do not become datoms in :tx-data; they abort the transaction before commit if a predicate returns falsey or throws.

5. Value Types

Value type Expected values Notes
:db.type/keyword Clojure keywords Common for enums and small symbolic values.
:db.type/symbol Clojure symbols Symbolic values.
:db.type/string strings Required for :db/embedding; typical for full-text.
:db.type/boolean booleans true or false.
:db.type/long 64-bit integers Use for integer range queries and timestamps stored as epoch values.
:db.type/double double-precision numbers Numeric range queries.
:db.type/float single-precision numbers Numeric range queries with float storage.
:db.type/ref entity ids, idents, or lookup refs in transactions Enables joins, reverse pull navigation, and component ownership.
:db.type/bigint arbitrary precision integers Larger integer values.
:db.type/bigdec arbitrary precision decimals Decimal values.
:db.type/instant java.util.Date / instants Time values.
:db.type/uuid UUID values Stable external ids.
:db.type/bytes byte arrays Binary values.
:db.type/tuple vectors Stored homogeneous or heterogeneous tuple values. Composite indexes use :db/tupleAttrs and do not require this value type.
:db.type/vec numeric vectors Maintains vector similarity indexes. Configure dimensions and metric in store options.
:db.type/idoc map values or supported document string payloads that normalize to maps Stored as one datom and indexed by path.

There is no :db.type/uri; store URI values as strings.

If :db/valueType is omitted, Datalevin stores values as serialized EDN data, which is flexible but not as useful for ordered range access.

6. Identity and Uniqueness

:db.unique/value means no two entities may have the same value for that attribute. Duplicates will be rejected with a transaction error.

Any :db/unique attribute can be used in a lookup ref. :db.unique/identity also enables upsert:

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

(d/pull db '[*] [:user/email "a@example.com"])

(d/transact! conn
  [{:user/email "a@example.com"
    :user/name  "Ada"}])

If an entity with that email already exists, the map transaction updates that entity instead of creating a duplicate.

:db/ident is the built-in identity attribute for globally named entities:

(d/transact! conn [{:db/ident :order.status/shipped}])

7. References and Components

Reference attributes must declare :db/valueType :db.type/ref:

{:order/customer {:db/valueType :db.type/ref}
 :order/items    {:db/valueType   :db.type/ref
                  :db/cardinality :db.cardinality/many
                  :db/isComponent true}}

Use :db/isComponent true only for owned children. Component entities are pulled recursively by wildcard pull and are retracted with the parent when using :db/retractEntity.

:db/isComponent does not enforce unique ownership. Datalevin will allow more than one parent to refer to the same component child, but that shape is not a well-formed component model. Parent retraction with :db/retractEntity, or attribute retraction with :db.fn/retractAttribute, treats the child as owned data and retracts it. Retracting the child also retracts declared reference datoms that point to it, so any other parent loses its reference. A value-specific [:db/retract parent component-attr child] removes only that one reference and does not cascade.

:db/isComponent true without :db/valueType :db.type/ref is invalid.

8. Tuples

Datalevin has two related tuple features that should not be confused:

Composite index derived from other attributes:

{:order/customer {:db/valueType :db.type/ref}
 :order/date     {:db/valueType :db.type/instant}
 :order/customer+date
 {:db/tupleAttrs [:order/customer :order/date]}}

Rules for :db/tupleAttrs:

Datalevin stores the derived tuple in the normal EAV and AVE indexes and updates it whenever a component changes. A query must reference the tuple attribute to use that composite path. Its read advantage comes from scanning the qualifying joint combinations instead of emitting a potentially much larger intermediate relation from separate component EAV datoms. It increases write work and index storage; Chapter 11, Section 1.2 gives the IC5 cardinalities and the selection criteria.

Stored homogeneous tuple value:

{:point/xy {:db/valueType :db.type/tuple
            :db/tupleType :db.type/double}}

Stored heterogeneous tuple value:

{:image/bounds {:db/valueType  :db.type/tuple
                :db/tupleTypes [:db.type/double
                                :db.type/double
                                :db.type/long
                                :db.type/long]}}

Rules for stored typed tuples:

For example, this homogeneous tuple schema is valid:

{:sf/emails {:db/valueType :db.type/tuple
             :db/tupleType :db.type/string}}

But transacting an empty tuple fails:

{:sf/foo "hi"
 :sf/emails []}

Datalevin rejects this data with Cannot store an empty homogeneous tuple for attribute :sf/emails. If the entity has no tuple value, omit :sf/emails from the transaction rather than transacting an empty vector.

9. Full-Text Schema Keys

Use :db/fulltext true for attributes that should participate in full-text search:

{:post/body {:db/valueType            :db.type/string
             :db/fulltext             true
             :db.fulltext/domains     ["posts"]
             :db.fulltext/autoDomain true}}

If no explicit domain is given, full-text attributes participate in the default "datalevin" domain. :db.fulltext/autoDomain true also creates an attribute-specific domain named by the attribute without the leading colon: :title becomes "title" and :post/body becomes "post/body". Unlike vector and embedding domains, full-text auto domains keep / in namespaced attribute names.

Store-level :search-opts and :search-domains configure search domain options such as :indexing-mode :async, :index-position? true, and :include-text? true. If a custom analyzer uses ngrams, prefer gram sizes of 3 characters or longer; shorter grams produce many low-selectivity tokens and can make the index much larger. Full-text indexes store document references by default. In Datalog, the document reference is the indexed datom itself; the index does not store a duplicate raw-text copy unless :include-text? true opts a domain into storing text for :display :texts, :display :texts+offsets, and re-indexing.

10. Vector and Embedding Schema Keys

Use :db.type/vec when your application supplies vectors:

{:item/embedding {:db/valueType   :db.type/vec
                  :db.vec/domains ["items"]}}

Every vector attribute has an attribute-specific domain derived from the attribute name with / replaced by _: :embedding becomes "embedding" and :item/embedding becomes "item_embedding". :db.vec/domains adds shared domains in addition to this attribute domain. Configure dimensions, metric, and indexing mode in :vector-opts or :vector-domains, and keep every domain that receives the same vector values compatible.

Use :db/embedding true when Datalevin should embed string datoms:

{:doc/text {:db/valueType             :db.type/string
            :db/embedding             true
            :db.embedding/domains     ["docs"]
            :db.embedding/autoDomain true}}

Embedding rules:

Store-level :embedding-opts, :embedding-domains, and :embedding-providers configure providers, dimensions, metric, and indexing mode.

11. Idoc Schema Keys

Use :db.type/idoc for nested document values that should be indexed by path:

{:doc/json {:db/valueType  :db.type/idoc
            :db/idocFormat :json
            :db/domain     "profiles"}
 :doc/edn  {:db/valueType :db.type/idoc}
 :doc/md   {:db/valueType  :db.type/idoc
            :db/idocFormat :markdown}}

Allowed :db/idocFormat values are :edn, :json, and :markdown. The default is :edn.

Idoc input must normalize to a map at the root. Map keys must be keywords or strings. Use vectors for arrays; lists are rejected. nil values normalize to :json/null, and the literal value :json/null is reserved and rejected on input.

:db/domain names the idoc path index used for that attribute. If it is omitted, Datalevin derives the domain from the attribute name without the leading colon, so :doc/edn becomes "doc/edn". Several idoc attributes may share a domain intentionally, but keep formats coherent: if attributes with different :db/idocFormat values share a domain, Datalevin treats the domain as mixed-format.

Use :db.idoc/indexedPaths and :db.idoc/excludedPaths to control which path prefixes enter the idoc path index. Changing those keys on a populated attribute is rejected; rebuild explicitly instead. Chapter 14 covers path selectors and store-level idoc domain options.

Python and JavaScript expose convenience builders for the same maps: idoc_attr / idocAttr build :db.type/idoc schema attributes, and idoc_domain / idocDomain build idoc option maps for :idoc-opts or entries under :idoc-domains. Clojure can write the EDN maps directly; Java can use the schema builder methods shown in Chapter 14.

12. Schema Evolution

Use d/update-schema to add, change, delete, or rename schema attributes on an open connection:

(d/update-schema conn
  {:user/age {:db/valueType :db.type/long}})

(d/update-schema conn
  {:user/age {:db/doc "Age in whole years"}})

(d/update-schema conn
  {:user/age {:db/doc :db/retract}})

(d/update-schema conn
  {:new/name {:db/valueType :db.type/string}}
  #{:old/name})

(d/update-schema conn nil nil
  {:old/name :new/name})

The schema-update map and each nested property map are patches:

Operation Effect
Omit an attribute or property Retain its stored definition.
Supply a property value Replace that property after validating the resulting complete definition.
Set a property to :db/retract Remove that property.
Add :db/valueType to a populated untyped attribute Atomically re-encode its existing EDN values, or fail without changing data.
Change a populated typed attribute's :db/valueType Reject the change.
Change populated :db/cardinality from many to one Reject the change.
Add :db/unique Accept only when existing values satisfy the requested uniqueness.
Delete an attribute Accept only when no datoms use it.
Rename an attribute Keep its internal attribute id and make existing datoms readable under the new name.
Change secondary-index metadata Plan any required re-index or rebuild; populated embedding and idoc path-index changes require an explicit rebuild.

Existing attributes retain their internal :db/aid, and supplied ids are ignored. The complete request is validated before writing, committed atomically, and safe to retry unchanged. Conflicting renames, rename chains or cycles, duplicate targets, and conflicting patch/delete or delete/rename operations are rejected before writing.

A schema supplied while opening an existing database uses the same patch semantics and persists its changes; it is not a read-only drift check. For production evolution, open from the stored schema, apply deliberate changes through update-schema, and verify with d/schema. Chapter 11, Section 6.6 explains declarative convergence and ordered migrations.

13. Datomic and DataScript Differences

Datalevin's Datalog model is familiar to Datomic and DataScript users, but the schema mechanics differ:

14. Minimal Production Template

(def schema
  {:user/id    {:db/valueType :db.type/uuid
                :db/unique    :db.unique/identity
                :db/doc       "Stable external user id."}
   :user/email {:db/valueType :db.type/string
                :db/unique    :db.unique/identity
                :db/doc       "Login and notification email."}
   :user/name  {:db/valueType :db.type/string}

   :post/id    {:db/valueType :db.type/uuid
                :db/unique    :db.unique/identity}
   :post/author {:db/valueType :db.type/ref}
   :post/body  {:db/valueType            :db.type/string
                :db/fulltext             true
                :db.fulltext/autoDomain true}
   :post/tags  {:db/valueType   :db.type/keyword
                :db/cardinality :db.cardinality/many}})

(def conn
  (d/get-conn "/data/app" schema
              {:closed-schema? true}))

No examples for this chapter yet.