From 7e8ff60f2f04ecd3d28bff02c0e4c25b4ad84f72 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <pj@illuminatedcomputing.com>
Date: Tue, 17 Jun 2025 17:12:10 -0700
Subject: [PATCH v57 01/11] Add docs chapter for temporal tables

This commit tries to outline the complete functionality described by the
SQL:2011 standard, which we hope to achieve. So it includes sections for
Application Time and System Time, but it notes that System Time is not
yet implemented. Likewise it covers temporal primary keys and unique
constraints in detail, but it only notes that temporal update/delete are
not yet supported.

Temporal foreign keys and periods are documented in the next commits.

This commit also adds glossary entries for temporal table, application
time, and system time.

Author: Paul A. Jungwirth <pj@illuminatedcomputing.com>
---
 doc/src/sgml/filelist.sgml                |   1 +
 doc/src/sgml/glossary.sgml                |  47 +++++
 doc/src/sgml/images/Makefile              |   3 +-
 doc/src/sgml/images/temporal-entities.svg |  34 ++++
 doc/src/sgml/images/temporal-entities.txt |  16 ++
 doc/src/sgml/postgres.sgml                |   1 +
 doc/src/sgml/temporal.sgml                | 213 ++++++++++++++++++++++
 7 files changed, 314 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/images/temporal-entities.svg
 create mode 100644 doc/src/sgml/images/temporal-entities.txt
 create mode 100644 doc/src/sgml/temporal.sgml

diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index ac66fcbdb57..c7905ae0052 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -30,6 +30,7 @@
 <!ENTITY rangetypes SYSTEM "rangetypes.sgml">
 <!ENTITY rowtypes   SYSTEM "rowtypes.sgml">
 <!ENTITY syntax     SYSTEM "syntax.sgml">
+<!ENTITY temporal   SYSTEM "temporal.sgml">
 <!ENTITY textsearch SYSTEM "textsearch.sgml">
 <!ENTITY typeconv   SYSTEM "typeconv.sgml">
 
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 8651f0cdb91..f4d5783d1d5 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -81,6 +81,21 @@
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-application-time">
+   <glossterm>Application time</glossterm>
+   <glossdef>
+    <para>
+     In a <glossterm linkend="glossary-temporal-table">temporal table</glossterm>,
+     the dimension of time that represents when the entity described by the table
+     changed (as opposed to the table itself).
+    </para>
+    <para>
+     For more information, see
+     <xref linkend="temporal-tables"/>.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-aio">
    <glossterm>Asynchronous <acronym>I/O</acronym></glossterm>
    <acronym>AIO</acronym>
@@ -1847,6 +1862,22 @@
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-system-time">
+   <glossterm>System time</glossterm>
+   <glossdef>
+    <para>
+     In a <glossterm linkend="glossary-temporal-table">temporal table</glossterm>,
+     the dimension of time that represents when the table itself was changed
+     (as opposed to the entity the table describes).
+     Often used for auditing, compliance, and debugging.
+    </para>
+    <para>
+     For more information, see
+     <xref linkend="temporal-tables"/>.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-table">
    <glossterm>Table</glossterm>
    <glossdef>
@@ -1885,6 +1916,22 @@
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-temporal-table">
+   <glossterm>Temporal table</glossterm>
+   <glossdef>
+    <para>
+     <glossterm linkend="glossary-table">Tables</glossterm>
+     that track <glossterm linkend="glossary-application-time">application time</glossterm>
+     or <glossterm linkend="glossary-system-time">system time</glossterm> (or both).
+     Not to be confused with <glossterm linkend="glossary-temporary-table">temporary tables</glossterm>.
+    </para>
+    <para>
+     For more information, see
+     <xref linkend="temporal-tables"/>.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-temporary-table">
    <glossterm>Temporary table</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index 645519095d0..1d99d4e30c8 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -5,7 +5,8 @@
 ALL_IMAGES = \
 	genetic-algorithm.svg \
 	gin.svg \
-	pagelayout.svg
+	pagelayout.svg \
+	temporal-entities.svg
 
 DITAA = ditaa
 DOT = dot
