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.
An OffsetDateTime is compared, sorted and indexed by the moment it denotes. Two values denoting the same moment at
different offsets — 2024-01-01T11:00+02:00 and 2024-01-01T10:00+01:00 — sort as ties, whether you order by the
attribute itself or by a sortable attribute compound containing it. The tie is broken like any other tie in the
ordering: by the next element of the compound, or by the primary key in the ordering direction. The value you read back
keeps the offset you wrote.
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. Two ranges whose boundaries differ by a millisecond or more are
different ranges, even when the boundaries fall within the same second.
LocalDate is the only date/time type left out — it carries no sub-day component, so there is nothing to truncate.
evitaDB holds every temporal value as a whole number of milliseconds since the epoch in a 64-bit number. That window
spans roughly ±292 million years around 1970, so every realistic calendar date fits comfortably. A value outside it is
clamped to its nearest edge — in practice only the JDK's own extreme sentinels such as OffsetDateTime.MAX, which
therefore work as "since forever" / "until forever" markers.
The window spans from -292275055-05-16T16:47:06Z to +292278994-08-17T07:12:54.999Z. An OffsetDateTime or
LocalDateTime outside it is clamped to its nearest edge, both when it is written as an attribute value and when it
is used as an argument of a query constraint. This concerns the JDK's own extreme sentinels — LocalDateTime.MIN /
LocalDateTime.MAX and their OffsetDateTime counterparts: OffsetDateTime.MAX is stored as the latest supported
instant, keeping its -18:00 offset (+292278994-08-16T13:12:54.999-18:00), it is read back as that value, and
attributeEquals("validUntil", OffsetDateTime.MAX) finds it, because the query argument is clamped to the very same
value. A LocalDateTime is clamped as the wall clock at UTC. A DateTimeRange boundary beyond the window is clamped
to it as well, which makes it behave exactly like an open-ended boundary.
The gRPC API narrows the window further: it carries temporal values as a protobuf Timestamp, which spans
0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. A value outside that range leaves the server as its
first or last second. A value reaching the server at exactly 0001-01-01T00:00:00Z, or anywhere within the last
second of the range, is taken for the MIN / MAX sentinel of its type (for a LocalDate, the first or last day of
the range is). Every other moment of year 1 or year 9999 is stored exactly as it was sent. The Java client therefore
round-trips OffsetDateTime.MAX as OffsetDateTime.MAX, where the embedded API reads back the clamped value.
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[].