GithubHelp home page GithubHelp logo

xocolatl / periods Goto Github PK

View Code? Open in Web Editor NEW
252.0 18.0 17.0 315 KB

PERIODs and SYSTEM VERSIONING for PostgreSQL

License: PostgreSQL License

PLpgSQL 95.37% Makefile 0.11% C 4.52%
postgresql periods sql-standards system-versioning extension proof-of-concept

periods's Introduction

Periods and SYSTEM VERSIONING for PostgreSQL

License Code of Conduct

compatible 9.5–15

This extension recreates the behavior defined in SQL:2016 (originally in SQL:2011) around periods and tables with SYSTEM VERSIONING. The idea is to figure out all the rules that PostgreSQL would like to adopt (there are some details missing in the standard) and to allow earlier versions of PostgreSQL to simulate the behavior once the feature is finally integrated.

What is a period?

A period is a definition on a table which specifies a name and two columns. The period’s name cannot be the same as any column name of the table.

-- Standard SQL

CREATE TABLE example (
    id bigint,
    start_date date,
    end_date date,
    PERIOD FOR validity (start_date, end_date)
);

Defining a period constrains the two columns such that the start column’s value must be strictly inferior to the end column’s value, and that both columns be non-null. The period’s value includes the start value but excludes the end value. A period is therefore very similar to PostgreSQL’s range types, but a bit more restricted.

Since extensions cannot modify PostgreSQL’s grammar, we use functions, views, and triggers to get as close to the same thing as possible.

CREATE TABLE example (
    id bigint,
    start_date date,
    end_date date
);
SELECT periods.add_period('example', 'validity', 'start_date', 'end_date');

Unique constraints

Periods may be part of PRIMARY KEYs and UNIQUE constraints.

-- Standard SQL

CREATE TABLE example (
    id bigint,
    start_date date,
    end_date date,
    PERIOD FOR validity (start_date, end_date),
    UNIQUE (id, validity WITHOUT OVERLAPS)
);
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');

The extension will create a unique constraint over all of the columns specified and the two columns of the period given. It will also create an exclusion constraint using gist to implement the WITHOUT OVERLAPS part of the constraint. The function also takes optional parameters if you already have such a constraint that you would like to use.

-- Standard SQL

CREATE TABLE example (
    id bigint,
    start_date date,
    end_date date,
    PERIOD FOR validity (start_date, end_date),
    CONSTRAINT example_pkey PRIMARY KEY (id, validity WITHOUT OVERLAPS)
);
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');

Unique constraints may only contain one period.

Foreign keys

If you can have unique keys with periods, you can also have foreign keys pointing at them.

SELECT periods.add_foreign_key('example2', 'ARRAY[ex_id]', 'validity', 'example_id_validity');

In this example, we give the name of the unique key instead of listing out the referenced columns as you would in normal SQL.

Portions

The SQL standard allows syntax for updating or deleting just a portion of a period. Rows are inserted as needed for the portions not being updated or deleted. Yes, that means a simple DELETE statement can actually INSERT rows!

-- Standard SQL

UPDATE example
FOR PORTION OF validity FROM '...' TO '...'
SET ...
WHERE ...;

DELETE FROM example
FOR PORTION OF validity FROM '...' TO '...'
WHERE ...;

When updating a portion of a period, it is illegal to modify either of the two columns contained in the period. This extension uses a view with an INSTEAD OF trigger to figure out what portion of the period you would like to modify, and issue the correct DML on the underlying table to do the job.

In order to use this feature, the table must have a primary key.

UPDATE example__for_portion_of_validity
SET ...,
    start_date = ...,
    end_date = ...
WHERE ...;

We see no way to simulate deleting portions of periods, alas.

Predicates

The SQL standard provides for several predicates on periods. We have implemented them as inlined functions for the sake of completeness but they require specifying the start and end column names instead of the period name.

-- Standard SQL and this extension's equivalent

-- "t" and "u" are tables with respective periods "p" and "q".
-- Both periods have underlying columns "s" and "e".

WHERE t.p CONTAINS 42
WHERE periods.contains(t.s, t.e, 42)

WHERE t.p CONTAINS u.q
WHERE periods.contains(t.s, t.e, u.s, u.e)

WHERE t.p EQUALS u.q
WHERE periods.equals(t.s, t.e, u.s, u.e)

WHERE t.p OVERLAPS u.q
WHERE periods.overlaps(t.s, t.e, u.s, u.e)

WHERE t.p PRECEDES u.q
WHERE periods.precedes(t.s, t.e, u.s, u.e)

WHERE t.p SUCCEEDS u.q
WHERE periods.succeeds(t.s, t.e, u.s, u.e)

WHERE t.p IMMEDIATELY PRECEDES u.q
WHERE periods.immediately_precedes(t.s, t.e, u.s, u.e)

WHERE t.p IMMEDIATELY SUCCEEDS u.q
WHERE periods.immediately_succeeds(t.s, t.e, u.s, u.e)

System-versioned tables

SYSTEM_TIME

If the period is named SYSTEM_TIME, then special rules apply. The type of the columns must be date, timestamp without time zone, or timestamp with time zone; and they are not modifiable by the user. In the SQL standard, the start column is GENERATED ALWAYS AS ROW START and the end column is GENERATED ALWAYS AS ROW END. This extension uses triggers to set the start column to transaction_timestamp() and the end column is always 'infinity'.

Note: It is generally unwise to use anything but timestamp with time zone because changes in the TimeZone configuration paramater or even just Daylight Savings Time changes can distort the history. Even when only using UTC, we recommend the timestamp with time zone type.

-- Standard SQL

CREATE TABLE example (
    id bigint PRIMARY KEY,
    value text,
    PERIOD FOR system_time (row_start, row_end)
);
CREATE TABLE example (
    id bigint PRIMARY KEY,
    value text
);
SELECT periods.add_system_time_period('example', 'row_start', 'row_end');

Note that the columns in this special case need not exist. They will be created both by the SQL standard and by this extension. A special function is provided as a convenience, but add_period can also be called.

Excluding columns