diff --git a/doc/src/sgml/images/temporal-entities.svg b/doc/src/sgml/images/temporal-entities.svg
new file mode 100644
index 00000000000..d5ee6a77e77
--- /dev/null
+++ b/doc/src/sgml/images/temporal-entities.svg
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 970 280" width="970" height="280" shape-rendering="geometricPrecision" version="1.0">
+  <defs>
+    <filter id="f2" x="0" y="0" width="200%" height="200%">
+      <feOffset result="offOut" in="SourceGraphic" dx="5" dy="5"/>
+      <feGaussianBlur result="blurOut" in="offOut" stdDeviation="3"/>
+      <feBlend in="SourceGraphic" in2="blurOut" mode="normal"/>
+    </filter>
+  </defs>
+  <g stroke-width="1" stroke-linecap="square" stroke-linejoin="round">
+    <rect x="0" y="0" width="970" height="280" style="fill: #ffffff"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M385.0 133.0 L925.0 133.0 L925.0 63.0 L385.0 63.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M205.0 133.0 L205.0 203.0 L745.0 203.0 L745.0 133.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M385.0 63.0 L385.0 133.0 L25.0 133.0 L25.0 63.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M205.0 224.0 L205.0 237.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M25.0 224.0 L25.0 237.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M385.0 224.0 L385.0 237.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M565.0 224.0 L565.0 237.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M745.0 224.0 L745.0 237.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M925.0 224.0 L925.0 237.0 "/>
+    <text x="40" y="96" font-family="Courier" font-size="15" stroke="none" fill="#000000">products</text>
+    <text x="40" y="110" font-family="Courier" font-size="15" stroke="none" fill="#000000">(5, $5, [1 Jan 2020,1 Jan 2022))</text>
+    <text x="200" y="250" font-family="Courier" font-size="15" stroke="none" fill="#000000">2021</text>
+    <text x="220" y="166" font-family="Courier" font-size="15" stroke="none" fill="#000000">products</text>
+    <text x="220" y="180" font-family="Courier" font-size="15" stroke="none" fill="#000000">(6, $9, [1 Jan 2021,1 Jan 2024))</text>
+    <text x="20" y="250" font-family="Courier" font-size="15" stroke="none" fill="#000000">2020</text>
+    <text x="400" y="96" font-family="Courier" font-size="15" stroke="none" fill="#000000">products</text>
+    <text x="400" y="110" font-family="Courier" font-size="15" stroke="none" fill="#000000">(5, $8, [1 Jan 2022,))</text>
+    <text x="560" y="250" font-family="Courier" font-size="15" stroke="none" fill="#000000">2023</text>
+    <text x="380" y="250" font-family="Courier" font-size="15" stroke="none" fill="#000000">2022</text>
+    <text x="929" y="250" font-family="Courier" font-size="15" stroke="none" fill="#000000">...</text>
+    <text x="740" y="250" font-family="Courier" font-size="15" stroke="none" fill="#000000">2024</text>
+  </g>
+</svg>
diff --git a/doc/src/sgml/images/temporal-entities.txt b/doc/src/sgml/images/temporal-entities.txt
new file mode 100644
index 00000000000..b869682efd7
--- /dev/null
+++ b/doc/src/sgml/images/temporal-entities.txt
@@ -0,0 +1,16 @@
+
+
++-----------------------------------+-----------------------------------------------------+
+| cGRE                              | cGRE                                                |
+| products                          | products                                            |
+| (5, $5, [1 Jan 2020,1 Jan 2022))  | (5, $8, [1 Jan 2022,))                              |
+|                                   |                                                     |
++-----------------+-----------------+-----------------------------------+-----------------+
+                  | cGRE                                                |
+                  | products                                            |
+                  | (6, $9, [1 Jan 2021,1 Jan 2024))                    |
+                  |                                                     |
+                  +-----------------------------------------------------+
+
+|                 |                 |                 |                 |                 |
+2020              2021              2022              2023              2024              ...
diff --git a/doc/src/sgml/postgres.sgml b/doc/src/sgml/postgres.sgml
index af476c82fcc..5eb03505cf1 100644
--- a/doc/src/sgml/postgres.sgml
+++ b/doc/src/sgml/postgres.sgml
@@ -112,6 +112,7 @@ break is not needed in a wider output rendering.
   &textsearch;
   &mvcc;
   &perform;
