Time and Timestamps: Architecture and Conventions
Table of Contents
Time is a first-class concern in ORE Studio. Every persisted entity carries a full audit trail, the database schema is bitemporal, and all services must agree on a single timezone standard. This article documents the decisions, the types used at each layer, and the conversion functions that bridge them. Return to Knowledge.
1. Bitemporality
ORE Studio uses a bitemporal data model for all reference and trading entities. Each row in the database carries two independent time axes:
1.1. Valid time (valid_from / valid_to)
Valid time records when the fact was true in the real world.
valid_from: the instant from which this version of the record is authoritative (set by the INSERT trigger tocurrent_timestamp; the application never writes it explicitly).valid_to: the instant from which this version is no longer current (set to the "infinity" sentinel9999-12-31 23:59:59for the live record; closed tocurrent_timestampwhen the record is updated or deleted).
Together they form an open interval [valid_from, valid_to). A query
for "current" data filters valid_to = MAX_TIMESTAMP. A query for
the state at a historical instant t filters
valid_from < t AND valid_to > t=.
The application never writes valid_from or valid_to directly.
The database trigger manages both columns. Entity structs therefore
declare them as read-only from the application's perspective:
valid_from and valid_to are populated on SELECT and ignored on
INSERT/UPDATE.
1.2. Transaction time (recorded_at)
recorded_at is the domain object field that surfaces valid_from to
the application layer. The mapper reads it from valid_from on SELECT
and never writes it back (the trigger owns it).
1.3. Audit fields
Every persisted entity also carries:
| Field | Who writes it | Meaning |
|---|---|---|
modified_by |
Application (stamp) | Authenticated actor |
performed_by |
Application (stamp) | Service account |
change_reason_code |
Application / client | Reason for the mutation |
change_commentary |
Client | Free-text audit note |
recorded_at |
DB trigger (read only) | When this version was written |
The stamp() helper in handler_helpers.hpp fills modified_by,
performed_by, and change_reason_code on every write.
2. The UTC Standard
All timestamps in ORE Studio are UTC, everywhere.
- PostgreSQL: every connection runs
SET TIME ZONE 'UTC'when it opens, the one setting a connection keeps for its life (A transaction sets its own tenant context, so any connection serves any tenant). PostgreSQL therefore returns allTIMESTAMPTZvalues as"YYYY-MM-DD HH:MM:SS.ffffff+00". - C++ domain layer:
std::chrono::system_clock::time_pointis UTC by definition (C++20). - NATS / JSON protocol: every instant crosses the wire in The Wire Shape below and nowhere else.
- User interface: Qt converts to local time only for display; all
internal passing uses
time_point.
There is no local-time arithmetic anywhere in the codebase.
3. The Wire Shape
Every instant that crosses the wire is written in exactly one shape:
YYYY-MM-DD HH:MM:SS[.ffffff]Z
- A space separates the date from the time. A
Tis not the wire shape, even though ISO 8601 permits it. - The
ZUTC designator is always present. - The decimal fraction is optional and carries one to six digits of
microseconds. The server's writer (
to_iso8601_utc) always states six digits; its reader (parse_utc_timestamp) also accepts one to five, and none, because a text column states none when the microsecond is zero. - The date and the time are UTC. The bytes do not depend on the timezone of the process, the reading session, or the browser.
This page is the single definition of the shape. The server implements it in
ores.platform/time/datetime.cpp, which the generated codecs call for JSON and
NATS. The TypeScript client mirrors it in
projects/ores.web/packages/wire-protocol/src/time.ts. Neither reference
restates the grammar. Where a client and this page disagree, this page and
the server are right, and the client is the bug; a pattern widened to admit
whatever happens to arrive is the same drift with a later alarm.
4. Time Types at Each Layer
PostgreSQL column C++ entity (DB layer) C++ domain ───────────────── ───────────────────── ────────── TIMESTAMPTZ NOT NULL → time_point → time_point TIMESTAMPTZ (nullable) → std::optional<time_point> → std::optional<time_point>
The instant has one C++ type from the column to the domain. No layer holds it as text.
4.1. PostgreSQL: TIMESTAMPTZ
All timestamp columns are declared TIMESTAMP WITH TIME ZONE NOT NULL
(or nullable where the DB schema allows NULL). PostgreSQL stores
timestamps internally as UTC microseconds since 2000-01-01; the
timezone is only relevant for display and input parsing. The repositories
read and bind timestamps in the binary format, which is that count of
microseconds, so no text and no session timezone take part. Every
transaction still sets TimeZone to UTC, for the hand-written statements
that read a timestamp as text.
4.2. C++ entity layer: std::chrono::system_clock::time_point
A generated repository entity holds each timestamp column as a time_point,
or a std::optional<time_point> for a nullable column. The row decoder turns
the binary column into the time_point directly, and the statement binds a
time_point parameter in the binary format.
- A NOT NULL column has a plain
time_pointfield, so a row that lacks the value fails to decode; it never becomes a default time. - A nullable column has an optional field, and SQL NULL is
std::nullopt.
4.3. C++ domain layer: std::chrono::system_clock::time_point
The canonical time type in ORE Studio. All domain structs use
time_point (or std::optional<time_point> for nullable instants).
- UTC by definition (C++20 guarantees
system_clockepoch is Unix epoch in UTC). - Supports arithmetic and comparison.
- Strongly typed — no accidental local/UTC confusion.
- The mapper assigns it from the entity field; there is no conversion.
5. Conversion Functions
The text conversions live in ores.platform/time/datetime.hpp. They serve the
wire and the hand-written statements that read or write a timestamp as text;
the repositories do not use them.
5.1. datetime::to_iso8601_utc and datetime::from_iso8601_utc
The writer and the reader of The Wire Shape. The reader accepts the wire
shape and the same text with the +00 or +00:00 designator that PostgreSQL
states, and throws on a missing UTC designator, an empty string or an invalid
date. It never returns the epoch in place of a failure.
5.2. datetime::to_db_string
Formats a time_point as "YYYY-MM-DD HH:MM:SS.ffffff", six digits of
microseconds and no designator, for a text parameter of a hand-written
statement. The transaction's TimeZone = UTC makes PostgreSQL read it as UTC.
5.3. NATS / JSON protocol
The generated JSON and MessagePack codecs write and read a time_point field
through to_iso8601_utc and from_iso8601_utc, so the grammar is The Wire
Shape above and is not restated there:
- Write:
time_point→to_iso8601_utc→ the wire shape. - Read: the wire shape →
from_iso8601_utc→time_point. It throws on a missing UTC designator.
5.4. Microsecond precision
The microsecond survives every layer. The binary column is a count of
microseconds, the entity and the domain hold a time_point, and the wire
shape carries six digits. A time_point therefore survives a round trip
through the database or the wire to the microsecond, and loses only what a
microseconds duration cannot hold.
An instant that falls between two microseconds is taken down to the one it
falls in, in every path that writes it: the binary codec, to_iso8601_utc and
therefore to_db_string. The rule is one rule because the paths are read
against each other: a hand-written statement passes an instant as text and the
server parses it, so a lookup by that text must find the row a generated
repository wrote through the codec, and the two agree only if they chose the
same microsecond.
6. The Infinity Sentinel
The sentinel value represents "this record is currently valid, with no
known expiry date". It is used in valid_to for all live bitemporal
records.
This value must be referenced via its canonical constant or function in each layer. Never hard-code the literal string directly.
6.1. PostgreSQL: ores_utility_infinity_timestamp_fn()
-- ores.sql/create/utility/utility_functions_create.sql CREATE OR REPLACE FUNCTION ores_utility_infinity_timestamp_fn() RETURNS timestamptz AS $$ SELECT '9999-12-31 23:59:59+00'::timestamptz; $$ LANGUAGE sql IMMUTABLE;
The function returns an explicit UTC timestamp (with +00 offset) to
remain timezone-independent when PostgreSQL optimizes the predicate at
plan time. Every SQL file that references the sentinel — triggers, stored
procedures, populate scripts, WHERE clauses in raw SQL strings — must
call ores_utility_infinity_timestamp_fn(). Grep to audit:
# Any remaining literal usage in SQL is a bug: grep -rn "'9999-12-31" projects/ores.sql --include="*.sql" | grep -v build
6.2. C++: MAX_TIMESTAMP
// ores.database/repository/helpers.hpp inline constexpr const char* MAX_TIMESTAMP = "9999-12-31 23:59:59+00";
MAX_TIMESTAMP is the sentinel as text, for a text parameter of a hand-written
statement. Its +00 offset matches ores_utility_infinity_timestamp_fn(), so
a comparison does not depend on the session timezone. A statement that can
name the sentinel in SQL calls the function instead.
The database INSERT trigger sets valid_from and valid_to on every write,
so the sentinel an entity holds before a write never reaches the database. A
generated row defaults both members to db_timestamp::max() all the same, so a
row read back and a row built in memory mean the same thing by "never".
7. Repository WHERE Clauses
A generated statement filters live records by comparing valid_to with
ores_utility_infinity_timestamp_fn() in its SQL, and filters an as-of read
by binding the instant as a time_point parameter:
-- live records: ... where valid_to = ores_utility_infinity_timestamp_fn() -- as of $1: ... where valid_from <= $1 and valid_to > $1
The column and the parameter are both TIMESTAMPTZ, so the comparison is on
instants, never on text.
8. See also
- Data storage — the structure note that orders this cluster, and where to read this page in it.
- LLM instructions — links to all architectural entry points.
- Optimistic Concurrency Versioning: the version=0 Overload and Why
It's a Problem — how the
versioncolumn (bitemporal SCD2's per-write sequence number) is checked at insert time, and an undocumented bypass that conflates four different write intents. - Temporal Composite Entity Versioning: Target State — versioning a parent when a child writes, the sibling gap in the same insert trigger.
- PostgreSQL Architecture — database schema, roles, session conventions,
extensions, and utility functions including
ores_utility_infinity_timestamp_fn(). - PostgreSQL Row-Level Security — tenant isolation and service-role separation via RLS policies.
ores.database/repository/helpers.hpp—MAX_TIMESTAMP.ores.sql/create/utility/utility_functions_create.sql—ores_utility_infinity_timestamp_fn()definition.ores.platform/time/datetime.hpp—from_iso8601_utc,to_db_string,to_iso8601_utc.