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
- Schema is optional. Undefined attributes are created on write and store values as generic EDN data.
:db/cardinalitydefaults to:db.cardinality/one.- Attribute names must be keywords.
- Built-in schema includes
:db/ident, which is unique and keyword-valued. It also includes internal/system attributes such as:db/created-at,:db/updated-at,:db/fn, and:db/udf. :db/created-atand:db/updated-atare maintained only when a database is opened with{:auto-entity-time? true}. They store epoch milliseconds as:db.type/longvalues and represent database mutation time, not domain event time.- Use the store option
{:closed-schema? true}to reject transactions that mention attributes not already defined in the schema.
3. Schema-Related Store Options
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:
(d/get-conn "/data/app"
schema
{:validate-data? true
:closed-schema? true})
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 indexes use
:db/tupleAttrsto derive an index entry from existing attributes. Application code does not transact the composite tuple value directly; Datalevin maintains it from the component attributes in the normal Datalog indexes. - Stored tuple values use
:db.type/tuplewith:db/tupleTypeor:db/tupleTypeswhen the tuple itself is application data and its elements usually do not need to stand alone as separate facts.
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]
:db/unique :db.unique/identity}}
Rules for :db/tupleAttrs:
- It must be a non-empty sequential collection.
- The tuple attribute must be cardinality one.
- It cannot depend on another tuple attribute.
- It cannot depend on a cardinality-many attribute.
- The derived tuple uses the same tuple value encoding as a transacted typed tuple, so tuple element limitations still apply.
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:
- Stored tuple values are transacted as vectors.
:db.type/tuplemust include:db/tupleTypeor:db/tupleTypeswhen the tuple value is stored directly.- Tuple element declarations support these scalar Datalevin value types:
:db.type/string,:db.type/bigdec,:db.type/bigint,:db.type/boolean,:db.type/double,:db.type/float,:db.type/long,:db.type/ref,:db.type/instant,:db.type/uuid,:db.type/keyword, and:db.type/symbol. :db/tupleTypemust be one supported tuple element type. It describes a homogeneous tuple: any number of elements, all of the same type.:db/tupleTypesmust contain more than one supported tuple element type. It describes a heterogeneous tuple: the vector arity must match the declaration.- Tuple elements cannot themselves be
:db.type/tuple,:db.type/vec, or:db.type/idoc. - Variable-length tuple elements, such as strings, must fit within Datalevin's tuple element encoding limit of 255 bytes. Keep stored tuples small; use ordinary attributes or document values when the structure is open-ended.
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:
- The attribute must have
:db/valueType :db.type/string. :db.embedding/domainsmust be a non-empty sequence of non-blank strings.- If no explicit domain is given, the attribute participates in the default
"datalevin"embedding domain. - If
:db.embedding/domainsis present, the attribute participates in those domains instead of the default unless"datalevin"is listed. :db.embedding/autoDomain trueadds an attribute-specific embedding domain using the same slash-to-underscore naming rule as vector domains.- Changing
:db/embedding,:db.embedding/domains, or:db.embedding/autoDomainon a populated attribute is rejected; rebuild explicitly instead.
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
{:new/name {:db/valueType :db.type/string}}
#{:old/name})
(d/update-schema conn nil nil
{:old/name :new/name})
Secondary-index metadata deserves separate planning. Domain and path settings for full-text, vector, embedding, and idoc indexes determine which existing datoms participate in extra indexes. A live schema update should not be treated as an automatic backfill under a new indexing policy: Datalevin rejects embedding and idoc path-index changes on populated attributes, and other domain changes should be planned with the relevant feature chapter's re-index or rebuild procedure.
Important mutation rules:
- Adding
:db/valueTypeto a previously untyped populated attribute is allowed and migrates existing EDN values to the typed encoding when possible. - Changing an existing typed
:db/valueTypeis rejected when data exists for that attribute. - Changing
:db/cardinalityfrom many to one is rejected when data exists for that attribute. - Adding
:db/uniqueto a populated attribute is allowed only if existing values do not violate uniqueness. - Deleting a schema attribute is allowed only when no datoms are associated with it.
- Renaming changes the attribute identity in the schema and stored datoms.
- Full-text, vector, embedding, and idoc secondary-index metadata changes may require explicit re-indexing or rebuild planning for existing data.
- Embedding schema changes on populated attributes require an explicit rebuild.
- Idoc path indexing changes on populated attributes require an explicit rebuild.
13. Datomic and DataScript Differences
Datalevin's Datalog model is familiar to Datomic and DataScript users, but the schema mechanics differ:
- Schema is a map of maps, not a vector of transaction maps.
- Schema is passed to
get-conn,create-conn,empty-db, orinit-db, and later changed withupdate-schema. - Undefined attributes are allowed unless
:closed-schema? trueis set. - Unspecified value types are stored as EDN data.
- AVE indexing is automatic for every attribute.
- Use
:db/identfor named enum/system entities. - Use
:db.attr/predsfor attribute-local value predicates, and use:db/ensurefor explicit entity invariants over the would-be transaction result.
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}))
User Examples
Log in to create examplesNo examples for this chapter yet.
