Skip to main content

Temporal Functions (Dates, Times, Durations)

LoraDB supports the Cypher temporal model end-to-end — see Temporal Data Types for the type details. Each value is first-class: store it as a property, compare it, do arithmetic on it.

Overview

GoalFunction
Current date/timetemporal.today(), temporal.now('date'), temporal.now() / now(), temporal.now('time'), temporal.now('local_time'), temporal.now('local_datetime')
Parse ISO string'…'::DATE, '…'::DATETIME, etc.
From components{year, month, day}::DATE, …
Construct duration'P…'::DURATION, {days, hours, }::DURATION
Truncatetemporal.truncate(unit, value)
Differencetemporal.between(a, b), temporal.in_days(a, b)
Component accessdt.year, dt.month, dt.hour, dur.days
Add/subtractdate + duration, datetime - datetime

Temporal types at a glance

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

Construction And Current Time

Construct temporal values with casts. value::TYPE is compact for handwritten Cypher, while CAST(value AS TYPE) is also supported by the Cypher grammar. TRY_CAST(value AS TYPE) returns null instead of reporting a conversion error.

The zero-argument current-value helpers also have bare aliases: now() for temporal.now(), timestamp() for temporal.timestamp(), and timezone() for temporal.timezone().

There are two separate jobs here:

  • Current-time helpers create a value from the database clock.
  • Casts create or convert a value from query text, maps, parameters, or other expressions.

Avoid wrapping an already-cast value in an old constructor-shaped helper. For example, write $value::DATETIME, not datetime($value::DATETIME) or temporal.datetime($value::DATETIME).

Current-time helpers

HelperReturnsUse when
temporal.today()DATEYou need the current calendar day.
temporal.now('date')DATEEquivalent current-day form when the kind is parameterized.
temporal.now() / now()DATETIMEYou need the current instant with timezone offset.
temporal.now('time')TIMEYou need only the current time-of-day with offset.
temporal.now('local_time')LOCAL_TIMEYou need a wall-clock time without timezone.
temporal.now('local_datetime')LOCAL_DATETIMEYou need date and wall-clock time without timezone.
temporal.timestamp() / timestamp()INTEGERYou need Unix epoch milliseconds.
temporal.timezone() / timezone()STRINGYou need the database timezone label, currently UTC.

Use temporal.now() for stored instants such as created_at and updated_at. Use temporal.today() for date-only concepts such as birthdays, billing days, and cohort dates. Use local variants only when the value is intentionally a wall-clock value rather than an absolute instant.

Date

FormExample
Current daytemporal.today()
ISO string'2024-01-15'::DATE
Map{year: 2024, month: 1, day: 15}::DATE
CAST formCAST('2024-01-15' AS DATE)
RETURN temporal.today();                         // today
RETURN '2024-01-15'::DATE;                       // 2024-01-15
RETURN {year: 2024, month: 1, day: 15}::DATE;    // 2024-01-15
RETURN TRY_CAST($maybe_date AS DATE)            // null on invalid input

DateTime

FormExample
Current instanttemporal.now() / now()
ISO string'2024-01-15T10:00:00Z'::DATETIME
Map{year, month, day, hour, minute, second, millisecond, timezone}::DATETIME
Local current instanttemporal.now('local_datetime')
RETURN '2024-01-15T10:00:00Z'::DATETIME;
RETURN {year: 2024, month: 1, day: 15, hour: 10, minute: 0}::DATETIME;
RETURN '2024-01-15T10:00:00+02:00'::DATETIME

Time / LocalTime / LocalDateTime

RETURN '12:34:56'::TIME;                 // with UTC offset (default Z)
RETURN '12:34:56+02:00'::TIME;
RETURN '12:34:56'::LOCAL_TIME;           // no timezone
RETURN '2024-01-15T10:00:00'::LOCAL_DATETIME;
RETURN temporal.now('time');
RETURN temporal.now('local_time');
RETURN temporal.now('local_datetime')

duration

ISO 8601 string or a component map.

