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.
Bitemporality
ORE Studio uses a bitemporal data model for all reference and trading entities. Each row in the database carries two independent time axes:
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.
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).
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.
The UTC Standard
All timestamps in ORE Studio are UTC, everywhere.
- PostgreSQL: every connection runs
SET TIME ZONE 'UTC'on acquire (tenant_aware_pool::acquire()). 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: timestamps serialise as ISO 8601 UTC strings
with a
Zsuffix (e.g."2026-06-03 10:57:32Z"). - 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.
Time Types at Each Layer
PostgreSQL column C++ entity (DB layer) C++ domain ───────────────── ───────────────────── ────────── TIMESTAMPTZ NOT NULL → db_timestamp → time_point TIMESTAMPTZ (nullable) → std::optional<db_timestamp> → std::optional<time_point>
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. With the
session timezone forced to UTC, the text representation returned to the
C++ client is always "YYYY-MM-DD HH:MM:SS.ffffff+00".
C++ entity layer: db_timestamp
// ores.database/repository/db_types.hpp using db_timestamp = sqlgen::Timestamp<"%Y-%m-%d %H:%M:%S">; // which is rfl::Timestamp<"%Y-%m-%d %H:%M:%S">
db_timestamp is a serialisation shim — its sole job is to let
sqlgen (via rfl reflection) read and write PostgreSQL timestamp strings.
Internally it wraps a std::tm (a C struct of integer fields) and uses
strptime / strftime with the format "%Y-%m-%d %H:%M:%S".
Important properties and limitations:
strptimeparses"2026-06-03 10:57:32.ffffff+00"up to the seconds, stopping at the fractional-second dot. The parse succeeds and the+00suffix is ignored. The resultingstd::tmholds the correct wall-clock fields.db_timestamphas no concept of UTC or timezone. Whether thestd::tmrepresents UTC or local time depends entirely on which conversion function is subsequently called.- A default-constructed
db_timestamp(std::tm{}) represents a near-epoch date (1900-01-01) — not the application epoch 1970. Callingtimegmon a zero-std::tmreturns a large negativetime_t. The oldtimestamp_to_timepoint(const db_timestamp&)implementation usedts.tm() → timegmdirectly and silently produced epoch (or negative) values on a failed parse. This is the root cause of the "56 years ago" history display bug. - Entity structs must declare
valid_fromandvalid_toas plaindb_timestamp(notstd::optional<db_timestamp>) because the DB columns areNOT NULL. Declaring them optional meant sqlgen could silently leave them unpopulated, masking parse failures.
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.
- Converted from
db_timestampexactly once at the mapper boundary.
Conversion Functions
All conversions live in ores.database/repository/mapper_helpers.hpp
and ores.platform/time/datetime.hpp.
timestamp_to_timepoint (read path: entity → domain)
// The single standard conversion function. // Parses a PostgreSQL timestamp string into a UTC time_point. // Throws std::invalid_argument on any parse failure (empty string, // missing UTC designator, invalid date). inline std::chrono::system_clock::time_point timestamp_to_timepoint(std::string_view timestamp_str); // Overload for db_timestamp entity fields. // Formats tm_ back to "YYYY-MM-DD HH:MM:SS" via strftime, appends // "+00" to assert UTC intent, then delegates to from_iso8601_utc. // Throws on a zero-initialised or otherwise invalid db_timestamp. inline std::chrono::system_clock::time_point timestamp_to_timepoint(const db_timestamp& ts);
Both overloads ultimately call platform::time::datetime::from_iso8601_utc.
They throw rather than returning epoch silently.
Mapper usage (read side)
// NOT NULL field (db_timestamp): r.recorded_at = timestamp_to_timepoint(v.valid_from); // Nullable field (optional<db_timestamp>): if (v.completed_at) r.completed_at = timestamp_to_timepoint(*v.completed_at);
datetime::to_db_string (write path: domain → entity)
// ores.platform/time/datetime.hpp // Formats a time_point as "YYYY-MM-DD HH:MM:SS" (no timezone suffix). // PostgreSQL accepts this for TIMESTAMPTZ when the session is UTC. std::string datetime::to_db_string( const std::chrono::system_clock::time_point& tp);
This is the only function used to write timestamps to entity fields.
The old timepoint_to_timestamp(tp, lg) helper (which returned an
empty db_timestamp on failure — silent epoch) has been removed.
Mapper usage (write side)
// NOT NULL field: entity.started_at = datetime::to_db_string(domain.started_at); // Nullable field: if (domain.completed_at) entity.completed_at = datetime::to_db_string(*domain.completed_at);
NATS / JSON protocol: time_point_parser.hpp
For over-the-wire serialisation, a custom rfl parser is registered in
ores.utility/rfl/time_point_parser.hpp:
- Write:
time_point→to_iso8601_utc→"2026-06-03 10:57:32Z" - Read: ISO 8601 string with
Z,+00, or+00:00suffix →from_iso8601_utc→time_point. Throws on missing UTC designator.
This parser is invoked automatically when rfl serialises or deserialises
any struct containing a time_point field (NATS request/response types,
domain JSON I/O).
The Infinity Sentinel
The sentinel value "9999-12-31 23:59:59" 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.
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'::timestamptz; $$ LANGUAGE sql IMMUTABLE;
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
C++: MAX_TIMESTAMP constant
// ores.database/repository/helpers.hpp inline constexpr const char* MAX_TIMESTAMP = "9999-12-31 23:59:59";
Every C++ repository file that must compare against the sentinel (sqlgen
WHERE clauses, make_timestamp calls) must use MAX_TIMESTAMP. The
default values in entity structs also use this string:
db_timestamp valid_from = "9999-12-31 23:59:59"; // matches MAX_TIMESTAMP db_timestamp valid_to = "9999-12-31 23:59:59"; // matches MAX_TIMESTAMP
These two constants are the only authorised representations of the sentinel. They both resolve to the same value; the split exists because PostgreSQL and C++ each have their own canonicalisation mechanism.
Repository WHERE Clauses
Queries that filter on valid_to or valid_from compare
db_timestamp entity column values against db_timestamp constants:
const auto max(make_timestamp(MAX_TIMESTAMP, lg())); // "current" records: where("valid_to"_c == max.value()) | ... const auto ts(make_timestamp(as_of, lg())); // "as-of" query: where("valid_from"_c <= ts.value() && "valid_to"_c > ts.value()) | ...
make_timestamp parses the string into a db_timestamp and throws if
parsing fails. sqlgen compares the column (db_timestamp) with the
value (db_timestamp) using rfl's type machinery. This is
type-safe and consistent.
Current State vs Target State
Current state (after unification story tasks 1–4)
The commit 1ad3f36a1 replaced db_timestamp entity fields with
std::string, removed the db_timestamp overload of
timestamp_to_timepoint, and updated WHERE clauses to compare strings
directly. This was reverted as incorrect (see below).
The actual fixes landed:
0446e9536:timestamp_to_timepoint(const db_timestamp&)now routes throughfrom_iso8601_utcand throws on failure (fixes the epoch bug).529baf7d7:currency_entityvalid_from=/=valid_tochanged fromoptional<db_timestamp>todb_timestamp(fixes the optional bug for currency).
Target state
The target state for the unify-entity-timestamps story is:
- All entity NOT-NULL timestamp fields are
db_timestamp(notoptional<db_timestamp>). The optional was never correct for NOT NULL columns; removing it means sqlgen always populates the field and a missed read surfaces as an error rather than epoch. - All entity nullable timestamp fields are
std::optional<db_timestamp>. These map to genuinely nullable DB columns. timepoint_to_timestampis deleted. Every mapper write site usesdatetime::to_db_string(tp)instead.timepoint_to_timestampreturned an emptydb_timestampon failure (silent epoch on write).- (Stretch goal) Entity timestamp fields migrate from
db_timestamptostd::chrono::system_clock::time_pointdirectly. This requires confirming that sqlgen correctly handlestime_pointvia the registered rfl custom parser. If it does, mappers become a simple assignment (r.recorded_at = v.valid_from) with no explicit conversion. This eliminatesdb_timestampfrom the entity layer entirely and makes the type chain:TIMESTAMPTZ → time_point → time_pointwith no intermediate.
See also
- LLM instructions — links to all architectural entry points.
- 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/db_types.hpp—db_timestamptypedef.ores.database/repository/mapper_helpers.hpp—timestamp_to_timepointoverloads.ores.database/repository/helpers.hpp—make_timestamp,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.ores.utility/rfl/time_point_parser.hpp— JSON/NATS serialisation oftime_point.