Oninit® UDR — tzdte: Time zone data
Where the rules come from, how generations work, how to install a new release, and how to find and repair values after a rule change.
Where the rules live
Zone rules exist in exactly one place: tables in each database, loaded from
bundles Oninit builds from the IANA time zone database. The operating system's
zone files and the server's TZ are never consulted, so the same SQL
gives the same answer on Linux and AIX, on a primary and on every secondary,
and in a restored archive.
| Table | Detail |
|---|---|
tzdte_zones | Zone names and ids only. Append-only and global: an id never changes name and a name never changes id. |
tzdte_generations | One row per installed IANA generation: id, tzdata version, release date if known, the generation checksum, and zone and alias counts. |
tzdte_zone_data | The compiled rules, keyed by generation and zone. A load only inserts. |
tzdte_meta | Which generation is current. Changed last in a load, and only forward. |
Generations
Each released IANA ruleset is a generation with an id Oninit assigns. The id is stored inside every value, next to the offset that was resolved when it was written, so a value can always be traced to the rules that produced it.
| Property | Detail |
|---|---|
| Immutable | An installed generation is never updated, replaced or deleted by a load. Loading a generation or tzdata version that is already installed is refused and changes nothing. |
| Kept | Old generations stay, because stored values may refer to them. Nothing scans your tables to decide whether a generation is still used. |
| Current | New conversions ('…Chicago'::tzdte, tzdte_from_local, tzdte_add_days, tzdte_now) use the current generation and record it. Installing an older generation (a restore) does not change which one is current. |
| Stored values | Default retrieval is the stored UTC instant plus the stored offset. It needs no rules, so it is unchanged by a new generation, a renamed zone or a missing generation. |
| Checksum | Each generation has a canonical SHA-256 over its id, version and every zone's name, link and rule checksum. tzdte_generation_checksum(gen) recomputes it and reports PASS, FAIL or MISSING. |
| Replication | Backups include the rules with the data. On an HDR secondary the tables arrive automatically: a generation loaded on the primary appears on the secondary with its checksum PASS, and earlier generations are unchanged (tested on Informix 14.10; RSS and SDS are not tested). With Enterprise Replication a generation loaded on one node also reached the other node with its checksum PASS (tested on a two-node domain), but rows arrive in no fixed order, so run tzdte_reload() on each node only after the data has arrived and the checksum reports PASS. The recommended method is to load the same bundles on every server and compare tzdte_current_generation(), tzdte_tzdata_version() and each generation's checksum across the domain. |
| Size | One generation is about 480 KB of compiled rules for 447 zones; the table rows and the in-memory cache are each under 1 MB per generation, and every installed generation is cached. |
| Zone ids | Assigned by the bundle and never reused or renumbered, so every server and database agrees on which id means which zone. Aliases keep their own id and the name the writer used. |
| Consistency | One statement sees one cache state. A concurrent reload cannot change the rules in the middle of a query. |
Installing a new release
- Obtain the new bundle:
tzdata_<version>.unl,tzdata_<version>.genand the manifest. Check each file againstunl_sha256andgen_sha256in the manifest. - Link the files as
tzdata.unlandtzdata.genand runtzdte_load_tzdata.sqlon each database. The load is one transaction: it validates every row and name, inserts the names, the generation and its rules, verifies the generation checksum, and moves the current marker last. Any failure rolls everything back and leaves the previous generation current. - Check it:
EXECUTE FUNCTION tzdte_generation_checksum(<gen>)should reportPASS. - Existing values are untouched and read exactly as before. Find the ones the new rules move (below) and recalculate or rebase them if you want.
New sessions pick up the new generation through sysdbopen().
tzdte_current_generation() and tzdte_tzdata_version()
show each database's current generation. A change to the rows after loading is
caught by tzdte_generation_checksum; the in-memory cache is built from
verified data and is rebuilt on the next reload.
Indexes that depend on rules
Functions that read only what a value stores give the same answer after any
load, so they are safe in functional indexes, check constraints and
fragmentation expressions: tzdte_utc, tzdte_local(t),
tzdte_offset(t), tzdte_zone,
tzdte_stored_generation, tzdte_iso(t) and
tzdte_text(t).
Functions that make a new conversion under the current generation
(tzdte_at, tzdte_from_local,
tzdte_add_days, tzdte_add_months,
tzdte_trunc_day, tzdte_rebase) and anything called with
RecalculateOffset, tzdte_recalc or
tzdte_rules_changed can change when the current generation does. Do
not index them, or rebuild such an index after each load. Expressions that
compare only instants are unaffected.
Finding and repairing affected values
Suppose a meeting at 09:00 London in 2027 is stored, and the UK then changes its daylight-time rules and a new generation is installed. The row does not change: it still reads 09:00 at its stored offset, and the instant is the same. What changed is what the rules now say for that wall-clock time. That is right for logs and audit records and wrong for schedules. You decide, per query, what to do:
-- which rows does the new generation move? SELECT COUNT(*) FROM appointments WHERE tzdte_rules_changed(starts_at); -- show the same instant under the current rules; the row is not touched SELECT tzdte_text(starts_at, 'RecalculateOffset') FROM appointments; -- for a schedule: keep the wall-clock promise (a new instant, a new generation) UPDATE appointments SET starts_at = tzdte_rebase(starts_at) WHERE tzdte_rules_changed(starts_at);
Schedulers should rebase after each load, or store a local
DATETIME plus a zone name instead. No rule change ever rewrites a
row by itself.
Unload and load
UNLOAD writes the local time at the stored
offset and, for named zones, the generation (… America/Chicago
#1). LOAD rebuilds the identical instant, zone, offset and
generation, so a dump taken before a tzdata update reloads cleanly whatever the
rules are now. If that generation is not installed in the target database, the
offset is accepted as the value's provenance because it cannot be checked.
Text without a # generation is a new conversion: its offset must
agree with the zone's rules in the current generation, or it is an error.
dbexport and dbimport use the same text, so the generation
is kept and the rules tables travel with the data. A plain dbimport of the
export stops with error 201, because dbexport writes the two
DATETIME casts without their qualifier; import with
tools/tzdte_dbimport.sh, which repairs them (see the Install page).
Afterwards check tzdte_generation_checksum(<gen>) for every generation.