RETURN 'P30D'::DURATION;                         // 30 days
RETURN 'P1Y2M3DT4H5M6S'::DURATION;               // full form
RETURN 'PT90M'::DURATION;                        // 90 minutes
RETURN {years: 1, months: 2, days: 3}::DURATION; // equivalent map form
RETURN CAST('PT90M' AS DURATION)                // CAST form

Query casts vs parameters

Every binding ships a helper so you can pass typed values in host-language parameter maps without writing query casts:

// Node.js / WASM
import { datetime, duration } from "@loradb/lora-node";

await db.execute(
"CREATE (:Event {at: $at, len: $len})",
{ at: datetime("2026-05-01T09:00:00Z"), len: duration("PT90M") }
);

See Node → typed helpers and Python → parameters.

Component access

Temporal values expose components via property access.

RETURN '2024-01-15'::DATE.year;                    // 2024
RETURN '2024-01-15'::DATE.month;                   // 1
RETURN '2024-01-15T10:30:00Z'::DATETIME.hour;      // 10
RETURN '2024-01-15T10:30:45Z'::DATETIME.second;    // 45
RETURN 'P30D'::DURATION.days;                      // 30
RETURN 'P1Y'::DURATION.months                     // 12

Available: .year, .month, .day, .hour, .minute, .second, .millisecond, .days, .months, .years, .hours, .minutes, .seconds.

Build a year-month key

MATCH (e:Event)
RETURN e.at.year AS year,
     e.at.month AS month,
     count(*) AS events
ORDER BY year, month

Truncation

Reduce a temporal value to a coarser unit.

FunctionSupported units
temporal.truncate(unit, date)"year", "month", "day"
temporal.truncate(unit, datetime)"year", "month", "day", "hour"
RETURN temporal.truncate('month', '2024-01-15'::DATE);       // 2024-01-01
RETURN temporal.truncate('year',  '2024-07-01'::DATE);       // 2024-01-01
RETURN temporal.truncate('hour', '2024-01-15T10:42:00Z'::DATETIME)
      // 2024-01-15T10:00:00Z

Bucketing rows

MATCH (e:Event)
RETURN temporal.truncate('month', e.at) AS month, count(*) AS events
ORDER BY month
MATCH (r:Request)
RETURN temporal.truncate('hour', r.at) AS hour, count(*) AS hits
ORDER BY hour

Arithmetic

  • Date + DurationDate
  • DateTime + DurationDateTime
  • DateTime - DateTimeDuration

Duration arithmetic preserves calendar semantics: months and days are stored separately from seconds.

RETURN '2024-01-15'::DATE + 'P30D'::DURATION
;          // 2024-02-14

RETURN '2024-01-15T00:00:00Z'::DATETIME + 'PT36H'::DURATION
;          // 2024-01-16T12:00:00Z

RETURN '2024-12-31T00:00:00Z'::DATETIME - '2024-01-01T00:00:00Z'::DATETIME
        // P365D (a Duration)

Calendar vs fixed durations

'P1M'::DURATION is "one month" — a variable number of days. 'P30D'::DURATION is exactly 30 days.

RETURN '2024-01-31'::DATE + 'P1M'::DURATION;     // 2024-02-29 (leap year)
RETURN '2024-01-31'::DATE + 'P30D'::DURATION    // 2024-03-01

temporal.between / temporal.in_days

RETURN temporal.between('2024-01-01'::DATE, '2024-12-31'::DATE)
;       // P365D (Duration)

RETURN temporal.in_days('2024-01-01'::DATE, '2024-04-10'::DATE)
     // 100

temporal.in_days is for DATE values. For DATETIME values, use temporal.between(a, b).days when you need the day component.

Comparison

Comparable within the same type using <, <=, >, >=, =, <>. Cross-type comparisons (e.g. Date vs DateTime) return null.

MATCH (e:Event)
WHERE e.at >= temporal.now() AND e.at < temporal.now() + 'P7D'::DURATION
RETURN e
ORDER BY e.at
MATCH (p:Person)
WHERE p.born < '1900-01-01'::DATE
RETURN p.name, p.born

