Skip to main content

Temporal Data Types

LoraDB has six first-class temporal types. Each can be stored as a node or relationship property, compared, ordered, and used in arithmetic with Duration.

TypeComponentsTimezone
Dateyear, month, day
Timehour, minute, second, nanosecondUTC offset
LocalTimehour, minute, second, nanosecond
DateTimeDate + Time fieldsUTC offset
LocalDateTimeDate + LocalTime fields
Durationmonths, days, seconds, nanoseconds

See Temporal Functions for the full constructor, truncation, and arithmetic reference. This page focuses on the types.

Which one do I use?

SituationType
A calendar day (invoice date, birthday)Date
An instant with offset (event timestamp, audit log)DateTime
A wall-clock time with offsetTime
A naive local wall-clock timeLocalTime
A naive local moment (meeting at 10:00 "Amsterdam time")LocalDateTime
A span (90 minutes, 30 days, 2 weeks)Duration

When in doubt, use DateTime for instants and Date for calendar days — they cover most real-world needs.

Writing temporals

Literals via constructor

There are no bare temporal literals — always use a constructor:

CREATE (e:Event {
title: 'Launch',
at: datetime('2026-05-01T09:00:00Z'),
day: date('2026-05-01'),
clock: localtime('09:00:00'),
runs_for: duration('PT90M')
})

Component maps

CREATE (d:Day {
on: date({year: 2024, month: 1, day: 15})
})

See more in Temporal Functions → Constructors.

From host language

Every binding ships a helper so you don't have to write constructor strings manually:

Comparison and ordering

Values of the same temporal type are totally ordered.

MATCH (e:Event)
WHERE e.at >= datetime() AND e.at < datetime() + duration('P7D')
RETURN e
ORDER BY e.at

Different temporal types are not cross-comparable — convert first or compare in matching units.

Arithmetic

  • Date + DurationDate
  • DateTime + DurationDateTime
  • DateTime - DateTimeDuration
RETURN date('2024-01-15') + duration('P30D')
-- 2024-02-14

RETURN datetime('2025-01-01T00:00:00Z') - datetime('2024-01-01T00:00:00Z')
-- P366D (a Duration — 2024 is a leap year)

Duration is calendar-aware: duration('P1M') is "one month" (variable length in days), not exactly 30 days. Use duration('P30D') for a fixed 30-day window.

Component access

WITH datetime('2024-01-15T10:30:45Z') AS dt
RETURN dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second

For the full list see Temporal Functions → component access.

Serialisation

Across host-language bindings, temporals are tagged:

TypeShape
Date{kind: "date", iso: "YYYY-MM-DD"}
Time{kind: "time", iso: "HH:MM:SS.nnnnn+ZZ:ZZ"}
LocalTime{kind: "localtime", iso: "HH:MM:SS.nnnnn"}
DateTime{kind: "datetime", iso: "YYYY-MM-DDTHH:MM:SS.nnnnn+ZZ:ZZ"}
LocalDateTime{kind: "localdatetime", iso: "YYYY-MM-DDTHH:MM:SS.nnnnn"}
Duration{kind: "duration", iso: "P…"}

Use the host-language helpers (date(), datetime(), duration() in each binding — see Node, Python) to build these values without touching the tagged shape manually.

Examples

Events in the next week

MATCH (e:Event)
WHERE e.at >= datetime() AND e.at < datetime() + duration('P7D')
RETURN e.title, e.at
ORDER BY e.at

Bucketed by month

MATCH (e:Event)
RETURN date.truncate('month', e.on) AS month, count(*) AS events
ORDER BY month

Age from birthday

MATCH (p:Person)
RETURN p.name,
duration.inDays(p.born, date()) / 365 AS approx_age_years

Duration arithmetic

CREATE (m:Meeting {
start: datetime('2026-05-01T09:00:00Z'),
len: duration('PT1H30M')
})

MATCH (m:Meeting)
RETURN m.start, m.start + m.len AS end

Active in last 30 days

MATCH (u:User)
WHERE u.last_seen >= datetime() - duration('P30D')
RETURN count(*) AS active_30d

Group by year of birth

MATCH (p:Person) WHERE p.born IS NOT NULL
RETURN p.born.year AS year, count(*) AS people
ORDER BY year

Window query — past N days

MATCH (e:Event)
WHERE e.at >= datetime() - duration({days: $days})
RETURN e
ORDER BY e.at DESC

Bind $days as an integer from the host — the duration({days: …}) constructor accepts a variable, unlike the ISO string form duration('P7D') which must be a literal.

Age-bracket bucketing

MATCH (p:Person)
WITH p, duration.inDays(p.born, date()) / 365 AS age_years
RETURN CASE
WHEN age_years < 18 THEN 'minor'
WHEN age_years < 65 THEN 'adult'
ELSE 'senior'
END AS bracket,
count(*) AS people
ORDER BY people DESC

Uses CASE to bucket a numeric age.

Retention cohort

MATCH (u:User)-[:SIGNED_UP_ON]->(d:Day)
WITH date.truncate('month', d.on) AS cohort, u
OPTIONAL MATCH (u)-[:LOGGED_IN]->(l:Login)
WHERE l.at >= datetime() - duration('P30D')
RETURN cohort,
count(DISTINCT u) AS total,
count(DISTINCT CASE WHEN l IS NOT NULL THEN u END) AS active_30d
ORDER BY cohort

Edge cases

Date arithmetic on month-ends

Duration calendar-aware arithmetic handles month-end clamping:

RETURN date('2024-01-31') + duration('P1M')    -- 2024-02-29
RETURN date('2024-03-31') + duration('P1M') -- 2024-04-30

Timezone-aware comparison

DateTime values in different offsets compare by the same UTC instant — ordering is timezone-safe.

RETURN datetime('2024-01-01T12:00:00Z') =
datetime('2024-01-01T13:00:00+01:00')
-- true

Cross-type comparison

Date and DateTime aren't directly comparable. Convert via component reconstruction:

MATCH (e:Event)
WHERE date({year: e.at.year, month: e.at.month, day: e.at.day}) = date('2024-01-15')
RETURN e

duration('P1M') vs duration('P30D')

Calendar-aware vs fixed:

RETURN date('2024-02-15') + duration('P1M')    -- 2024-03-15
RETURN date('2024-02-15') + duration('P30D') -- 2024-03-16

Storing as string

If you only want ISO string storage, use String — but then sorting by date requires parsing on every comparison. Prefer the typed form.

Limitations

  • date.truncate supports only "year" and "month".
  • datetime.truncate supports only "day", "hour", "month".
  • Parsing is strict ISO 8601 — non-ISO shapes (MM/DD/YYYY, RFC-2822) are rejected.
  • Arithmetic between different temporal types (e.g. Date - Time) is not supported.

See also