It might be desirable to prevent some columns from updating the SYSTEM_TIME values. For example, perhaps your users table has a column last_login which gets updated all the time and you don’t want to generate a new historical row (see below) for just that. Ideally such a column would be in its own table, but if not then it can be excluded with an optional parameter:

SELECT periods.add_system_time_period(
            'example',
            excluded_column_names => ARRAY['foo', 'bar']);

Excluded columns can be define after the fact, as well.

SELECT periods.set_system_time_period_excluded_columns(
            'example',
            ARRAY['foo', 'bar']);

This functionality is not present in the SQL standard.

WITH SYSTEM VERSIONING

This special SYSTEM_TIME period can be used to keep track of changes in the table.

-- Standard SQL

CREATE TABLE example (
    id bigint PRIMARY KEY,
    value text,
    PERIOD FOR system_time (row_start, row_end)
) WITH SYSTEM VERSIONING;
CREATE TABLE example (
    id bigint PRIMARY KEY,
    value text
);
SELECT periods.add_system_time_period('example', 'row_start', 'row_end');
SELECT periods.add_system_versioning('example');

This instructs the system to keep a record of all changes in the table. We use a separate history table for this. You can create the history table yourself and instruct the extension to use it if you want to do things like add partitioning.

Temporal querying

The SQL standard extends the FROM and JOIN clauses to allow specifying a point in time, or even a range of time (shall we say a period of time?) for which we want the data. This only applies to base tables and so this extension implements them through inlined functions.

-- Standard SQL and this extension's equivalent

SELECT * FROM t FOR system_time AS OF '...';
SELECT * FROM t__as_of('...');

SELECT * FROM t FOR system_time FROM '...' TO '...';
SELECT * FROM t__from_to('...', '...');

SELECT * FROM t FOR system_time BETWEEN '...' AND '...';
SELECT * FROM t__between('...', '...');

SELECT * FROM t FOR system_time BETWEEN SYMMETRIC '...' AND '...';
SELECT * FROM t__between_symmetric('...', '...');

Access control

The history table as well as the helper functions all follow the ownership and access privileges of the base table. It is not possible to change the privileges independently. The history data is also read-only. In order to trim old data, SYSTEM VERSIONING must be suspended.

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;

The privileges are automatically fixed when system versioning is resumed.

Altering a table with system versioning

The SQL Standard does not say much about what should happen to a table with system versioning when the table is altered. This extension prevents you from dropping objects while system versioning is active, and other changes will be prevented in the future. The suggested way to make changes is:

BEGIN;
SELECT periods.drop_system_versioning('t');
ALTER TABLE t ...;
ALTER TABLE t_history ...;
SELECT periods.add_system_versioning('t');
COMMIT;

It is up to you to make sure you alter the history table in a way that is compatible with the main table. Re-activating system versioning will verify this.

Future

Completion

This extension is pretty much feature complete, but there are still many aspects that need to be handled.

Performance

Performance for the temporal queries should be already very similar to what we can expect from a native implementation in PostgreSQL.

Unique keys should also be as performant as a native implementation, except that two indexes are needed instead of just one. One of the goals of this extension is to fork btree to a new access method that handles the WITHOUT OVERLAPS and then patch that back into PostgreSQL when periods are added.

Foreign key performance should mostly be reasonable, except perhaps when validating existing data. Some benchmarks would be helpful here.

Performance for the DDL stuff isn’t all that important, but those functions will likely also be rewritten in C, if only to start being the patch to present to the PostgreSQL community.

Contributions

Contributions are very much welcome!

If you would like to help implement the missing features, optimize them, rewrite them in C, and especially modify btree; please don’t hesitate to do so.

This project adheres to the PostgreSQL Community Code of Conduct.

Released under the PostgreSQL License.

Acknowledgements

The project would like extend special thanks to:

periods's People

Contributors

df7cb avatar mlt avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

periods's Issues

Capitalised role throws error on add_system_versioning

Hi,

Trying to add system versioning to a table owned by the role 'TestUser' throws an exception.

PostgreSQL 13 on Windows
Periods extension compiled locally from source
SQL to reproduce:

CREATE ROLE "TestUser";

CREATE TABLE public."TestTable"
(
    "Id" bigint NOT NULL,
    CONSTRAINT "TestTable_pkey" PRIMARY KEY ("Id")
);

ALTER TABLE public."TestTable" owner to "TestUser";

select periods.add_system_time_period('public."TestTable"', 'FromDate', 'ToDate');
select periods.add_system_versioning('public."TestTable"');

Exception thrown:

