xocolatl/periods

repository·master·Indexed 43 days ago

https://github.com/xocolatl/periods

A PostgreSQL extension (compatible with 9.5–15) that implements SQL:2016 temporal features, specifically Periods and SYSTEM VERSIONING. It provides tools to manage valid time ranges, track historical data changes, and perform temporal queries using helper functions for AS OF and BETWEEN logic. Features include WITHOUT OVERLAPS unique constraints, system-time period configuration, and automated history table management.

Tokens
2.4K
Snippets
7
Records
8
Agent score
6%

What's inside xocolatl-periods

  1. Configure SYSTEM_TIME and System Versioning

    master

    A SYSTEM_TIME period tracks when rows were valid in the system. The columns used for this (start and end) are managed by triggers: the start column is set to transaction_timestamp() and the end column is set to 'infinity'.

    Best Practice: Use timestamp with time zone for these columns to avoid issues with TimeZone or Daylight Savings changes.

    Setup

    Use periods.add_system_time_period to create the columns and the period. You can also exclude specific columns from triggering a new historical row when they are updated.

    System Versioning

    To enable full system versioning (which maintains a separate history table for all changes), call periods.add_system_versioning after setting up the SYSTEM_TIME period.

    -- 1. Create table
    CREATE TABLE example (
        id bigint PRIMARY KEY,
        value text
    );
    
    -- 2. Add SYSTEM_TIME period (columns are created automatically)
    SELECT periods.add_system_time_period('example', 'row_start', 'row_end');
    
    -- 3. (Optional) Exclude columns from triggering history updates
    SELECT periods.set_system_time_period_excluded_columns('example', ARRAY['last_login']);
    
    -- 4. Enable System Versioning to create a history table
    SELECT periods.add_system_versioning('example');
  2. Manage access control and schema changes for system-versioned tables

    master

    System-versioned tables involve a base table and a history table.

    Access Control

    • The history table and helper functions inherit the ownership and privileges of the base table.
    • History data is read-only.
    • To perform maintenance on history data (like trimming old rows), you must temporarily suspend system versioning.

    Altering Tables

    To prevent errors during DDL operations, you should suspend system versioning, perform your ALTER TABLE commands on both the base table and the history table, and then re-enable versioning.

    Warning: Re-activating versioning will verify that the history table remains compatible with the base table.

    -- How to safely trim old history data
    BEGIN;
    SELECT periods.drop_system_versioning('t');
    GRANT DELETE ON TABLE t TO CURRENT_USER;
    DELETE FROM t_history WHERE system_time_end < now() - interval '1 year';
    SELECT periods.add_system_versioning('t');
    COMMIT;
    
    -- How to safely alter a versioned table
    BEGIN;
    SELECT periods.drop_system_versioning('t');
    ALTER TABLE t ADD COLUMN new_col text;
    ALTER TABLE t_history ADD COLUMN new_col text;
    SELECT periods.add_system_versioning('t');
    COMMIT;
  3. Update a portion of a period

    master

    The extension allows you to update or delete only a specific portion of a period. When updating a portion, the extension uses an INSTEAD OF trigger on a generated view to split or modify rows as needed.

    Note:

    • The table must have a primary key.
    • It is illegal to modify the two columns that actually define the period during a portion update.
    • Deleting portions of periods is currently not supported.

    To use this, target the view named {table_name}__for_portion_of_{period_name}.

    -- If the table is 'example' and the period is 'validity'
    UPDATE example__for_portion_of_validity
    SET ..., 
        start_date = ..., 
        end_date = ...
    WHERE ...;
  4. Define a period on a table

    master

    A period is a definition on a table that specifies a name and two columns (start and end). The start column's value must be strictly less than the end column's value, and both must be non-null. The period includes the start value but excludes the end value.

    Because this extension cannot modify PostgreSQL's grammar, use the periods.add_period function to define a period on an existing table.

    -- 1. Create the table
    CREATE TABLE example (
        id bigint,
        start_date date,
        end_date date
    );
    
    -- 2. Add the period using the extension function
    SELECT periods.add_period('example', 'validity', 'start_date', 'end_date');
  5. Add unique constraints with periods (WITHOUT OVERLAPS)

    master

    You can include a period in PRIMARY KEY or UNIQUE constraints to ensure that no two rows for the same identifier have overlapping periods.

    Use periods.add_unique_key to implement this. The extension creates a unique constraint over the specified columns and the period columns, and uses a GIST exclusion constraint to enforce the WITHOUT OVERLAPS logic.

    If you have an existing constraint (like a primary key) that you want to use as the basis for the period uniqueness, provide its name via the unique_constraint parameter.

    -- Basic unique key with a period
    CREATE TABLE example (
        id bigint,
        start_date date,
        end_date date
    );
    SELECT periods.add_period('example', 'validity', 'start_date', 'end_date');
    SELECT periods.add_unique_key('example', ARRAY['id'], 'validity');
    
    -- Using an existing primary key
    CREATE TABLE example (
        id bigint,
        start_date date,
        end_date date,
        CONSTRAINT example_pkey PRIMARY KEY (id, start_date, end_date)
    );
    SELECT periods.add_period('example', 'validity', 'start_date', 'end_date');
    SELECT periods.add_unique_key('example', ARRAY['id'], 'validity', unique_constraint => 'example_pkey');
  6. Perform temporal queries (AS OF, BETWEEN)

    master

    To query data as it existed at a specific point in time or during a specific range, use the extension's helper functions. These simulate the SQL standard FOR SYSTEM_TIME syntax.

    GoalFunctionSQL Equivalent Example
    Point in timet__as_of('...')SELECT * FROM t FOR system_time AS OF '...'
    Ranget__from_to('...', '...')SELECT * FROM t FOR system_time FROM '...' TO '...'
    Ranget__between('...', '...')SELECT * FROM t FOR system_time BETWEEN '...' AND '...'
    Symmetric Ranget__between_symmetric('...', '...')SELECT * FROM t FOR system_time BETWEEN SYMMETRIC '...' AND '...'
    -- Query data as of a specific timestamp
    SELECT * FROM example__as_of('2023-01-01 00:00:00+00');
    
    -- Query data within a specific time range
    SELECT * FROM example__between('2023-01-01', '2023-02-01');
  7. Add foreign keys to period-based unique keys

    master

    You can create foreign keys that point to unique keys containing a period. Instead of listing all referenced columns, you provide the name of the unique key and the period name.

    -- example2 has a foreign key pointing to the unique key 'example_id_validity' on table 'example'
    SELECT periods.add_foreign_key('example2', 'ARRAY[ex_id]', 'validity', 'example_id_validity');
  8. Use period predicates (contains, overlaps, etc.)

    master

    The extension provides inlined functions to perform temporal logic comparisons. Because the extension cannot change SQL syntax, you must pass the start and end columns explicitly instead of using the period name.

    LogicFunctionSQL Equivalent Example
    Containsperiods.contains(s, e, val)WHERE t.p CONTAINS 42
    Containsperiods.contains(s, e, s2, e2)WHERE t.p CONTAINS u.q
    Equalsperiods.equals(s, e, s2, e2)WHERE t.p EQUALS u.q
    Overlapsperiods.overlaps(s, e, s2, e2)WHERE t.p OVERLAPS u.q
    Precedesperiods.precedes(s, e, s2, e2)WHERE t.p PRECEDES u.q
    Succeedsperiods.succeeds(s, e, s2, e2)WHERE t.p SUCCEEDS u.q
    Immediately Precedesperiods.immediately_precedes(s, e, s2, e2)WHERE t.p IMMEDIATELY PRECEDES u.q
    Immediately Succeedsperiods.immediately_succeeds(s, e, s2, e2)WHERE t.p IMMEDIATELY SUCCEEDS u.q