The article gives an introduction to data types in EvitaDB query language, including basic and complex types,
and provides code examples to demonstrate their usage.
This document lists all data types supported by evitaDB that can be used in
attributes or associated data
for storing client relevant information.
The string type is internally encoded with the character set UTF-8. evitaDB
query language and other I/O methods of evitaDB implicitly use this encoding.
Dates and times
The local variants of the date time — such as
LocalDateTime — are
first-class attribute data types. An attribute declared as LocalDateTime stores and returns exactly the wall-clock
value you wrote, and the entity schema records LocalDateTime as its data type — whether you declared the attribute
up front or let the schema evolve from the first value written.
A local date time carries no offset, so it does not identify a moment in time on its own — two clients in different
time zones reading the same value read the same wall clock, not the same instant. That is exactly what you want for
wall-clock data such as opening hours or a recurring schedule. If your data is time zone specific, work directly
with the
OffsetDateTime on the
client side and be explicit about the offset from the first day.
Why do we internally use OffsetDateTime for time information?
Offset/time zone handling varies from database to database. We wanted to avoid setting the timezone in session or
database configuration properties, as this mechanism is error-prone and impractical. Saving/loading date times with
timezone information would be the best option, but we run into problems with
parsing in certain
environments, and only the date with offset information seems to be widely supported. The offset information is good
enough for our case - it identifies a globally valid time that is known at the time the data value is stored.
Millisecond precision
evitaDB stores and matches temporal values at millisecond precision. An OffsetDateTime, LocalDateTime or
LocalTime is truncated to whole milliseconds the moment it enters the database — both when it is written as an
attribute value and when it is used as an argument of a query constraint. Digits below the millisecond are discarded
and cannot be read back.
The truncation happens on both paths on purpose. Cutting only the stored value would leave a nano-precise query probe
hunting a value that no longer exists, and the filter would silently match nothing. Because both halves are cut to the
same precision, a value written as 2026-05-20T12:19:26.123456789Z is stored as 2026-05-20T12:19:26.123Z and is
still found by a query written with the original, nano-precise value. Two values that differ only below the
millisecond become indistinguishable — they compare equal, sort as ties and share one index entry.
DateTimeRange follows the same rule. It compares, sorts and matches its boundaries at millisecond
precision, so two ranges whose boundaries differ only below the millisecond are the same range — they compare equal,
sort as ties and share one index entry. A range keeps the sub-millisecond digits of the boundary values you gave it,
but nothing in the database ever looks at them. (Ranges used to compare at whole-second granularity; they are
strictly more precise now, and two ranges less than a second apart are no longer treated as one.)
LocalDate is the only date/time type left out — it carries no sub-day component, so there is nothing to truncate.
The instant a temporal value denotes must be expressible as a whole number of milliseconds since the epoch in a
64-bit number. That range spans roughly ±292 million years around 1970, so every realistic calendar date fits
comfortably; only the JDK's own extreme sentinels — LocalDateTime.MIN / LocalDateTime.MAX and their
OffsetDateTime counterparts — fall outside it and are rejected with an error naming the value and the supported
range. Use an explicit finite boundary, or an open-ended DateTimeRange, instead of a sentinel. A
DateTimeRange boundary beyond that span is not rejected but clamped to it, which makes it behave exactly like an
open-ended boundary.
DateTimeRange
The DateTimeRange represents a specific implementation of the
defining from and to boundaries
by the OffsetDateTime data
types. The offset date times are written in the ISO format. Both boundaries are compared at
millisecond precision.
Range is written as:
when both boundaries are specified:
when a left boundary (since) is specified:
when a right boundary (until) is specified:
NumberRange
The NumberRange represents a specific implementation of the
defining from and to boundaries by the Number
data types. The supported number types are: Byte, Short, Integer, Long and BigDecimal.
Both boundaries of the number range must be of the same type - you cannot mix for example BigDecimal as lower bound
and Byte as upper bound.
Range is written as:
when both boundaries are specified:
when a left boundary (since) is specified:
when a right boundary (until) is specified:
Predecessor
The
is a special data type
used to define a single oriented linked list of entities of the same type. It represents a pointer to a previous entity
in the list. The head element is a special case and is represented by the constant Predecessor#HEAD. The predecessor
attribute can only be used in the attributes of an
entity or its reference to another entity. It cannot be used to filter entities, but is very useful for sorting.
Motivation for linked lists in database sorting
The linked list is a very optimal data structure for sorting entities in a database that holds large amounts of data.
Inserting a new element into a linked list is a constant time operation and requires only two updates:
inserting a new element into the list, pointing to an existing element as its predecessor
updating the original element pointing to the predecessor to point to the new element.
Moving (updating) an element or removing an existing element from a linked list is also a constant time operation,
requiring similar two updates. The disadvantage of the linked list is its poor random access performance (get element
at n-th index) and list traversal, which requires a lot of random access to different parts of memory. However, these
disadvantages can be mitigated by keeping the linked list in the form of an array or binary tree of properly positioned
primary keys.
Constructing a linked list could be a tricky process from a consistency point of view - especially in the
warm-up phase, when you need to reconstruct the data from an external primary store.
To be consistent at all times, you'd need to start with the entity that represents the head of the chain, then insert
its successor, and vice versa. This is often not trivial, and if you have two predecessor attributes with different
"order" for the same entities, it's absolutely impossible.
That's why we designed our linked list implementation to tolerate partial inconsistencies, and to converge to
a consistent state as missing data is inserted. We support these inconsistency scenarios:
multiple head elements
multiple successor elements for a single predecessor
circular dependencies, where a head element points to an element in its tail
The sorting by an inconsistent predecessor attribute sorts the entities by the chains in the following order:
the chains starting with a head element (starting with the chain with most elements, to the chain with least elements)
the chains with elements sharing the same predecessor (starting with the chain with most elements, to the chain with least elements)
the chains with circular dependencies (starting with the chain with most elements, to the chain with least elements)
When the dependencies are fixed, the sort order will converge to the correct one.
The will contain only
a single chain of correctly ordered elements and will return true when the isConsistent() method is called on it.
The inconsistent state is also allowed in the transactional phase, but we recommend avoiding it and updating all
the elements involved (in any order) within a single transaction, which will ensure that the linked list remains
consistent for all other transactions.
Complex data types
The complex types are types that don't qualify as simple evitaDB types (or an array of simple
evitaDB types). Complex types are stored in a
data structure that is
intentionally similar to the JSON data structure so that it can be easily converted to JSON format and can also accept
and store any valid JSON document.
Associated data may even contain array of complex objects. Such data will be automatically converted to an array of
ComplexDataObject types - i.e. ComplexDataObject[].