ERROR:  role ""TestUser"" does not exist
CONTEXT:  SQL statement "ALTER TABLE public."TestTable_history" OWNER TO """TestUser""""
PL/pgSQL function periods.add_system_versioning(regclass,name,name,name,name,name,name) line 147 at EXECUTE
SQL state: 42704

Looking at the sql in periods--1.2.sql, as far as I can tell the table_owner variable will already double quote the user name when converted to text, so the $I formatting in the execute statement causes another double quote, hence the error.

table_owner is set here on line 2410:

    SELECT n.nspname, c.relname, c.relowner, c.relpersistence, c.relkind
    INTO schema_name, table_name, table_owner, persistence, kind
    FROM pg_catalog.pg_class AS c
    JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
    WHERE c.oid = table_class;

then used here, which is where it gets double quoted again on line 2521 (presumably everywhere table_owner is used with $I):

EXECUTE format('ALTER TABLE %1$I.%2$I OWNER TO %3$I', schema_name, history_table_name, table_owner);

For now I can just user lowercase, but if this project makes it to production casing will be out of my control.

My PostgreSQL knowledge is severely lacking too, sorry. My background is SQL Server.

Thanks,
Mark

error when inserting new record

I get an error when I try to insert a record

CREATE TABLE _example (
id bigint PRIMARY KEY,
value text
);
SELECT periods.add_system_time_period('_example', 'row_start', 'row_end');
SELECT periods.add_system_versioning('_example');

insert into _example select '123434'; <--- error function "generated_always_as_row_start_end" was not called by trigger manager

pg_dump -n periods cannot dump the tables ?

mydb=# select version();
                                                     version                                                      
------------------------------------------------------------------------------------------------------------------
 PostgreSQL 14.2 (Debian 14.2-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
(1 row)

I am trying to do a pg_restore for a pg_dump that includes the periods schema.
I thought the restore was successful, because I connected my app to the restored db and read ops ran fine until my app tried to insert to a table which had been add_system_time_period-ed, an error happened:
period "system_time" not found on table "carts"

So I digged around, and found out that my restored db has empty periods schema.
And I tried a normal pg_dump on the live server that is still running fine.

This is the pg_dump command and output:

pg_dump -n periods -d mydb
--
-- PostgreSQL database dump
--

-- Dumped from database version 14.1 (Debian 14.1-1.pgdg110+1)
-- Dumped by pg_dump version 14.1 (Debian 14.1-1.pgdg110+1)

SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;

--
-- PostgreSQL database dump complete
--

Notice that the tables were not dumped.

And this is my cmd to list all tables for this periods schema:

psql -d mydb -c '\dt+ periods'
Schema  |        Name         | Type  |  Owner   | Persistence | Access method |    Size    |                                                   Description                   >
---------+---------------------+-------+----------+-------------+---------------+------------+--------------------------------------------------------------------------------->
 periods | for_portion_views   | table | postgres | permanent   | heap          | 0 bytes    | 
 periods | foreign_keys        | table | postgres | permanent   | heap          | 8192 bytes | A registry of foreign keys using periods WITHOUT OVERLAPS
 periods | periods             | table | postgres | permanent   | heap          | 64 kB      | The main catalog for periods.  All "DDL" operations for periods must first take >
 periods | system_time_periods | table | postgres | permanent   | heap          | 80 kB      | 
 periods | system_versioning   | table | postgres | permanent   | heap          | 88 kB      | A registry of tables with SYSTEM VERSIONING
 periods | unique_keys         | table | postgres | permanent   | heap          | 8192 bytes | A registry of UNIQUE/PRIMARY keys using periods WITHOUT OVERLAPS

I wonder what did I miss in order to do a successful pg_dump that also includes the periods schema ?

Is this bi-temporal + scd approach correct ?

Hi, i've setup a simple example to test bi-temporal and scd with system versioning provided by periods. Could you help me validate whether this is the 'correct' approach ? I have anxieties over not using periods for the 'business validity' time.

The example had been tested successfully in psql 12. It includes ddl, dml statements and output in the comments:

CREATE TABLE example (
    id bigint PRIMARY KEY,
    value varchar(255) NOT NULL,
    start_date TIMESTAMPTZ NOT NULL DEFAULT 'infinity'::timestamp,
    end_date TIMESTAMPTZ NOT NULL DEFAULT 'infinity'::timestamp);
SELECT periods.add_system_time_period('example', 'row_start', 'row_end');
SELECT periods.add_system_versioning('example');

INSERT INTO example (id, value) VALUES (1, 'first');
INSERT INTO example (id, value) VALUES (2, 'second');

-- having start date value means it means something to the system
-- having end date value means it's 'deleted'
CREATE VIEW valid_example AS 
SELECT * FROM example WHERE start_date <> 'infinity'::timestamp
AND end_date = 'infinity'::timestamp;

-- the record is still in 'draft mode', not having any special meaning in the system
INSERT INTO example (id, value) VALUES (1, 'draft value');
SELECT * FROM valid_example;
/*
 id | value | start_date | end_date | row_start | row_end 
----+-------+------------+----------+-----------+---------
(0 rows)
*/

-- set start_date, meaning we're now switching 
-- from 'draft mode' to 'effective mode' in the system
UPDATE example SET start_date = NOW() WHERE id = 1;
SELECT * FROM valid_example;
/*
 id | value |          start_date           | end_date |           row_start           | row_end  
----+-------+-------------------------------+----------+-------------------------------+----------
  1 | first | 2020-06-23 02:11:02.362354+00 | infinity | 2020-06-23 02:11:02.362354+00 | infinity
(1 row)
*/

-- adding child to test scd join
CREATE TABLE example_child (
    id bigint PRIMARY KEY,
    example_id bigint NOT NULL REFERENCES example(id),
    value text,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
INSERT INTO example_child (id, example_id, value) VALUES (1, 1, 'first child 1');

CREATE VIEW view_example_child_scd AS 
SELECT e.id parent_id, e.value parent_value, c.id child_id, c.value child_value 
FROM example_child c
JOIN example__as_of(c.created_at) e ON e.id = c.example_id
ORDER BY c.id;

SELECT * FROM view_example_child_scd;
/*
 parent_id | parent_value | child_id |  child_value  
-----------+--------------+----------+---------------
         1 | first        |        1 | first child 1
(1 row)
*/

-- test scd join after updates to the example rec and then a new insert
UPDATE example SET value = 'first modified' WHERE id = 1;
INSERT INTO example_child (id, example_id, value) VALUES (2, 1, 'first child 2');
SELECT * FROM view_example_child_scd;
/*
 parent_id |  parent_value  | child_id |  child_value  
-----------+----------------+----------+---------------
         1 | first          |        1 | first child 1
         1 | first modified |        2 | first child 2
(2 rows)
*/

-- the record is now 'deleted', also not having any special meaning in the system, 
-- but can still be joined with as_of in scd join
UPDATE example SET end_date = NOW() WHERE id = 1;
SELECT * FROM valid_example;
/*
 id | value | start_date | end_date | row_start | row_end 
----+-------+------------+----------+-----------+---------
(0 rows)
*/

SELECT * FROM view_example_child_scd;
/*
scd=# SELECT * FROM view_example_child_scd;
 parent_id |  parent_value  | child_id |  child_value  
-----------+----------------+----------+---------------
         1 | first          |        1 | first child 1
         1 | first modified |        2 | first child 2
(2 rows)
*/

table example;
/*
 id |     value      |          start_date           |           end_date            |           row_start           | row_end  
----+----------------+-------------------------------+-------------------------------+-------------------------------+----------
  2 | second         | infinity                      | infinity                      | 2020-06-23 02:09:51.486705+00 | infinity
  1 | first modified | 2020-06-23 02:11:02.362354+00 | 2020-06-23 02:13:02.290119+00 | 2020-06-23 02:13:02.290119+00 | infinity
*/

table example_history;
/*
 id |     value      |          start_date           | end_date |           row_start           |            row_end            
----+----------------+-------------------------------+----------+-------------------------------+-------------------------------
  1 | first          | infinity                      | infinity | 2020-06-23 02:09:51.483929+00 | 2020-06-23 02:09:51.500365+00
  1 | first          | 2020-06-23 02:09:51.500365+00 | infinity | 2020-06-23 02:09:51.500365+00 | 2020-06-23 02:11:02.362354+00
  1 | first          | 2020-06-23 02:11:02.362354+00 | infinity | 2020-06-23 02:11:02.362354+00 | 2020-06-23 02:12:14.343354+00
  1 | first modified | 2020-06-23 02:11:02.362354+00 | infinity | 2020-06-23 02:12:14.343354+00 | 2020-06-23 02:13:02.290119+00
*/

drop table example_child cascade;
drop table example cascade;
drop table example_history cascade;

syntax error on Windows

PostgreSQL-12 (12.2.1) on Win-10 / Test version can change if needed.

Hi,

thank you for this extension.
I'm learning "Temporal tables" and "System Versioned Tables" from SQL:2011. I've quickly found there's no support as of now in PostgreSQL. Some users mentionned using "temporal_tables" extension and from there found your project that I had missed out.

I've never used PostgreSQL extensions, I might have missed the real how-to.
I got the binaries (thanks for providing them) (in my case: periods-v1.1-pgREL_12_STABLE-windows-x64.zip). I unzipped and installed in folders lib/periods.dll and share/extension/[periods.control, periods--1.0.sql, periods--1.0--1.1.sql, periods--1.1.sql]. Hope it's the right way!

I run the commands

test=# create extension if not exists periods cascade; -- cascade required for 'btree_gist'
CREATE EXTENSION
-- from the README:
test=# CREATE TABLE example ( id bigint PRIMARY KEY, value text, PERIOD FOR system_time (row_start, row_end) ) WITH SYSTEM VERSIONING;
ERREUR:  erreur de syntaxe sur ou près de « FOR » (sorry in French: syntax error on FOR)
LIGNE 1 : ...ample ( id bigint PRIMARY KEY, value text, PERIOD FOR system...
                                                               ^ 

I tried with another PostgreSQL version, but still no luck. Would you mind explaining the error I might have done? Thanks!

Allow history table to be partitioned

Is it possible to allow history table to be partitioned?
Function add_system_versioning doesn't allow me to do so, probably here:

CASE object_type

But if I create history table as normal and then substitute its name with name of partitioned
table all works fine:

UPDATE periods.system_versioning SET history_table_name = TO_REGCLASS('partitioned_table_hist')
WHERE history_table_name='normal_table_hist'::regclass;

The only problem is management. I have to change all partitioned table names every time I need to add
a new history table.

QUESTION: How to handle Schema changes?

Hello there!

Imagine I have a temporal table for personal information:

  • UUID (varchar)
  • main_document (varchar)
  • name (varchar)
  • DoB (timestamp)
  • genre (varchar)
  • address (varchar)
  • salary (decimal)

at T1 I run a schema migration and add a new column, from now on the table has:

  • UUID (varchar)
  • main_document (varchar)
  • name (varchar)
  • DoB (timestamp)
  • genre (varchar)
  • address (varchar)
  • salary (decimal)
  • EMAIL (varchar)*

Then at T2 I run another schema migration and change the data type of main_document to NUMBER.

  • UUID (varchar)
  • main_document (NUMBER)*
  • name (varchar)
  • DoB (timestamp)
  • genre (varchar)
  • address (varchar)
  • salary (decimal)
  • email (varchar)

Then at T3 I run another schema migration and remove the genre column

  • UUID (varchar)
  • main_document (number)
  • name (varchar)
  • DoB (timestamp)
  • ---------------*
  • address (varchar)
  • salary (decimal)
  • email (varchar)

Then at T4 I run another schema migration and add the genre column, but now it has the data type NUMBER.

  • UUID (varchar)
  • main_document (number)
  • name (varchar)
  • DoB (timestamp)
  • genre (NUMBER)*
  • address (varchar)
  • salary (decimal)
  • email (varchar)

How can I query my DB (going back in time) without breaking my existing queries? SQL:2016

Are there any best practices or strategies to avoid all this complexity with these temporal tables + schema migration?

Any help would be much appreciated.

P.S: I didn't find the RIGHT place to post a question to the community. If that's the wrong place, please feel free to remove it. (sorry)

Thanks

Add support for custom system time

Thanks for the extension. Very useful.
I am wondering if it is planned to support the possibility of having a custom system time to be sent for versioning together with the inserts or updates. Something similar to what is described here.

Currently our system stores changes and writes them (few seconds/minutes later) to the postgresql. For making sense of the data it would be useful to associate the timestamp at which the changes happened rather than relying on the system time (the transaction_timestamp() )

I was trying to use only add.periods and using the start_time and end_time as columns for the validity period and playing with add.periods_for_partion_views in order to send updates but it becomes a nightmare (I am not an expert of postgresql). From what I read I might need to implement triggers but I wouldn't like to redo what you already did (surely better than me).

In my understanding I cannot create an history table without first calling the periods.add_system_time_period and periods.add_system_versioning

Is there a workaround to avoid using the transaction_timestamp() but still leverage on all the work you did for versioning? Thanks!

Usage of "_as_of ()"

Hi there,

using postgresql-11-periods/focal-pgdg,now 1.2-1.pgdg20.04+2 amd64

I've defined a table with system versioning as follow:

create table test1 (
  id serial primary key,
  status varchar (30) default 'on'
);
select periods.add_system_time_period ('test1', 'row_start', 'row_end');
select periods.add_system_versioning ('test1');

I've then add some rows to my table via:

insert into test1 (status) values ('on');
insert into test1 (status) values ('off');
insert into test1 (status) values ('on');

Now I'm trying to get my value at given date via test1__as_of ():

ls=# SELECT * FROM test1__as_of (now ());
 id | status |           row_start           | row_end             
----+--------+-------------------------------+----------
  1 | on     | 2020-10-21 19:37:07.567824+02 | infinity
  2 | off    | 2020-10-21 19:52:33.665405+02 | infinity
  3 | on     | 2020-10-21 19:53:44.834846+02 | infinity
(3 rows)

ls=# SELECT * FROM test1__as_of ('2020-10-21 19:52:35.665405+02');
 id | status |           row_start           | row_end             
----+--------+-------------------------------+----------
  1 | on    | 2020-10-21 19:37:07.567824+02 | infinity
  2 | off   | 2020-10-21 19:52:33.665405+02 | infinity
(2 rows)

ls=# SELECT * FROM test1__as_of ('2020-10-21 19:40:07.567824+02');
 id | status |           row_start           | row_end             
----+--------+-------------------------------+----------
  1 | on     | 2020-10-21 19:37:07.567824+02 | infinity
(1 row)

The question is: why I'm gotting all the records instead of a single one containing my status value at given date ?
Am I missing something ?

Best.

Existing rows system_time_start is set to infinity

Hello

I encountered this scenario when there are already data in the tables and when executing add_system_time_period the default value for system_time_start is set to infinity.

Is this the expected behavior or am I doing something wrong?

Thank you

Support for Range types (e.g. tstzrange?)

Hi, this extension looks excellent. I have been looking to refactor out a home-grown trigger based solution with something closer to standards-based. I'm wondering if this extension will support range types so we can avoid having to have separate from and to columns?

Remove add_period from a table

Hello,
For the last few years I used this extension on a specific table

SELECT periods.add_period ('measurement', 'validity', 'tstart', 'tend');

Now I would like to go back to the original behavior. Basically I'd like to be able to write everything on the table without checks on tstart and tend values. I cannot figure out how to do it.

I believe I could leverage on a temporary table, but it seems a bit convoluted. Is there a better approach to remove this function?

Cannot update a row

create extension if not exists periods cascade;
create table store (
	ID serial primary key,
	name text not null
);
SELECT periods.add_system_time_period('store');
SELECT periods.add_system_versioning('store');
insert into store values (default, 'lol');
update store set name = 'newname' where id = 1;

Everything before the update seems to have run correctly

 add_system_time_period
------------------------
 t
(1 row)
NOTICE:  history table "store_history" created for "store", be sure to index it properly
 add_system_versioning
-----------------------

(1 row)
INSERT 0 1

but after the update

ERROR:  column stp.excluded_column_names does not exist
LINE 1: ...iods.system_time_periods AS stp CROSS JOIN unnest(stp.exclud...
                                                             ^
QUERY:  SELECT u.name FROM periods.system_time_periods AS stp CROSS JOIN unnest(stp.excluded_column_names) AS u (name) WHERE stp.table_name = $1

I'm using 783475d with postgres 12 on debian sid via wsl1

Installation error

Good afternoon.
Please tell me how to solve the problem of installing the extension.
I compiled the extension from sources and installed it on PostgreSQL 14 version (not the default version). The extension is present in the list of available ones, but during installation an error occurs -

SQL Error [58P01]: ERROR: could not load library "/usr/local/pgsql/lib/periods.so": /usr/local/pgsql/lib/periods.so: undefined symbol: elog_finish
ERROR: could not load library "/usr/local/pgsql/lib/periods.so": /usr/local/pgsql/lib/periods.so: undefined symbol: elog_finish
ERROR: could not load library "/usr/local/pgsql/lib/periods.so": /usr/local/pgsql/lib/periods.so: undefined symbol: elog_finish

Trigger system_time_write_history fails

Hello again

I have a database with 30 tables. Executing add_system_time_period and add_system_versioning completes with success. The issue I have is that updates in some of the tables simply crashes the database process and I get this message FATAL: the database system is in recovery mode.

I am executing like this
SELECT periods.add_system_time_period('"MyTable"'); SELECT periods.add_system_versioning('"MyTable"');

Update just a column for this table crashes the process, any column.

Also I tried recreating the same table with a different name and it just works, no more errors.

Can you help me in some way? How can I investigate this to know at least what is the issue or the exact error?

Thank you

relation "example2__for_portion_of_validity" does not exist

CREATE TABLE example2 (
    id bigint,
	value text,
    start_date date,
    end_date date,
    CONSTRAINT example2_pkey PRIMARY KEY (id, start_date, end_date)
);

SELECT periods.add_period('example2', 'validity', 'start_date', 'end_date');
SELECT periods.add_unique_key('example2', ARRAY['id'], 'validity', unique_constraint => 'example2_pkey');

insert into example2 values(1, 'one', '2022-01-01', '2022-12-31');
select * from example2;

update example2__for_portion_of_validity
	set value='one one',
	start_date='2022-03-01',
	end_date='2022-05-01'
where id=1;

This fails with:

ERROR:  relation "example2__for_portion_of_validity" does not exist
LINE 1: update example2__for_portion_of_validity
               ^
SQL state: 42P01
Character: 8

Am I doing something wrong?

Can't adjust GRANTs after backup and restore

I tried the following on PostgreSQL 13+14 on Windows 10.

I created a new database and ran:

create extension periods cascade;

create table test (id int not null primary key);

select periods.add_system_time_period('test');
select periods.add_system_versioning('test');

Everything works fine.

Then I dump and restore the database:

pg_dump.exe --file=periods_test.dump --format=custom periods_test
psql -c "DROP DATABASE periods_test;"
pg_restore -d postgres -C periods_test.dump

pg_restore raises these errors:

pg_restore: while PROCESSING TOC:
pg_restore: from TOC entry 3392; 0 0 ACL TABLE test_history postgres
pg_restore: error: could not execute query: ERROR:  cannot revoke SELECT directly from "public.test_history", revoke SELECT from "public.test" instead
CONTEXT:  PL/pgSQL function periods.health_checks() line 255 at RAISE
Command was: REVOKE ALL ON TABLE public.test_history FROM postgres;
GRANT SELECT ON TABLE public.test_history TO postgres;


pg_restore: from TOC entry 3393; 0 0 ACL TABLE test_with_history postgres
pg_restore: error: could not execute query: ERROR:  cannot revoke SELECT directly from "public.test_with_history", revoke SELECT from "public.test" instead
CONTEXT:  PL/pgSQL function periods.health_checks() line 255 at RAISE
Command was: REVOKE ALL ON TABLE public.test_with_history FROM postgres;
GRANT SELECT ON TABLE public.test_with_history TO postgres;


pg_restore: warning: errors ignored on restore: 2

From now on, I can't change privileges.

If I run grant select on test to postgres; I get

ERROR:  cannot grant DELETE to "test_history"; history objects are read-only
CONTEXT:  PL/pgSQL function periods.health_checks() line 138 at RAISE

I think the reason for this is in

periods/periods--1.2.sql

Lines 3397 to 3459 in 9bb3df9

FOR r IN
SELECT *,
EXISTS (
SELECT
FROM pg_class AS _c
CROSS JOIN LATERAL aclexplode(COALESCE(_c.relacl, acldefault('r', _c.relowner))) AS _acl
WHERE _c.oid = objects.table_name
AND _acl.grantee = objects.grantee
AND _acl.privilege_type = 'SELECT'
) AS on_base_table
FROM (
SELECT sv.table_name,
c.oid::regclass::text AS object_name,
c.relkind AS object_type,
acl.privilege_type,
acl.privilege_type AS base_privilege_type,
acl.grantee,
'h' AS history_or_portion
FROM periods.system_versioning AS sv
JOIN pg_class AS c ON c.oid IN (sv.history_table_name, sv.view_name)
CROSS JOIN LATERAL aclexplode(COALESCE(c.relacl, acldefault('r', c.relowner))) AS acl
UNION ALL
SELECT fpv.table_name,
c.oid::regclass::text,
c.relkind,
acl.privilege_type,
acl.privilege_type,
acl.grantee,
'p' AS history_or_portion
FROM periods.for_portion_views AS fpv
JOIN pg_class AS c ON c.oid = fpv.view_name
CROSS JOIN LATERAL aclexplode(COALESCE(c.relacl, acldefault('r', c.relowner))) AS acl
UNION ALL
SELECT sv.table_name,
p.oid::regprocedure::text,
'f',
acl.privilege_type,
'SELECT',
acl.grantee,
'h'
FROM periods.system_versioning AS sv
JOIN pg_proc AS p ON p.oid = ANY (ARRAY[sv.func_as_of, sv.func_between, sv.func_between_symmetric, sv.func_from_to]::regprocedure[])
CROSS JOIN LATERAL aclexplode(COALESCE(p.proacl, acldefault('f', p.proowner))) AS acl
) AS objects
ORDER BY object_name, object_type, privilege_type
LOOP
IF
r.history_or_portion = 'h' AND
(r.object_type, r.privilege_type) NOT IN (('r', 'SELECT'), ('v', 'SELECT'), ('f', 'EXECUTE'))
THEN
RAISE EXCEPTION 'cannot grant % to "%"; history objects are read-only',
r.privilege_type, r.object_name;
END IF;
IF NOT r.on_base_table THEN
RAISE EXCEPTION 'cannot grant % directly to "%"; grant % to "%" instead',
r.privilege_type, r.object_name, r.base_privilege_type, r.table_name;
END IF;
END LOOP;

In the CROSS JOIN is always COALESCE(c.relacl, acldefault('r', c.relowner)).

When pg_restore creates the table test_history, the table only has the default acl belonging to the owner (in the query c.relacl is null, so COALESCE returns acldefault('r', c.relowner) which is {postgres=arwdDxt/postgres}).

The query returns:

table_name object_name object_type privilege_type base_privilege_type grantee history_or_portion on_base_table
test test__as_of(timestamp with time zone) f EXECUTE SELECT 10 h t
test test__between(timestamp with time zone,timestamp with time zone) f EXECUTE SELECT 10 h t
test test__between_symmetric(timestamp with time zone,timestamp with time zone) f EXECUTE SELECT 10 h t
test test__from_to(timestamp with time zone,timestamp with time zone) f EXECUTE SELECT 10 h t
test test_history r DELETE DELETE 10 h t
test test_history r INSERT INSERT 10 h t
test test_history r REFERENCES REFERENCES 10 h t
test test_history r SELECT SELECT 10 h t
test test_history r TRIGGER TRIGGER 10 h t
test test_history r TRUNCATE TRUNCATE 10 h t
test test_history r UPDATE UPDATE 10 h t
test test_with_history v DELETE DELETE 10 h t
test test_with_history v INSERT INSERT 10 h t
test test_with_history v REFERENCES REFERENCES 10 h t
test test_with_history v SELECT SELECT 10 h t
test test_with_history v TRIGGER TRIGGER 10 h t
test test_with_history v TRUNCATE TRUNCATE 10 h t
test test_with_history v UPDATE UPDATE 10 h t

Because all of the default acl privilege types are returned,

periods/periods--1.2.sql

Lines 3447 to 3453 in 9bb3df9

IF
r.history_or_portion = 'h' AND
(r.object_type, r.privilege_type) NOT IN (('r', 'SELECT'), ('v', 'SELECT'), ('f', 'EXECUTE'))
THEN
RAISE EXCEPTION 'cannot grant % to "%"; history objects are read-only',
r.privilege_type, r.object_name;
END IF;

raises the exception.

pg_restore can't GRANT the SELECT privilege, so c.relacl will always be null and the health_check() trigger will block every GRANT. Even for tables which don't have periods.

Indexes not updated

Thanks for working on bringing system versioning to PostgreSQL. Looking forward to see this in vanilla PostgreSQL.

I think I found a bug (at least I'm out of ideas) regarding indexes on history tables not being updated.

Minimal example:

SET enable_seqscan=FALSE

CREATE TABLE "test" (
    "id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    "revision" INTEGER NOT NULL
)

SELECT periods.add_system_time_period('test', 'row_start_time', 'row_end_time')
SELECT periods.add_system_versioning('test')

INSERT INTO "test" ("revision") VALUES (1)
INSERT INTO "test" ("revision") VALUES (10)

UPDATE "test" SET "revision" = 2 WHERE ("id" = 1)
UPDATE "test" SET "revision" = 3 WHERE ("id" = 1)

CREATE INDEX "yolo" ON "test_history" ("id", "revision")

UPDATE "test" SET "revision" = 11 WHERE ("id" = 2)

# returns 2 rows
SELECT * FROM "test_history" WHERE "id" = 1

# returns 0 rows if index is used / 1 row if seq scan is used
SELECT * FROM "test_history" WHERE "id" = 2 

I'm using a Mac (10.5.4) with PostgreSQL 12.2 with dacb886 build from source.

Crash on PG14

Periods is crashing with PG14:

15:49:29  UPDATE sysver SET flap = not flap;
15:49:29 -UPDATE sysver SET flap = not flap;
15:49:29 -UPDATE sysver SET flap = not flap;

https://pgdgbuild.dus.dg-i.net/view/Testsuite-Beta/job/postgresql-periods-autopkgtest-beta/lastFailedBuild/architecture=amd64,distribution=sid/console

15:49:29 #0  0x0000559618975bd1 in ?? ()
15:49:29 No symbol table info available.
15:49:29 #1  0x0000559618975ef7 in datumIsEqual ()
15:49:29 No symbol table info available.
15:49:29 #2  0x00007f0a94d58068 in OnlyExcludedColumnsChanged (rel=<optimized out>, old_row=0x55961b09d7f0, new_row=0x55961b09d618) at ./periods.c:291
15:49:29         old_datum = <optimized out>
15:49:29         new_isnull = false
15:49:29         typlen = <optimized out>
15:49:29         new_datum = <optimized out>
15:49:29         old_isnull = false
15:49:29         typbyval = <optimized out>
15:49:29         ret = <optimized out>
15:49:29         i = 3
15:49:29         values = {17318}
15:49:29         tupdesc = 0x7f0a9b662640
15:49:29         excluded_attnums = 0x55961a8989b0
15:49:29         mcxt = <optimized out>
15:49:29         sql = 0x7f0a94d590f8 "SELECT u.name FROM periods.system_time_periods AS stp CROSS JOIN unnest(stp.excluded_column_names) AS u (name) WHERE stp.table_name = $1"
15:49:29         qplan = 0x55961b367fb0
15:49:29         __func__ = "OnlyExcludedColumnsChanged"
15:49:29 #3  0x00007f0a94d584a6 in generated_always_as_row_start_end (fcinfo=<optimized out>) at ./periods.c:487
15:49:29         old_row = <optimized out>
15:49:29         trigdata = <optimized out>
15:49:29         funcname = 0x7f0a94d592a0 "generated_always_as_row_start_end"
15:49:29         rel = 0x7f0a9b4e7ff8
15:49:29         new_row = 0x55961b09d618
15:49:29         new_tupdesc = 0x7f0a9b662640
15:49:29         values = {94103179959864, 139683532017304}
15:49:29         nulls = {false, false}
15:49:29         columns = {-1689350504, 32522}
15:49:29         start_name = 0x1af35ff1c <error: Cannot access memory at address 0x1af35ff1c>
15:49:29         end_name = 0x55961a7cbb30 "\200\216N\233\n\177"
15:49:29         start_num = <optimized out>
15:49:29         end_num = <optimized out>
15:49:29         typeid = <optimized out>
15:49:29         __func__ = "generated_always_as_row_start_end"
15:49:29 #4  0x0000559618a732c5 in fmgr_security_definer ()
15:49:29 No symbol table info available.

Why version 1.2 is not visible in my pg14 ?

This is out of curiosity, but after installing postgresql-14-periods in debian buster, the available versions do not include 1.2, only 1.0 and 1.1. Is this intentional ?

Below are the details of my system, package files, and extension infos.

Linux localdebian 4.19.0-18-amd64 #1 SMP Debian 4.19.208-1 (2021-09-29) x86_64 GNU/Linux
Debian 10 Buster
# dpkg -L postgresql-14-periods
/.
/usr
/usr/lib
/usr/lib/postgresql
/usr/lib/postgresql/14
/usr/lib/postgresql/14/lib
/usr/lib/postgresql/14/lib/bitcode
/usr/lib/postgresql/14/lib/bitcode/periods
/usr/lib/postgresql/14/lib/bitcode/periods/periods.bc
/usr/lib/postgresql/14/lib/bitcode/periods.index.bc
/usr/lib/postgresql/14/lib/periods.so
/usr/share
/usr/share/doc
/usr/share/doc/postgresql-14-periods
/usr/share/doc/postgresql-14-periods/README.md.gz
/usr/share/doc/postgresql-14-periods/changelog.Debian.amd64.gz
/usr/share/doc/postgresql-14-periods/changelog.Debian.gz
/usr/share/doc/postgresql-14-periods/changelog.gz
/usr/share/doc/postgresql-14-periods/copyright
/usr/share/doc/postgresql-doc-14
/usr/share/doc/postgresql-doc-14/extension
/usr/share/doc/postgresql-doc-14/extension/README.periods.gz
/usr/share/postgresql
/usr/share/postgresql/14
/usr/share/postgresql/14/extension
/usr/share/postgresql/14/extension/periods--1.0--1.1.sql
/usr/share/postgresql/14/extension/periods--1.0.sql
/usr/share/postgresql/14/extension/periods--1.1--1.2.sql
/usr/share/postgresql/14/extension/periods--1.1.sql
/usr/share/postgresql/14/extension/periods--1.2.sql
/usr/share/postgresql/14/extension/periods.control
/usr/share/doc/postgresql-14-periods/README.periods.gz
# select * from pg_extension where extname = 'periods';
   oid   | extname | extowner | extnamespace | extrelocatable | extversion |                     extconfig                     |    extcondition     
---------+---------+----------+--------------+----------------+------------+---------------------------------------------------+---------------------
 2406036 | periods |       10 |         2200 | f              | 1.1        | {2406063,2406069,2406089,2406101,2406114,2406137} | {"","","","","",""}
(1 row)
# alter extension periods update;
NOTICE:  version "1.1" of extension "periods" is already installed
ALTER EXTENSION
# SELECT * FROM pg_available_extension_versions WHERE name ='periods';
  name   | version | installed | superuser | relocatable | schema |   requires   |                               comment                                
---------+---------+-----------+-----------+-------------+--------+--------------+----------------------------------------------------------------------
 periods | 1.0     | f         | t         | f           |        | {btree_gist} | Provide Standard SQL functionality for PERIODs and SYSTEM VERSIONING
 periods | 1.1     | t         | t         | f           |        | {btree_gist} | Provide Standard SQL functionality for PERIODs and SYSTEM VERSIONING
(2 rows)

Support system versioning for views

SQL Server supports querying views with the AS OF clause and SQL Server will transparently apply the clause to all temporal tables participating in the view definition's query. See https://docs.microsoft.com/en-us/sql/relational-databases/tables/querying-data-in-a-system-versioned-temporal-table?view=sql-server-ver15#using-views-with-as-of-sub-clause-in-temporal-queries for details.

I don't know if this feature is defined by the SQL standard or not, but it looks very powerful indeed.

Clarify how to use pg_dump & pg_restore with periods

Relating to #22, I've recently stumbled over the problem of "how to dump & restore system versioned tables".

My personal preference of pg_dump --format=custom --file=dump.psql my_source_database & pg_restore --format=custom --clean --if-exists --single-transcation -d my_target_database doesn't seem to work with periods, as it doesn't seem to like, how pg_restore directly fiddles with its managed tables.

My current workaround for this is

  1. ensure the periods extension is installed in the target database
  2. create the dump with pg_dump as described above
  3. create a use-list, that excludes the periods extension from the restore: pg_restore --format=custom --list dump.psql | grep -Ev 'EXTENSION - (periods|btree_gist) > dump.use-list
  4. disable periods's event-triggers:
    ALTER EVENT TRIGGER periods_drop_protection DISABLE;
    ALTER EVENT TRIGGER periods_health_checks DISABLE;
    ALTER EVENT TRIGGER periods_rename_following DISABLE;
  5. restore the dump, using the use-list pg_restore --format=custom --single-transaction --clean --if-exists --use-list=dump.use-list -d my_target_database dump.psql
  6. re-enable the event-triggers:
    ALTER EVENT TRIGGER periods_drop_protection ENABLE;
    ALTER EVENT TRIGGER periods_health_checks ENABLE;
    ALTER EVENT TRIGGER periods_rename_following ENABLE;

Is there any other recommended way to dumping and restoring a database with periods?

A couple of notes:

  • The above workaround probably only works correctly, if the entire database is dumped and restored - or at least all periods employing tables all at once -, as the dumped & restored tables in periods's schema (also called periods by default) contain meta-data for all periods employing tables.
  • btree_gist needs to be excluded from the use-list too, as it's a dependency of periods and thus can't be dropped and recreated during restore
  • pg_dump currently only supports including named extensions (-e argument) but doesn't support excluding named extensions, which makes the use of use-lists necessary

System Versioning on table with dropped columns contains corrupted data in history table

Hi, I'm trying to migrate my postgresql 11 database from temporal_tables to periods, and I ran into this issue.

From the looks of it, it kind of makes sense how this could happen since dropped columns in postgres don't just disappear. Is this a known limitation? I could always recreate the source table, but I'd like to make sure there's nothing I'm missing.

Minimal script:

CREATE TABLE test (
    test_id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    value VARCHAR NOT NULL
);

ALTER TABLE test
    DROP COLUMN value;

ALTER TABLE test
    ADD COLUMN value2 varchar NOT NULL;

INSERT INTO test (value2)
    VALUES ('hello'), ('world');

SELECT periods.add_system_time_period ('test');
SELECT periods.add_system_versioning ('test');


UPDATE test
SET value2 = 'goodbye'
WHERE test_id = 2;

Expected:

SELECT * FROM test_with_history;
test_id | value2  | system_time_start             | system_time_end               |
--------|---------|-------------------------------|-------------------------------|
      1 | hello   |                     -infinity |                      infinity |
      2 | goodbye | 2019-12-04 18:04:55.825756-08 |                      infinity |
      2 | world   |                     -infinity | 2019-12-04 18:04:55.825756-08 |

Actual:

test_id | value2  | system_time_start             | system_time_end |
--------|---------|-------------------------------|-----------------|
      1 | hello   |                     -infinity |        infinity |
      2 | goodbye | 2019-12-04 18:04:55.825756-08 |        infinity |
      2 |  [NULL] | 2004-06-09 20:21:18.209996-07 |       -infinity |

Foreign key constraints allow inconsistent state?

Hi, I'm fiddling around with the project and this behavior seems odd to me, can anyone confirm it is the expected outcome?

CREATE TABLE uk (id integer, s integer, e integer, CONSTRAINT uk_pkey PRIMARY KEY (id, s, e));
SELECT periods.add_period('uk', 'p', 's', 'e');
SELECT periods.add_unique_key('uk', ARRAY['id'], 'p', key_name => 'uk_id_p', unique_constraint => 'uk_pkey');

CREATE TABLE fk (id integer, uk_id integer, s integer, e integer, PRIMARY KEY (id));
SELECT periods.add_period('fk', 'q', 's', 'e');
SELECT periods.add_foreign_key('fk', ARRAY['uk_id'], 'q', 'uk_id_p', key_name => 'fk_uk_id_q');

INSERT INTO uk(id, s, e)
    VALUES (1, 1, 3)
         , (1, 3, 5);

INSERT INTO fk(uk_id, id, s, e)
    VALUES (1, 1, 1, 2)
         , (1, 2, 2, 5);

--expected: fail,    behavior: deleted
delete from uk where (id, s, e) = (1, 1 ,3);

--expected: fail,    behavior: failed
delete from uk where (id, s, e) = (1, 3 ,5);

INSERT INTO uk(id, s, e)
    VALUES (2, 1, 5);

INSERT INTO fk(uk_id, id, s, e)
    VALUES (2, 4, 2, 4);

--expected: fail,    behavior: updated
update uk set e = 3 where (id,s,e) = (2,1,5);

How to update system versioning after altering a table?

Hello, I would like to know if there is a suggested way to update system versioning of a table after altering it.
E.g. after adding a column to a table with system versioning, how to update the history table, triggers, etc. so that system versioning works also for the new column.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.