Storing temporal values

Temporals serialise tagged: {"kind": "date", "iso": "2024-01-15"} etc. (see Temporal Data Types). They round-trip cleanly through CREATE and MATCH.

CREATE (e:Event {
title:    'Launch',
at:       '2026-05-01T09:00:00Z'::DATETIME,
runs_for: 'PT90M'::DURATION,
day:      '2026-05-01'::DATE
});

MATCH (e:Event)
RETURN e.title,
     e.at,
     e.at + e.runs_for AS ends_at

Common patterns

Events in the next week

MATCH (e:Event)
WHERE e.at >= temporal.now()
AND e.at <  temporal.now() + 'P7D'::DURATION
RETURN e
ORDER BY e.at

Events in a month

MATCH (e:Event)
WHERE temporal.truncate('month', e.at) = '2026-05-01'::DATE
RETURN e

Age from birthday

MATCH (p:Person)
RETURN p.name,
     temporal.in_days(p.born, temporal.today()) / 365 AS approx_age_years

Rolling 30-day active users

MATCH (u:User)-[:VIEWED]->(:Page)
WHERE u.last_seen >= temporal.now() - 'P30D'::DURATION
RETURN count(DISTINCT u) AS active_30d

Session length

MATCH (s:Session)
RETURN s.id, (s.ended - s.started) AS duration
ORDER BY duration DESC

First / last event per user

MATCH (u:User)-[:DID]->(e:Event)
RETURN u.id,
     min(e.at) AS first_event,
     max(e.at) AS last_event

Cohorts by signup month

MATCH (u:User)
RETURN temporal.truncate('month', u.created) AS cohort,
     count(*)                           AS signups
ORDER BY cohort

"Since last seen" bucket

MATCH (u:User)
WITH u,
   temporal.between(u.last_seen, temporal.now()).days AS days_away
RETURN CASE
       WHEN days_away <= 1   THEN 'today'
       WHEN days_away <= 7   THEN 'week'
       WHEN days_away <= 30  THEN 'month'
       ELSE                       'dormant'
     END AS freshness,
     count(*) AS users
ORDER BY users DESC

Uses CASE to bucket a continuous duration into named tiers.

Time-of-day histogram

MATCH (e:Event)
RETURN e.at.hour AS hour, count(*) AS events
ORDER BY hour

Component access on a DateTime returns integers — no string parsing needed.

Recurring window — "same time next week"

MATCH (m:Meeting {id: $id})
RETURN m.start,
     m.start + 'P7D'::DURATION AS next_week,
     m.start + 'P14D'::DURATION AS two_weeks

Build ISO timestamp for serialisation

MATCH (e:Event)
RETURN e.id, e.at::STRING AS iso

CAST(e.at AS STRING) / e.at::STRING on a DateTime emits a round-trippable ISO 8601 string.

Edge cases

Mixing types

Date - DateTime, Time + Duration — not supported. Convert first to matching types.

Timezone handling

DateTime carries a UTC offset. Compare DateTime values across zones freely — they're normalised to UTC internally. LocalDateTime has no zone; two LocalDateTime values compare by naive wall-clock order.

Strict ISO parsing

Non-ISO shapes (MM/DD/YYYY, RFC-2822, ISO week-dates) are rejected at parse time. Normalise on the host side before passing to '…'::DATE / '…'::DATETIME.

temporal.today() with no args — now vs wall clock

In WASM, temporal.today() resolves to Date.now() at millisecond precision — nanosecond fields are zero. In native builds, it reflects the OS clock. See WASM → gotchas.

Limitations

  • temporal.truncate supports "year", "month", and "day" for DATE values; "year", "month", "day", and "hour" for DATETIME values.
  • Arithmetic between values of different temporal types (e.g. Date - Time) is not supported. Convert first.
  • Parsing is strict ISO 8601 — non-ISO shapes (MM/DD/YYYY, RFC-2822) are rejected.
  • No component-access shortcuts on Duration beyond the listed fields.

See also