public inbox for [email protected]
help / color / mirror / Atom feed[PATCH 3/5] Make archiver process an auxiliary process
37+ messages / 15 participants
[nested] [flat]
* [PATCH 3/5] Make archiver process an auxiliary process
@ 2018-11-07 07:53 Kyotaro Horiguchi <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Kyotaro Horiguchi @ 2018-11-07 07:53 UTC (permalink / raw)
This is a preliminary patch for shared-memory based stats collector.
Archiver process must be a auxiliary process since it uses shared
memory after stats data wes moved onto shared-memory. Make the process
an auxiliary process in order to make it work.
---
src/backend/bootstrap/bootstrap.c | 8 +++
src/backend/postmaster/pgarch.c | 98 +++++++++----------------------------
src/backend/postmaster/pgstat.c | 6 +++
src/backend/postmaster/postmaster.c | 35 +++++++++----
src/include/miscadmin.h | 2 +
src/include/pgstat.h | 1 +
src/include/postmaster/pgarch.h | 4 +-
7 files changed, 67 insertions(+), 87 deletions(-)
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index d8776e192e..f58652fd4f 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -329,6 +329,9 @@ AuxiliaryProcessMain(int argc, char *argv[])
case BgWriterProcess:
statmsg = pgstat_get_backend_desc(B_BG_WRITER);
break;
+ case ArchiverProcess:
+ statmsg = pgstat_get_backend_desc(B_ARCHIVER);
+ break;
case CheckpointerProcess:
statmsg = pgstat_get_backend_desc(B_CHECKPOINTER);
break;
@@ -456,6 +459,11 @@ AuxiliaryProcessMain(int argc, char *argv[])
BackgroundWriterMain();
proc_exit(1); /* should never return */
+ case ArchiverProcess:
+ /* don't set signals, archiver has its own agenda */
+ PgArchiverMain();
+ proc_exit(1); /* should never return */
+
case CheckpointerProcess:
/* don't set signals, checkpointer has its own agenda */
CheckpointerMain();
diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c
index f84f882c4c..4342ebdab4 100644
--- a/src/backend/postmaster/pgarch.c
+++ b/src/backend/postmaster/pgarch.c
@@ -77,7 +77,6 @@
* Local data
* ----------
*/
-static time_t last_pgarch_start_time;
static time_t last_sigterm_time = 0;
/*
@@ -96,7 +95,6 @@ static volatile sig_atomic_t ready_to_stop = false;
static pid_t pgarch_forkexec(void);
#endif
-NON_EXEC_STATIC void PgArchiverMain(int argc, char *argv[]) pg_attribute_noreturn();
static void pgarch_exit(SIGNAL_ARGS);
static void ArchSigHupHandler(SIGNAL_ARGS);
static void ArchSigTermHandler(SIGNAL_ARGS);
@@ -114,75 +112,6 @@ static void pgarch_archiveDone(char *xlog);
* ------------------------------------------------------------
*/
-/*
- * pgarch_start
- *
- * Called from postmaster at startup or after an existing archiver
- * died. Attempt to fire up a fresh archiver process.
- *
- * Returns PID of child process, or 0 if fail.
- *
- * Note: if fail, we will be called again from the postmaster main loop.
- */
-int
-pgarch_start(void)
-{
- time_t curtime;
- pid_t pgArchPid;
-
- /*
- * Do nothing if no archiver needed
- */
- if (!XLogArchivingActive())
- return 0;
-
- /*
- * Do nothing if too soon since last archiver start. This is a safety
- * valve to protect against continuous respawn attempts if the archiver is
- * dying immediately at launch. Note that since we will be re-called from
- * the postmaster main loop, we will get another chance later.
- */
- curtime = time(NULL);
- if ((unsigned int) (curtime - last_pgarch_start_time) <
- (unsigned int) PGARCH_RESTART_INTERVAL)
- return 0;
- last_pgarch_start_time = curtime;
-
-#ifdef EXEC_BACKEND
- switch ((pgArchPid = pgarch_forkexec()))
-#else
- switch ((pgArchPid = fork_process()))
-#endif
- {
- case -1:
- ereport(LOG,
- (errmsg("could not fork archiver: %m")));
- return 0;
-
-#ifndef EXEC_BACKEND
- case 0:
- /* in postmaster child ... */
- InitPostmasterChild();
-
- /* Close the postmaster's sockets */
- ClosePostmasterPorts(false);
-
- /* Drop our connection to postmaster's shared memory, as well */
- dsm_detach_all();
- PGSharedMemoryDetach();
-
- PgArchiverMain(0, NULL);
- break;
-#endif
-
- default:
- return (int) pgArchPid;
- }
-
- /* shouldn't get here */
- return 0;
-}
-
/* ------------------------------------------------------------
* Local functions called by archiver follow
* ------------------------------------------------------------
@@ -222,8 +151,8 @@ pgarch_forkexec(void)
* The argc/argv parameters are valid only in EXEC_BACKEND case. However,
* since we don't use 'em, it hardly matters...
*/
-NON_EXEC_STATIC void
-PgArchiverMain(int argc, char *argv[])
+void
+PgArchiverMain(void)
{
/*
* Ignore all signals usually bound to some action in the postmaster,
@@ -255,8 +184,27 @@ PgArchiverMain(int argc, char *argv[])
static void
pgarch_exit(SIGNAL_ARGS)
{
- /* SIGQUIT means curl up and die ... */
- exit(1);
+ PG_SETMASK(&BlockSig);
+
+ /*
+ * We DO NOT want to run proc_exit() callbacks -- we're here because
+ * shared memory may be corrupted, so we don't want to try to clean up our
+ * transaction. Just nail the windows shut and get out of town. Now that
+ * there's an atexit callback to prevent third-party code from breaking
+ * things by calling exit() directly, we have to reset the callbacks
+ * explicitly to make this work as intended.
+ */
+ on_exit_reset();
+
+ /*
+ * Note we do exit(2) not exit(0). This is to force the postmaster into a
+ * system reset cycle if some idiot DBA sends a manual SIGQUIT to a random
+ * backend. This is necessary precisely because we don't clean up our
+ * shared memory state. (The "dead man switch" mechanism in pmsignal.c
+ * should ensure the postmaster sees this as a crash, too, but no harm in
+ * being doubly sure.)
+ */
+ exit(2);
}
/* SIGHUP signal handler for archiver process */
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index 2a8472b91a..6c554d9736 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -2889,6 +2889,9 @@ pgstat_bestart(void)
case StartupProcess:
beentry->st_backendType = B_STARTUP;
break;
+ case ArchiverProcess:
+ beentry->st_backendType = B_ARCHIVER;
+ break;
case BgWriterProcess:
beentry->st_backendType = B_BG_WRITER;
break;
@@ -4147,6 +4150,9 @@ pgstat_get_backend_desc(BackendType backendType)
switch (backendType)
{
+ case B_ARCHIVER:
+ backendDesc = "archiver";
+ break;
case B_AUTOVAC_LAUNCHER:
backendDesc = "autovacuum launcher";
break;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index fe599632d3..36e49b3e9e 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -146,7 +146,8 @@
#define BACKEND_TYPE_AUTOVAC 0x0002 /* autovacuum worker process */
#define BACKEND_TYPE_WALSND 0x0004 /* walsender process */
#define BACKEND_TYPE_BGWORKER 0x0008 /* bgworker process */
-#define BACKEND_TYPE_ALL 0x000F /* OR of all the above */
+#define BACKEND_TYPE_ARCHIVER 0x0010 /* archiver process */
+#define BACKEND_TYPE_ALL 0x001F /* OR of all the above */
#define BACKEND_TYPE_WORKER (BACKEND_TYPE_AUTOVAC | BACKEND_TYPE_BGWORKER)
@@ -538,6 +539,7 @@ static void ShmemBackendArrayRemove(Backend *bn);
#endif /* EXEC_BACKEND */
#define StartupDataBase() StartChildProcess(StartupProcess)
+#define StartArchiver() StartChildProcess(ArchiverProcess)
#define StartBackgroundWriter() StartChildProcess(BgWriterProcess)
#define StartCheckpointer() StartChildProcess(CheckpointerProcess)
#define StartWalWriter() StartChildProcess(WalWriterProcess)
@@ -1761,7 +1763,7 @@ ServerLoop(void)
/* If we have lost the archiver, try to start a new one. */
if (PgArchPID == 0 && PgArchStartupAllowed())
- PgArchPID = pgarch_start();
+ PgArchPID = StartArchiver();
/* If we need to signal the autovacuum launcher, do so now */
if (avlauncher_needs_signal)
@@ -2941,7 +2943,7 @@ reaper(SIGNAL_ARGS)
if (!IsBinaryUpgrade && AutoVacuumingActive() && AutoVacPID == 0)
AutoVacPID = StartAutoVacLauncher();
if (PgArchStartupAllowed() && PgArchPID == 0)
- PgArchPID = pgarch_start();
+ PgArchPID = StartArchiver();
if (PgStatPID == 0)
PgStatPID = pgstat_start();
@@ -3086,10 +3088,8 @@ reaper(SIGNAL_ARGS)
{
PgArchPID = 0;
if (!EXIT_STATUS_0(exitstatus))
- LogChildExit(LOG, _("archiver process"),
- pid, exitstatus);
- if (PgArchStartupAllowed())
- PgArchPID = pgarch_start();
+ HandleChildCrash(pid, exitstatus,
+ _("archiver process"));
continue;
}
@@ -3335,7 +3335,7 @@ CleanupBackend(int pid,
/*
* HandleChildCrash -- cleanup after failed backend, bgwriter, checkpointer,
- * walwriter, autovacuum, or background worker.
+ * walwriter, autovacuum, archiver or background worker.
*
* The objectives here are to clean up our local state about the child
* process, and to signal all other remaining children to quickdie.
@@ -3540,6 +3540,18 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
signal_child(AutoVacPID, (SendStop ? SIGSTOP : SIGQUIT));
}
+ /* Take care of the archiver too */
+ if (pid == PgArchPID)
+ PgArchPID = 0;
+ else if (PgArchPID != 0 && take_action)
+ {
+ ereport(DEBUG2,
+ (errmsg_internal("sending %s to process %d",
+ (SendStop ? "SIGSTOP" : "SIGQUIT"),
+ (int) PgArchPID)));
+ signal_child(PgArchPID, (SendStop ? SIGSTOP : SIGQUIT));
+ }
+
/*
* Force a power-cycle of the pgarch process too. (This isn't absolutely
* necessary, but it seems like a good idea for robustness, and it
@@ -3816,6 +3828,7 @@ PostmasterStateMachine(void)
Assert(CheckpointerPID == 0);
Assert(WalWriterPID == 0);
Assert(AutoVacPID == 0);
+ Assert(PgArchPID == 0);
/* syslogger is not considered here */
pmState = PM_NO_CHILDREN;
}
@@ -5085,7 +5098,7 @@ sigusr1_handler(SIGNAL_ARGS)
*/
Assert(PgArchPID == 0);
if (XLogArchivingAlways())
- PgArchPID = pgarch_start();
+ PgArchPID = StartArchiver();
/*
* If we aren't planning to enter hot standby mode later, treat
@@ -5359,6 +5372,10 @@ StartChildProcess(AuxProcType type)
ereport(LOG,
(errmsg("could not fork startup process: %m")));
break;
+ case ArchiverProcess:
+ ereport(LOG,
+ (errmsg("could not fork archiver process: %m")));
+ break;
case BgWriterProcess:
ereport(LOG,
(errmsg("could not fork background writer process: %m")));
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index b677c7e821..c33abee7aa 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -399,6 +399,7 @@ typedef enum
BootstrapProcess,
StartupProcess,
BgWriterProcess,
+ ArchiverProcess,
CheckpointerProcess,
WalWriterProcess,
WalReceiverProcess,
@@ -411,6 +412,7 @@ extern AuxProcType MyAuxProcType;
#define AmBootstrapProcess() (MyAuxProcType == BootstrapProcess)
#define AmStartupProcess() (MyAuxProcType == StartupProcess)
#define AmBackgroundWriterProcess() (MyAuxProcType == BgWriterProcess)
+#define AmArchiverProcess() (MyAuxProcType == ArchiverProcess)
#define AmCheckpointerProcess() (MyAuxProcType == CheckpointerProcess)
#define AmWalWriterProcess() (MyAuxProcType == WalWriterProcess)
#define AmWalReceiverProcess() (MyAuxProcType == WalReceiverProcess)
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index c080fa6388..e99cd4a72e 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -715,6 +715,7 @@ typedef struct PgStat_GlobalStats
*/
typedef enum BackendType
{
+ B_ARCHIVER,
B_AUTOVAC_LAUNCHER,
B_AUTOVAC_WORKER,
B_BACKEND,
diff --git a/src/include/postmaster/pgarch.h b/src/include/postmaster/pgarch.h
index 2474eac26a..88f16863d4 100644
--- a/src/include/postmaster/pgarch.h
+++ b/src/include/postmaster/pgarch.h
@@ -32,8 +32,6 @@
*/
extern int pgarch_start(void);
-#ifdef EXEC_BACKEND
-extern void PgArchiverMain(int argc, char *argv[]) pg_attribute_noreturn();
-#endif
+extern void PgArchiverMain(void) pg_attribute_noreturn();
#endif /* _PGARCH_H */
--
2.16.3
----Next_Part(Wed_Mar_27_16_36_25_2019_836)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="v18-0004-Shared-memory-based-stats-collector.patch"
^ permalink raw reply [nested|flat] 37+ messages in thread
* [PATCH v5] ALTER tbl rewrite loses CLUSTER ON index
@ 2020-02-06 09:14 Amit Langote <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Amit Langote @ 2020-02-06 09:14 UTC (permalink / raw)
---
src/backend/commands/tablecmds.c | 39 +++++++++++++++++++++++
src/test/regress/expected/alter_table.out | 34 ++++++++++++++++++++
src/test/regress/sql/alter_table.sql | 16 +++++++++-
3 files changed, 88 insertions(+), 1 deletion(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 02a7c04fdb..6b2469bd09 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -490,6 +490,7 @@ static void ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId,
static void RebuildConstraintComment(AlteredTableInfo *tab, int pass,
Oid objid, Relation rel, List *domname,
const char *conname);
+static void PreserveClusterOn(AlteredTableInfo *tab, int pass, Oid indoid);
static void TryReuseIndex(Oid oldId, IndexStmt *stmt);
static void TryReuseForeignKey(Oid oldId, Constraint *con);
static ObjectAddress ATExecAlterColumnGenericOptions(Relation rel, const char *colName,
@@ -11838,6 +11839,9 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd,
newcmd->def = (Node *) stmt;
tab->subcmds[AT_PASS_OLD_INDEX] =
lappend(tab->subcmds[AT_PASS_OLD_INDEX], newcmd);
+
+ /* Preserve index's indisclustered property, if set. */
+ PreserveClusterOn(tab, AT_PASS_OLD_INDEX, oldId);
}
else if (IsA(stm, AlterTableStmt))
{
@@ -11874,6 +11878,9 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd,
rel,
NIL,
indstmt->idxname);
+
+ /* Preserve index's indisclustered property, if set. */
+ PreserveClusterOn(tab, AT_PASS_OLD_INDEX, indoid);
}
else if (cmd->subtype == AT_AddConstraint)
{
@@ -11996,6 +12003,38 @@ RebuildConstraintComment(AlteredTableInfo *tab, int pass, Oid objid,
tab->subcmds[pass] = lappend(tab->subcmds[pass], newcmd);
}
+/*
+ * For a table's index that is to be recreated due to PostAlterType
+ * processing, preserve its indisclustered property by issuing ALTER TABLE
+ * CLUSTER ON command on the table that will run after the command to recreate
+ * the index.
+ */
+static void
+PreserveClusterOn(AlteredTableInfo *tab, int pass, Oid indoid)
+{
+ HeapTuple indexTuple;
+ Form_pg_index indexForm;
+
+ Assert(OidIsValid(indoid));
+ Assert(pass == AT_PASS_OLD_INDEX);
+
+ indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indoid));
+ if (!HeapTupleIsValid(indexTuple))
+ elog(ERROR, "cache lookup failed for index %u", indoid);
+ indexForm = (Form_pg_index) GETSTRUCT(indexTuple);
+
+ if (indexForm->indisclustered)
+ {
+ AlterTableCmd *newcmd = makeNode(AlterTableCmd);
+
+ newcmd->subtype = AT_ClusterOn;
+ newcmd->name = get_rel_name(indoid);
+ tab->subcmds[pass] = lappend(tab->subcmds[pass], newcmd);
+ }
+
+ ReleaseSysCache(indexTuple);
+}
+
/*
* Subroutine for ATPostAlterTypeParse(). Calls out to CheckIndexCompatible()
* for the real analysis, then mutates the IndexStmt based on that verdict.
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index fb6d86a269..a01c6d6ec5 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -4296,3 +4296,37 @@ create trigger xtrig
update bar1 set a = a + 1;
INFO: a=1, b=1
/* End test case for bug #16242 */
+-- alter type rewrite/rebuild should preserve cluster marking on index
+create table alttype_cluster (a int);
+create index alttype_cluster_a on alttype_cluster (a);
+alter table alttype_cluster cluster on alttype_cluster_a;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+ indisclustered
+----------------
+ t
+(1 row)
+
+alter table alttype_cluster alter a type bigint;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+ indisclustered
+----------------
+ t
+(1 row)
+
+drop index alttype_cluster_a;
+alter table alttype_cluster add primary key (a);
+alter table alttype_cluster cluster on alttype_cluster_pkey;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+ indisclustered
+----------------
+ t
+(1 row)
+
+alter table alttype_cluster alter a type int;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+ indisclustered
+----------------
+ t
+(1 row)
+
+drop table alttype_cluster;
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 3801f19c58..6e9048bbec 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -2802,7 +2802,6 @@ drop table at_test_sql_partop;
drop operator class at_test_sql_partop using btree;
drop function at_test_sql_partop;
-
/* Test case for bug #16242 */
-- We create a parent and child where the child has missing
@@ -2840,3 +2839,18 @@ create trigger xtrig
update bar1 set a = a + 1;
/* End test case for bug #16242 */
+
+-- alter type rewrite/rebuild should preserve cluster marking on index
+create table alttype_cluster (a int);
+create index alttype_cluster_a on alttype_cluster (a);
+alter table alttype_cluster cluster on alttype_cluster_a;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+alter table alttype_cluster alter a type bigint;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+drop index alttype_cluster_a;
+alter table alttype_cluster add primary key (a);
+alter table alttype_cluster cluster on alttype_cluster_pkey;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+alter table alttype_cluster alter a type int;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+drop table alttype_cluster;
--
2.17.0
--bg08WKrSYDhXBjb5--
^ permalink raw reply [nested|flat] 37+ messages in thread
* [PATCH v7 1/2] ALTER tbl rewrite loses CLUSTER ON index
@ 2020-03-16 07:01 Amit Langote <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Amit Langote @ 2020-03-16 07:01 UTC (permalink / raw)
On Fri, Mar 13, 2020 at 2:19 AM Tom Lane <[email protected]> wrote:
> Justin Pryzby <[email protected]> writes:
> > @cfbot: resending with only Amit's 0001, since Michael pushed a variation on
> > 0002.
Thank you for taking a look at it.
> Boy, I really dislike this patch. ATPostAlterTypeParse is documented as
> using the supplied definition string, and nothing else, to reconstruct
> the index. This breaks that without even the courtesy of documenting
> the breakage. Moreover, the reason why it's designed like that is to
> avoid requiring the old index objects to still be accessible. So I'm
> surprised that this hack works at all. I don't think it would have
> worked at the time the code was first written, and I think it's imposing
> a constraint we'll have problems with (again?) in future.
Okay, so maybe in the middle of ATPostAlterTypeParse() is not a place
to do it, but don't these arguments apply to
RebuildConstraintComment(), which I based the patch on?
> The right way to fix this is to note the presence of the indisclustered
> flag when we're examining the index earlier, and include a suitable
> command in the definition string. So probably pg_get_indexdef_string()
> is what needs to change. It doesn't look like that's used anywhere
> else, so we can just redefine its behavior as needed.
I came across a commit that recently went in:
commit 1cc9c2412cc9a2fbe6a381170097d315fd40ccca
Author: Peter Eisentraut <[email protected]>
Date: Fri Mar 13 11:28:11 2020 +0100
Preserve replica identity index across ALTER TABLE rewrite
which fixes something very similar to what we are trying to with this
patch. The way it's done looks to me very close to what you are
telling. I have updated the patch to be similar to the above fix.
--
Thank you,
Amit
---
src/backend/commands/tablecmds.c | 49 +++++++++++++++++++++++
src/backend/utils/cache/lsyscache.c | 23 +++++++++++
src/include/utils/lsyscache.h | 1 +
src/test/regress/expected/alter_table.out | 34 ++++++++++++++++
src/test/regress/sql/alter_table.sql | 15 +++++++
5 files changed, 122 insertions(+)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 8c33b67c1b..73d9392878 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -177,6 +177,7 @@ typedef struct AlteredTableInfo
List *changedIndexOids; /* OIDs of indexes to rebuild */
List *changedIndexDefs; /* string definitions of same */
char *replicaIdentityIndex; /* index to reset as REPLICA IDENTITY */
+ char *clusterOnIndex; /* index to CLUSTER ON */
} AlteredTableInfo;
/* Struct describing one new constraint to check in Phase 3 scan */
@@ -11579,9 +11580,28 @@ RememberReplicaIdentityForRebuilding(Oid indoid, AlteredTableInfo *tab)
tab->replicaIdentityIndex = get_rel_name(indoid);
}
+/*
+ * Subroutine for ATExecAlterColumnType: remember any clustered index.
+ */
+static void
+RememberClusterOnForRebuilding(Oid indoid, AlteredTableInfo *tab)
+{
+ if (!get_index_isclustered(indoid))
+ return;
+
+ if (tab->clusterOnIndex)
+ elog(ERROR, "relation %u has multiple clustered indexes", tab->relid);
+
+ tab->clusterOnIndex = get_rel_name(indoid);
+}
+
/*
* Subroutine for ATExecAlterColumnType: remember that a constraint needs
* to be rebuilt (which we might already know).
+ *
+ * For constraint's index (if any), also remember if it's the table's replica
+ * identity or its clustered index, so that ATPostAlterTypeCleanup() can
+ * queue up commands necessary to restore that property.
*/
static void
RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab)
@@ -11604,9 +11624,18 @@ RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab)
tab->changedConstraintDefs = lappend(tab->changedConstraintDefs,
defstring);
+ /*
+ * For constraint's index (if any), remember if it's the table's
+ * replica identity or its clustered index, so that
+ * ATPostAlterTypeCleanup() can queue up commands necessary to restore
+ * that property.
+ */
indoid = get_constraint_index(conoid);
if (OidIsValid(indoid))
+ {
RememberReplicaIdentityForRebuilding(indoid, tab);
+ RememberClusterOnForRebuilding(indoid, tab);
+ }
}
}
@@ -11650,7 +11679,13 @@ RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab)
tab->changedIndexDefs = lappend(tab->changedIndexDefs,
defstring);
+ /*
+ * Remember if it's the table's replica identity or its clustered
+ * index, so that ATPostAlterTypeCleanup() can queue up commands
+ * necessary to restore that property.
+ */
RememberReplicaIdentityForRebuilding(indoid, tab);
+ RememberClusterOnForRebuilding(indoid, tab);
}
}
}
@@ -11777,6 +11812,20 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd);
}
+ /*
+ * Queue up command to restore clustered index marking
+ */
+ if (tab->clusterOnIndex)
+ {
+ AlterTableCmd *cmd = makeNode(AlterTableCmd);
+
+ cmd->subtype = AT_ClusterOn;
+ cmd->name = tab->clusterOnIndex;
+
+ /* do it after indexes and constraints */
+ tab->subcmds[AT_PASS_OLD_CONSTR] =
+ lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd);
+ }
/*
* It should be okay to use DROP_RESTRICT here, since nothing else should
* be depending on these objects.
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 27bbb58f56..9c5b806c22 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -3299,3 +3299,26 @@ get_index_isvalid(Oid index_oid)
return isvalid;
}
+
+/*
+ * get_index_isclustered
+ *
+ * Given the index OID, return pg_index.indisclustered.
+ */
+bool
+get_index_isclustered(Oid index_oid)
+{
+ bool isclustered;
+ HeapTuple tuple;
+ Form_pg_index rd_index;
+
+ tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(index_oid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for index %u", index_oid);
+
+ rd_index = (Form_pg_index) GETSTRUCT(tuple);
+ isclustered = rd_index->indisclustered;
+ ReleaseSysCache(tuple);
+
+ return isclustered;
+}
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4e646c55e9..9459c6a94f 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -184,6 +184,7 @@ extern Oid get_range_collation(Oid rangeOid);
extern Oid get_index_column_opclass(Oid index_oid, int attno);
extern bool get_index_isreplident(Oid index_oid);
extern bool get_index_isvalid(Oid index_oid);
+extern bool get_index_isclustered(Oid index_oid);
#define type_is_array(typid) (get_element_type(typid) != InvalidOid)
/* type_is_array_domain accepts both plain arrays and domains over arrays */
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index fb6d86a269..a01c6d6ec5 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -4296,3 +4296,37 @@ create trigger xtrig
update bar1 set a = a + 1;
INFO: a=1, b=1
/* End test case for bug #16242 */
+-- alter type rewrite/rebuild should preserve cluster marking on index
+create table alttype_cluster (a int);
+create index alttype_cluster_a on alttype_cluster (a);
+alter table alttype_cluster cluster on alttype_cluster_a;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+ indisclustered
+----------------
+ t
+(1 row)
+
+alter table alttype_cluster alter a type bigint;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+ indisclustered
+----------------
+ t
+(1 row)
+
+drop index alttype_cluster_a;
+alter table alttype_cluster add primary key (a);
+alter table alttype_cluster cluster on alttype_cluster_pkey;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+ indisclustered
+----------------
+ t
+(1 row)
+
+alter table alttype_cluster alter a type int;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+ indisclustered
+----------------
+ t
+(1 row)
+
+drop table alttype_cluster;
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 3801f19c58..4eeec24e58 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -2840,3 +2840,18 @@ create trigger xtrig
update bar1 set a = a + 1;
/* End test case for bug #16242 */
+
+-- alter type rewrite/rebuild should preserve cluster marking on index
+create table alttype_cluster (a int);
+create index alttype_cluster_a on alttype_cluster (a);
+alter table alttype_cluster cluster on alttype_cluster_a;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+alter table alttype_cluster alter a type bigint;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+drop index alttype_cluster_a;
+alter table alttype_cluster add primary key (a);
+alter table alttype_cluster cluster on alttype_cluster_pkey;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+alter table alttype_cluster alter a type int;
+select indisclustered from pg_index where indrelid = 'alttype_cluster'::regclass;
+drop table alttype_cluster;
--
2.17.0
--aZoGpuMECXJckB41
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v7-0002-Use-get_index_isclustered-in-cluster.c.patch"
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: SQL:2011 application time
@ 2023-07-06 08:12 Peter Eisentraut <[email protected]>
0 siblings, 2 replies; 37+ messages in thread
From: Peter Eisentraut @ 2023-07-06 08:12 UTC (permalink / raw)
To: Daniel Gustafsson <[email protected]>; Paul Jungwirth <[email protected]>; +Cc: Corey Huinker <[email protected]>; PostgreSQL Hackers <[email protected]>; Jaime Casanova <[email protected]>
On 04.07.23 14:48, Daniel Gustafsson wrote:
>> On 8 May 2023, at 09:10, Peter Eisentraut <[email protected]> wrote:
>>
>> On 03.05.23 23:02, Paul Jungwirth wrote:
>>> Thank you again for the review. Here is a patch with most of your feedback addressed. Sorry it has taken so long! These patches are rebased up to 1ab763fc22adc88e5d779817e7b42b25a9dd7c9e
>>> (May 3).
>>
>> Here are a few small fixup patches to get your patch set compiling cleanly.
>>
>> Also, it looks like the patches 0002, 0003, and 0004 are not split up correctly. 0002 contains tests using the FOR PORTION OF syntax introduced in 0003, and 0003 uses the function build_period_range() from 0004.
>
> These patches no longer apply without a new rebase. Should this patch be
> closed in while waiting for the prequisite of adding btree_gist to core
> mentioned upthread? I see no patch registered in the CF for this unless I'm
> missing sometihng.
I had talked to Paul about this offline a while ago. btree_gist to core
is no longer considered a prerequisite. But Paul was planning to
produce a new patch set that is arranged and sequenced a bit
differently. Apparently, that new version is not done yet, so it would
make sense to either close this entry as returned with feedback, or move
it to the next commit fest as waiting on author.
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: SQL:2011 application time
@ 2023-07-06 08:24 Daniel Gustafsson <[email protected]>
parent: Peter Eisentraut <[email protected]>
1 sibling, 0 replies; 37+ messages in thread
From: Daniel Gustafsson @ 2023-07-06 08:24 UTC (permalink / raw)
To: Peter Eisentraut <[email protected]>; +Cc: Paul Jungwirth <[email protected]>; Corey Huinker <[email protected]>; PostgreSQL Hackers <[email protected]>; Jaime Casanova <[email protected]>
> On 6 Jul 2023, at 10:12, Peter Eisentraut <[email protected]> wrote:
> it would make sense to either close this entry as returned with feedback, or move it to the next commit fest as waiting on author.
Fair enough, done.
--
Daniel Gustafsson
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: SQL:2011 application time
@ 2023-07-07 01:03 Paul A Jungwirth <[email protected]>
parent: Peter Eisentraut <[email protected]>
1 sibling, 2 replies; 37+ messages in thread
From: Paul A Jungwirth @ 2023-07-07 01:03 UTC (permalink / raw)
To: Peter Eisentraut <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Corey Huinker <[email protected]>; PostgreSQL Hackers <[email protected]>; Jaime Casanova <[email protected]>
On Thu, Jul 6, 2023 at 1:13 AM Peter Eisentraut
<[email protected]> wrote:
>
> I had talked to Paul about this offline a while ago. btree_gist to core
> is no longer considered a prerequisite. But Paul was planning to
> produce a new patch set that is arranged and sequenced a bit
> differently. Apparently, that new version is not done yet, so it would
> make sense to either close this entry as returned with feedback, or move
> it to the next commit fest as waiting on author.
Here are some new patch files based on discussions from PGCon. The
patches are reorganized a bit to hopefully make them easier to review:
Initially I implement all functionality on just range columns, without
supporting PERIODs yet. There are patches for temporal PRIMARY
KEY/UNIQUE constraints, for simple foreign keys (without CASCADE/SET
NULL/SET DEFAULT), for UPDATE/DELETE FOR PORTION OF, and then for the
rest of the FK support (which depends on FOR PORTION OF). If you
compare these patches to the v11 ones, you'll see that a ton of
clutter disappears by not supporting PERIODs as a separate "thing".
Finally there is a patch adding PERIOD syntax, but with a new
implementation where a PERIOD causes us to just define a GENERATED
range column. That means we can support all the same things as before
but without adding the clutter. This patch isn't quite working yet
(especially ALTER TABLE), but I thought I'd send where I'm at so far,
since it sounds like folks are interested in doing a review. Also it
was a little tricky dealing with the dependency between the PERIOD and
the GENERATED column. (See the comments in the patch.) If anyone has a
suggestion there I'd be happy to hear it.
My goal is to include another patch soon to support hidden columns, so
that the period's GENERATED column can be hidden. I read the
conversation about a recent patch attempt for something similar, and I
think I can use most of that (but cut some of the things the community
was worried about).
All these patches need some polishing, but I think there is enough new
here for them to be worth reading for anyone interested in temporal
progress.
I'll set this commitfest entry back to Needs Review. Thanks for taking a look!
Paul
Attachments:
[application/octet-stream] v12-0001-Add-temporal-PRIMARY-KEY-and-UNIQUE-constraints.patch (70.6K, ../../CA+renyVyMOb+t6QNoq=PBK1CY_oa9wdJrObjj6S1-RK9CxYwKQ@mail.gmail.com/2-v12-0001-Add-temporal-PRIMARY-KEY-and-UNIQUE-constraints.patch)
download | inline diff:
From 26bb9452a86b2293f26806cbbde13bbe1a15adee Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Mon, 28 Jun 2021 17:33:27 -0700
Subject: [PATCH v12 1/5] Add temporal PRIMARY KEY and UNIQUE constraints
- Added WITHOUT OVERLAPS to the bison grammar. We permit either range
columns but not yets PERIODs (which we'll add soon).
- Temporal PRIMARY KEYs and UNIQUE constraints are backed by GiST
indexes instead of B-tree indexes, since they are essentially
exclusion constraints with = for the scalar parts of the key and &&
for the temporal part.
- Added pg_constraint.contemporal to say whether a constraint is a
temporal constraint.
- Added docs and tests.
- Added pg_dump support.
---
doc/src/sgml/ref/create_table.sgml | 38 ++-
src/backend/catalog/heap.c | 1 +
src/backend/catalog/index.c | 17 +-
src/backend/catalog/pg_constraint.c | 2 +
src/backend/commands/indexcmds.c | 137 +++++++-
src/backend/commands/tablecmds.c | 14 +-
src/backend/commands/trigger.c | 1 +
src/backend/commands/typecmds.c | 1 +
src/backend/nodes/makefuncs.c | 3 +-
src/backend/parser/gram.y | 32 +-
src/backend/parser/parse_utilcmd.c | 120 ++++++-
src/backend/utils/adt/ruleutils.c | 29 +-
src/backend/utils/cache/relcache.c | 20 +-
src/bin/pg_dump/pg_dump.c | 34 +-
src/bin/pg_dump/pg_dump.h | 1 +
src/bin/pg_dump/t/002_pg_dump.pl | 46 +++
src/bin/psql/describe.c | 12 +-
src/include/catalog/index.h | 1 +
src/include/catalog/pg_constraint.h | 9 +-
src/include/commands/defrem.h | 3 +-
src/include/nodes/execnodes.h | 2 +
src/include/nodes/makefuncs.h | 2 +-
src/include/nodes/parsenodes.h | 4 +
.../regress/expected/without_overlaps.out | 296 ++++++++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/without_overlaps.sql | 233 ++++++++++++++
26 files changed, 997 insertions(+), 63 deletions(-)
create mode 100644 src/test/regress/expected/without_overlaps.out
create mode 100644 src/test/regress/sql/without_overlaps.sql
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 10ef699fab..58c77d09b4 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -77,8 +77,8 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
[ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
{ CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
- UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> |
- PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> |
+ UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">temporal_interval</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
+ PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">temporal_interval</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
@@ -106,6 +106,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
{ <replaceable class="parameter">column_name</replaceable> | ( <replaceable class="parameter">expression</replaceable> ) } [ <replaceable class="parameter">opclass</replaceable> ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ]
+<phrase><replaceable class="parameter">temporal_interval</replaceable> in a <literal>PRIMARY KEY</literal>, <literal>UNIQUE</literal>, or <literal>FOREIGN KEY</literal> constraint is:</phrase>
+
+<replaceable class="parameter">range_column_name</replaceable>
+
<phrase><replaceable class="parameter">referential_action</replaceable> in a <literal>FOREIGN KEY</literal>/<literal>REFERENCES</literal> constraint is:</phrase>
{ NO ACTION | RESTRICT | CASCADE | SET NULL [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] | SET DEFAULT [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] }
@@ -1000,7 +1004,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<para>
Adding a unique constraint will automatically create a unique btree
- index on the column or group of columns used in the constraint.
+ index on the column or group of columns used in the constraint. But if
+ the constraint includes a <literal>WITHOUT OVERLAPS</literal> clause,
+ it will use a GiST index and behave like a temporal <literal>PRIMARY
+ KEY</literal>: preventing duplicates only in overlapping time periods.
</para>
<para>
@@ -1018,7 +1025,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<varlistentry id="sql-createtable-parms-primary-key">
<term><literal>PRIMARY KEY</literal> (column constraint)</term>
- <term><literal>PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] )</literal>
+ <term><literal>PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ]
+ [, <replaceable class="parameter">temporal_interval</replaceable> WITHOUT OVERLAPS ] )</literal>
<optional> <literal>INCLUDE ( <replaceable class="parameter">column_name</replaceable> [, ...])</literal> </optional> (table constraint)</term>
<listitem>
<para>
@@ -1052,8 +1060,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<para>
Adding a <literal>PRIMARY KEY</literal> constraint will automatically
- create a unique btree index on the column or group of columns used in the
- constraint.
+ create a unique btree index (or GiST if temporal) on the column or group of
+ columns used in the constraint.
</para>
<para>
@@ -1066,6 +1074,24 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
(e.g., <literal>DROP COLUMN</literal>) can cause cascaded constraint and
index deletion.
</para>
+
+ <para>
+ A <literal>PRIMARY KEY</literal> with a <literal>WITHOUT OVERLAPS</literal> option
+ is a <emphasis>temporal</emphasis> primary key.
+ The <literal>WITHOUT OVERLAPS</literal> value
+ must be a range type and is used to constrain the record's applicability
+ to just that interval (usually a range of dates or timestamps).
+ The main part of the primary key may be repeated in other rows of the table,
+ as long as records with the same key don't overlap in the
+ <literal>WITHOUT OVERLAPS</literal> column.
+ </para>
+
+ <para>
+ A temporal <literal>PRIMARY KEY</literal> is enforced with an
+ <literal>EXCLUDE</literal> constraint rather than a <literal>UNIQUE</literal>
+ constraint, backed by a GiST index. You may need to install the
+ <xref linkend="btree-gist"/> extension to create temporal primary keys.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 2a0d82aedd..b67fb8400c 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2153,6 +2153,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
is_local, /* conislocal */
inhcount, /* coninhcount */
is_no_inherit, /* connoinherit */
+ false, /* contemporal */
is_internal); /* internally constructed? */
pfree(ccbin);
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 67b743e251..8715231605 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -255,8 +255,8 @@ index_check_primary_key(Relation heapRel,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("primary keys cannot be expressions")));
- /* System attributes are never null, so no need to check */
- if (attnum < 0)
+ /* * System attributes are never null, so no need to check. */
+ if (attnum <= 0)
continue;
atttuple = SearchSysCache2(ATTNUM,
@@ -1381,7 +1381,8 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId,
oldInfo->ii_NullsNotDistinct,
false, /* not ready for inserts */
true,
- indexRelation->rd_indam->amsummarizing);
+ indexRelation->rd_indam->amsummarizing,
+ oldInfo->ii_Temporal);
/*
* Extract the list of column names and the column numbers for the new
@@ -1901,6 +1902,7 @@ index_concurrently_set_dead(Oid heapId, Oid indexId)
* INDEX_CONSTR_CREATE_UPDATE_INDEX: update the pg_index row
* INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS: remove existing dependencies
* of index on table's columns
+ * INDEX_CONSTR_CREATE_TEMPORAL: constraint is for a temporal primary key
* allow_system_table_mods: allow table to be a system catalog
* is_internal: index is constructed due to internal process
*/
@@ -1924,11 +1926,13 @@ index_constraint_create(Relation heapRelation,
bool mark_as_primary;
bool islocal;
bool noinherit;
+ bool is_temporal;
int inhcount;
deferrable = (constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) != 0;
initdeferred = (constr_flags & INDEX_CONSTR_CREATE_INIT_DEFERRED) != 0;
mark_as_primary = (constr_flags & INDEX_CONSTR_CREATE_MARK_AS_PRIMARY) != 0;
+ is_temporal = (constr_flags & INDEX_CONSTR_CREATE_TEMPORAL) != 0;
/* constraint creation support doesn't work while bootstrapping */
Assert(!IsBootstrapProcessingMode());
@@ -2005,6 +2009,7 @@ index_constraint_create(Relation heapRelation,
islocal,
inhcount,
noinherit,
+ is_temporal, /* contemporal */
is_internal);
/*
@@ -2454,7 +2459,8 @@ BuildIndexInfo(Relation index)
indexStruct->indnullsnotdistinct,
indexStruct->indisready,
false,
- index->rd_indam->amsummarizing);
+ index->rd_indam->amsummarizing,
+ false);
/* fill in attribute numbers */
for (i = 0; i < numAtts; i++)
@@ -2515,7 +2521,8 @@ BuildDummyIndexInfo(Relation index)
indexStruct->indnullsnotdistinct,
indexStruct->indisready,
false,
- index->rd_indam->amsummarizing);
+ index->rd_indam->amsummarizing,
+ false);
/* fill in attribute numbers */
for (i = 0; i < numAtts; i++)
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 4002317f70..edce9f4b75 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -77,6 +77,7 @@ CreateConstraintEntry(const char *constraintName,
bool conIsLocal,
int conInhCount,
bool conNoInherit,
+ bool conTemporal,
bool is_internal)
{
Relation conDesc;
@@ -192,6 +193,7 @@ CreateConstraintEntry(const char *constraintName,
values[Anum_pg_constraint_conislocal - 1] = BoolGetDatum(conIsLocal);
values[Anum_pg_constraint_coninhcount - 1] = Int16GetDatum(conInhCount);
values[Anum_pg_constraint_connoinherit - 1] = BoolGetDatum(conNoInherit);
+ values[Anum_pg_constraint_contemporal - 1] = BoolGetDatum(conTemporal);
if (conkeyArray)
values[Anum_pg_constraint_conkey - 1] = PointerGetDatum(conkeyArray);
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 403f5fc143..7c47884e9c 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -74,6 +74,11 @@
/* non-export function prototypes */
static bool CompareOpclassOptions(Datum *opts1, Datum *opts2, int natts);
static void CheckPredicate(Expr *predicate);
+static void get_index_attr_temporal_operator(Oid opclass,
+ Oid atttype,
+ bool isoverlaps,
+ Oid *opid,
+ int *strat);
static void ComputeIndexAttrs(IndexInfo *indexInfo,
Oid *typeOidP,
Oid *collationOidP,
@@ -85,6 +90,7 @@ static void ComputeIndexAttrs(IndexInfo *indexInfo,
const char *accessMethodName, Oid accessMethodId,
bool amcanorder,
bool isconstraint,
+ bool istemporal,
Oid ddl_userid,
int ddl_sec_context,
int *ddl_save_nestlevel);
@@ -142,6 +148,7 @@ typedef struct ReindexErrorInfo
* to index on.
* 'exclusionOpNames': list of names of exclusion-constraint operators,
* or NIL if not an exclusion constraint.
+ * 'istemporal': true iff this index has a WITHOUT OVERLAPS clause.
*
* This is tailored to the needs of ALTER TABLE ALTER TYPE, which recreates
* any indexes that depended on a changing column from their pg_get_indexdef
@@ -171,7 +178,8 @@ bool
CheckIndexCompatible(Oid oldId,
const char *accessMethodName,
List *attributeList,
- List *exclusionOpNames)
+ List *exclusionOpNames,
+ bool istemporal)
{
bool isconstraint;
Oid *typeObjectId;
@@ -234,7 +242,7 @@ CheckIndexCompatible(Oid oldId,
*/
indexInfo = makeIndexInfo(numberOfAttributes, numberOfAttributes,
accessMethodId, NIL, NIL, false, false,
- false, false, amsummarizing);
+ false, false, amsummarizing, false);
typeObjectId = palloc_array(Oid, numberOfAttributes);
collationObjectId = palloc_array(Oid, numberOfAttributes);
classObjectId = palloc_array(Oid, numberOfAttributes);
@@ -244,8 +252,8 @@ CheckIndexCompatible(Oid oldId,
coloptions, attributeList,
exclusionOpNames, relationId,
accessMethodName, accessMethodId,
- amcanorder, isconstraint, InvalidOid, 0, NULL);
-
+ amcanorder, isconstraint, istemporal, InvalidOid,
+ 0, NULL);
/* Get the soon-obsolete pg_index tuple. */
tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(oldId));
@@ -847,7 +855,7 @@ DefineIndex(Oid relationId,
pgstat_progress_update_param(PROGRESS_CREATEIDX_ACCESS_METHOD_OID,
accessMethodId);
- if (stmt->unique && !amRoutine->amcanunique)
+ if (stmt->unique && !stmt->istemporal && !amRoutine->amcanunique)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("access method \"%s\" does not support unique indexes",
@@ -903,7 +911,8 @@ DefineIndex(Oid relationId,
stmt->nulls_not_distinct,
!concurrent,
concurrent,
- amissummarizing);
+ amissummarizing,
+ stmt->istemporal);
typeObjectId = palloc_array(Oid, numberOfAttributes);
collationObjectId = palloc_array(Oid, numberOfAttributes);
@@ -914,8 +923,9 @@ DefineIndex(Oid relationId,
coloptions, allIndexParams,
stmt->excludeOpNames, relationId,
accessMethodName, accessMethodId,
- amcanorder, stmt->isconstraint, root_save_userid,
- root_save_sec_context, &root_save_nestlevel);
+ amcanorder, stmt->isconstraint, stmt->istemporal,
+ root_save_userid, root_save_sec_context,
+ &root_save_nestlevel);
/*
* Extra checks when creating a PRIMARY KEY index.
@@ -940,6 +950,8 @@ DefineIndex(Oid relationId,
if (stmt->primary)
constraint_type = "PRIMARY KEY";
+ else if (stmt->unique && stmt->istemporal)
+ constraint_type = "temporal UNIQUE";
else if (stmt->unique)
constraint_type = "UNIQUE";
else if (stmt->excludeOpNames != NIL)
@@ -1159,6 +1171,8 @@ DefineIndex(Oid relationId,
constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE;
if (stmt->initdeferred)
constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED;
+ if (stmt->istemporal)
+ constr_flags |= INDEX_CONSTR_CREATE_TEMPORAL;
indexRelationId =
index_create(rel, indexRelationName, indexRelationId, parentIndexId,
@@ -1828,6 +1842,91 @@ CheckPredicate(Expr *predicate)
errmsg("functions in index predicate must be marked IMMUTABLE")));
}
+/*
+ * get_index_attr_temporal_operator
+ *
+ * Finds an operator for a temporal index attribute.
+ * We need an equality operator for normal keys
+ * and an overlaps operator for the range.
+ * Returns the operator oid and strategy in opid and strat,
+ * respectively.
+ */
+static void
+get_index_attr_temporal_operator(Oid opclass,
+ Oid atttype,
+ bool isoverlaps,
+ Oid *opid,
+ int *strat)
+{
+ Oid opfamily;
+ const char *opname;
+
+ opfamily = get_opclass_family(opclass);
+ /*
+ * If we have a range type, fall back on anyrange.
+ * This seems like a hack
+ * but I can't find any existing lookup function
+ * that knows about pseudotypes.
+ * compatible_oper is close but wants a *name*,
+ * and the point here is to avoid hardcoding a name
+ * (although it should always be = and &&).
+ *
+ * In addition for the normal key elements
+ * try both RTEqualStrategyNumber and BTEqualStrategyNumber.
+ * If you're using btree_gist then you'll need the latter.
+ */
+ if (isoverlaps)
+ {
+ *strat = RTOverlapStrategyNumber;
+ opname = "overlaps";
+ *opid = get_opfamily_member(opfamily, atttype, atttype, *strat);
+ if (!OidIsValid(*opid) && type_is_range(atttype))
+ *opid = get_opfamily_member(opfamily, ANYRANGEOID, ANYRANGEOID, *strat);
+ }
+ else
+ {
+ *strat = RTEqualStrategyNumber;
+ opname = "equality";
+ *opid = get_opfamily_member(opfamily, atttype, atttype, *strat);
+ if (!OidIsValid(*opid) && type_is_range(atttype))
+ *opid = get_opfamily_member(opfamily, ANYRANGEOID, ANYRANGEOID, *strat);
+
+ if (!OidIsValid(*opid))
+ {
+ *strat = BTEqualStrategyNumber;
+ *opid = get_opfamily_member(opfamily, atttype, atttype, *strat);
+ if (!OidIsValid(*opid) && type_is_range(atttype))
+ *opid = get_opfamily_member(opfamily, ANYRANGEOID, ANYRANGEOID, *strat);
+ }
+ }
+
+ if (!OidIsValid(*opid))
+ {
+ HeapTuple opftuple;
+ Form_pg_opfamily opfform;
+
+ /*
+ * attribute->opclass might not explicitly name the opfamily,
+ * so fetch the name of the selected opfamily for use in the
+ * error message.
+ */
+ opftuple = SearchSysCache1(OPFAMILYOID,
+ ObjectIdGetDatum(opfamily));
+ if (!HeapTupleIsValid(opftuple))
+ elog(ERROR, "cache lookup failed for opfamily %u",
+ opfamily);
+ opfform = (Form_pg_opfamily) GETSTRUCT(opftuple);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("no %s operator found for WITHOUT OVERLAPS constraint", opname),
+ errdetail("There must be an %s operator within opfamily \"%s\" for type \"%s\".",
+ opname,
+ NameStr(opfform->opfname),
+ format_type_be(atttype))));
+ }
+}
+
/*
* Compute per-index-column information, including indexed column numbers
* or index expressions, opclasses and their options. Note, all output vectors
@@ -1850,6 +1949,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
Oid accessMethodId,
bool amcanorder,
bool isconstraint,
+ bool istemporal,
Oid ddl_userid,
int ddl_sec_context,
int *ddl_save_nestlevel)
@@ -1873,6 +1973,14 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
else
nextExclOp = NULL;
+ if (istemporal)
+ {
+ Assert(exclusionOpNames == NIL);
+ indexInfo->ii_ExclusionOps = palloc_array(Oid, nkeycols);
+ indexInfo->ii_ExclusionProcs = palloc_array(Oid, nkeycols);
+ indexInfo->ii_ExclusionStrats = palloc_array(uint16, nkeycols);
+ }
+
if (OidIsValid(ddl_userid))
GetUserIdAndSecContext(&save_userid, &save_sec_context);
@@ -2148,6 +2256,19 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
indexInfo->ii_ExclusionStrats[attn] = strat;
nextExclOp = lnext(exclusionOpNames, nextExclOp);
}
+ else if (istemporal)
+ {
+ int strat;
+ Oid opid;
+ get_index_attr_temporal_operator(classOidP[attn],
+ atttype,
+ attn == nkeycols - 1, /* TODO: Don't assume it's last? */
+ &opid,
+ &strat);
+ indexInfo->ii_ExclusionOps[attn] = opid;
+ indexInfo->ii_ExclusionProcs[attn] = get_opcode(opid);
+ indexInfo->ii_ExclusionStrats[attn] = strat;
+ }
/*
* Set up the per-column options (indoption field). For now, this is
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index fce5e6f220..9e3a88a7dd 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -208,6 +208,7 @@ typedef struct NewConstraint
Oid refrelid; /* PK rel, if FOREIGN */
Oid refindid; /* OID of PK's index, if FOREIGN */
Oid conid; /* OID of pg_constraint entry, if FOREIGN */
+ bool contemporal; /* Whether the new constraint is temporal */
Node *qual; /* Check expr or CONSTR_FOREIGN Constraint */
ExprState *qualstate; /* Execution state for CHECK expr */
} NewConstraint;
@@ -9696,6 +9697,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
conislocal, /* islocal */
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
+ false, /* conTemporal */
false); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -9994,6 +9996,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
false,
1,
false,
+ false, /* conTemporal */
false);
/*
@@ -10499,6 +10502,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
false, /* islocal */
1, /* inhcount */
false, /* conNoInherit */
+ false, /* conTemporal */
true);
/* Set up partition dependencies for the new constraint */
@@ -11173,6 +11177,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
newcon->refrelid = con->confrelid;
newcon->refindid = con->conindid;
newcon->conid = con->oid;
+ newcon->contemporal = con->contemporal;
newcon->qual = (Node *) fkconstraint;
/* Find or create work queue entry for this table */
@@ -11339,10 +11344,12 @@ transformColumnNameList(Oid relId, List *colList,
*
* Look up the names, attnums, and types of the primary key attributes
* for the pkrel. Also return the index OID and index opclasses of the
- * index supporting the primary key.
+ * index supporting the primary key. If this is a temporal primary key,
+ * also set the WITHOUT OVERLAPS attribute name, attnum, and atttypid.
*
* All parameters except pkrel are output parameters. Also, the function
- * return value is the number of attributes in the primary key.
+ * return value is the number of attributes in the primary key,
+ * not including the WITHOUT OVERLAPS if any.
*
* Used when the column list in the REFERENCES specification is omitted.
*/
@@ -13635,7 +13642,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)
if (CheckIndexCompatible(oldId,
stmt->accessMethod,
stmt->indexParams,
- stmt->excludeOpNames))
+ stmt->excludeOpNames,
+ stmt->istemporal))
{
Relation irel = index_open(oldId, NoLock);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 52177759ab..0577b60415 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -841,6 +841,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
true, /* islocal */
0, /* inhcount */
true, /* noinherit */
+ false, /* contemporal */
isInternal); /* is_internal */
}
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 216482095d..4fff196477 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3544,6 +3544,7 @@ domainAddConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
true, /* is local */
0, /* inhcount */
false, /* connoinherit */
+ false, /* contemporal */
false); /* is_internal */
if (constrAddr)
ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid);
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 39e1884cf4..25e8d914f1 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -745,7 +745,7 @@ make_ands_implicit(Expr *clause)
IndexInfo *
makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid, List *expressions,
List *predicates, bool unique, bool nulls_not_distinct,
- bool isready, bool concurrent, bool summarizing)
+ bool isready, bool concurrent, bool summarizing, bool temporal)
{
IndexInfo *n = makeNode(IndexInfo);
@@ -755,6 +755,7 @@ makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid, List *expressions,
Assert(n->ii_NumIndexKeyAttrs <= n->ii_NumIndexAttrs);
n->ii_Unique = unique;
n->ii_NullsNotDistinct = nulls_not_distinct;
+ n->ii_Temporal = temporal;
n->ii_ReadyForInserts = isready;
n->ii_CheckedUnchanged = false;
n->ii_IndexUnchanged = false;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 39ab7eac0d..a5d51e21f8 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -523,7 +523,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <node> TableElement TypedTableElement ConstraintElem TableFuncElement
%type <node> columnDef columnOptions
%type <defelt> def_elem reloption_elem old_aggr_elem operator_def_elem
-%type <node> def_arg columnElem where_clause where_or_current_clause
+%type <node> def_arg columnElem withoutOverlapsClause
+ where_clause where_or_current_clause
a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound
columnref in_expr having_clause func_table xmltable array_expr
OptWhereClause operator_def_arg
@@ -3875,6 +3876,7 @@ ColConstraintElem:
n->contype = CONSTR_PRIMARY;
n->location = @1;
n->keys = NULL;
+ n->without_overlaps = NULL;
n->options = $3;
n->indexname = NULL;
n->indexspace = $4;
@@ -4081,7 +4083,7 @@ ConstraintElem:
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
- | UNIQUE opt_unique_null_treatment '(' columnList ')' opt_c_include opt_definition OptConsTableSpace
+ | UNIQUE opt_unique_null_treatment '(' columnList withoutOverlapsClause ')' opt_c_include opt_definition OptConsTableSpace
ConstraintAttributeSpec
{
Constraint *n = makeNode(Constraint);
@@ -4090,11 +4092,12 @@ ConstraintElem:
n->location = @1;
n->nulls_not_distinct = !$2;
n->keys = $4;
- n->including = $6;
- n->options = $7;
+ n->without_overlaps = $5;
+ n->including = $7;
+ n->options = $8;
n->indexname = NULL;
- n->indexspace = $8;
- processCASbits($9, @9, "UNIQUE",
+ n->indexspace = $9;
+ processCASbits($10, @10, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
NULL, yyscanner);
$$ = (Node *) n;
@@ -4115,7 +4118,7 @@ ConstraintElem:
NULL, yyscanner);
$$ = (Node *) n;
}
- | PRIMARY KEY '(' columnList ')' opt_c_include opt_definition OptConsTableSpace
+ | PRIMARY KEY '(' columnList withoutOverlapsClause ')' opt_c_include opt_definition OptConsTableSpace
ConstraintAttributeSpec
{
Constraint *n = makeNode(Constraint);
@@ -4123,11 +4126,12 @@ ConstraintElem:
n->contype = CONSTR_PRIMARY;
n->location = @1;
n->keys = $4;
- n->including = $6;
- n->options = $7;
+ n->without_overlaps = $5;
+ n->including = $7;
+ n->options = $8;
n->indexname = NULL;
- n->indexspace = $8;
- processCASbits($9, @9, "PRIMARY KEY",
+ n->indexspace = $9;
+ processCASbits($10, @10, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
NULL, yyscanner);
$$ = (Node *) n;
@@ -4139,6 +4143,7 @@ ConstraintElem:
n->contype = CONSTR_PRIMARY;
n->location = @1;
n->keys = NIL;
+ n->without_overlaps = NULL;
n->including = NIL;
n->options = NIL;
n->indexname = $3;
@@ -4205,6 +4210,11 @@ columnList:
| columnList ',' columnElem { $$ = lappend($1, $3); }
;
+withoutOverlapsClause:
+ ',' columnElem WITHOUT OVERLAPS { $$ = $2; }
+ | /*EMPTY*/ { $$ = NULL; }
+ ;
+
columnElem: ColId
{
$$ = (Node *) makeString($1);
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index d67580fc77..04ca087973 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -123,6 +123,8 @@ static List *get_opclass(Oid opclass, Oid actual_datatype);
static void transformIndexConstraints(CreateStmtContext *cxt);
static IndexStmt *transformIndexConstraint(Constraint *constraint,
CreateStmtContext *cxt);
+static bool findNewOrOldColumn(CreateStmtContext *cxt, char *colname, char **typname,
+ Oid *typid);
static void transformExtendedStatistics(CreateStmtContext *cxt);
static void transformFKConstraints(CreateStmtContext *cxt,
bool skipValidation,
@@ -1588,6 +1590,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
index->unique = idxrec->indisunique;
index->nulls_not_distinct = idxrec->indnullsnotdistinct;
index->primary = idxrec->indisprimary;
+ index->istemporal = idxrec->indisprimary && idxrec->indisexclusion;
index->transformed = true; /* don't need transformIndexStmt */
index->concurrent = false;
index->if_not_exists = false;
@@ -1637,7 +1640,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
int nElems;
int i;
- Assert(conrec->contype == CONSTRAINT_EXCLUSION);
+ Assert(conrec->contype == CONSTRAINT_EXCLUSION || (index->istemporal && conrec->contype == CONSTRAINT_PRIMARY));
/* Extract operator OIDs from the pg_constraint tuple */
datum = SysCacheGetAttrNotNull(CONSTROID, ht_constr,
Anum_pg_constraint_conexclop);
@@ -2179,6 +2182,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
}
index->nulls_not_distinct = constraint->nulls_not_distinct;
index->isconstraint = true;
+ index->istemporal = constraint->without_overlaps != NULL;
index->deferrable = constraint->deferrable;
index->initdeferred = constraint->initdeferred;
@@ -2271,6 +2275,11 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
errmsg("index \"%s\" is not valid", index_name),
parser_errposition(cxt->pstate, constraint->location)));
+ /*
+ * Today we forbid non-unique indexes, but we could permit GiST
+ * indexes whose last entry is a range type and use that to create a
+ * WITHOUT OVERLAPS constraint (i.e. a temporal constraint).
+ */
if (!index_form->indisunique)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
@@ -2559,6 +2568,66 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
notnullcmds = lappend(notnullcmds, notnullcmd);
}
}
+
+ /*
+ * Anything in without_overlaps should be included,
+ * but with the overlaps operator (&&) instead of equality.
+ */
+ if (constraint->without_overlaps != NULL) {
+ char *without_overlaps_str = strVal(constraint->without_overlaps);
+ IndexElem *iparam = makeNode(IndexElem);
+ char *typname;
+ Oid typid;
+
+ /*
+ * Iterate through the table's columns
+ * (like just a little bit above).
+ * If we find one whose name is the same as without_overlaps,
+ * validate that it's a range type.
+ *
+ * Otherwise report an error.
+ */
+
+ if (findNewOrOldColumn(cxt, without_overlaps_str, &typname, &typid))
+ {
+ if (type_is_range(typid))
+ {
+ AlterTableCmd *notnullcmd;
+
+ iparam->name = pstrdup(without_overlaps_str);
+ iparam->expr = NULL;
+
+ /*
+ * Force the column to NOT NULL since it is part of the primary key.
+ */
+ notnullcmd = makeNode(AlterTableCmd);
+
+ notnullcmd->subtype = AT_SetNotNull;
+ notnullcmd->name = pstrdup(without_overlaps_str);
+ notnullcmds = lappend(notnullcmds, notnullcmd);
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("column \"%s\" named in WITHOUT OVERLAPS is not a range type",
+ without_overlaps_str)));
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("range or PERIOD \"%s\" named in WITHOUT OVERLAPS does not exist",
+ without_overlaps_str)));
+
+ iparam->indexcolname = NULL;
+ iparam->collation = NIL;
+ iparam->opclass = NIL;
+ iparam->ordering = SORTBY_DEFAULT;
+ iparam->nulls_ordering = SORTBY_NULLS_DEFAULT;
+ index->indexParams = lappend(index->indexParams, iparam);
+
+ index->accessMethod = "gist";
+ constraint->access_method = "gist";
+ }
}
/*
@@ -2678,6 +2747,55 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
return index;
}
+/*
+ * Tries to find a column by name among the existing ones (if it's an ALTER TABLE)
+ * and the new ones. Sets typname and typid if one is found. Returns false if we
+ * couldn't find a match.
+ */
+static bool
+findNewOrOldColumn(CreateStmtContext *cxt, char *colname, char **typname, Oid *typid)
+{
+ /* Check the new columns first in case their type is changing. */
+
+ ColumnDef *column = NULL;
+ ListCell *columns;
+
+ foreach(columns, cxt->columns)
+ {
+ column = lfirst_node(ColumnDef, columns);
+ if (strcmp(column->colname, colname) == 0)
+ {
+ *typid = typenameTypeId(NULL, column->typeName);
+ *typname = TypeNameToString(column->typeName);
+ return true;
+ }
+ }
+
+ // TODO: should I consider DROP COLUMN?
+
+ /* Look up columns on existing table. */
+
+ if (cxt->isalter)
+ {
+ Relation rel = cxt->rel;
+ for (int i = 0; i < rel->rd_att->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(rel->rd_att, i);
+ const char *attname = NameStr(attr->attname);
+ if (strcmp(attname, colname) == 0)
+ {
+ Type type = typeidType(attr->atttypid);
+ *typid = attr->atttypid;
+ *typname = pstrdup(typeTypeName(type));
+ ReleaseSysCache(type);
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
/*
* transformExtendedStatistics
* Handle extended statistic objects
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index d3a973d86b..0ef2038e5d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -338,8 +338,8 @@ static char *deparse_expression_pretty(Node *expr, List *dpcontext,
static char *pg_get_viewdef_worker(Oid viewoid,
int prettyFlags, int wrapColumn);
static char *pg_get_triggerdef_worker(Oid trigid, bool pretty);
-static int decompile_column_index_array(Datum column_index_array, Oid relId,
- StringInfo buf);
+static int decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
+ bool withoutOverlaps, StringInfo buf);
static char *pg_get_ruledef_worker(Oid ruleoid, int prettyFlags);
static char *pg_get_indexdef_worker(Oid indexrelid, int colno,
const Oid *excludeOps,
@@ -2247,7 +2247,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
val = SysCacheGetAttrNotNull(CONSTROID, tup,
Anum_pg_constraint_conkey);
- decompile_column_index_array(val, conForm->conrelid, &buf);
+ decompile_column_index_array(val, conForm->conrelid, conForm->conindid, false, &buf);
/* add foreign relation name */
appendStringInfo(&buf, ") REFERENCES %s(",
@@ -2258,7 +2258,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
val = SysCacheGetAttrNotNull(CONSTROID, tup,
Anum_pg_constraint_confkey);
- decompile_column_index_array(val, conForm->confrelid, &buf);
+ decompile_column_index_array(val, conForm->confrelid, conForm->conindid, false, &buf);
appendStringInfoChar(&buf, ')');
@@ -2344,7 +2344,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
if (!isnull)
{
appendStringInfoString(&buf, " (");
- decompile_column_index_array(val, conForm->conrelid, &buf);
+ decompile_column_index_array(val, conForm->conrelid, InvalidOid, false, &buf);
appendStringInfoChar(&buf, ')');
}
@@ -2357,6 +2357,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
Oid indexId;
int keyatts;
HeapTuple indtup;
+ bool isnull;
/* Start off the constraint definition */
if (conForm->contype == CONSTRAINT_PRIMARY)
@@ -2379,7 +2380,14 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
val = SysCacheGetAttrNotNull(CONSTROID, tup,
Anum_pg_constraint_conkey);
- keyatts = decompile_column_index_array(val, conForm->conrelid, &buf);
+ /*
+ * If it has exclusion-style operator OIDs
+ * then it uses WITHOUT OVERLAPS.
+ */
+ indexId = conForm->conindid;
+ SysCacheGetAttr(CONSTROID, tup,
+ Anum_pg_constraint_conexclop, &isnull);
+ keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, &buf);
appendStringInfoChar(&buf, ')');
@@ -2560,8 +2568,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
* of keys.
*/
static int
-decompile_column_index_array(Datum column_index_array, Oid relId,
- StringInfo buf)
+decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
+ bool withoutOverlaps, StringInfo buf)
{
Datum *keys;
int nKeys;
@@ -2574,11 +2582,14 @@ decompile_column_index_array(Datum column_index_array, Oid relId,
for (j = 0; j < nKeys; j++)
{
char *colName;
+ int colid = DatumGetInt16(keys[j]);
- colName = get_attname(relId, DatumGetInt16(keys[j]), false);
+ colName = get_attname(relId, colid, false);
if (j == 0)
appendStringInfoString(buf, quote_identifier(colName));
+ else if (withoutOverlaps && j == nKeys - 1)
+ appendStringInfo(buf, ", %s WITHOUT OVERLAPS", quote_identifier(colName));
else
appendStringInfo(buf, ", %s", quote_identifier(colName));
}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8a08463c2b..98ac1e4c1e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4792,11 +4792,17 @@ RelationGetIndexList(Relation relation)
* interesting for either oid indexes or replication identity indexes,
* so don't check them.
*/
- if (!index->indisvalid || !index->indisunique ||
- !index->indimmediate ||
+ if (!index->indisvalid || !index->indimmediate ||
!heap_attisnull(htup, Anum_pg_index_indpred, NULL))
continue;
+ /*
+ * Non-unique indexes aren't interesting either,
+ * except when they are temporal primary keys.
+ */
+ if (!index->indisunique && !index->indisprimary)
+ continue;
+
/* remember primary key index if any */
if (index->indisprimary)
pkeyIndex = index->indexrelid;
@@ -5507,8 +5513,9 @@ RelationGetIdentityKeyBitmap(Relation relation)
* RelationGetExclusionInfo -- get info about index's exclusion constraint
*
* This should be called only for an index that is known to have an
- * associated exclusion constraint. It returns arrays (palloc'd in caller's
- * context) of the exclusion operator OIDs, their underlying functions'
+ * associated exclusion constraint or temporal primary key/unique
+ * constraint. It returns arrays (palloc'd in caller's context)
+ * of the exclusion operator OIDs, their underlying functions'
* OIDs, and their strategy numbers in the index's opclasses. We cache
* all this information since it requires a fair amount of work to get.
*/
@@ -5574,7 +5581,10 @@ RelationGetExclusionInfo(Relation indexRelation,
int nelem;
/* We want the exclusion constraint owning the index */
- if (conform->contype != CONSTRAINT_EXCLUSION ||
+ if ((conform->contype != CONSTRAINT_EXCLUSION &&
+ !(conform->contemporal && (
+ conform->contype == CONSTRAINT_PRIMARY
+ || conform->contype == CONSTRAINT_UNIQUE))) ||
conform->conindid != RelationGetRelid(indexRelation))
continue;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 5dab1ba9ea..a95e1d3696 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6964,7 +6964,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
i_tablespace,
i_indreloptions,
i_indstatcols,
- i_indstatvals;
+ i_indstatvals,
+ i_withoutoverlaps;
/*
* We want to perform just one query against pg_index. However, we
@@ -7028,21 +7029,23 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
"(SELECT pg_catalog.array_agg(attstattarget ORDER BY attnum) "
" FROM pg_catalog.pg_attribute "
" WHERE attrelid = i.indexrelid AND "
- " attstattarget >= 0) AS indstatvals, ");
+ " attstattarget >= 0) AS indstatvals, "
+ "c.conexclop IS NOT NULL AS withoutoverlaps, ");
else
appendPQExpBufferStr(query,
"0 AS parentidx, "
"i.indnatts AS indnkeyatts, "
"i.indnatts AS indnatts, "
"'' AS indstatcols, "
- "'' AS indstatvals, ");
+ "'' AS indstatvals, "
+ "null AS withoutoverlaps, ");
if (fout->remoteVersion >= 150000)
appendPQExpBufferStr(query,
- "i.indnullsnotdistinct ");
+ "i.indnullsnotdistinct, ");
else
appendPQExpBufferStr(query,
- "false AS indnullsnotdistinct ");
+ "false AS indnullsnotdistinct, ");
/*
* The point of the messy-looking outer join is to find a constraint that
@@ -7117,6 +7120,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
i_indreloptions = PQfnumber(res, "indreloptions");
i_indstatcols = PQfnumber(res, "indstatcols");
i_indstatvals = PQfnumber(res, "indstatvals");
+ i_withoutoverlaps = PQfnumber(res, "withoutoverlaps");
indxinfo = (IndxInfo *) pg_malloc(ntups * sizeof(IndxInfo));
@@ -7219,6 +7223,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
constrinfo->condeferred = *(PQgetvalue(res, j, i_condeferred)) == 't';
constrinfo->conislocal = true;
constrinfo->separate = true;
+ constrinfo->withoutoverlaps = *(PQgetvalue(res, j, i_withoutoverlaps)) == 't';
indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
}
@@ -16657,9 +16662,22 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
break;
attname = getAttrName(indkey, tbinfo);
- appendPQExpBuffer(q, "%s%s",
- (k == 0) ? "" : ", ",
- fmtId(attname));
+ if (k == 0)
+ {
+ appendPQExpBuffer(q, "%s",
+ fmtId(attname));
+ }
+ else if (k == indxinfo->indnkeyattrs - 1 &&
+ coninfo->withoutoverlaps)
+ {
+ appendPQExpBuffer(q, ", %s WITHOUT OVERLAPS",
+ fmtId(attname));
+ }
+ else
+ {
+ appendPQExpBuffer(q, ", %s",
+ fmtId(attname));
+ }
}
if (indxinfo->indnkeyattrs < indxinfo->indnattrs)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bc8f2ec36d..2afb5d5294 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -484,6 +484,7 @@ typedef struct _constraintInfo
bool condeferred; /* true if constraint is INITIALLY DEFERRED */
bool conislocal; /* true if constraint has local definition */
bool separate; /* true if must dump as separate item */
+ bool withoutoverlaps; /* true if the last elem is WITHOUT OVERLAPS */
} ConstraintInfo;
typedef struct _procLangInfo
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 63bb4689d4..095a3e4d25 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1015,6 +1015,52 @@ my %tests = (
},
},
+ 'ALTER TABLE ONLY test_table_tpk ADD CONSTRAINT ... PRIMARY KEY (..., ... WITHOUT OVERLAPS)' => {
+ create_sql => 'CREATE TABLE dump_test.test_table_tpk (
+ col1 int4range,
+ col2 tstzrange,
+ CONSTRAINT test_table_tpk_pkey PRIMARY KEY
+ (col1, col2 WITHOUT OVERLAPS));',
+ regexp => qr/^
+ \QALTER TABLE ONLY dump_test.test_table_tpk\E \n^\s+
+ \QADD CONSTRAINT test_table_tpk_pkey PRIMARY KEY (col1, col2 WITHOUT OVERLAPS);\E
+ /xm,
+ like => {
+ %full_runs,
+ %dump_test_schema_runs,
+ section_post_data => 1,
+ exclude_test_table => 1,
+ },
+ unlike => {
+ only_dump_test_table => 1,
+ exclude_dump_test_schema => 1,
+ only_dump_measurement => 1,
+ },
+ },
+
+ 'ALTER TABLE ONLY test_table_tuq ADD CONSTRAINT ... UNIQUE (..., ... WITHOUT OVERLAPS)' => {
+ create_sql => 'CREATE TABLE dump_test.test_table_tuq (
+ col1 int4range,
+ col2 tstzrange,
+ CONSTRAINT test_table_tuq_uq UNIQUE
+ (col1, col2 WITHOUT OVERLAPS));',
+ regexp => qr/^
+ \QALTER TABLE ONLY dump_test.test_table_tuq\E \n^\s+
+ \QADD CONSTRAINT test_table_tuq_uq UNIQUE (col1, col2 WITHOUT OVERLAPS);\E
+ /xm,
+ like => {
+ %full_runs,
+ %dump_test_schema_runs,
+ section_post_data => 1,
+ exclude_test_table => 1,
+ },
+ unlike => {
+ only_dump_test_table => 1,
+ exclude_dump_test_schema => 1,
+ only_dump_measurement => 1,
+ },
+ },
+
'ALTER TABLE (partitioned) ADD CONSTRAINT ... FOREIGN KEY' => {
create_order => 4,
create_sql => 'CREATE TABLE dump_test.test_table_fk (
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 9325a46b8f..2846489eb0 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2384,6 +2384,10 @@ describeOneTableDetails(const char *schemaname,
else
appendPQExpBufferStr(&buf, ", false AS indisreplident");
appendPQExpBufferStr(&buf, ", c2.reltablespace");
+ if (pset.sversion >= 160000)
+ appendPQExpBufferStr(&buf, ", con.contemporal");
+ else
+ appendPQExpBufferStr(&buf, ", false AS contemporal");
appendPQExpBuffer(&buf,
"\nFROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i\n"
" LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x'))\n"
@@ -2405,8 +2409,12 @@ describeOneTableDetails(const char *schemaname,
printfPQExpBuffer(&buf, " \"%s\"",
PQgetvalue(result, i, 0));
- /* If exclusion constraint, print the constraintdef */
- if (strcmp(PQgetvalue(result, i, 7), "x") == 0)
+ /*
+ * If exclusion constraint or temporal PK/UNIQUE constraint,
+ * print the constraintdef
+ */
+ if (strcmp(PQgetvalue(result, i, 7), "x") == 0 ||
+ strcmp(PQgetvalue(result, i, 12), "t") == 0)
{
appendPQExpBuffer(&buf, " %s",
PQgetvalue(result, i, 6));
diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h
index c8532fb97c..7afbc8cfc4 100644
--- a/src/include/catalog/index.h
+++ b/src/include/catalog/index.h
@@ -91,6 +91,7 @@ extern Oid index_create(Relation heapRelation,
#define INDEX_CONSTR_CREATE_INIT_DEFERRED (1 << 2)
#define INDEX_CONSTR_CREATE_UPDATE_INDEX (1 << 3)
#define INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS (1 << 4)
+#define INDEX_CONSTR_CREATE_TEMPORAL (1 << 5)
extern Oid index_concurrently_create_copy(Relation heapRelation,
Oid oldIndexId,
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 16bf5f5576..67048e3006 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -107,6 +107,12 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
/* Has a local definition and cannot be inherited */
bool connoinherit;
+ /*
+ * For primary and foreign keys, signifies the last column is a range
+ * and should use overlaps instead of equals.
+ */
+ bool contemporal;
+
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/*
@@ -146,7 +152,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
/*
* If an exclusion constraint, the OIDs of the exclusion operators for
- * each column of the constraint
+ * each column of the constraint. Also set for temporal primary keys.
*/
Oid conexclop[1] BKI_LOOKUP(pg_operator);
@@ -235,6 +241,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
bool conIsLocal,
int conInhCount,
bool conNoInherit,
+ bool conTemporal,
bool is_internal);
extern void RemoveConstraintById(Oid conId);
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index 478203ed4c..9a21717a3e 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -44,7 +44,8 @@ extern char *ChooseRelationName(const char *name1, const char *name2,
extern bool CheckIndexCompatible(Oid oldId,
const char *accessMethodName,
List *attributeList,
- List *exclusionOpNames);
+ List *exclusionOpNames,
+ bool istemporal);
extern Oid GetDefaultOpClass(Oid type_id, Oid am_id);
extern Oid ResolveOpClass(List *opclass, Oid attrType,
const char *accessMethodName, Oid accessMethodId);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cb714f4a19..2327b55f15 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -155,6 +155,7 @@ typedef struct ExprState
* UniqueProcs
* UniqueStrats
* Unique is it a unique index?
+ * Temporal is it for a temporal constraint?
* OpclassOptions opclass-specific options, or NULL if none
* ReadyForInserts is it valid for inserts?
* CheckedUnchanged IndexUnchanged status determined yet?
@@ -190,6 +191,7 @@ typedef struct IndexInfo
Datum *ii_OpclassOptions; /* array with one entry per column */
bool ii_Unique;
bool ii_NullsNotDistinct;
+ bool ii_Temporal;
bool ii_ReadyForInserts;
bool ii_CheckedUnchanged;
bool ii_IndexUnchanged;
diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h
index 06d991b725..4ed3af1864 100644
--- a/src/include/nodes/makefuncs.h
+++ b/src/include/nodes/makefuncs.h
@@ -98,7 +98,7 @@ extern IndexInfo *makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid,
List *expressions, List *predicates,
bool unique, bool nulls_not_distinct,
bool isready, bool concurrent,
- bool summarizing);
+ bool summarizing, bool temporal);
extern DefElem *makeDefElem(char *name, Node *arg, int location);
extern DefElem *makeDefElemExtended(char *nameSpace, char *name, Node *arg,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 88b03cc472..76ad76ddd7 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2601,6 +2601,9 @@ typedef struct Constraint
Oid old_pktable_oid; /* pg_constraint.confrelid of my former
* self */
+ /* Fields used for temporal PRIMARY KEY and FOREIGN KEY constraints: */
+ Node *without_overlaps; /* String node naming range column */
+
/* Fields used for constraints that allow a NOT VALID specification */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
@@ -3202,6 +3205,7 @@ typedef struct IndexStmt
bool nulls_not_distinct; /* null treatment for UNIQUE constraints */
bool primary; /* is index a primary key? */
bool isconstraint; /* is it for a pkey/unique constraint? */
+ bool istemporal; /* is it for a temporal pkey? */
bool deferrable; /* is the constraint DEFERRABLE? */
bool initdeferred; /* is the constraint INITIALLY DEFERRED? */
bool transformed; /* true when transformIndexStmt is finished */
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
new file mode 100644
index 0000000000..c911c716ca
--- /dev/null
+++ b/src/test/regress/expected/without_overlaps.out
@@ -0,0 +1,296 @@
+-- Tests for WITHOUT OVERLAPS.
+--
+-- We leave behind several tables to test pg_dump etc:
+-- temporal_rng, temporal_rng2,
+-- temporal_fk_rng2rng.
+--
+-- test input parser
+--
+-- PK with no columns just WITHOUT OVERLAPS:
+CREATE TABLE temporal_rng (
+ valid_at tsrange,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS)
+);
+ERROR: syntax error at or near "WITHOUT"
+LINE 3: CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OV...
+ ^
+-- PK with a range column/PERIOD that isn't there:
+CREATE TABLE temporal_rng (
+ id INTEGER,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR: range or PERIOD "valid_at" named in WITHOUT OVERLAPS does not exist
+-- PK with a non-range column:
+CREATE TABLE temporal_rng (
+ id INTEGER,
+ valid_at TEXT,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR: column "valid_at" named in WITHOUT OVERLAPS is not a range type
+-- PK with one column plus a range:
+CREATE TABLE temporal_rng (
+ -- Since we can't depend on having btree_gist here,
+ -- use an int4range instead of an int.
+ -- (The rangetypes regression test uses the same trick.)
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng
+ Table "public.temporal_rng"
+ Column | Type | Collation | Nullable | Default
+----------+-----------+-----------+----------+---------
+ id | int4range | | not null |
+ valid_at | tsrange | | not null |
+Indexes:
+ "temporal_rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+ pg_get_constraintdef
+---------------------------------------------
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+ pg_get_indexdef
+-------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng_pk ON temporal_rng USING gist (id, valid_at)
+(1 row)
+
+-- PK with two columns plus a range:
+CREATE TABLE temporal_rng2 (
+ id1 int4range,
+ id2 int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+ Table "public.temporal_rng2"
+ Column | Type | Collation | Nullable | Default
+----------+-----------+-----------+----------+---------
+ id1 | int4range | | not null |
+ id2 | int4range | | not null |
+ valid_at | tsrange | | not null |
+Indexes:
+ "temporal_rng2_pk" PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+ pg_get_constraintdef
+---------------------------------------------------
+ PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+ pg_get_indexdef
+---------------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng2_pk ON temporal_rng2 USING gist (id1, id2, valid_at)
+(1 row)
+
+DROP TABLE temporal_rng2;
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng2 (
+ id int4range,
+ valid_at textrange2,
+ CONSTRAINT temporal_rng2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng2 DROP CONSTRAINT temporal_rng2_pk;
+DROP TABLE temporal_rng2;
+DROP TYPE textrange2;
+-- UNIQUE with no columns just WITHOUT OVERLAPS:
+CREATE TABLE temporal_rng2 (
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+);
+ERROR: syntax error at or near "WITHOUT"
+LINE 3: CONSTRAINT temporal_rng2_uq UNIQUE (valid_at WITHOUT OVERLA...
+ ^
+-- UNIQUE with a range column/PERIOD that isn't there:
+CREATE TABLE temporal_rng2 (
+ id INTEGER,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR: range or PERIOD "valid_at" named in WITHOUT OVERLAPS does not exist
+-- UNIQUE with a non-range column:
+CREATE TABLE temporal_rng2 (
+ id INTEGER,
+ valid_at TEXT,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR: column "valid_at" named in WITHOUT OVERLAPS is not a range type
+-- UNIQUE with one column plus a range:
+CREATE TABLE temporal_rng2 (
+ -- Since we can't depend on having btree_gist here,
+ -- use an int4range instead of an int.
+ -- (The rangetypes regression test uses the same trick.)
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+ Table "public.temporal_rng2"
+ Column | Type | Collation | Nullable | Default
+----------+-----------+-----------+----------+---------
+ id | int4range | | |
+ valid_at | tsrange | | not null |
+Indexes:
+ "temporal_rng2_uq" UNIQUE (id, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+ pg_get_constraintdef
+----------------------------------------
+ UNIQUE (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+ pg_get_indexdef
+---------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng2_uq ON temporal_rng2 USING gist (id, valid_at)
+(1 row)
+
+-- UNIQUE with two columns plus a range:
+CREATE TABLE temporal_rng3 (
+ id1 int4range,
+ id2 int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng3_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng3
+ Table "public.temporal_rng3"
+ Column | Type | Collation | Nullable | Default
+----------+-----------+-----------+----------+---------
+ id1 | int4range | | |
+ id2 | int4range | | |
+ valid_at | tsrange | | not null |
+Indexes:
+ "temporal_rng3_uq" UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+ pg_get_constraintdef
+----------------------------------------------
+ UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+ pg_get_indexdef
+---------------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng3_uq ON temporal_rng3 USING gist (id1, id2, valid_at)
+(1 row)
+
+DROP TABLE temporal_rng3;
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+DROP TABLE temporal_rng;
+CREATE TABLE temporal_rng (
+ id int4range,
+ valid_at tsrange
+);
+ALTER TABLE temporal_rng
+ ADD CONSTRAINT temporal_rng_pk
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+-- PK with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+ ADD CONSTRAINT temporal3_pk
+ PRIMARY KEY USING INDEX idx_temporal3_uq;
+ERROR: "idx_temporal3_uq" is not a unique index
+LINE 2: ADD CONSTRAINT temporal3_pk
+ ^
+DETAIL: Cannot create a primary key or unique constraint using such an index.
+DROP TABLE temporal3;
+-- UNIQUE with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+ ADD CONSTRAINT temporal3_uq
+ UNIQUE USING INDEX idx_temporal3_uq;
+ERROR: "idx_temporal3_uq" is not a unique index
+LINE 2: ADD CONSTRAINT temporal3_uq
+ ^
+DETAIL: Cannot create a primary key or unique constraint using such an index.
+DROP TABLE temporal3;
+-- Add range column and the PK at the same time
+CREATE TABLE temporal3 (
+ id int4range
+);
+ALTER TABLE temporal3
+ ADD COLUMN valid_at tsrange,
+ ADD CONSTRAINT temporal3_pk
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+-- Add range column and UNIQUE constraint at the same time
+CREATE TABLE temporal3 (
+ id int4range
+);
+ALTER TABLE temporal3
+ ADD COLUMN valid_at tsrange,
+ ADD CONSTRAINT temporal3_uq
+ UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+--
+-- test PK inserts
+--
+-- okay:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng VALUES ('[2,2]', tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng VALUES ('[3,3]', tsrange('2018-01-01', NULL));
+-- should fail:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-01', '2018-01-05'));
+ERROR: conflicting key value violates exclusion constraint "temporal_rng_pk"
+DETAIL: Key (id, valid_at)=([1,2), ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018")) conflicts with existing key (id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sat Feb 03 00:00:00 2018")).
+INSERT INTO temporal_rng VALUES (NULL, tsrange('2018-01-01', '2018-01-05'));
+ERROR: null value in column "id" of relation "temporal_rng" violates not-null constraint
+DETAIL: Failing row contains (null, ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018")).
+INSERT INTO temporal_rng VALUES ('[3,3]', NULL);
+ERROR: null value in column "valid_at" of relation "temporal_rng" violates not-null constraint
+DETAIL: Failing row contains ([3,4), null).
+--
+-- test a range with both a PK and a UNIQUE constraint
+--
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at daterange,
+ id2 int8range,
+ name TEXT,
+ CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal3_uniq UNIQUE (id2, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO temporal3 (id, valid_at, id2, name)
+ VALUES
+ ('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'),
+ ('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', 'bar')
+;
+DROP TABLE temporal3;
+--
+-- test changing the PK's dependencies
+--
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal3 ALTER COLUMN valid_at DROP NOT NULL;
+ERROR: column "valid_at" is in a primary key
+ALTER TABLE temporal3 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at));
+ALTER TABLE temporal3 RENAME COLUMN valid_at TO valid_thru;
+ALTER TABLE temporal3 DROP COLUMN valid_thru;
+DROP TABLE temporal3;
+--
+-- test PARTITION BY for ranges
+--
+CREATE TABLE temporal_partitioned (
+ id int4range,
+ valid_at daterange,
+ CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+ERROR: cannot match partition key to an index using access method "gist"
+-- TODO: attach some partitions, insert into them, update them with and without FOR PORTION OF, delete them the same way.
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index cf46fa3359..b02b8dd4f6 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
# geometry depends on point, lseg, line, box, path, polygon, circle
# horology depends on date, time, timetz, timestamp, timestamptz, interval
# ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc without_overlaps
# ----------
# Load huge amounts of data
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
new file mode 100644
index 0000000000..41a094f580
--- /dev/null
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -0,0 +1,233 @@
+-- Tests for WITHOUT OVERLAPS.
+--
+-- We leave behind several tables to test pg_dump etc:
+-- temporal_rng, temporal_rng2,
+-- temporal_fk_rng2rng.
+
+--
+-- test input parser
+--
+
+-- PK with no columns just WITHOUT OVERLAPS:
+
+CREATE TABLE temporal_rng (
+ valid_at tsrange,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with a range column/PERIOD that isn't there:
+
+CREATE TABLE temporal_rng (
+ id INTEGER,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with a non-range column:
+
+CREATE TABLE temporal_rng (
+ id INTEGER,
+ valid_at TEXT,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with one column plus a range:
+
+CREATE TABLE temporal_rng (
+ -- Since we can't depend on having btree_gist here,
+ -- use an int4range instead of an int.
+ -- (The rangetypes regression test uses the same trick.)
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+
+-- PK with two columns plus a range:
+CREATE TABLE temporal_rng2 (
+ id1 int4range,
+ id2 int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+DROP TABLE temporal_rng2;
+
+
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng2 (
+ id int4range,
+ valid_at textrange2,
+ CONSTRAINT temporal_rng2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng2 DROP CONSTRAINT temporal_rng2_pk;
+DROP TABLE temporal_rng2;
+DROP TYPE textrange2;
+
+-- UNIQUE with no columns just WITHOUT OVERLAPS:
+
+CREATE TABLE temporal_rng2 (
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with a range column/PERIOD that isn't there:
+
+CREATE TABLE temporal_rng2 (
+ id INTEGER,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with a non-range column:
+
+CREATE TABLE temporal_rng2 (
+ id INTEGER,
+ valid_at TEXT,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with one column plus a range:
+
+CREATE TABLE temporal_rng2 (
+ -- Since we can't depend on having btree_gist here,
+ -- use an int4range instead of an int.
+ -- (The rangetypes regression test uses the same trick.)
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+
+-- UNIQUE with two columns plus a range:
+CREATE TABLE temporal_rng3 (
+ id1 int4range,
+ id2 int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng3_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng3
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+DROP TABLE temporal_rng3;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+DROP TABLE temporal_rng;
+CREATE TABLE temporal_rng (
+ id int4range,
+ valid_at tsrange
+);
+ALTER TABLE temporal_rng
+ ADD CONSTRAINT temporal_rng_pk
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+-- PK with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+ ADD CONSTRAINT temporal3_pk
+ PRIMARY KEY USING INDEX idx_temporal3_uq;
+DROP TABLE temporal3;
+
+-- UNIQUE with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+ ADD CONSTRAINT temporal3_uq
+ UNIQUE USING INDEX idx_temporal3_uq;
+DROP TABLE temporal3;
+
+-- Add range column and the PK at the same time
+CREATE TABLE temporal3 (
+ id int4range
+);
+ALTER TABLE temporal3
+ ADD COLUMN valid_at tsrange,
+ ADD CONSTRAINT temporal3_pk
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+
+-- Add range column and UNIQUE constraint at the same time
+CREATE TABLE temporal3 (
+ id int4range
+);
+ALTER TABLE temporal3
+ ADD COLUMN valid_at tsrange,
+ ADD CONSTRAINT temporal3_uq
+ UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+
+--
+-- test PK inserts
+--
+
+-- okay:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng VALUES ('[2,2]', tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng VALUES ('[3,3]', tsrange('2018-01-01', NULL));
+
+-- should fail:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng VALUES (NULL, tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng VALUES ('[3,3]', NULL);
+
+--
+-- test a range with both a PK and a UNIQUE constraint
+--
+
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at daterange,
+ id2 int8range,
+ name TEXT,
+ CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal3_uniq UNIQUE (id2, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO temporal3 (id, valid_at, id2, name)
+ VALUES
+ ('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'),
+ ('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', 'bar')
+;
+DROP TABLE temporal3;
+
+--
+-- test changing the PK's dependencies
+--
+
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+ALTER TABLE temporal3 ALTER COLUMN valid_at DROP NOT NULL;
+ALTER TABLE temporal3 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at));
+ALTER TABLE temporal3 RENAME COLUMN valid_at TO valid_thru;
+ALTER TABLE temporal3 DROP COLUMN valid_thru;
+DROP TABLE temporal3;
+
+--
+-- test PARTITION BY for ranges
+--
+
+CREATE TABLE temporal_partitioned (
+ id int4range,
+ valid_at daterange,
+ CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+-- TODO: attach some partitions, insert into them, update them with and without FOR PORTION OF, delete them the same way.
--
2.32.0 (Apple Git-132)
[application/octet-stream] v12-0004-Add-CASCADE-SET-NULL-SET-DEFAULT-for-temporal-fo.patch (45.6K, ../../CA+renyVyMOb+t6QNoq=PBK1CY_oa9wdJrObjj6S1-RK9CxYwKQ@mail.gmail.com/3-v12-0004-Add-CASCADE-SET-NULL-SET-DEFAULT-for-temporal-fo.patch)
download | inline diff:
From df302feef6f5ffd05e96600f931a77831526b859 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Sat, 3 Jun 2023 21:41:11 -0400
Subject: [PATCH v12 4/5] Add CASCADE/SET NULL/SET DEFAULT for temporal foreign
keys
---
src/backend/commands/tablecmds.c | 30 +-
src/backend/utils/adt/ri_triggers.c | 612 +++++++++++++++++-
src/include/catalog/pg_proc.dat | 20 +
.../regress/expected/without_overlaps.out | 181 +++++-
src/test/regress/sql/without_overlaps.sql | 66 ++
5 files changed, 895 insertions(+), 14 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f819fe0fcb..409e470fbd 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11990,11 +11990,20 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_del");
break;
case FKCONSTR_ACTION_CASCADE:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_cascade_del");
+ break;
case FKCONSTR_ACTION_SETNULL:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_setnull_del");
+ break;
case FKCONSTR_ACTION_SETDEFAULT:
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("action not supported for temporal foreign keys")));
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_setdefault_del");
+ break;
default:
elog(ERROR, "unrecognized FK action type: %d",
(int) fkconstraint->fk_del_action);
@@ -12080,11 +12089,20 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_upd");
break;
case FKCONSTR_ACTION_CASCADE:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_cascade_upd");
+ break;
case FKCONSTR_ACTION_SETNULL:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_setnull_upd");
+ break;
case FKCONSTR_ACTION_SETDEFAULT:
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("action not supported for temporal foreign keys")));
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_setdefault_upd");
+ break;
default:
elog(ERROR, "unrecognized FK action type: %d",
(int) fkconstraint->fk_upd_action);
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 08c6e4c1c4..85441af42b 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -85,9 +85,9 @@
#define TRI_PLAN_CASCADE_ONDELETE 10
#define TRI_PLAN_CASCADE_ONUPDATE 11
#define TRI_PLAN_SETNULL_ONUPDATE 12
-// TODO: Need TRI SETNULL_ONDELETE
-#define TRI_PLAN_SETDEFAULT_ONUPDATE 13
-// TODO: Need TRI SETDEFAULT_ONDELETE
+#define TRI_PLAN_SETNULL_ONDELETE 13
+#define TRI_PLAN_SETDEFAULT_ONUPDATE 14
+#define TRI_PLAN_SETDEFAULT_ONDELETE 15
#define MAX_QUOTED_NAME_LEN (NAMEDATALEN*2+3)
#define MAX_QUOTED_REL_NAME_LEN (MAX_QUOTED_NAME_LEN*2)
@@ -195,6 +195,7 @@ static bool ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
const RI_ConstraintInfo *riinfo);
static Datum ri_restrict(TriggerData *trigdata, bool is_no_action);
static Datum ri_set(TriggerData *trigdata, bool is_set_null, int tgkind);
+static Datum tri_set(TriggerData *trigdata, bool is_set_null, int tgkind);
static void quoteOneName(char *buffer, const char *name);
static void quoteRelationName(char *buffer, Relation rel);
static void ri_GenerateQual(StringInfo buf,
@@ -231,6 +232,7 @@ static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo,
RI_QueryKey *qkey, SPIPlanPtr qplan,
Relation fk_rel, Relation pk_rel,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
+ bool hasForPortionOf, Datum forPortionOf,
bool detectNewRows, int expect_OK);
static void ri_ExtractValues(Relation rel, TupleTableSlot *slot,
const RI_ConstraintInfo *riinfo, bool rel_is_pk,
@@ -239,6 +241,11 @@ static void ri_ReportViolation(const RI_ConstraintInfo *riinfo,
Relation pk_rel, Relation fk_rel,
TupleTableSlot *violatorslot, TupleDesc tupdesc,
int queryno, bool partgone) pg_attribute_noreturn();
+static bool fpo_targets_pk_range(const ForPortionOfState *tg_temporal,
+ const RI_ConstraintInfo *riinfo);
+static Datum restrict_cascading_range(const ForPortionOfState *tg_temporal,
+ const RI_ConstraintInfo *riinfo,
+ TupleTableSlot *oldslot);
/*
@@ -447,6 +454,7 @@ RI_FKey_check(TriggerData *trigdata)
ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
NULL, newslot,
+ false, 0,
pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE,
SPI_OK_SELECT);
@@ -576,6 +584,7 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
result = ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
oldslot, NULL,
+ false, 0,
true, /* treat like update */
SPI_OK_SELECT);
@@ -774,6 +783,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
oldslot, NULL,
+ false, 0,
true, /* must detect new rows */
SPI_OK_SELECT);
@@ -880,6 +890,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
oldslot, NULL,
+ false, 0,
true, /* must detect new rows */
SPI_OK_DELETE);
@@ -1001,6 +1012,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
oldslot, newslot,
+ false, 0,
true, /* must detect new rows */
SPI_OK_UPDATE);
@@ -1233,6 +1245,7 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
oldslot, NULL,
+ false, 0,
true, /* must detect new rows */
SPI_OK_UPDATE);
@@ -1346,6 +1359,127 @@ TRI_FKey_restrict_del(PG_FUNCTION_ARGS)
return ri_restrict((TriggerData *) fcinfo->context, false);
}
+/*
+ * TRI_FKey_cascade_del -
+ *
+ * Cascaded delete foreign key references at delete event on temporal PK table.
+ */
+Datum
+TRI_FKey_cascade_del(PG_FUNCTION_ARGS)
+{
+ TriggerData *trigdata = (TriggerData *) fcinfo->context;
+ const RI_ConstraintInfo *riinfo;
+ Relation fk_rel;
+ Relation pk_rel;
+ TupleTableSlot *oldslot;
+ RI_QueryKey qkey;
+ SPIPlanPtr qplan;
+ Datum targetRange;
+
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_cascade_del", RI_TRIGTYPE_DELETE);
+
+ riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger,
+ trigdata->tg_relation, true);
+
+ /*
+ * Get the relation descriptors of the FK and PK tables and the old tuple.
+ *
+ * fk_rel is opened in RowExclusiveLock mode since that's what our
+ * eventual DELETE will get on it.
+ */
+ fk_rel = table_open(riinfo->fk_relid, RowExclusiveLock);
+ pk_rel = trigdata->tg_relation;
+ oldslot = trigdata->tg_trigslot;
+
+ /*
+ * Don't delete than more than the PK's duration,
+ * trimmed by an original FOR PORTION OF if necessary.
+ */
+ targetRange = restrict_cascading_range(trigdata->tg_temporal, riinfo, oldslot);
+
+ if (SPI_connect() != SPI_OK_CONNECT)
+ elog(ERROR, "SPI_connect failed");
+
+ /* Fetch or prepare a saved plan for the cascaded delete */
+ ri_BuildQueryKey(&qkey, riinfo, TRI_PLAN_CASCADE_ONDELETE);
+
+ if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL)
+ {
+ StringInfoData querybuf;
+ char fkrelname[MAX_QUOTED_REL_NAME_LEN];
+ char attname[MAX_QUOTED_NAME_LEN];
+ char paramname[16];
+ const char *querysep;
+ Oid queryoids[RI_MAX_NUMKEYS + 1];
+ const char *fk_only;
+
+ /* ----------
+ * The query string built is
+ * DELETE FROM [ONLY] <fktable>
+ * FOR PORTION OF $fkatt FROM lower(${n+1}) TO upper(${n+1})
+ * WHERE $1 = fkatt1 [AND ...]
+ * The type id's for the $ parameters are those of the
+ * corresponding PK attributes.
+ * ----------
+ */
+ initStringInfo(&querybuf);
+ fk_only = fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
+ "" : "ONLY ";
+ quoteRelationName(fkrelname, fk_rel);
+ quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]));
+
+ appendStringInfo(&querybuf, "DELETE FROM %s%s FOR PORTION OF %s FROM lower($%d) TO upper($%d)",
+ fk_only, fkrelname, attname, riinfo->nkeys + 1, riinfo->nkeys + 1);
+ querysep = "WHERE";
+ for (int i = 0; i < riinfo->nkeys; i++)
+ {
+ Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+ Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+ Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+ Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
+
+ quoteOneName(attname,
+ RIAttName(fk_rel, riinfo->fk_attnums[i]));
+ sprintf(paramname, "$%d", i + 1);
+ ri_GenerateQual(&querybuf, querysep,
+ paramname, pk_type,
+ riinfo->pf_eq_oprs[i],
+ attname, fk_type);
+ if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll))
+ ri_GenerateQualCollation(&querybuf, pk_coll);
+ querysep = "AND";
+ queryoids[i] = pk_type;
+ }
+
+ /* Set a param for FOR PORTION OF TO/FROM */
+ queryoids[riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]);
+
+ /* Prepare and save the plan */
+ qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys + 1, queryoids,
+ &qkey, fk_rel, pk_rel);
+ }
+
+ /*
+ * We have a plan now. Build up the arguments from the key values in the
+ * deleted PK tuple and delete the referencing rows
+ */
+ ri_PerformCheck(riinfo, &qkey, qplan,
+ fk_rel, pk_rel,
+ oldslot, NULL,
+ true, targetRange,
+ true, /* must detect new rows */
+ SPI_OK_DELETE);
+
+ if (SPI_finish() != SPI_OK_FINISH)
+ elog(ERROR, "SPI_finish failed");
+
+ table_close(fk_rel, RowExclusiveLock);
+
+ return PointerGetDatum(NULL);
+}
+
+
/*
* TRI_FKey_noaction_upd -
*
@@ -1383,6 +1517,415 @@ TRI_FKey_restrict_upd(PG_FUNCTION_ARGS)
return ri_restrict((TriggerData *) fcinfo->context, false);
}
+/*
+ * TRI_FKey_cascade_upd -
+ *
+ * Cascaded update foreign key references at update event on temporal PK table.
+ */
+Datum
+TRI_FKey_cascade_upd(PG_FUNCTION_ARGS)
+{
+ TriggerData *trigdata = (TriggerData *) fcinfo->context;
+ const RI_ConstraintInfo *riinfo;
+ Relation fk_rel;
+ Relation pk_rel;
+ TupleTableSlot *oldslot;
+ TupleTableSlot *newslot;
+ RI_QueryKey qkey;
+ SPIPlanPtr qplan;
+ Datum targetRange;
+
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_cascade_upd", RI_TRIGTYPE_UPDATE);
+
+ riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger,
+ trigdata->tg_relation, true);
+
+ /*
+ * Get the relation descriptors of the FK and PK tables and the new and
+ * old tuple.
+ *
+ * fk_rel is opened in RowExclusiveLock mode since that's what our
+ * eventual UPDATE will get on it.
+ */
+ fk_rel = table_open(riinfo->fk_relid, RowExclusiveLock);
+ pk_rel = trigdata->tg_relation;
+ newslot = trigdata->tg_newslot;
+ oldslot = trigdata->tg_trigslot;
+
+ /*
+ * Don't delete than more than the PK's duration,
+ * trimmed by an original FOR PORTION OF if necessary.
+ */
+ targetRange = restrict_cascading_range(trigdata->tg_temporal, riinfo, oldslot);
+
+ if (SPI_connect() != SPI_OK_CONNECT)
+ elog(ERROR, "SPI_connect failed");
+
+ /* Fetch or prepare a saved plan for the cascaded update */
+ ri_BuildQueryKey(&qkey, riinfo, TRI_PLAN_CASCADE_ONUPDATE);
+
+ if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL)
+ {
+ StringInfoData querybuf;
+ StringInfoData qualbuf;
+ char fkrelname[MAX_QUOTED_REL_NAME_LEN];
+ char attname[MAX_QUOTED_NAME_LEN];
+ char paramname[16];
+ const char *querysep;
+ const char *qualsep;
+ Oid queryoids[2 * RI_MAX_NUMKEYS + 1];
+ const char *fk_only;
+
+ /* ----------
+ * The query string built is
+ * UPDATE [ONLY] <fktable>
+ * FOR PORTION OF $fkatt FROM lower(${2n+1}) TO upper(${2n+1})
+ * SET fkatt1 = $1, [, ...]
+ * WHERE $n = fkatt1 [AND ...]
+ * The type id's for the $ parameters are those of the
+ * corresponding PK attributes. Note that we are assuming
+ * there is an assignment cast from the PK to the FK type;
+ * else the parser will fail.
+ * ----------
+ */
+ initStringInfo(&querybuf);
+ initStringInfo(&qualbuf);
+ fk_only = fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
+ "" : "ONLY ";
+ quoteRelationName(fkrelname, fk_rel);
+ quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]));
+
+ appendStringInfo(&querybuf, "UPDATE %s%s FOR PORTION OF %s FROM lower($%d) TO upper($%d) SET",
+ fk_only, fkrelname, attname, 2 * riinfo->nkeys + 1, 2 * riinfo->nkeys + 1);
+
+ querysep = "";
+ qualsep = "WHERE";
+ for (int i = 0, j = riinfo->nkeys; i < riinfo->nkeys; i++, j++)
+ {
+ Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+ Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+ Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+ Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
+
+ quoteOneName(attname,
+ RIAttName(fk_rel, riinfo->fk_attnums[i]));
+ /*
+ * Don't set the temporal column(s).
+ * FOR PORTION OF will take care of that.
+ */
+ if (i < riinfo->nkeys - 1)
+ appendStringInfo(&querybuf,
+ "%s %s = $%d",
+ querysep, attname, i + 1);
+
+ sprintf(paramname, "$%d", j + 1);
+ ri_GenerateQual(&qualbuf, qualsep,
+ paramname, pk_type,
+ riinfo->pf_eq_oprs[i],
+ attname, fk_type);
+ if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll))
+ ri_GenerateQualCollation(&querybuf, pk_coll);
+ querysep = ",";
+ qualsep = "AND";
+ queryoids[i] = pk_type;
+ queryoids[j] = pk_type;
+ }
+ appendBinaryStringInfo(&querybuf, qualbuf.data, qualbuf.len);
+
+ /* Set a param for FOR PORTION OF TO/FROM */
+ queryoids[2 * riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]);
+
+ /* Prepare and save the plan */
+ qplan = ri_PlanCheck(querybuf.data, 2 * riinfo->nkeys + 1, queryoids,
+ &qkey, fk_rel, pk_rel);
+ }
+
+ /*
+ * We have a plan now. Run it to update the existing references.
+ */
+ ri_PerformCheck(riinfo, &qkey, qplan,
+ fk_rel, pk_rel,
+ oldslot, newslot,
+ true, targetRange,
+ true, /* must detect new rows */
+ SPI_OK_UPDATE);
+
+ if (SPI_finish() != SPI_OK_FINISH)
+ elog(ERROR, "SPI_finish failed");
+
+ table_close(fk_rel, RowExclusiveLock);
+
+ return PointerGetDatum(NULL);
+}
+
+/*
+ * TRI_FKey_setnull_del -
+ *
+ * Set foreign key references to NULL values at delete event on PK table.
+ */
+Datum
+TRI_FKey_setnull_del(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_setnull_del", RI_TRIGTYPE_DELETE);
+
+ /* Share code with UPDATE case */
+ return tri_set((TriggerData *) fcinfo->context, true, RI_TRIGTYPE_DELETE);
+}
+
+/*
+ * TRI_FKey_setnull_upd -
+ *
+ * Set foreign key references to NULL at update event on PK table.
+ */
+Datum
+TRI_FKey_setnull_upd(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_setnull_upd", RI_TRIGTYPE_UPDATE);
+
+ /* Share code with DELETE case */
+ return tri_set((TriggerData *) fcinfo->context, true, RI_TRIGTYPE_UPDATE);
+}
+
+/*
+ * TRI_FKey_setdefault_del -
+ *
+ * Set foreign key references to defaults at delete event on PK table.
+ */
+Datum
+TRI_FKey_setdefault_del(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_setdefault_del", RI_TRIGTYPE_DELETE);
+
+ /* Share code with UPDATE case */
+ return tri_set((TriggerData *) fcinfo->context, false, RI_TRIGTYPE_DELETE);
+}
+
+/*
+ * TRI_FKey_setdefault_upd -
+ *
+ * Set foreign key references to defaults at update event on PK table.
+ */
+Datum
+TRI_FKey_setdefault_upd(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_setdefault_upd", RI_TRIGTYPE_UPDATE);
+
+ /* Share code with DELETE case */
+ return tri_set((TriggerData *) fcinfo->context, false, RI_TRIGTYPE_UPDATE);
+}
+
+/*
+ * tri_set -
+ *
+ * Common code for temporal ON DELETE SET NULL, ON DELETE SET DEFAULT, ON
+ * UPDATE SET NULL, and ON UPDATE SET DEFAULT.
+ */
+static Datum
+tri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
+{
+ const RI_ConstraintInfo *riinfo;
+ Relation fk_rel;
+ Relation pk_rel;
+ TupleTableSlot *oldslot;
+ RI_QueryKey qkey;
+ SPIPlanPtr qplan;
+ Datum targetRange;
+ int32 queryno;
+
+ riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger,
+ trigdata->tg_relation, true);
+
+ /*
+ * Get the relation descriptors of the FK and PK tables and the old tuple.
+ *
+ * fk_rel is opened in RowExclusiveLock mode since that's what our
+ * eventual UPDATE will get on it.
+ */
+ fk_rel = table_open(riinfo->fk_relid, RowExclusiveLock);
+ pk_rel = trigdata->tg_relation;
+ oldslot = trigdata->tg_trigslot;
+
+ /*
+ * Don't SET NULL/DEFAULT than more than the PK's duration,
+ * trimmed by an original FOR PORTION OF if necessary.
+ */
+ targetRange = restrict_cascading_range(trigdata->tg_temporal, riinfo, oldslot);
+
+ if (SPI_connect() != SPI_OK_CONNECT)
+ elog(ERROR, "SPI_connect failed");
+
+ /*
+ * Fetch or prepare a saved plan for the trigger.
+ */
+ switch (tgkind)
+ {
+ case RI_TRIGTYPE_UPDATE:
+ queryno = is_set_null
+ ? TRI_PLAN_SETNULL_ONUPDATE
+ : TRI_PLAN_SETDEFAULT_ONUPDATE;
+ break;
+ case RI_TRIGTYPE_DELETE:
+ queryno = is_set_null
+ ? TRI_PLAN_SETNULL_ONDELETE
+ : TRI_PLAN_SETDEFAULT_ONDELETE;
+ break;
+ default:
+ elog(ERROR, "invalid tgkind passed to ri_set");
+ }
+
+ ri_BuildQueryKey(&qkey, riinfo, queryno);
+
+ if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL)
+ {
+ StringInfoData querybuf;
+ StringInfoData qualbuf;
+ char fkrelname[MAX_QUOTED_REL_NAME_LEN];
+ char attname[MAX_QUOTED_NAME_LEN];
+ char paramname[16];
+ const char *querysep;
+ const char *qualsep;
+ Oid queryoids[RI_MAX_NUMKEYS + 1]; /* +1 for FOR PORTION OF */
+ const char *fk_only;
+ int num_cols_to_set;
+ const int16 *set_cols;
+
+ switch (tgkind)
+ {
+ case RI_TRIGTYPE_UPDATE:
+ /* -1 so we let FOR PORTION OF set the range. */
+ num_cols_to_set = riinfo->nkeys - 1;
+ set_cols = riinfo->fk_attnums;
+ break;
+ case RI_TRIGTYPE_DELETE:
+ /*
+ * If confdelsetcols are present, then we only update the
+ * columns specified in that array, otherwise we update all
+ * the referencing columns.
+ */
+ if (riinfo->ndelsetcols != 0)
+ {
+ num_cols_to_set = riinfo->ndelsetcols;
+ set_cols = riinfo->confdelsetcols;
+ }
+ else
+ {
+ /* -1 so we let FOR PORTION OF set the range. */
+ num_cols_to_set = riinfo->nkeys - 1;
+ set_cols = riinfo->fk_attnums;
+ }
+ break;
+ default:
+ elog(ERROR, "invalid tgkind passed to ri_set");
+ }
+
+ /* ----------
+ * The query string built is
+ * UPDATE [ONLY] <fktable>
+ * FOR PORTION OF $fkatt FROM lower(${n+1}) TO upper(${n+1})
+ * SET fkatt1 = {NULL|DEFAULT} [, ...]
+ * WHERE $1 = fkatt1 [AND ...]
+ * The type id's for the $ parameters are those of the
+ * corresponding PK attributes.
+ * ----------
+ */
+ initStringInfo(&querybuf);
+ initStringInfo(&qualbuf);
+ fk_only = fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
+ "" : "ONLY ";
+ quoteRelationName(fkrelname, fk_rel);
+ quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]));
+
+ appendStringInfo(&querybuf, "UPDATE %s%s FOR PORTION OF %s FROM lower($%d) TO upper($%d) SET",
+ fk_only, fkrelname, attname, riinfo->nkeys + 1, riinfo->nkeys + 1);
+
+ /*
+ * Add assignment clauses
+ */
+ querysep = "";
+ for (int i = 0; i < num_cols_to_set; i++)
+ {
+ quoteOneName(attname, RIAttName(fk_rel, set_cols[i]));
+ appendStringInfo(&querybuf,
+ "%s %s = %s",
+ querysep, attname,
+ is_set_null ? "NULL" : "DEFAULT");
+ querysep = ",";
+ }
+
+ /*
+ * Add WHERE clause
+ */
+ qualsep = "WHERE";
+ for (int i = 0; i < riinfo->nkeys; i++)
+ {
+ Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+ Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+ Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+ Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
+
+ quoteOneName(attname,
+ RIAttName(fk_rel, riinfo->fk_attnums[i]));
+
+ sprintf(paramname, "$%d", i + 1);
+ ri_GenerateQual(&querybuf, qualsep,
+ paramname, pk_type,
+ riinfo->pf_eq_oprs[i],
+ attname, fk_type);
+ if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll))
+ ri_GenerateQualCollation(&querybuf, pk_coll);
+ qualsep = "AND";
+ queryoids[i] = pk_type;
+ }
+
+ /* Set a param for FOR PORTION OF TO/FROM */
+ queryoids[riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]);
+
+ /* Prepare and save the plan */
+ qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys + 1, queryoids,
+ &qkey, fk_rel, pk_rel);
+ }
+
+ /*
+ * We have a plan now. Run it to update the existing references.
+ */
+ ri_PerformCheck(riinfo, &qkey, qplan,
+ fk_rel, pk_rel,
+ oldslot, NULL,
+ true, targetRange,
+ true, /* must detect new rows */
+ SPI_OK_UPDATE);
+
+ if (SPI_finish() != SPI_OK_FINISH)
+ elog(ERROR, "SPI_finish failed");
+
+ table_close(fk_rel, RowExclusiveLock);
+
+ if (is_set_null)
+ return PointerGetDatum(NULL);
+ else
+ {
+ /*
+ * If we just deleted or updated the PK row whose key was equal to the
+ * FK columns' default values, and a referencing row exists in the FK
+ * table, we would have updated that row to the same values it already
+ * had --- and RI_FKey_fk_upd_check_required would hence believe no
+ * check is necessary. So we need to do another lookup now and in
+ * case a reference still exists, abort the operation. That is
+ * already implemented in the NO ACTION trigger, so just run it. (This
+ * recheck is only needed in the SET DEFAULT case, since CASCADE would
+ * remove such rows in case of a DELETE operation or would change the
+ * FK key values in case of an UPDATE, while SET NULL is certain to
+ * result in rows that satisfy the FK constraint.)
+ */
+ return ri_restrict(trigdata, true);
+ }
+}
+
/*
* RI_FKey_pk_upd_check_required -
*
@@ -2495,6 +3038,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
RI_QueryKey *qkey, SPIPlanPtr qplan,
Relation fk_rel, Relation pk_rel,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
+ bool hasForPortionOf, Datum forPortionOf,
bool detectNewRows, int expect_OK)
{
Relation query_rel,
@@ -2506,8 +3050,9 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
int spi_result;
Oid save_userid;
int save_sec_context;
- Datum vals[RI_MAX_NUMKEYS * 2];
- char nulls[RI_MAX_NUMKEYS * 2];
+ Datum vals[RI_MAX_NUMKEYS * 2 + 1];
+ char nulls[RI_MAX_NUMKEYS * 2 + 1];
+ int last_param;
/*
* Use the query type code to determine whether the query is run against
@@ -2537,19 +3082,29 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
}
/* Extract the parameters to be passed into the query */
+ last_param = riinfo->nkeys;
if (newslot)
{
ri_ExtractValues(source_rel, newslot, riinfo, source_is_pk,
vals, nulls);
if (oldslot)
+ {
ri_ExtractValues(source_rel, oldslot, riinfo, source_is_pk,
vals + riinfo->nkeys, nulls + riinfo->nkeys);
+ last_param = 2 * riinfo->nkeys;
+ }
}
else
{
ri_ExtractValues(source_rel, oldslot, riinfo, source_is_pk,
vals, nulls);
}
+ /* Add one more query param if we are using FOR PORTION OF */
+ if (hasForPortionOf)
+ {
+ vals[last_param] = forPortionOf;
+ nulls[last_param] = ' ';
+ }
/*
* In READ COMMITTED mode, we just need to use an up-to-date regular
@@ -3206,3 +3761,50 @@ RI_FKey_trigger_type(Oid tgfoid)
return RI_TRIGGER_NONE;
}
+
+/*
+ * fpo_targets_pk_range
+ *
+ * Returns true iff the primary key referenced by riinfo includes the range
+ * column targeted by the FOR PORTION OF clause (according to tg_temporal).
+ */
+static bool
+fpo_targets_pk_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo)
+{
+ if (tg_temporal == NULL)
+ return false;
+
+ return riinfo->pk_attnums[riinfo->nkeys - 1] == tg_temporal->fp_rangeAttno;
+}
+
+/*
+ * restrict_cascading_range -
+ *
+ * Returns a Datum of RangeTypeP holding the appropriate timespan
+ * to target child records when we CASCADE/SET NULL/SET DEFAULT.
+ *
+ * In a normal UPDATE/DELETE this should be the parent's own valid time,
+ * but if there was a FOR PORTION OF clause, then we should use that to
+ * trim down the parent's span further.
+ */
+static Datum
+restrict_cascading_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo, TupleTableSlot *oldslot)
+{
+ Datum pkRecordRange;
+ bool isnull;
+
+ pkRecordRange = slot_getattr(oldslot, riinfo->pk_attnums[riinfo->nkeys - 1], &isnull);
+ if (isnull)
+ elog(ERROR, "application time should not be null");
+
+ if (fpo_targets_pk_range(tg_temporal, riinfo))
+ {
+ RangeType *r1 = DatumGetRangeTypeP(pkRecordRange);
+ RangeType *r2 = DatumGetRangeTypeP(tg_temporal->fp_targetRange);
+ Oid rngtypid = RangeTypeGetOid(r1);
+ TypeCacheEntry *typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO);
+ return RangeTypePGetDatum(range_intersect_internal(typcache, r1, r2));
+ }
+ else
+ return pkRecordRange;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index eac76d2af8..2dcadb2236 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3937,6 +3937,12 @@
{ oid => '6123', descr => 'temporal referential integrity FOREIGN KEY ... REFERENCES',
proname => 'TRI_FKey_check_upd', provolatile => 'v', prorettype => 'trigger',
proargtypes => '', prosrc => 'TRI_FKey_check_upd' },
+{ oid => '6124', descr => 'temporal referential integrity ON DELETE CASCADE',
+ proname => 'TRI_FKey_cascade_del', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'TRI_FKey_cascade_del' },
+{ oid => '6125', descr => 'temporal referential integrity ON UPDATE CASCADE',
+ proname => 'TRI_FKey_cascade_upd', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'TRI_FKey_cascade_upd' },
{ oid => '6126', descr => 'temporal referential integrity ON DELETE RESTRICT',
proname => 'TRI_FKey_restrict_del', provolatile => 'v',
prorettype => 'trigger', proargtypes => '',
@@ -3945,6 +3951,20 @@
proname => 'TRI_FKey_restrict_upd', provolatile => 'v',
prorettype => 'trigger', proargtypes => '',
prosrc => 'TRI_FKey_restrict_upd' },
+{ oid => '6128', descr => 'temporal referential integrity ON DELETE SET NULL',
+ proname => 'TRI_FKey_setnull_del', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'TRI_FKey_setnull_del' },
+{ oid => '6129', descr => 'temporal referential integrity ON UPDATE SET NULL',
+ proname => 'TRI_FKey_setnull_upd', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'TRI_FKey_setnull_upd' },
+{ oid => '6130', descr => 'temporal referential integrity ON DELETE SET DEFAULT',
+ proname => 'TRI_FKey_setdefault_del', provolatile => 'v',
+ prorettype => 'trigger', proargtypes => '',
+ prosrc => 'TRI_FKey_setdefault_del' },
+{ oid => '6131', descr => 'temporal referential integrity ON UPDATE SET DEFAULT',
+ proname => 'TRI_FKey_setdefault_upd', provolatile => 'v',
+ prorettype => 'trigger', proargtypes => '',
+ prosrc => 'TRI_FKey_setdefault_upd' },
{ oid => '6132', descr => 'temporal referential integrity ON DELETE NO ACTION',
proname => 'TRI_FKey_noaction_del', provolatile => 'v',
prorettype => 'trigger', proargtypes => '',
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 143f19981f..8926888a37 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -593,7 +593,62 @@ ALTER TABLE temporal_fk_rng2rng
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng
ON DELETE CASCADE ON UPDATE CASCADE;
-ERROR: action not supported for temporal foreign keys
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[4,4]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [4,5) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8)
+ [4,5) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)
+ [4,5) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)
+(3 rows)
+
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[4,4]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [4,5) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8)
+ [4,5) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [7,8)
+ [4,5) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [7,8)
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]');
+UPDATE temporal_rng SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[10,10]';
+ id | valid_at | parent_id
+---------+---------------------------------------------------------+-----------
+ [10,11) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [16,17)
+ [10,11) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [15,16)
+(2 rows)
+
+-- test FK parent deletes CASCADE
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[5,5]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [8,9)
+ [5,6) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)
+(2 rows)
+
+DELETE FROM temporal_rng WHERE id = '[8,8]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[5,5]';
+ id | valid_at | parent_id
+----+----------+-----------
+(0 rows)
+
+INSERT INTO temporal_rng VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]');
+DELETE FROM temporal_rng WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[11,11]';
+ id | valid_at | parent_id
+---------+---------------------------------------------------------+-----------
+ [11,12) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [17,18)
+(1 row)
+
-- test FK parent updates SET NULL
INSERT INTO temporal_rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'));
INSERT INTO temporal_fk_rng2rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]');
@@ -603,7 +658,67 @@ ALTER TABLE temporal_fk_rng2rng
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng
ON DELETE SET NULL ON UPDATE SET NULL;
-ERROR: action not supported for temporal foreign keys
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[6,6]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [6,7) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |
+ [6,7) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [9,10)
+ [6,7) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [9,10)
+(3 rows)
+
+UPDATE temporal_rng SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[6,6]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [6,7) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |
+ [6,7) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") |
+ [6,7) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") |
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]');
+UPDATE temporal_rng SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[12,12]';
+ id | valid_at | parent_id
+---------+---------------------------------------------------------+-----------
+ [12,13) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") |
+ [12,13) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [18,19)
+(2 rows)
+
+-- test FK parent deletes SET NULL
+INSERT INTO temporal_rng VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[7,7]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [7,8) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |
+ [7,8) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [11,12)
+ [7,8) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [11,12)
+(3 rows)
+
+DELETE FROM temporal_rng WHERE id = '[11,11]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[7,7]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [7,8) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |
+ [7,8) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") |
+ [7,8) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") |
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]');
+DELETE FROM temporal_rng WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[13,13]';
+ id | valid_at | parent_id
+---------+---------------------------------------------------------+-----------
+ [13,14) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") |
+ [13,14) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [20,21)
+(2 rows)
+
-- test FK parent updates SET DEFAULT
INSERT INTO temporal_rng VALUES ('[-1,-1]', tsrange(null, null));
INSERT INTO temporal_rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'));
@@ -615,4 +730,64 @@ ALTER TABLE temporal_fk_rng2rng
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng
ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
-ERROR: action not supported for temporal foreign keys
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[8,8]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [8,9) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [8,9) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [12,13)
+ [8,9) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [12,13)
+(3 rows)
+
+UPDATE temporal_rng SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[8,8]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [8,9) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [8,9) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0)
+ [8,9) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0)
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]');
+UPDATE temporal_rng SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[14,14]';
+ id | valid_at | parent_id
+---------+---------------------------------------------------------+-----------
+ [14,15) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [14,15) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [22,23)
+(2 rows)
+
+-- test FK parent deletes SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[9,9]';
+ id | valid_at | parent_id
+--------+---------------------------------------------------------+-----------
+ [9,10) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [9,10) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [14,15)
+ [9,10) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [14,15)
+(3 rows)
+
+DELETE FROM temporal_rng WHERE id = '[14,14]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[9,9]';
+ id | valid_at | parent_id
+--------+---------------------------------------------------------+-----------
+ [9,10) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [9,10) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0)
+ [9,10) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0)
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]');
+DELETE FROM temporal_rng WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[15,15]';
+ id | valid_at | parent_id
+---------+---------------------------------------------------------+-----------
+ [15,16) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [15,16) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [24,25)
+(2 rows)
+
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 0dd9370a50..a1d69a9d99 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -510,6 +510,28 @@ ALTER TABLE temporal_fk_rng2rng
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng
ON DELETE CASCADE ON UPDATE CASCADE;
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[4,4]';
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[4,4]';
+INSERT INTO temporal_rng VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]');
+UPDATE temporal_rng SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[10,10]';
+
+-- test FK parent deletes CASCADE
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[8,8]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]');
+DELETE FROM temporal_rng WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[11,11]';
-- test FK parent updates SET NULL
@@ -521,6 +543,28 @@ ALTER TABLE temporal_fk_rng2rng
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng
ON DELETE SET NULL ON UPDATE SET NULL;
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[6,6]';
+UPDATE temporal_rng SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[6,6]';
+INSERT INTO temporal_rng VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]');
+UPDATE temporal_rng SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[12,12]';
+
+-- test FK parent deletes SET NULL
+INSERT INTO temporal_rng VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[7,7]';
+DELETE FROM temporal_rng WHERE id = '[11,11]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[7,7]';
+INSERT INTO temporal_rng VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]');
+DELETE FROM temporal_rng WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[13,13]';
-- test FK parent updates SET DEFAULT
INSERT INTO temporal_rng VALUES ('[-1,-1]', tsrange(null, null));
@@ -533,3 +577,25 @@ ALTER TABLE temporal_fk_rng2rng
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng
ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[8,8]';
+UPDATE temporal_rng SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[8,8]';
+INSERT INTO temporal_rng VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]');
+UPDATE temporal_rng SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[14,14]';
+
+-- test FK parent deletes SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[9,9]';
+DELETE FROM temporal_rng WHERE id = '[14,14]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[9,9]';
+INSERT INTO temporal_rng VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]');
+DELETE FROM temporal_rng WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[15,15]';
--
2.32.0 (Apple Git-132)
[application/octet-stream] v12-0002-Add-temporal-FOREIGN-KEYs.patch (95.7K, ../../CA+renyVyMOb+t6QNoq=PBK1CY_oa9wdJrObjj6S1-RK9CxYwKQ@mail.gmail.com/4-v12-0002-Add-temporal-FOREIGN-KEYs.patch)
download | inline diff:
From 810d672ef3003698dc1f50716b032ff34e66e0bf Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Sun, 27 Jun 2021 12:58:51 -0700
Subject: [PATCH v12 2/5] Add temporal FOREIGN KEYs
- Added bison support for temporal FOREIGN KEYs. They should have a
range column for the temporal component.
- Added temporal trigger functions akin to existing RI trigger
functions.
- Don't yet support ON {UPDATE,DELETE} {CASCADE,SET NULL,SET DEFAULT}.
- Added pg_dump support.
- Show the correct syntax in psql \d output for foreign keys.
- Added tests and documentation.
---
doc/src/sgml/ref/create_table.sgml | 42 +-
src/backend/commands/tablecmds.c | 750 +++++++++++-------
src/backend/parser/gram.y | 37 +-
src/backend/utils/adt/ri_triggers.c | 218 ++++-
src/backend/utils/adt/ruleutils.c | 19 +-
src/include/catalog/pg_constraint.h | 8 +-
src/include/catalog/pg_proc.dat | 24 +
src/include/nodes/parsenodes.h | 2 +
src/include/parser/kwlist.h | 1 +
.../regress/expected/without_overlaps.out | 292 +++++++
src/test/regress/sql/without_overlaps.sql | 288 +++++++
11 files changed, 1359 insertions(+), 322 deletions(-)
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 58c77d09b4..aa0d4cda19 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -80,7 +80,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">temporal_interval</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">temporal_interval</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
- FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
+ FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">temporal_interval</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">temporal_interval</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
@@ -1148,8 +1148,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<varlistentry id="sql-createtable-parms-references">
<term><literal>REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH <replaceable class="parameter">matchtype</replaceable> ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ]</literal> (column constraint)</term>
- <term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] )
- REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
+ <term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">temporal_interval</replaceable> ] )
+ REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">temporal_interval</replaceable> ] ) ]
[ MATCH <replaceable class="parameter">matchtype</replaceable> ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ]
[ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ]</literal>
@@ -1160,11 +1160,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
These clauses specify a foreign key constraint, which requires
that a group of one or more columns of the new table must only
contain values that match values in the referenced
- column(s) of some row of the referenced table. If the <replaceable
+ column(s) of some row of the referenced table.
+ If the <replaceable
class="parameter">refcolumn</replaceable> list is omitted, the
primary key of the <replaceable class="parameter">reftable</replaceable>
is used. The referenced columns must be the columns of a non-deferrable
- unique or primary key constraint in the referenced table. The user
+ unique or primary key constraint in the referenced table.
+ </para>
+
+ <para>
+ If the last column is marked with <literal>PERIOD</literal>,
+ it must be a period or range column, and the referenced table
+ must have a temporal primary key.
+ The non-<literal>PERIOD</literal> columns are treated normally
+ (and there must be at least one of them),
+ but the <literal>PERIOD</literal> column is not compared for equality.
+ Instead the constraint is considered satisfied
+ if the referenced table has matching records whose combined ranges completely cover
+ the referencing record.
+ In other words, the reference must have a referent for its entire duration.
+ </para>
+
+ <para>
+ The user
must have <literal>REFERENCES</literal> permission on the referenced table
(either the whole table, or the specific referenced columns). The
addition of a foreign key constraint requires a
@@ -1236,7 +1254,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<para>
Delete any rows referencing the deleted row, or update the
values of the referencing column(s) to the new values of the
- referenced columns, respectively.
+ referenced columns, respectively. In a temporal foreign key,
+ the delete will use <literal>FOR PORTION OF</literal> semantics
+ to constrain the effect to the bounds of the referenced row
+ being deleted.
</para>
</listitem>
</varlistentry>
@@ -1249,6 +1270,12 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
referencing columns, to null. A subset of columns can only be
specified for <literal>ON DELETE</literal> actions.
</para>
+
+ <para>
+ In a temporal foreign key, the change will use <literal>FOR
+ PORTION OF</literal> semantics to constrain the effect to the bounds
+ of the referenced row.
+ </para>
</listitem>
</varlistentry>
@@ -1261,6 +1288,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
can only be specified for <literal>ON DELETE</literal> actions.
(There must be a row in the referenced table matching the default
values, if they are not null, or the operation will fail.)
+ In a temporal foreign key, the change will use <literal>FOR PORTION
+ OF</literal> semantics to constrain the effect to the bounds of the
+ referenced row.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 9e3a88a7dd..be1734e6ed 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -380,16 +380,18 @@ static int transformColumnNameList(Oid relId, List *colList,
static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
List **attnamelist,
int16 *attnums, Oid *atttypids,
+ int16 *periodattnums, Oid *periodatttypids,
Oid *opclasses);
static Oid transformFkeyCheckAttrs(Relation pkrel,
int numattrs, int16 *attnums,
+ bool is_temporal, int16 periodattnum,
Oid *opclasses);
static void checkFkeyPermissions(Relation rel, int16 *attnums, int natts);
static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId,
Oid *funcid);
static void validateForeignKeyConstraint(char *conname,
Relation rel, Relation pkrel,
- Oid pkindOid, Oid constraintOid);
+ Oid pkindOid, Oid constraintOid, bool temporal);
static void ATController(AlterTableStmt *parsetree,
Relation rel, List *cmds, bool recurse, LOCKMODE lockmode,
AlterTableUtilityContext *context);
@@ -497,7 +499,8 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger);
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ bool is_temporal);
static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
int numfksetcols, const int16 *fksetcolsattnums,
List *fksetcols);
@@ -507,7 +510,9 @@ static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
bool old_check_ok, LOCKMODE lockmode,
- Oid parentInsTrigger, Oid parentUpdTrigger);
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ bool is_temporal);
+
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -539,6 +544,12 @@ static void GetForeignKeyCheckTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *insertTriggerOid,
Oid *updateTriggerOid);
+static void FindFKComparisonOperators(Constraint *fkconstraint,
+ AlteredTableInfo *tab, int i, int16 *fkattnum,
+ bool *old_check_ok, ListCell **old_pfeqop_item,
+ Oid pktype, Oid fktype, Oid opclass,
+ bool is_temporal, bool for_overlaps,
+ Oid *pfeqopOut, Oid *ppeqopOut, Oid *ffeqopOut);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -5717,7 +5728,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
validateForeignKeyConstraint(fkconstraint->conname, rel, refrel,
con->refindid,
- con->conid);
+ con->conid,
+ con->contemporal);
/*
* No need to mark the constraint row as validated, we did
@@ -9131,6 +9143,11 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
Oid ppeqoperators[INDEX_MAX_KEYS] = {0};
Oid ffeqoperators[INDEX_MAX_KEYS] = {0};
int16 fkdelsetcols[INDEX_MAX_KEYS] = {0};
+ bool is_temporal = (fkconstraint->fk_period != NULL);
+ int16 pkperiodattnums[1] = {0};
+ int16 fkperiodattnums[1] = {0};
+ Oid pkperiodtypoids[1] = {0};
+ Oid fkperiodtypoids[1] = {0};
int i;
int numfks,
numpks,
@@ -9225,6 +9242,10 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
numfks = transformColumnNameList(RelationGetRelid(rel),
fkconstraint->fk_attrs,
fkattnum, fktypoid);
+ if (is_temporal)
+ transformColumnNameList(RelationGetRelid(rel),
+ list_make1(fkconstraint->fk_period),
+ fkperiodattnums, fkperiodtypoids);
numfkdelsetcols = transformColumnNameList(RelationGetRelid(rel),
fkconstraint->fk_del_set_cols,
@@ -9244,6 +9265,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
numpks = transformFkeyGetPrimaryKey(pkrel, &indexOid,
&fkconstraint->pk_attrs,
pkattnum, pktypoid,
+ pkperiodattnums, pkperiodtypoids,
opclasses);
}
else
@@ -9251,8 +9273,14 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
numpks = transformColumnNameList(RelationGetRelid(pkrel),
fkconstraint->pk_attrs,
pkattnum, pktypoid);
+ if (is_temporal)
+ transformColumnNameList(RelationGetRelid(pkrel),
+ list_make1(fkconstraint->pk_period),
+ pkperiodattnums, pkperiodtypoids);
+
/* Look for an index matching the column list */
indexOid = transformFkeyCheckAttrs(pkrel, numpks, pkattnum,
+ is_temporal, pkperiodattnums[0],
opclasses);
}
@@ -9311,187 +9339,27 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
for (i = 0; i < numpks; i++)
{
- Oid pktype = pktypoid[i];
- Oid fktype = fktypoid[i];
- Oid fktyped;
- HeapTuple cla_ht;
- Form_pg_opclass cla_tup;
- Oid amid;
- Oid opfamily;
- Oid opcintype;
- Oid pfeqop;
- Oid ppeqop;
- Oid ffeqop;
- int16 eqstrategy;
- Oid pfeqop_right;
-
- /* We need several fields out of the pg_opclass entry */
- cla_ht = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclasses[i]));
- if (!HeapTupleIsValid(cla_ht))
- elog(ERROR, "cache lookup failed for opclass %u", opclasses[i]);
- cla_tup = (Form_pg_opclass) GETSTRUCT(cla_ht);
- amid = cla_tup->opcmethod;
- opfamily = cla_tup->opcfamily;
- opcintype = cla_tup->opcintype;
- ReleaseSysCache(cla_ht);
-
- /*
- * Check it's a btree; currently this can never fail since no other
- * index AMs support unique indexes. If we ever did have other types
- * of unique indexes, we'd need a way to determine which operator
- * strategy number is equality. (Is it reasonable to insist that
- * every such index AM use btree's number for equality?)
- */
- if (amid != BTREE_AM_OID)
- elog(ERROR, "only b-tree indexes are supported for foreign keys");
- eqstrategy = BTEqualStrategyNumber;
-
- /*
- * There had better be a primary equality operator for the index.
- * We'll use it for PK = PK comparisons.
- */
- ppeqop = get_opfamily_member(opfamily, opcintype, opcintype,
- eqstrategy);
-
- if (!OidIsValid(ppeqop))
- elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
- eqstrategy, opcintype, opcintype, opfamily);
-
- /*
- * Are there equality operators that take exactly the FK type? Assume
- * we should look through any domain here.
- */
- fktyped = getBaseType(fktype);
-
- pfeqop = get_opfamily_member(opfamily, opcintype, fktyped,
- eqstrategy);
- if (OidIsValid(pfeqop))
- {
- pfeqop_right = fktyped;
- ffeqop = get_opfamily_member(opfamily, fktyped, fktyped,
- eqstrategy);
- }
- else
- {
- /* keep compiler quiet */
- pfeqop_right = InvalidOid;
- ffeqop = InvalidOid;
- }
-
- if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
- {
- /*
- * Otherwise, look for an implicit cast from the FK type to the
- * opcintype, and if found, use the primary equality operator.
- * This is a bit tricky because opcintype might be a polymorphic
- * type such as ANYARRAY or ANYENUM; so what we have to test is
- * whether the two actual column types can be concurrently cast to
- * that type. (Otherwise, we'd fail to reject combinations such
- * as int[] and point[].)
- */
- Oid input_typeids[2];
- Oid target_typeids[2];
-
- input_typeids[0] = pktype;
- input_typeids[1] = fktype;
- target_typeids[0] = opcintype;
- target_typeids[1] = opcintype;
- if (can_coerce_type(2, input_typeids, target_typeids,
- COERCION_IMPLICIT))
- {
- pfeqop = ffeqop = ppeqop;
- pfeqop_right = opcintype;
- }
- }
-
- if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("foreign key constraint \"%s\" cannot be implemented",
- fkconstraint->conname),
- errdetail("Key columns \"%s\" and \"%s\" "
- "are of incompatible types: %s and %s.",
- strVal(list_nth(fkconstraint->fk_attrs, i)),
- strVal(list_nth(fkconstraint->pk_attrs, i)),
- format_type_be(fktype),
- format_type_be(pktype))));
-
- if (old_check_ok)
- {
- /*
- * When a pfeqop changes, revalidate the constraint. We could
- * permit intra-opfamily changes, but that adds subtle complexity
- * without any concrete benefit for core types. We need not
- * assess ppeqop or ffeqop, which RI_Initial_Check() does not use.
- */
- old_check_ok = (pfeqop == lfirst_oid(old_pfeqop_item));
- old_pfeqop_item = lnext(fkconstraint->old_conpfeqop,
- old_pfeqop_item);
- }
- if (old_check_ok)
- {
- Oid old_fktype;
- Oid new_fktype;
- CoercionPathType old_pathtype;
- CoercionPathType new_pathtype;
- Oid old_castfunc;
- Oid new_castfunc;
- Form_pg_attribute attr = TupleDescAttr(tab->oldDesc,
- fkattnum[i] - 1);
-
- /*
- * Identify coercion pathways from each of the old and new FK-side
- * column types to the right (foreign) operand type of the pfeqop.
- * We may assume that pg_constraint.conkey is not changing.
- */
- old_fktype = attr->atttypid;
- new_fktype = fktype;
- old_pathtype = findFkeyCast(pfeqop_right, old_fktype,
- &old_castfunc);
- new_pathtype = findFkeyCast(pfeqop_right, new_fktype,
- &new_castfunc);
-
- /*
- * Upon a change to the cast from the FK column to its pfeqop
- * operand, revalidate the constraint. For this evaluation, a
- * binary coercion cast is equivalent to no cast at all. While
- * type implementors should design implicit casts with an eye
- * toward consistency of operations like equality, we cannot
- * assume here that they have done so.
- *
- * A function with a polymorphic argument could change behavior
- * arbitrarily in response to get_fn_expr_argtype(). Therefore,
- * when the cast destination is polymorphic, we only avoid
- * revalidation if the input type has not changed at all. Given
- * just the core data types and operator classes, this requirement
- * prevents no would-be optimizations.
- *
- * If the cast converts from a base type to a domain thereon, then
- * that domain type must be the opcintype of the unique index.
- * Necessarily, the primary key column must then be of the domain
- * type. Since the constraint was previously valid, all values on
- * the foreign side necessarily exist on the primary side and in
- * turn conform to the domain. Consequently, we need not treat
- * domains specially here.
- *
- * Since we require that all collations share the same notion of
- * equality (which they do, because texteq reduces to bitwise
- * equality), we don't compare collation here.
- *
- * We need not directly consider the PK type. It's necessarily
- * binary coercible to the opcintype of the unique index column,
- * and ri_triggers.c will only deal with PK datums in terms of
- * that opcintype. Changing the opcintype also changes pfeqop.
- */
- old_check_ok = (new_pathtype == old_pathtype &&
- new_castfunc == old_castfunc &&
- (!IsPolymorphicType(pfeqop_right) ||
- new_fktype == old_fktype));
- }
+ FindFKComparisonOperators(
+ fkconstraint, tab, i, fkattnum,
+ &old_check_ok, &old_pfeqop_item,
+ pktypoid[i], fktypoid[i], opclasses[i],
+ is_temporal, false,
+ &pfeqoperators[i], &ppeqoperators[i], &ffeqoperators[i]);
+ }
+ if (is_temporal) {
+ pkattnum[numpks] = pkperiodattnums[0];
+ pktypoid[numpks] = pkperiodtypoids[0];
+ fkattnum[numpks] = fkperiodattnums[0];
+ fktypoid[numpks] = fkperiodtypoids[0];
- pfeqoperators[i] = pfeqop;
- ppeqoperators[i] = ppeqop;
- ffeqoperators[i] = ffeqop;
+ FindFKComparisonOperators(
+ fkconstraint, tab, numpks, fkattnum,
+ &old_check_ok, &old_pfeqop_item,
+ pkperiodtypoids[0], fkperiodtypoids[0], opclasses[numpks],
+ is_temporal, true,
+ &pfeqoperators[numpks], &ppeqoperators[numpks], &ffeqoperators[numpks]);
+ numfks += 1;
+ numpks += 1;
}
/*
@@ -9510,7 +9378,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
numfkdelsetcols,
fkdelsetcols,
old_check_ok,
- InvalidOid, InvalidOid);
+ InvalidOid, InvalidOid,
+ is_temporal);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -9526,7 +9395,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
fkdelsetcols,
old_check_ok,
lockmode,
- InvalidOid, InvalidOid);
+ InvalidOid, InvalidOid,
+ is_temporal);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -9611,7 +9481,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger)
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ bool is_temporal)
{
ObjectAddress address;
Oid constrOid;
@@ -9697,7 +9568,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
conislocal, /* islocal */
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
- false, /* conTemporal */
+ is_temporal, /* conTemporal */
false); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -9773,7 +9644,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols,
old_check_ok,
- deleteTriggerOid, updateTriggerOid);
+ deleteTriggerOid, updateTriggerOid,
+ is_temporal);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9831,7 +9703,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
bool old_check_ok, LOCKMODE lockmode,
- Oid parentInsTrigger, Oid parentUpdTrigger)
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ bool is_temporal)
{
Oid insertTriggerOid,
updateTriggerOid;
@@ -9879,6 +9752,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
newcon->refrelid = RelationGetRelid(pkrel);
newcon->refindid = indexOid;
newcon->conid = parentConstr;
+ newcon->contemporal = fkconstraint->fk_period != NULL;
newcon->qual = (Node *) fkconstraint;
tab->constraints = lappend(tab->constraints, newcon);
@@ -9996,7 +9870,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
false,
1,
false,
- false, /* conTemporal */
+ is_temporal, /* conTemporal */
false);
/*
@@ -10027,7 +9901,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
old_check_ok,
lockmode,
insertTriggerOid,
- updateTriggerOid);
+ updateTriggerOid,
+ is_temporal);
table_close(partition, NoLock);
}
@@ -10263,7 +10138,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
confdelsetcols,
true,
deleteTriggerOid,
- updateTriggerOid);
+ updateTriggerOid,
+ constrForm->contemporal);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
@@ -10502,7 +10378,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
false, /* islocal */
1, /* inhcount */
false, /* conNoInherit */
- false, /* conTemporal */
+ constrForm->contemporal, /* conTemporal */
true);
/* Set up partition dependencies for the new constraint */
@@ -10536,13 +10412,232 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
false, /* no old check exists */
AccessExclusiveLock,
insertTriggerOid,
- updateTriggerOid);
+ updateTriggerOid,
+ constrForm->contemporal);
table_close(pkrel, NoLock);
}
table_close(trigrel, RowExclusiveLock);
}
+static void
+FindFKComparisonOperators(Constraint *fkconstraint,
+ AlteredTableInfo *tab,
+ int i,
+ int16 *fkattnum,
+ bool *old_check_ok,
+ ListCell **old_pfeqop_item,
+ Oid pktype, Oid fktype, Oid opclass,
+ bool is_temporal, bool for_overlaps,
+ Oid *pfeqopOut, Oid *ppeqopOut, Oid *ffeqopOut)
+{
+ Oid fktyped;
+ HeapTuple cla_ht;
+ Form_pg_opclass cla_tup;
+ Oid amid;
+ Oid opfamily;
+ Oid opcintype;
+ Oid pfeqop;
+ Oid ppeqop;
+ Oid ffeqop;
+ int16 eqstrategy;
+ Oid pfeqop_right;
+
+ /* We need several fields out of the pg_opclass entry */
+ cla_ht = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclass));
+ if (!HeapTupleIsValid(cla_ht))
+ elog(ERROR, "cache lookup failed for opclass %u", opclass);
+ cla_tup = (Form_pg_opclass) GETSTRUCT(cla_ht);
+ amid = cla_tup->opcmethod;
+ opfamily = cla_tup->opcfamily;
+ opcintype = cla_tup->opcintype;
+ ReleaseSysCache(cla_ht);
+
+ if (is_temporal)
+ {
+ if (amid != GIST_AM_OID)
+ elog(ERROR, "only GiST indexes are supported for temporal foreign keys");
+ eqstrategy = for_overlaps ? RTOverlapStrategyNumber : RTEqualStrategyNumber;
+ }
+ else
+ {
+ /*
+ * Check it's a btree; currently this can never fail since no other
+ * index AMs support unique indexes. If we ever did have other types
+ * of unique indexes, we'd need a way to determine which operator
+ * strategy number is equality. (Is it reasonable to insist that
+ * every such index AM use btree's number for equality?)
+ */
+ if (amid != BTREE_AM_OID)
+ elog(ERROR, "only b-tree indexes are supported for foreign keys");
+ eqstrategy = BTEqualStrategyNumber;
+ }
+
+ /*
+ * There had better be a primary equality operator for the index.
+ * We'll use it for PK = PK comparisons.
+ */
+ ppeqop = get_opfamily_member(opfamily, opcintype, opcintype,
+ eqstrategy);
+
+ if (!OidIsValid(ppeqop))
+ elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+ eqstrategy, opcintype, opcintype, opfamily);
+
+ /*
+ * Are there equality operators that take exactly the FK type? Assume
+ * we should look through any domain here.
+ */
+ fktyped = getBaseType(fktype);
+
+ pfeqop = get_opfamily_member(opfamily, opcintype, fktyped,
+ eqstrategy);
+ if (OidIsValid(pfeqop))
+ {
+ pfeqop_right = fktyped;
+ ffeqop = get_opfamily_member(opfamily, fktyped, fktyped,
+ eqstrategy);
+ }
+ else
+ {
+ /* keep compiler quiet */
+ pfeqop_right = InvalidOid;
+ ffeqop = InvalidOid;
+ }
+
+ if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
+ {
+ /*
+ * Otherwise, look for an implicit cast from the FK type to the
+ * opcintype, and if found, use the primary equality operator.
+ * This is a bit tricky because opcintype might be a polymorphic
+ * type such as ANYARRAY or ANYENUM; so what we have to test is
+ * whether the two actual column types can be concurrently cast to
+ * that type. (Otherwise, we'd fail to reject combinations such
+ * as int[] and point[].)
+ */
+ Oid input_typeids[2];
+ Oid target_typeids[2];
+
+ input_typeids[0] = pktype;
+ input_typeids[1] = fktype;
+ target_typeids[0] = opcintype;
+ target_typeids[1] = opcintype;
+ if (can_coerce_type(2, input_typeids, target_typeids,
+ COERCION_IMPLICIT))
+ {
+ pfeqop = ffeqop = ppeqop;
+ pfeqop_right = opcintype;
+ }
+ }
+
+ if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
+ {
+ char *fkattr_name;
+ char *pkattr_name;
+
+ if (for_overlaps)
+ {
+ fkattr_name = strVal(fkconstraint->fk_period);
+ pkattr_name = strVal(fkconstraint->pk_period);
+ }
+ else
+ {
+ fkattr_name = strVal(list_nth(fkconstraint->fk_attrs, i));
+ pkattr_name = strVal(list_nth(fkconstraint->pk_attrs, i));
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("foreign key constraint \"%s\" cannot be implemented",
+ fkconstraint->conname),
+ errdetail("Key columns \"%s\" and \"%s\" "
+ "are of incompatible types: %s and %s.",
+ fkattr_name,
+ pkattr_name,
+ format_type_be(fktype),
+ format_type_be(pktype))));
+ }
+
+ if (*old_check_ok)
+ {
+ /*
+ * When a pfeqop changes, revalidate the constraint. We could
+ * permit intra-opfamily changes, but that adds subtle complexity
+ * without any concrete benefit for core types. We need not
+ * assess ppeqop or ffeqop, which RI_Initial_Check() does not use.
+ */
+ *old_check_ok = (pfeqop == lfirst_oid(*old_pfeqop_item));
+ *old_pfeqop_item = lnext(fkconstraint->old_conpfeqop,
+ *old_pfeqop_item);
+ }
+ if (*old_check_ok)
+ {
+ Oid old_fktype;
+ Oid new_fktype;
+ CoercionPathType old_pathtype;
+ CoercionPathType new_pathtype;
+ Oid old_castfunc;
+ Oid new_castfunc;
+ Form_pg_attribute attr = TupleDescAttr(tab->oldDesc,
+ fkattnum[i] - 1);
+
+ /*
+ * Identify coercion pathways from each of the old and new FK-side
+ * column types to the right (foreign) operand type of the pfeqop.
+ * We may assume that pg_constraint.conkey is not changing.
+ */
+ old_fktype = attr->atttypid;
+ new_fktype = fktype;
+ old_pathtype = findFkeyCast(pfeqop_right, old_fktype,
+ &old_castfunc);
+ new_pathtype = findFkeyCast(pfeqop_right, new_fktype,
+ &new_castfunc);
+
+ /*
+ * Upon a change to the cast from the FK column to its pfeqop
+ * operand, revalidate the constraint. For this evaluation, a
+ * binary coercion cast is equivalent to no cast at all. While
+ * type implementors should design implicit casts with an eye
+ * toward consistency of operations like equality, we cannot
+ * assume here that they have done so.
+ *
+ * A function with a polymorphic argument could change behavior
+ * arbitrarily in response to get_fn_expr_argtype(). Therefore,
+ * when the cast destination is polymorphic, we only avoid
+ * revalidation if the input type has not changed at all. Given
+ * just the core data types and operator classes, this requirement
+ * prevents no would-be optimizations.
+ *
+ * If the cast converts from a base type to a domain thereon, then
+ * that domain type must be the opcintype of the unique index.
+ * Necessarily, the primary key column must then be of the domain
+ * type. Since the constraint was previously valid, all values on
+ * the foreign side necessarily exist on the primary side and in
+ * turn conform to the domain. Consequently, we need not treat
+ * domains specially here.
+ *
+ * Since we require that all collations share the same notion of
+ * equality (which they do, because texteq reduces to bitwise
+ * equality), we don't compare collation here.
+ *
+ * We need not directly consider the PK type. It's necessarily
+ * binary coercible to the opcintype of the unique index column,
+ * and ri_triggers.c will only deal with PK datums in terms of
+ * that opcintype. Changing the opcintype also changes pfeqop.
+ */
+ *old_check_ok = (new_pathtype == old_pathtype &&
+ new_castfunc == old_castfunc &&
+ (!IsPolymorphicType(pfeqop_right) ||
+ new_fktype == old_fktype));
+
+ }
+
+ *pfeqopOut = pfeqop;
+ *ppeqopOut = ppeqop;
+ *ffeqopOut = ffeqop;
+}
+
/*
* When the parent of a partition receives [the referencing side of] a foreign
* key, we must propagate that foreign key to the partition. However, the
@@ -11288,7 +11383,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
return address;
}
-
/*
* transformColumnNameList - transform list of column names
*
@@ -11357,6 +11451,7 @@ static int
transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
List **attnamelist,
int16 *attnums, Oid *atttypids,
+ int16 *periodattnums, Oid *periodatttypids,
Oid *opclasses)
{
List *indexoidlist;
@@ -11421,36 +11516,52 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
/*
* Now build the list of PK attributes from the indkey definition (we
- * assume a primary key cannot have expressional elements)
+ * assume a primary key cannot have expressional elements, unless it
+ * has a PERIOD)
*/
*attnamelist = NIL;
for (i = 0; i < indexStruct->indnkeyatts; i++)
{
int pkattno = indexStruct->indkey.values[i];
- attnums[i] = pkattno;
- atttypids[i] = attnumTypeId(pkrel, pkattno);
- opclasses[i] = indclass->values[i];
- *attnamelist = lappend(*attnamelist,
- makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno)))));
+ if (i == indexStruct->indnkeyatts - 1 && indexStruct->indisexclusion)
+ {
+ /* we have a range */
+ /* The caller will set attnums[i] */
+ periodattnums[0] = pkattno;
+ periodatttypids[0] = attnumTypeId(pkrel, pkattno);
+ opclasses[i] = indclass->values[i];
+ }
+ else
+ {
+ attnums[i] = pkattno;
+ atttypids[i] = attnumTypeId(pkrel, pkattno);
+ opclasses[i] = indclass->values[i];
+ *attnamelist = lappend(*attnamelist,
+ makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno)))));
+ }
}
ReleaseSysCache(indexTuple);
- return i;
+ if (indexStruct->indisexclusion) return i - 1;
+ else return i;
}
/*
* transformFkeyCheckAttrs -
*
* Make sure that the attributes of a referenced table belong to a unique
- * (or primary key) constraint. Return the OID of the index supporting
- * the constraint, as well as the opclasses associated with the index
+ * (or primary key) constraint. Or if this is a temporal foreign key
+ * the primary key should be an exclusion constraint instead.
+ * Return the OID of the index supporting the constraint,
+ * as well as the opclasses associated with the index
* columns.
*/
static Oid
transformFkeyCheckAttrs(Relation pkrel,
int numattrs, int16 *attnums,
+ bool is_temporal, int16 periodattnum,
Oid *opclasses) /* output parameter */
{
Oid indexoid = InvalidOid;
@@ -11477,6 +11588,10 @@ transformFkeyCheckAttrs(Relation pkrel,
(errcode(ERRCODE_INVALID_FOREIGN_KEY),
errmsg("foreign key referenced-columns list must not contain duplicates")));
}
+ if (is_temporal && attnums[i] == periodattnum)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_FOREIGN_KEY),
+ errmsg("foreign key referenced-columns list must not contain duplicates")));
}
/*
@@ -11498,12 +11613,16 @@ transformFkeyCheckAttrs(Relation pkrel,
indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
/*
- * Must have the right number of columns; must be unique and not a
- * partial index; forget it if there are any expressions, too. Invalid
- * indexes are out as well.
+ * Must have the right number of columns; must be unique
+ * (or if temporal then exclusion instead) and not a
+ * partial index; forget it if there are any expressions, too.
+ * Invalid indexes are out as well.
*/
- if (indexStruct->indnkeyatts == numattrs &&
- indexStruct->indisunique &&
+ if ((is_temporal
+ ? (indexStruct->indnkeyatts == numattrs + 1 &&
+ indexStruct->indisexclusion)
+ : (indexStruct->indnkeyatts == numattrs &&
+ indexStruct->indisunique)) &&
indexStruct->indisvalid &&
heap_attisnull(indexTuple, Anum_pg_index_indpred, NULL) &&
heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL))
@@ -11541,6 +11660,19 @@ transformFkeyCheckAttrs(Relation pkrel,
if (!found)
break;
}
+ if (found && is_temporal)
+ {
+ found = false;
+ for (j = 0; j < numattrs + 1; j++)
+ {
+ if (periodattnum == indexStruct->indkey.values[j])
+ {
+ opclasses[numattrs] = indclass->values[j];
+ found = true;
+ break;
+ }
+ }
+ }
/*
* Refuse to use a deferrable unique/primary key. This is per SQL
@@ -11650,7 +11782,8 @@ validateForeignKeyConstraint(char *conname,
Relation rel,
Relation pkrel,
Oid pkindOid,
- Oid constraintOid)
+ Oid constraintOid,
+ bool temporal)
{
TupleTableSlot *slot;
TableScanDesc scan;
@@ -11679,8 +11812,10 @@ validateForeignKeyConstraint(char *conname,
/*
* See if we can do it with a single LEFT JOIN query. A false result
* indicates we must proceed with the fire-the-trigger method.
+ * We can't do a LEFT JOIN for temporal FKs yet,
+ * but we can once we support temporal left joins.
*/
- if (RI_Initial_Check(&trig, rel, pkrel))
+ if (!temporal && RI_Initial_Check(&trig, rel, pkrel))
return;
/*
@@ -11749,6 +11884,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
{
ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
+ bool is_temporal = fkconstraint->fk_period;
/*
* Note: for a self-referential FK (referencing and referenced tables are
@@ -11768,12 +11904,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
/* Either ON INSERT or ON UPDATE */
if (on_insert)
{
- fk_trigger->funcname = SystemFuncName("RI_FKey_check_ins");
+ if (is_temporal)
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_check_ins");
+ else
+ fk_trigger->funcname = SystemFuncName("RI_FKey_check_ins");
fk_trigger->events = TRIGGER_TYPE_INSERT;
}
else
{
- fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd");
+ if (is_temporal)
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_check_upd");
+ else
+ fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd");
fk_trigger->events = TRIGGER_TYPE_UPDATE;
}
@@ -11831,37 +11973,67 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
- switch (fkconstraint->fk_del_action)
+ if (fkconstraint->fk_period != NULL)
{
- case FKCONSTR_ACTION_NOACTION:
- fk_trigger->deferrable = fkconstraint->deferrable;
- fk_trigger->initdeferred = fkconstraint->initdeferred;
- fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_del");
- break;
- case FKCONSTR_ACTION_RESTRICT:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_del");
- break;
- case FKCONSTR_ACTION_CASCADE:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_del");
- break;
- case FKCONSTR_ACTION_SETNULL:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_del");
- break;
- case FKCONSTR_ACTION_SETDEFAULT:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del");
- break;
- default:
- elog(ERROR, "unrecognized FK action type: %d",
- (int) fkconstraint->fk_del_action);
- break;
+ /* Temporal foreign keys */
+ switch (fkconstraint->fk_del_action)
+ {
+ case FKCONSTR_ACTION_NOACTION:
+ fk_trigger->deferrable = fkconstraint->deferrable;
+ fk_trigger->initdeferred = fkconstraint->initdeferred;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_noaction_del");
+ break;
+ case FKCONSTR_ACTION_RESTRICT:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_del");
+ break;
+ case FKCONSTR_ACTION_CASCADE:
+ case FKCONSTR_ACTION_SETNULL:
+ case FKCONSTR_ACTION_SETDEFAULT:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("action not supported for temporal foreign keys")));
+ default:
+ elog(ERROR, "unrecognized FK action type: %d",
+ (int) fkconstraint->fk_del_action);
+ break;
+ }
+ }
+ else
+ {
+ switch (fkconstraint->fk_del_action)
+ {
+ case FKCONSTR_ACTION_NOACTION:
+ fk_trigger->deferrable = fkconstraint->deferrable;
+ fk_trigger->initdeferred = fkconstraint->initdeferred;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_del");
+ break;
+ case FKCONSTR_ACTION_RESTRICT:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_del");
+ break;
+ case FKCONSTR_ACTION_CASCADE:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_del");
+ break;
+ case FKCONSTR_ACTION_SETNULL:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_del");
+ break;
+ case FKCONSTR_ACTION_SETDEFAULT:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del");
+ break;
+ default:
+ elog(ERROR, "unrecognized FK action type: %d",
+ (int) fkconstraint->fk_del_action);
+ break;
+ }
}
trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
@@ -11891,37 +12063,67 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
- switch (fkconstraint->fk_upd_action)
+ if (fkconstraint->fk_period != NULL)
{
- case FKCONSTR_ACTION_NOACTION:
- fk_trigger->deferrable = fkconstraint->deferrable;
- fk_trigger->initdeferred = fkconstraint->initdeferred;
- fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_upd");
- break;
- case FKCONSTR_ACTION_RESTRICT:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_upd");
- break;
- case FKCONSTR_ACTION_CASCADE:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_upd");
- break;
- case FKCONSTR_ACTION_SETNULL:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_upd");
- break;
- case FKCONSTR_ACTION_SETDEFAULT:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_upd");
- break;
- default:
- elog(ERROR, "unrecognized FK action type: %d",
- (int) fkconstraint->fk_upd_action);
- break;
+ /* Temporal foreign keys */
+ switch (fkconstraint->fk_upd_action)
+ {
+ case FKCONSTR_ACTION_NOACTION:
+ fk_trigger->deferrable = fkconstraint->deferrable;
+ fk_trigger->initdeferred = fkconstraint->initdeferred;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_noaction_upd");
+ break;
+ case FKCONSTR_ACTION_RESTRICT:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_upd");
+ break;
+ case FKCONSTR_ACTION_CASCADE:
+ case FKCONSTR_ACTION_SETNULL:
+ case FKCONSTR_ACTION_SETDEFAULT:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("action not supported for temporal foreign keys")));
+ default:
+ elog(ERROR, "unrecognized FK action type: %d",
+ (int) fkconstraint->fk_upd_action);
+ break;
+ }
+ }
+ else
+ {
+ switch (fkconstraint->fk_upd_action)
+ {
+ case FKCONSTR_ACTION_NOACTION:
+ fk_trigger->deferrable = fkconstraint->deferrable;
+ fk_trigger->initdeferred = fkconstraint->initdeferred;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_upd");
+ break;
+ case FKCONSTR_ACTION_RESTRICT:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_upd");
+ break;
+ case FKCONSTR_ACTION_CASCADE:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_upd");
+ break;
+ case FKCONSTR_ACTION_SETNULL:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_upd");
+ break;
+ case FKCONSTR_ACTION_SETDEFAULT:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_upd");
+ break;
+ default:
+ elog(ERROR, "unrecognized FK action type: %d",
+ (int) fkconstraint->fk_upd_action);
+ break;
+ }
}
trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a5d51e21f8..612813832c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -523,11 +523,12 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <node> TableElement TypedTableElement ConstraintElem TableFuncElement
%type <node> columnDef columnOptions
%type <defelt> def_elem reloption_elem old_aggr_elem operator_def_elem
-%type <node> def_arg columnElem withoutOverlapsClause
+%type <node> def_arg columnElem withoutOverlapsClause optionalPeriodName
where_clause where_or_current_clause
a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound
columnref in_expr having_clause func_table xmltable array_expr
OptWhereClause operator_def_arg
+%type <list> opt_column_and_period_list
%type <list> rowsfrom_item rowsfrom_list opt_col_def_list
%type <boolean> opt_ordinality
%type <list> ExclusionConstraintList ExclusionConstraintElem
@@ -747,7 +748,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD
- PLACING PLANS POLICY
+ PERIOD PLACING PLANS POLICY
POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
@@ -4173,21 +4174,23 @@ ConstraintElem:
NULL, yyscanner);
$$ = (Node *) n;
}
- | FOREIGN KEY '(' columnList ')' REFERENCES qualified_name
- opt_column_list key_match key_actions ConstraintAttributeSpec
+ | FOREIGN KEY '(' columnList optionalPeriodName ')' REFERENCES qualified_name
+ opt_column_and_period_list key_match key_actions ConstraintAttributeSpec
{
Constraint *n = makeNode(Constraint);
n->contype = CONSTR_FOREIGN;
n->location = @1;
- n->pktable = $7;
+ n->pktable = $8;
n->fk_attrs = $4;
- n->pk_attrs = $8;
- n->fk_matchtype = $9;
- n->fk_upd_action = ($10)->updateAction->action;
- n->fk_del_action = ($10)->deleteAction->action;
- n->fk_del_set_cols = ($10)->deleteAction->cols;
- processCASbits($11, @11, "FOREIGN KEY",
+ n->fk_period = $5;
+ n->pk_attrs = linitial($9);
+ n->pk_period = lsecond($9);
+ n->fk_matchtype = $10;
+ n->fk_upd_action = ($11)->updateAction->action;
+ n->fk_del_action = ($11)->deleteAction->action;
+ n->fk_del_set_cols = ($11)->deleteAction->cols;
+ processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
&n->skip_validation, NULL,
yyscanner);
@@ -4215,6 +4218,16 @@ withoutOverlapsClause:
| /*EMPTY*/ { $$ = NULL; }
;
+optionalPeriodName:
+ ',' PERIOD columnElem { $$ = $3; }
+ | /*EMPTY*/ { $$ = NULL; }
+ ;
+
+opt_column_and_period_list:
+ '(' columnList optionalPeriodName ')' { $$ = list_make2($2, $3); }
+ | /*EMPTY*/ { $$ = list_make2(NIL, NULL); }
+ ;
+
columnElem: ColId
{
$$ = (Node *) makeString($1);
@@ -17429,6 +17442,7 @@ reserved_keyword:
| ONLY
| OR
| ORDER
+ | PERIOD
| PLACING
| PRIMARY
| REFERENCES
@@ -17736,6 +17750,7 @@ bare_label_keyword:
| PARTITION
| PASSING
| PASSWORD
+ | PERIOD
| PLACING
| PLANS
| POLICY
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 6945d99b3d..08c6e4c1c4 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -31,6 +31,7 @@
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
#include "catalog/pg_operator.h"
+#include "catalog/pg_range.h"
#include "catalog/pg_type.h"
#include "commands/trigger.h"
#include "executor/executor.h"
@@ -81,6 +82,12 @@
#define RI_PLAN_SETNULL_ONUPDATE 7
#define RI_PLAN_SETDEFAULT_ONDELETE 8
#define RI_PLAN_SETDEFAULT_ONUPDATE 9
+#define TRI_PLAN_CASCADE_ONDELETE 10
+#define TRI_PLAN_CASCADE_ONUPDATE 11
+#define TRI_PLAN_SETNULL_ONUPDATE 12
+// TODO: Need TRI SETNULL_ONDELETE
+#define TRI_PLAN_SETDEFAULT_ONUPDATE 13
+// TODO: Need TRI SETDEFAULT_ONDELETE
#define MAX_QUOTED_NAME_LEN (NAMEDATALEN*2+3)
#define MAX_QUOTED_REL_NAME_LEN (MAX_QUOTED_NAME_LEN*2)
@@ -118,6 +125,7 @@ typedef struct RI_ConstraintInfo
int16 confdelsetcols[RI_MAX_NUMKEYS]; /* attnums of cols to set on
* delete */
char confmatchtype; /* foreign key's match type */
+ bool temporal; /* if the foreign key is temporal */
int nkeys; /* number of key columns */
int16 pk_attnums[RI_MAX_NUMKEYS]; /* attnums of referenced cols */
int16 fk_attnums[RI_MAX_NUMKEYS]; /* attnums of referencing cols */
@@ -200,7 +208,7 @@ static int ri_NullCheck(TupleDesc tupDesc, TupleTableSlot *slot,
static void ri_BuildQueryKey(RI_QueryKey *key,
const RI_ConstraintInfo *riinfo,
int32 constr_queryno);
-static bool ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
+static bool ri_KeysStable(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
const RI_ConstraintInfo *riinfo, bool rel_is_pk);
static bool ri_AttributesEqual(Oid eq_opr, Oid typeid,
Datum oldvalue, Datum newvalue);
@@ -361,26 +369,57 @@ RI_FKey_check(TriggerData *trigdata)
/* ----------
* The query string built is
- * SELECT 1 FROM [ONLY] <pktable> x WHERE pkatt1 = $1 [AND ...]
- * FOR KEY SHARE OF x
+ * SELECT 1
+ * FROM [ONLY] <pktable> x WHERE pkatt1 = $1 [AND ...]
+ * FOR KEY SHARE OF x
* The type id's for the $ parameters are those of the
* corresponding FK attributes.
+ *
+ * But for temporal FKs we need to make sure
+ * the FK's range is completely covered.
+ * So we use this query instead:
+ * SELECT 1
+ * FROM (
+ * SELECT pkperiodatt AS r
+ * FROM [ONLY] pktable x
+ * WHERE pkatt1 = $1 [AND ...]
+ * AND pkperiodatt && $n
+ * FOR KEY SHARE OF x
+ * ) x1
+ * HAVING $n <@ range_agg(x1.r)
+ * Note if FOR KEY SHARE ever allows GROUP BY and HAVING
+ * we can make this a bit simpler.
* ----------
*/
initStringInfo(&querybuf);
pk_only = pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
"" : "ONLY ";
quoteRelationName(pkrelname, pk_rel);
- appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
- pk_only, pkrelname);
+ if (riinfo->temporal)
+ {
+ quoteOneName(attname,
+ RIAttName(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]));
+
+ appendStringInfo(&querybuf,
+ "SELECT 1 FROM (SELECT %s AS r FROM %s%s x",
+ attname, pk_only, pkrelname);
+ }
+ else {
+ appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
+ pk_only, pkrelname);
+ }
querysep = "WHERE";
for (int i = 0; i < riinfo->nkeys; i++)
{
- Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
- Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+ Oid pk_type;
+ Oid fk_type;
+ pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
quoteOneName(attname,
RIAttName(pk_rel, riinfo->pk_attnums[i]));
+
+ fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+
sprintf(paramname, "$%d", i + 1);
ri_GenerateQual(&querybuf, querysep,
attname, pk_type,
@@ -390,6 +429,8 @@ RI_FKey_check(TriggerData *trigdata)
queryoids[i] = fk_type;
}
appendStringInfoString(&querybuf, " FOR KEY SHARE OF x");
+ if (riinfo->temporal)
+ appendStringInfo(&querybuf, ") x1 HAVING $%d <@ pg_catalog.range_agg(x1.r)", riinfo->nkeys);
/* Prepare and save the plan */
qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids,
@@ -508,10 +549,12 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
querysep = "WHERE";
for (int i = 0; i < riinfo->nkeys; i++)
{
- Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+ Oid pk_type;
+ pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
quoteOneName(attname,
RIAttName(pk_rel, riinfo->pk_attnums[i]));
+
sprintf(paramname, "$%d", i + 1);
ri_GenerateQual(&querybuf, querysep,
attname, pk_type,
@@ -695,10 +738,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
querysep = "WHERE";
for (int i = 0; i < riinfo->nkeys; i++)
{
- Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
- Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
- Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
- Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
+ Oid pk_type;
+ Oid fk_type;
+ Oid pk_coll;
+ Oid fk_coll;
+
+ pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+ pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+
+ fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+ fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
quoteOneName(attname,
RIAttName(fk_rel, riinfo->fk_attnums[i]));
@@ -1213,6 +1262,126 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
}
}
+/* ----------
+ * TRI_FKey_check_ins -
+ *
+ * Check temporal foreign key existence at insert event on FK table.
+ * ----------
+ */
+Datum
+TRI_FKey_check_ins(PG_FUNCTION_ARGS)
+{
+ /*
+ * Check that this is a valid trigger call on the right time and event.
+ */
+ ri_CheckTrigger(fcinfo, "RI_FKey_check_ins", RI_TRIGTYPE_INSERT);
+
+ /*
+ * Share code with UPDATE case.
+ */
+ return RI_FKey_check((TriggerData *) fcinfo->context);
+}
+
+
+/* ----------
+ * TRI_FKey_check_upd -
+ *
+ * Check temporal foreign key existence at update event on FK table.
+ * ----------
+ */
+Datum
+TRI_FKey_check_upd(PG_FUNCTION_ARGS)
+{
+ /*
+ * Check that this is a valid trigger call on the right time and event.
+ */
+ ri_CheckTrigger(fcinfo, "RI_FKey_check_upd", RI_TRIGTYPE_UPDATE);
+
+ /*
+ * Share code with INSERT case.
+ */
+ return RI_FKey_check((TriggerData *) fcinfo->context);
+}
+
+
+/* ----------
+ * TRI_FKey_noaction_del -
+ *
+ * Give an error and roll back the current transaction if the
+ * delete has resulted in a violation of the given temporal
+ * referential integrity constraint.
+ * ----------
+ */
+Datum
+TRI_FKey_noaction_del(PG_FUNCTION_ARGS)
+{
+ /*
+ * Check that this is a valid trigger call on the right time and event.
+ */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_noaction_del", RI_TRIGTYPE_DELETE);
+
+ /*
+ * Share code with RESTRICT/UPDATE cases.
+ */
+ return ri_restrict((TriggerData *) fcinfo->context, true);
+}
+
+/*
+ * TRI_FKey_restrict_del -
+ *
+ * Restrict delete from PK table to rows unreferenced by foreign key.
+ *
+ * The SQL standard intends that this referential action occur exactly when
+ * the delete is performed, rather than after. This appears to be
+ * the only difference between "NO ACTION" and "RESTRICT". In Postgres
+ * we still implement this as an AFTER trigger, but it's non-deferrable.
+ */
+Datum
+TRI_FKey_restrict_del(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_restrict_del", RI_TRIGTYPE_DELETE);
+
+ /* Share code with NO ACTION/UPDATE cases. */
+ return ri_restrict((TriggerData *) fcinfo->context, false);
+}
+
+/*
+ * TRI_FKey_noaction_upd -
+ *
+ * Give an error and roll back the current transaction if the
+ * update has resulted in a violation of the given referential
+ * integrity constraint.
+ */
+Datum
+TRI_FKey_noaction_upd(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_noaction_upd", RI_TRIGTYPE_UPDATE);
+
+ /* Share code with RESTRICT/DELETE cases. */
+ return ri_restrict((TriggerData *) fcinfo->context, true);
+}
+
+/*
+ * TRI_FKey_restrict_upd -
+ *
+ * Restrict update of PK to rows unreferenced by foreign key.
+ *
+ * The SQL standard intends that this referential action occur exactly when
+ * the update is performed, rather than after. This appears to be
+ * the only difference between "NO ACTION" and "RESTRICT". In Postgres
+ * we still implement this as an AFTER trigger, but it's non-deferrable.
+ */
+Datum
+TRI_FKey_restrict_upd(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_restrict_upd", RI_TRIGTYPE_UPDATE);
+
+ /* Share code with NO ACTION/DELETE cases. */
+ return ri_restrict((TriggerData *) fcinfo->context, false);
+}
/*
* RI_FKey_pk_upd_check_required -
@@ -1241,7 +1410,7 @@ RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel,
return false;
/* If all old and new key values are equal, no check is needed */
- if (newslot && ri_KeysEqual(pk_rel, oldslot, newslot, riinfo, true))
+ if (newslot && ri_KeysStable(pk_rel, oldslot, newslot, riinfo, true))
return false;
/* Else we need to fire the trigger. */
@@ -1340,7 +1509,7 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
return true;
/* If all old and new key values are equal, no check is needed */
- if (ri_KeysEqual(fk_rel, oldslot, newslot, riinfo, false))
+ if (ri_KeysStable(fk_rel, oldslot, newslot, riinfo, false))
return false;
/* Else we need to fire the trigger. */
@@ -1488,15 +1657,17 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel)
sep = "(";
for (int i = 0; i < riinfo->nkeys; i++)
{
- Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
- Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
- Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
- Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
+ Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+ Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+ Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+ Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
quoteOneName(pkattname + 3,
RIAttName(pk_rel, riinfo->pk_attnums[i]));
+
quoteOneName(fkattname + 3,
RIAttName(fk_rel, riinfo->fk_attnums[i]));
+
ri_GenerateQual(&querybuf, sep,
pkattname, pk_type,
riinfo->pf_eq_oprs[i],
@@ -2137,6 +2308,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
riinfo = (RI_ConstraintInfo *) hash_search(ri_constraint_cache,
&constraintOid,
HASH_ENTER, &found);
+
if (!found)
riinfo->valid = false;
else if (riinfo->valid)
@@ -2171,6 +2343,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
riinfo->confupdtype = conForm->confupdtype;
riinfo->confdeltype = conForm->confdeltype;
riinfo->confmatchtype = conForm->confmatchtype;
+ riinfo->temporal = conForm->contemporal;
DeconstructFkConstraintRow(tup,
&riinfo->nkeys,
@@ -2791,9 +2964,12 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan)
/*
- * ri_KeysEqual -
+ * ri_KeysStable -
*
- * Check if all key values in OLD and NEW are equal.
+ * Check if all key values in OLD and NEW are "equivalent":
+ * For normal FKs we check for equality.
+ * For temporal FKs we check that the PK side is a superset of its old value,
+ * or the FK side is a subset.
*
* Note: at some point we might wish to redefine this as checking for
* "IS NOT DISTINCT" rather than "=", that is, allow two nulls to be
@@ -2801,7 +2977,7 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan)
* previously found at least one of the rows to contain no nulls.
*/
static bool
-ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
+ri_KeysStable(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
const RI_ConstraintInfo *riinfo, bool rel_is_pk)
{
const int16 *attnums;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 0ef2038e5d..37cb3a29a8 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -339,7 +339,7 @@ static char *pg_get_viewdef_worker(Oid viewoid,
int prettyFlags, int wrapColumn);
static char *pg_get_triggerdef_worker(Oid trigid, bool pretty);
static int decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
- bool withoutOverlaps, StringInfo buf);
+ bool withoutOverlaps, bool withPeriod, StringInfo buf);
static char *pg_get_ruledef_worker(Oid ruleoid, int prettyFlags);
static char *pg_get_indexdef_worker(Oid indexrelid, int colno,
const Oid *excludeOps,
@@ -2247,7 +2247,10 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
val = SysCacheGetAttrNotNull(CONSTROID, tup,
Anum_pg_constraint_conkey);
- decompile_column_index_array(val, conForm->conrelid, conForm->conindid, false, &buf);
+ /* If it is a temporal foreign key then it uses PERIOD. */
+ decompile_column_index_array(val, conForm->conrelid,
+ conForm->conindid, false,
+ conForm->contemporal, &buf);
/* add foreign relation name */
appendStringInfo(&buf, ") REFERENCES %s(",
@@ -2258,7 +2261,9 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
val = SysCacheGetAttrNotNull(CONSTROID, tup,
Anum_pg_constraint_confkey);
- decompile_column_index_array(val, conForm->confrelid, conForm->conindid, false, &buf);
+ decompile_column_index_array(val, conForm->confrelid,
+ conForm->conindid, false,
+ conForm->contemporal, &buf);
appendStringInfoChar(&buf, ')');
@@ -2344,7 +2349,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
if (!isnull)
{
appendStringInfoString(&buf, " (");
- decompile_column_index_array(val, conForm->conrelid, InvalidOid, false, &buf);
+ decompile_column_index_array(val, conForm->conrelid, InvalidOid, false, false, &buf);
appendStringInfoChar(&buf, ')');
}
@@ -2387,7 +2392,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
indexId = conForm->conindid;
SysCacheGetAttr(CONSTROID, tup,
Anum_pg_constraint_conexclop, &isnull);
- keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, &buf);
+ keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, false, &buf);
appendStringInfoChar(&buf, ')');
@@ -2569,7 +2574,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
*/
static int
decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
- bool withoutOverlaps, StringInfo buf)
+ bool withoutOverlaps, bool withPeriod, StringInfo buf)
{
Datum *keys;
int nKeys;
@@ -2590,6 +2595,8 @@ decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
appendStringInfoString(buf, quote_identifier(colName));
else if (withoutOverlaps && j == nKeys - 1)
appendStringInfo(buf, ", %s WITHOUT OVERLAPS", quote_identifier(colName));
+ else if (withPeriod && j == nKeys - 1)
+ appendStringInfo(buf, ", PERIOD %s", quote_identifier(colName));
else
appendStringInfo(buf, ", %s", quote_identifier(colName));
}
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 67048e3006..458dc060bc 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -127,19 +127,19 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
int16 confkey[1];
/*
- * If a foreign key, the OIDs of the PK = FK equality operators for each
+ * If a foreign key, the OIDs of the PK = FK comparison operators for each
* column of the constraint
*/
Oid conpfeqop[1] BKI_LOOKUP(pg_operator);
/*
- * If a foreign key, the OIDs of the PK = PK equality operators for each
+ * If a foreign key, the OIDs of the PK = PK comparison operators for each
* column of the constraint (i.e., equality for the referenced columns)
*/
Oid conppeqop[1] BKI_LOOKUP(pg_operator);
/*
- * If a foreign key, the OIDs of the FK = FK equality operators for each
+ * If a foreign key, the OIDs of the FK = FK comparison operators for each
* column of the constraint (i.e., equality for the referencing columns)
*/
Oid conffeqop[1] BKI_LOOKUP(pg_operator);
@@ -180,7 +180,7 @@ DECLARE_INDEX(pg_constraint_conparentid_index, 2579, ConstraintParentIndexId, on
/* conkey can contain zero (InvalidAttrNumber) if a whole-row Var is used */
DECLARE_ARRAY_FOREIGN_KEY_OPT((conrelid, conkey), pg_attribute, (attrelid, attnum));
-DECLARE_ARRAY_FOREIGN_KEY((confrelid, confkey), pg_attribute, (attrelid, attnum));
+DECLARE_ARRAY_FOREIGN_KEY_OPT((confrelid, confkey), pg_attribute, (attrelid, attnum));
#ifdef EXPOSE_TO_CLIENT_CODE
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6996073989..eac76d2af8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3930,6 +3930,30 @@
prorettype => 'trigger', proargtypes => '',
prosrc => 'RI_FKey_noaction_upd' },
+# Temporal referential integrity constraint triggers
+{ oid => '6122', descr => 'temporal referential integrity FOREIGN KEY ... REFERENCES',
+ proname => 'TRI_FKey_check_ins', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'TRI_FKey_check_ins' },
+{ oid => '6123', descr => 'temporal referential integrity FOREIGN KEY ... REFERENCES',
+ proname => 'TRI_FKey_check_upd', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'TRI_FKey_check_upd' },
+{ oid => '6126', descr => 'temporal referential integrity ON DELETE RESTRICT',
+ proname => 'TRI_FKey_restrict_del', provolatile => 'v',
+ prorettype => 'trigger', proargtypes => '',
+ prosrc => 'TRI_FKey_restrict_del' },
+{ oid => '6127', descr => 'temporal referential integrity ON UPDATE RESTRICT',
+ proname => 'TRI_FKey_restrict_upd', provolatile => 'v',
+ prorettype => 'trigger', proargtypes => '',
+ prosrc => 'TRI_FKey_restrict_upd' },
+{ oid => '6132', descr => 'temporal referential integrity ON DELETE NO ACTION',
+ proname => 'TRI_FKey_noaction_del', provolatile => 'v',
+ prorettype => 'trigger', proargtypes => '',
+ prosrc => 'TRI_FKey_noaction_del' },
+{ oid => '6133', descr => 'temporal referential integrity ON UPDATE NO ACTION',
+ proname => 'TRI_FKey_noaction_upd', provolatile => 'v',
+ prorettype => 'trigger', proargtypes => '',
+ prosrc => 'TRI_FKey_noaction_upd' },
+
{ oid => '1666',
proname => 'varbiteq', proleakproof => 't', prorettype => 'bool',
proargtypes => 'varbit varbit', prosrc => 'biteq' },
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 76ad76ddd7..74cf88f0e9 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2592,7 +2592,9 @@ typedef struct Constraint
/* Fields used for FOREIGN KEY constraints: */
RangeVar *pktable; /* Primary key table */
List *fk_attrs; /* Attributes of foreign key */
+ Node *fk_period; /* String node naming Period or range column */
List *pk_attrs; /* Corresponding attrs in PK table */
+ Node *pk_period; /* String node naming Period or range column */
char fk_matchtype; /* FULL, PARTIAL, SIMPLE */
char fk_upd_action; /* ON UPDATE action */
char fk_del_action; /* ON DELETE action */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f5b2e61ca5..199cd74738 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -324,6 +324,7 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("period", PERIOD, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index c911c716ca..19d8cd6292 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -294,3 +294,295 @@ CREATE TABLE temporal_partitioned (
) PARTITION BY LIST (id);
ERROR: cannot match partition key to an index using access method "gist"
-- TODO: attach some partitions, insert into them, update them with and without FOR PORTION OF, delete them the same way.
+--
+-- test FK dependencies
+--
+-- can't drop a range referenced by an FK, unless with CASCADE
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal3 (id, PERIOD valid_at)
+);
+ALTER TABLE temporal3 DROP COLUMN valid_at;
+ERROR: cannot drop column valid_at of table temporal3 because other objects depend on it
+DETAIL: constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng depends on column valid_at of table temporal3
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+ALTER TABLE temporal3 DROP COLUMN valid_at CASCADE;
+NOTICE: drop cascades to constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng
+DROP TABLE temporal_fk_rng2rng;
+DROP TABLE temporal3;
+--
+-- test FOREIGN KEY, range references range
+--
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at int4range,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk2 FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+ERROR: foreign key constraint "temporal_fk_rng2rng_fk2" cannot be implemented
+DETAIL: Key columns "valid_at" and "valid_at" are of incompatible types: int4range and tsrange.
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+DROP TABLE temporal_fk_rng2rng;
+-- with inferred PK on the referenced table:
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+);
+DROP TABLE temporal_fk_rng2rng;
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+ REFERENCES temporal_rng (id, PERIOD id)
+);
+ERROR: foreign key referenced-columns list must not contain duplicates
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng (id, PERIOD valid_at);
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk2
+ FOREIGN KEY (parent_id, PERIOD parent_id)
+ REFERENCES temporal_rng (id, PERIOD id);
+ERROR: foreign key referenced-columns list must not contain duplicates
+--
+-- test with rows already
+--
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]');
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+ERROR: insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL: Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sun Apr 01 00:00:00 2018")) is not present in table "temporal_rng".
+-- okay again:
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+--
+-- test pg_get_constraintdef
+--
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2rng_fk';
+ pg_get_constraintdef
+---------------------------------------------------------------------------------------
+ FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng(id, PERIOD valid_at)
+(1 row)
+
+--
+-- test FK child inserts
+--
+INSERT INTO temporal_fk_rng2rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+ERROR: insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL: Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sun Apr 01 00:00:00 2018")) is not present in table "temporal_rng".
+-- now it should work:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+--
+-- test FK child updates
+--
+UPDATE temporal_fk_rng2rng SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_rng2rng SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]';
+ERROR: insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL: Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Tue May 01 00:00:00 2018")) is not present in table "temporal_rng".
+UPDATE temporal_fk_rng2rng SET parent_id = '[8,8]' WHERE id = '[1,1]';
+ERROR: insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL: Key (parent_id, valid_at)=([8,9), ["Tue Jan 02 00:00:00 2018","Thu Mar 01 00:00:00 2018")) is not present in table "temporal_rng".
+--
+-- test FK parent updates NO ACTION
+--
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_rng SET valid_at = tsrange('2016-02-01', '2016-03-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+ERROR: update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng"
+DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+--
+-- test FK parent updates RESTRICT
+--
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_rng SET valid_at = tsrange('2016-02-01', '2016-03-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+ERROR: update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng"
+DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+--
+-- test FK parent deletes NO ACTION
+--
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+ERROR: update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng"
+DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+--
+-- test FK parent deletes RESTRICT
+--
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE RESTRICT;
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+ERROR: update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng"
+DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+--
+-- test ON UPDATE/DELETE options
+--
+-- test FK parent updates CASCADE
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE CASCADE ON UPDATE CASCADE;
+ERROR: action not supported for temporal foreign keys
+-- test FK parent updates SET NULL
+INSERT INTO temporal_rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]');
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE SET NULL ON UPDATE SET NULL;
+ERROR: action not supported for temporal foreign keys
+-- test FK parent updates SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[-1,-1]', tsrange(null, null));
+INSERT INTO temporal_rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]');
+ALTER TABLE temporal_fk_rng2rng
+ ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+ DROP CONSTRAINT temporal_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+ERROR: action not supported for temporal foreign keys
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 41a094f580..d9d4c0b2db 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -231,3 +231,291 @@ CREATE TABLE temporal_partitioned (
CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
) PARTITION BY LIST (id);
-- TODO: attach some partitions, insert into them, update them with and without FOR PORTION OF, delete them the same way.
+--
+-- test FK dependencies
+--
+
+-- can't drop a range referenced by an FK, unless with CASCADE
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal3 (id, PERIOD valid_at)
+);
+ALTER TABLE temporal3 DROP COLUMN valid_at;
+ALTER TABLE temporal3 DROP COLUMN valid_at CASCADE;
+DROP TABLE temporal_fk_rng2rng;
+DROP TABLE temporal3;
+
+--
+-- test FOREIGN KEY, range references range
+--
+
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at int4range,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk2 FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+DROP TABLE temporal_fk_rng2rng;
+
+-- with inferred PK on the referenced table:
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+);
+DROP TABLE temporal_fk_rng2rng;
+
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+ REFERENCES temporal_rng (id, PERIOD id)
+);
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng (id, PERIOD valid_at);
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk2
+ FOREIGN KEY (parent_id, PERIOD parent_id)
+ REFERENCES temporal_rng (id, PERIOD id);
+
+--
+-- test with rows already
+--
+
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]');
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+-- okay again:
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2rng_fk';
+
+--
+-- test FK child inserts
+--
+
+INSERT INTO temporal_fk_rng2rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+-- now it should work:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+
+--
+-- test FK child updates
+--
+
+UPDATE temporal_fk_rng2rng SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_rng2rng SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]';
+UPDATE temporal_fk_rng2rng SET parent_id = '[8,8]' WHERE id = '[1,1]';
+
+--
+-- test FK parent updates NO ACTION
+--
+
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_rng SET valid_at = tsrange('2016-02-01', '2016-03-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+
+--
+-- test FK parent updates RESTRICT
+--
+
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_rng SET valid_at = tsrange('2016-02-01', '2016-03-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+--
+-- test FK parent deletes NO ACTION
+--
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+
+--
+-- test FK parent deletes RESTRICT
+--
+
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE RESTRICT;
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+
+--
+-- test ON UPDATE/DELETE options
+--
+
+-- test FK parent updates CASCADE
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE CASCADE ON UPDATE CASCADE;
+
+
+-- test FK parent updates SET NULL
+INSERT INTO temporal_rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]');
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE SET NULL ON UPDATE SET NULL;
+
+-- test FK parent updates SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[-1,-1]', tsrange(null, null));
+INSERT INTO temporal_rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]');
+ALTER TABLE temporal_fk_rng2rng
+ ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+ DROP CONSTRAINT temporal_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
--
2.32.0 (Apple Git-132)
[application/octet-stream] v12-0003-Add-UPDATE-DELETE-FOR-PORTION-OF.patch (111.7K, ../../CA+renyVyMOb+t6QNoq=PBK1CY_oa9wdJrObjj6S1-RK9CxYwKQ@mail.gmail.com/5-v12-0003-Add-UPDATE-DELETE-FOR-PORTION-OF.patch)
download | inline diff:
From f3aa8f1db8228f5f530a64781a1380c92c2f583d Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Fri, 25 Jun 2021 18:54:35 -0700
Subject: [PATCH v12 3/5] Add UPDATE/DELETE FOR PORTION OF
- Added bison support for FOR PORTION OF syntax. The bounds must be
constant, so we forbid column references, subqueries, etc. But we
permit the magic UNBOUNDED keyword as the FROM or the TO (or both)
to perform an unbounded update/delete. We also accept functions like
NOW().
- Added logic to executor to insert new rows for the "leftover" part of
a record touched by a FOR PORTION OF query.
- Added tg_temporal descriptor to the TriggerData struct that we pass to
trigger functions. Our triggers use this to learn what bounds were
given in the FOR PORTION OF clause.
- Documented FOR PORTION OF.
- Documented tg_temporal struct.
- Added tests.
---
doc/src/sgml/ref/delete.sgml | 46 ++
doc/src/sgml/ref/update.sgml | 47 +++
doc/src/sgml/trigger.sgml | 60 ++-
src/backend/commands/tablecmds.c | 1 +
src/backend/commands/trigger.c | 49 +++
src/backend/executor/execMain.c | 1 +
src/backend/executor/nodeModifyTable.c | 217 +++++++++-
src/backend/nodes/nodeFuncs.c | 24 ++
src/backend/optimizer/plan/createplan.c | 8 +-
src/backend/optimizer/plan/planner.c | 1 +
src/backend/optimizer/util/pathnode.c | 4 +-
src/backend/parser/analyze.c | 210 +++++++++-
src/backend/parser/gram.y | 47 ++-
src/backend/parser/parse_agg.c | 10 +
src/backend/parser/parse_collate.c | 1 +
src/backend/parser/parse_expr.c | 8 +
src/backend/parser/parse_func.c | 3 +
src/backend/parser/parse_merge.c | 2 +-
src/backend/rewrite/rewriteHandler.c | 40 ++
src/backend/tcop/utility.c | 2 +-
src/backend/utils/adt/Makefile | 1 +
src/backend/utils/adt/period.c | 56 +++
src/backend/utils/adt/rangetypes.c | 42 ++
src/backend/utils/cache/lsyscache.c | 26 ++
src/include/commands/trigger.h | 1 +
src/include/nodes/execnodes.h | 27 ++
src/include/nodes/parsenodes.h | 44 +-
src/include/nodes/pathnodes.h | 1 +
src/include/nodes/plannodes.h | 1 +
src/include/nodes/primnodes.h | 25 ++
src/include/optimizer/pathnode.h | 3 +-
src/include/parser/analyze.h | 3 +-
src/include/parser/kwlist.h | 1 +
src/include/parser/parse_node.h | 1 +
src/include/utils/lsyscache.h | 1 +
src/include/utils/period.h | 19 +
src/include/utils/rangetypes.h | 3 +
src/test/regress/expected/for_portion_of.out | 393 ++++++++++++++++++
src/test/regress/expected/privileges.out | 18 +
src/test/regress/expected/updatable_views.out | 32 ++
.../regress/expected/without_overlaps.out | 30 ++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/for_portion_of.sql | 307 ++++++++++++++
src/test/regress/sql/privileges.sql | 18 +
src/test/regress/sql/updatable_views.sql | 14 +
src/test/regress/sql/without_overlaps.sql | 14 +
46 files changed, 1812 insertions(+), 52 deletions(-)
create mode 100644 src/backend/utils/adt/period.c
create mode 100644 src/include/utils/period.h
create mode 100644 src/test/regress/expected/for_portion_of.out
create mode 100644 src/test/regress/sql/for_portion_of.sql
diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml
index 1b81b4e7d7..868cf0d1f9 100644
--- a/doc/src/sgml/ref/delete.sgml
+++ b/doc/src/sgml/ref/delete.sgml
@@ -23,6 +23,7 @@ PostgreSQL documentation
<synopsis>
[ WITH [ RECURSIVE ] <replaceable class="parameter">with_query</replaceable> [, ...] ]
DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">alias</replaceable> ]
+ [ FOR PORTION OF <replaceable class="parameter">range_or_period_name</replaceable> FROM <replaceable class="parameter">start_time</replaceable> TO <replaceable class="parameter">end_time</replaceable> ]
[ USING <replaceable class="parameter">from_item</replaceable> [, ...] ]
[ WHERE <replaceable class="parameter">condition</replaceable> | WHERE CURRENT OF <replaceable class="parameter">cursor_name</replaceable> ]
[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
@@ -54,6 +55,15 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
circumstances.
</para>
+ <para>
+ If the table has a <link linkend="ddl-periods-application-periods">temporal
+ primary key</link>, you may supply a
+ <literal>FOR PORTION OF</literal> clause, and your delete will only affect rows
+ that overlap the given interval. Furthermore, if a row's span extends outside
+ the <literal>FOR PORTION OF</literal> bounds, then new rows spanning the "cut
+ off" duration will be inserted to preserve the old values.
+ </para>
+
<para>
The optional <literal>RETURNING</literal> clause causes <command>DELETE</command>
to compute and return value(s) based on each row actually deleted.
@@ -116,6 +126,42 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="parameter">range_or_period_name</replaceable></term>
+ <listitem>
+ <para>
+ The range column or period to use when performing a temporal delete. This
+ must match the range or period used in the table's temporal primary key.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">start_time</replaceable></term>
+ <listitem>
+ <para>
+ The earliest time (inclusive) to change in a temporal delete.
+ This must be a value matching the base type of the range or period from
+ <replaceable class="parameter">range_or_period_name</replaceable>. It may also
+ be the special value <literal>MINVALUE</literal> to indicate a delete whose
+ beginning is unbounded.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">end_time</replaceable></term>
+ <listitem>
+ <para>
+ The latest time (exclusive) to change in a temporal delete.
+ This must be a value matching the base type of the range or period from
+ <replaceable class="parameter">range_or_period_name</replaceable>. It may also
+ be the special value <literal>MAXVALUE</literal> to indicate a delete whose
+ end is unbounded.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><replaceable class="parameter">from_item</replaceable></term>
<listitem>
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 2ab24b0523..f2042e0b25 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -23,6 +23,7 @@ PostgreSQL documentation
<synopsis>
[ WITH [ RECURSIVE ] <replaceable class="parameter">with_query</replaceable> [, ...] ]
UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">alias</replaceable> ]
+ [ FOR PORTION OF <replaceable class="parameter">range_or_period_name</replaceable> FROM <replaceable class="parameter">start_time</replaceable> TO <replaceable class="parameter">end_time</replaceable> ]
SET { <replaceable class="parameter">column_name</replaceable> = { <replaceable class="parameter">expression</replaceable> | DEFAULT } |
( <replaceable class="parameter">column_name</replaceable> [, ...] ) = [ ROW ] ( { <replaceable class="parameter">expression</replaceable> | DEFAULT } [, ...] ) |
( <replaceable class="parameter">column_name</replaceable> [, ...] ) = ( <replaceable class="parameter">sub-SELECT</replaceable> )
@@ -51,6 +52,16 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
circumstances.
</para>
+ <para>
+ If the table has a <link linkend="ddl-periods-application-periods">temporal
+ primary key</link>, you may supply a
+ <literal>FOR PORTION OF</literal> clause, and your update will only affect rows
+ that overlap the given interval. Furthermore, if a row's span extends outside
+ the <literal>FOR PORTION OF</literal> bounds, then it will be truncated to fit
+ within the bounds, and new rows spanning the "cut off" duration will be
+ inserted to preserve the old values.
+ </para>
+
<para>
The optional <literal>RETURNING</literal> clause causes <command>UPDATE</command>
to compute and return value(s) based on each row actually updated.
@@ -114,6 +125,42 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="parameter">range_or_period_name</replaceable></term>
+ <listitem>
+ <para>
+ The range column or period to use when performing a temporal update. This
+ must match the range or period used in the table's temporal primary key.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">start_time</replaceable></term>
+ <listitem>
+ <para>
+ The earliest time (inclusive) to change in a temporal update.
+ This must be a value matching the base type of the range or period from
+ <replaceable class="parameter">range_or_period_name</replaceable>. It may also
+ be the special value <literal>MINVALUE</literal> to indicate an update whose
+ beginning is unbounded.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">end_time</replaceable></term>
+ <listitem>
+ <para>
+ The latest time (exclusive) to change in a temporal update.
+ This must be a value matching the base type of the range or period from
+ <replaceable class="parameter">range_or_period_name</replaceable>. It may also
+ be the special value <literal>MAXVALUE</literal> to indicate an update whose
+ end is unbounded.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><replaceable class="parameter">column_name</replaceable></term>
<listitem>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index 6e1f370b21..b58e6348ad 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -537,17 +537,18 @@ CALLED_AS_TRIGGER(fcinfo)
<programlisting>
typedef struct TriggerData
{
- NodeTag type;
- TriggerEvent tg_event;
- Relation tg_relation;
- HeapTuple tg_trigtuple;
- HeapTuple tg_newtuple;
- Trigger *tg_trigger;
- TupleTableSlot *tg_trigslot;
- TupleTableSlot *tg_newslot;
- Tuplestorestate *tg_oldtable;
- Tuplestorestate *tg_newtable;
- const Bitmapset *tg_updatedcols;
+ NodeTag type;
+ TriggerEvent tg_event;
+ Relation tg_relation;
+ HeapTuple tg_trigtuple;
+ HeapTuple tg_newtuple;
+ Trigger *tg_trigger;
+ TupleTableSlot *tg_trigslot;
+ TupleTableSlot *tg_newslot;
+ Tuplestorestate *tg_oldtable;
+ Tuplestorestate *tg_newtable;
+ const Bitmapset *tg_updatedcols;
+ ForPortionOfState *tg_temporal;
} TriggerData;
</programlisting>
@@ -815,6 +816,43 @@ typedef struct Trigger
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><structfield>tg_temporal</structfield></term>
+ <listitem>
+ <para>
+ Set for <literal>UPDATE</literal> and <literal>DELETE</literal> queries
+ that use <literal>FOR PORTION OF</literal>, otherwise <symbol>NULL</symbol>.
+ Contains a pointer to a structure of type
+ <structname>ForPortionOfState</structname>, defined in
+ <filename>nodes/execnodes.h</filename>:
+
+<programlisting>
+typedef struct ForPortionOfState
+{
+ NodeTag type;
+
+ char *fp_rangeName; /* the column/PERIOD named in FOR PORTION OF */
+ Oid fp_rangeType; /* the type of the FOR PORTION OF expression */
+ bool fp_hasPeriod; /* true iff this is a PERIOD not a range */
+ char *fp_periodStartName; /* the PERIOD's start column */
+ char *fp_periodEndName; /* the PERIOD's end column */
+ Datum fp_targetRange; /* the range from FOR PORTION OF */
+} ForPortionOfState;
+</programlisting>
+
+ where <structfield>fp_rangeName</structfield> is the period or range
+ column named in the <literal>FOR PORTION OF</literal> clause,
+ <structfield>fp_rangeType</structfield> is its range type,
+ <structfield>fp_hasPeriod</structfield> indicates whether a period was used
+ or a range column, <structfield>fp_periodStartName</structfield> and
+ <structfield>fp_periodEndName</structfield> are the names of the period's
+ start and end columns (or <symbol>NULL</symbol> if a range column was used),
+ and <structfield>fp_targetRange</structfield> is a rangetype value created
+ by evaluating the <literal>FOR PORTION OF</literal> bounds.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index be1734e6ed..f819fe0fcb 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -11855,6 +11855,7 @@ validateForeignKeyConstraint(char *conname,
trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL);
trigdata.tg_trigslot = slot;
trigdata.tg_trigger = &trig;
+ trigdata.tg_temporal = NULL;
fcinfo->context = (Node *) &trigdata;
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 0577b60415..2f05557b34 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -54,6 +54,7 @@
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/bytea.h"
+#include "utils/datum.h"
#include "utils/fmgroids.h"
#include "utils/guc_hooks.h"
#include "utils/inval.h"
@@ -2638,6 +2639,7 @@ ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
TRIGGER_EVENT_BEFORE;
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -2737,6 +2739,7 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
TRIGGER_EVENT_ROW |
TRIGGER_EVENT_BEFORE;
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;
for (i = 0; i < trigdesc->numtriggers; i++)
{
HeapTuple newtuple;
@@ -2828,6 +2831,7 @@ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TRIGGER_EVENT_ROW |
TRIGGER_EVENT_INSTEAD;
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;
ExecForceStoreHeapTuple(trigtuple, slot, false);
@@ -2891,6 +2895,7 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
TRIGGER_EVENT_BEFORE;
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
LocTriggerData.tg_updatedcols = updatedCols;
+ LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -3008,6 +3013,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
TRIGGER_EVENT_ROW |
TRIGGER_EVENT_BEFORE;
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;
updatedCols = ExecGetAllUpdatedCols(relinfo, estate);
LocTriggerData.tg_updatedcols = updatedCols;
for (i = 0; i < trigdesc->numtriggers; i++)
@@ -3157,6 +3163,7 @@ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
TRIGGER_EVENT_ROW |
TRIGGER_EVENT_INSTEAD;
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;
ExecForceStoreHeapTuple(trigtuple, oldslot, false);
@@ -3623,6 +3630,7 @@ typedef struct AfterTriggerSharedData
Oid ats_tgoid; /* the trigger's ID */
Oid ats_relid; /* the relation it's on */
CommandId ats_firing_id; /* ID for firing cycle */
+ ForPortionOfState *for_portion_of; /* the FOR PORTION OF clause */
struct AfterTriggersTableData *ats_table; /* transition table access */
Bitmapset *ats_modifiedcols; /* modified columns */
} AfterTriggerSharedData;
@@ -3896,6 +3904,7 @@ static SetConstraintState SetConstraintStateCreate(int numalloc);
static SetConstraintState SetConstraintStateCopy(SetConstraintState origstate);
static SetConstraintState SetConstraintStateAddItem(SetConstraintState state,
Oid tgoid, bool tgisdeferred);
+static ForPortionOfState *CopyForPortionOfState(ForPortionOfState *src);
static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);
@@ -4105,6 +4114,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events,
newshared->ats_relid == evtshared->ats_relid &&
newshared->ats_event == evtshared->ats_event &&
newshared->ats_table == evtshared->ats_table &&
+ newshared->for_portion_of == evtshared->for_portion_of &&
newshared->ats_firing_id == 0)
break;
}
@@ -4469,6 +4479,7 @@ AfterTriggerExecute(EState *estate,
LocTriggerData.tg_relation = rel;
if (TRIGGER_FOR_UPDATE(LocTriggerData.tg_trigger->tgtype))
LocTriggerData.tg_updatedcols = evtshared->ats_modifiedcols;
+ LocTriggerData.tg_temporal = evtshared->for_portion_of;
MemoryContextReset(per_tuple_context);
@@ -6005,6 +6016,43 @@ AfterTriggerPendingOnRel(Oid relid)
return false;
}
+/* ----------
+ * ForPortionOfState()
+ *
+ * Copies a ForPortionOfState into the current memory context.
+ */
+static ForPortionOfState *
+CopyForPortionOfState(ForPortionOfState *src)
+{
+ ForPortionOfState *dst = NULL;
+ if (src) {
+ MemoryContext oldctx;
+ RangeType *r;
+ TypeCacheEntry *typcache;
+
+ /*
+ * Need to lift the FOR PORTION OF details into a higher memory context
+ * because cascading foreign key update/deletes can cause triggers to fire
+ * triggers, and the AfterTriggerEvents will outlive the FPO
+ * details of the original query.
+ */
+ oldctx = MemoryContextSwitchTo(TopTransactionContext);
+ dst = makeNode(ForPortionOfState);
+ dst->fp_rangeName = pstrdup(src->fp_rangeName);
+ dst->fp_rangeType = src->fp_rangeType;
+ dst->fp_hasPeriod = src->fp_hasPeriod;
+ dst->fp_rangeAttno = src->fp_rangeAttno;
+ dst->fp_periodStartAttno = src->fp_periodStartAttno;
+ dst->fp_periodEndAttno = src->fp_periodEndAttno;
+
+ r = DatumGetRangeTypeP(src->fp_targetRange);
+ typcache = lookup_type_cache(RangeTypeGetOid(r), TYPECACHE_RANGE_INFO);
+ dst->fp_targetRange = datumCopy(src->fp_targetRange, typcache->typbyval, typcache->typlen);
+ MemoryContextSwitchTo(oldctx);
+ }
+ return dst;
+}
+
/* ----------
* AfterTriggerSaveEvent()
*
@@ -6420,6 +6468,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
else
new_shared.ats_table = NULL;
new_shared.ats_modifiedcols = afterTriggerCopyBitmap(modifiedCols);
+ new_shared.for_portion_of = CopyForPortionOfState(relinfo->ri_forPortionOf);
afterTriggerAddEvent(&afterTriggers.query_stack[afterTriggers.query_depth].events,
&new_event, &new_shared);
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4c5a7bbf62..69096cbefb 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1274,6 +1274,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
resultRelInfo->ri_projectReturning = NULL;
resultRelInfo->ri_onConflictArbiterIndexes = NIL;
resultRelInfo->ri_onConflict = NULL;
+ resultRelInfo->ri_forPortionOf = NULL;
resultRelInfo->ri_ReturningSlot = NULL;
resultRelInfo->ri_TrigOldSlot = NULL;
resultRelInfo->ri_TrigNewSlot = NULL;
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 2a5fec8d01..9ee4514fb6 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -61,6 +61,9 @@
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/memutils.h"
+#include "utils/period.h"
+#include "utils/rangetypes.h"
+#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -141,6 +144,10 @@ static bool ExecOnConflictUpdate(ModifyTableContext *context,
TupleTableSlot *excludedSlot,
bool canSetTag,
TupleTableSlot **returning);
+static void ExecForPortionOfLeftovers(ModifyTableContext *context,
+ EState *estate,
+ ResultRelInfo *resultRelInfo,
+ ItemPointer tupleid);
static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
EState *estate,
PartitionTupleRouting *proute,
@@ -1205,6 +1212,140 @@ ExecInsert(ModifyTableContext *context,
return result;
}
+/* ----------------------------------------------------------------
+ * set_leftover_tuple_bounds
+ *
+ * Saves the new bounds into the range attribute
+ * ----------------------------------------------------------------
+ */
+static void set_leftover_tuple_bounds(TupleTableSlot *leftoverTuple,
+ ForPortionOfExpr *forPortionOf,
+ TypeCacheEntry *typcache,
+ RangeType *leftoverRangeType)
+{
+ /* Store the range directly */
+
+ leftoverTuple->tts_values[forPortionOf->rangeVar->varattno - 1] = RangeTypePGetDatum(leftoverRangeType);
+ leftoverTuple->tts_isnull[forPortionOf->rangeVar->varattno - 1] = false;
+}
+
+/* ----------------------------------------------------------------
+ * ExecForPortionOfLeftovers
+ *
+ * Insert tuples for the untouched timestamp of a row in a FOR
+ * PORTION OF UPDATE/DELETE
+ * ----------------------------------------------------------------
+ */
+static void
+ExecForPortionOfLeftovers(ModifyTableContext *context,
+ EState *estate,
+ ResultRelInfo *resultRelInfo,
+ ItemPointer tupleid)
+{
+ // TODO: figure out if I need to make a copy of the slot somehow in order to insert it...
+
+ ModifyTableState *mtstate = context->mtstate;
+ ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+ ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
+ Datum oldRange;
+ RangeType *oldRangeType;
+ RangeType *targetRangeType;
+ RangeType *leftoverRangeType1;
+ RangeType *leftoverRangeType2;
+ Oid rangeTypeOid = forPortionOf->rangeType;
+ bool isNull = false;
+ TypeCacheEntry *typcache;
+ TupleTableSlot *oldtupleSlot = resultRelInfo->ri_forPortionOf->fp_Existing;
+ TupleTableSlot *leftoverTuple1 = resultRelInfo->ri_forPortionOf->fp_Leftover1;
+ TupleTableSlot *leftoverTuple2 = resultRelInfo->ri_forPortionOf->fp_Leftover2;
+
+ /*
+ * Get the range of the old pre-UPDATE/DELETE tuple,
+ * so we can intersect it with the FOR PORTION OF target
+ * and see if there are any "leftovers" to insert.
+ *
+ * We have already locked the tuple in ExecUpdate/ExecDelete
+ * (TODO: if it was *not* concurrently updated, does table_tuple_update lock the tuple itself?
+ * I don't found the code for that yet, and maybe it depends on the AM?)
+ * and it has passed EvalPlanQual.
+ * Make sure we're looking at the most recent version.
+ * Otherwise concurrent updates of the same tuple in READ COMMITTED
+ * could insert conflicting "leftovers".
+ */
+ if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc, tupleid, SnapshotAny, oldtupleSlot))
+ elog(ERROR, "failed to fetch tuple for FOR PORTION OF");
+
+ oldRange = slot_getattr(oldtupleSlot, forPortionOf->rangeVar->varattno, &isNull);
+
+ if (isNull)
+ elog(ERROR, "found a NULL range in a temporal table");
+ oldRangeType = DatumGetRangeTypeP(oldRange);
+
+ /* Get the target range. */
+
+ targetRangeType = DatumGetRangeTypeP(resultRelInfo->ri_forPortionOf->fp_targetRange);
+
+ /*
+ * Get the range's type cache entry. This is worth caching for the whole UPDATE
+ * like range functions do.
+ */
+
+ typcache = resultRelInfo->ri_forPortionOf->fp_rangetypcache;
+ if (typcache == NULL)
+ {
+ typcache = lookup_type_cache(rangeTypeOid, TYPECACHE_RANGE_INFO);
+ if (typcache->rngelemtype == NULL)
+ elog(ERROR, "type %u is not a range type", rangeTypeOid);
+ resultRelInfo->ri_forPortionOf->fp_rangetypcache = typcache;
+ }
+
+ /* Get the ranges to the left/right of the targeted range. */
+
+ // TODO: set memory context?
+ range_leftover_internal(typcache, oldRangeType, targetRangeType, &leftoverRangeType1,
+ &leftoverRangeType2);
+
+ /*
+ * Insert a copy of the tuple with the lower leftover range.
+ * Even if the table is partitioned,
+ * our insert won't extend past the current row, so we don't need to re-route.
+ * TODO: Really? What if you update the partition key?
+ */
+
+ if (!RangeIsEmpty(leftoverRangeType1))
+ {
+ // TODO: anything we need to clear here?
+ // Are we in the row context?
+ HeapTuple oldtuple = ExecFetchSlotHeapTuple(oldtupleSlot, false, NULL);
+ ExecForceStoreHeapTuple(oldtuple, leftoverTuple1, false);
+
+ set_leftover_tuple_bounds(leftoverTuple1, forPortionOf, typcache, leftoverRangeType1);
+ ExecMaterializeSlot(leftoverTuple1);
+
+ // TODO: Need to save context->mtstate->mt_transition_capture? (See comment on ExecInsert)
+ ExecInsert(context, resultRelInfo, leftoverTuple1, node->canSetTag, NULL, NULL);
+ }
+
+ /*
+ * Insert a copy of the tuple with the upper leftover range
+ * Even if the table is partitioned,
+ * our insert won't extend past the current row, so we don't need to re-route.
+ * TODO: Really? What if you update the partition key?
+ */
+
+ if (!RangeIsEmpty(leftoverRangeType2))
+ {
+ HeapTuple oldtuple = ExecFetchSlotHeapTuple(oldtupleSlot, false, NULL);
+ ExecForceStoreHeapTuple(oldtuple, leftoverTuple2, false);
+
+ set_leftover_tuple_bounds(leftoverTuple2, forPortionOf, typcache, leftoverRangeType2);
+ ExecMaterializeSlot(leftoverTuple2);
+
+ // TODO: Need to save context->mtstate->mt_transition_capture? (See comment on ExecInsert)
+ ExecInsert(context, resultRelInfo, leftoverTuple2, node->canSetTag, NULL, NULL);
+ }
+}
+
/* ----------------------------------------------------------------
* ExecBatchInsert
*
@@ -1357,11 +1498,13 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
*
* Closing steps of tuple deletion; this invokes AFTER FOR EACH ROW triggers,
* including the UPDATE triggers if the deletion is being done as part of a
- * cross-partition tuple move.
+ * cross-partition tuple move. It also inserts leftovers from a FOR PORTION OF
+ * delete.
*/
static void
ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
- ItemPointer tupleid, HeapTuple oldtuple, bool changingPart)
+ ItemPointer tupleid, HeapTuple oldtuple, bool changingPart,
+ ForPortionOfExpr *forPortionOf)
{
ModifyTableState *mtstate = context->mtstate;
EState *estate = context->estate;
@@ -1390,6 +1533,11 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ar_delete_trig_tcs = NULL;
}
+ /* Compute leftovers in FOR PORTION OF */
+ // TODO: Skip this for FDW deletes?
+ if (forPortionOf)
+ ExecForPortionOfLeftovers(context, estate, resultRelInfo, tupleid);
+
/* AFTER ROW DELETE Triggers */
ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs, changingPart);
@@ -1661,7 +1809,8 @@ ldelete:
if (tupleDeleted)
*tupleDeleted = true;
- ExecDeleteEpilogue(context, resultRelInfo, tupleid, oldtuple, changingPart);
+ ExecDeleteEpilogue(context, resultRelInfo, tupleid, oldtuple, changingPart,
+ (ForPortionOfExpr *)((ModifyTable *) context->mtstate->ps.plan)->forPortionOf);
/* Process RETURNING if present and if requested */
if (processReturning && resultRelInfo->ri_projectReturning)
@@ -2120,7 +2269,8 @@ lreplace:
static void
ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
ResultRelInfo *resultRelInfo, ItemPointer tupleid,
- HeapTuple oldtuple, TupleTableSlot *slot)
+ HeapTuple oldtuple, TupleTableSlot *slot,
+ ForPortionOfExpr *forPortionOf)
{
ModifyTableState *mtstate = context->mtstate;
List *recheckIndexes = NIL;
@@ -2133,6 +2283,11 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
NULL, NIL,
(updateCxt->updateIndexes == TU_Summarizing));
+ /* Compute leftovers in FOR PORTION OF */
+ // TODO: Skip this for FDW updates?
+ if (forPortionOf)
+ ExecForPortionOfLeftovers(context, context->estate, resultRelInfo, tupleid);
+
/* AFTER ROW UPDATE Triggers */
ExecARUpdateTriggers(context->estate, resultRelInfo,
NULL, NULL,
@@ -2476,7 +2631,8 @@ redo_act:
(estate->es_processed)++;
ExecUpdateEpilogue(context, &updateCxt, resultRelInfo, tupleid, oldtuple,
- slot);
+ slot,
+ (ForPortionOfExpr *) ((ModifyTable *) context->mtstate->ps.plan)->forPortionOf);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
@@ -2901,7 +3057,7 @@ lmerge_matched:
if (result == TM_Ok && updateCxt.updated)
{
ExecUpdateEpilogue(context, &updateCxt, resultRelInfo,
- tupleid, NULL, newslot);
+ tupleid, NULL, newslot, NULL);
mtstate->mt_merge_updated += 1;
}
break;
@@ -2919,7 +3075,7 @@ lmerge_matched:
if (result == TM_Ok)
{
ExecDeleteEpilogue(context, resultRelInfo, tupleid, NULL,
- false);
+ false, NULL);
mtstate->mt_merge_deleted += 1;
}
break;
@@ -3609,6 +3765,7 @@ ExecModifyTable(PlanState *pstate)
* Reset per-tuple memory context used for processing on conflict and
* returning clauses, to free any expression evaluation storage
* allocated in the previous cycle.
+ * TODO: It sounds like FOR PORTION OF might need to do something here too?
*/
if (pstate->ps_ExprContext)
ResetExprContext(pstate->ps_ExprContext);
@@ -4281,6 +4438,52 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
}
}
+ /*
+ * If needed, initialize the target range for FOR PORTION OF.
+ */
+ if (node->forPortionOf)
+ {
+ TupleDesc tupDesc = resultRelInfo->ri_RelationDesc->rd_att;
+ ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
+ Datum targetRange;
+ bool isNull;
+ ExprContext *econtext;
+ ExprState *exprState;
+
+ /* Eval the FOR PORTION OF target */
+ if (mtstate->ps.ps_ExprContext == NULL)
+ ExecAssignExprContext(estate, &mtstate->ps);
+ econtext = mtstate->ps.ps_ExprContext;
+
+ exprState = ExecPrepareExpr((Expr *) forPortionOf->targetRange, estate);
+ targetRange = ExecEvalExpr(exprState, econtext, &isNull);
+ if (isNull)
+ elog(ERROR, "Got a NULL FOR PORTION OF target range");
+
+ /* Create state for FOR PORTION OF operation */
+
+ resultRelInfo->ri_forPortionOf = makeNode(ForPortionOfState);
+ resultRelInfo->ri_forPortionOf->fp_rangeName = forPortionOf->range_name;
+ resultRelInfo->ri_forPortionOf->fp_rangeType = forPortionOf->rangeType;
+ resultRelInfo->ri_forPortionOf->fp_rangeAttno = forPortionOf->rangeVar->varattno;
+ resultRelInfo->ri_forPortionOf->fp_targetRange = targetRange;
+
+ /* Initialize slot for the existing tuple */
+
+ resultRelInfo->ri_forPortionOf->fp_Existing =
+ table_slot_create(resultRelInfo->ri_RelationDesc,
+ &mtstate->ps.state->es_tupleTable);
+
+ /* Create the tuple slots for INSERTing the leftovers */
+
+ resultRelInfo->ri_forPortionOf->fp_Leftover1 =
+ ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc, &TTSOpsVirtual);
+ resultRelInfo->ri_forPortionOf->fp_Leftover2 =
+ ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc, &TTSOpsVirtual);
+
+ /* Don't free the ExprContext here because the result must last for the whole query */
+ }
+
/*
* If we have any secondary relations in an UPDATE or DELETE, they need to
* be treated like non-locked relations in SELECT FOR UPDATE, ie, the
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index c41e6bb984..fd3349832d 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2444,6 +2444,14 @@ expression_tree_walker_impl(Node *node,
return true;
}
break;
+ case T_ForPortionOfExpr:
+ {
+ ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node;
+
+ if (WALK(forPortionOf->targetRange))
+ return true;
+ }
+ break;
case T_PartitionPruneStepOp:
{
PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node;
@@ -2584,6 +2592,8 @@ query_tree_walker_impl(Query *query,
return true;
if (WALK(query->mergeActionList))
return true;
+ if (WALK(query->forPortionOf))
+ return true;
if (WALK(query->returningList))
return true;
if (WALK(query->jointree))
@@ -3427,6 +3437,19 @@ expression_tree_mutator_impl(Node *node,
return (Node *) newnode;
}
break;
+ case T_ForPortionOfExpr:
+ {
+ ForPortionOfExpr *fpo = (ForPortionOfExpr *) node;
+ ForPortionOfExpr *newnode;
+
+ FLATCOPY(newnode, fpo, ForPortionOfExpr);
+ MUTATE(newnode->rangeVar, fpo->rangeVar, Var *);
+ MUTATE(newnode->targetRange, fpo->targetRange, Node *);
+ MUTATE(newnode->rangeSet, fpo->rangeSet, List *);
+
+ return (Node *) newnode;
+ }
+ break;
case T_PartitionPruneStepOp:
{
PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node;
@@ -3605,6 +3628,7 @@ query_tree_mutator_impl(Query *query,
MUTATE(query->withCheckOptions, query->withCheckOptions, List *);
MUTATE(query->onConflict, query->onConflict, OnConflictExpr *);
MUTATE(query->mergeActionList, query->mergeActionList, List *);
+ MUTATE(query->forPortionOf, query->forPortionOf, ForPortionOfExpr *);
MUTATE(query->returningList, query->returningList, List *);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ec73789bc2..7e5bf63c18 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -311,7 +311,8 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
List *updateColnosLists,
List *withCheckOptionLists, List *returningLists,
List *rowMarks, OnConflictExpr *onconflict,
- List *mergeActionLists, int epqParam);
+ List *mergeActionLists, ForPortionOfExpr *forPortionOf,
+ int epqParam);
static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
GatherMergePath *best_path);
@@ -2816,6 +2817,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
best_path->rowMarks,
best_path->onconflict,
best_path->mergeActionLists,
+ best_path->forPortionOf,
best_path->epqParam);
copy_generic_path_info(&plan->plan, &best_path->path);
@@ -6979,7 +6981,8 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
List *updateColnosLists,
List *withCheckOptionLists, List *returningLists,
List *rowMarks, OnConflictExpr *onconflict,
- List *mergeActionLists, int epqParam)
+ List *mergeActionLists, ForPortionOfExpr *forPortionOf,
+ int epqParam)
{
ModifyTable *node = makeNode(ModifyTable);
List *fdw_private_list;
@@ -7045,6 +7048,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
node->exclRelTlist = onconflict->exclRelTlist;
}
node->updateColnosLists = updateColnosLists;
+ node->forPortionOf = (Node *) forPortionOf;
node->withCheckOptionLists = withCheckOptionLists;
node->returningLists = returningLists;
node->rowMarks = rowMarks;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 0e12fdeb60..0fec5be7ff 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1949,6 +1949,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction)
rowMarks,
parse->onConflict,
mergeActionLists,
+ parse->forPortionOf,
assign_special_exec_param(root));
}
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 5f5596841c..e7afd5586c 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3653,7 +3653,8 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
List *updateColnosLists,
List *withCheckOptionLists, List *returningLists,
List *rowMarks, OnConflictExpr *onconflict,
- List *mergeActionLists, int epqParam)
+ List *mergeActionLists, ForPortionOfExpr *forPortionOf,
+ int epqParam)
{
ModifyTablePath *pathnode = makeNode(ModifyTablePath);
@@ -3719,6 +3720,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
pathnode->returningLists = returningLists;
pathnode->rowMarks = rowMarks;
pathnode->onconflict = onconflict;
+ pathnode->forPortionOf = forPortionOf;
pathnode->epqParam = epqParam;
pathnode->mergeActionLists = mergeActionLists;
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 4006632092..0b2109d1bb 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -47,11 +47,13 @@
#include "parser/parse_relation.h"
#include "parser/parse_target.h"
#include "parser/parse_type.h"
+#include "parser/parser.h"
#include "parser/parsetree.h"
#include "rewrite/rewriteManip.h"
#include "utils/backend_status.h"
#include "utils/builtins.h"
#include "utils/guc.h"
+#include "utils/lsyscache.h"
#include "utils/rel.h"
#include "utils/syscache.h"
@@ -60,10 +62,17 @@
post_parse_analyze_hook_type post_parse_analyze_hook = NULL;
static Query *transformOptionalSelectInto(ParseState *pstate, Node *parseTree);
+static Node *addForPortionOfWhereConditions(Query *qry, ForPortionOfClause *forPortionOf,
+ Node *whereClause);
static Query *transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt);
static Query *transformInsertStmt(ParseState *pstate, InsertStmt *stmt);
static OnConflictExpr *transformOnConflictClause(ParseState *pstate,
OnConflictClause *onConflictClause);
+static Node *transformForPortionOfBound(Node *n, bool isLowerBound);
+static ForPortionOfExpr *transformForPortionOfClause(ParseState *pstate,
+ int rtindex,
+ ForPortionOfClause *forPortionOfClause,
+ bool isUpdate);
static int count_rowexpr_columns(ParseState *pstate, Node *expr);
static Query *transformSelectStmt(ParseState *pstate, SelectStmt *stmt);
static Query *transformValuesClause(ParseState *pstate, SelectStmt *stmt);
@@ -465,6 +474,20 @@ analyze_requires_snapshot(RawStmt *parseTree)
return result;
}
+static Node *
+addForPortionOfWhereConditions(Query *qry, ForPortionOfClause *forPortionOf, Node *whereClause)
+{
+ if (forPortionOf)
+ {
+ if (whereClause)
+ return (Node *) makeBoolExpr(AND_EXPR, list_make2(qry->forPortionOf->overlapsExpr, whereClause), -1);
+ else
+ return qry->forPortionOf->overlapsExpr;
+ }
+ else
+ return whereClause;
+}
+
/*
* transformDeleteStmt -
* transforms a Delete Statement
@@ -474,6 +497,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
{
Query *qry = makeNode(Query);
ParseNamespaceItem *nsitem;
+ Node *whereClause;
Node *qual;
qry->commandType = CMD_DELETE;
@@ -512,7 +536,11 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
nsitem->p_lateral_only = false;
nsitem->p_lateral_ok = true;
- qual = transformWhereClause(pstate, stmt->whereClause,
+ if (stmt->forPortionOf)
+ qry->forPortionOf = transformForPortionOfClause(pstate, qry->resultRelation, stmt->forPortionOf, false);
+
+ whereClause = addForPortionOfWhereConditions(qry, stmt->forPortionOf, stmt->whereClause);
+ qual = transformWhereClause(pstate, whereClause,
EXPR_KIND_WHERE, "WHERE");
qry->returningList = transformReturningList(pstate, stmt->returningList);
@@ -1135,7 +1163,7 @@ transformOnConflictClause(ParseState *pstate,
* Now transform the UPDATE subexpressions.
*/
onConflictSet =
- transformUpdateTargetList(pstate, onConflictClause->targetList);
+ transformUpdateTargetList(pstate, onConflictClause->targetList, NULL);
onConflictWhere = transformWhereClause(pstate,
onConflictClause->whereClause,
@@ -1165,6 +1193,157 @@ transformOnConflictClause(ParseState *pstate,
return result;
}
+/*
+ * transformForPortionOfBound
+ * transforms UNBOUNDED pseudo-column references to NULL
+ * (which represent "unbounded" in a range type, otherwise returns
+ * its input unchanged.
+ */
+static Node *
+transformForPortionOfBound(Node *n, bool isLowerBound)
+{
+ if (nodeTag(n) == T_ColumnRef)
+ {
+ ColumnRef *cref = (ColumnRef *) n;
+ char *cname = "";
+ A_Const *n2;
+
+ if (list_length(cref->fields) == 1 &&
+ IsA(linitial(cref->fields), String))
+ cname = strVal(linitial(cref->fields));
+
+ if (strcmp("unbounded", cname) != 0)
+ return n;
+
+ n2 = makeNode(A_Const);
+ n2->isnull = true;
+ n2->location = ((ColumnRef *)n)->location;
+
+ return (Node *)n2;
+ }
+ else
+ return n;
+}
+
+/*
+ * transformForPortionOfClause
+ *
+ * Transforms a ForPortionOfClause in an UPDATE/DELETE statement.
+ *
+ * - Look up the range/period requested.
+ * - Build a compatible range value from the FROM and TO expressions.
+ * - Build an "overlaps" expression for filtering.
+ * - For UPDATEs, build an "intersects" expression the rewriter can add
+ * - to the targetList to change the temporal bounds.
+ */
+static ForPortionOfExpr *
+transformForPortionOfClause(ParseState *pstate,
+ int rtindex,
+ ForPortionOfClause *forPortionOf,
+ bool isUpdate)
+{
+ Relation targetrel = pstate->p_target_relation;
+ RTEPermissionInfo *target_perminfo = pstate->p_target_nsitem->p_perminfo;
+ char *range_name = forPortionOf->range_name;
+ char *range_type_name = NULL;
+ int range_attno = InvalidAttrNumber;
+ ForPortionOfExpr *result;
+ List *targetList;
+ Node *target_start, *target_end;
+ Var *rangeVar;
+ FuncCall *fc;
+
+ result = makeNode(ForPortionOfExpr);
+
+ /* Look up the FOR PORTION OF name requested. */
+ range_attno = attnameAttNum(targetrel, range_name, false);
+ if (range_attno == InvalidAttrNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column or period \"%s\" of relation \"%s\" does not exist",
+ range_name,
+ RelationGetRelationName(targetrel)),
+ parser_errposition(pstate, forPortionOf->range_name_location)));
+ Form_pg_attribute attr = TupleDescAttr(targetrel->rd_att, range_attno - 1);
+ // TODO: check attr->attisdropped (?),
+ // and figure out concurrency issues with that in general.
+ // It should work the same as updating any other column.
+
+ /* Make sure it's a range column */
+ if (!type_is_range(attr->atttypid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+ errmsg("column \"%s\" of relation \"%s\" is not a range type",
+ range_name,
+ RelationGetRelationName(targetrel)),
+ parser_errposition(pstate, forPortionOf->range_name_location)));
+
+ rangeVar = makeVar(
+ rtindex,
+ range_attno,
+ attr->atttypid,
+ attr->atttypmod,
+ attr->attcollation,
+ 0);
+ rangeVar->location = forPortionOf->range_name_location;
+ result->rangeVar = rangeVar;
+ result->rangeType = attr->atttypid;
+ range_type_name = get_typname(attr->atttypid);
+
+
+ /*
+ * Build a range from the FROM ... TO .... bounds.
+ * This should give a constant result, so we accept functions like NOW()
+ * but not column references, subqueries, etc.
+ *
+ * It also permits UNBOUNDED in either place.
+ */
+ target_start = transformForPortionOfBound(forPortionOf->target_start, true);
+ target_end = transformForPortionOfBound(forPortionOf->target_end, false);
+ fc = makeFuncCall(SystemFuncName(range_type_name),
+ list_make2(target_start, target_end),
+ COERCE_EXPLICIT_CALL,
+ forPortionOf->range_name_location);
+ result->targetRange = transformExpr(pstate, (Node *) fc, EXPR_KIND_UPDATE_PORTION);
+
+ /* overlapsExpr is something we can add to the whereClause */
+ result->overlapsExpr = (Node *) makeSimpleA_Expr(AEXPR_OP, "&&",
+ (Node *) copyObject(rangeVar), (Node *) fc,
+ forPortionOf->range_name_location);
+
+ if (isUpdate)
+ {
+ /*
+ * Now make sure we update the start/end time of the record.
+ * For a range col (r) this is `r = r * targetRange`.
+ */
+ targetList = NIL;
+ Expr *rangeSetExpr = (Expr *) makeSimpleA_Expr(AEXPR_OP, "*",
+ (Node *) copyObject(rangeVar), (Node *) fc,
+ forPortionOf->range_name_location);
+ TargetEntry *tle;
+
+ rangeSetExpr = (Expr *) transformExpr(pstate, (Node *) rangeSetExpr, EXPR_KIND_UPDATE_PORTION);
+ tle = makeTargetEntry(rangeSetExpr,
+ range_attno,
+ range_name,
+ false);
+
+ targetList = lappend(targetList, tle);
+
+ /* Mark the range column as requiring update permissions */
+ target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
+ range_attno - FirstLowInvalidHeapAttributeNumber);
+
+ result->rangeSet = targetList;
+ }
+ else
+ result->rangeSet = NIL;
+
+ result->range_name = range_name;
+
+ return result;
+}
/*
* BuildOnConflictExcludedTargetlist
@@ -2371,6 +2550,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
{
Query *qry = makeNode(Query);
ParseNamespaceItem *nsitem;
+ Node *whereClause;
Node *qual;
qry->commandType = CMD_UPDATE;
@@ -2388,6 +2568,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
stmt->relation->inh,
true,
ACL_UPDATE);
+
+ if (stmt->forPortionOf)
+ qry->forPortionOf = transformForPortionOfClause(pstate, qry->resultRelation, stmt->forPortionOf, true);
+
nsitem = pstate->p_target_nsitem;
/* subqueries in FROM cannot access the result relation */
@@ -2404,7 +2588,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
nsitem->p_lateral_only = false;
nsitem->p_lateral_ok = true;
- qual = transformWhereClause(pstate, stmt->whereClause,
+ whereClause = addForPortionOfWhereConditions(qry, stmt->forPortionOf, stmt->whereClause);
+ qual = transformWhereClause(pstate, whereClause,
EXPR_KIND_WHERE, "WHERE");
qry->returningList = transformReturningList(pstate, stmt->returningList);
@@ -2413,7 +2598,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
* Now we are done with SELECT-like processing, and can get on with
* transforming the target list to match the UPDATE target columns.
*/
- qry->targetList = transformUpdateTargetList(pstate, stmt->targetList);
+ qry->targetList = transformUpdateTargetList(pstate, stmt->targetList, qry->forPortionOf);
qry->rtable = pstate->p_rtable;
qry->rteperminfos = pstate->p_rteperminfos;
@@ -2432,7 +2617,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
* handle SET clause in UPDATE/MERGE/INSERT ... ON CONFLICT UPDATE
*/
List *
-transformUpdateTargetList(ParseState *pstate, List *origTlist)
+transformUpdateTargetList(ParseState *pstate, List *origTlist, ForPortionOfExpr *forPortionOf)
{
List *tlist = NIL;
RTEPermissionInfo *target_perminfo;
@@ -2482,6 +2667,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
RelationGetRelationName(pstate->p_target_relation)),
parser_errposition(pstate, origTarget->location)));
+ /*
+ * If this is a FOR PORTION OF update,
+ * forbid directly setting the range column,
+ * since that would conflict with the implicit updates.
+ */
+ if (forPortionOf != NULL)
+ {
+ if (attrno == forPortionOf->rangeVar->varattno)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("can't directly assign to \"%s\" in a FOR PORTION OF update",
+ origTarget->name),
+ parser_errposition(pstate, origTarget->location)));
+ }
+
updateTargetListEntry(pstate, tle, origTarget->name,
attrno,
origTarget->indirection,
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 612813832c..c3ab65896f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -258,6 +258,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
RangeVar *range;
IntoClause *into;
WithClause *with;
+ ForPortionOfClause *forportionof;
InferClause *infer;
OnConflictClause *onconflict;
A_Indices *aind;
@@ -551,6 +552,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <range> relation_expr
%type <range> extended_relation_expr
%type <range> relation_expr_opt_alias
+%type <forportionof> for_portion_of_clause
%type <node> tablesample_clause opt_repeatable_clause
%type <target> target_el set_target insert_column_item
@@ -749,7 +751,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD
PERIOD PLACING PLANS POLICY
- POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
+ PORTION POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
QUOTE
@@ -830,6 +832,16 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%nonassoc KEYS OBJECT_P SCALAR VALUE_P
%nonassoc WITH WITHOUT
+/*
+ * We need to handle this shift/reduce conflict:
+ * FOR PORTION OF valid_at FROM INTERVAL YEAR TO MONTH TO foo.
+ * This is basically the classic "dangling else" problem, and we want a
+ * similar resolution: treat the TO as part of the INTERVAL, not as part of
+ * the FROM ... TO .... Users can add parentheses if that's a problem.
+ * TO just needs to be higher precedence than YEAR_P etc.
+ */
+%nonassoc YEAR_P MONTH_P DAY_P HOUR_P MINUTE_P
+%nonassoc TO
/*
* To support target_el without AS, it used to be necessary to assign IDENT an
* explicit precedence just less than Op. While that's not really necessary
@@ -12122,14 +12134,16 @@ returning_clause:
*****************************************************************************/
DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias
+ for_portion_of_clause
using_clause where_or_current_clause returning_clause
{
DeleteStmt *n = makeNode(DeleteStmt);
n->relation = $4;
- n->usingClause = $5;
- n->whereClause = $6;
- n->returningList = $7;
+ n->forPortionOf = $5;
+ n->usingClause = $6;
+ n->whereClause = $7;
+ n->returningList = $8;
n->withClause = $1;
$$ = (Node *) n;
}
@@ -12192,6 +12206,7 @@ opt_nowait_or_skip:
*****************************************************************************/
UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
+ for_portion_of_clause
SET set_clause_list
from_clause
where_or_current_clause
@@ -12200,10 +12215,11 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
UpdateStmt *n = makeNode(UpdateStmt);
n->relation = $3;
- n->targetList = $5;
- n->fromClause = $6;
- n->whereClause = $7;
- n->returningList = $8;
+ n->forPortionOf = $4;
+ n->targetList = $6;
+ n->fromClause = $7;
+ n->whereClause = $8;
+ n->returningList = $9;
n->withClause = $1;
$$ = (Node *) n;
}
@@ -13639,6 +13655,19 @@ relation_expr_opt_alias: relation_expr %prec UMINUS
}
;
+for_portion_of_clause:
+ FOR PORTION OF ColId FROM a_expr TO a_expr
+ {
+ ForPortionOfClause *n = makeNode(ForPortionOfClause);
+ n->range_name = $4;
+ n->range_name_location = @4;
+ n->target_start = $6;
+ n->target_end = $8;
+ $$ = n;
+ }
+ | /*EMPTY*/ { $$ = NULL; }
+ ;
+
/*
* TABLESAMPLE decoration in a FROM item
*/
@@ -17155,6 +17184,7 @@ unreserved_keyword:
| PASSWORD
| PLANS
| POLICY
+ | PORTION
| PRECEDING
| PREPARE
| PREPARED
@@ -17754,6 +17784,7 @@ bare_label_keyword:
| PLACING
| PLANS
| POLICY
+ | PORTION
| POSITION
| PRECEDING
| PREPARE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 85cd47b7ae..ce8e0be045 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -563,6 +563,13 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
case EXPR_KIND_CYCLE_MARK:
errkind = true;
break;
+ case EXPR_KIND_UPDATE_PORTION:
+ if (isAgg)
+ err = _("aggregate functions are not allowed in FOR PORTION OF expressions");
+ else
+ err = _("grouping operations are not allowed in FOR PORTION OF expressions");
+
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -953,6 +960,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_CYCLE_MARK:
errkind = true;
break;
+ case EXPR_KIND_UPDATE_PORTION:
+ err = _("window functions are not allowed in FOR PORTION OF expressions");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c
index 9f6afc351c..d09b2d2b1c 100644
--- a/src/backend/parser/parse_collate.c
+++ b/src/backend/parser/parse_collate.c
@@ -484,6 +484,7 @@ assign_collations_walker(Node *node, assign_collations_context *context)
case T_JoinExpr:
case T_FromExpr:
case T_OnConflictExpr:
+ case T_ForPortionOfExpr:
case T_SortGroupClause:
case T_MergeAction:
(void) expression_tree_walker(node,
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 346fd272b6..4fe13b1365 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -550,6 +550,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
case EXPR_KIND_PARTITION_BOUND:
err = _("cannot use column reference in partition bound expression");
break;
+ case EXPR_KIND_UPDATE_PORTION:
+ err = _("cannot use column reference in FOR PORTION OF expression");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -1797,6 +1800,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
case EXPR_KIND_GENERATED_COLUMN:
err = _("cannot use subquery in column generation expression");
break;
+ case EXPR_KIND_UPDATE_PORTION:
+ err = _("cannot use subquery in FOR PORTION OF expression");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -3088,6 +3094,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "UPDATE";
case EXPR_KIND_MERGE_WHEN:
return "MERGE WHEN";
+ case EXPR_KIND_UPDATE_PORTION:
+ return "FOR PORTION OF";
case EXPR_KIND_GROUP_BY:
return "GROUP BY";
case EXPR_KIND_ORDER_BY:
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index b3f0b6a137..31ed01a84e 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2656,6 +2656,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
case EXPR_KIND_CYCLE_MARK:
errkind = true;
break;
+ case EXPR_KIND_UPDATE_PORTION:
+ err = _("set-returning functions are not allowed in FOR PORTION OF expressions");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c
index 91b1156d99..c00802ae23 100644
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -372,7 +372,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
pstate->p_is_insert = false;
action->targetList =
transformUpdateTargetList(pstate,
- mergeWhenClause->targetList);
+ mergeWhenClause->targetList, NULL);
}
break;
case CMD_DELETE:
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index b486ab559a..576a98e0f4 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3473,6 +3473,30 @@ rewriteTargetView(Query *parsetree, Relation view)
&parsetree->hasSubLinks);
}
+ if (parsetree->forPortionOf && parsetree->commandType == CMD_UPDATE)
+ {
+ /*
+ * Like the INSERT/UPDATE code above, update the resnos in the
+ * auxiliary UPDATE targetlist to refer to columns of the base
+ * relation.
+ */
+ foreach(lc, parsetree->forPortionOf->rangeSet)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ TargetEntry *view_tle;
+
+ if (tle->resjunk)
+ continue;
+
+ view_tle = get_tle_by_resno(view_targetlist, tle->resno);
+ if (view_tle != NULL && !view_tle->resjunk && IsA(view_tle->expr, Var))
+ tle->resno = ((Var *) view_tle->expr)->varattno;
+ else
+ elog(ERROR, "attribute number %d not found in view targetlist",
+ tle->resno);
+ }
+ }
+
/*
* For UPDATE/DELETE, pull up any WHERE quals from the view. We know that
* any Vars in the quals must reference the one base relation, so we need
@@ -3811,6 +3835,22 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
else if (event == CMD_UPDATE)
{
Assert(parsetree->override == OVERRIDING_NOT_SET);
+ /*
+ * Update FOR PORTION OF column(s) automatically. Don't
+ * do this until we're done rewriting a view update, so
+ * that we don't add the same update on the recursion.
+ */
+ if (parsetree->forPortionOf &&
+ rt_entry_relation->rd_rel->relkind != RELKIND_VIEW)
+ {
+ ListCell *tl;
+ foreach(tl, parsetree->forPortionOf->rangeSet)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(tl);
+ parsetree->targetList = lappend(parsetree->targetList, tle);
+ }
+ }
+
parsetree->targetList =
rewriteTargetListIU(parsetree->targetList,
parsetree->commandType,
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 30b51bf4d3..7e6b579f64 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1559,7 +1559,7 @@ ProcessUtilitySlow(ParseState *pstate,
true, /* check_rights */
true, /* check_not_in_use */
false, /* skip_build */
- false); /* quiet */
+ false); /* quiet */
/*
* Add the CREATE INDEX node itself to stash right away;
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 0de0bbb1b8..a4f226ec8d 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -78,6 +78,7 @@ OBJS = \
oracle_compat.o \
orderedsetaggs.o \
partitionfuncs.o \
+ period.o \
pg_locale.o \
pg_lsn.o \
pg_upgrade_support.o \
diff --git a/src/backend/utils/adt/period.c b/src/backend/utils/adt/period.c
new file mode 100644
index 0000000000..0ed4304e16
--- /dev/null
+++ b/src/backend/utils/adt/period.c
@@ -0,0 +1,56 @@
+/*-------------------------------------------------------------------------
+ *
+ * period.c
+ * Functions to support periods.
+ *
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/adt/period.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "executor/tuptable.h"
+#include "fmgr.h"
+#include "nodes/primnodes.h"
+#include "utils/fmgrprotos.h"
+#include "utils/period.h"
+#include "utils/rangetypes.h"
+
+Datum period_to_range(TupleTableSlot *slot, int startattno, int endattno, Oid rangetype)
+{
+ Datum startvalue;
+ Datum endvalue;
+ Datum result;
+ bool startisnull;
+ bool endisnull;
+ LOCAL_FCINFO(fcinfo, 2);
+ FmgrInfo flinfo;
+ FuncExpr *f;
+
+ InitFunctionCallInfoData(*fcinfo, &flinfo, 2, InvalidOid, NULL, NULL);
+ f = makeNode(FuncExpr);
+ f->funcresulttype = rangetype;
+ flinfo.fn_expr = (Node *) f;
+ flinfo.fn_extra = NULL;
+
+ /* compute oldvalue */
+ startvalue = slot_getattr(slot, startattno, &startisnull);
+ endvalue = slot_getattr(slot, endattno, &endisnull);
+
+ fcinfo->args[0].value = startvalue;
+ fcinfo->args[0].isnull = startisnull;
+ fcinfo->args[1].value = endvalue;
+ fcinfo->args[1].isnull = endisnull;
+
+ result = range_constructor2(fcinfo);
+ if (fcinfo->isnull)
+ elog(ERROR, "function %u returned NULL", flinfo.fn_oid);
+
+ return result;
+}
diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index d65e5625c7..d4e1732377 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -1206,6 +1206,48 @@ range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeT
return false;
}
+/*
+ * range_leftover_internal - Sets output1 and output2 to the remaining parts of r1
+ * after subtracting r2, or if nothing is left then to the empty range.
+ * output1 will always be "before" r2 and output2 "after".
+ */
+void
+range_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1,
+ const RangeType *r2, RangeType **output1, RangeType **output2)
+{
+ RangeBound lower1,
+ lower2;
+ RangeBound upper1,
+ upper2;
+ bool empty1,
+ empty2;
+
+ range_deserialize(typcache, r1, &lower1, &upper1, &empty1);
+ range_deserialize(typcache, r2, &lower2, &upper2, &empty2);
+
+ if (range_cmp_bounds(typcache, &lower1, &lower2) < 0)
+ {
+ lower2.inclusive = !lower2.inclusive;
+ lower2.lower = false;
+ *output1 = make_range(typcache, &lower1, &lower2, false, NULL);
+ }
+ else
+ {
+ *output1 = make_empty_range(typcache);
+ }
+
+ if (range_cmp_bounds(typcache, &upper1, &upper2) > 0)
+ {
+ upper2.inclusive = !upper2.inclusive;
+ upper2.lower = true;
+ *output2 = make_range(typcache, &upper2, &upper1, false, NULL);
+ }
+ else
+ {
+ *output2 = make_empty_range(typcache);
+ }
+}
+
/* range -> range aggregate functions */
Datum
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 60978f9415..353e9f2cad 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -2146,6 +2146,32 @@ get_typisdefined(Oid typid)
return false;
}
+/*
+ * get_typname
+ *
+ * Returns the name of a given type
+ *
+ * Returns a palloc'd copy of the string, or NULL if no such type.
+ */
+char *
+get_typname(Oid typid)
+{
+ HeapTuple tp;
+
+ tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+ if (HeapTupleIsValid(tp))
+ {
+ Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+ char *result;
+
+ result = pstrdup(NameStr(typtup->typname));
+ ReleaseSysCache(tp);
+ return result;
+ }
+ else
+ return NULL;
+}
+
/*
* get_typlen
*
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 430e3ca7dd..b3d90deaa7 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -41,6 +41,7 @@ typedef struct TriggerData
Tuplestorestate *tg_oldtable;
Tuplestorestate *tg_newtable;
const Bitmapset *tg_updatedcols;
+ ForPortionOfState *tg_temporal;
} TriggerData;
/*
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 2327b55f15..dcbfd048a0 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -41,12 +41,14 @@
#include "storage/condition_variable.h"
#include "utils/hsearch.h"
#include "utils/queryenvironment.h"
+#include "utils/rangetypes.h"
#include "utils/reltrigger.h"
#include "utils/sharedtuplestore.h"
#include "utils/snapshot.h"
#include "utils/sortsupport.h"
#include "utils/tuplesort.h"
#include "utils/tuplestore.h"
+#include "utils/typcache.h"
struct PlanState; /* forward references in this file */
struct ParallelHashJoinState;
@@ -424,6 +426,28 @@ typedef struct MergeActionState
ExprState *mas_whenqual; /* WHEN [NOT] MATCHED AND conditions */
} MergeActionState;
+/*
+ * ForPortionOfState
+ *
+ * Executor state of a FOR PORTION OF operation.
+ */
+typedef struct ForPortionOfState
+{
+ NodeTag type;
+
+ char *fp_rangeName; /* the column/PERIOD named in FOR PORTION OF */
+ Oid fp_rangeType; /* the type of the FOR PORTION OF expression */
+ bool fp_hasPeriod; /* true iff this is a PERIOD not a range */
+ int fp_rangeAttno; /* the attno of the range column (or 0 for a PERIOD) */
+ int fp_periodStartAttno; /* the attno of the PERIOD start column (or 0 for a range) */
+ int fp_periodEndAttno; /* the attno of the PERIOD end column (or 0 for a range) */
+ Datum fp_targetRange; /* the range from FOR PORTION OF */
+ TypeCacheEntry *fp_rangetypcache; /* type cache entry of the range */
+ TupleTableSlot *fp_Existing; /* slot to store existing target tuple in */
+ TupleTableSlot *fp_Leftover1; /* slot to store leftover below the target range */
+ TupleTableSlot *fp_Leftover2; /* slot to store leftover above the target range */
+} ForPortionOfState;
+
/*
* ResultRelInfo
*
@@ -544,6 +568,9 @@ typedef struct ResultRelInfo
List *ri_matchedMergeAction;
List *ri_notMatchedMergeAction;
+ /* FOR PORTION OF evaluation state */
+ ForPortionOfState *ri_forPortionOf;
+
/* partition check expression state (NULL if not set up yet) */
ExprState *ri_PartitionCheckExpr;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 74cf88f0e9..7b7e64c99f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -149,6 +149,9 @@ typedef struct Query
*/
int resultRelation pg_node_attr(query_jumble_ignore);
+ /* FOR PORTION OF clause for UPDATE/DELETE */
+ ForPortionOfExpr *forPortionOf;
+
/* has aggregates in tlist or havingQual */
bool hasAggs pg_node_attr(query_jumble_ignore);
/* has window functions in tlist */
@@ -1538,6 +1541,19 @@ typedef struct RowMarkClause
bool pushedDown; /* pushed down from higher query level? */
} RowMarkClause;
+/*
+ * ForPortionOfClause
+ * representation of FOR PORTION OF <period-name> FROM <t1> TO <t2>
+ */
+typedef struct ForPortionOfClause
+{
+ NodeTag type;
+ char *range_name;
+ int range_name_location;
+ Node *target_start;
+ Node *target_end;
+} ForPortionOfClause;
+
/*
* WithClause -
* representation of WITH clause
@@ -1881,12 +1897,13 @@ typedef struct InsertStmt
*/
typedef struct DeleteStmt
{
- NodeTag type;
- RangeVar *relation; /* relation to delete from */
- List *usingClause; /* optional using clause for more tables */
- Node *whereClause; /* qualifications */
- List *returningList; /* list of expressions to return */
- WithClause *withClause; /* WITH clause */
+ NodeTag type;
+ RangeVar *relation; /* relation to delete from */
+ ForPortionOfClause *forPortionOf; /* FOR PORTION OF clause */
+ List *usingClause; /* optional using clause for more tables */
+ Node *whereClause; /* qualifications */
+ List *returningList; /* list of expressions to return */
+ WithClause *withClause; /* WITH clause */
} DeleteStmt;
/* ----------------------
@@ -1895,13 +1912,14 @@ typedef struct DeleteStmt
*/
typedef struct UpdateStmt
{
- NodeTag type;
- RangeVar *relation; /* relation to update */
- List *targetList; /* the target list (of ResTarget) */
- Node *whereClause; /* qualifications */
- List *fromClause; /* optional from clause for more tables */
- List *returningList; /* list of expressions to return */
- WithClause *withClause; /* WITH clause */
+ NodeTag type;
+ RangeVar *relation; /* relation to update */
+ ForPortionOfClause *forPortionOf; /* FOR PORTION OF clause */
+ List *targetList; /* the target list (of ResTarget) */
+ Node *whereClause; /* qualifications */
+ List *fromClause; /* optional from clause for more tables */
+ List *returningList; /* list of expressions to return */
+ WithClause *withClause; /* WITH clause */
} UpdateStmt;
/* ----------------------
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index c17b53f7ad..2a04fed13d 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2339,6 +2339,7 @@ typedef struct ModifyTablePath
List *returningLists; /* per-target-table RETURNING tlists */
List *rowMarks; /* PlanRowMarks (non-locking only) */
OnConflictExpr *onconflict; /* ON CONFLICT clause, or NULL */
+ ForPortionOfExpr *forPortionOf; /* FOR PORTION OF clause for UPDATE/DELETE */
int epqParam; /* ID of Param for EvalPlanQual re-eval */
List *mergeActionLists; /* per-target-table lists of actions for
* MERGE */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 1b787fe031..911fed3f9c 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -243,6 +243,7 @@ typedef struct ModifyTable
List *rowMarks; /* PlanRowMarks (non-locking only) */
int epqParam; /* ID of Param for EvalPlanQual re-eval */
OnConflictAction onConflictAction; /* ON CONFLICT action */
+ Node *forPortionOf; /* FOR PORTION OF clause for UPDATE/DELETE // TODO: Instead of re-using Expr here, break it into pieces like onConflict{Action,Set,Where}? */
List *arbiterIndexes; /* List of ON CONFLICT arbiter index OIDs */
List *onConflictSet; /* INSERT ON CONFLICT DO UPDATE targetlist */
List *onConflictCols; /* target column numbers for onConflictSet */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 792a743f72..2c24daca6c 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -2032,4 +2032,29 @@ typedef struct OnConflictExpr
List *exclRelTlist; /* tlist of the EXCLUDED pseudo relation */
} OnConflictExpr;
+/*----------
+ * ForPortionOfExpr - represents a FOR PORTION OF ... expression
+ *
+ * We set up an expression to make a range from the FROM/TO bounds,
+ * so that we can use range operators with it.
+ *
+ * Then we set up an overlaps expression between that and the range column,
+ * so that we can find the rows we need to update/delete.
+ *
+ * In the executor we'll also build an intersect expression between the
+ * targeted range and the range column, so that we can update the start/end
+ * bounds of the UPDATE'd record.
+ *----------
+ */
+typedef struct ForPortionOfExpr
+{
+ NodeTag type;
+ Var *rangeVar; /* Range column */
+ char *range_name; /* Range name */
+ Node *targetRange; /* FOR PORTION OF bounds as a range */
+ Oid rangeType; /* type of targetRange */
+ Node *overlapsExpr; /* range && targetRange */
+ List *rangeSet; /* List of TargetEntrys to set the time column(s) */
+} ForPortionOfExpr;
+
#endif /* PRIMNODES_H */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 001e75b5b7..636488946d 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -280,7 +280,8 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root,
List *updateColnosLists,
List *withCheckOptionLists, List *returningLists,
List *rowMarks, OnConflictExpr *onconflict,
- List *mergeActionLists, int epqParam);
+ List *mergeActionLists,
+ ForPortionOfExpr *forPortionOf, int epqParam);
extern LimitPath *create_limit_path(PlannerInfo *root, RelOptInfo *rel,
Path *subpath,
Node *limitOffset, Node *limitCount,
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index 1cef1833a6..084e17187f 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -43,7 +43,8 @@ extern List *transformInsertRow(ParseState *pstate, List *exprlist,
List *stmtcols, List *icolumns, List *attrnos,
bool strip_indirection);
extern List *transformUpdateTargetList(ParseState *pstate,
- List *origTlist);
+ List *origTlist,
+ ForPortionOfExpr *forPortionOf);
extern Query *transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree);
extern Query *transformStmt(ParseState *pstate, Node *parseTree);
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 199cd74738..ecae82ed55 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -328,6 +328,7 @@ PG_KEYWORD("period", PERIOD, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("portion", PORTION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("preceding", PRECEDING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("precision", PRECISION, COL_NAME_KEYWORD, AS_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index f589112d5e..0d601f34a5 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -56,6 +56,7 @@ typedef enum ParseExprKind
EXPR_KIND_UPDATE_SOURCE, /* UPDATE assignment source item */
EXPR_KIND_UPDATE_TARGET, /* UPDATE assignment target item */
EXPR_KIND_MERGE_WHEN, /* MERGE WHEN [NOT] MATCHED condition */
+ EXPR_KIND_UPDATE_PORTION, /* UPDATE FOR PORTION OF item */
EXPR_KIND_GROUP_BY, /* GROUP BY */
EXPR_KIND_ORDER_BY, /* ORDER BY */
EXPR_KIND_DISTINCT_ON, /* DISTINCT ON */
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4f5418b972..a2b6ced904 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -142,6 +142,7 @@ extern char get_rel_persistence(Oid relid);
extern Oid get_transform_fromsql(Oid typid, Oid langid, List *trftypes);
extern Oid get_transform_tosql(Oid typid, Oid langid, List *trftypes);
extern bool get_typisdefined(Oid typid);
+extern char *get_typname(Oid typid);
extern int16 get_typlen(Oid typid);
extern bool get_typbyval(Oid typid);
extern void get_typlenbyval(Oid typid, int16 *typlen, bool *typbyval);
diff --git a/src/include/utils/period.h b/src/include/utils/period.h
new file mode 100644
index 0000000000..0a8af3edb0
--- /dev/null
+++ b/src/include/utils/period.h
@@ -0,0 +1,19 @@
+/*-------------------------------------------------------------------------
+ *
+ * period.h
+ * support for Postgres periods.
+ *
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/period.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PERIOD_H
+#define PERIOD_H
+
+extern Datum period_to_range(TupleTableSlot *slot, int startattno, int endattno, Oid rangetype);
+
+#endif /* PERIOD_H */
diff --git a/src/include/utils/rangetypes.h b/src/include/utils/rangetypes.h
index 6b420a8618..48ee2debed 100644
--- a/src/include/utils/rangetypes.h
+++ b/src/include/utils/rangetypes.h
@@ -164,5 +164,8 @@ extern RangeType *make_empty_range(TypeCacheEntry *typcache);
extern bool range_split_internal(TypeCacheEntry *typcache, const RangeType *r1,
const RangeType *r2, RangeType **output1,
RangeType **output2);
+extern void range_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1,
+ const RangeType *r2, RangeType **output1,
+ RangeType **output2);
#endif /* RANGETYPES_H */
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
new file mode 100644
index 0000000000..04556a2c8e
--- /dev/null
+++ b/src/test/regress/expected/for_portion_of.out
@@ -0,0 +1,393 @@
+-- Tests for UPDATE/DELETE FOR PORTION OF
+-- Works on non-PK columns
+CREATE TABLE for_portion_of_test (
+ id int4range,
+ valid_at tsrange,
+ name text NOT NULL
+);
+INSERT INTO for_portion_of_test VALUES
+('[1,2)', '[2018-01-02,2020-01-01)', 'one');
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+SET name = 'foo';
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2019-01-15' TO UNBOUNDED;
+SELECT * FROM for_portion_of_test;
+ id | valid_at | name
+-------+---------------------------------------------------------+------
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Tue Jan 01 00:00:00 2019") | foo
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | one
+ [1,2) | ["Tue Jan 01 00:00:00 2019","Tue Jan 15 00:00:00 2019") | one
+(3 rows)
+
+-- Works on more than one period
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+ id int4range,
+ valid1_at tsrange,
+ valid2_at tsrange,
+ name text NOT NULL
+);
+INSERT INTO for_portion_of_test VALUES
+('[1,2)', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', 'one');
+UPDATE for_portion_of_test
+FOR PORTION OF valid1_at FROM '2018-01-15' TO UNBOUNDED
+SET name = 'foo';
+SELECT * FROM for_portion_of_test;
+ id | valid1_at | valid2_at | name
+-------+---------------------------------------------------------+---------------------------------------------------------+------
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Feb 03 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Wed Jan 01 00:00:00 2025") | foo
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Wed Jan 01 00:00:00 2025") | one
+(2 rows)
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid2_at FROM '2018-01-15' TO UNBOUNDED
+SET name = 'bar';
+SELECT * FROM for_portion_of_test;
+ id | valid1_at | valid2_at | name
+-------+---------------------------------------------------------+---------------------------------------------------------+------
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Feb 03 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Wed Jan 01 00:00:00 2025") | bar
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Feb 03 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | foo
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Wed Jan 01 00:00:00 2025") | bar
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | one
+(4 rows)
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid1_at FROM '2018-01-20' TO UNBOUNDED;
+SELECT * FROM for_portion_of_test;
+ id | valid1_at | valid2_at | name
+-------+---------------------------------------------------------+---------------------------------------------------------+------
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Wed Jan 01 00:00:00 2025") | bar
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | one
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Wed Jan 01 00:00:00 2025") | bar
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | foo
+(4 rows)
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid2_at FROM '2018-01-20' TO UNBOUNDED;
+SELECT * FROM for_portion_of_test;
+ id | valid1_at | valid2_at | name
+-------+---------------------------------------------------------+---------------------------------------------------------+------
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | one
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | foo
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | bar
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | bar
+(4 rows)
+
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+ id int4range NOT NULL,
+ valid_at tsrange NOT NULL,
+ name text NOT NULL,
+ CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test
+VALUES
+('[1,2)', '[2018-01-02,2018-02-03)', 'one'),
+('[1,2)', '[2018-02-03,2018-03-03)', 'one'),
+('[1,2)', '[2018-03-03,2018-04-04)', 'one'),
+('[2,3)', '[2018-01-01,2018-01-05)', 'two'),
+('[3,4)', '[2018-01-01,)', 'three'),
+('[4,5)', '(,2018-04-01)', 'four'),
+('[5,6)', '(,)', 'five')
+;
+--
+-- UPDATE tests
+--
+-- Setting with a missing column fails
+UPDATE for_portion_of_test
+FOR PORTION OF invalid_at FROM '2018-06-01' TO UNBOUNDED
+SET name = 'foo'
+WHERE id = '[5,6)';
+ERROR: column or period "invalid_at" of relation "for_portion_of_test" does not exist
+LINE 2: FOR PORTION OF invalid_at FROM '2018-06-01' TO UNBOUNDED
+ ^
+-- Setting the range fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO UNBOUNDED
+SET valid_at = '[1990-01-01,1999-01-01)'
+WHERE id = '[5,6)';
+ERROR: can't directly assign to "valid_at" in a FOR PORTION OF update
+LINE 3: SET valid_at = '[1990-01-01,1999-01-01)'
+ ^
+-- The wrong type fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM 1 TO 4
+SET name = 'nope'
+WHERE id = '[3,4)';
+ERROR: function pg_catalog.tsrange(integer, integer) does not exist
+LINE 2: FOR PORTION OF valid_at FROM 1 TO 4
+ ^
+HINT: No function matches the given name and argument types. You might need to add explicit type casts.
+-- Setting with timestamps reversed fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+SET name = 'three^1'
+WHERE id = '[3,4)';
+ERROR: range lower bound must be less than or equal to range upper bound
+-- Setting with a subquery fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01'
+SET name = 'nope'
+WHERE id = '[3,4)';
+ERROR: cannot use subquery in FOR PORTION OF expression
+LINE 2: FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-...
+ ^
+-- Setting with a column fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM lower(valid_at) TO UNBOUNDED
+SET name = 'nope'
+WHERE id = '[3,4)';
+ERROR: cannot use column reference in FOR PORTION OF expression
+LINE 2: FOR PORTION OF valid_at FROM lower(valid_at) TO UNBOUNDED
+ ^
+-- Setting with timestamps equal does nothing
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+SET name = 'three^0'
+WHERE id = '[3,4)';
+-- Updating a finite/open portion with a finite/open target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO UNBOUNDED
+SET name = 'three^1'
+WHERE id = '[3,4)';
+-- Updating a finite/open portion with an open/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM UNBOUNDED TO '2018-03-01'
+SET name = 'three^2'
+WHERE id = '[3,4)';
+-- Updating an open/finite portion with an open/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM UNBOUNDED TO '2018-02-01'
+SET name = 'four^1'
+WHERE id = '[4,5)';
+-- Updating an open/finite portion with a finite/open target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO UNBOUNDED
+SET name = 'four^2'
+WHERE id = '[4,5)';
+-- Updating a finite/finite portion with an exact fit
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01'
+SET name = 'four^3'
+WHERE id = '[4,5)';
+-- Updating an enclosed span
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM UNBOUNDED TO UNBOUNDED
+SET name = 'two^2'
+WHERE id = '[2,3)';
+-- Updating an open/open portion with a finite/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01'
+SET name = 'five^2'
+WHERE id = '[5,6)';
+-- Updating an enclosed span with separate protruding spans
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01'
+SET name = 'five^3'
+WHERE id = '[5,6)';
+-- Updating multiple enclosed spans
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO NULL
+SET name = 'one^2'
+WHERE id = '[1,2)';
+-- Updating with a shift/reduce conflict
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+ FROM '2018-03-01' AT TIME ZONE INTERVAL '1' HOUR TO MINUTE
+ TO '2019-01-01'
+SET name = 'one^3'
+WHERE id = '[1,2)';
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+ FROM '2018-03-01' AT TIME ZONE INTERVAL '2' HOUR
+ TO '2019-01-01'
+SET name = 'one^4'
+WHERE id = '[1,2)';
+ERROR: syntax error at or near "'2019-01-01'"
+LINE 4: TO '2019-01-01'
+ ^
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+ FROM ('2018-03-01' AT TIME ZONE INTERVAL '2' HOUR)
+ TO '2019-01-01'
+SET name = 'one^4'
+WHERE id = '[1,2)';
+-- Updating the non-range part of the PK:
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-02-15' TO NULL
+SET id = '[6,7)'
+WHERE id = '[1,2)';
+-- UPDATE with no WHERE clause
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2030-01-01' TO NULL
+SET name = name || '*';
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+ id | valid_at | name
+-------+---------------------------------------------------------+----------
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Sat Feb 03 00:00:00 2018") | one^2
+ [1,2) | ["Sat Feb 03 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2
+ [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2
+ [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Mar 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | three
+ [3,4) | ["Fri Jun 01 00:00:00 2018","Tue Jan 01 00:00:00 2030") | three^1
+ [3,4) | ["Tue Jan 01 00:00:00 2030",) | three^1*
+ [4,5) | (,"Sun Jan 01 00:00:00 2017") | four^1
+ [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3
+ [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2
+ [5,6) | (,"Sun Jan 01 00:00:00 2017") | five
+ [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | five^3
+ [5,6) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | five^3
+ [5,6) | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | five
+ [5,6) | ["Tue Jan 01 00:00:00 2030",) | five*
+ [6,7) | ["Thu Feb 15 00:00:00 2018","Thu Mar 01 08:01:00 2018") | one^2
+ [6,7) | ["Thu Mar 01 08:01:00 2018","Thu Mar 01 10:00:00 2018") | one^3
+ [6,7) | ["Thu Mar 01 10:00:00 2018","Sat Mar 03 00:00:00 2018") | one^4
+ [6,7) | ["Sat Mar 03 00:00:00 2018","Wed Apr 04 00:00:00 2018") | one^4
+(20 rows)
+
+--
+-- DELETE tests
+--
+-- Deleting with a missing column fails
+DELETE FROM for_portion_of_test
+FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL
+WHERE id = '[5,6)';
+ERROR: column or period "invalid_at" of relation "for_portion_of_test" does not exist
+LINE 2: FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL
+ ^
+-- Deleting with timestamps reversed fails
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+WHERE id = '[3,4)';
+ERROR: range lower bound must be less than or equal to range upper bound
+-- Deleting with timestamps equal does nothing
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+WHERE id = '[3,4)';
+-- Deleting with a closed/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2020-06-01'
+WHERE id = '[5,6)';
+-- Deleting with a closed/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO NULL
+WHERE id = '[3,4)';
+-- Deleting with an open/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO '2018-02-08'
+WHERE id = '[1,2)';
+-- Deleting with an open/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO NULL
+WHERE id = '[6,7)';
+-- DELETE with no WHERE clause
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2025-01-01' TO NULL;
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+ id | valid_at | name
+-------+---------------------------------------------------------+---------
+ [1,2) | ["Thu Feb 08 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2
+ [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2
+ [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Mar 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | three
+ [4,5) | (,"Sun Jan 01 00:00:00 2017") | four^1
+ [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3
+ [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2
+ [5,6) | (,"Sun Jan 01 00:00:00 2017") | five
+ [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025") | five
+(11 rows)
+
+-- UPDATE ... RETURNING returns only the updated values (not the inserted side values)
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15'
+SET name = 'three^3'
+WHERE id = '[3,4)'
+RETURNING *;
+ id | valid_at | name
+-------+---------------------------------------------------------+---------
+ [3,4) | ["Thu Feb 01 00:00:00 2018","Thu Feb 15 00:00:00 2018") | three^3
+(1 row)
+
+-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows
+CREATE FUNCTION for_portion_of_trigger()
+RETURNS trigger
+AS
+$$
+BEGIN
+ RAISE NOTICE '% % % of %', TG_WHEN, TG_OP, NEW.valid_at, OLD.valid_at;
+ IF TG_OP = 'DELETE' THEN
+ RETURN OLD;
+ ELSE
+ RETURN NEW;
+ END IF;
+END;
+$$
+LANGUAGE plpgsql;
+CREATE TRIGGER trg_for_portion_of_before_insert
+ BEFORE INSERT ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_insert
+ AFTER INSERT ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_update
+ BEFORE UPDATE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_update
+ AFTER UPDATE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_delete
+ BEFORE DELETE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_delete
+ AFTER DELETE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01'
+SET name = 'five^4'
+WHERE id = '[5,6)';
+NOTICE: BEFORE UPDATE ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") of ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025")
+NOTICE: BEFORE INSERT ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") of <NULL>
+NOTICE: BEFORE INSERT ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025") of <NULL>
+NOTICE: AFTER INSERT ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") of <NULL>
+NOTICE: AFTER INSERT ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025") of <NULL>
+NOTICE: AFTER UPDATE ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") of ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025")
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01'
+WHERE id = '[5,6)';
+NOTICE: BEFORE DELETE <NULL> of ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025")
+NOTICE: BEFORE INSERT ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") of <NULL>
+NOTICE: BEFORE INSERT ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") of <NULL>
+NOTICE: AFTER INSERT ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") of <NULL>
+NOTICE: AFTER INSERT ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") of <NULL>
+NOTICE: AFTER DELETE <NULL> of ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025")
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+ id | valid_at | name
+-------+---------------------------------------------------------+---------
+ [1,2) | ["Thu Feb 08 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2
+ [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2
+ [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Feb 01 00:00:00 2018","Thu Feb 15 00:00:00 2018") | three^3
+ [3,4) | ["Thu Feb 15 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Mar 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | three
+ [4,5) | (,"Sun Jan 01 00:00:00 2017") | four^1
+ [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3
+ [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2
+ [5,6) | (,"Sun Jan 01 00:00:00 2017") | five
+ [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | five
+ [5,6) | ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") | five^4
+ [5,6) | ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") | five
+ [5,6) | ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") | five
+(16 rows)
+
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 3e4dfcc2ec..e10ba537a9 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -985,6 +985,24 @@ ERROR: null value in column "b" of relation "errtst_part_2" violates not-null c
DETAIL: Failing row contains (a, b, c) = (aaaa, null, ccc).
SET SESSION AUTHORIZATION regress_priv_user1;
DROP TABLE errtst;
+-- test column-level privileges on the range/PERIOD used in FOR PORTION OF
+SET SESSION AUTHORIZATION regress_priv_user1;
+CREATE TABLE t1 (
+ c1 int4range,
+ valid_at tsrange,
+ CONSTRAINT t1pk PRIMARY KEY (c1, valid_at WITHOUT OVERLAPS)
+);
+GRANT SELECT ON t1 TO regress_priv_user2;
+GRANT SELECT ON t1 TO regress_priv_user3;
+GRANT UPDATE (c1) ON t1 TO regress_priv_user2;
+GRANT UPDATE (c1, valid_at) ON t1 TO regress_priv_user3;
+SET SESSION AUTHORIZATION regress_priv_user2;
+UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)';
+ERROR: permission denied for table t1
+SET SESSION AUTHORIZATION regress_priv_user3;
+UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)';
+SET SESSION AUTHORIZATION regress_priv_user1;
+DROP TABLE t1;
-- test column-level privileges when involved with DELETE
SET SESSION AUTHORIZATION regress_priv_user1;
ALTER TABLE atest6 ADD COLUMN three integer;
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 1950e6f281..f4b88bcc38 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -3023,6 +3023,38 @@ select * from uv_iocu_tab;
drop view uv_iocu_view;
drop table uv_iocu_tab;
+-- Check UPDATE FOR PORTION OF works correctly
+create table uv_fpo_tab (id int4range, valid_at tsrange, b float,
+ constraint pk_uv_fpo_tab primary key (id, valid_at without overlaps));
+insert into uv_fpo_tab values ('[1,1]', '[2020-01-01, 2030-01-01)', 0);
+create view uv_fpo_view as
+ select b, b+1 as c, valid_at, id, '2.0'::text as two from uv_fpo_tab;
+insert into uv_fpo_view (id, valid_at, b) values ('[1,1]', '[2010-01-01, 2020-01-01)', 1);
+select * from uv_fpo_view;
+ b | c | valid_at | id | two
+---+---+---------------------------------------------------------+-------+-----
+ 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Wed Jan 01 00:00:00 2020") | [1,2) | 2.0
+(2 rows)
+
+update uv_fpo_view for portion of valid_at from '2015-01-01' to '2020-01-01' set b = 2 where id = '[1,1]';
+select * from uv_fpo_view;
+ b | c | valid_at | id | two
+---+---+---------------------------------------------------------+-------+-----
+ 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 2 | 3 | ["Thu Jan 01 00:00:00 2015","Wed Jan 01 00:00:00 2020") | [1,2) | 2.0
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Thu Jan 01 00:00:00 2015") | [1,2) | 2.0
+(3 rows)
+
+delete from uv_fpo_view for portion of valid_at from '2017-01-01' to '2022-01-01' where id = '[1,1]';
+select * from uv_fpo_view;
+ b | c | valid_at | id | two
+---+---+---------------------------------------------------------+-------+-----
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Thu Jan 01 00:00:00 2015") | [1,2) | 2.0
+ 0 | 1 | ["Sat Jan 01 00:00:00 2022","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 2 | 3 | ["Thu Jan 01 00:00:00 2015","Sun Jan 01 00:00:00 2017") | [1,2) | 2.0
+(3 rows)
+
-- Test whole-row references to the view
create table uv_iocu_tab (a int unique, b text);
create view uv_iocu_view as
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 19d8cd6292..143f19981f 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -269,6 +269,36 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'),
('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', 'bar')
;
+UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-05-01' TO '2000-07-01'
+ SET name = name || '1';
+UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-04-01' TO '2000-06-01'
+ SET name = name || '2'
+ WHERE id = '[2,2]';
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+ id | valid_at | id2 | name
+-------+-------------------------+--------+-------
+ [1,2) | [01-01-2000,05-01-2000) | [7,8) | foo
+ [1,2) | [05-01-2000,07-01-2000) | [7,8) | foo1
+ [1,2) | [07-01-2000,01-01-2010) | [7,8) | foo
+ [2,3) | [01-01-2000,04-01-2000) | [9,10) | bar
+ [2,3) | [04-01-2000,05-01-2000) | [9,10) | bar2
+ [2,3) | [05-01-2000,06-01-2000) | [9,10) | bar12
+ [2,3) | [06-01-2000,07-01-2000) | [9,10) | bar1
+ [2,3) | [07-01-2000,01-01-2010) | [9,10) | bar
+(8 rows)
+
+-- conflicting id only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+ VALUES
+ ('[1,1]', daterange('2005-01-01', '2006-01-01'), '[8,8]', 'foo3');
+ERROR: conflicting key value violates exclusion constraint "temporal3_pk"
+DETAIL: Key (id, valid_at)=([1,2), [01-01-2005,01-01-2006)) conflicts with existing key (id, valid_at)=([1,2), [07-01-2000,01-01-2010)).
+-- conflicting id2 only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+ VALUES
+ ('[3,3]', daterange('2005-01-01', '2010-01-01'), '[9,9]', 'bar3');
+ERROR: conflicting key value violates exclusion constraint "temporal3_uniq"
+DETAIL: Key (id2, valid_at)=([9,10), [01-01-2005,01-01-2010)) conflicts with existing key (id2, valid_at)=([9,10), [07-01-2000,01-01-2010)).
DROP TABLE temporal3;
--
-- test changing the PK's dependencies
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index b02b8dd4f6..ce732bda3c 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
# ----------
# Another group of parallel tests
# ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse for_portion_of
# ----------
# sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
new file mode 100644
index 0000000000..75bf550f91
--- /dev/null
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -0,0 +1,307 @@
+-- Tests for UPDATE/DELETE FOR PORTION OF
+
+-- Works on non-PK columns
+CREATE TABLE for_portion_of_test (
+ id int4range,
+ valid_at tsrange,
+ name text NOT NULL
+);
+INSERT INTO for_portion_of_test VALUES
+('[1,2)', '[2018-01-02,2020-01-01)', 'one');
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+SET name = 'foo';
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2019-01-15' TO UNBOUNDED;
+
+SELECT * FROM for_portion_of_test;
+
+-- Works on more than one period
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+ id int4range,
+ valid1_at tsrange,
+ valid2_at tsrange,
+ name text NOT NULL
+);
+INSERT INTO for_portion_of_test VALUES
+('[1,2)', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', 'one');
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid1_at FROM '2018-01-15' TO UNBOUNDED
+SET name = 'foo';
+SELECT * FROM for_portion_of_test;
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid2_at FROM '2018-01-15' TO UNBOUNDED
+SET name = 'bar';
+SELECT * FROM for_portion_of_test;
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid1_at FROM '2018-01-20' TO UNBOUNDED;
+SELECT * FROM for_portion_of_test;
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid2_at FROM '2018-01-20' TO UNBOUNDED;
+SELECT * FROM for_portion_of_test;
+
+
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+ id int4range NOT NULL,
+ valid_at tsrange NOT NULL,
+ name text NOT NULL,
+ CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test
+VALUES
+('[1,2)', '[2018-01-02,2018-02-03)', 'one'),
+('[1,2)', '[2018-02-03,2018-03-03)', 'one'),
+('[1,2)', '[2018-03-03,2018-04-04)', 'one'),
+('[2,3)', '[2018-01-01,2018-01-05)', 'two'),
+('[3,4)', '[2018-01-01,)', 'three'),
+('[4,5)', '(,2018-04-01)', 'four'),
+('[5,6)', '(,)', 'five')
+;
+
+--
+-- UPDATE tests
+--
+
+-- Setting with a missing column fails
+UPDATE for_portion_of_test
+FOR PORTION OF invalid_at FROM '2018-06-01' TO UNBOUNDED
+SET name = 'foo'
+WHERE id = '[5,6)';
+
+-- Setting the range fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO UNBOUNDED
+SET valid_at = '[1990-01-01,1999-01-01)'
+WHERE id = '[5,6)';
+
+-- The wrong type fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM 1 TO 4
+SET name = 'nope'
+WHERE id = '[3,4)';
+
+-- Setting with timestamps reversed fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+SET name = 'three^1'
+WHERE id = '[3,4)';
+
+-- Setting with a subquery fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01'
+SET name = 'nope'
+WHERE id = '[3,4)';
+
+-- Setting with a column fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM lower(valid_at) TO UNBOUNDED
+SET name = 'nope'
+WHERE id = '[3,4)';
+
+-- Setting with timestamps equal does nothing
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+SET name = 'three^0'
+WHERE id = '[3,4)';
+
+-- Updating a finite/open portion with a finite/open target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO UNBOUNDED
+SET name = 'three^1'
+WHERE id = '[3,4)';
+
+-- Updating a finite/open portion with an open/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM UNBOUNDED TO '2018-03-01'
+SET name = 'three^2'
+WHERE id = '[3,4)';
+
+-- Updating an open/finite portion with an open/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM UNBOUNDED TO '2018-02-01'
+SET name = 'four^1'
+WHERE id = '[4,5)';
+
+-- Updating an open/finite portion with a finite/open target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO UNBOUNDED
+SET name = 'four^2'
+WHERE id = '[4,5)';
+
+-- Updating a finite/finite portion with an exact fit
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01'
+SET name = 'four^3'
+WHERE id = '[4,5)';
+
+-- Updating an enclosed span
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM UNBOUNDED TO UNBOUNDED
+SET name = 'two^2'
+WHERE id = '[2,3)';
+
+-- Updating an open/open portion with a finite/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01'
+SET name = 'five^2'
+WHERE id = '[5,6)';
+
+-- Updating an enclosed span with separate protruding spans
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01'
+SET name = 'five^3'
+WHERE id = '[5,6)';
+
+-- Updating multiple enclosed spans
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO NULL
+SET name = 'one^2'
+WHERE id = '[1,2)';
+
+-- Updating with a shift/reduce conflict
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+ FROM '2018-03-01' AT TIME ZONE INTERVAL '1' HOUR TO MINUTE
+ TO '2019-01-01'
+SET name = 'one^3'
+WHERE id = '[1,2)';
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+ FROM '2018-03-01' AT TIME ZONE INTERVAL '2' HOUR
+ TO '2019-01-01'
+SET name = 'one^4'
+WHERE id = '[1,2)';
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+ FROM ('2018-03-01' AT TIME ZONE INTERVAL '2' HOUR)
+ TO '2019-01-01'
+SET name = 'one^4'
+WHERE id = '[1,2)';
+
+-- Updating the non-range part of the PK:
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-02-15' TO NULL
+SET id = '[6,7)'
+WHERE id = '[1,2)';
+
+-- UPDATE with no WHERE clause
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2030-01-01' TO NULL
+SET name = name || '*';
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+--
+-- DELETE tests
+--
+
+-- Deleting with a missing column fails
+DELETE FROM for_portion_of_test
+FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL
+WHERE id = '[5,6)';
+
+-- Deleting with timestamps reversed fails
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+WHERE id = '[3,4)';
+
+-- Deleting with timestamps equal does nothing
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+WHERE id = '[3,4)';
+
+-- Deleting with a closed/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2020-06-01'
+WHERE id = '[5,6)';
+
+-- Deleting with a closed/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO NULL
+WHERE id = '[3,4)';
+
+-- Deleting with an open/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO '2018-02-08'
+WHERE id = '[1,2)';
+
+-- Deleting with an open/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO NULL
+WHERE id = '[6,7)';
+
+-- DELETE with no WHERE clause
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2025-01-01' TO NULL;
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+-- UPDATE ... RETURNING returns only the updated values (not the inserted side values)
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15'
+SET name = 'three^3'
+WHERE id = '[3,4)'
+RETURNING *;
+
+-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows
+
+CREATE FUNCTION for_portion_of_trigger()
+RETURNS trigger
+AS
+$$
+BEGIN
+ RAISE NOTICE '% % % of %', TG_WHEN, TG_OP, NEW.valid_at, OLD.valid_at;
+ IF TG_OP = 'DELETE' THEN
+ RETURN OLD;
+ ELSE
+ RETURN NEW;
+ END IF;
+END;
+$$
+LANGUAGE plpgsql;
+
+CREATE TRIGGER trg_for_portion_of_before_insert
+ BEFORE INSERT ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_insert
+ AFTER INSERT ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_update
+ BEFORE UPDATE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_update
+ AFTER UPDATE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_delete
+ BEFORE DELETE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_delete
+ AFTER DELETE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01'
+SET name = 'five^4'
+WHERE id = '[5,6)';
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01'
+WHERE id = '[5,6)';
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 134809e8cc..b8f67bdf63 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -713,6 +713,24 @@ UPDATE errtst SET a = 'aaaa', b = NULL WHERE a = 'aaa';
SET SESSION AUTHORIZATION regress_priv_user1;
DROP TABLE errtst;
+-- test column-level privileges on the range/PERIOD used in FOR PORTION OF
+SET SESSION AUTHORIZATION regress_priv_user1;
+CREATE TABLE t1 (
+ c1 int4range,
+ valid_at tsrange,
+ CONSTRAINT t1pk PRIMARY KEY (c1, valid_at WITHOUT OVERLAPS)
+);
+GRANT SELECT ON t1 TO regress_priv_user2;
+GRANT SELECT ON t1 TO regress_priv_user3;
+GRANT UPDATE (c1) ON t1 TO regress_priv_user2;
+GRANT UPDATE (c1, valid_at) ON t1 TO regress_priv_user3;
+SET SESSION AUTHORIZATION regress_priv_user2;
+UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)';
+SET SESSION AUTHORIZATION regress_priv_user3;
+UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)';
+SET SESSION AUTHORIZATION regress_priv_user1;
+DROP TABLE t1;
+
-- test column-level privileges when involved with DELETE
SET SESSION AUTHORIZATION regress_priv_user1;
ALTER TABLE atest6 ADD COLUMN three integer;
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
index eaee0b7e1d..f814701a43 100644
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -1541,6 +1541,20 @@ select * from uv_iocu_tab;
drop view uv_iocu_view;
drop table uv_iocu_tab;
+-- Check UPDATE FOR PORTION OF works correctly
+create table uv_fpo_tab (id int4range, valid_at tsrange, b float,
+ constraint pk_uv_fpo_tab primary key (id, valid_at without overlaps));
+insert into uv_fpo_tab values ('[1,1]', '[2020-01-01, 2030-01-01)', 0);
+create view uv_fpo_view as
+ select b, b+1 as c, valid_at, id, '2.0'::text as two from uv_fpo_tab;
+
+insert into uv_fpo_view (id, valid_at, b) values ('[1,1]', '[2010-01-01, 2020-01-01)', 1);
+select * from uv_fpo_view;
+update uv_fpo_view for portion of valid_at from '2015-01-01' to '2020-01-01' set b = 2 where id = '[1,1]';
+select * from uv_fpo_view;
+delete from uv_fpo_view for portion of valid_at from '2017-01-01' to '2022-01-01' where id = '[1,1]';
+select * from uv_fpo_view;
+
-- Test whole-row references to the view
create table uv_iocu_tab (a int unique, b text);
create view uv_iocu_view as
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index d9d4c0b2db..0dd9370a50 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -203,6 +203,20 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'),
('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', 'bar')
;
+UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-05-01' TO '2000-07-01'
+ SET name = name || '1';
+UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-04-01' TO '2000-06-01'
+ SET name = name || '2'
+ WHERE id = '[2,2]';
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+-- conflicting id only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+ VALUES
+ ('[1,1]', daterange('2005-01-01', '2006-01-01'), '[8,8]', 'foo3');
+-- conflicting id2 only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+ VALUES
+ ('[3,3]', daterange('2005-01-01', '2010-01-01'), '[9,9]', 'bar3');
DROP TABLE temporal3;
--
--
2.32.0 (Apple Git-132)
[application/octet-stream] v12-0005-Add-PERIODs.patch (124.7K, ../../CA+renyVyMOb+t6QNoq=PBK1CY_oa9wdJrObjj6S1-RK9CxYwKQ@mail.gmail.com/6-v12-0005-Add-PERIODs.patch)
download | inline diff:
From 533dbf10e67b61a5940935889d17a1d32b0972a0 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Sat, 9 Jan 2021 21:59:43 -0800
Subject: [PATCH v12 5/5] Add PERIODs
- Added parsing for SQL:2011 syntax to define an application-time PERIOD on a
table (in both CREATE TABLE and ALTER TABLE). Make sure we create the PERIOD
after columns are known (since PERIODs can refer to them) but before
constraints are handled (since PERIODs can appear in them).
- Added ALTER TABLE DROP support for PERIODs.
- Created postgres.pg_period table.
- Created information_schema.periods view.
- Added pg_dump support.
- Added tests and documentation.
- Automatically define a constraint for each PERIOD requiring the start column
to be less than the end column.
- When creating a PERIOD, choose an appropriate range type we can use to
implement PERIOD-related operations. You can choose one explicitly if there
is ambiguity (due to multiple range types created over the same base type).
---
doc/src/sgml/catalogs.sgml | 112 +++
doc/src/sgml/ddl.sgml | 58 ++
doc/src/sgml/information_schema.sgml | 63 ++
doc/src/sgml/ref/alter_table.sgml | 25 +
doc/src/sgml/ref/comment.sgml | 2 +
doc/src/sgml/ref/create_table.sgml | 40 ++
src/backend/catalog/Makefile | 3 +-
src/backend/catalog/aclchk.c | 2 +
src/backend/catalog/dependency.c | 9 +
src/backend/catalog/heap.c | 75 ++
src/backend/catalog/information_schema.sql | 23 +-
src/backend/catalog/objectaddress.c | 73 ++
src/backend/catalog/pg_period.c | 105 +++
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/alter.c | 1 +
src/backend/commands/comment.c | 10 +
src/backend/commands/dropcmds.c | 1 +
src/backend/commands/event_trigger.c | 4 +
src/backend/commands/seclabel.c | 1 +
src/backend/commands/tablecmds.c | 764 ++++++++++++++++++++-
src/backend/commands/view.c | 4 +-
src/backend/nodes/nodeFuncs.c | 3 +
src/backend/parser/gram.y | 43 +-
src/backend/parser/parse_utilcmd.c | 136 ++++
src/backend/utils/cache/lsyscache.c | 113 +++
src/backend/utils/cache/syscache.c | 21 +
src/bin/pg_dump/pg_backup_archiver.c | 1 +
src/bin/pg_dump/pg_dump.c | 173 ++++-
src/bin/pg_dump/pg_dump.h | 14 +
src/bin/pg_dump/pg_dump_sort.c | 7 +
src/bin/psql/describe.c | 34 +
src/include/catalog/dependency.h | 1 +
src/include/catalog/heap.h | 4 +
src/include/catalog/pg_index.h | 2 +-
src/include/catalog/pg_period.h | 53 ++
src/include/catalog/pg_range.h | 1 +
src/include/commands/tablecmds.h | 4 +-
src/include/nodes/parsenodes.h | 35 +-
src/include/parser/parse_utilcmd.h | 1 +
src/include/utils/lsyscache.h | 4 +
src/include/utils/syscache.h | 3 +
src/test/regress/expected/periods.out | 152 ++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/periods.sql | 102 +++
44 files changed, 2245 insertions(+), 41 deletions(-)
create mode 100644 src/backend/catalog/pg_period.c
create mode 100644 src/include/catalog/pg_period.h
create mode 100644 src/test/regress/expected/periods.out
create mode 100644 src/test/regress/sql/periods.sql
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 852cb30ae1..007bec90fc 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -230,6 +230,11 @@
<entry>information about partition key of tables</entry>
</row>
+ <row>
+ <entry><link linkend="catalog-pg-period"><structname>pg_period</structname></link></entry>
+ <entry>periods</entry>
+ </row>
+
<row>
<entry><link linkend="catalog-pg-policy"><structname>pg_policy</structname></link></entry>
<entry>row-security policies</entry>
@@ -5700,6 +5705,113 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
are simple references.
</para></entry>
</row>
+
+ </tbody>
+ </tgroup>
+ </table>
+ </sect1>
+
+
+ <sect1 id="catalog-pg-period">
+ <title><structname>pg_period</structname></title>
+
+ <indexterm zone="catalog-pg-period">
+ <primary>pg_period</primary>
+ </indexterm>
+
+ <para>
+ The catalog <structname>pg_period</structname> stores
+ information about system and application time periods.
+ </para>
+
+ <para>
+ Periods are described in <xref linkend="ddl-periods"/>.
+ </para>
+
+ <table>
+ <title><structname>pg_period</structname> Columns</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>oid</structfield> <type>oid</type>
+ </para>
+ <para>
+ Row identifier
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>pername</structfield> <type>text</type>
+ </para>
+ <para>
+ Period name
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>perrelid</structfield> <type>oid</type>
+ (references <link linkend="catalog-pg-class"><structname>pg_class</structname></link>.<structfield>oid</structfield>)
+ </para>
+ <para>
+ The table this period belongs to
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>perstart</structfield> <type>int2</type>
+ (references <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>.<structfield>attnum</structfield>)
+ </para>
+ <para>
+ The number of the start column
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>perend</structfield> <type>int2</type>
+ (references <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>.<structfield>attnum</structfield>)
+ </para>
+ <para>
+ The number of the end column
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>perrngtype</structfield> <type>oid</type>
+ (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+ </para>
+ <para>
+ The OID of the range type associated with this period
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>perconstraint</structfield> <type>oid</type>
+ (references <link linkend="catalog-pg-constraint"><structname>pg_constraint</structname></link>.<structfield>oid</structfield>)
+ </para>
+ <para>
+ The OID of the period's <literal>CHECK</literal> constraint
+ </para></entry>
+ </row>
+
</tbody>
</tgroup>
</table>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index e32f8253d0..6f52c2bacf 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1238,6 +1238,64 @@ CREATE TABLE circles (
</sect2>
</sect1>
+ <sect1 id="ddl-periods">
+ <title>Periods</title>
+
+ <para>
+ Periods are definitions on a table that associate a period name with a start
+ column and an end column. Both columns must be of exactly the same type
+ (including collation), have a btree operator class, and the start column
+ value must be strictly less than the end column value.
+ </para>
+
+ <para>
+ There are two types of periods: application and system. System periods are
+ distinguished by their name which must be <literal>SYSTEM_TIME</literal>. Any
+ other name is an application period.
+ </para>
+
+ <sect2 id="ddl-periods-application-periods">
+ <title>Application Periods</title>
+
+ <indexterm>
+ <primary>period</primary>
+ <secondary>application</secondary>
+ </indexterm>
+
+ <para>
+ Application periods are defined on a table using the following syntax:
+ </para>
+
+<programlisting>
+CREATE TABLE billing_addresses (
+ customer_id integer,
+ address_id integer,
+ valid_from date,
+ valid_to date,
+ <emphasis>PERIOD FOR validity (valid_from, valid_to)</emphasis>
+);
+</programlisting>
+
+ <para>
+ Application periods can be used to define temporal primary and foreign keys.
+ Any table with a temporal primary key is a temporal table and supports temporal update and delete commands.
+ </para>
+ </sect2>
+
+ <sect2 id="ddl-periods-system-periods">
+ <title>System Periods</title>
+
+ <indexterm>
+ <primary>period</primary>
+ <secondary>system</secondary>
+ </indexterm>
+
+ <para>
+ Periods for <literal>SYSTEM_TIME</literal> are currently not implemented.
+ </para>
+ </sect2>
+ </sect1>
+
<sect1 id="ddl-system-columns">
<title>System Columns</title>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 350c75bc31..ee9faa8079 100644
--- a/doc/src/sgml/information_schema.sgml
+++ b/doc/src/sgml/information_schema.sgml
@@ -4171,6 +4171,69 @@ ORDER BY c.ordinal_position;
</table>
</sect1>
+ <sect1 id="infoschema-periods">
+ <title><literal>periods</literal></title>
+
+ <para>
+ The view <literal>parameters</literal> contains information about the
+ periods of all tables in the current database. The start and end column
+ names are only shown if the current user has access to them (by way of being
+ the owner or having some privilege).
+ </para>
+
+ <table>
+ <title><literal>periods</literal> Columns</title>
+
+ <tgroup cols="3">
+ <thead>
+ <row>
+ <entry>Name</entry>
+ <entry>Data Type</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry><literal>table_catalog</literal></entry>
+ <entry><type>sql_identifier</type></entry>
+ <entry>Name of the database containing the period (always the current database)</entry>
+ </row>
+
+ <row>
+ <entry><literal>table_schema</literal></entry>
+ <entry><type>sql_identifier</type></entry>
+ <entry>Name of the schema containing the period</entry>
+ </row>
+
+ <row>
+ <entry><literal>table_name</literal></entry>
+ <entry><type>sql_identifier</type></entry>
+ <entry>Name of the table containing the period</entry>
+ </row>
+
+ <row>
+ <entry><literal>period_name</literal></entry>
+ <entry><type>sql_identifier</type></entry>
+ <entry>Name of the period</entry>
+ </row>
+
+ <row>
+ <entry><literal>start_column_name</literal></entry>
+ <entry><type>sql_identifier</type></entry>
+ <entry>Name of the start column for the period</entry>
+ </row>
+
+ <row>
+ <entry><literal>end_column_name</literal></entry>
+ <entry><type>sql_identifier</type></entry>
+ <entry>Name of the end column for the period</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect1>
+
<sect1 id="infoschema-referential-constraints">
<title><literal>referential_constraints</literal></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index d4d93eeb7c..487f09f88a 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -60,6 +60,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
+ ADD PERIOD FOR <replaceable class="parameter">period_name</replaceable> ( <replaceable class="parameter">start_column</replaceable>, <replaceable class="parameter">end_column</replaceable> ) [ WITH ( <replaceable class="parameter">period_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]
+ DROP PERIOD FOR <replaceable class="parameter">period_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
ENABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
ENABLE REPLICA TRIGGER <replaceable class="parameter">trigger_name</replaceable>
@@ -570,6 +572,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-altertable-desc-add-period">
+ <term><literal>ADD PERIOD FOR</literal></term>
+ <listitem>
+ <para>
+ This form adds a new period to a table using the same syntax as
+ <xref linkend="sql-createtable"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>DROP PERIOD FOR</literal></term>
+ <listitem>
+ <para>
+ This form drops the specified period on a table. The start and end
+ columns will not be dropped by this command but the
+ <literal>CHECK</literal> constraint will be. You will need to say
+ <literal>CASCADE</literal> if anything outside the table depends on the
+ column.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-altertable-desc-disable-enable-trigger">
<term><literal>DISABLE</literal>/<literal>ENABLE [ REPLICA | ALWAYS ] TRIGGER</literal></term>
<listitem>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b13..49c2df9944 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -44,6 +44,7 @@ COMMENT ON
OPERATOR <replaceable class="parameter">operator_name</replaceable> (<replaceable class="parameter">left_type</replaceable>, <replaceable class="parameter">right_type</replaceable>) |
OPERATOR CLASS <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
OPERATOR FAMILY <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
+ PERIOD <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">period_name</replaceable> |
POLICY <replaceable class="parameter">policy_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
[ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
@@ -341,6 +342,7 @@ COMMENT ON OPERATOR ^ (text, text) IS 'Performs intersection of two texts';
COMMENT ON OPERATOR - (NONE, integer) IS 'Unary minus';
COMMENT ON OPERATOR CLASS int4ops USING btree IS '4 byte integer operators for btrees';
COMMENT ON OPERATOR FAMILY integer_ops USING btree IS 'all integer operators for btrees';
+COMMENT ON PERIOD my_table.my_column IS 'Sales promotion validity';
COMMENT ON POLICY my_policy ON mytable IS 'Filter rows by users';
COMMENT ON PROCEDURE my_proc (integer, integer) IS 'Runs a report';
COMMENT ON PUBLICATION alltables IS 'Publishes all operations on all tables';
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index aa0d4cda19..312d6e7be6 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -23,6 +23,7 @@ PostgreSQL documentation
<synopsis>
CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable> ( [
{ <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+ | <replaceable>period_definition</replaceable>
| <replaceable>table_constraint</replaceable>
| LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
[, ... ]
@@ -37,6 +38,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable>
OF <replaceable class="parameter">type_name</replaceable> [ (
{ <replaceable class="parameter">column_name</replaceable> [ WITH OPTIONS ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+ | <replaceable>period_definition</replaceable>
| <replaceable>table_constraint</replaceable> }
[, ... ]
) ]
@@ -49,6 +51,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable>
PARTITION OF <replaceable class="parameter">parent_table</replaceable> [ (
{ <replaceable class="parameter">column_name</replaceable> [ WITH OPTIONS ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+ | <replaceable>period_definition</replaceable>
| <replaceable>table_constraint</replaceable> }
[, ... ]
) ] { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT }
@@ -73,6 +76,11 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+<phrase>and <replaceable class="parameter">period_definition</replaceable> is:</phrase>
+
+PERIOD FOR { <replaceable class="parameter">period_name</replaceable> | SYSTEM_TIME } ( <replaceable class="parameter">column_name</replaceable>, <replaceable class="parameter">column_name</replaceable> )
+[ WITH ( <replaceable class="parameter">period_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]
+
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
[ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
@@ -143,6 +151,14 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
name as any existing data type in the same schema.
</para>
+ <para>
+ Periods my be defined on tables, specifying that two existing columns
+ represent start and end values for the period. Periods may have any name
+ that doesn't conflict with a column name, but the name
+ <literal>SYSTEM_TIME</literal> is special, used for versioning tables.
+ System periods are not yet implemented. See <xref linkend="ddl-periods"/> for more details.
+ </para>
+
<para>
The optional constraint clauses specify constraints (tests) that
new or updated rows must satisfy for an insert or update operation
@@ -803,6 +819,30 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-period">
+ <term><literal>PERIOD FOR <replaceable class="parameter">period_name</replaceable> ( <replaceable class="parameter">column_name</replaceable>, <replaceable class="parameter">column_name</replaceable> ) [ WITH ( <replaceable class="parameter">period_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]</literal></term>
+ <listitem>
+ <para>
+ A period definition gives semantic meaning to two existing columns of
+ the table. It defines a "start column" and an "end column" where the
+ start value is strictly less than the end value. A
+ <literal>CHECK</literal> constraint is automatically created to enforce
+ this. You can specify the name of that constraint with the
+ <literal>check_constraint_name</literal> <replaceable class="parameter">period_option</replaceable>.
+ </para>
+
+ <para>
+ Both columns must have exactly the same type and must have a range type
+ defined from their base type. If there are several range types for that
+ base type, you must specify which one you want by using the
+ <literal>rangetype</literal> <replaceable class="parameter">period_option</replaceable>.
+ Any base type is allowed, as long as it has a range type, although it is
+ expected that most periods will use temporal types like <literal>timestamptz</literal>
+ or <literal>date</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-parms-constraint">
<term><literal>CONSTRAINT <replaceable class="parameter">constraint_name</replaceable></literal></term>
<listitem>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index a60107bf94..c9e37a4d71 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -39,6 +39,7 @@ OBJS = \
pg_namespace.o \
pg_operator.o \
pg_parameter_acl.o \
+ pg_period.o \
pg_proc.o \
pg_publication.o \
pg_range.o \
@@ -70,7 +71,7 @@ CATALOG_HEADERS := \
pg_foreign_table.h pg_policy.h pg_replication_origin.h \
pg_default_acl.h pg_init_privs.h pg_seclabel.h pg_shseclabel.h \
pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
- pg_range.h pg_transform.h \
+ pg_period.h pg_range.h pg_transform.h \
pg_sequence.h pg_publication.h pg_publication_namespace.h \
pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index bc2ad773c9..1672ed342b 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -2795,6 +2795,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DEFAULT:
case OBJECT_DEFACL:
case OBJECT_DOMCONSTRAINT:
+ case OBJECT_PERIOD:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
case OBJECT_ROLE:
@@ -2936,6 +2937,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DEFACL:
case OBJECT_DOMCONSTRAINT:
case OBJECT_PARAMETER_ACL:
+ case OBJECT_PERIOD:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
case OBJECT_ROLE:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index f8a136ba0a..232f77c96e 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -48,6 +48,7 @@
#include "catalog/pg_operator.h"
#include "catalog/pg_opfamily.h"
#include "catalog/pg_parameter_acl.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_policy.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_publication.h"
@@ -154,6 +155,7 @@ static const Oid object_classes[] = {
CastRelationId, /* OCLASS_CAST */
CollationRelationId, /* OCLASS_COLLATION */
ConstraintRelationId, /* OCLASS_CONSTRAINT */
+ PeriodRelationId, /* OCLASS_PERIOD */
ConversionRelationId, /* OCLASS_CONVERSION */
AttrDefaultRelationId, /* OCLASS_DEFAULT */
LanguageRelationId, /* OCLASS_LANGUAGE */
@@ -1443,6 +1445,10 @@ doDeletion(const ObjectAddress *object, int flags)
RemoveConstraintById(object->objectId);
break;
+ case OCLASS_PERIOD:
+ RemovePeriodById(object->objectId);
+ break;
+
case OCLASS_DEFAULT:
RemoveAttrDefaultById(object->objectId);
break;
@@ -2862,6 +2868,9 @@ getObjectClass(const ObjectAddress *object)
case ConstraintRelationId:
return OCLASS_CONSTRAINT;
+ case PeriodRelationId:
+ return OCLASS_PERIOD;
+
case ConversionRelationId:
return OCLASS_CONVERSION;
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index b67fb8400c..022a2ede4c 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -49,6 +49,7 @@
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
#include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_statistic.h"
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_tablespace.h"
@@ -2052,6 +2053,80 @@ SetAttrMissing(Oid relid, char *attname, char *value)
table_close(tablerel, AccessExclusiveLock);
}
+/*
+ * Store a period of relation rel.
+ *
+ * Returns the OID of the new pg_period tuple.
+ */
+Oid
+StorePeriod(Relation rel, const char *periodname, AttrNumber startnum,
+ AttrNumber endnum, AttrNumber rangenum, Oid conoid)
+{
+ Assert(rangenum != InvalidAttrNumber);
+ Datum values[Natts_pg_period];
+ bool nulls[Natts_pg_period];
+ Relation pg_period;
+ HeapTuple tuple;
+ Oid oid;
+ NameData pername;
+ ObjectAddress myself, referenced;
+
+ namestrcpy(&pername, periodname);
+
+ MemSet(values, 0, sizeof(values));
+ MemSet(nulls, false, sizeof(nulls));
+
+ pg_period = table_open(PeriodRelationId, RowExclusiveLock);
+
+ oid = GetNewOidWithIndex(pg_period, AttrDefaultOidIndexId, Anum_pg_period_oid);
+ values[Anum_pg_period_oid - 1] = ObjectIdGetDatum(oid);
+ values[Anum_pg_period_pername - 1] = NameGetDatum(&pername);
+ values[Anum_pg_period_perrelid - 1] = RelationGetRelid(rel);
+ values[Anum_pg_period_perstart - 1] = startnum;
+ values[Anum_pg_period_perend - 1] = endnum;
+ values[Anum_pg_period_perrange - 1] = rangenum;
+ values[Anum_pg_period_perconstraint - 1] = conoid;
+
+ tuple = heap_form_tuple(RelationGetDescr(pg_period), values, nulls);
+ CatalogTupleInsert(pg_period, tuple);
+
+ ObjectAddressSet(myself, PeriodRelationId, oid);
+
+ /* Drop the period when the table is dropped. */
+ ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
+ recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
+
+ /* Forbid dropping the columns of the period. */
+ ObjectAddressSubSet(referenced, RelationRelationId, RelationGetRelid(rel), startnum);
+ recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+ ObjectAddressSubSet(referenced, RelationRelationId, RelationGetRelid(rel), endnum);
+ recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+ /*
+ * The range column is an implementation detail,
+ * but we can't use DEPENDENCY_INTERNAL
+ * because dropping the table will check for dependencies on all subobjects too
+ * (in findDependentObjects).
+ * But if we make an AUTO dependency one way we will auto-drop the column
+ * when we drop the PERIOD,
+ * and a NORMAL dependency the other way we will forbid dropping the column directly.
+ */
+ ObjectAddressSubSet(referenced, RelationRelationId, RelationGetRelid(rel), rangenum);
+ recordDependencyOn(&referenced, &myself, DEPENDENCY_AUTO);
+ recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+ /*
+ * The constraint is an implementation detail, so we mark it as such.
+ * (Note that myself and referenced are reversed for this one.)
+ */
+ ObjectAddressSet(referenced, ConstraintRelationId, conoid);
+ recordDependencyOn(&referenced, &myself, DEPENDENCY_INTERNAL);
+
+ table_close(pg_period, RowExclusiveLock);
+
+ return oid;
+}
+
/*
* Store a check-constraint expression for the given relation.
*
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 8bcd42467a..4afd207ade 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -1200,7 +1200,28 @@ GRANT SELECT ON parameters TO PUBLIC;
* PERIODS view
*/
--- feature not supported
+CREATE VIEW periods AS
+ SELECT current_database()::information_schema.sql_identifier AS table_catalog,
+ nc.nspname::information_schema.sql_identifier AS table_schema,
+ c.relname::information_schema.sql_identifier AS table_name,
+ p.pername::information_schema.sql_identifier AS period_name,
+ CASE WHEN pg_has_role(c.relowner, 'USAGE')
+ OR has_column_privilege(sa.attrelid, sa.attnum, 'SELECT, INSERT, UPDATE, REFERENCES')
+ THEN sa.attname::information_schema.sql_identifier
+ END AS start_column_name,
+ CASE WHEN pg_has_role(c.relowner, 'USAGE')
+ OR has_column_privilege(ea.attrelid, ea.attnum, 'SELECT, INSERT, UPDATE, REFERENCES')
+ THEN ea.attname::information_schema.sql_identifier
+ END AS end_column_name
+ FROM pg_period AS p
+ JOIN pg_class AS c ON c.oid = p.perrelid
+ JOIN pg_namespace AS nc ON nc.oid = c.relnamespace
+ JOIN pg_attribute AS sa ON (sa.attrelid, sa.attnum) = (p.perrelid, p.perstart)
+ JOIN pg_attribute AS ea ON (ea.attrelid, ea.attnum) = (p.perrelid, p.perend)
+ WHERE NOT pg_is_other_temp_schema(nc.oid)
+ AND c.relkind IN ('r', 'v');
+
+GRANT SELECT ON periods TO PUBLIC;
/*
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 95fefc7565..a979233e6b 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -47,6 +47,7 @@
#include "catalog/pg_operator.h"
#include "catalog/pg_opfamily.h"
#include "catalog/pg_parameter_acl.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_policy.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_publication.h"
@@ -730,6 +731,10 @@ static const struct object_type_map
{
"domain constraint", OBJECT_DOMCONSTRAINT
},
+ /* OCLASS_PERIOD */
+ {
+ "period", OBJECT_PERIOD
+ },
/* OCLASS_CONVERSION */
{
"conversion", OBJECT_CONVERSION
@@ -1008,6 +1013,7 @@ get_object_address(ObjectType objtype, Node *object,
case OBJECT_TRIGGER:
case OBJECT_TABCONSTRAINT:
case OBJECT_POLICY:
+ case OBJECT_PERIOD:
address = get_object_address_relobject(objtype, castNode(List, object),
&relation, missing_ok);
break;
@@ -1506,6 +1512,13 @@ get_object_address_relobject(ObjectType objtype, List *object,
InvalidOid;
address.objectSubId = 0;
break;
+ case OBJECT_PERIOD:
+ address.classId = PeriodRelationId;
+ address.objectId = relation ?
+ get_relation_period_oid(reloid, depname, missing_ok) :
+ InvalidOid;
+ address.objectSubId = 0;
+ break;
default:
elog(ERROR, "unrecognized object type: %d", (int) objtype);
}
@@ -2323,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
case OBJECT_RULE:
case OBJECT_TRIGGER:
case OBJECT_TABCONSTRAINT:
+ case OBJECT_PERIOD:
case OBJECT_OPCLASS:
case OBJECT_OPFAMILY:
objnode = (Node *) name;
@@ -2433,6 +2447,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
case OBJECT_TRIGGER:
case OBJECT_POLICY:
case OBJECT_TABCONSTRAINT:
+ case OBJECT_PERIOD:
if (!object_ownercheck(RelationRelationId, RelationGetRelid(relation), roleid))
aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
RelationGetRelationName(relation));
@@ -3081,6 +3096,38 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
break;
}
+ case OCLASS_PERIOD:
+ {
+ HeapTuple perTup;
+ Form_pg_period per;
+
+ perTup = SearchSysCache1(PERIODOID,
+ ObjectIdGetDatum(object->objectId));
+ if (!HeapTupleIsValid(perTup))
+ elog(ERROR, "cache lookup failed for period %u",
+ object->objectId);
+ per = (Form_pg_period) GETSTRUCT(perTup);
+
+ if (OidIsValid(per->perrelid))
+ {
+ StringInfoData rel;
+
+ initStringInfo(&rel);
+ getRelationDescription(&rel, per->perrelid, false);
+ appendStringInfo(&buffer, _("period %s on %s"),
+ NameStr(per->pername), rel.data);
+ pfree(rel.data);
+ }
+ else
+ {
+ appendStringInfo(&buffer, _("period %s"),
+ NameStr(per->pername));
+ }
+
+ ReleaseSysCache(perTup);
+ break;
+ }
+
case OCLASS_CONVERSION:
{
HeapTuple conTup;
@@ -4445,6 +4492,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
missing_ok);
break;
+ case OCLASS_PERIOD:
+ appendStringInfoString(&buffer, "period");
+ break;
+
case OCLASS_CONVERSION:
appendStringInfoString(&buffer, "conversion");
break;
@@ -4952,6 +5003,28 @@ getObjectIdentityParts(const ObjectAddress *object,
break;
}
+ case OCLASS_PERIOD:
+ {
+ HeapTuple perTup;
+ Form_pg_period per;
+
+ perTup = SearchSysCache1(PERIODOID,
+ ObjectIdGetDatum(object->objectId));
+ if (!HeapTupleIsValid(perTup))
+ elog(ERROR, "cache lookup failed for period %u",
+ object->objectId);
+ per = (Form_pg_period) GETSTRUCT(perTup);
+
+ appendStringInfo(&buffer, "%s on ",
+ quote_identifier(NameStr(per->pername)));
+ getRelationIdentity(&buffer, per->perrelid, objname, false);
+ if (objname)
+ *objname = lappend(*objname, pstrdup(NameStr(per->pername)));
+
+ ReleaseSysCache(perTup);
+ break;
+ }
+
case OCLASS_CONVERSION:
{
HeapTuple conTup;
diff --git a/src/backend/catalog/pg_period.c b/src/backend/catalog/pg_period.c
new file mode 100644
index 0000000000..8b7968311b
--- /dev/null
+++ b/src/backend/catalog/pg_period.c
@@ -0,0 +1,105 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_period.c
+ * routines to support manipulation of the pg_period relation
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/catalog/pg_period.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_period.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+
+/*
+ * Delete a single period record.
+ */
+void
+RemovePeriodById(Oid periodId)
+{
+ Relation pg_period;
+ HeapTuple tup;
+
+ pg_period = table_open(PeriodRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(PERIODOID, ObjectIdGetDatum(periodId));
+ if (!HeapTupleIsValid(tup)) /* should not happen */
+ elog(ERROR, "cache lookup failed for period %u", periodId);
+
+ /* Fry the period itself */
+ CatalogTupleDelete(pg_period, &tup->t_self);
+
+ /* Clean up */
+ ReleaseSysCache(tup);
+ table_close(pg_period, RowExclusiveLock);
+}
+
+/*
+ * get_relation_period_oid
+ * Find a period on the specified relation with the specified name.
+ * Returns period's OID.
+ */
+Oid
+get_relation_period_oid(Oid relid, const char *pername, bool missing_ok)
+{
+ Relation pg_period;
+ HeapTuple tuple;
+ SysScanDesc scan;
+ ScanKeyData skey[1];
+ Oid perOid = InvalidOid;
+
+ /*
+ * Fetch the period tuple from pg_period. Periods should have unique
+ * names, but if we find a duplicate then error out.
+ */
+ pg_period = table_open(PeriodRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_period_perrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relid));
+
+ scan = systable_beginscan(pg_period, PeriodRelidNameIndexId, true,
+ NULL, 1, skey);
+
+ while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+ {
+ Form_pg_period period = (Form_pg_period) GETSTRUCT(tuple);
+
+ if (strcmp(NameStr(period->pername), pername) == 0)
+ {
+ if (OidIsValid(perOid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_OBJECT),
+ errmsg("table \"%s\" has multiple periods named \"%s\"",
+ get_rel_name(relid), pername)));
+ perOid = period->oid;
+ }
+ }
+
+ systable_endscan(scan);
+
+ /* If no such period exists, complain */
+ if (!OidIsValid(perOid) && !missing_ok)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("period \"%s\" for table \"%s\" does not exist",
+ pername, get_rel_name(relid))));
+
+ table_close(pg_period, AccessShareLock);
+
+ return perOid;
+}
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index b33065d7bf..775090a6ec 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -449,7 +449,7 @@ T176 Sequence generator support NO supported except for NEXT VALUE FOR
T177 Sequence generator support: simple restart option YES
T178 Identity columns: simple restart option YES
T180 System-versioned tables NO
-T181 Application-time period tables NO
+T181 Application-time period tables YES
T191 Referential action RESTRICT YES
T200 Trigger DDL NO similar but not fully compatible
T201 Comparable data types for referential constraints YES
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index e95dc31bde..2b378acd2a 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -668,6 +668,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
case OCLASS_CAST:
case OCLASS_CONSTRAINT:
+ case OCLASS_PERIOD:
case OCLASS_DEFAULT:
case OCLASS_LANGUAGE:
case OCLASS_LARGEOBJECT:
diff --git a/src/backend/commands/comment.c b/src/backend/commands/comment.c
index 26c4d59202..452af1d413 100644
--- a/src/backend/commands/comment.c
+++ b/src/backend/commands/comment.c
@@ -102,6 +102,16 @@ CommentObject(CommentStmt *stmt)
RelationGetRelationName(relation)),
errdetail_relkind_not_supported(relation->rd_rel->relkind)));
break;
+
+ case OBJECT_PERIOD:
+ /* Periods can only go on tables */
+ if (relation->rd_rel->relkind != RELKIND_RELATION)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a table",
+ RelationGetRelationName(relation))));
+ break;
+
default:
break;
}
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 469a6c2ee9..1b9754e998 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -509,6 +509,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
case OBJECT_DOMCONSTRAINT:
case OBJECT_LARGEOBJECT:
case OBJECT_PARAMETER_ACL:
+ case OBJECT_PERIOD:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
case OBJECT_TABCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..19e6dfd37b 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -970,6 +970,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
case OBJECT_OPCLASS:
case OBJECT_OPERATOR:
case OBJECT_OPFAMILY:
+ case OBJECT_PERIOD:
case OBJECT_POLICY:
case OBJECT_PROCEDURE:
case OBJECT_PUBLICATION:
@@ -1028,6 +1029,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
case OCLASS_CAST:
case OCLASS_COLLATION:
case OCLASS_CONSTRAINT:
+ case OCLASS_PERIOD:
case OCLASS_CONVERSION:
case OCLASS_DEFAULT:
case OCLASS_LANGUAGE:
@@ -2069,6 +2071,7 @@ stringify_grant_objtype(ObjectType objtype)
case OBJECT_OPCLASS:
case OBJECT_OPERATOR:
case OBJECT_OPFAMILY:
+ case OBJECT_PERIOD:
case OBJECT_POLICY:
case OBJECT_PUBLICATION:
case OBJECT_PUBLICATION_NAMESPACE:
@@ -2153,6 +2156,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
case OBJECT_OPERATOR:
case OBJECT_OPFAMILY:
case OBJECT_PARAMETER_ACL:
+ case OBJECT_PERIOD:
case OBJECT_POLICY:
case OBJECT_PUBLICATION:
case OBJECT_PUBLICATION_NAMESPACE:
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ff16e3276..3ddc99581a 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -79,6 +79,7 @@ SecLabelSupportsObjectType(ObjectType objtype)
case OBJECT_OPERATOR:
case OBJECT_OPFAMILY:
case OBJECT_PARAMETER_ACL:
+ case OBJECT_PERIOD:
case OBJECT_POLICY:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 409e470fbd..bce1551bfc 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -43,6 +43,7 @@
#include "catalog/pg_largeobject.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
@@ -149,13 +150,18 @@ static List *on_commits = NIL;
#define AT_PASS_OLD_CONSTR 3 /* re-add existing constraints */
/* We could support a RENAME COLUMN pass here, but not currently used */
#define AT_PASS_ADD_COL 4 /* ADD COLUMN */
-#define AT_PASS_ADD_CONSTR 5 /* ADD constraints (initial examination) */
-#define AT_PASS_COL_ATTRS 6 /* set column attributes, eg NOT NULL */
-#define AT_PASS_ADD_INDEXCONSTR 7 /* ADD index-based constraints */
-#define AT_PASS_ADD_INDEX 8 /* ADD indexes */
-#define AT_PASS_ADD_OTHERCONSTR 9 /* ADD other constraints, defaults */
-#define AT_PASS_MISC 10 /* other stuff */
-#define AT_NUM_PASSES 11
+/*
+ * We must add PERIODs after columns, in case they reference a newly-added column,
+ * and before constraints, in case a newly-added PK/FK references them.
+ */
+#define AT_PASS_ADD_PERIOD 5 /* ADD PERIOD */
+#define AT_PASS_ADD_CONSTR 6 /* ADD constraints (initial examination) */
+#define AT_PASS_COL_ATTRS 7 /* set column attributes, eg NOT NULL */
+#define AT_PASS_ADD_INDEXCONSTR 8 /* ADD index-based constraints */
+#define AT_PASS_ADD_INDEX 9 /* ADD indexes */
+#define AT_PASS_ADD_OTHERCONSTR 10 /* ADD other constraints, defaults */
+#define AT_PASS_MISC 11 /* other stuff */
+#define AT_NUM_PASSES 12
typedef struct AlteredTableInfo
{
@@ -353,6 +359,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
Oid relId, Oid oldRelId, void *arg);
static List *MergeAttributes(List *schema, List *supers, char relpersistence,
bool is_partition, List **supconstr);
+static List *MergePeriods(char *relname, List *periods, List *tableElts, List *supers);
static bool MergeCheckConstraint(List *constraints, char *name, Node *expr);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel);
static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel);
@@ -433,6 +440,8 @@ static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab,
AlterTableUtilityContext *context);
static bool check_for_column_name_collision(Relation rel, const char *colname,
bool if_not_exists);
+static bool check_for_period_name_collision(Relation rel, const char *pername,
+ bool if_not_exists);
static void add_column_datatype_dependency(Oid relid, int32 attnum, Oid typid);
static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid);
static void ATPrepDropNotNull(Relation rel, bool recurse, bool recursing);
@@ -452,6 +461,12 @@ static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,
Node *newDefault, LOCKMODE lockmode);
static ObjectAddress ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
Node *newDefault);
+static ObjectAddress ATExecAddPeriod(Relation rel, PeriodDef *period,
+ AlterTableUtilityContext *context);
+static void ATExecDropPeriod(Relation rel, const char *periodName,
+ DropBehavior behavior,
+ bool recurse, bool recursing,
+ bool missing_ok);
static ObjectAddress ATExecAddIdentity(Relation rel, const char *colName,
Node *def, LOCKMODE lockmode);
static ObjectAddress ATExecSetIdentity(Relation rel, const char *colName,
@@ -651,6 +666,10 @@ static List *GetParentedForeignKeyRefs(Relation partition);
static void ATDetachCheckNoForeignKeyRefs(Relation partition);
static char GetAttributeCompression(Oid atttypid, char *compression);
static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static void AddRelationNewPeriod(Relation rel, PeriodDef *period);
+static void ValidatePeriod(Relation rel, PeriodDef *period);
+static Constraint *make_constraint_for_period(Relation rel, PeriodDef *period);
+static ColumnDef *make_range_column_for_period(PeriodDef *period);
/* ----------------------------------------------------------------
@@ -877,6 +896,26 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
stmt->partbound != NULL,
&old_constraints);
+ /*
+ * Using the column list (including inherited columns),
+ * find the start/end columns for each period.
+ * PERIODs should be inherited too (but aren't yet).
+ */
+ stmt->periods = MergePeriods(relname, stmt->periods, stmt->tableElts, inheritOids);
+
+ /*
+ * For each PERIOD we need to create a GENERATED column,
+ * so add those to tableElts.
+ * These columns are not inherited,
+ * so we don't worry about conflicts in tableElts.
+ */
+ foreach(listptr, stmt->periods)
+ {
+ PeriodDef *period = (PeriodDef *) lfirst(listptr);
+ ColumnDef *col = make_range_column_for_period(period);
+ stmt->tableElts = lappend(stmt->tableElts, col);
+ }
+
/*
* Create a tuple descriptor from the relation schema. Note that this
* deals with column names, types, and NOT NULL constraints, but not
@@ -1263,6 +1302,21 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
AddRelationNewConstraints(rel, NIL, stmt->constraints,
true, true, false, queryString);
+ /*
+ * Create periods for the table. This must come after we create columns
+ * and before we create index constraints. It will automatically create
+ * a CHECK constraint for the period.
+ */
+ foreach(listptr, stmt->periods)
+ {
+ PeriodDef *period = (PeriodDef *) lfirst(listptr);
+
+ /* Don't update the count of check constraints twice */
+ CommandCounterIncrement();
+
+ AddRelationNewPeriod(rel, period);
+ }
+
ObjectAddressSet(address, RelationRelationId, relationId);
/*
@@ -1274,6 +1328,264 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
return address;
}
+/*
+ * make_constraint_for_period
+ *
+ * Builds a CHECK Constraint to ensure start < end.
+ * Returns the CHECK Constraint.
+ * Also fills in period->constraintname if needed.
+ */
+static Constraint *
+make_constraint_for_period(Relation rel, PeriodDef *period)
+{
+ ColumnRef *scol, *ecol;
+ Constraint *constr;
+ TypeCacheEntry *type;
+
+ if (period->constraintname == NULL)
+ period->constraintname = ChooseConstraintName(RelationGetRelationName(rel),
+ period->periodname,
+ "check",
+ RelationGetNamespace(rel),
+ NIL);
+ scol = makeNode(ColumnRef);
+ scol->fields = list_make1(makeString(pstrdup(period->startcolname)));
+ scol->location = 0;
+
+ ecol = makeNode(ColumnRef);
+ ecol->fields = list_make1(makeString(pstrdup(period->endcolname)));
+ ecol->location = 0;
+
+ type = lookup_type_cache(period->coltypid, TYPECACHE_LT_OPR);
+ if (type->lt_opr == InvalidOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("column \"%s\" cannot be used in a PERIOD because its type %s has no less than operator",
+ period->startcolname, format_type_be(period->coltypid))));
+
+ constr = makeNode(Constraint);
+ constr->contype = CONSTR_CHECK;
+ constr->conname = period->constraintname;
+ constr->deferrable = false;
+ constr->initdeferred = false;
+ constr->location = -1;
+ constr->is_no_inherit = false;
+ constr->raw_expr = (Node *) makeSimpleA_Expr(AEXPR_OP,
+ get_opname(type->lt_opr),
+ (Node *) scol,
+ (Node *) ecol,
+ 0);
+ constr->cooked_expr = NULL;
+ constr->skip_validation = false;
+ constr->initially_valid = true;
+
+ return constr;
+}
+
+/*
+ * make_range_column_for_period
+ *
+ * Builds a GENERATED ALWAYS range column based on the PERIOD
+ * start/end columns. Returns the ColumnDef.
+ */
+ColumnDef *
+make_range_column_for_period(PeriodDef *period)
+{
+ char *range_type_namespace;
+ char *range_type_name;
+ ColumnDef *col = makeNode(ColumnDef);
+ ColumnRef *startvar, *endvar;
+ Expr *rangeConstructor;
+
+ if (!get_typname_and_namespace(period->rngtypid, &range_type_name, &range_type_namespace))
+ elog(ERROR, "missing range type %d", period->rngtypid);
+
+ startvar = makeNode(ColumnRef);
+ startvar->fields = list_make1(makeString(pstrdup(period->startcolname)));
+ endvar = makeNode(ColumnRef);
+ endvar->fields = list_make1(makeString(pstrdup(period->endcolname)));
+ rangeConstructor = (Expr *) makeFuncCall(
+ list_make2(makeString(range_type_namespace), makeString(range_type_name)),
+ list_make2(startvar, endvar),
+ COERCE_EXPLICIT_CALL,
+ period->location);
+
+ col->colname = pstrdup(period->periodname);
+ col->typeName = makeTypeName(range_type_name);
+ col->compression = NULL;
+ col->inhcount = 0;
+ col->is_local = true;
+ col->is_not_null = true;
+ col->is_from_type = false;
+ col->storage = 0;
+ col->storage_name = NULL;
+ col->raw_default = (Node *) rangeConstructor;
+ col->cooked_default = NULL;
+ col->identity = 0;
+ col->generated = ATTRIBUTE_GENERATED_STORED;
+ col->collClause = NULL;
+ col->collOid = InvalidOid;
+ col->fdwoptions = NIL;
+ col->location = period->location;
+
+ return col;
+}
+
+/*
+ * ValidatePeriod
+ *
+ * Look up the attributes used by the PERIOD,
+ * make sure they exist, are not system columns,
+ * and have the same type and collation.
+ *
+ * You must have a RowExclusiveLock on pg_attribute
+ * before calling this function.
+ *
+ * Add our findings to these PeriodDef fields:
+ *
+ * coltypid - the type of PERIOD columns.
+ * startattnum - the attnum of the start column.
+ * endattnum - the attnum of the end column.
+ */
+static void
+ValidatePeriod(Relation rel, PeriodDef *period)
+{
+ HeapTuple starttuple;
+ HeapTuple endtuple;
+ Form_pg_attribute atttuple;
+ Oid attcollation;
+
+ /* Find the start column */
+ starttuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), period->startcolname);
+ if (!HeapTupleIsValid(starttuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column \"%s\" of relation \"%s\" does not exist",
+ period->startcolname, RelationGetRelationName(rel))));
+ atttuple = (Form_pg_attribute) GETSTRUCT(starttuple);
+ period->coltypid = atttuple->atttypid;
+ attcollation = atttuple->attcollation;
+ period->startattnum = atttuple->attnum;
+
+ /* Make sure it's not a system column */
+ if (period->startattnum <= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("cannot use system column \"%s\" in period",
+ period->startcolname)));
+
+ /* Find the end column */
+ endtuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), period->endcolname);
+ if (!HeapTupleIsValid(endtuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column \"%s\" of relation \"%s\" does not exist",
+ period->endcolname, RelationGetRelationName(rel))));
+ atttuple = (Form_pg_attribute) GETSTRUCT(endtuple);
+ period->endattnum = atttuple->attnum;
+
+ /* Make sure it's not a system column */
+ if (period->endattnum <= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("cannot use system column \"%s\" in period",
+ period->endcolname)));
+
+ /* Both columns must be of same type */
+ if (period->coltypid != atttuple->atttypid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("start and end columns of period must be of same type")));
+
+ /* Both columns must have the same collation */
+ if (attcollation != atttuple->attcollation)
+ ereport(ERROR,
+ (errcode(ERRCODE_COLLATION_MISMATCH),
+ errmsg("start and end columns of period must have same collation")));
+
+ heap_freetuple(starttuple);
+ heap_freetuple(endtuple);
+}
+
+/*
+ * choose_rangetype_for_period
+ *
+ * Find a suitable range type for operations involving this period.
+ * Use the rangetype option if provided, otherwise try to find a
+ * non-ambiguous existing type.
+ */
+// TODO: This could be static now
+Oid
+choose_rangetype_for_period(PeriodDef *period)
+{
+ Oid rngtypid;
+
+ if (period->rangetypename != NULL)
+ {
+ /* Make sure it exists */
+ rngtypid = TypenameGetTypidExtended(period->rangetypename, false);
+ if (rngtypid == InvalidOid)
+ ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("Range type %s not found", period->rangetypename)));
+
+ /* Make sure it is a range type */
+ if (!type_is_range(rngtypid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("Type %s is not a range type", period->rangetypename)));
+
+ /* Make sure it matches the column type */
+ if (get_range_subtype(rngtypid) != period->coltypid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("Range type %s does not match column type %s",
+ period->rangetypename,
+ format_type_be(period->coltypid))));
+ }
+ else
+ {
+ rngtypid = get_subtype_range(period->coltypid);
+ if (rngtypid == InvalidOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("no range type for %s found for period %s",
+ format_type_be(period->coltypid),
+ period->periodname),
+ errhint("You can define a custom range type with CREATE TYPE")));
+
+ }
+
+ return rngtypid;
+}
+
+static void
+AddRelationNewPeriod(Relation rel, PeriodDef *period)
+{
+ Relation attrelation;
+ Oid conoid;
+ Constraint *constr;
+ List *newconstrs;
+
+ attrelation = table_open(AttributeRelationId, RowExclusiveLock);
+
+ /* Find the GENERATED range column */
+
+ period->rngattnum = get_attnum(RelationGetRelid(rel), period->periodname);
+ if (period->rngattnum == InvalidAttrNumber)
+ elog(ERROR, "missing attribute %s", period->periodname);
+
+ /* The parser has already found period->coltypid */
+
+ constr = make_constraint_for_period(rel, period);
+ newconstrs = AddRelationNewConstraints(rel, NIL, list_make1(constr), false, true, true, NULL);
+ conoid = ((CookedConstraint *) linitial(newconstrs))->conoid;
+
+ /* Save it */
+ StorePeriod(rel, period->periodname, period->startattnum, period->endattnum, period->rngattnum, conoid);
+
+ table_close(attrelation, RowExclusiveLock);
+}
+
/*
* Emit the right error or warning message for a "DROP" command issued on a
* non-existent relation
@@ -3175,6 +3487,168 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
}
+/*----------
+ * MergePeriods
+ * Returns new period list given initial periods and superclasses.
+ *
+ * For now we don't support inheritence with PERIODs,
+ * but we might make it work eventually.
+ *
+ * We can omit lots of checks here and assume MergeAttributes already did them,
+ * for example that child & parents are not a mix of permanent and temp.
+ */
+static List *
+MergePeriods(char *relname, List *periods, List *tableElts, List *supers)
+{
+ ListCell *entry;
+
+ /* If we have a PERIOD then supers must be empty. */
+
+ if (list_length(periods) > 0 && list_length(supers) > 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Inheriting is not supported when a table has a PERIOD")));
+
+ /* If any parent table has a PERIOD, then fail. */
+
+ foreach(entry, supers)
+ {
+ Oid parent = lfirst_oid(entry);
+ Relation relation;
+ Relation pg_period;
+ SysScanDesc scan;
+ ScanKeyData skey[1];
+ HeapTuple tuple;
+
+ /* caller already got lock */
+ relation = table_open(parent, NoLock);
+ pg_period = table_open(PeriodRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_period_perrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(parent));
+
+ scan = systable_beginscan(pg_period, PeriodRelidNameIndexId, true,
+ NULL, 1, skey);
+
+ if (HeapTupleIsValid(tuple = systable_getnext(scan)))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Inheriting from a table with a PERIOD is not supported")));
+
+ systable_endscan(scan);
+ table_close(pg_period, AccessShareLock);
+ table_close(relation, NoLock);
+ }
+
+ /*
+ * Find the start & end columns and get their attno and type.
+ * In the same pass, make sure the period doesn't conflict with any column names.
+ * Also make sure the same period name isn't used more than once.
+ */
+ foreach (entry, periods)
+ {
+ PeriodDef *period = lfirst(entry);
+ ListCell *entry2;
+ int i = 1;
+ Oid startcoltypid = InvalidOid;
+ Oid endcoltypid = InvalidOid;
+ Oid startcolcollation = InvalidOid;
+ Oid endcolcollation = InvalidOid;
+
+ period->startattnum = InvalidAttrNumber;
+ period->endattnum = InvalidAttrNumber;
+
+ if (SystemAttributeByName(period->periodname) != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("period name \"%s\" conflicts with a system column name",
+ period->periodname)));
+
+ foreach (entry2, periods)
+ {
+ PeriodDef *period2 = lfirst(entry2);
+
+ if (period != period2 && strcmp(period->periodname, period2->periodname) == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_OBJECT),
+ errmsg("period name \"%s\" specified more than once",
+ period->periodname)));
+ }
+
+ foreach (entry2, tableElts)
+ {
+ ColumnDef *col = lfirst(entry2);
+ int32 atttypmod;
+ AclResult aclresult;
+
+ if (strcmp(period->periodname, col->colname) == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("period name \"%s\" conflicts with a column name",
+ period->periodname)));
+
+ if (strcmp(period->startcolname, col->colname) == 0)
+ {
+ period->startattnum = i;
+
+ typenameTypeIdAndMod(NULL, col->typeName, &startcoltypid, &atttypmod);
+
+ aclresult = object_aclcheck(TypeRelationId, startcoltypid, GetUserId(), ACL_USAGE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error_type(aclresult, startcoltypid);
+
+ startcolcollation = GetColumnDefCollation(NULL, col, startcoltypid);
+ }
+
+ if (strcmp(period->endcolname, col->colname) == 0)
+ {
+ period->endattnum = i;
+
+ typenameTypeIdAndMod(NULL, col->typeName, &endcoltypid, &atttypmod);
+
+ aclresult = object_aclcheck(TypeRelationId, endcoltypid, GetUserId(), ACL_USAGE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error_type(aclresult, endcoltypid);
+
+ endcolcollation = GetColumnDefCollation(NULL, col, endcoltypid);
+ }
+
+ i++;
+ }
+
+ /* Did we find the columns? */
+ if (period->startattnum == InvalidAttrNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column \"%s\" of relation \"%s\" does not exist",
+ period->startcolname, relname)));
+ if (period->endattnum == InvalidAttrNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column \"%s\" of relation \"%s\" does not exist",
+ period->endcolname, relname)));
+
+ /* Both columns must be of same type */
+ if (startcoltypid != endcoltypid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("start and end columns of period must be of same type")));
+
+ /* Both columns must have the same collation */
+ if (startcolcollation != endcolcollation)
+ ereport(ERROR,
+ (errcode(ERRCODE_COLLATION_MISMATCH),
+ errmsg("start and end columns of period must have same collation")));
+
+ period->coltypid = startcoltypid;
+ period->rngtypid = choose_rangetype_for_period(period);
+ }
+
+ return periods;
+}
+
/*
* MergeCheckConstraint
* Try to merge an inherited CHECK constraint with previous ones
@@ -4217,12 +4691,12 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
* existing query plans. On the assumption it's not used for such, we
* don't have to reject pending AFTER triggers, either.
*
- * Also, since we don't have an AlterTableUtilityContext, this cannot be
+ * Also, if you don't pass an AlterTableUtilityContext, this cannot be
* used for any subcommand types that require parse transformation or
* could generate subcommands that have to be passed to ProcessUtility.
*/
void
-AlterTableInternal(Oid relid, List *cmds, bool recurse)
+AlterTableInternal(Oid relid, List *cmds, bool recurse, AlterTableUtilityContext *context)
{
Relation rel;
LOCKMODE lockmode = AlterTableGetLockLevel(cmds);
@@ -4231,7 +4705,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)
EventTriggerAlterTableRelid(relid);
- ATController(NULL, rel, cmds, recurse, lockmode, NULL);
+ ATController(NULL, rel, cmds, recurse, lockmode, context);
}
/*
@@ -4324,6 +4798,8 @@ AlterTableGetLockLevel(List *cmds)
case AT_EnableReplicaRule: /* may change SELECT rules */
case AT_EnableRule: /* may change SELECT rules */
case AT_DisableRule: /* may change SELECT rules */
+ case AT_AddPeriod: /* shares namespace with columns, adds constraint */
+ case AT_DropPeriod:
cmd_lockmode = AccessExclusiveLock;
break;
@@ -4647,6 +5123,14 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
/* This command never recurses */
pass = AT_PASS_ADD_OTHERCONSTR;
break;
+ case AT_AddPeriod: /* ALTER TABLE ... ADD PERIOD FOR name (start, end) */
+ ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
+ pass = AT_PASS_ADD_PERIOD;
+ break;
+ case AT_DropPeriod: /* ALTER TABLE ... DROP PERIOD FOR name */
+ ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
+ pass = AT_PASS_DROP;
+ break;
case AT_AddIdentity:
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_VIEW | ATT_FOREIGN_TABLE);
/* This command never recurses */
@@ -5042,6 +5526,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
case AT_CookedColumnDefault: /* add a pre-cooked default */
address = ATExecCookedColumnDefault(rel, cmd->num, cmd->def);
break;
+ case AT_AddPeriod:
+ address = ATExecAddPeriod(rel, (PeriodDef *) cmd->def, context);
+ break;
+ case AT_DropPeriod:
+ ATExecDropPeriod(rel, cmd->name, cmd->behavior,
+ false, false,
+ cmd->missing_ok);
+ break;
case AT_AddIdentity:
cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
cur_pass, context);
@@ -6182,6 +6674,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
case AT_AddColumn:
case AT_AddColumnToView:
return "ADD COLUMN";
+ case AT_AddPeriod:
+ return "ADD PERIOD";
case AT_ColumnDefault:
case AT_CookedColumnDefault:
return "ALTER COLUMN ... SET DEFAULT";
@@ -6205,6 +6699,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
return "ALTER COLUMN ... SET COMPRESSION";
case AT_DropColumn:
return "DROP COLUMN";
+ case AT_DropPeriod:
+ return "DROP PERIOD";
case AT_AddIndex:
case AT_ReAddIndex:
return NULL; /* not real grammar */
@@ -7211,14 +7707,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
/*
* If a new or renamed column will collide with the name of an existing
* column and if_not_exists is false then error out, else do nothing.
+ *
+ * See also check_for_period_name_collision.
*/
static bool
check_for_column_name_collision(Relation rel, const char *colname,
bool if_not_exists)
{
- HeapTuple attTuple;
+ HeapTuple attTuple, perTuple;
int attnum;
+ /* If the name exists as a period, we're done. */
+ perTuple = SearchSysCache2(PERIODNAME,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ PointerGetDatum(colname));
+ if (HeapTupleIsValid(perTuple))
+ {
+ ReleaseSysCache(perTuple);
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("column name \"%s\" conflicts with a period name",
+ colname)));
+ }
+
/*
* this test is deliberately not attisdropped-aware, since if one tries to
* add a column matching a dropped column name, it's gonna fail anyway.
@@ -7262,6 +7773,78 @@ check_for_column_name_collision(Relation rel, const char *colname,
return true;
}
+/*
+ * If a new period name will collide with the name of an existing column or
+ * period [and if_not_exists is false] then error out, else do nothing.
+ *
+ * See also check_for_column_name_collision.
+ */
+static bool
+check_for_period_name_collision(Relation rel, const char *pername,
+ bool if_not_exists)
+{
+ HeapTuple attTuple, perTuple;
+ int attnum;
+
+ /* TODO: implement IF [NOT] EXISTS for periods */
+ Assert(!if_not_exists);
+
+ /* If there is already a period with this name, then we're done. */
+ perTuple = SearchSysCache2(PERIODNAME,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ PointerGetDatum(pername));
+ if (HeapTupleIsValid(perTuple))
+ {
+ if (if_not_exists)
+ {
+ ReleaseSysCache(perTuple);
+
+ ereport(NOTICE,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("period \"%s\" of relation \"%s\" already exists, skipping",
+ pername, RelationGetRelationName(rel))));
+ return false;
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("period \"%s\" of relation \"%s\" already exists",
+ pername, RelationGetRelationName(rel))));
+ }
+
+ /*
+ * this test is deliberately not attisdropped-aware, since if one tries to
+ * add a column matching a dropped column name, it's gonna fail anyway.
+ *
+ * XXX: Does this hold for periods?
+ */
+ attTuple = SearchSysCache2(ATTNAME,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ PointerGetDatum(pername));
+ if (HeapTupleIsValid(attTuple))
+ {
+ attnum = ((Form_pg_attribute) GETSTRUCT(attTuple))->attnum;
+ ReleaseSysCache(attTuple);
+
+ /*
+ * We throw a different error message for conflicts with system column
+ * names, since they are normally not shown and the user might otherwise
+ * be confused about the reason for the conflict.
+ */
+ if (attnum <= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("period name \"%s\" conflicts with a system column name",
+ pername)));
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("period name \"%s\" conflicts with a column name",
+ pername)));
+ }
+
+ return true;
+}
+
/*
* Install a column's dependency on its datatype.
*/
@@ -7790,6 +8373,154 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
return address;
}
+/*
+ * ALTER TABLE ADD PERIOD
+ *
+ * Return the address of the period.
+ */
+static ObjectAddress
+ATExecAddPeriod(Relation rel, PeriodDef *period, AlterTableUtilityContext *context)
+{
+ Relation attrelation;
+ ObjectAddress address = InvalidObjectAddress;
+ Constraint *constr;
+ Oid conoid, periodoid;
+ List *cmds = NIL;
+ AlterTableCmd *cmd;
+
+ /*
+ * PERIOD FOR SYSTEM_TIME is not yet implemented, but make sure no one uses
+ * the name.
+ */
+ if (strcmp(period->periodname, "system_time") == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("PERIOD FOR SYSTEM_TIME is not supported")));
+
+ /* The period name must not already exist */
+ (void) check_for_period_name_collision(rel, period->periodname, false);
+
+ /* Parse options */
+ transformPeriodOptions(period);
+
+ attrelation = table_open(AttributeRelationId, RowExclusiveLock);
+ ValidatePeriod(rel, period);
+
+ period->rngtypid = choose_rangetype_for_period(period);
+
+ /* Make the CHECK constraint */
+ constr = make_constraint_for_period(rel, period);
+ cmd = makeNode(AlterTableCmd);
+ cmd->subtype = AT_AddConstraint;
+ cmd->def = (Node *) constr;
+ cmds = lappend(cmds, cmd);
+ AlterTableInternal(RelationGetRelid(rel), cmds, true, context);
+ conoid = get_relation_constraint_oid(RelationGetRelid(rel), period->constraintname, false);
+
+ /* Make the range column */
+ ColumnDef *rangecol = make_range_column_for_period(period);
+ cmd = makeNode(AlterTableCmd);
+ cmd->subtype = AT_AddColumn;
+ cmd->def = (Node *) rangecol;
+ cmd->name = period->periodname;
+ cmd->recurse = false; /* no, let the PERIOD recurse instead */
+ AlterTableInternal(RelationGetRelid(rel), list_make1(cmd), true, context);
+ period->rngattnum = get_attnum(RelationGetRelid(rel), period->periodname);
+ if (period->rngattnum == InvalidAttrNumber)
+ elog(ERROR, "missing attribute %s", period->periodname);
+
+ /* Save the Period */
+ periodoid = StorePeriod(rel, period->periodname, period->startattnum, period->endattnum, period->rngattnum, conoid);
+
+ ObjectAddressSet(address, PeriodRelationId, periodoid);
+
+ table_close(attrelation, RowExclusiveLock);
+
+ return address;
+}
+
+/*
+ * ALTER TABLE DROP PERIOD
+ *
+ * Like DROP COLUMN, we can't use the normal ALTER TABLE recursion mechanism.
+ */
+static void
+ATExecDropPeriod(Relation rel, const char *periodName,
+ DropBehavior behavior,
+ bool recurse, bool recursing,
+ bool missing_ok)
+{
+ Relation pg_period;
+ Form_pg_period period;
+ SysScanDesc scan;
+ ScanKeyData key;
+ HeapTuple tuple;
+ bool found = false;
+
+ /* At top level, permission check was done in ATPrepCmd, else do it */
+ if (recursing)
+ ATSimplePermissions(AT_DropPeriod, rel, ATT_TABLE);
+
+ pg_period = table_open(PeriodRelationId, RowExclusiveLock);
+
+ /*
+ * Find and drop the target period
+ */
+ ScanKeyInit(&key,
+ Anum_pg_period_perrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(rel)));
+ scan = systable_beginscan(pg_period, PeriodRelidNameIndexId,
+ true, NULL, 1, &key);
+
+ while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+ {
+ ObjectAddress perobj;
+
+ period = (Form_pg_period) GETSTRUCT(tuple);
+
+ if (strcmp(NameStr(period->pername), periodName) != 0)
+ continue;
+
+ /*
+ * Perform the actual period deletion
+ */
+ perobj.classId = PeriodRelationId;
+ perobj.objectId = period->oid;
+ perobj.objectSubId = 0;
+
+ performDeletion(&perobj, behavior, 0);
+
+ found = true;
+
+ /* period found and dropped -- no need to keep looping */
+ break;
+ }
+
+ systable_endscan(scan);
+
+ if (!found)
+ {
+ if (!missing_ok)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("period \"%s\" on relation \"%s\" does not exist",
+ periodName, RelationGetRelationName(rel))));
+ }
+ else
+ {
+ ereport(NOTICE,
+ (errmsg("period \"%s\" on relation \"%s\" does not exist, skipping",
+ periodName, RelationGetRelationName(rel))));
+ table_close(pg_period, RowExclusiveLock);
+ return;
+ }
+ }
+
+ table_close(pg_period, RowExclusiveLock);
+}
+
/*
* ALTER TABLE ALTER COLUMN ADD IDENTITY
*
@@ -12939,6 +13670,15 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
RememberConstraintForRebuilding(foundObject.objectId, tab);
break;
+ case OCLASS_PERIOD:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter type of a column used by a period"),
+ errdetail("%s depends on column \"%s\"",
+ getObjectDescription(&foundObject, false),
+ colName)));
+ break;
+
case OCLASS_REWRITE:
/* XXX someday see if we can cope with revising views */
ereport(ERROR,
@@ -14999,7 +15739,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)
EventTriggerAlterTableStart((Node *) stmt);
/* OID is set by AlterTableInternal */
- AlterTableInternal(lfirst_oid(l), cmds, false);
+ AlterTableInternal(lfirst_oid(l), cmds, false, NULL);
EventTriggerAlterTableEnd();
}
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 9bd77546b9..be4637287f 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -169,7 +169,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
}
/* EventTriggerAlterTableStart called by ProcessUtilitySlow */
- AlterTableInternal(viewOid, atcmds, true);
+ AlterTableInternal(viewOid, atcmds, true, NULL);
/* Make the new view columns visible */
CommandCounterIncrement();
@@ -201,7 +201,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
atcmds = list_make1(atcmd);
/* EventTriggerAlterTableStart called by ProcessUtilitySlow */
- AlterTableInternal(viewOid, atcmds, true);
+ AlterTableInternal(viewOid, atcmds, true, NULL);
/*
* There is very little to do here to update the view's dependencies.
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index fd3349832d..aa1965e913 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1641,6 +1641,9 @@ exprLocation(const Node *expr)
case T_Constraint:
loc = ((const Constraint *) expr)->location;
break;
+ case T_PeriodDef:
+ loc = ((const PeriodDef *) expr)->location;
+ break;
case T_FunctionParameter:
/* just use typename's location */
loc = exprLocation((Node *) ((const FunctionParameter *) expr)->argType);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c3ab65896f..7ca9682ff4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -595,7 +595,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <keyword> col_name_keyword reserved_keyword
%type <keyword> bare_label_keyword
-%type <node> TableConstraint TableLikeClause
+%type <node> TableConstraint TableLikeClause TablePeriod
%type <ival> TableLikeOptionList TableLikeOption
%type <str> column_compression opt_column_compression column_storage opt_column_storage
%type <list> ColQualList
@@ -2603,6 +2603,24 @@ alter_table_cmd:
n->def = (Node *) $4;
$$ = (Node *) n;
}
+ /* ALTER TABLE <name> ADD PERIOD FOR <name> (<name>, <name>) */
+ | ADD_P TablePeriod
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_AddPeriod;
+ n->def = $2;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE <name> DROP PERIOD FOR <name> [RESTRICT|CASCADE] */
+ | DROP PERIOD FOR name opt_drop_behavior
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_DropPeriod;
+ n->name = $4;
+ n->behavior = $5;
+ n->missing_ok = false;
+ $$ = (Node *)n;
+ }
/* ALTER TABLE <name> ADD CONSTRAINT ... */
| ADD_P TableConstraint
{
@@ -3714,8 +3732,10 @@ TableElement:
columnDef { $$ = $1; }
| TableLikeClause { $$ = $1; }
| TableConstraint { $$ = $1; }
+ | TablePeriod { $$ = $1; }
;
+
TypedTableElement:
columnOptions { $$ = $1; }
| TableConstraint { $$ = $1; }
@@ -4065,6 +4085,19 @@ TableLikeOption:
;
+TablePeriod:
+ PERIOD FOR name '(' name ',' name ')' opt_definition
+ {
+ PeriodDef *n = makeNode(PeriodDef);
+ n->periodname = $3;
+ n->startcolname = $5;
+ n->endcolname = $7;
+ n->options = $9;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ ;
+
/* ConstraintElem specifies constraint syntax which is not embedded into
* a column definition. ColConstraintElem specifies the embedded form.
* - thomas 1997-12-03
@@ -7083,6 +7116,14 @@ CommentStmt:
n->comment = $9;
$$ = (Node *) n;
}
+ | COMMENT ON PERIOD any_name IS comment_text
+ {
+ CommentStmt *n = makeNode(CommentStmt);
+ n->objtype = OBJECT_PERIOD;
+ n->object = (Node *) $4;
+ n->comment = $6;
+ $$ = (Node *) n;
+ }
| COMMENT ON LARGE_P OBJECT_P NumericOnly IS comment_text
{
CommentStmt *n = makeNode(CommentStmt);
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 04ca087973..c90bcd058f 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -80,6 +80,7 @@ typedef struct
bool isforeign; /* true if CREATE/ALTER FOREIGN TABLE */
bool isalter; /* true if altering existing table */
List *columns; /* ColumnDef items */
+ List *periods; /* PeriodDef items */
List *ckconstraints; /* CHECK constraints */
List *fkconstraints; /* FOREIGN KEY constraints */
List *ixconstraints; /* index-creating constraints */
@@ -110,6 +111,8 @@ typedef struct
static void transformColumnDefinition(CreateStmtContext *cxt,
ColumnDef *column);
+static void transformTablePeriod(CreateStmtContext *cxt,
+ PeriodDef *period);
static void transformTableConstraint(CreateStmtContext *cxt,
Constraint *constraint);
static void transformTableLikeClause(CreateStmtContext *cxt,
@@ -241,6 +244,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
cxt.inhRelations = stmt->inhRelations;
cxt.isalter = false;
cxt.columns = NIL;
+ cxt.periods = NIL;
cxt.ckconstraints = NIL;
cxt.fkconstraints = NIL;
cxt.ixconstraints = NIL;
@@ -280,6 +284,10 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
transformColumnDefinition(&cxt, (ColumnDef *) element);
break;
+ case T_PeriodDef:
+ transformTablePeriod(&cxt, (PeriodDef *) element);
+ break;
+
case T_Constraint:
transformTableConstraint(&cxt, (Constraint *) element);
break;
@@ -347,6 +355,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
* Output results.
*/
stmt->tableElts = cxt.columns;
+ stmt->periods = cxt.periods;
stmt->constraints = cxt.ckconstraints;
result = lappend(cxt.blist, stmt);
@@ -866,6 +875,113 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
}
}
+void
+transformPeriodOptions(PeriodDef *period)
+{
+ ListCell *option;
+ DefElem *dconstraintname = NULL;
+ DefElem *drangetypename = NULL;
+
+ foreach(option, period->options)
+ {
+ DefElem *defel = (DefElem *) lfirst(option);
+
+ if (strcmp(defel->defname, "check_constraint_name") == 0)
+ {
+ if (dconstraintname)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+ dconstraintname = defel;
+ }
+ else if (strcmp(defel->defname, "rangetype") == 0)
+ {
+ if (drangetypename)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+ drangetypename = defel;
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("option \"%s\" not recognized", defel->defname)));
+ }
+
+ if (dconstraintname != NULL)
+ period->constraintname = defGetString(dconstraintname);
+ else
+ period->constraintname = NULL;
+
+ if (drangetypename != NULL)
+ period->rangetypename = defGetString(drangetypename);
+ else
+ period->rangetypename = NULL;
+}
+
+/*
+ * transformTablePeriod
+ * transform a PeriodDef node within CREATE TABLE or ALTER TABLE
+ * TODO: Does ALTER TABLE really call us?? It doesn't seem like it does. Maybe it should. Did it in Vik's original patch?
+ */
+static void
+transformTablePeriod(CreateStmtContext *cxt, PeriodDef *period)
+{
+ // Oid coltypid;
+ // ColumnDef *col;
+ // ListCell *columns;
+
+ if (strcmp(period->periodname, "system_time") == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("PERIOD FOR SYSTEM_TIME is not supported"),
+ parser_errposition(cxt->pstate,
+ period->location)));
+
+ if (strcmp(period->startcolname, period->endcolname) == 0)
+ ereport(ERROR, (errmsg("column \"%s\" can't be the start and end column for period \"%s\"",
+ period->startcolname, period->periodname)));
+
+ /*
+ * Determine the column info and range type so that transformIndexConstraints
+ * knows how to create PRIMARY KEY/UNIQUE constraints using this PERIOD.
+ */
+ transformPeriodOptions(period);
+
+ /*
+ * Find a suitable range type for operations involving this period.
+ * Use the rangetype option if provided, otherwise try to find a
+ * non-ambiguous existing type.
+ */
+
+#ifdef asdsdfa
+ /* First find out the type of the period's columns */
+ // TODO: I'm doing this in DefineRelation now,
+ // which seems like a better place since it knows about inherited columns.
+ period->coltypid = InvalidOid;
+ period->rngtypid = InvalidOid;
+ foreach (columns, cxt->columns)
+ {
+ col = (ColumnDef *) lfirst(columns);
+ if (strcmp(col->colname, period->startcolname) == 0)
+ {
+ coltypid = typenameTypeId(cxt->pstate, col->typeName);
+ break;
+ }
+ }
+ if (coltypid == InvalidOid)
+ ereport(ERROR, (errmsg("column \"%s\" of relation \"%s\" does not exist",
+ period->startcolname, cxt->relation->relname)));
+
+ period->coltypid = coltypid;
+
+ /* Now make sure it matches rangetypename or we can find a matching range */
+ period->rngtypid = choose_rangetype_for_period(period);
+#endif
+
+ cxt->periods = lappend(cxt->periods, period);
+}
+
/*
* transformTableConstraint
* transform a Constraint node within CREATE TABLE or ALTER TABLE
@@ -1539,6 +1655,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
Oid keycoltype;
Datum datum;
bool isnull;
+ PeriodDef *period;
if (constraintOid)
*constraintOid = InvalidOid;
@@ -1596,6 +1713,11 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
index->if_not_exists = false;
index->reset_default_tblspc = false;
+ /* Copy the period */
+ period = makeNode(PeriodDef);
+ period->oid = idxrec->indperiod;
+ index->period = period;
+
/*
* We don't try to preserve the name of the source index; instead, just
* let DefineIndex() choose a reasonable name. (If we tried to preserve
@@ -2986,6 +3108,10 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
}
}
+ /* take care of the period */
+ if (stmt->period)
+ stmt->period->oid = get_period_oid(relid, stmt->period->periodname, false);
+
/*
* Check that only the base rel is mentioned. (This should be dead code
* now that add_missing_from is history.)
@@ -3443,6 +3569,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
cxt.inhRelations = NIL;
cxt.isalter = true;
cxt.columns = NIL;
+ cxt.periods = NIL;
cxt.ckconstraints = NIL;
cxt.fkconstraints = NIL;
cxt.ixconstraints = NIL;
@@ -3504,6 +3631,15 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
(int) nodeTag(cmd->def));
break;
+ case AT_AddPeriod:
+ {
+ newcmds = lappend(newcmds, cmd);
+ // Why not call transformTablePeriod here?
+ // Ah because it looks at cxt->columns
+ // and in an ALTER statement the columns might already exist (or not).
+ break;
+ }
+
case AT_AlterColumnType:
{
ColumnDef *def = castNode(ColumnDef, cmd->def);
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 353e9f2cad..9ebfea971e 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -30,6 +30,7 @@
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
#include "catalog/pg_operator.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_range.h"
#include "catalog/pg_statistic.h"
@@ -1021,6 +1022,68 @@ get_attoptions(Oid relid, int16 attnum)
return result;
}
+/* ---------- PG_PERIOD CACHE ---------- */
+
+/*
+ * get_periodname - given its OID, look up a period
+ *
+ * If missing_ok is false, throw an error if the period is not found.
+ * If true, just return InvalidOid.
+ */
+char *
+get_periodname(Oid periodid, bool missing_ok)
+{
+ HeapTuple tp;
+
+ tp = SearchSysCache1(PERIODOID,
+ ObjectIdGetDatum(periodid));
+ if (HeapTupleIsValid(tp))
+ {
+ Form_pg_period period_tup = (Form_pg_period) GETSTRUCT(tp);
+ char *result;
+
+ result = pstrdup(NameStr(period_tup->pername));
+ ReleaseSysCache(tp);
+ return result;
+ }
+
+ if (!missing_ok)
+ elog(ERROR, "cache lookup failed for period %d",
+ periodid);
+ return NULL;
+}
+
+/*
+ * get_period_oid - gets its relation and name, look up a period
+ *
+ * If missing_ok is false, throw an error if the cast is not found. If
+ * true, just return InvalidOid.
+ */
+Oid
+get_period_oid(Oid relid, const char *periodname, bool missing_ok)
+{
+ HeapTuple tp;
+
+ tp = SearchSysCache2(PERIODNAME,
+ ObjectIdGetDatum(relid),
+ PointerGetDatum(periodname));
+
+ if (HeapTupleIsValid(tp))
+ {
+ Form_pg_period period_tup = (Form_pg_period) GETSTRUCT(tp);
+ Oid result;
+
+ result = period_tup->oid;
+ ReleaseSysCache(tp);
+ return result;
+ }
+
+ if (!missing_ok)
+ elog(ERROR, "cache lookup failed for period %s",
+ periodname);
+ return InvalidOid;
+}
+
/* ---------- PG_CAST CACHE ---------- */
/*
@@ -2172,6 +2235,32 @@ get_typname(Oid typid)
return NULL;
}
+/*
+ * get_typname_and_namespace
+ *
+ * Returns the name and namespace of a given type
+ *
+ * Returns true if one found, or false if not.
+ */
+bool
+get_typname_and_namespace(Oid typid, char **typname, char **typnamespace)
+{
+ HeapTuple tp;
+
+ tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+ if (HeapTupleIsValid(tp))
+ {
+ Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+ *typname = pstrdup(NameStr(typtup->typname));
+ *typnamespace = get_namespace_name(typtup->typnamespace);
+ ReleaseSysCache(tp);
+ return *typnamespace;
+ }
+ else
+ return false;
+}
+
/*
* get_typlen
*
@@ -3482,6 +3571,30 @@ get_multirange_range(Oid multirangeOid)
return InvalidOid;
}
+Oid
+get_subtype_range(Oid subtypeOid)
+{
+ CatCList *catlist;
+ Oid result = InvalidOid;
+
+ catlist = SearchSysCacheList1(RANGESUBTYPE, ObjectIdGetDatum(subtypeOid));
+
+ if (catlist->n_members == 1)
+ {
+ HeapTuple tuple = &catlist->members[0]->tuple;
+ Form_pg_range rngtup = (Form_pg_range) GETSTRUCT(tuple);
+ result = rngtup->rngtypid;
+ ReleaseCatCacheList(catlist);
+ }
+ else if (catlist->n_members > 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDETERMINATE_DATATYPE),
+ errmsg("ambiguous range for type %s",
+ format_type_be(subtypeOid))));
+
+ return result;
+}
+
/* ---------- PG_INDEX CACHE ---------- */
/*
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 4e4a34bde8..2a3503a289 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -49,6 +49,7 @@
#include "catalog/pg_opfamily.h"
#include "catalog/pg_parameter_acl.h"
#include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_publication.h"
#include "catalog/pg_publication_namespace.h"
@@ -422,6 +423,19 @@ static const struct cachedesc cacheinfo[] = {
KEY(Anum_pg_partitioned_table_partrelid),
32
},
+ [PERIODNAME] = {
+ PeriodRelationId,
+ PeriodRelidNameIndexId,
+ KEY(Anum_pg_period_perrelid,
+ Anum_pg_period_pername),
+ 32
+ },
+ [PERIODOID] = {
+ PeriodRelationId,
+ PeriodObjectIndexId,
+ KEY(Anum_pg_period_oid),
+ 32
+ },
[PROCNAMEARGSNSP] = {
ProcedureRelationId,
ProcedureNameArgsNspIndexId,
@@ -480,6 +494,13 @@ static const struct cachedesc cacheinfo[] = {
KEY(Anum_pg_range_rngmultitypid),
4
},
+ [RANGESUBTYPE] = {
+ RangeRelationId,
+ RangeSubTypidTypidIndexId,
+ KEY(Anum_pg_range_rngsubtype,
+ Anum_pg_range_rngtypid),
+ 4
+ },
[RANGETYPE] = {
RangeRelationId,
RangeTypidIndexId,
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 39ebcfec32..191eba1990 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3517,6 +3517,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
strcmp(type, "DATABASE PROPERTIES") == 0 ||
strcmp(type, "DEFAULT") == 0 ||
strcmp(type, "FK CONSTRAINT") == 0 ||
+ strcmp(type, "PERIOD") == 0 ||
strcmp(type, "INDEX") == 0 ||
strcmp(type, "RULE") == 0 ||
strcmp(type, "TRIGGER") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a95e1d3696..5b81a7e5a0 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6325,6 +6325,7 @@ getTables(Archive *fout, int *numTables)
int i_reltype;
int i_relowner;
int i_relchecks;
+ int i_nperiod;
int i_relhasindex;
int i_relhasrules;
int i_relpages;
@@ -6402,6 +6403,14 @@ getTables(Archive *fout, int *numTables)
appendPQExpBufferStr(query,
"c.relhasoids, ");
+ /* In PG16 upwards we have PERIODs. */
+ if (fout->remoteVersion >= 160000)
+ appendPQExpBufferStr(query,
+ "(SELECT count(*) FROM pg_period WHERE perrelid = c.oid) AS nperiods, ");
+ else
+ appendPQExpBufferStr(query,
+ "0 AS nperiods, ");
+
if (fout->remoteVersion >= 90300)
appendPQExpBufferStr(query,
"c.relispopulated, ");
@@ -6539,6 +6548,7 @@ getTables(Archive *fout, int *numTables)
i_reltype = PQfnumber(res, "reltype");
i_relowner = PQfnumber(res, "relowner");
i_relchecks = PQfnumber(res, "relchecks");
+ i_nperiod = PQfnumber(res, "nperiods");
i_relhasindex = PQfnumber(res, "relhasindex");
i_relhasrules = PQfnumber(res, "relhasrules");
i_relpages = PQfnumber(res, "relpages");
@@ -6622,6 +6632,7 @@ getTables(Archive *fout, int *numTables)
}
tblinfo[i].reltablespace = pg_strdup(PQgetvalue(res, i, i_reltablespace));
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
+ tblinfo[i].nperiod = atoi(PQgetvalue(res, i, i_nperiod));
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
tblinfo[i].relpersistence = *(PQgetvalue(res, i, i_relpersistence));
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
@@ -8351,7 +8362,7 @@ getTransforms(Archive *fout, int *numTransforms)
/*
* getTableAttrs -
* for each interesting table, read info about its attributes
- * (names, types, default values, CHECK constraints, etc)
+ * (names, types, default values, CHECK constraints, PERIODs, etc)
*
* modifies tblinfo
*/
@@ -8401,6 +8412,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
for (int i = 0; i < numTables; i++)
{
TableInfo *tbinfo = &tblinfo[i];
+ int ndumpablechecks; /* number of CHECK constraints that do
+ not belong to a period */
/* Don't bother to collect info for sequences */
if (tbinfo->relkind == RELKIND_SEQUENCE)
@@ -8415,7 +8428,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
appendPQExpBufferChar(tbloids, ',');
appendPQExpBuffer(tbloids, "%u", tbinfo->dobj.catId.oid);
- if (tbinfo->ncheck > 0)
+ ndumpablechecks = tbinfo->ncheck - tbinfo->nperiod;
+ if (ndumpablechecks > 0)
{
/* Also make a list of the ones with check constraints */
if (checkoids->len > 1) /* do we have more than the '{'? */
@@ -8774,15 +8788,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
pg_log_info("finding table check constraints");
resetPQExpBuffer(q);
- appendPQExpBuffer(q,
- "SELECT c.tableoid, c.oid, conrelid, conname, "
- "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
- "conislocal, convalidated "
- "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
- "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
- "WHERE contype = 'c' "
- "ORDER BY c.conrelid, c.conname",
- checkoids->data);
+ if (fout->remoteVersion >= 160000)
+ {
+ /*
+ * PERIODs were added in v17 and we don't dump CHECK
+ * constraints for them.
+ */
+ appendPQExpBuffer(q,
+ "SELECT c.tableoid, c.oid, conrelid, conname, "
+ "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
+ "conislocal, convalidated "
+ "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+ "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
+ "WHERE contype = 'c' "
+ " AND NOT EXISTS (SELECT FROM pg_period "
+ " WHERE (perrelid, perconstraint) = (conrelid, c.oid)) "
+ "ORDER BY c.conrelid, c.conname",
+ checkoids->data);
+ }
+ else
+ {
+ appendPQExpBuffer(q,
+ "SELECT c.tableoid, c.oid, conrelid, conname, "
+ "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
+ "conislocal, convalidated "
+ "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+ "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
+ "WHERE contype = 'c' "
+ "ORDER BY c.conrelid, c.conname",
+ checkoids->data);
+ }
res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
@@ -8804,6 +8839,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
Oid conrelid = atooid(PQgetvalue(res, j, i_conrelid));
TableInfo *tbinfo = NULL;
int numcons;
+ int ndumpablechecks;
/* Count rows for this table */
for (numcons = 1; numcons < numConstrs - j; numcons++)
@@ -8823,12 +8859,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
if (curtblindx >= numTables)
pg_fatal("unrecognized table OID %u", conrelid);
- if (numcons != tbinfo->ncheck)
+ ndumpablechecks = tbinfo->ncheck - tbinfo->nperiod;
+ if (numcons != ndumpablechecks)
{
pg_log_error(ngettext("expected %d check constraint on table \"%s\" but found %d",
"expected %d check constraints on table \"%s\" but found %d",
- tbinfo->ncheck),
- tbinfo->ncheck, tbinfo->dobj.name, numcons);
+ ndumpablechecks),
+ ndumpablechecks, tbinfo->dobj.name, numcons);
pg_log_error_hint("The system catalogs might be corrupted.");
exit_nicely(1);
}
@@ -8887,6 +8924,79 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQclear(res);
}
+ for (int i = 0; i < numTables; i++)
+ {
+ TableInfo *tbinfo = &tblinfo[i];
+
+ /*
+ * Get info about PERIOD definitions
+ */
+ if (tbinfo->nperiod > 0)
+ {
+ PeriodInfo *periods;
+ int numPeriods;
+ int j;
+
+ /* We shouldn't have any periods before v17 */
+ Assert(fout->remoteVersion >= 160000);
+
+ pg_log_info("finding periods for table \"%s.%s\"",
+ tbinfo->dobj.namespace->dobj.name,
+ tbinfo->dobj.name);
+
+ resetPQExpBuffer(q);
+ appendPQExpBuffer(q,
+ "SELECT p.tableoid, p.oid, p.pername, "
+ " sa.attname AS perstart, ea.attname AS perend, "
+ " r.typname AS rngtype, "
+ " c.conname AS conname "
+ "FROM pg_catalog.pg_period AS p "
+ "JOIN pg_catalog.pg_attribute AS sa ON (sa.attrelid, sa.attnum) = (p.perrelid, p.perstart) "
+ "JOIN pg_catalog.pg_attribute AS ea ON (ea.attrelid, ea.attnum) = (p.perrelid, p.perend) "
+ "JOIN pg_catalog.pg_type AS r ON r.oid = p.perrngtype "
+ "JOIN pg_catalog.pg_constraint AS c ON c.oid = p.perconstraint "
+ "WHERE p.perrelid = '%u'::pg_catalog.oid "
+ "ORDER BY p.pername",
+ tbinfo->dobj.catId.oid);
+
+ res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
+
+ /*
+ * If we didn't get the number of rows we thought we were going to,
+ * then those JOINs didn't work.
+ */
+ numPeriods = PQntuples(res);
+ if (numPeriods != tbinfo->nperiod)
+ {
+ pg_log_info(ngettext("expected %d period on table \"%s\" but found %d",
+ "expected %d periods on table \"%s\" but found %d",
+ tbinfo->nperiod),
+ tbinfo->nperiod, tbinfo->dobj.name, numPeriods);
+ pg_log_info("(The system catalogs might be corrupted.)");
+ exit_nicely(1);
+ }
+
+ periods = (PeriodInfo *) pg_malloc(numPeriods * sizeof(PeriodInfo));
+ tbinfo->periods = periods;
+
+ for (j = 0; j < numPeriods; j++)
+ {
+ periods[j].dobj.objType = DO_PERIOD;
+ periods[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, 0));
+ periods[j].dobj.catId.oid = atooid(PQgetvalue(res, j, 1));
+ AssignDumpId(&periods[j].dobj);
+ periods[j].dobj.name = pg_strdup(PQgetvalue(res, j, 2));
+ periods[j].dobj.namespace = tbinfo->dobj.namespace;
+ periods[j].pertable = tbinfo;
+ periods[j].perstart = pg_strdup(PQgetvalue(res, j, 3));
+ periods[j].perend = pg_strdup(PQgetvalue(res, j, 4));
+ periods[j].rngtype = pg_strdup(PQgetvalue(res, j, 5));
+ periods[j].conname = pg_strdup(PQgetvalue(res, j, 6));
+ }
+ PQclear(res);
+ }
+ }
+
destroyPQExpBuffer(q);
destroyPQExpBuffer(tbloids);
destroyPQExpBuffer(checkoids);
@@ -10157,6 +10267,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
case DO_FK_CONSTRAINT:
dumpConstraint(fout, (const ConstraintInfo *) dobj);
break;
+ case DO_PERIOD:
+ break;
case DO_PROCLANG:
dumpProcLang(fout, (const ProcLangInfo *) dobj);
break;
@@ -15645,6 +15757,32 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
}
}
+ /*
+ * Add non-inherited PERIOD definitions, if any.
+ */
+ for (j = 0; j < tbinfo->nperiod; j++)
+ {
+ PeriodInfo *period = &(tbinfo->periods[j]);
+
+ char *name = pg_strdup(fmtId(period->dobj.name));
+ char *start = pg_strdup(fmtId(period->perstart));
+ char *end = pg_strdup(fmtId(period->perend));
+ char *rngtype = pg_strdup(fmtId(period->rngtype));
+ char *conname = pg_strdup(fmtId(period->conname));
+
+ if (actual_atts == 0)
+ appendPQExpBufferStr(q, " (\n ");
+ else
+ appendPQExpBufferStr(q, ",\n ");
+
+ appendPQExpBuffer(q, "PERIOD FOR %s (%s, %s) "
+ "WITH (rangetype = %s, check_constraint_name = %s)",
+ name, start, end,
+ rngtype, conname);
+
+ actual_atts++;
+ }
+
/*
* Add non-inherited CHECK constraints, if any.
*
@@ -15653,7 +15791,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
* PARTITION that we'll emit later expects the constraint to be
* there. (No need to fix conislocal: ATTACH PARTITION does that)
*/
- for (j = 0; j < tbinfo->ncheck; j++)
+ for (j = 0; j < tbinfo->ncheck - tbinfo->nperiod; j++)
{
ConstraintInfo *constr = &(tbinfo->checkexprs[j]);
@@ -15852,7 +15990,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
* For partitions, they were already dumped, and conislocal
* doesn't need fixing.
*/
- for (k = 0; k < tbinfo->ncheck; k++)
+ for (k = 0; k < tbinfo->ncheck - tbinfo->nperiod; k++)
{
ConstraintInfo *constr = &(tbinfo->checkexprs[k]);
@@ -16143,7 +16281,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
dumpTableSecLabel(fout, tbinfo, reltypename);
/* Dump comments on inlined table constraints */
- for (j = 0; j < tbinfo->ncheck; j++)
+ for (j = 0; j < tbinfo->ncheck - tbinfo->nperiod; j++)
{
ConstraintInfo *constr = &(tbinfo->checkexprs[j]);
@@ -18231,6 +18369,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
case DO_TRIGGER:
case DO_EVENT_TRIGGER:
case DO_DEFAULT_ACL:
+ case DO_PERIOD:
case DO_POLICY:
case DO_PUBLICATION:
case DO_PUBLICATION_REL:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2afb5d5294..3ffea100da 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -59,6 +59,7 @@ typedef enum
DO_TRIGGER,
DO_CONSTRAINT,
DO_FK_CONSTRAINT, /* see note for ConstraintInfo */
+ DO_PERIOD,
DO_PROCLANG,
DO_CAST,
DO_TABLE_DATA,
@@ -299,12 +300,14 @@ typedef struct _tableInfo
bool rowsec; /* is row security enabled? */
bool forcerowsec; /* is row security forced? */
bool hasoids; /* does it have OIDs? */
+ bool hasperiods; /* does it have any periods? */
uint32 frozenxid; /* table's relfrozenxid */
uint32 minmxid; /* table's relminmxid */
Oid toast_oid; /* toast table's OID, or 0 if none */
uint32 toast_frozenxid; /* toast table's relfrozenxid, if any */
uint32 toast_minmxid; /* toast table's relminmxid */
int ncheck; /* # of CHECK expressions */
+ int nperiod; /* # of PERIOD definitions */
Oid reltype; /* OID of table's composite type, if any */
Oid reloftype; /* underlying type for typed table */
Oid foreign_server; /* foreign server oid, if applicable */
@@ -349,6 +352,7 @@ typedef struct _tableInfo
bool *inhNotNull; /* true if NOT NULL is inherited */
struct _attrDefInfo **attrdefs; /* DEFAULT expressions */
struct _constraintInfo *checkexprs; /* CHECK constraints */
+ struct _periodInfo *periods; /* PERIOD definitions */
bool needs_override; /* has GENERATED ALWAYS AS IDENTITY */
char *amname; /* relation access method */
@@ -487,6 +491,16 @@ typedef struct _constraintInfo
bool withoutoverlaps; /* true if the last elem is WITHOUT OVERLAPS */
} ConstraintInfo;
+typedef struct _periodInfo
+{
+ DumpableObject dobj;
+ TableInfo *pertable;
+ char *perstart; /* the name of the start column */
+ char *perend; /* the name of the end column */
+ char *rngtype; /* the name of the range type */
+ char *conname; /* the name of the CHECK constraint */
+} PeriodInfo;
+
typedef struct _procLangInfo
{
DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 523a19c155..55f2b68d0f 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -84,6 +84,7 @@ enum dbObjectTypePriorities
PRIO_CONSTRAINT,
PRIO_INDEX,
PRIO_INDEX_ATTACH,
+ PRIO_PERIOD,
PRIO_STATSEXT,
PRIO_RULE,
PRIO_TRIGGER,
@@ -118,6 +119,7 @@ static const int dbObjectTypePriority[] =
PRIO_ATTRDEF, /* DO_ATTRDEF */
PRIO_INDEX, /* DO_INDEX */
PRIO_INDEX_ATTACH, /* DO_INDEX_ATTACH */
+ PRIO_PERIOD, /* DO_PERIOD */
PRIO_STATSEXT, /* DO_STATSEXT */
PRIO_RULE, /* DO_RULE */
PRIO_TRIGGER, /* DO_TRIGGER */
@@ -1438,6 +1440,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
"FK CONSTRAINT %s (ID %d OID %u)",
obj->name, obj->dumpId, obj->catId.oid);
return;
+ case DO_PERIOD:
+ snprintf(buf, bufsize,
+ "PERIOD %s (ID %d OID %u)",
+ obj->name, obj->dumpId, obj->catId.oid);
+ return;
case DO_PROCLANG:
snprintf(buf, bufsize,
"PROCEDURAL LANGUAGE %s (ID %d OID %u)",
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 2846489eb0..1c974efff5 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2370,6 +2370,40 @@ describeOneTableDetails(const char *schemaname,
PGresult *result = NULL;
int tuples = 0;
+ /* print periods */
+ if (pset.sversion >= 150000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT quote_ident(p.pername), quote_ident(s.attname) AS startatt, quote_ident(e.attname) AS endatt\n"
+ "FROM pg_period AS p\n"
+ "JOIN pg_attribute AS s ON (s.attrelid, s.attnum) = (p.perrelid, p.perstart)\n"
+ "JOIN pg_attribute AS e ON (e.attrelid, e.attnum) = (p.perrelid, p.perend)\n"
+ "WHERE p.perrelid = '%s'\n"
+ "ORDER BY 1;",
+ oid);
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ printTableAddFooter(&cont, _("Periods:"));
+ for (i = 0; i < tuples; i++)
+ {
+ /* untranslated constraint name and def */
+ printfPQExpBuffer(&buf, " %s (%s, %s)",
+ PQgetvalue(result, i, 0),
+ PQgetvalue(result, i, 1),
+ PQgetvalue(result, i, 2));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
+ }
+
/* print indexes */
if (tableinfo.hasindex)
{
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index ffd5e9dc82..8f03bf3de1 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -93,6 +93,7 @@ typedef enum ObjectClass
OCLASS_CAST, /* pg_cast */
OCLASS_COLLATION, /* pg_collation */
OCLASS_CONSTRAINT, /* pg_constraint */
+ OCLASS_PERIOD, /* pg_period */
OCLASS_CONVERSION, /* pg_conversion */
OCLASS_DEFAULT, /* pg_attrdef */
OCLASS_LANGUAGE, /* pg_language */
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index d01ab504b6..31ec0f8c34 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -117,6 +117,10 @@ extern List *AddRelationNewConstraints(Relation rel,
extern void RelationClearMissing(Relation rel);
extern void SetAttrMissing(Oid relid, char *attname, char *value);
+extern Oid StorePeriod(Relation rel, const char *period,
+ AttrNumber startnum, AttrNumber endnum,
+ AttrNumber rangenum, Oid conoid);
+
extern Node *cookDefault(ParseState *pstate,
Node *raw_default,
Oid atttypid,
diff --git a/src/include/catalog/pg_index.h b/src/include/catalog/pg_index.h
index b0592571da..6af677d814 100644
--- a/src/include/catalog/pg_index.h
+++ b/src/include/catalog/pg_index.h
@@ -44,11 +44,11 @@ CATALOG(pg_index,2610,IndexRelationId) BKI_SCHEMA_MACRO
bool indisready; /* is this index ready for inserts? */
bool indislive; /* is this index alive at all? */
bool indisreplident; /* is this index the identity for replication? */
+ Oid indperiod; /* the period it contains, if any */
/* variable-length fields start here, but we allow direct access to indkey */
int2vector indkey BKI_FORCE_NOT_NULL; /* column numbers of indexed cols,
* or 0 */
-
#ifdef CATALOG_VARLEN
oidvector indcollation BKI_LOOKUP_OPT(pg_collation) BKI_FORCE_NOT_NULL; /* collation identifiers */
oidvector indclass BKI_LOOKUP(pg_opclass) BKI_FORCE_NOT_NULL; /* opclass identifiers */
diff --git a/src/include/catalog/pg_period.h b/src/include/catalog/pg_period.h
new file mode 100644
index 0000000000..f4066cdb06
--- /dev/null
+++ b/src/include/catalog/pg_period.h
@@ -0,0 +1,53 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_period.h
+ * definition of the "period" system catalog (pg_period)
+ *
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_period.h
+ *
+ * NOTES
+ * The Catalog.pm module reads this file and derives schema
+ * information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_PERIOD_H
+#define PG_PERIOD_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_period_d.h"
+
+/* ----------------
+ * pg_period definition. cpp turns this into
+ * typedef struct FormData_pg_period
+ * ----------------
+ */
+CATALOG(pg_period,8000,PeriodRelationId)
+{
+ Oid oid; /* OID of the period */
+ NameData pername; /* name of period */
+ Oid perrelid; /* OID of relation containing this period */
+ int16 perstart; /* column for start value */
+ int16 perend; /* column for end value */
+ int16 perrange; /* column for range value */
+ Oid perconstraint; /* OID of (start < end) constraint */
+} FormData_pg_period;
+
+/* ----------------
+ * Form_pg_period corresponds to a pointer to a tuple with
+ * the format of pg_period relation.
+ * ----------------
+ */
+typedef FormData_pg_period *Form_pg_period;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_period_oid_index, 8001, PeriodObjectIndexId, on pg_period using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_period_perrelid_pername_index, 8002, PeriodRelidNameIndexId, on pg_period using btree(perrelid oid_ops, pername name_ops));
+
+extern void RemovePeriodById(Oid periodId);
+
+extern Oid get_relation_period_oid(Oid relid, const char *pername, bool missing_ok);
+
+#endif /* PG_PERIOD_H */
diff --git a/src/include/catalog/pg_range.h b/src/include/catalog/pg_range.h
index d03ef18851..f3031070f7 100644
--- a/src/include/catalog/pg_range.h
+++ b/src/include/catalog/pg_range.h
@@ -59,6 +59,7 @@ typedef FormData_pg_range *Form_pg_range;
DECLARE_UNIQUE_INDEX_PKEY(pg_range_rngtypid_index, 3542, RangeTypidIndexId, on pg_range using btree(rngtypid oid_ops));
DECLARE_UNIQUE_INDEX(pg_range_rngmultitypid_index, 2228, RangeMultirangeTypidIndexId, on pg_range using btree(rngmultitypid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_range_rngsubtype_rngtypid_index, 8003, RangeSubTypidTypidIndexId, on pg_range using btree(rngsubtype oid_ops, rngtypid oid_ops));
/*
* prototypes for functions in pg_range.c
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 250d89ff88..419cf1e84f 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -38,7 +38,8 @@ extern LOCKMODE AlterTableGetLockLevel(List *cmds);
extern void ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lockmode);
-extern void AlterTableInternal(Oid relid, List *cmds, bool recurse);
+extern void AlterTableInternal(Oid relid, List *cmds, bool recurse,
+ struct AlterTableUtilityContext *context);
extern Oid AlterTableMoveAll(AlterTableMoveAllStmt *stmt);
@@ -104,5 +105,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
Oid relId, Oid oldRelId, void *arg);
extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
List *partConstraint);
+extern Oid choose_rangetype_for_period(PeriodDef *period);
#endif /* TABLECMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 7b7e64c99f..6d107259b9 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2127,6 +2127,7 @@ typedef enum ObjectType
OBJECT_OPERATOR,
OBJECT_OPFAMILY,
OBJECT_PARAMETER_ACL,
+ OBJECT_PERIOD,
OBJECT_POLICY,
OBJECT_PROCEDURE,
OBJECT_PUBLICATION,
@@ -2214,6 +2215,8 @@ typedef enum AlterTableType
AT_ValidateConstraint, /* validate constraint */
AT_AddIndexConstraint, /* add constraint using existing index */
AT_DropConstraint, /* drop constraint */
+ AT_AddPeriod, /* ADD PERIOD */
+ AT_DropPeriod, /* DROP PERIOD */
AT_ReAddComment, /* internal to commands/tablecmds.c */
AT_AlterColumnType, /* alter column type */
AT_AlterColumnGenericOptions, /* alter column OPTIONS (...) */
@@ -2480,9 +2483,9 @@ typedef struct VariableShowStmt
/* ----------------------
* Create Table Statement
*
- * NOTE: in the raw gram.y output, ColumnDef and Constraint nodes are
- * intermixed in tableElts, and constraints is NIL. After parse analysis,
- * tableElts contains just ColumnDefs, and constraints contains just
+ * NOTE: in the raw gram.y output, ColumnDef, PeriodDef, and Constraint nodes are
+ * intermixed in tableElts; periods and constraints are NIL. After parse analysis,
+ * tableElts contains just ColumnDefs, periods contains just PeriodDef nodes, and constraints contains just
* Constraint nodes (in fact, only CONSTR_CHECK nodes, in the present
* implementation).
* ----------------------
@@ -2493,6 +2496,7 @@ typedef struct CreateStmt
NodeTag type;
RangeVar *relation; /* relation to create */
List *tableElts; /* column definitions (list of ColumnDef) */
+ List *periods; /* periods (list of PeriodDef nodes) */
List *inhRelations; /* relations to inherit from (list of
* RangeVar) */
PartitionBoundSpec *partbound; /* FOR VALUES clause */
@@ -2506,6 +2510,30 @@ typedef struct CreateStmt
bool if_not_exists; /* just do nothing if it already exists? */
} CreateStmt;
+
+/* ----------
+ * Definitions for periods in CreateStmt
+ * ----------
+ */
+
+typedef struct PeriodDef
+{
+ NodeTag type;
+ Oid oid; /* period oid, once it's transformed */
+ char *periodname; /* period name */
+ char *startcolname; /* name of start column */
+ char *endcolname; /* name of end column */
+ AttrNumber startattnum; /* attnum of the start column */
+ AttrNumber endattnum; /* attnum of the end column */
+ AttrNumber rngattnum; /* attnum of the GENERATED range column */
+ List *options; /* options from WITH clause */
+ char *constraintname; /* name of the CHECK constraint */
+ char *rangetypename; /* name of the range type */
+ Oid coltypid; /* the start/end col type */
+ Oid rngtypid; /* the range type to use */
+ int location; /* token location, or -1 if unknown */
+} PeriodDef;
+
/* ----------
* Definitions for constraints in CreateStmt
*
@@ -3212,6 +3240,7 @@ typedef struct IndexStmt
List *indexParams; /* columns to index: a list of IndexElem */
List *indexIncludingParams; /* additional columns to index: a list
* of IndexElem */
+ PeriodDef *period; /* The period included in the index */
List *options; /* WITH clause options: a list of DefElem */
Node *whereClause; /* qualification (partial-index predicate) */
List *excludeOpNames; /* exclusion operator names, or NIL if none */
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index 2b40a0b5b1..feb6c26007 100644
--- a/src/include/parser/parse_utilcmd.h
+++ b/src/include/parser/parse_utilcmd.h
@@ -40,5 +40,6 @@ extern IndexStmt *generateClonedIndexStmt(RangeVar *heapRel,
Relation source_idx,
const struct AttrMap *attmap,
Oid *constraintOid);
+extern void transformPeriodOptions(PeriodDef *period);
#endif /* PARSE_UTILCMD_H */
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index a2b6ced904..e8dcf17b7d 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -96,6 +96,8 @@ extern Oid get_atttype(Oid relid, AttrNumber attnum);
extern void get_atttypetypmodcoll(Oid relid, AttrNumber attnum,
Oid *typid, int32 *typmod, Oid *collid);
extern Datum get_attoptions(Oid relid, int16 attnum);
+extern char *get_periodname(Oid periodid, bool missing_ok);
+extern Oid get_period_oid(Oid relid, const char *periodname, bool missing_ok);
extern Oid get_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok);
extern char *get_collation_name(Oid colloid);
extern bool get_collation_isdeterministic(Oid colloid);
@@ -143,6 +145,7 @@ extern Oid get_transform_fromsql(Oid typid, Oid langid, List *trftypes);
extern Oid get_transform_tosql(Oid typid, Oid langid, List *trftypes);
extern bool get_typisdefined(Oid typid);
extern char *get_typname(Oid typid);
+extern bool get_typname_and_namespace(Oid typid, char **typname, char **typnamespace);
extern int16 get_typlen(Oid typid);
extern bool get_typbyval(Oid typid);
extern void get_typlenbyval(Oid typid, int16 *typlen, bool *typbyval);
@@ -195,6 +198,7 @@ extern Oid get_range_subtype(Oid rangeOid);
extern Oid get_range_collation(Oid rangeOid);
extern Oid get_range_multirange(Oid rangeOid);
extern Oid get_multirange_range(Oid multirangeOid);
+extern Oid get_subtype_range(Oid subtypeOid);
extern Oid get_index_column_opclass(Oid index_oid, int attno);
extern bool get_index_isreplident(Oid index_oid);
extern bool get_index_isvalid(Oid index_oid);
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index 67ea6e4945..33bab4061a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -75,6 +75,8 @@ enum SysCacheIdentifier
PARAMETERACLNAME,
PARAMETERACLOID,
PARTRELID,
+ PERIODNAME,
+ PERIODOID,
PROCNAMEARGSNSP,
PROCOID,
PUBLICATIONNAME,
@@ -84,6 +86,7 @@ enum SysCacheIdentifier
PUBLICATIONREL,
PUBLICATIONRELMAP,
RANGEMULTIRANGE,
+ RANGESUBTYPE,
RANGETYPE,
RELNAMENSP,
RELOID,
diff --git a/src/test/regress/expected/periods.out b/src/test/regress/expected/periods.out
new file mode 100644
index 0000000000..9d24e342ac
--- /dev/null
+++ b/src/test/regress/expected/periods.out
@@ -0,0 +1,152 @@
+/* System periods are not implemented */
+create table pt (id integer, ds date, de date, period for system_time (ds, de));
+ERROR: PERIOD FOR SYSTEM_TIME is not supported
+LINE 2: create table pt (id integer, ds date, de date, period for sy...
+ ^
+/* Periods must specify actual columns */
+create table pt (id integer, ds date, de date, period for p (bogus, de));
+ERROR: column "bogus" of relation "pt" does not exist
+create table pt (id integer, ds date, de date, period for p (ds, bogus));
+ERROR: column "bogus" of relation "pt" does not exist
+/* Data types must match exactly */
+create table pt (id integer, ds date, de timestamp, period for p (ds, de));
+ERROR: start and end columns of period must be of same type
+create table pt (id integer, ds text collate "C", de text collate "POSIX", period for p (ds, de));
+ERROR: start and end columns of period must have same collation
+/* Periods must have a default BTree operator class */
+create table pt (id integer, ds xml, de xml, period for p (ds, de));
+ERROR: no range type for xml found for period p
+HINT: You can define a custom range type with CREATE TYPE
+/* Period and column names are in the same namespace */
+create table pt (id integer, ds date, de date, period for ctid (ds, de));
+ERROR: period name "ctid" conflicts with a system column name
+create table pt (id integer, ds date, de date, period for id (ds, de));
+ERROR: period name "id" conflicts with a column name
+/* Period name can't be given more than once */
+create table pt (id integer, ds date, de date, period for p (ds, de), period for p (ds, de));
+ERROR: period name "p" specified more than once
+/* Now make one that works */
+create table pt (id integer, ds date, de date, period for p (ds, de));
+/*
+ * CREATE TABLE currently adds an ALTER TABLE to add the periods, but let's do
+ * some explicit testing anyway
+ */
+alter table pt drop period for p;
+alter table pt add period for system_time (ds, de);
+ERROR: PERIOD FOR SYSTEM_TIME is not supported
+alter table pt add period for p (ds, de);
+/* Can't drop its columns */
+alter table pt drop column ds;
+ERROR: cannot drop column ds of table pt because other objects depend on it
+DETAIL: period p on table pt depends on column ds of table pt
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+alter table pt drop column de;
+ERROR: cannot drop column de of table pt because other objects depend on it
+DETAIL: period p on table pt depends on column de of table pt
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+/* Can't change the data types */
+alter table pt alter column ds type timestamp;
+ERROR: cannot alter type of a column used by a period
+DETAIL: period p on table pt depends on column "ds"
+alter table pt alter column ds type timestamp;
+ERROR: cannot alter type of a column used by a period
+DETAIL: period p on table pt depends on column "ds"
+/* column/period namespace conflicts */
+alter table pt add column p integer;
+ERROR: column name "p" conflicts with a period name
+alter table pt rename column id to p;
+ERROR: column name "p" conflicts with a period name
+/* adding columns and the period at the same time */
+create table pt2 (id integer);
+alter table pt2 add column ds date, add column de date, add period for p (ds, de);
+drop table pt2;
+/* Ambiguous range types raise an error */
+create type mydaterange as range(subtype=date);
+create table pt2 (id int, ds date, de date, period for p (ds, de));
+ERROR: ambiguous range for type date
+/* You can give an explicit range type */
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'mydaterange'));
+drop type mydaterange;
+ERROR: cannot drop type mydaterange because other objects depend on it
+DETAIL: period p on table pt2 depends on type mydaterange
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+drop type mydaterange cascade;
+NOTICE: drop cascades to period p on table pt2
+drop table pt2;
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'daterange'));
+/* Range type is not found */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'notarange'));
+ERROR: Range type notarange not found
+/* Range type is the wrong type */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'tstzrange'));
+ERROR: Range type tstzrange does not match column type date
+drop table pt2;
+/* CREATE TABLE (LIKE ...) */
+/* Periods are not copied by LIKE */
+create table pt2 (like pt);
+\d pt2
+ Table "public.pt2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | |
+ ds | date | | |
+ de | date | | |
+
+drop table pt2;
+/* Can add a period referring to LIKE'd columns */
+create table not_p (id integer, ds date, de date);
+create table pt2 (like not_p, period for p (ds, de));
+\d pt2
+ Table "public.pt2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | |
+ ds | date | | |
+ de | date | | |
+Periods:
+ p (ds, de)
+Check constraints:
+ "pt2_p_check" CHECK (ds < de)
+
+drop table pt2;
+/* Can add a period with the same name */
+create table pt2 (like pt, period for p (ds, de));
+\d pt2
+ Table "public.pt2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | |
+ ds | date | | |
+ de | date | | |
+Periods:
+ p (ds, de)
+Check constraints:
+ "pt2_p_check" CHECK (ds < de)
+
+drop table pt2;
+/* Can add a period with a different name */
+create table pt2 (like pt, period for p2 (ds, de));
+\d pt2
+ Table "public.pt2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | |
+ ds | date | | |
+ de | date | | |
+Periods:
+ p2 (ds, de)
+Check constraints:
+ "pt2_p2_check" CHECK (ds < de)
+
+drop table pt2;
+/* Can't add a period whose name conflicts with a LIKE'd column */
+create table pt2 (like pt, period for id (ds, de));
+ERROR: period name "id" conflicts with a column name
+/* CREATE TALBE INHERITS */
+/* Can't inherit from a table with a period */
+create table pt2 (name text) inherits (pt);
+ERROR: Inheriting from a table with a PERIOD is not supported
+/* Can't inherit with a period */
+create table pt2 (d2s date, d2e date, period for p (d2s, d2e)) inherits (not_p);
+ERROR: Inheriting is not supported when a table has a PERIOD
+drop table not_p;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ce732bda3c..202c320ed8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
# ----------
# Another group of parallel tests
# ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse for_portion_of
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table periods vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse for_portion_of
# ----------
# sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/periods.sql b/src/test/regress/sql/periods.sql
new file mode 100644
index 0000000000..5632706c39
--- /dev/null
+++ b/src/test/regress/sql/periods.sql
@@ -0,0 +1,102 @@
+/* System periods are not implemented */
+create table pt (id integer, ds date, de date, period for system_time (ds, de));
+
+/* Periods must specify actual columns */
+create table pt (id integer, ds date, de date, period for p (bogus, de));
+create table pt (id integer, ds date, de date, period for p (ds, bogus));
+
+/* Data types must match exactly */
+create table pt (id integer, ds date, de timestamp, period for p (ds, de));
+create table pt (id integer, ds text collate "C", de text collate "POSIX", period for p (ds, de));
+
+/* Periods must have a default BTree operator class */
+create table pt (id integer, ds xml, de xml, period for p (ds, de));
+
+/* Period and column names are in the same namespace */
+create table pt (id integer, ds date, de date, period for ctid (ds, de));
+create table pt (id integer, ds date, de date, period for id (ds, de));
+
+/* Period name can't be given more than once */
+create table pt (id integer, ds date, de date, period for p (ds, de), period for p (ds, de));
+
+/* Now make one that works */
+create table pt (id integer, ds date, de date, period for p (ds, de));
+
+/*
+ * CREATE TABLE currently adds an ALTER TABLE to add the periods, but let's do
+ * some explicit testing anyway
+ */
+alter table pt drop period for p;
+alter table pt add period for system_time (ds, de);
+alter table pt add period for p (ds, de);
+
+/* Can't drop its columns */
+alter table pt drop column ds;
+alter table pt drop column de;
+
+/* Can't change the data types */
+alter table pt alter column ds type timestamp;
+alter table pt alter column ds type timestamp;
+
+/* column/period namespace conflicts */
+alter table pt add column p integer;
+alter table pt rename column id to p;
+
+/* adding columns and the period at the same time */
+create table pt2 (id integer);
+alter table pt2 add column ds date, add column de date, add period for p (ds, de);
+drop table pt2;
+
+/* Ambiguous range types raise an error */
+create type mydaterange as range(subtype=date);
+create table pt2 (id int, ds date, de date, period for p (ds, de));
+
+/* You can give an explicit range type */
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'mydaterange'));
+drop type mydaterange;
+drop type mydaterange cascade;
+drop table pt2;
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'daterange'));
+
+/* Range type is not found */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'notarange'));
+
+/* Range type is the wrong type */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'tstzrange'));
+drop table pt2;
+
+/* CREATE TABLE (LIKE ...) */
+
+/* Periods are not copied by LIKE */
+create table pt2 (like pt);
+\d pt2
+drop table pt2;
+
+/* Can add a period referring to LIKE'd columns */
+create table not_p (id integer, ds date, de date);
+create table pt2 (like not_p, period for p (ds, de));
+\d pt2
+drop table pt2;
+
+/* Can add a period with the same name */
+create table pt2 (like pt, period for p (ds, de));
+\d pt2
+drop table pt2;
+
+/* Can add a period with a different name */
+create table pt2 (like pt, period for p2 (ds, de));
+\d pt2
+drop table pt2;
+
+/* Can't add a period whose name conflicts with a LIKE'd column */
+create table pt2 (like pt, period for id (ds, de));
+
+/* CREATE TALBE INHERITS */
+
+/* Can't inherit from a table with a period */
+create table pt2 (name text) inherits (pt);
+
+/* Can't inherit with a period */
+create table pt2 (d2s date, d2e date, period for p (d2s, d2e)) inherits (not_p);
+
+drop table not_p;
--
2.32.0 (Apple Git-132)
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: SQL:2011 application time
@ 2023-07-12 08:24 Peter Eisentraut <[email protected]>
parent: Paul A Jungwirth <[email protected]>
1 sibling, 1 reply; 37+ messages in thread
From: Peter Eisentraut @ 2023-07-12 08:24 UTC (permalink / raw)
To: Paul A Jungwirth <[email protected]>; +Cc: Corey Huinker <[email protected]>; PostgreSQL Hackers <[email protected]>; Jaime Casanova <[email protected]>
On 07.07.23 03:03, Paul A Jungwirth wrote:
> Here are some new patch files based on discussions from PGCon.
Here are a few fixup patches to get things building without warnings and
errors.
The last patch (your 0005) fails the regression test for me and it
didn't appear to be a trivial problem, so please take another look at
that sometime. (Since it's the last patch, it's obviously lower priority.)
From 6694912b0fdd8742e583ff2a524f32284c330711 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Wed, 12 Jul 2023 09:45:56 +0200
Subject: [PATCH v12] fixup! Add temporal PRIMARY KEY and UNIQUE constraints
---
src/bin/pg_dump/pg_dump.c | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a95e1d3696..33ad34ad66 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7029,16 +7029,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
"(SELECT pg_catalog.array_agg(attstattarget ORDER BY attnum) "
" FROM pg_catalog.pg_attribute "
" WHERE attrelid = i.indexrelid AND "
- " attstattarget >= 0) AS indstatvals, "
- "c.conexclop IS NOT NULL AS withoutoverlaps, ");
+ " attstattarget >= 0) AS indstatvals, ");
else
appendPQExpBufferStr(query,
"0 AS parentidx, "
"i.indnatts AS indnkeyatts, "
"i.indnatts AS indnatts, "
"'' AS indstatcols, "
- "'' AS indstatvals, "
- "null AS withoutoverlaps, ");
+ "'' AS indstatvals, ");
if (fout->remoteVersion >= 150000)
appendPQExpBufferStr(query,
@@ -7047,6 +7045,13 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
appendPQExpBufferStr(query,
"false AS indnullsnotdistinct, ");
+ if (fout->remoteVersion >= 170000)
+ appendPQExpBufferStr(query,
+ "c.conexclop IS NOT NULL AS withoutoverlaps ");
+ else
+ appendPQExpBufferStr(query,
+ "null AS withoutoverlaps ");
+
/*
* The point of the messy-looking outer join is to find a constraint that
* is related by an internal dependency link to the index. If we find one,
--
2.41.0
From 49932adb8e626036b8d8edf26ed1465f39db82bd Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Wed, 12 Jul 2023 10:16:53 +0200
Subject: [PATCH v12] fixup! Add UPDATE/DELETE FOR PORTION OF
---
doc/src/sgml/ref/delete.sgml | 2 ++
doc/src/sgml/ref/update.sgml | 2 ++
src/backend/parser/analyze.c | 5 +++--
3 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml
index 868cf0d1f9..aec593239b 100644
--- a/doc/src/sgml/ref/delete.sgml
+++ b/doc/src/sgml/ref/delete.sgml
@@ -55,6 +55,7 @@ <title>Description</title>
circumstances.
</para>
+ <!-- FIXME
<para>
If the table has a <link linkend="ddl-periods-application-periods">temporal
primary key</link>, you may supply a
@@ -63,6 +64,7 @@ <title>Description</title>
the <literal>FOR PORTION OF</literal> bounds, then new rows spanning the "cut
off" duration will be inserted to preserve the old values.
</para>
+ -->
<para>
The optional <literal>RETURNING</literal> clause causes <command>DELETE</command>
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index f2042e0b25..62e9e0e1f0 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -52,6 +52,7 @@ <title>Description</title>
circumstances.
</para>
+ <!-- FIXME
<para>
If the table has a <link linkend="ddl-periods-application-periods">temporal
primary key</link>, you may supply a
@@ -61,6 +62,7 @@ <title>Description</title>
within the bounds, and new rows spanning the "cut off" duration will be
inserted to preserve the old values.
</para>
+ -->
<para>
The optional <literal>RETURNING</literal> clause causes <command>UPDATE</command>
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 0b2109d1bb..c6d2b7e1d1 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1247,6 +1247,7 @@ transformForPortionOfClause(ParseState *pstate,
char *range_name = forPortionOf->range_name;
char *range_type_name = NULL;
int range_attno = InvalidAttrNumber;
+ Form_pg_attribute attr;
ForPortionOfExpr *result;
List *targetList;
Node *target_start, *target_end;
@@ -1264,7 +1265,7 @@ transformForPortionOfClause(ParseState *pstate,
range_name,
RelationGetRelationName(targetrel)),
parser_errposition(pstate, forPortionOf->range_name_location)));
- Form_pg_attribute attr = TupleDescAttr(targetrel->rd_att, range_attno - 1);
+ attr = TupleDescAttr(targetrel->rd_att, range_attno - 1);
// TODO: check attr->attisdropped (?),
// and figure out concurrency issues with that in general.
// It should work the same as updating any other column.
@@ -1317,12 +1318,12 @@ transformForPortionOfClause(ParseState *pstate,
* Now make sure we update the start/end time of the record.
* For a range col (r) this is `r = r * targetRange`.
*/
- targetList = NIL;
Expr *rangeSetExpr = (Expr *) makeSimpleA_Expr(AEXPR_OP, "*",
(Node *) copyObject(rangeVar), (Node *) fc,
forPortionOf->range_name_location);
TargetEntry *tle;
+ targetList = NIL;
rangeSetExpr = (Expr *) transformExpr(pstate, (Node *) rangeSetExpr, EXPR_KIND_UPDATE_PORTION);
tle = makeTargetEntry(rangeSetExpr,
range_attno,
--
2.41.0
From a831c9d79bb979a1360e90db00adddb156dd582a Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Wed, 12 Jul 2023 10:13:01 +0200
Subject: [PATCH v12] fixup! Add PERIODs
---
doc/src/sgml/ref/alter_table.sgml | 2 +-
src/backend/catalog/heap.c | 3 ++-
src/backend/catalog/meson.build | 1 +
src/backend/commands/tablecmds.c | 3 ++-
src/include/catalog/meson.build | 1 +
src/test/modules/test_ddl_deparse/test_ddl_deparse.c | 6 ++++++
6 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 487f09f88a..d6aed3dff8 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -582,7 +582,7 @@ <title>Description</title>
</listitem>
</varlistentry>
- <varlistentry>
+ <varlistentry id="sql-altertable-desc-drop-period">
<term><literal>DROP PERIOD FOR</literal></term>
<listitem>
<para>
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 022a2ede4c..a31c22c6b7 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2062,7 +2062,6 @@ Oid
StorePeriod(Relation rel, const char *periodname, AttrNumber startnum,
AttrNumber endnum, AttrNumber rangenum, Oid conoid)
{
- Assert(rangenum != InvalidAttrNumber);
Datum values[Natts_pg_period];
bool nulls[Natts_pg_period];
Relation pg_period;
@@ -2071,6 +2070,8 @@ StorePeriod(Relation rel, const char *periodname, AttrNumber startnum,
NameData pername;
ObjectAddress myself, referenced;
+ Assert(rangenum != InvalidAttrNumber);
+
namestrcpy(&pername, periodname);
MemSet(values, 0, sizeof(values));
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index fa6609e577..c499cd2f5d 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -26,6 +26,7 @@ backend_sources += files(
'pg_namespace.c',
'pg_operator.c',
'pg_parameter_acl.c',
+ 'pg_period.c',
'pg_proc.c',
'pg_publication.c',
'pg_range.c',
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ef17b3e31e..d95f742b29 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8384,6 +8384,7 @@ ATExecAddPeriod(Relation rel, PeriodDef *period, AlterTableUtilityContext *conte
Relation attrelation;
ObjectAddress address = InvalidObjectAddress;
Constraint *constr;
+ ColumnDef *rangecol;
Oid conoid, periodoid;
List *cmds = NIL;
AlterTableCmd *cmd;
@@ -8418,7 +8419,7 @@ ATExecAddPeriod(Relation rel, PeriodDef *period, AlterTableUtilityContext *conte
conoid = get_relation_constraint_oid(RelationGetRelid(rel), period->constraintname, false);
/* Make the range column */
- ColumnDef *rangecol = make_range_column_for_period(period);
+ rangecol = make_range_column_for_period(period);
cmd = makeNode(AlterTableCmd);
cmd->subtype = AT_AddColumn;
cmd->def = (Node *) rangecol;
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index c3fd05d027..158496d19d 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -57,6 +57,7 @@ catalog_headers = [
'pg_collation.h',
'pg_parameter_acl.h',
'pg_partitioned_table.h',
+ 'pg_period.h',
'pg_range.h',
'pg_transform.h',
'pg_sequence.h',
diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
index 82f937fca4..2436996b1c 100644
--- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
+++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
@@ -180,6 +180,12 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS)
case AT_DropConstraint:
strtype = "DROP CONSTRAINT";
break;
+ case AT_AddPeriod:
+ strtype = "ADD PERIOD";
+ break;
+ case AT_DropPeriod:
+ strtype = "DROP PERIOD";
+ break;
case AT_ReAddComment:
strtype = "(re) ADD COMMENT";
break;
--
2.41.0
Attachments:
[text/plain] v12-0002-fixup-Add-temporal-PRIMARY-KEY-and-UNIQU.patch.nocfbot (1.8K, ../../[email protected]/2-v12-0002-fixup-Add-temporal-PRIMARY-KEY-and-UNIQU.patch.nocfbot)
download | inline diff:
From 6694912b0fdd8742e583ff2a524f32284c330711 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Wed, 12 Jul 2023 09:45:56 +0200
Subject: [PATCH v12] fixup! Add temporal PRIMARY KEY and UNIQUE constraints
---
src/bin/pg_dump/pg_dump.c | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a95e1d3696..33ad34ad66 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7029,16 +7029,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
"(SELECT pg_catalog.array_agg(attstattarget ORDER BY attnum) "
" FROM pg_catalog.pg_attribute "
" WHERE attrelid = i.indexrelid AND "
- " attstattarget >= 0) AS indstatvals, "
- "c.conexclop IS NOT NULL AS withoutoverlaps, ");
+ " attstattarget >= 0) AS indstatvals, ");
else
appendPQExpBufferStr(query,
"0 AS parentidx, "
"i.indnatts AS indnkeyatts, "
"i.indnatts AS indnatts, "
"'' AS indstatcols, "
- "'' AS indstatvals, "
- "null AS withoutoverlaps, ");
+ "'' AS indstatvals, ");
if (fout->remoteVersion >= 150000)
appendPQExpBufferStr(query,
@@ -7047,6 +7045,13 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
appendPQExpBufferStr(query,
"false AS indnullsnotdistinct, ");
+ if (fout->remoteVersion >= 170000)
+ appendPQExpBufferStr(query,
+ "c.conexclop IS NOT NULL AS withoutoverlaps ");
+ else
+ appendPQExpBufferStr(query,
+ "null AS withoutoverlaps ");
+
/*
* The point of the messy-looking outer join is to find a constraint that
* is related by an internal dependency link to the index. If we find one,
--
2.41.0
[text/plain] v12-0005-fixup-Add-UPDATE-DELETE-FOR-PORTION-OF.patch.nocfbot (3.3K, ../../[email protected]/3-v12-0005-fixup-Add-UPDATE-DELETE-FOR-PORTION-OF.patch.nocfbot)
download | inline diff:
From 49932adb8e626036b8d8edf26ed1465f39db82bd Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Wed, 12 Jul 2023 10:16:53 +0200
Subject: [PATCH v12] fixup! Add UPDATE/DELETE FOR PORTION OF
---
doc/src/sgml/ref/delete.sgml | 2 ++
doc/src/sgml/ref/update.sgml | 2 ++
src/backend/parser/analyze.c | 5 +++--
3 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml
index 868cf0d1f9..aec593239b 100644
--- a/doc/src/sgml/ref/delete.sgml
+++ b/doc/src/sgml/ref/delete.sgml
@@ -55,6 +55,7 @@ <title>Description</title>
circumstances.
</para>
+ <!-- FIXME
<para>
If the table has a <link linkend="ddl-periods-application-periods">temporal
primary key</link>, you may supply a
@@ -63,6 +64,7 @@ <title>Description</title>
the <literal>FOR PORTION OF</literal> bounds, then new rows spanning the "cut
off" duration will be inserted to preserve the old values.
</para>
+ -->
<para>
The optional <literal>RETURNING</literal> clause causes <command>DELETE</command>
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index f2042e0b25..62e9e0e1f0 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -52,6 +52,7 @@ <title>Description</title>
circumstances.
</para>
+ <!-- FIXME
<para>
If the table has a <link linkend="ddl-periods-application-periods">temporal
primary key</link>, you may supply a
@@ -61,6 +62,7 @@ <title>Description</title>
within the bounds, and new rows spanning the "cut off" duration will be
inserted to preserve the old values.
</para>
+ -->
<para>
The optional <literal>RETURNING</literal> clause causes <command>UPDATE</command>
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 0b2109d1bb..c6d2b7e1d1 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1247,6 +1247,7 @@ transformForPortionOfClause(ParseState *pstate,
char *range_name = forPortionOf->range_name;
char *range_type_name = NULL;
int range_attno = InvalidAttrNumber;
+ Form_pg_attribute attr;
ForPortionOfExpr *result;
List *targetList;
Node *target_start, *target_end;
@@ -1264,7 +1265,7 @@ transformForPortionOfClause(ParseState *pstate,
range_name,
RelationGetRelationName(targetrel)),
parser_errposition(pstate, forPortionOf->range_name_location)));
- Form_pg_attribute attr = TupleDescAttr(targetrel->rd_att, range_attno - 1);
+ attr = TupleDescAttr(targetrel->rd_att, range_attno - 1);
// TODO: check attr->attisdropped (?),
// and figure out concurrency issues with that in general.
// It should work the same as updating any other column.
@@ -1317,12 +1318,12 @@ transformForPortionOfClause(ParseState *pstate,
* Now make sure we update the start/end time of the record.
* For a range col (r) this is `r = r * targetRange`.
*/
- targetList = NIL;
Expr *rangeSetExpr = (Expr *) makeSimpleA_Expr(AEXPR_OP, "*",
(Node *) copyObject(rangeVar), (Node *) fc,
forPortionOf->range_name_location);
TargetEntry *tle;
+ targetList = NIL;
rangeSetExpr = (Expr *) transformExpr(pstate, (Node *) rangeSetExpr, EXPR_KIND_UPDATE_PORTION);
tle = makeTargetEntry(rangeSetExpr,
range_attno,
--
2.41.0
[text/plain] v12-0008-fixup-Add-PERIODs.patch.nocfbot (4.0K, ../../[email protected]/4-v12-0008-fixup-Add-PERIODs.patch.nocfbot)
download | inline diff:
From a831c9d79bb979a1360e90db00adddb156dd582a Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Wed, 12 Jul 2023 10:13:01 +0200
Subject: [PATCH v12] fixup! Add PERIODs
---
doc/src/sgml/ref/alter_table.sgml | 2 +-
src/backend/catalog/heap.c | 3 ++-
src/backend/catalog/meson.build | 1 +
src/backend/commands/tablecmds.c | 3 ++-
src/include/catalog/meson.build | 1 +
src/test/modules/test_ddl_deparse/test_ddl_deparse.c | 6 ++++++
6 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 487f09f88a..d6aed3dff8 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -582,7 +582,7 @@ <title>Description</title>
</listitem>
</varlistentry>
- <varlistentry>
+ <varlistentry id="sql-altertable-desc-drop-period">
<term><literal>DROP PERIOD FOR</literal></term>
<listitem>
<para>
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 022a2ede4c..a31c22c6b7 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2062,7 +2062,6 @@ Oid
StorePeriod(Relation rel, const char *periodname, AttrNumber startnum,
AttrNumber endnum, AttrNumber rangenum, Oid conoid)
{
- Assert(rangenum != InvalidAttrNumber);
Datum values[Natts_pg_period];
bool nulls[Natts_pg_period];
Relation pg_period;
@@ -2071,6 +2070,8 @@ StorePeriod(Relation rel, const char *periodname, AttrNumber startnum,
NameData pername;
ObjectAddress myself, referenced;
+ Assert(rangenum != InvalidAttrNumber);
+
namestrcpy(&pername, periodname);
MemSet(values, 0, sizeof(values));
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index fa6609e577..c499cd2f5d 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -26,6 +26,7 @@ backend_sources += files(
'pg_namespace.c',
'pg_operator.c',
'pg_parameter_acl.c',
+ 'pg_period.c',
'pg_proc.c',
'pg_publication.c',
'pg_range.c',
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ef17b3e31e..d95f742b29 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8384,6 +8384,7 @@ ATExecAddPeriod(Relation rel, PeriodDef *period, AlterTableUtilityContext *conte
Relation attrelation;
ObjectAddress address = InvalidObjectAddress;
Constraint *constr;
+ ColumnDef *rangecol;
Oid conoid, periodoid;
List *cmds = NIL;
AlterTableCmd *cmd;
@@ -8418,7 +8419,7 @@ ATExecAddPeriod(Relation rel, PeriodDef *period, AlterTableUtilityContext *conte
conoid = get_relation_constraint_oid(RelationGetRelid(rel), period->constraintname, false);
/* Make the range column */
- ColumnDef *rangecol = make_range_column_for_period(period);
+ rangecol = make_range_column_for_period(period);
cmd = makeNode(AlterTableCmd);
cmd->subtype = AT_AddColumn;
cmd->def = (Node *) rangecol;
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index c3fd05d027..158496d19d 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -57,6 +57,7 @@ catalog_headers = [
'pg_collation.h',
'pg_parameter_acl.h',
'pg_partitioned_table.h',
+ 'pg_period.h',
'pg_range.h',
'pg_transform.h',
'pg_sequence.h',
diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
index 82f937fca4..2436996b1c 100644
--- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
+++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
@@ -180,6 +180,12 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS)
case AT_DropConstraint:
strtype = "DROP CONSTRAINT";
break;
+ case AT_AddPeriod:
+ strtype = "ADD PERIOD";
+ break;
+ case AT_DropPeriod:
+ strtype = "DROP PERIOD";
+ break;
case AT_ReAddComment:
strtype = "(re) ADD COMMENT";
break;
--
2.41.0
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: SQL:2011 application time
@ 2023-07-15 09:04 jian he <[email protected]>
parent: Paul A Jungwirth <[email protected]>
1 sibling, 0 replies; 37+ messages in thread
From: jian he @ 2023-07-15 09:04 UTC (permalink / raw)
To: Paul A Jungwirth <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Daniel Gustafsson <[email protected]>; Corey Huinker <[email protected]>; PostgreSQL Hackers <[email protected]>; Jaime Casanova <[email protected]>
On Fri, Jul 7, 2023 at 9:04 AM Paul A Jungwirth
<[email protected]> wrote:
>
> On Thu, Jul 6, 2023 at 1:13 AM Peter Eisentraut
> <[email protected]> wrote:
> >
> > I had talked to Paul about this offline a while ago. btree_gist to core
> > is no longer considered a prerequisite. But Paul was planning to
> > produce a new patch set that is arranged and sequenced a bit
> > differently. Apparently, that new version is not done yet, so it would
> > make sense to either close this entry as returned with feedback, or move
> > it to the next commit fest as waiting on author.
>
> Here are some new patch files based on discussions from PGCon. The
> patches are reorganized a bit to hopefully make them easier to review:
>
> Initially I implement all functionality on just range columns, without
> supporting PERIODs yet. There are patches for temporal PRIMARY
> KEY/UNIQUE constraints, for simple foreign keys (without CASCADE/SET
> NULL/SET DEFAULT), for UPDATE/DELETE FOR PORTION OF, and then for the
> rest of the FK support (which depends on FOR PORTION OF). If you
> compare these patches to the v11 ones, you'll see that a ton of
> clutter disappears by not supporting PERIODs as a separate "thing".
>
> Finally there is a patch adding PERIOD syntax, but with a new
> implementation where a PERIOD causes us to just define a GENERATED
> range column. That means we can support all the same things as before
> but without adding the clutter. This patch isn't quite working yet
> (especially ALTER TABLE), but I thought I'd send where I'm at so far,
> since it sounds like folks are interested in doing a review. Also it
> was a little tricky dealing with the dependency between the PERIOD and
> the GENERATED column. (See the comments in the patch.) If anyone has a
> suggestion there I'd be happy to hear it.
>
> My goal is to include another patch soon to support hidden columns, so
> that the period's GENERATED column can be hidden. I read the
> conversation about a recent patch attempt for something similar, and I
> think I can use most of that (but cut some of the things the community
> was worried about).
>
> All these patches need some polishing, but I think there is enough new
> here for them to be worth reading for anyone interested in temporal
> progress.
>
> I'll set this commitfest entry back to Needs Review. Thanks for taking a look!
>
> Paul
due to change in:
https://www.postgresql.org/message-id/flat/ec8b1d9b-502e-d1f8-e909-1bf9dffe6fa5@illuminatedcomputing...
git apply $DOWNLOADS/patches/v12-0001-Add-temporal-PRIMARY-KEY-and-UNIQUE-constraints.patch
error: patch failed: src/backend/commands/indexcmds.c:940
error: src/backend/commands/indexcmds.c: patch does not apply
probably need some adjustment.
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: SQL:2011 application time
@ 2023-08-31 21:26 Paul Jungwirth <[email protected]>
parent: Peter Eisentraut <[email protected]>
0 siblings, 2 replies; 37+ messages in thread
From: Paul Jungwirth @ 2023-08-31 21:26 UTC (permalink / raw)
To: Peter Eisentraut <[email protected]>; +Cc: Corey Huinker <[email protected]>; PostgreSQL Hackers <[email protected]>; Jaime Casanova <[email protected]>; jian he <[email protected]>; Daniel Gustafsson <[email protected]>
On 7/12/23 01:24, Peter Eisentraut wrote:
> On 07.07.23 03:03, Paul A Jungwirth wrote:
>> Here are some new patch files based on discussions from PGCon.
>
> Here are a few fixup patches to get things building without warnings and
> errors.
>
> The last patch (your 0005) fails the regression test for me and it
> didn't appear to be a trivial problem, so please take another look at
> that sometime. (Since it's the last patch, it's obviously lower priority.)
Hello,
Here are the latest patches for my temporal tables work. They are
rebased on e8d74ad6 from Aug 31.
I incorporated Peter's edits mentioned above and have made various other
improvements.
The most significant change is support for partitioned tables. Those now
work with temporal PRIMARY KEY and UNIQUE constraints, FOR PORTION OF
commands, and FOREIGN KEYs.
I've tried to clean up the first four patches to get them ready for
committing, since they could get committed before the PERIOD patch. I
think there is a little more cleanup needed but they should be ready for
a review.
The PERIOD patch is not finished and includes some deliberately-failing
tests. I did make some progress here finishing ALTER TABLE ADD PERIOD.
I could use help handling the INTERNAL depenency from the PERIOD to its
(hidden) GENERATED column. The problem is in findDependentObjects: if
you drop the PERIOD, then Postgres automatically tries to drop the
column (correctly), but then it seems to think it needs to drop the
whole table. I think this is because a column's object address is the
table's object address plus a subaddress equaling the attno, and
findDependentObjects thinks it should drop the whole thing. I'm sure I
can sort this out, but if anyone has a suggestion it might save me some
time.
Thanks for taking a look!
Yours,
--
Paul ~{:-)
[email protected]
Attachments:
[text/x-patch] v13-0001-Add-temporal-PRIMARY-KEY-and-UNIQUE-constraints.patch (76.4K, ../../[email protected]/2-v13-0001-Add-temporal-PRIMARY-KEY-and-UNIQUE-constraints.patch)
download | inline diff:
From 61d8bd082e9649af3c55d4332f6b4c48260fd607 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Mon, 28 Jun 2021 17:33:27 -0700
Subject: [PATCH v13 1/5] Add temporal PRIMARY KEY and UNIQUE constraints
- Added WITHOUT OVERLAPS to the bison grammar. We permit only range
columns, not yet PERIODs (which we'll add soon).
- Temporal PRIMARY KEYs and UNIQUE constraints are backed by GiST
indexes instead of B-tree indexes, since they are essentially
exclusion constraints with = for the scalar parts of the key and &&
for the temporal part.
- Added pg_constraint.contemporal to say whether a constraint is a
temporal constraint.
- Added docs and tests.
- Added pg_dump support.
---
doc/src/sgml/ref/create_table.sgml | 38 +-
src/backend/catalog/heap.c | 2 +
src/backend/catalog/index.c | 17 +-
src/backend/catalog/pg_constraint.c | 2 +
src/backend/commands/indexcmds.c | 160 +++++++-
src/backend/commands/tablecmds.c | 14 +-
src/backend/commands/trigger.c | 1 +
src/backend/commands/typecmds.c | 1 +
src/backend/nodes/makefuncs.c | 3 +-
src/backend/parser/gram.y | 32 +-
src/backend/parser/parse_utilcmd.c | 124 +++++-
src/backend/utils/adt/ruleutils.c | 29 +-
src/backend/utils/cache/relcache.c | 10 +-
src/bin/pg_dump/pg_dump.c | 35 +-
src/bin/pg_dump/pg_dump.h | 1 +
src/bin/pg_dump/t/002_pg_dump.pl | 46 +++
src/bin/psql/describe.c | 12 +-
src/include/catalog/index.h | 1 +
src/include/catalog/pg_constraint.h | 9 +-
src/include/commands/defrem.h | 3 +-
src/include/nodes/execnodes.h | 2 +
src/include/nodes/makefuncs.h | 2 +-
src/include/nodes/parsenodes.h | 4 +
.../regress/expected/without_overlaps.out | 359 ++++++++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/without_overlaps.sql | 262 +++++++++++++
26 files changed, 1104 insertions(+), 67 deletions(-)
create mode 100644 src/test/regress/expected/without_overlaps.out
create mode 100644 src/test/regress/sql/without_overlaps.sql
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e04a0692c4..87f0aab13b 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -78,8 +78,8 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
[ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
{ CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
NOT NULL <replaceable class="parameter">column_name</replaceable> [ NO INHERIT ] |
- UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> |
- PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> |
+ UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">temporal_interval</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
+ PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">temporal_interval</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
@@ -107,6 +107,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
{ <replaceable class="parameter">column_name</replaceable> | ( <replaceable class="parameter">expression</replaceable> ) } [ <replaceable class="parameter">opclass</replaceable> ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ]
+<phrase><replaceable class="parameter">temporal_interval</replaceable> in a <literal>PRIMARY KEY</literal>, <literal>UNIQUE</literal>, or <literal>FOREIGN KEY</literal> constraint is:</phrase>
+
+<replaceable class="parameter">range_column_name</replaceable>
+
<phrase><replaceable class="parameter">referential_action</replaceable> in a <literal>FOREIGN KEY</literal>/<literal>REFERENCES</literal> constraint is:</phrase>
{ NO ACTION | RESTRICT | CASCADE | SET NULL [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] | SET DEFAULT [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] }
@@ -1001,7 +1005,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<para>
Adding a unique constraint will automatically create a unique btree
- index on the column or group of columns used in the constraint.
+ index on the column or group of columns used in the constraint. But if
+ the constraint includes a <literal>WITHOUT OVERLAPS</literal> clause,
+ it will use a GiST index and behave like a temporal <literal>PRIMARY
+ KEY</literal>: preventing duplicates only in overlapping time periods.
</para>
<para>
@@ -1019,7 +1026,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<varlistentry id="sql-createtable-parms-primary-key">
<term><literal>PRIMARY KEY</literal> (column constraint)</term>
- <term><literal>PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] )</literal>
+ <term><literal>PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ]
+ [, <replaceable class="parameter">temporal_interval</replaceable> WITHOUT OVERLAPS ] )</literal>
<optional> <literal>INCLUDE ( <replaceable class="parameter">column_name</replaceable> [, ...])</literal> </optional> (table constraint)</term>
<listitem>
<para>
@@ -1053,8 +1061,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<para>
Adding a <literal>PRIMARY KEY</literal> constraint will automatically
- create a unique btree index on the column or group of columns used in the
- constraint.
+ create a unique btree index (or GiST if temporal) on the column or group of
+ columns used in the constraint.
</para>
<para>
@@ -1067,6 +1075,24 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
(e.g., <literal>DROP COLUMN</literal>) can cause cascaded constraint and
index deletion.
</para>
+
+ <para>
+ A <literal>PRIMARY KEY</literal> with a <literal>WITHOUT OVERLAPS</literal> option
+ is a <emphasis>temporal</emphasis> primary key.
+ The <literal>WITHOUT OVERLAPS</literal> value
+ must be a range type and is used to constrain the record's applicability
+ to just that interval (usually a range of dates or timestamps).
+ The main part of the primary key may be repeated in other rows of the table,
+ as long as records with the same key don't overlap in the
+ <literal>WITHOUT OVERLAPS</literal> column.
+ </para>
+
+ <para>
+ A temporal <literal>PRIMARY KEY</literal> is enforced with an
+ <literal>EXCLUDE</literal> constraint rather than a <literal>UNIQUE</literal>
+ constraint, backed by a GiST index. You may need to install the
+ <xref linkend="btree-gist"/> extension to create temporal primary keys.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index b42711f574..7148dd1787 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2140,6 +2140,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
is_local, /* conislocal */
inhcount, /* coninhcount */
is_no_inherit, /* connoinherit */
+ false, /* contemporal */
is_internal); /* internally constructed? */
pfree(ccbin);
@@ -2190,6 +2191,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
is_local,
inhcount,
is_no_inherit,
+ false, /* contemporal */
false);
return constrOid;
}
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index fd09378848..2bef287951 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -255,8 +255,8 @@ index_check_primary_key(Relation heapRel,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("primary keys cannot be expressions")));
- /* System attributes are never null, so no need to check */
- if (attnum < 0)
+ /* * System attributes are never null, so no need to check. */
+ if (attnum <= 0)
continue;
atttuple = SearchSysCache2(ATTNUM,
@@ -1377,7 +1377,8 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId,
oldInfo->ii_NullsNotDistinct,
false, /* not ready for inserts */
true,
- indexRelation->rd_indam->amsummarizing);
+ indexRelation->rd_indam->amsummarizing,
+ oldInfo->ii_Temporal);
/*
* Extract the list of column names and the column numbers for the new
@@ -1897,6 +1898,7 @@ index_concurrently_set_dead(Oid heapId, Oid indexId)
* INDEX_CONSTR_CREATE_UPDATE_INDEX: update the pg_index row
* INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS: remove existing dependencies
* of index on table's columns
+ * INDEX_CONSTR_CREATE_TEMPORAL: constraint is for a temporal primary key
* allow_system_table_mods: allow table to be a system catalog
* is_internal: index is constructed due to internal process
*/
@@ -1920,11 +1922,13 @@ index_constraint_create(Relation heapRelation,
bool mark_as_primary;
bool islocal;
bool noinherit;
+ bool is_temporal;
int inhcount;
deferrable = (constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) != 0;
initdeferred = (constr_flags & INDEX_CONSTR_CREATE_INIT_DEFERRED) != 0;
mark_as_primary = (constr_flags & INDEX_CONSTR_CREATE_MARK_AS_PRIMARY) != 0;
+ is_temporal = (constr_flags & INDEX_CONSTR_CREATE_TEMPORAL) != 0;
/* constraint creation support doesn't work while bootstrapping */
Assert(!IsBootstrapProcessingMode());
@@ -2001,6 +2005,7 @@ index_constraint_create(Relation heapRelation,
islocal,
inhcount,
noinherit,
+ is_temporal, /* contemporal */
is_internal);
/*
@@ -2450,7 +2455,8 @@ BuildIndexInfo(Relation index)
indexStruct->indnullsnotdistinct,
indexStruct->indisready,
false,
- index->rd_indam->amsummarizing);
+ index->rd_indam->amsummarizing,
+ false);
/* fill in attribute numbers */
for (i = 0; i < numAtts; i++)
@@ -2511,7 +2517,8 @@ BuildDummyIndexInfo(Relation index)
indexStruct->indnullsnotdistinct,
indexStruct->indisready,
false,
- index->rd_indam->amsummarizing);
+ index->rd_indam->amsummarizing,
+ false);
/* fill in attribute numbers */
for (i = 0; i < numAtts; i++)
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index e9d4d6006e..d5329ca8c9 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -78,6 +78,7 @@ CreateConstraintEntry(const char *constraintName,
bool conIsLocal,
int conInhCount,
bool conNoInherit,
+ bool conTemporal,
bool is_internal)
{
Relation conDesc;
@@ -193,6 +194,7 @@ CreateConstraintEntry(const char *constraintName,
values[Anum_pg_constraint_conislocal - 1] = BoolGetDatum(conIsLocal);
values[Anum_pg_constraint_coninhcount - 1] = Int16GetDatum(conInhCount);
values[Anum_pg_constraint_connoinherit - 1] = BoolGetDatum(conNoInherit);
+ values[Anum_pg_constraint_contemporal - 1] = BoolGetDatum(conTemporal);
if (conkeyArray)
values[Anum_pg_constraint_conkey - 1] = PointerGetDatum(conkeyArray);
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index ab8b81b302..87d6bc3304 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -73,6 +73,11 @@
/* non-export function prototypes */
static bool CompareOpclassOptions(const Datum *opts1, const Datum *opts2, int natts);
static void CheckPredicate(Expr *predicate);
+static void get_index_attr_temporal_operator(Oid opclass,
+ Oid atttype,
+ bool isoverlaps,
+ Oid *opid,
+ int *strat);
static void ComputeIndexAttrs(IndexInfo *indexInfo,
Oid *typeOids,
Oid *collationOids,
@@ -85,6 +90,7 @@ static void ComputeIndexAttrs(IndexInfo *indexInfo,
Oid accessMethodId,
bool amcanorder,
bool isconstraint,
+ bool istemporal,
Oid ddl_userid,
int ddl_sec_context,
int *ddl_save_nestlevel);
@@ -142,6 +148,7 @@ typedef struct ReindexErrorInfo
* to index on.
* 'exclusionOpNames': list of names of exclusion-constraint operators,
* or NIL if not an exclusion constraint.
+ * 'istemporal': true iff this index has a WITHOUT OVERLAPS clause.
*
* This is tailored to the needs of ALTER TABLE ALTER TYPE, which recreates
* any indexes that depended on a changing column from their pg_get_indexdef
@@ -171,7 +178,8 @@ bool
CheckIndexCompatible(Oid oldId,
const char *accessMethodName,
const List *attributeList,
- const List *exclusionOpNames)
+ const List *exclusionOpNames,
+ bool istemporal)
{
bool isconstraint;
Oid *typeIds;
@@ -234,7 +242,7 @@ CheckIndexCompatible(Oid oldId,
*/
indexInfo = makeIndexInfo(numberOfAttributes, numberOfAttributes,
accessMethodId, NIL, NIL, false, false,
- false, false, amsummarizing);
+ false, false, amsummarizing, false);
typeIds = palloc_array(Oid, numberOfAttributes);
collationIds = palloc_array(Oid, numberOfAttributes);
opclassIds = palloc_array(Oid, numberOfAttributes);
@@ -244,8 +252,8 @@ CheckIndexCompatible(Oid oldId,
coloptions, attributeList,
exclusionOpNames, relationId,
accessMethodName, accessMethodId,
- amcanorder, isconstraint, InvalidOid, 0, NULL);
-
+ amcanorder, isconstraint, istemporal, InvalidOid,
+ 0, NULL);
/* Get the soon-obsolete pg_index tuple. */
tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(oldId));
@@ -553,6 +561,7 @@ DefineIndex(Oid tableId,
bool amcanorder;
bool amissummarizing;
amoptions_function amoptions;
+ bool exclusion;
bool partitioned;
bool safe_index;
Datum reloptions;
@@ -671,6 +680,12 @@ DefineIndex(Oid tableId,
namespaceId = RelationGetNamespace(rel);
+ /*
+ * It has exclusion constraint behavior if it's an EXCLUDE constraint
+ * or a temporal PRIMARY KEY/UNIQUE constraint
+ */
+ exclusion = stmt->excludeOpNames || stmt->istemporal;
+
/* Ensure that it makes sense to index this kind of relation */
switch (rel->rd_rel->relkind)
{
@@ -839,7 +854,7 @@ DefineIndex(Oid tableId,
pgstat_progress_update_param(PROGRESS_CREATEIDX_ACCESS_METHOD_OID,
accessMethodId);
- if (stmt->unique && !amRoutine->amcanunique)
+ if (stmt->unique && !stmt->istemporal && !amRoutine->amcanunique)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("access method \"%s\" does not support unique indexes",
@@ -854,7 +869,7 @@ DefineIndex(Oid tableId,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("access method \"%s\" does not support multicolumn indexes",
accessMethodName)));
- if (stmt->excludeOpNames && amRoutine->amgettuple == NULL)
+ if (exclusion && amRoutine->amgettuple == NULL)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("access method \"%s\" does not support exclusion constraints",
@@ -895,7 +910,8 @@ DefineIndex(Oid tableId,
stmt->nulls_not_distinct,
!concurrent,
concurrent,
- amissummarizing);
+ amissummarizing,
+ stmt->istemporal);
typeIds = palloc_array(Oid, numberOfAttributes);
collationIds = palloc_array(Oid, numberOfAttributes);
@@ -906,8 +922,9 @@ DefineIndex(Oid tableId,
coloptions, allIndexParams,
stmt->excludeOpNames, tableId,
accessMethodName, accessMethodId,
- amcanorder, stmt->isconstraint, root_save_userid,
- root_save_sec_context, &root_save_nestlevel);
+ amcanorder, stmt->isconstraint, stmt->istemporal,
+ root_save_userid, root_save_sec_context,
+ &root_save_nestlevel);
/*
* Extra checks when creating a PRIMARY KEY index.
@@ -925,7 +942,7 @@ DefineIndex(Oid tableId,
* We could lift this limitation if we had global indexes, but those have
* their own problems, so this is a useful feature combination.
*/
- if (partitioned && (stmt->unique || stmt->excludeOpNames))
+ if (partitioned && (stmt->unique || exclusion))
{
PartitionKey key = RelationGetPartitionKey(rel);
const char *constraint_type;
@@ -933,6 +950,8 @@ DefineIndex(Oid tableId,
if (stmt->primary)
constraint_type = "PRIMARY KEY";
+ else if (stmt->unique && stmt->istemporal)
+ constraint_type = "temporal UNIQUE";
else if (stmt->unique)
constraint_type = "UNIQUE";
else if (stmt->excludeOpNames)
@@ -979,10 +998,10 @@ DefineIndex(Oid tableId,
* associated with index columns, too. We know what to do with
* btree opclasses; if there are ever any other index types that
* support unique indexes, this logic will need extension. But if
- * we have an exclusion constraint, it already knows the
- * operators, so we don't have to infer them.
+ * we have an exclusion constraint (or a temporal PK), it already
+ * knows the operators, so we don't have to infer them.
*/
- if (stmt->unique && accessMethodId != BTREE_AM_OID)
+ if (stmt->unique && !stmt->istemporal && accessMethodId != BTREE_AM_OID)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot match partition key to an index using access method \"%s\"",
@@ -1015,12 +1034,12 @@ DefineIndex(Oid tableId,
{
Oid idx_eqop = InvalidOid;
- if (stmt->unique)
+ if (stmt->unique && !stmt->istemporal)
idx_eqop = get_opfamily_member(idx_opfamily,
idx_opcintype,
idx_opcintype,
BTEqualStrategyNumber);
- else if (stmt->excludeOpNames)
+ else if (exclusion)
idx_eqop = indexInfo->ii_ExclusionOps[j];
Assert(idx_eqop);
@@ -1029,7 +1048,7 @@ DefineIndex(Oid tableId,
found = true;
break;
}
- else if (stmt->excludeOpNames)
+ else if (exclusion)
{
/*
* We found a match, but it's not an equality
@@ -1173,6 +1192,8 @@ DefineIndex(Oid tableId,
constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE;
if (stmt->initdeferred)
constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED;
+ if (stmt->istemporal)
+ constr_flags |= INDEX_CONSTR_CREATE_TEMPORAL;
indexRelationId =
index_create(rel, indexRelationName, indexRelationId, parentIndexId,
@@ -1841,6 +1862,91 @@ CheckPredicate(Expr *predicate)
errmsg("functions in index predicate must be marked IMMUTABLE")));
}
+/*
+ * get_index_attr_temporal_operator
+ *
+ * Finds an operator for a temporal index attribute.
+ * We need an equality operator for normal keys
+ * and an overlaps operator for the range.
+ * Returns the operator oid and strategy in opid and strat,
+ * respectively.
+ */
+static void
+get_index_attr_temporal_operator(Oid opclass,
+ Oid atttype,
+ bool isoverlaps,
+ Oid *opid,
+ int *strat)
+{
+ Oid opfamily;
+ const char *opname;
+
+ opfamily = get_opclass_family(opclass);
+ /*
+ * If we have a range type, fall back on anyrange.
+ * This seems like a hack
+ * but I can't find any existing lookup function
+ * that knows about pseudotypes.
+ * compatible_oper is close but wants a *name*,
+ * and the point here is to avoid hardcoding a name
+ * (although it should always be = and &&).
+ *
+ * In addition for the normal key elements
+ * try both RTEqualStrategyNumber and BTEqualStrategyNumber.
+ * If you're using btree_gist then you'll need the latter.
+ */
+ if (isoverlaps)
+ {
+ *strat = RTOverlapStrategyNumber;
+ opname = "overlaps";
+ *opid = get_opfamily_member(opfamily, atttype, atttype, *strat);
+ if (!OidIsValid(*opid) && type_is_range(atttype))
+ *opid = get_opfamily_member(opfamily, ANYRANGEOID, ANYRANGEOID, *strat);
+ }
+ else
+ {
+ *strat = RTEqualStrategyNumber;
+ opname = "equality";
+ *opid = get_opfamily_member(opfamily, atttype, atttype, *strat);
+ if (!OidIsValid(*opid) && type_is_range(atttype))
+ *opid = get_opfamily_member(opfamily, ANYRANGEOID, ANYRANGEOID, *strat);
+
+ if (!OidIsValid(*opid))
+ {
+ *strat = BTEqualStrategyNumber;
+ *opid = get_opfamily_member(opfamily, atttype, atttype, *strat);
+ if (!OidIsValid(*opid) && type_is_range(atttype))
+ *opid = get_opfamily_member(opfamily, ANYRANGEOID, ANYRANGEOID, *strat);
+ }
+ }
+
+ if (!OidIsValid(*opid))
+ {
+ HeapTuple opftuple;
+ Form_pg_opfamily opfform;
+
+ /*
+ * attribute->opclass might not explicitly name the opfamily,
+ * so fetch the name of the selected opfamily for use in the
+ * error message.
+ */
+ opftuple = SearchSysCache1(OPFAMILYOID,
+ ObjectIdGetDatum(opfamily));
+ if (!HeapTupleIsValid(opftuple))
+ elog(ERROR, "cache lookup failed for opfamily %u",
+ opfamily);
+ opfform = (Form_pg_opfamily) GETSTRUCT(opftuple);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("no %s operator found for WITHOUT OVERLAPS constraint", opname),
+ errdetail("There must be an %s operator within opfamily \"%s\" for type \"%s\".",
+ opname,
+ NameStr(opfform->opfname),
+ format_type_be(atttype))));
+ }
+}
+
/*
* Compute per-index-column information, including indexed column numbers
* or index expressions, opclasses and their options. Note, all output vectors
@@ -1863,6 +1969,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
Oid accessMethodId,
bool amcanorder,
bool isconstraint,
+ bool istemporal,
Oid ddl_userid,
int ddl_sec_context,
int *ddl_save_nestlevel)
@@ -1886,6 +1993,14 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
else
nextExclOp = NULL;
+ /* exclusionOpNames can be non-NIL if we are creating a partition */
+ if (istemporal && exclusionOpNames == NIL)
+ {
+ indexInfo->ii_ExclusionOps = palloc_array(Oid, nkeycols);
+ indexInfo->ii_ExclusionProcs = palloc_array(Oid, nkeycols);
+ indexInfo->ii_ExclusionStrats = palloc_array(uint16, nkeycols);
+ }
+
if (OidIsValid(ddl_userid))
GetUserIdAndSecContext(&save_userid, &save_sec_context);
@@ -2161,6 +2276,19 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
indexInfo->ii_ExclusionStrats[attn] = strat;
nextExclOp = lnext(exclusionOpNames, nextExclOp);
}
+ else if (istemporal)
+ {
+ int strat;
+ Oid opid;
+ get_index_attr_temporal_operator(opclassOids[attn],
+ atttype,
+ attn == nkeycols - 1, /* TODO: Don't assume it's last? */
+ &opid,
+ &strat);
+ indexInfo->ii_ExclusionOps[attn] = opid;
+ indexInfo->ii_ExclusionProcs[attn] = get_opcode(opid);
+ indexInfo->ii_ExclusionStrats[attn] = strat;
+ }
/*
* Set up the per-column options (indoption field). For now, this is
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d097da3c78..768eaa1deb 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -208,6 +208,7 @@ typedef struct NewConstraint
Oid refrelid; /* PK rel, if FOREIGN */
Oid refindid; /* OID of PK's index, if FOREIGN */
Oid conid; /* OID of pg_constraint entry, if FOREIGN */
+ bool contemporal; /* Whether the new constraint is temporal */
Node *qual; /* Check expr or CONSTR_FOREIGN Constraint */
ExprState *qualstate; /* Execution state for CHECK expr */
} NewConstraint;
@@ -9998,6 +9999,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
conislocal, /* islocal */
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
+ false, /* conTemporal */
false); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10296,6 +10298,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
false,
1,
false,
+ false, /* conTemporal */
false);
/*
@@ -10801,6 +10804,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
false, /* islocal */
1, /* inhcount */
false, /* conNoInherit */
+ false, /* conTemporal */
true);
/* Set up partition dependencies for the new constraint */
@@ -11475,6 +11479,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
newcon->refrelid = con->confrelid;
newcon->refindid = con->conindid;
newcon->conid = con->oid;
+ newcon->contemporal = con->contemporal;
newcon->qual = (Node *) fkconstraint;
/* Find or create work queue entry for this table */
@@ -11641,10 +11646,12 @@ transformColumnNameList(Oid relId, List *colList,
*
* Look up the names, attnums, and types of the primary key attributes
* for the pkrel. Also return the index OID and index opclasses of the
- * index supporting the primary key.
+ * index supporting the primary key. If this is a temporal primary key,
+ * also set the WITHOUT OVERLAPS attribute name, attnum, and atttypid.
*
* All parameters except pkrel are output parameters. Also, the function
- * return value is the number of attributes in the primary key.
+ * return value is the number of attributes in the primary key,
+ * not including the WITHOUT OVERLAPS if any.
*
* Used when the column list in the REFERENCES specification is omitted.
*/
@@ -14187,7 +14194,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)
if (CheckIndexCompatible(oldId,
stmt->accessMethod,
stmt->indexParams,
- stmt->excludeOpNames))
+ stmt->excludeOpNames,
+ stmt->istemporal))
{
Relation irel = index_open(oldId, NoLock);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 52177759ab..0577b60415 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -841,6 +841,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
true, /* islocal */
0, /* inhcount */
true, /* noinherit */
+ false, /* contemporal */
isInternal); /* is_internal */
}
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 5e97606793..6d0e946684 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3544,6 +3544,7 @@ domainAddConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
true, /* is local */
0, /* inhcount */
false, /* connoinherit */
+ false, /* contemporal */
false); /* is_internal */
if (constrAddr)
ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid);
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 0e7e6e46d9..19dfac5f27 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -745,7 +745,7 @@ make_ands_implicit(Expr *clause)
IndexInfo *
makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid, List *expressions,
List *predicates, bool unique, bool nulls_not_distinct,
- bool isready, bool concurrent, bool summarizing)
+ bool isready, bool concurrent, bool summarizing, bool temporal)
{
IndexInfo *n = makeNode(IndexInfo);
@@ -755,6 +755,7 @@ makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid, List *expressions,
Assert(n->ii_NumIndexKeyAttrs <= n->ii_NumIndexAttrs);
n->ii_Unique = unique;
n->ii_NullsNotDistinct = nulls_not_distinct;
+ n->ii_Temporal = temporal;
n->ii_ReadyForInserts = isready;
n->ii_CheckedUnchanged = false;
n->ii_IndexUnchanged = false;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7d2032885e..ca6d13d349 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -523,7 +523,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <node> TableElement TypedTableElement ConstraintElem TableFuncElement
%type <node> columnDef columnOptions
%type <defelt> def_elem reloption_elem old_aggr_elem operator_def_elem
-%type <node> def_arg columnElem where_clause where_or_current_clause
+%type <node> def_arg columnElem withoutOverlapsClause
+ where_clause where_or_current_clause
a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound
columnref in_expr having_clause func_table xmltable array_expr
OptWhereClause operator_def_arg
@@ -3876,6 +3877,7 @@ ColConstraintElem:
n->contype = CONSTR_PRIMARY;
n->location = @1;
n->keys = NULL;
+ n->without_overlaps = NULL;
n->options = $3;
n->indexname = NULL;
n->indexspace = $4;
@@ -4096,7 +4098,7 @@ ConstraintElem:
n->initially_valid = true;
$$ = (Node *) n;
}
- | UNIQUE opt_unique_null_treatment '(' columnList ')' opt_c_include opt_definition OptConsTableSpace
+ | UNIQUE opt_unique_null_treatment '(' columnList withoutOverlapsClause ')' opt_c_include opt_definition OptConsTableSpace
ConstraintAttributeSpec
{
Constraint *n = makeNode(Constraint);
@@ -4105,11 +4107,12 @@ ConstraintElem:
n->location = @1;
n->nulls_not_distinct = !$2;
n->keys = $4;
- n->including = $6;
- n->options = $7;
+ n->without_overlaps = $5;
+ n->including = $7;
+ n->options = $8;
n->indexname = NULL;
- n->indexspace = $8;
- processCASbits($9, @9, "UNIQUE",
+ n->indexspace = $9;
+ processCASbits($10, @10, "UNIQUE",
&n->deferrable, &n->initdeferred, NULL,
NULL, yyscanner);
$$ = (Node *) n;
@@ -4130,7 +4133,7 @@ ConstraintElem:
NULL, yyscanner);
$$ = (Node *) n;
}
- | PRIMARY KEY '(' columnList ')' opt_c_include opt_definition OptConsTableSpace
+ | PRIMARY KEY '(' columnList withoutOverlapsClause ')' opt_c_include opt_definition OptConsTableSpace
ConstraintAttributeSpec
{
Constraint *n = makeNode(Constraint);
@@ -4138,11 +4141,12 @@ ConstraintElem:
n->contype = CONSTR_PRIMARY;
n->location = @1;
n->keys = $4;
- n->including = $6;
- n->options = $7;
+ n->without_overlaps = $5;
+ n->including = $7;
+ n->options = $8;
n->indexname = NULL;
- n->indexspace = $8;
- processCASbits($9, @9, "PRIMARY KEY",
+ n->indexspace = $9;
+ processCASbits($10, @10, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
NULL, yyscanner);
$$ = (Node *) n;
@@ -4154,6 +4158,7 @@ ConstraintElem:
n->contype = CONSTR_PRIMARY;
n->location = @1;
n->keys = NIL;
+ n->without_overlaps = NULL;
n->including = NIL;
n->options = NIL;
n->indexname = $3;
@@ -4220,6 +4225,11 @@ columnList:
| columnList ',' columnElem { $$ = lappend($1, $3); }
;
+withoutOverlapsClause:
+ ',' columnElem WITHOUT OVERLAPS { $$ = $2; }
+ | /*EMPTY*/ { $$ = NULL; }
+ ;
+
columnElem: ColId
{
$$ = (Node *) makeString($1);
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 55c315f0e2..6723a74f2f 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -124,6 +124,8 @@ static List *get_opclass(Oid opclass, Oid actual_datatype);
static void transformIndexConstraints(CreateStmtContext *cxt);
static IndexStmt *transformIndexConstraint(Constraint *constraint,
CreateStmtContext *cxt);
+static bool findNewOrOldColumn(CreateStmtContext *cxt, char *colname, char **typname,
+ Oid *typid);
static void transformExtendedStatistics(CreateStmtContext *cxt);
static void transformFKConstraints(CreateStmtContext *cxt,
bool skipValidation,
@@ -1725,6 +1727,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
index->unique = idxrec->indisunique;
index->nulls_not_distinct = idxrec->indnullsnotdistinct;
index->primary = idxrec->indisprimary;
+ index->istemporal = (idxrec->indisprimary || idxrec->indisunique) && idxrec->indisexclusion;
index->transformed = true; /* don't need transformIndexStmt */
index->concurrent = false;
index->if_not_exists = false;
@@ -1774,7 +1777,9 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
int nElems;
int i;
- Assert(conrec->contype == CONSTRAINT_EXCLUSION);
+ Assert(conrec->contype == CONSTRAINT_EXCLUSION ||
+ (index->istemporal &&
+ (conrec->contype == CONSTRAINT_PRIMARY || conrec->contype == CONSTRAINT_UNIQUE)));
/* Extract operator OIDs from the pg_constraint tuple */
datum = SysCacheGetAttrNotNull(CONSTROID, ht_constr,
Anum_pg_constraint_conexclop);
@@ -2314,6 +2319,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
}
index->nulls_not_distinct = constraint->nulls_not_distinct;
index->isconstraint = true;
+ index->istemporal = constraint->without_overlaps != NULL;
index->deferrable = constraint->deferrable;
index->initdeferred = constraint->initdeferred;
@@ -2406,6 +2412,11 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
errmsg("index \"%s\" is not valid", index_name),
parser_errposition(cxt->pstate, constraint->location)));
+ /*
+ * Today we forbid non-unique indexes, but we could permit GiST
+ * indexes whose last entry is a range type and use that to create a
+ * WITHOUT OVERLAPS constraint (i.e. a temporal constraint).
+ */
if (!index_form->indisunique)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
@@ -2682,6 +2693,65 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
notnullcmds = lappend(notnullcmds, notnullcmd);
}
}
+
+ /*
+ * Anything in without_overlaps should be included,
+ * but with the overlaps operator (&&) instead of equality.
+ */
+ if (constraint->without_overlaps != NULL) {
+ char *without_overlaps_str = strVal(constraint->without_overlaps);
+ IndexElem *iparam = makeNode(IndexElem);
+ char *typname;
+ Oid typid;
+
+ /*
+ * Iterate through the table's columns
+ * (like just a little bit above).
+ * If we find one whose name is the same as without_overlaps,
+ * validate that it's a range type.
+ *
+ * Otherwise report an error.
+ */
+
+ if (findNewOrOldColumn(cxt, without_overlaps_str, &typname, &typid))
+ {
+ if (!type_is_range(typid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("column \"%s\" named in WITHOUT OVERLAPS is not a range type",
+ without_overlaps_str)));
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("range or PERIOD \"%s\" named in WITHOUT OVERLAPS does not exist",
+ without_overlaps_str)));
+
+ iparam->name = pstrdup(without_overlaps_str);
+ iparam->expr = NULL;
+ iparam->indexcolname = NULL;
+ iparam->collation = NIL;
+ iparam->opclass = NIL;
+ iparam->opclassopts = NIL;
+ iparam->ordering = SORTBY_DEFAULT;
+ iparam->nulls_ordering = SORTBY_NULLS_DEFAULT;
+ index->indexParams = lappend(index->indexParams, iparam);
+
+ index->accessMethod = "gist";
+ constraint->access_method = "gist";
+
+ if (constraint->contype == CONSTR_PRIMARY)
+ {
+ /*
+ * Force the column to NOT NULL since it is part of the primary key.
+ */
+ AlterTableCmd *notnullcmd = makeNode(AlterTableCmd);
+
+ notnullcmd->subtype = AT_SetAttNotNull;
+ notnullcmd->name = pstrdup(without_overlaps_str);
+ notnullcmds = lappend(notnullcmds, notnullcmd);
+ }
+ }
}
/*
@@ -2801,6 +2871,58 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
return index;
}
+/*
+ * Tries to find a column by name among the existing ones (if it's an ALTER TABLE)
+ * and the new ones. Sets typname and typid if one is found. Returns false if we
+ * couldn't find a match.
+ */
+static bool
+findNewOrOldColumn(CreateStmtContext *cxt, char *colname, char **typname, Oid *typid)
+{
+ /* Check the new columns first in case their type is changing. */
+
+ ColumnDef *column = NULL;
+ ListCell *columns;
+
+ foreach(columns, cxt->columns)
+ {
+ column = lfirst_node(ColumnDef, columns);
+ if (strcmp(column->colname, colname) == 0)
+ {
+ *typid = typenameTypeId(NULL, column->typeName);
+ *typname = TypeNameToString(column->typeName);
+ return true;
+ }
+ }
+
+ /* Look up columns on existing table. */
+
+ if (cxt->isalter)
+ {
+ Relation rel = cxt->rel;
+ for (int i = 0; i < rel->rd_att->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(rel->rd_att, i);
+ const char *attname;
+
+ if (attr->attisdropped)
+ continue;
+
+ attname = NameStr(attr->attname);
+ if (strcmp(attname, colname) == 0)
+ {
+ Type type = typeidType(attr->atttypid);
+ *typid = attr->atttypid;
+ *typname = pstrdup(typeTypeName(type));
+ ReleaseSysCache(type);
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
/*
* transformExtendedStatistics
* Handle extended statistic objects
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 97b0ef22ac..778d3e0334 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -338,8 +338,8 @@ static char *deparse_expression_pretty(Node *expr, List *dpcontext,
static char *pg_get_viewdef_worker(Oid viewoid,
int prettyFlags, int wrapColumn);
static char *pg_get_triggerdef_worker(Oid trigid, bool pretty);
-static int decompile_column_index_array(Datum column_index_array, Oid relId,
- StringInfo buf);
+static int decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
+ bool withoutOverlaps, StringInfo buf);
static char *pg_get_ruledef_worker(Oid ruleoid, int prettyFlags);
static char *pg_get_indexdef_worker(Oid indexrelid, int colno,
const Oid *excludeOps,
@@ -2247,7 +2247,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
val = SysCacheGetAttrNotNull(CONSTROID, tup,
Anum_pg_constraint_conkey);
- decompile_column_index_array(val, conForm->conrelid, &buf);
+ decompile_column_index_array(val, conForm->conrelid, conForm->conindid, false, &buf);
/* add foreign relation name */
appendStringInfo(&buf, ") REFERENCES %s(",
@@ -2258,7 +2258,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
val = SysCacheGetAttrNotNull(CONSTROID, tup,
Anum_pg_constraint_confkey);
- decompile_column_index_array(val, conForm->confrelid, &buf);
+ decompile_column_index_array(val, conForm->confrelid, conForm->conindid, false, &buf);
appendStringInfoChar(&buf, ')');
@@ -2344,7 +2344,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
if (!isnull)
{
appendStringInfoString(&buf, " (");
- decompile_column_index_array(val, conForm->conrelid, &buf);
+ decompile_column_index_array(val, conForm->conrelid, InvalidOid, false, &buf);
appendStringInfoChar(&buf, ')');
}
@@ -2357,6 +2357,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
Oid indexId;
int keyatts;
HeapTuple indtup;
+ bool isnull;
/* Start off the constraint definition */
if (conForm->contype == CONSTRAINT_PRIMARY)
@@ -2379,7 +2380,14 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
val = SysCacheGetAttrNotNull(CONSTROID, tup,
Anum_pg_constraint_conkey);
- keyatts = decompile_column_index_array(val, conForm->conrelid, &buf);
+ /*
+ * If it has exclusion-style operator OIDs
+ * then it uses WITHOUT OVERLAPS.
+ */
+ indexId = conForm->conindid;
+ SysCacheGetAttr(CONSTROID, tup,
+ Anum_pg_constraint_conexclop, &isnull);
+ keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, &buf);
appendStringInfoChar(&buf, ')');
@@ -2574,8 +2582,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
* of keys.
*/
static int
-decompile_column_index_array(Datum column_index_array, Oid relId,
- StringInfo buf)
+decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
+ bool withoutOverlaps, StringInfo buf)
{
Datum *keys;
int nKeys;
@@ -2588,11 +2596,14 @@ decompile_column_index_array(Datum column_index_array, Oid relId,
for (j = 0; j < nKeys; j++)
{
char *colName;
+ int colid = DatumGetInt16(keys[j]);
- colName = get_attname(relId, DatumGetInt16(keys[j]), false);
+ colName = get_attname(relId, colid, false);
if (j == 0)
appendStringInfoString(buf, quote_identifier(colName));
+ else if (withoutOverlaps && j == nKeys - 1)
+ appendStringInfo(buf, ", %s WITHOUT OVERLAPS", quote_identifier(colName));
else
appendStringInfo(buf, ", %s", quote_identifier(colName));
}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 7234cb3da6..77a5b25889 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5536,8 +5536,9 @@ RelationGetIdentityKeyBitmap(Relation relation)
* RelationGetExclusionInfo -- get info about index's exclusion constraint
*
* This should be called only for an index that is known to have an
- * associated exclusion constraint. It returns arrays (palloc'd in caller's
- * context) of the exclusion operator OIDs, their underlying functions'
+ * associated exclusion constraint or temporal primary key/unique
+ * constraint. It returns arrays (palloc'd in caller's context)
+ * of the exclusion operator OIDs, their underlying functions'
* OIDs, and their strategy numbers in the index's opclasses. We cache
* all this information since it requires a fair amount of work to get.
*/
@@ -5603,7 +5604,10 @@ RelationGetExclusionInfo(Relation indexRelation,
int nelem;
/* We want the exclusion constraint owning the index */
- if (conform->contype != CONSTRAINT_EXCLUSION ||
+ if ((conform->contype != CONSTRAINT_EXCLUSION &&
+ !(conform->contemporal && (
+ conform->contype == CONSTRAINT_PRIMARY
+ || conform->contype == CONSTRAINT_UNIQUE))) ||
conform->conindid != RelationGetRelid(indexRelation))
continue;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 65f64c282d..8fd51e9f9f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6964,7 +6964,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
i_tablespace,
i_indreloptions,
i_indstatcols,
- i_indstatvals;
+ i_indstatvals,
+ i_withoutoverlaps;
/*
* We want to perform just one query against pg_index. However, we
@@ -7039,10 +7040,17 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
if (fout->remoteVersion >= 150000)
appendPQExpBufferStr(query,
- "i.indnullsnotdistinct ");
+ "i.indnullsnotdistinct, ");
else
appendPQExpBufferStr(query,
- "false AS indnullsnotdistinct ");
+ "false AS indnullsnotdistinct, ");
+
+ if (fout->remoteVersion >= 170000)
+ appendPQExpBufferStr(query,
+ "c.conexclop IS NOT NULL AS withoutoverlaps ");
+ else
+ appendPQExpBufferStr(query,
+ "null AS withoutoverlaps ");
/*
* The point of the messy-looking outer join is to find a constraint that
@@ -7117,6 +7125,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
i_indreloptions = PQfnumber(res, "indreloptions");
i_indstatcols = PQfnumber(res, "indstatcols");
i_indstatvals = PQfnumber(res, "indstatvals");
+ i_withoutoverlaps = PQfnumber(res, "withoutoverlaps");
indxinfo = (IndxInfo *) pg_malloc(ntups * sizeof(IndxInfo));
@@ -7219,6 +7228,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
constrinfo->condeferred = *(PQgetvalue(res, j, i_condeferred)) == 't';
constrinfo->conislocal = true;
constrinfo->separate = true;
+ constrinfo->withoutoverlaps = *(PQgetvalue(res, j, i_withoutoverlaps)) == 't';
indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
}
@@ -16914,9 +16924,22 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
break;
attname = getAttrName(indkey, tbinfo);
- appendPQExpBuffer(q, "%s%s",
- (k == 0) ? "" : ", ",
- fmtId(attname));
+ if (k == 0)
+ {
+ appendPQExpBuffer(q, "%s",
+ fmtId(attname));
+ }
+ else if (k == indxinfo->indnkeyattrs - 1 &&
+ coninfo->withoutoverlaps)
+ {
+ appendPQExpBuffer(q, ", %s WITHOUT OVERLAPS",
+ fmtId(attname));
+ }
+ else
+ {
+ appendPQExpBuffer(q, ", %s",
+ fmtId(attname));
+ }
}
if (indxinfo->indnkeyattrs < indxinfo->indnattrs)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9036b13f6a..773a2de18b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -489,6 +489,7 @@ typedef struct _constraintInfo
bool condeferred; /* true if constraint is INITIALLY DEFERRED */
bool conislocal; /* true if constraint has local definition */
bool separate; /* true if must dump as separate item */
+ bool withoutoverlaps; /* true if the last elem is WITHOUT OVERLAPS */
} ConstraintInfo;
typedef struct _procLangInfo
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 0758fe5ea0..22aa87d3dc 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1015,6 +1015,52 @@ my %tests = (
},
},
+ 'ALTER TABLE ONLY test_table_tpk ADD CONSTRAINT ... PRIMARY KEY (..., ... WITHOUT OVERLAPS)' => {
+ create_sql => 'CREATE TABLE dump_test.test_table_tpk (
+ col1 int4range,
+ col2 tstzrange,
+ CONSTRAINT test_table_tpk_pkey PRIMARY KEY
+ (col1, col2 WITHOUT OVERLAPS));',
+ regexp => qr/^
+ \QALTER TABLE ONLY dump_test.test_table_tpk\E \n^\s+
+ \QADD CONSTRAINT test_table_tpk_pkey PRIMARY KEY (col1, col2 WITHOUT OVERLAPS);\E
+ /xm,
+ like => {
+ %full_runs,
+ %dump_test_schema_runs,
+ section_post_data => 1,
+ exclude_test_table => 1,
+ },
+ unlike => {
+ only_dump_test_table => 1,
+ exclude_dump_test_schema => 1,
+ only_dump_measurement => 1,
+ },
+ },
+
+ 'ALTER TABLE ONLY test_table_tuq ADD CONSTRAINT ... UNIQUE (..., ... WITHOUT OVERLAPS)' => {
+ create_sql => 'CREATE TABLE dump_test.test_table_tuq (
+ col1 int4range,
+ col2 tstzrange,
+ CONSTRAINT test_table_tuq_uq UNIQUE
+ (col1, col2 WITHOUT OVERLAPS));',
+ regexp => qr/^
+ \QALTER TABLE ONLY dump_test.test_table_tuq\E \n^\s+
+ \QADD CONSTRAINT test_table_tuq_uq UNIQUE (col1, col2 WITHOUT OVERLAPS);\E
+ /xm,
+ like => {
+ %full_runs,
+ %dump_test_schema_runs,
+ section_post_data => 1,
+ exclude_test_table => 1,
+ },
+ unlike => {
+ only_dump_test_table => 1,
+ exclude_dump_test_schema => 1,
+ only_dump_measurement => 1,
+ },
+ },
+
'ALTER TABLE (partitioned) ADD CONSTRAINT ... FOREIGN KEY' => {
create_order => 4,
create_sql => 'CREATE TABLE dump_test.test_table_fk (
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index bac94a338c..4fbfc07460 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2384,6 +2384,10 @@ describeOneTableDetails(const char *schemaname,
else
appendPQExpBufferStr(&buf, ", false AS indisreplident");
appendPQExpBufferStr(&buf, ", c2.reltablespace");
+ if (pset.sversion >= 160000)
+ appendPQExpBufferStr(&buf, ", con.contemporal");
+ else
+ appendPQExpBufferStr(&buf, ", false AS contemporal");
appendPQExpBuffer(&buf,
"\nFROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i\n"
" LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x'))\n"
@@ -2405,8 +2409,12 @@ describeOneTableDetails(const char *schemaname,
printfPQExpBuffer(&buf, " \"%s\"",
PQgetvalue(result, i, 0));
- /* If exclusion constraint, print the constraintdef */
- if (strcmp(PQgetvalue(result, i, 7), "x") == 0)
+ /*
+ * If exclusion constraint or temporal PK/UNIQUE constraint,
+ * print the constraintdef
+ */
+ if (strcmp(PQgetvalue(result, i, 7), "x") == 0 ||
+ strcmp(PQgetvalue(result, i, 12), "t") == 0)
{
appendPQExpBuffer(&buf, " %s",
PQgetvalue(result, i, 6));
diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h
index 4d8ba81f90..a604546f48 100644
--- a/src/include/catalog/index.h
+++ b/src/include/catalog/index.h
@@ -91,6 +91,7 @@ extern Oid index_create(Relation heapRelation,
#define INDEX_CONSTR_CREATE_INIT_DEFERRED (1 << 2)
#define INDEX_CONSTR_CREATE_UPDATE_INDEX (1 << 3)
#define INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS (1 << 4)
+#define INDEX_CONSTR_CREATE_TEMPORAL (1 << 5)
extern Oid index_concurrently_create_copy(Relation heapRelation,
Oid oldIndexId,
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 9b9d8faf35..786a9ca904 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -107,6 +107,12 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
/* Has a local definition and cannot be inherited */
bool connoinherit;
+ /*
+ * For primary and foreign keys, signifies the last column is a range
+ * and should use overlaps instead of equals.
+ */
+ bool contemporal;
+
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/*
@@ -146,7 +152,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
/*
* If an exclusion constraint, the OIDs of the exclusion operators for
- * each column of the constraint
+ * each column of the constraint. Also set for temporal primary keys.
*/
Oid conexclop[1] BKI_LOOKUP(pg_operator);
@@ -236,6 +242,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
bool conIsLocal,
int conInhCount,
bool conNoInherit,
+ bool conTemporal,
bool is_internal);
extern bool ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId,
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index 179eb9901f..f10886f6ca 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -44,7 +44,8 @@ extern char *ChooseRelationName(const char *name1, const char *name2,
extern bool CheckIndexCompatible(Oid oldId,
const char *accessMethodName,
const List *attributeList,
- const List *exclusionOpNames);
+ const List *exclusionOpNames,
+ bool istemporal);
extern Oid GetDefaultOpClass(Oid type_id, Oid am_id);
extern Oid ResolveOpClass(const List *opclass, Oid attrType,
const char *accessMethodName, Oid accessMethodId);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cb714f4a19..2327b55f15 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -155,6 +155,7 @@ typedef struct ExprState
* UniqueProcs
* UniqueStrats
* Unique is it a unique index?
+ * Temporal is it for a temporal constraint?
* OpclassOptions opclass-specific options, or NULL if none
* ReadyForInserts is it valid for inserts?
* CheckedUnchanged IndexUnchanged status determined yet?
@@ -190,6 +191,7 @@ typedef struct IndexInfo
Datum *ii_OpclassOptions; /* array with one entry per column */
bool ii_Unique;
bool ii_NullsNotDistinct;
+ bool ii_Temporal;
bool ii_ReadyForInserts;
bool ii_CheckedUnchanged;
bool ii_IndexUnchanged;
diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h
index 3180703005..4363467147 100644
--- a/src/include/nodes/makefuncs.h
+++ b/src/include/nodes/makefuncs.h
@@ -98,7 +98,7 @@ extern IndexInfo *makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid,
List *expressions, List *predicates,
bool unique, bool nulls_not_distinct,
bool isready, bool concurrent,
- bool summarizing);
+ bool summarizing, bool temporal);
extern DefElem *makeDefElem(char *name, Node *arg, int location);
extern DefElem *makeDefElemExtended(char *nameSpace, char *name, Node *arg,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index fef4c714b8..0747b54b26 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2641,6 +2641,9 @@ typedef struct Constraint
Oid old_pktable_oid; /* pg_constraint.confrelid of my former
* self */
+ /* Fields used for temporal PRIMARY KEY and FOREIGN KEY constraints: */
+ Node *without_overlaps; /* String node naming range column */
+
/* Fields used for constraints that allow a NOT VALID specification */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
@@ -3242,6 +3245,7 @@ typedef struct IndexStmt
bool nulls_not_distinct; /* null treatment for UNIQUE constraints */
bool primary; /* is index a primary key? */
bool isconstraint; /* is it for a pkey/unique constraint? */
+ bool istemporal; /* is it for a temporal pkey? */
bool deferrable; /* is the constraint DEFERRABLE? */
bool initdeferred; /* is the constraint INITIALLY DEFERRED? */
bool transformed; /* true when transformIndexStmt is finished */
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
new file mode 100644
index 0000000000..d7ee0c4ff9
--- /dev/null
+++ b/src/test/regress/expected/without_overlaps.out
@@ -0,0 +1,359 @@
+-- Tests for WITHOUT OVERLAPS.
+--
+-- We leave behind several tables to test pg_dump etc:
+-- temporal_rng, temporal_rng2,
+-- temporal_fk_rng2rng.
+--
+-- test input parser
+--
+-- PK with no columns just WITHOUT OVERLAPS:
+CREATE TABLE temporal_rng (
+ valid_at tsrange,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS)
+);
+ERROR: syntax error at or near "WITHOUT"
+LINE 3: CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OV...
+ ^
+-- PK with a range column/PERIOD that isn't there:
+CREATE TABLE temporal_rng (
+ id INTEGER,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR: range or PERIOD "valid_at" named in WITHOUT OVERLAPS does not exist
+-- PK with a non-range column:
+CREATE TABLE temporal_rng (
+ id INTEGER,
+ valid_at TEXT,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR: column "valid_at" named in WITHOUT OVERLAPS is not a range type
+-- PK with one column plus a range:
+CREATE TABLE temporal_rng (
+ -- Since we can't depend on having btree_gist here,
+ -- use an int4range instead of an int.
+ -- (The rangetypes regression test uses the same trick.)
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng
+ Table "public.temporal_rng"
+ Column | Type | Collation | Nullable | Default
+----------+-----------+-----------+----------+---------
+ id | int4range | | not null |
+ valid_at | tsrange | | not null |
+Indexes:
+ "temporal_rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+ pg_get_constraintdef
+---------------------------------------------
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+ pg_get_indexdef
+-------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng_pk ON temporal_rng USING gist (id, valid_at)
+(1 row)
+
+-- PK with two columns plus a range:
+CREATE TABLE temporal_rng2 (
+ id1 int4range,
+ id2 int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+ Table "public.temporal_rng2"
+ Column | Type | Collation | Nullable | Default
+----------+-----------+-----------+----------+---------
+ id1 | int4range | | not null |
+ id2 | int4range | | not null |
+ valid_at | tsrange | | not null |
+Indexes:
+ "temporal_rng2_pk" PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+ pg_get_constraintdef
+---------------------------------------------------
+ PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+ pg_get_indexdef
+---------------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng2_pk ON temporal_rng2 USING gist (id1, id2, valid_at)
+(1 row)
+
+DROP TABLE temporal_rng2;
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng2 (
+ id int4range,
+ valid_at textrange2,
+ CONSTRAINT temporal_rng2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng2 DROP CONSTRAINT temporal_rng2_pk;
+DROP TABLE temporal_rng2;
+DROP TYPE textrange2;
+-- UNIQUE with no columns just WITHOUT OVERLAPS:
+CREATE TABLE temporal_rng2 (
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+);
+ERROR: syntax error at or near "WITHOUT"
+LINE 3: CONSTRAINT temporal_rng2_uq UNIQUE (valid_at WITHOUT OVERLA...
+ ^
+-- UNIQUE with a range column/PERIOD that isn't there:
+CREATE TABLE temporal_rng2 (
+ id INTEGER,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR: range or PERIOD "valid_at" named in WITHOUT OVERLAPS does not exist
+-- UNIQUE with a non-range column:
+CREATE TABLE temporal_rng2 (
+ id INTEGER,
+ valid_at TEXT,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR: column "valid_at" named in WITHOUT OVERLAPS is not a range type
+-- UNIQUE with one column plus a range:
+CREATE TABLE temporal_rng2 (
+ -- Since we can't depend on having btree_gist here,
+ -- use an int4range instead of an int.
+ -- (The rangetypes regression test uses the same trick.)
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+ Table "public.temporal_rng2"
+ Column | Type | Collation | Nullable | Default
+----------+-----------+-----------+----------+---------
+ id | int4range | | |
+ valid_at | tsrange | | |
+Indexes:
+ "temporal_rng2_uq" UNIQUE (id, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+ pg_get_constraintdef
+----------------------------------------
+ UNIQUE (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+ pg_get_indexdef
+---------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng2_uq ON temporal_rng2 USING gist (id, valid_at)
+(1 row)
+
+-- UNIQUE with two columns plus a range:
+CREATE TABLE temporal_rng3 (
+ id1 int4range,
+ id2 int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng3_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng3
+ Table "public.temporal_rng3"
+ Column | Type | Collation | Nullable | Default
+----------+-----------+-----------+----------+---------
+ id1 | int4range | | |
+ id2 | int4range | | |
+ valid_at | tsrange | | |
+Indexes:
+ "temporal_rng3_uq" UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+ pg_get_constraintdef
+----------------------------------------------
+ UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+ pg_get_indexdef
+---------------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng3_uq ON temporal_rng3 USING gist (id1, id2, valid_at)
+(1 row)
+
+DROP TABLE temporal_rng3;
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+DROP TABLE temporal_rng;
+CREATE TABLE temporal_rng (
+ id int4range,
+ valid_at tsrange
+);
+ALTER TABLE temporal_rng
+ ADD CONSTRAINT temporal_rng_pk
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+-- PK with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+ ADD CONSTRAINT temporal3_pk
+ PRIMARY KEY USING INDEX idx_temporal3_uq;
+ERROR: "idx_temporal3_uq" is not a unique index
+LINE 2: ADD CONSTRAINT temporal3_pk
+ ^
+DETAIL: Cannot create a primary key or unique constraint using such an index.
+DROP TABLE temporal3;
+-- UNIQUE with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+ ADD CONSTRAINT temporal3_uq
+ UNIQUE USING INDEX idx_temporal3_uq;
+ERROR: "idx_temporal3_uq" is not a unique index
+LINE 2: ADD CONSTRAINT temporal3_uq
+ ^
+DETAIL: Cannot create a primary key or unique constraint using such an index.
+DROP TABLE temporal3;
+-- Add range column and the PK at the same time
+CREATE TABLE temporal3 (
+ id int4range
+);
+ALTER TABLE temporal3
+ ADD COLUMN valid_at tsrange,
+ ADD CONSTRAINT temporal3_pk
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+-- Add range column and UNIQUE constraint at the same time
+CREATE TABLE temporal3 (
+ id int4range
+);
+ALTER TABLE temporal3
+ ADD COLUMN valid_at tsrange,
+ ADD CONSTRAINT temporal3_uq
+ UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+--
+-- test PK inserts
+--
+-- okay:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng VALUES ('[2,2]', tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng VALUES ('[3,3]', tsrange('2018-01-01', NULL));
+-- should fail:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-01', '2018-01-05'));
+ERROR: conflicting key value violates exclusion constraint "temporal_rng_pk"
+DETAIL: Key (id, valid_at)=([1,2), ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018")) conflicts with existing key (id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sat Feb 03 00:00:00 2018")).
+INSERT INTO temporal_rng VALUES (NULL, tsrange('2018-01-01', '2018-01-05'));
+ERROR: null value in column "id" of relation "temporal_rng" violates not-null constraint
+DETAIL: Failing row contains (null, ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018")).
+INSERT INTO temporal_rng VALUES ('[3,3]', NULL);
+ERROR: null value in column "valid_at" of relation "temporal_rng" violates not-null constraint
+DETAIL: Failing row contains ([3,4), null).
+--
+-- test a range with both a PK and a UNIQUE constraint
+--
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at daterange,
+ id2 int8range,
+ name TEXT,
+ CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal3_uniq UNIQUE (id2, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO temporal3 (id, valid_at, id2, name)
+ VALUES
+ ('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'),
+ ('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', 'bar')
+;
+DROP TABLE temporal3;
+--
+-- test changing the PK's dependencies
+--
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal3 ALTER COLUMN valid_at DROP NOT NULL;
+ERROR: column "valid_at" is in a primary key
+ALTER TABLE temporal3 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at));
+ALTER TABLE temporal3 RENAME COLUMN valid_at TO valid_thru;
+ALTER TABLE temporal3 DROP COLUMN valid_thru;
+DROP TABLE temporal3;
+--
+-- test PARTITION BY for ranges
+--
+-- temporal PRIMARY KEY:
+CREATE TABLE temporal_partitioned (
+ id int4range,
+ valid_at daterange,
+ name text,
+ CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE tp1 partition OF temporal_partitioned FOR VALUES IN ('[1,1]', '[2,2]');
+CREATE TABLE tp2 partition OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]');
+INSERT INTO temporal_partitioned VALUES
+ ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
+ ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
+ ('[3,3]', daterange('2000-01-01', '2010-01-01'), 'three');
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+ id | valid_at | name
+-------+-------------------------+-------
+ [1,2) | [01-01-2000,02-01-2000) | one
+ [1,2) | [02-01-2000,03-01-2000) | one
+ [3,4) | [01-01-2000,01-01-2010) | three
+(3 rows)
+
+SELECT * FROM tp1 ORDER BY id, valid_at;
+ id | valid_at | name
+-------+-------------------------+------
+ [1,2) | [01-01-2000,02-01-2000) | one
+ [1,2) | [02-01-2000,03-01-2000) | one
+(2 rows)
+
+SELECT * FROM tp2 ORDER BY id, valid_at;
+ id | valid_at | name
+-------+-------------------------+-------
+ [3,4) | [01-01-2000,01-01-2010) | three
+(1 row)
+
+DROP TABLE temporal_partitioned;
+-- temporal UNIQUE:
+CREATE TABLE temporal_partitioned (
+ id int4range,
+ valid_at daterange,
+ name text,
+ CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE tp1 partition OF temporal_partitioned FOR VALUES IN ('[1,1]', '[2,2]');
+CREATE TABLE tp2 partition OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]');
+INSERT INTO temporal_partitioned VALUES
+ ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
+ ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
+ ('[3,3]', daterange('2000-01-01', '2010-01-01'), 'three');
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+ id | valid_at | name
+-------+-------------------------+-------
+ [1,2) | [01-01-2000,02-01-2000) | one
+ [1,2) | [02-01-2000,03-01-2000) | one
+ [3,4) | [01-01-2000,01-01-2010) | three
+(3 rows)
+
+SELECT * FROM tp1 ORDER BY id, valid_at;
+ id | valid_at | name
+-------+-------------------------+------
+ [1,2) | [01-01-2000,02-01-2000) | one
+ [1,2) | [02-01-2000,03-01-2000) | one
+(2 rows)
+
+SELECT * FROM tp2 ORDER BY id, valid_at;
+ id | valid_at | name
+-------+-------------------------+-------
+ [3,4) | [01-01-2000,01-01-2010) | three
+(1 row)
+
+DROP TABLE temporal_partitioned;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 4df9d8503b..66b75fa12f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
# geometry depends on point, lseg, line, box, path, polygon, circle
# horology depends on date, time, timetz, timestamp, timestamptz, interval
# ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc without_overlaps
# ----------
# Load huge amounts of data
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
new file mode 100644
index 0000000000..3c3236618c
--- /dev/null
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -0,0 +1,262 @@
+-- Tests for WITHOUT OVERLAPS.
+--
+-- We leave behind several tables to test pg_dump etc:
+-- temporal_rng, temporal_rng2,
+-- temporal_fk_rng2rng.
+
+--
+-- test input parser
+--
+
+-- PK with no columns just WITHOUT OVERLAPS:
+
+CREATE TABLE temporal_rng (
+ valid_at tsrange,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with a range column/PERIOD that isn't there:
+
+CREATE TABLE temporal_rng (
+ id INTEGER,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with a non-range column:
+
+CREATE TABLE temporal_rng (
+ id INTEGER,
+ valid_at TEXT,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with one column plus a range:
+
+CREATE TABLE temporal_rng (
+ -- Since we can't depend on having btree_gist here,
+ -- use an int4range instead of an int.
+ -- (The rangetypes regression test uses the same trick.)
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+
+-- PK with two columns plus a range:
+CREATE TABLE temporal_rng2 (
+ id1 int4range,
+ id2 int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+DROP TABLE temporal_rng2;
+
+
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng2 (
+ id int4range,
+ valid_at textrange2,
+ CONSTRAINT temporal_rng2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng2 DROP CONSTRAINT temporal_rng2_pk;
+DROP TABLE temporal_rng2;
+DROP TYPE textrange2;
+
+-- UNIQUE with no columns just WITHOUT OVERLAPS:
+
+CREATE TABLE temporal_rng2 (
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with a range column/PERIOD that isn't there:
+
+CREATE TABLE temporal_rng2 (
+ id INTEGER,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with a non-range column:
+
+CREATE TABLE temporal_rng2 (
+ id INTEGER,
+ valid_at TEXT,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with one column plus a range:
+
+CREATE TABLE temporal_rng2 (
+ -- Since we can't depend on having btree_gist here,
+ -- use an int4range instead of an int.
+ -- (The rangetypes regression test uses the same trick.)
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+
+-- UNIQUE with two columns plus a range:
+CREATE TABLE temporal_rng3 (
+ id1 int4range,
+ id2 int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal_rng3_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng3
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+DROP TABLE temporal_rng3;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+DROP TABLE temporal_rng;
+CREATE TABLE temporal_rng (
+ id int4range,
+ valid_at tsrange
+);
+ALTER TABLE temporal_rng
+ ADD CONSTRAINT temporal_rng_pk
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+-- PK with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+ ADD CONSTRAINT temporal3_pk
+ PRIMARY KEY USING INDEX idx_temporal3_uq;
+DROP TABLE temporal3;
+
+-- UNIQUE with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+ ADD CONSTRAINT temporal3_uq
+ UNIQUE USING INDEX idx_temporal3_uq;
+DROP TABLE temporal3;
+
+-- Add range column and the PK at the same time
+CREATE TABLE temporal3 (
+ id int4range
+);
+ALTER TABLE temporal3
+ ADD COLUMN valid_at tsrange,
+ ADD CONSTRAINT temporal3_pk
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+
+-- Add range column and UNIQUE constraint at the same time
+CREATE TABLE temporal3 (
+ id int4range
+);
+ALTER TABLE temporal3
+ ADD COLUMN valid_at tsrange,
+ ADD CONSTRAINT temporal3_uq
+ UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+
+--
+-- test PK inserts
+--
+
+-- okay:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng VALUES ('[2,2]', tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng VALUES ('[3,3]', tsrange('2018-01-01', NULL));
+
+-- should fail:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng VALUES (NULL, tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng VALUES ('[3,3]', NULL);
+
+--
+-- test a range with both a PK and a UNIQUE constraint
+--
+
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at daterange,
+ id2 int8range,
+ name TEXT,
+ CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal3_uniq UNIQUE (id2, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO temporal3 (id, valid_at, id2, name)
+ VALUES
+ ('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'),
+ ('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', 'bar')
+;
+DROP TABLE temporal3;
+
+--
+-- test changing the PK's dependencies
+--
+
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+ALTER TABLE temporal3 ALTER COLUMN valid_at DROP NOT NULL;
+ALTER TABLE temporal3 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at));
+ALTER TABLE temporal3 RENAME COLUMN valid_at TO valid_thru;
+ALTER TABLE temporal3 DROP COLUMN valid_thru;
+DROP TABLE temporal3;
+
+--
+-- test PARTITION BY for ranges
+--
+
+-- temporal PRIMARY KEY:
+CREATE TABLE temporal_partitioned (
+ id int4range,
+ valid_at daterange,
+ name text,
+ CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE tp1 partition OF temporal_partitioned FOR VALUES IN ('[1,1]', '[2,2]');
+CREATE TABLE tp2 partition OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]');
+INSERT INTO temporal_partitioned VALUES
+ ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
+ ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
+ ('[3,3]', daterange('2000-01-01', '2010-01-01'), 'three');
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+SELECT * FROM tp1 ORDER BY id, valid_at;
+SELECT * FROM tp2 ORDER BY id, valid_at;
+DROP TABLE temporal_partitioned;
+
+-- temporal UNIQUE:
+CREATE TABLE temporal_partitioned (
+ id int4range,
+ valid_at daterange,
+ name text,
+ CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE tp1 partition OF temporal_partitioned FOR VALUES IN ('[1,1]', '[2,2]');
+CREATE TABLE tp2 partition OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]');
+INSERT INTO temporal_partitioned VALUES
+ ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
+ ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
+ ('[3,3]', daterange('2000-01-01', '2010-01-01'), 'three');
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+SELECT * FROM tp1 ORDER BY id, valid_at;
+SELECT * FROM tp2 ORDER BY id, valid_at;
+DROP TABLE temporal_partitioned;
--
2.25.1
[text/x-patch] v13-0002-Add-temporal-FOREIGN-KEYs.patch (115.1K, ../../[email protected]/3-v13-0002-Add-temporal-FOREIGN-KEYs.patch)
download | inline diff:
From 27a33d1d40e41c2aabc2e6ebe82625cec68034f9 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Sun, 27 Jun 2021 12:58:51 -0700
Subject: [PATCH v13 2/5] Add temporal FOREIGN KEYs
- Added bison support for temporal FOREIGN KEYs. They should have a
range column for the temporal component.
- Added temporal trigger functions akin to existing RI trigger
functions.
- Don't yet support ON {UPDATE,DELETE} {CASCADE,SET NULL,SET DEFAULT}.
- Added pg_dump support.
- Show the correct syntax in psql \d output for foreign keys.
- Added tests and documentation.
---
doc/src/sgml/ref/create_table.sgml | 42 +-
src/backend/commands/tablecmds.c | 752 +++++++++++-------
src/backend/parser/gram.y | 37 +-
src/backend/utils/adt/ri_triggers.c | 309 ++++++-
src/backend/utils/adt/ruleutils.c | 19 +-
src/include/catalog/pg_constraint.h | 8 +-
src/include/catalog/pg_proc.dat | 24 +
src/include/nodes/parsenodes.h | 2 +
src/include/parser/kwlist.h | 1 +
.../regress/expected/without_overlaps.out | 427 ++++++++++
src/test/regress/sql/without_overlaps.sql | 431 ++++++++++
11 files changed, 1711 insertions(+), 341 deletions(-)
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 87f0aab13b..fea8fc3c7a 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -81,7 +81,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">temporal_interval</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">temporal_interval</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
- FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
+ FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">temporal_interval</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">temporal_interval</replaceable> ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
@@ -1149,8 +1149,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<varlistentry id="sql-createtable-parms-references">
<term><literal>REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH <replaceable class="parameter">matchtype</replaceable> ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ]</literal> (column constraint)</term>
- <term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] )
- REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
+ <term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">temporal_interval</replaceable> ] )
+ REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">temporal_interval</replaceable> ] ) ]
[ MATCH <replaceable class="parameter">matchtype</replaceable> ]
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ]
[ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ]</literal>
@@ -1161,11 +1161,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
These clauses specify a foreign key constraint, which requires
that a group of one or more columns of the new table must only
contain values that match values in the referenced
- column(s) of some row of the referenced table. If the <replaceable
+ column(s) of some row of the referenced table.
+ If the <replaceable
class="parameter">refcolumn</replaceable> list is omitted, the
primary key of the <replaceable class="parameter">reftable</replaceable>
is used. The referenced columns must be the columns of a non-deferrable
- unique or primary key constraint in the referenced table. The user
+ unique or primary key constraint in the referenced table.
+ </para>
+
+ <para>
+ If the last column is marked with <literal>PERIOD</literal>,
+ it must be a period or range column, and the referenced table
+ must have a temporal primary key.
+ The non-<literal>PERIOD</literal> columns are treated normally
+ (and there must be at least one of them),
+ but the <literal>PERIOD</literal> column is not compared for equality.
+ Instead the constraint is considered satisfied
+ if the referenced table has matching records whose combined ranges completely cover
+ the referencing record.
+ In other words, the reference must have a referent for its entire duration.
+ </para>
+
+ <para>
+ The user
must have <literal>REFERENCES</literal> permission on the referenced table
(either the whole table, or the specific referenced columns). The
addition of a foreign key constraint requires a
@@ -1237,7 +1255,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<para>
Delete any rows referencing the deleted row, or update the
values of the referencing column(s) to the new values of the
- referenced columns, respectively.
+ referenced columns, respectively. In a temporal foreign key,
+ the delete will use <literal>FOR PORTION OF</literal> semantics
+ to constrain the effect to the bounds of the referenced row
+ being deleted.
</para>
</listitem>
</varlistentry>
@@ -1250,6 +1271,12 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
referencing columns, to null. A subset of columns can only be
specified for <literal>ON DELETE</literal> actions.
</para>
+
+ <para>
+ In a temporal foreign key, the change will use <literal>FOR
+ PORTION OF</literal> semantics to constrain the effect to the bounds
+ of the referenced row.
+ </para>
</listitem>
</varlistentry>
@@ -1262,6 +1289,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
can only be specified for <literal>ON DELETE</literal> actions.
(There must be a row in the referenced table matching the default
values, if they are not null, or the operation will fail.)
+ In a temporal foreign key, the change will use <literal>FOR PORTION
+ OF</literal> semantics to constrain the effect to the bounds of the
+ referenced row.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 768eaa1deb..5f4a8aa809 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -381,16 +381,18 @@ static int transformColumnNameList(Oid relId, List *colList,
static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
List **attnamelist,
int16 *attnums, Oid *atttypids,
+ int16 *periodattnums, Oid *periodatttypids,
Oid *opclasses);
static Oid transformFkeyCheckAttrs(Relation pkrel,
int numattrs, int16 *attnums,
+ bool is_temporal, int16 periodattnum,
Oid *opclasses);
static void checkFkeyPermissions(Relation rel, int16 *attnums, int natts);
static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId,
Oid *funcid);
static void validateForeignKeyConstraint(char *conname,
Relation rel, Relation pkrel,
- Oid pkindOid, Oid constraintOid);
+ Oid pkindOid, Oid constraintOid, bool temporal);
static void ATController(AlterTableStmt *parsetree,
Relation rel, List *cmds, bool recurse, LOCKMODE lockmode,
AlterTableUtilityContext *context);
@@ -500,7 +502,8 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger);
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ bool is_temporal);
static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
int numfksetcols, const int16 *fksetcolsattnums,
List *fksetcols);
@@ -510,7 +513,9 @@ static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
bool old_check_ok, LOCKMODE lockmode,
- Oid parentInsTrigger, Oid parentUpdTrigger);
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ bool is_temporal);
+
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -542,6 +547,12 @@ static void GetForeignKeyCheckTriggers(Relation trigrel,
Oid conoid, Oid confrelid, Oid conrelid,
Oid *insertTriggerOid,
Oid *updateTriggerOid);
+static void FindFKComparisonOperators(Constraint *fkconstraint,
+ AlteredTableInfo *tab, int i, int16 *fkattnum,
+ bool *old_check_ok, ListCell **old_pfeqop_item,
+ Oid pktype, Oid fktype, Oid opclass,
+ bool is_temporal, bool for_overlaps,
+ Oid *pfeqopOut, Oid *ppeqopOut, Oid *ffeqopOut);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -5823,7 +5834,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
validateForeignKeyConstraint(fkconstraint->conname, rel, refrel,
con->refindid,
- con->conid);
+ con->conid,
+ con->contemporal);
/*
* No need to mark the constraint row as validated, we did
@@ -9433,6 +9445,11 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
Oid ppeqoperators[INDEX_MAX_KEYS] = {0};
Oid ffeqoperators[INDEX_MAX_KEYS] = {0};
int16 fkdelsetcols[INDEX_MAX_KEYS] = {0};
+ bool is_temporal = (fkconstraint->fk_period != NULL);
+ int16 pkperiodattnums[1] = {0};
+ int16 fkperiodattnums[1] = {0};
+ Oid pkperiodtypoids[1] = {0};
+ Oid fkperiodtypoids[1] = {0};
int i;
int numfks,
numpks,
@@ -9527,6 +9544,10 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
numfks = transformColumnNameList(RelationGetRelid(rel),
fkconstraint->fk_attrs,
fkattnum, fktypoid);
+ if (is_temporal)
+ transformColumnNameList(RelationGetRelid(rel),
+ list_make1(fkconstraint->fk_period),
+ fkperiodattnums, fkperiodtypoids);
numfkdelsetcols = transformColumnNameList(RelationGetRelid(rel),
fkconstraint->fk_del_set_cols,
@@ -9546,6 +9567,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
numpks = transformFkeyGetPrimaryKey(pkrel, &indexOid,
&fkconstraint->pk_attrs,
pkattnum, pktypoid,
+ pkperiodattnums, pkperiodtypoids,
opclasses);
}
else
@@ -9553,8 +9575,14 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
numpks = transformColumnNameList(RelationGetRelid(pkrel),
fkconstraint->pk_attrs,
pkattnum, pktypoid);
+ if (is_temporal)
+ transformColumnNameList(RelationGetRelid(pkrel),
+ list_make1(fkconstraint->pk_period),
+ pkperiodattnums, pkperiodtypoids);
+
/* Look for an index matching the column list */
indexOid = transformFkeyCheckAttrs(pkrel, numpks, pkattnum,
+ is_temporal, pkperiodattnums[0],
opclasses);
}
@@ -9613,187 +9641,27 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
for (i = 0; i < numpks; i++)
{
- Oid pktype = pktypoid[i];
- Oid fktype = fktypoid[i];
- Oid fktyped;
- HeapTuple cla_ht;
- Form_pg_opclass cla_tup;
- Oid amid;
- Oid opfamily;
- Oid opcintype;
- Oid pfeqop;
- Oid ppeqop;
- Oid ffeqop;
- int16 eqstrategy;
- Oid pfeqop_right;
-
- /* We need several fields out of the pg_opclass entry */
- cla_ht = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclasses[i]));
- if (!HeapTupleIsValid(cla_ht))
- elog(ERROR, "cache lookup failed for opclass %u", opclasses[i]);
- cla_tup = (Form_pg_opclass) GETSTRUCT(cla_ht);
- amid = cla_tup->opcmethod;
- opfamily = cla_tup->opcfamily;
- opcintype = cla_tup->opcintype;
- ReleaseSysCache(cla_ht);
-
- /*
- * Check it's a btree; currently this can never fail since no other
- * index AMs support unique indexes. If we ever did have other types
- * of unique indexes, we'd need a way to determine which operator
- * strategy number is equality. (Is it reasonable to insist that
- * every such index AM use btree's number for equality?)
- */
- if (amid != BTREE_AM_OID)
- elog(ERROR, "only b-tree indexes are supported for foreign keys");
- eqstrategy = BTEqualStrategyNumber;
-
- /*
- * There had better be a primary equality operator for the index.
- * We'll use it for PK = PK comparisons.
- */
- ppeqop = get_opfamily_member(opfamily, opcintype, opcintype,
- eqstrategy);
-
- if (!OidIsValid(ppeqop))
- elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
- eqstrategy, opcintype, opcintype, opfamily);
-
- /*
- * Are there equality operators that take exactly the FK type? Assume
- * we should look through any domain here.
- */
- fktyped = getBaseType(fktype);
-
- pfeqop = get_opfamily_member(opfamily, opcintype, fktyped,
- eqstrategy);
- if (OidIsValid(pfeqop))
- {
- pfeqop_right = fktyped;
- ffeqop = get_opfamily_member(opfamily, fktyped, fktyped,
- eqstrategy);
- }
- else
- {
- /* keep compiler quiet */
- pfeqop_right = InvalidOid;
- ffeqop = InvalidOid;
- }
-
- if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
- {
- /*
- * Otherwise, look for an implicit cast from the FK type to the
- * opcintype, and if found, use the primary equality operator.
- * This is a bit tricky because opcintype might be a polymorphic
- * type such as ANYARRAY or ANYENUM; so what we have to test is
- * whether the two actual column types can be concurrently cast to
- * that type. (Otherwise, we'd fail to reject combinations such
- * as int[] and point[].)
- */
- Oid input_typeids[2];
- Oid target_typeids[2];
-
- input_typeids[0] = pktype;
- input_typeids[1] = fktype;
- target_typeids[0] = opcintype;
- target_typeids[1] = opcintype;
- if (can_coerce_type(2, input_typeids, target_typeids,
- COERCION_IMPLICIT))
- {
- pfeqop = ffeqop = ppeqop;
- pfeqop_right = opcintype;
- }
- }
-
- if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("foreign key constraint \"%s\" cannot be implemented",
- fkconstraint->conname),
- errdetail("Key columns \"%s\" and \"%s\" "
- "are of incompatible types: %s and %s.",
- strVal(list_nth(fkconstraint->fk_attrs, i)),
- strVal(list_nth(fkconstraint->pk_attrs, i)),
- format_type_be(fktype),
- format_type_be(pktype))));
-
- if (old_check_ok)
- {
- /*
- * When a pfeqop changes, revalidate the constraint. We could
- * permit intra-opfamily changes, but that adds subtle complexity
- * without any concrete benefit for core types. We need not
- * assess ppeqop or ffeqop, which RI_Initial_Check() does not use.
- */
- old_check_ok = (pfeqop == lfirst_oid(old_pfeqop_item));
- old_pfeqop_item = lnext(fkconstraint->old_conpfeqop,
- old_pfeqop_item);
- }
- if (old_check_ok)
- {
- Oid old_fktype;
- Oid new_fktype;
- CoercionPathType old_pathtype;
- CoercionPathType new_pathtype;
- Oid old_castfunc;
- Oid new_castfunc;
- Form_pg_attribute attr = TupleDescAttr(tab->oldDesc,
- fkattnum[i] - 1);
-
- /*
- * Identify coercion pathways from each of the old and new FK-side
- * column types to the right (foreign) operand type of the pfeqop.
- * We may assume that pg_constraint.conkey is not changing.
- */
- old_fktype = attr->atttypid;
- new_fktype = fktype;
- old_pathtype = findFkeyCast(pfeqop_right, old_fktype,
- &old_castfunc);
- new_pathtype = findFkeyCast(pfeqop_right, new_fktype,
- &new_castfunc);
-
- /*
- * Upon a change to the cast from the FK column to its pfeqop
- * operand, revalidate the constraint. For this evaluation, a
- * binary coercion cast is equivalent to no cast at all. While
- * type implementors should design implicit casts with an eye
- * toward consistency of operations like equality, we cannot
- * assume here that they have done so.
- *
- * A function with a polymorphic argument could change behavior
- * arbitrarily in response to get_fn_expr_argtype(). Therefore,
- * when the cast destination is polymorphic, we only avoid
- * revalidation if the input type has not changed at all. Given
- * just the core data types and operator classes, this requirement
- * prevents no would-be optimizations.
- *
- * If the cast converts from a base type to a domain thereon, then
- * that domain type must be the opcintype of the unique index.
- * Necessarily, the primary key column must then be of the domain
- * type. Since the constraint was previously valid, all values on
- * the foreign side necessarily exist on the primary side and in
- * turn conform to the domain. Consequently, we need not treat
- * domains specially here.
- *
- * Since we require that all collations share the same notion of
- * equality (which they do, because texteq reduces to bitwise
- * equality), we don't compare collation here.
- *
- * We need not directly consider the PK type. It's necessarily
- * binary coercible to the opcintype of the unique index column,
- * and ri_triggers.c will only deal with PK datums in terms of
- * that opcintype. Changing the opcintype also changes pfeqop.
- */
- old_check_ok = (new_pathtype == old_pathtype &&
- new_castfunc == old_castfunc &&
- (!IsPolymorphicType(pfeqop_right) ||
- new_fktype == old_fktype));
- }
+ FindFKComparisonOperators(
+ fkconstraint, tab, i, fkattnum,
+ &old_check_ok, &old_pfeqop_item,
+ pktypoid[i], fktypoid[i], opclasses[i],
+ is_temporal, false,
+ &pfeqoperators[i], &ppeqoperators[i], &ffeqoperators[i]);
+ }
+ if (is_temporal) {
+ pkattnum[numpks] = pkperiodattnums[0];
+ pktypoid[numpks] = pkperiodtypoids[0];
+ fkattnum[numpks] = fkperiodattnums[0];
+ fktypoid[numpks] = fkperiodtypoids[0];
- pfeqoperators[i] = pfeqop;
- ppeqoperators[i] = ppeqop;
- ffeqoperators[i] = ffeqop;
+ FindFKComparisonOperators(
+ fkconstraint, tab, numpks, fkattnum,
+ &old_check_ok, &old_pfeqop_item,
+ pkperiodtypoids[0], fkperiodtypoids[0], opclasses[numpks],
+ is_temporal, true,
+ &pfeqoperators[numpks], &ppeqoperators[numpks], &ffeqoperators[numpks]);
+ numfks += 1;
+ numpks += 1;
}
/*
@@ -9812,7 +9680,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
numfkdelsetcols,
fkdelsetcols,
old_check_ok,
- InvalidOid, InvalidOid);
+ InvalidOid, InvalidOid,
+ is_temporal);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -9828,7 +9697,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
fkdelsetcols,
old_check_ok,
lockmode,
- InvalidOid, InvalidOid);
+ InvalidOid, InvalidOid,
+ is_temporal);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -9913,7 +9783,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
bool old_check_ok,
- Oid parentDelTrigger, Oid parentUpdTrigger)
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ bool is_temporal)
{
ObjectAddress address;
Oid constrOid;
@@ -9999,7 +9870,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
conislocal, /* islocal */
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
- false, /* conTemporal */
+ is_temporal, /* conTemporal */
false); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10075,7 +9946,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols,
old_check_ok,
- deleteTriggerOid, updateTriggerOid);
+ deleteTriggerOid, updateTriggerOid,
+ is_temporal);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -10133,7 +10005,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
bool old_check_ok, LOCKMODE lockmode,
- Oid parentInsTrigger, Oid parentUpdTrigger)
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ bool is_temporal)
{
Oid insertTriggerOid,
updateTriggerOid;
@@ -10181,6 +10054,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
newcon->refrelid = RelationGetRelid(pkrel);
newcon->refindid = indexOid;
newcon->conid = parentConstr;
+ newcon->contemporal = fkconstraint->fk_period != NULL;
newcon->qual = (Node *) fkconstraint;
tab->constraints = lappend(tab->constraints, newcon);
@@ -10298,7 +10172,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
false,
1,
false,
- false, /* conTemporal */
+ is_temporal, /* conTemporal */
false);
/*
@@ -10329,7 +10203,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
old_check_ok,
lockmode,
insertTriggerOid,
- updateTriggerOid);
+ updateTriggerOid,
+ is_temporal);
table_close(partition, NoLock);
}
@@ -10565,7 +10440,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
confdelsetcols,
true,
deleteTriggerOid,
- updateTriggerOid);
+ updateTriggerOid,
+ constrForm->contemporal);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
@@ -10804,7 +10680,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
false, /* islocal */
1, /* inhcount */
false, /* conNoInherit */
- false, /* conTemporal */
+ constrForm->contemporal, /* conTemporal */
true);
/* Set up partition dependencies for the new constraint */
@@ -10838,13 +10714,232 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
false, /* no old check exists */
AccessExclusiveLock,
insertTriggerOid,
- updateTriggerOid);
+ updateTriggerOid,
+ constrForm->contemporal);
table_close(pkrel, NoLock);
}
table_close(trigrel, RowExclusiveLock);
}
+static void
+FindFKComparisonOperators(Constraint *fkconstraint,
+ AlteredTableInfo *tab,
+ int i,
+ int16 *fkattnum,
+ bool *old_check_ok,
+ ListCell **old_pfeqop_item,
+ Oid pktype, Oid fktype, Oid opclass,
+ bool is_temporal, bool for_overlaps,
+ Oid *pfeqopOut, Oid *ppeqopOut, Oid *ffeqopOut)
+{
+ Oid fktyped;
+ HeapTuple cla_ht;
+ Form_pg_opclass cla_tup;
+ Oid amid;
+ Oid opfamily;
+ Oid opcintype;
+ Oid pfeqop;
+ Oid ppeqop;
+ Oid ffeqop;
+ int16 eqstrategy;
+ Oid pfeqop_right;
+
+ /* We need several fields out of the pg_opclass entry */
+ cla_ht = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclass));
+ if (!HeapTupleIsValid(cla_ht))
+ elog(ERROR, "cache lookup failed for opclass %u", opclass);
+ cla_tup = (Form_pg_opclass) GETSTRUCT(cla_ht);
+ amid = cla_tup->opcmethod;
+ opfamily = cla_tup->opcfamily;
+ opcintype = cla_tup->opcintype;
+ ReleaseSysCache(cla_ht);
+
+ if (is_temporal)
+ {
+ if (amid != GIST_AM_OID)
+ elog(ERROR, "only GiST indexes are supported for temporal foreign keys");
+ eqstrategy = for_overlaps ? RTOverlapStrategyNumber : RTEqualStrategyNumber;
+ }
+ else
+ {
+ /*
+ * Check it's a btree; currently this can never fail since no other
+ * index AMs support unique indexes. If we ever did have other types
+ * of unique indexes, we'd need a way to determine which operator
+ * strategy number is equality. (Is it reasonable to insist that
+ * every such index AM use btree's number for equality?)
+ */
+ if (amid != BTREE_AM_OID)
+ elog(ERROR, "only b-tree indexes are supported for foreign keys");
+ eqstrategy = BTEqualStrategyNumber;
+ }
+
+ /*
+ * There had better be a primary equality operator for the index.
+ * We'll use it for PK = PK comparisons.
+ */
+ ppeqop = get_opfamily_member(opfamily, opcintype, opcintype,
+ eqstrategy);
+
+ if (!OidIsValid(ppeqop))
+ elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+ eqstrategy, opcintype, opcintype, opfamily);
+
+ /*
+ * Are there equality operators that take exactly the FK type? Assume
+ * we should look through any domain here.
+ */
+ fktyped = getBaseType(fktype);
+
+ pfeqop = get_opfamily_member(opfamily, opcintype, fktyped,
+ eqstrategy);
+ if (OidIsValid(pfeqop))
+ {
+ pfeqop_right = fktyped;
+ ffeqop = get_opfamily_member(opfamily, fktyped, fktyped,
+ eqstrategy);
+ }
+ else
+ {
+ /* keep compiler quiet */
+ pfeqop_right = InvalidOid;
+ ffeqop = InvalidOid;
+ }
+
+ if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
+ {
+ /*
+ * Otherwise, look for an implicit cast from the FK type to the
+ * opcintype, and if found, use the primary equality operator.
+ * This is a bit tricky because opcintype might be a polymorphic
+ * type such as ANYARRAY or ANYENUM; so what we have to test is
+ * whether the two actual column types can be concurrently cast to
+ * that type. (Otherwise, we'd fail to reject combinations such
+ * as int[] and point[].)
+ */
+ Oid input_typeids[2];
+ Oid target_typeids[2];
+
+ input_typeids[0] = pktype;
+ input_typeids[1] = fktype;
+ target_typeids[0] = opcintype;
+ target_typeids[1] = opcintype;
+ if (can_coerce_type(2, input_typeids, target_typeids,
+ COERCION_IMPLICIT))
+ {
+ pfeqop = ffeqop = ppeqop;
+ pfeqop_right = opcintype;
+ }
+ }
+
+ if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
+ {
+ char *fkattr_name;
+ char *pkattr_name;
+
+ if (for_overlaps)
+ {
+ fkattr_name = strVal(fkconstraint->fk_period);
+ pkattr_name = strVal(fkconstraint->pk_period);
+ }
+ else
+ {
+ fkattr_name = strVal(list_nth(fkconstraint->fk_attrs, i));
+ pkattr_name = strVal(list_nth(fkconstraint->pk_attrs, i));
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("foreign key constraint \"%s\" cannot be implemented",
+ fkconstraint->conname),
+ errdetail("Key columns \"%s\" and \"%s\" "
+ "are of incompatible types: %s and %s.",
+ fkattr_name,
+ pkattr_name,
+ format_type_be(fktype),
+ format_type_be(pktype))));
+ }
+
+ if (*old_check_ok)
+ {
+ /*
+ * When a pfeqop changes, revalidate the constraint. We could
+ * permit intra-opfamily changes, but that adds subtle complexity
+ * without any concrete benefit for core types. We need not
+ * assess ppeqop or ffeqop, which RI_Initial_Check() does not use.
+ */
+ *old_check_ok = (pfeqop == lfirst_oid(*old_pfeqop_item));
+ *old_pfeqop_item = lnext(fkconstraint->old_conpfeqop,
+ *old_pfeqop_item);
+ }
+ if (*old_check_ok)
+ {
+ Oid old_fktype;
+ Oid new_fktype;
+ CoercionPathType old_pathtype;
+ CoercionPathType new_pathtype;
+ Oid old_castfunc;
+ Oid new_castfunc;
+ Form_pg_attribute attr = TupleDescAttr(tab->oldDesc,
+ fkattnum[i] - 1);
+
+ /*
+ * Identify coercion pathways from each of the old and new FK-side
+ * column types to the right (foreign) operand type of the pfeqop.
+ * We may assume that pg_constraint.conkey is not changing.
+ */
+ old_fktype = attr->atttypid;
+ new_fktype = fktype;
+ old_pathtype = findFkeyCast(pfeqop_right, old_fktype,
+ &old_castfunc);
+ new_pathtype = findFkeyCast(pfeqop_right, new_fktype,
+ &new_castfunc);
+
+ /*
+ * Upon a change to the cast from the FK column to its pfeqop
+ * operand, revalidate the constraint. For this evaluation, a
+ * binary coercion cast is equivalent to no cast at all. While
+ * type implementors should design implicit casts with an eye
+ * toward consistency of operations like equality, we cannot
+ * assume here that they have done so.
+ *
+ * A function with a polymorphic argument could change behavior
+ * arbitrarily in response to get_fn_expr_argtype(). Therefore,
+ * when the cast destination is polymorphic, we only avoid
+ * revalidation if the input type has not changed at all. Given
+ * just the core data types and operator classes, this requirement
+ * prevents no would-be optimizations.
+ *
+ * If the cast converts from a base type to a domain thereon, then
+ * that domain type must be the opcintype of the unique index.
+ * Necessarily, the primary key column must then be of the domain
+ * type. Since the constraint was previously valid, all values on
+ * the foreign side necessarily exist on the primary side and in
+ * turn conform to the domain. Consequently, we need not treat
+ * domains specially here.
+ *
+ * Since we require that all collations share the same notion of
+ * equality (which they do, because texteq reduces to bitwise
+ * equality), we don't compare collation here.
+ *
+ * We need not directly consider the PK type. It's necessarily
+ * binary coercible to the opcintype of the unique index column,
+ * and ri_triggers.c will only deal with PK datums in terms of
+ * that opcintype. Changing the opcintype also changes pfeqop.
+ */
+ *old_check_ok = (new_pathtype == old_pathtype &&
+ new_castfunc == old_castfunc &&
+ (!IsPolymorphicType(pfeqop_right) ||
+ new_fktype == old_fktype));
+
+ }
+
+ *pfeqopOut = pfeqop;
+ *ppeqopOut = ppeqop;
+ *ffeqopOut = ffeqop;
+}
+
/*
* When the parent of a partition receives [the referencing side of] a foreign
* key, we must propagate that foreign key to the partition. However, the
@@ -11590,7 +11685,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
return address;
}
-
/*
* transformColumnNameList - transform list of column names
*
@@ -11659,6 +11753,7 @@ static int
transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
List **attnamelist,
int16 *attnums, Oid *atttypids,
+ int16 *periodattnums, Oid *periodatttypids,
Oid *opclasses)
{
List *indexoidlist;
@@ -11723,36 +11818,52 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
/*
* Now build the list of PK attributes from the indkey definition (we
- * assume a primary key cannot have expressional elements)
+ * assume a primary key cannot have expressional elements, unless it
+ * has a PERIOD)
*/
*attnamelist = NIL;
for (i = 0; i < indexStruct->indnkeyatts; i++)
{
int pkattno = indexStruct->indkey.values[i];
- attnums[i] = pkattno;
- atttypids[i] = attnumTypeId(pkrel, pkattno);
- opclasses[i] = indclass->values[i];
- *attnamelist = lappend(*attnamelist,
- makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno)))));
+ if (i == indexStruct->indnkeyatts - 1 && indexStruct->indisexclusion)
+ {
+ /* we have a range */
+ /* The caller will set attnums[i] */
+ periodattnums[0] = pkattno;
+ periodatttypids[0] = attnumTypeId(pkrel, pkattno);
+ opclasses[i] = indclass->values[i];
+ }
+ else
+ {
+ attnums[i] = pkattno;
+ atttypids[i] = attnumTypeId(pkrel, pkattno);
+ opclasses[i] = indclass->values[i];
+ *attnamelist = lappend(*attnamelist,
+ makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno)))));
+ }
}
ReleaseSysCache(indexTuple);
- return i;
+ if (indexStruct->indisexclusion) return i - 1;
+ else return i;
}
/*
* transformFkeyCheckAttrs -
*
* Make sure that the attributes of a referenced table belong to a unique
- * (or primary key) constraint. Return the OID of the index supporting
- * the constraint, as well as the opclasses associated with the index
+ * (or primary key) constraint. Or if this is a temporal foreign key
+ * the primary key should be an exclusion constraint instead.
+ * Return the OID of the index supporting the constraint,
+ * as well as the opclasses associated with the index
* columns.
*/
static Oid
transformFkeyCheckAttrs(Relation pkrel,
int numattrs, int16 *attnums,
+ bool is_temporal, int16 periodattnum,
Oid *opclasses) /* output parameter */
{
Oid indexoid = InvalidOid;
@@ -11779,6 +11890,10 @@ transformFkeyCheckAttrs(Relation pkrel,
(errcode(ERRCODE_INVALID_FOREIGN_KEY),
errmsg("foreign key referenced-columns list must not contain duplicates")));
}
+ if (is_temporal && attnums[i] == periodattnum)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_FOREIGN_KEY),
+ errmsg("foreign key referenced-columns list must not contain duplicates")));
}
/*
@@ -11800,12 +11915,16 @@ transformFkeyCheckAttrs(Relation pkrel,
indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
/*
- * Must have the right number of columns; must be unique and not a
- * partial index; forget it if there are any expressions, too. Invalid
- * indexes are out as well.
+ * Must have the right number of columns; must be unique
+ * (or if temporal then exclusion instead) and not a
+ * partial index; forget it if there are any expressions, too.
+ * Invalid indexes are out as well.
*/
- if (indexStruct->indnkeyatts == numattrs &&
- indexStruct->indisunique &&
+ if ((is_temporal
+ ? (indexStruct->indnkeyatts == numattrs + 1 &&
+ indexStruct->indisexclusion)
+ : (indexStruct->indnkeyatts == numattrs &&
+ indexStruct->indisunique)) &&
indexStruct->indisvalid &&
heap_attisnull(indexTuple, Anum_pg_index_indpred, NULL) &&
heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL))
@@ -11843,6 +11962,19 @@ transformFkeyCheckAttrs(Relation pkrel,
if (!found)
break;
}
+ if (found && is_temporal)
+ {
+ found = false;
+ for (j = 0; j < numattrs + 1; j++)
+ {
+ if (periodattnum == indexStruct->indkey.values[j])
+ {
+ opclasses[numattrs] = indclass->values[j];
+ found = true;
+ break;
+ }
+ }
+ }
/*
* Refuse to use a deferrable unique/primary key. This is per SQL
@@ -11952,7 +12084,8 @@ validateForeignKeyConstraint(char *conname,
Relation rel,
Relation pkrel,
Oid pkindOid,
- Oid constraintOid)
+ Oid constraintOid,
+ bool temporal)
{
TupleTableSlot *slot;
TableScanDesc scan;
@@ -11981,8 +12114,10 @@ validateForeignKeyConstraint(char *conname,
/*
* See if we can do it with a single LEFT JOIN query. A false result
* indicates we must proceed with the fire-the-trigger method.
+ * We can't do a LEFT JOIN for temporal FKs yet,
+ * but we can once we support temporal left joins.
*/
- if (RI_Initial_Check(&trig, rel, pkrel))
+ if (!temporal && RI_Initial_Check(&trig, rel, pkrel))
return;
/*
@@ -12051,6 +12186,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
{
ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
+ bool is_temporal = fkconstraint->fk_period;
/*
* Note: for a self-referential FK (referencing and referenced tables are
@@ -12070,12 +12206,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
/* Either ON INSERT or ON UPDATE */
if (on_insert)
{
- fk_trigger->funcname = SystemFuncName("RI_FKey_check_ins");
+ if (is_temporal)
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_check_ins");
+ else
+ fk_trigger->funcname = SystemFuncName("RI_FKey_check_ins");
fk_trigger->events = TRIGGER_TYPE_INSERT;
}
else
{
- fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd");
+ if (is_temporal)
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_check_upd");
+ else
+ fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd");
fk_trigger->events = TRIGGER_TYPE_UPDATE;
}
@@ -12133,37 +12275,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
- switch (fkconstraint->fk_del_action)
+ if (fkconstraint->fk_period != NULL)
{
- case FKCONSTR_ACTION_NOACTION:
- fk_trigger->deferrable = fkconstraint->deferrable;
- fk_trigger->initdeferred = fkconstraint->initdeferred;
- fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_del");
- break;
- case FKCONSTR_ACTION_RESTRICT:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_del");
- break;
- case FKCONSTR_ACTION_CASCADE:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_del");
- break;
- case FKCONSTR_ACTION_SETNULL:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_del");
- break;
- case FKCONSTR_ACTION_SETDEFAULT:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del");
- break;
- default:
- elog(ERROR, "unrecognized FK action type: %d",
- (int) fkconstraint->fk_del_action);
- break;
+ /* Temporal foreign keys */
+ switch (fkconstraint->fk_del_action)
+ {
+ case FKCONSTR_ACTION_NOACTION:
+ fk_trigger->deferrable = fkconstraint->deferrable;
+ fk_trigger->initdeferred = fkconstraint->initdeferred;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_noaction_del");
+ break;
+ case FKCONSTR_ACTION_RESTRICT:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_del");
+ break;
+ case FKCONSTR_ACTION_CASCADE:
+ case FKCONSTR_ACTION_SETNULL:
+ case FKCONSTR_ACTION_SETDEFAULT:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("action not supported for temporal foreign keys")));
+ break;
+ default:
+ elog(ERROR, "unrecognized FK action type: %d",
+ (int) fkconstraint->fk_del_action);
+ break;
+ }
+ }
+ else
+ {
+ switch (fkconstraint->fk_del_action)
+ {
+ case FKCONSTR_ACTION_NOACTION:
+ fk_trigger->deferrable = fkconstraint->deferrable;
+ fk_trigger->initdeferred = fkconstraint->initdeferred;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_del");
+ break;
+ case FKCONSTR_ACTION_RESTRICT:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_del");
+ break;
+ case FKCONSTR_ACTION_CASCADE:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_del");
+ break;
+ case FKCONSTR_ACTION_SETNULL:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_del");
+ break;
+ case FKCONSTR_ACTION_SETDEFAULT:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del");
+ break;
+ default:
+ elog(ERROR, "unrecognized FK action type: %d",
+ (int) fkconstraint->fk_del_action);
+ break;
+ }
}
trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
@@ -12193,37 +12366,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->transitionRels = NIL;
fk_trigger->constrrel = NULL;
- switch (fkconstraint->fk_upd_action)
+ if (fkconstraint->fk_period != NULL)
{
- case FKCONSTR_ACTION_NOACTION:
- fk_trigger->deferrable = fkconstraint->deferrable;
- fk_trigger->initdeferred = fkconstraint->initdeferred;
- fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_upd");
- break;
- case FKCONSTR_ACTION_RESTRICT:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_upd");
- break;
- case FKCONSTR_ACTION_CASCADE:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_upd");
- break;
- case FKCONSTR_ACTION_SETNULL:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_upd");
- break;
- case FKCONSTR_ACTION_SETDEFAULT:
- fk_trigger->deferrable = false;
- fk_trigger->initdeferred = false;
- fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_upd");
- break;
- default:
- elog(ERROR, "unrecognized FK action type: %d",
- (int) fkconstraint->fk_upd_action);
- break;
+ /* Temporal foreign keys */
+ switch (fkconstraint->fk_upd_action)
+ {
+ case FKCONSTR_ACTION_NOACTION:
+ fk_trigger->deferrable = fkconstraint->deferrable;
+ fk_trigger->initdeferred = fkconstraint->initdeferred;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_noaction_upd");
+ break;
+ case FKCONSTR_ACTION_RESTRICT:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_upd");
+ break;
+ case FKCONSTR_ACTION_CASCADE:
+ case FKCONSTR_ACTION_SETNULL:
+ case FKCONSTR_ACTION_SETDEFAULT:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("action not supported for temporal foreign keys")));
+ break;
+ default:
+ elog(ERROR, "unrecognized FK action type: %d",
+ (int) fkconstraint->fk_upd_action);
+ break;
+ }
+ }
+ else
+ {
+ switch (fkconstraint->fk_upd_action)
+ {
+ case FKCONSTR_ACTION_NOACTION:
+ fk_trigger->deferrable = fkconstraint->deferrable;
+ fk_trigger->initdeferred = fkconstraint->initdeferred;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_upd");
+ break;
+ case FKCONSTR_ACTION_RESTRICT:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_upd");
+ break;
+ case FKCONSTR_ACTION_CASCADE:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_upd");
+ break;
+ case FKCONSTR_ACTION_SETNULL:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_upd");
+ break;
+ case FKCONSTR_ACTION_SETDEFAULT:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_upd");
+ break;
+ default:
+ elog(ERROR, "unrecognized FK action type: %d",
+ (int) fkconstraint->fk_upd_action);
+ break;
+ }
}
trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ca6d13d349..0544ffb63d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -523,11 +523,12 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <node> TableElement TypedTableElement ConstraintElem TableFuncElement
%type <node> columnDef columnOptions
%type <defelt> def_elem reloption_elem old_aggr_elem operator_def_elem
-%type <node> def_arg columnElem withoutOverlapsClause
+%type <node> def_arg columnElem withoutOverlapsClause optionalPeriodName
where_clause where_or_current_clause
a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound
columnref in_expr having_clause func_table xmltable array_expr
OptWhereClause operator_def_arg
+%type <list> opt_column_and_period_list
%type <list> rowsfrom_item rowsfrom_list opt_col_def_list
%type <boolean> opt_ordinality
%type <list> ExclusionConstraintList ExclusionConstraintElem
@@ -745,7 +746,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD
- PLACING PLANS POLICY
+ PERIOD PLACING PLANS POLICY
POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
@@ -4188,21 +4189,23 @@ ConstraintElem:
NULL, yyscanner);
$$ = (Node *) n;
}
- | FOREIGN KEY '(' columnList ')' REFERENCES qualified_name
- opt_column_list key_match key_actions ConstraintAttributeSpec
+ | FOREIGN KEY '(' columnList optionalPeriodName ')' REFERENCES qualified_name
+ opt_column_and_period_list key_match key_actions ConstraintAttributeSpec
{
Constraint *n = makeNode(Constraint);
n->contype = CONSTR_FOREIGN;
n->location = @1;
- n->pktable = $7;
+ n->pktable = $8;
n->fk_attrs = $4;
- n->pk_attrs = $8;
- n->fk_matchtype = $9;
- n->fk_upd_action = ($10)->updateAction->action;
- n->fk_del_action = ($10)->deleteAction->action;
- n->fk_del_set_cols = ($10)->deleteAction->cols;
- processCASbits($11, @11, "FOREIGN KEY",
+ n->fk_period = $5;
+ n->pk_attrs = linitial($9);
+ n->pk_period = lsecond($9);
+ n->fk_matchtype = $10;
+ n->fk_upd_action = ($11)->updateAction->action;
+ n->fk_del_action = ($11)->deleteAction->action;
+ n->fk_del_set_cols = ($11)->deleteAction->cols;
+ processCASbits($12, @12, "FOREIGN KEY",
&n->deferrable, &n->initdeferred,
&n->skip_validation, NULL,
yyscanner);
@@ -4230,6 +4233,16 @@ withoutOverlapsClause:
| /*EMPTY*/ { $$ = NULL; }
;
+optionalPeriodName:
+ ',' PERIOD columnElem { $$ = $3; }
+ | /*EMPTY*/ { $$ = NULL; }
+ ;
+
+opt_column_and_period_list:
+ '(' columnList optionalPeriodName ')' { $$ = list_make2($2, $3); }
+ | /*EMPTY*/ { $$ = list_make2(NIL, NULL); }
+ ;
+
columnElem: ColId
{
$$ = (Node *) makeString($1);
@@ -17517,6 +17530,7 @@ reserved_keyword:
| ONLY
| OR
| ORDER
+ | PERIOD
| PLACING
| PRIMARY
| REFERENCES
@@ -17826,6 +17840,7 @@ bare_label_keyword:
| PARTITION
| PASSING
| PASSWORD
+ | PERIOD
| PLACING
| PLANS
| POLICY
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 6945d99b3d..70466a94d9 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -31,6 +31,7 @@
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
#include "catalog/pg_operator.h"
+#include "catalog/pg_range.h"
#include "catalog/pg_type.h"
#include "commands/trigger.h"
#include "executor/executor.h"
@@ -48,6 +49,7 @@
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
+#include "utils/rangetypes.h"
#include "utils/rel.h"
#include "utils/rls.h"
#include "utils/ruleutils.h"
@@ -118,6 +120,7 @@ typedef struct RI_ConstraintInfo
int16 confdelsetcols[RI_MAX_NUMKEYS]; /* attnums of cols to set on
* delete */
char confmatchtype; /* foreign key's match type */
+ bool temporal; /* if the foreign key is temporal */
int nkeys; /* number of key columns */
int16 pk_attnums[RI_MAX_NUMKEYS]; /* attnums of referenced cols */
int16 fk_attnums[RI_MAX_NUMKEYS]; /* attnums of referencing cols */
@@ -200,8 +203,9 @@ static int ri_NullCheck(TupleDesc tupDesc, TupleTableSlot *slot,
static void ri_BuildQueryKey(RI_QueryKey *key,
const RI_ConstraintInfo *riinfo,
int32 constr_queryno);
-static bool ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
+static bool ri_KeysStable(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
const RI_ConstraintInfo *riinfo, bool rel_is_pk);
+static bool ri_RangeAttributeNeedsCheck(bool rel_is_pk, Datum oldvalue, Datum newvalue);
static bool ri_AttributesEqual(Oid eq_opr, Oid typeid,
Datum oldvalue, Datum newvalue);
@@ -361,26 +365,57 @@ RI_FKey_check(TriggerData *trigdata)
/* ----------
* The query string built is
- * SELECT 1 FROM [ONLY] <pktable> x WHERE pkatt1 = $1 [AND ...]
- * FOR KEY SHARE OF x
+ * SELECT 1
+ * FROM [ONLY] <pktable> x WHERE pkatt1 = $1 [AND ...]
+ * FOR KEY SHARE OF x
* The type id's for the $ parameters are those of the
* corresponding FK attributes.
+ *
+ * But for temporal FKs we need to make sure
+ * the FK's range is completely covered.
+ * So we use this query instead:
+ * SELECT 1
+ * FROM (
+ * SELECT pkperiodatt AS r
+ * FROM [ONLY] pktable x
+ * WHERE pkatt1 = $1 [AND ...]
+ * AND pkperiodatt && $n
+ * FOR KEY SHARE OF x
+ * ) x1
+ * HAVING $n <@ range_agg(x1.r)
+ * Note if FOR KEY SHARE ever allows GROUP BY and HAVING
+ * we can make this a bit simpler.
* ----------
*/
initStringInfo(&querybuf);
pk_only = pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
"" : "ONLY ";
quoteRelationName(pkrelname, pk_rel);
- appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
- pk_only, pkrelname);
+ if (riinfo->temporal)
+ {
+ quoteOneName(attname,
+ RIAttName(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]));
+
+ appendStringInfo(&querybuf,
+ "SELECT 1 FROM (SELECT %s AS r FROM %s%s x",
+ attname, pk_only, pkrelname);
+ }
+ else {
+ appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
+ pk_only, pkrelname);
+ }
querysep = "WHERE";
for (int i = 0; i < riinfo->nkeys; i++)
{
- Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
- Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+ Oid pk_type;
+ Oid fk_type;
+ pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
quoteOneName(attname,
RIAttName(pk_rel, riinfo->pk_attnums[i]));
+
+ fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+
sprintf(paramname, "$%d", i + 1);
ri_GenerateQual(&querybuf, querysep,
attname, pk_type,
@@ -390,6 +425,8 @@ RI_FKey_check(TriggerData *trigdata)
queryoids[i] = fk_type;
}
appendStringInfoString(&querybuf, " FOR KEY SHARE OF x");
+ if (riinfo->temporal)
+ appendStringInfo(&querybuf, ") x1 HAVING $%d <@ pg_catalog.range_agg(x1.r)", riinfo->nkeys);
/* Prepare and save the plan */
qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids,
@@ -508,10 +545,12 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
querysep = "WHERE";
for (int i = 0; i < riinfo->nkeys; i++)
{
- Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+ Oid pk_type;
+ pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
quoteOneName(attname,
RIAttName(pk_rel, riinfo->pk_attnums[i]));
+
sprintf(paramname, "$%d", i + 1);
ri_GenerateQual(&querybuf, querysep,
attname, pk_type,
@@ -695,10 +734,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
querysep = "WHERE";
for (int i = 0; i < riinfo->nkeys; i++)
{
- Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
- Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
- Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
- Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
+ Oid pk_type;
+ Oid fk_type;
+ Oid pk_coll;
+ Oid fk_coll;
+
+ pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+ pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+
+ fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+ fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
quoteOneName(attname,
RIAttName(fk_rel, riinfo->fk_attnums[i]));
@@ -1213,6 +1258,126 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
}
}
+/* ----------
+ * TRI_FKey_check_ins -
+ *
+ * Check temporal foreign key existence at insert event on FK table.
+ * ----------
+ */
+Datum
+TRI_FKey_check_ins(PG_FUNCTION_ARGS)
+{
+ /*
+ * Check that this is a valid trigger call on the right time and event.
+ */
+ ri_CheckTrigger(fcinfo, "RI_FKey_check_ins", RI_TRIGTYPE_INSERT);
+
+ /*
+ * Share code with UPDATE case.
+ */
+ return RI_FKey_check((TriggerData *) fcinfo->context);
+}
+
+
+/* ----------
+ * TRI_FKey_check_upd -
+ *
+ * Check temporal foreign key existence at update event on FK table.
+ * ----------
+ */
+Datum
+TRI_FKey_check_upd(PG_FUNCTION_ARGS)
+{
+ /*
+ * Check that this is a valid trigger call on the right time and event.
+ */
+ ri_CheckTrigger(fcinfo, "RI_FKey_check_upd", RI_TRIGTYPE_UPDATE);
+
+ /*
+ * Share code with INSERT case.
+ */
+ return RI_FKey_check((TriggerData *) fcinfo->context);
+}
+
+
+/* ----------
+ * TRI_FKey_noaction_del -
+ *
+ * Give an error and roll back the current transaction if the
+ * delete has resulted in a violation of the given temporal
+ * referential integrity constraint.
+ * ----------
+ */
+Datum
+TRI_FKey_noaction_del(PG_FUNCTION_ARGS)
+{
+ /*
+ * Check that this is a valid trigger call on the right time and event.
+ */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_noaction_del", RI_TRIGTYPE_DELETE);
+
+ /*
+ * Share code with RESTRICT/UPDATE cases.
+ */
+ return ri_restrict((TriggerData *) fcinfo->context, true);
+}
+
+/*
+ * TRI_FKey_restrict_del -
+ *
+ * Restrict delete from PK table to rows unreferenced by foreign key.
+ *
+ * The SQL standard intends that this referential action occur exactly when
+ * the delete is performed, rather than after. This appears to be
+ * the only difference between "NO ACTION" and "RESTRICT". In Postgres
+ * we still implement this as an AFTER trigger, but it's non-deferrable.
+ */
+Datum
+TRI_FKey_restrict_del(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_restrict_del", RI_TRIGTYPE_DELETE);
+
+ /* Share code with NO ACTION/UPDATE cases. */
+ return ri_restrict((TriggerData *) fcinfo->context, false);
+}
+
+/*
+ * TRI_FKey_noaction_upd -
+ *
+ * Give an error and roll back the current transaction if the
+ * update has resulted in a violation of the given referential
+ * integrity constraint.
+ */
+Datum
+TRI_FKey_noaction_upd(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_noaction_upd", RI_TRIGTYPE_UPDATE);
+
+ /* Share code with RESTRICT/DELETE cases. */
+ return ri_restrict((TriggerData *) fcinfo->context, true);
+}
+
+/*
+ * TRI_FKey_restrict_upd -
+ *
+ * Restrict update of PK to rows unreferenced by foreign key.
+ *
+ * The SQL standard intends that this referential action occur exactly when
+ * the update is performed, rather than after. This appears to be
+ * the only difference between "NO ACTION" and "RESTRICT". In Postgres
+ * we still implement this as an AFTER trigger, but it's non-deferrable.
+ */
+Datum
+TRI_FKey_restrict_upd(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_restrict_upd", RI_TRIGTYPE_UPDATE);
+
+ /* Share code with NO ACTION/DELETE cases. */
+ return ri_restrict((TriggerData *) fcinfo->context, false);
+}
/*
* RI_FKey_pk_upd_check_required -
@@ -1241,7 +1406,7 @@ RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel,
return false;
/* If all old and new key values are equal, no check is needed */
- if (newslot && ri_KeysEqual(pk_rel, oldslot, newslot, riinfo, true))
+ if (newslot && ri_KeysStable(pk_rel, oldslot, newslot, riinfo, true))
return false;
/* Else we need to fire the trigger. */
@@ -1340,7 +1505,7 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
return true;
/* If all old and new key values are equal, no check is needed */
- if (ri_KeysEqual(fk_rel, oldslot, newslot, riinfo, false))
+ if (ri_KeysStable(fk_rel, oldslot, newslot, riinfo, false))
return false;
/* Else we need to fire the trigger. */
@@ -1488,15 +1653,17 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel)
sep = "(";
for (int i = 0; i < riinfo->nkeys; i++)
{
- Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
- Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
- Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
- Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
+ Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+ Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+ Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+ Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
quoteOneName(pkattname + 3,
RIAttName(pk_rel, riinfo->pk_attnums[i]));
+
quoteOneName(fkattname + 3,
RIAttName(fk_rel, riinfo->fk_attnums[i]));
+
ri_GenerateQual(&querybuf, sep,
pkattname, pk_type,
riinfo->pf_eq_oprs[i],
@@ -2137,6 +2304,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
riinfo = (RI_ConstraintInfo *) hash_search(ri_constraint_cache,
&constraintOid,
HASH_ENTER, &found);
+
if (!found)
riinfo->valid = false;
else if (riinfo->valid)
@@ -2171,6 +2339,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
riinfo->confupdtype = conForm->confupdtype;
riinfo->confdeltype = conForm->confdeltype;
riinfo->confmatchtype = conForm->confmatchtype;
+ riinfo->temporal = conForm->contemporal;
DeconstructFkConstraintRow(tup,
&riinfo->nkeys,
@@ -2791,9 +2960,12 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan)
/*
- * ri_KeysEqual -
+ * ri_KeysStable -
*
- * Check if all key values in OLD and NEW are equal.
+ * Check if all key values in OLD and NEW are "equivalent":
+ * For normal FKs we check for equality.
+ * For temporal FKs we check that the PK side is a superset of its old value,
+ * or the FK side is a subset.
*
* Note: at some point we might wish to redefine this as checking for
* "IS NOT DISTINCT" rather than "=", that is, allow two nulls to be
@@ -2801,7 +2973,7 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan)
* previously found at least one of the rows to contain no nulls.
*/
static bool
-ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
+ri_KeysStable(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
const RI_ConstraintInfo *riinfo, bool rel_is_pk)
{
const int16 *attnums;
@@ -2834,35 +3006,86 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
if (rel_is_pk)
{
- /*
- * If we are looking at the PK table, then do a bytewise
- * comparison. We must propagate PK changes if the value is
- * changed to one that "looks" different but would compare as
- * equal using the equality operator. This only makes a
- * difference for ON UPDATE CASCADE, but for consistency we treat
- * all changes to the PK the same.
- */
- Form_pg_attribute att = TupleDescAttr(oldslot->tts_tupleDescriptor, attnums[i] - 1);
+ if (riinfo->temporal && i == riinfo->nkeys - 1)
+ {
+ if (ri_RangeAttributeNeedsCheck(true, oldvalue, newvalue))
+ return false;
+ }
+ else
+ {
+ /*
+ * If we are looking at the PK table, then do a bytewise
+ * comparison. We must propagate PK changes if the value is
+ * changed to one that "looks" different but would compare as
+ * equal using the equality operator. This only makes a
+ * difference for ON UPDATE CASCADE, but for consistency we treat
+ * all changes to the PK the same.
+ */
+ Form_pg_attribute att = TupleDescAttr(oldslot->tts_tupleDescriptor, attnums[i] - 1);
- if (!datum_image_eq(oldvalue, newvalue, att->attbyval, att->attlen))
- return false;
+ if (!datum_image_eq(oldvalue, newvalue, att->attbyval, att->attlen))
+ return false;
+ }
}
else
{
- /*
- * For the FK table, compare with the appropriate equality
- * operator. Changes that compare equal will still satisfy the
- * constraint after the update.
- */
- if (!ri_AttributesEqual(riinfo->ff_eq_oprs[i], RIAttType(rel, attnums[i]),
- oldvalue, newvalue))
- return false;
+ if (riinfo->temporal && i == riinfo->nkeys - 1)
+ {
+ if (ri_RangeAttributeNeedsCheck(false, oldvalue, newvalue))
+ return false;
+ }
+ else
+ {
+ /*
+ * For the FK table, compare with the appropriate equality
+ * operator. Changes that compare equal will still satisfy the
+ * constraint after the update.
+ */
+ if (!ri_AttributesEqual(riinfo->ff_eq_oprs[i], RIAttType(rel, attnums[i]),
+ oldvalue, newvalue))
+ return false;
+ }
}
}
return true;
}
+/*
+ * ri_RangeAttributeNeedsCheck -
+ *
+ * Compare old and new values, and return true if we need to check the FK.
+ *
+ * NB: we have already checked that neither value is null.
+ */
+static bool
+ri_RangeAttributeNeedsCheck(bool rel_is_pk, Datum oldvalue, Datum newvalue)
+{
+ RangeType *oldrange, *newrange;
+ Oid oldrngtype, newrngtype;
+ TypeCacheEntry *typcache;
+
+ oldrange = DatumGetRangeTypeP(oldvalue);
+ newrange = DatumGetRangeTypeP(newvalue);
+ oldrngtype = RangeTypeGetOid(oldrange);
+ newrngtype = RangeTypeGetOid(newrange);
+
+ if (oldrngtype != newrngtype)
+ elog(ERROR, "range types are inconsistent");
+
+ typcache = lookup_type_cache(oldrngtype, TYPECACHE_RANGE_INFO);
+ if (typcache->rngelemtype == NULL)
+ elog(ERROR, "type %u is not a range type", oldrngtype);
+
+ if (rel_is_pk)
+ /* If the PK's range shrunk, a conflict is possible. */
+ return !range_eq_internal(typcache, oldrange, newrange) &&
+ !range_contains_internal(typcache, newrange, oldrange);
+ else
+ /* If the FK's range grew, a conflict is possible. */
+ return !range_eq_internal(typcache, oldrange, newrange) &&
+ range_contains_internal(typcache, newrange, oldrange);
+}
/*
* ri_AttributesEqual -
@@ -3021,10 +3244,16 @@ RI_FKey_trigger_type(Oid tgfoid)
case F_RI_FKEY_SETDEFAULT_UPD:
case F_RI_FKEY_NOACTION_DEL:
case F_RI_FKEY_NOACTION_UPD:
+ case F_TRI_FKEY_RESTRICT_DEL:
+ case F_TRI_FKEY_RESTRICT_UPD:
+ case F_TRI_FKEY_NOACTION_DEL:
+ case F_TRI_FKEY_NOACTION_UPD:
return RI_TRIGGER_PK;
case F_RI_FKEY_CHECK_INS:
case F_RI_FKEY_CHECK_UPD:
+ case F_TRI_FKEY_CHECK_INS:
+ case F_TRI_FKEY_CHECK_UPD:
return RI_TRIGGER_FK;
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 778d3e0334..1db5ed557b 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -339,7 +339,7 @@ static char *pg_get_viewdef_worker(Oid viewoid,
int prettyFlags, int wrapColumn);
static char *pg_get_triggerdef_worker(Oid trigid, bool pretty);
static int decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
- bool withoutOverlaps, StringInfo buf);
+ bool withoutOverlaps, bool withPeriod, StringInfo buf);
static char *pg_get_ruledef_worker(Oid ruleoid, int prettyFlags);
static char *pg_get_indexdef_worker(Oid indexrelid, int colno,
const Oid *excludeOps,
@@ -2247,7 +2247,10 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
val = SysCacheGetAttrNotNull(CONSTROID, tup,
Anum_pg_constraint_conkey);
- decompile_column_index_array(val, conForm->conrelid, conForm->conindid, false, &buf);
+ /* If it is a temporal foreign key then it uses PERIOD. */
+ decompile_column_index_array(val, conForm->conrelid,
+ conForm->conindid, false,
+ conForm->contemporal, &buf);
/* add foreign relation name */
appendStringInfo(&buf, ") REFERENCES %s(",
@@ -2258,7 +2261,9 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
val = SysCacheGetAttrNotNull(CONSTROID, tup,
Anum_pg_constraint_confkey);
- decompile_column_index_array(val, conForm->confrelid, conForm->conindid, false, &buf);
+ decompile_column_index_array(val, conForm->confrelid,
+ conForm->conindid, false,
+ conForm->contemporal, &buf);
appendStringInfoChar(&buf, ')');
@@ -2344,7 +2349,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
if (!isnull)
{
appendStringInfoString(&buf, " (");
- decompile_column_index_array(val, conForm->conrelid, InvalidOid, false, &buf);
+ decompile_column_index_array(val, conForm->conrelid, InvalidOid, false, false, &buf);
appendStringInfoChar(&buf, ')');
}
@@ -2387,7 +2392,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
indexId = conForm->conindid;
SysCacheGetAttr(CONSTROID, tup,
Anum_pg_constraint_conexclop, &isnull);
- keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, &buf);
+ keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, false, &buf);
appendStringInfoChar(&buf, ')');
@@ -2583,7 +2588,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
*/
static int
decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
- bool withoutOverlaps, StringInfo buf)
+ bool withoutOverlaps, bool withPeriod, StringInfo buf)
{
Datum *keys;
int nKeys;
@@ -2604,6 +2609,8 @@ decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
appendStringInfoString(buf, quote_identifier(colName));
else if (withoutOverlaps && j == nKeys - 1)
appendStringInfo(buf, ", %s WITHOUT OVERLAPS", quote_identifier(colName));
+ else if (withPeriod && j == nKeys - 1)
+ appendStringInfo(buf, ", PERIOD %s", quote_identifier(colName));
else
appendStringInfo(buf, ", %s", quote_identifier(colName));
}
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 786a9ca904..7ca0534b48 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -127,19 +127,19 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
int16 confkey[1];
/*
- * If a foreign key, the OIDs of the PK = FK equality operators for each
+ * If a foreign key, the OIDs of the PK = FK comparison operators for each
* column of the constraint
*/
Oid conpfeqop[1] BKI_LOOKUP(pg_operator);
/*
- * If a foreign key, the OIDs of the PK = PK equality operators for each
+ * If a foreign key, the OIDs of the PK = PK comparison operators for each
* column of the constraint (i.e., equality for the referenced columns)
*/
Oid conppeqop[1] BKI_LOOKUP(pg_operator);
/*
- * If a foreign key, the OIDs of the FK = FK equality operators for each
+ * If a foreign key, the OIDs of the FK = FK comparison operators for each
* column of the constraint (i.e., equality for the referencing columns)
*/
Oid conffeqop[1] BKI_LOOKUP(pg_operator);
@@ -180,7 +180,7 @@ DECLARE_INDEX(pg_constraint_conparentid_index, 2579, ConstraintParentIndexId, pg
/* conkey can contain zero (InvalidAttrNumber) if a whole-row Var is used */
DECLARE_ARRAY_FOREIGN_KEY_OPT((conrelid, conkey), pg_attribute, (attrelid, attnum));
-DECLARE_ARRAY_FOREIGN_KEY((confrelid, confkey), pg_attribute, (attrelid, attnum));
+DECLARE_ARRAY_FOREIGN_KEY_OPT((confrelid, confkey), pg_attribute, (attrelid, attnum));
#ifdef EXPOSE_TO_CLIENT_CODE
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9805bc6118..3132468a9c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3942,6 +3942,30 @@
prorettype => 'trigger', proargtypes => '',
prosrc => 'RI_FKey_noaction_upd' },
+# Temporal referential integrity constraint triggers
+{ oid => '6122', descr => 'temporal referential integrity FOREIGN KEY ... REFERENCES',
+ proname => 'TRI_FKey_check_ins', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'TRI_FKey_check_ins' },
+{ oid => '6123', descr => 'temporal referential integrity FOREIGN KEY ... REFERENCES',
+ proname => 'TRI_FKey_check_upd', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'TRI_FKey_check_upd' },
+{ oid => '6126', descr => 'temporal referential integrity ON DELETE RESTRICT',
+ proname => 'TRI_FKey_restrict_del', provolatile => 'v',
+ prorettype => 'trigger', proargtypes => '',
+ prosrc => 'TRI_FKey_restrict_del' },
+{ oid => '6127', descr => 'temporal referential integrity ON UPDATE RESTRICT',
+ proname => 'TRI_FKey_restrict_upd', provolatile => 'v',
+ prorettype => 'trigger', proargtypes => '',
+ prosrc => 'TRI_FKey_restrict_upd' },
+{ oid => '6132', descr => 'temporal referential integrity ON DELETE NO ACTION',
+ proname => 'TRI_FKey_noaction_del', provolatile => 'v',
+ prorettype => 'trigger', proargtypes => '',
+ prosrc => 'TRI_FKey_noaction_del' },
+{ oid => '6133', descr => 'temporal referential integrity ON UPDATE NO ACTION',
+ proname => 'TRI_FKey_noaction_upd', provolatile => 'v',
+ prorettype => 'trigger', proargtypes => '',
+ prosrc => 'TRI_FKey_noaction_upd' },
+
{ oid => '1666',
proname => 'varbiteq', proleakproof => 't', prorettype => 'bool',
proargtypes => 'varbit varbit', prosrc => 'biteq' },
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0747b54b26..479b5ffa35 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2632,7 +2632,9 @@ typedef struct Constraint
/* Fields used for FOREIGN KEY constraints: */
RangeVar *pktable; /* Primary key table */
List *fk_attrs; /* Attributes of foreign key */
+ Node *fk_period; /* String node naming Period or range column */
List *pk_attrs; /* Corresponding attrs in PK table */
+ Node *pk_period; /* String node naming Period or range column */
char fk_matchtype; /* FULL, PARTIAL, SIMPLE */
char fk_upd_action; /* ON UPDATE action */
char fk_del_action; /* ON DELETE action */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 5984dcfa4b..ccf5bf8aa6 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -326,6 +326,7 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("period", PERIOD, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index d7ee0c4ff9..e61fd1d906 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -357,3 +357,430 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
(1 row)
DROP TABLE temporal_partitioned;
+--
+-- test FK dependencies
+--
+-- can't drop a range referenced by an FK, unless with CASCADE
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal3 (id, PERIOD valid_at)
+);
+ALTER TABLE temporal3 DROP COLUMN valid_at;
+ERROR: cannot drop column valid_at of table temporal3 because other objects depend on it
+DETAIL: constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng depends on column valid_at of table temporal3
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+ALTER TABLE temporal3 DROP COLUMN valid_at CASCADE;
+NOTICE: drop cascades to constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng
+DROP TABLE temporal_fk_rng2rng;
+DROP TABLE temporal3;
+--
+-- test FOREIGN KEY, range references range
+--
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at int4range,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk2 FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+ERROR: foreign key constraint "temporal_fk_rng2rng_fk2" cannot be implemented
+DETAIL: Key columns "valid_at" and "valid_at" are of incompatible types: int4range and tsrange.
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+DROP TABLE temporal_fk_rng2rng;
+-- with inferred PK on the referenced table:
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+);
+DROP TABLE temporal_fk_rng2rng;
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+ REFERENCES temporal_rng (id, PERIOD id)
+);
+ERROR: foreign key referenced-columns list must not contain duplicates
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng (id, PERIOD valid_at);
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk2
+ FOREIGN KEY (parent_id, PERIOD parent_id)
+ REFERENCES temporal_rng (id, PERIOD id);
+ERROR: foreign key referenced-columns list must not contain duplicates
+--
+-- test with rows already
+--
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]');
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+ERROR: insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL: Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sun Apr 01 00:00:00 2018")) is not present in table "temporal_rng".
+-- okay again:
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+--
+-- test pg_get_constraintdef
+--
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2rng_fk';
+ pg_get_constraintdef
+---------------------------------------------------------------------------------------
+ FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng(id, PERIOD valid_at)
+(1 row)
+
+--
+-- test FK child inserts
+--
+INSERT INTO temporal_fk_rng2rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+ERROR: insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL: Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sun Apr 01 00:00:00 2018")) is not present in table "temporal_rng".
+-- now it should work:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+--
+-- test FK child updates
+--
+UPDATE temporal_fk_rng2rng SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_rng2rng SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]';
+ERROR: insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL: Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Tue May 01 00:00:00 2018")) is not present in table "temporal_rng".
+UPDATE temporal_fk_rng2rng SET parent_id = '[8,8]' WHERE id = '[1,1]';
+ERROR: insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL: Key (parent_id, valid_at)=([8,9), ["Tue Jan 02 00:00:00 2018","Thu Mar 01 00:00:00 2018")) is not present in table "temporal_rng".
+--
+-- test FK parent updates NO ACTION
+--
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_rng SET valid_at = tsrange('2016-02-01', '2016-03-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+ERROR: update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng"
+DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+--
+-- test FK parent updates RESTRICT
+--
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_rng SET valid_at = tsrange('2016-02-01', '2016-03-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+ERROR: update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng"
+DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+--
+-- test FK parent deletes NO ACTION
+--
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+ERROR: update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng"
+DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+--
+-- test FK parent deletes RESTRICT
+--
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE RESTRICT;
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+ERROR: update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng"
+DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+--
+-- test ON UPDATE/DELETE options
+--
+-- test FK parent updates CASCADE
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE CASCADE ON UPDATE CASCADE;
+ERROR: action not supported for temporal foreign keys
+-- test FK parent updates SET NULL
+INSERT INTO temporal_rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]');
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE SET NULL ON UPDATE SET NULL;
+ERROR: action not supported for temporal foreign keys
+-- test FK parent updates SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[-1,-1]', tsrange(null, null));
+INSERT INTO temporal_rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]');
+ALTER TABLE temporal_fk_rng2rng
+ ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+ DROP CONSTRAINT temporal_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+ERROR: action not supported for temporal foreign keys
+-- FK between partitioned tables
+CREATE TABLE temporal_partitioned_rng (
+ id int4range,
+ valid_at daterange,
+ name text,
+ CONSTRAINT temporal_paritioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE tp1 partition OF temporal_partitioned_rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
+CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+INSERT INTO temporal_partitioned_rng VALUES
+ ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
+ ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
+ ('[2,2]', daterange('2000-01-01', '2010-01-01'), 'two');
+CREATE TABLE temporal_partitioned_fk_rng2rng (
+ id int4range,
+ valid_at daterange,
+ parent_id int4range,
+ CONSTRAINT temporal_partitioned_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_partitioned_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_partitioned_rng (id, PERIOD valid_at)
+) PARTITION BY LIST (id);
+CREATE TABLE tfkp1 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
+CREATE TABLE tfkp2 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+-- partitioned FK child inserts
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES
+ ('[1,1]', daterange('2000-01-01', '2000-02-15'), '[1,1]'),
+ ('[1,1]', daterange('2001-01-01', '2002-01-01'), '[2,2]'),
+ ('[2,2]', daterange('2000-01-01', '2000-02-15'), '[1,1]');
+-- should fail:
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES
+ ('[3,3]', daterange('2010-01-01', '2010-02-15'), '[1,1]');
+ERROR: insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL: Key (parent_id, valid_at)=([1,2), [01-01-2010,02-15-2010)) is not present in table "temporal_partitioned_rng".
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES
+ ('[3,3]', daterange('2000-01-01', '2000-02-15'), '[3,3]');
+ERROR: insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL: Key (parent_id, valid_at)=([3,4), [01-01-2000,02-15-2000)) is not present in table "temporal_partitioned_rng".
+-- partitioned FK child updates
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-02-13') WHERE id = '[2,2]';
+-- move a row from the first partition to the second
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[4,4]' WHERE id = '[1,1]';
+-- move a row from the second partition to the first
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[1,1]' WHERE id = '[4,4]';
+-- should fail:
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-04-01') WHERE id = '[1,1]';
+ERROR: conflicting key value violates exclusion constraint "tfkp1_pkey"
+DETAIL: Key (id, valid_at)=([1,2), [01-01-2000,04-01-2000)) conflicts with existing key (id, valid_at)=([1,2), [01-01-2000,04-01-2000)).
+-- partitioned FK parent updates NO ACTION
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2016-01-01', '2016-02-01'));
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,5]';
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+ WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
+ WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
+ERROR: update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+DETAIL: Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
+-- clean up:
+DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]';
+-- partitioned FK parent deletes NO ACTION
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
+ERROR: update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+DETAIL: Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
+-- clean up:
+DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]';
+-- partitioned FK parent updates RESTRICT
+ALTER TABLE temporal_partitioned_fk_rng2rng
+ DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk;
+ALTER TABLE temporal_partitioned_fk_rng2rng
+ ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_partitioned_rng
+ ON DELETE RESTRICT;
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2016-01-01', '2016-02-01'));
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,5]';
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+ WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
+ WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
+ERROR: update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+DETAIL: Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
+-- clean up:
+DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]';
+-- partitioned FK parent deletes RESTRICT
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
+ERROR: update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+DETAIL: Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
+-- partitioned FK parent updates CASCADE
+ALTER TABLE temporal_partitioned_fk_rng2rng
+ DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_partitioned_rng
+ ON DELETE CASCADE ON UPDATE CASCADE;
+ERROR: action not supported for temporal foreign keys
+-- partitioned FK parent deletes CASCADE
+-- partitioned FK parent updates SET NULL
+ALTER TABLE temporal_partitioned_fk_rng2rng
+ DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_partitioned_rng
+ ON DELETE SET NULL ON UPDATE SET NULL;
+ERROR: action not supported for temporal foreign keys
+-- partitioned FK parent deletes SET NULL
+-- partitioned FK parent updates SET DEFAULT
+ALTER TABLE temporal_partitioned_fk_rng2rng
+ ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+ DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_partitioned_rng
+ ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+ERROR: action not supported for temporal foreign keys
+-- partitioned FK parent deletes SET DEFAULT
+DROP TABLE temporal_partitioned_fk_rng2rng;
+DROP TABLE temporal_partitioned_rng;
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 3c3236618c..3248390744 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -260,3 +260,434 @@ SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
SELECT * FROM tp1 ORDER BY id, valid_at;
SELECT * FROM tp2 ORDER BY id, valid_at;
DROP TABLE temporal_partitioned;
+
+--
+-- test FK dependencies
+--
+
+-- can't drop a range referenced by an FK, unless with CASCADE
+CREATE TABLE temporal3 (
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal3 (id, PERIOD valid_at)
+);
+ALTER TABLE temporal3 DROP COLUMN valid_at;
+ALTER TABLE temporal3 DROP COLUMN valid_at CASCADE;
+DROP TABLE temporal_fk_rng2rng;
+DROP TABLE temporal3;
+
+--
+-- test FOREIGN KEY, range references range
+--
+
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at int4range,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk2 FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+DROP TABLE temporal_fk_rng2rng;
+
+-- with inferred PK on the referenced table:
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+);
+DROP TABLE temporal_fk_rng2rng;
+
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+ REFERENCES temporal_rng (id, PERIOD id)
+);
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+CREATE TABLE temporal_fk_rng2rng (
+ id int4range,
+ valid_at tsrange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng (id, PERIOD valid_at);
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk2
+ FOREIGN KEY (parent_id, PERIOD parent_id)
+ REFERENCES temporal_rng (id, PERIOD id);
+
+--
+-- test with rows already
+--
+
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]');
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+-- okay again:
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2rng_fk';
+
+--
+-- test FK child inserts
+--
+
+INSERT INTO temporal_fk_rng2rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+-- now it should work:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+
+--
+-- test FK child updates
+--
+
+UPDATE temporal_fk_rng2rng SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_rng2rng SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]';
+UPDATE temporal_fk_rng2rng SET parent_id = '[8,8]' WHERE id = '[1,1]';
+
+--
+-- test FK parent updates NO ACTION
+--
+
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_rng SET valid_at = tsrange('2016-02-01', '2016-03-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+
+--
+-- test FK parent updates RESTRICT
+--
+
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_rng SET valid_at = tsrange('2016-02-01', '2016-03-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+--
+-- test FK parent deletes NO ACTION
+--
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+
+--
+-- test FK parent deletes RESTRICT
+--
+
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE RESTRICT;
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+
+--
+-- test ON UPDATE/DELETE options
+--
+
+-- test FK parent updates CASCADE
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- test FK parent updates SET NULL
+INSERT INTO temporal_rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]');
+ALTER TABLE temporal_fk_rng2rng
+ DROP CONSTRAINT temporal_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE SET NULL ON UPDATE SET NULL;
+
+-- test FK parent updates SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[-1,-1]', tsrange(null, null));
+INSERT INTO temporal_rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]');
+ALTER TABLE temporal_fk_rng2rng
+ ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+ DROP CONSTRAINT temporal_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng
+ ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+
+-- FK between partitioned tables
+
+CREATE TABLE temporal_partitioned_rng (
+ id int4range,
+ valid_at daterange,
+ name text,
+ CONSTRAINT temporal_paritioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE tp1 partition OF temporal_partitioned_rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
+CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+INSERT INTO temporal_partitioned_rng VALUES
+ ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
+ ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
+ ('[2,2]', daterange('2000-01-01', '2010-01-01'), 'two');
+
+CREATE TABLE temporal_partitioned_fk_rng2rng (
+ id int4range,
+ valid_at daterange,
+ parent_id int4range,
+ CONSTRAINT temporal_partitioned_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_partitioned_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_partitioned_rng (id, PERIOD valid_at)
+) PARTITION BY LIST (id);
+CREATE TABLE tfkp1 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
+CREATE TABLE tfkp2 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+
+-- partitioned FK child inserts
+
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES
+ ('[1,1]', daterange('2000-01-01', '2000-02-15'), '[1,1]'),
+ ('[1,1]', daterange('2001-01-01', '2002-01-01'), '[2,2]'),
+ ('[2,2]', daterange('2000-01-01', '2000-02-15'), '[1,1]');
+-- should fail:
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES
+ ('[3,3]', daterange('2010-01-01', '2010-02-15'), '[1,1]');
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES
+ ('[3,3]', daterange('2000-01-01', '2000-02-15'), '[3,3]');
+
+-- partitioned FK child updates
+
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-02-13') WHERE id = '[2,2]';
+-- move a row from the first partition to the second
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[4,4]' WHERE id = '[1,1]';
+-- move a row from the second partition to the first
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[1,1]' WHERE id = '[4,4]';
+-- should fail:
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-04-01') WHERE id = '[1,1]';
+
+-- partitioned FK parent updates NO ACTION
+
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2016-01-01', '2016-02-01'));
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,5]';
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+ WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
+ WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]';
+
+-- partitioned FK parent deletes NO ACTION
+
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]';
+
+-- partitioned FK parent updates RESTRICT
+
+ALTER TABLE temporal_partitioned_fk_rng2rng
+ DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk;
+ALTER TABLE temporal_partitioned_fk_rng2rng
+ ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_partitioned_rng
+ ON DELETE RESTRICT;
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2016-01-01', '2016-02-01'));
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,5]';
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+ WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
+ WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]';
+
+-- partitioned FK parent deletes RESTRICT
+
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
+
+-- partitioned FK parent updates CASCADE
+
+ALTER TABLE temporal_partitioned_fk_rng2rng
+ DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_partitioned_rng
+ ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- partitioned FK parent deletes CASCADE
+
+-- partitioned FK parent updates SET NULL
+
+ALTER TABLE temporal_partitioned_fk_rng2rng
+ DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_partitioned_rng
+ ON DELETE SET NULL ON UPDATE SET NULL;
+
+-- partitioned FK parent deletes SET NULL
+
+-- partitioned FK parent updates SET DEFAULT
+
+ALTER TABLE temporal_partitioned_fk_rng2rng
+ ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+ DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
+ ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
+ FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_partitioned_rng
+ ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+
+-- partitioned FK parent deletes SET DEFAULT
+
+DROP TABLE temporal_partitioned_fk_rng2rng;
+DROP TABLE temporal_partitioned_rng;
--
2.25.1
[text/x-patch] v13-0003-Add-UPDATE-DELETE-FOR-PORTION-OF.patch (117.3K, ../../[email protected]/4-v13-0003-Add-UPDATE-DELETE-FOR-PORTION-OF.patch)
download | inline diff:
From 7b1fb4db3291b12b48ad4321ce00f8a89d035399 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Fri, 25 Jun 2021 18:54:35 -0700
Subject: [PATCH v13 3/5] Add UPDATE/DELETE FOR PORTION OF
- Added bison support for FOR PORTION OF syntax. The bounds must be
constant, so we forbid column references, subqueries, etc. But we
permit the magic UNBOUNDED keyword as the FROM or the TO (or both)
to perform an unbounded update/delete. We also accept functions like
NOW().
- Added logic to executor to insert new rows for the "leftover" part of
a record touched by a FOR PORTION OF query.
- Added tg_temporal descriptor to the TriggerData struct that we pass to
trigger functions. Our triggers use this to learn what bounds were
given in the FOR PORTION OF clause.
- Documented FOR PORTION OF.
- Documented tg_temporal struct.
- Added tests.
---
doc/src/sgml/ref/delete.sgml | 46 ++
doc/src/sgml/ref/update.sgml | 47 ++
doc/src/sgml/trigger.sgml | 60 ++-
src/backend/commands/tablecmds.c | 1 +
src/backend/commands/trigger.c | 49 ++
src/backend/executor/execMain.c | 1 +
src/backend/executor/nodeModifyTable.c | 222 ++++++++-
src/backend/nodes/nodeFuncs.c | 24 +
src/backend/optimizer/plan/createplan.c | 8 +-
src/backend/optimizer/plan/planner.c | 1 +
src/backend/optimizer/util/pathnode.c | 4 +-
src/backend/parser/analyze.c | 214 ++++++++-
src/backend/parser/gram.y | 47 +-
src/backend/parser/parse_agg.c | 10 +
src/backend/parser/parse_collate.c | 1 +
src/backend/parser/parse_expr.c | 8 +
src/backend/parser/parse_func.c | 3 +
src/backend/parser/parse_merge.c | 2 +-
src/backend/rewrite/rewriteHandler.c | 40 ++
src/backend/tcop/utility.c | 2 +-
src/backend/utils/adt/Makefile | 1 +
src/backend/utils/adt/period.c | 56 +++
src/backend/utils/adt/rangetypes.c | 42 ++
src/backend/utils/cache/lsyscache.c | 52 +++
src/include/commands/trigger.h | 1 +
src/include/nodes/execnodes.h | 27 ++
src/include/nodes/parsenodes.h | 44 +-
src/include/nodes/pathnodes.h | 1 +
src/include/nodes/plannodes.h | 1 +
src/include/nodes/primnodes.h | 25 +
src/include/optimizer/pathnode.h | 3 +-
src/include/parser/analyze.h | 3 +-
src/include/parser/kwlist.h | 1 +
src/include/parser/parse_node.h | 1 +
src/include/utils/lsyscache.h | 2 +
src/include/utils/period.h | 19 +
src/include/utils/rangetypes.h | 3 +
src/test/regress/expected/for_portion_of.out | 431 ++++++++++++++++++
src/test/regress/expected/privileges.out | 18 +
src/test/regress/expected/updatable_views.out | 32 ++
.../regress/expected/without_overlaps.out | 90 ++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/for_portion_of.sql | 340 ++++++++++++++
src/test/regress/sql/privileges.sql | 18 +
src/test/regress/sql/updatable_views.sql | 14 +
src/test/regress/sql/without_overlaps.sql | 46 ++
46 files changed, 2017 insertions(+), 46 deletions(-)
create mode 100644 src/backend/utils/adt/period.c
create mode 100644 src/include/utils/period.h
create mode 100644 src/test/regress/expected/for_portion_of.out
create mode 100644 src/test/regress/sql/for_portion_of.sql
diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml
index 1b81b4e7d7..c0903ec361 100644
--- a/doc/src/sgml/ref/delete.sgml
+++ b/doc/src/sgml/ref/delete.sgml
@@ -23,6 +23,7 @@ PostgreSQL documentation
<synopsis>
[ WITH [ RECURSIVE ] <replaceable class="parameter">with_query</replaceable> [, ...] ]
DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">alias</replaceable> ]
+ [ FOR PORTION OF <replaceable class="parameter">range_or_period_name</replaceable> FROM <replaceable class="parameter">start_time</replaceable> TO <replaceable class="parameter">end_time</replaceable> ]
[ USING <replaceable class="parameter">from_item</replaceable> [, ...] ]
[ WHERE <replaceable class="parameter">condition</replaceable> | WHERE CURRENT OF <replaceable class="parameter">cursor_name</replaceable> ]
[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
@@ -54,6 +55,15 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
circumstances.
</para>
+ <para>
+ If the table has a <link linkend="ddl-periods-application-periods">range column
+ or <literal>PERIOD</literal></link>, you may supply a
+ <literal>FOR PORTION OF</literal> clause, and your delete will only affect rows
+ that overlap the given interval. Furthermore, if a row's span extends outside
+ the <literal>FOR PORTION OF</literal> bounds, then new rows spanning the "cut
+ off" duration will be inserted to preserve the old values.
+ </para>
+
<para>
The optional <literal>RETURNING</literal> clause causes <command>DELETE</command>
to compute and return value(s) based on each row actually deleted.
@@ -116,6 +126,42 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="parameter">range_or_period_name</replaceable></term>
+ <listitem>
+ <para>
+ The range column or period to use when performing a temporal delete. This
+ must match the range or period used in the table's temporal primary key.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">start_time</replaceable></term>
+ <listitem>
+ <para>
+ The earliest time (inclusive) to change in a temporal delete.
+ This must be a value matching the base type of the range or period from
+ <replaceable class="parameter">range_or_period_name</replaceable>. It may also
+ be the special value <literal>MINVALUE</literal> to indicate a delete whose
+ beginning is unbounded.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">end_time</replaceable></term>
+ <listitem>
+ <para>
+ The latest time (exclusive) to change in a temporal delete.
+ This must be a value matching the base type of the range or period from
+ <replaceable class="parameter">range_or_period_name</replaceable>. It may also
+ be the special value <literal>MAXVALUE</literal> to indicate a delete whose
+ end is unbounded.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><replaceable class="parameter">from_item</replaceable></term>
<listitem>
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 2ab24b0523..069cb3ca5c 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -23,6 +23,7 @@ PostgreSQL documentation
<synopsis>
[ WITH [ RECURSIVE ] <replaceable class="parameter">with_query</replaceable> [, ...] ]
UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">alias</replaceable> ]
+ [ FOR PORTION OF <replaceable class="parameter">range_or_period_name</replaceable> FROM <replaceable class="parameter">start_time</replaceable> TO <replaceable class="parameter">end_time</replaceable> ]
SET { <replaceable class="parameter">column_name</replaceable> = { <replaceable class="parameter">expression</replaceable> | DEFAULT } |
( <replaceable class="parameter">column_name</replaceable> [, ...] ) = [ ROW ] ( { <replaceable class="parameter">expression</replaceable> | DEFAULT } [, ...] ) |
( <replaceable class="parameter">column_name</replaceable> [, ...] ) = ( <replaceable class="parameter">sub-SELECT</replaceable> )
@@ -51,6 +52,16 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
circumstances.
</para>
+ <para>
+ If the table has a <link linkend="ddl-periods-application-periods">range column
+ or <literal>PERIOD</literal></link>, you may supply a
+ <literal>FOR PORTION OF</literal> clause, and your update will only affect rows
+ that overlap the given interval. Furthermore, if a row's span extends outside
+ the <literal>FOR PORTION OF</literal> bounds, then it will be truncated to fit
+ within the bounds, and new rows spanning the "cut off" duration will be
+ inserted to preserve the old values.
+ </para>
+
<para>
The optional <literal>RETURNING</literal> clause causes <command>UPDATE</command>
to compute and return value(s) based on each row actually updated.
@@ -114,6 +125,42 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="parameter">range_or_period_name</replaceable></term>
+ <listitem>
+ <para>
+ The range column or period to use when performing a temporal update. This
+ must match the range or period used in the table's temporal primary key.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">start_time</replaceable></term>
+ <listitem>
+ <para>
+ The earliest time (inclusive) to change in a temporal update.
+ This must be a value matching the base type of the range or period from
+ <replaceable class="parameter">range_or_period_name</replaceable>. It may also
+ be the special value <literal>MINVALUE</literal> to indicate an update whose
+ beginning is unbounded.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">end_time</replaceable></term>
+ <listitem>
+ <para>
+ The latest time (exclusive) to change in a temporal update.
+ This must be a value matching the base type of the range or period from
+ <replaceable class="parameter">range_or_period_name</replaceable>. It may also
+ be the special value <literal>MAXVALUE</literal> to indicate an update whose
+ end is unbounded.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><replaceable class="parameter">column_name</replaceable></term>
<listitem>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index 6e1f370b21..b58e6348ad 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -537,17 +537,18 @@ CALLED_AS_TRIGGER(fcinfo)
<programlisting>
typedef struct TriggerData
{
- NodeTag type;
- TriggerEvent tg_event;
- Relation tg_relation;
- HeapTuple tg_trigtuple;
- HeapTuple tg_newtuple;
- Trigger *tg_trigger;
- TupleTableSlot *tg_trigslot;
- TupleTableSlot *tg_newslot;
- Tuplestorestate *tg_oldtable;
- Tuplestorestate *tg_newtable;
- const Bitmapset *tg_updatedcols;
+ NodeTag type;
+ TriggerEvent tg_event;
+ Relation tg_relation;
+ HeapTuple tg_trigtuple;
+ HeapTuple tg_newtuple;
+ Trigger *tg_trigger;
+ TupleTableSlot *tg_trigslot;
+ TupleTableSlot *tg_newslot;
+ Tuplestorestate *tg_oldtable;
+ Tuplestorestate *tg_newtable;
+ const Bitmapset *tg_updatedcols;
+ ForPortionOfState *tg_temporal;
} TriggerData;
</programlisting>
@@ -815,6 +816,43 @@ typedef struct Trigger
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><structfield>tg_temporal</structfield></term>
+ <listitem>
+ <para>
+ Set for <literal>UPDATE</literal> and <literal>DELETE</literal> queries
+ that use <literal>FOR PORTION OF</literal>, otherwise <symbol>NULL</symbol>.
+ Contains a pointer to a structure of type
+ <structname>ForPortionOfState</structname>, defined in
+ <filename>nodes/execnodes.h</filename>:
+
+<programlisting>
+typedef struct ForPortionOfState
+{
+ NodeTag type;
+
+ char *fp_rangeName; /* the column/PERIOD named in FOR PORTION OF */
+ Oid fp_rangeType; /* the type of the FOR PORTION OF expression */
+ bool fp_hasPeriod; /* true iff this is a PERIOD not a range */
+ char *fp_periodStartName; /* the PERIOD's start column */
+ char *fp_periodEndName; /* the PERIOD's end column */
+ Datum fp_targetRange; /* the range from FOR PORTION OF */
+} ForPortionOfState;
+</programlisting>
+
+ where <structfield>fp_rangeName</structfield> is the period or range
+ column named in the <literal>FOR PORTION OF</literal> clause,
+ <structfield>fp_rangeType</structfield> is its range type,
+ <structfield>fp_hasPeriod</structfield> indicates whether a period was used
+ or a range column, <structfield>fp_periodStartName</structfield> and
+ <structfield>fp_periodEndName</structfield> are the names of the period's
+ start and end columns (or <symbol>NULL</symbol> if a range column was used),
+ and <structfield>fp_targetRange</structfield> is a rangetype value created
+ by evaluating the <literal>FOR PORTION OF</literal> bounds.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5f4a8aa809..af92e78ac9 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12157,6 +12157,7 @@ validateForeignKeyConstraint(char *conname,
trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL);
trigdata.tg_trigslot = slot;
trigdata.tg_trigger = &trig;
+ trigdata.tg_temporal = NULL;
fcinfo->context = (Node *) &trigdata;
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 0577b60415..2f05557b34 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -54,6 +54,7 @@
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/bytea.h"
+#include "utils/datum.h"
#include "utils/fmgroids.h"
#include "utils/guc_hooks.h"
#include "utils/inval.h"
@@ -2638,6 +2639,7 @@ ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
LocTriggerData.tg_event = TRIGGER_EVENT_DELETE |
TRIGGER_EVENT_BEFORE;
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -2737,6 +2739,7 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
TRIGGER_EVENT_ROW |
TRIGGER_EVENT_BEFORE;
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;
for (i = 0; i < trigdesc->numtriggers; i++)
{
HeapTuple newtuple;
@@ -2828,6 +2831,7 @@ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TRIGGER_EVENT_ROW |
TRIGGER_EVENT_INSTEAD;
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;
ExecForceStoreHeapTuple(trigtuple, slot, false);
@@ -2891,6 +2895,7 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
TRIGGER_EVENT_BEFORE;
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
LocTriggerData.tg_updatedcols = updatedCols;
+ LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -3008,6 +3013,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
TRIGGER_EVENT_ROW |
TRIGGER_EVENT_BEFORE;
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;
updatedCols = ExecGetAllUpdatedCols(relinfo, estate);
LocTriggerData.tg_updatedcols = updatedCols;
for (i = 0; i < trigdesc->numtriggers; i++)
@@ -3157,6 +3163,7 @@ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
TRIGGER_EVENT_ROW |
TRIGGER_EVENT_INSTEAD;
LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+ LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;
ExecForceStoreHeapTuple(trigtuple, oldslot, false);
@@ -3623,6 +3630,7 @@ typedef struct AfterTriggerSharedData
Oid ats_tgoid; /* the trigger's ID */
Oid ats_relid; /* the relation it's on */
CommandId ats_firing_id; /* ID for firing cycle */
+ ForPortionOfState *for_portion_of; /* the FOR PORTION OF clause */
struct AfterTriggersTableData *ats_table; /* transition table access */
Bitmapset *ats_modifiedcols; /* modified columns */
} AfterTriggerSharedData;
@@ -3896,6 +3904,7 @@ static SetConstraintState SetConstraintStateCreate(int numalloc);
static SetConstraintState SetConstraintStateCopy(SetConstraintState origstate);
static SetConstraintState SetConstraintStateAddItem(SetConstraintState state,
Oid tgoid, bool tgisdeferred);
+static ForPortionOfState *CopyForPortionOfState(ForPortionOfState *src);
static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);
@@ -4105,6 +4114,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events,
newshared->ats_relid == evtshared->ats_relid &&
newshared->ats_event == evtshared->ats_event &&
newshared->ats_table == evtshared->ats_table &&
+ newshared->for_portion_of == evtshared->for_portion_of &&
newshared->ats_firing_id == 0)
break;
}
@@ -4469,6 +4479,7 @@ AfterTriggerExecute(EState *estate,
LocTriggerData.tg_relation = rel;
if (TRIGGER_FOR_UPDATE(LocTriggerData.tg_trigger->tgtype))
LocTriggerData.tg_updatedcols = evtshared->ats_modifiedcols;
+ LocTriggerData.tg_temporal = evtshared->for_portion_of;
MemoryContextReset(per_tuple_context);
@@ -6005,6 +6016,43 @@ AfterTriggerPendingOnRel(Oid relid)
return false;
}
+/* ----------
+ * ForPortionOfState()
+ *
+ * Copies a ForPortionOfState into the current memory context.
+ */
+static ForPortionOfState *
+CopyForPortionOfState(ForPortionOfState *src)
+{
+ ForPortionOfState *dst = NULL;
+ if (src) {
+ MemoryContext oldctx;
+ RangeType *r;
+ TypeCacheEntry *typcache;
+
+ /*
+ * Need to lift the FOR PORTION OF details into a higher memory context
+ * because cascading foreign key update/deletes can cause triggers to fire
+ * triggers, and the AfterTriggerEvents will outlive the FPO
+ * details of the original query.
+ */
+ oldctx = MemoryContextSwitchTo(TopTransactionContext);
+ dst = makeNode(ForPortionOfState);
+ dst->fp_rangeName = pstrdup(src->fp_rangeName);
+ dst->fp_rangeType = src->fp_rangeType;
+ dst->fp_hasPeriod = src->fp_hasPeriod;
+ dst->fp_rangeAttno = src->fp_rangeAttno;
+ dst->fp_periodStartAttno = src->fp_periodStartAttno;
+ dst->fp_periodEndAttno = src->fp_periodEndAttno;
+
+ r = DatumGetRangeTypeP(src->fp_targetRange);
+ typcache = lookup_type_cache(RangeTypeGetOid(r), TYPECACHE_RANGE_INFO);
+ dst->fp_targetRange = datumCopy(src->fp_targetRange, typcache->typbyval, typcache->typlen);
+ MemoryContextSwitchTo(oldctx);
+ }
+ return dst;
+}
+
/* ----------
* AfterTriggerSaveEvent()
*
@@ -6420,6 +6468,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
else
new_shared.ats_table = NULL;
new_shared.ats_modifiedcols = afterTriggerCopyBitmap(modifiedCols);
+ new_shared.for_portion_of = CopyForPortionOfState(relinfo->ri_forPortionOf);
afterTriggerAddEvent(&afterTriggers.query_stack[afterTriggers.query_depth].events,
&new_event, &new_shared);
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4c5a7bbf62..69096cbefb 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1274,6 +1274,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
resultRelInfo->ri_projectReturning = NULL;
resultRelInfo->ri_onConflictArbiterIndexes = NIL;
resultRelInfo->ri_onConflict = NULL;
+ resultRelInfo->ri_forPortionOf = NULL;
resultRelInfo->ri_ReturningSlot = NULL;
resultRelInfo->ri_TrigOldSlot = NULL;
resultRelInfo->ri_TrigNewSlot = NULL;
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 5005d8c0d1..92d6611f51 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -61,6 +61,9 @@
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/memutils.h"
+#include "utils/period.h"
+#include "utils/rangetypes.h"
+#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -141,6 +144,10 @@ static bool ExecOnConflictUpdate(ModifyTableContext *context,
TupleTableSlot *excludedSlot,
bool canSetTag,
TupleTableSlot **returning);
+static void ExecForPortionOfLeftovers(ModifyTableContext *context,
+ EState *estate,
+ ResultRelInfo *resultRelInfo,
+ ItemPointer tupleid);
static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
EState *estate,
PartitionTupleRouting *proute,
@@ -1205,6 +1212,138 @@ ExecInsert(ModifyTableContext *context,
return result;
}
+/* ----------------------------------------------------------------
+ * set_leftover_tuple_bounds
+ *
+ * Saves the new bounds into the range attribute
+ * ----------------------------------------------------------------
+ */
+static void set_leftover_tuple_bounds(TupleTableSlot *leftoverTuple,
+ ForPortionOfExpr *forPortionOf,
+ TypeCacheEntry *typcache,
+ RangeType *leftoverRangeType)
+{
+ /* Store the range directly */
+
+ leftoverTuple->tts_values[forPortionOf->rangeVar->varattno - 1] = RangeTypePGetDatum(leftoverRangeType);
+ leftoverTuple->tts_isnull[forPortionOf->rangeVar->varattno - 1] = false;
+}
+
+/* ----------------------------------------------------------------
+ * ExecForPortionOfLeftovers
+ *
+ * Insert tuples for the untouched timestamp of a row in a FOR
+ * PORTION OF UPDATE/DELETE
+ * ----------------------------------------------------------------
+ */
+static void
+ExecForPortionOfLeftovers(ModifyTableContext *context,
+ EState *estate,
+ ResultRelInfo *resultRelInfo,
+ ItemPointer tupleid)
+{
+ ModifyTableState *mtstate = context->mtstate;
+ ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+ ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
+ Datum oldRange;
+ RangeType *oldRangeType;
+ RangeType *targetRangeType;
+ RangeType *leftoverRangeType1;
+ RangeType *leftoverRangeType2;
+ Oid rangeTypeOid = forPortionOf->rangeType;
+ bool isNull = false;
+ TypeCacheEntry *typcache;
+ ForPortionOfState *fpoState = resultRelInfo->ri_forPortionOf;
+ TupleTableSlot *oldtupleSlot = fpoState->fp_Existing;
+ TupleTableSlot *leftoverTuple1 = fpoState->fp_Leftover1;
+ TupleTableSlot *leftoverTuple2 = fpoState->fp_Leftover2;
+
+ /*
+ * Get the range of the old pre-UPDATE/DELETE tuple,
+ * so we can intersect it with the FOR PORTION OF target
+ * and see if there are any "leftovers" to insert.
+ *
+ * We have already locked the tuple in ExecUpdate/ExecDelete
+ * (TODO: if it was *not* concurrently updated, does table_tuple_update lock the tuple itself?
+ * I don't found the code for that yet, and maybe it depends on the AM?)
+ * and it has passed EvalPlanQual.
+ * Make sure we're looking at the most recent version.
+ * Otherwise concurrent updates of the same tuple in READ COMMITTED
+ * could insert conflicting "leftovers".
+ */
+ if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc, tupleid, SnapshotAny, oldtupleSlot))
+ elog(ERROR, "failed to fetch tuple for FOR PORTION OF");
+
+ oldRange = slot_getattr(oldtupleSlot, forPortionOf->rangeVar->varattno, &isNull);
+
+ if (isNull)
+ elog(ERROR, "found a NULL range in a temporal table");
+ oldRangeType = DatumGetRangeTypeP(oldRange);
+
+ /* Get the target range. */
+
+ targetRangeType = DatumGetRangeTypeP(fpoState->fp_targetRange);
+
+ /*
+ * Get the range's type cache entry. This is worth caching for the whole UPDATE
+ * like range functions do.
+ */
+
+ typcache = fpoState->fp_rangetypcache;
+ if (typcache == NULL)
+ {
+ typcache = lookup_type_cache(rangeTypeOid, TYPECACHE_RANGE_INFO);
+ if (typcache->rngelemtype == NULL)
+ elog(ERROR, "type %u is not a range type", rangeTypeOid);
+ fpoState->fp_rangetypcache = typcache;
+ }
+
+ /* Get the ranges to the left/right of the targeted range. */
+
+ range_leftover_internal(typcache, oldRangeType, targetRangeType, &leftoverRangeType1,
+ &leftoverRangeType2);
+
+ /*
+ * Insert a copy of the tuple with the lower leftover range.
+ * Even if the table is partitioned,
+ * our insert won't extend past the current row, so we don't need to re-route.
+ * TODO: Really? What if you update the partition key?
+ */
+
+ if (!RangeIsEmpty(leftoverRangeType1))
+ {
+ // TODO: anything we need to clear here?
+ // Are we in the row context?
+ HeapTuple oldtuple = ExecFetchSlotHeapTuple(oldtupleSlot, false, NULL);
+ ExecForceStoreHeapTuple(oldtuple, leftoverTuple1, false);
+
+ set_leftover_tuple_bounds(leftoverTuple1, forPortionOf, typcache, leftoverRangeType1);
+ ExecMaterializeSlot(leftoverTuple1);
+
+ // TODO: Need to save context->mtstate->mt_transition_capture? (See comment on ExecInsert)
+ ExecInsert(context, resultRelInfo, leftoverTuple1, node->canSetTag, NULL, NULL);
+ }
+
+ /*
+ * Insert a copy of the tuple with the upper leftover range
+ * Even if the table is partitioned,
+ * our insert won't extend past the current row, so we don't need to re-route.
+ * TODO: Really? What if you update the partition key?
+ */
+
+ if (!RangeIsEmpty(leftoverRangeType2))
+ {
+ HeapTuple oldtuple = ExecFetchSlotHeapTuple(oldtupleSlot, false, NULL);
+ ExecForceStoreHeapTuple(oldtuple, leftoverTuple2, false);
+
+ set_leftover_tuple_bounds(leftoverTuple2, forPortionOf, typcache, leftoverRangeType2);
+ ExecMaterializeSlot(leftoverTuple2);
+
+ // TODO: Need to save context->mtstate->mt_transition_capture? (See comment on ExecInsert)
+ ExecInsert(context, resultRelInfo, leftoverTuple2, node->canSetTag, NULL, NULL);
+ }
+}
+
/* ----------------------------------------------------------------
* ExecBatchInsert
*
@@ -1357,7 +1496,8 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
*
* Closing steps of tuple deletion; this invokes AFTER FOR EACH ROW triggers,
* including the UPDATE triggers if the deletion is being done as part of a
- * cross-partition tuple move.
+ * cross-partition tuple move. It also inserts leftovers from a FOR PORTION OF
+ * delete.
*/
static void
ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
@@ -1390,6 +1530,11 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ar_delete_trig_tcs = NULL;
}
+ /* Compute leftovers in FOR PORTION OF */
+ // TODO: Skip this for FDW deletes?
+ if (((ModifyTable *) context->mtstate->ps.plan)->forPortionOf)
+ ExecForPortionOfLeftovers(context, estate, resultRelInfo, tupleid);
+
/* AFTER ROW DELETE Triggers */
ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs, changingPart);
@@ -2133,6 +2278,11 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
NULL, NIL,
(updateCxt->updateIndexes == TU_Summarizing));
+ /* Compute leftovers in FOR PORTION OF */
+ // TODO: Skip this for FDW updates?
+ if (((ModifyTable *) context->mtstate->ps.plan)->forPortionOf)
+ ExecForPortionOfLeftovers(context, context->estate, resultRelInfo, tupleid);
+
/* AFTER ROW UPDATE Triggers */
ExecARUpdateTriggers(context->estate, resultRelInfo,
NULL, NULL,
@@ -3610,6 +3760,7 @@ ExecModifyTable(PlanState *pstate)
* Reset per-tuple memory context used for processing on conflict and
* returning clauses, to free any expression evaluation storage
* allocated in the previous cycle.
+ * TODO: It sounds like FOR PORTION OF might need to do something here too?
*/
if (pstate->ps_ExprContext)
ResetExprContext(pstate->ps_ExprContext);
@@ -4282,6 +4433,75 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
}
}
+ /*
+ * If needed, initialize the target range for FOR PORTION OF.
+ */
+ if (node->forPortionOf)
+ {
+ TupleDesc tupDesc;
+ ForPortionOfExpr *forPortionOf;
+ Datum targetRange;
+ bool isNull;
+ ExprContext *econtext;
+ ExprState *exprState;
+ ForPortionOfState *fpoState;
+
+ resultRelInfo = mtstate->resultRelInfo;
+ tupDesc = resultRelInfo->ri_RelationDesc->rd_att;
+ forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
+
+ /* Eval the FOR PORTION OF target */
+ if (mtstate->ps.ps_ExprContext == NULL)
+ ExecAssignExprContext(estate, &mtstate->ps);
+ econtext = mtstate->ps.ps_ExprContext;
+
+ exprState = ExecPrepareExpr((Expr *) forPortionOf->targetRange, estate);
+ targetRange = ExecEvalExpr(exprState, econtext, &isNull);
+ if (isNull)
+ elog(ERROR, "Got a NULL FOR PORTION OF target range");
+
+ /* Create state for FOR PORTION OF operation */
+
+ fpoState = makeNode(ForPortionOfState);
+ fpoState->fp_rangeName = forPortionOf->range_name;
+ fpoState->fp_rangeType = forPortionOf->rangeType;
+ fpoState->fp_rangeAttno = forPortionOf->rangeVar->varattno;
+ fpoState->fp_targetRange = targetRange;
+
+ /* Initialize slot for the existing tuple */
+
+ fpoState->fp_Existing =
+ table_slot_create(resultRelInfo->ri_RelationDesc,
+ &mtstate->ps.state->es_tupleTable);
+
+ /* Create the tuple slots for INSERTing the leftovers */
+
+ fpoState->fp_Leftover1 =
+ ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc, &TTSOpsVirtual);
+ fpoState->fp_Leftover2 =
+ ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc, &TTSOpsVirtual);
+
+ /*
+ * We must attach the ForPortionOfState to all result rels,
+ * in case of a cross-partition update or triggers firing
+ * on partitions.
+ *
+ * They should all have the same tupledesc, so it's okay
+ * that any one of them can use the tuple table slots there.
+ */
+ for (i = 0; i < nrels; i++)
+ {
+ resultRelInfo = &mtstate->resultRelInfo[i];
+ resultRelInfo->ri_forPortionOf = fpoState;
+ }
+
+ /* Make sure the root relation has the FOR PORTION OF clause too. */
+ if (node->rootRelation > 0)
+ mtstate->rootResultRelInfo->ri_forPortionOf = fpoState;
+
+ /* Don't free the ExprContext here because the result must last for the whole query */
+ }
+
/*
* If we have any secondary relations in an UPDATE or DELETE, they need to
* be treated like non-locked relations in SELECT FOR UPDATE, ie, the
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index c03f4f23e2..62ccbf9d3c 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2442,6 +2442,14 @@ expression_tree_walker_impl(Node *node,
return true;
}
break;
+ case T_ForPortionOfExpr:
+ {
+ ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node;
+
+ if (WALK(forPortionOf->targetRange))
+ return true;
+ }
+ break;
case T_PartitionPruneStepOp:
{
PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node;
@@ -2582,6 +2590,8 @@ query_tree_walker_impl(Query *query,
return true;
if (WALK(query->mergeActionList))
return true;
+ if (WALK(query->forPortionOf))
+ return true;
if (WALK(query->returningList))
return true;
if (WALK(query->jointree))
@@ -3425,6 +3435,19 @@ expression_tree_mutator_impl(Node *node,
return (Node *) newnode;
}
break;
+ case T_ForPortionOfExpr:
+ {
+ ForPortionOfExpr *fpo = (ForPortionOfExpr *) node;
+ ForPortionOfExpr *newnode;
+
+ FLATCOPY(newnode, fpo, ForPortionOfExpr);
+ MUTATE(newnode->rangeVar, fpo->rangeVar, Var *);
+ MUTATE(newnode->targetRange, fpo->targetRange, Node *);
+ MUTATE(newnode->rangeSet, fpo->rangeSet, List *);
+
+ return (Node *) newnode;
+ }
+ break;
case T_PartitionPruneStepOp:
{
PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node;
@@ -3603,6 +3626,7 @@ query_tree_mutator_impl(Query *query,
MUTATE(query->withCheckOptions, query->withCheckOptions, List *);
MUTATE(query->onConflict, query->onConflict, OnConflictExpr *);
MUTATE(query->mergeActionList, query->mergeActionList, List *);
+ MUTATE(query->forPortionOf, query->forPortionOf, ForPortionOfExpr *);
MUTATE(query->returningList, query->returningList, List *);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 34ca6d4ac2..954eb4da83 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -311,7 +311,8 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
List *updateColnosLists,
List *withCheckOptionLists, List *returningLists,
List *rowMarks, OnConflictExpr *onconflict,
- List *mergeActionLists, int epqParam);
+ List *mergeActionLists, ForPortionOfExpr *forPortionOf,
+ int epqParam);
static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
GatherMergePath *best_path);
@@ -2835,6 +2836,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
best_path->rowMarks,
best_path->onconflict,
best_path->mergeActionLists,
+ best_path->forPortionOf,
best_path->epqParam);
copy_generic_path_info(&plan->plan, &best_path->path);
@@ -7005,7 +7007,8 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
List *updateColnosLists,
List *withCheckOptionLists, List *returningLists,
List *rowMarks, OnConflictExpr *onconflict,
- List *mergeActionLists, int epqParam)
+ List *mergeActionLists, ForPortionOfExpr *forPortionOf,
+ int epqParam)
{
ModifyTable *node = makeNode(ModifyTable);
List *fdw_private_list;
@@ -7071,6 +7074,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
node->exclRelTlist = onconflict->exclRelTlist;
}
node->updateColnosLists = updateColnosLists;
+ node->forPortionOf = (Node *) forPortionOf;
node->withCheckOptionLists = withCheckOptionLists;
node->returningLists = returningLists;
node->rowMarks = rowMarks;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 44efb1f4eb..b2d8a67f4c 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1969,6 +1969,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction)
rowMarks,
parse->onConflict,
mergeActionLists,
+ parse->forPortionOf,
assign_special_exec_param(root));
}
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 211ba65389..823d1fbab1 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3670,7 +3670,8 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
List *updateColnosLists,
List *withCheckOptionLists, List *returningLists,
List *rowMarks, OnConflictExpr *onconflict,
- List *mergeActionLists, int epqParam)
+ List *mergeActionLists, ForPortionOfExpr *forPortionOf,
+ int epqParam)
{
ModifyTablePath *pathnode = makeNode(ModifyTablePath);
@@ -3736,6 +3737,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
pathnode->returningLists = returningLists;
pathnode->rowMarks = rowMarks;
pathnode->onconflict = onconflict;
+ pathnode->forPortionOf = forPortionOf;
pathnode->epqParam = epqParam;
pathnode->mergeActionLists = mergeActionLists;
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 7a1dfb6364..c4b6d443cf 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -47,11 +47,13 @@
#include "parser/parse_relation.h"
#include "parser/parse_target.h"
#include "parser/parse_type.h"
+#include "parser/parser.h"
#include "parser/parsetree.h"
#include "rewrite/rewriteManip.h"
#include "utils/backend_status.h"
#include "utils/builtins.h"
#include "utils/guc.h"
+#include "utils/lsyscache.h"
#include "utils/rel.h"
#include "utils/syscache.h"
@@ -60,10 +62,17 @@
post_parse_analyze_hook_type post_parse_analyze_hook = NULL;
static Query *transformOptionalSelectInto(ParseState *pstate, Node *parseTree);
+static Node *addForPortionOfWhereConditions(Query *qry, ForPortionOfClause *forPortionOf,
+ Node *whereClause);
static Query *transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt);
static Query *transformInsertStmt(ParseState *pstate, InsertStmt *stmt);
static OnConflictExpr *transformOnConflictClause(ParseState *pstate,
OnConflictClause *onConflictClause);
+static Node *transformForPortionOfBound(Node *n, bool isLowerBound);
+static ForPortionOfExpr *transformForPortionOfClause(ParseState *pstate,
+ int rtindex,
+ ForPortionOfClause *forPortionOfClause,
+ bool isUpdate);
static int count_rowexpr_columns(ParseState *pstate, Node *expr);
static Query *transformSelectStmt(ParseState *pstate, SelectStmt *stmt);
static Query *transformValuesClause(ParseState *pstate, SelectStmt *stmt);
@@ -479,6 +488,20 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
return result;
}
+static Node *
+addForPortionOfWhereConditions(Query *qry, ForPortionOfClause *forPortionOf, Node *whereClause)
+{
+ if (forPortionOf)
+ {
+ if (whereClause)
+ return (Node *) makeBoolExpr(AND_EXPR, list_make2(qry->forPortionOf->overlapsExpr, whereClause), -1);
+ else
+ return qry->forPortionOf->overlapsExpr;
+ }
+ else
+ return whereClause;
+}
+
/*
* analyze_requires_snapshot
* Returns true if a snapshot must be set before doing parse analysis
@@ -512,6 +535,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
{
Query *qry = makeNode(Query);
ParseNamespaceItem *nsitem;
+ Node *whereClause;
Node *qual;
qry->commandType = CMD_DELETE;
@@ -550,7 +574,11 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
nsitem->p_lateral_only = false;
nsitem->p_lateral_ok = true;
- qual = transformWhereClause(pstate, stmt->whereClause,
+ if (stmt->forPortionOf)
+ qry->forPortionOf = transformForPortionOfClause(pstate, qry->resultRelation, stmt->forPortionOf, false);
+
+ whereClause = addForPortionOfWhereConditions(qry, stmt->forPortionOf, stmt->whereClause);
+ qual = transformWhereClause(pstate, whereClause,
EXPR_KIND_WHERE, "WHERE");
qry->returningList = transformReturningList(pstate, stmt->returningList);
@@ -1173,7 +1201,7 @@ transformOnConflictClause(ParseState *pstate,
* Now transform the UPDATE subexpressions.
*/
onConflictSet =
- transformUpdateTargetList(pstate, onConflictClause->targetList);
+ transformUpdateTargetList(pstate, onConflictClause->targetList, NULL);
onConflictWhere = transformWhereClause(pstate,
onConflictClause->whereClause,
@@ -1203,6 +1231,161 @@ transformOnConflictClause(ParseState *pstate,
return result;
}
+/*
+ * transformForPortionOfBound
+ * transforms UNBOUNDED pseudo-column references to NULL
+ * (which represent "unbounded" in a range type, otherwise returns
+ * its input unchanged.
+ */
+static Node *
+transformForPortionOfBound(Node *n, bool isLowerBound)
+{
+ if (nodeTag(n) == T_ColumnRef)
+ {
+ ColumnRef *cref = (ColumnRef *) n;
+ char *cname = "";
+ A_Const *n2;
+
+ if (list_length(cref->fields) == 1 &&
+ IsA(linitial(cref->fields), String))
+ cname = strVal(linitial(cref->fields));
+
+ if (strcmp("unbounded", cname) != 0)
+ return n;
+
+ n2 = makeNode(A_Const);
+ n2->isnull = true;
+ n2->location = ((ColumnRef *)n)->location;
+
+ return (Node *)n2;
+ }
+ else
+ return n;
+}
+
+/*
+ * transformForPortionOfClause
+ *
+ * Transforms a ForPortionOfClause in an UPDATE/DELETE statement.
+ *
+ * - Look up the range/period requested.
+ * - Build a compatible range value from the FROM and TO expressions.
+ * - Build an "overlaps" expression for filtering.
+ * - For UPDATEs, build an "intersects" expression the rewriter can add
+ * - to the targetList to change the temporal bounds.
+ */
+static ForPortionOfExpr *
+transformForPortionOfClause(ParseState *pstate,
+ int rtindex,
+ ForPortionOfClause *forPortionOf,
+ bool isUpdate)
+{
+ Relation targetrel = pstate->p_target_relation;
+ RTEPermissionInfo *target_perminfo = pstate->p_target_nsitem->p_perminfo;
+ char *range_name = forPortionOf->range_name;
+ char *range_type_namespace = NULL;
+ char *range_type_name = NULL;
+ int range_attno = InvalidAttrNumber;
+ Form_pg_attribute attr;
+ ForPortionOfExpr *result;
+ List *targetList;
+ Node *target_start, *target_end;
+ Var *rangeVar;
+ FuncCall *fc;
+
+ result = makeNode(ForPortionOfExpr);
+
+ /* Look up the FOR PORTION OF name requested. */
+ range_attno = attnameAttNum(targetrel, range_name, false);
+ if (range_attno == InvalidAttrNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column or period \"%s\" of relation \"%s\" does not exist",
+ range_name,
+ RelationGetRelationName(targetrel)),
+ parser_errposition(pstate, forPortionOf->range_name_location)));
+ attr = TupleDescAttr(targetrel->rd_att, range_attno - 1);
+ // TODO: check attr->attisdropped (?),
+ // and figure out concurrency issues with that in general.
+ // It should work the same as updating any other column.
+
+ /* Make sure it's a range column */
+ if (!type_is_range(attr->atttypid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+ errmsg("column \"%s\" of relation \"%s\" is not a range type",
+ range_name,
+ RelationGetRelationName(targetrel)),
+ parser_errposition(pstate, forPortionOf->range_name_location)));
+
+ rangeVar = makeVar(
+ rtindex,
+ range_attno,
+ attr->atttypid,
+ attr->atttypmod,
+ attr->attcollation,
+ 0);
+ rangeVar->location = forPortionOf->range_name_location;
+ result->rangeVar = rangeVar;
+ result->rangeType = attr->atttypid;
+ if (!get_typname_and_namespace(attr->atttypid, &range_type_name, &range_type_namespace))
+ elog(ERROR, "missing range type %d", attr->atttypid);
+
+
+ /*
+ * Build a range from the FROM ... TO .... bounds.
+ * This should give a constant result, so we accept functions like NOW()
+ * but not column references, subqueries, etc.
+ *
+ * It also permits UNBOUNDED in either place.
+ */
+ target_start = transformForPortionOfBound(forPortionOf->target_start, true);
+ target_end = transformForPortionOfBound(forPortionOf->target_end, false);
+ fc = makeFuncCall(
+ list_make2(makeString(range_type_namespace), makeString(range_type_name)),
+ list_make2(target_start, target_end),
+ COERCE_EXPLICIT_CALL,
+ forPortionOf->range_name_location);
+ result->targetRange = transformExpr(pstate, (Node *) fc, EXPR_KIND_UPDATE_PORTION);
+
+ /* overlapsExpr is something we can add to the whereClause */
+ result->overlapsExpr = (Node *) makeSimpleA_Expr(AEXPR_OP, "&&",
+ (Node *) copyObject(rangeVar), (Node *) fc,
+ forPortionOf->range_name_location);
+
+ if (isUpdate)
+ {
+ /*
+ * Now make sure we update the start/end time of the record.
+ * For a range col (r) this is `r = r * targetRange`.
+ */
+ Expr *rangeSetExpr = (Expr *) makeSimpleA_Expr(AEXPR_OP, "*",
+ (Node *) copyObject(rangeVar), (Node *) fc,
+ forPortionOf->range_name_location);
+ TargetEntry *tle;
+
+ targetList = NIL;
+ rangeSetExpr = (Expr *) transformExpr(pstate, (Node *) rangeSetExpr, EXPR_KIND_UPDATE_PORTION);
+ tle = makeTargetEntry(rangeSetExpr,
+ range_attno,
+ range_name,
+ false);
+
+ targetList = lappend(targetList, tle);
+
+ /* Mark the range column as requiring update permissions */
+ target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
+ range_attno - FirstLowInvalidHeapAttributeNumber);
+
+ result->rangeSet = targetList;
+ }
+ else
+ result->rangeSet = NIL;
+
+ result->range_name = range_name;
+
+ return result;
+}
/*
* BuildOnConflictExcludedTargetlist
@@ -2409,6 +2592,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
{
Query *qry = makeNode(Query);
ParseNamespaceItem *nsitem;
+ Node *whereClause;
Node *qual;
qry->commandType = CMD_UPDATE;
@@ -2426,6 +2610,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
stmt->relation->inh,
true,
ACL_UPDATE);
+
+ if (stmt->forPortionOf)
+ qry->forPortionOf = transformForPortionOfClause(pstate, qry->resultRelation, stmt->forPortionOf, true);
+
nsitem = pstate->p_target_nsitem;
/* subqueries in FROM cannot access the result relation */
@@ -2442,7 +2630,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
nsitem->p_lateral_only = false;
nsitem->p_lateral_ok = true;
- qual = transformWhereClause(pstate, stmt->whereClause,
+ whereClause = addForPortionOfWhereConditions(qry, stmt->forPortionOf, stmt->whereClause);
+ qual = transformWhereClause(pstate, whereClause,
EXPR_KIND_WHERE, "WHERE");
qry->returningList = transformReturningList(pstate, stmt->returningList);
@@ -2451,7 +2640,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
* Now we are done with SELECT-like processing, and can get on with
* transforming the target list to match the UPDATE target columns.
*/
- qry->targetList = transformUpdateTargetList(pstate, stmt->targetList);
+ qry->targetList = transformUpdateTargetList(pstate, stmt->targetList, qry->forPortionOf);
qry->rtable = pstate->p_rtable;
qry->rteperminfos = pstate->p_rteperminfos;
@@ -2470,7 +2659,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
* handle SET clause in UPDATE/MERGE/INSERT ... ON CONFLICT UPDATE
*/
List *
-transformUpdateTargetList(ParseState *pstate, List *origTlist)
+transformUpdateTargetList(ParseState *pstate, List *origTlist, ForPortionOfExpr *forPortionOf)
{
List *tlist = NIL;
RTEPermissionInfo *target_perminfo;
@@ -2520,6 +2709,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
RelationGetRelationName(pstate->p_target_relation)),
parser_errposition(pstate, origTarget->location)));
+ /*
+ * If this is a FOR PORTION OF update,
+ * forbid directly setting the range column,
+ * since that would conflict with the implicit updates.
+ */
+ if (forPortionOf != NULL)
+ {
+ if (attrno == forPortionOf->rangeVar->varattno)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("can't directly assign to \"%s\" in a FOR PORTION OF update",
+ origTarget->name),
+ parser_errposition(pstate, origTarget->location)));
+ }
+
updateTargetListEntry(pstate, tle, origTarget->name,
attrno,
origTarget->indirection,
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0544ffb63d..492f30ff69 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -258,6 +258,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
RangeVar *range;
IntoClause *into;
WithClause *with;
+ ForPortionOfClause *forportionof;
InferClause *infer;
OnConflictClause *onconflict;
A_Indices *aind;
@@ -551,6 +552,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <range> relation_expr
%type <range> extended_relation_expr
%type <range> relation_expr_opt_alias
+%type <forportionof> for_portion_of_clause
%type <node> tablesample_clause opt_repeatable_clause
%type <target> target_el set_target insert_column_item
@@ -747,7 +749,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD
PERIOD PLACING PLANS POLICY
- POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
+ PORTION POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
QUOTE
@@ -828,6 +830,16 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%nonassoc KEYS OBJECT_P SCALAR VALUE_P
%nonassoc WITH WITHOUT
+/*
+ * We need to handle this shift/reduce conflict:
+ * FOR PORTION OF valid_at FROM INTERVAL YEAR TO MONTH TO foo.
+ * This is basically the classic "dangling else" problem, and we want a
+ * similar resolution: treat the TO as part of the INTERVAL, not as part of
+ * the FROM ... TO .... Users can add parentheses if that's a problem.
+ * TO just needs to be higher precedence than YEAR_P etc.
+ */
+%nonassoc YEAR_P MONTH_P DAY_P HOUR_P MINUTE_P
+%nonassoc TO
/*
* To support target_el without AS, it used to be necessary to assign IDENT an
* explicit precedence just less than Op. While that's not really necessary
@@ -12168,14 +12180,16 @@ returning_clause:
*****************************************************************************/
DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias
+ for_portion_of_clause
using_clause where_or_current_clause returning_clause
{
DeleteStmt *n = makeNode(DeleteStmt);
n->relation = $4;
- n->usingClause = $5;
- n->whereClause = $6;
- n->returningList = $7;
+ n->forPortionOf = $5;
+ n->usingClause = $6;
+ n->whereClause = $7;
+ n->returningList = $8;
n->withClause = $1;
$$ = (Node *) n;
}
@@ -12238,6 +12252,7 @@ opt_nowait_or_skip:
*****************************************************************************/
UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
+ for_portion_of_clause
SET set_clause_list
from_clause
where_or_current_clause
@@ -12246,10 +12261,11 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
UpdateStmt *n = makeNode(UpdateStmt);
n->relation = $3;
- n->targetList = $5;
- n->fromClause = $6;
- n->whereClause = $7;
- n->returningList = $8;
+ n->forPortionOf = $4;
+ n->targetList = $6;
+ n->fromClause = $7;
+ n->whereClause = $8;
+ n->returningList = $9;
n->withClause = $1;
$$ = (Node *) n;
}
@@ -13685,6 +13701,19 @@ relation_expr_opt_alias: relation_expr %prec UMINUS
}
;
+for_portion_of_clause:
+ FOR PORTION OF ColId FROM a_expr TO a_expr
+ {
+ ForPortionOfClause *n = makeNode(ForPortionOfClause);
+ n->range_name = $4;
+ n->range_name_location = @4;
+ n->target_start = $6;
+ n->target_end = $8;
+ $$ = n;
+ }
+ | /*EMPTY*/ { $$ = NULL; }
+ ;
+
/*
* TABLESAMPLE decoration in a FROM item
*/
@@ -17240,6 +17269,7 @@ unreserved_keyword:
| PASSWORD
| PLANS
| POLICY
+ | PORTION
| PRECEDING
| PREPARE
| PREPARED
@@ -17844,6 +17874,7 @@ bare_label_keyword:
| PLACING
| PLANS
| POLICY
+ | PORTION
| POSITION
| PRECEDING
| PREPARE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 85cd47b7ae..ce8e0be045 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -563,6 +563,13 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
case EXPR_KIND_CYCLE_MARK:
errkind = true;
break;
+ case EXPR_KIND_UPDATE_PORTION:
+ if (isAgg)
+ err = _("aggregate functions are not allowed in FOR PORTION OF expressions");
+ else
+ err = _("grouping operations are not allowed in FOR PORTION OF expressions");
+
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -953,6 +960,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_CYCLE_MARK:
errkind = true;
break;
+ case EXPR_KIND_UPDATE_PORTION:
+ err = _("window functions are not allowed in FOR PORTION OF expressions");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c
index 9f6afc351c..d09b2d2b1c 100644
--- a/src/backend/parser/parse_collate.c
+++ b/src/backend/parser/parse_collate.c
@@ -484,6 +484,7 @@ assign_collations_walker(Node *node, assign_collations_context *context)
case T_JoinExpr:
case T_FromExpr:
case T_OnConflictExpr:
+ case T_ForPortionOfExpr:
case T_SortGroupClause:
case T_MergeAction:
(void) expression_tree_walker(node,
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 64c582c344..fee6bd902a 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -566,6 +566,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
case EXPR_KIND_PARTITION_BOUND:
err = _("cannot use column reference in partition bound expression");
break;
+ case EXPR_KIND_UPDATE_PORTION:
+ err = _("cannot use column reference in FOR PORTION OF expression");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -1813,6 +1816,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
case EXPR_KIND_GENERATED_COLUMN:
err = _("cannot use subquery in column generation expression");
break;
+ case EXPR_KIND_UPDATE_PORTION:
+ err = _("cannot use subquery in FOR PORTION OF expression");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -3104,6 +3110,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "UPDATE";
case EXPR_KIND_MERGE_WHEN:
return "MERGE WHEN";
+ case EXPR_KIND_UPDATE_PORTION:
+ return "FOR PORTION OF";
case EXPR_KIND_GROUP_BY:
return "GROUP BY";
case EXPR_KIND_ORDER_BY:
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index b3f0b6a137..31ed01a84e 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2656,6 +2656,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
case EXPR_KIND_CYCLE_MARK:
errkind = true;
break;
+ case EXPR_KIND_UPDATE_PORTION:
+ err = _("set-returning functions are not allowed in FOR PORTION OF expressions");
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c
index 91b1156d99..c00802ae23 100644
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -372,7 +372,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
pstate->p_is_insert = false;
action->targetList =
transformUpdateTargetList(pstate,
- mergeWhenClause->targetList);
+ mergeWhenClause->targetList, NULL);
}
break;
case CMD_DELETE:
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index b486ab559a..576a98e0f4 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3473,6 +3473,30 @@ rewriteTargetView(Query *parsetree, Relation view)
&parsetree->hasSubLinks);
}
+ if (parsetree->forPortionOf && parsetree->commandType == CMD_UPDATE)
+ {
+ /*
+ * Like the INSERT/UPDATE code above, update the resnos in the
+ * auxiliary UPDATE targetlist to refer to columns of the base
+ * relation.
+ */
+ foreach(lc, parsetree->forPortionOf->rangeSet)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ TargetEntry *view_tle;
+
+ if (tle->resjunk)
+ continue;
+
+ view_tle = get_tle_by_resno(view_targetlist, tle->resno);
+ if (view_tle != NULL && !view_tle->resjunk && IsA(view_tle->expr, Var))
+ tle->resno = ((Var *) view_tle->expr)->varattno;
+ else
+ elog(ERROR, "attribute number %d not found in view targetlist",
+ tle->resno);
+ }
+ }
+
/*
* For UPDATE/DELETE, pull up any WHERE quals from the view. We know that
* any Vars in the quals must reference the one base relation, so we need
@@ -3811,6 +3835,22 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
else if (event == CMD_UPDATE)
{
Assert(parsetree->override == OVERRIDING_NOT_SET);
+ /*
+ * Update FOR PORTION OF column(s) automatically. Don't
+ * do this until we're done rewriting a view update, so
+ * that we don't add the same update on the recursion.
+ */
+ if (parsetree->forPortionOf &&
+ rt_entry_relation->rd_rel->relkind != RELKIND_VIEW)
+ {
+ ListCell *tl;
+ foreach(tl, parsetree->forPortionOf->rangeSet)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(tl);
+ parsetree->targetList = lappend(parsetree->targetList, tle);
+ }
+ }
+
parsetree->targetList =
rewriteTargetListIU(parsetree->targetList,
parsetree->commandType,
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index e3ccf6c7f7..6781e55020 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1560,7 +1560,7 @@ ProcessUtilitySlow(ParseState *pstate,
true, /* check_rights */
true, /* check_not_in_use */
false, /* skip_build */
- false); /* quiet */
+ false); /* quiet */
/*
* Add the CREATE INDEX node itself to stash right away;
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 0de0bbb1b8..a4f226ec8d 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -78,6 +78,7 @@ OBJS = \
oracle_compat.o \
orderedsetaggs.o \
partitionfuncs.o \
+ period.o \
pg_locale.o \
pg_lsn.o \
pg_upgrade_support.o \
diff --git a/src/backend/utils/adt/period.c b/src/backend/utils/adt/period.c
new file mode 100644
index 0000000000..0ed4304e16
--- /dev/null
+++ b/src/backend/utils/adt/period.c
@@ -0,0 +1,56 @@
+/*-------------------------------------------------------------------------
+ *
+ * period.c
+ * Functions to support periods.
+ *
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/adt/period.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "executor/tuptable.h"
+#include "fmgr.h"
+#include "nodes/primnodes.h"
+#include "utils/fmgrprotos.h"
+#include "utils/period.h"
+#include "utils/rangetypes.h"
+
+Datum period_to_range(TupleTableSlot *slot, int startattno, int endattno, Oid rangetype)
+{
+ Datum startvalue;
+ Datum endvalue;
+ Datum result;
+ bool startisnull;
+ bool endisnull;
+ LOCAL_FCINFO(fcinfo, 2);
+ FmgrInfo flinfo;
+ FuncExpr *f;
+
+ InitFunctionCallInfoData(*fcinfo, &flinfo, 2, InvalidOid, NULL, NULL);
+ f = makeNode(FuncExpr);
+ f->funcresulttype = rangetype;
+ flinfo.fn_expr = (Node *) f;
+ flinfo.fn_extra = NULL;
+
+ /* compute oldvalue */
+ startvalue = slot_getattr(slot, startattno, &startisnull);
+ endvalue = slot_getattr(slot, endattno, &endisnull);
+
+ fcinfo->args[0].value = startvalue;
+ fcinfo->args[0].isnull = startisnull;
+ fcinfo->args[1].value = endvalue;
+ fcinfo->args[1].isnull = endisnull;
+
+ result = range_constructor2(fcinfo);
+ if (fcinfo->isnull)
+ elog(ERROR, "function %u returned NULL", flinfo.fn_oid);
+
+ return result;
+}
diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index d65e5625c7..d4e1732377 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -1206,6 +1206,48 @@ range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeT
return false;
}
+/*
+ * range_leftover_internal - Sets output1 and output2 to the remaining parts of r1
+ * after subtracting r2, or if nothing is left then to the empty range.
+ * output1 will always be "before" r2 and output2 "after".
+ */
+void
+range_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1,
+ const RangeType *r2, RangeType **output1, RangeType **output2)
+{
+ RangeBound lower1,
+ lower2;
+ RangeBound upper1,
+ upper2;
+ bool empty1,
+ empty2;
+
+ range_deserialize(typcache, r1, &lower1, &upper1, &empty1);
+ range_deserialize(typcache, r2, &lower2, &upper2, &empty2);
+
+ if (range_cmp_bounds(typcache, &lower1, &lower2) < 0)
+ {
+ lower2.inclusive = !lower2.inclusive;
+ lower2.lower = false;
+ *output1 = make_range(typcache, &lower1, &lower2, false, NULL);
+ }
+ else
+ {
+ *output1 = make_empty_range(typcache);
+ }
+
+ if (range_cmp_bounds(typcache, &upper1, &upper2) > 0)
+ {
+ upper2.inclusive = !upper2.inclusive;
+ upper2.lower = true;
+ *output2 = make_range(typcache, &upper2, &upper1, false, NULL);
+ }
+ else
+ {
+ *output2 = make_empty_range(typcache);
+ }
+}
+
/* range -> range aggregate functions */
Datum
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index fc6d267e44..391762f308 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -2170,6 +2170,58 @@ get_typisdefined(Oid typid)
return false;
}
+/*
+ * get_typname
+ *
+ * Returns the name of a given type
+ *
+ * Returns a palloc'd copy of the string, or NULL if no such type.
+ */
+char *
+get_typname(Oid typid)
+{
+ HeapTuple tp;
+
+ tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+ if (HeapTupleIsValid(tp))
+ {
+ Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+ char *result;
+
+ result = pstrdup(NameStr(typtup->typname));
+ ReleaseSysCache(tp);
+ return result;
+ }
+ else
+ return NULL;
+}
+
+/*
+ * get_typname_and_namespace
+ *
+ * Returns the name and namespace of a given type
+ *
+ * Returns true if one found, or false if not.
+ */
+bool
+get_typname_and_namespace(Oid typid, char **typname, char **typnamespace)
+{
+ HeapTuple tp;
+
+ tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+ if (HeapTupleIsValid(tp))
+ {
+ Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+ *typname = pstrdup(NameStr(typtup->typname));
+ *typnamespace = get_namespace_name(typtup->typnamespace);
+ ReleaseSysCache(tp);
+ return *typnamespace;
+ }
+ else
+ return false;
+}
+
/*
* get_typlen
*
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 430e3ca7dd..b3d90deaa7 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -41,6 +41,7 @@ typedef struct TriggerData
Tuplestorestate *tg_oldtable;
Tuplestorestate *tg_newtable;
const Bitmapset *tg_updatedcols;
+ ForPortionOfState *tg_temporal;
} TriggerData;
/*
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 2327b55f15..dcbfd048a0 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -41,12 +41,14 @@
#include "storage/condition_variable.h"
#include "utils/hsearch.h"
#include "utils/queryenvironment.h"
+#include "utils/rangetypes.h"
#include "utils/reltrigger.h"
#include "utils/sharedtuplestore.h"
#include "utils/snapshot.h"
#include "utils/sortsupport.h"
#include "utils/tuplesort.h"
#include "utils/tuplestore.h"
+#include "utils/typcache.h"
struct PlanState; /* forward references in this file */
struct ParallelHashJoinState;
@@ -424,6 +426,28 @@ typedef struct MergeActionState
ExprState *mas_whenqual; /* WHEN [NOT] MATCHED AND conditions */
} MergeActionState;
+/*
+ * ForPortionOfState
+ *
+ * Executor state of a FOR PORTION OF operation.
+ */
+typedef struct ForPortionOfState
+{
+ NodeTag type;
+
+ char *fp_rangeName; /* the column/PERIOD named in FOR PORTION OF */
+ Oid fp_rangeType; /* the type of the FOR PORTION OF expression */
+ bool fp_hasPeriod; /* true iff this is a PERIOD not a range */
+ int fp_rangeAttno; /* the attno of the range column (or 0 for a PERIOD) */
+ int fp_periodStartAttno; /* the attno of the PERIOD start column (or 0 for a range) */
+ int fp_periodEndAttno; /* the attno of the PERIOD end column (or 0 for a range) */
+ Datum fp_targetRange; /* the range from FOR PORTION OF */
+ TypeCacheEntry *fp_rangetypcache; /* type cache entry of the range */
+ TupleTableSlot *fp_Existing; /* slot to store existing target tuple in */
+ TupleTableSlot *fp_Leftover1; /* slot to store leftover below the target range */
+ TupleTableSlot *fp_Leftover2; /* slot to store leftover above the target range */
+} ForPortionOfState;
+
/*
* ResultRelInfo
*
@@ -544,6 +568,9 @@ typedef struct ResultRelInfo
List *ri_matchedMergeAction;
List *ri_notMatchedMergeAction;
+ /* FOR PORTION OF evaluation state */
+ ForPortionOfState *ri_forPortionOf;
+
/* partition check expression state (NULL if not set up yet) */
ExprState *ri_PartitionCheckExpr;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 479b5ffa35..177df96c86 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -148,6 +148,9 @@ typedef struct Query
*/
int resultRelation pg_node_attr(query_jumble_ignore);
+ /* FOR PORTION OF clause for UPDATE/DELETE */
+ ForPortionOfExpr *forPortionOf;
+
/* has aggregates in tlist or havingQual */
bool hasAggs pg_node_attr(query_jumble_ignore);
/* has window functions in tlist */
@@ -1537,6 +1540,19 @@ typedef struct RowMarkClause
bool pushedDown; /* pushed down from higher query level? */
} RowMarkClause;
+/*
+ * ForPortionOfClause
+ * representation of FOR PORTION OF <period-name> FROM <t1> TO <t2>
+ */
+typedef struct ForPortionOfClause
+{
+ NodeTag type;
+ char *range_name;
+ int range_name_location;
+ Node *target_start;
+ Node *target_end;
+} ForPortionOfClause;
+
/*
* WithClause -
* representation of WITH clause
@@ -1917,12 +1933,13 @@ typedef struct InsertStmt
*/
typedef struct DeleteStmt
{
- NodeTag type;
- RangeVar *relation; /* relation to delete from */
- List *usingClause; /* optional using clause for more tables */
- Node *whereClause; /* qualifications */
- List *returningList; /* list of expressions to return */
- WithClause *withClause; /* WITH clause */
+ NodeTag type;
+ RangeVar *relation; /* relation to delete from */
+ ForPortionOfClause *forPortionOf; /* FOR PORTION OF clause */
+ List *usingClause; /* optional using clause for more tables */
+ Node *whereClause; /* qualifications */
+ List *returningList; /* list of expressions to return */
+ WithClause *withClause; /* WITH clause */
} DeleteStmt;
/* ----------------------
@@ -1931,13 +1948,14 @@ typedef struct DeleteStmt
*/
typedef struct UpdateStmt
{
- NodeTag type;
- RangeVar *relation; /* relation to update */
- List *targetList; /* the target list (of ResTarget) */
- Node *whereClause; /* qualifications */
- List *fromClause; /* optional from clause for more tables */
- List *returningList; /* list of expressions to return */
- WithClause *withClause; /* WITH clause */
+ NodeTag type;
+ RangeVar *relation; /* relation to update */
+ ForPortionOfClause *forPortionOf; /* FOR PORTION OF clause */
+ List *targetList; /* the target list (of ResTarget) */
+ Node *whereClause; /* qualifications */
+ List *fromClause; /* optional from clause for more tables */
+ List *returningList; /* list of expressions to return */
+ WithClause *withClause; /* WITH clause */
} UpdateStmt;
/* ----------------------
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 5702fbba60..d2be7f26d1 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2352,6 +2352,7 @@ typedef struct ModifyTablePath
List *returningLists; /* per-target-table RETURNING tlists */
List *rowMarks; /* PlanRowMarks (non-locking only) */
OnConflictExpr *onconflict; /* ON CONFLICT clause, or NULL */
+ ForPortionOfExpr *forPortionOf; /* FOR PORTION OF clause for UPDATE/DELETE */
int epqParam; /* ID of Param for EvalPlanQual re-eval */
List *mergeActionLists; /* per-target-table lists of actions for
* MERGE */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 1b787fe031..2bca7086e2 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -243,6 +243,7 @@ typedef struct ModifyTable
List *rowMarks; /* PlanRowMarks (non-locking only) */
int epqParam; /* ID of Param for EvalPlanQual re-eval */
OnConflictAction onConflictAction; /* ON CONFLICT action */
+ Node *forPortionOf; /* FOR PORTION OF clause for UPDATE/DELETE */
List *arbiterIndexes; /* List of ON CONFLICT arbiter index OIDs */
List *onConflictSet; /* INSERT ON CONFLICT DO UPDATE targetlist */
List *onConflictCols; /* target column numbers for onConflictSet */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 60d72a876b..b3c6f6cecf 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -2041,4 +2041,29 @@ typedef struct OnConflictExpr
List *exclRelTlist; /* tlist of the EXCLUDED pseudo relation */
} OnConflictExpr;
+/*----------
+ * ForPortionOfExpr - represents a FOR PORTION OF ... expression
+ *
+ * We set up an expression to make a range from the FROM/TO bounds,
+ * so that we can use range operators with it.
+ *
+ * Then we set up an overlaps expression between that and the range column,
+ * so that we can find the rows we need to update/delete.
+ *
+ * In the executor we'll also build an intersect expression between the
+ * targeted range and the range column, so that we can update the start/end
+ * bounds of the UPDATE'd record.
+ *----------
+ */
+typedef struct ForPortionOfExpr
+{
+ NodeTag type;
+ Var *rangeVar; /* Range column */
+ char *range_name; /* Range name */
+ Node *targetRange; /* FOR PORTION OF bounds as a range */
+ Oid rangeType; /* type of targetRange */
+ Node *overlapsExpr; /* range && targetRange */
+ List *rangeSet; /* List of TargetEntrys to set the time column(s) */
+} ForPortionOfExpr;
+
#endif /* PRIMNODES_H */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 6e557bebc4..c932d1204c 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -283,7 +283,8 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root,
List *updateColnosLists,
List *withCheckOptionLists, List *returningLists,
List *rowMarks, OnConflictExpr *onconflict,
- List *mergeActionLists, int epqParam);
+ List *mergeActionLists,
+ ForPortionOfExpr *forPortionOf, int epqParam);
extern LimitPath *create_limit_path(PlannerInfo *root, RelOptInfo *rel,
Path *subpath,
Node *limitOffset, Node *limitCount,
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index c96483ae78..b6d5065e12 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -43,7 +43,8 @@ extern List *transformInsertRow(ParseState *pstate, List *exprlist,
List *stmtcols, List *icolumns, List *attrnos,
bool strip_indirection);
extern List *transformUpdateTargetList(ParseState *pstate,
- List *origTlist);
+ List *origTlist,
+ ForPortionOfExpr *forPortionOf);
extern Query *transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree);
extern Query *transformStmt(ParseState *pstate, Node *parseTree);
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index ccf5bf8aa6..c89ceea9f6 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -330,6 +330,7 @@ PG_KEYWORD("period", PERIOD, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("portion", PORTION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("preceding", PRECEDING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("precision", PRECISION, COL_NAME_KEYWORD, AS_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index f589112d5e..0d601f34a5 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -56,6 +56,7 @@ typedef enum ParseExprKind
EXPR_KIND_UPDATE_SOURCE, /* UPDATE assignment source item */
EXPR_KIND_UPDATE_TARGET, /* UPDATE assignment target item */
EXPR_KIND_MERGE_WHEN, /* MERGE WHEN [NOT] MATCHED condition */
+ EXPR_KIND_UPDATE_PORTION, /* UPDATE FOR PORTION OF item */
EXPR_KIND_GROUP_BY, /* GROUP BY */
EXPR_KIND_ORDER_BY, /* ORDER BY */
EXPR_KIND_DISTINCT_ON, /* DISTINCT ON */
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index f5fdbfe116..a117527d8a 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -143,6 +143,8 @@ extern char get_rel_persistence(Oid relid);
extern Oid get_transform_fromsql(Oid typid, Oid langid, List *trftypes);
extern Oid get_transform_tosql(Oid typid, Oid langid, List *trftypes);
extern bool get_typisdefined(Oid typid);
+extern char *get_typname(Oid typid);
+extern bool get_typname_and_namespace(Oid typid, char **typname, char **typnamespace);
extern int16 get_typlen(Oid typid);
extern bool get_typbyval(Oid typid);
extern void get_typlenbyval(Oid typid, int16 *typlen, bool *typbyval);
diff --git a/src/include/utils/period.h b/src/include/utils/period.h
new file mode 100644
index 0000000000..0a8af3edb0
--- /dev/null
+++ b/src/include/utils/period.h
@@ -0,0 +1,19 @@
+/*-------------------------------------------------------------------------
+ *
+ * period.h
+ * support for Postgres periods.
+ *
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/period.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PERIOD_H
+#define PERIOD_H
+
+extern Datum period_to_range(TupleTableSlot *slot, int startattno, int endattno, Oid rangetype);
+
+#endif /* PERIOD_H */
diff --git a/src/include/utils/rangetypes.h b/src/include/utils/rangetypes.h
index 6b420a8618..48ee2debed 100644
--- a/src/include/utils/rangetypes.h
+++ b/src/include/utils/rangetypes.h
@@ -164,5 +164,8 @@ extern RangeType *make_empty_range(TypeCacheEntry *typcache);
extern bool range_split_internal(TypeCacheEntry *typcache, const RangeType *r1,
const RangeType *r2, RangeType **output1,
RangeType **output2);
+extern void range_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1,
+ const RangeType *r2, RangeType **output1,
+ RangeType **output2);
#endif /* RANGETYPES_H */
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
new file mode 100644
index 0000000000..30919b55e5
--- /dev/null
+++ b/src/test/regress/expected/for_portion_of.out
@@ -0,0 +1,431 @@
+-- Tests for UPDATE/DELETE FOR PORTION OF
+-- Works on non-PK columns
+CREATE TABLE for_portion_of_test (
+ id int4range,
+ valid_at tsrange,
+ name text NOT NULL
+);
+INSERT INTO for_portion_of_test VALUES
+('[1,2)', '[2018-01-02,2020-01-01)', 'one');
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+SET name = 'foo';
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2019-01-15' TO UNBOUNDED;
+SELECT * FROM for_portion_of_test;
+ id | valid_at | name
+-------+---------------------------------------------------------+------
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Tue Jan 01 00:00:00 2019") | foo
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | one
+ [1,2) | ["Tue Jan 01 00:00:00 2019","Tue Jan 15 00:00:00 2019") | one
+(3 rows)
+
+-- Works on more than one range
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+ id int4range,
+ valid1_at tsrange,
+ valid2_at tsrange,
+ name text NOT NULL
+);
+INSERT INTO for_portion_of_test VALUES
+('[1,2)', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', 'one');
+UPDATE for_portion_of_test
+FOR PORTION OF valid1_at FROM '2018-01-15' TO UNBOUNDED
+SET name = 'foo';
+SELECT * FROM for_portion_of_test;
+ id | valid1_at | valid2_at | name
+-------+---------------------------------------------------------+---------------------------------------------------------+------
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Feb 03 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Wed Jan 01 00:00:00 2025") | foo
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Wed Jan 01 00:00:00 2025") | one
+(2 rows)
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid2_at FROM '2018-01-15' TO UNBOUNDED
+SET name = 'bar';
+SELECT * FROM for_portion_of_test;
+ id | valid1_at | valid2_at | name
+-------+---------------------------------------------------------+---------------------------------------------------------+------
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Feb 03 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Wed Jan 01 00:00:00 2025") | bar
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Feb 03 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | foo
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Wed Jan 01 00:00:00 2025") | bar
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | one
+(4 rows)
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid1_at FROM '2018-01-20' TO UNBOUNDED;
+SELECT * FROM for_portion_of_test;
+ id | valid1_at | valid2_at | name
+-------+---------------------------------------------------------+---------------------------------------------------------+------
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Wed Jan 01 00:00:00 2025") | bar
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | one
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Wed Jan 01 00:00:00 2025") | bar
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | foo
+(4 rows)
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid2_at FROM '2018-01-20' TO UNBOUNDED;
+SELECT * FROM for_portion_of_test;
+ id | valid1_at | valid2_at | name
+-------+---------------------------------------------------------+---------------------------------------------------------+------
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | one
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | foo
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | bar
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | bar
+(4 rows)
+
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+ id int4range NOT NULL,
+ valid_at tsrange NOT NULL,
+ name text NOT NULL,
+ CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test
+VALUES
+('[1,2)', '[2018-01-02,2018-02-03)', 'one'),
+('[1,2)', '[2018-02-03,2018-03-03)', 'one'),
+('[1,2)', '[2018-03-03,2018-04-04)', 'one'),
+('[2,3)', '[2018-01-01,2018-01-05)', 'two'),
+('[3,4)', '[2018-01-01,)', 'three'),
+('[4,5)', '(,2018-04-01)', 'four'),
+('[5,6)', '(,)', 'five')
+;
+--
+-- UPDATE tests
+--
+-- Setting with a missing column fails
+UPDATE for_portion_of_test
+FOR PORTION OF invalid_at FROM '2018-06-01' TO UNBOUNDED
+SET name = 'foo'
+WHERE id = '[5,6)';
+ERROR: column or period "invalid_at" of relation "for_portion_of_test" does not exist
+LINE 2: FOR PORTION OF invalid_at FROM '2018-06-01' TO UNBOUNDED
+ ^
+-- Setting the range fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO UNBOUNDED
+SET valid_at = '[1990-01-01,1999-01-01)'
+WHERE id = '[5,6)';
+ERROR: can't directly assign to "valid_at" in a FOR PORTION OF update
+LINE 3: SET valid_at = '[1990-01-01,1999-01-01)'
+ ^
+-- The wrong type fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM 1 TO 4
+SET name = 'nope'
+WHERE id = '[3,4)';
+ERROR: function pg_catalog.tsrange(integer, integer) does not exist
+LINE 2: FOR PORTION OF valid_at FROM 1 TO 4
+ ^
+HINT: No function matches the given name and argument types. You might need to add explicit type casts.
+-- Setting with timestamps reversed fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+SET name = 'three^1'
+WHERE id = '[3,4)';
+ERROR: range lower bound must be less than or equal to range upper bound
+-- Setting with a subquery fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01'
+SET name = 'nope'
+WHERE id = '[3,4)';
+ERROR: cannot use subquery in FOR PORTION OF expression
+LINE 2: FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-...
+ ^
+-- Setting with a column fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM lower(valid_at) TO UNBOUNDED
+SET name = 'nope'
+WHERE id = '[3,4)';
+ERROR: cannot use column reference in FOR PORTION OF expression
+LINE 2: FOR PORTION OF valid_at FROM lower(valid_at) TO UNBOUNDED
+ ^
+-- Setting with timestamps equal does nothing
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+SET name = 'three^0'
+WHERE id = '[3,4)';
+-- Updating a finite/open portion with a finite/open target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO UNBOUNDED
+SET name = 'three^1'
+WHERE id = '[3,4)';
+-- Updating a finite/open portion with an open/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM UNBOUNDED TO '2018-03-01'
+SET name = 'three^2'
+WHERE id = '[3,4)';
+-- Updating an open/finite portion with an open/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM UNBOUNDED TO '2018-02-01'
+SET name = 'four^1'
+WHERE id = '[4,5)';
+-- Updating an open/finite portion with a finite/open target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO UNBOUNDED
+SET name = 'four^2'
+WHERE id = '[4,5)';
+-- Updating a finite/finite portion with an exact fit
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01'
+SET name = 'four^3'
+WHERE id = '[4,5)';
+-- Updating an enclosed span
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM UNBOUNDED TO UNBOUNDED
+SET name = 'two^2'
+WHERE id = '[2,3)';
+-- Updating an open/open portion with a finite/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01'
+SET name = 'five^2'
+WHERE id = '[5,6)';
+-- Updating an enclosed span with separate protruding spans
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01'
+SET name = 'five^3'
+WHERE id = '[5,6)';
+-- Updating multiple enclosed spans
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO NULL
+SET name = 'one^2'
+WHERE id = '[1,2)';
+-- Updating with a shift/reduce conflict
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+ FROM '2018-03-01' AT TIME ZONE INTERVAL '1' HOUR TO MINUTE
+ TO '2019-01-01'
+SET name = 'one^3'
+WHERE id = '[1,2)';
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+ FROM '2018-03-01' AT TIME ZONE INTERVAL '2' HOUR
+ TO '2019-01-01'
+SET name = 'one^4'
+WHERE id = '[1,2)';
+ERROR: syntax error at or near "'2019-01-01'"
+LINE 4: TO '2019-01-01'
+ ^
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+ FROM ('2018-03-01' AT TIME ZONE INTERVAL '2' HOUR)
+ TO '2019-01-01'
+SET name = 'one^4'
+WHERE id = '[1,2)';
+-- Updating the non-range part of the PK:
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-02-15' TO NULL
+SET id = '[6,7)'
+WHERE id = '[1,2)';
+-- UPDATE with no WHERE clause
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2030-01-01' TO NULL
+SET name = name || '*';
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+ id | valid_at | name
+-------+---------------------------------------------------------+----------
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Sat Feb 03 00:00:00 2018") | one^2
+ [1,2) | ["Sat Feb 03 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2
+ [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2
+ [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Mar 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | three
+ [3,4) | ["Fri Jun 01 00:00:00 2018","Tue Jan 01 00:00:00 2030") | three^1
+ [3,4) | ["Tue Jan 01 00:00:00 2030",) | three^1*
+ [4,5) | (,"Sun Jan 01 00:00:00 2017") | four^1
+ [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3
+ [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2
+ [5,6) | (,"Sun Jan 01 00:00:00 2017") | five
+ [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | five^3
+ [5,6) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | five^3
+ [5,6) | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | five
+ [5,6) | ["Tue Jan 01 00:00:00 2030",) | five*
+ [6,7) | ["Thu Feb 15 00:00:00 2018","Thu Mar 01 08:01:00 2018") | one^2
+ [6,7) | ["Thu Mar 01 08:01:00 2018","Thu Mar 01 10:00:00 2018") | one^3
+ [6,7) | ["Thu Mar 01 10:00:00 2018","Sat Mar 03 00:00:00 2018") | one^4
+ [6,7) | ["Sat Mar 03 00:00:00 2018","Wed Apr 04 00:00:00 2018") | one^4
+(20 rows)
+
+--
+-- DELETE tests
+--
+-- Deleting with a missing column fails
+DELETE FROM for_portion_of_test
+FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL
+WHERE id = '[5,6)';
+ERROR: column or period "invalid_at" of relation "for_portion_of_test" does not exist
+LINE 2: FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL
+ ^
+-- Deleting with timestamps reversed fails
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+WHERE id = '[3,4)';
+ERROR: range lower bound must be less than or equal to range upper bound
+-- Deleting with timestamps equal does nothing
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+WHERE id = '[3,4)';
+-- Deleting with a closed/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2020-06-01'
+WHERE id = '[5,6)';
+-- Deleting with a closed/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO NULL
+WHERE id = '[3,4)';
+-- Deleting with an open/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO '2018-02-08'
+WHERE id = '[1,2)';
+-- Deleting with an open/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO NULL
+WHERE id = '[6,7)';
+-- DELETE with no WHERE clause
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2025-01-01' TO NULL;
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+ id | valid_at | name
+-------+---------------------------------------------------------+---------
+ [1,2) | ["Thu Feb 08 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2
+ [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2
+ [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Mar 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | three
+ [4,5) | (,"Sun Jan 01 00:00:00 2017") | four^1
+ [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3
+ [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2
+ [5,6) | (,"Sun Jan 01 00:00:00 2017") | five
+ [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025") | five
+(11 rows)
+
+-- UPDATE ... RETURNING returns only the updated values (not the inserted side values)
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15'
+SET name = 'three^3'
+WHERE id = '[3,4)'
+RETURNING *;
+ id | valid_at | name
+-------+---------------------------------------------------------+---------
+ [3,4) | ["Thu Feb 01 00:00:00 2018","Thu Feb 15 00:00:00 2018") | three^3
+(1 row)
+
+-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows
+CREATE FUNCTION for_portion_of_trigger()
+RETURNS trigger
+AS
+$$
+BEGIN
+ RAISE NOTICE '% % % of %', TG_WHEN, TG_OP, NEW.valid_at, OLD.valid_at;
+ IF TG_OP = 'DELETE' THEN
+ RETURN OLD;
+ ELSE
+ RETURN NEW;
+ END IF;
+END;
+$$
+LANGUAGE plpgsql;
+CREATE TRIGGER trg_for_portion_of_before_insert
+ BEFORE INSERT ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_insert
+ AFTER INSERT ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_update
+ BEFORE UPDATE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_update
+ AFTER UPDATE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_delete
+ BEFORE DELETE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_delete
+ AFTER DELETE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01'
+SET name = 'five^4'
+WHERE id = '[5,6)';
+NOTICE: BEFORE UPDATE ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") of ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025")
+NOTICE: BEFORE INSERT ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") of <NULL>
+NOTICE: BEFORE INSERT ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025") of <NULL>
+NOTICE: AFTER INSERT ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") of <NULL>
+NOTICE: AFTER INSERT ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025") of <NULL>
+NOTICE: AFTER UPDATE ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") of ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025")
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01'
+WHERE id = '[5,6)';
+NOTICE: BEFORE DELETE <NULL> of ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025")
+NOTICE: BEFORE INSERT ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") of <NULL>
+NOTICE: BEFORE INSERT ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") of <NULL>
+NOTICE: AFTER INSERT ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") of <NULL>
+NOTICE: AFTER INSERT ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") of <NULL>
+NOTICE: AFTER DELETE <NULL> of ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025")
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+ id | valid_at | name
+-------+---------------------------------------------------------+---------
+ [1,2) | ["Thu Feb 08 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2
+ [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2
+ [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Feb 01 00:00:00 2018","Thu Feb 15 00:00:00 2018") | three^3
+ [3,4) | ["Thu Feb 15 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Mar 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | three
+ [4,5) | (,"Sun Jan 01 00:00:00 2017") | four^1
+ [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3
+ [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2
+ [5,6) | (,"Sun Jan 01 00:00:00 2017") | five
+ [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | five
+ [5,6) | ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") | five^4
+ [5,6) | ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") | five
+ [5,6) | ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") | five
+(16 rows)
+
+-- Test with a custom range type
+CREATE TYPE mydaterange AS range(subtype=date);
+CREATE TABLE for_portion_of_test2 (
+ id int4range NOT NULL,
+ valid_at mydaterange NOT NULL,
+ name text NOT NULL,
+ CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test2
+VALUES
+('[1,1]', '[2018-01-02,2018-02-03)', 'one'),
+('[1,1]', '[2018-02-03,2018-03-03)', 'one'),
+('[1,1]', '[2018-03-03,2018-04-04)', 'one'),
+('[2,2]', '[2018-01-01,2018-05-01)', 'two'),
+('[3,3]', '[2018-01-01,)', 'three');
+;
+UPDATE for_portion_of_test2
+FOR PORTION OF valid_at FROM '2018-01-10' TO '2018-02-10'
+SET name = 'one^1'
+WHERE id = '[1,1]';
+DELETE FROM for_portion_of_test2
+FOR PORTION OF valid_at FROM '2018-01-15' TO '2018-02-15'
+WHERE id = '[2,2]';
+SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
+ id | valid_at | name
+-------+-------------------------+-------
+ [1,2) | [01-02-2018,01-10-2018) | one
+ [1,2) | [01-10-2018,02-03-2018) | one^1
+ [1,2) | [02-03-2018,02-10-2018) | one^1
+ [1,2) | [02-10-2018,03-03-2018) | one
+ [1,2) | [03-03-2018,04-04-2018) | one
+ [2,3) | [01-01-2018,01-15-2018) | two
+ [2,3) | [02-15-2018,05-01-2018) | two
+ [3,4) | [01-01-2018,) | three
+(8 rows)
+
+DROP TABLE for_portion_of_test2;
+DROP TYPE mydaterange;
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index c1e610e62f..cf2c7df900 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -985,6 +985,24 @@ ERROR: null value in column "b" of relation "errtst_part_2" violates not-null c
DETAIL: Failing row contains (a, b, c) = (aaaa, null, ccc).
SET SESSION AUTHORIZATION regress_priv_user1;
DROP TABLE errtst;
+-- test column-level privileges on the range/PERIOD used in FOR PORTION OF
+SET SESSION AUTHORIZATION regress_priv_user1;
+CREATE TABLE t1 (
+ c1 int4range,
+ valid_at tsrange,
+ CONSTRAINT t1pk PRIMARY KEY (c1, valid_at WITHOUT OVERLAPS)
+);
+GRANT SELECT ON t1 TO regress_priv_user2;
+GRANT SELECT ON t1 TO regress_priv_user3;
+GRANT UPDATE (c1) ON t1 TO regress_priv_user2;
+GRANT UPDATE (c1, valid_at) ON t1 TO regress_priv_user3;
+SET SESSION AUTHORIZATION regress_priv_user2;
+UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)';
+ERROR: permission denied for table t1
+SET SESSION AUTHORIZATION regress_priv_user3;
+UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)';
+SET SESSION AUTHORIZATION regress_priv_user1;
+DROP TABLE t1;
-- test column-level privileges when involved with DELETE
SET SESSION AUTHORIZATION regress_priv_user1;
ALTER TABLE atest6 ADD COLUMN three integer;
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 1950e6f281..f4b88bcc38 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -3023,6 +3023,38 @@ select * from uv_iocu_tab;
drop view uv_iocu_view;
drop table uv_iocu_tab;
+-- Check UPDATE FOR PORTION OF works correctly
+create table uv_fpo_tab (id int4range, valid_at tsrange, b float,
+ constraint pk_uv_fpo_tab primary key (id, valid_at without overlaps));
+insert into uv_fpo_tab values ('[1,1]', '[2020-01-01, 2030-01-01)', 0);
+create view uv_fpo_view as
+ select b, b+1 as c, valid_at, id, '2.0'::text as two from uv_fpo_tab;
+insert into uv_fpo_view (id, valid_at, b) values ('[1,1]', '[2010-01-01, 2020-01-01)', 1);
+select * from uv_fpo_view;
+ b | c | valid_at | id | two
+---+---+---------------------------------------------------------+-------+-----
+ 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Wed Jan 01 00:00:00 2020") | [1,2) | 2.0
+(2 rows)
+
+update uv_fpo_view for portion of valid_at from '2015-01-01' to '2020-01-01' set b = 2 where id = '[1,1]';
+select * from uv_fpo_view;
+ b | c | valid_at | id | two
+---+---+---------------------------------------------------------+-------+-----
+ 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 2 | 3 | ["Thu Jan 01 00:00:00 2015","Wed Jan 01 00:00:00 2020") | [1,2) | 2.0
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Thu Jan 01 00:00:00 2015") | [1,2) | 2.0
+(3 rows)
+
+delete from uv_fpo_view for portion of valid_at from '2017-01-01' to '2022-01-01' where id = '[1,1]';
+select * from uv_fpo_view;
+ b | c | valid_at | id | two
+---+---+---------------------------------------------------------+-------+-----
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Thu Jan 01 00:00:00 2015") | [1,2) | 2.0
+ 0 | 1 | ["Sat Jan 01 00:00:00 2022","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 2 | 3 | ["Thu Jan 01 00:00:00 2015","Sun Jan 01 00:00:00 2017") | [1,2) | 2.0
+(3 rows)
+
-- Test whole-row references to the view
create table uv_iocu_tab (a int unique, b text);
create view uv_iocu_view as
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index e61fd1d906..04b4b02f2a 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -269,6 +269,36 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'),
('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', 'bar')
;
+UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-05-01' TO '2000-07-01'
+ SET name = name || '1';
+UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-04-01' TO '2000-06-01'
+ SET name = name || '2'
+ WHERE id = '[2,2]';
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+ id | valid_at | id2 | name
+-------+-------------------------+--------+-------
+ [1,2) | [01-01-2000,05-01-2000) | [7,8) | foo
+ [1,2) | [05-01-2000,07-01-2000) | [7,8) | foo1
+ [1,2) | [07-01-2000,01-01-2010) | [7,8) | foo
+ [2,3) | [01-01-2000,04-01-2000) | [9,10) | bar
+ [2,3) | [04-01-2000,05-01-2000) | [9,10) | bar2
+ [2,3) | [05-01-2000,06-01-2000) | [9,10) | bar12
+ [2,3) | [06-01-2000,07-01-2000) | [9,10) | bar1
+ [2,3) | [07-01-2000,01-01-2010) | [9,10) | bar
+(8 rows)
+
+-- conflicting id only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+ VALUES
+ ('[1,1]', daterange('2005-01-01', '2006-01-01'), '[8,8]', 'foo3');
+ERROR: conflicting key value violates exclusion constraint "temporal3_pk"
+DETAIL: Key (id, valid_at)=([1,2), [01-01-2005,01-01-2006)) conflicts with existing key (id, valid_at)=([1,2), [07-01-2000,01-01-2010)).
+-- conflicting id2 only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+ VALUES
+ ('[3,3]', daterange('2005-01-01', '2010-01-01'), '[9,9]', 'bar3');
+ERROR: conflicting key value violates exclusion constraint "temporal3_uniq"
+DETAIL: Key (id2, valid_at)=([9,10), [01-01-2005,01-01-2010)) conflicts with existing key (id2, valid_at)=([9,10), [07-01-2000,01-01-2010)).
DROP TABLE temporal3;
--
-- test changing the PK's dependencies
@@ -321,6 +351,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
[3,4) | [01-01-2000,01-01-2010) | three
(1 row)
+UPDATE temporal_partitioned
+ FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+ SET name = 'one2'
+ WHERE id = '[1,1]';
+UPDATE temporal_partitioned
+ FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25'
+ SET id = '[4,4]'
+ WHERE name = 'one';
+UPDATE temporal_partitioned
+ FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+ SET id = '[2,2]'
+ WHERE name = 'three';
+DELETE FROM temporal_partitioned
+ FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+ WHERE id = '[3,3]';
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+ id | valid_at | name
+-------+-------------------------+-------
+ [1,2) | [01-01-2000,01-15-2000) | one
+ [1,2) | [01-15-2000,02-01-2000) | one2
+ [1,2) | [02-01-2000,02-15-2000) | one2
+ [1,2) | [02-15-2000,02-20-2000) | one
+ [1,2) | [02-25-2000,03-01-2000) | one
+ [2,3) | [01-01-2002,01-01-2003) | three
+ [3,4) | [01-01-2000,01-15-2000) | three
+ [3,4) | [02-15-2000,01-01-2002) | three
+ [3,4) | [01-01-2003,01-01-2010) | three
+ [4,5) | [02-20-2000,02-25-2000) | one
+(10 rows)
+
DROP TABLE temporal_partitioned;
-- temporal UNIQUE:
CREATE TABLE temporal_partitioned (
@@ -356,6 +416,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
[3,4) | [01-01-2000,01-01-2010) | three
(1 row)
+UPDATE temporal_partitioned
+ FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+ SET name = 'one2'
+ WHERE id = '[1,1]';
+UPDATE temporal_partitioned
+ FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25'
+ SET id = '[4,4]'
+ WHERE name = 'one';
+UPDATE temporal_partitioned
+ FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+ SET id = '[2,2]'
+ WHERE name = 'three';
+DELETE FROM temporal_partitioned
+ FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+ WHERE id = '[3,3]';
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+ id | valid_at | name
+-------+-------------------------+-------
+ [1,2) | [01-01-2000,01-15-2000) | one
+ [1,2) | [01-15-2000,02-01-2000) | one2
+ [1,2) | [02-01-2000,02-15-2000) | one2
+ [1,2) | [02-15-2000,02-20-2000) | one
+ [1,2) | [02-25-2000,03-01-2000) | one
+ [2,3) | [01-01-2002,01-01-2003) | three
+ [3,4) | [01-01-2000,01-15-2000) | three
+ [3,4) | [02-15-2000,01-01-2002) | three
+ [3,4) | [01-01-2003,01-01-2010) | three
+ [4,5) | [02-20-2000,02-25-2000) | one
+(10 rows)
+
DROP TABLE temporal_partitioned;
--
-- test FK dependencies
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 66b75fa12f..99cf6f18d6 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
# ----------
# Another group of parallel tests
# ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse for_portion_of
# ----------
# sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
new file mode 100644
index 0000000000..bf2ab093a6
--- /dev/null
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -0,0 +1,340 @@
+-- Tests for UPDATE/DELETE FOR PORTION OF
+
+-- Works on non-PK columns
+CREATE TABLE for_portion_of_test (
+ id int4range,
+ valid_at tsrange,
+ name text NOT NULL
+);
+INSERT INTO for_portion_of_test VALUES
+('[1,2)', '[2018-01-02,2020-01-01)', 'one');
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+SET name = 'foo';
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2019-01-15' TO UNBOUNDED;
+
+SELECT * FROM for_portion_of_test;
+
+-- Works on more than one range
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+ id int4range,
+ valid1_at tsrange,
+ valid2_at tsrange,
+ name text NOT NULL
+);
+INSERT INTO for_portion_of_test VALUES
+('[1,2)', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', 'one');
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid1_at FROM '2018-01-15' TO UNBOUNDED
+SET name = 'foo';
+SELECT * FROM for_portion_of_test;
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid2_at FROM '2018-01-15' TO UNBOUNDED
+SET name = 'bar';
+SELECT * FROM for_portion_of_test;
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid1_at FROM '2018-01-20' TO UNBOUNDED;
+SELECT * FROM for_portion_of_test;
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid2_at FROM '2018-01-20' TO UNBOUNDED;
+SELECT * FROM for_portion_of_test;
+
+
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+ id int4range NOT NULL,
+ valid_at tsrange NOT NULL,
+ name text NOT NULL,
+ CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test
+VALUES
+('[1,2)', '[2018-01-02,2018-02-03)', 'one'),
+('[1,2)', '[2018-02-03,2018-03-03)', 'one'),
+('[1,2)', '[2018-03-03,2018-04-04)', 'one'),
+('[2,3)', '[2018-01-01,2018-01-05)', 'two'),
+('[3,4)', '[2018-01-01,)', 'three'),
+('[4,5)', '(,2018-04-01)', 'four'),
+('[5,6)', '(,)', 'five')
+;
+
+--
+-- UPDATE tests
+--
+
+-- Setting with a missing column fails
+UPDATE for_portion_of_test
+FOR PORTION OF invalid_at FROM '2018-06-01' TO UNBOUNDED
+SET name = 'foo'
+WHERE id = '[5,6)';
+
+-- Setting the range fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO UNBOUNDED
+SET valid_at = '[1990-01-01,1999-01-01)'
+WHERE id = '[5,6)';
+
+-- The wrong type fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM 1 TO 4
+SET name = 'nope'
+WHERE id = '[3,4)';
+
+-- Setting with timestamps reversed fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+SET name = 'three^1'
+WHERE id = '[3,4)';
+
+-- Setting with a subquery fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01'
+SET name = 'nope'
+WHERE id = '[3,4)';
+
+-- Setting with a column fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM lower(valid_at) TO UNBOUNDED
+SET name = 'nope'
+WHERE id = '[3,4)';
+
+-- Setting with timestamps equal does nothing
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+SET name = 'three^0'
+WHERE id = '[3,4)';
+
+-- Updating a finite/open portion with a finite/open target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO UNBOUNDED
+SET name = 'three^1'
+WHERE id = '[3,4)';
+
+-- Updating a finite/open portion with an open/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM UNBOUNDED TO '2018-03-01'
+SET name = 'three^2'
+WHERE id = '[3,4)';
+
+-- Updating an open/finite portion with an open/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM UNBOUNDED TO '2018-02-01'
+SET name = 'four^1'
+WHERE id = '[4,5)';
+
+-- Updating an open/finite portion with a finite/open target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO UNBOUNDED
+SET name = 'four^2'
+WHERE id = '[4,5)';
+
+-- Updating a finite/finite portion with an exact fit
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01'
+SET name = 'four^3'
+WHERE id = '[4,5)';
+
+-- Updating an enclosed span
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM UNBOUNDED TO UNBOUNDED
+SET name = 'two^2'
+WHERE id = '[2,3)';
+
+-- Updating an open/open portion with a finite/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01'
+SET name = 'five^2'
+WHERE id = '[5,6)';
+
+-- Updating an enclosed span with separate protruding spans
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01'
+SET name = 'five^3'
+WHERE id = '[5,6)';
+
+-- Updating multiple enclosed spans
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO NULL
+SET name = 'one^2'
+WHERE id = '[1,2)';
+
+-- Updating with a shift/reduce conflict
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+ FROM '2018-03-01' AT TIME ZONE INTERVAL '1' HOUR TO MINUTE
+ TO '2019-01-01'
+SET name = 'one^3'
+WHERE id = '[1,2)';
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+ FROM '2018-03-01' AT TIME ZONE INTERVAL '2' HOUR
+ TO '2019-01-01'
+SET name = 'one^4'
+WHERE id = '[1,2)';
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+ FROM ('2018-03-01' AT TIME ZONE INTERVAL '2' HOUR)
+ TO '2019-01-01'
+SET name = 'one^4'
+WHERE id = '[1,2)';
+
+-- Updating the non-range part of the PK:
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-02-15' TO NULL
+SET id = '[6,7)'
+WHERE id = '[1,2)';
+
+-- UPDATE with no WHERE clause
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2030-01-01' TO NULL
+SET name = name || '*';
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+--
+-- DELETE tests
+--
+
+-- Deleting with a missing column fails
+DELETE FROM for_portion_of_test
+FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL
+WHERE id = '[5,6)';
+
+-- Deleting with timestamps reversed fails
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+WHERE id = '[3,4)';
+
+-- Deleting with timestamps equal does nothing
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+WHERE id = '[3,4)';
+
+-- Deleting with a closed/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2020-06-01'
+WHERE id = '[5,6)';
+
+-- Deleting with a closed/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO NULL
+WHERE id = '[3,4)';
+
+-- Deleting with an open/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO '2018-02-08'
+WHERE id = '[1,2)';
+
+-- Deleting with an open/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO NULL
+WHERE id = '[6,7)';
+
+-- DELETE with no WHERE clause
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2025-01-01' TO NULL;
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+-- UPDATE ... RETURNING returns only the updated values (not the inserted side values)
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15'
+SET name = 'three^3'
+WHERE id = '[3,4)'
+RETURNING *;
+
+-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows
+
+CREATE FUNCTION for_portion_of_trigger()
+RETURNS trigger
+AS
+$$
+BEGIN
+ RAISE NOTICE '% % % of %', TG_WHEN, TG_OP, NEW.valid_at, OLD.valid_at;
+ IF TG_OP = 'DELETE' THEN
+ RETURN OLD;
+ ELSE
+ RETURN NEW;
+ END IF;
+END;
+$$
+LANGUAGE plpgsql;
+
+CREATE TRIGGER trg_for_portion_of_before_insert
+ BEFORE INSERT ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_insert
+ AFTER INSERT ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_update
+ BEFORE UPDATE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_update
+ AFTER UPDATE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_delete
+ BEFORE DELETE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_delete
+ AFTER DELETE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01'
+SET name = 'five^4'
+WHERE id = '[5,6)';
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01'
+WHERE id = '[5,6)';
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+-- Test with a custom range type
+
+CREATE TYPE mydaterange AS range(subtype=date);
+
+CREATE TABLE for_portion_of_test2 (
+ id int4range NOT NULL,
+ valid_at mydaterange NOT NULL,
+ name text NOT NULL,
+ CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test2
+VALUES
+('[1,1]', '[2018-01-02,2018-02-03)', 'one'),
+('[1,1]', '[2018-02-03,2018-03-03)', 'one'),
+('[1,1]', '[2018-03-03,2018-04-04)', 'one'),
+('[2,2]', '[2018-01-01,2018-05-01)', 'two'),
+('[3,3]', '[2018-01-01,)', 'three');
+;
+
+UPDATE for_portion_of_test2
+FOR PORTION OF valid_at FROM '2018-01-10' TO '2018-02-10'
+SET name = 'one^1'
+WHERE id = '[1,1]';
+
+DELETE FROM for_portion_of_test2
+FOR PORTION OF valid_at FROM '2018-01-15' TO '2018-02-15'
+WHERE id = '[2,2]';
+
+SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
+
+DROP TABLE for_portion_of_test2;
+DROP TYPE mydaterange;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index bf0035d96d..366be99a27 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -713,6 +713,24 @@ UPDATE errtst SET a = 'aaaa', b = NULL WHERE a = 'aaa';
SET SESSION AUTHORIZATION regress_priv_user1;
DROP TABLE errtst;
+-- test column-level privileges on the range/PERIOD used in FOR PORTION OF
+SET SESSION AUTHORIZATION regress_priv_user1;
+CREATE TABLE t1 (
+ c1 int4range,
+ valid_at tsrange,
+ CONSTRAINT t1pk PRIMARY KEY (c1, valid_at WITHOUT OVERLAPS)
+);
+GRANT SELECT ON t1 TO regress_priv_user2;
+GRANT SELECT ON t1 TO regress_priv_user3;
+GRANT UPDATE (c1) ON t1 TO regress_priv_user2;
+GRANT UPDATE (c1, valid_at) ON t1 TO regress_priv_user3;
+SET SESSION AUTHORIZATION regress_priv_user2;
+UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)';
+SET SESSION AUTHORIZATION regress_priv_user3;
+UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)';
+SET SESSION AUTHORIZATION regress_priv_user1;
+DROP TABLE t1;
+
-- test column-level privileges when involved with DELETE
SET SESSION AUTHORIZATION regress_priv_user1;
ALTER TABLE atest6 ADD COLUMN three integer;
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
index eaee0b7e1d..f814701a43 100644
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -1541,6 +1541,20 @@ select * from uv_iocu_tab;
drop view uv_iocu_view;
drop table uv_iocu_tab;
+-- Check UPDATE FOR PORTION OF works correctly
+create table uv_fpo_tab (id int4range, valid_at tsrange, b float,
+ constraint pk_uv_fpo_tab primary key (id, valid_at without overlaps));
+insert into uv_fpo_tab values ('[1,1]', '[2020-01-01, 2030-01-01)', 0);
+create view uv_fpo_view as
+ select b, b+1 as c, valid_at, id, '2.0'::text as two from uv_fpo_tab;
+
+insert into uv_fpo_view (id, valid_at, b) values ('[1,1]', '[2010-01-01, 2020-01-01)', 1);
+select * from uv_fpo_view;
+update uv_fpo_view for portion of valid_at from '2015-01-01' to '2020-01-01' set b = 2 where id = '[1,1]';
+select * from uv_fpo_view;
+delete from uv_fpo_view for portion of valid_at from '2017-01-01' to '2022-01-01' where id = '[1,1]';
+select * from uv_fpo_view;
+
-- Test whole-row references to the view
create table uv_iocu_tab (a int unique, b text);
create view uv_iocu_view as
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 3248390744..1cd272a575 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -203,6 +203,20 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'),
('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', 'bar')
;
+UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-05-01' TO '2000-07-01'
+ SET name = name || '1';
+UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-04-01' TO '2000-06-01'
+ SET name = name || '2'
+ WHERE id = '[2,2]';
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+-- conflicting id only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+ VALUES
+ ('[1,1]', daterange('2005-01-01', '2006-01-01'), '[8,8]', 'foo3');
+-- conflicting id2 only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+ VALUES
+ ('[3,3]', daterange('2005-01-01', '2010-01-01'), '[9,9]', 'bar3');
DROP TABLE temporal3;
--
@@ -241,6 +255,22 @@ INSERT INTO temporal_partitioned VALUES
SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
SELECT * FROM tp1 ORDER BY id, valid_at;
SELECT * FROM tp2 ORDER BY id, valid_at;
+UPDATE temporal_partitioned
+ FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+ SET name = 'one2'
+ WHERE id = '[1,1]';
+UPDATE temporal_partitioned
+ FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25'
+ SET id = '[4,4]'
+ WHERE name = 'one';
+UPDATE temporal_partitioned
+ FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+ SET id = '[2,2]'
+ WHERE name = 'three';
+DELETE FROM temporal_partitioned
+ FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+ WHERE id = '[3,3]';
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
DROP TABLE temporal_partitioned;
-- temporal UNIQUE:
@@ -259,6 +289,22 @@ INSERT INTO temporal_partitioned VALUES
SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
SELECT * FROM tp1 ORDER BY id, valid_at;
SELECT * FROM tp2 ORDER BY id, valid_at;
+UPDATE temporal_partitioned
+ FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+ SET name = 'one2'
+ WHERE id = '[1,1]';
+UPDATE temporal_partitioned
+ FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25'
+ SET id = '[4,4]'
+ WHERE name = 'one';
+UPDATE temporal_partitioned
+ FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+ SET id = '[2,2]'
+ WHERE name = 'three';
+DELETE FROM temporal_partitioned
+ FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+ WHERE id = '[3,3]';
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
DROP TABLE temporal_partitioned;
--
--
2.25.1
[text/x-patch] v13-0004-Add-CASCADE-SET-NULL-SET-DEFAULT-for-temporal-fo.patch (74.6K, ../../[email protected]/5-v13-0004-Add-CASCADE-SET-NULL-SET-DEFAULT-for-temporal-fo.patch)
download | inline diff:
From 5a231603a86f899acd1bf6751e44d5fd6243dbb5 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Sat, 3 Jun 2023 21:41:11 -0400
Subject: [PATCH v13 4/5] Add CASCADE/SET NULL/SET DEFAULT for temporal foreign
keys
---
src/backend/commands/tablecmds.c | 28 +-
src/backend/utils/adt/ri_triggers.c | 618 +++++++++++++++++-
src/include/catalog/pg_proc.dat | 20 +
.../regress/expected/without_overlaps.out | 413 +++++++++++-
src/test/regress/sql/without_overlaps.sql | 173 ++++-
5 files changed, 1224 insertions(+), 28 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index af92e78ac9..e230dfc569 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12292,11 +12292,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_del");
break;
case FKCONSTR_ACTION_CASCADE:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_cascade_del");
+ break;
case FKCONSTR_ACTION_SETNULL:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_setnull_del");
+ break;
case FKCONSTR_ACTION_SETDEFAULT:
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("action not supported for temporal foreign keys")));
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_setdefault_del");
break;
default:
elog(ERROR, "unrecognized FK action type: %d",
@@ -12383,11 +12391,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_upd");
break;
case FKCONSTR_ACTION_CASCADE:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_cascade_upd");
+ break;
case FKCONSTR_ACTION_SETNULL:
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_setnull_upd");
+ break;
case FKCONSTR_ACTION_SETDEFAULT:
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("action not supported for temporal foreign keys")));
+ fk_trigger->deferrable = false;
+ fk_trigger->initdeferred = false;
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_setdefault_upd");
break;
default:
elog(ERROR, "unrecognized FK action type: %d",
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 70466a94d9..51890f538a 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -83,6 +83,12 @@
#define RI_PLAN_SETNULL_ONUPDATE 7
#define RI_PLAN_SETDEFAULT_ONDELETE 8
#define RI_PLAN_SETDEFAULT_ONUPDATE 9
+#define TRI_PLAN_CASCADE_ONDELETE 10
+#define TRI_PLAN_CASCADE_ONUPDATE 11
+#define TRI_PLAN_SETNULL_ONUPDATE 12
+#define TRI_PLAN_SETNULL_ONDELETE 13
+#define TRI_PLAN_SETDEFAULT_ONUPDATE 14
+#define TRI_PLAN_SETDEFAULT_ONDELETE 15
#define MAX_QUOTED_NAME_LEN (NAMEDATALEN*2+3)
#define MAX_QUOTED_REL_NAME_LEN (MAX_QUOTED_NAME_LEN*2)
@@ -190,6 +196,7 @@ static bool ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
const RI_ConstraintInfo *riinfo);
static Datum ri_restrict(TriggerData *trigdata, bool is_no_action);
static Datum ri_set(TriggerData *trigdata, bool is_set_null, int tgkind);
+static Datum tri_set(TriggerData *trigdata, bool is_set_null, int tgkind);
static void quoteOneName(char *buffer, const char *name);
static void quoteRelationName(char *buffer, Relation rel);
static void ri_GenerateQual(StringInfo buf,
@@ -227,6 +234,7 @@ static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo,
RI_QueryKey *qkey, SPIPlanPtr qplan,
Relation fk_rel, Relation pk_rel,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
+ bool hasForPortionOf, Datum forPortionOf,
bool detectNewRows, int expect_OK);
static void ri_ExtractValues(Relation rel, TupleTableSlot *slot,
const RI_ConstraintInfo *riinfo, bool rel_is_pk,
@@ -235,6 +243,11 @@ static void ri_ReportViolation(const RI_ConstraintInfo *riinfo,
Relation pk_rel, Relation fk_rel,
TupleTableSlot *violatorslot, TupleDesc tupdesc,
int queryno, bool partgone) pg_attribute_noreturn();
+static bool fpo_targets_pk_range(const ForPortionOfState *tg_temporal,
+ const RI_ConstraintInfo *riinfo);
+static Datum restrict_cascading_range(const ForPortionOfState *tg_temporal,
+ const RI_ConstraintInfo *riinfo,
+ TupleTableSlot *oldslot);
/*
@@ -443,6 +456,7 @@ RI_FKey_check(TriggerData *trigdata)
ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
NULL, newslot,
+ false, 0,
pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE,
SPI_OK_SELECT);
@@ -572,6 +586,7 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
result = ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
oldslot, NULL,
+ false, 0,
true, /* treat like update */
SPI_OK_SELECT);
@@ -770,6 +785,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
oldslot, NULL,
+ false, 0,
true, /* must detect new rows */
SPI_OK_SELECT);
@@ -876,6 +892,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
oldslot, NULL,
+ false, 0,
true, /* must detect new rows */
SPI_OK_DELETE);
@@ -997,6 +1014,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
oldslot, newslot,
+ false, 0,
true, /* must detect new rows */
SPI_OK_UPDATE);
@@ -1229,6 +1247,7 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
oldslot, NULL,
+ false, 0,
true, /* must detect new rows */
SPI_OK_UPDATE);
@@ -1342,6 +1361,127 @@ TRI_FKey_restrict_del(PG_FUNCTION_ARGS)
return ri_restrict((TriggerData *) fcinfo->context, false);
}
+/*
+ * TRI_FKey_cascade_del -
+ *
+ * Cascaded delete foreign key references at delete event on temporal PK table.
+ */
+Datum
+TRI_FKey_cascade_del(PG_FUNCTION_ARGS)
+{
+ TriggerData *trigdata = (TriggerData *) fcinfo->context;
+ const RI_ConstraintInfo *riinfo;
+ Relation fk_rel;
+ Relation pk_rel;
+ TupleTableSlot *oldslot;
+ RI_QueryKey qkey;
+ SPIPlanPtr qplan;
+ Datum targetRange;
+
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_cascade_del", RI_TRIGTYPE_DELETE);
+
+ riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger,
+ trigdata->tg_relation, true);
+
+ /*
+ * Get the relation descriptors of the FK and PK tables and the old tuple.
+ *
+ * fk_rel is opened in RowExclusiveLock mode since that's what our
+ * eventual DELETE will get on it.
+ */
+ fk_rel = table_open(riinfo->fk_relid, RowExclusiveLock);
+ pk_rel = trigdata->tg_relation;
+ oldslot = trigdata->tg_trigslot;
+
+ /*
+ * Don't delete than more than the PK's duration,
+ * trimmed by an original FOR PORTION OF if necessary.
+ */
+ targetRange = restrict_cascading_range(trigdata->tg_temporal, riinfo, oldslot);
+
+ if (SPI_connect() != SPI_OK_CONNECT)
+ elog(ERROR, "SPI_connect failed");
+
+ /* Fetch or prepare a saved plan for the cascaded delete */
+ ri_BuildQueryKey(&qkey, riinfo, TRI_PLAN_CASCADE_ONDELETE);
+
+ if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL)
+ {
+ StringInfoData querybuf;
+ char fkrelname[MAX_QUOTED_REL_NAME_LEN];
+ char attname[MAX_QUOTED_NAME_LEN];
+ char paramname[16];
+ const char *querysep;
+ Oid queryoids[RI_MAX_NUMKEYS + 1];
+ const char *fk_only;
+
+ /* ----------
+ * The query string built is
+ * DELETE FROM [ONLY] <fktable>
+ * FOR PORTION OF $fkatt FROM lower(${n+1}) TO upper(${n+1})
+ * WHERE $1 = fkatt1 [AND ...]
+ * The type id's for the $ parameters are those of the
+ * corresponding PK attributes.
+ * ----------
+ */
+ initStringInfo(&querybuf);
+ fk_only = fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
+ "" : "ONLY ";
+ quoteRelationName(fkrelname, fk_rel);
+ quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]));
+
+ appendStringInfo(&querybuf, "DELETE FROM %s%s FOR PORTION OF %s FROM lower($%d) TO upper($%d)",
+ fk_only, fkrelname, attname, riinfo->nkeys + 1, riinfo->nkeys + 1);
+ querysep = "WHERE";
+ for (int i = 0; i < riinfo->nkeys; i++)
+ {
+ Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+ Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+ Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+ Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
+
+ quoteOneName(attname,
+ RIAttName(fk_rel, riinfo->fk_attnums[i]));
+ sprintf(paramname, "$%d", i + 1);
+ ri_GenerateQual(&querybuf, querysep,
+ paramname, pk_type,
+ riinfo->pf_eq_oprs[i],
+ attname, fk_type);
+ if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll))
+ ri_GenerateQualCollation(&querybuf, pk_coll);
+ querysep = "AND";
+ queryoids[i] = pk_type;
+ }
+
+ /* Set a param for FOR PORTION OF TO/FROM */
+ queryoids[riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]);
+
+ /* Prepare and save the plan */
+ qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys + 1, queryoids,
+ &qkey, fk_rel, pk_rel);
+ }
+
+ /*
+ * We have a plan now. Build up the arguments from the key values in the
+ * deleted PK tuple and delete the referencing rows
+ */
+ ri_PerformCheck(riinfo, &qkey, qplan,
+ fk_rel, pk_rel,
+ oldslot, NULL,
+ true, targetRange,
+ true, /* must detect new rows */
+ SPI_OK_DELETE);
+
+ if (SPI_finish() != SPI_OK_FINISH)
+ elog(ERROR, "SPI_finish failed");
+
+ table_close(fk_rel, RowExclusiveLock);
+
+ return PointerGetDatum(NULL);
+}
+
+
/*
* TRI_FKey_noaction_upd -
*
@@ -1379,6 +1519,415 @@ TRI_FKey_restrict_upd(PG_FUNCTION_ARGS)
return ri_restrict((TriggerData *) fcinfo->context, false);
}
+/*
+ * TRI_FKey_cascade_upd -
+ *
+ * Cascaded update foreign key references at update event on temporal PK table.
+ */
+Datum
+TRI_FKey_cascade_upd(PG_FUNCTION_ARGS)
+{
+ TriggerData *trigdata = (TriggerData *) fcinfo->context;
+ const RI_ConstraintInfo *riinfo;
+ Relation fk_rel;
+ Relation pk_rel;
+ TupleTableSlot *oldslot;
+ TupleTableSlot *newslot;
+ RI_QueryKey qkey;
+ SPIPlanPtr qplan;
+ Datum targetRange;
+
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_cascade_upd", RI_TRIGTYPE_UPDATE);
+
+ riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger,
+ trigdata->tg_relation, true);
+
+ /*
+ * Get the relation descriptors of the FK and PK tables and the new and
+ * old tuple.
+ *
+ * fk_rel is opened in RowExclusiveLock mode since that's what our
+ * eventual UPDATE will get on it.
+ */
+ fk_rel = table_open(riinfo->fk_relid, RowExclusiveLock);
+ pk_rel = trigdata->tg_relation;
+ newslot = trigdata->tg_newslot;
+ oldslot = trigdata->tg_trigslot;
+
+ /*
+ * Don't delete than more than the PK's duration,
+ * trimmed by an original FOR PORTION OF if necessary.
+ */
+ targetRange = restrict_cascading_range(trigdata->tg_temporal, riinfo, oldslot);
+
+ if (SPI_connect() != SPI_OK_CONNECT)
+ elog(ERROR, "SPI_connect failed");
+
+ /* Fetch or prepare a saved plan for the cascaded update */
+ ri_BuildQueryKey(&qkey, riinfo, TRI_PLAN_CASCADE_ONUPDATE);
+
+ if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL)
+ {
+ StringInfoData querybuf;
+ StringInfoData qualbuf;
+ char fkrelname[MAX_QUOTED_REL_NAME_LEN];
+ char attname[MAX_QUOTED_NAME_LEN];
+ char paramname[16];
+ const char *querysep;
+ const char *qualsep;
+ Oid queryoids[2 * RI_MAX_NUMKEYS + 1];
+ const char *fk_only;
+
+ /* ----------
+ * The query string built is
+ * UPDATE [ONLY] <fktable>
+ * FOR PORTION OF $fkatt FROM lower(${2n+1}) TO upper(${2n+1})
+ * SET fkatt1 = $1, [, ...]
+ * WHERE $n = fkatt1 [AND ...]
+ * The type id's for the $ parameters are those of the
+ * corresponding PK attributes. Note that we are assuming
+ * there is an assignment cast from the PK to the FK type;
+ * else the parser will fail.
+ * ----------
+ */
+ initStringInfo(&querybuf);
+ initStringInfo(&qualbuf);
+ fk_only = fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
+ "" : "ONLY ";
+ quoteRelationName(fkrelname, fk_rel);
+ quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]));
+
+ appendStringInfo(&querybuf, "UPDATE %s%s FOR PORTION OF %s FROM lower($%d) TO upper($%d) SET",
+ fk_only, fkrelname, attname, 2 * riinfo->nkeys + 1, 2 * riinfo->nkeys + 1);
+
+ querysep = "";
+ qualsep = "WHERE";
+ for (int i = 0, j = riinfo->nkeys; i < riinfo->nkeys; i++, j++)
+ {
+ Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+ Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+ Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+ Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
+
+ quoteOneName(attname,
+ RIAttName(fk_rel, riinfo->fk_attnums[i]));
+ /*
+ * Don't set the temporal column(s).
+ * FOR PORTION OF will take care of that.
+ */
+ if (i < riinfo->nkeys - 1)
+ appendStringInfo(&querybuf,
+ "%s %s = $%d",
+ querysep, attname, i + 1);
+
+ sprintf(paramname, "$%d", j + 1);
+ ri_GenerateQual(&qualbuf, qualsep,
+ paramname, pk_type,
+ riinfo->pf_eq_oprs[i],
+ attname, fk_type);
+ if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll))
+ ri_GenerateQualCollation(&querybuf, pk_coll);
+ querysep = ",";
+ qualsep = "AND";
+ queryoids[i] = pk_type;
+ queryoids[j] = pk_type;
+ }
+ appendBinaryStringInfo(&querybuf, qualbuf.data, qualbuf.len);
+
+ /* Set a param for FOR PORTION OF TO/FROM */
+ queryoids[2 * riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]);
+
+ /* Prepare and save the plan */
+ qplan = ri_PlanCheck(querybuf.data, 2 * riinfo->nkeys + 1, queryoids,
+ &qkey, fk_rel, pk_rel);
+ }
+
+ /*
+ * We have a plan now. Run it to update the existing references.
+ */
+ ri_PerformCheck(riinfo, &qkey, qplan,
+ fk_rel, pk_rel,
+ oldslot, newslot,
+ true, targetRange,
+ true, /* must detect new rows */
+ SPI_OK_UPDATE);
+
+ if (SPI_finish() != SPI_OK_FINISH)
+ elog(ERROR, "SPI_finish failed");
+
+ table_close(fk_rel, RowExclusiveLock);
+
+ return PointerGetDatum(NULL);
+}
+
+/*
+ * TRI_FKey_setnull_del -
+ *
+ * Set foreign key references to NULL values at delete event on PK table.
+ */
+Datum
+TRI_FKey_setnull_del(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_setnull_del", RI_TRIGTYPE_DELETE);
+
+ /* Share code with UPDATE case */
+ return tri_set((TriggerData *) fcinfo->context, true, RI_TRIGTYPE_DELETE);
+}
+
+/*
+ * TRI_FKey_setnull_upd -
+ *
+ * Set foreign key references to NULL at update event on PK table.
+ */
+Datum
+TRI_FKey_setnull_upd(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_setnull_upd", RI_TRIGTYPE_UPDATE);
+
+ /* Share code with DELETE case */
+ return tri_set((TriggerData *) fcinfo->context, true, RI_TRIGTYPE_UPDATE);
+}
+
+/*
+ * TRI_FKey_setdefault_del -
+ *
+ * Set foreign key references to defaults at delete event on PK table.
+ */
+Datum
+TRI_FKey_setdefault_del(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_setdefault_del", RI_TRIGTYPE_DELETE);
+
+ /* Share code with UPDATE case */
+ return tri_set((TriggerData *) fcinfo->context, false, RI_TRIGTYPE_DELETE);
+}
+
+/*
+ * TRI_FKey_setdefault_upd -
+ *
+ * Set foreign key references to defaults at update event on PK table.
+ */
+Datum
+TRI_FKey_setdefault_upd(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_setdefault_upd", RI_TRIGTYPE_UPDATE);
+
+ /* Share code with DELETE case */
+ return tri_set((TriggerData *) fcinfo->context, false, RI_TRIGTYPE_UPDATE);
+}
+
+/*
+ * tri_set -
+ *
+ * Common code for temporal ON DELETE SET NULL, ON DELETE SET DEFAULT, ON
+ * UPDATE SET NULL, and ON UPDATE SET DEFAULT.
+ */
+static Datum
+tri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
+{
+ const RI_ConstraintInfo *riinfo;
+ Relation fk_rel;
+ Relation pk_rel;
+ TupleTableSlot *oldslot;
+ RI_QueryKey qkey;
+ SPIPlanPtr qplan;
+ Datum targetRange;
+ int32 queryno;
+
+ riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger,
+ trigdata->tg_relation, true);
+
+ /*
+ * Get the relation descriptors of the FK and PK tables and the old tuple.
+ *
+ * fk_rel is opened in RowExclusiveLock mode since that's what our
+ * eventual UPDATE will get on it.
+ */
+ fk_rel = table_open(riinfo->fk_relid, RowExclusiveLock);
+ pk_rel = trigdata->tg_relation;
+ oldslot = trigdata->tg_trigslot;
+
+ /*
+ * Don't SET NULL/DEFAULT than more than the PK's duration,
+ * trimmed by an original FOR PORTION OF if necessary.
+ */
+ targetRange = restrict_cascading_range(trigdata->tg_temporal, riinfo, oldslot);
+
+ if (SPI_connect() != SPI_OK_CONNECT)
+ elog(ERROR, "SPI_connect failed");
+
+ /*
+ * Fetch or prepare a saved plan for the trigger.
+ */
+ switch (tgkind)
+ {
+ case RI_TRIGTYPE_UPDATE:
+ queryno = is_set_null
+ ? TRI_PLAN_SETNULL_ONUPDATE
+ : TRI_PLAN_SETDEFAULT_ONUPDATE;
+ break;
+ case RI_TRIGTYPE_DELETE:
+ queryno = is_set_null
+ ? TRI_PLAN_SETNULL_ONDELETE
+ : TRI_PLAN_SETDEFAULT_ONDELETE;
+ break;
+ default:
+ elog(ERROR, "invalid tgkind passed to ri_set");
+ }
+
+ ri_BuildQueryKey(&qkey, riinfo, queryno);
+
+ if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL)
+ {
+ StringInfoData querybuf;
+ StringInfoData qualbuf;
+ char fkrelname[MAX_QUOTED_REL_NAME_LEN];
+ char attname[MAX_QUOTED_NAME_LEN];
+ char paramname[16];
+ const char *querysep;
+ const char *qualsep;
+ Oid queryoids[RI_MAX_NUMKEYS + 1]; /* +1 for FOR PORTION OF */
+ const char *fk_only;
+ int num_cols_to_set;
+ const int16 *set_cols;
+
+ switch (tgkind)
+ {
+ case RI_TRIGTYPE_UPDATE:
+ /* -1 so we let FOR PORTION OF set the range. */
+ num_cols_to_set = riinfo->nkeys - 1;
+ set_cols = riinfo->fk_attnums;
+ break;
+ case RI_TRIGTYPE_DELETE:
+ /*
+ * If confdelsetcols are present, then we only update the
+ * columns specified in that array, otherwise we update all
+ * the referencing columns.
+ */
+ if (riinfo->ndelsetcols != 0)
+ {
+ num_cols_to_set = riinfo->ndelsetcols;
+ set_cols = riinfo->confdelsetcols;
+ }
+ else
+ {
+ /* -1 so we let FOR PORTION OF set the range. */
+ num_cols_to_set = riinfo->nkeys - 1;
+ set_cols = riinfo->fk_attnums;
+ }
+ break;
+ default:
+ elog(ERROR, "invalid tgkind passed to ri_set");
+ }
+
+ /* ----------
+ * The query string built is
+ * UPDATE [ONLY] <fktable>
+ * FOR PORTION OF $fkatt FROM lower(${n+1}) TO upper(${n+1})
+ * SET fkatt1 = {NULL|DEFAULT} [, ...]
+ * WHERE $1 = fkatt1 [AND ...]
+ * The type id's for the $ parameters are those of the
+ * corresponding PK attributes.
+ * ----------
+ */
+ initStringInfo(&querybuf);
+ initStringInfo(&qualbuf);
+ fk_only = fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
+ "" : "ONLY ";
+ quoteRelationName(fkrelname, fk_rel);
+ quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]));
+
+ appendStringInfo(&querybuf, "UPDATE %s%s FOR PORTION OF %s FROM lower($%d) TO upper($%d) SET",
+ fk_only, fkrelname, attname, riinfo->nkeys + 1, riinfo->nkeys + 1);
+
+ /*
+ * Add assignment clauses
+ */
+ querysep = "";
+ for (int i = 0; i < num_cols_to_set; i++)
+ {
+ quoteOneName(attname, RIAttName(fk_rel, set_cols[i]));
+ appendStringInfo(&querybuf,
+ "%s %s = %s",
+ querysep, attname,
+ is_set_null ? "NULL" : "DEFAULT");
+ querysep = ",";
+ }
+
+ /*
+ * Add WHERE clause
+ */
+ qualsep = "WHERE";
+ for (int i = 0; i < riinfo->nkeys; i++)
+ {
+ Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+ Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+ Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+ Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
+
+ quoteOneName(attname,
+ RIAttName(fk_rel, riinfo->fk_attnums[i]));
+
+ sprintf(paramname, "$%d", i + 1);
+ ri_GenerateQual(&querybuf, qualsep,
+ paramname, pk_type,
+ riinfo->pf_eq_oprs[i],
+ attname, fk_type);
+ if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll))
+ ri_GenerateQualCollation(&querybuf, pk_coll);
+ qualsep = "AND";
+ queryoids[i] = pk_type;
+ }
+
+ /* Set a param for FOR PORTION OF TO/FROM */
+ queryoids[riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]);
+
+ /* Prepare and save the plan */
+ qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys + 1, queryoids,
+ &qkey, fk_rel, pk_rel);
+ }
+
+ /*
+ * We have a plan now. Run it to update the existing references.
+ */
+ ri_PerformCheck(riinfo, &qkey, qplan,
+ fk_rel, pk_rel,
+ oldslot, NULL,
+ true, targetRange,
+ true, /* must detect new rows */
+ SPI_OK_UPDATE);
+
+ if (SPI_finish() != SPI_OK_FINISH)
+ elog(ERROR, "SPI_finish failed");
+
+ table_close(fk_rel, RowExclusiveLock);
+
+ if (is_set_null)
+ return PointerGetDatum(NULL);
+ else
+ {
+ /*
+ * If we just deleted or updated the PK row whose key was equal to the
+ * FK columns' default values, and a referencing row exists in the FK
+ * table, we would have updated that row to the same values it already
+ * had --- and RI_FKey_fk_upd_check_required would hence believe no
+ * check is necessary. So we need to do another lookup now and in
+ * case a reference still exists, abort the operation. That is
+ * already implemented in the NO ACTION trigger, so just run it. (This
+ * recheck is only needed in the SET DEFAULT case, since CASCADE would
+ * remove such rows in case of a DELETE operation or would change the
+ * FK key values in case of an UPDATE, while SET NULL is certain to
+ * result in rows that satisfy the FK constraint.)
+ */
+ return ri_restrict(trigdata, true);
+ }
+}
+
/*
* RI_FKey_pk_upd_check_required -
*
@@ -2491,6 +3040,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
RI_QueryKey *qkey, SPIPlanPtr qplan,
Relation fk_rel, Relation pk_rel,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
+ bool hasForPortionOf, Datum forPortionOf,
bool detectNewRows, int expect_OK)
{
Relation query_rel,
@@ -2502,8 +3052,9 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
int spi_result;
Oid save_userid;
int save_sec_context;
- Datum vals[RI_MAX_NUMKEYS * 2];
- char nulls[RI_MAX_NUMKEYS * 2];
+ Datum vals[RI_MAX_NUMKEYS * 2 + 1];
+ char nulls[RI_MAX_NUMKEYS * 2 + 1];
+ int last_param;
/*
* Use the query type code to determine whether the query is run against
@@ -2533,19 +3084,29 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
}
/* Extract the parameters to be passed into the query */
+ last_param = riinfo->nkeys;
if (newslot)
{
ri_ExtractValues(source_rel, newslot, riinfo, source_is_pk,
vals, nulls);
if (oldslot)
+ {
ri_ExtractValues(source_rel, oldslot, riinfo, source_is_pk,
vals + riinfo->nkeys, nulls + riinfo->nkeys);
+ last_param = 2 * riinfo->nkeys;
+ }
}
else
{
ri_ExtractValues(source_rel, oldslot, riinfo, source_is_pk,
vals, nulls);
}
+ /* Add one more query param if we are using FOR PORTION OF */
+ if (hasForPortionOf)
+ {
+ vals[last_param] = forPortionOf;
+ nulls[last_param] = ' ';
+ }
/*
* In READ COMMITTED mode, we just need to use an up-to-date regular
@@ -3244,8 +3805,14 @@ RI_FKey_trigger_type(Oid tgfoid)
case F_RI_FKEY_SETDEFAULT_UPD:
case F_RI_FKEY_NOACTION_DEL:
case F_RI_FKEY_NOACTION_UPD:
+ case F_TRI_FKEY_CASCADE_DEL:
+ case F_TRI_FKEY_CASCADE_UPD:
case F_TRI_FKEY_RESTRICT_DEL:
case F_TRI_FKEY_RESTRICT_UPD:
+ case F_TRI_FKEY_SETNULL_DEL:
+ case F_TRI_FKEY_SETNULL_UPD:
+ case F_TRI_FKEY_SETDEFAULT_DEL:
+ case F_TRI_FKEY_SETDEFAULT_UPD:
case F_TRI_FKEY_NOACTION_DEL:
case F_TRI_FKEY_NOACTION_UPD:
return RI_TRIGGER_PK;
@@ -3259,3 +3826,50 @@ RI_FKey_trigger_type(Oid tgfoid)
return RI_TRIGGER_NONE;
}
+
+/*
+ * fpo_targets_pk_range
+ *
+ * Returns true iff the primary key referenced by riinfo includes the range
+ * column targeted by the FOR PORTION OF clause (according to tg_temporal).
+ */
+static bool
+fpo_targets_pk_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo)
+{
+ if (tg_temporal == NULL)
+ return false;
+
+ return riinfo->pk_attnums[riinfo->nkeys - 1] == tg_temporal->fp_rangeAttno;
+}
+
+/*
+ * restrict_cascading_range -
+ *
+ * Returns a Datum of RangeTypeP holding the appropriate timespan
+ * to target child records when we CASCADE/SET NULL/SET DEFAULT.
+ *
+ * In a normal UPDATE/DELETE this should be the parent's own valid time,
+ * but if there was a FOR PORTION OF clause, then we should use that to
+ * trim down the parent's span further.
+ */
+static Datum
+restrict_cascading_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo, TupleTableSlot *oldslot)
+{
+ Datum pkRecordRange;
+ bool isnull;
+
+ pkRecordRange = slot_getattr(oldslot, riinfo->pk_attnums[riinfo->nkeys - 1], &isnull);
+ if (isnull)
+ elog(ERROR, "application time should not be null");
+
+ if (fpo_targets_pk_range(tg_temporal, riinfo))
+ {
+ RangeType *r1 = DatumGetRangeTypeP(pkRecordRange);
+ RangeType *r2 = DatumGetRangeTypeP(tg_temporal->fp_targetRange);
+ Oid rngtypid = RangeTypeGetOid(r1);
+ TypeCacheEntry *typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO);
+ return RangeTypePGetDatum(range_intersect_internal(typcache, r1, r2));
+ }
+ else
+ return pkRecordRange;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3132468a9c..725eca126d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3949,6 +3949,12 @@
{ oid => '6123', descr => 'temporal referential integrity FOREIGN KEY ... REFERENCES',
proname => 'TRI_FKey_check_upd', provolatile => 'v', prorettype => 'trigger',
proargtypes => '', prosrc => 'TRI_FKey_check_upd' },
+{ oid => '6124', descr => 'temporal referential integrity ON DELETE CASCADE',
+ proname => 'TRI_FKey_cascade_del', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'TRI_FKey_cascade_del' },
+{ oid => '6125', descr => 'temporal referential integrity ON UPDATE CASCADE',
+ proname => 'TRI_FKey_cascade_upd', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'TRI_FKey_cascade_upd' },
{ oid => '6126', descr => 'temporal referential integrity ON DELETE RESTRICT',
proname => 'TRI_FKey_restrict_del', provolatile => 'v',
prorettype => 'trigger', proargtypes => '',
@@ -3957,6 +3963,20 @@
proname => 'TRI_FKey_restrict_upd', provolatile => 'v',
prorettype => 'trigger', proargtypes => '',
prosrc => 'TRI_FKey_restrict_upd' },
+{ oid => '6128', descr => 'temporal referential integrity ON DELETE SET NULL',
+ proname => 'TRI_FKey_setnull_del', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'TRI_FKey_setnull_del' },
+{ oid => '6129', descr => 'temporal referential integrity ON UPDATE SET NULL',
+ proname => 'TRI_FKey_setnull_upd', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'TRI_FKey_setnull_upd' },
+{ oid => '6130', descr => 'temporal referential integrity ON DELETE SET DEFAULT',
+ proname => 'TRI_FKey_setdefault_del', provolatile => 'v',
+ prorettype => 'trigger', proargtypes => '',
+ prosrc => 'TRI_FKey_setdefault_del' },
+{ oid => '6131', descr => 'temporal referential integrity ON UPDATE SET DEFAULT',
+ proname => 'TRI_FKey_setdefault_upd', provolatile => 'v',
+ prorettype => 'trigger', proargtypes => '',
+ prosrc => 'TRI_FKey_setdefault_upd' },
{ oid => '6132', descr => 'temporal referential integrity ON DELETE NO ACTION',
proname => 'TRI_FKey_noaction_del', provolatile => 'v',
prorettype => 'trigger', proargtypes => '',
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 04b4b02f2a..5e3f89921d 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -148,6 +148,7 @@ SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'te
CREATE UNIQUE INDEX temporal_rng2_uq ON temporal_rng2 USING gist (id, valid_at)
(1 row)
+DROP TABLE temporal_rng2;
-- UNIQUE with two columns plus a range:
CREATE TABLE temporal_rng3 (
id1 int4range,
@@ -716,7 +717,62 @@ ALTER TABLE temporal_fk_rng2rng
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng
ON DELETE CASCADE ON UPDATE CASCADE;
-ERROR: action not supported for temporal foreign keys
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[4,4]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [4,5) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8)
+ [4,5) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)
+ [4,5) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)
+(3 rows)
+
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[4,4]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [4,5) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8)
+ [4,5) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [7,8)
+ [4,5) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [7,8)
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]');
+UPDATE temporal_rng SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[10,10]';
+ id | valid_at | parent_id
+---------+---------------------------------------------------------+-----------
+ [10,11) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [16,17)
+ [10,11) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [15,16)
+(2 rows)
+
+-- test FK parent deletes CASCADE
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[5,5]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [8,9)
+ [5,6) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)
+(2 rows)
+
+DELETE FROM temporal_rng WHERE id = '[8,8]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[5,5]';
+ id | valid_at | parent_id
+----+----------+-----------
+(0 rows)
+
+INSERT INTO temporal_rng VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]');
+DELETE FROM temporal_rng WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[11,11]';
+ id | valid_at | parent_id
+---------+---------------------------------------------------------+-----------
+ [11,12) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [17,18)
+(1 row)
+
-- test FK parent updates SET NULL
INSERT INTO temporal_rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'));
INSERT INTO temporal_fk_rng2rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]');
@@ -726,7 +782,67 @@ ALTER TABLE temporal_fk_rng2rng
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng
ON DELETE SET NULL ON UPDATE SET NULL;
-ERROR: action not supported for temporal foreign keys
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[6,6]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [6,7) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |
+ [6,7) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [9,10)
+ [6,7) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [9,10)
+(3 rows)
+
+UPDATE temporal_rng SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[6,6]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [6,7) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |
+ [6,7) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") |
+ [6,7) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") |
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]');
+UPDATE temporal_rng SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[12,12]';
+ id | valid_at | parent_id
+---------+---------------------------------------------------------+-----------
+ [12,13) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") |
+ [12,13) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [18,19)
+(2 rows)
+
+-- test FK parent deletes SET NULL
+INSERT INTO temporal_rng VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[7,7]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [7,8) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |
+ [7,8) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [11,12)
+ [7,8) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [11,12)
+(3 rows)
+
+DELETE FROM temporal_rng WHERE id = '[11,11]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[7,7]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [7,8) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |
+ [7,8) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") |
+ [7,8) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") |
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]');
+DELETE FROM temporal_rng WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[13,13]';
+ id | valid_at | parent_id
+---------+---------------------------------------------------------+-----------
+ [13,14) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") |
+ [13,14) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [20,21)
+(2 rows)
+
-- test FK parent updates SET DEFAULT
INSERT INTO temporal_rng VALUES ('[-1,-1]', tsrange(null, null));
INSERT INTO temporal_rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'));
@@ -738,7 +854,95 @@ ALTER TABLE temporal_fk_rng2rng
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng
ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
-ERROR: action not supported for temporal foreign keys
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[8,8]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [8,9) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [8,9) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [12,13)
+ [8,9) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [12,13)
+(3 rows)
+
+UPDATE temporal_rng SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[8,8]';
+ id | valid_at | parent_id
+-------+---------------------------------------------------------+-----------
+ [8,9) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [8,9) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0)
+ [8,9) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0)
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]');
+UPDATE temporal_rng SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[14,14]';
+ id | valid_at | parent_id
+---------+---------------------------------------------------------+-----------
+ [14,15) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [14,15) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [22,23)
+(2 rows)
+
+-- test FK parent deletes SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[9,9]';
+ id | valid_at | parent_id
+--------+---------------------------------------------------------+-----------
+ [9,10) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [9,10) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [14,15)
+ [9,10) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [14,15)
+(3 rows)
+
+DELETE FROM temporal_rng WHERE id = '[14,14]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[9,9]';
+ id | valid_at | parent_id
+--------+---------------------------------------------------------+-----------
+ [9,10) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [9,10) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0)
+ [9,10) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0)
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]');
+DELETE FROM temporal_rng WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[15,15]';
+ id | valid_at | parent_id
+---------+---------------------------------------------------------+-----------
+ [15,16) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [15,16) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [24,25)
+(2 rows)
+
+-- FK with a custom range type
+CREATE TYPE mydaterange AS range(subtype=date);
+CREATE TABLE temporal_rng2 (
+ id int4range,
+ valid_at mydaterange,
+ CONSTRAINT temporal_rng2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk2_rng2rng (
+ id int4range,
+ valid_at mydaterange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk2_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk2_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng2 (id, PERIOD valid_at) ON DELETE CASCADE
+);
+INSERT INTO temporal_rng2 VALUES ('[8,8]', mydaterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[5,5]', mydaterange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[5,5]';
+ id | valid_at | parent_id
+-------+-------------------------+-----------
+ [5,6) | [01-01-2018,01-01-2019) | [8,9)
+ [5,6) | [01-01-2020,01-01-2021) | [8,9)
+(2 rows)
+
+DROP TABLE temporal_fk2_rng2rng;
+DROP TABLE temporal_rng2;
+DROP TYPE mydaterange;
-- FK between partitioned tables
CREATE TABLE temporal_partitioned_rng (
id int4range,
@@ -746,8 +950,8 @@ CREATE TABLE temporal_partitioned_rng (
name text,
CONSTRAINT temporal_paritioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
) PARTITION BY LIST (id);
-CREATE TABLE tp1 partition OF temporal_partitioned_rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
-CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+CREATE TABLE tp1 partition OF temporal_partitioned_rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]', '[13,13]', '[15,15]', '[17,17]', '[19,19]', '[21,21]', '[23,23]');
+CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[0,0]', '[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]', '[14,14]', '[16,16]', '[18,18]', '[20,20]', '[22,22]', '[24,24]');
INSERT INTO temporal_partitioned_rng VALUES
('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
@@ -760,8 +964,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
CONSTRAINT temporal_partitioned_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_partitioned_rng (id, PERIOD valid_at)
) PARTITION BY LIST (id);
-CREATE TABLE tfkp1 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
-CREATE TABLE tfkp2 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+CREATE TABLE tfkp1 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]', '[13,13]', '[15,15]', '[17,17]', '[19,19]', '[21,21]', '[23,23]');
+CREATE TABLE tfkp2 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[0,0]', '[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]', '[14,14]', '[16,16]', '[18,18]', '[20,20]', '[22,22]', '[24,24]');
-- partitioned FK child inserts
INSERT INTO temporal_partitioned_fk_rng2rng VALUES
('[1,1]', daterange('2000-01-01', '2000-02-15'), '[1,1]'),
@@ -796,7 +1000,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
-- should fail:
UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
-ERROR: update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR: update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey1" on table "temporal_partitioned_fk_rng2rng"
DETAIL: Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
-- clean up:
DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
@@ -808,7 +1012,7 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-
DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
-- should fail:
DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
-ERROR: update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR: update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey1" on table "temporal_partitioned_fk_rng2rng"
DETAIL: Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
-- clean up:
DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
@@ -830,7 +1034,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
-- should fail:
UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
-ERROR: update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR: update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey1" on table "temporal_partitioned_fk_rng2rng"
DETAIL: Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
-- clean up:
DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
@@ -842,35 +1046,214 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-
DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
-- should fail:
DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
-ERROR: update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR: update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey1" on table "temporal_partitioned_fk_rng2rng"
DETAIL: Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
-- partitioned FK parent updates CASCADE
+INSERT INTO temporal_partitioned_rng VALUES ('[6,6]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[4,4]', daterange('2018-01-01', '2021-01-01'), '[6,6]');
ALTER TABLE temporal_partitioned_fk_rng2rng
DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_partitioned_rng
ON DELETE CASCADE ON UPDATE CASCADE;
-ERROR: action not supported for temporal foreign keys
+UPDATE temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[4,4]';
+ id | valid_at | parent_id
+-------+-------------------------+-----------
+ [4,5) | [01-01-2019,01-01-2020) | [7,8)
+ [4,5) | [01-01-2018,01-01-2019) | [6,7)
+ [4,5) | [01-01-2020,01-01-2021) | [6,7)
+(3 rows)
+
+UPDATE temporal_partitioned_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[4,4]';
+ id | valid_at | parent_id
+-------+-------------------------+-----------
+ [4,5) | [01-01-2019,01-01-2020) | [7,8)
+ [4,5) | [01-01-2018,01-01-2019) | [7,8)
+ [4,5) | [01-01-2020,01-01-2021) | [7,8)
+(3 rows)
+
+INSERT INTO temporal_partitioned_rng VALUES ('[15,15]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[15,15]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[10,10]', daterange('2018-01-01', '2021-01-01'), '[15,15]');
+UPDATE temporal_partitioned_rng SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[10,10]';
+ id | valid_at | parent_id
+---------+-------------------------+-----------
+ [10,11) | [01-01-2018,01-01-2020) | [16,17)
+ [10,11) | [01-01-2020,01-01-2021) | [15,16)
+(2 rows)
+
-- partitioned FK parent deletes CASCADE
+INSERT INTO temporal_partitioned_rng VALUES ('[8,8]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[5,5]', daterange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[5,5]';
+ id | valid_at | parent_id
+-------+-------------------------+-----------
+ [5,6) | [01-01-2018,01-01-2019) | [8,9)
+ [5,6) | [01-01-2020,01-01-2021) | [8,9)
+(2 rows)
+
+DELETE FROM temporal_partitioned_rng WHERE id = '[8,8]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[5,5]';
+ id | valid_at | parent_id
+----+----------+-----------
+(0 rows)
+
+INSERT INTO temporal_partitioned_rng VALUES ('[17,17]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[17,17]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[11,11]', daterange('2018-01-01', '2021-01-01'), '[17,17]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[11,11]';
+ id | valid_at | parent_id
+---------+-------------------------+-----------
+ [11,12) | [01-01-2020,01-01-2021) | [17,18)
+(1 row)
+
-- partitioned FK parent updates SET NULL
+INSERT INTO temporal_partitioned_rng VALUES ('[9,9]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[6,6]', daterange('2018-01-01', '2021-01-01'), '[9,9]');
ALTER TABLE temporal_partitioned_fk_rng2rng
DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_partitioned_rng
ON DELETE SET NULL ON UPDATE SET NULL;
-ERROR: action not supported for temporal foreign keys
+UPDATE temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[6,6]';
+ id | valid_at | parent_id
+-------+-------------------------+-----------
+ [6,7) | [01-01-2019,01-01-2020) |
+ [6,7) | [01-01-2018,01-01-2019) | [9,10)
+ [6,7) | [01-01-2020,01-01-2021) | [9,10)
+(3 rows)
+
+UPDATE temporal_partitioned_rng SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[6,6]';
+ id | valid_at | parent_id
+-------+-------------------------+-----------
+ [6,7) | [01-01-2019,01-01-2020) |
+ [6,7) | [01-01-2018,01-01-2019) |
+ [6,7) | [01-01-2020,01-01-2021) |
+(3 rows)
+
+INSERT INTO temporal_partitioned_rng VALUES ('[18,18]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[18,18]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[12,12]', daterange('2018-01-01', '2021-01-01'), '[18,18]');
+UPDATE temporal_partitioned_rng SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[12,12]';
+ id | valid_at | parent_id
+---------+-------------------------+-----------
+ [12,13) | [01-01-2018,01-01-2020) |
+ [12,13) | [01-01-2020,01-01-2021) | [18,19)
+(2 rows)
+
-- partitioned FK parent deletes SET NULL
+INSERT INTO temporal_partitioned_rng VALUES ('[11,11]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[7,7]', daterange('2018-01-01', '2021-01-01'), '[11,11]');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[7,7]';
+ id | valid_at | parent_id
+-------+-------------------------+-----------
+ [7,8) | [01-01-2019,01-01-2020) |
+ [7,8) | [01-01-2018,01-01-2019) | [11,12)
+ [7,8) | [01-01-2020,01-01-2021) | [11,12)
+(3 rows)
+
+DELETE FROM temporal_partitioned_rng WHERE id = '[11,11]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[7,7]';
+ id | valid_at | parent_id
+-------+-------------------------+-----------
+ [7,8) | [01-01-2019,01-01-2020) |
+ [7,8) | [01-01-2018,01-01-2019) |
+ [7,8) | [01-01-2020,01-01-2021) |
+(3 rows)
+
+INSERT INTO temporal_partitioned_rng VALUES ('[20,20]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[20,20]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[13,13]', daterange('2018-01-01', '2021-01-01'), '[20,20]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[13,13]';
+ id | valid_at | parent_id
+---------+-------------------------+-----------
+ [13,14) | [01-01-2018,01-01-2020) |
+ [13,14) | [01-01-2020,01-01-2021) | [20,21)
+(2 rows)
+
-- partitioned FK parent updates SET DEFAULT
+INSERT INTO temporal_partitioned_rng VALUES ('[0,0]', daterange(null, null));
+INSERT INTO temporal_partitioned_rng VALUES ('[12,12]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[8,8]', daterange('2018-01-01', '2021-01-01'), '[12,12]');
ALTER TABLE temporal_partitioned_fk_rng2rng
- ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+ ALTER COLUMN parent_id SET DEFAULT '[0,0]',
DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_partitioned_rng
ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
-ERROR: action not supported for temporal foreign keys
+UPDATE temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[8,8]';
+ id | valid_at | parent_id
+-------+-------------------------+-----------
+ [8,9) | [01-01-2019,01-01-2020) | [0,1)
+ [8,9) | [01-01-2018,01-01-2019) | [12,13)
+ [8,9) | [01-01-2020,01-01-2021) | [12,13)
+(3 rows)
+
+UPDATE temporal_partitioned_rng SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[8,8]';
+ id | valid_at | parent_id
+-------+-------------------------+-----------
+ [8,9) | [01-01-2019,01-01-2020) | [0,1)
+ [8,9) | [01-01-2018,01-01-2019) | [0,1)
+ [8,9) | [01-01-2020,01-01-2021) | [0,1)
+(3 rows)
+
+INSERT INTO temporal_partitioned_rng VALUES ('[22,22]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[22,22]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[14,14]', daterange('2018-01-01', '2021-01-01'), '[22,22]');
+UPDATE temporal_partitioned_rng SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[14,14]';
+ id | valid_at | parent_id
+---------+-------------------------+-----------
+ [14,15) | [01-01-2018,01-01-2020) | [0,1)
+ [14,15) | [01-01-2020,01-01-2021) | [22,23)
+(2 rows)
+
-- partitioned FK parent deletes SET DEFAULT
+INSERT INTO temporal_partitioned_rng VALUES ('[14,14]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[9,9]', daterange('2018-01-01', '2021-01-01'), '[14,14]');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[9,9]';
+ id | valid_at | parent_id
+--------+-------------------------+-----------
+ [9,10) | [01-01-2019,01-01-2020) | [0,1)
+ [9,10) | [01-01-2018,01-01-2019) | [14,15)
+ [9,10) | [01-01-2020,01-01-2021) | [14,15)
+(3 rows)
+
+DELETE FROM temporal_partitioned_rng WHERE id = '[14,14]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[9,9]';
+ id | valid_at | parent_id
+--------+-------------------------+-----------
+ [9,10) | [01-01-2019,01-01-2020) | [0,1)
+ [9,10) | [01-01-2018,01-01-2019) | [0,1)
+ [9,10) | [01-01-2020,01-01-2021) | [0,1)
+(3 rows)
+
+INSERT INTO temporal_partitioned_rng VALUES ('[24,24]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[24,24]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[15,15]', daterange('2018-01-01', '2021-01-01'), '[24,24]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[15,15]';
+ id | valid_at | parent_id
+---------+-------------------------+-----------
+ [15,16) | [01-01-2018,01-01-2020) | [0,1)
+ [15,16) | [01-01-2020,01-01-2021) | [24,25)
+(2 rows)
+
DROP TABLE temporal_partitioned_fk_rng2rng;
DROP TABLE temporal_partitioned_rng;
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 1cd272a575..e485e3f891 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -103,6 +103,7 @@ CREATE TABLE temporal_rng2 (
\d temporal_rng2
SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+DROP TABLE temporal_rng2;
-- UNIQUE with two columns plus a range:
CREATE TABLE temporal_rng3 (
@@ -572,6 +573,28 @@ ALTER TABLE temporal_fk_rng2rng
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng
ON DELETE CASCADE ON UPDATE CASCADE;
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[4,4]';
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[4,4]';
+INSERT INTO temporal_rng VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]');
+UPDATE temporal_rng SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[10,10]';
+
+-- test FK parent deletes CASCADE
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[8,8]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]');
+DELETE FROM temporal_rng WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[11,11]';
-- test FK parent updates SET NULL
INSERT INTO temporal_rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'));
@@ -582,6 +605,28 @@ ALTER TABLE temporal_fk_rng2rng
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng
ON DELETE SET NULL ON UPDATE SET NULL;
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[6,6]';
+UPDATE temporal_rng SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[6,6]';
+INSERT INTO temporal_rng VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]');
+UPDATE temporal_rng SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[12,12]';
+
+-- test FK parent deletes SET NULL
+INSERT INTO temporal_rng VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[7,7]';
+DELETE FROM temporal_rng WHERE id = '[11,11]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[7,7]';
+INSERT INTO temporal_rng VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]');
+DELETE FROM temporal_rng WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[13,13]';
-- test FK parent updates SET DEFAULT
INSERT INTO temporal_rng VALUES ('[-1,-1]', tsrange(null, null));
@@ -594,6 +639,54 @@ ALTER TABLE temporal_fk_rng2rng
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng
ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[8,8]';
+UPDATE temporal_rng SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[8,8]';
+INSERT INTO temporal_rng VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]');
+UPDATE temporal_rng SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[14,14]';
+
+-- test FK parent deletes SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[9,9]';
+DELETE FROM temporal_rng WHERE id = '[14,14]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[9,9]';
+INSERT INTO temporal_rng VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]');
+DELETE FROM temporal_rng WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[15,15]';
+
+-- FK with a custom range type
+
+CREATE TYPE mydaterange AS range(subtype=date);
+
+CREATE TABLE temporal_rng2 (
+ id int4range,
+ valid_at mydaterange,
+ CONSTRAINT temporal_rng2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk2_rng2rng (
+ id int4range,
+ valid_at mydaterange,
+ parent_id int4range,
+ CONSTRAINT temporal_fk2_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+ CONSTRAINT temporal_fk2_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+ REFERENCES temporal_rng2 (id, PERIOD valid_at) ON DELETE CASCADE
+);
+INSERT INTO temporal_rng2 VALUES ('[8,8]', mydaterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[5,5]', mydaterange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[5,5]';
+
+DROP TABLE temporal_fk2_rng2rng;
+DROP TABLE temporal_rng2;
+DROP TYPE mydaterange;
-- FK between partitioned tables
@@ -603,8 +696,8 @@ CREATE TABLE temporal_partitioned_rng (
name text,
CONSTRAINT temporal_paritioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
) PARTITION BY LIST (id);
-CREATE TABLE tp1 partition OF temporal_partitioned_rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
-CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+CREATE TABLE tp1 partition OF temporal_partitioned_rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]', '[13,13]', '[15,15]', '[17,17]', '[19,19]', '[21,21]', '[23,23]');
+CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[0,0]', '[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]', '[14,14]', '[16,16]', '[18,18]', '[20,20]', '[22,22]', '[24,24]');
INSERT INTO temporal_partitioned_rng VALUES
('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
@@ -618,8 +711,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
CONSTRAINT temporal_partitioned_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_partitioned_rng (id, PERIOD valid_at)
) PARTITION BY LIST (id);
-CREATE TABLE tfkp1 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
-CREATE TABLE tfkp2 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+CREATE TABLE tfkp1 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]', '[13,13]', '[15,15]', '[17,17]', '[19,19]', '[21,21]', '[23,23]');
+CREATE TABLE tfkp2 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[0,0]', '[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]', '[14,14]', '[16,16]', '[18,18]', '[20,20]', '[22,22]', '[24,24]');
-- partitioned FK child inserts
@@ -703,37 +796,107 @@ DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange
-- partitioned FK parent updates CASCADE
+INSERT INTO temporal_partitioned_rng VALUES ('[6,6]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[4,4]', daterange('2018-01-01', '2021-01-01'), '[6,6]');
ALTER TABLE temporal_partitioned_fk_rng2rng
DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_partitioned_rng
ON DELETE CASCADE ON UPDATE CASCADE;
+UPDATE temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[4,4]';
+UPDATE temporal_partitioned_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[4,4]';
+INSERT INTO temporal_partitioned_rng VALUES ('[15,15]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[15,15]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[10,10]', daterange('2018-01-01', '2021-01-01'), '[15,15]');
+UPDATE temporal_partitioned_rng SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[10,10]';
-- partitioned FK parent deletes CASCADE
+INSERT INTO temporal_partitioned_rng VALUES ('[8,8]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[5,5]', daterange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[8,8]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[5,5]';
+INSERT INTO temporal_partitioned_rng VALUES ('[17,17]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[17,17]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[11,11]', daterange('2018-01-01', '2021-01-01'), '[17,17]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[11,11]';
+
-- partitioned FK parent updates SET NULL
+INSERT INTO temporal_partitioned_rng VALUES ('[9,9]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[6,6]', daterange('2018-01-01', '2021-01-01'), '[9,9]');
ALTER TABLE temporal_partitioned_fk_rng2rng
DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_partitioned_rng
ON DELETE SET NULL ON UPDATE SET NULL;
+UPDATE temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[6,6]';
+UPDATE temporal_partitioned_rng SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[6,6]';
+INSERT INTO temporal_partitioned_rng VALUES ('[18,18]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[18,18]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[12,12]', daterange('2018-01-01', '2021-01-01'), '[18,18]');
+UPDATE temporal_partitioned_rng SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[12,12]';
-- partitioned FK parent deletes SET NULL
+INSERT INTO temporal_partitioned_rng VALUES ('[11,11]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[7,7]', daterange('2018-01-01', '2021-01-01'), '[11,11]');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[7,7]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[11,11]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[7,7]';
+INSERT INTO temporal_partitioned_rng VALUES ('[20,20]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[20,20]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[13,13]', daterange('2018-01-01', '2021-01-01'), '[20,20]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[13,13]';
+
-- partitioned FK parent updates SET DEFAULT
+INSERT INTO temporal_partitioned_rng VALUES ('[0,0]', daterange(null, null));
+INSERT INTO temporal_partitioned_rng VALUES ('[12,12]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[8,8]', daterange('2018-01-01', '2021-01-01'), '[12,12]');
ALTER TABLE temporal_partitioned_fk_rng2rng
- ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+ ALTER COLUMN parent_id SET DEFAULT '[0,0]',
DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_partitioned_rng
ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+UPDATE temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[8,8]';
+UPDATE temporal_partitioned_rng SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[8,8]';
+INSERT INTO temporal_partitioned_rng VALUES ('[22,22]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[22,22]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[14,14]', daterange('2018-01-01', '2021-01-01'), '[22,22]');
+UPDATE temporal_partitioned_rng SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[14,14]';
-- partitioned FK parent deletes SET DEFAULT
+INSERT INTO temporal_partitioned_rng VALUES ('[14,14]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[9,9]', daterange('2018-01-01', '2021-01-01'), '[14,14]');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[9,9]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[14,14]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[9,9]';
+INSERT INTO temporal_partitioned_rng VALUES ('[24,24]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[24,24]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[15,15]', daterange('2018-01-01', '2021-01-01'), '[24,24]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[15,15]';
+
DROP TABLE temporal_partitioned_fk_rng2rng;
DROP TABLE temporal_partitioned_rng;
--
2.25.1
[text/x-patch] v13-0005-Add-PERIODs.patch (130.0K, ../../[email protected]/6-v13-0005-Add-PERIODs.patch)
download | inline diff:
From f9b15993304296c0018c1bceb4e496234447f8c8 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Sat, 9 Jan 2021 21:59:43 -0800
Subject: [PATCH v13 5/5] Add PERIODs
- Added parsing for SQL:2011 syntax to define an application-time PERIOD on a
table (in both CREATE TABLE and ALTER TABLE). Make sure we create the PERIOD
after columns are known (since PERIODs can refer to them) but before
constraints are handled (since PERIODs can appear in them).
- Added ALTER TABLE DROP support for PERIODs.
- Created postgres.pg_period table.
- Created information_schema.periods view.
- Added pg_dump support.
- Added tests and documentation.
- Automatically define a constraint for each PERIOD requiring the start column
to be less than the end column.
- When creating a PERIOD, choose an appropriate range type we can use to
implement PERIOD-related operations. You can choose one explicitly if there
is ambiguity (due to multiple range types created over the same base type).
---
doc/src/sgml/catalogs.sgml | 112 +++
doc/src/sgml/ddl.sgml | 58 ++
doc/src/sgml/information_schema.sgml | 63 ++
doc/src/sgml/ref/alter_table.sgml | 25 +
doc/src/sgml/ref/comment.sgml | 2 +
doc/src/sgml/ref/create_table.sgml | 40 +
src/backend/catalog/Makefile | 2 +
src/backend/catalog/aclchk.c | 2 +
src/backend/catalog/dependency.c | 9 +
src/backend/catalog/heap.c | 76 ++
src/backend/catalog/information_schema.sql | 23 +-
src/backend/catalog/meson.build | 1 +
src/backend/catalog/objectaddress.c | 73 ++
src/backend/catalog/pg_period.c | 132 +++
src/backend/catalog/sql_features.txt | 2 +-
src/backend/commands/alter.c | 1 +
src/backend/commands/comment.c | 10 +
src/backend/commands/dropcmds.c | 1 +
src/backend/commands/event_trigger.c | 4 +
src/backend/commands/seclabel.c | 1 +
src/backend/commands/tablecmds.c | 765 +++++++++++++++++-
src/backend/commands/view.c | 4 +-
src/backend/nodes/nodeFuncs.c | 3 +
src/backend/parser/gram.y | 43 +-
src/backend/parser/parse_relation.c | 10 +
src/backend/parser/parse_utilcmd.c | 151 +++-
src/backend/utils/cache/lsyscache.c | 87 ++
src/backend/utils/cache/syscache.c | 21 +
src/bin/pg_dump/pg_backup_archiver.c | 1 +
src/bin/pg_dump/pg_dump.c | 173 +++-
src/bin/pg_dump/pg_dump.h | 14 +
src/bin/pg_dump/pg_dump_sort.c | 7 +
src/bin/psql/describe.c | 35 +
src/include/catalog/dependency.h | 1 +
src/include/catalog/heap.h | 4 +
src/include/catalog/meson.build | 1 +
src/include/catalog/pg_index.h | 2 +-
src/include/catalog/pg_period.h | 54 ++
src/include/catalog/pg_range.h | 1 +
src/include/commands/tablecmds.h | 4 +-
src/include/nodes/parsenodes.h | 39 +-
src/include/parser/parse_utilcmd.h | 1 +
src/include/utils/lsyscache.h | 3 +
src/include/utils/syscache.h | 3 +
.../test_ddl_deparse/test_ddl_deparse.c | 6 +
src/test/regress/expected/periods.out | 173 ++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/periods.sql | 117 +++
48 files changed, 2318 insertions(+), 44 deletions(-)
create mode 100644 src/backend/catalog/pg_period.c
create mode 100644 src/include/catalog/pg_period.h
create mode 100644 src/test/regress/expected/periods.out
create mode 100644 src/test/regress/sql/periods.sql
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index d17ff51e28..6cb2d6106e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -230,6 +230,11 @@
<entry>information about partition key of tables</entry>
</row>
+ <row>
+ <entry><link linkend="catalog-pg-period"><structname>pg_period</structname></link></entry>
+ <entry>periods</entry>
+ </row>
+
<row>
<entry><link linkend="catalog-pg-policy"><structname>pg_policy</structname></link></entry>
<entry>row-security policies</entry>
@@ -5700,6 +5705,113 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
are simple references.
</para></entry>
</row>
+
+ </tbody>
+ </tgroup>
+ </table>
+ </sect1>
+
+
+ <sect1 id="catalog-pg-period">
+ <title><structname>pg_period</structname></title>
+
+ <indexterm zone="catalog-pg-period">
+ <primary>pg_period</primary>
+ </indexterm>
+
+ <para>
+ The catalog <structname>pg_period</structname> stores
+ information about system and application time periods.
+ </para>
+
+ <para>
+ Periods are described in <xref linkend="ddl-periods"/>.
+ </para>
+
+ <table>
+ <title><structname>pg_period</structname> Columns</title>
+
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>oid</structfield> <type>oid</type>
+ </para>
+ <para>
+ Row identifier
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>pername</structfield> <type>text</type>
+ </para>
+ <para>
+ Period name
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>perrelid</structfield> <type>oid</type>
+ (references <link linkend="catalog-pg-class"><structname>pg_class</structname></link>.<structfield>oid</structfield>)
+ </para>
+ <para>
+ The table this period belongs to
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>perstart</structfield> <type>int2</type>
+ (references <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>.<structfield>attnum</structfield>)
+ </para>
+ <para>
+ The number of the start column
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>perend</structfield> <type>int2</type>
+ (references <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>.<structfield>attnum</structfield>)
+ </para>
+ <para>
+ The number of the end column
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>perrngtype</structfield> <type>oid</type>
+ (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+ </para>
+ <para>
+ The OID of the range type associated with this period
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>perconstraint</structfield> <type>oid</type>
+ (references <link linkend="catalog-pg-constraint"><structname>pg_constraint</structname></link>.<structfield>oid</structfield>)
+ </para>
+ <para>
+ The OID of the period's <literal>CHECK</literal> constraint
+ </para></entry>
+ </row>
+
</tbody>
</tgroup>
</table>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 075ff32991..94a892ca29 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1263,6 +1263,64 @@ CREATE TABLE circles (
</sect2>
</sect1>
+ <sect1 id="ddl-periods">
+ <title>Periods</title>
+
+ <para>
+ Periods are definitions on a table that associate a period name with a start
+ column and an end column. Both columns must be of exactly the same type
+ (including collation), have a btree operator class, and the start column
+ value must be strictly less than the end column value.
+ </para>
+
+ <para>
+ There are two types of periods: application and system. System periods are
+ distinguished by their name which must be <literal>SYSTEM_TIME</literal>. Any
+ other name is an application period.
+ </para>
+
+ <sect2 id="ddl-periods-application-periods">
+ <title>Application Periods</title>
+
+ <indexterm>
+ <primary>period</primary>
+ <secondary>application</secondary>
+ </indexterm>
+
+ <para>
+ Application periods are defined on a table using the following syntax:
+ </para>
+
+<programlisting>
+CREATE TABLE billing_addresses (
+ customer_id integer,
+ address_id integer,
+ valid_from date,
+ valid_to date,
+ <emphasis>PERIOD FOR validity (valid_from, valid_to)</emphasis>
+);
+</programlisting>
+
+ <para>
+ Application periods can be used to define temporal primary and foreign keys.
+ Any table with a temporal primary key is a temporal table and supports temporal update and delete commands.
+ </para>
+ </sect2>
+
+ <sect2 id="ddl-periods-system-periods">
+ <title>System Periods</title>
+
+ <indexterm>
+ <primary>period</primary>
+ <secondary>system</secondary>
+ </indexterm>
+
+ <para>
+ Periods for <literal>SYSTEM_TIME</literal> are currently not implemented.
+ </para>
+ </sect2>
+ </sect1>
+
<sect1 id="ddl-system-columns">
<title>System Columns</title>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index d57a132340..8cc952a085 100644
--- a/doc/src/sgml/information_schema.sgml
+++ b/doc/src/sgml/information_schema.sgml
@@ -4162,6 +4162,69 @@ ORDER BY c.ordinal_position;
</table>
</sect1>
+ <sect1 id="infoschema-periods">
+ <title><literal>periods</literal></title>
+
+ <para>
+ The view <literal>parameters</literal> contains information about the
+ periods of all tables in the current database. The start and end column
+ names are only shown if the current user has access to them (by way of being
+ the owner or having some privilege).
+ </para>
+
+ <table>
+ <title><literal>periods</literal> Columns</title>
+
+ <tgroup cols="3">
+ <thead>
+ <row>
+ <entry>Name</entry>
+ <entry>Data Type</entry>
+ <entry>Description</entry>
+ </row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry><literal>table_catalog</literal></entry>
+ <entry><type>sql_identifier</type></entry>
+ <entry>Name of the database containing the period (always the current database)</entry>
+ </row>
+
+ <row>
+ <entry><literal>table_schema</literal></entry>
+ <entry><type>sql_identifier</type></entry>
+ <entry>Name of the schema containing the period</entry>
+ </row>
+
+ <row>
+ <entry><literal>table_name</literal></entry>
+ <entry><type>sql_identifier</type></entry>
+ <entry>Name of the table containing the period</entry>
+ </row>
+
+ <row>
+ <entry><literal>period_name</literal></entry>
+ <entry><type>sql_identifier</type></entry>
+ <entry>Name of the period</entry>
+ </row>
+
+ <row>
+ <entry><literal>start_column_name</literal></entry>
+ <entry><type>sql_identifier</type></entry>
+ <entry>Name of the start column for the period</entry>
+ </row>
+
+ <row>
+ <entry><literal>end_column_name</literal></entry>
+ <entry><type>sql_identifier</type></entry>
+ <entry>Name of the end column for the period</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+ </sect1>
+
<sect1 id="infoschema-referential-constraints">
<title><literal>referential_constraints</literal></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 2c4138e4e9..aee986efd5 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -60,6 +60,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
DROP CONSTRAINT [ IF EXISTS ] <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
+ ADD PERIOD FOR <replaceable class="parameter">period_name</replaceable> ( <replaceable class="parameter">start_column</replaceable>, <replaceable class="parameter">end_column</replaceable> ) [ WITH ( <replaceable class="parameter">period_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]
+ DROP PERIOD FOR <replaceable class="parameter">period_name</replaceable> [ RESTRICT | CASCADE ]
DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
ENABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
ENABLE REPLICA TRIGGER <replaceable class="parameter">trigger_name</replaceable>
@@ -571,6 +573,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-altertable-desc-add-period">
+ <term><literal>ADD PERIOD FOR</literal></term>
+ <listitem>
+ <para>
+ This form adds a new period to a table using the same syntax as
+ <xref linkend="sql-createtable"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="sql-altertable-desc-drop-period">
+ <term><literal>DROP PERIOD FOR</literal></term>
+ <listitem>
+ <para>
+ This form drops the specified period on a table. The start and end
+ columns will not be dropped by this command but the
+ <literal>CHECK</literal> constraint will be. You will need to say
+ <literal>CASCADE</literal> if anything outside the table depends on the
+ column.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-altertable-desc-disable-enable-trigger">
<term><literal>DISABLE</literal>/<literal>ENABLE [ REPLICA | ALWAYS ] TRIGGER</literal></term>
<listitem>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b13..49c2df9944 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -44,6 +44,7 @@ COMMENT ON
OPERATOR <replaceable class="parameter">operator_name</replaceable> (<replaceable class="parameter">left_type</replaceable>, <replaceable class="parameter">right_type</replaceable>) |
OPERATOR CLASS <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
OPERATOR FAMILY <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
+ PERIOD <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">period_name</replaceable> |
POLICY <replaceable class="parameter">policy_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
[ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
@@ -341,6 +342,7 @@ COMMENT ON OPERATOR ^ (text, text) IS 'Performs intersection of two texts';
COMMENT ON OPERATOR - (NONE, integer) IS 'Unary minus';
COMMENT ON OPERATOR CLASS int4ops USING btree IS '4 byte integer operators for btrees';
COMMENT ON OPERATOR FAMILY integer_ops USING btree IS 'all integer operators for btrees';
+COMMENT ON PERIOD my_table.my_column IS 'Sales promotion validity';
COMMENT ON POLICY my_policy ON mytable IS 'Filter rows by users';
COMMENT ON PROCEDURE my_proc (integer, integer) IS 'Runs a report';
COMMENT ON PUBLICATION alltables IS 'Publishes all operations on all tables';
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index fea8fc3c7a..c4a08b2ca6 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -23,6 +23,7 @@ PostgreSQL documentation
<synopsis>
CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable> ( [
{ <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+ | <replaceable>period_definition</replaceable>
| <replaceable>table_constraint</replaceable>
| LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
[, ... ]
@@ -37,6 +38,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable>
OF <replaceable class="parameter">type_name</replaceable> [ (
{ <replaceable class="parameter">column_name</replaceable> [ WITH OPTIONS ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+ | <replaceable>period_definition</replaceable>
| <replaceable>table_constraint</replaceable> }
[, ... ]
) ]
@@ -49,6 +51,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable>
PARTITION OF <replaceable class="parameter">parent_table</replaceable> [ (
{ <replaceable class="parameter">column_name</replaceable> [ WITH OPTIONS ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+ | <replaceable>period_definition</replaceable>
| <replaceable>table_constraint</replaceable> }
[, ... ]
) ] { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT }
@@ -73,6 +76,11 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
+<phrase>and <replaceable class="parameter">period_definition</replaceable> is:</phrase>
+
+PERIOD FOR { <replaceable class="parameter">period_name</replaceable> | SYSTEM_TIME } ( <replaceable class="parameter">column_name</replaceable>, <replaceable class="parameter">column_name</replaceable> )
+[ WITH ( <replaceable class="parameter">period_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]
+
<phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
[ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
@@ -144,6 +152,14 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
name as any existing data type in the same schema.
</para>
+ <para>
+ Periods my be defined on tables, specifying that two existing columns
+ represent start and end values for the period. Periods may have any name
+ that doesn't conflict with a column name, but the name
+ <literal>SYSTEM_TIME</literal> is special, used for versioning tables.
+ System periods are not yet implemented. See <xref linkend="ddl-periods"/> for more details.
+ </para>
+
<para>
The optional constraint clauses specify constraints (tests) that
new or updated rows must satisfy for an insert or update operation
@@ -804,6 +820,30 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</listitem>
</varlistentry>
+ <varlistentry id="sql-createtable-parms-period">
+ <term><literal>PERIOD FOR <replaceable class="parameter">period_name</replaceable> ( <replaceable class="parameter">column_name</replaceable>, <replaceable class="parameter">column_name</replaceable> ) [ WITH ( <replaceable class="parameter">period_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]</literal></term>
+ <listitem>
+ <para>
+ A period definition gives semantic meaning to two existing columns of
+ the table. It defines a "start column" and an "end column" where the
+ start value is strictly less than the end value. A
+ <literal>CHECK</literal> constraint is automatically created to enforce
+ this. You can specify the name of that constraint with the
+ <literal>check_constraint_name</literal> <replaceable class="parameter">period_option</replaceable>.
+ </para>
+
+ <para>
+ Both columns must have exactly the same type and must have a range type
+ defined from their base type. If there are several range types for that
+ base type, you must specify which one you want by using the
+ <literal>rangetype</literal> <replaceable class="parameter">period_option</replaceable>.
+ Any base type is allowed, as long as it has a range type, although it is
+ expected that most periods will use temporal types like <literal>timestamptz</literal>
+ or <literal>date</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="sql-createtable-parms-constraint">
<term><literal>CONSTRAINT <replaceable class="parameter">constraint_name</replaceable></literal></term>
<listitem>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 3e9994793d..500b80fd19 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -39,6 +39,7 @@ OBJS = \
pg_namespace.o \
pg_operator.o \
pg_parameter_acl.o \
+ pg_period.o \
pg_proc.o \
pg_publication.o \
pg_range.o \
@@ -111,6 +112,7 @@ CATALOG_HEADERS := \
pg_collation.h \
pg_parameter_acl.h \
pg_partitioned_table.h \
+ pg_period.h \
pg_range.h \
pg_transform.h \
pg_sequence.h \
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index d1f5dcd8be..bd13f16b86 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -2791,6 +2791,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DEFAULT:
case OBJECT_DEFACL:
case OBJECT_DOMCONSTRAINT:
+ case OBJECT_PERIOD:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
case OBJECT_ROLE:
@@ -2932,6 +2933,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DEFACL:
case OBJECT_DOMCONSTRAINT:
case OBJECT_PARAMETER_ACL:
+ case OBJECT_PERIOD:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
case OBJECT_ROLE:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index f8a136ba0a..232f77c96e 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -48,6 +48,7 @@
#include "catalog/pg_operator.h"
#include "catalog/pg_opfamily.h"
#include "catalog/pg_parameter_acl.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_policy.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_publication.h"
@@ -154,6 +155,7 @@ static const Oid object_classes[] = {
CastRelationId, /* OCLASS_CAST */
CollationRelationId, /* OCLASS_COLLATION */
ConstraintRelationId, /* OCLASS_CONSTRAINT */
+ PeriodRelationId, /* OCLASS_PERIOD */
ConversionRelationId, /* OCLASS_CONVERSION */
AttrDefaultRelationId, /* OCLASS_DEFAULT */
LanguageRelationId, /* OCLASS_LANGUAGE */
@@ -1443,6 +1445,10 @@ doDeletion(const ObjectAddress *object, int flags)
RemoveConstraintById(object->objectId);
break;
+ case OCLASS_PERIOD:
+ RemovePeriodById(object->objectId);
+ break;
+
case OCLASS_DEFAULT:
RemoveAttrDefaultById(object->objectId);
break;
@@ -2862,6 +2868,9 @@ getObjectClass(const ObjectAddress *object)
case ConstraintRelationId:
return OCLASS_CONSTRAINT;
+ case PeriodRelationId:
+ return OCLASS_PERIOD;
+
case ConversionRelationId:
return OCLASS_CONVERSION;
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 7148dd1787..ae7da0e821 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -49,6 +49,7 @@
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
#include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_statistic.h"
#include "catalog/pg_subscription_rel.h"
#include "catalog/pg_tablespace.h"
@@ -2039,6 +2040,81 @@ SetAttrMissing(Oid relid, char *attname, char *value)
table_close(tablerel, AccessExclusiveLock);
}
+/*
+ * Store a period of relation rel.
+ *
+ * Returns the OID of the new pg_period tuple.
+ */
+Oid
+StorePeriod(Relation rel, const char *periodname, AttrNumber startnum,
+ AttrNumber endnum, AttrNumber rangenum, Oid conoid)
+{
+ Datum values[Natts_pg_period];
+ bool nulls[Natts_pg_period];
+ Relation pg_period;
+ HeapTuple tuple;
+ Oid oid;
+ NameData pername;
+ ObjectAddress myself, referenced;
+
+ Assert(rangenum != InvalidAttrNumber);
+
+ namestrcpy(&pername, periodname);
+
+ MemSet(values, 0, sizeof(values));
+ MemSet(nulls, false, sizeof(nulls));
+
+ pg_period = table_open(PeriodRelationId, RowExclusiveLock);
+
+ oid = GetNewOidWithIndex(pg_period, AttrDefaultOidIndexId, Anum_pg_period_oid);
+ values[Anum_pg_period_oid - 1] = ObjectIdGetDatum(oid);
+ values[Anum_pg_period_pername - 1] = NameGetDatum(&pername);
+ values[Anum_pg_period_perrelid - 1] = RelationGetRelid(rel);
+ values[Anum_pg_period_perstart - 1] = startnum;
+ values[Anum_pg_period_perend - 1] = endnum;
+ values[Anum_pg_period_perrange - 1] = rangenum;
+ values[Anum_pg_period_perconstraint - 1] = conoid;
+
+ tuple = heap_form_tuple(RelationGetDescr(pg_period), values, nulls);
+ CatalogTupleInsert(pg_period, tuple);
+
+ ObjectAddressSet(myself, PeriodRelationId, oid);
+
+ /* Drop the period when the table is dropped. */
+ ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
+ recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
+
+ /* Forbid dropping the columns of the period. */
+ ObjectAddressSubSet(referenced, RelationRelationId, RelationGetRelid(rel), startnum);
+ recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+ ObjectAddressSubSet(referenced, RelationRelationId, RelationGetRelid(rel), endnum);
+ recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+ /*
+ * The range column is an implementation detail,
+ * but we can't use DEPENDENCY_INTERNAL
+ * because dropping the table will check for dependencies on all subobjects too
+ * (in findDependentObjects).
+ * But if we make an AUTO dependency one way we will auto-drop the column
+ * when we drop the PERIOD,
+ * and a NORMAL dependency the other way we will forbid dropping the column directly.
+ */
+ ObjectAddressSubSet(referenced, RelationRelationId, RelationGetRelid(rel), rangenum);
+ recordDependencyOn(&referenced, &myself, DEPENDENCY_AUTO);
+ recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+ /*
+ * The constraint is an implementation detail, so we mark it as such.
+ * (Note that myself and referenced are reversed for this one.)
+ */
+ ObjectAddressSet(referenced, ConstraintRelationId, conoid);
+ recordDependencyOn(&referenced, &myself, DEPENDENCY_INTERNAL);
+
+ table_close(pg_period, RowExclusiveLock);
+
+ return oid;
+}
+
/*
* Store a check-constraint expression for the given relation.
*
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index a06ec7a0a8..66f924e979 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -1200,7 +1200,28 @@ GRANT SELECT ON parameters TO PUBLIC;
* PERIODS view
*/
--- feature not supported
+CREATE VIEW periods AS
+ SELECT current_database()::information_schema.sql_identifier AS table_catalog,
+ nc.nspname::information_schema.sql_identifier AS table_schema,
+ c.relname::information_schema.sql_identifier AS table_name,
+ p.pername::information_schema.sql_identifier AS period_name,
+ CASE WHEN pg_has_role(c.relowner, 'USAGE')
+ OR has_column_privilege(sa.attrelid, sa.attnum, 'SELECT, INSERT, UPDATE, REFERENCES')
+ THEN sa.attname::information_schema.sql_identifier
+ END AS start_column_name,
+ CASE WHEN pg_has_role(c.relowner, 'USAGE')
+ OR has_column_privilege(ea.attrelid, ea.attnum, 'SELECT, INSERT, UPDATE, REFERENCES')
+ THEN ea.attname::information_schema.sql_identifier
+ END AS end_column_name
+ FROM pg_period AS p
+ JOIN pg_class AS c ON c.oid = p.perrelid
+ JOIN pg_namespace AS nc ON nc.oid = c.relnamespace
+ JOIN pg_attribute AS sa ON (sa.attrelid, sa.attnum) = (p.perrelid, p.perstart)
+ JOIN pg_attribute AS ea ON (ea.attrelid, ea.attnum) = (p.perrelid, p.perend)
+ WHERE NOT pg_is_other_temp_schema(nc.oid)
+ AND c.relkind IN ('r', 'v');
+
+GRANT SELECT ON periods TO PUBLIC;
/*
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index fa6609e577..c499cd2f5d 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -26,6 +26,7 @@ backend_sources += files(
'pg_namespace.c',
'pg_operator.c',
'pg_parameter_acl.c',
+ 'pg_period.c',
'pg_proc.c',
'pg_publication.c',
'pg_range.c',
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 715201f5a2..f5f88d5fdd 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -47,6 +47,7 @@
#include "catalog/pg_operator.h"
#include "catalog/pg_opfamily.h"
#include "catalog/pg_parameter_acl.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_policy.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_publication.h"
@@ -736,6 +737,10 @@ static const struct object_type_map
{
"domain constraint", OBJECT_DOMCONSTRAINT
},
+ /* OCLASS_PERIOD */
+ {
+ "period", OBJECT_PERIOD
+ },
/* OCLASS_CONVERSION */
{
"conversion", OBJECT_CONVERSION
@@ -1014,6 +1019,7 @@ get_object_address(ObjectType objtype, Node *object,
case OBJECT_TRIGGER:
case OBJECT_TABCONSTRAINT:
case OBJECT_POLICY:
+ case OBJECT_PERIOD:
address = get_object_address_relobject(objtype, castNode(List, object),
&relation, missing_ok);
break;
@@ -1512,6 +1518,13 @@ get_object_address_relobject(ObjectType objtype, List *object,
InvalidOid;
address.objectSubId = 0;
break;
+ case OBJECT_PERIOD:
+ address.classId = PeriodRelationId;
+ address.objectId = relation ?
+ get_relation_period_oid(reloid, depname, missing_ok) :
+ InvalidOid;
+ address.objectSubId = 0;
+ break;
default:
elog(ERROR, "unrecognized object type: %d", (int) objtype);
}
@@ -2329,6 +2342,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
case OBJECT_RULE:
case OBJECT_TRIGGER:
case OBJECT_TABCONSTRAINT:
+ case OBJECT_PERIOD:
case OBJECT_OPCLASS:
case OBJECT_OPFAMILY:
objnode = (Node *) name;
@@ -2439,6 +2453,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
case OBJECT_TRIGGER:
case OBJECT_POLICY:
case OBJECT_TABCONSTRAINT:
+ case OBJECT_PERIOD:
if (!object_ownercheck(RelationRelationId, RelationGetRelid(relation), roleid))
aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
RelationGetRelationName(relation));
@@ -3087,6 +3102,38 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
break;
}
+ case OCLASS_PERIOD:
+ {
+ HeapTuple perTup;
+ Form_pg_period per;
+
+ perTup = SearchSysCache1(PERIODOID,
+ ObjectIdGetDatum(object->objectId));
+ if (!HeapTupleIsValid(perTup))
+ elog(ERROR, "cache lookup failed for period %u",
+ object->objectId);
+ per = (Form_pg_period) GETSTRUCT(perTup);
+
+ if (OidIsValid(per->perrelid))
+ {
+ StringInfoData rel;
+
+ initStringInfo(&rel);
+ getRelationDescription(&rel, per->perrelid, false);
+ appendStringInfo(&buffer, _("period %s on %s"),
+ NameStr(per->pername), rel.data);
+ pfree(rel.data);
+ }
+ else
+ {
+ appendStringInfo(&buffer, _("period %s"),
+ NameStr(per->pername));
+ }
+
+ ReleaseSysCache(perTup);
+ break;
+ }
+
case OCLASS_CONVERSION:
{
HeapTuple conTup;
@@ -4451,6 +4498,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
missing_ok);
break;
+ case OCLASS_PERIOD:
+ appendStringInfoString(&buffer, "period");
+ break;
+
case OCLASS_CONVERSION:
appendStringInfoString(&buffer, "conversion");
break;
@@ -4958,6 +5009,28 @@ getObjectIdentityParts(const ObjectAddress *object,
break;
}
+ case OCLASS_PERIOD:
+ {
+ HeapTuple perTup;
+ Form_pg_period per;
+
+ perTup = SearchSysCache1(PERIODOID,
+ ObjectIdGetDatum(object->objectId));
+ if (!HeapTupleIsValid(perTup))
+ elog(ERROR, "cache lookup failed for period %u",
+ object->objectId);
+ per = (Form_pg_period) GETSTRUCT(perTup);
+
+ appendStringInfo(&buffer, "%s on ",
+ quote_identifier(NameStr(per->pername)));
+ getRelationIdentity(&buffer, per->perrelid, objname, false);
+ if (objname)
+ *objname = lappend(*objname, pstrdup(NameStr(per->pername)));
+
+ ReleaseSysCache(perTup);
+ break;
+ }
+
case OCLASS_CONVERSION:
{
HeapTuple conTup;
diff --git a/src/backend/catalog/pg_period.c b/src/backend/catalog/pg_period.c
new file mode 100644
index 0000000000..465b5b675e
--- /dev/null
+++ b/src/backend/catalog/pg_period.c
@@ -0,0 +1,132 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_period.c
+ * routines to support manipulation of the pg_period relation
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/catalog/pg_period.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_period.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+
+/*
+ * Delete a single period record.
+ */
+void
+RemovePeriodById(Oid periodId)
+{
+ Relation pg_period;
+ HeapTuple tup;
+
+ pg_period = table_open(PeriodRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(PERIODOID, ObjectIdGetDatum(periodId));
+ if (!HeapTupleIsValid(tup)) /* should not happen */
+ elog(ERROR, "cache lookup failed for period %u", periodId);
+
+ /* Fry the period itself */
+ CatalogTupleDelete(pg_period, &tup->t_self);
+
+ /* Clean up */
+ ReleaseSysCache(tup);
+ table_close(pg_period, RowExclusiveLock);
+}
+
+/*
+ * get_relation_period_oid
+ * Find a period on the specified relation with the specified name.
+ * Returns period's OID.
+ */
+Oid
+get_relation_period_oid(Oid relid, const char *pername, bool missing_ok)
+{
+ Relation pg_period;
+ HeapTuple tuple;
+ SysScanDesc scan;
+ ScanKeyData skey[2];
+ Oid perOid = InvalidOid;
+
+ /* Fetch the period tuple from pg_period. */
+ pg_period = table_open(PeriodRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_period_perrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relid));
+ ScanKeyInit(&skey[1],
+ Anum_pg_period_pername,
+ BTEqualStrategyNumber, F_NAMEEQ,
+ CStringGetDatum(pername));
+
+ scan = systable_beginscan(pg_period, PeriodRelidNameIndexId, true,
+ NULL, 2, skey);
+
+ /* There can be at most one matching row */
+ if (HeapTupleIsValid(tuple = systable_getnext(scan)))
+ perOid = ((Form_pg_period) GETSTRUCT(tuple))->oid;
+
+ systable_endscan(scan);
+
+ /* If no such period exists, complain */
+ if (!OidIsValid(perOid) && !missing_ok)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("period \"%s\" for table \"%s\" does not exist",
+ pername, get_rel_name(relid))));
+
+ table_close(pg_period, AccessShareLock);
+
+ return perOid;
+}
+
+/*
+ * get_period_attnos
+ * Get the attno of the GENERATED rangetype column
+ * for all PERIODs in this table.
+ */
+extern Bitmapset
+*get_period_attnos(Oid relid)
+{
+ Bitmapset *attnos = NULL;
+ Relation pg_period;
+ HeapTuple tuple;
+ SysScanDesc scan;
+ ScanKeyData skey[1];
+
+ pg_period = table_open(PeriodRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_period_perrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relid));
+
+ scan = systable_beginscan(pg_period, PeriodRelidNameIndexId, true,
+ NULL, 1, skey);
+
+ while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+ {
+ Form_pg_period period = (Form_pg_period) GETSTRUCT(tuple);
+
+ attnos = bms_add_member(attnos, period->perrange);
+ }
+
+ systable_endscan(scan);
+ table_close(pg_period, AccessShareLock);
+
+ return attnos;
+}
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index b33065d7bf..775090a6ec 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -449,7 +449,7 @@ T176 Sequence generator support NO supported except for NEXT VALUE FOR
T177 Sequence generator support: simple restart option YES
T178 Identity columns: simple restart option YES
T180 System-versioned tables NO
-T181 Application-time period tables NO
+T181 Application-time period tables YES
T191 Referential action RESTRICT YES
T200 Trigger DDL NO similar but not fully compatible
T201 Comparable data types for referential constraints YES
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index ff8d003876..2ebe458648 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -669,6 +669,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
case OCLASS_CAST:
case OCLASS_CONSTRAINT:
+ case OCLASS_PERIOD:
case OCLASS_DEFAULT:
case OCLASS_LANGUAGE:
case OCLASS_LARGEOBJECT:
diff --git a/src/backend/commands/comment.c b/src/backend/commands/comment.c
index 26c4d59202..452af1d413 100644
--- a/src/backend/commands/comment.c
+++ b/src/backend/commands/comment.c
@@ -102,6 +102,16 @@ CommentObject(CommentStmt *stmt)
RelationGetRelationName(relation)),
errdetail_relkind_not_supported(relation->rd_rel->relkind)));
break;
+
+ case OBJECT_PERIOD:
+ /* Periods can only go on tables */
+ if (relation->rd_rel->relkind != RELKIND_RELATION)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a table",
+ RelationGetRelationName(relation))));
+ break;
+
default:
break;
}
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 469a6c2ee9..1b9754e998 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -509,6 +509,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
case OBJECT_DOMCONSTRAINT:
case OBJECT_LARGEOBJECT:
case OBJECT_PARAMETER_ACL:
+ case OBJECT_PERIOD:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
case OBJECT_TABCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..19e6dfd37b 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -970,6 +970,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
case OBJECT_OPCLASS:
case OBJECT_OPERATOR:
case OBJECT_OPFAMILY:
+ case OBJECT_PERIOD:
case OBJECT_POLICY:
case OBJECT_PROCEDURE:
case OBJECT_PUBLICATION:
@@ -1028,6 +1029,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
case OCLASS_CAST:
case OCLASS_COLLATION:
case OCLASS_CONSTRAINT:
+ case OCLASS_PERIOD:
case OCLASS_CONVERSION:
case OCLASS_DEFAULT:
case OCLASS_LANGUAGE:
@@ -2069,6 +2071,7 @@ stringify_grant_objtype(ObjectType objtype)
case OBJECT_OPCLASS:
case OBJECT_OPERATOR:
case OBJECT_OPFAMILY:
+ case OBJECT_PERIOD:
case OBJECT_POLICY:
case OBJECT_PUBLICATION:
case OBJECT_PUBLICATION_NAMESPACE:
@@ -2153,6 +2156,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
case OBJECT_OPERATOR:
case OBJECT_OPFAMILY:
case OBJECT_PARAMETER_ACL:
+ case OBJECT_PERIOD:
case OBJECT_POLICY:
case OBJECT_PUBLICATION:
case OBJECT_PUBLICATION_NAMESPACE:
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ff16e3276..3ddc99581a 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -79,6 +79,7 @@ SecLabelSupportsObjectType(ObjectType objtype)
case OBJECT_OPERATOR:
case OBJECT_OPFAMILY:
case OBJECT_PARAMETER_ACL:
+ case OBJECT_PERIOD:
case OBJECT_POLICY:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index e230dfc569..0d69f9370b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -43,6 +43,7 @@
#include "catalog/pg_largeobject.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_tablespace.h"
#include "catalog/pg_trigger.h"
@@ -149,13 +150,18 @@ static List *on_commits = NIL;
#define AT_PASS_OLD_CONSTR 3 /* re-add existing constraints */
/* We could support a RENAME COLUMN pass here, but not currently used */
#define AT_PASS_ADD_COL 4 /* ADD COLUMN */
-#define AT_PASS_ADD_CONSTR 5 /* ADD constraints (initial examination) */
-#define AT_PASS_COL_ATTRS 6 /* set column attributes, eg NOT NULL */
-#define AT_PASS_ADD_INDEXCONSTR 7 /* ADD index-based constraints */
-#define AT_PASS_ADD_INDEX 8 /* ADD indexes */
-#define AT_PASS_ADD_OTHERCONSTR 9 /* ADD other constraints, defaults */
-#define AT_PASS_MISC 10 /* other stuff */
-#define AT_NUM_PASSES 11
+/*
+ * We must add PERIODs after columns, in case they reference a newly-added column,
+ * and before constraints, in case a newly-added PK/FK references them.
+ */
+#define AT_PASS_ADD_PERIOD 5 /* ADD PERIOD */
+#define AT_PASS_ADD_CONSTR 6 /* ADD constraints (initial examination) */
+#define AT_PASS_COL_ATTRS 7 /* set column attributes, eg NOT NULL */
+#define AT_PASS_ADD_INDEXCONSTR 8 /* ADD index-based constraints */
+#define AT_PASS_ADD_INDEX 9 /* ADD indexes */
+#define AT_PASS_ADD_OTHERCONSTR 10 /* ADD other constraints, defaults */
+#define AT_PASS_MISC 11 /* other stuff */
+#define AT_NUM_PASSES 12
typedef struct AlteredTableInfo
{
@@ -354,6 +360,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
static List *MergeAttributes(List *schema, List *supers, char relpersistence,
bool is_partition, List **supconstr,
List **supnotnulls);
+static List *MergePeriods(char *relname, List *periods, List *tableElts, List *supers);
static bool MergeCheckConstraint(List *constraints, char *name, Node *expr);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel);
static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel);
@@ -434,6 +441,8 @@ static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab,
AlterTableUtilityContext *context);
static bool check_for_column_name_collision(Relation rel, const char *colname,
bool if_not_exists);
+static bool check_for_period_name_collision(Relation rel, const char *pername,
+ bool if_not_exists);
static void add_column_datatype_dependency(Oid relid, int32 attnum, Oid typid);
static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid);
static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
@@ -453,6 +462,12 @@ static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,
Node *newDefault, LOCKMODE lockmode);
static ObjectAddress ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
Node *newDefault);
+static ObjectAddress ATExecAddPeriod(Relation rel, PeriodDef *period,
+ AlterTableUtilityContext *context);
+static void ATExecDropPeriod(Relation rel, const char *periodName,
+ DropBehavior behavior,
+ bool recurse, bool recursing,
+ bool missing_ok);
static ObjectAddress ATExecAddIdentity(Relation rel, const char *colName,
Node *def, LOCKMODE lockmode);
static ObjectAddress ATExecSetIdentity(Relation rel, const char *colName,
@@ -662,6 +677,10 @@ static List *GetParentedForeignKeyRefs(Relation partition);
static void ATDetachCheckNoForeignKeyRefs(Relation partition);
static char GetAttributeCompression(Oid atttypid, const char *compression);
static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static void AddRelationNewPeriod(Relation rel, PeriodDef *period);
+static void ValidatePeriod(Relation rel, PeriodDef *period);
+static Constraint *make_constraint_for_period(Relation rel, PeriodDef *period);
+static ColumnDef *make_range_column_for_period(PeriodDef *period);
/* ----------------------------------------------------------------
@@ -890,6 +909,26 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
stmt->partbound != NULL,
&old_constraints, &old_notnulls);
+ /*
+ * Using the column list (including inherited columns),
+ * find the start/end columns for each period.
+ * PERIODs should be inherited too (but aren't yet).
+ */
+ stmt->periods = MergePeriods(relname, stmt->periods, stmt->tableElts, inheritOids);
+
+ /*
+ * For each PERIOD we need to create a GENERATED column,
+ * so add those to tableElts.
+ * These columns are not inherited,
+ * so we don't worry about conflicts in tableElts.
+ */
+ foreach(listptr, stmt->periods)
+ {
+ PeriodDef *period = (PeriodDef *) lfirst(listptr);
+ ColumnDef *col = make_range_column_for_period(period);
+ stmt->tableElts = lappend(stmt->tableElts, col);
+ }
+
/*
* Create a tuple descriptor from the relation schema. Note that this
* deals with column names, types, and in-descriptor NOT NULL flags, but
@@ -1281,6 +1320,21 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
foreach(listptr, nncols)
set_attnotnull(NULL, rel, lfirst_int(listptr), false, NoLock);
+ /*
+ * Create periods for the table. This must come after we create columns
+ * and before we create index constraints. It will automatically create
+ * a CHECK constraint for the period.
+ */
+ foreach(listptr, stmt->periods)
+ {
+ PeriodDef *period = (PeriodDef *) lfirst(listptr);
+
+ /* Don't update the count of check constraints twice */
+ CommandCounterIncrement();
+
+ AddRelationNewPeriod(rel, period);
+ }
+
ObjectAddressSet(address, RelationRelationId, relationId);
/*
@@ -1292,6 +1346,264 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
return address;
}
+/*
+ * make_constraint_for_period
+ *
+ * Builds a CHECK Constraint to ensure start < end.
+ * Returns the CHECK Constraint.
+ * Also fills in period->constraintname if needed.
+ */
+static Constraint *
+make_constraint_for_period(Relation rel, PeriodDef *period)
+{
+ ColumnRef *scol, *ecol;
+ Constraint *constr;
+ TypeCacheEntry *type;
+
+ if (period->constraintname == NULL)
+ period->constraintname = ChooseConstraintName(RelationGetRelationName(rel),
+ period->periodname,
+ "check",
+ RelationGetNamespace(rel),
+ NIL);
+ scol = makeNode(ColumnRef);
+ scol->fields = list_make1(makeString(pstrdup(period->startcolname)));
+ scol->location = 0;
+
+ ecol = makeNode(ColumnRef);
+ ecol->fields = list_make1(makeString(pstrdup(period->endcolname)));
+ ecol->location = 0;
+
+ type = lookup_type_cache(period->coltypid, TYPECACHE_LT_OPR);
+ if (type->lt_opr == InvalidOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("column \"%s\" cannot be used in a PERIOD because its type %s has no less than operator",
+ period->startcolname, format_type_be(period->coltypid))));
+
+ constr = makeNode(Constraint);
+ constr->contype = CONSTR_CHECK;
+ constr->conname = period->constraintname;
+ constr->deferrable = false;
+ constr->initdeferred = false;
+ constr->location = -1;
+ constr->is_no_inherit = false;
+ constr->raw_expr = (Node *) makeSimpleA_Expr(AEXPR_OP,
+ get_opname(type->lt_opr),
+ (Node *) scol,
+ (Node *) ecol,
+ 0);
+ constr->cooked_expr = NULL;
+ constr->skip_validation = false;
+ constr->initially_valid = true;
+
+ return constr;
+}
+
+/*
+ * make_range_column_for_period
+ *
+ * Builds a GENERATED ALWAYS range column based on the PERIOD
+ * start/end columns. Returns the ColumnDef.
+ */
+ColumnDef *
+make_range_column_for_period(PeriodDef *period)
+{
+ char *range_type_namespace;
+ char *range_type_name;
+ ColumnDef *col = makeNode(ColumnDef);
+ ColumnRef *startvar, *endvar;
+ Expr *rangeConstructor;
+
+ if (!get_typname_and_namespace(period->rngtypid, &range_type_name, &range_type_namespace))
+ elog(ERROR, "missing range type %d", period->rngtypid);
+
+ startvar = makeNode(ColumnRef);
+ startvar->fields = list_make1(makeString(pstrdup(period->startcolname)));
+ endvar = makeNode(ColumnRef);
+ endvar->fields = list_make1(makeString(pstrdup(period->endcolname)));
+ rangeConstructor = (Expr *) makeFuncCall(
+ list_make2(makeString(range_type_namespace), makeString(range_type_name)),
+ list_make2(startvar, endvar),
+ COERCE_EXPLICIT_CALL,
+ period->location);
+
+ col->colname = pstrdup(period->periodname);
+ col->typeName = makeTypeName(range_type_name);
+ col->compression = NULL;
+ col->inhcount = 0;
+ col->is_local = true;
+ col->is_not_null = true;
+ col->is_from_type = false;
+ col->storage = 0;
+ col->storage_name = NULL;
+ col->raw_default = (Node *) rangeConstructor;
+ col->cooked_default = NULL;
+ col->identity = 0;
+ col->generated = ATTRIBUTE_GENERATED_STORED;
+ col->collClause = NULL;
+ col->collOid = InvalidOid;
+ col->fdwoptions = NIL;
+ col->location = period->location;
+
+ return col;
+}
+
+/*
+ * ValidatePeriod
+ *
+ * Look up the attributes used by the PERIOD,
+ * make sure they exist, are not system columns,
+ * and have the same type and collation.
+ *
+ * You must have a RowExclusiveLock on pg_attribute
+ * before calling this function.
+ *
+ * Add our findings to these PeriodDef fields:
+ *
+ * coltypid - the type of PERIOD columns.
+ * startattnum - the attnum of the start column.
+ * endattnum - the attnum of the end column.
+ */
+static void
+ValidatePeriod(Relation rel, PeriodDef *period)
+{
+ HeapTuple starttuple;
+ HeapTuple endtuple;
+ Form_pg_attribute atttuple;
+ Oid attcollation;
+
+ /* Find the start column */
+ starttuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), period->startcolname);
+ if (!HeapTupleIsValid(starttuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column \"%s\" of relation \"%s\" does not exist",
+ period->startcolname, RelationGetRelationName(rel))));
+ atttuple = (Form_pg_attribute) GETSTRUCT(starttuple);
+ period->coltypid = atttuple->atttypid;
+ attcollation = atttuple->attcollation;
+ period->startattnum = atttuple->attnum;
+
+ /* Make sure it's not a system column */
+ if (period->startattnum <= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("cannot use system column \"%s\" in period",
+ period->startcolname)));
+
+ /* Find the end column */
+ endtuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), period->endcolname);
+ if (!HeapTupleIsValid(endtuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column \"%s\" of relation \"%s\" does not exist",
+ period->endcolname, RelationGetRelationName(rel))));
+ atttuple = (Form_pg_attribute) GETSTRUCT(endtuple);
+ period->endattnum = atttuple->attnum;
+
+ /* Make sure it's not a system column */
+ if (period->endattnum <= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("cannot use system column \"%s\" in period",
+ period->endcolname)));
+
+ /* Both columns must be of same type */
+ if (period->coltypid != atttuple->atttypid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("start and end columns of period must be of same type")));
+
+ /* Both columns must have the same collation */
+ if (attcollation != atttuple->attcollation)
+ ereport(ERROR,
+ (errcode(ERRCODE_COLLATION_MISMATCH),
+ errmsg("start and end columns of period must have same collation")));
+
+ heap_freetuple(starttuple);
+ heap_freetuple(endtuple);
+}
+
+/*
+ * choose_rangetype_for_period
+ *
+ * Find a suitable range type for operations involving this period.
+ * Use the rangetype option if provided, otherwise try to find a
+ * non-ambiguous existing type.
+ */
+// TODO: This could be static now
+Oid
+choose_rangetype_for_period(PeriodDef *period)
+{
+ Oid rngtypid;
+
+ if (period->rangetypename != NULL)
+ {
+ /* Make sure it exists */
+ rngtypid = TypenameGetTypidExtended(period->rangetypename, false);
+ if (rngtypid == InvalidOid)
+ ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("Range type %s not found", period->rangetypename)));
+
+ /* Make sure it is a range type */
+ if (!type_is_range(rngtypid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("Type %s is not a range type", period->rangetypename)));
+
+ /* Make sure it matches the column type */
+ if (get_range_subtype(rngtypid) != period->coltypid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("Range type %s does not match column type %s",
+ period->rangetypename,
+ format_type_be(period->coltypid))));
+ }
+ else
+ {
+ rngtypid = get_subtype_range(period->coltypid);
+ if (rngtypid == InvalidOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("no range type for %s found for period %s",
+ format_type_be(period->coltypid),
+ period->periodname),
+ errhint("You can define a custom range type with CREATE TYPE")));
+
+ }
+
+ return rngtypid;
+}
+
+static void
+AddRelationNewPeriod(Relation rel, PeriodDef *period)
+{
+ Relation attrelation;
+ Oid conoid;
+ Constraint *constr;
+ List *newconstrs;
+
+ attrelation = table_open(AttributeRelationId, RowExclusiveLock);
+
+ /* Find the GENERATED range column */
+
+ period->rngattnum = get_attnum(RelationGetRelid(rel), period->periodname);
+ if (period->rngattnum == InvalidAttrNumber)
+ elog(ERROR, "missing attribute %s", period->periodname);
+
+ /* The parser has already found period->coltypid */
+
+ constr = make_constraint_for_period(rel, period);
+ newconstrs = AddRelationNewConstraints(rel, NIL, list_make1(constr), false, true, true, NULL);
+ conoid = ((CookedConstraint *) linitial(newconstrs))->conoid;
+
+ /* Save it */
+ StorePeriod(rel, period->periodname, period->startattnum, period->endattnum, period->rngattnum, conoid);
+
+ table_close(attrelation, RowExclusiveLock);
+}
+
/*
* Emit the right error or warning message for a "DROP" command issued on a
* non-existent relation
@@ -3281,6 +3593,168 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
}
+/*----------
+ * MergePeriods
+ * Returns new period list given initial periods and superclasses.
+ *
+ * For now we don't support inheritence with PERIODs,
+ * but we might make it work eventually.
+ *
+ * We can omit lots of checks here and assume MergeAttributes already did them,
+ * for example that child & parents are not a mix of permanent and temp.
+ */
+static List *
+MergePeriods(char *relname, List *periods, List *tableElts, List *supers)
+{
+ ListCell *entry;
+
+ /* If we have a PERIOD then supers must be empty. */
+
+ if (list_length(periods) > 0 && list_length(supers) > 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Inheriting is not supported when a table has a PERIOD")));
+
+ /* If any parent table has a PERIOD, then fail. */
+
+ foreach(entry, supers)
+ {
+ Oid parent = lfirst_oid(entry);
+ Relation relation;
+ Relation pg_period;
+ SysScanDesc scan;
+ ScanKeyData skey[1];
+ HeapTuple tuple;
+
+ /* caller already got lock */
+ relation = table_open(parent, NoLock);
+ pg_period = table_open(PeriodRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_period_perrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(parent));
+
+ scan = systable_beginscan(pg_period, PeriodRelidNameIndexId, true,
+ NULL, 1, skey);
+
+ if (HeapTupleIsValid(tuple = systable_getnext(scan)))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("Inheriting from a table with a PERIOD is not supported")));
+
+ systable_endscan(scan);
+ table_close(pg_period, AccessShareLock);
+ table_close(relation, NoLock);
+ }
+
+ /*
+ * Find the start & end columns and get their attno and type.
+ * In the same pass, make sure the period doesn't conflict with any column names.
+ * Also make sure the same period name isn't used more than once.
+ */
+ foreach (entry, periods)
+ {
+ PeriodDef *period = lfirst(entry);
+ ListCell *entry2;
+ int i = 1;
+ Oid startcoltypid = InvalidOid;
+ Oid endcoltypid = InvalidOid;
+ Oid startcolcollation = InvalidOid;
+ Oid endcolcollation = InvalidOid;
+
+ period->startattnum = InvalidAttrNumber;
+ period->endattnum = InvalidAttrNumber;
+
+ if (SystemAttributeByName(period->periodname) != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("period name \"%s\" conflicts with a system column name",
+ period->periodname)));
+
+ foreach (entry2, periods)
+ {
+ PeriodDef *period2 = lfirst(entry2);
+
+ if (period != period2 && strcmp(period->periodname, period2->periodname) == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_OBJECT),
+ errmsg("period name \"%s\" specified more than once",
+ period->periodname)));
+ }
+
+ foreach (entry2, tableElts)
+ {
+ ColumnDef *col = lfirst(entry2);
+ int32 atttypmod;
+ AclResult aclresult;
+
+ if (strcmp(period->periodname, col->colname) == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("period name \"%s\" conflicts with a column name",
+ period->periodname)));
+
+ if (strcmp(period->startcolname, col->colname) == 0)
+ {
+ period->startattnum = i;
+
+ typenameTypeIdAndMod(NULL, col->typeName, &startcoltypid, &atttypmod);
+
+ aclresult = object_aclcheck(TypeRelationId, startcoltypid, GetUserId(), ACL_USAGE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error_type(aclresult, startcoltypid);
+
+ startcolcollation = GetColumnDefCollation(NULL, col, startcoltypid);
+ }
+
+ if (strcmp(period->endcolname, col->colname) == 0)
+ {
+ period->endattnum = i;
+
+ typenameTypeIdAndMod(NULL, col->typeName, &endcoltypid, &atttypmod);
+
+ aclresult = object_aclcheck(TypeRelationId, endcoltypid, GetUserId(), ACL_USAGE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error_type(aclresult, endcoltypid);
+
+ endcolcollation = GetColumnDefCollation(NULL, col, endcoltypid);
+ }
+
+ i++;
+ }
+
+ /* Did we find the columns? */
+ if (period->startattnum == InvalidAttrNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column \"%s\" of relation \"%s\" does not exist",
+ period->startcolname, relname)));
+ if (period->endattnum == InvalidAttrNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column \"%s\" of relation \"%s\" does not exist",
+ period->endcolname, relname)));
+
+ /* Both columns must be of same type */
+ if (startcoltypid != endcoltypid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("start and end columns of period must be of same type")));
+
+ /* Both columns must have the same collation */
+ if (startcolcollation != endcolcollation)
+ ereport(ERROR,
+ (errcode(ERRCODE_COLLATION_MISMATCH),
+ errmsg("start and end columns of period must have same collation")));
+
+ period->coltypid = startcoltypid;
+ period->rngtypid = choose_rangetype_for_period(period);
+ }
+
+ return periods;
+}
+
/*
* MergeCheckConstraint
* Try to merge an inherited CHECK constraint with previous ones
@@ -4326,12 +4800,12 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
* existing query plans. On the assumption it's not used for such, we
* don't have to reject pending AFTER triggers, either.
*
- * Also, since we don't have an AlterTableUtilityContext, this cannot be
+ * Also, if you don't pass an AlterTableUtilityContext, this cannot be
* used for any subcommand types that require parse transformation or
* could generate subcommands that have to be passed to ProcessUtility.
*/
void
-AlterTableInternal(Oid relid, List *cmds, bool recurse)
+AlterTableInternal(Oid relid, List *cmds, bool recurse, AlterTableUtilityContext *context)
{
Relation rel;
LOCKMODE lockmode = AlterTableGetLockLevel(cmds);
@@ -4340,7 +4814,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)
EventTriggerAlterTableRelid(relid);
- ATController(NULL, rel, cmds, recurse, lockmode, NULL);
+ ATController(NULL, rel, cmds, recurse, lockmode, context);
}
/*
@@ -4433,6 +4907,8 @@ AlterTableGetLockLevel(List *cmds)
case AT_EnableReplicaRule: /* may change SELECT rules */
case AT_EnableRule: /* may change SELECT rules */
case AT_DisableRule: /* may change SELECT rules */
+ case AT_AddPeriod: /* shares namespace with columns, adds constraint */
+ case AT_DropPeriod:
cmd_lockmode = AccessExclusiveLock;
break;
@@ -4748,6 +5224,14 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
/* This command never recurses */
pass = AT_PASS_ADD_OTHERCONSTR;
break;
+ case AT_AddPeriod: /* ALTER TABLE ... ADD PERIOD FOR name (start, end) */
+ ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
+ pass = AT_PASS_ADD_PERIOD;
+ break;
+ case AT_DropPeriod: /* ALTER TABLE ... DROP PERIOD FOR name */
+ ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
+ pass = AT_PASS_DROP;
+ break;
case AT_AddIdentity:
ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_VIEW | ATT_FOREIGN_TABLE);
/* This command never recurses */
@@ -5145,6 +5629,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
case AT_CookedColumnDefault: /* add a pre-cooked default */
address = ATExecCookedColumnDefault(rel, cmd->num, cmd->def);
break;
+ case AT_AddPeriod:
+ address = ATExecAddPeriod(rel, (PeriodDef *) cmd->def, context);
+ break;
+ case AT_DropPeriod:
+ ATExecDropPeriod(rel, cmd->name, cmd->behavior,
+ false, false,
+ cmd->missing_ok);
+ break;
case AT_AddIdentity:
cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
cur_pass, context);
@@ -6289,6 +6781,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
case AT_AddColumn:
case AT_AddColumnToView:
return "ADD COLUMN";
+ case AT_AddPeriod:
+ return "ADD PERIOD";
case AT_ColumnDefault:
case AT_CookedColumnDefault:
return "ALTER COLUMN ... SET DEFAULT";
@@ -6312,6 +6806,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
return "ALTER COLUMN ... SET COMPRESSION";
case AT_DropColumn:
return "DROP COLUMN";
+ case AT_DropPeriod:
+ return "DROP PERIOD";
case AT_AddIndex:
case AT_ReAddIndex:
return NULL; /* not real grammar */
@@ -7317,14 +7813,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
/*
* If a new or renamed column will collide with the name of an existing
* column and if_not_exists is false then error out, else do nothing.
+ *
+ * See also check_for_period_name_collision.
*/
static bool
check_for_column_name_collision(Relation rel, const char *colname,
bool if_not_exists)
{
- HeapTuple attTuple;
+ HeapTuple attTuple, perTuple;
int attnum;
+ /* If the name exists as a period, we're done. */
+ perTuple = SearchSysCache2(PERIODNAME,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ PointerGetDatum(colname));
+ if (HeapTupleIsValid(perTuple))
+ {
+ ReleaseSysCache(perTuple);
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("column name \"%s\" conflicts with a period name",
+ colname)));
+ }
+
/*
* this test is deliberately not attisdropped-aware, since if one tries to
* add a column matching a dropped column name, it's gonna fail anyway.
@@ -7368,6 +7879,78 @@ check_for_column_name_collision(Relation rel, const char *colname,
return true;
}
+/*
+ * If a new period name will collide with the name of an existing column or
+ * period [and if_not_exists is false] then error out, else do nothing.
+ *
+ * See also check_for_column_name_collision.
+ */
+static bool
+check_for_period_name_collision(Relation rel, const char *pername,
+ bool if_not_exists)
+{
+ HeapTuple attTuple, perTuple;
+ int attnum;
+
+ /* TODO: implement IF [NOT] EXISTS for periods */
+ Assert(!if_not_exists);
+
+ /* If there is already a period with this name, then we're done. */
+ perTuple = SearchSysCache2(PERIODNAME,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ PointerGetDatum(pername));
+ if (HeapTupleIsValid(perTuple))
+ {
+ if (if_not_exists)
+ {
+ ReleaseSysCache(perTuple);
+
+ ereport(NOTICE,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("period \"%s\" of relation \"%s\" already exists, skipping",
+ pername, RelationGetRelationName(rel))));
+ return false;
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("period \"%s\" of relation \"%s\" already exists",
+ pername, RelationGetRelationName(rel))));
+ }
+
+ /*
+ * this test is deliberately not attisdropped-aware, since if one tries to
+ * add a column matching a dropped column name, it's gonna fail anyway.
+ *
+ * XXX: Does this hold for periods?
+ */
+ attTuple = SearchSysCache2(ATTNAME,
+ ObjectIdGetDatum(RelationGetRelid(rel)),
+ PointerGetDatum(pername));
+ if (HeapTupleIsValid(attTuple))
+ {
+ attnum = ((Form_pg_attribute) GETSTRUCT(attTuple))->attnum;
+ ReleaseSysCache(attTuple);
+
+ /*
+ * We throw a different error message for conflicts with system column
+ * names, since they are normally not shown and the user might otherwise
+ * be confused about the reason for the conflict.
+ */
+ if (attnum <= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("period name \"%s\" conflicts with a system column name",
+ pername)));
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_COLUMN),
+ errmsg("period name \"%s\" conflicts with a column name",
+ pername)));
+ }
+
+ return true;
+}
+
/*
* Install a column's dependency on its datatype.
*/
@@ -8008,6 +8591,155 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
return address;
}
+/*
+ * ALTER TABLE ADD PERIOD
+ *
+ * Return the address of the period.
+ */
+static ObjectAddress
+ATExecAddPeriod(Relation rel, PeriodDef *period, AlterTableUtilityContext *context)
+{
+ Relation attrelation;
+ ObjectAddress address = InvalidObjectAddress;
+ Constraint *constr;
+ ColumnDef *rangecol;
+ Oid conoid, periodoid;
+ List *cmds = NIL;
+ AlterTableCmd *cmd;
+
+ /*
+ * PERIOD FOR SYSTEM_TIME is not yet implemented, but make sure no one uses
+ * the name.
+ */
+ if (strcmp(period->periodname, "system_time") == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("PERIOD FOR SYSTEM_TIME is not supported")));
+
+ /* The period name must not already exist */
+ (void) check_for_period_name_collision(rel, period->periodname, false);
+
+ /* Parse options */
+ transformPeriodOptions(period);
+
+ attrelation = table_open(AttributeRelationId, RowExclusiveLock);
+ ValidatePeriod(rel, period);
+
+ period->rngtypid = choose_rangetype_for_period(period);
+
+ /* Make the CHECK constraint */
+ constr = make_constraint_for_period(rel, period);
+ cmd = makeNode(AlterTableCmd);
+ cmd->subtype = AT_AddConstraint;
+ cmd->def = (Node *) constr;
+ cmds = lappend(cmds, cmd);
+ AlterTableInternal(RelationGetRelid(rel), cmds, true, context);
+ conoid = get_relation_constraint_oid(RelationGetRelid(rel), period->constraintname, false);
+
+ /* Make the range column */
+ rangecol = make_range_column_for_period(period);
+ cmd = makeNode(AlterTableCmd);
+ cmd->subtype = AT_AddColumn;
+ cmd->def = (Node *) rangecol;
+ cmd->name = period->periodname;
+ cmd->recurse = false; /* no, let the PERIOD recurse instead */
+ AlterTableInternal(RelationGetRelid(rel), list_make1(cmd), true, context);
+ period->rngattnum = get_attnum(RelationGetRelid(rel), period->periodname);
+ if (period->rngattnum == InvalidAttrNumber)
+ elog(ERROR, "missing attribute %s", period->periodname);
+
+ /* Save the Period */
+ periodoid = StorePeriod(rel, period->periodname, period->startattnum, period->endattnum, period->rngattnum, conoid);
+
+ ObjectAddressSet(address, PeriodRelationId, periodoid);
+
+ table_close(attrelation, RowExclusiveLock);
+
+ return address;
+}
+
+/*
+ * ALTER TABLE DROP PERIOD
+ *
+ * Like DROP COLUMN, we can't use the normal ALTER TABLE recursion mechanism.
+ */
+static void
+ATExecDropPeriod(Relation rel, const char *periodName,
+ DropBehavior behavior,
+ bool recurse, bool recursing,
+ bool missing_ok)
+{
+ Relation pg_period;
+ Form_pg_period period;
+ SysScanDesc scan;
+ ScanKeyData key;
+ HeapTuple tuple;
+ bool found = false;
+
+ /* At top level, permission check was done in ATPrepCmd, else do it */
+ if (recursing)
+ ATSimplePermissions(AT_DropPeriod, rel, ATT_TABLE);
+
+ pg_period = table_open(PeriodRelationId, RowExclusiveLock);
+
+ /*
+ * Find and drop the target period
+ */
+ ScanKeyInit(&key,
+ Anum_pg_period_perrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(rel)));
+ scan = systable_beginscan(pg_period, PeriodRelidNameIndexId,
+ true, NULL, 1, &key);
+
+ while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+ {
+ ObjectAddress perobj;
+
+ period = (Form_pg_period) GETSTRUCT(tuple);
+
+ if (strcmp(NameStr(period->pername), periodName) != 0)
+ continue;
+
+ /*
+ * Perform the actual period deletion
+ */
+ perobj.classId = PeriodRelationId;
+ perobj.objectId = period->oid;
+ perobj.objectSubId = 0;
+
+ performDeletion(&perobj, behavior, 0);
+
+ found = true;
+
+ /* period found and dropped -- no need to keep looping */
+ break;
+ }
+
+ systable_endscan(scan);
+
+ if (!found)
+ {
+ if (!missing_ok)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("period \"%s\" on relation \"%s\" does not exist",
+ periodName, RelationGetRelationName(rel))));
+ }
+ else
+ {
+ ereport(NOTICE,
+ (errmsg("period \"%s\" on relation \"%s\" does not exist, skipping",
+ periodName, RelationGetRelationName(rel))));
+ table_close(pg_period, RowExclusiveLock);
+ return;
+ }
+ }
+
+ table_close(pg_period, RowExclusiveLock);
+}
+
/*
* ALTER TABLE ALTER COLUMN ADD IDENTITY
*
@@ -13490,6 +14222,15 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
RememberConstraintForRebuilding(foundObject.objectId, tab);
break;
+ case OCLASS_PERIOD:
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot alter type of a column used by a period"),
+ errdetail("%s depends on column \"%s\"",
+ getObjectDescription(&foundObject, false),
+ colName)));
+ break;
+
case OCLASS_REWRITE:
/* XXX someday see if we can cope with revising views */
ereport(ERROR,
@@ -15551,7 +16292,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)
EventTriggerAlterTableStart((Node *) stmt);
/* OID is set by AlterTableInternal */
- AlterTableInternal(lfirst_oid(l), cmds, false);
+ AlterTableInternal(lfirst_oid(l), cmds, false, NULL);
EventTriggerAlterTableEnd();
}
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 9bd77546b9..be4637287f 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -169,7 +169,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
}
/* EventTriggerAlterTableStart called by ProcessUtilitySlow */
- AlterTableInternal(viewOid, atcmds, true);
+ AlterTableInternal(viewOid, atcmds, true, NULL);
/* Make the new view columns visible */
CommandCounterIncrement();
@@ -201,7 +201,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
atcmds = list_make1(atcmd);
/* EventTriggerAlterTableStart called by ProcessUtilitySlow */
- AlterTableInternal(viewOid, atcmds, true);
+ AlterTableInternal(viewOid, atcmds, true, NULL);
/*
* There is very little to do here to update the view's dependencies.
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 62ccbf9d3c..a0c069fc5f 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1639,6 +1639,9 @@ exprLocation(const Node *expr)
case T_Constraint:
loc = ((const Constraint *) expr)->location;
break;
+ case T_PeriodDef:
+ loc = ((const PeriodDef *) expr)->location;
+ break;
case T_FunctionParameter:
/* just use typename's location */
loc = exprLocation((Node *) ((const FunctionParameter *) expr)->argType);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 492f30ff69..88850284e0 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -595,7 +595,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <keyword> col_name_keyword reserved_keyword
%type <keyword> bare_label_keyword
-%type <node> TableConstraint TableLikeClause
+%type <node> TableConstraint TableLikeClause TablePeriod
%type <ival> TableLikeOptionList TableLikeOption
%type <str> column_compression opt_column_compression column_storage opt_column_storage
%type <list> ColQualList
@@ -2601,6 +2601,24 @@ alter_table_cmd:
n->def = (Node *) $4;
$$ = (Node *) n;
}
+ /* ALTER TABLE <name> ADD PERIOD FOR <name> (<name>, <name>) */
+ | ADD_P TablePeriod
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_AddPeriod;
+ n->def = $2;
+ $$ = (Node *)n;
+ }
+ /* ALTER TABLE <name> DROP PERIOD FOR <name> [RESTRICT|CASCADE] */
+ | DROP PERIOD FOR name opt_drop_behavior
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_DropPeriod;
+ n->name = $4;
+ n->behavior = $5;
+ n->missing_ok = false;
+ $$ = (Node *)n;
+ }
/* ALTER TABLE <name> ADD CONSTRAINT ... */
| ADD_P TableConstraint
{
@@ -3712,8 +3730,10 @@ TableElement:
columnDef { $$ = $1; }
| TableLikeClause { $$ = $1; }
| TableConstraint { $$ = $1; }
+ | TablePeriod { $$ = $1; }
;
+
TypedTableElement:
columnOptions { $$ = $1; }
| TableConstraint { $$ = $1; }
@@ -4066,6 +4086,19 @@ TableLikeOption:
;
+TablePeriod:
+ PERIOD FOR name '(' name ',' name ')' opt_definition
+ {
+ PeriodDef *n = makeNode(PeriodDef);
+ n->periodname = $3;
+ n->startcolname = $5;
+ n->endcolname = $7;
+ n->options = $9;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ ;
+
/* ConstraintElem specifies constraint syntax which is not embedded into
* a column definition. ColConstraintElem specifies the embedded form.
* - thomas 1997-12-03
@@ -7098,6 +7131,14 @@ CommentStmt:
n->comment = $9;
$$ = (Node *) n;
}
+ | COMMENT ON PERIOD any_name IS comment_text
+ {
+ CommentStmt *n = makeNode(CommentStmt);
+ n->objtype = OBJECT_PERIOD;
+ n->object = (Node *) $4;
+ n->comment = $6;
+ $$ = (Node *) n;
+ }
| COMMENT ON LARGE_P OBJECT_P NumericOnly IS comment_text
{
CommentStmt *n = makeNode(CommentStmt);
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 864ea9b0d5..fbbade5140 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -22,6 +22,7 @@
#include "access/table.h"
#include "catalog/heap.h"
#include "catalog/namespace.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_type.h"
#include "funcapi.h"
#include "nodes/makefuncs.h"
@@ -3189,6 +3190,7 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,
int sublevels_up, bool require_col_privs, int location)
{
RangeTblEntry *rte = nsitem->p_rte;
+ Bitmapset *periodatts = NULL;
RTEPermissionInfo *perminfo = nsitem->p_perminfo;
List *names,
*vars;
@@ -3212,12 +3214,20 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,
perminfo->requiredPerms |= ACL_SELECT;
}
+ /* Get PERIOD columns to exclude */
+ if (rte->rtekind == RTE_RELATION)
+ periodatts = get_period_attnos(rte->relid);
+
forboth(name, names, var, vars)
{
char *label = strVal(lfirst(name));
Var *varnode = (Var *) lfirst(var);
TargetEntry *te;
+ /* If this column is from a PERIOD, skip it */
+ if (bms_is_member(pstate->p_next_resno, periodatts))
+ continue;
+
te = makeTargetEntry((Expr *) varnode,
(AttrNumber) pstate->p_next_resno++,
label,
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 6723a74f2f..cbaff69237 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -37,6 +37,7 @@
#include "catalog/pg_constraint.h"
#include "catalog/pg_opclass.h"
#include "catalog/pg_operator.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_type.h"
#include "commands/comment.h"
@@ -80,6 +81,7 @@ typedef struct
bool isforeign; /* true if CREATE/ALTER FOREIGN TABLE */
bool isalter; /* true if altering existing table */
List *columns; /* ColumnDef items */
+ List *periods; /* PeriodDef items */
List *ckconstraints; /* CHECK constraints */
List *nnconstraints; /* NOT NULL constraints */
List *fkconstraints; /* FOREIGN KEY constraints */
@@ -111,6 +113,8 @@ typedef struct
static void transformColumnDefinition(CreateStmtContext *cxt,
ColumnDef *column);
+static void transformTablePeriod(CreateStmtContext *cxt,
+ PeriodDef *period);
static void transformTableConstraint(CreateStmtContext *cxt,
Constraint *constraint);
static void transformTableLikeClause(CreateStmtContext *cxt,
@@ -242,6 +246,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
cxt.inhRelations = stmt->inhRelations;
cxt.isalter = false;
cxt.columns = NIL;
+ cxt.periods = NIL;
cxt.ckconstraints = NIL;
cxt.nnconstraints = NIL;
cxt.fkconstraints = NIL;
@@ -282,6 +287,10 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
transformColumnDefinition(&cxt, (ColumnDef *) element);
break;
+ case T_PeriodDef:
+ transformTablePeriod(&cxt, (PeriodDef *) element);
+ break;
+
case T_Constraint:
transformTableConstraint(&cxt, (Constraint *) element);
break;
@@ -349,6 +358,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
* Output results.
*/
stmt->tableElts = cxt.columns;
+ stmt->periods = cxt.periods;
stmt->constraints = cxt.ckconstraints;
stmt->nnconstraints = cxt.nnconstraints;
@@ -915,6 +925,113 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
}
}
+void
+transformPeriodOptions(PeriodDef *period)
+{
+ ListCell *option;
+ DefElem *dconstraintname = NULL;
+ DefElem *drangetypename = NULL;
+
+ foreach(option, period->options)
+ {
+ DefElem *defel = (DefElem *) lfirst(option);
+
+ if (strcmp(defel->defname, "check_constraint_name") == 0)
+ {
+ if (dconstraintname)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+ dconstraintname = defel;
+ }
+ else if (strcmp(defel->defname, "rangetype") == 0)
+ {
+ if (drangetypename)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+ drangetypename = defel;
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("option \"%s\" not recognized", defel->defname)));
+ }
+
+ if (dconstraintname != NULL)
+ period->constraintname = defGetString(dconstraintname);
+ else
+ period->constraintname = NULL;
+
+ if (drangetypename != NULL)
+ period->rangetypename = defGetString(drangetypename);
+ else
+ period->rangetypename = NULL;
+}
+
+/*
+ * transformTablePeriod
+ * transform a PeriodDef node within CREATE TABLE or ALTER TABLE
+ * TODO: Does ALTER TABLE really call us?? It doesn't seem like it does. Maybe it should. Did it in Vik's original patch?
+ */
+static void
+transformTablePeriod(CreateStmtContext *cxt, PeriodDef *period)
+{
+ // Oid coltypid;
+ // ColumnDef *col;
+ // ListCell *columns;
+
+ if (strcmp(period->periodname, "system_time") == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("PERIOD FOR SYSTEM_TIME is not supported"),
+ parser_errposition(cxt->pstate,
+ period->location)));
+
+ if (strcmp(period->startcolname, period->endcolname) == 0)
+ ereport(ERROR, (errmsg("column \"%s\" can't be the start and end column for period \"%s\"",
+ period->startcolname, period->periodname)));
+
+ /*
+ * Determine the column info and range type so that transformIndexConstraints
+ * knows how to create PRIMARY KEY/UNIQUE constraints using this PERIOD.
+ */
+ transformPeriodOptions(period);
+
+ /*
+ * Find a suitable range type for operations involving this period.
+ * Use the rangetype option if provided, otherwise try to find a
+ * non-ambiguous existing type.
+ */
+
+#ifdef asdsdfa
+ /* First find out the type of the period's columns */
+ // TODO: I'm doing this in DefineRelation now,
+ // which seems like a better place since it knows about inherited columns.
+ period->coltypid = InvalidOid;
+ period->rngtypid = InvalidOid;
+ foreach (columns, cxt->columns)
+ {
+ col = (ColumnDef *) lfirst(columns);
+ if (strcmp(col->colname, period->startcolname) == 0)
+ {
+ coltypid = typenameTypeId(cxt->pstate, col->typeName);
+ break;
+ }
+ }
+ if (coltypid == InvalidOid)
+ ereport(ERROR, (errmsg("column \"%s\" of relation \"%s\" does not exist",
+ period->startcolname, cxt->relation->relname)));
+
+ period->coltypid = coltypid;
+
+ /* Now make sure it matches rangetypename or we can find a matching range */
+ period->rngtypid = choose_rangetype_for_period(period);
+#endif
+
+ cxt->periods = lappend(cxt->periods, period);
+}
+
/*
* transformTableConstraint
* transform a Constraint node within CREATE TABLE or ALTER TABLE
@@ -1005,6 +1122,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
AttrNumber parent_attno;
Relation relation;
TupleDesc tupleDesc;
+ Bitmapset *periodatts;
AclResult aclresult;
char *comment;
ParseCallbackState pcbstate;
@@ -1057,6 +1175,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
}
tupleDesc = RelationGetDescr(relation);
+ periodatts = get_period_attnos(RelationGetRelid(relation));
/*
* Insert the copied attributes into the cxt for the new table definition.
@@ -1066,10 +1185,18 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
for (parent_attno = 1; parent_attno <= tupleDesc->natts;
parent_attno++)
{
- Form_pg_attribute attribute = TupleDescAttr(tupleDesc,
- parent_attno - 1);
+ Form_pg_attribute attribute;
ColumnDef *def;
+ /*
+ * If this column is from a PERIOD, skip it
+ * (since LIKE never copies PERIODs).
+ */
+ if (bms_is_member(parent_attno, periodatts))
+ continue;
+
+ attribute = TupleDescAttr(tupleDesc, parent_attno - 1);
+
/*
* Ignore dropped columns in the parent.
*/
@@ -1676,6 +1803,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
Oid keycoltype;
Datum datum;
bool isnull;
+ PeriodDef *period;
if (constraintOid)
*constraintOid = InvalidOid;
@@ -1733,6 +1861,11 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
index->if_not_exists = false;
index->reset_default_tblspc = false;
+ /* Copy the period */
+ period = makeNode(PeriodDef);
+ period->oid = idxrec->indperiod;
+ index->period = period;
+
/*
* We don't try to preserve the name of the source index; instead, just
* let DefineIndex() choose a reasonable name. (If we tried to preserve
@@ -3113,6 +3246,10 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
}
}
+ /* take care of the period */
+ if (stmt->period)
+ stmt->period->oid = get_period_oid(relid, stmt->period->periodname, false);
+
/*
* Check that only the base rel is mentioned. (This should be dead code
* now that add_missing_from is history.)
@@ -3570,6 +3707,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
cxt.inhRelations = NIL;
cxt.isalter = true;
cxt.columns = NIL;
+ cxt.periods = NIL;
cxt.ckconstraints = NIL;
cxt.nnconstraints = NIL;
cxt.fkconstraints = NIL;
@@ -3632,6 +3770,15 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
(int) nodeTag(cmd->def));
break;
+ case AT_AddPeriod:
+ {
+ newcmds = lappend(newcmds, cmd);
+ // Why not call transformTablePeriod here?
+ // Ah because it looks at cxt->columns
+ // and in an ALTER statement the columns might already exist (or not).
+ break;
+ }
+
case AT_AlterColumnType:
{
ColumnDef *def = castNode(ColumnDef, cmd->def);
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 391762f308..4c2965c151 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -30,6 +30,7 @@
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
#include "catalog/pg_operator.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_range.h"
#include "catalog/pg_statistic.h"
@@ -1021,6 +1022,68 @@ get_attoptions(Oid relid, int16 attnum)
return result;
}
+/* ---------- PG_PERIOD CACHE ---------- */
+
+/*
+ * get_periodname - given its OID, look up a period
+ *
+ * If missing_ok is false, throw an error if the period is not found.
+ * If true, just return InvalidOid.
+ */
+char *
+get_periodname(Oid periodid, bool missing_ok)
+{
+ HeapTuple tp;
+
+ tp = SearchSysCache1(PERIODOID,
+ ObjectIdGetDatum(periodid));
+ if (HeapTupleIsValid(tp))
+ {
+ Form_pg_period period_tup = (Form_pg_period) GETSTRUCT(tp);
+ char *result;
+
+ result = pstrdup(NameStr(period_tup->pername));
+ ReleaseSysCache(tp);
+ return result;
+ }
+
+ if (!missing_ok)
+ elog(ERROR, "cache lookup failed for period %d",
+ periodid);
+ return NULL;
+}
+
+/*
+ * get_period_oid - gets its relation and name, look up a period
+ *
+ * If missing_ok is false, throw an error if the cast is not found. If
+ * true, just return InvalidOid.
+ */
+Oid
+get_period_oid(Oid relid, const char *periodname, bool missing_ok)
+{
+ HeapTuple tp;
+
+ tp = SearchSysCache2(PERIODNAME,
+ ObjectIdGetDatum(relid),
+ PointerGetDatum(periodname));
+
+ if (HeapTupleIsValid(tp))
+ {
+ Form_pg_period period_tup = (Form_pg_period) GETSTRUCT(tp);
+ Oid result;
+
+ result = period_tup->oid;
+ ReleaseSysCache(tp);
+ return result;
+ }
+
+ if (!missing_ok)
+ elog(ERROR, "cache lookup failed for period %s",
+ periodname);
+ return InvalidOid;
+}
+
/* ---------- PG_CAST CACHE ---------- */
/*
@@ -3532,6 +3595,30 @@ get_multirange_range(Oid multirangeOid)
return InvalidOid;
}
+Oid
+get_subtype_range(Oid subtypeOid)
+{
+ CatCList *catlist;
+ Oid result = InvalidOid;
+
+ catlist = SearchSysCacheList1(RANGESUBTYPE, ObjectIdGetDatum(subtypeOid));
+
+ if (catlist->n_members == 1)
+ {
+ HeapTuple tuple = &catlist->members[0]->tuple;
+ Form_pg_range rngtup = (Form_pg_range) GETSTRUCT(tuple);
+ result = rngtup->rngtypid;
+ ReleaseCatCacheList(catlist);
+ }
+ else if (catlist->n_members > 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_INDETERMINATE_DATATYPE),
+ errmsg("ambiguous range for type %s",
+ format_type_be(subtypeOid))));
+
+ return result;
+}
+
/* ---------- PG_INDEX CACHE ---------- */
/*
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 8dbda0024f..44e636e5be 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -49,6 +49,7 @@
#include "catalog/pg_opfamily.h"
#include "catalog/pg_parameter_acl.h"
#include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_period.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_publication.h"
#include "catalog/pg_publication_namespace.h"
@@ -422,6 +423,19 @@ static const struct cachedesc cacheinfo[] = {
KEY(Anum_pg_partitioned_table_partrelid),
32
},
+ [PERIODNAME] = {
+ PeriodRelationId,
+ PeriodRelidNameIndexId,
+ KEY(Anum_pg_period_perrelid,
+ Anum_pg_period_pername),
+ 32
+ },
+ [PERIODOID] = {
+ PeriodRelationId,
+ PeriodObjectIndexId,
+ KEY(Anum_pg_period_oid),
+ 32
+ },
[PROCNAMEARGSNSP] = {
ProcedureRelationId,
ProcedureNameArgsNspIndexId,
@@ -480,6 +494,13 @@ static const struct cachedesc cacheinfo[] = {
KEY(Anum_pg_range_rngmultitypid),
4
},
+ [RANGESUBTYPE] = {
+ RangeRelationId,
+ RangeSubTypidTypidIndexId,
+ KEY(Anum_pg_range_rngsubtype,
+ Anum_pg_range_rngtypid),
+ 4
+ },
[RANGETYPE] = {
RangeRelationId,
RangeTypidIndexId,
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 39ebcfec32..191eba1990 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3517,6 +3517,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
strcmp(type, "DATABASE PROPERTIES") == 0 ||
strcmp(type, "DEFAULT") == 0 ||
strcmp(type, "FK CONSTRAINT") == 0 ||
+ strcmp(type, "PERIOD") == 0 ||
strcmp(type, "INDEX") == 0 ||
strcmp(type, "RULE") == 0 ||
strcmp(type, "TRIGGER") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8fd51e9f9f..6076a7952d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6325,6 +6325,7 @@ getTables(Archive *fout, int *numTables)
int i_reltype;
int i_relowner;
int i_relchecks;
+ int i_nperiod;
int i_relhasindex;
int i_relhasrules;
int i_relpages;
@@ -6402,6 +6403,14 @@ getTables(Archive *fout, int *numTables)
appendPQExpBufferStr(query,
"c.relhasoids, ");
+ /* In PG16 upwards we have PERIODs. */
+ if (fout->remoteVersion >= 160000)
+ appendPQExpBufferStr(query,
+ "(SELECT count(*) FROM pg_period WHERE perrelid = c.oid) AS nperiods, ");
+ else
+ appendPQExpBufferStr(query,
+ "0 AS nperiods, ");
+
if (fout->remoteVersion >= 90300)
appendPQExpBufferStr(query,
"c.relispopulated, ");
@@ -6539,6 +6548,7 @@ getTables(Archive *fout, int *numTables)
i_reltype = PQfnumber(res, "reltype");
i_relowner = PQfnumber(res, "relowner");
i_relchecks = PQfnumber(res, "relchecks");
+ i_nperiod = PQfnumber(res, "nperiods");
i_relhasindex = PQfnumber(res, "relhasindex");
i_relhasrules = PQfnumber(res, "relhasrules");
i_relpages = PQfnumber(res, "relpages");
@@ -6622,6 +6632,7 @@ getTables(Archive *fout, int *numTables)
}
tblinfo[i].reltablespace = pg_strdup(PQgetvalue(res, i, i_reltablespace));
tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
+ tblinfo[i].nperiod = atoi(PQgetvalue(res, i, i_nperiod));
tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
tblinfo[i].relpersistence = *(PQgetvalue(res, i, i_relpersistence));
tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
@@ -8356,7 +8367,7 @@ getTransforms(Archive *fout, int *numTransforms)
/*
* getTableAttrs -
* for each interesting table, read info about its attributes
- * (names, types, default values, CHECK constraints, etc)
+ * (names, types, default values, CHECK constraints, PERIODs, etc)
*
* modifies tblinfo
*/
@@ -8409,6 +8420,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
for (int i = 0; i < numTables; i++)
{
TableInfo *tbinfo = &tblinfo[i];
+ int ndumpablechecks; /* number of CHECK constraints that do
+ not belong to a period */
/* Don't bother to collect info for sequences */
if (tbinfo->relkind == RELKIND_SEQUENCE)
@@ -8423,7 +8436,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
appendPQExpBufferChar(tbloids, ',');
appendPQExpBuffer(tbloids, "%u", tbinfo->dobj.catId.oid);
- if (tbinfo->ncheck > 0)
+ ndumpablechecks = tbinfo->ncheck - tbinfo->nperiod;
+ if (ndumpablechecks > 0)
{
/* Also make a list of the ones with check constraints */
if (checkoids->len > 1) /* do we have more than the '{'? */
@@ -8959,15 +8973,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
pg_log_info("finding table check constraints");
resetPQExpBuffer(q);
- appendPQExpBuffer(q,
- "SELECT c.tableoid, c.oid, conrelid, conname, "
- "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
- "conislocal, convalidated "
- "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
- "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
- "WHERE contype = 'c' "
- "ORDER BY c.conrelid, c.conname",
- checkoids->data);
+ if (fout->remoteVersion >= 160000)
+ {
+ /*
+ * PERIODs were added in v17 and we don't dump CHECK
+ * constraints for them.
+ */
+ appendPQExpBuffer(q,
+ "SELECT c.tableoid, c.oid, conrelid, conname, "
+ "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
+ "conislocal, convalidated "
+ "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+ "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
+ "WHERE contype = 'c' "
+ " AND NOT EXISTS (SELECT FROM pg_period "
+ " WHERE (perrelid, perconstraint) = (conrelid, c.oid)) "
+ "ORDER BY c.conrelid, c.conname",
+ checkoids->data);
+ }
+ else
+ {
+ appendPQExpBuffer(q,
+ "SELECT c.tableoid, c.oid, conrelid, conname, "
+ "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
+ "conislocal, convalidated "
+ "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+ "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
+ "WHERE contype = 'c' "
+ "ORDER BY c.conrelid, c.conname",
+ checkoids->data);
+ }
res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
@@ -8989,6 +9024,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
Oid conrelid = atooid(PQgetvalue(res, j, i_conrelid));
TableInfo *tbinfo = NULL;
int numcons;
+ int ndumpablechecks;
/* Count rows for this table */
for (numcons = 1; numcons < numConstrs - j; numcons++)
@@ -9008,12 +9044,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
if (curtblindx >= numTables)
pg_fatal("unrecognized table OID %u", conrelid);
- if (numcons != tbinfo->ncheck)
+ ndumpablechecks = tbinfo->ncheck - tbinfo->nperiod;
+ if (numcons != ndumpablechecks)
{
pg_log_error(ngettext("expected %d check constraint on table \"%s\" but found %d",
"expected %d check constraints on table \"%s\" but found %d",
- tbinfo->ncheck),
- tbinfo->ncheck, tbinfo->dobj.name, numcons);
+ ndumpablechecks),
+ ndumpablechecks, tbinfo->dobj.name, numcons);
pg_log_error_hint("The system catalogs might be corrupted.");
exit_nicely(1);
}
@@ -9072,6 +9109,79 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQclear(res);
}
+ for (int i = 0; i < numTables; i++)
+ {
+ TableInfo *tbinfo = &tblinfo[i];
+
+ /*
+ * Get info about PERIOD definitions
+ */
+ if (tbinfo->nperiod > 0)
+ {
+ PeriodInfo *periods;
+ int numPeriods;
+ int j;
+
+ /* We shouldn't have any periods before v17 */
+ Assert(fout->remoteVersion >= 160000);
+
+ pg_log_info("finding periods for table \"%s.%s\"",
+ tbinfo->dobj.namespace->dobj.name,
+ tbinfo->dobj.name);
+
+ resetPQExpBuffer(q);
+ appendPQExpBuffer(q,
+ "SELECT p.tableoid, p.oid, p.pername, "
+ " sa.attname AS perstart, ea.attname AS perend, "
+ " r.typname AS rngtype, "
+ " c.conname AS conname "
+ "FROM pg_catalog.pg_period AS p "
+ "JOIN pg_catalog.pg_attribute AS sa ON (sa.attrelid, sa.attnum) = (p.perrelid, p.perstart) "
+ "JOIN pg_catalog.pg_attribute AS ea ON (ea.attrelid, ea.attnum) = (p.perrelid, p.perend) "
+ "JOIN pg_catalog.pg_type AS r ON r.oid = p.perrngtype "
+ "JOIN pg_catalog.pg_constraint AS c ON c.oid = p.perconstraint "
+ "WHERE p.perrelid = '%u'::pg_catalog.oid "
+ "ORDER BY p.pername",
+ tbinfo->dobj.catId.oid);
+
+ res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
+
+ /*
+ * If we didn't get the number of rows we thought we were going to,
+ * then those JOINs didn't work.
+ */
+ numPeriods = PQntuples(res);
+ if (numPeriods != tbinfo->nperiod)
+ {
+ pg_log_info(ngettext("expected %d period on table \"%s\" but found %d",
+ "expected %d periods on table \"%s\" but found %d",
+ tbinfo->nperiod),
+ tbinfo->nperiod, tbinfo->dobj.name, numPeriods);
+ pg_log_info("(The system catalogs might be corrupted.)");
+ exit_nicely(1);
+ }
+
+ periods = (PeriodInfo *) pg_malloc(numPeriods * sizeof(PeriodInfo));
+ tbinfo->periods = periods;
+
+ for (j = 0; j < numPeriods; j++)
+ {
+ periods[j].dobj.objType = DO_PERIOD;
+ periods[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, 0));
+ periods[j].dobj.catId.oid = atooid(PQgetvalue(res, j, 1));
+ AssignDumpId(&periods[j].dobj);
+ periods[j].dobj.name = pg_strdup(PQgetvalue(res, j, 2));
+ periods[j].dobj.namespace = tbinfo->dobj.namespace;
+ periods[j].pertable = tbinfo;
+ periods[j].perstart = pg_strdup(PQgetvalue(res, j, 3));
+ periods[j].perend = pg_strdup(PQgetvalue(res, j, 4));
+ periods[j].rngtype = pg_strdup(PQgetvalue(res, j, 5));
+ periods[j].conname = pg_strdup(PQgetvalue(res, j, 6));
+ }
+ PQclear(res);
+ }
+ }
+
destroyPQExpBuffer(q);
destroyPQExpBuffer(tbloids);
destroyPQExpBuffer(checkoids);
@@ -10342,6 +10452,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
case DO_FK_CONSTRAINT:
dumpConstraint(fout, (const ConstraintInfo *) dobj);
break;
+ case DO_PERIOD:
+ break;
case DO_PROCLANG:
dumpProcLang(fout, (const ProcLangInfo *) dobj);
break;
@@ -15877,6 +15989,32 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
}
}
+ /*
+ * Add non-inherited PERIOD definitions, if any.
+ */
+ for (j = 0; j < tbinfo->nperiod; j++)
+ {
+ PeriodInfo *period = &(tbinfo->periods[j]);
+
+ char *name = pg_strdup(fmtId(period->dobj.name));
+ char *start = pg_strdup(fmtId(period->perstart));
+ char *end = pg_strdup(fmtId(period->perend));
+ char *rngtype = pg_strdup(fmtId(period->rngtype));
+ char *conname = pg_strdup(fmtId(period->conname));
+
+ if (actual_atts == 0)
+ appendPQExpBufferStr(q, " (\n ");
+ else
+ appendPQExpBufferStr(q, ",\n ");
+
+ appendPQExpBuffer(q, "PERIOD FOR %s (%s, %s) "
+ "WITH (rangetype = %s, check_constraint_name = %s)",
+ name, start, end,
+ rngtype, conname);
+
+ actual_atts++;
+ }
+
/*
* Add non-inherited CHECK constraints, if any.
*
@@ -15885,7 +16023,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
* PARTITION that we'll emit later expects the constraint to be
* there. (No need to fix conislocal: ATTACH PARTITION does that)
*/
- for (j = 0; j < tbinfo->ncheck; j++)
+ for (j = 0; j < tbinfo->ncheck - tbinfo->nperiod; j++)
{
ConstraintInfo *constr = &(tbinfo->checkexprs[j]);
@@ -16103,7 +16241,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
* For partitions, they were already dumped, and conislocal
* doesn't need fixing.
*/
- for (k = 0; k < tbinfo->ncheck; k++)
+ for (k = 0; k < tbinfo->ncheck - tbinfo->nperiod; k++)
{
ConstraintInfo *constr = &(tbinfo->checkexprs[k]);
@@ -16405,7 +16543,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
dumpTableSecLabel(fout, tbinfo, reltypename);
/* Dump comments on inlined table constraints */
- for (j = 0; j < tbinfo->ncheck; j++)
+ for (j = 0; j < tbinfo->ncheck - tbinfo->nperiod; j++)
{
ConstraintInfo *constr = &(tbinfo->checkexprs[j]);
@@ -18501,6 +18639,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
case DO_TRIGGER:
case DO_EVENT_TRIGGER:
case DO_DEFAULT_ACL:
+ case DO_PERIOD:
case DO_POLICY:
case DO_PUBLICATION:
case DO_PUBLICATION_REL:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 773a2de18b..199b25571b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -59,6 +59,7 @@ typedef enum
DO_TRIGGER,
DO_CONSTRAINT,
DO_FK_CONSTRAINT, /* see note for ConstraintInfo */
+ DO_PERIOD,
DO_PROCLANG,
DO_CAST,
DO_TABLE_DATA,
@@ -299,12 +300,14 @@ typedef struct _tableInfo
bool rowsec; /* is row security enabled? */
bool forcerowsec; /* is row security forced? */
bool hasoids; /* does it have OIDs? */
+ bool hasperiods; /* does it have any periods? */
uint32 frozenxid; /* table's relfrozenxid */
uint32 minmxid; /* table's relminmxid */
Oid toast_oid; /* toast table's OID, or 0 if none */
uint32 toast_frozenxid; /* toast table's relfrozenxid, if any */
uint32 toast_minmxid; /* toast table's relminmxid */
int ncheck; /* # of CHECK expressions */
+ int nperiod; /* # of PERIOD definitions */
Oid reltype; /* OID of table's composite type, if any */
Oid reloftype; /* underlying type for typed table */
Oid foreign_server; /* foreign server oid, if applicable */
@@ -354,6 +357,7 @@ typedef struct _tableInfo
bool *notnull_inh; /* true if NOT NULL has no local definition */
struct _attrDefInfo **attrdefs; /* DEFAULT expressions */
struct _constraintInfo *checkexprs; /* CHECK constraints */
+ struct _periodInfo *periods; /* PERIOD definitions */
bool needs_override; /* has GENERATED ALWAYS AS IDENTITY */
char *amname; /* relation access method */
@@ -492,6 +496,16 @@ typedef struct _constraintInfo
bool withoutoverlaps; /* true if the last elem is WITHOUT OVERLAPS */
} ConstraintInfo;
+typedef struct _periodInfo
+{
+ DumpableObject dobj;
+ TableInfo *pertable;
+ char *perstart; /* the name of the start column */
+ char *perend; /* the name of the end column */
+ char *rngtype; /* the name of the range type */
+ char *conname; /* the name of the CHECK constraint */
+} PeriodInfo;
+
typedef struct _procLangInfo
{
DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 523a19c155..55f2b68d0f 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -84,6 +84,7 @@ enum dbObjectTypePriorities
PRIO_CONSTRAINT,
PRIO_INDEX,
PRIO_INDEX_ATTACH,
+ PRIO_PERIOD,
PRIO_STATSEXT,
PRIO_RULE,
PRIO_TRIGGER,
@@ -118,6 +119,7 @@ static const int dbObjectTypePriority[] =
PRIO_ATTRDEF, /* DO_ATTRDEF */
PRIO_INDEX, /* DO_INDEX */
PRIO_INDEX_ATTACH, /* DO_INDEX_ATTACH */
+ PRIO_PERIOD, /* DO_PERIOD */
PRIO_STATSEXT, /* DO_STATSEXT */
PRIO_RULE, /* DO_RULE */
PRIO_TRIGGER, /* DO_TRIGGER */
@@ -1438,6 +1440,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
"FK CONSTRAINT %s (ID %d OID %u)",
obj->name, obj->dumpId, obj->catId.oid);
return;
+ case DO_PERIOD:
+ snprintf(buf, bufsize,
+ "PERIOD %s (ID %d OID %u)",
+ obj->name, obj->dumpId, obj->catId.oid);
+ return;
case DO_PROCLANG:
snprintf(buf, bufsize,
"PROCEDURAL LANGUAGE %s (ID %d OID %u)",
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 4fbfc07460..e38b9a1c3a 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1948,6 +1948,7 @@ describeOneTableDetails(const char *schemaname,
appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_attribute a");
appendPQExpBuffer(&buf, "\nWHERE a.attrelid = '%s' AND a.attnum > 0 AND NOT a.attisdropped", oid);
+ appendPQExpBuffer(&buf, "\nAND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_period p WHERE p.perrelid = a.attrelid AND p.perrange = a.attnum)");
appendPQExpBufferStr(&buf, "\nORDER BY a.attnum;");
res = PSQLexec(buf.data);
@@ -2370,6 +2371,40 @@ describeOneTableDetails(const char *schemaname,
PGresult *result = NULL;
int tuples = 0;
+ /* print periods */
+ if (pset.sversion >= 150000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT quote_ident(p.pername), quote_ident(s.attname) AS startatt, quote_ident(e.attname) AS endatt\n"
+ "FROM pg_period AS p\n"
+ "JOIN pg_attribute AS s ON (s.attrelid, s.attnum) = (p.perrelid, p.perstart)\n"
+ "JOIN pg_attribute AS e ON (e.attrelid, e.attnum) = (p.perrelid, p.perend)\n"
+ "WHERE p.perrelid = '%s'\n"
+ "ORDER BY 1;",
+ oid);
+ result = PSQLexec(buf.data);
+ if (!result)
+ goto error_return;
+ else
+ tuples = PQntuples(result);
+
+ if (tuples > 0)
+ {
+ printTableAddFooter(&cont, _("Periods:"));
+ for (i = 0; i < tuples; i++)
+ {
+ /* untranslated constraint name and def */
+ printfPQExpBuffer(&buf, " %s (%s, %s)",
+ PQgetvalue(result, i, 0),
+ PQgetvalue(result, i, 1),
+ PQgetvalue(result, i, 2));
+
+ printTableAddFooter(&cont, buf.data);
+ }
+ }
+ PQclear(result);
+ }
+
/* print indexes */
if (tableinfo.hasindex)
{
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index ffd5e9dc82..8f03bf3de1 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -93,6 +93,7 @@ typedef enum ObjectClass
OCLASS_CAST, /* pg_cast */
OCLASS_COLLATION, /* pg_collation */
OCLASS_CONSTRAINT, /* pg_constraint */
+ OCLASS_PERIOD, /* pg_period */
OCLASS_CONVERSION, /* pg_conversion */
OCLASS_DEFAULT, /* pg_attrdef */
OCLASS_LANGUAGE, /* pg_language */
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index 51f7b12aa3..1f646f3d3b 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -121,6 +121,10 @@ extern List *AddRelationNotNullConstraints(Relation rel,
extern void RelationClearMissing(Relation rel);
extern void SetAttrMissing(Oid relid, char *attname, char *value);
+extern Oid StorePeriod(Relation rel, const char *period,
+ AttrNumber startnum, AttrNumber endnum,
+ AttrNumber rangenum, Oid conoid);
+
extern Node *cookDefault(ParseState *pstate,
Node *raw_default,
Oid atttypid,
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index dcb3c5f766..19437291a4 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -61,6 +61,7 @@ catalog_headers = [
'pg_collation.h',
'pg_parameter_acl.h',
'pg_partitioned_table.h',
+ 'pg_period.h',
'pg_range.h',
'pg_transform.h',
'pg_sequence.h',
diff --git a/src/include/catalog/pg_index.h b/src/include/catalog/pg_index.h
index 32930411b6..acfbef9e27 100644
--- a/src/include/catalog/pg_index.h
+++ b/src/include/catalog/pg_index.h
@@ -44,11 +44,11 @@ CATALOG(pg_index,2610,IndexRelationId) BKI_SCHEMA_MACRO
bool indisready; /* is this index ready for inserts? */
bool indislive; /* is this index alive at all? */
bool indisreplident; /* is this index the identity for replication? */
+ Oid indperiod; /* the period it contains, if any */
/* variable-length fields start here, but we allow direct access to indkey */
int2vector indkey BKI_FORCE_NOT_NULL; /* column numbers of indexed cols,
* or 0 */
-
#ifdef CATALOG_VARLEN
oidvector indcollation BKI_LOOKUP_OPT(pg_collation) BKI_FORCE_NOT_NULL; /* collation identifiers */
oidvector indclass BKI_LOOKUP(pg_opclass) BKI_FORCE_NOT_NULL; /* opclass identifiers */
diff --git a/src/include/catalog/pg_period.h b/src/include/catalog/pg_period.h
new file mode 100644
index 0000000000..d2cffa07bc
--- /dev/null
+++ b/src/include/catalog/pg_period.h
@@ -0,0 +1,54 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_period.h
+ * definition of the "period" system catalog (pg_period)
+ *
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_period.h
+ *
+ * NOTES
+ * The Catalog.pm module reads this file and derives schema
+ * information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_PERIOD_H
+#define PG_PERIOD_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_period_d.h"
+
+/* ----------------
+ * pg_period definition. cpp turns this into
+ * typedef struct FormData_pg_period
+ * ----------------
+ */
+CATALOG(pg_period,8000,PeriodRelationId)
+{
+ Oid oid; /* OID of the period */
+ NameData pername; /* name of period */
+ Oid perrelid; /* OID of relation containing this period */
+ int16 perstart; /* column for start value */
+ int16 perend; /* column for end value */
+ int16 perrange; /* column for range value */
+ Oid perconstraint; /* OID of (start < end) constraint */
+} FormData_pg_period;
+
+/* ----------------
+ * Form_pg_period corresponds to a pointer to a tuple with
+ * the format of pg_period relation.
+ * ----------------
+ */
+typedef FormData_pg_period *Form_pg_period;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_period_oid_index, 8001, PeriodObjectIndexId, pg_period, btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_period_perrelid_pername_index, 8002, PeriodRelidNameIndexId, pg_period, btree(perrelid oid_ops, pername name_ops));
+
+extern void RemovePeriodById(Oid periodId);
+
+extern Oid get_relation_period_oid(Oid relid, const char *pername, bool missing_ok);
+extern Bitmapset *get_period_attnos(Oid relid);
+
+#endif /* PG_PERIOD_H */
diff --git a/src/include/catalog/pg_range.h b/src/include/catalog/pg_range.h
index 1247fa7c77..7ed014b71d 100644
--- a/src/include/catalog/pg_range.h
+++ b/src/include/catalog/pg_range.h
@@ -59,6 +59,7 @@ typedef FormData_pg_range *Form_pg_range;
DECLARE_UNIQUE_INDEX_PKEY(pg_range_rngtypid_index, 3542, RangeTypidIndexId, pg_range, btree(rngtypid oid_ops));
DECLARE_UNIQUE_INDEX(pg_range_rngmultitypid_index, 2228, RangeMultirangeTypidIndexId, pg_range, btree(rngmultitypid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_range_rngsubtype_rngtypid_index, 8003, RangeSubTypidTypidIndexId, pg_range, btree(rngsubtype oid_ops, rngtypid oid_ops));
/*
* prototypes for functions in pg_range.c
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 16b6126669..e918d9a633 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -38,7 +38,8 @@ extern LOCKMODE AlterTableGetLockLevel(List *cmds);
extern void ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lockmode);
-extern void AlterTableInternal(Oid relid, List *cmds, bool recurse);
+extern void AlterTableInternal(Oid relid, List *cmds, bool recurse,
+ struct AlterTableUtilityContext *context);
extern Oid AlterTableMoveAll(AlterTableMoveAllStmt *stmt);
@@ -103,5 +104,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
Oid relId, Oid oldRelId, void *arg);
extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
List *partConstraint);
+extern Oid choose_rangetype_for_period(PeriodDef *period);
#endif /* TABLECMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 177df96c86..52de618d1f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2163,6 +2163,7 @@ typedef enum ObjectType
OBJECT_OPERATOR,
OBJECT_OPFAMILY,
OBJECT_PARAMETER_ACL,
+ OBJECT_PERIOD,
OBJECT_POLICY,
OBJECT_PROCEDURE,
OBJECT_PUBLICATION,
@@ -2250,6 +2251,8 @@ typedef enum AlterTableType
AT_ValidateConstraint, /* validate constraint */
AT_AddIndexConstraint, /* add constraint using existing index */
AT_DropConstraint, /* drop constraint */
+ AT_AddPeriod, /* ADD PERIOD */
+ AT_DropPeriod, /* DROP PERIOD */
AT_ReAddComment, /* internal to commands/tablecmds.c */
AT_AlterColumnType, /* alter column type */
AT_AlterColumnGenericOptions, /* alter column OPTIONS (...) */
@@ -2516,11 +2519,11 @@ typedef struct VariableShowStmt
/* ----------------------
* Create Table Statement
*
- * NOTE: in the raw gram.y output, ColumnDef and Constraint nodes are
- * intermixed in tableElts, and constraints and nnconstraints are NIL. After
- * parse analysis, tableElts contains just ColumnDefs, nnconstraints contains
- * Constraint nodes of CONSTR_NOTNULL type from various sources, and
- * constraints contains just CONSTR_CHECK Constraint nodes.
+ * NOTE: in the raw gram.y output, ColumnDef, PeriodDef, and Constraint nodes are
+ * intermixed in tableElts; periods, constraints, and nnconstraints are NIL. After
+ * parse analysis, tableElts contains just ColumnDefs, periods contains just PeriodDefs,
+ * nnconstraints contains Constraint nodes of CONSTR_NOTNULL type from various sources,
+ * and constraints contains just CONSTR_CHECK Constraint nodes.
* ----------------------
*/
@@ -2529,6 +2532,7 @@ typedef struct CreateStmt
NodeTag type;
RangeVar *relation; /* relation to create */
List *tableElts; /* column definitions (list of ColumnDef) */
+ List *periods; /* periods (list of PeriodDef nodes) */
List *inhRelations; /* relations to inherit from (list of
* RangeVar) */
PartitionBoundSpec *partbound; /* FOR VALUES clause */
@@ -2543,6 +2547,30 @@ typedef struct CreateStmt
bool if_not_exists; /* just do nothing if it already exists? */
} CreateStmt;
+
+/* ----------
+ * Definitions for periods in CreateStmt
+ * ----------
+ */
+
+typedef struct PeriodDef
+{
+ NodeTag type;
+ Oid oid; /* period oid, once it's transformed */
+ char *periodname; /* period name */
+ char *startcolname; /* name of start column */
+ char *endcolname; /* name of end column */
+ AttrNumber startattnum; /* attnum of the start column */
+ AttrNumber endattnum; /* attnum of the end column */
+ AttrNumber rngattnum; /* attnum of the GENERATED range column */
+ List *options; /* options from WITH clause */
+ char *constraintname; /* name of the CHECK constraint */
+ char *rangetypename; /* name of the range type */
+ Oid coltypid; /* the start/end col type */
+ Oid rngtypid; /* the range type to use */
+ int location; /* token location, or -1 if unknown */
+} PeriodDef;
+
/* ----------
* Definitions for constraints in CreateStmt
*
@@ -3252,6 +3280,7 @@ typedef struct IndexStmt
List *indexParams; /* columns to index: a list of IndexElem */
List *indexIncludingParams; /* additional columns to index: a list
* of IndexElem */
+ PeriodDef *period; /* The period included in the index */
List *options; /* WITH clause options: a list of DefElem */
Node *whereClause; /* qualification (partial-index predicate) */
List *excludeOpNames; /* exclusion operator names, or NIL if none */
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index 2b40a0b5b1..feb6c26007 100644
--- a/src/include/parser/parse_utilcmd.h
+++ b/src/include/parser/parse_utilcmd.h
@@ -40,5 +40,6 @@ extern IndexStmt *generateClonedIndexStmt(RangeVar *heapRel,
Relation source_idx,
const struct AttrMap *attmap,
Oid *constraintOid);
+extern void transformPeriodOptions(PeriodDef *period);
#endif /* PARSE_UTILCMD_H */
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index a117527d8a..f015b51c86 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -96,6 +96,8 @@ extern Oid get_atttype(Oid relid, AttrNumber attnum);
extern void get_atttypetypmodcoll(Oid relid, AttrNumber attnum,
Oid *typid, int32 *typmod, Oid *collid);
extern Datum get_attoptions(Oid relid, int16 attnum);
+extern char *get_periodname(Oid periodid, bool missing_ok);
+extern Oid get_period_oid(Oid relid, const char *periodname, bool missing_ok);
extern Oid get_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok);
extern char *get_collation_name(Oid colloid);
extern bool get_collation_isdeterministic(Oid colloid);
@@ -197,6 +199,7 @@ extern Oid get_range_subtype(Oid rangeOid);
extern Oid get_range_collation(Oid rangeOid);
extern Oid get_range_multirange(Oid rangeOid);
extern Oid get_multirange_range(Oid multirangeOid);
+extern Oid get_subtype_range(Oid subtypeOid);
extern Oid get_index_column_opclass(Oid index_oid, int attno);
extern bool get_index_isreplident(Oid index_oid);
extern bool get_index_isvalid(Oid index_oid);
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index 67ea6e4945..33bab4061a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -75,6 +75,8 @@ enum SysCacheIdentifier
PARAMETERACLNAME,
PARAMETERACLOID,
PARTRELID,
+ PERIODNAME,
+ PERIODOID,
PROCNAMEARGSNSP,
PROCOID,
PUBLICATIONNAME,
@@ -84,6 +86,7 @@ enum SysCacheIdentifier
PUBLICATIONREL,
PUBLICATIONRELMAP,
RANGEMULTIRANGE,
+ RANGESUBTYPE,
RANGETYPE,
RELNAMENSP,
RELOID,
diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
index 0302f79bb7..05f9780ecf 100644
--- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
+++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
@@ -180,6 +180,12 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS)
case AT_DropConstraint:
strtype = "DROP CONSTRAINT";
break;
+ case AT_AddPeriod:
+ strtype = "ADD PERIOD";
+ break;
+ case AT_DropPeriod:
+ strtype = "DROP PERIOD";
+ break;
case AT_ReAddComment:
strtype = "(re) ADD COMMENT";
break;
diff --git a/src/test/regress/expected/periods.out b/src/test/regress/expected/periods.out
new file mode 100644
index 0000000000..0f2b2d453d
--- /dev/null
+++ b/src/test/regress/expected/periods.out
@@ -0,0 +1,173 @@
+/* System periods are not implemented */
+create table pt (id integer, ds date, de date, period for system_time (ds, de));
+ERROR: PERIOD FOR SYSTEM_TIME is not supported
+LINE 2: create table pt (id integer, ds date, de date, period for sy...
+ ^
+/* Periods must specify actual columns */
+create table pt (id integer, ds date, de date, period for p (bogus, de));
+ERROR: column "bogus" of relation "pt" does not exist
+create table pt (id integer, ds date, de date, period for p (ds, bogus));
+ERROR: column "bogus" of relation "pt" does not exist
+/* Data types must match exactly */
+create table pt (id integer, ds date, de timestamp, period for p (ds, de));
+ERROR: start and end columns of period must be of same type
+create table pt (id integer, ds text collate "C", de text collate "POSIX", period for p (ds, de));
+ERROR: start and end columns of period must have same collation
+/* Periods must have a default BTree operator class */
+create table pt (id integer, ds xml, de xml, period for p (ds, de));
+ERROR: no range type for xml found for period p
+HINT: You can define a custom range type with CREATE TYPE
+/* Period and column names are in the same namespace */
+create table pt (id integer, ds date, de date, period for ctid (ds, de));
+ERROR: period name "ctid" conflicts with a system column name
+create table pt (id integer, ds date, de date, period for id (ds, de));
+ERROR: period name "id" conflicts with a column name
+/* Period name can't be given more than once */
+create table pt (id integer, ds date, de date, period for p (ds, de), period for p (ds, de));
+ERROR: period name "p" specified more than once
+/* Now make one that works */
+create table pt (id integer, ds date, de date, period for p (ds, de));
+/* SELECT * excludes the PERIOD */
+insert into pt values (1, '2000-01-01', '2001-01-01');
+select * from pt;
+ id | ds | de
+----+------------+------------
+ 1 | 01-01-2000 | 01-01-2001
+(1 row)
+
+/* You can get it if you want */
+select *, p from pt;
+ id | ds | de | p
+----+------------+------------+-------------------------
+ 1 | 01-01-2000 | 01-01-2001 | [01-01-2000,01-01-2001)
+(1 row)
+
+/* Two are okay */
+create table pt2 (id integer, ds date, de date, period for p1 (ds, de), period for p2 (ds, de));
+drop table pt2;
+/*
+ * ALTER TABLE tests
+ */
+alter table pt drop period for p;
+alter table pt add period for system_time (ds, de);
+ERROR: PERIOD FOR SYSTEM_TIME is not supported
+alter table pt add period for p (ds, de);
+/* Adding a second one */
+create table pt2 (id integer, ds date, de date, period for p1 (ds, de));
+alter table pt2 add period for p2 (ds, de);
+drop table pt2;
+/* Can't drop its columns */
+alter table pt drop column ds;
+ERROR: cannot drop column ds of table pt because other objects depend on it
+DETAIL: period p on table pt depends on column ds of table pt
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+alter table pt drop column de;
+ERROR: cannot drop column de of table pt because other objects depend on it
+DETAIL: period p on table pt depends on column de of table pt
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+/* Can't change the data types */
+alter table pt alter column ds type timestamp;
+ERROR: cannot alter type of a column used by a period
+DETAIL: period p on table pt depends on column "ds"
+alter table pt alter column ds type timestamp;
+ERROR: cannot alter type of a column used by a period
+DETAIL: period p on table pt depends on column "ds"
+/* column/period namespace conflicts */
+alter table pt add column p integer;
+ERROR: column name "p" conflicts with a period name
+alter table pt rename column id to p;
+ERROR: column name "p" conflicts with a period name
+/* adding columns and the period at the same time */
+create table pt2 (id integer);
+alter table pt2 add column ds date, add column de date, add period for p (ds, de);
+drop table pt2;
+/* Ambiguous range types raise an error */
+create type mydaterange as range(subtype=date);
+create table pt2 (id int, ds date, de date, period for p (ds, de));
+ERROR: ambiguous range for type date
+/* You can give an explicit range type */
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'mydaterange'));
+drop type mydaterange;
+ERROR: cannot drop type mydaterange because other objects depend on it
+DETAIL: period p on table pt2 depends on type mydaterange
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
+drop type mydaterange cascade;
+NOTICE: drop cascades to period p on table pt2
+drop table pt2;
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'daterange'));
+/* Range type is not found */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'notarange'));
+ERROR: Range type notarange not found
+/* Range type is the wrong type */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'tstzrange'));
+ERROR: Range type tstzrange does not match column type date
+drop table pt2;
+/* CREATE TABLE (LIKE ...) */
+/* Periods are not copied by LIKE, so their columns aren't either */
+create table pt2 (like pt);
+\d pt2
+ Table "public.pt2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | |
+ ds | date | | |
+ de | date | | |
+
+drop table pt2;
+/* Can add a period referring to LIKE'd columns */
+create table not_p (id integer, ds date, de date);
+create table pt2 (like not_p, period for p (ds, de));
+\d pt2
+ Table "public.pt2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | |
+ ds | date | | |
+ de | date | | |
+Periods:
+ p (ds, de)
+Check constraints:
+ "pt2_p_check" CHECK (ds < de)
+
+drop table pt2;
+/* Can add a period with the same name */
+create table pt2 (like pt, period for p (ds, de));
+\d pt2
+ Table "public.pt2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | |
+ ds | date | | |
+ de | date | | |
+Periods:
+ p (ds, de)
+Check constraints:
+ "pt2_p_check" CHECK (ds < de)
+
+drop table pt2;
+/* Can add a period with a different name */
+create table pt2 (like pt, period for p2 (ds, de));
+\d pt2
+ Table "public.pt2"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ id | integer | | |
+ ds | date | | |
+ de | date | | |
+Periods:
+ p2 (ds, de)
+Check constraints:
+ "pt2_p2_check" CHECK (ds < de)
+
+drop table pt2;
+/* Can't add a period whose name conflicts with a LIKE'd column */
+create table pt2 (like pt, period for id (ds, de));
+ERROR: period name "id" conflicts with a column name
+/* CREATE TALBE INHERITS */
+/* Can't inherit from a table with a period */
+create table pt2 (name text) inherits (pt);
+ERROR: Inheriting from a table with a PERIOD is not supported
+/* Can't inherit with a period */
+create table pt2 (d2s date, d2e date, period for p (d2s, d2e)) inherits (not_p);
+ERROR: Inheriting is not supported when a table has a PERIOD
+drop table not_p;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 99cf6f18d6..25167712c9 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -43,7 +43,7 @@ test: copy copyselect copydml insert insert_conflict
# Note: many of the tests in later groups depend on create_index
# ----------
test: create_function_c create_misc create_operator create_procedure create_table create_type create_schema
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist periods
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/sql/periods.sql b/src/test/regress/sql/periods.sql
new file mode 100644
index 0000000000..516f1cef9d
--- /dev/null
+++ b/src/test/regress/sql/periods.sql
@@ -0,0 +1,117 @@
+/* System periods are not implemented */
+create table pt (id integer, ds date, de date, period for system_time (ds, de));
+
+/* Periods must specify actual columns */
+create table pt (id integer, ds date, de date, period for p (bogus, de));
+create table pt (id integer, ds date, de date, period for p (ds, bogus));
+
+/* Data types must match exactly */
+create table pt (id integer, ds date, de timestamp, period for p (ds, de));
+create table pt (id integer, ds text collate "C", de text collate "POSIX", period for p (ds, de));
+
+/* Periods must have a default BTree operator class */
+create table pt (id integer, ds xml, de xml, period for p (ds, de));
+
+/* Period and column names are in the same namespace */
+create table pt (id integer, ds date, de date, period for ctid (ds, de));
+create table pt (id integer, ds date, de date, period for id (ds, de));
+
+/* Period name can't be given more than once */
+create table pt (id integer, ds date, de date, period for p (ds, de), period for p (ds, de));
+
+/* Now make one that works */
+create table pt (id integer, ds date, de date, period for p (ds, de));
+
+/* SELECT * excludes the PERIOD */
+insert into pt values (1, '2000-01-01', '2001-01-01');
+select * from pt;
+
+/* You can get it if you want */
+select *, p from pt;
+
+/* Two are okay */
+create table pt2 (id integer, ds date, de date, period for p1 (ds, de), period for p2 (ds, de));
+drop table pt2;
+
+/*
+ * ALTER TABLE tests
+ */
+alter table pt drop period for p;
+alter table pt add period for system_time (ds, de);
+alter table pt add period for p (ds, de);
+
+/* Adding a second one */
+create table pt2 (id integer, ds date, de date, period for p1 (ds, de));
+alter table pt2 add period for p2 (ds, de);
+drop table pt2;
+
+/* Can't drop its columns */
+alter table pt drop column ds;
+alter table pt drop column de;
+
+/* Can't change the data types */
+alter table pt alter column ds type timestamp;
+alter table pt alter column ds type timestamp;
+
+/* column/period namespace conflicts */
+alter table pt add column p integer;
+alter table pt rename column id to p;
+
+/* adding columns and the period at the same time */
+create table pt2 (id integer);
+alter table pt2 add column ds date, add column de date, add period for p (ds, de);
+drop table pt2;
+
+/* Ambiguous range types raise an error */
+create type mydaterange as range(subtype=date);
+create table pt2 (id int, ds date, de date, period for p (ds, de));
+
+/* You can give an explicit range type */
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'mydaterange'));
+drop type mydaterange;
+drop type mydaterange cascade;
+drop table pt2;
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'daterange'));
+
+/* Range type is not found */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'notarange'));
+
+/* Range type is the wrong type */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'tstzrange'));
+drop table pt2;
+
+/* CREATE TABLE (LIKE ...) */
+
+/* Periods are not copied by LIKE, so their columns aren't either */
+create table pt2 (like pt);
+\d pt2
+drop table pt2;
+
+/* Can add a period referring to LIKE'd columns */
+create table not_p (id integer, ds date, de date);
+create table pt2 (like not_p, period for p (ds, de));
+\d pt2
+drop table pt2;
+
+/* Can add a period with the same name */
+create table pt2 (like pt, period for p (ds, de));
+\d pt2
+drop table pt2;
+
+/* Can add a period with a different name */
+create table pt2 (like pt, period for p2 (ds, de));
+\d pt2
+drop table pt2;
+
+/* Can't add a period whose name conflicts with a LIKE'd column */
+create table pt2 (like pt, period for id (ds, de));
+
+/* CREATE TALBE INHERITS */
+
+/* Can't inherit from a table with a period */
+create table pt2 (name text) inherits (pt);
+
+/* Can't inherit with a period */
+create table pt2 (d2s date, d2e date, period for p (d2s, d2e)) inherits (not_p);
+
+drop table not_p;
--
2.25.1
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: SQL:2011 application time
@ 2023-09-01 04:02 Corey Huinker <[email protected]>
parent: Paul Jungwirth <[email protected]>
1 sibling, 0 replies; 37+ messages in thread
From: Corey Huinker @ 2023-09-01 04:02 UTC (permalink / raw)
To: Paul Jungwirth <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; PostgreSQL Hackers <[email protected]>; Jaime Casanova <[email protected]>; jian he <[email protected]>; Daniel Gustafsson <[email protected]>
>
> The PERIOD patch is not finished and includes some deliberately-failing
> tests. I did make some progress here finishing ALTER TABLE ADD PERIOD.
>
If it's ok with you, I need PERIODs for System Versioning, and planned on
developing a highly similar version, albeit closer to the standard. It
shouldn't interfere with your work as you're heavily leveraging range
types.
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: SQL:2011 application time
@ 2023-09-01 09:30 Peter Eisentraut <[email protected]>
parent: Paul Jungwirth <[email protected]>
1 sibling, 0 replies; 37+ messages in thread
From: Peter Eisentraut @ 2023-09-01 09:30 UTC (permalink / raw)
To: Paul Jungwirth <[email protected]>; +Cc: Corey Huinker <[email protected]>; PostgreSQL Hackers <[email protected]>
On 31.08.23 23:26, Paul Jungwirth wrote:
> I've tried to clean up the first four patches to get them ready for
> committing, since they could get committed before the PERIOD patch. I
> think there is a little more cleanup needed but they should be ready for
> a review.
Looking at the patch 0001 "Add temporal PRIMARY KEY and UNIQUE constraints":
Generally, this looks like a good direction. The patch looks
comprehensive, with documentation and tests, and appears to cover all
the required pieces (client programs, ruleutils, etc.).
I have two conceptual questions that should be clarified before we go
much further:
1) If I write UNIQUE (a, b, c WITHOUT OVERLAPS), does the WITHOUT
OVERLAPS clause attach to the last column, or to the whole column list?
In the SQL standard, you can only have one period and it has to be
listed last, so this question does not arise. But here we are building
a more general facility to then build the SQL facility on top of. So I
think it doesn't make sense that the range column must be last or that
there can only be one. Also, your implementation requires at least one
non-overlaps column, which also seems like a confusing restriction.
I think the WITHOUT OVERLAPS clause should be per-column, so that
something like UNIQUE (a WITHOUT OVERLAPS, b, c WITHOUT OVERLAPS) would
be possible. Then the WITHOUT OVERLAPS clause would directly correspond
to the choice between equality or overlaps operator per column.
An alternative interpretation would be that WITHOUT OVERLAPS applies to
the whole column list, and we would take it to mean, for any range
column, use the overlaps operator, for any non-range column, use the
equals operator. But I think this would be confusing and would prevent
the case of using the equality operator for some ranges and the overlaps
operator for some other ranges in the same key.
2) The logic hinges on get_index_attr_temporal_operator(), to pick the
equality and overlaps operator for each column. For btree indexes, the
strategy numbers are fixed, so this is straightforward. But for gist
indexes, the strategy numbers are more like recommendations. Are we
comfortable with how this works? I mean, we could say, if you want to
be able to take advantage of the WITHOUT OVERLAPS syntax, you have to
use these numbers, otherwise you're on your own. It looks like the gist
strategy numbers are already hardcoded in a number of places, so maybe
that's all okay, but I feel we should be more explicit about this
somewhere, maybe in the documentation, or at least in code comments.
Besides that, some stylistic comments:
* There is a lot of talk about "temporal" in this patch, but this
functionality is more general than temporal. I would prefer to change
this to more neutral terms like "overlaps".
* The field ii_Temporal in IndexInfo doesn't seem necessary and could be
handled via local variables. See [0] for a similar discussion:
[0]:
https://www.postgresql.org/message-id/flat/[email protected]
* In gram.y, change withoutOverlapsClause -> without_overlaps_clause for
consistency with the surrounding code.
* No-op assignments like n->without_overlaps = NULL; can be omitted.
(Or you should put them everywhere. But only in some places seems
inconsistent and confusing.)
^ permalink raw reply [nested|flat] 37+ messages in thread
* [PATCH v25 8/9] Row pattern recognition patch (typedefs.list).
@ 2024-12-21 06:19 Tatsuo Ishii <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Tatsuo Ishii @ 2024-12-21 06:19 UTC (permalink / raw)
---
src/tools/pgindent/typedefs.list | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 971a150dbb..bf78e162bc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1690,6 +1690,7 @@ NamedLWLockTrancheRequest
NamedTuplestoreScan
NamedTuplestoreScanState
NamespaceInfo
+NavigationInfo
NestLoop
NestLoopParam
NestLoopState
@@ -2316,6 +2317,9 @@ RI_CompareKey
RI_ConstraintInfo
RI_QueryHashEntry
RI_QueryKey
+RPCommonSyntax
+RPSkipTo
+RPSubsetItem
RTEKind
RTEPermissionInfo
RWConflict
@@ -2670,6 +2674,7 @@ SimpleStringList
SimpleStringListCell
SingleBoundSortItem
Size
+SkipContext
SkipPages
SlabBlock
SlabContext
@@ -2761,6 +2766,7 @@ StreamStopReason
String
StringInfo
StringInfoData
+StringSet
StripnullState
SubLink
SubLinkType
@@ -3085,6 +3091,7 @@ VarString
VarStringSortSupport
Variable
VariableAssignHook
+VariablePos
VariableSetKind
VariableSetStmt
VariableShowStmt
--
2.25.1
----Next_Part(Sat_Dec_21_18_20_04_2024_526)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="v25-0009-Allow-to-print-raw-parse-tree.patch"
^ permalink raw reply [nested|flat] 37+ messages in thread
* [PATCH v25 8/9] Row pattern recognition patch (typedefs.list).
@ 2024-12-21 06:19 Tatsuo Ishii <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Tatsuo Ishii @ 2024-12-21 06:19 UTC (permalink / raw)
---
src/tools/pgindent/typedefs.list | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 971a150dbb..bf78e162bc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1690,6 +1690,7 @@ NamedLWLockTrancheRequest
NamedTuplestoreScan
NamedTuplestoreScanState
NamespaceInfo
+NavigationInfo
NestLoop
NestLoopParam
NestLoopState
@@ -2316,6 +2317,9 @@ RI_CompareKey
RI_ConstraintInfo
RI_QueryHashEntry
RI_QueryKey
+RPCommonSyntax
+RPSkipTo
+RPSubsetItem
RTEKind
RTEPermissionInfo
RWConflict
@@ -2670,6 +2674,7 @@ SimpleStringList
SimpleStringListCell
SingleBoundSortItem
Size
+SkipContext
SkipPages
SlabBlock
SlabContext
@@ -2761,6 +2766,7 @@ StreamStopReason
String
StringInfo
StringInfoData
+StringSet
StripnullState
SubLink
SubLinkType
@@ -3085,6 +3091,7 @@ VarString
VarStringSortSupport
Variable
VariableAssignHook
+VariablePos
VariableSetKind
VariableSetStmt
VariableShowStmt
--
2.25.1
----Next_Part(Sat_Dec_21_18_20_04_2024_526)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="v25-0009-Allow-to-print-raw-parse-tree.patch"
^ permalink raw reply [nested|flat] 37+ messages in thread
* [PATCH v25 8/9] Row pattern recognition patch (typedefs.list).
@ 2024-12-21 06:19 Tatsuo Ishii <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Tatsuo Ishii @ 2024-12-21 06:19 UTC (permalink / raw)
---
src/tools/pgindent/typedefs.list | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 971a150dbb..bf78e162bc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1690,6 +1690,7 @@ NamedLWLockTrancheRequest
NamedTuplestoreScan
NamedTuplestoreScanState
NamespaceInfo
+NavigationInfo
NestLoop
NestLoopParam
NestLoopState
@@ -2316,6 +2317,9 @@ RI_CompareKey
RI_ConstraintInfo
RI_QueryHashEntry
RI_QueryKey
+RPCommonSyntax
+RPSkipTo
+RPSubsetItem
RTEKind
RTEPermissionInfo
RWConflict
@@ -2670,6 +2674,7 @@ SimpleStringList
SimpleStringListCell
SingleBoundSortItem
Size
+SkipContext
SkipPages
SlabBlock
SlabContext
@@ -2761,6 +2766,7 @@ StreamStopReason
String
StringInfo
StringInfoData
+StringSet
StripnullState
SubLink
SubLinkType
@@ -3085,6 +3091,7 @@ VarString
VarStringSortSupport
Variable
VariableAssignHook
+VariablePos
VariableSetKind
VariableSetStmt
VariableShowStmt
--
2.25.1
----Next_Part(Sat_Dec_21_18_20_04_2024_526)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="v25-0009-Allow-to-print-raw-parse-tree.patch"
^ permalink raw reply [nested|flat] 37+ messages in thread
* [PATCH v26 8/9] Row pattern recognition patch (typedefs.list).
@ 2024-12-30 12:44 Tatsuo Ishii <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Tatsuo Ishii @ 2024-12-30 12:44 UTC (permalink / raw)
---
src/tools/pgindent/typedefs.list | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e1c4f913f8..72906d0fc1 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1691,6 +1691,7 @@ NamedLWLockTrancheRequest
NamedTuplestoreScan
NamedTuplestoreScanState
NamespaceInfo
+NavigationInfo
NestLoop
NestLoopParam
NestLoopState
@@ -2317,6 +2318,9 @@ RI_CompareKey
RI_ConstraintInfo
RI_QueryHashEntry
RI_QueryKey
+RPCommonSyntax
+RPSkipTo
+RPSubsetItem
RTEKind
RTEPermissionInfo
RWConflict
@@ -2673,6 +2677,7 @@ SimpleStringList
SimpleStringListCell
SingleBoundSortItem
Size
+SkipContext
SkipPages
SlabBlock
SlabContext
@@ -2764,6 +2769,7 @@ StreamStopReason
String
StringInfo
StringInfoData
+StringSet
StripnullState
SubLink
SubLinkType
@@ -3088,6 +3094,7 @@ VarString
VarStringSortSupport
Variable
VariableAssignHook
+VariablePos
VariableSetKind
VariableSetStmt
VariableShowStmt
--
2.25.1
----Next_Part(Mon_Dec_30_22_37_18_2024_171)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="v26-0009-Allow-to-print-raw-parse-tree.patch"
^ permalink raw reply [nested|flat] 37+ messages in thread
* [PATCH v26 8/9] Row pattern recognition patch (typedefs.list).
@ 2024-12-30 12:44 Tatsuo Ishii <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Tatsuo Ishii @ 2024-12-30 12:44 UTC (permalink / raw)
---
src/tools/pgindent/typedefs.list | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e1c4f913f8..72906d0fc1 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1691,6 +1691,7 @@ NamedLWLockTrancheRequest
NamedTuplestoreScan
NamedTuplestoreScanState
NamespaceInfo
+NavigationInfo
NestLoop
NestLoopParam
NestLoopState
@@ -2317,6 +2318,9 @@ RI_CompareKey
RI_ConstraintInfo
RI_QueryHashEntry
RI_QueryKey
+RPCommonSyntax
+RPSkipTo
+RPSubsetItem
RTEKind
RTEPermissionInfo
RWConflict
@@ -2673,6 +2677,7 @@ SimpleStringList
SimpleStringListCell
SingleBoundSortItem
Size
+SkipContext
SkipPages
SlabBlock
SlabContext
@@ -2764,6 +2769,7 @@ StreamStopReason
String
StringInfo
StringInfoData
+StringSet
StripnullState
SubLink
SubLinkType
@@ -3088,6 +3094,7 @@ VarString
VarStringSortSupport
Variable
VariableAssignHook
+VariablePos
VariableSetKind
VariableSetStmt
VariableShowStmt
--
2.25.1
----Next_Part(Mon_Dec_30_22_37_18_2024_171)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="v26-0009-Allow-to-print-raw-parse-tree.patch"
^ permalink raw reply [nested|flat] 37+ messages in thread
* [PATCH v26 8/9] Row pattern recognition patch (typedefs.list).
@ 2024-12-30 12:44 Tatsuo Ishii <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Tatsuo Ishii @ 2024-12-30 12:44 UTC (permalink / raw)
---
src/tools/pgindent/typedefs.list | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e1c4f913f8..72906d0fc1 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1691,6 +1691,7 @@ NamedLWLockTrancheRequest
NamedTuplestoreScan
NamedTuplestoreScanState
NamespaceInfo
+NavigationInfo
NestLoop
NestLoopParam
NestLoopState
@@ -2317,6 +2318,9 @@ RI_CompareKey
RI_ConstraintInfo
RI_QueryHashEntry
RI_QueryKey
+RPCommonSyntax
+RPSkipTo
+RPSubsetItem
RTEKind
RTEPermissionInfo
RWConflict
@@ -2673,6 +2677,7 @@ SimpleStringList
SimpleStringListCell
SingleBoundSortItem
Size
+SkipContext
SkipPages
SlabBlock
SlabContext
@@ -2764,6 +2769,7 @@ StreamStopReason
String
StringInfo
StringInfoData
+StringSet
StripnullState
SubLink
SubLinkType
@@ -3088,6 +3094,7 @@ VarString
VarStringSortSupport
Variable
VariableAssignHook
+VariablePos
VariableSetKind
VariableSetStmt
VariableShowStmt
--
2.25.1
----Next_Part(Mon_Dec_30_22_37_18_2024_171)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="v26-0009-Allow-to-print-raw-parse-tree.patch"
^ permalink raw reply [nested|flat] 37+ messages in thread
* [PATCH v27 8/9] Row pattern recognition patch (typedefs.list).
@ 2024-12-30 23:53 Tatsuo Ishii <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Tatsuo Ishii @ 2024-12-30 23:53 UTC (permalink / raw)
---
src/tools/pgindent/typedefs.list | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e1c4f913f8..72906d0fc1 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1691,6 +1691,7 @@ NamedLWLockTrancheRequest
NamedTuplestoreScan
NamedTuplestoreScanState
NamespaceInfo
+NavigationInfo
NestLoop
NestLoopParam
NestLoopState
@@ -2317,6 +2318,9 @@ RI_CompareKey
RI_ConstraintInfo
RI_QueryHashEntry
RI_QueryKey
+RPCommonSyntax
+RPSkipTo
+RPSubsetItem
RTEKind
RTEPermissionInfo
RWConflict
@@ -2673,6 +2677,7 @@ SimpleStringList
SimpleStringListCell
SingleBoundSortItem
Size
+SkipContext
SkipPages
SlabBlock
SlabContext
@@ -2764,6 +2769,7 @@ StreamStopReason
String
StringInfo
StringInfoData
+StringSet
StripnullState
SubLink
SubLinkType
@@ -3088,6 +3094,7 @@ VarString
VarStringSortSupport
Variable
VariableAssignHook
+VariablePos
VariableSetKind
VariableSetStmt
VariableShowStmt
--
2.25.1
----Next_Part(Tue_Dec_31_08_57_07_2024_963)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="v27-0009-Allow-to-print-raw-parse-tree.patch"
^ permalink raw reply [nested|flat] 37+ messages in thread
* [PATCH v27 8/9] Row pattern recognition patch (typedefs.list).
@ 2024-12-30 23:53 Tatsuo Ishii <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Tatsuo Ishii @ 2024-12-30 23:53 UTC (permalink / raw)
---
src/tools/pgindent/typedefs.list | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e1c4f913f8..72906d0fc1 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1691,6 +1691,7 @@ NamedLWLockTrancheRequest
NamedTuplestoreScan
NamedTuplestoreScanState
NamespaceInfo
+NavigationInfo
NestLoop
NestLoopParam
NestLoopState
@@ -2317,6 +2318,9 @@ RI_CompareKey
RI_ConstraintInfo
RI_QueryHashEntry
RI_QueryKey
+RPCommonSyntax
+RPSkipTo
+RPSubsetItem
RTEKind
RTEPermissionInfo
RWConflict
@@ -2673,6 +2677,7 @@ SimpleStringList
SimpleStringListCell
SingleBoundSortItem
Size
+SkipContext
SkipPages
SlabBlock
SlabContext
@@ -2764,6 +2769,7 @@ StreamStopReason
String
StringInfo
StringInfoData
+StringSet
StripnullState
SubLink
SubLinkType
@@ -3088,6 +3094,7 @@ VarString
VarStringSortSupport
Variable
VariableAssignHook
+VariablePos
VariableSetKind
VariableSetStmt
VariableShowStmt
--
2.25.1
----Next_Part(Tue_Dec_31_08_57_07_2024_963)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="v27-0009-Allow-to-print-raw-parse-tree.patch"
^ permalink raw reply [nested|flat] 37+ messages in thread
* [PATCH v27 8/9] Row pattern recognition patch (typedefs.list).
@ 2024-12-30 23:53 Tatsuo Ishii <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Tatsuo Ishii @ 2024-12-30 23:53 UTC (permalink / raw)
---
src/tools/pgindent/typedefs.list | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e1c4f913f8..72906d0fc1 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1691,6 +1691,7 @@ NamedLWLockTrancheRequest
NamedTuplestoreScan
NamedTuplestoreScanState
NamespaceInfo
+NavigationInfo
NestLoop
NestLoopParam
NestLoopState
@@ -2317,6 +2318,9 @@ RI_CompareKey
RI_ConstraintInfo
RI_QueryHashEntry
RI_QueryKey
+RPCommonSyntax
+RPSkipTo
+RPSubsetItem
RTEKind
RTEPermissionInfo
RWConflict
@@ -2673,6 +2677,7 @@ SimpleStringList
SimpleStringListCell
SingleBoundSortItem
Size
+SkipContext
SkipPages
SlabBlock
SlabContext
@@ -2764,6 +2769,7 @@ StreamStopReason
String
StringInfo
StringInfoData
+StringSet
StripnullState
SubLink
SubLinkType
@@ -3088,6 +3094,7 @@ VarString
VarStringSortSupport
Variable
VariableAssignHook
+VariablePos
VariableSetKind
VariableSetStmt
VariableShowStmt
--
2.25.1
----Next_Part(Tue_Dec_31_08_57_07_2024_963)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="v27-0009-Allow-to-print-raw-parse-tree.patch"
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-17 15:37 Vik Fearing <[email protected]>
0 siblings, 2 replies; 37+ messages in thread
From: Vik Fearing @ 2025-06-17 15:37 UTC (permalink / raw)
To: Jeff Davis <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; +Cc: pgsql-hackers; Peter Eisentraut <[email protected]>
On 16/12/2024 18:49, Jeff Davis wrote:
> One question I have is whether we want this function to normalize the
> output.
Yes, we do.
I am sorry that I am so late to the party, but I am currently writing
the Change Proposal for the SQL Standard for this function.
For <fold> (which includes LOWER() and UPPER()), the text says in
Section 6.35 GR 7.e:
If the character set of <character factor> is UTF8, UTF16, or UTF32,
then FR is replaced by
Case:
i) If the <search condition> S IS NORMALIZED evaluates to True,
then NORMALIZE (FR)
ii) Otherwise, FR.
Here, FR is the result of the function and S is its argument.
It does not appear to me that our LOWER and UPPER functions obey this
rule, so there is a valid argument that we should continue to ignore it.
Or, we can say that we have at least one of three compliant.
--
Vik Fearing
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-17 18:14 Jeff Davis <[email protected]>
parent: Vik Fearing <[email protected]>
1 sibling, 1 reply; 37+ messages in thread
From: Jeff Davis @ 2025-06-17 18:14 UTC (permalink / raw)
To: Vik Fearing <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; +Cc: pgsql-hackers; Peter Eisentraut <[email protected]>
On Tue, 2025-06-17 at 17:37 +0200, Vik Fearing wrote:
> If the character set of <character factor> is UTF8, UTF16, or UTF32,
> then FR is replaced by
> Case:
> i) If the <search condition> S IS NORMALIZED evaluates to
> True, then NORMALIZE (FR)
> ii) Otherwise, FR.
I read that as "if the input is normalized, then the output should be
normalized", IOW preserve the normalization. But does it mean "preserve
whatever the input normal form is" or "preserve NFC if the input is
NFC, otherwise the normalization is undefined"?
The above wording seems to mean "preserve NFC if the input is NFC",
because that's what NORMALIZE(FR) does when the normal form is
unspecified.
> It does not appear to me that our LOWER and UPPER functions obey this
> rule,
You are correct:
WITH s(t) AS
(SELECT NORMALIZE(U&'\00C1\00DF\0301' COLLATE "en-US-x-icu"))
SELECT UPPER(t) = NORMALIZE(UPPER(t)) FROM s;
?column?
----------
f
> so there is a valid argument that we should continue to ignore it.
> Or, we can say that we have at least one of three compliant.
What do other databases do?
Given how costly normalization can be, imposing that on every caller
seems like a bit much. And favoring NFC for the user unconditionally
might not be the best thing. Then again, NFC is good most of the time,
and there are patches to speed up normalization.
I tend to think that a lot of users who want casefolding would also
want normalization, but it's hard to weigh that against the performance
cost. It might not matter outside of a few edge cases, though I'm not
sure exactly how many.
Regards,
Jeff Davis
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-18 17:09 Vik Fearing <[email protected]>
parent: Jeff Davis <[email protected]>
0 siblings, 1 reply; 37+ messages in thread
From: Vik Fearing @ 2025-06-18 17:09 UTC (permalink / raw)
To: Jeff Davis <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; +Cc: pgsql-hackers; Peter Eisentraut <[email protected]>
On 17/06/2025 20:14, Jeff Davis wrote:
> On Tue, 2025-06-17 at 17:37 +0200, Vik Fearing wrote:
>> If the character set of <character factor> is UTF8, UTF16, or UTF32,
>> then FR is replaced by
>> Case:
>> i) If the <search condition> S IS NORMALIZED evaluates to
>> True, then NORMALIZE (FR)
>> ii) Otherwise, FR.
> I read that as "if the input is normalized, then the output should be
> normalized", IOW preserve the normalization. But does it mean "preserve
> whatever the input normal form is" or "preserve NFC if the input is
> NFC, otherwise the normalization is undefined"?
>
> The above wording seems to mean "preserve NFC if the input is NFC",
> because that's what NORMALIZE(FR) does when the normal form is
> unspecified.
Yes, and that is also the default for <normalized predicate>.
>> It does not appear to me that our LOWER and UPPER functions obey this
>> rule,
> You are correct:
>
> WITH s(t) AS
> (SELECT NORMALIZE(U&'\00C1\00DF\0301' COLLATE "en-US-x-icu"))
> SELECT UPPER(t) = NORMALIZE(UPPER(t)) FROM s;
> ?column?
> ----------
> f
>
>> so there is a valid argument that we should continue to ignore it.
>> Or, we can say that we have at least one of three compliant.
> What do other databases do?
I don't know. I am just pointing out what the Standard says. I think
we should either comply, or say that we don't do it for LOWER and UPPER
so let's keep things implementation-consistent.
> Given how costly normalization can be, imposing that on every caller
> seems like a bit much.
How much does it cost to check for NFC? I honestly don't know the
answer to that question, but that is the only case where we need to
maintain normalization.
> And favoring NFC for the user unconditionally
> might not be the best thing. Then again, NFC is good most of the time,
> and there are patches to speed up normalization.
It's not unconditionally, it's only if the input was NFC.
> I tend to think that a lot of users who want casefolding would also
> want normalization, but it's hard to weigh that against the performance
> cost. It might not matter outside of a few edge cases, though I'm not
> sure exactly how many.
I defer to you and others in the thread to make this decision.
--
Vik Fearing
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 02:53 Jeff Davis <[email protected]>
parent: Vik Fearing <[email protected]>
0 siblings, 1 reply; 37+ messages in thread
From: Jeff Davis @ 2025-06-19 02:53 UTC (permalink / raw)
To: Vik Fearing <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; +Cc: pgsql-hackers; Peter Eisentraut <[email protected]>
On Wed, 2025-06-18 at 19:09 +0200, Vik Fearing wrote:
> I don't know. I am just pointing out what the Standard says. I
> think
> we should either comply, or say that we don't do it for LOWER and
> UPPER
> so let's keep things implementation-consistent.
For the standard, I see two potential philosophies:
I. CASEFOLD() is another variant of LOWER()/UPPER(), and it should
preserve NFC in the same way.
II. CASEFOLD() is not like LOWER()/UPPER(); it returns a semi-opaque
text value that is useful for caseless matching, but should not
ordinarily be used for display or sent to the application (those things
would be allowed, just not encouraged). For normalization, either:
(A) Follow Unicode Default Caseless Matching (16.0 3.13.5 D144), and
don't require any kind of normalization; or
(B) Follow Unicode Canonical Caseless Matching (D145), and require
that the input and output are normalized appropriately, but leave the
precise normal form as implementation-defined.
The current implementation could either be seen as philosophy (I) where
we've chosen to ignore the normalization part for the sake of
consistency with LOWER()/UPPER(); or it could be seen as philosophy
(II)(A).
> How much does it cost to check for NFC? I honestly don't know the
> answer to that question, but that is the only case where we need to
> maintain normalization.
I attached a very rough patch and ran a very simple test on strings
averaging 36 bytes in length, all already in NFC and the result is also
NFC. Before the patch, doing a CASEFOLD() on 10M tuples took about 3
seconds, afterward about 8.
There's a patch to optimize some of the normalization paths, which I
haven't had a chance to review yet. So those numbers might come down.
>
> It's not unconditionally, it's only if the input was NFC.
Optimizing the case where the input is _not_ NFC seems strange to me.
If we are normalizing the output, I'd say we should just make the
output always NFC. Being more strict, this seems likely to comply with
the eventual standard.
Additionally, if we are normalizing the output, then we should also do
the input fixup for U+0345, which would make the result usable for
Canonical Caseless Matching. Again, this seems likely to comply with
the eventual standard.
>
So I only see two reasonable implementations:
1. The current CASEFOLD() implementation.
2. Do the input fixup for U+0345 and unconditionally normalize the
output in NFC.
If there's a case to be made for both implementations, we could also
consider having two functions, say, CASEFOLD() for #1 and NCASEFOLD()
for #2. I'm not sure whether we'd want to standardize one or both of
those functions.
And if you think there's likely to be a collision with the standard
that's hard to anticipate and fix now, then we should consider
reverting CASEFOLD() for 18 and wait for more progress on the
standardization. What's the likelihood that the name changes or
something like that?
Regards,
Jeff Davis
Attachments:
[text/x-patch] casefold_normalize.diff (1.9K, ../../[email protected]/2-casefold_normalize.diff)
download | inline diff:
diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c
index 5bd1e01f7e4..12e688acec6 100644
--- a/src/backend/utils/adt/formatting.c
+++ b/src/backend/utils/adt/formatting.c
@@ -79,6 +79,7 @@
#include "common/int.h"
#include "common/unicode_case.h"
#include "common/unicode_category.h"
+#include "common/unicode_norm.h"
#include "mb/pg_wchar.h"
#include "nodes/miscnodes.h"
#include "parser/scansup.h"
@@ -1866,6 +1867,9 @@ str_casefold(const char *buff, size_t nbytes, Oid collid)
size_t dstsize;
char *dst;
size_t needed;
+ int mblen, i;
+ unsigned char *p;
+ pg_wchar *decoded;
/* first try buffer of equal size plus terminating NUL */
dstsize = srclen + 1;
@@ -1882,7 +1886,54 @@ str_casefold(const char *buff, size_t nbytes, Oid collid)
}
Assert(dst[needed] == '\0');
- result = dst;
+
+ /* convert to pg_wchar */
+ mblen = pg_mbstrlen_with_len(dst, needed);
+ decoded = palloc((mblen + 1) * sizeof(pg_wchar));
+ p = (unsigned char *) dst;
+ for (i = 0; i < mblen; i++)
+ {
+ decoded[i] = utf8_to_unicode(p);
+ p += pg_utf_mblen(p);
+ }
+ decoded[i] = (pg_wchar) '\0';
+
+ if (unicode_is_normalized_quickcheck(UNICODE_NFC, decoded) == UNICODE_NORM_QC_YES)
+ {
+ pfree(decoded);
+ result = dst;
+ }
+ else
+ {
+ pg_wchar *normalized;
+ unsigned char *normalized_utf8;
+
+ normalized = unicode_normalize(UNICODE_NFC, decoded);
+ pfree(decoded);
+
+ /* convert back to UTF-8 string */
+ mblen = 0;
+ for (pg_wchar *wp = normalized; *wp; wp++)
+ {
+ unsigned char buf[4];
+
+ unicode_to_utf8(*wp, buf);
+ mblen += pg_utf_mblen(buf);
+ }
+
+ normalized_utf8 = palloc(mblen + 1);
+
+ p = normalized_utf8;
+ for (pg_wchar *wp = normalized; *wp; wp++)
+ {
+ unicode_to_utf8(*wp, p);
+ p += pg_utf_mblen(p);
+ }
+ *p = '\0';
+ pfree(normalized);
+
+ result = (char *) normalized_utf8;
+ }
}
return result;
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 04:03 Thom Brown <[email protected]>
parent: Jeff Davis <[email protected]>
0 siblings, 2 replies; 37+ messages in thread
From: Thom Brown @ 2025-06-19 04:03 UTC (permalink / raw)
To: Jeff Davis <[email protected]>; +Cc: Vik Fearing <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; pgsql-hackers; Peter Eisentraut <[email protected]>
On Thu, 19 Jun 2025, 03:53 Jeff Davis, <[email protected]> wrote:
> On Wed, 2025-06-18 at 19:09 +0200, Vik Fearing wrote:
> > I don't know. I am just pointing out what the Standard says. I
> > think
> > we should either comply, or say that we don't do it for LOWER and
> > UPPER
> > so let's keep things implementation-consistent.
>
> For the standard, I see two potential philosophies:
>
> I. CASEFOLD() is another variant of LOWER()/UPPER(), and it should
> preserve NFC in the same way.
>
> II. CASEFOLD() is not like LOWER()/UPPER(); it returns a semi-opaque
> text value that is useful for caseless matching, but should not
> ordinarily be used for display or sent to the application (those things
> would be allowed, just not encouraged). For normalization, either:
> (A) Follow Unicode Default Caseless Matching (16.0 3.13.5 D144), and
> don't require any kind of normalization; or
> (B) Follow Unicode Canonical Caseless Matching (D145), and require
> that the input and output are normalized appropriately, but leave the
> precise normal form as implementation-defined.
>
>
> The current implementation could either be seen as philosophy (I) where
> we've chosen to ignore the normalization part for the sake of
> consistency with LOWER()/UPPER(); or it could be seen as philosophy
> (II)(A).
>
> > How much does it cost to check for NFC? I honestly don't know the
> > answer to that question, but that is the only case where we need to
> > maintain normalization.
>
> I attached a very rough patch and ran a very simple test on strings
> averaging 36 bytes in length, all already in NFC and the result is also
> NFC. Before the patch, doing a CASEFOLD() on 10M tuples took about 3
> seconds, afterward about 8.
>
> There's a patch to optimize some of the normalization paths, which I
> haven't had a chance to review yet. So those numbers might come down.
>
> >
> > It's not unconditionally, it's only if the input was NFC.
>
> Optimizing the case where the input is _not_ NFC seems strange to me.
> If we are normalizing the output, I'd say we should just make the
> output always NFC. Being more strict, this seems likely to comply with
> the eventual standard.
>
> Additionally, if we are normalizing the output, then we should also do
> the input fixup for U+0345, which would make the result usable for
> Canonical Caseless Matching. Again, this seems likely to comply with
> the eventual standard.
>
> >
>
> So I only see two reasonable implementations:
>
> 1. The current CASEFOLD() implementation.
>
> 2. Do the input fixup for U+0345 and unconditionally normalize the
> output in NFC.
>
> If there's a case to be made for both implementations, we could also
> consider having two functions, say, CASEFOLD() for #1 and NCASEFOLD()
> for #2. I'm not sure whether we'd want to standardize one or both of
> those functions.
>
> And if you think there's likely to be a collision with the standard
> that's hard to anticipate and fix now, then we should consider
> reverting CASEFOLD() for 18 and wait for more progress on the
> standardization. What's the likelihood that the name changes or
> something like that?
>
Late to the party, but is there an argument for porting this to the citext
type? Or supplementing the extension with an additional type ("cftext"?
*shrug*). It currently uses lower(), so our current recommendation for
dealing with all unicode characters is to use nondeterministic collations.
Thom
>
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 04:26 Jeff Davis <[email protected]>
parent: Thom Brown <[email protected]>
1 sibling, 0 replies; 37+ messages in thread
From: Jeff Davis @ 2025-06-19 04:26 UTC (permalink / raw)
To: Thom Brown <[email protected]>; +Cc: Vik Fearing <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; pgsql-hackers; Peter Eisentraut <[email protected]>
On Thu, 2025-06-19 at 05:03 +0100, Thom Brown wrote:
> Late to the party, but is there an argument for porting this to the
> citext type? Or supplementing the extension with an additional type
> ("cftext"? *shrug*).
CASEFOLD() addresses a lot of the problems with using LOWER(), so that
sounds like a good idea. I'd be interested to hear from users of
citext.
Regards,
Jeff Davis
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 14:47 Peter Eisentraut <[email protected]>
parent: Vik Fearing <[email protected]>
1 sibling, 1 reply; 37+ messages in thread
From: Peter Eisentraut @ 2025-06-19 14:47 UTC (permalink / raw)
To: Vik Fearing <[email protected]>; Jeff Davis <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; +Cc: pgsql-hackers
On 17.06.25 17:37, Vik Fearing wrote:
> For <fold> (which includes LOWER() and UPPER()), the text says in
> Section 6.35 GR 7.e:
>
>
> If the character set of <character factor> is UTF8, UTF16, or UTF32,
> then FR is replaced by
> Case:
> i) If the <search condition> S IS NORMALIZED evaluates to True,
> then NORMALIZE (FR)
> ii) Otherwise, FR.
>
>
> Here, FR is the result of the function and S is its argument.
>
>
> It does not appear to me that our LOWER and UPPER functions obey this
> rule, so there is a valid argument that we should continue to ignore it.
> Or, we can say that we have at least one of three compliant.
The SQL standard also says in a few other places that normalization
should be applied, and we do none of those, so this is probably not a
reason to change CASEFOLD at this point.
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 14:51 Peter Eisentraut <[email protected]>
parent: Thom Brown <[email protected]>
1 sibling, 1 reply; 37+ messages in thread
From: Peter Eisentraut @ 2025-06-19 14:51 UTC (permalink / raw)
To: Thom Brown <[email protected]>; Jeff Davis <[email protected]>; +Cc: Vik Fearing <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; pgsql-hackers
On 19.06.25 06:03, Thom Brown wrote:
> Late to the party, but is there an argument for porting this to the
> citext type? Or supplementing the extension with an additional type
> ("cftext"? *shrug*). It currently uses lower(), so our current
> recommendation for dealing with all unicode characters is to use
> nondeterministic collations.
What is the motivation for wanting a citext variant instead of using
nondeterministic collations?
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 15:36 Thom Brown <[email protected]>
parent: Peter Eisentraut <[email protected]>
0 siblings, 2 replies; 37+ messages in thread
From: Thom Brown @ 2025-06-19 15:36 UTC (permalink / raw)
To: Peter Eisentraut <[email protected]>; +Cc: Jeff Davis <[email protected]>; Vik Fearing <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; pgsql-hackers
On Thu, 19 Jun 2025 at 15:51, Peter Eisentraut <[email protected]> wrote:
>
> On 19.06.25 06:03, Thom Brown wrote:
> > Late to the party, but is there an argument for porting this to the
> > citext type? Or supplementing the extension with an additional type
> > ("cftext"? *shrug*). It currently uses lower(), so our current
> > recommendation for dealing with all unicode characters is to use
> > nondeterministic collations.
>
> What is the motivation for wanting a citext variant instead of using
> nondeterministic collations?
Ease of use, perhaps. It seems easier to use:
column_name cftext
rather than:
CREATE COLLATION case_insensitive_collation (
PROVIDER = icu,
LOCALE = 'und-u-ks-level2',
DETERMINISTIC = FALSE
);
column_name text COLLATE case_insensitive_collation
But I see the arguments against it. It creates an unnecessary
dependency on an extension, and if someone wants to ignore both case
and accents, they may resort to using 2 extensions (citext + unaccent)
when none are needed. I guess I don't feel strongly about it either
way.
Thom
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 16:15 Robert Treat <[email protected]>
parent: Thom Brown <[email protected]>
1 sibling, 0 replies; 37+ messages in thread
From: Robert Treat @ 2025-06-19 16:15 UTC (permalink / raw)
To: Thom Brown <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Jeff Davis <[email protected]>; Vik Fearing <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; pgsql-hackers
On Thu, Jun 19, 2025 at 11:37 AM Thom Brown <[email protected]> wrote:
> On Thu, 19 Jun 2025 at 15:51, Peter Eisentraut <[email protected]> wrote:
> > On 19.06.25 06:03, Thom Brown wrote:
> > > Late to the party, but is there an argument for porting this to the
> > > citext type? Or supplementing the extension with an additional type
> > > ("cftext"? *shrug*). It currently uses lower(), so our current
> > > recommendation for dealing with all unicode characters is to use
> > > nondeterministic collations.
> >
> > What is the motivation for wanting a citext variant instead of using
> > nondeterministic collations?
>
> Ease of use, perhaps. It seems easier to use:
>
> column_name cftext
>
> rather than:
>
> CREATE COLLATION case_insensitive_collation (
> PROVIDER = icu,
> LOCALE = 'und-u-ks-level2',
> DETERMINISTIC = FALSE
> );
>
> column_name text COLLATE case_insensitive_collation
>
> But I see the arguments against it. It creates an unnecessary
> dependency on an extension, and if someone wants to ignore both case
> and accents, they may resort to using 2 extensions (citext + unaccent)
> when none are needed. I guess I don't feel strongly about it either
> way.
Don't forget, if you have a defined insensitive / normalized
collations, you also enable on-the-fly collation based matching, a la
"SELECT 'Å' = 'A' COLLATE ignore_accent_case;" regardless of the
provided collations (which I think is much more common certain in
other databases)
Robert Treat
https://xzilla.net
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 16:21 Vik Fearing <[email protected]>
parent: Peter Eisentraut <[email protected]>
0 siblings, 1 reply; 37+ messages in thread
From: Vik Fearing @ 2025-06-19 16:21 UTC (permalink / raw)
To: Peter Eisentraut <[email protected]>; Jeff Davis <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; +Cc: pgsql-hackers
On 19/06/2025 16:47, Peter Eisentraut wrote:
> On 17.06.25 17:37, Vik Fearing wrote:
>> For <fold> (which includes LOWER() and UPPER()), the text says in
>> Section 6.35 GR 7.e:
>>
>>
>> If the character set of <character factor> is UTF8, UTF16, or UTF32,
>> then FR is replaced by
>> Case:
>> i) If the <search condition> S IS NORMALIZED evaluates to
>> True, then NORMALIZE (FR)
>> ii) Otherwise, FR.
>>
>>
>> Here, FR is the result of the function and S is its argument.
>>
>>
>> It does not appear to me that our LOWER and UPPER functions obey this
>> rule, so there is a valid argument that we should continue to ignore
>> it. Or, we can say that we have at least one of three compliant.
>
> The SQL standard also says in a few other places that normalization
> should be applied, and we do none of those, so this is probably not a
> reason to change CASEFOLD at this point.
>
Works for me.
--
Vik Fearing.
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 16:33 Jeff Davis <[email protected]>
parent: Thom Brown <[email protected]>
1 sibling, 2 replies; 37+ messages in thread
From: Jeff Davis @ 2025-06-19 16:33 UTC (permalink / raw)
To: Thom Brown <[email protected]>; Peter Eisentraut <[email protected]>; +Cc: Vik Fearing <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; pgsql-hackers
On Thu, 2025-06-19 at 16:36 +0100, Thom Brown wrote:
> Ease of use, perhaps. It seems easier to use:
>
> column_name cftext
>
> rather than:
>
> CREATE COLLATION case_insensitive_collation (
> PROVIDER = icu,
> LOCALE = 'und-u-ks-level2',
> DETERMINISTIC = FALSE
> );
We could auto-create such a collation at initdb time for ICU-enabled
builds.
> But I see the arguments against it. It creates an unnecessary
> dependency on an extension, and if someone wants to ignore both case
> and accents, they may resort to using 2 extensions (citext +
> unaccent)
> when none are needed.
There are at least three ways to do case insensitivity (or other kinds
of equivalence):
* Explicit function calls in queries, as well as index and constraint
definitions. E.g. expression index on LOWER(), queries that explicitly
do "LOWER(x) = ..."
* Wrap those function calls up in a separate data type, like citext.
* Non-deterministic collations.
Given that we have collations, which are a way of organizing alternate
behaviors for existing data types, I'm not sure I see the need for
creating an entirely separate data type.
> I guess I don't feel strongly about it either
> way.
Are you a user of citext? I'm genuinely interested in the use cases,
and whether the separate-data-type approach has merits that are missing
in the other approaches.
Regards,
Jeff Davis
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 16:51 Robert Treat <[email protected]>
parent: Jeff Davis <[email protected]>
1 sibling, 0 replies; 37+ messages in thread
From: Robert Treat @ 2025-06-19 16:51 UTC (permalink / raw)
To: Jeff Davis <[email protected]>; +Cc: Thom Brown <[email protected]>; Peter Eisentraut <[email protected]>; Vik Fearing <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; pgsql-hackers
On Thu, Jun 19, 2025 at 12:33 PM Jeff Davis <[email protected]> wrote:
>
> On Thu, 2025-06-19 at 16:36 +0100, Thom Brown wrote:
> > Ease of use, perhaps. It seems easier to use:
> >
> > column_name cftext
> >
> > rather than:
> >
> > CREATE COLLATION case_insensitive_collation (
> > PROVIDER = icu,
> > LOCALE = 'und-u-ks-level2',
> > DETERMINISTIC = FALSE
> > );
>
> We could auto-create such a collation at initdb time for ICU-enabled
> builds.
>
Providing a generic insensitive/non-deterministic collation by default
would solve a number of different use cases, so +1 on the idea from
me.
And TBH I usually build --without-icu but this would likely cause me
to change that.
> > But I see the arguments against it. It creates an unnecessary
> > dependency on an extension, and if someone wants to ignore both case
> > and accents, they may resort to using 2 extensions (citext +
> > unaccent)
> > when none are needed.
>
> There are at least three ways to do case insensitivity (or other kinds
> of equivalence):
>
> * Explicit function calls in queries, as well as index and constraint
> definitions. E.g. expression index on LOWER(), queries that explicitly
> do "LOWER(x) = ..."
>
> * Wrap those function calls up in a separate data type, like citext.
>
> * Non-deterministic collations.
>
> Given that we have collations, which are a way of organizing alternate
> behaviors for existing data types, I'm not sure I see the need for
> creating an entirely separate data type.
>
> > I guess I don't feel strongly about it either
> > way.
>
> Are you a user of citext? I'm genuinely interested in the use cases,
> and whether the separate-data-type approach has merits that are missing
> in the other approaches.
>
Yeah, I'd be interested to hear if there is some missing bit that
existing users have concerns over; as a former user of citext, it was
a great workaround at the time, but there are "better ways" to handle
those things now (imho).
Robert Treat
https://xzilla.net
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 16:52 Jeff Davis <[email protected]>
parent: Vik Fearing <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Jeff Davis @ 2025-06-19 16:52 UTC (permalink / raw)
To: Vik Fearing <[email protected]>; Peter Eisentraut <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; +Cc: pgsql-hackers
On Thu, 2025-06-19 at 18:21 +0200, Vik Fearing wrote:
> >
> > The SQL standard also says in a few other places that normalization
> > should be applied, and we do none of those, so this is probably not
> > a
> > reason to change CASEFOLD at this point.
> >
>
> Works for me.
Sounds good. We can document compatibility notes around this point.
If normalization becomes important, we can take the time to work out
the performance implications more carefully, and potentially introduce
an NCASEFOLD() if needed.
Regards,
Jeff Davis
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 16:59 Thom Brown <[email protected]>
parent: Jeff Davis <[email protected]>
1 sibling, 1 reply; 37+ messages in thread
From: Thom Brown @ 2025-06-19 16:59 UTC (permalink / raw)
To: Jeff Davis <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Vik Fearing <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; pgsql-hackers
On Thu, 19 Jun 2025, 17:33 Jeff Davis, <[email protected]> wrote:
> On Thu, 2025-06-19 at 16:36 +0100, Thom Brown wrote:
> > Ease of use, perhaps. It seems easier to use:
> >
> > column_name cftext
> >
> > rather than:
> >
> > CREATE COLLATION case_insensitive_collation (
> > PROVIDER = icu,
> > LOCALE = 'und-u-ks-level2',
> > DETERMINISTIC = FALSE
> > );
>
> We could auto-create such a collation at initdb time for ICU-enabled
> builds.
>
> > But I see the arguments against it. It creates an unnecessary
> > dependency on an extension, and if someone wants to ignore both case
> > and accents, they may resort to using 2 extensions (citext +
> > unaccent)
> > when none are needed.
>
> There are at least three ways to do case insensitivity (or other kinds
> of equivalence):
>
> * Explicit function calls in queries, as well as index and constraint
> definitions. E.g. expression index on LOWER(), queries that explicitly
> do "LOWER(x) = ..."
>
> * Wrap those function calls up in a separate data type, like citext.
>
> * Non-deterministic collations.
>
> Given that we have collations, which are a way of organizing alternate
> behaviors for existing data types, I'm not sure I see the need for
> creating an entirely separate data type.
>
> > I guess I don't feel strongly about it either
> > way.
>
> Are you a user of citext? I'm genuinely interested in the use cases,
> and whether the separate-data-type approach has merits that are missing
> in the other approaches.
>
No. But given the options, I would personally choose nondeterministic
collations now that they are available. I just wish they were more
user-friendly as I suspect the majority of people either won't know about
them, or won't know how to use them. But like you say, maybe having a set
of predefined nd-collections would help. As it stands, I'm just bringing up
the consideration of citext in case it has any value, which it doesn't
appear to. In fact it's probably even an argument to begin the process of
deprecation.
Thom
>
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 17:38 David E. Wheeler <[email protected]>
parent: Thom Brown <[email protected]>
0 siblings, 1 reply; 37+ messages in thread
From: David E. Wheeler @ 2025-06-19 17:38 UTC (permalink / raw)
To: Thom Brown <[email protected]>; +Cc: Jeff Davis <[email protected]>; Peter Eisentraut <[email protected]>; Vik Fearing <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; pgsql-hackers
On Jun 19, 2025, at 12:59, Thom Brown <[email protected]> wrote:
> No. But given the options, I would personally choose nondeterministic collations now that they are available. I just wish they were more user-friendly as I suspect the majority of people either won't know about them, or won't know how to use them.
I suspect there are a lot of uses of citext for databases created before nondeterministic collations existed and people are unaware of them or unclear on the migration path from one to the other, let alone implications for any infrastructure they built around cutest (like function signatures and return values). As long as citext conteinues to be maintained there and there’s no super clear path to migrate, I’d bet good money that few would bother to switch.
Best,
David
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 37+ messages in thread
* Re: Add CASEFOLD() function.
@ 2025-06-19 18:55 Thom Brown <[email protected]>
parent: David E. Wheeler <[email protected]>
0 siblings, 0 replies; 37+ messages in thread
From: Thom Brown @ 2025-06-19 18:55 UTC (permalink / raw)
To: David E. Wheeler <[email protected]>; +Cc: Jeff Davis <[email protected]>; Peter Eisentraut <[email protected]>; Vik Fearing <[email protected]>; Joe Conway <[email protected]>; Ian Lawrence Barwick <[email protected]>; pgsql-hackers
On Thu, 19 Jun 2025 at 18:39, David E. Wheeler <[email protected]> wrote:
>
> On Jun 19, 2025, at 12:59, Thom Brown <[email protected]> wrote:
>
> > No. But given the options, I would personally choose nondeterministic collations now that they are available. I just wish they were more user-friendly as I suspect the majority of people either won't know about them, or won't know how to use them.
>
> I suspect there are a lot of uses of citext for databases created before nondeterministic collations existed and people are unaware of them or unclear on the migration path from one to the other, let alone implications for any infrastructure they built around cutest (like function signatures and return values). As long as citext conteinues to be maintained there and there’s no super clear path to migrate, I’d bet good money that few would bother to switch.
Maybe the citext doc page should explain how to get unhooked from it.
Something like:
ALTER TABLE mytable
ALTER COLUMN ci_column
SET DATA TYPE TEXT COLLATE case_insensitive_collation;
or
CREATE DOMAIN ci_text AS text
COLLATE case_insensitive_collation;
ALTER TABLE mytable
ALTER COLUMN ci_column
SET DATA TYPE ci_text;
And because they're binary-compatible, they should also be free. No
doubt a procedure could do this to every instance in the database,
although I guess it gets trickier when it comes to functions that
accept citext as a parameter type, and other similar examples.
Thom
^ permalink raw reply [nested|flat] 37+ messages in thread
end of thread, other threads:[~2025-06-19 18:55 UTC | newest]
Thread overview: 37+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2018-11-07 07:53 [PATCH 3/5] Make archiver process an auxiliary process Kyotaro Horiguchi <[email protected]>
2020-02-06 09:14 [PATCH v5] ALTER tbl rewrite loses CLUSTER ON index Amit Langote <[email protected]>
2020-03-16 07:01 [PATCH v7 1/2] ALTER tbl rewrite loses CLUSTER ON index Amit Langote <[email protected]>
2023-07-06 08:12 Re: SQL:2011 application time Peter Eisentraut <[email protected]>
2023-07-06 08:24 ` Re: SQL:2011 application time Daniel Gustafsson <[email protected]>
2023-07-07 01:03 ` Re: SQL:2011 application time Paul A Jungwirth <[email protected]>
2023-07-12 08:24 ` Re: SQL:2011 application time Peter Eisentraut <[email protected]>
2023-08-31 21:26 ` Re: SQL:2011 application time Paul Jungwirth <[email protected]>
2023-09-01 04:02 ` Re: SQL:2011 application time Corey Huinker <[email protected]>
2023-09-01 09:30 ` Re: SQL:2011 application time Peter Eisentraut <[email protected]>
2023-07-15 09:04 ` Re: SQL:2011 application time jian he <[email protected]>
2024-12-21 06:19 [PATCH v25 8/9] Row pattern recognition patch (typedefs.list). Tatsuo Ishii <[email protected]>
2024-12-21 06:19 [PATCH v25 8/9] Row pattern recognition patch (typedefs.list). Tatsuo Ishii <[email protected]>
2024-12-21 06:19 [PATCH v25 8/9] Row pattern recognition patch (typedefs.list). Tatsuo Ishii <[email protected]>
2024-12-30 12:44 [PATCH v26 8/9] Row pattern recognition patch (typedefs.list). Tatsuo Ishii <[email protected]>
2024-12-30 12:44 [PATCH v26 8/9] Row pattern recognition patch (typedefs.list). Tatsuo Ishii <[email protected]>
2024-12-30 12:44 [PATCH v26 8/9] Row pattern recognition patch (typedefs.list). Tatsuo Ishii <[email protected]>
2024-12-30 23:53 [PATCH v27 8/9] Row pattern recognition patch (typedefs.list). Tatsuo Ishii <[email protected]>
2024-12-30 23:53 [PATCH v27 8/9] Row pattern recognition patch (typedefs.list). Tatsuo Ishii <[email protected]>
2024-12-30 23:53 [PATCH v27 8/9] Row pattern recognition patch (typedefs.list). Tatsuo Ishii <[email protected]>
2025-06-17 15:37 Re: Add CASEFOLD() function. Vik Fearing <[email protected]>
2025-06-17 18:14 ` Re: Add CASEFOLD() function. Jeff Davis <[email protected]>
2025-06-18 17:09 ` Re: Add CASEFOLD() function. Vik Fearing <[email protected]>
2025-06-19 02:53 ` Re: Add CASEFOLD() function. Jeff Davis <[email protected]>
2025-06-19 04:03 ` Re: Add CASEFOLD() function. Thom Brown <[email protected]>
2025-06-19 04:26 ` Re: Add CASEFOLD() function. Jeff Davis <[email protected]>
2025-06-19 14:51 ` Re: Add CASEFOLD() function. Peter Eisentraut <[email protected]>
2025-06-19 15:36 ` Re: Add CASEFOLD() function. Thom Brown <[email protected]>
2025-06-19 16:15 ` Re: Add CASEFOLD() function. Robert Treat <[email protected]>
2025-06-19 16:33 ` Re: Add CASEFOLD() function. Jeff Davis <[email protected]>
2025-06-19 16:51 ` Re: Add CASEFOLD() function. Robert Treat <[email protected]>
2025-06-19 16:59 ` Re: Add CASEFOLD() function. Thom Brown <[email protected]>
2025-06-19 17:38 ` Re: Add CASEFOLD() function. David E. Wheeler <[email protected]>
2025-06-19 18:55 ` Re: Add CASEFOLD() function. Thom Brown <[email protected]>
2025-06-19 14:47 ` Re: Add CASEFOLD() function. Peter Eisentraut <[email protected]>
2025-06-19 16:21 ` Re: Add CASEFOLD() function. Vik Fearing <[email protected]>
2025-06-19 16:52 ` Re: Add CASEFOLD() function. Jeff Davis <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox