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 to current_timestamp; the application never writes it explicitly).
  • valid_to: the instant from which this version is no longer current (set to the "infinity" sentinel 9999-12-31 23:59:59 for the live record; closed to current_timestamp when 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 all TIMESTAMPTZ values as "YYYY-MM-DD HH:MM:SS.ffffff+00".
  • C++ domain layer: std::chrono::system_clock::time_point is 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 T is not the wire shape, even though ISO 8601 permits it.
  • The Z UTC 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_point field, 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_clock epoch 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 version column (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.

Emacs 29.3 (Org mode 9.6.15)