+  &temporal;
   &parallel;
 
  </part>
diff --git a/doc/src/sgml/temporal.sgml b/doc/src/sgml/temporal.sgml
new file mode 100644
index 00000000000..5d1f697fee8
--- /dev/null
+++ b/doc/src/sgml/temporal.sgml
@@ -0,0 +1,213 @@
+<!-- doc/src/sgml/temporal.sgml -->
+
+ <chapter id="temporal-tables">
+  <title>Temporal Tables</title>
+
+  <indexterm zone="temporal-tables">
+   <primary>temporal</primary>
+  </indexterm>
+
+  <para>
+   Temporal tables allow users to track different dimensions of
+   history. Application time tracks the history of a thing out in the
+   world, and system time tracks the history of the database itself. This
+   chapter describes how to express and manage such histories in temporal
+   tables.
+  </para>
+
+ <sect1 id="application-time">
+  <title>Application Time</title>
+
+   <indexterm zone="application-time">
+    <primary>application time</primary>
+   </indexterm>
+
+   <para>
+    <firstterm>Application time</firstterm> refers to a history of the
+    entity described by a table. In a typical non-temporal table, there is
+    single row for each entity. In a temporal table, an entity may have
+    multiple rows, as long as those rows describe non-overlapping periods
+    from its history. Application time requires each row to have a start
+    and end time, expressing when the row is true.
+   </para>
+
+   <para>
+    Records in a temporal table can be plotted on a timeline, as in
+    <xref linkend="temporal-entities-figure"/>. Here we show three records
+    describing two products. Each record is a tuple with three attributes:
+    the id, the price, and the application time. So product 5 was first
+    offered for $5 starting January 1, 2020, but then became $8 starting
+    January 1, 2022. Its second record has no specified end time,
+    indicating that it is true indefinitely, or for all future time. The
+    last record shows that product 6 was introduced January 1, 2021 for $9,
+    then canceled January 1, 2024.
+   </para>
+
+   <figure id="temporal-entities-figure">
+    <title>Application Time Example</title>
+    <mediaobject>
+     <imageobject>
+      <imagedata fileref="images/temporal-entities.svg" format="SVG" width="100%"/>
+     </imageobject>
+    </mediaobject>
+   </figure>
+
+   <para>
+    In a table, these records would be:
+<programlisting>
+ id | price |        valid_at
+----+-------+-------------------------
+  5 |     5 | [2020-01-01,2023-01-01)
+  5 |     8 | [2023-01-01,)
+  6 |     9 | [2021-01-01,2024-01-01)
+</programlisting>
+   </para>
+
+   <para>
+    We show the application time using rangetype notation, because it
+    is stored as a single column (either a range or multirange). By
+    convention ranges include their start point but exclude their end
+    point. That way two adjacent ranges cover all points without
+    overlapping.
+   </para>
+
+   <para>
+    In principle, a table with application-time ranges/multiranges is
+    equivalent to a table that stores application-time "instants": one for
+    each second, millisecond, nanosecond, or whatever finest granularity is
+    available. But such a table would contain far too many rows, so
+    ranges/multiranges offer an optimization to represent the same
+    information in a compact form. In addition, ranges and multiranges
+    offer a more convenient interface for typical temporal operations,
+    where records change infrequently enough that separate "versions"
+    persist for extended periods of time.
+   </para>
+
+  <sect2 id="application-time-primary-keys">
+   <title>Temporal Primary Keys and Unique Constraints</title>
+
+   <para>
+    A table with application time has a different concept of entity
+    integrity than a non-temporal table. Temporal entity integrity can be
+    enforced with a temporal primary key. A regular primary key has at
+    least one element, all elements are <literal>NOT NULL</literal>, and
+    the combined value of all elements is unique. A temporal primary key
+    also has at least one such element, but in addition it has a final
+    element that is a rangetype or multirangetype that shows when it was
+    true. The regular parts of the key must be unique for any moment in
+    time, but non-unique records are allowed if their application time does
+    not overlap.
+   </para>
+
+   <para>
+    The syntax to create a temporal primary key is as follows:
+
+<programlisting>
+ALTER TABLE products
+  ADD CONSTRAINT products_pkey
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+</programlisting>
+
+    In this example, <literal>id</literal> is the non-temporal part of
+    the key, and <literal>valid_at</literal> is a range column containing
+    the application time. You can also create the primary key as part of
+    the <link linkend="sql-createtable"><literal>CREATE
+    TABLE</literal></link> command.
+   </para>
+
+   <para>
+    The <literal>WITHOUT OVERLAPS</literal> column must be <literal>NOT
+    NULL</literal> (like the other parts of the key). In addition it may
+    not contain empty values: a rangetype of <literal>'empty'</literal> or
+    a multirange of <literal>{}</literal>. An empty application time would
+    have no meaning.
+   </para>
+
+   <para>
+    It is also possible to create a temporal unique constraint that is
+    not a primary key. The syntax is similar:
+
+<programlisting>
+ALTER TABLE products
+  ADD CONSTRAINT products_id_valid_at_key
+  UNIQUE (id, valid_at WITHOUT OVERLAPS);
+</programlisting>
+
+    You can also create the unique constraint as part of the <link
+linkend="sql-createtable"><literal>CREATE TABLE</literal></link>
+command.
+   </para>
+
+   <para>
+    Temporal unique constraints also forbid empty ranges/multiranges
+    for their application time, although that column is permitted to be
+    null (like other elements of the key).
+   </para>
+
+   <para>
+    Temporal primary keys and unique constraints are backed by
+    <link linkend="gist">GiST indexes</link> rather than B-Tree indexes. In
+    practice, creating a temporal primary key or exclusion constraint
+    requires installing the <xref linkend="btree-gist"/> extension, so that
+    the database has opclasses for the non-temporal parts of the key.
+   </para>
+
+   <para>
+    Temporal primary keys and unique constraints have the same behavior
+    as <xref linkend="ddl-constraints-exclusion"/>, where each regular key
+    part is compared with equality, and the application time is compared
+    with overlaps, for example <literal>EXCLUDE USING gist (id WITH =,
+    valid_at WITH &amp;&amp;)</literal>. The only difference is that they
+    also forbid an empty application time.
+   </para>
+  </sect2>
+
+  <sect2 id="application-time-update-delete">
+   <title>Temporal Update and Delete</title>
+
+   <para>
+    <productname>PostgreSQL</productname> does not yet support special
+    syntax to update and delete portions of history in temporal tables.
+   </para>
+  </sect2>
+  </sect1>
+
+  <sect1 id="system-time">
+   <title>System Time</title>
+
+   <indexterm zone="system-time">
+    <primary>system time</primary>
+   </indexterm>
+
+   <para>
+    <firstterm>System time</firstterm> refers to the history of the
+    database table, not the entity it describes. It captures when each row
+    was inserted/updated/deleted.
+   </para>
+
+   <para>
+    Like application time, system time has two timestamps: a start time
+    and an end time. The start time shows when the row was added (either by
+    an insert or an update), and the end time shows when it stopped being
+    asserted (either by an update or a delete). The database maintains
+    these values automatically; the user is not able to set them.
+   </para>
+
+   <para>
+    If a query filters rows for those with a system time containing a
+    given moment in time, the result is equivalent to a non-temporal table
+    from that moment. In that way, you can ask what the table asserted at
+    different times in the past. This is useful for auditing, compliance,
+    and debugging.
+   </para>
+
+   <para>
+    <productname>PostgreSQL</productname> does not currently support
+    system time, but there are several extensions that provide its
+    functionality. See
+    <ulink url="https://wiki.postgresql.org/wiki/SQL2011Temporal">the SQL:2011
+    Temporal wiki page</ulink> for possibilities.
+   </para>
+  </sect1>
+
+ </chapter>
-- 
2.39.5

