public inbox for [email protected]help / color / mirror / Atom feed
[PATCH v54 3/7] Make archiver process an auxiliary process 6+ messages / 4 participants [nested] [flat]
* [PATCH v54 3/7] Make archiver process an auxiliary process @ 2021-03-11 09:01 Kyotaro Horiguchi <[email protected]> 0 siblings, 0 replies; 6+ messages in thread From: Kyotaro Horiguchi @ 2021-03-11 09:01 UTC (permalink / raw) This makes it possible to archiver process uses ipc functions and let us monitor archiver process status. --- doc/src/sgml/monitoring.sgml | 1 + src/backend/access/transam/xlogarchive.c | 4 +- src/backend/bootstrap/bootstrap.c | 22 ++- src/backend/postmaster/pgarch.c | 241 +++++++++-------------- src/backend/postmaster/postmaster.c | 81 ++++---- src/backend/storage/ipc/ipci.c | 2 + src/include/miscadmin.h | 2 + src/include/postmaster/pgarch.h | 14 +- src/include/storage/pmsignal.h | 1 - src/include/storage/proc.h | 8 +- src/tools/pgindent/typedefs.list | 1 + 11 files changed, 160 insertions(+), 217 deletions(-) diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 1ba813bbb9..a96455bdb2 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -935,6 +935,7 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser <literal>logical replication worker</literal>, <literal>parallel worker</literal>, <literal>background writer</literal>, <literal>client backend</literal>, <literal>checkpointer</literal>, + <literal>archiver</literal>, <literal>startup</literal>, <literal>walreceiver</literal>, <literal>walsender</literal> and <literal>walwriter</literal>. In addition, background workers registered by extensions may have diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c index 1c5a4f8b5a..26b023e754 100644 --- a/src/backend/access/transam/xlogarchive.c +++ b/src/backend/access/transam/xlogarchive.c @@ -25,11 +25,11 @@ #include "common/archive.h" #include "miscadmin.h" #include "postmaster/startup.h" +#include "postmaster/pgarch.h" #include "replication/walsender.h" #include "storage/fd.h" #include "storage/ipc.h" #include "storage/lwlock.h" -#include "storage/pmsignal.h" /* * Attempt to retrieve the specified file from off-line archival storage. @@ -491,7 +491,7 @@ XLogArchiveNotify(const char *xlog) /* Notify archiver that it's got something to do */ if (IsUnderPostmaster) - SendPostmasterSignal(PMSIGNAL_WAKEN_ARCHIVER); + PgArchWakeup(); } /* diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c index 6f615e6622..41da0c5059 100644 --- a/src/backend/bootstrap/bootstrap.c +++ b/src/backend/bootstrap/bootstrap.c @@ -317,6 +317,9 @@ AuxiliaryProcessMain(int argc, char *argv[]) case StartupProcess: MyBackendType = B_STARTUP; break; + case ArchiverProcess: + MyBackendType = B_ARCHIVER; + break; case BgWriterProcess: MyBackendType = B_BG_WRITER; break; @@ -437,30 +440,29 @@ AuxiliaryProcessMain(int argc, char *argv[]) proc_exit(1); /* should never return */ case StartupProcess: - /* don't set signals, startup process has its own agenda */ StartupProcessMain(); - proc_exit(1); /* should never return */ + proc_exit(1); + + case ArchiverProcess: + PgArchiverMain(); + proc_exit(1); case BgWriterProcess: - /* don't set signals, bgwriter has its own agenda */ BackgroundWriterMain(); - proc_exit(1); /* should never return */ + proc_exit(1); case CheckpointerProcess: - /* don't set signals, checkpointer has its own agenda */ CheckpointerMain(); - proc_exit(1); /* should never return */ + proc_exit(1); case WalWriterProcess: - /* don't set signals, walwriter has its own agenda */ InitXLOGAccess(); WalWriterMain(); - proc_exit(1); /* should never return */ + proc_exit(1); case WalReceiverProcess: - /* don't set signals, walreceiver has its own agenda */ WalReceiverMain(); - proc_exit(1); /* should never return */ + proc_exit(1); default: elog(PANIC, "unrecognized process type: %d", (int) MyAuxProcType); diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index edec311f12..8e572afea3 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -38,16 +38,14 @@ #include "libpq/pqsignal.h" #include "miscadmin.h" #include "pgstat.h" -#include "postmaster/fork_process.h" #include "postmaster/interrupt.h" #include "postmaster/pgarch.h" -#include "postmaster/postmaster.h" -#include "storage/dsm.h" #include "storage/fd.h" #include "storage/ipc.h" #include "storage/latch.h" -#include "storage/pg_shmem.h" #include "storage/pmsignal.h" +#include "storage/procsignal.h" +#include "storage/spin.h" #include "utils/guc.h" #include "utils/ps_status.h" @@ -73,6 +71,12 @@ */ #define NUM_ORPHAN_CLEANUP_RETRIES 3 +/* Shared memory area for archiver process */ +typedef struct PgArchData +{ + int pgprocno; /* pgprocno of archiver process */ +} PgArchData; + /* ---------- * Local data @@ -80,146 +84,79 @@ */ static time_t last_pgarch_start_time; static time_t last_sigterm_time = 0; +static PgArchData *PgArch = NULL; /* * Flags set by interrupt handlers for later service in the main loop. */ -static volatile sig_atomic_t wakened = false; static volatile sig_atomic_t ready_to_stop = false; /* ---------- * Local function forward declarations * ---------- */ -#ifdef EXEC_BACKEND -static pid_t pgarch_forkexec(void); -#endif - -NON_EXEC_STATIC void PgArchiverMain(int argc, char *argv[]) pg_attribute_noreturn(); -static void pgarch_waken(SIGNAL_ARGS); static void pgarch_waken_stop(SIGNAL_ARGS); static void pgarch_MainLoop(void); static void pgarch_ArchiverCopyLoop(void); static bool pgarch_archiveXlog(char *xlog); static bool pgarch_readyXlog(char *xlog); static void pgarch_archiveDone(char *xlog); +static void pgarch_die(int code, Datum arg); +/* Report shared memory space needed by PgArchShmemInit */ +Size +PgArchShmemSize(void) +{ + Size size = 0; -/* ------------------------------------------------------------ - * Public functions called from postmaster follow - * ------------------------------------------------------------ - */ + size = add_size(size, sizeof(PgArchData)); + + return size; +} + +/* Allocate and initialize archiver-related shared memory */ +void +PgArchShmemInit(void) +{ + bool found; + + PgArch = (PgArchData *) + ShmemInitStruct("Archiver Data", PgArchShmemSize(), &found); + + if (!found) + { + /* First time through, so initialize */ + MemSet(PgArch, 0, PgArchShmemSize()); + PgArch->pgprocno = INVALID_PGPROCNO; + } +} /* - * pgarch_start + * PgArchIsSuppressed * - * Called from postmaster at startup or after an existing archiver - * died. Attempt to fire up a fresh archiver process. + * Return true if archiver relaunch is suppressed. * - * Returns PID of child process, or 0 if fail. - * - * Note: if fail, we will be called again from the postmaster main loop. + * This is a safety valve to protect against continuous respawn attempts if the + * archiver is dying immediately at launch. Note that since we will retry to + * launch the archiver from the postmaster main loop, we will get another + * chance later. */ -int -pgarch_start(void) +bool +PgArchIsSuppressed(void) { - time_t curtime; - pid_t pgArchPid; + time_t curtime = time(NULL); - /* - * Do nothing if no archiver needed - */ - if (!XLogArchivingActive()) - return 0; + if ((curtime - last_pgarch_start_time) < PGARCH_RESTART_INTERVAL) + return true; - /* - * 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 - * ------------------------------------------------------------ - */ - - -#ifdef EXEC_BACKEND - -/* - * pgarch_forkexec() - - * - * Format up the arglist for, then fork and exec, archive process - */ -static pid_t -pgarch_forkexec(void) -{ - char *av[10]; - int ac = 0; - - av[ac++] = "postgres"; - - av[ac++] = "--forkarch"; - - av[ac++] = NULL; /* filled in by postmaster_forkexec */ - - av[ac] = NULL; - Assert(ac < lengthof(av)); - - return postmaster_forkexec(ac, av); + return false; } -#endif /* EXEC_BACKEND */ -/* - * PgArchiverMain - * - * 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[]) +/* Main entry point for archiver process */ +void +PgArchiverMain(void) { /* * Ignore all signals usually bound to some action in the postmaster, @@ -231,33 +168,51 @@ PgArchiverMain(int argc, char *argv[]) /* SIGQUIT handler was already set up by InitPostmasterChild */ pqsignal(SIGALRM, SIG_IGN); pqsignal(SIGPIPE, SIG_IGN); - pqsignal(SIGUSR1, pgarch_waken); + pqsignal(SIGUSR1, procsignal_sigusr1_handler); pqsignal(SIGUSR2, pgarch_waken_stop); + /* Reset some signals that are accepted by postmaster but not here */ pqsignal(SIGCHLD, SIG_DFL); + + /* Unblock signals (they were blocked when the postmaster forked us) */ PG_SETMASK(&UnBlockSig); - MyBackendType = B_ARCHIVER; - init_ps_display(NULL); + /* We shouldn't be launched unnecessarily. */ + Assert(XLogArchivingActive()); + + /* Arrange to clean up at archiver exit */ + on_shmem_exit(pgarch_die, 0); + + /* + * Advertise our latch that backends can use to wake us up while we're + * sleeping. + */ + PgArch->pgprocno = MyProc->pgprocno; pgarch_MainLoop(); - exit(0); + proc_exit(0); } -/* SIGUSR1 signal handler for archiver process */ -static void -pgarch_waken(SIGNAL_ARGS) +/* + * Wake up the archiver + */ +void +PgArchWakeup(void) { - int save_errno = errno; + int arch_pgprocno = PgArch->pgprocno; - /* set flag that there is work to be done */ - wakened = true; - SetLatch(MyLatch); - - errno = save_errno; + /* + * We don't acquire ProcArrayLock here. It's actually fine because + * procLatch isn't ever freed, so we just can potentially set the wrong + * process' (or no process') latch. Even in that case the archiver will be + * relaunched shortly and will start archiving. + */ + if (arch_pgprocno != INVALID_PGPROCNO) + SetLatch(&ProcGlobal->allProcs[arch_pgprocno].procLatch); } + /* SIGUSR2 signal handler for archiver process */ static void pgarch_waken_stop(SIGNAL_ARGS) @@ -282,14 +237,6 @@ pgarch_MainLoop(void) pg_time_t last_copy_time = 0; bool time_to_stop; - /* - * We run the copy loop immediately upon entry, in case there are - * unarchived files left over from a previous database run (or maybe the - * archiver died unexpectedly). After that we wait for a signal or - * timeout before doing more. - */ - wakened = true; - /* * There shouldn't be anything for the archiver to do except to wait for a * signal ... however, the archiver exists to protect our data, so she @@ -328,12 +275,8 @@ pgarch_MainLoop(void) } /* Do what we're here for */ - if (wakened || time_to_stop) - { - wakened = false; - pgarch_ArchiverCopyLoop(); - last_copy_time = time(NULL); - } + pgarch_ArchiverCopyLoop(); + last_copy_time = time(NULL); /* * Sleep until a signal is received, or until a poll is forced by @@ -354,13 +297,9 @@ pgarch_MainLoop(void) WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH, timeout * 1000L, WAIT_EVENT_ARCHIVER_MAIN); - if (rc & WL_TIMEOUT) - wakened = true; if (rc & WL_POSTMASTER_DEATH) time_to_stop = true; } - else - wakened = true; } /* @@ -744,3 +683,15 @@ pgarch_archiveDone(char *xlog) StatusFilePath(rlogdone, xlog, ".done"); (void) durable_rename(rlogready, rlogdone, WARNING); } + + +/* + * pgarch_die + * + * Exit-time cleanup handler + */ +static void +pgarch_die(int code, Datum arg) +{ + PgArch->pgprocno = INVALID_PGPROCNO; +} diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index edab95a19e..4f8e364284 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -443,9 +443,10 @@ static void InitPostmasterDeathWatchHandle(void); * even during recovery. */ #define PgArchStartupAllowed() \ - ((XLogArchivingActive() && pmState == PM_RUN) || \ - (XLogArchivingAlways() && \ - (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) + (((XLogArchivingActive() && pmState == PM_RUN) || \ + (XLogArchivingAlways() && \ + (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \ + !PgArchIsSuppressed()) #ifdef EXEC_BACKEND @@ -548,6 +549,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) @@ -1792,7 +1794,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) @@ -3007,7 +3009,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(); @@ -3142,20 +3144,22 @@ reaper(SIGNAL_ARGS) } /* - * Was it the archiver? If so, just try to start a new one; no need - * to force reset of the rest of the system. (If fail, we'll try - * again in future cycles of the main loop.). Unless we were waiting - * for it to shut down; don't restart it in that case, and + * Was it the archiver? If exit status is zero (normal) or one (FATAL + * exit), we assume everything is all right just like normal backends + * and just try to restart a new one so that we immediately retry + * archiving remaining files. (If fail, we'll try again in future + * cycles of the postmaster's main loop.) Unless we were waiting for it + * to shut down; don't restart it in that case, and * PostmasterStateMachine() will advance to the next shutdown step. */ if (pid == PgArchPID) { PgArchPID = 0; - if (!EXIT_STATUS_0(exitstatus)) - LogChildExit(LOG, _("archiver process"), - pid, exitstatus); + if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus)) + HandleChildCrash(pid, exitstatus, + _("archiver process")); if (PgArchStartupAllowed()) - PgArchPID = pgarch_start(); + PgArchPID = StartArchiver(); continue; } @@ -3403,7 +3407,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. @@ -3609,19 +3613,16 @@ HandleChildCrash(int pid, int exitstatus, const char *procname) signal_child(AutoVacPID, (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 - * simplifies the state-machine logic in the case where a shutdown request - * arrives during crash processing.) - */ - if (PgArchPID != 0 && take_action) + /* 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", - "SIGQUIT", + (SendStop ? "SIGSTOP" : "SIGQUIT"), (int) PgArchPID))); - signal_child(PgArchPID, SIGQUIT); + signal_child(PgArchPID, (SendStop ? SIGSTOP : SIGQUIT)); } /* @@ -3804,12 +3805,11 @@ PostmasterStateMachine(void) * (including autovac workers), no bgworkers (including unconnected * ones), and no walwriter, autovac launcher or bgwriter. If we are * doing crash recovery or an immediate shutdown then we expect the - * checkpointer to exit as well, otherwise not. The archiver, stats, - * and syslogger processes are disregarded since they are not - * connected to shared memory; we also disregard dead_end children - * here. Walsenders are also disregarded, they will be terminated - * later after writing the checkpoint record, like the archiver - * process. + * checkpointer to exit as well, otherwise not. The stats and syslogger + * processes are disregarded since they are not connected to shared + * memory; we also disregard dead_end children here. Walsenders and + * archiver are also disregarded, they will be terminated later after + * writing the checkpoint record. */ if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 && StartupPID == 0 && @@ -3912,6 +3912,7 @@ PostmasterStateMachine(void) Assert(CheckpointerPID == 0); Assert(WalWriterPID == 0); Assert(AutoVacPID == 0); + Assert(PgArchPID == 0); /* syslogger is not considered here */ pmState = PM_NO_CHILDREN; } @@ -5037,12 +5038,6 @@ SubPostmasterMain(int argc, char *argv[]) StartBackgroundWorker(); } - if (strcmp(argv[1], "--forkarch") == 0) - { - /* Do not want to attach to shared memory */ - - PgArchiverMain(argc, argv); /* does not return */ - } if (strcmp(argv[1], "--forkcol") == 0) { /* Do not want to attach to shared memory */ @@ -5140,7 +5135,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 @@ -5194,16 +5189,6 @@ sigusr1_handler(SIGNAL_ARGS) if (StartWorkerNeeded || HaveCrashedWorker) maybe_start_bgworkers(); - if (CheckPostmasterSignal(PMSIGNAL_WAKEN_ARCHIVER) && - PgArchPID != 0) - { - /* - * Send SIGUSR1 to archiver process, to wake it up and begin archiving - * next WAL file. - */ - signal_child(PgArchPID, SIGUSR1); - } - /* Tell syslogger to rotate logfile if requested */ if (SysLoggerPID != 0) { @@ -5445,6 +5430,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/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index f9bbe97b50..3e4ec53a97 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -144,6 +144,7 @@ CreateSharedMemoryAndSemaphores(void) size = add_size(size, ReplicationOriginShmemSize()); size = add_size(size, WalSndShmemSize()); size = add_size(size, WalRcvShmemSize()); + size = add_size(size, PgArchShmemSize()); size = add_size(size, ApplyLauncherShmemSize()); size = add_size(size, SnapMgrShmemSize()); size = add_size(size, BTreeShmemSize()); @@ -258,6 +259,7 @@ CreateSharedMemoryAndSemaphores(void) ReplicationOriginShmemInit(); WalSndShmemInit(); WalRcvShmemInit(); + PgArchShmemInit(); ApplyLauncherShmemInit(); /* diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 54693e047a..013850ac28 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -417,6 +417,7 @@ typedef enum BootstrapProcess, StartupProcess, BgWriterProcess, + ArchiverProcess, CheckpointerProcess, WalWriterProcess, WalReceiverProcess, @@ -429,6 +430,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/postmaster/pgarch.h b/src/include/postmaster/pgarch.h index d102a21ab7..640692d33e 100644 --- a/src/include/postmaster/pgarch.h +++ b/src/include/postmaster/pgarch.h @@ -26,14 +26,10 @@ #define MAX_XFN_CHARS 40 #define VALID_XFN_CHARS "0123456789ABCDEF.history.backup.partial" -/* ---------- - * Functions called from postmaster - * ---------- - */ -extern int pgarch_start(void); - -#ifdef EXEC_BACKEND -extern void PgArchiverMain(int argc, char *argv[]) pg_attribute_noreturn(); -#endif +extern Size PgArchShmemSize(void); +extern void PgArchShmemInit(void); +extern bool PgArchIsSuppressed(void); +extern void PgArchiverMain(void) pg_attribute_noreturn(); +extern void PgArchWakeup(void); #endif /* _PGARCH_H */ diff --git a/src/include/storage/pmsignal.h b/src/include/storage/pmsignal.h index dbbed18f61..8ed4d87ae6 100644 --- a/src/include/storage/pmsignal.h +++ b/src/include/storage/pmsignal.h @@ -34,7 +34,6 @@ typedef enum { PMSIGNAL_RECOVERY_STARTED, /* recovery has started */ PMSIGNAL_BEGIN_HOT_STANDBY, /* begin Hot Standby */ - PMSIGNAL_WAKEN_ARCHIVER, /* send a NOTIFY signal to xlog archiver */ PMSIGNAL_ROTATE_LOGFILE, /* send SIGUSR1 to syslogger to rotate logfile */ PMSIGNAL_START_AUTOVAC_LAUNCHER, /* start an autovacuum launcher */ PMSIGNAL_START_AUTOVAC_WORKER, /* start an autovacuum worker */ diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index a777cb64a1..2fd1ff09a7 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -370,11 +370,11 @@ extern PGPROC *PreparedXactProcs; * We set aside some extra PGPROC structures for auxiliary processes, * ie things that aren't full-fledged backends but need shmem access. * - * Background writer, checkpointer and WAL writer run during normal operation. - * Startup process and WAL receiver also consume 2 slots, but WAL writer is - * launched only after startup has exited, so we only need 4 slots. + * Background writer, checkpointer, WAL writer and archiver run during normal + * operation. Startup process and WAL receiver also consume 2 slots, but WAL + * writer is launched only after startup has exited, so we only need 5 slots. */ -#define NUM_AUXILIARY_PROCS 4 +#define NUM_AUXILIARY_PROCS 5 /* configurable options */ extern PGDLLIMPORT int DeadlockTimeout; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 99a34be465..78e852569e 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1572,6 +1572,7 @@ PGresAttValue PGresParamDesc PGresult PGresult_data +PgArchData PHANDLE PLAINTREE PLUID_AND_ATTRIBUTES -- 2.27.0 ----Next_Part(Fri_Mar_12_13_49_06_2021_682)---- ^ permalink raw reply [nested|flat] 6+ messages in thread
* Re: not null constraints, again @ 2024-09-20 21:15 Alvaro Herrera <[email protected]> 0 siblings, 2 replies; 6+ messages in thread From: Alvaro Herrera @ 2024-09-20 21:15 UTC (permalink / raw) To: Tender Wang <[email protected]>; +Cc: jian he <[email protected]>; Pg Hackers <[email protected]> On 2024-Sep-20, Alvaro Herrera wrote: > Yeah, there's a bunch of conflicts in current master. I rebased > yesterday but I'm still composing the email for v4. Coming soon. Okay, so here is v4 with these problems fixed, including correct propagation of constraint names to children tables, which I had inadvertently broken earlier. This one does pass the pg_upgrade tests and as far as I can see pg_dump does all the correct things also. I cleaned up the tests to remove everything that's unneeded, redundant, or testing behavior that no longer exists. I changed the behavior of ALTER TABLE ONLY <parent> ADD PRIMARY KEY, so that it throws error in case a child does not have a NOT NULL constraint on one of the columns, rather than silently creating such a constraint. (This is how `master` currently behaves). I think this is better behavior, because it lets the user decide whether they want to scan the table to create that constraint or not. It's a bit crude at present, because (1) a child could have a NO INHERIT constraint and have further children, which would foil the check (I think changing find_inheritance_children to find_all_inheritors would be sufficient to fix this, but that's only needed in legacy inheritance not partitioning); (2) the error message doesn't have an errcode, and the wording might need work. -- Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/ "Learn about compilers. Then everything looks like either a compiler or a database, and now you have two problems but one of them is fun." https://twitter.com/thingskatedid/status/1456027786158776329 Attachments: [text/x-diff] v4-0001-Catalog-not-null-constraints.patch (290.4K, ../../[email protected]/2-v4-0001-Catalog-not-null-constraints.patch) download | inline diff: From 6a0a09ce8e9a6a7b82a3c9aa1be3fd5df5707428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <[email protected]> Date: Fri, 20 Sep 2024 22:34:31 +0200 Subject: [PATCH v4] Catalog not-null constraints --- contrib/sepgsql/expected/alter.out | 3 - contrib/sepgsql/expected/ddl.out | 2 + contrib/test_decoding/expected/ddl.out | 8 + doc/src/sgml/catalogs.sgml | 12 +- doc/src/sgml/ddl.sgml | 65 +- doc/src/sgml/ref/alter_table.sgml | 16 +- doc/src/sgml/ref/create_table.sgml | 8 +- src/backend/catalog/heap.c | 343 ++++- src/backend/catalog/information_schema.sql | 71 +- src/backend/catalog/pg_constraint.c | 245 +++ src/backend/commands/tablecmds.c | 1346 +++++++++++------ src/backend/commands/typecmds.c | 4 + src/backend/nodes/makefuncs.c | 24 + src/backend/optimizer/util/plancat.c | 2 + src/backend/parser/gram.y | 23 +- src/backend/parser/parse_utilcmd.c | 251 +-- src/backend/utils/adt/ruleutils.c | 22 + src/backend/utils/cache/relcache.c | 42 +- src/bin/pg_dump/common.c | 31 +- src/bin/pg_dump/pg_dump.c | 270 +++- src/bin/pg_dump/pg_dump.h | 8 +- src/bin/pg_dump/t/002_pg_dump.pl | 6 +- src/bin/psql/describe.c | 50 + src/bin/psql/t/010_tab_completion.pl | 8 +- src/include/catalog/heap.h | 8 +- src/include/catalog/pg_constraint.h | 6 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 10 +- .../test_ddl_deparse/expected/alter_table.out | 17 +- .../expected/create_table.out | 2 - .../test_ddl_deparse/test_ddl_deparse.c | 3 - src/test/regress/expected/alter_table.out | 30 +- src/test/regress/expected/cluster.out | 7 +- src/test/regress/expected/constraints.out | 346 +++++ src/test/regress/expected/create_table.out | 35 +- .../regress/expected/create_table_like.out | 10 + src/test/regress/expected/foreign_data.out | 110 +- src/test/regress/expected/foreign_key.out | 16 +- .../regress/expected/generated_stored.out | 2 + src/test/regress/expected/identity.out | 6 +- src/test/regress/expected/index_including.out | 4 +- src/test/regress/expected/indexing.out | 56 +- src/test/regress/expected/inherit.out | 569 +++++++ .../regress/expected/replica_identity.out | 19 + src/test/regress/expected/rowsecurity.out | 2 + src/test/regress/sql/alter_table.sql | 13 +- src/test/regress/sql/constraints.sql | 122 ++ src/test/regress/sql/index_including.sql | 4 +- src/test/regress/sql/indexing.sql | 3 +- src/test/regress/sql/inherit.sql | 253 ++++ src/test/regress/sql/replica_identity.sql | 15 + 51 files changed, 3723 insertions(+), 806 deletions(-) diff --git a/contrib/sepgsql/expected/alter.out b/contrib/sepgsql/expected/alter.out index ae43537505..1462cfa3cb 100644 --- a/contrib/sepgsql/expected/alter.out +++ b/contrib/sepgsql/expected/alter.out @@ -164,7 +164,6 @@ LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_re LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_2.b" permissive=0 ALTER TABLE regtest_table ALTER b DROP NOT NULL; LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table.b" permissive=0 -LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_2.b" permissive=0 ALTER TABLE regtest_table ALTER b SET STATISTICS -1; LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table.b" permissive=0 LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_2.b" permissive=0 @@ -249,8 +248,6 @@ LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_re LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_ptable_1_tens.p" permissive=0 ALTER TABLE regtest_ptable ALTER p DROP NOT NULL; LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_ptable.p" permissive=0 -LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table_part.p" permissive=0 -LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_ptable_1_tens.p" permissive=0 ALTER TABLE regtest_ptable ALTER p SET STATISTICS -1; LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_ptable.p" permissive=0 LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table_part.p" permissive=0 diff --git a/contrib/sepgsql/expected/ddl.out b/contrib/sepgsql/expected/ddl.out index 93c677e546..7fbaab84d5 100644 --- a/contrib/sepgsql/expected/ddl.out +++ b/contrib/sepgsql/expected/ddl.out @@ -49,6 +49,7 @@ LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_reg LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0 LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=system_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="pg_catalog" permissive=0 LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0 +LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0 LOG: SELinux: allowed { add_name } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0 LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_table name="regtest_schema.regtest_table" permissive=0 LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0 @@ -284,6 +285,7 @@ LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:sepgsql_reg LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_4.y" permissive=0 LOG: SELinux: allowed { create } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_4.z" permissive=0 LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0 +LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0 LOG: SELinux: allowed { add_name } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0 LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_table name="regtest_schema.regtest_table_4" permissive=0 CREATE INDEX regtest_index_tbl4_y ON regtest_table_4(y); diff --git a/contrib/test_decoding/expected/ddl.out b/contrib/test_decoding/expected/ddl.out index 5713b8ab1c..d8d300fd49 100644 --- a/contrib/test_decoding/expected/ddl.out +++ b/contrib/test_decoding/expected/ddl.out @@ -492,6 +492,8 @@ WITH (user_catalog_table = true) options | text[] | | | | extended | | Indexes: "replication_metadata_pkey" PRIMARY KEY, btree (id) +Not-null constraints: + "replication_metadata_relation_not_null" NOT NULL "relation" Options: user_catalog_table=true INSERT INTO replication_metadata(relation, options) @@ -506,6 +508,8 @@ ALTER TABLE replication_metadata RESET (user_catalog_table); options | text[] | | | | extended | | Indexes: "replication_metadata_pkey" PRIMARY KEY, btree (id) +Not-null constraints: + "replication_metadata_relation_not_null" NOT NULL "relation" INSERT INTO replication_metadata(relation, options) VALUES ('bar', ARRAY['a', 'b']); @@ -519,6 +523,8 @@ ALTER TABLE replication_metadata SET (user_catalog_table = true); options | text[] | | | | extended | | Indexes: "replication_metadata_pkey" PRIMARY KEY, btree (id) +Not-null constraints: + "replication_metadata_relation_not_null" NOT NULL "relation" Options: user_catalog_table=true INSERT INTO replication_metadata(relation, options) @@ -538,6 +544,8 @@ ALTER TABLE replication_metadata SET (user_catalog_table = false); rewritemeornot | integer | | | | plain | | Indexes: "replication_metadata_pkey" PRIMARY KEY, btree (id) +Not-null constraints: + "replication_metadata_relation_not_null" NOT NULL "relation" Options: user_catalog_table=false INSERT INTO replication_metadata(relation, options) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index bfb97865e1..1ff5c639ad 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1271,7 +1271,7 @@ <structfield>attnotnull</structfield> <type>bool</type> </para> <para> - This represents a not-null constraint. + This column has a not-null constraint. </para></entry> </row> @@ -2502,14 +2502,10 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </indexterm> <para> - The catalog <structname>pg_constraint</structname> stores check, primary - key, unique, foreign key, and exclusion constraints on tables, as well as - not-null constraints on domains. + The catalog <structname>pg_constraint</structname> stores check, not-null, + primary key, unique, foreign key, and exclusion constraints on tables. (Column constraints are not treated specially. Every column constraint is equivalent to some table constraint.) - Not-null constraints on relations are represented in the - <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link> - catalog, not here. </para> <para> @@ -2571,7 +2567,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <para> <literal>c</literal> = check constraint, <literal>f</literal> = foreign key constraint, - <literal>n</literal> = not-null constraint (domains only), + <literal>n</literal> = not-null constraint, <literal>p</literal> = primary key constraint, <literal>u</literal> = unique constraint, <literal>t</literal> = constraint trigger, diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index b671858627..c8b17a7dbc 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -768,18 +768,39 @@ CREATE TABLE products ( name text <emphasis>NOT NULL</emphasis>, price numeric ); +</programlisting> + An explicit constraint name can also be specified, for example: +<programlisting> +CREATE TABLE products ( + product_no integer NOT NULL, + name text <emphasis>CONSTRAINT products_name_not_null</emphasis> NOT NULL, + price numeric +); </programlisting> </para> <para> - A not-null constraint is always written as a column constraint. A - not-null constraint is functionally equivalent to creating a check + A not-null constraint is usually written as a column constraint. The + syntax for writing it as a table constraint is +<programlisting> +CREATE TABLE products ( + product_no integer, + name text, + price numeric, + <emphasis>NOT NULL product_no</emphasis>, + <emphasis>NOT NULL name</emphasis> +); +</programlisting> + But this syntax is not standard and mainly intended for use by + <application>pg_dump</application>. + </para> + + <para> + A not-null constraint is functionally equivalent to creating a check constraint <literal>CHECK (<replaceable>column_name</replaceable> IS NOT NULL)</literal>, but in <productname>PostgreSQL</productname> creating an explicit - not-null constraint is more efficient. The drawback is that you - cannot give explicit names to not-null constraints created this - way. + not-null constraint is more efficient. </para> <para> @@ -796,6 +817,10 @@ CREATE TABLE products ( order the constraints are checked. </para> + <para> + However, a column can have at most one explicit not-null constraint. + </para> + <para> The <literal>NOT NULL</literal> constraint has an inverse: the <literal>NULL</literal> constraint. This does not mean that the @@ -989,7 +1014,7 @@ CREATE TABLE example ( <para> A table can have at most one primary key. (There can be any number - of unique and not-null constraints, which are functionally almost the + of unique constraints, which combined with not-null constraints are functionally almost the same thing, but only one can be identified as the primary key.) Relational database theory dictates that every table must have a primary key. This rule is @@ -1649,11 +1674,16 @@ ALTER TABLE products ADD CHECK (name <> ''); ALTER TABLE products ADD CONSTRAINT some_name UNIQUE (product_no); ALTER TABLE products ADD FOREIGN KEY (product_group_id) REFERENCES product_groups; </programlisting> - To add a not-null constraint, which cannot be written as a table - constraint, use this syntax: + </para> + + <para> + To add a not-null constraint, which is normally not written as a table + constraint, this special syntax is available: <programlisting> ALTER TABLE products ALTER COLUMN product_no SET NOT NULL; </programlisting> + This command silently does nothing if the column already has a + not-null constraint. </para> <para> @@ -1694,12 +1724,15 @@ ALTER TABLE products DROP CONSTRAINT some_name; </para> <para> - This works the same for all constraint types except not-null - constraints. To drop a not-null constraint use: + Simplified syntax is available to drop a not-null constraint: <programlisting> ALTER TABLE products ALTER COLUMN product_no DROP NOT NULL; </programlisting> - (Recall that not-null constraints do not have names.) + This mirrors the <literal>SET NOT NULL</literal> syntax for adding a + not-null constraint. This command will silently do nothing if the column + does not have a not-null constraint. (Recall that a column can have at + most one not-null constraint, so it is never ambiguous which constraint + this command acts on.) </para> </sect2> @@ -4441,12 +4474,10 @@ ALTER INDEX measurement_city_id_logdate_key <para> Both <literal>CHECK</literal> and <literal>NOT NULL</literal> constraints of a partitioned table are always inherited by all its - partitions. <literal>CHECK</literal> constraints that are marked - <literal>NO INHERIT</literal> are not allowed to be created on - partitioned tables. - You cannot drop a <literal>NOT NULL</literal> constraint on a - partition's column if the same constraint is present in the parent - table. + partitions; it is not allowed to create <literal>NO INHERIT</literal> + constraints of those types. + You cannot drop a constraint of those types if the same constraint + is present in the parent table. </para> </listitem> diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 1a49f321cf..9b5d719416 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -98,7 +98,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM <phrase>and <replaceable class="parameter">column_constraint</replaceable> is:</phrase> [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ] -{ NOT NULL | +{ NOT NULL [ NO INHERIT ] | NULL | CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] | DEFAULT <replaceable>default_expr</replaceable> | @@ -114,6 +114,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM [ 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> | 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> ) ] | @@ -1024,6 +1025,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM attached are marked <literal>NO INHERIT</literal>, the command will fail; such constraints must be recreated without the <literal>NO INHERIT</literal> clause. + By contrast, a <literal>NOT NULL</literal> constraint that was created + as <literal>NO INHERIT</literal> will be changed to a normal inheriting + one during attach. </para> <para> @@ -1789,11 +1793,17 @@ ALTER TABLE measurement <title>Compatibility</title> <para> - The forms <literal>ADD</literal> (without <literal>USING INDEX</literal>), + The forms <literal>ADD [COLUMN]</literal>, <literal>DROP [COLUMN]</literal>, <literal>DROP IDENTITY</literal>, <literal>RESTART</literal>, <literal>SET DEFAULT</literal>, <literal>SET DATA TYPE</literal> (without <literal>USING</literal>), <literal>SET GENERATED</literal>, and <literal>SET <replaceable>sequence_option</replaceable></literal> - conform with the SQL standard. The other forms are + conform with the SQL standard. + The form <literal>ADD <replaceable>table_constraint</replaceable></literal> + conforms with the SQL standard when the <literal>USING INDEX</literal> and + <literal>NOT VALID</literal> clauses are omitted and the constraint type is + one of <literal>CHECK</literal>, <literal>UNIQUE</literal>, <literal>PRIMARY KEY</literal>, + or <literal>REFERENCES</literal>. + The other forms are <productname>PostgreSQL</productname> extensions of the SQL standard. Also, the ability to specify more than one manipulation in a single <command>ALTER TABLE</command> command is an extension. diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index c1855b8d82..f9a098e77a 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -77,6 +77,7 @@ 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">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> | PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</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> ) ] | @@ -2394,13 +2395,6 @@ CREATE TABLE cities_partdef constraint, and index names must be unique across all relations within the same schema. </para> - - <para> - Currently, <productname>PostgreSQL</productname> does not record names - for not-null constraints at all, so they are not - subject to the uniqueness restriction. This might change in a future - release. - </para> </refsect2> <refsect2> diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 78e59384d1..1488520a5d 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -2171,6 +2171,56 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr, return constrOid; } +/* + * Store a not-null constraint for the given relation + * + * The OID of the new constraint is returned. + */ +static Oid +StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum, + bool is_validated, bool is_local, int inhcount, + bool is_no_inherit) +{ + Oid constrOid; + + Assert(attnum > InvalidAttrNumber); + + constrOid = + CreateConstraintEntry(nnname, + RelationGetNamespace(rel), + CONSTRAINT_NOTNULL, + false, + false, + is_validated, + InvalidOid, + RelationGetRelid(rel), + &attnum, + 1, + 1, + InvalidOid, /* not a domain constraint */ + InvalidOid, /* no associated index */ + InvalidOid, /* Foreign key fields */ + NULL, + NULL, + NULL, + NULL, + 0, + ' ', + ' ', + NULL, + 0, + ' ', + NULL, /* not an exclusion constraint */ + NULL, + NULL, + is_local, + inhcount, + is_no_inherit, + false, + false); + return constrOid; +} + /* * Store defaults and constraints (passed as a list of CookedConstraint). * @@ -2215,6 +2265,14 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal) is_internal); numchecks++; break; + + case CONSTR_NOTNULL: + con->conoid = + StoreRelNotNull(rel, con->name, con->attnum, + !con->skip_validation, con->is_local, + con->inhcount, con->is_no_inherit); + break; + default: elog(ERROR, "unrecognized constraint type: %d", (int) con->contype); @@ -2243,7 +2301,7 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal) * cooked CHECK constraints * * All entries in newColDefaults will be processed. Entries in newConstraints - * will be processed only if they are CONSTR_CHECK type. + * will be processed only if they are CONSTR_CHECK or CONSTR_NOTNULL types. * * Returns a list of CookedConstraint nodes that shows the cooked form of * the default and constraint expressions added to the relation. @@ -2272,6 +2330,7 @@ AddRelationNewConstraints(Relation rel, ParseNamespaceItem *nsitem; int numchecks; List *checknames; + List *nnnames; Node *expr; CookedConstraint *cooked; @@ -2356,6 +2415,7 @@ AddRelationNewConstraints(Relation rel, */ numchecks = numoldchecks; checknames = NIL; + nnnames = NIL; foreach_node(Constraint, cdef, newConstraints) { Oid constrOid; @@ -2410,7 +2470,7 @@ AddRelationNewConstraints(Relation rel, * Check against pre-existing constraints. If we are allowed * to merge with an existing constraint, there's no more to do * here. (We omit the duplicate constraint from the result, - * which is what ATAddCheckConstraint wants.) + * which is what ATAddCheckNNConstraint wants.) */ if (MergeWithExistingConstraint(rel, ccname, expr, allow_merge, is_local, @@ -2479,6 +2539,76 @@ AddRelationNewConstraints(Relation rel, cooked->is_no_inherit = cdef->is_no_inherit; cookedConstraints = lappend(cookedConstraints, cooked); } + else if (cdef->contype == CONSTR_NOTNULL) + { + CookedConstraint *nncooked; + AttrNumber colnum; + char *nnname; + + /* Determine which column to modify */ + colnum = get_attnum(RelationGetRelid(rel), strVal(linitial(cdef->keys))); + if (colnum == InvalidAttrNumber) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + strVal(linitial(cdef->keys)), RelationGetRelationName(rel))); + if (colnum < InvalidAttrNumber) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot add not-null constraint on system column \"%s\"", + strVal(linitial(cdef->keys)))); + + /* + * If the column already has a not-null constraint, we don't want + * to add another one; just adjust inheritance status as needed. + */ + if (AdjustNotNullInheritance(RelationGetRelid(rel), colnum, + cdef->inhcount, is_local, cdef->is_no_inherit)) + continue; + + /* + * If a constraint name is specified, check that it isn't already + * used. Otherwise, choose a non-conflicting one ourselves. + */ + if (cdef->conname) + { + if (ConstraintNameIsUsed(CONSTRAINT_RELATION, + RelationGetRelid(rel), + cdef->conname)) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("constraint \"%s\" for relation \"%s\" already exists", + cdef->conname, RelationGetRelationName(rel))); + nnname = cdef->conname; + } + else + nnname = ChooseConstraintName(RelationGetRelationName(rel), + strVal(linitial(cdef->keys)), + "not_null", + RelationGetNamespace(rel), + nnnames); + nnnames = lappend(nnnames, nnname); + + constrOid = + StoreRelNotNull(rel, nnname, colnum, + cdef->initially_valid, + cdef->inhcount == 0, + cdef->inhcount, + cdef->is_no_inherit); + + nncooked = (CookedConstraint *) palloc(sizeof(CookedConstraint)); + nncooked->contype = CONSTR_NOTNULL; + nncooked->conoid = constrOid; + nncooked->name = nnname; + nncooked->attnum = colnum; + nncooked->expr = NULL; + nncooked->skip_validation = cdef->skip_validation; + nncooked->is_local = is_local; + nncooked->inhcount = cdef->inhcount; + nncooked->is_no_inherit = cdef->is_no_inherit; + + cookedConstraints = lappend(cookedConstraints, nncooked); + } } /* @@ -2648,6 +2778,215 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr, return found; } +/* + * Create the not-null constraints when creating a new relation + * + * These come from two sources: the 'constraints' list (of Constraint) is + * specified directly by the user; the 'old_notnulls' list (of + * CookedConstraint) comes from inheritance. We create one constraint + * for each column, giving priority to user-specified ones, and setting + * inhcount according to how many parents cause each column to get a + * not-null constraint. If a user-specified name clashes with another + * user-specified name, an error is raised. + * + * Returns a list of AttrNumber for columns that need to have the attnotnull + * flag set. + */ +List * +AddRelationNotNullConstraints(Relation rel, List *constraints, + List *old_notnulls) +{ + List *givennames; + List *nnnames; + List *nncols = NIL; + + /* + * We track two lists of names: nnnames keeps all the constraint names, + * givennames tracks user-generated names. The distinction is important, + * because we must raise error for user-generated name conflicts, but for + * system-generated name conflicts we just generate another. + */ + nnnames = NIL; + givennames = NIL; + + /* + * First, create all not-null constraints that are directly specified by + * the user. Note that inheritance might have given us another source for + * each, so we must scan the old_notnulls list and increment inhcount for + * each element with identical attnum. We delete from there any element + * that we process. + */ + foreach_node(Constraint, constr, constraints) + { + AttrNumber attnum; + char *conname; + bool is_local = true; + int inhcount = 0; + + Assert(constr->contype == CONSTR_NOTNULL); + + attnum = get_attnum(RelationGetRelid(rel), + strVal(linitial(constr->keys))); + if (attnum == InvalidAttrNumber) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + strVal(linitial(constr->keys)), + RelationGetRelationName(rel))); + if (attnum < InvalidAttrNumber) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot add not-null constraint on system column \"%s\"", + strVal(linitial(constr->keys)))); + + /* + * A column can only have one not-null constraint, so discard any + * additional ones that appear for columns we already saw. + */ + if (list_member_int(nncols, attnum)) + continue; + + /* + * Search in the list of inherited constraints for any entries on the + * same column. + */ + foreach_ptr(CookedConstraint, old, old_notnulls) + { + if (old->attnum == attnum) + { + /* + * If we get a constraint from the parent, having a local NO + * INHERIT one doesn't work. + */ + if (constr->is_no_inherit) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot define not-null constraint on column \"%s\" with NO INHERIT", + strVal(linitial(constr->keys))), + errdetail("The column has an inherited not-null constraint."))); + + inhcount++; + old_notnulls = foreach_delete_current(old_notnulls, old); + } + } + + /* + * Determine a constraint name, which may have been specified by the + * user, or raise an error if a conflict exists with another + * user-specified name. + */ + if (constr->conname) + { + foreach_ptr(char, thisname, givennames) + { + if (strcmp(thisname, constr->conname) == 0) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("constraint \"%s\" for relation \"%s\" already exists", + constr->conname, + RelationGetRelationName(rel))); + } + + conname = constr->conname; + givennames = lappend(givennames, conname); + } + else + conname = ChooseConstraintName(RelationGetRelationName(rel), + get_attname(RelationGetRelid(rel), + attnum, false), + "not_null", + RelationGetNamespace(rel), + nnnames); + nnnames = lappend(nnnames, conname); + + StoreRelNotNull(rel, conname, + attnum, true, is_local, + inhcount, constr->is_no_inherit); + + nncols = lappend_int(nncols, attnum); + } + + /* + * If any column remains in the old_notnulls list, we must create a not- + * null constraint marked not-local. Because multiple parents could + * specify a not-null constraint for the same column, we must count how + * many there are and add to the original inhcount accordingly, deleting + * elements we've already processed. + * + * We don't use foreach() here because we have two nested loops over the + * constraint list, with possible element deletions in the inner one. If + * we used foreach_delete_current() it could only fix up the state of one + * of the loops, so it seems cleaner to use looping over list indexes for + * both loops. Note that any deletion will happen beyond where the outer + * loop is, so its index never needs adjustment. + */ + for (int outerpos = 0; outerpos < list_length(old_notnulls); outerpos++) + { + CookedConstraint *cooked; + char *conname = NULL; + int add_inhcount = 0; + + cooked = (CookedConstraint *) list_nth(old_notnulls, outerpos); + Assert(cooked->contype == CONSTR_NOTNULL); + Assert(cooked->name); + + /* + * Preserve the first non-conflicting constraint name we come across. + */ + if (conname == NULL) + conname = cooked->name; + + for (int restpos = outerpos + 1; restpos < list_length(old_notnulls);) + { + CookedConstraint *other; + + other = (CookedConstraint *) list_nth(old_notnulls, restpos); + Assert(other->name); + if (other->attnum == cooked->attnum) + { + if (conname == NULL) + conname = other->name; + + add_inhcount++; + old_notnulls = list_delete_nth_cell(old_notnulls, restpos); + } + else + restpos++; + } + + /* If we got a name, make sure it isn't one we've already used */ + if (conname != NULL) + { + foreach_ptr(char, thisname, nnnames) + { + if (strcmp(thisname, conname) == 0) + { + conname = NULL; + break; + } + } + } + + /* and choose a name, if needed */ + if (conname == NULL) + conname = ChooseConstraintName(RelationGetRelationName(rel), + get_attname(RelationGetRelid(rel), + cooked->attnum, false), + "not_null", + RelationGetNamespace(rel), + nnnames); + nnnames = lappend(nnnames, conname); + + StoreRelNotNull(rel, conname, cooked->attnum, true, + cooked->is_local, cooked->inhcount + add_inhcount, + cooked->is_no_inherit); + + nncols = lappend_int(nncols, cooked->attnum); + } + + return nncols; +} + /* * Update the count of constraints in the relation's pg_class tuple. * diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql index c4145131ce..76c78c0d18 100644 --- a/src/backend/catalog/information_schema.sql +++ b/src/backend/catalog/information_schema.sql @@ -440,9 +440,8 @@ CREATE VIEW check_constraints AS WHERE pg_has_role(coalesce(c.relowner, t.typowner), 'USAGE') AND con.contype = 'c' - UNION - -- not-null constraints on domains - + UNION ALL + -- not-null constraints SELECT current_database()::information_schema.sql_identifier AS constraint_catalog, rs.nspname::information_schema.sql_identifier AS constraint_schema, con.conname::information_schema.sql_identifier AS constraint_name, @@ -453,24 +452,7 @@ CREATE VIEW check_constraints AS LEFT JOIN pg_type t ON t.oid = con.contypid LEFT JOIN pg_attribute at ON (con.conrelid = at.attrelid AND con.conkey[1] = at.attnum) WHERE pg_has_role(coalesce(c.relowner, t.typowner), 'USAGE'::text) - AND con.contype = 'n' - - UNION - -- not-null constraints on relations - - SELECT CAST(current_database() AS sql_identifier) AS constraint_catalog, - CAST(n.nspname AS sql_identifier) AS constraint_schema, - CAST(CAST(n.oid AS text) || '_' || CAST(r.oid AS text) || '_' || CAST(a.attnum AS text) || '_not_null' AS sql_identifier) AS constraint_name, -- XXX - CAST(a.attname || ' IS NOT NULL' AS character_data) - AS check_clause - FROM pg_namespace n, pg_class r, pg_attribute a - WHERE n.oid = r.relnamespace - AND r.oid = a.attrelid - AND a.attnum > 0 - AND NOT a.attisdropped - AND a.attnotnull - AND r.relkind IN ('r', 'p') - AND pg_has_role(r.relowner, 'USAGE'); + AND con.contype = 'n'; GRANT SELECT ON check_constraints TO PUBLIC; @@ -839,6 +821,20 @@ CREATE VIEW constraint_column_usage AS UNION ALL + /* not-null constraints */ + SELECT DISTINCT nr.nspname, r.relname, r.relowner, a.attname, nc.nspname, c.conname + FROM pg_namespace nr, pg_class r, pg_attribute a, pg_namespace nc, pg_constraint c + WHERE nr.oid = r.relnamespace + AND r.oid = a.attrelid + AND r.oid = c.conrelid + AND a.attnum = c.conkey[1] + AND c.connamespace = nc.oid + AND c.contype = 'n' + AND r.relkind in ('r', 'p') + AND not a.attisdropped + + UNION ALL + /* unique/primary key/foreign key constraints */ SELECT nr.nspname, r.relname, r.relowner, a.attname, nc.nspname, c.conname FROM pg_namespace nr, pg_class r, pg_attribute a, pg_namespace nc, @@ -1839,6 +1835,7 @@ CREATE VIEW table_constraints AS CAST(r.relname AS sql_identifier) AS table_name, CAST( CASE c.contype WHEN 'c' THEN 'CHECK' + WHEN 'n' THEN 'CHECK' WHEN 'f' THEN 'FOREIGN KEY' WHEN 'p' THEN 'PRIMARY KEY' WHEN 'u' THEN 'UNIQUE' END @@ -1863,38 +1860,6 @@ CREATE VIEW table_constraints AS AND c.contype NOT IN ('t', 'x') -- ignore nonstandard constraints AND r.relkind IN ('r', 'p') AND (NOT pg_is_other_temp_schema(nr.oid)) - AND (pg_has_role(r.relowner, 'USAGE') - -- SELECT privilege omitted, per SQL standard - OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') - OR has_any_column_privilege(r.oid, 'INSERT, UPDATE, REFERENCES') ) - - UNION ALL - - -- not-null constraints - - SELECT CAST(current_database() AS sql_identifier) AS constraint_catalog, - CAST(nr.nspname AS sql_identifier) AS constraint_schema, - CAST(CAST(nr.oid AS text) || '_' || CAST(r.oid AS text) || '_' || CAST(a.attnum AS text) || '_not_null' AS sql_identifier) AS constraint_name, -- XXX - CAST(current_database() AS sql_identifier) AS table_catalog, - CAST(nr.nspname AS sql_identifier) AS table_schema, - CAST(r.relname AS sql_identifier) AS table_name, - CAST('CHECK' AS character_data) AS constraint_type, - CAST('NO' AS yes_or_no) AS is_deferrable, - CAST('NO' AS yes_or_no) AS initially_deferred, - CAST('YES' AS yes_or_no) AS enforced, - CAST(NULL AS yes_or_no) AS nulls_distinct - - FROM pg_namespace nr, - pg_class r, - pg_attribute a - - WHERE nr.oid = r.relnamespace - AND r.oid = a.attrelid - AND a.attnotnull - AND a.attnum > 0 - AND NOT a.attisdropped - AND r.relkind IN ('r', 'p') - AND (NOT pg_is_other_temp_schema(nr.oid)) AND (pg_has_role(r.relowner, 'USAGE') -- SELECT privilege omitted, per SQL standard OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index 1e2df031a8..5db647fac4 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -19,8 +19,10 @@ #include "access/htup_details.h" #include "access/sysattr.h" #include "access/table.h" +#include "access/xact.h" #include "catalog/catalog.h" #include "catalog/dependency.h" +#include "catalog/heap.h" #include "catalog/indexing.h" #include "catalog/objectaccess.h" #include "catalog/pg_constraint.h" @@ -563,6 +565,73 @@ ChooseConstraintName(const char *name1, const char *name2, return conname; } +/* + * Find and return a copy of the pg_constraint tuple that implements a + * validated not-null constraint for the given column of the given relation. + * If no such constraint exists, return NULL. + * + * XXX This would be easier if we had pg_attribute.notnullconstr with the OID + * of the constraint that implements the not-null constraint for that column. + * I'm not sure it's worth the catalog bloat and de-normalization, however. + */ +HeapTuple +findNotNullConstraintAttnum(Oid relid, AttrNumber attnum) +{ + Relation pg_constraint; + HeapTuple conTup, + retval = NULL; + SysScanDesc scan; + ScanKeyData key; + + pg_constraint = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + scan = systable_beginscan(pg_constraint, ConstraintRelidTypidNameIndexId, + true, NULL, 1, &key); + + while (HeapTupleIsValid(conTup = systable_getnext(scan))) + { + Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup); + AttrNumber conkey; + + /* + * We're looking for a NOTNULL constraint that's marked validated, + * with the column we're looking for as the sole element in conkey. + */ + if (con->contype != CONSTRAINT_NOTNULL) + continue; + if (!con->convalidated) + continue; + + conkey = extractNotNullColumn(conTup); + if (conkey != attnum) + continue; + + /* Found it */ + retval = heap_copytuple(conTup); + break; + } + + systable_endscan(scan); + table_close(pg_constraint, AccessShareLock); + + return retval; +} + +/* + * Find and return the pg_constraint tuple that implements a validated + * not-null constraint for the given column of the given relation. + */ +HeapTuple +findNotNullConstraint(Oid relid, const char *colname) +{ + AttrNumber attnum = get_attnum(relid, colname); + + return findNotNullConstraintAttnum(relid, attnum); +} + /* * Find and return the pg_constraint tuple that implements a validated * not-null constraint for the given domain. @@ -607,6 +676,182 @@ findDomainNotNullConstraint(Oid typid) return retval; } +/* + * Given a pg_constraint tuple for a not-null constraint, return the column + * number it is for. + */ +AttrNumber +extractNotNullColumn(HeapTuple constrTup) +{ + AttrNumber colnum; + Datum adatum; + ArrayType *arr; + + /* only tuples for not-null constraints should be given */ + Assert(((Form_pg_constraint) GETSTRUCT(constrTup))->contype == CONSTRAINT_NOTNULL); + + adatum = SysCacheGetAttrNotNull(CONSTROID, constrTup, + Anum_pg_constraint_conkey); + arr = DatumGetArrayTypeP(adatum); /* ensure not toasted */ + if (ARR_NDIM(arr) != 1 || + ARR_HASNULL(arr) || + ARR_ELEMTYPE(arr) != INT2OID || + ARR_DIMS(arr)[0] != 1) + elog(ERROR, "conkey is not a 1-D smallint array"); + + memcpy(&colnum, ARR_DATA_PTR(arr), sizeof(AttrNumber)); + Assert(colnum > 0 && colnum <= MaxAttrNumber); + + if ((Pointer) arr != DatumGetPointer(adatum)) + pfree(arr); /* free de-toasted copy, if any */ + + return colnum; +} + +/* + * AdjustNotNullInheritance + * Adjust inheritance count for a single not-null constraint + * + * If no not-null constraint is found for the column, return false. + * Caller can create one. + * + * If the constraint does exist and matches the requested inheritability + * status, adjust its inheritance count and islocal status as requested, and + * return true. If the inheritability status doesn't match, an error is + * raised. + */ +bool +AdjustNotNullInheritance(Oid relid, AttrNumber attnum, int count, + bool is_local, bool is_no_inherit) +{ + HeapTuple tup; + + Assert(count == 0 || count == 1); + + tup = findNotNullConstraintAttnum(relid, attnum); + if (HeapTupleIsValid(tup)) + { + Relation pg_constraint; + Form_pg_constraint conform; + bool changed = false; + + pg_constraint = table_open(ConstraintRelationId, RowExclusiveLock); + conform = (Form_pg_constraint) GETSTRUCT(tup); + + /* + * If the NO INHERIT flag we're asked for doesn't match what the + * existing constraint has, throw an error. + */ + if (is_no_inherit != conform->connoinherit) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"", + NameStr(conform->conname), get_rel_name(relid))); + + if (count > 0) + { + conform->coninhcount += count; + changed = true; + } + if (is_local) + { + conform->conislocal = true; + changed = true; + } + + if (changed) + CatalogTupleUpdate(pg_constraint, &tup->t_self, tup); + + table_close(pg_constraint, RowExclusiveLock); + + return true; + } + + return false; +} + +/* + * RelationGetNotNullConstraints + * Return the list of not-null constraints for the given rel + * + * Caller can request cooked constraints, or raw. + * + * This is seldom needed, so we just scan pg_constraint each time. + * + * XXX This is only used to create derived tables, so NO INHERIT constraints + * are always skipped. + */ +List * +RelationGetNotNullConstraints(Oid relid, bool cooked) +{ + List *notnulls = NIL; + Relation constrRel; + HeapTuple htup; + SysScanDesc conscan; + ScanKeyData skey; + + constrRel = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&skey, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + conscan = systable_beginscan(constrRel, ConstraintRelidTypidNameIndexId, true, + NULL, 1, &skey); + + while (HeapTupleIsValid(htup = systable_getnext(conscan))) + { + Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(htup); + AttrNumber colnum; + + if (conForm->contype != CONSTRAINT_NOTNULL) + continue; + if (conForm->connoinherit) + continue; + + colnum = extractNotNullColumn(htup); + + if (cooked) + { + CookedConstraint *cooked; + + cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint)); + + cooked->contype = CONSTR_NOTNULL; + cooked->name = pstrdup(NameStr(conForm->conname)); + cooked->attnum = colnum; + cooked->expr = NULL; + cooked->skip_validation = false; + cooked->is_local = true; + cooked->inhcount = 0; + cooked->is_no_inherit = conForm->connoinherit; + + notnulls = lappend(notnulls, cooked); + } + else + { + Constraint *constr; + + constr = makeNode(Constraint); + constr->contype = CONSTR_NOTNULL; + constr->conname = pstrdup(NameStr(conForm->conname)); + constr->deferrable = false; + constr->initdeferred = false; + constr->location = -1; + constr->keys = list_make1(makeString(get_attname(relid, colnum, + false))); + constr->skip_validation = false; + constr->initially_valid = true; + notnulls = lappend(notnulls, constr); + } + } + + systable_endscan(conscan); + table_close(constrRel, AccessShareLock); + + return notnulls; +} + + /* * Delete a single constraint record. */ diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2d703aa22e..9c299b5c20 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -362,7 +362,8 @@ static void truncate_check_activity(Relation rel); static void RangeVarCallbackForTruncate(const RangeVar *relation, Oid relId, Oid oldRelId, void *arg); static List *MergeAttributes(List *columns, const List *supers, char relpersistence, - bool is_partition, List **supconstr); + bool is_partition, List **supconstr, + List **supnotnulls); static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr); static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef); static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef); @@ -447,16 +448,14 @@ static bool check_for_column_name_collision(Relation rel, const char *colname, 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); -static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode); -static void ATPrepSetNotNull(List **wqueue, Relation rel, - AlterTableCmd *cmd, bool recurse, bool recursing, - LOCKMODE lockmode, - AlterTableUtilityContext *context); -static ObjectAddress ATExecSetNotNull(AlteredTableInfo *tab, Relation rel, - const char *colName, LOCKMODE lockmode); -static void ATExecCheckNotNull(AlteredTableInfo *tab, Relation rel, - const char *colName, LOCKMODE lockmode); +static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse, + LOCKMODE lockmode); +static void set_attnotnull(List **wqueue, Relation rel, + AttrNumber attnum, bool recurse, LOCKMODE lockmode); +static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel, + char *constrname, char *colName, + bool recurse, bool recursing, + LOCKMODE lockmode); static bool NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr); static bool ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *provenConstraint); @@ -488,6 +487,9 @@ static ObjectAddress ATExecDropColumn(List **wqueue, Relation rel, const char *c bool recurse, bool recursing, bool missing_ok, LOCKMODE lockmode, ObjectAddresses *addrs); +static void ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd, + bool recurse, LOCKMODE lockmode, + AlterTableUtilityContext *context); static ObjectAddress ATExecAddIndex(AlteredTableInfo *tab, Relation rel, IndexStmt *stmt, bool is_rebuild, LOCKMODE lockmode); static ObjectAddress ATExecAddStatistics(AlteredTableInfo *tab, Relation rel, @@ -499,11 +501,11 @@ static ObjectAddress ATExecAddConstraint(List **wqueue, static char *ChooseForeignKeyConstraintNameAddition(List *colnames); static ObjectAddress ATExecAddIndexConstraint(AlteredTableInfo *tab, Relation rel, IndexStmt *stmt, LOCKMODE lockmode); -static ObjectAddress ATAddCheckConstraint(List **wqueue, - AlteredTableInfo *tab, Relation rel, - Constraint *constr, - bool recurse, bool recursing, bool is_readd, - LOCKMODE lockmode); +static ObjectAddress ATAddCheckNNConstraint(List **wqueue, + AlteredTableInfo *tab, Relation rel, + Constraint *constr, + bool recurse, bool recursing, bool is_readd, + LOCKMODE lockmode); static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, Constraint *fkconstraint, bool recurse, bool recursing, @@ -560,9 +562,12 @@ static void GetForeignKeyCheckTriggers(Relation trigrel, Oid *insertTriggerOid, Oid *updateTriggerOid); static void ATExecDropConstraint(Relation rel, const char *constrName, - DropBehavior behavior, - bool recurse, bool recursing, + DropBehavior behavior, bool recurse, bool missing_ok, LOCKMODE lockmode); +static ObjectAddress dropconstraint_internal(Relation rel, + HeapTuple constraintTup, DropBehavior behavior, + bool recurse, bool recursing, + bool missing_ok, LOCKMODE lockmode); static void ATPrepAlterColumnType(List **wqueue, AlteredTableInfo *tab, Relation rel, bool recurse, bool recursing, @@ -660,6 +665,7 @@ static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl); static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, Relation partitionTbl); +static void verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partIdx); static List *GetParentedForeignKeyRefs(Relation partition); static void ATDetachCheckNoForeignKeyRefs(Relation partition); static char GetAttributeCompression(Oid atttypid, const char *compression); @@ -697,8 +703,10 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, TupleDesc descriptor; List *inheritOids; List *old_constraints; + List *old_notnulls; List *rawDefaults; List *cookedDefaults; + List *nncols; Datum reloptions; ListCell *listptr; AttrNumber attnum; @@ -883,12 +891,13 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, MergeAttributes(stmt->tableElts, inheritOids, stmt->relation->relpersistence, stmt->partbound != NULL, - &old_constraints); + &old_constraints, &old_notnulls); /* * Create a tuple descriptor from the relation schema. Note that this - * deals with column names, types, and not-null constraints, but not - * default values or CHECK constraints; we handle those below. + * deals with column names, types, and in-descriptor NOT NULL flags, but + * not default values, NOT NULL or CHECK constraints; we handle those + * below. */ descriptor = BuildDescForRelation(stmt->tableElts); @@ -1260,6 +1269,17 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, AddRelationNewConstraints(rel, NIL, stmt->constraints, true, true, false, queryString); + /* + * Finally, merge the not-null constraints that are declared directly with + * those that come from parent relations (making sure to count inheritance + * appropriately for each), create them, and set the attnotnull flag on + * columns that don't yet have it. + */ + nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints, + old_notnulls); + foreach_int(attrnum, nncols) + set_attnotnull(NULL, rel, attrnum, false, NoLock); + ObjectAddressSet(address, RelationRelationId, relationId); /* @@ -2391,6 +2411,8 @@ storage_name(char c) * Output arguments: * 'supconstr' receives a list of constraints belonging to the parents, * updated as necessary to be valid for the child. + * 'supnotnulls' receives a list of CookedConstraints that corresponds to + * constraints coming from inheritance parents. * * Return value: * Completed schema list. @@ -2421,7 +2443,10 @@ storage_name(char c) * * Constraints (including not-null constraints) for the child table * are the union of all relevant constraints, from both the child schema - * and parent tables. + * and parent tables. In addition, in legacy inheritance, each column that + * appears in a primary key in any of the parents also gets a NOT NULL + * constraint (partitioning doesn't need this, because the PK itself gets + * inherited.) * * The default value for a child column is defined as: * (1) If the child schema specifies a default, that value is used. @@ -2440,10 +2465,11 @@ storage_name(char c) */ static List * MergeAttributes(List *columns, const List *supers, char relpersistence, - bool is_partition, List **supconstr) + bool is_partition, List **supconstr, List **supnotnulls) { List *inh_columns = NIL; List *constraints = NIL; + List *nnconstraints = NIL; bool have_bogus_defaults = false; int child_attno; static Node bogus_marker = {0}; /* marks conflicting defaults */ @@ -2554,8 +2580,10 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, AttrMap *newattmap; List *inherited_defaults; List *cols_with_defaults; + List *nnconstrs; ListCell *lc1; ListCell *lc2; + Bitmapset *nncols = NULL; /* caller already got lock */ relation = table_open(parent, NoLock); @@ -2643,6 +2671,15 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, /* We can't process inherited defaults until newattmap is complete. */ inherited_defaults = cols_with_defaults = NIL; + /* + * Request attnotnull on columns that have a not-null constraint + * that's not marked NO INHERIT. + */ + nnconstrs = RelationGetNotNullConstraints(RelationGetRelid(relation), + true); + foreach_ptr(CookedConstraint, cc, nnconstrs) + nncols = bms_add_member(nncols, cc->attnum); + for (AttrNumber parent_attno = 1; parent_attno <= tupleDesc->natts; parent_attno++) { @@ -2664,7 +2701,6 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, */ newdef = makeColumnDef(attributeName, attribute->atttypid, attribute->atttypmod, attribute->attcollation); - newdef->is_not_null = attribute->attnotnull; newdef->storage = attribute->attstorage; newdef->generated = attribute->attgenerated; if (CompressionMethodIsValid(attribute->attcompression)) @@ -2712,6 +2748,12 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, mergeddef = newdef; } + /* + * mark attnotnull if parent has it + */ + if (bms_is_member(parent_attno, nncols)) + mergeddef->is_not_null = true; + /* * Locate default/generation expression if any */ @@ -2823,6 +2865,21 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, } } + /* + * Also copy the not-null constraints from this parent. The + * attnotnull markings were already installed above. + */ + foreach_ptr(CookedConstraint, nn, nnconstrs) + { + Assert(nn->contype == CONSTR_NOTNULL); + + nn->attnum = newattmap->attnums[nn->attnum - 1]; + nn->is_local = false; + nn->inhcount = 1; + + nnconstraints = lappend(nnconstraints, nn); + } + free_attrmap(newattmap); /* @@ -2893,8 +2950,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, /* * Now that we have the column definition list for a partition, we can * check whether the columns referenced in the column constraint specs - * actually exist. Also, we merge parent's not-null constraints and - * defaults into each corresponding column definition. + * actually exist. Also, merge column defaults. */ if (is_partition) { @@ -2911,7 +2967,6 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, if (strcmp(coldef->colname, restdef->colname) == 0) { found = true; - coldef->is_not_null |= restdef->is_not_null; /* * Check for conflicts related to generated columns. @@ -3000,6 +3055,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, } *supconstr = constraints; + *supnotnulls = nnconstraints; return columns; } @@ -3280,11 +3336,6 @@ MergeInheritedAttribute(List *inh_columns, format_type_with_typemod(prevtypeid, prevtypmod), format_type_with_typemod(newtypeid, newtypmod)))); - /* - * Merge of not-null constraints = OR 'em together - */ - prevdef->is_not_null |= newdef->is_not_null; - /* * Must have the same collation */ @@ -3920,7 +3971,10 @@ rename_constraint_internal(Oid myrelid, constraintOid); con = (Form_pg_constraint) GETSTRUCT(tuple); - if (myrelid && con->contype == CONSTRAINT_CHECK && !con->connoinherit) + if (myrelid && + (con->contype == CONSTRAINT_CHECK || + con->contype == CONSTRAINT_NOTNULL) && + !con->connoinherit) { if (recurse) { @@ -4675,15 +4729,6 @@ AlterTableGetLockLevel(List *cmds) cmd_lockmode = ShareUpdateExclusiveLock; break; - case AT_CheckNotNull: - - /* - * This only examines the table's schema; but lock must be - * strong enough to prevent concurrent DROP NOT NULL. - */ - cmd_lockmode = AccessShareLock; - break; - default: /* oops */ elog(ERROR, "unrecognized alter table type: %d", (int) cmd->subtype); @@ -4852,23 +4897,17 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, case AT_DropNotNull: /* ALTER COLUMN DROP NOT NULL */ ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - ATPrepDropNotNull(rel, recurse, recursing); - ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context); + /* Set up recursion for phase 2; no other prep needed */ + if (recurse) + cmd->recurse = true; pass = AT_PASS_DROP; break; case AT_SetNotNull: /* ALTER COLUMN SET NOT NULL */ ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - /* Need command-specific recursion decision */ - ATPrepSetNotNull(wqueue, rel, cmd, recurse, recursing, - lockmode, context); - pass = AT_PASS_COL_ATTRS; - break; - case AT_CheckNotNull: /* check column is already marked NOT NULL */ - ATSimplePermissions(cmd->subtype, rel, - ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context); - /* No command-specific prep needed */ + /* Set up recursion for phase 2; no other prep needed */ + if (recurse) + cmd->recurse = true; pass = AT_PASS_COL_ATTRS; break; case AT_SetExpression: /* ALTER COLUMN SET EXPRESSION */ @@ -4933,10 +4972,13 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, case AT_AddConstraint: /* ADD CONSTRAINT */ ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - /* Recursion occurs during execution phase */ - /* No command-specific prep needed except saving recurse flag */ + ATPrepAddPrimaryKey(wqueue, rel, cmd, recurse, lockmode, context); if (recurse) + { + /* recurses at exec time; lock descendants and set flag */ + (void) find_all_inheritors(RelationGetRelid(rel), lockmode, NULL); cmd->recurse = true; + } pass = AT_PASS_ADD_CONSTR; break; case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */ @@ -5254,13 +5296,11 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, address = ATExecDropIdentity(rel, cmd->name, cmd->missing_ok, lockmode, cmd->recurse, false); break; case AT_DropNotNull: /* ALTER COLUMN DROP NOT NULL */ - address = ATExecDropNotNull(rel, cmd->name, lockmode); + address = ATExecDropNotNull(rel, cmd->name, cmd->recurse, lockmode); break; case AT_SetNotNull: /* ALTER COLUMN SET NOT NULL */ - address = ATExecSetNotNull(tab, rel, cmd->name, lockmode); - break; - case AT_CheckNotNull: /* check column is already marked NOT NULL */ - ATExecCheckNotNull(tab, rel, cmd->name, lockmode); + address = ATExecSetNotNull(wqueue, rel, NULL, cmd->name, + cmd->recurse, false, lockmode); break; case AT_SetExpression: address = ATExecSetExpression(tab, rel, cmd->name, cmd->def, lockmode); @@ -5343,7 +5383,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, break; case AT_DropConstraint: /* DROP CONSTRAINT */ ATExecDropConstraint(rel, cmd->name, cmd->behavior, - cmd->recurse, false, + cmd->recurse, cmd->missing_ok, lockmode); break; case AT_AlterColumnType: /* ALTER COLUMN TYPE */ @@ -5606,21 +5646,10 @@ ATParseTransformCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, */ switch (cmd2->subtype) { - case AT_SetNotNull: - /* Need command-specific recursion decision */ - ATPrepSetNotNull(wqueue, rel, cmd2, - recurse, false, - lockmode, context); - pass = AT_PASS_COL_ATTRS; - break; case AT_AddIndex: - /* This command never recurses */ - /* No command-specific prep needed */ pass = AT_PASS_ADD_INDEX; break; case AT_AddIndexConstraint: - /* This command never recurses */ - /* No command-specific prep needed */ pass = AT_PASS_ADD_INDEXCONSTR; break; case AT_AddConstraint: @@ -5629,6 +5658,9 @@ ATParseTransformCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, cmd2->recurse = true; switch (castNode(Constraint, cmd2->def)->contype) { + case CONSTR_NOTNULL: + pass = AT_PASS_COL_ATTRS; + break; case CONSTR_PRIMARY: case CONSTR_UNIQUE: case CONSTR_EXCLUSION: @@ -6289,6 +6321,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode) RelationGetRelationName(oldrel)), errtableconstraint(oldrel, con->name))); break; + case CONSTR_NOTNULL: case CONSTR_FOREIGN: /* Nothing to do here */ break; @@ -6402,8 +6435,6 @@ alter_table_type_to_string(AlterTableType cmdtype) return "ALTER COLUMN ... SET EXPRESSION"; case AT_DropExpression: return "ALTER COLUMN ... DROP EXPRESSION"; - case AT_CheckNotNull: - return NULL; /* not real grammar */ case AT_SetStatistics: return "ALTER COLUMN ... SET STATISTICS"; case AT_SetOptions: @@ -7494,40 +7525,19 @@ add_column_collation_dependency(Oid relid, int32 attnum, Oid collid) /* * ALTER TABLE ALTER COLUMN DROP NOT NULL - */ - -static void -ATPrepDropNotNull(Relation rel, bool recurse, bool recursing) -{ - /* - * If the parent is a partitioned table, like check constraints, we do not - * support removing the NOT NULL while partitions exist. - */ - if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - { - PartitionDesc partdesc = RelationGetPartitionDesc(rel, true); - - Assert(partdesc != NULL); - if (partdesc->nparts > 0 && !recurse && !recursing) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("cannot remove constraint from only the partitioned table when partitions exist"), - errhint("Do not specify the ONLY keyword."))); - } -} - -/* + * * Return the address of the modified column. If the column was already * nullable, InvalidObjectAddress is returned. */ static ObjectAddress -ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) +ATExecDropNotNull(Relation rel, const char *colName, bool recurse, + LOCKMODE lockmode) { HeapTuple tuple; + HeapTuple conTup; Form_pg_attribute attTup; AttrNumber attnum; Relation attr_rel; - List *indexoidlist; ObjectAddress address; /* @@ -7543,6 +7553,15 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) colName, RelationGetRelationName(rel)))); attTup = (Form_pg_attribute) GETSTRUCT(tuple); attnum = attTup->attnum; + ObjectAddressSubSet(address, RelationRelationId, + RelationGetRelid(rel), attnum); + + /* If the column is already nullable there's nothing to do. */ + if (!attTup->attnotnull) + { + table_close(attr_rel, RowExclusiveLock); + return InvalidObjectAddress; + } /* Prevent them from altering a system attribute */ if (attnum <= 0) @@ -7558,60 +7577,37 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) colName, RelationGetRelationName(rel)))); /* - * Check that the attribute is not in a primary key or in an index used as - * a replica identity. - * - * Note: we'll throw error even if the pkey index is not valid. + * It's not OK to remove a constraint only for the parent and leave it in + * the children, so disallow that. */ - - /* Loop over all indexes on the relation */ - indexoidlist = RelationGetIndexList(rel); - - foreach_oid(indexoid, indexoidlist) + if (!recurse) { - HeapTuple indexTuple; - Form_pg_index indexStruct; - - indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid)); - if (!HeapTupleIsValid(indexTuple)) - elog(ERROR, "cache lookup failed for index %u", indexoid); - indexStruct = (Form_pg_index) GETSTRUCT(indexTuple); - - /* - * If the index is not a primary key or an index used as replica - * identity, skip the check. - */ - if (indexStruct->indisprimary || indexStruct->indisreplident) + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) { - /* - * Loop over each attribute in the primary key or the index used - * as replica identity and see if it matches the to-be-altered - * attribute. - */ - for (int i = 0; i < indexStruct->indnkeyatts; i++) - { - if (indexStruct->indkey.values[i] == attnum) - { - if (indexStruct->indisprimary) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("column \"%s\" is in a primary key", - colName))); - else - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("column \"%s\" is in index used as replica identity", - colName))); - } - } - } + PartitionDesc partdesc; - ReleaseSysCache(indexTuple); + partdesc = RelationGetPartitionDesc(rel, true); + + if (partdesc->nparts > 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot remove constraint from only the partitioned table when partitions exist"), + errhint("Do not specify the ONLY keyword.")); + } + else if (rel->rd_rel->relhassubclass && + find_inheritance_children(RelationGetRelid(rel), NoLock) != NIL) + { + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("not-null constraint on column \"%s\" must be removed in child tables too", + colName), + errhint("Do not specify the ONLY keyword.")); + } } - list_free(indexoidlist); - - /* If rel is partition, shouldn't drop NOT NULL if parent has the same */ + /* + * If rel is partition, shouldn't drop NOT NULL if parent has the same. + */ if (rel->rd_rel->relispartition) { Oid parentId = get_partition_parent(RelationGetRelid(rel), false); @@ -7629,19 +7625,18 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) } /* - * Okay, actually perform the catalog change ... if needed + * Find the constraint that makes this column NOT NULL, and drop it. + * dropconstraint_internal() resets attnotnull. */ - if (attTup->attnotnull) - { - attTup->attnotnull = false; + conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum); + if (conTup == NULL) + elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"", + colName, RelationGetRelationName(rel)); - CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple); - - ObjectAddressSubSet(address, RelationRelationId, - RelationGetRelid(rel), attnum); - } - else - address = InvalidObjectAddress; + /* The normal case: we have a pg_constraint row, remove it */ + dropconstraint_internal(rel, conTup, DROP_RESTRICT, recurse, false, + false, lockmode); + heap_freetuple(conTup); InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), attnum); @@ -7652,104 +7647,123 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) } /* - * ALTER TABLE ALTER COLUMN SET NOT NULL + * Helper to set pg_attribute.attnotnull if it isn't set, and to tell phase 3 + * to verify it; recurses to apply the same to children. + * + * When called to alter an existing table, 'wqueue' must be given so that we + * can queue a check that existing tuples pass the constraint. When called + * from table creation, 'wqueue' should be passed as NULL. */ - static void -ATPrepSetNotNull(List **wqueue, Relation rel, - AlterTableCmd *cmd, bool recurse, bool recursing, - LOCKMODE lockmode, AlterTableUtilityContext *context) +set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum, bool recurse, + LOCKMODE lockmode) { - /* - * If we're already recursing, there's nothing to do; the topmost - * invocation of ATSimpleRecursion already visited all children. - */ - if (recursing) - return; + HeapTuple tuple; + Form_pg_attribute attForm; + bool changed = false; - /* - * If the target column is already marked NOT NULL, we can skip recursing - * to children, because their columns should already be marked NOT NULL as - * well. But there's no point in checking here unless the relation has - * some children; else we can just wait till execution to check. (If it - * does have children, however, this can save taking per-child locks - * unnecessarily. This greatly improves concurrency in some parallel - * restore scenarios.) - * - * Unfortunately, we can only apply this optimization to partitioned - * tables, because traditional inheritance doesn't enforce that child - * columns be NOT NULL when their parent is. (That's a bug that should - * get fixed someday.) - */ - if (rel->rd_rel->relhassubclass && - rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + /* Guard against stack overflow due to overly deep inheritance tree. */ + check_stack_depth(); + + tuple = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, RelationGetRelid(rel)); + attForm = (Form_pg_attribute) GETSTRUCT(tuple); + if (!attForm->attnotnull) { - HeapTuple tuple; - bool attnotnull; + Relation attr_rel; - tuple = SearchSysCacheAttName(RelationGetRelid(rel), cmd->name); + attr_rel = table_open(AttributeRelationId, RowExclusiveLock); - /* Might as well throw the error now, if name is bad */ - if (!HeapTupleIsValid(tuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", - cmd->name, RelationGetRelationName(rel)))); + attForm->attnotnull = true; + CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple); - attnotnull = ((Form_pg_attribute) GETSTRUCT(tuple))->attnotnull; - ReleaseSysCache(tuple); - if (attnotnull) - return; + table_close(attr_rel, RowExclusiveLock); + + /* + * And set up for existing values to be checked, unless another + * constraint already proves this. + */ + if (wqueue && !NotNullImpliedByRelConstraints(rel, attForm)) + { + AlteredTableInfo *tab; + + tab = ATGetQueueEntry(wqueue, rel); + tab->verify_new_notnull = true; + } + + changed = true; } - /* - * If we have ALTER TABLE ONLY ... SET NOT NULL on a partitioned table, - * apply ALTER TABLE ... CHECK NOT NULL to every child. Otherwise, use - * normal recursion logic. - */ - if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && - !recurse) + if (recurse) { - AlterTableCmd *newcmd = makeNode(AlterTableCmd); + List *children; - newcmd->subtype = AT_CheckNotNull; - newcmd->name = pstrdup(cmd->name); - ATSimpleRecursion(wqueue, rel, newcmd, true, lockmode, context); + /* Make above update visible, for multiple inheritance cases */ + if (changed) + CommandCounterIncrement(); + + children = find_inheritance_children(RelationGetRelid(rel), lockmode); + foreach_oid(childrelid, children) + { + Relation childrel; + AttrNumber childattno; + + /* find_inheritance_children already got lock */ + childrel = table_open(childrelid, NoLock); + CheckAlterTableIsSafe(childrel); + + childattno = get_attnum(RelationGetRelid(childrel), + get_attname(RelationGetRelid(rel), attnum, + false)); + set_attnotnull(wqueue, childrel, childattno, + recurse, lockmode); + table_close(childrel, NoLock); + } } - else - ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context); } /* - * Return the address of the modified column. If the column was already NOT - * NULL, InvalidObjectAddress is returned. + * ALTER TABLE ALTER COLUMN SET NOT NULL + * + * Add a not-null constraint to a single table and its children. Returns + * the address of the constraint added to the parent relation, if one gets + * added, or InvalidObjectAddress otherwise. + * + * We must recurse to child tables during execution, rather than using + * ALTER TABLE's normal prep-time recursion. */ static ObjectAddress -ATExecSetNotNull(AlteredTableInfo *tab, Relation rel, - const char *colName, LOCKMODE lockmode) +ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName, + bool recurse, bool recursing, LOCKMODE lockmode) { HeapTuple tuple; - Form_pg_attribute attTup; AttrNumber attnum; - Relation attr_rel; ObjectAddress address; + Constraint *constraint; + CookedConstraint *ccon; + List *cooked; + bool is_no_inherit = false; - /* - * lookup the attribute - */ - attr_rel = table_open(AttributeRelationId, RowExclusiveLock); + /* Guard against stack overflow due to overly deep inheritance tree. */ + check_stack_depth(); - tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName); + /* At top level, permission check was done in ATPrepCmd, else do it */ + if (recursing) + { + ATSimplePermissions(AT_AddConstraint, rel, + ATT_PARTITIONED_TABLE | ATT_TABLE | ATT_FOREIGN_TABLE); + Assert(conName != NULL); + } - if (!HeapTupleIsValid(tuple)) + attnum = get_attnum(RelationGetRelid(rel), colName); + if (attnum == InvalidAttrNumber) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), errmsg("column \"%s\" of relation \"%s\" does not exist", colName, RelationGetRelationName(rel)))); - attTup = (Form_pg_attribute) GETSTRUCT(tuple); - attnum = attTup->attnum; - /* Prevent them from altering a system attribute */ if (attnum <= 0) ereport(ERROR, @@ -7757,81 +7771,132 @@ ATExecSetNotNull(AlteredTableInfo *tab, Relation rel, errmsg("cannot alter system column \"%s\"", colName))); - /* - * Okay, actually perform the catalog change ... if needed - */ - if (!attTup->attnotnull) + /* See if there's already a constraint */ + tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum); + if (HeapTupleIsValid(tuple)) { - attTup->attnotnull = true; - - CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple); + Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple); + bool changed = false; /* - * Ordinarily phase 3 must ensure that no NULLs exist in columns that - * are set NOT NULL; however, if we can find a constraint which proves - * this then we can skip that. We needn't bother looking if we've - * already found that we must verify some other not-null constraint. + * Don't let a NO INHERIT constraint be changed into inherit. */ - if (!tab->verify_new_notnull && !NotNullImpliedByRelConstraints(rel, attTup)) + if (conForm->connoinherit && recurse) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"", + NameStr(conForm->conname), + RelationGetRelationName(rel))); + + /* + * If we find an appropriate constraint, we're almost done, but just + * need to change some properties on it: if we're recursing, increment + * coninhcount; if not, set conislocal if not already set. + */ + if (recursing) { - /* Tell Phase 3 it needs to test the constraint */ - tab->verify_new_notnull = true; + conForm->coninhcount++; + changed = true; + } + else if (!conForm->conislocal) + { + conForm->conislocal = true; + changed = true; } - ObjectAddressSubSet(address, RelationRelationId, - RelationGetRelid(rel), attnum); + if (changed) + { + Relation constr_rel; + + constr_rel = table_open(ConstraintRelationId, RowExclusiveLock); + + CatalogTupleUpdate(constr_rel, &tuple->t_self, tuple); + ObjectAddressSet(address, ConstraintRelationId, conForm->oid); + table_close(constr_rel, RowExclusiveLock); + } + + if (changed) + return address; + else + return InvalidObjectAddress; } - else - address = InvalidObjectAddress; + + /* + * If we're asked not to recurse, and children exist, raise an error for + * partitioned tables. For inheritance, we act as if NO INHERIT had been + * specified. + */ + if (!recurse && + find_inheritance_children(RelationGetRelid(rel), + NoLock) != NIL) + { + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraint must be added to child tables too"), + errhint("Do not specify the ONLY keyword.")); + else + is_no_inherit = true; + } + + /* + * No constraint exists; we must add one. First determine a name to use, + * if we haven't already. + */ + if (!recursing) + { + Assert(conName == NULL); + conName = ChooseConstraintName(RelationGetRelationName(rel), + colName, "not_null", + RelationGetNamespace(rel), + NIL); + } + + constraint = makeNotNullConstraint(makeString(colName)); + constraint->is_no_inherit = is_no_inherit; + constraint->inhcount = recursing ? 1 : 0; + constraint->conname = conName; + + /* and do it */ + cooked = AddRelationNewConstraints(rel, NIL, list_make1(constraint), + false, !recursing, false, NULL); + ccon = linitial(cooked); + ObjectAddressSet(address, ConstraintRelationId, ccon->conoid); InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), attnum); - table_close(attr_rel, RowExclusiveLock); + /* + * Mark pg_attribute.attnotnull for the column. Tell that function not to + * recurse, because we're going to do it here. + */ + set_attnotnull(wqueue, rel, attnum, false, lockmode); + + /* + * Recurse to propagate the constraint to children that don't have one. + */ + if (recurse) + { + List *children; + + children = find_inheritance_children(RelationGetRelid(rel), + lockmode); + + foreach_oid(childoid, children) + { + Relation childrel = table_open(childoid, NoLock); + + CommandCounterIncrement(); + + ATExecSetNotNull(wqueue, childrel, conName, colName, + recurse, true, lockmode); + table_close(childrel, NoLock); + } + } return address; } -/* - * ALTER TABLE ALTER COLUMN CHECK NOT NULL - * - * This doesn't exist in the grammar, but we generate AT_CheckNotNull - * commands against the partitions of a partitioned table if the user - * writes ALTER TABLE ONLY ... SET NOT NULL on the partitioned table, - * or tries to create a primary key on it (which internally creates - * AT_SetNotNull on the partitioned table). Such a command doesn't - * allow us to actually modify any partition, but we want to let it - * go through if the partitions are already properly marked. - * - * In future, this might need to adjust the child table's state, likely - * by incrementing an inheritance count for the attnotnull constraint. - * For now we need only check for the presence of the flag. - */ -static void -ATExecCheckNotNull(AlteredTableInfo *tab, Relation rel, - const char *colName, LOCKMODE lockmode) -{ - HeapTuple tuple; - - tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName); - - if (!HeapTupleIsValid(tuple)) - ereport(ERROR, - errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", - colName, RelationGetRelationName(rel))); - - if (!((Form_pg_attribute) GETSTRUCT(tuple))->attnotnull) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("constraint must be added to child tables too"), - errdetail("Column \"%s\" of relation \"%s\" is not already NOT NULL.", - colName, RelationGetRelationName(rel)), - errhint("Do not specify the ONLY keyword."))); - - ReleaseSysCache(tuple); -} - /* * NotNullImpliedByRelConstraints * Does rel's existing constraints imply NOT NULL for the given attribute? @@ -9136,6 +9201,72 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName, return object; } +/* + * Prepare to add a primary key on table, by adding not-null constraints + * on all columns. + */ +static void +ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd, + bool recurse, LOCKMODE lockmode, + AlterTableUtilityContext *context) +{ + ListCell *lc; + Constraint *pkconstr; + + pkconstr = castNode(Constraint, cmd->def); + if (pkconstr->contype != CONSTR_PRIMARY) + return; + + /* + * If not recursing, we must ensure that all children have a NOT NULL + * constraint on the columns, and error out if not. + */ + if (!recurse) + { + List *children; + + children = find_inheritance_children(RelationGetRelid(rel), + lockmode); + foreach_oid(childrelid, children) + { + foreach(lc, pkconstr->keys) + { + HeapTuple tup; + Form_pg_attribute attrForm; + char *attname = strVal(lfirst(lc)); + + tup = SearchSysCacheAttName(childrelid, attname); + if (!tup) + elog(ERROR, "cache lookup failed for attribute %s of relation %u", + attname, childrelid); + attrForm = (Form_pg_attribute) GETSTRUCT(tup); + if (!attrForm->attnotnull) + ereport(ERROR, + errmsg("column \"%s\" of table \"%s\" is not marked NOT NULL", + attname, get_rel_name(childrelid))); + ReleaseSysCache(tup); + } + } + } + + /* Insert not-null constraints in the queue for the PK columns */ + foreach(lc, pkconstr->keys) + { + AlterTableCmd *newcmd; + Constraint *nnconstr; + + nnconstr = makeNotNullConstraint(lfirst(lc)); + nnconstr->inhcount = 0; + + newcmd = makeNode(AlterTableCmd); + newcmd->subtype = AT_AddConstraint; + newcmd->recurse = true; + newcmd->def = (Node *) nnconstr; + + ATPrepCmd(wqueue, rel, newcmd, true, false, lockmode, context); + } +} + /* * ALTER TABLE ADD INDEX * @@ -9331,17 +9462,18 @@ ATExecAddConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, Assert(IsA(newConstraint, Constraint)); /* - * Currently, we only expect to see CONSTR_CHECK and CONSTR_FOREIGN nodes - * arriving here (see the preprocessing done in parse_utilcmd.c). Use a - * switch anyway to make it easier to add more code later. + * Currently, we only expect to see CONSTR_CHECK, CONSTR_NOTNULL and + * CONSTR_FOREIGN nodes arriving here (see the preprocessing done in + * parse_utilcmd.c). */ switch (newConstraint->contype) { case CONSTR_CHECK: + case CONSTR_NOTNULL: address = - ATAddCheckConstraint(wqueue, tab, rel, - newConstraint, recurse, false, is_readd, - lockmode); + ATAddCheckNNConstraint(wqueue, tab, rel, + newConstraint, recurse, false, is_readd, + lockmode); break; case CONSTR_FOREIGN: @@ -9422,9 +9554,9 @@ ChooseForeignKeyConstraintNameAddition(List *colnames) } /* - * Add a check constraint to a single table and its children. Returns the - * address of the constraint added to the parent relation, if one gets added, - * or InvalidObjectAddress otherwise. + * Add a check or not-null constraint to a single table and its children. + * Returns the address of the constraint added to the parent relation, + * if one gets added, or InvalidObjectAddress otherwise. * * Subroutine for ATExecAddConstraint. * @@ -9437,9 +9569,9 @@ ChooseForeignKeyConstraintNameAddition(List *colnames) * the parent table and pass that down. */ static ObjectAddress -ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, - Constraint *constr, bool recurse, bool recursing, - bool is_readd, LOCKMODE lockmode) +ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, + Constraint *constr, bool recurse, bool recursing, + bool is_readd, LOCKMODE lockmode) { List *newcons; ListCell *lcon; @@ -9447,6 +9579,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, ListCell *child; ObjectAddress address = InvalidObjectAddress; + /* Guard against stack overflow due to overly deep inheritance tree. */ + check_stack_depth(); + /* At top level, permission check was done in ATPrepCmd, else do it */ if (recursing) ATSimplePermissions(AT_AddConstraint, rel, @@ -9478,7 +9613,7 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, { CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon); - if (!ccon->skip_validation) + if (!ccon->skip_validation && ccon->contype != CONSTR_NOTNULL) { NewConstraint *newcon; @@ -9494,11 +9629,19 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, if (constr->conname == NULL) constr->conname = ccon->name; + /* + * If adding a not-null constraint, set the pg_attribute flag and tell + * phase 3 to verify existing rows, if needed. + */ + if (constr->contype == CONSTR_NOTNULL) + set_attnotnull(wqueue, rel, ccon->attnum, + !ccon->is_no_inherit, lockmode); + ObjectAddressSet(address, ConstraintRelationId, ccon->conoid); } /* At this point we must have a locked-down name to use */ - Assert(constr->conname != NULL); + Assert(newcons == NIL || constr->conname != NULL); /* Advance command counter in case same table is visited multiple times */ CommandCounterIncrement(); @@ -9528,14 +9671,20 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, /* * Check if ONLY was specified with ALTER TABLE. If so, allow the - * constraint creation only if there are no children currently. Error out - * otherwise. + * constraint creation only if there are no children currently. + * Error out otherwise. */ if (!recurse && children != NIL) ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("constraint must be added to child tables too"))); + /* + * The constraint must appear as inherited in children, so create a + * modified constraint object to use. + */ + constr = copyObject(constr); + constr->inhcount = 1; foreach(child, children) { Oid childrelid = lfirst_oid(child); @@ -9549,9 +9698,13 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, /* Find or create work queue entry for this table */ childtab = ATGetQueueEntry(wqueue, childrel); - /* Recurse to child */ - ATAddCheckConstraint(wqueue, childtab, childrel, - constr, recurse, true, is_readd, lockmode); + /* + * Recurse to child. XXX if we didn't create a constraint on the + * parent because it already existed, and we do create one on a child, + * should we return that child's constraint ObjectAddress here? + */ + ATAddCheckNNConstraint(wqueue, childtab, childrel, + constr, recurse, true, is_readd, lockmode); table_close(childrel, NoLock); } @@ -12581,24 +12734,14 @@ createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid, */ static void ATExecDropConstraint(Relation rel, const char *constrName, - DropBehavior behavior, - bool recurse, bool recursing, + DropBehavior behavior, bool recurse, bool missing_ok, LOCKMODE lockmode) { - List *children; Relation conrel; - Form_pg_constraint con; SysScanDesc scan; ScanKeyData skey[3]; HeapTuple tuple; bool found = false; - bool is_no_inherit_constraint = false; - char contype; - - /* At top level, permission check was done in ATPrepCmd, else do it */ - if (recursing) - ATSimplePermissions(AT_DropConstraint, rel, - ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); conrel = table_open(ConstraintRelationId, RowExclusiveLock); @@ -12623,47 +12766,8 @@ ATExecDropConstraint(Relation rel, const char *constrName, /* There can be at most one matching row */ if (HeapTupleIsValid(tuple = systable_getnext(scan))) { - ObjectAddress conobj; - - con = (Form_pg_constraint) GETSTRUCT(tuple); - - /* Don't drop inherited constraints */ - if (con->coninhcount > 0 && !recursing) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("cannot drop inherited constraint \"%s\" of relation \"%s\"", - constrName, RelationGetRelationName(rel)))); - - is_no_inherit_constraint = con->connoinherit; - contype = con->contype; - - /* - * If it's a foreign-key constraint, we'd better lock the referenced - * table and check that that's not in use, just as we've already done - * for the constrained table (else we might, eg, be dropping a trigger - * that has unfired events). But we can/must skip that in the - * self-referential case. - */ - if (contype == CONSTRAINT_FOREIGN && - con->confrelid != RelationGetRelid(rel)) - { - Relation frel; - - /* Must match lock taken by RemoveTriggerById: */ - frel = table_open(con->confrelid, AccessExclusiveLock); - CheckAlterTableIsSafe(frel); - table_close(frel, NoLock); - } - - /* - * Perform the actual constraint deletion - */ - conobj.classId = ConstraintRelationId; - conobj.objectId = con->oid; - conobj.objectSubId = 0; - - performDeletion(&conobj, behavior, 0); - + dropconstraint_internal(rel, tuple, behavior, recurse, false, + missing_ok, lockmode); found = true; } @@ -12672,31 +12776,155 @@ ATExecDropConstraint(Relation rel, const char *constrName, if (!found) { if (!missing_ok) - { ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("constraint \"%s\" of relation \"%s\" does not exist", - constrName, RelationGetRelationName(rel)))); - } + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + constrName, RelationGetRelationName(rel))); else - { ereport(NOTICE, - (errmsg("constraint \"%s\" of relation \"%s\" does not exist, skipping", - constrName, RelationGetRelationName(rel)))); - table_close(conrel, RowExclusiveLock); - return; + errmsg("constraint \"%s\" of relation \"%s\" does not exist, skipping", + constrName, RelationGetRelationName(rel))); + } + + table_close(conrel, RowExclusiveLock); +} + +/* + * Remove a constraint, using its pg_constraint tuple + * + * Implementation for ALTER TABLE DROP CONSTRAINT and ALTER TABLE ALTER COLUMN + * DROP NOT NULL. + * + * Returns the address of the constraint being removed. + */ +static ObjectAddress +dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior behavior, + bool recurse, bool recursing, bool missing_ok, + LOCKMODE lockmode) +{ + Relation conrel; + Form_pg_constraint con; + ObjectAddress conobj; + List *children; + bool is_no_inherit_constraint = false; + char *constrName; + char *colname = NULL; + + /* Guard against stack overflow due to overly deep inheritance tree. */ + check_stack_depth(); + + /* At top level, permission check was done in ATPrepCmd, else do it */ + if (recursing) + ATSimplePermissions(AT_DropConstraint, rel, + ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); + + conrel = table_open(ConstraintRelationId, RowExclusiveLock); + + con = (Form_pg_constraint) GETSTRUCT(constraintTup); + constrName = NameStr(con->conname); + + /* Don't allow drop of inherited constraints */ + if (con->coninhcount > 0 && !recursing) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot drop inherited constraint \"%s\" of relation \"%s\"", + constrName, RelationGetRelationName(rel)))); + + /* + * Reset pg_constraint.attnotnull, if this is a not-null constraint. + * + * While doing that, we're in a good position to disallow dropping a not- + * null constraint underneath a primary key, a replica identity index, or a + * generated identity column. + */ + if (con->contype == CONSTRAINT_NOTNULL) + { + Relation attrel = table_open(AttributeRelationId, RowExclusiveLock); + AttrNumber attnum = extractNotNullColumn(constraintTup); + Bitmapset *pkattrs; + Bitmapset *irattrs; + HeapTuple atttup; + Form_pg_attribute attForm; + + /* save column name for recursion step */ + colname = get_attname(RelationGetRelid(rel), attnum, false); + + /* Disallow if it's in the primary key */ + pkattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_PRIMARY_KEY); + if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, pkattrs)) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is in a primary key", + get_attname(RelationGetRelid(rel), attnum, false))); + + /* Disallow if it's in the replica identity */ + irattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_IDENTITY_KEY); + if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, irattrs)) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is in index used as replica identity", + get_attname(RelationGetRelid(rel), attnum, false))); + + /* Disallow if it's a GENERATED AS IDENTITY column */ + atttup = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum); + if (!HeapTupleIsValid(atttup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, RelationGetRelid(rel)); + attForm = (Form_pg_attribute) GETSTRUCT(atttup); + if (attForm->attidentity != '\0') + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("column \"%s\" of relation \"%s\" is an identity column", + get_attname(RelationGetRelid(rel), attnum, + false), + RelationGetRelationName(rel))); + + /* All good -- reset attnotnull if needed */ + if (attForm->attnotnull) + { + attForm->attnotnull = false; + CatalogTupleUpdate(attrel, &atttup->t_self, atttup); } + + table_close(attrel, RowExclusiveLock); + } + + is_no_inherit_constraint = con->connoinherit; + + /* + * If it's a foreign-key constraint, we'd better lock the referenced table + * and check that that's not in use, just as we've already done for the + * constrained table (else we might, eg, be dropping a trigger that has + * unfired events). But we can/must skip that in the self-referential + * case. + */ + if (con->contype == CONSTRAINT_FOREIGN && + con->confrelid != RelationGetRelid(rel)) + { + Relation frel; + + /* Must match lock taken by RemoveTriggerById: */ + frel = table_open(con->confrelid, AccessExclusiveLock); + CheckAlterTableIsSafe(frel); + table_close(frel, NoLock); } /* - * For partitioned tables, non-CHECK inherited constraints are dropped via - * the dependency mechanism, so we're done here. + * Perform the actual constraint deletion */ - if (contype != CONSTRAINT_CHECK && + ObjectAddressSet(conobj, ConstraintRelationId, con->oid); + performDeletion(&conobj, behavior, 0); + + /* + * For partitioned tables, non-CHECK, non-NOT-NULL inherited constraints + * are dropped via the dependency mechanism, so we're done here. + */ + if (con->contype != CONSTRAINT_CHECK && + con->contype != CONSTRAINT_NOTNULL && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) { table_close(conrel, RowExclusiveLock); - return; + return conobj; } /* @@ -12724,48 +12952,65 @@ ATExecDropConstraint(Relation rel, const char *constrName, foreach_oid(childrelid, children) { Relation childrel; - HeapTuple copy_tuple; + HeapTuple tuple; + Form_pg_constraint childcon; /* find_inheritance_children already got lock */ childrel = table_open(childrelid, NoLock); CheckAlterTableIsSafe(childrel); - ScanKeyInit(&skey[0], - Anum_pg_constraint_conrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(childrelid)); - ScanKeyInit(&skey[1], - Anum_pg_constraint_contypid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(InvalidOid)); - ScanKeyInit(&skey[2], - Anum_pg_constraint_conname, - BTEqualStrategyNumber, F_NAMEEQ, - CStringGetDatum(constrName)); - scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, - true, NULL, 3, skey); + /* + * We search for not-null constraints by column name, and others by + * constraint name. + */ + if (con->contype == CONSTRAINT_NOTNULL) + { + tuple = findNotNullConstraint(childrelid, colname); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u", + colname, RelationGetRelid(childrel)); + } + else + { + SysScanDesc scan; + ScanKeyData skey[3]; - /* There can be at most one matching row */ - if (!HeapTupleIsValid(tuple = systable_getnext(scan))) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("constraint \"%s\" of relation \"%s\" does not exist", - constrName, - RelationGetRelationName(childrel)))); + ScanKeyInit(&skey[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(childrelid)); + ScanKeyInit(&skey[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + ScanKeyInit(&skey[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(constrName)); + scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, + true, NULL, 3, skey); + /* There can only be one, so no need to loop */ + tuple = systable_getnext(scan); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + constrName, + RelationGetRelationName(childrel)))); + tuple = heap_copytuple(tuple); + systable_endscan(scan); + } - copy_tuple = heap_copytuple(tuple); + childcon = (Form_pg_constraint) GETSTRUCT(tuple); - systable_endscan(scan); + /* Right now only CHECK and not-null constraints can be inherited */ + if (childcon->contype != CONSTRAINT_CHECK && + childcon->contype != CONSTRAINT_NOTNULL) + elog(ERROR, "inherited constraint is not a CHECK or not-null constraint"); - con = (Form_pg_constraint) GETSTRUCT(copy_tuple); - - /* Right now only CHECK constraints can be inherited */ - if (con->contype != CONSTRAINT_CHECK) - elog(ERROR, "inherited constraint is not a CHECK constraint"); - - if (con->coninhcount <= 0) /* shouldn't happen */ + if (childcon->coninhcount <= 0) /* shouldn't happen */ elog(ERROR, "relation %u has non-inherited constraint \"%s\"", - childrelid, constrName); + childrelid, NameStr(childcon->conname)); if (recurse) { @@ -12773,18 +13018,18 @@ ATExecDropConstraint(Relation rel, const char *constrName, * If the child constraint has other definition sources, just * decrement its inheritance count; if not, recurse to delete it. */ - if (con->coninhcount == 1 && !con->conislocal) + if (childcon->coninhcount == 1 && !childcon->conislocal) { /* Time to delete this child constraint, too */ - ATExecDropConstraint(childrel, constrName, behavior, - true, true, - false, lockmode); + dropconstraint_internal(childrel, tuple, behavior, + recurse, true, missing_ok, + lockmode); } else { /* Child constraint must survive my deletion */ - con->coninhcount--; - CatalogTupleUpdate(conrel, ©_tuple->t_self, copy_tuple); + childcon->coninhcount--; + CatalogTupleUpdate(conrel, &tuple->t_self, tuple); /* Make update visible */ CommandCounterIncrement(); @@ -12793,25 +13038,29 @@ ATExecDropConstraint(Relation rel, const char *constrName, else { /* - * If we were told to drop ONLY in this table (no recursion), we - * need to mark the inheritors' constraints as locally defined - * rather than inherited. + * If we were told to drop ONLY in this table (no recursion) and + * there are no further parents for this constraint, we need to + * mark the inheritors' constraints as locally defined rather than + * inherited. */ - con->coninhcount--; - con->conislocal = true; + childcon->coninhcount--; + if (childcon->coninhcount == 0) + childcon->conislocal = true; - CatalogTupleUpdate(conrel, ©_tuple->t_self, copy_tuple); + CatalogTupleUpdate(conrel, &tuple->t_self, tuple); /* Make update visible */ CommandCounterIncrement(); } - heap_freetuple(copy_tuple); + heap_freetuple(tuple); table_close(childrel, NoLock); } table_close(conrel, RowExclusiveLock); + + return conobj; } /* @@ -13760,10 +14009,26 @@ RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab) char *defstring = pg_get_constraintdef_command(conoid); Oid indoid; - tab->changedConstraintOids = lappend_oid(tab->changedConstraintOids, - conoid); - tab->changedConstraintDefs = lappend(tab->changedConstraintDefs, - defstring); + /* + * It is critical to create not-null constraints ahead of primary key + * indexes; otherwise, the not-null constraint would be created by the + * primary key, and the constraint name would be wrong. + */ + if (get_constraint_type(conoid) == CONSTRAINT_NOTNULL) + { + tab->changedConstraintOids = lcons_oid(conoid, + tab->changedConstraintOids); + tab->changedConstraintDefs = lcons(defstring, + tab->changedConstraintDefs); + } + else + { + + tab->changedConstraintOids = lappend_oid(tab->changedConstraintOids, + conoid); + tab->changedConstraintDefs = lappend(tab->changedConstraintDefs, + defstring); + } /* * For the index of a constraint, if any, remember if it is used for @@ -13926,9 +14191,10 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) /* * If the constraint is inherited (only), we don't want to inject a - * new definition here; it'll get recreated when ATAddCheckConstraint - * recurses from adding the parent table's constraint. But we had to - * carry the info this far so that we can drop the constraint below. + * new definition here; it'll get recreated when + * ATAddCheckNNConstraint recurses from adding the parent table's + * constraint. But we had to carry the info this far so that we can + * drop the constraint below. */ if (!conislocal) continue; @@ -14167,23 +14433,21 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd, tab->subcmds[AT_PASS_OLD_CONSTR] = lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); - /* recreate any comment on the constraint */ - RebuildConstraintComment(tab, - AT_PASS_OLD_CONSTR, - oldId, - rel, - NIL, - con->conname); - } - else if (cmd->subtype == AT_SetNotNull) - { /* - * The parser will create AT_SetNotNull subcommands for - * columns of PRIMARY KEY indexes/constraints, but we need - * not do anything with them here, because the columns' - * NOT NULL marks will already have been propagated into - * the new table definition. + * Recreate any comment on the constraint. If we have + * recreated a primary key, then transformTableConstraint + * has added an unnamed not-null constraint here; skip + * this in that case. */ + if (con->conname) + RebuildConstraintComment(tab, + AT_PASS_OLD_CONSTR, + oldId, + rel, + NIL, + con->conname); + else + Assert(con->contype == CONSTR_NOTNULL); } else elog(ERROR, "unexpected statement subtype: %d", @@ -15937,14 +16201,24 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart RelationGetRelationName(child_rel), parent_attname))); /* - * Check child doesn't discard NOT NULL property. (Other - * constraints are checked elsewhere.) + * If the parent has a not-null constraint that's not NO INHERIT, + * make sure the child has one too. + * + * Other constraints are checked elsewhere. */ if (parent_att->attnotnull && !child_att->attnotnull) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("column \"%s\" in child table must be marked NOT NULL", - parent_attname))); + { + HeapTuple contup; + + contup = findNotNullConstraintAttnum(RelationGetRelid(parent_rel), + parent_att->attnum); + if (HeapTupleIsValid(contup) && + !((Form_pg_constraint) GETSTRUCT(contup))->connoinherit) + ereport(ERROR, + errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL", + parent_attname, RelationGetRelationName(child_rel))); + } /* * Child column must be generated if and only if parent column is. @@ -16045,7 +16319,8 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) HeapTuple child_tuple; bool found = false; - if (parent_con->contype != CONSTRAINT_CHECK) + if (parent_con->contype != CONSTRAINT_CHECK && + parent_con->contype != CONSTRAINT_NOTNULL) continue; /* if the parent's constraint is marked NO INHERIT, it's not inherited */ @@ -16065,21 +16340,50 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) Form_pg_constraint child_con = (Form_pg_constraint) GETSTRUCT(child_tuple); HeapTuple child_copy; - if (child_con->contype != CONSTRAINT_CHECK) + if (child_con->contype != parent_con->contype) continue; - if (strcmp(NameStr(parent_con->conname), - NameStr(child_con->conname)) != 0) - continue; + /* + * CHECK constraint are matched by name, NOT NULL ones by + * attribute number + */ + if (child_con->contype == CONSTRAINT_CHECK) + { + if (strcmp(NameStr(parent_con->conname), + NameStr(child_con->conname)) != 0) + continue; + } + else if (child_con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(parent_tuple); + AttrNumber child_attno = extractNotNullColumn(child_tuple); - if (!constraints_equivalent(parent_tuple, child_tuple, RelationGetDescr(constraintrel))) + if (strcmp(get_attname(parent_relid, parent_attno, false), + get_attname(RelationGetRelid(child_rel), child_attno, + false)) != 0) + continue; + } + + if (child_con->contype == CONSTRAINT_CHECK && + !constraints_equivalent(parent_tuple, child_tuple, RelationGetDescr(constraintrel))) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("child table \"%s\" has different definition for check constraint \"%s\"", RelationGetRelationName(child_rel), NameStr(parent_con->conname)))); - /* If the child constraint is "no inherit" then cannot merge */ - if (child_con->connoinherit) + /* + * If the CHECK child constraint is "no inherit" then cannot + * merge. + * + * This is not desirable for not-null constraints, mostly because + * it breaks our pg_upgrade strategy, but it also makes sense on + * its own: if a child has its own not-null constraint and then + * acquires a parent with the same constraint, then we start to + * enforce that constraint for all the descendants of that child + * too, if any. + */ + if (child_con->contype == CONSTRAINT_CHECK && + child_con->connoinherit) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("constraint \"%s\" conflicts with non-inherited constraint on child table \"%s\"", @@ -16106,6 +16410,27 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) ereport(ERROR, errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("too many inheritance parents")); + if (child_con->contype == CONSTRAINT_NOTNULL && + child_con->connoinherit) + { + /* + * If the child has children, it's not possible to turn a NO + * INHERIT constraint into an inheritable one: we would need + * to recurse to create constraints in those children, but + * this is not a good place to do that. + */ + if (child_rel->rd_rel->relhassubclass) + ereport(ERROR, + errmsg("cannot add NOT NULL constraint to column \"%s\" of relation \"%s\" with inheritance children", + get_attname(RelationGetRelid(child_rel), + extractNotNullColumn(child_tuple), + false), + RelationGetRelationName(child_rel)), + errdetail("Existing constraint \"%s\" is marked NO INHERIT.", + NameStr(child_con->conname))); + + child_con->connoinherit = false; + } /* * In case of partitions, an inherited constraint must be @@ -16128,10 +16453,21 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) systable_endscan(child_scan); if (!found) + { + if (parent_con->contype == CONSTRAINT_NOTNULL) + ereport(ERROR, + errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL", + get_attname(parent_relid, + extractNotNullColumn(parent_tuple), + false), + RelationGetRelationName(child_rel))); + ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("child table is missing constraint \"%s\"", NameStr(parent_con->conname)))); + } } systable_endscan(parent_scan); @@ -16169,6 +16505,11 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) /* Off to RemoveInheritance() where most of the work happens */ RemoveInheritance(rel, parent_rel, false); + /* + * If the parent has a primary key, then we decrement counts for all NOT + * NULL constraints + */ + ObjectAddressSet(address, RelationRelationId, RelationGetRelid(parent_rel)); @@ -16277,6 +16618,7 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) HeapTuple attributeTuple, constraintTuple; List *connames; + List *nncolumns; bool found; bool is_partitioning; @@ -16345,6 +16687,8 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) * this, we first need a list of the names of the parent's check * constraints. (We cheat a bit by only checking for name matches, * assuming that the expressions will match.) + * + * For NOT NULL columns, we store column numbers to match. */ catalogRelation = table_open(ConstraintRelationId, RowExclusiveLock); ScanKeyInit(&key[0], @@ -16355,6 +16699,7 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) true, NULL, 1, key); connames = NIL; + nncolumns = NIL; while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) { @@ -16362,6 +16707,8 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) if (con->contype == CONSTRAINT_CHECK) connames = lappend(connames, pstrdup(NameStr(con->conname))); + if (con->contype == CONSTRAINT_NOTNULL) + nncolumns = lappend_int(nncolumns, extractNotNullColumn(constraintTuple)); } systable_endscan(scan); @@ -16377,20 +16724,39 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) { Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple); - bool match; + bool match = false; - if (con->contype != CONSTRAINT_CHECK) - continue; - - match = false; - foreach_ptr(char, chkname, connames) + /* + * Match CHECK constraints by name, not-null constraints by column + * number, and ignore all others. + */ + if (con->contype == CONSTRAINT_CHECK) { - if (strcmp(NameStr(con->conname), chkname) == 0) + foreach_ptr(char, chkname, connames) { - match = true; - break; + if (con->contype == CONSTRAINT_CHECK && + strcmp(NameStr(con->conname), chkname) == 0) + { + match = true; + break; + } } } + else if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber child_attno = extractNotNullColumn(constraintTuple); + + foreach_int(prevattno, nncolumns) + { + if (prevattno == child_attno) + { + match = true; + break; + } + } + } + else + continue; if (match) { @@ -18960,6 +19326,28 @@ AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel) stmt = generateClonedIndexStmt(NULL, idxRel, attmap, &conOid); + + /* + * If the index is a primary key, mark all columns as NOT NULL if + * they aren't already. + */ + if (stmt->primary) + { + MemoryContextSwitchTo(oldcxt); + for (int j = 0; j < info->ii_NumIndexKeyAttrs; j++) + { + AttrNumber childattno; + + childattno = get_attnum(RelationGetRelid(attachrel), + get_attname(RelationGetRelid(rel), + info->ii_IndexAttrNumbers[j], + false)); + set_attnotnull(wqueue, attachrel, childattno, + true, AccessExclusiveLock); + } + MemoryContextSwitchTo(cxt); + } + DefineIndex(RelationGetRelid(attachrel), stmt, InvalidOid, RelationGetRelid(idxRel), conOid, @@ -19591,7 +19979,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name) * DetachAddConstraintIfNeeded * Subroutine for ATExecDetachPartition. Create a constraint that * takes the place of the partition constraint, but avoid creating - * a dupe if an constraint already exists which implies the needed + * a dupe if a constraint already exists which implies the needed * constraint. */ static void @@ -19624,8 +20012,8 @@ DetachAddConstraintIfNeeded(List **wqueue, Relation partRel) n->initially_valid = true; n->skip_validation = true; /* It's a re-add, since it nominally already exists */ - ATAddCheckConstraint(wqueue, tab, partRel, n, - true, false, true, ShareUpdateExclusiveLock); + ATAddCheckNNConstraint(wqueue, tab, partRel, n, + true, false, true, ShareUpdateExclusiveLock); } } @@ -19894,6 +20282,13 @@ ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name) RelationGetRelationName(partIdx)))); } + /* + * If it's a primary key, make sure the columns in the partition are + * NOT NULL. + */ + if (parentIdx->rd_index->indisprimary) + verifyPartitionIndexNotNull(childInfo, partTbl); + /* All good -- do it */ IndexSetParentIndex(partIdx, RelationGetRelid(parentIdx)); if (OidIsValid(constraintOid)) @@ -20037,6 +20432,29 @@ validatePartitionedIndex(Relation partedIdx, Relation partedTbl) } } +/* + * When attaching an index as a partition of a partitioned index which is a + * primary key, verify that all the columns in the partition are marked NOT + * NULL. + */ +static void +verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition) +{ + for (int i = 0; i < iinfo->ii_NumIndexKeyAttrs; i++) + { + Form_pg_attribute att = TupleDescAttr(RelationGetDescr(partition), + iinfo->ii_IndexAttrNumbers[i] - 1); + + if (!att->attnotnull) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("invalid primary key definition"), + errdetail("Column \"%s\" of relation \"%s\" is not marked NOT NULL.", + NameStr(att->attname), + RelationGetRelationName(partition))); + } +} + /* * Return an OID list of constraints that reference the given relation * that are marked as having a parent constraints. diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 2a6550de90..859e2191f0 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -944,6 +944,10 @@ DefineDomain(CreateDomainStmt *stmt) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("conflicting NULL/NOT NULL constraints"))); + if (constr->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("not-null constraints for domains cannot be marked NO INHERIT")); typNotNull = true; nullDefined = true; break; diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 9cac3c1c27..341a17a6fe 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -436,6 +436,30 @@ makeRangeVar(char *schemaname, char *relname, int location) return r; } +/* + * makeNotNullConstraint - + * creates a Constraint node for NOT NULL constraints + */ +Constraint * +makeNotNullConstraint(String *colname) +{ + Constraint *notnull; + + notnull = makeNode(Constraint); + notnull->contype = CONSTR_NOTNULL; + notnull->conname = NULL; + notnull->is_no_inherit = false; + notnull->inhcount = 0; + notnull->deferrable = false; + notnull->initdeferred = false; + notnull->location = -1; + notnull->keys = list_make1(colname); + notnull->skip_validation = false; + notnull->initially_valid = true; + + return notnull; +} + /* * makeTypeName - * build a TypeName node for an unqualified name. diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index b913f91ff0..37b0ca2e43 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -1698,6 +1698,8 @@ relation_excluded_by_constraints(PlannerInfo *root, * Currently, attnotnull constraints must be treated as NO INHERIT unless * this is a partitioned table. In future we might track their * inheritance status more accurately, allowing this to be refined. + * + * XXX do we need/want to change this? */ include_notnull = (!rte->inh || rte->relkind == RELKIND_PARTITIONED_TABLE); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index ab304ca989..e90e177b8b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -3900,12 +3900,15 @@ ColConstraint: * or be part of a_expr NOT LIKE or similar constructs). */ ColConstraintElem: - NOT NULL_P + NOT NULL_P opt_no_inherit { Constraint *n = makeNode(Constraint); n->contype = CONSTR_NOTNULL; n->location = @1; + n->is_no_inherit = $3; + n->skip_validation = false; + n->initially_valid = true; $$ = (Node *) n; } | NULL_P @@ -4142,6 +4145,20 @@ ConstraintElem: n->initially_valid = !n->skip_validation; $$ = (Node *) n; } + | NOT NULL_P ColId ConstraintAttributeSpec + { + Constraint *n = makeNode(Constraint); + + n->contype = CONSTR_NOTNULL; + n->location = @1; + n->keys = list_make1(makeString($3)); + /* no NOT VALID support yet */ + processCASbits($4, @4, "NOT NULL", + NULL, NULL, NULL, + &n->is_no_inherit, yyscanner); + n->initially_valid = true; + $$ = (Node *) n; + } | UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace ConstraintAttributeSpec { @@ -4309,10 +4326,10 @@ DomainConstraintElem: n->contype = CONSTR_NOTNULL; n->location = @1; n->keys = list_make1(makeString("value")); - /* no NOT VALID support yet */ + /* no NOT VALID, NO INHERIT support */ processCASbits($3, @3, "NOT NULL", NULL, NULL, NULL, - &n->is_no_inherit, yyscanner); + NULL, yyscanner); n->initially_valid = true; $$ = (Node *) n; } diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 1e15ce10b4..64a3120367 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -81,6 +81,7 @@ typedef struct bool isalter; /* true if altering existing table */ List *columns; /* ColumnDef items */ List *ckconstraints; /* CHECK constraints */ + List *nnconstraints; /* NOT NULL constraints */ List *fkconstraints; /* FOREIGN KEY constraints */ List *ixconstraints; /* index-creating constraints */ List *likeclauses; /* LIKE clauses that need post-processing */ @@ -240,6 +241,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) cxt.isalter = false; cxt.columns = NIL; cxt.ckconstraints = NIL; + cxt.nnconstraints = NIL; cxt.fkconstraints = NIL; cxt.ixconstraints = NIL; cxt.likeclauses = NIL; @@ -303,6 +305,32 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) Assert(stmt->constraints == NIL); + /* + * Before processing index constraints, which could include a primary key, + * we must scan all not-null constraints to propagate the is_not_null flag + * to each corresponding ColumnDef. This is necessary because table-level + * not-null constraints have not been marked in each ColumnDef, and the PK + * processing code needs to know whether one constraint has already been + * declared in order not to declare a redundant one. + */ + foreach_node(Constraint, nn, cxt.nnconstraints) + { + char *colname = strVal(linitial(nn->keys)); + + foreach_node(ColumnDef, cd, cxt.columns) + { + /* not our column? */ + if (strcmp(cd->colname, colname) != 0) + continue; + /* Already marked not-null? Nothing to do */ + if (cd->is_not_null) + break; + /* Bingo, we're done for this constraint */ + cd->is_not_null = true; + break; + } + } + /* * Postprocess constraints that give rise to index definitions. */ @@ -340,6 +368,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) */ stmt->tableElts = cxt.columns; stmt->constraints = cxt.ckconstraints; + stmt->nnconstraints = cxt.nnconstraints; result = lappend(cxt.blist, stmt); result = list_concat(result, cxt.alist); @@ -566,6 +595,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) bool saw_default; bool saw_identity; bool saw_generated; + bool need_notnull = false; ListCell *clist; cxt->columns = lappend(cxt->columns, column); @@ -663,10 +693,8 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) constraint->cooked_expr = NULL; column->constraints = lappend(column->constraints, constraint); - constraint = makeNode(Constraint); - constraint->contype = CONSTR_NOTNULL; - constraint->location = -1; - column->constraints = lappend(column->constraints, constraint); + /* have a not-null constraint added later */ + need_notnull = true; } /* Process column constraints, if any... */ @@ -684,7 +712,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) switch (constraint->contype) { case CONSTR_NULL: - if (saw_nullable && column->is_not_null) + if ((saw_nullable && column->is_not_null) || need_notnull) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("conflicting NULL/NOT NULL declarations for column \"%s\" of table \"%s\"", @@ -696,6 +724,14 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) break; case CONSTR_NOTNULL: + if (cxt->ispartitioned && constraint->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("not-null constraints on partitioned tables cannot be NO INHERIT")); + + /* + * Disallow conflicting [NOT] NULL markings + */ if (saw_nullable && !column->is_not_null) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), @@ -703,8 +739,25 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) column->colname, cxt->relation->relname), parser_errposition(cxt->pstate, constraint->location))); - column->is_not_null = true; - saw_nullable = true; + /* Ignore redundant NOT NULL markings */ + + /* + * If this is the first time we see this column being marked + * not null, add the constraint entry; and get rid of any + * previous markings to mark the column NOT NULL. + */ + if (!column->is_not_null) + { + column->is_not_null = true; + saw_nullable = true; + + constraint->keys = list_make1(makeString(column->colname)); + cxt->nnconstraints = lappend(cxt->nnconstraints, constraint); + + /* Don't need this anymore, if we had it */ + need_notnull = false; + } + break; case CONSTR_DEFAULT: @@ -754,16 +807,19 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) column->identity = constraint->generated_when; saw_identity = true; - /* An identity column is implicitly NOT NULL */ - if (saw_nullable && !column->is_not_null) + /* + * Identity columns are always NOT NULL, but we may have a + * constraint already. + */ + if (!saw_nullable) + need_notnull = true; + else if (!column->is_not_null) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("conflicting NULL/NOT NULL declarations for column \"%s\" of table \"%s\"", column->colname, cxt->relation->relname), parser_errposition(cxt->pstate, constraint->location))); - column->is_not_null = true; - saw_nullable = true; break; } @@ -790,6 +846,16 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) break; case CONSTR_PRIMARY: + /* primary key columns need a NOT NULL constraint */ + if (!saw_nullable) + need_notnull = true; + else if (!column->is_not_null) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting NULL/NOT NULL declarations for column \"%s\" of table \"%s\"", + column->colname, cxt->relation->relname), + parser_errposition(cxt->pstate, + constraint->location))); if (cxt->isforeign) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -869,6 +935,18 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) constraint->location))); } + /* + * If we need a not-null constraint for PRIMARY KEY, SERIAL or IDENTITY, + * and one was not explicitly specified, add one now. + */ + if (need_notnull && !(saw_nullable && column->is_not_null)) + { + column->is_not_null = true; + cxt->nnconstraints = + lappend(cxt->nnconstraints, + makeNotNullConstraint(makeString(column->colname))); + } + /* * If needed, generate ALTER FOREIGN TABLE ALTER COLUMN statement to add * per-column foreign data wrapper options to this column after creation. @@ -938,6 +1016,15 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint) cxt->ckconstraints = lappend(cxt->ckconstraints, constraint); break; + case CONSTR_NOTNULL: + if (cxt->ispartitioned && constraint->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("not-null constraints on partitioned tables cannot be NO INHERIT")); + + cxt->nnconstraints = lappend(cxt->nnconstraints, constraint); + break; + case CONSTR_FOREIGN: if (cxt->isforeign) ereport(ERROR, @@ -949,7 +1036,6 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint) break; case CONSTR_NULL: - case CONSTR_NOTNULL: case CONSTR_DEFAULT: case CONSTR_ATTR_DEFERRABLE: case CONSTR_ATTR_NOT_DEFERRABLE: @@ -1053,14 +1139,10 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla continue; /* - * Create a new column, which is marked as NOT inherited. - * - * For constraints, ONLY the not-null constraint is inherited by the - * new column definition per SQL99. + * Create a new column definition */ def = makeColumnDef(NameStr(attribute->attname), attribute->atttypid, attribute->atttypmod, attribute->attcollation); - def->is_not_null = attribute->attnotnull; /* * Add to column list @@ -1129,14 +1211,27 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla } } + /* + * Reproduce not-null constraints, if any, by copying them. We do this + * regardless of options given. + */ + if (tupleDesc->constr && tupleDesc->constr->has_not_null) + { + List *lst; + + lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false); + cxt->nnconstraints = list_concat(cxt->nnconstraints, lst); + } + /* * We cannot yet deal with defaults, CHECK constraints, indexes, or * statistics, since we don't yet know what column numbers the copied * columns will have in the finished table. If any of those options are * specified, add the LIKE clause to cxt->likeclauses so that - * expandTableLikeClause will be called after we do know that. Also, - * remember the relation OID so that expandTableLikeClause is certain to - * open the same table. + * expandTableLikeClause will be called after we do know that. + * + * In order for this to work, we remember the relation OID so that + * expandTableLikeClause is certain to open the same table. */ if (table_like_clause->options & (CREATE_TABLE_LIKE_DEFAULTS | @@ -1506,8 +1601,8 @@ transformOfType(CreateStmtContext *cxt, TypeName *ofTypename) * with the index there. * * Unlike transformIndexConstraint, we don't make any effort to force primary - * key columns to be NOT NULL. The larger cloning process this is part of - * should have cloned their NOT NULL status separately (and DefineIndex will + * key columns to be not-null. The larger cloning process this is part of + * should have cloned their not-null status separately (and DefineIndex will * complain if that fails to happen). */ IndexStmt * @@ -2066,10 +2161,12 @@ transformIndexConstraints(CreateStmtContext *cxt) ListCell *lc; /* - * Run through the constraints that need to generate an index. For PRIMARY - * KEY, mark each column as NOT NULL and create an index. For UNIQUE or - * EXCLUDE, create an index as for PRIMARY KEY, but do not insist on NOT - * NULL. + * Run through the constraints that need to generate an index, and do so. + * + * For PRIMARY KEY, in addition we set each column's attnotnull flag true. + * We do not create a separate not-null constraint, as that would be + * redundant: the PRIMARY KEY constraint itself fulfills that role. Other + * constraint types don't need any not-null markings. */ foreach(lc, cxt->ixconstraints) { @@ -2143,9 +2240,7 @@ transformIndexConstraints(CreateStmtContext *cxt) } /* - * Now append all the IndexStmts to cxt->alist. If we generated an ALTER - * TABLE SET NOT NULL statement to support a primary key, it's already in - * cxt->alist. + * Now append all the IndexStmts to cxt->alist. */ cxt->alist = list_concat(cxt->alist, finalindexlist); } @@ -2153,18 +2248,15 @@ transformIndexConstraints(CreateStmtContext *cxt) /* * transformIndexConstraint * Transform one UNIQUE, PRIMARY KEY, or EXCLUDE constraint for - * transformIndexConstraints. + * transformIndexConstraints. An IndexStmt is returned. * - * We return an IndexStmt. For a PRIMARY KEY constraint, we additionally - * produce not-null constraints, either by marking ColumnDefs in cxt->columns - * as is_not_null or by adding an ALTER TABLE SET NOT NULL command to - * cxt->alist. + * For a PRIMARY KEY constraint, we additionally create not-null constraints + * for columns that don't already have them. */ static IndexStmt * transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) { IndexStmt *index; - List *notnullcmds = NIL; ListCell *lc; index = makeNode(IndexStmt); @@ -2384,6 +2476,11 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) errdetail("Cannot create a primary key or unique constraint using such an index."), parser_errposition(cxt->pstate, constraint->location))); + /* Ensure these columns get a NOT NULL constraint */ + cxt->nnconstraints = + lappend(cxt->nnconstraints, + makeNotNullConstraint(makeString(attname))); + constraint->keys = lappend(constraint->keys, makeString(attname)); } else @@ -2422,7 +2519,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) * For UNIQUE and PRIMARY KEY, we just have a list of column names. * * Make sure referenced keys exist. If we are making a PRIMARY KEY index, - * also make sure they are NOT NULL. For WITHOUT OVERLAPS constraints, we + * also make sure they are not-null. For WITHOUT OVERLAPS constraints, we * make sure the last part is a range or multirange. */ else @@ -2431,7 +2528,6 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) { char *key = strVal(lfirst(lc)); bool found = false; - bool forced_not_null = false; ColumnDef *column = NULL; ListCell *columns; IndexElem *iparam; @@ -2454,15 +2550,17 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) { /* * column is defined in the new table. For PRIMARY KEY, we - * can apply the not-null constraint cheaply here ... unless - * the column is marked is_from_type, in which case marking it - * here would be ineffective (see MergeAttributes). + * can apply the not-null constraint cheaply here. Note that + * this isn't effective in ALTER TABLE, unless the column is + * being added in the same command. */ if (constraint->contype == CONSTR_PRIMARY && - !column->is_from_type) + !column->is_not_null) { column->is_not_null = true; - forced_not_null = true; + cxt->nnconstraints = + lappend(cxt->nnconstraints, + makeNotNullConstraint(makeString(key))); } } else if (SystemAttributeByName(key) != NULL) @@ -2470,7 +2568,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) /* * column will be a system column in the new table, so accept * it. System columns can't ever be null, so no need to worry - * about PRIMARY/not-null constraint. + * about PRIMARY/NOT NULL constraint. */ found = true; } @@ -2507,13 +2605,9 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) found = true; typid = inhattr->atttypid; - /* - * It's tempting to set forced_not_null if the - * parent column is already NOT NULL, but that - * seems unsafe because the column's NOT NULL - * marking might disappear between now and - * execution. Do the runtime check to be safe. - */ + cxt->nnconstraints = + lappend(cxt->nnconstraints, + makeNotNullConstraint(makeString(pstrdup(inhname)))); break; } } @@ -2611,18 +2705,6 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; index->indexParams = lappend(index->indexParams, iparam); - /* - * For a primary-key column, also create an item for ALTER TABLE - * SET NOT NULL if we couldn't ensure it via is_not_null above. - */ - if (constraint->contype == CONSTR_PRIMARY && !forced_not_null) - { - AlterTableCmd *notnullcmd = makeNode(AlterTableCmd); - - notnullcmd->subtype = AT_SetNotNull; - notnullcmd->name = pstrdup(key); - notnullcmds = lappend(notnullcmds, notnullcmd); - } } if (constraint->without_overlaps) @@ -2741,22 +2823,6 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) index->indexIncludingParams = lappend(index->indexIncludingParams, iparam); } - /* - * If we found anything that requires run-time SET NOT NULL, build a full - * ALTER TABLE command for that and add it to cxt->alist. - */ - if (notnullcmds) - { - AlterTableStmt *alterstmt = makeNode(AlterTableStmt); - - alterstmt->relation = copyObject(cxt->relation); - alterstmt->cmds = notnullcmds; - alterstmt->objtype = OBJECT_TABLE; - alterstmt->missing_ok = false; - - cxt->alist = lappend(cxt->alist, alterstmt); - } - return index; } @@ -3395,6 +3461,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, cxt.isalter = true; cxt.columns = NIL; cxt.ckconstraints = NIL; + cxt.nnconstraints = NIL; cxt.fkconstraints = NIL; cxt.ixconstraints = NIL; cxt.likeclauses = NIL; @@ -3644,9 +3711,8 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, Node *istmt = (Node *) lfirst(l); /* - * We assume here that cxt.alist contains only IndexStmts and possibly - * ALTER TABLE SET NOT NULL statements generated from primary key - * constraints. We absorb the subcommands of the latter directly. + * We assume here that cxt.alist contains only IndexStmts generated + * from primary key constraints. */ if (IsA(istmt, IndexStmt)) { @@ -3658,30 +3724,31 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, newcmd->def = (Node *) idxstmt; newcmds = lappend(newcmds, newcmd); } - else if (IsA(istmt, AlterTableStmt)) - { - AlterTableStmt *alterstmt = (AlterTableStmt *) istmt; - - newcmds = list_concat(newcmds, alterstmt->cmds); - } else elog(ERROR, "unexpected stmt type %d", (int) nodeTag(istmt)); } cxt.alist = NIL; - /* Append any CHECK or FK constraints to the commands list */ - foreach(l, cxt.ckconstraints) + /* Append any CHECK, NOT NULL or FK constraints to the commands list */ + foreach_node(Constraint, def, cxt.ckconstraints) { newcmd = makeNode(AlterTableCmd); newcmd->subtype = AT_AddConstraint; - newcmd->def = (Node *) lfirst_node(Constraint, l); + newcmd->def = (Node *) def; newcmds = lappend(newcmds, newcmd); } - foreach(l, cxt.fkconstraints) + foreach_node(Constraint, def, cxt.nnconstraints) { newcmd = makeNode(AlterTableCmd); newcmd->subtype = AT_AddConstraint; - newcmd->def = (Node *) lfirst_node(Constraint, l); + newcmd->def = (Node *) def; + newcmds = lappend(newcmds, newcmd); + } + foreach_node(Constraint, def, cxt.fkconstraints) + { + newcmd = makeNode(AlterTableCmd); + newcmd->subtype = AT_AddConstraint; + newcmd->def = (Node *) def; newcmds = lappend(newcmds, newcmd); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 2177d17e27..a39068d1bf 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2516,6 +2516,28 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, conForm->connoinherit ? " NO INHERIT" : ""); break; } + case CONSTRAINT_NOTNULL: + { + if (conForm->conrelid) + { + AttrNumber attnum; + + attnum = extractNotNullColumn(tup); + + appendStringInfo(&buf, "NOT NULL %s", + quote_identifier(get_attname(conForm->conrelid, + attnum, false))); + if (((Form_pg_constraint) GETSTRUCT(tup))->connoinherit) + appendStringInfoString(&buf, " NO INHERIT"); + } + else if (conForm->contypid) + { + /* conkey is null for domain not-null constraints */ + appendStringInfoString(&buf, "NOT NULL"); + } + break; + } + case CONSTRAINT_TRIGGER: /* diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 5b6b7b809c..37dddaed2d 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -4851,18 +4851,46 @@ RelationGetIndexList(Relation relation) result = lappend_oid(result, index->indexrelid); /* - * Invalid, non-unique, non-immediate or predicate indexes aren't - * interesting for either oid indexes or replication identity indexes, - * so don't check them. + * Non-unique or predicate indexes aren't interesting for either oid + * indexes or replication identity indexes, so don't check them. + * Deferred ones are not useful for replication identity either; but + * we do include them if they are PKs. */ - if (!index->indisvalid || !index->indisunique || - !index->indimmediate || + if (!index->indisunique || !heap_attisnull(htup, Anum_pg_index_indpred, NULL)) continue; - /* remember primary key index if any */ - if (index->indisprimary) + /* + * Remember primary key index, if any. We do this only if the index + * is valid; but if the table is partitioned, then we do it even if + * it's invalid. + * + * The reason for returning invalid primary keys for foreign tables is + * because of pg_dump of NOT NULL constraints, and the fact that PKs + * remain marked invalid until the partitions' PKs are attached to it. + * If we make rd_pkindex invalid, then the attnotnull flag is reset + * after the PK is created, which causes the ALTER INDEX ATTACH + * PARTITION to fail with 'column ... is not marked NOT NULL'. With + * this, dropconstraint_internal() will believe that the columns must + * not have attnotnull reset, so the PKs-on-partitions can be attached + * correctly, until finally the PK-on-parent is marked valid. + * + * Also, this doesn't harm anything, because rd_pkindex is not a + * "real" index anyway, but a RELKIND_PARTITIONED_INDEX. + */ + if (index->indisprimary && + (index->indisvalid || + relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)) + { pkeyIndex = index->indexrelid; + pkdeferrable = !index->indimmediate; + } + + if (!index->indimmediate) + continue; + + if (!index->indisvalid) + continue; /* remember explicitly chosen replica index */ if (index->indisreplident) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index c323b5bd3d..e0463b8671 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -85,7 +85,8 @@ static catalogid_hash *catalogIdHash = NULL; static void flagInhTables(Archive *fout, TableInfo *tblinfo, int numTables, InhInfo *inhinfo, int numInherits); static void flagInhIndexes(Archive *fout, TableInfo *tblinfo, int numTables); -static void flagInhAttrs(Archive *fout, TableInfo *tblinfo, int numTables); +static void flagInhAttrs(Archive *fout, DumpOptions *dopt, TableInfo *tblinfo, + int numTables); static int strInArray(const char *pattern, char **arr, int arr_size); static IndxInfo *findIndexByOid(Oid oid); @@ -206,7 +207,7 @@ getSchemaData(Archive *fout, int *numTablesPtr) getTableAttrs(fout, tblinfo, numTables); pg_log_info("flagging inherited columns in subtables"); - flagInhAttrs(fout, tblinfo, numTables); + flagInhAttrs(fout, fout->dopt, tblinfo, numTables); pg_log_info("reading partitioning data"); getPartitioningInfo(fout); @@ -454,7 +455,8 @@ flagInhIndexes(Archive *fout, TableInfo tblinfo[], int numTables) * What we need to do here is: * * - Detect child columns that inherit NOT NULL bits from their parents, so - * that we needn't specify that again for the child. + * that we needn't specify that again for the child. (Versions >= 18 no + * longer need this.) * * - Detect child columns that have DEFAULT NULL when their parents had some * non-null default. In this case, we make up a dummy AttrDefInfo object so @@ -474,9 +476,8 @@ flagInhIndexes(Archive *fout, TableInfo tblinfo[], int numTables) * modifies tblinfo */ static void -flagInhAttrs(Archive *fout, TableInfo *tblinfo, int numTables) +flagInhAttrs(Archive *fout, DumpOptions *dopt, TableInfo *tblinfo, int numTables) { - DumpOptions *dopt = fout->dopt; int i, j, k; @@ -538,7 +539,16 @@ flagInhAttrs(Archive *fout, TableInfo *tblinfo, int numTables) { AttrDefInfo *parentDef = parent->attrdefs[inhAttrInd]; - foundNotNull |= parent->notnull[inhAttrInd]; + /* + * Account for each parent having a not-null constraint + * not marked NO INHERIT on this column. In versions + * 18 and later, we don't need this. + */ + if (fout->remoteVersion < 180000 && + parent->notnull_constrs[inhAttrInd] != NULL && + !parent->notnull_noinh[inhAttrInd]) + foundNotNull = true; + foundDefault |= (parentDef != NULL && strcmp(parentDef->adef_expr, "NULL") != 0 && !parent->attgenerated[inhAttrInd]); @@ -556,8 +566,13 @@ flagInhAttrs(Archive *fout, TableInfo *tblinfo, int numTables) } } - /* Remember if we found inherited NOT NULL */ - tbinfo->inhNotNull[j] = foundNotNull; + /* + * In versions < 18, for lack of a better system, we arbitrarily + * decide that a not-null constraint is not locally defined if at + * least one of the parents has it. + */ + if (fout->remoteVersion < 180000 && !foundNotNull) + tbinfo->notnull_islocal[j] = false; /* * Manufacture a DEFAULT NULL clause if necessary. This breaks diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 130b80775d..3e43c1d1d6 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -345,6 +345,10 @@ static void getTableData(DumpOptions *dopt, TableInfo *tblinfo, int numTables, c static void makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo); static void buildMatViewRefreshDependencies(Archive *fout); static void getTableDataFKConstraints(void); +static void determineNotNullFlags(Archive *fout, PGresult *res, int r, + TableInfo *tbinfo, int j, + int i_notnull_name, int i_notnull_noinherit, + int i_notnull_islocal); static char *format_function_arguments(const FuncInfo *finfo, const char *funcargs, bool is_agg); static char *format_function_signature(Archive *fout, @@ -8736,7 +8740,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) int i_attlen; int i_attalign; int i_attislocal; - int i_attnotnull; + int i_notnull_name; + int i_notnull_noinherit; + int i_notnull_islocal; int i_attoptions; int i_attcollation; int i_attcompression; @@ -8746,13 +8752,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) /* * We want to perform just one query against pg_attribute, and then just - * one against pg_attrdef (for DEFAULTs) and one against pg_constraint - * (for CHECK constraints). However, we mustn't try to select every row - * of those catalogs and then sort it out on the client side, because some - * of the server-side functions we need would be unsafe to apply to tables - * we don't have lock on. Hence, we build an array of the OIDs of tables - * we care about (and now have lock on!), and use a WHERE clause to - * constrain which rows are selected. + * one against pg_attrdef (for DEFAULTs) and two against pg_constraint + * (for CHECK constraints and for NOT NULL constraints). However, we + * mustn't try to select every row of those catalogs and then sort it out + * on the client side, because some of the server-side functions we need + * would be unsafe to apply to tables we don't have lock on. Hence, we + * build an array of the OIDs of tables we care about (and now have lock + * on!), and use a WHERE clause to constrain which rows are selected. */ appendPQExpBufferChar(tbloids, '{'); appendPQExpBufferChar(checkoids, '{'); @@ -8799,7 +8805,6 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) "a.attstattarget,\n" "a.attstorage,\n" "t.typstorage,\n" - "a.attnotnull,\n" "a.atthasdef,\n" "a.attisdropped,\n" "a.attlen,\n" @@ -8816,6 +8821,30 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) "ORDER BY option_name" "), E',\n ') AS attfdwoptions,\n"); + /* + * Find out any NOT NULL markings for each column. In 18 and up we read + * pg_constraint to obtain the constraint name. notnull_noinherit is set + * according to the NO INHERIT property. For versions prior to 18, we + * store an empty string as the name when a constraint is marked as + * attnotnull (this cues dumpTableSchema to print the NOT NULL clause + * without a name); also, such cases are never NO INHERIT. + * + * We track in notnull_islocal whether the constraint was defined directly + * in this table or via an ancestor, for binary upgrade. flagInhAttrs + * might modify this later for servers older than 18; it's also in charge + * of determining the correct inhcount. + */ + if (fout->remoteVersion >= 180000) + appendPQExpBufferStr(q, + "co.conname AS notnull_name,\n" + "co.connoinherit AS notnull_noinherit,\n" + "co.conislocal AS notnull_islocal,\n"); + else + appendPQExpBufferStr(q, + "CASE WHEN a.attnotnull THEN '' ELSE NULL END AS notnull_name,\n" + "false AS notnull_noinherit,\n" + "a.attislocal AS notnull_islocal,\n"); + if (fout->remoteVersion >= 140000) appendPQExpBufferStr(q, "a.attcompression AS attcompression,\n"); @@ -8850,11 +8879,25 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n" "JOIN pg_catalog.pg_attribute a ON (src.tbloid = a.attrelid) " "LEFT JOIN pg_catalog.pg_type t " - "ON (a.atttypid = t.oid)\n" - "WHERE a.attnum > 0::pg_catalog.int2\n" - "ORDER BY a.attrelid, a.attnum", + "ON (a.atttypid = t.oid)\n", tbloids->data); + /* + * In versions 18 and up, we need pg_constraint for explicit NOT NULL + * entries. Also, we need to know if the NOT NULL for each column is + * backing a primary key. + */ + if (fout->remoteVersion >= 180000) + appendPQExpBufferStr(q, + " LEFT JOIN pg_catalog.pg_constraint co ON " + "(a.attrelid = co.conrelid\n" + " AND co.contype = 'n' AND " + "co.conkey = array[a.attnum])\n"); + + appendPQExpBufferStr(q, + "WHERE a.attnum > 0::pg_catalog.int2\n" + "ORDER BY a.attrelid, a.attnum"); + res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK); ntups = PQntuples(res); @@ -8872,7 +8915,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) i_attlen = PQfnumber(res, "attlen"); i_attalign = PQfnumber(res, "attalign"); i_attislocal = PQfnumber(res, "attislocal"); - i_attnotnull = PQfnumber(res, "attnotnull"); + i_notnull_name = PQfnumber(res, "notnull_name"); + i_notnull_noinherit = PQfnumber(res, "notnull_noinherit"); + i_notnull_islocal = PQfnumber(res, "notnull_islocal"); i_attoptions = PQfnumber(res, "attoptions"); i_attcollation = PQfnumber(res, "attcollation"); i_attcompression = PQfnumber(res, "attcompression"); @@ -8937,8 +8982,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) tbinfo->attcompression = (char *) pg_malloc(numatts * sizeof(char)); tbinfo->attfdwoptions = (char **) pg_malloc(numatts * sizeof(char *)); tbinfo->attmissingval = (char **) pg_malloc(numatts * sizeof(char *)); - tbinfo->notnull = (bool *) pg_malloc(numatts * sizeof(bool)); - tbinfo->inhNotNull = (bool *) pg_malloc(numatts * sizeof(bool)); + tbinfo->notnull_constrs = (char **) pg_malloc(numatts * sizeof(char *)); + tbinfo->notnull_noinh = (bool *) pg_malloc(numatts * sizeof(bool)); + tbinfo->notnull_islocal = (bool *) pg_malloc(numatts * sizeof(bool)); tbinfo->attrdefs = (AttrDefInfo **) pg_malloc(numatts * sizeof(AttrDefInfo *)); hasdefaults = false; @@ -8962,7 +9008,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen)); tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign)); tbinfo->attislocal[j] = (PQgetvalue(res, r, i_attislocal)[0] == 't'); - tbinfo->notnull[j] = (PQgetvalue(res, r, i_attnotnull)[0] == 't'); + + /* Handle not-null constraint name and flags */ + determineNotNullFlags(fout, res, r, + tbinfo, j, + i_notnull_name, i_notnull_noinherit, + i_notnull_islocal); + tbinfo->attoptions[j] = pg_strdup(PQgetvalue(res, r, i_attoptions)); tbinfo->attcollation[j] = atooid(PQgetvalue(res, r, i_attcollation)); tbinfo->attcompression[j] = *(PQgetvalue(res, r, i_attcompression)); @@ -8971,8 +9023,6 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) tbinfo->attrdefs[j] = NULL; /* fix below */ if (PQgetvalue(res, r, i_atthasdef)[0] == 't') hasdefaults = true; - /* these flags will be set in flagInhAttrs() */ - tbinfo->inhNotNull[j] = false; } if (hasdefaults) @@ -9253,6 +9303,110 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) destroyPQExpBuffer(checkoids); } +/* + * Based on the getTableAttrs query's row corresponding to one column, set + * the name and flags to handle a not-null constraint for that column in + * the tbinfo struct. + * + * Result row 'r' is for tbinfo's attribute 'j'. + * + * There are three possibilities: + * 1) the column has no not-null constraints. In that case, ->notnull_constrs + * (the constraint name) remains NULL. + * 2) The column has a constraint with no name (this is the case when + * constraints come from pre-18 servers). In this case, ->notnull_constrs + * is set to the empty string; dumpTableSchema will print just "NOT NULL". + * 3) The column has a constraint with a known name; in that case + * notnull_constrs carries that name and dumpTableSchema will print + * "CONSTRAINT the_name NOT NULL". However, if the name is the default + * (table_column_not_null), there's no need to print that name in the dump, + * so notnull_constrs is set to the empty string and it behaves as the case + * above. + * + * In a child table that inherits from a parent already containing NOT NULL + * constraints and the columns in the child don't have their own NOT NULL + * declarations, we suppress printing constraints in the child: the + * constraints are acquired at the point where the child is attached to the + * parent. This is tracked in ->notnull_inh (which is set in flagInhAttrs for + * servers pre-18). + * + * Any of these constraints might have the NO INHERIT bit. If so we set + * ->notnull_noinh and NO INHERIT will be printed by dumpTableSchema. + * + * In case 3 above, the name comparison is a bit of a hack; it actually fails + * to do the right thing in all but the trivial case. However, the downside + * of getting it wrong is simply that the name is printed rather than + * suppressed, so it's not a big deal. + */ +static void +determineNotNullFlags(Archive *fout, PGresult *res, int r, + TableInfo *tbinfo, int j, + int i_notnull_name, int i_notnull_noinherit, + int i_notnull_islocal) +{ + DumpOptions *dopt = fout->dopt; + + /* + * notnull_noinh is straight from the query result. notnull_islocal + * also, though flagInhAttrs may change that one later in versions < 18. + */ + tbinfo->notnull_noinh[j] = PQgetvalue(res, r, i_notnull_noinherit)[0] == 't'; + tbinfo->notnull_islocal[j] = PQgetvalue(res, r, i_notnull_islocal)[0] == 't'; + + /* + * Determine a constraint name to use. If the column is not marked not- + * null, we set NULL which cues ... to do nothing. An empty string says + * to print an unnamed NOT NULL, and anything else is a constraint name + * to use. + */ + if (fout->remoteVersion < 180000) + { + /* + * < 18 doesn't have not-null names, so an unnamed constraint is + * sufficient. + */ + if (PQgetisnull(res, r, i_notnull_name)) + tbinfo->notnull_constrs[j] = NULL; + else + tbinfo->notnull_constrs[j] = ""; + } + else + { + if (PQgetisnull(res, r, i_notnull_name)) + tbinfo->notnull_constrs[j] = NULL; + else + { + /* + * In binary upgrade of inheritance child tables, must have a + * constraint name that we can UPDATE later. + */ + if (dopt->binary_upgrade && + !tbinfo->ispartition && + !tbinfo->notnull_islocal) + { + tbinfo->notnull_constrs[j] = + pstrdup(PQgetvalue(res, r, i_notnull_name)); + } + else + { + char *default_name; + + /* XXX should match ChooseConstraintName better */ + default_name = psprintf("%s_%s_not_null", tbinfo->dobj.name, + tbinfo->attnames[j]); + if (strcmp(default_name, + PQgetvalue(res, r, i_notnull_name)) == 0) + tbinfo->notnull_constrs[j] = ""; + else + { + tbinfo->notnull_constrs[j] = + pstrdup(PQgetvalue(res, r, i_notnull_name)); + } + } + } + } +} + /* * Test whether a column should be printed as part of table's CREATE TABLE. * Column number is zero-based. @@ -15950,13 +16104,14 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) !tbinfo->attrdefs[j]->separate); /* - * Not Null constraint --- suppress if inherited, except - * if partition, or in binary-upgrade case where that - * won't work. + * Not Null constraint --- print it if it is locally + * defined, or if binary upgrade. (In the latter case, + * we reset conislocal below.) */ - print_notnull = (tbinfo->notnull[j] && - (!tbinfo->inhNotNull[j] || - tbinfo->ispartition || dopt->binary_upgrade)); + print_notnull = (tbinfo->notnull_constrs[j] != NULL && + (tbinfo->notnull_islocal[j] || + dopt->binary_upgrade || + tbinfo->ispartition)); /* * Skip column if fully defined by reloftype, except in @@ -16014,7 +16169,16 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) if (print_notnull) - appendPQExpBufferStr(q, " NOT NULL"); + { + if (tbinfo->notnull_constrs[j][0] == '\0') + appendPQExpBufferStr(q, " NOT NULL"); + else + appendPQExpBuffer(q, " CONSTRAINT %s NOT NULL", + fmtId(tbinfo->notnull_constrs[j])); + + if (tbinfo->notnull_noinh[j]) + appendPQExpBufferStr(q, " NO INHERIT"); + } /* Add collation if not default for the type */ if (OidIsValid(tbinfo->attcollation[j])) @@ -16269,6 +16433,41 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) if (!firstitem) appendPQExpBufferStr(q, ");\n"); + /* + * Fix up not-null constraints that come from inheritance. As + * above, do the pg_constraint manipulations in a single SQL + * command. + */ + firstitem = true; + for (j = 0; j < tbinfo->numatts; j++) + { + /* + * If a not-null constraint comes from inheritance, reset + * conislocal. The inhcount is fixed by ALTER TABLE INHERIT, + * below. + */ + if (tbinfo->notnull_constrs[j] != NULL && + tbinfo->notnull_constrs[j][0] != '\0' && + !tbinfo->notnull_islocal[j]) + { + if (firstitem) + { + appendPQExpBufferStr(q, "UPDATE pg_catalog.pg_constraint\n" + "SET conislocal = false\n" + "WHERE contype = 'n' AND conrelid = "); + appendStringLiteralAH(q, qualrelname, fout); + appendPQExpBufferStr(q, "::pg_catalog.regclass AND\n" + "conname IN ("); + firstitem = false; + } + else + appendPQExpBufferStr(q, ", "); + appendStringLiteralAH(q, tbinfo->notnull_constrs[j], fout); + } + } + if (!firstitem) + appendPQExpBufferStr(q, ");\n"); + /* * Add inherited CHECK constraints, if any. * @@ -16408,11 +16607,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) * we have to mark it separately. */ if (!shouldPrintColumn(dopt, tbinfo, j) && - tbinfo->notnull[j] && !tbinfo->inhNotNull[j]) - appendPQExpBuffer(q, - "ALTER %sTABLE ONLY %s ALTER COLUMN %s SET NOT NULL;\n", - foreign, qualrelname, - fmtId(tbinfo->attnames[j])); + tbinfo->notnull_constrs[j] != NULL && + (tbinfo->notnull_islocal[j] && !tbinfo->ispartition && !dopt->binary_upgrade)) + { + /* No constraint name desired? */ + if (tbinfo->notnull_constrs[j][0] == '\0') + appendPQExpBuffer(q, + "ALTER %sTABLE ONLY %s ALTER COLUMN %s SET NOT NULL;\n", + foreign, qualrelname, + fmtId(tbinfo->attnames[j])); + else + appendPQExpBuffer(q, + "ALTER %sTABLE ONLY %s ADD CONSTRAINT %s NOT NULL %s;\n", + foreign, qualrelname, + tbinfo->notnull_constrs[j], + fmtId(tbinfo->attnames[j])); + } /* * Dump per-column statistics information. We only issue an ALTER diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 9f907ed5ad..7c654500c1 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -347,8 +347,12 @@ typedef struct _tableInfo char *attcompression; /* per-attribute compression method */ char **attfdwoptions; /* per-attribute fdw options */ char **attmissingval; /* per attribute missing value */ - bool *notnull; /* not-null constraints on attributes */ - bool *inhNotNull; /* true if NOT NULL is inherited */ + char **notnull_constrs; /* NOT NULL constraint names. If null, + * there isn't one on this column. If + * empty string, unnamed constraint + * (pre-v17) */ + bool *notnull_noinh; /* NOT NULL is NO INHERIT */ + bool *notnull_islocal; /* true if NOT NULL has local definition */ struct _attrDefInfo **attrdefs; /* DEFAULT expressions */ struct _constraintInfo *checkexprs; /* CHECK constraints */ bool needs_override; /* has GENERATED ALWAYS AS IDENTITY */ diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index ab6c830491..1094534598 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -3340,8 +3340,8 @@ my %tests = ( FOR VALUES FROM (\'2006-02-01\') TO (\'2006-03-01\');', regexp => qr/^ \QCREATE TABLE dump_test_second_schema.measurement_y2006m2 (\E\n - \s+\Qcity_id integer DEFAULT nextval('dump_test.measurement_city_id_seq'::regclass) NOT NULL,\E\n - \s+\Qlogdate date NOT NULL,\E\n + \s+\Qcity_id integer DEFAULT nextval('dump_test.measurement_city_id_seq'::regclass) CONSTRAINT measurement_city_id_not_null NOT NULL,\E\n + \s+\Qlogdate date CONSTRAINT measurement_logdate_not_null NOT NULL,\E\n \s+\Qpeaktemp integer,\E\n \s+\Qunitsales integer DEFAULT 0,\E\n \s+\QCONSTRAINT measurement_peaktemp_check CHECK ((peaktemp >= '-460'::integer)),\E\n @@ -3749,7 +3749,7 @@ my %tests = ( ) INHERITS (dump_test.test_inheritance_parent);', regexp => qr/^ \QCREATE TABLE dump_test.test_inheritance_child (\E\n - \s+\Qcol1 integer,\E\n + \s+\Qcol1 integer NOT NULL,\E\n \s+\QCONSTRAINT test_inheritance_child CHECK ((col2 >= 142857))\E\n \)\n \QINHERITS (dump_test.test_inheritance_parent);\E\n diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index faabecbc76..a64ad26b65 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -3058,6 +3058,56 @@ describeOneTableDetails(const char *schemaname, } PQclear(result); } + + /* + * If verbose, print NOT NULL constraints, omitting those in columns of + * the primary key. + */ + if (verbose) + { + printfPQExpBuffer(&buf, + "SELECT c.conname, a.attname, c.connoinherit,\n" + " c.conislocal, c.coninhcount <> 0\n" + "FROM pg_catalog.pg_constraint c JOIN\n" + " pg_catalog.pg_attribute a ON\n" + " (a.attrelid = c.conrelid AND a.attnum = c.conkey[1])\n" + "WHERE c.contype = 'n' AND\n" + " c.conrelid = '%s'::pg_catalog.regclass AND\n" + " NOT EXISTS (SELECT 1 FROM pg_catalog.pg_constraint co1\n" + " WHERE co1.contype = 'p' AND\n" + " a.attnum = any(co1.conkey) AND\n" + " co1.conrelid = '%s'::pg_catalog.regclass)\n" + "ORDER BY a.attnum", + oid, + oid); + + result = PSQLexec(buf.data); + if (!result) + goto error_return; + else + tuples = PQntuples(result); + + if (tuples > 0) + printTableAddFooter(&cont, _("Not-null constraints:")); + + /* Might be an empty set - that's ok */ + for (i = 0; i < tuples; i++) + { + bool islocal = PQgetvalue(result, i, 3)[0] == 't'; + bool inherited = PQgetvalue(result, i, 4)[0] == 't'; + + printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s", + PQgetvalue(result, i, 0), + PQgetvalue(result, i, 1), + PQgetvalue(result, i, 2)[0] == 't' ? + " NO INHERIT" : + islocal && inherited ? _(" (local, inherited)") : + inherited ? _(" (inherited)") : ""); + + printTableAddFooter(&cont, buf.data); + } + PQclear(result); + } } /* Get view_def if table is a view or materialized view */ diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl index fd645896c9..d9a1301503 100644 --- a/src/bin/psql/t/010_tab_completion.pl +++ b/src/bin/psql/t/010_tab_completion.pl @@ -39,7 +39,7 @@ $node->start; # set up a few database objects $node->safe_psql('postgres', - "CREATE TABLE tab1 (c1 int primary key, c2 text);\n" + "CREATE TABLE tab1 (c1 int primary key constraint foo not null, c2 text);\n" . "CREATE TABLE mytab123 (f1 int, f2 text);\n" . "CREATE TABLE mytab246 (f1 int, f2 text);\n" . "CREATE TABLE \"mixedName\" (f1 int, f2 text);\n" @@ -209,21 +209,21 @@ clear_query(); # check interpretation of referenced names check_completion( - "alter table tab1 drop constraint \t", + "alter table tab1 drop constraint t\t", qr/tab1_pkey /, "complete index name for referenced table"); clear_query(); check_completion( - "alter table TAB1 drop constraint \t", + "alter table TAB1 drop constraint t\t", qr/tab1_pkey /, "complete index name for referenced table, with downcasing"); clear_query(); check_completion( - "alter table public.\"tab1\" drop constraint \t", + "alter table public.\"tab1\" drop constraint t\t", qr/tab1_pkey /, "complete index name for referenced table, with schema and quoting"); diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h index c512824cd1..e446d49b3e 100644 --- a/src/include/catalog/heap.h +++ b/src/include/catalog/heap.h @@ -34,10 +34,11 @@ typedef struct RawColumnDefault typedef struct CookedConstraint { - ConstrType contype; /* CONSTR_DEFAULT or CONSTR_CHECK */ + ConstrType contype; /* CONSTR_DEFAULT, CONSTR_CHECK, + * CONSTR_NOTNULL */ Oid conoid; /* constr OID if created, otherwise Invalid */ char *name; /* name, or NULL if none */ - AttrNumber attnum; /* which attr (only for DEFAULT) */ + AttrNumber attnum; /* which attr (only for NOTNULL, DEFAULT) */ Node *expr; /* transformed default or check expr */ bool skip_validation; /* skip validation? (only for CHECK) */ bool is_local; /* constraint has local (non-inherited) def */ @@ -113,6 +114,9 @@ extern List *AddRelationNewConstraints(Relation rel, bool is_local, bool is_internal, const char *queryString); +extern List *AddRelationNotNullConstraints(Relation rel, + List *constraints, + List *old_notnulls); extern void RelationClearMissing(Relation rel); extern void SetAttrMissing(Oid relid, char *attname, char *value); diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h index 115217a616..eb62a145fc 100644 --- a/src/include/catalog/pg_constraint.h +++ b/src/include/catalog/pg_constraint.h @@ -257,7 +257,13 @@ extern char *ChooseConstraintName(const char *name1, const char *name2, const char *label, Oid namespaceid, List *others); +extern HeapTuple findNotNullConstraintAttnum(Oid relid, AttrNumber attnum); +extern HeapTuple findNotNullConstraint(Oid relid, const char *colname); extern HeapTuple findDomainNotNullConstraint(Oid typid); +extern AttrNumber extractNotNullColumn(HeapTuple constrTup); +extern bool AdjustNotNullInheritance(Oid relid, AttrNumber attnum, int count, + bool is_local, bool is_no_inherit); +extern List *RelationGetNotNullConstraints(Oid relid, bool cooked); extern void RemoveConstraintById(Oid conId); extern void RenameConstraintById(Oid conId, const char *newname); diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 0765e5c57b..028f8815d1 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -68,6 +68,7 @@ extern RelabelType *makeRelabelType(Expr *arg, Oid rtype, int32 rtypmod, Oid rcollid, CoercionForm rformat); extern RangeVar *makeRangeVar(char *schemaname, char *relname, int location); +extern Constraint *makeNotNullConstraint(String *colname); extern TypeName *makeTypeName(char *typnam); extern TypeName *makeTypeNameFromNameList(List *names); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index e62ce1b753..3572f6da6c 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2354,7 +2354,6 @@ typedef enum AlterTableType AT_SetNotNull, /* alter column set not null */ AT_SetExpression, /* alter column set expression */ AT_DropExpression, /* alter column drop expression */ - AT_CheckNotNull, /* check column is already marked not null */ AT_SetStatistics, /* alter column set statistics */ AT_SetOptions, /* alter column set ( options ) */ AT_ResetOptions, /* alter column reset ( options ) */ @@ -2637,10 +2636,10 @@ 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 - * Constraint nodes (in fact, only CONSTR_CHECK nodes, in the present - * implementation). + * 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. * ---------------------- */ @@ -2655,6 +2654,7 @@ typedef struct CreateStmt PartitionSpec *partspec; /* PARTITION BY clause */ TypeName *ofTypename; /* OF typename */ List *constraints; /* constraints (list of Constraint nodes) */ + List *nnconstraints; /* NOT NULL constraints (ditto) */ List *options; /* options from WITH clause */ OnCommitAction oncommit; /* what do we do at COMMIT? */ char *tablespacename; /* table space to use, or NULL */ diff --git a/src/test/modules/test_ddl_deparse/expected/alter_table.out b/src/test/modules/test_ddl_deparse/expected/alter_table.out index 6daa186a84..50d0354a34 100644 --- a/src/test/modules/test_ddl_deparse/expected/alter_table.out +++ b/src/test/modules/test_ddl_deparse/expected/alter_table.out @@ -28,6 +28,7 @@ ALTER TABLE parent ADD COLUMN b serial; NOTICE: DDL test: type simple, tag CREATE SEQUENCE NOTICE: DDL test: type alter table, tag ALTER TABLE NOTICE: subcommand: type ADD COLUMN (and recurse) desc column b of table parent +NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint parent_b_not_null on table parent NOTICE: DDL test: type simple, tag ALTER SEQUENCE ALTER TABLE parent RENAME COLUMN b TO c; NOTICE: DDL test: type simple, tag ALTER TABLE @@ -57,24 +58,17 @@ NOTICE: subcommand: type DETACH PARTITION desc table part2 DROP TABLE part2; ALTER TABLE part ADD PRIMARY KEY (a); NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type SET NOT NULL desc column a of table part -NOTICE: subcommand: type SET NOT NULL desc column a of table part1 +NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint part_a_not_null on table part NOTICE: subcommand: type ADD INDEX desc index part_pkey ALTER TABLE parent ALTER COLUMN a SET NOT NULL; NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type SET NOT NULL desc column a of table parent -NOTICE: subcommand: type SET NOT NULL desc column a of table child -NOTICE: subcommand: type SET NOT NULL desc column a of table grandchild +NOTICE: subcommand: type SET NOT NULL (and recurse) desc constraint parent_a_not_null on table parent ALTER TABLE parent ALTER COLUMN a DROP NOT NULL; NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type DROP NOT NULL desc column a of table parent -NOTICE: subcommand: type DROP NOT NULL desc column a of table child -NOTICE: subcommand: type DROP NOT NULL desc column a of table grandchild +NOTICE: subcommand: type DROP NOT NULL (and recurse) desc column a of table parent ALTER TABLE parent ALTER COLUMN a SET NOT NULL; NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type SET NOT NULL desc column a of table parent -NOTICE: subcommand: type SET NOT NULL desc column a of table child -NOTICE: subcommand: type SET NOT NULL desc column a of table grandchild +NOTICE: subcommand: type SET NOT NULL (and recurse) desc constraint parent_a_not_null on table parent ALTER TABLE parent ALTER COLUMN a ADD GENERATED ALWAYS AS IDENTITY; NOTICE: DDL test: type simple, tag CREATE SEQUENCE NOTICE: DDL test: type simple, tag ALTER SEQUENCE @@ -116,6 +110,7 @@ NOTICE: DDL test: type alter table, tag ALTER TABLE NOTICE: subcommand: type ALTER COLUMN SET TYPE desc column c of table parent NOTICE: subcommand: type ALTER COLUMN SET TYPE desc column c of table child NOTICE: subcommand: type ALTER COLUMN SET TYPE desc column c of table grandchild +NOTICE: subcommand: type (re) ADD CONSTRAINT desc constraint parent_b_not_null on table parent NOTICE: subcommand: type (re) ADD STATS desc statistics object parent_stat ALTER TABLE parent ALTER COLUMN c SET DEFAULT 0; NOTICE: DDL test: type alter table, tag ALTER TABLE diff --git a/src/test/modules/test_ddl_deparse/expected/create_table.out b/src/test/modules/test_ddl_deparse/expected/create_table.out index 2178ce83e9..14915f661a 100644 --- a/src/test/modules/test_ddl_deparse/expected/create_table.out +++ b/src/test/modules/test_ddl_deparse/expected/create_table.out @@ -85,8 +85,6 @@ CREATE TABLE employees OF employee_type ( salary WITH OPTIONS DEFAULT 1000 ); NOTICE: DDL test: type simple, tag CREATE TABLE -NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type SET NOT NULL desc column name of table employees NOTICE: DDL test: type simple, tag CREATE INDEX -- Inheritance CREATE TABLE person ( 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 2758ae82d7..3922dc0f33 100644 --- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c +++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c @@ -135,9 +135,6 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS) case AT_DropExpression: strtype = "DROP EXPRESSION"; break; - case AT_CheckNotNull: - strtype = "CHECK NOT NULL"; - break; case AT_SetStatistics: strtype = "SET STATS"; break; diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index 79cf82b5ae..b748dfca29 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -1216,20 +1216,6 @@ alter table only parent alter a set not null; ERROR: column "a" of relation "parent" contains null values alter table child alter a set not null; ERROR: column "a" of relation "child" contains null values -delete from parent; -alter table only parent alter a set not null; -insert into parent values (NULL); -ERROR: null value in column "a" of relation "parent" violates not-null constraint -DETAIL: Failing row contains (null). -alter table child alter a set not null; -insert into child (a, b) values (NULL, 'foo'); -ERROR: null value in column "a" of relation "child" violates not-null constraint -DETAIL: Failing row contains (null, foo). -delete from child; -alter table child alter a set not null; -insert into child (a, b) values (NULL, 'foo'); -ERROR: null value in column "a" of relation "child" violates not-null constraint -DETAIL: Failing row contains (null, foo). drop table child; drop table parent; -- test setting and removing default values @@ -3386,6 +3372,7 @@ DROP TABLE tt9; -- Check that comments on constraints and indexes are not lost at ALTER TABLE. CREATE TABLE comment_test ( id int, + constraint id_notnull_constraint not null id, positive_col int CHECK (positive_col > 0), indexed_col int, CONSTRAINT comment_test_pk PRIMARY KEY (id)); @@ -3394,6 +3381,7 @@ COMMENT ON COLUMN comment_test.id IS 'Column ''id'' on comment_test'; COMMENT ON INDEX comment_test_index IS 'Simple index on comment_test'; COMMENT ON CONSTRAINT comment_test_positive_col_check ON comment_test IS 'CHECK constraint on comment_test.positive_col'; COMMENT ON CONSTRAINT comment_test_pk ON comment_test IS 'PRIMARY KEY constraint of comment_test'; +COMMENT ON CONSTRAINT id_notnull_constraint ON comment_test IS 'NOT NULL constraint of comment_test'; COMMENT ON INDEX comment_test_pk IS 'Index backing the PRIMARY KEY of comment_test'; SELECT col_description('comment_test'::regclass, 1) as comment; comment @@ -3413,7 +3401,8 @@ SELECT conname as constraint, obj_description(oid, 'pg_constraint') as comment F ---------------------------------+----------------------------------------------- comment_test_pk | PRIMARY KEY constraint of comment_test comment_test_positive_col_check | CHECK constraint on comment_test.positive_col -(2 rows) + id_notnull_constraint | NOT NULL constraint of comment_test +(3 rows) -- Change the datatype of all the columns. ALTER TABLE is optimized to not -- rebuild an index if the new data type is binary compatible with the old @@ -3444,7 +3433,8 @@ SELECT conname as constraint, obj_description(oid, 'pg_constraint') as comment F ---------------------------------+----------------------------------------------- comment_test_pk | PRIMARY KEY constraint of comment_test comment_test_positive_col_check | CHECK constraint on comment_test.positive_col -(2 rows) + id_notnull_constraint | NOT NULL constraint of comment_test +(3 rows) -- Check compatibility for foreign keys and comments. This is done -- separately as rebuilding the column type of the parent leads @@ -3860,6 +3850,9 @@ CREATE TABLE atnotnull1 (); ALTER TABLE atnotnull1 ADD COLUMN a INT, ALTER a SET NOT NULL; +ALTER TABLE atnotnull1 + ADD COLUMN b INT, + ADD NOT NULL b; ALTER TABLE atnotnull1 ADD COLUMN c INT, ADD PRIMARY KEY (c); @@ -3868,9 +3861,13 @@ ALTER TABLE atnotnull1 Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+---------+-----------+----------+---------+---------+--------------+------------- a | integer | | not null | | plain | | + b | integer | | not null | | plain | | c | integer | | not null | | plain | | Indexes: "atnotnull1_pkey" PRIMARY KEY, btree (c) +Not-null constraints: + "atnotnull1_a_not_null" NOT NULL "a" + "atnotnull1_b_not_null" NOT NULL "b" -- cannot drop column that is part of the partition key CREATE TABLE partitioned ( @@ -4397,7 +4394,6 @@ ERROR: cannot alter inherited column "b" -- partitions exist ALTER TABLE ONLY list_parted2 ALTER b SET NOT NULL; ERROR: constraint must be added to child tables too -DETAIL: Column "b" of relation "part_2" is not already NOT NULL. HINT: Do not specify the ONLY keyword. ALTER TABLE ONLY list_parted2 ADD CONSTRAINT check_b CHECK (b <> 'zz'); ERROR: constraint must be added to child tables too diff --git a/src/test/regress/expected/cluster.out b/src/test/regress/expected/cluster.out index a13aafff0b..4d40a6809a 100644 --- a/src/test/regress/expected/cluster.out +++ b/src/test/regress/expected/cluster.out @@ -247,11 +247,12 @@ ERROR: insert or update on table "clstr_tst" violates foreign key constraint "c DETAIL: Key (b)=(1111) is not present in table "clstr_tst_s". SELECT conname FROM pg_constraint WHERE conrelid = 'clstr_tst'::regclass ORDER BY 1; - conname ----------------- + conname +---------------------- + clstr_tst_a_not_null clstr_tst_con clstr_tst_pkey -(2 rows) +(3 rows) SELECT relname, relkind, EXISTS(SELECT 1 FROM pg_class WHERE oid = c.reltoastrelid) AS hastoast diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out index cf0b80d616..1862c71bd4 100644 --- a/src/test/regress/expected/constraints.out +++ b/src/test/regress/expected/constraints.out @@ -6,6 +6,7 @@ -- - PRIMARY KEY clauses -- - UNIQUE clauses -- - EXCLUDE clauses +-- - NOT NULL clauses -- -- directory paths are passed to us in environment variables \getenv abs_srcdir PG_ABS_SRCDIR @@ -797,6 +798,351 @@ ALTER TABLE deferred_excl ADD EXCLUDE (f1 WITH =); ERROR: could not create exclusion constraint "deferred_excl_f1_excl" DETAIL: Key (f1)=(3) conflicts with key (f1)=(3). DROP TABLE deferred_excl; +-- verify constraints created for NOT NULL clauses +CREATE TABLE notnull_tbl1 (a INTEGER NOT NULL NOT NULL); +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl1_a_not_null" NOT NULL "a" + +-- no-op +ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl1_a_not_null" NOT NULL "a" + +-- duplicate name +ALTER TABLE notnull_tbl1 ADD COLUMN b INT CONSTRAINT notnull_tbl1_a_not_null NOT NULL; +ERROR: constraint "notnull_tbl1_a_not_null" for relation "notnull_tbl1" already exists +-- DROP NOT NULL gets rid of both the attnotnull flag and the constraint itself +ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + +-- SET NOT NULL puts both back +ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl1_a_not_null" NOT NULL "a" + +-- Doing it twice doesn't create a redundant constraint +ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; +select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass; + conname | contype | conkey +-------------------------+---------+-------- + notnull_tbl1_a_not_null | n | {1} +(1 row) + +-- Using the "table constraint" syntax also works +ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL; +ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "foobar" NOT NULL "a" + +DROP TABLE notnull_tbl1; +-- NOT NULL NO INHERIT +CREATE TABLE ATACC1 (a int, not null a no inherit); +CREATE TABLE ATACC2 () INHERITS (ATACC1); +\d+ ATACC2 + Table "public.atacc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: atacc1 + +DROP TABLE ATACC1, ATACC2; +CREATE TABLE ATACC1 (a int); +ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT; +CREATE TABLE ATACC2 () INHERITS (ATACC1); +\d+ ATACC2 + Table "public.atacc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: atacc1 + +DROP TABLE ATACC1, ATACC2; +CREATE TABLE ATACC1 (a int); +CREATE TABLE ATACC2 () INHERITS (ATACC1); +ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT; +\d+ ATACC2 + Table "public.atacc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: atacc1 + +DROP TABLE ATACC1, ATACC2; +-- NOT NULL NO INHERIT is not possible on partitioned tables +CREATE TABLE ATACC1 (a int NOT NULL NO INHERIT) PARTITION BY LIST (a); +ERROR: not-null constraints on partitioned tables cannot be NO INHERIT +CREATE TABLE ATACC1 (a int, NOT NULL a NO INHERIT) PARTITION BY LIST (a); +ERROR: not-null constraints on partitioned tables cannot be NO INHERIT +-- it's not possible to override a no-inherit constraint with an inheritable one +CREATE TABLE ATACC2 (a int, CONSTRAINT a_is_not_null NOT NULL a NO INHERIT); +CREATE TABLE ATACC1 (a int); +CREATE TABLE ATACC3 (a int) INHERITS (ATACC2); +NOTICE: merging column "a" with inherited definition +ALTER TABLE ATACC2 INHERIT ATACC1; +-- can't override +ALTER TABLE ATACC1 ADD CONSTRAINT ditto NOT NULL a; +ERROR: cannot change NO INHERIT status of NOT NULL constraint "a_is_not_null" on relation "atacc2" +-- dropping the NO INHERIT constraint allows this to work +ALTER TABLE ATACC2 DROP CONSTRAINT a_is_not_null; +ALTER TABLE ATACC1 ADD CONSTRAINT ditto NOT NULL a; +\d+ ATACC3 + Table "public.atacc3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "ditto" NOT NULL "a" (inherited) +Inherits: atacc2 + +DROP TABLE ATACC1, ATACC2, ATACC3; +-- Can't have two constraints with the same name +CREATE TABLE notnull_tbl2 (a INTEGER CONSTRAINT blah NOT NULL, b INTEGER CONSTRAINT blah NOT NULL); +ERROR: constraint "blah" for relation "notnull_tbl2" already exists +-- can't drop not-null in primary key +CREATE TABLE notnull_tbl2 (a INTEGER PRIMARY KEY); +ALTER TABLE notnull_tbl2 ALTER a DROP NOT NULL; +ERROR: column "a" is in a primary key +DROP TABLE notnull_tbl2; +CREATE TABLE notnull_tbl3 (a INTEGER NOT NULL, CHECK (a IS NOT NULL)); +ALTER TABLE notnull_tbl3 ALTER A DROP NOT NULL; +ALTER TABLE notnull_tbl3 ADD b int, ADD CONSTRAINT pk PRIMARY KEY (a, b); +\d notnull_tbl3 + Table "public.notnull_tbl3" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | not null | + b | integer | | not null | +Indexes: + "pk" PRIMARY KEY, btree (a, b) +Check constraints: + "notnull_tbl3_a_check" CHECK (a IS NOT NULL) + +ALTER TABLE notnull_tbl3 DROP CONSTRAINT pk; +\d notnull_tbl3 + Table "public.notnull_tbl3" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | not null | + b | integer | | not null | +Check constraints: + "notnull_tbl3_a_check" CHECK (a IS NOT NULL) + +-- Primary keys cause not-null constraints to be created. +CREATE TABLE cnn_pk (a int, b int); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +ALTER TABLE cnn_pk ADD CONSTRAINT cnn_primarykey PRIMARY KEY (b); +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Indexes: + "cnn_primarykey" PRIMARY KEY, btree (b) +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +DROP TABLE cnn_pk, cnn_pk_child; +-- As above, but create the primary key ahead of time +CREATE TABLE cnn_pk (a int, b int, CONSTRAINT cnn_primarykey PRIMARY KEY (b)); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Indexes: + "cnn_primarykey" PRIMARY KEY, btree (b) +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +DROP TABLE cnn_pk, cnn_pk_child; +-- As above, but create the primary key using a UNIQUE index +CREATE TABLE cnn_pk (a int, b int); +CREATE UNIQUE INDEX cnn_uq ON cnn_pk (b); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +ALTER TABLE cnn_pk ADD CONSTRAINT cnn_primarykey PRIMARY KEY USING INDEX cnn_uq; +NOTICE: ALTER TABLE / ADD CONSTRAINT USING INDEX will rename index "cnn_uq" to "cnn_primarykey" +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Indexes: + "cnn_primarykey" PRIMARY KEY, btree (b) +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +DROP TABLE cnn_pk, cnn_pk_child; +-- Ensure partitions are scanned for null values when adding a PK +create table cnn2_parted(a int) partition by list (a); +create table cnn_part1 partition of cnn2_parted for values in (1, null); +insert into cnn_part1 values (null); +alter table cnn2_parted add primary key (a); +ERROR: column "a" of relation "cnn_part1" contains null values +drop table cnn2_parted; +-- columns in regular and LIKE inheritance should be marked not-nullable +-- for primary keys, even if those are deferred +CREATE TABLE notnull_tbl4 (a INTEGER PRIMARY KEY INITIALLY DEFERRED); +CREATE TABLE notnull_tbl4_lk (LIKE notnull_tbl4); +CREATE TABLE notnull_tbl4_lk2 (LIKE notnull_tbl4 INCLUDING INDEXES); +CREATE TABLE notnull_tbl4_lk3 (LIKE notnull_tbl4 INCLUDING INDEXES, CONSTRAINT a_nn NOT NULL a); +CREATE TABLE notnull_tbl4_cld () INHERITS (notnull_tbl4); +CREATE TABLE notnull_tbl4_cld2 (PRIMARY KEY (a) DEFERRABLE) INHERITS (notnull_tbl4); +CREATE TABLE notnull_tbl4_cld3 (PRIMARY KEY (a) DEFERRABLE, CONSTRAINT a_nn NOT NULL a) INHERITS (notnull_tbl4); +\d+ notnull_tbl4 + Table "public.notnull_tbl4" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_pkey" PRIMARY KEY, btree (a) DEFERRABLE INITIALLY DEFERRED +Child tables: notnull_tbl4_cld, + notnull_tbl4_cld2, + notnull_tbl4_cld3 + +\d+ notnull_tbl4_lk + Table "public.notnull_tbl4_lk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl4_a_not_null" NOT NULL "a" + +\d+ notnull_tbl4_lk2 + Table "public.notnull_tbl4_lk2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_lk2_pkey" PRIMARY KEY, btree (a) DEFERRABLE INITIALLY DEFERRED + +\d+ notnull_tbl4_lk3 + Table "public.notnull_tbl4_lk3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_lk3_pkey" PRIMARY KEY, btree (a) DEFERRABLE INITIALLY DEFERRED + +\d+ notnull_tbl4_cld + Table "public.notnull_tbl4_cld" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl4_a_not_null" NOT NULL "a" (inherited) +Inherits: notnull_tbl4 + +\d+ notnull_tbl4_cld2 + Table "public.notnull_tbl4_cld2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_cld2_pkey" PRIMARY KEY, btree (a) DEFERRABLE +Inherits: notnull_tbl4 + +\d+ notnull_tbl4_cld3 + Table "public.notnull_tbl4_cld3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_cld3_pkey" PRIMARY KEY, btree (a) DEFERRABLE +Inherits: notnull_tbl4 + +-- leave these tables around for pg_upgrade testing -- Comments -- Setup a low-level role to enforce non-superuser checks. CREATE ROLE regress_constraint_comments; diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out index 284a7fb85c..344d05233a 100644 --- a/src/test/regress/expected/create_table.out +++ b/src/test/regress/expected/create_table.out @@ -759,21 +759,23 @@ CREATE TABLE part_b PARTITION OF parted ( NOTICE: merging constraint "check_a" with inherited definition -- conislocal should be false for any merged constraints, true otherwise SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname; - conname | conislocal | coninhcount ----------+------------+------------- - check_a | f | 1 - check_b | t | 0 -(2 rows) + conname | conislocal | coninhcount +-------------------+------------+------------- + check_a | f | 1 + part_b_b_not_null | t | 1 + check_b | t | 0 +(3 rows) -- Once check_b is added to the parent, it should be made non-local for part_b ALTER TABLE parted ADD CONSTRAINT check_b CHECK (b >= 0); NOTICE: merging constraint "check_b" with inherited definition SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname; - conname | conislocal | coninhcount ----------+------------+------------- - check_a | f | 1 - check_b | f | 1 -(2 rows) + conname | conislocal | coninhcount +-------------------+------------+------------- + check_a | f | 1 + check_b | f | 1 + part_b_b_not_null | t | 1 +(3 rows) -- Neither check_a nor check_b are droppable from part_b ALTER TABLE part_b DROP CONSTRAINT check_a; @@ -785,9 +787,10 @@ ERROR: cannot drop inherited constraint "check_b" of relation "part_b" -- be local constraints. ALTER TABLE parted DROP CONSTRAINT check_a, DROP CONSTRAINT check_b; SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname; - conname | conislocal | coninhcount ----------+------------+------------- -(0 rows) + conname | conislocal | coninhcount +-------------------+------------+------------- + part_b_b_not_null | t | 1 +(1 row) -- specify PARTITION BY for a partition CREATE TABLE fail_part_col_not_found PARTITION OF parted FOR VALUES IN ('c') PARTITION BY RANGE (c); @@ -851,6 +854,8 @@ drop table test_part_coll_posix; b | integer | | not null | 1 | plain | | Partition of: parted FOR VALUES IN ('b') Partition constraint: ((a IS NOT NULL) AND (a = 'b'::text)) +Not-null constraints: + "part_b_b_not_null" NOT NULL "b" (local, inherited) -- Both partition bound and partition key in describe output \d+ part_c @@ -862,6 +867,8 @@ Partition constraint: ((a IS NOT NULL) AND (a = 'b'::text)) Partition of: parted FOR VALUES IN ('c') Partition constraint: ((a IS NOT NULL) AND (a = 'c'::text)) Partition key: RANGE (b) +Not-null constraints: + "part_c_b_not_null" NOT NULL "b" (local, inherited) Partitions: part_c_1_10 FOR VALUES FROM (1) TO (10) -- a level-2 partition's constraint will include the parent's expressions @@ -873,6 +880,8 @@ Partitions: part_c_1_10 FOR VALUES FROM (1) TO (10) b | integer | | not null | 0 | plain | | Partition of: part_c FOR VALUES FROM (1) TO (10) Partition constraint: ((a IS NOT NULL) AND (a = 'c'::text) AND (b IS NOT NULL) AND (b >= 1) AND (b < 10)) +Not-null constraints: + "part_c_b_not_null" NOT NULL "b" (inherited) -- Show partition count in the parent's describe output -- Tempted to include \d+ output listing partitions with bound info but diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out index 6bfc6d040f..7296b03cb4 100644 --- a/src/test/regress/expected/create_table_like.out +++ b/src/test/regress/expected/create_table_like.out @@ -348,6 +348,8 @@ CREATE TABLE ctlt12_storage (LIKE ctlt1 INCLUDING STORAGE, LIKE ctlt2 INCLUDING a | text | | not null | | main | | b | text | | | | extended | | c | text | | | | external | | +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" CREATE TABLE ctlt12_comments (LIKE ctlt1 INCLUDING COMMENTS, LIKE ctlt2 INCLUDING COMMENTS); \d+ ctlt12_comments @@ -357,6 +359,8 @@ CREATE TABLE ctlt12_comments (LIKE ctlt1 INCLUDING COMMENTS, LIKE ctlt2 INCLUDIN a | text | | not null | | extended | | A b | text | | | | extended | | B c | text | | | | extended | | C +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" CREATE TABLE ctlt1_inh (LIKE ctlt1 INCLUDING CONSTRAINTS INCLUDING COMMENTS) INHERITS (ctlt1); NOTICE: merging column "a" with inherited definition @@ -370,6 +374,8 @@ NOTICE: merging constraint "ctlt1_a_check" with inherited definition b | text | | | | extended | | B Check constraints: "ctlt1_a_check" CHECK (length(a) > 2) +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" (local, inherited) Inherits: ctlt1 SELECT description FROM pg_description, pg_constraint c WHERE classoid = 'pg_constraint'::regclass AND objoid = c.oid AND c.conrelid = 'ctlt1_inh'::regclass; @@ -391,6 +397,8 @@ Check constraints: "ctlt1_a_check" CHECK (length(a) > 2) "ctlt3_a_check" CHECK (length(a) < 5) "ctlt3_c_check" CHECK (length(c) < 7) +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" (inherited) Inherits: ctlt1, ctlt3 @@ -409,6 +417,8 @@ Check constraints: "ctlt1_a_check" CHECK (length(a) > 2) "ctlt3_a_check" CHECK (length(a) < 5) "ctlt3_c_check" CHECK (length(c) < 7) +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" (inherited) Inherits: ctlt1 SELECT description FROM pg_description, pg_constraint c WHERE classoid = 'pg_constraint'::regclass AND objoid = c.oid AND c.conrelid = 'ctlt13_like'::regclass; diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out index 6ed50fdcfa..cce49e509a 100644 --- a/src/test/regress/expected/foreign_data.out +++ b/src/test/regress/expected/foreign_data.out @@ -742,6 +742,8 @@ COMMENT ON COLUMN ft1.c1 IS 'ft1.c1'; Check constraints: "ft1_c2_check" CHECK (c2 <> ''::text) "ft1_c3_check" CHECK (c3 >= '01-01-1994'::date AND c3 <= '01-31-1994'::date) +Not-null constraints: + "ft1_c1_not_null" NOT NULL "c1" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -864,6 +866,9 @@ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET STORAGE PLAIN; Check constraints: "ft1_c2_check" CHECK (c2 <> ''::text) "ft1_c3_check" CHECK (c3 >= '01-01-1994'::date AND c3 <= '01-31-1994'::date) +Not-null constraints: + "ft1_c1_not_null" NOT NULL "c1" + "ft1_c6_not_null" NOT NULL "c6" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1409,6 +1414,8 @@ CREATE FOREIGN TABLE ft2 () INHERITS (fd_pt1) c1 | integer | | not null | | plain | | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1418,6 +1425,8 @@ Child tables: ft2, FOREIGN c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1430,6 +1439,8 @@ DROP FOREIGN TABLE ft2; c1 | integer | | not null | | plain | | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" CREATE FOREIGN TABLE ft2 ( c1 integer NOT NULL, @@ -1443,6 +1454,8 @@ CREATE FOREIGN TABLE ft2 ( c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1454,6 +1467,8 @@ ALTER FOREIGN TABLE ft2 INHERIT fd_pt1; c1 | integer | | not null | | plain | | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1463,6 +1478,8 @@ Child tables: ft2, FOREIGN c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1484,6 +1501,8 @@ NOTICE: merging column "c3" with inherited definition c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1497,6 +1516,8 @@ Child tables: ct3, c1 | integer | | not null | | plain | | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (inherited) Inherits: ft2 \d+ ft3 @@ -1506,6 +1527,8 @@ Inherits: ft2 c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft3_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 Inherits: ft2 @@ -1527,6 +1550,9 @@ ALTER TABLE fd_pt1 ADD COLUMN c8 integer; c6 | integer | | | | plain | | c7 | integer | | not null | | plain | | c8 | integer | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" + "fd_pt1_c7_not_null" NOT NULL "c7" Child tables: ft2, FOREIGN \d+ ft2 @@ -1541,6 +1567,9 @@ Child tables: ft2, FOREIGN c6 | integer | | | | | plain | | c7 | integer | | not null | | | plain | | c8 | integer | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) + "fd_pt1_c7_not_null" NOT NULL "c7" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1559,6 +1588,9 @@ Child tables: ct3, c6 | integer | | | | plain | | c7 | integer | | not null | | plain | | c8 | integer | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (inherited) + "fd_pt1_c7_not_null" NOT NULL "c7" (inherited) Inherits: ft2 \d+ ft3 @@ -1573,6 +1605,9 @@ Inherits: ft2 c6 | integer | | | | | plain | | c7 | integer | | not null | | | plain | | c8 | integer | | | | | plain | | +Not-null constraints: + "ft3_c1_not_null" NOT NULL "c1" (local, inherited) + "fd_pt1_c7_not_null" NOT NULL "c7" (inherited) Server: s0 Inherits: ft2 @@ -1601,6 +1636,9 @@ ALTER TABLE fd_pt1 ALTER COLUMN c8 SET STORAGE EXTERNAL; c6 | integer | | not null | | plain | | c7 | integer | | | | plain | | c8 | text | | | | external | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" + "fd_pt1_c6_not_null" NOT NULL "c6" Child tables: ft2, FOREIGN \d+ ft2 @@ -1615,6 +1653,9 @@ Child tables: ft2, FOREIGN c6 | integer | | not null | | | plain | | c7 | integer | | | | | plain | | c8 | text | | | | | external | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) + "fd_pt1_c6_not_null" NOT NULL "c6" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1634,6 +1675,8 @@ ALTER TABLE fd_pt1 DROP COLUMN c8; c1 | integer | | not null | | plain | 10000 | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1643,6 +1686,8 @@ Child tables: ft2, FOREIGN c1 | integer | | not null | | | plain | 10000 | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1657,11 +1702,12 @@ SELECT relname, conname, contype, conislocal, coninhcount, connoinherit FROM pg_class AS pc JOIN pg_constraint AS pgc ON (conrelid = pc.oid) WHERE pc.relname = 'fd_pt1' ORDER BY 1,2; - relname | conname | contype | conislocal | coninhcount | connoinherit ----------+------------+---------+------------+-------------+-------------- - fd_pt1 | fd_pt1chk1 | c | t | 0 | t - fd_pt1 | fd_pt1chk2 | c | t | 0 | f -(2 rows) + relname | conname | contype | conislocal | coninhcount | connoinherit +---------+--------------------+---------+------------+-------------+-------------- + fd_pt1 | fd_pt1_c1_not_null | n | t | 0 | f + fd_pt1 | fd_pt1chk1 | c | t | 0 | t + fd_pt1 | fd_pt1chk2 | c | t | 0 | f +(3 rows) -- child does not inherit NO INHERIT constraints \d+ fd_pt1 @@ -1674,6 +1720,8 @@ SELECT relname, conname, contype, conislocal, coninhcount, connoinherit Check constraints: "fd_pt1chk1" CHECK (c1 > 0) NO INHERIT "fd_pt1chk2" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1685,6 +1733,8 @@ Child tables: ft2, FOREIGN c3 | date | | | | | plain | | Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1721,6 +1771,8 @@ ALTER FOREIGN TABLE ft2 INHERIT fd_pt1; Check constraints: "fd_pt1chk1" CHECK (c1 > 0) NO INHERIT "fd_pt1chk2" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1732,6 +1784,8 @@ Child tables: ft2, FOREIGN c3 | date | | | | | plain | | Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1751,6 +1805,8 @@ ALTER TABLE fd_pt1 ADD CONSTRAINT fd_pt1chk3 CHECK (c2 <> '') NOT VALID; c3 | date | | | | plain | | Check constraints: "fd_pt1chk3" CHECK (c2 <> ''::text) NOT VALID +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1763,6 +1819,8 @@ Child tables: ft2, FOREIGN Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) "fd_pt1chk3" CHECK (c2 <> ''::text) NOT VALID +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1778,6 +1836,8 @@ ALTER TABLE fd_pt1 VALIDATE CONSTRAINT fd_pt1chk3; c3 | date | | | | plain | | Check constraints: "fd_pt1chk3" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1790,6 +1850,8 @@ Child tables: ft2, FOREIGN Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) "fd_pt1chk3" CHECK (c2 <> ''::text) +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1809,6 +1871,8 @@ ALTER TABLE fd_pt1 RENAME CONSTRAINT fd_pt1chk3 TO f2_check; f3 | date | | | | plain | | Check constraints: "f2_check" CHECK (f2 <> ''::text) +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "f1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1821,6 +1885,8 @@ Child tables: ft2, FOREIGN Check constraints: "f2_check" CHECK (f2 <> ''::text) "fd_pt1chk2" CHECK (f2 <> ''::text) +Not-null constraints: + "ft2_c1_not_null" NOT NULL "f1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1867,6 +1933,8 @@ CREATE FOREIGN TABLE fd_pt2_1 PARTITION OF fd_pt2 FOR VALUES IN (1) c2 | text | | | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN \d+ fd_pt2_1 @@ -1878,6 +1946,8 @@ Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN c3 | date | | | | | plain | | Partition of: fd_pt2 FOR VALUES IN (1) Partition constraint: ((c1 IS NOT NULL) AND (c1 = 1)) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1897,6 +1967,8 @@ CREATE FOREIGN TABLE fd_pt2_1 ( c2 | text | | | | | extended | | c3 | date | | | | | plain | | c4 | character(1) | | | | | extended | | +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1912,6 +1984,8 @@ DROP FOREIGN TABLE fd_pt2_1; c2 | text | | | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" Number of partitions: 0 CREATE FOREIGN TABLE fd_pt2_1 ( @@ -1926,6 +2000,8 @@ CREATE FOREIGN TABLE fd_pt2_1 ( c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1939,6 +2015,8 @@ ALTER TABLE fd_pt2 ATTACH PARTITION fd_pt2_1 FOR VALUES IN (1); c2 | text | | | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN \d+ fd_pt2_1 @@ -1950,6 +2028,8 @@ Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN c3 | date | | | | | plain | | Partition of: fd_pt2 FOR VALUES IN (1) Partition constraint: ((c1 IS NOT NULL) AND (c1 = 1)) +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1967,6 +2047,8 @@ ALTER TABLE fd_pt2_1 ADD CONSTRAINT p21chk CHECK (c2 <> ''); c2 | text | | | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN \d+ fd_pt2_1 @@ -1980,6 +2062,9 @@ Partition of: fd_pt2 FOR VALUES IN (1) Partition constraint: ((c1 IS NOT NULL) AND (c1 = 1)) Check constraints: "p21chk" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" (inherited) + "fd_pt2_1_c3_not_null" NOT NULL "c3" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1997,6 +2082,9 @@ ALTER TABLE fd_pt2 ALTER c2 SET NOT NULL; c2 | text | | not null | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" + "fd_pt2_c2_not_null" NOT NULL "c2" Number of partitions: 0 \d+ fd_pt2_1 @@ -2008,11 +2096,14 @@ Number of partitions: 0 c3 | date | | not null | | | plain | | Check constraints: "p21chk" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" + "fd_pt2_1_c3_not_null" NOT NULL "c3" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') ALTER TABLE fd_pt2 ATTACH PARTITION fd_pt2_1 FOR VALUES IN (1); -- ERROR -ERROR: column "c2" in child table must be marked NOT NULL +ERROR: column "c2" in child table "fd_pt2_1" must be marked NOT NULL ALTER FOREIGN TABLE fd_pt2_1 ALTER c2 SET NOT NULL; ALTER TABLE fd_pt2 ATTACH PARTITION fd_pt2_1 FOR VALUES IN (1); ALTER TABLE fd_pt2 DETACH PARTITION fd_pt2_1; @@ -2027,6 +2118,9 @@ ALTER TABLE fd_pt2 ADD CONSTRAINT fd_pt2chk1 CHECK (c1 > 0); Partition key: LIST (c1) Check constraints: "fd_pt2chk1" CHECK (c1 > 0) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" + "fd_pt2_c2_not_null" NOT NULL "c2" Number of partitions: 0 \d+ fd_pt2_1 @@ -2038,6 +2132,10 @@ Number of partitions: 0 c3 | date | | not null | | | plain | | Check constraints: "p21chk" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" + "fd_pt2_1_c2_not_null" NOT NULL "c2" + "fd_pt2_1_c3_not_null" NOT NULL "c3" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 8c04a24b37..17c84e0cfb 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -2053,13 +2053,19 @@ ORDER BY co.contype, cr.relname, co.conname, p.conname; part33_self_fk | parted_self_fk_id_abc_fkey | f | t | parted_self_fk_id_abc_fkey | t | parted_self_fk part3_self_fk | parted_self_fk_id_abc_fkey | f | t | parted_self_fk_id_abc_fkey | t | parted_self_fk parted_self_fk | parted_self_fk_id_abc_fkey | f | t | | | parted_self_fk + part1_self_fk | part1_self_fk_id_not_null | n | t | | | + part2_self_fk | parted_self_fk_id_not_null | n | t | | | + part32_self_fk | part3_self_fk_id_not_null | n | t | | | + part33_self_fk | part33_self_fk_id_not_null | n | t | | | + part3_self_fk | part3_self_fk_id_not_null | n | t | | | + parted_self_fk | parted_self_fk_id_not_null | n | t | | | part1_self_fk | part1_self_fk_pkey | p | t | parted_self_fk_pkey | t | part2_self_fk | part2_self_fk_pkey | p | t | parted_self_fk_pkey | t | part32_self_fk | part32_self_fk_pkey | p | t | part3_self_fk_pkey | t | part33_self_fk | part33_self_fk_pkey | p | t | part3_self_fk_pkey | t | part3_self_fk | part3_self_fk_pkey | p | t | parted_self_fk_pkey | t | parted_self_fk | parted_self_fk_pkey | p | t | | | -(12 rows) +(18 rows) -- detach and re-attach multiple times just to ensure everything is kosher ALTER TABLE parted_self_fk DETACH PARTITION part2_self_fk; @@ -2082,13 +2088,19 @@ ORDER BY co.contype, cr.relname, co.conname, p.conname; part33_self_fk | parted_self_fk_id_abc_fkey | f | t | parted_self_fk_id_abc_fkey | t | parted_self_fk part3_self_fk | parted_self_fk_id_abc_fkey | f | t | parted_self_fk_id_abc_fkey | t | parted_self_fk parted_self_fk | parted_self_fk_id_abc_fkey | f | t | | | parted_self_fk + part1_self_fk | part1_self_fk_id_not_null | n | t | | | + part2_self_fk | parted_self_fk_id_not_null | n | t | | | + part32_self_fk | part3_self_fk_id_not_null | n | t | | | + part33_self_fk | part33_self_fk_id_not_null | n | t | | | + part3_self_fk | part3_self_fk_id_not_null | n | t | | | + parted_self_fk | parted_self_fk_id_not_null | n | t | | | part1_self_fk | part1_self_fk_pkey | p | t | parted_self_fk_pkey | t | part2_self_fk | part2_self_fk_pkey | p | t | parted_self_fk_pkey | t | part32_self_fk | part32_self_fk_pkey | p | t | part3_self_fk_pkey | t | part33_self_fk | part33_self_fk_pkey | p | t | part3_self_fk_pkey | t | part3_self_fk | part3_self_fk_pkey | p | t | parted_self_fk_pkey | t | parted_self_fk | parted_self_fk_pkey | p | t | | | -(12 rows) +(18 rows) -- Leave this table around, for pg_upgrade/pg_dump tests -- Test creating a constraint at the parent that already exists in partitions. diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_stored.out index 8ea8a3a92d..0d037d48ca 100644 --- a/src/test/regress/expected/generated_stored.out +++ b/src/test/regress/expected/generated_stored.out @@ -321,6 +321,8 @@ NOTICE: merging column "b" with inherited definition a | integer | | not null | | plain | | b | integer | | | generated always as (a * 22) stored | plain | | x | integer | | | | plain | | +Not-null constraints: + "gtest1_a_not_null" NOT NULL "a" (inherited) Inherits: gtest1 CREATE TABLE gtestxx_1 (a int NOT NULL, b int); diff --git a/src/test/regress/expected/identity.out b/src/test/regress/expected/identity.out index f14bfccfb1..2a2b777c89 100644 --- a/src/test/regress/expected/identity.out +++ b/src/test/regress/expected/identity.out @@ -578,6 +578,10 @@ TABLE itest8; f3 | integer | | not null | generated by default as identity | plain | | f4 | bigint | | not null | generated always as identity | plain | | f5 | bigint | | | | plain | | +Not-null constraints: + "itest8_f2_not_null" NOT NULL "f2" + "itest8_f3_not_null" NOT NULL "f3" + "itest8_f4_not_null" NOT NULL "f4" \d itest8_f2_seq Sequence "public.itest8_f2_seq" @@ -618,7 +622,7 @@ INSERT into pitest1_p1 (f1, f2) VALUES ('2016-07-3', 'from pitest1_p1'); CREATE TABLE pitest1_p2 (f3 bigint, f2 text, f1 date NOT NULL); INSERT INTO pitest1_p2 (f1, f2, f3) VALUES ('2016-08-2', 'before attaching', 100); ALTER TABLE pitest1 ATTACH PARTITION pitest1_p2 FOR VALUES FROM ('2016-08-01') TO ('2016-09-01'); -- requires NOT NULL constraint -ERROR: column "f3" in child table must be marked NOT NULL +ERROR: column "f3" in child table "pitest1_p2" must be marked NOT NULL ALTER TABLE pitest1_p2 ALTER COLUMN f3 SET NOT NULL; ALTER TABLE pitest1 ATTACH PARTITION pitest1_p2 FOR VALUES FROM ('2016-08-01') TO ('2016-09-01'); INSERT INTO pitest1_p2 (f1, f2) VALUES ('2016-08-3', 'from pitest1_p2'); diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out index ea8b2454bf..4e8fe49c8c 100644 --- a/src/test/regress/expected/index_including.out +++ b/src/test/regress/expected/index_including.out @@ -113,7 +113,7 @@ SELECT indexrelid::regclass, indnatts, indnkeyatts, indisunique, indisprimary, i covering | 4 | 2 | t | t | 1 2 3 4 | 1978 1978 (1 row) -SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid AND contype = 'p'; pg_get_constraintdef | conname | conkey ---------------------------------------+----------+-------- PRIMARY KEY (c1, c2) INCLUDE (c3, c4) | covering | {1,2} @@ -191,7 +191,7 @@ SELECT indexrelid::regclass, indnatts, indnkeyatts, indisunique, indisprimary, i tbl_pkey | 4 | 2 | t | t | 1 2 3 4 | 1978 1978 (1 row) -SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid AND contype = 'p'; pg_get_constraintdef | conname | conkey ---------------------------------------+----------+-------- PRIMARY KEY (c1, c2) INCLUDE (c3, c4) | tbl_pkey | {1,2} diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out index f25723da92..c99db4e700 100644 --- a/src/test/regress/expected/indexing.out +++ b/src/test/regress/expected/indexing.out @@ -1117,15 +1117,27 @@ alter table idxpart attach partition idxpart3 for values from (20, 20) to (30, 3 select conname, contype, conrelid::regclass, conindid::regclass, conkey from pg_constraint where conrelid::regclass::text like 'idxpart%' order by conrelid::regclass::text, conname; - conname | contype | conrelid | conindid | conkey -----------------+---------+-----------+----------------+-------- - idxpart_pkey | p | idxpart | idxpart_pkey | {1,2} - idxpart1_pkey | p | idxpart1 | idxpart1_pkey | {1,2} - idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2} - idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2} - idxpart22_pkey | p | idxpart22 | idxpart22_pkey | {1,2} - idxpart3_pkey | p | idxpart3 | idxpart3_pkey | {2,1} -(6 rows) + conname | contype | conrelid | conindid | conkey +---------------------+---------+-----------+----------------+-------- + idxpart_a_not_null | n | idxpart | - | {1} + idxpart_b_not_null | n | idxpart | - | {2} + idxpart_pkey | p | idxpart | idxpart_pkey | {1,2} + idxpart1_pkey | p | idxpart1 | idxpart1_pkey | {1,2} + idxpart_a_not_null | n | idxpart1 | - | {1} + idxpart_b_not_null | n | idxpart1 | - | {2} + idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2} + idxpart_a_not_null | n | idxpart2 | - | {1} + idxpart_b_not_null | n | idxpart2 | - | {2} + idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2} + idxpart_a_not_null | n | idxpart21 | - | {1} + idxpart_b_not_null | n | idxpart21 | - | {2} + idxpart22_pkey | p | idxpart22 | idxpart22_pkey | {1,2} + idxpart_a_not_null | n | idxpart22 | - | {1} + idxpart_b_not_null | n | idxpart22 | - | {2} + idxpart3_a_not_null | n | idxpart3 | - | {2} + idxpart3_b_not_null | n | idxpart3 | - | {1} + idxpart3_pkey | p | idxpart3 | idxpart3_pkey | {2,1} +(18 rows) drop table idxpart; -- Verify that multi-layer partitioning honors the requirement that all @@ -1150,13 +1162,19 @@ create table idxpart2 partition of idxpart for values from (0) to (1000) partiti create table idxpart21 partition of idxpart2 for values from (0) to (1000); select conname, contype, conrelid::regclass, conindid::regclass, conkey from pg_constraint where conrelid::regclass::text like 'idxpart%' - order by conname; - conname | contype | conrelid | conindid | conkey -----------------+---------+-----------+----------------+-------- - idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2} - idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2} - idxpart_pkey | p | idxpart | idxpart_pkey | {1,2} -(3 rows) + order by conrelid::regclass::text, conname; + conname | contype | conrelid | conindid | conkey +--------------------+---------+-----------+----------------+-------- + idxpart_a_not_null | n | idxpart | - | {1} + idxpart_b_not_null | n | idxpart | - | {2} + idxpart_pkey | p | idxpart | idxpart_pkey | {1,2} + idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2} + idxpart_a_not_null | n | idxpart2 | - | {1} + idxpart_b_not_null | n | idxpart2 | - | {2} + idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2} + idxpart_a_not_null | n | idxpart21 | - | {1} + idxpart_b_not_null | n | idxpart21 | - | {2} +(9 rows) drop table idxpart; -- If a partitioned table has a unique/PK constraint, then it's not possible @@ -1259,14 +1277,10 @@ create table idxpart0 (like idxpart); alter table idxpart0 add unique (a); alter table idxpart attach partition idxpart0 default; alter table only idxpart add primary key (a); -- fail, no not-null constraint -ERROR: constraint must be added to child tables too -DETAIL: Column "a" of relation "idxpart0" is not already NOT NULL. -HINT: Do not specify the ONLY keyword. +ERROR: column a of table idxpart0 is not marked not null alter table idxpart0 alter column a set not null; alter table only idxpart add primary key (a); -- now it works alter index idxpart_pkey attach partition idxpart0_a_key; -alter table idxpart0 alter column a drop not null; -- fail, pkey needs it -ERROR: column "a" is marked NOT NULL in parent table drop table idxpart; -- if a partition has a unique index without a constraint, does not attach -- automatically; creates a new index instead. diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out index dbb748a2d2..4829eecefa 100644 --- a/src/test/regress/expected/inherit.out +++ b/src/test/regress/expected/inherit.out @@ -2025,6 +2025,575 @@ select * from cnullparent where f1 = 2; drop table cnullparent cascade; NOTICE: drop cascades to table cnullchild -- +-- Test inheritance of NOT NULL constraints +-- +create table pp1 (f1 int); +create table cc1 (f2 text, f3 int) inherits (pp1); +create table cc2 (f4 float) inherits (pp1,cc1); +NOTICE: merging multiple inherited definitions of column "f1" +create table cc3 () inherits (pp1,cc1,cc2); +NOTICE: merging multiple inherited definitions of column "f1" +NOTICE: merging multiple inherited definitions of column "f1" +NOTICE: merging multiple inherited definitions of column "f2" +NOTICE: merging multiple inherited definitions of column "f3" +alter table pp1 alter f1 set not null; +\d+ cc3 + Table "public.cc3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) +Inherits: pp1, + cc1, + cc2 + +alter table cc3 no inherit pp1; +alter table cc3 no inherit cc1; +alter table cc3 no inherit cc2; +\d+ cc3 + Table "public.cc3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" + +drop table cc3; +-- named NOT NULL constraint +alter table cc1 add column a2 int constraint nn not null; +\d+ cc1 + Table "public.cc1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + a2 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) + "nn" NOT NULL "a2" +Inherits: pp1 +Child tables: cc2 + +\d+ cc2 + Table "public.cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | + a2 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) + "nn" NOT NULL "a2" (inherited) +Inherits: pp1, + cc1 + +alter table pp1 alter column f1 set not null; +\d+ pp1 + Table "public.pp1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" +Child tables: cc1, + cc2 + +\d+ cc1 + Table "public.cc1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + a2 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) + "nn" NOT NULL "a2" +Inherits: pp1 +Child tables: cc2 + +\d+ cc2 + Table "public.cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | + a2 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) + "nn" NOT NULL "a2" (inherited) +Inherits: pp1, + cc1 + +-- cannot create table with inconsistent NO INHERIT constraint +create table cc3 (a2 int not null no inherit) inherits (cc1); +NOTICE: moving and merging column "a2" with inherited definition +DETAIL: User-specified column moved to the position of the inherited column. +ERROR: cannot define not-null constraint on column "a2" with NO INHERIT +DETAIL: The column has an inherited not-null constraint. +-- change NO INHERIT status of inherited constraint: no dice, it's inherited +alter table cc2 add not null a2 no inherit; +ERROR: cannot change NO INHERIT status of NOT NULL constraint "nn" on relation "cc2" +-- remove constraint from cc2: no dice, it's inherited +alter table cc2 alter column a2 drop not null; +ERROR: cannot drop inherited constraint "nn" of relation "cc2" +-- remove constraint from cc1, should succeed +alter table cc1 alter column a2 drop not null; +\d+ cc1 + Table "public.cc1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + a2 | integer | | | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) +Inherits: pp1 +Child tables: cc2 + +-- same for cc2 +alter table cc2 alter column f1 drop not null; +ERROR: cannot drop inherited constraint "pp1_f1_not_null" of relation "cc2" +\d+ cc2 + Table "public.cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | + a2 | integer | | | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) +Inherits: pp1, + cc1 + +-- remove from cc1, should fail again +alter table cc1 alter column f1 drop not null; +ERROR: cannot drop inherited constraint "pp1_f1_not_null" of relation "cc1" +-- remove from pp1, should succeed +alter table pp1 alter column f1 drop not null; +\d+ pp1 + Table "public.pp1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | | | plain | | +Child tables: cc1, + cc2 + +alter table pp1 add primary key (f1); +-- Leave these tables around, for pg_upgrade testing +-- test that inhcount is updated correctly through multiple inheritance +create table inh_pp1 (f1 int); +create table inh_cc1 (f2 text, f3 int) inherits (inh_pp1); +create table inh_cc2(f4 float) inherits(inh_pp1,inh_cc1); +NOTICE: merging multiple inherited definitions of column "f1" +alter table inh_pp1 alter column f1 set not null; +alter table inh_cc2 no inherit inh_pp1; +alter table inh_cc2 no inherit inh_cc1; +\d+ inh_cc2 + Table "public.inh_cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | +Not-null constraints: + "inh_pp1_f1_not_null" NOT NULL "f1" + +drop table inh_pp1, inh_cc1, inh_cc2; +create table inh_pp1 (f1 int not null); +create table inh_cc1 (f2 text, f3 int) inherits (inh_pp1); +create table inh_cc2(f4 float) inherits(inh_pp1,inh_cc1); +NOTICE: merging multiple inherited definitions of column "f1" +alter table inh_pp1 alter column f1 drop not null; +\d+ inh_cc2 + Table "public.inh_cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | +Inherits: inh_pp1, + inh_cc1 + +drop table inh_pp1, inh_cc1, inh_cc2; +-- Test a not-null addition that must walk down the hierarchy +CREATE TABLE inh_parent (); +CREATE TABLE inh_child (i int) INHERITS (inh_parent); +CREATE TABLE inh_grandchild () INHERITS (inh_parent, inh_child); +ALTER TABLE inh_parent ADD COLUMN i int NOT NULL; +NOTICE: merging definition of column "i" for child "inh_child" +NOTICE: merging definition of column "i" for child "inh_grandchild" +drop table inh_parent, inh_child, inh_grandchild; +-- Test the same constraint name for different columns in different parents +create table inh_parent1(a int constraint nn not null); +create table inh_parent2(b int constraint nn not null); +create table inh_child () inherits (inh_parent1, inh_parent2); +\d+ inh_child + Table "public.inh_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "nn" NOT NULL "a" (inherited) + "inh_child_b_not_null" NOT NULL "b" (inherited) +Inherits: inh_parent1, + inh_parent2 + +drop table inh_parent1, inh_parent2, inh_child; +-- Test multiple parents with overlapping primary keys +create table inh_parent1(a int, b int, c int, primary key (a, b)); +create table inh_parent2(d int, e int, b int, primary key (d, b)); +create table inh_child() inherits (inh_parent1, inh_parent2); +NOTICE: merging multiple inherited definitions of column "b" +select conrelid::regclass, conname, contype, conkey, + coninhcount, conislocal, connoinherit + from pg_constraint where contype in ('n','p') and + conrelid::regclass::text in ('inh_child', 'inh_parent1', 'inh_parent2') + order by 1, 2; + conrelid | conname | contype | conkey | coninhcount | conislocal | connoinherit +-------------+------------------------+---------+--------+-------------+------------+-------------- + inh_parent1 | inh_parent1_a_not_null | n | {1} | 0 | t | f + inh_parent1 | inh_parent1_b_not_null | n | {2} | 0 | t | f + inh_parent1 | inh_parent1_pkey | p | {1,2} | 0 | t | t + inh_parent2 | inh_parent2_b_not_null | n | {3} | 0 | t | f + inh_parent2 | inh_parent2_d_not_null | n | {1} | 0 | t | f + inh_parent2 | inh_parent2_pkey | p | {1,3} | 0 | t | t + inh_child | inh_parent1_a_not_null | n | {1} | 1 | f | f + inh_child | inh_parent1_b_not_null | n | {2} | 2 | f | f + inh_child | inh_parent2_d_not_null | n | {4} | 1 | f | f +(9 rows) + +\d+ inh_child + Table "public.inh_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | + b | integer | | not null | | plain | | + c | integer | | | | plain | | + d | integer | | not null | | plain | | + e | integer | | | | plain | | +Not-null constraints: + "inh_parent1_a_not_null" NOT NULL "a" (inherited) + "inh_parent1_b_not_null" NOT NULL "b" (inherited) + "inh_parent2_d_not_null" NOT NULL "d" (inherited) +Inherits: inh_parent1, + inh_parent2 + +drop table inh_parent1, inh_parent2, inh_child; +-- NOT NULL NO INHERIT +create table inh_nn_parent(a int); +create table inh_nn_child() inherits (inh_nn_parent); +alter table inh_nn_parent add not null a no inherit; +create table inh_nn_child2() inherits (inh_nn_parent); +select conrelid::regclass, conname, contype, conkey, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal, connoinherit + from pg_constraint where contype = 'n' and + conrelid::regclass::text like 'inh\_nn\_%' + order by 2, 1; + conrelid | conname | contype | conkey | attname | coninhcount | conislocal | connoinherit +---------------+--------------------------+---------+--------+---------+-------------+------------+-------------- + inh_nn_parent | inh_nn_parent_a_not_null | n | {1} | a | 0 | t | t +(1 row) + +\d+ inh_nn* + Table "public.inh_nn_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: inh_nn_parent + + Table "public.inh_nn_child2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: inh_nn_parent + + Table "public.inh_nn_parent" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "inh_nn_parent_a_not_null" NOT NULL "a" NO INHERIT +Child tables: inh_nn_child, + inh_nn_child2 + +drop table inh_nn_parent, inh_nn_child, inh_nn_child2; +CREATE TABLE inh_nn_parent (a int, NOT NULL a NO INHERIT); +CREATE TABLE inh_nn_child() INHERITS (inh_nn_parent); +ALTER TABLE inh_nn_parent ADD CONSTRAINT nna NOT NULL a; +ERROR: cannot change NO INHERIT status of NOT NULL constraint "inh_nn_parent_a_not_null" on relation "inh_nn_parent" +ALTER TABLE inh_nn_parent ALTER a SET NOT NULL; +ERROR: cannot change NO INHERIT status of NOT NULL constraint "inh_nn_parent_a_not_null" on relation "inh_nn_parent" +DROP TABLE inh_nn_parent cascade; +NOTICE: drop cascades to table inh_nn_child +-- Adding a PK at the top level of a hierarchy should cause all descendants +-- to be checked for nulls, even past a no-inherit constraint +CREATE TABLE inh_nn_lvl1 (a int); +CREATE TABLE inh_nn_lvl2 () INHERITS (inh_nn_lvl1); +CREATE TABLE inh_nn_lvl3 (CONSTRAINT foo NOT NULL a NO INHERIT) INHERITS (inh_nn_lvl2); +CREATE TABLE inh_nn_lvl4 () INHERITS (inh_nn_lvl3); +CREATE TABLE inh_nn_lvl5 () INHERITS (inh_nn_lvl4); +INSERT INTO inh_nn_lvl2 VALUES (NULL); +ALTER TABLE inh_nn_lvl1 ADD PRIMARY KEY (a); +ERROR: cannot change NO INHERIT status of NOT NULL constraint "foo" on relation "inh_nn_lvl3" +DELETE FROM inh_nn_lvl2; +INSERT INTO inh_nn_lvl5 VALUES (NULL); +ALTER TABLE inh_nn_lvl1 ADD PRIMARY KEY (a); +ERROR: cannot change NO INHERIT status of NOT NULL constraint "foo" on relation "inh_nn_lvl3" +DROP TABLE inh_nn_lvl1 CASCADE; +NOTICE: drop cascades to 4 other objects +DETAIL: drop cascades to table inh_nn_lvl2 +drop cascades to table inh_nn_lvl3 +drop cascades to table inh_nn_lvl4 +drop cascades to table inh_nn_lvl5 +-- +-- test inherit/deinherit +-- +create table inh_parent(f1 int); +create table inh_child1(f1 int not null); +create table inh_child2(f1 int); +-- inh_child1 should have not null constraint +alter table inh_child1 inherit inh_parent; +-- should fail, missing NOT NULL constraint +alter table inh_child2 inherit inh_child1; +ERROR: column "f1" in child table "inh_child2" must be marked NOT NULL +alter table inh_child2 alter column f1 set not null; +alter table inh_child2 inherit inh_child1; +-- add NOT NULL constraint recursively +alter table inh_parent alter column f1 set not null; +\d+ inh_parent + Table "public.inh_parent" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_parent_f1_not_null" NOT NULL "f1" +Child tables: inh_child1 + +\d+ inh_child1 + Table "public.inh_child1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_child1_f1_not_null" NOT NULL "f1" (local, inherited) +Inherits: inh_parent +Child tables: inh_child2 + +\d+ inh_child2 + Table "public.inh_child2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_child2_f1_not_null" NOT NULL "f1" (local, inherited) +Inherits: inh_child1 + +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass) + order by 2, 1; + conrelid | conname | contype | coninhcount | conislocal +------------+------------------------+---------+-------------+------------ + inh_child1 | inh_child1_f1_not_null | n | 1 | t + inh_child2 | inh_child2_f1_not_null | n | 1 | t + inh_parent | inh_parent_f1_not_null | n | 0 | t +(3 rows) + +-- +-- test deinherit procedure +-- +-- deinherit inh_child1 +create table inh_child3 () inherits (inh_child1); +alter table inh_child1 no inherit inh_parent; +\d+ inh_parent + Table "public.inh_parent" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_parent_f1_not_null" NOT NULL "f1" + +\d+ inh_child1 + Table "public.inh_child1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_child1_f1_not_null" NOT NULL "f1" +Child tables: inh_child2, + inh_child3 + +\d+ inh_child2 + Table "public.inh_child2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_child2_f1_not_null" NOT NULL "f1" (local, inherited) +Inherits: inh_child1 + +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass::text in ('inh_parent', 'inh_child1', 'inh_child2', 'inh_child3') + order by 2, 1; + conrelid | conname | contype | coninhcount | conislocal +------------+------------------------+---------+-------------+------------ + inh_child1 | inh_child1_f1_not_null | n | 0 | t + inh_child3 | inh_child1_f1_not_null | n | 1 | f + inh_child2 | inh_child2_f1_not_null | n | 1 | t + inh_parent | inh_parent_f1_not_null | n | 0 | t +(4 rows) + +drop table inh_parent, inh_child1, inh_child2, inh_child3; +-- a PK in parent must have a not-null in child that it can mark inherited +create table inh_parent (a int primary key); +create table inh_child (a int primary key); +alter table inh_child inherit inh_parent; -- nope +alter table inh_child alter a set not null; +alter table inh_child inherit inh_parent; -- now it works +ERROR: relation "inh_parent" would be inherited from more than once +-- don't interfere with other types of constraints +alter table inh_parent add constraint inh_parent_excl exclude ((1) with =); +alter table inh_parent add constraint inh_parent_uq unique (a); +alter table inh_parent add constraint inh_parent_fk foreign key (a) references inh_parent (a); +create table inh_child2 () inherits (inh_parent); +create table inh_child3 (like inh_parent); +alter table inh_child3 inherit inh_parent; +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint + where conrelid::regclass::text in ('inh_parent', 'inh_child', 'inh_child2', 'inh_child3') + order by 2, 1; + conrelid | conname | contype | coninhcount | conislocal +------------+-----------------------+---------+-------------+------------ + inh_child | inh_child_a_not_null | n | 1 | t + inh_child | inh_child_pkey | p | 0 | t + inh_parent | inh_parent_a_not_null | n | 0 | t + inh_child2 | inh_parent_a_not_null | n | 1 | f + inh_child3 | inh_parent_a_not_null | n | 1 | t + inh_parent | inh_parent_excl | x | 0 | t + inh_parent | inh_parent_fk | f | 0 | t + inh_parent | inh_parent_pkey | p | 0 | t + inh_parent | inh_parent_uq | u | 0 | t +(9 rows) + +drop table inh_parent, inh_child, inh_child2, inh_child3; +-- +-- test multi inheritance tree +-- +create table inh_parent(f1 int not null); +create table inh_child1() inherits(inh_parent); +create table inh_child2() inherits(inh_parent); +create table inh_child3() inherits(inh_child1, inh_child2); +NOTICE: merging multiple inherited definitions of column "f1" +-- show constraint info +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass, 'inh_child3'::regclass) + order by 2, conrelid::regclass::text; + conrelid | conname | contype | coninhcount | conislocal +------------+------------------------+---------+-------------+------------ + inh_child1 | inh_parent_f1_not_null | n | 1 | f + inh_child2 | inh_parent_f1_not_null | n | 1 | f + inh_child3 | inh_parent_f1_not_null | n | 2 | f + inh_parent | inh_parent_f1_not_null | n | 0 | t +(4 rows) + +drop table inh_parent cascade; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table inh_child1 +drop cascades to table inh_child2 +drop cascades to table inh_child3 +-- test child table with inherited columns and +-- with explicitly specified not null constraints +create table inh_parent_1(f1 int); +create table inh_parent_2(f2 text); +create table inh_child(f1 int not null, f2 text not null) inherits(inh_parent_1, inh_parent_2); +NOTICE: merging column "f1" with inherited definition +NOTICE: merging column "f2" with inherited definition +-- show constraint info +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent_1'::regclass, 'inh_parent_2'::regclass, 'inh_child'::regclass) + order by 2, conrelid::regclass::text; + conrelid | conname | contype | coninhcount | conislocal +-----------+-----------------------+---------+-------------+------------ + inh_child | inh_child_f1_not_null | n | 0 | t + inh_child | inh_child_f2_not_null | n | 0 | t +(2 rows) + +-- also drops inh_child table +drop table inh_parent_1 cascade; +NOTICE: drop cascades to table inh_child +drop table inh_parent_2; +-- test multi layer inheritance tree +create table inh_p1(f1 int not null); +create table inh_p2(f1 int not null); +create table inh_p3(f2 int); +create table inh_p4(f1 int not null, f3 text not null); +create table inh_multiparent() inherits(inh_p1, inh_p2, inh_p3, inh_p4); +NOTICE: merging multiple inherited definitions of column "f1" +NOTICE: merging multiple inherited definitions of column "f1" +-- constraint on f1 should have three parents +select conrelid::regclass, contype, conname, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass in ('inh_p1', 'inh_p2', 'inh_p3', 'inh_p4', + 'inh_multiparent') + order by conrelid::regclass::text, conname; + conrelid | contype | conname | attname | coninhcount | conislocal +-----------------+---------+--------------------+---------+-------------+------------ + inh_multiparent | n | inh_p1_f1_not_null | f1 | 3 | f + inh_multiparent | n | inh_p4_f3_not_null | f3 | 1 | f + inh_p1 | n | inh_p1_f1_not_null | f1 | 0 | t + inh_p2 | n | inh_p2_f1_not_null | f1 | 0 | t + inh_p4 | n | inh_p4_f1_not_null | f1 | 0 | t + inh_p4 | n | inh_p4_f3_not_null | f3 | 0 | t +(6 rows) + +create table inh_multiparent2 (a int not null, f1 int) inherits(inh_p3, inh_multiparent); +NOTICE: merging multiple inherited definitions of column "f2" +NOTICE: merging column "f1" with inherited definition +select conrelid::regclass, contype, conname, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass in ('inh_p3', 'inh_multiparent', 'inh_multiparent2') + order by conrelid::regclass::text, conname; + conrelid | contype | conname | attname | coninhcount | conislocal +------------------+---------+-----------------------------+---------+-------------+------------ + inh_multiparent | n | inh_p1_f1_not_null | f1 | 3 | f + inh_multiparent | n | inh_p4_f3_not_null | f3 | 1 | f + inh_multiparent2 | n | inh_multiparent2_a_not_null | a | 0 | t + inh_multiparent2 | n | inh_p1_f1_not_null | f1 | 1 | f + inh_multiparent2 | n | inh_p4_f3_not_null | f3 | 1 | f +(5 rows) + +drop table inh_p1, inh_p2, inh_p3, inh_p4 cascade; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table inh_multiparent +drop cascades to table inh_multiparent2 +-- -- Mixed ownership inheritance tree -- create role regress_alice; diff --git a/src/test/regress/expected/replica_identity.out b/src/test/regress/expected/replica_identity.out index e9d7315a9c..8357761808 100644 --- a/src/test/regress/expected/replica_identity.out +++ b/src/test/regress/expected/replica_identity.out @@ -174,6 +174,9 @@ Indexes: "test_replica_identity_partial" UNIQUE, btree (keya, keyb) WHERE keyb <> '3'::text "test_replica_identity_unique_defer" UNIQUE CONSTRAINT, btree (keya, keyb) DEFERRABLE "test_replica_identity_unique_nondefer" UNIQUE CONSTRAINT, btree (keya, keyb) +Not-null constraints: + "test_replica_identity_keya_not_null" NOT NULL "keya" + "test_replica_identity_keyb_not_null" NOT NULL "keyb" Replica Identity: FULL ALTER TABLE test_replica_identity REPLICA IDENTITY NOTHING; @@ -231,6 +234,9 @@ Indexes: -- used as replica identity. ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL; ERROR: column "id" is in index used as replica identity +-- but it's OK when the identity is FULL +ALTER TABLE test_replica_identity3 REPLICA IDENTITY FULL; +ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL; -- -- Test that replica identity can be set on an index that's not yet valid. -- (This matches the way pg_dump will try to dump a partitioned table.) @@ -267,9 +273,22 @@ Indexes: "test_replica_identity4_pkey" PRIMARY KEY, btree (id) REPLICA IDENTITY Partitions: test_replica_identity4_1 FOR VALUES IN (1) +-- Dropping the primary key is not allowed if that would leave the replica +-- identity as nullable +CREATE TABLE test_replica_identity5 (a int not null, b int, c int, + PRIMARY KEY (b, c)); +CREATE UNIQUE INDEX test_replica_identity5_a_b_key ON test_replica_identity5 (a, b); +ALTER TABLE test_replica_identity5 REPLICA IDENTITY USING INDEX test_replica_identity5_a_b_key; +ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey; +ALTER TABLE test_replica_identity5 ALTER b SET NOT NULL; +ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey; +ERROR: constraint "test_replica_identity5_pkey" of relation "test_replica_identity5" does not exist +ALTER TABLE test_replica_identity5 ALTER b DROP NOT NULL; +ERROR: column "b" is in index used as replica identity DROP TABLE test_replica_identity; DROP TABLE test_replica_identity2; DROP TABLE test_replica_identity3; DROP TABLE test_replica_identity4; +DROP TABLE test_replica_identity5; DROP TABLE test_replica_identity_othertable; DROP TABLE test_replica_identity_t3; diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out index 319190855b..4ccf98d8e9 100644 --- a/src/test/regress/expected/rowsecurity.out +++ b/src/test/regress/expected/rowsecurity.out @@ -955,6 +955,8 @@ Policies: POLICY "pp1r" AS RESTRICTIVE TO regress_rls_dave USING ((cid < 55)) +Not-null constraints: + "part_document_dlevel_not_null" NOT NULL "dlevel" Partitions: part_document_fiction FOR VALUES FROM (11) TO (12), part_document_nonfiction FOR VALUES FROM (99) TO (100), part_document_satire FOR VALUES FROM (55) TO (56) diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index 28cabc49e9..5db80cc3f5 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -920,14 +920,6 @@ insert into parent values (NULL); insert into child (a, b) values (NULL, 'foo'); alter table only parent alter a set not null; alter table child alter a set not null; -delete from parent; -alter table only parent alter a set not null; -insert into parent values (NULL); -alter table child alter a set not null; -insert into child (a, b) values (NULL, 'foo'); -delete from child; -alter table child alter a set not null; -insert into child (a, b) values (NULL, 'foo'); drop table child; drop table parent; @@ -2127,6 +2119,7 @@ DROP TABLE tt9; -- Check that comments on constraints and indexes are not lost at ALTER TABLE. CREATE TABLE comment_test ( id int, + constraint id_notnull_constraint not null id, positive_col int CHECK (positive_col > 0), indexed_col int, CONSTRAINT comment_test_pk PRIMARY KEY (id)); @@ -2136,6 +2129,7 @@ COMMENT ON COLUMN comment_test.id IS 'Column ''id'' on comment_test'; COMMENT ON INDEX comment_test_index IS 'Simple index on comment_test'; COMMENT ON CONSTRAINT comment_test_positive_col_check ON comment_test IS 'CHECK constraint on comment_test.positive_col'; COMMENT ON CONSTRAINT comment_test_pk ON comment_test IS 'PRIMARY KEY constraint of comment_test'; +COMMENT ON CONSTRAINT id_notnull_constraint ON comment_test IS 'NOT NULL constraint of comment_test'; COMMENT ON INDEX comment_test_pk IS 'Index backing the PRIMARY KEY of comment_test'; SELECT col_description('comment_test'::regclass, 1) as comment; @@ -2349,6 +2343,9 @@ CREATE TABLE atnotnull1 (); ALTER TABLE atnotnull1 ADD COLUMN a INT, ALTER a SET NOT NULL; +ALTER TABLE atnotnull1 + ADD COLUMN b INT, + ADD NOT NULL b; ALTER TABLE atnotnull1 ADD COLUMN c INT, ADD PRIMARY KEY (c); diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql index e3e3bea709..7190512a10 100644 --- a/src/test/regress/sql/constraints.sql +++ b/src/test/regress/sql/constraints.sql @@ -6,6 +6,7 @@ -- - PRIMARY KEY clauses -- - UNIQUE clauses -- - EXCLUDE clauses +-- - NOT NULL clauses -- -- directory paths are passed to us in environment variables @@ -597,6 +598,127 @@ ALTER TABLE deferred_excl ADD EXCLUDE (f1 WITH =); DROP TABLE deferred_excl; +-- verify constraints created for NOT NULL clauses +CREATE TABLE notnull_tbl1 (a INTEGER NOT NULL NOT NULL); +\d+ notnull_tbl1 +-- no-op +ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; +\d+ notnull_tbl1 +-- duplicate name +ALTER TABLE notnull_tbl1 ADD COLUMN b INT CONSTRAINT notnull_tbl1_a_not_null NOT NULL; +-- DROP NOT NULL gets rid of both the attnotnull flag and the constraint itself +ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL; +\d+ notnull_tbl1 +-- SET NOT NULL puts both back +ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; +\d+ notnull_tbl1 +-- Doing it twice doesn't create a redundant constraint +ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; +select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass; +-- Using the "table constraint" syntax also works +ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL; +ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a; +\d+ notnull_tbl1 +DROP TABLE notnull_tbl1; + +-- NOT NULL NO INHERIT +CREATE TABLE ATACC1 (a int, not null a no inherit); +CREATE TABLE ATACC2 () INHERITS (ATACC1); +\d+ ATACC2 +DROP TABLE ATACC1, ATACC2; +CREATE TABLE ATACC1 (a int); +ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT; +CREATE TABLE ATACC2 () INHERITS (ATACC1); +\d+ ATACC2 +DROP TABLE ATACC1, ATACC2; +CREATE TABLE ATACC1 (a int); +CREATE TABLE ATACC2 () INHERITS (ATACC1); +ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT; +\d+ ATACC2 +DROP TABLE ATACC1, ATACC2; + +-- NOT NULL NO INHERIT is not possible on partitioned tables +CREATE TABLE ATACC1 (a int NOT NULL NO INHERIT) PARTITION BY LIST (a); +CREATE TABLE ATACC1 (a int, NOT NULL a NO INHERIT) PARTITION BY LIST (a); + +-- it's not possible to override a no-inherit constraint with an inheritable one +CREATE TABLE ATACC2 (a int, CONSTRAINT a_is_not_null NOT NULL a NO INHERIT); +CREATE TABLE ATACC1 (a int); +CREATE TABLE ATACC3 (a int) INHERITS (ATACC2); +ALTER TABLE ATACC2 INHERIT ATACC1; +-- can't override +ALTER TABLE ATACC1 ADD CONSTRAINT ditto NOT NULL a; +-- dropping the NO INHERIT constraint allows this to work +ALTER TABLE ATACC2 DROP CONSTRAINT a_is_not_null; +ALTER TABLE ATACC1 ADD CONSTRAINT ditto NOT NULL a; +\d+ ATACC3 +DROP TABLE ATACC1, ATACC2, ATACC3; + +-- Can't have two constraints with the same name +CREATE TABLE notnull_tbl2 (a INTEGER CONSTRAINT blah NOT NULL, b INTEGER CONSTRAINT blah NOT NULL); + +-- can't drop not-null in primary key +CREATE TABLE notnull_tbl2 (a INTEGER PRIMARY KEY); +ALTER TABLE notnull_tbl2 ALTER a DROP NOT NULL; +DROP TABLE notnull_tbl2; + +CREATE TABLE notnull_tbl3 (a INTEGER NOT NULL, CHECK (a IS NOT NULL)); +ALTER TABLE notnull_tbl3 ALTER A DROP NOT NULL; +ALTER TABLE notnull_tbl3 ADD b int, ADD CONSTRAINT pk PRIMARY KEY (a, b); +\d notnull_tbl3 +ALTER TABLE notnull_tbl3 DROP CONSTRAINT pk; +\d notnull_tbl3 + +-- Primary keys cause not-null constraints to be created. +CREATE TABLE cnn_pk (a int, b int); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +ALTER TABLE cnn_pk ADD CONSTRAINT cnn_primarykey PRIMARY KEY (b); +\d+ cnn_pk* +ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; +\d+ cnn_pk* +DROP TABLE cnn_pk, cnn_pk_child; + +-- As above, but create the primary key ahead of time +CREATE TABLE cnn_pk (a int, b int, CONSTRAINT cnn_primarykey PRIMARY KEY (b)); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +\d+ cnn_pk* +ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; +\d+ cnn_pk* +DROP TABLE cnn_pk, cnn_pk_child; + +-- As above, but create the primary key using a UNIQUE index +CREATE TABLE cnn_pk (a int, b int); +CREATE UNIQUE INDEX cnn_uq ON cnn_pk (b); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +ALTER TABLE cnn_pk ADD CONSTRAINT cnn_primarykey PRIMARY KEY USING INDEX cnn_uq; +\d+ cnn_pk* +DROP TABLE cnn_pk, cnn_pk_child; + +-- Ensure partitions are scanned for null values when adding a PK +create table cnn2_parted(a int) partition by list (a); +create table cnn_part1 partition of cnn2_parted for values in (1, null); +insert into cnn_part1 values (null); +alter table cnn2_parted add primary key (a); +drop table cnn2_parted; + +-- columns in regular and LIKE inheritance should be marked not-nullable +-- for primary keys, even if those are deferred +CREATE TABLE notnull_tbl4 (a INTEGER PRIMARY KEY INITIALLY DEFERRED); +CREATE TABLE notnull_tbl4_lk (LIKE notnull_tbl4); +CREATE TABLE notnull_tbl4_lk2 (LIKE notnull_tbl4 INCLUDING INDEXES); +CREATE TABLE notnull_tbl4_lk3 (LIKE notnull_tbl4 INCLUDING INDEXES, CONSTRAINT a_nn NOT NULL a); +CREATE TABLE notnull_tbl4_cld () INHERITS (notnull_tbl4); +CREATE TABLE notnull_tbl4_cld2 (PRIMARY KEY (a) DEFERRABLE) INHERITS (notnull_tbl4); +CREATE TABLE notnull_tbl4_cld3 (PRIMARY KEY (a) DEFERRABLE, CONSTRAINT a_nn NOT NULL a) INHERITS (notnull_tbl4); +\d+ notnull_tbl4 +\d+ notnull_tbl4_lk +\d+ notnull_tbl4_lk2 +\d+ notnull_tbl4_lk3 +\d+ notnull_tbl4_cld +\d+ notnull_tbl4_cld2 +\d+ notnull_tbl4_cld3 +-- leave these tables around for pg_upgrade testing + -- Comments -- Setup a low-level role to enforce non-superuser checks. CREATE ROLE regress_constraint_comments; diff --git a/src/test/regress/sql/index_including.sql b/src/test/regress/sql/index_including.sql index 11c95974ec..43bb6ea585 100644 --- a/src/test/regress/sql/index_including.sql +++ b/src/test/regress/sql/index_including.sql @@ -68,7 +68,7 @@ DROP TABLE tbl; CREATE TABLE tbl (c1 int,c2 int, c3 int, c4 box, CONSTRAINT covering PRIMARY KEY(c1,c2) INCLUDE(c3,c4)); SELECT indexrelid::regclass, indnatts, indnkeyatts, indisunique, indisprimary, indkey, indclass FROM pg_index WHERE indrelid = 'tbl'::regclass::oid; -SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid AND contype = 'p'; -- ensure that constraint works INSERT INTO tbl SELECT 1, 2, 3*x, box('4,4,4,4') FROM generate_series(1,10) AS x; INSERT INTO tbl SELECT 1, NULL, 3*x, box('4,4,4,4') FROM generate_series(1,10) AS x; @@ -95,7 +95,7 @@ DROP TABLE tbl; CREATE TABLE tbl (c1 int,c2 int, c3 int, c4 box, PRIMARY KEY(c1,c2) INCLUDE(c3,c4)); SELECT indexrelid::regclass, indnatts, indnkeyatts, indisunique, indisprimary, indkey, indclass FROM pg_index WHERE indrelid = 'tbl'::regclass::oid; -SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid AND contype = 'p'; -- ensure that constraint works INSERT INTO tbl SELECT 1, 2, 3*x, box('4,4,4,4') FROM generate_series(1,10) AS x; INSERT INTO tbl SELECT 1, NULL, 3*x, box('4,4,4,4') FROM generate_series(1,10) AS x; diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql index 5f1f4b80c9..7165d343cd 100644 --- a/src/test/regress/sql/indexing.sql +++ b/src/test/regress/sql/indexing.sql @@ -592,7 +592,7 @@ create table idxpart2 partition of idxpart for values from (0) to (1000) partiti create table idxpart21 partition of idxpart2 for values from (0) to (1000); select conname, contype, conrelid::regclass, conindid::regclass, conkey from pg_constraint where conrelid::regclass::text like 'idxpart%' - order by conname; + order by conrelid::regclass::text, conname; drop table idxpart; -- If a partitioned table has a unique/PK constraint, then it's not possible @@ -671,7 +671,6 @@ alter table only idxpart add primary key (a); -- fail, no not-null constraint alter table idxpart0 alter column a set not null; alter table only idxpart add primary key (a); -- now it works alter index idxpart_pkey attach partition idxpart0_a_key; -alter table idxpart0 alter column a drop not null; -- fail, pkey needs it drop table idxpart; -- if a partition has a unique index without a constraint, does not attach diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql index e3bcfdb181..fd4c257378 100644 --- a/src/test/regress/sql/inherit.sql +++ b/src/test/regress/sql/inherit.sql @@ -759,6 +759,259 @@ select * from cnullparent; select * from cnullparent where f1 = 2; drop table cnullparent cascade; +-- +-- Test inheritance of NOT NULL constraints +-- +create table pp1 (f1 int); +create table cc1 (f2 text, f3 int) inherits (pp1); +create table cc2 (f4 float) inherits (pp1,cc1); +create table cc3 () inherits (pp1,cc1,cc2); +alter table pp1 alter f1 set not null; +\d+ cc3 +alter table cc3 no inherit pp1; +alter table cc3 no inherit cc1; +alter table cc3 no inherit cc2; +\d+ cc3 +drop table cc3; + +-- named NOT NULL constraint +alter table cc1 add column a2 int constraint nn not null; +\d+ cc1 +\d+ cc2 +alter table pp1 alter column f1 set not null; +\d+ pp1 +\d+ cc1 +\d+ cc2 + +-- cannot create table with inconsistent NO INHERIT constraint +create table cc3 (a2 int not null no inherit) inherits (cc1); + +-- change NO INHERIT status of inherited constraint: no dice, it's inherited +alter table cc2 add not null a2 no inherit; + +-- remove constraint from cc2: no dice, it's inherited +alter table cc2 alter column a2 drop not null; + +-- remove constraint from cc1, should succeed +alter table cc1 alter column a2 drop not null; +\d+ cc1 + +-- same for cc2 +alter table cc2 alter column f1 drop not null; +\d+ cc2 + +-- remove from cc1, should fail again +alter table cc1 alter column f1 drop not null; + +-- remove from pp1, should succeed +alter table pp1 alter column f1 drop not null; +\d+ pp1 + +alter table pp1 add primary key (f1); +-- Leave these tables around, for pg_upgrade testing + +-- test that inhcount is updated correctly through multiple inheritance +create table inh_pp1 (f1 int); +create table inh_cc1 (f2 text, f3 int) inherits (inh_pp1); +create table inh_cc2(f4 float) inherits(inh_pp1,inh_cc1); +alter table inh_pp1 alter column f1 set not null; +alter table inh_cc2 no inherit inh_pp1; +alter table inh_cc2 no inherit inh_cc1; +\d+ inh_cc2 +drop table inh_pp1, inh_cc1, inh_cc2; + +create table inh_pp1 (f1 int not null); +create table inh_cc1 (f2 text, f3 int) inherits (inh_pp1); +create table inh_cc2(f4 float) inherits(inh_pp1,inh_cc1); +alter table inh_pp1 alter column f1 drop not null; +\d+ inh_cc2 +drop table inh_pp1, inh_cc1, inh_cc2; + + +-- Test a not-null addition that must walk down the hierarchy +CREATE TABLE inh_parent (); +CREATE TABLE inh_child (i int) INHERITS (inh_parent); +CREATE TABLE inh_grandchild () INHERITS (inh_parent, inh_child); +ALTER TABLE inh_parent ADD COLUMN i int NOT NULL; +drop table inh_parent, inh_child, inh_grandchild; + +-- Test the same constraint name for different columns in different parents +create table inh_parent1(a int constraint nn not null); +create table inh_parent2(b int constraint nn not null); +create table inh_child () inherits (inh_parent1, inh_parent2); +\d+ inh_child +drop table inh_parent1, inh_parent2, inh_child; + +-- Test multiple parents with overlapping primary keys +create table inh_parent1(a int, b int, c int, primary key (a, b)); +create table inh_parent2(d int, e int, b int, primary key (d, b)); +create table inh_child() inherits (inh_parent1, inh_parent2); +select conrelid::regclass, conname, contype, conkey, + coninhcount, conislocal, connoinherit + from pg_constraint where contype in ('n','p') and + conrelid::regclass::text in ('inh_child', 'inh_parent1', 'inh_parent2') + order by 1, 2; +\d+ inh_child +drop table inh_parent1, inh_parent2, inh_child; + +-- NOT NULL NO INHERIT +create table inh_nn_parent(a int); +create table inh_nn_child() inherits (inh_nn_parent); +alter table inh_nn_parent add not null a no inherit; +create table inh_nn_child2() inherits (inh_nn_parent); +select conrelid::regclass, conname, contype, conkey, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal, connoinherit + from pg_constraint where contype = 'n' and + conrelid::regclass::text like 'inh\_nn\_%' + order by 2, 1; +\d+ inh_nn* +drop table inh_nn_parent, inh_nn_child, inh_nn_child2; + +CREATE TABLE inh_nn_parent (a int, NOT NULL a NO INHERIT); +CREATE TABLE inh_nn_child() INHERITS (inh_nn_parent); +ALTER TABLE inh_nn_parent ADD CONSTRAINT nna NOT NULL a; +ALTER TABLE inh_nn_parent ALTER a SET NOT NULL; +DROP TABLE inh_nn_parent cascade; + +-- Adding a PK at the top level of a hierarchy should cause all descendants +-- to be checked for nulls, even past a no-inherit constraint +CREATE TABLE inh_nn_lvl1 (a int); +CREATE TABLE inh_nn_lvl2 () INHERITS (inh_nn_lvl1); +CREATE TABLE inh_nn_lvl3 (CONSTRAINT foo NOT NULL a NO INHERIT) INHERITS (inh_nn_lvl2); +CREATE TABLE inh_nn_lvl4 () INHERITS (inh_nn_lvl3); +CREATE TABLE inh_nn_lvl5 () INHERITS (inh_nn_lvl4); +INSERT INTO inh_nn_lvl2 VALUES (NULL); +ALTER TABLE inh_nn_lvl1 ADD PRIMARY KEY (a); +DELETE FROM inh_nn_lvl2; +INSERT INTO inh_nn_lvl5 VALUES (NULL); +ALTER TABLE inh_nn_lvl1 ADD PRIMARY KEY (a); +DROP TABLE inh_nn_lvl1 CASCADE; + +-- +-- test inherit/deinherit +-- +create table inh_parent(f1 int); +create table inh_child1(f1 int not null); +create table inh_child2(f1 int); + +-- inh_child1 should have not null constraint +alter table inh_child1 inherit inh_parent; + +-- should fail, missing NOT NULL constraint +alter table inh_child2 inherit inh_child1; + +alter table inh_child2 alter column f1 set not null; +alter table inh_child2 inherit inh_child1; + +-- add NOT NULL constraint recursively +alter table inh_parent alter column f1 set not null; + +\d+ inh_parent +\d+ inh_child1 +\d+ inh_child2 + +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass) + order by 2, 1; + +-- +-- test deinherit procedure +-- + +-- deinherit inh_child1 +create table inh_child3 () inherits (inh_child1); +alter table inh_child1 no inherit inh_parent; +\d+ inh_parent +\d+ inh_child1 +\d+ inh_child2 +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass::text in ('inh_parent', 'inh_child1', 'inh_child2', 'inh_child3') + order by 2, 1; +drop table inh_parent, inh_child1, inh_child2, inh_child3; + +-- a PK in parent must have a not-null in child that it can mark inherited +create table inh_parent (a int primary key); +create table inh_child (a int primary key); +alter table inh_child inherit inh_parent; -- nope +alter table inh_child alter a set not null; +alter table inh_child inherit inh_parent; -- now it works + +-- don't interfere with other types of constraints +alter table inh_parent add constraint inh_parent_excl exclude ((1) with =); +alter table inh_parent add constraint inh_parent_uq unique (a); +alter table inh_parent add constraint inh_parent_fk foreign key (a) references inh_parent (a); +create table inh_child2 () inherits (inh_parent); +create table inh_child3 (like inh_parent); +alter table inh_child3 inherit inh_parent; +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint + where conrelid::regclass::text in ('inh_parent', 'inh_child', 'inh_child2', 'inh_child3') + order by 2, 1; + +drop table inh_parent, inh_child, inh_child2, inh_child3; + +-- +-- test multi inheritance tree +-- +create table inh_parent(f1 int not null); +create table inh_child1() inherits(inh_parent); +create table inh_child2() inherits(inh_parent); +create table inh_child3() inherits(inh_child1, inh_child2); + +-- show constraint info +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass, 'inh_child3'::regclass) + order by 2, conrelid::regclass::text; + +drop table inh_parent cascade; + +-- test child table with inherited columns and +-- with explicitly specified not null constraints +create table inh_parent_1(f1 int); +create table inh_parent_2(f2 text); +create table inh_child(f1 int not null, f2 text not null) inherits(inh_parent_1, inh_parent_2); + +-- show constraint info +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent_1'::regclass, 'inh_parent_2'::regclass, 'inh_child'::regclass) + order by 2, conrelid::regclass::text; + +-- also drops inh_child table +drop table inh_parent_1 cascade; +drop table inh_parent_2; + +-- test multi layer inheritance tree +create table inh_p1(f1 int not null); +create table inh_p2(f1 int not null); +create table inh_p3(f2 int); +create table inh_p4(f1 int not null, f3 text not null); + +create table inh_multiparent() inherits(inh_p1, inh_p2, inh_p3, inh_p4); + +-- constraint on f1 should have three parents +select conrelid::regclass, contype, conname, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass in ('inh_p1', 'inh_p2', 'inh_p3', 'inh_p4', + 'inh_multiparent') + order by conrelid::regclass::text, conname; + +create table inh_multiparent2 (a int not null, f1 int) inherits(inh_p3, inh_multiparent); +select conrelid::regclass, contype, conname, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass in ('inh_p3', 'inh_multiparent', 'inh_multiparent2') + order by conrelid::regclass::text, conname; + +drop table inh_p1, inh_p2, inh_p3, inh_p4 cascade; + -- -- Mixed ownership inheritance tree -- diff --git a/src/test/regress/sql/replica_identity.sql b/src/test/regress/sql/replica_identity.sql index 039cca25e8..30daec05b7 100644 --- a/src/test/regress/sql/replica_identity.sql +++ b/src/test/regress/sql/replica_identity.sql @@ -100,6 +100,9 @@ ALTER TABLE test_replica_identity3 ALTER COLUMN id TYPE bigint; -- ALTER TABLE DROP NOT NULL is not allowed for columns part of an index -- used as replica identity. ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL; +-- but it's OK when the identity is FULL +ALTER TABLE test_replica_identity3 REPLICA IDENTITY FULL; +ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL; -- -- Test that replica identity can be set on an index that's not yet valid. @@ -120,9 +123,21 @@ ALTER INDEX test_replica_identity4_pkey ATTACH PARTITION test_replica_identity4_1_pkey; \d+ test_replica_identity4 +-- Dropping the primary key is not allowed if that would leave the replica +-- identity as nullable +CREATE TABLE test_replica_identity5 (a int not null, b int, c int, + PRIMARY KEY (b, c)); +CREATE UNIQUE INDEX test_replica_identity5_a_b_key ON test_replica_identity5 (a, b); +ALTER TABLE test_replica_identity5 REPLICA IDENTITY USING INDEX test_replica_identity5_a_b_key; +ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey; +ALTER TABLE test_replica_identity5 ALTER b SET NOT NULL; +ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey; +ALTER TABLE test_replica_identity5 ALTER b DROP NOT NULL; + DROP TABLE test_replica_identity; DROP TABLE test_replica_identity2; DROP TABLE test_replica_identity3; DROP TABLE test_replica_identity4; +DROP TABLE test_replica_identity5; DROP TABLE test_replica_identity_othertable; DROP TABLE test_replica_identity_t3; base-commit: a2ebf3274a43acf7ae36d608fc26090b803ab6e1 -- 2.39.5 ^ permalink raw reply [nested|flat] 6+ messages in thread
* Re: not null constraints, again @ 2024-09-23 06:40 Tender Wang <[email protected]> parent: Alvaro Herrera <[email protected]> 1 sibling, 0 replies; 6+ messages in thread From: Tender Wang @ 2024-09-23 06:40 UTC (permalink / raw) To: Alvaro Herrera <[email protected]>; +Cc: jian he <[email protected]>; Pg Hackers <[email protected]> Alvaro Herrera <[email protected]> 于2024年9月21日周六 05:15写道: > On 2024-Sep-20, Alvaro Herrera wrote: > > > Yeah, there's a bunch of conflicts in current master. I rebased > > yesterday but I'm still composing the email for v4. Coming soon. > > Okay, so here is v4 with these problems fixed, including correct > propagation of constraint names to children tables, which I had > inadvertently broken earlier. This one does pass the pg_upgrade tests > and as far as I can see pg_dump does all the correct things also. I > cleaned up the tests to remove everything that's unneeded, redundant, or > testing behavior that no longer exists. > > I changed the behavior of ALTER TABLE ONLY <parent> ADD PRIMARY KEY, so > that it throws error in case a child does not have a NOT NULL constraint > on one of the columns, rather than silently creating such a constraint. > (This is how `master` currently behaves). I think this is better > behavior, because it lets the user decide whether they want to scan the > table to create that constraint or not. It's a bit crude at present, > because (1) a child could have a NO INHERIT constraint and have further > children, which would foil the check (I think changing > find_inheritance_children to find_all_inheritors would be sufficient to > fix this, but that's only needed in legacy inheritance not > partitioning); (2) the error message doesn't have an errcode, and the > wording might need work. > The indexing test case in regress failed with v4 patch. alter table only idxpart add primary key (a); -- fail, no not-null constraint -ERROR: column a of table idxpart0 is not marked not null +ERROR: column "a" of table "idxpart0" is not marked NOT NULL It seemed the error message forgot to change. -- Thanks, Tender Wang https://www.openpie.com/ ^ permalink raw reply [nested|flat] 6+ messages in thread
* Re: not null constraints, again @ 2024-09-24 03:22 jian he <[email protected]> parent: Alvaro Herrera <[email protected]> 1 sibling, 1 reply; 6+ messages in thread From: jian he @ 2024-09-24 03:22 UTC (permalink / raw) To: Alvaro Herrera <[email protected]>; +Cc: Tender Wang <[email protected]>; Pg Hackers <[email protected]> On Sat, Sep 21, 2024 at 5:15 AM Alvaro Herrera <[email protected]> wrote: > > Okay, so here is v4 with these problems fixed, including correct > propagation of constraint names to children tables, which I had > inadvertently broken earlier. This one does pass the pg_upgrade tests > and as far as I can see pg_dump does all the correct things also. I > cleaned up the tests to remove everything that's unneeded, redundant, or > testing behavior that no longer exists. > in findNotNullConstraintAttnum if (con->contype != CONSTRAINT_NOTNULL) continue; if (!con->convalidated) continue; if con->convalidated is false, then we have a bigger problem? maybe we can change to ERROR to expose/capture potential problems. like: if (con->contype != CONSTRAINT_NOTNULL) continue; if (!con->convalidated) elog(ERROR, "not-null constraint is not validated"); ------<<<<<<<<------------------ HeapTuple findNotNullConstraint(Oid relid, const char *colname) { AttrNumber attnum = get_attnum(relid, colname); return findNotNullConstraintAttnum(relid, attnum); } we can change to HeapTuple findNotNullConstraint(Oid relid, const char *colname) { AttrNumber attnum = get_attnum(relid, colname); if (attnum <= InvalidAttrNumber) return NULL; return findNotNullConstraintAttnum(relid, attnum); } ------<<<<<<<<------------------ sql-createtable.html SECTION: LIKE source_table [ like_option ... ] INCLUDING CONSTRAINTS CHECK constraints will be copied. No distinction is made between column constraints and table constraints. Not-null constraints are always copied to the new table. drop table if exists t, t_1,ssa; create table t(a int, b int, not null a no inherit); create table ssa (like t INCLUDING all); Here create table like won't include no inherit not-null constraint, seems to conflict with the doc? ------<<<<<<<<------------------ drop table if exists t, t_1; create table t(a int primary key, b int, not null a no inherit); create table t_1 () inherits (t); t_1 will inherit the not-null constraint from t, so the syntax "not null a no inherit" information is ignored. other cases: create table t(a int not null, b int, not null a no inherit); create table t(a int not null no inherit, b int, not null a); seems currently, column constraint have not-null constraint, then use it and table constraint (not-null) are ignored. but if column constraint don't have not-null then according to table constraint. ^ permalink raw reply [nested|flat] 6+ messages in thread
* Re: not null constraints, again @ 2024-09-24 16:59 Alvaro Herrera <[email protected]> parent: jian he <[email protected]> 0 siblings, 1 reply; 6+ messages in thread From: Alvaro Herrera @ 2024-09-24 16:59 UTC (permalink / raw) To: jian he <[email protected]>; +Cc: Tender Wang <[email protected]>; Pg Hackers <[email protected]> On 2024-Sep-24, jian he wrote: > sql-createtable.html > SECTION: LIKE source_table [ like_option ... ] > INCLUDING CONSTRAINTS > CHECK constraints will be copied. No distinction is made between > column constraints and table constraints. Not-null constraints are > always copied to the new table. > > drop table if exists t, t_1,ssa; > create table t(a int, b int, not null a no inherit); > create table ssa (like t INCLUDING all); > > Here create table like won't include no inherit not-null constraint, > seems to conflict with the doc? Hmm, actually I think this is a bug, because if you have CHECK constraint with NO INHERIT, it will be copied: create table t (a int check (a > 0) no inherit); create table ssa (like t including constraints); 55490 18devel 141626=# \d+ ssa Tabla «public.ssa» Columna │ Tipo │ Ordenamiento │ Nulable │ Por omisión │ Almacenamiento> ─────────┼─────────┼──────────────┼─────────┼─────────────┼───────────────> a │ integer │ │ │ │ plain > Restricciones CHECK: "t_a_check" CHECK (a > 0) NO INHERIT Método de acceso: heap It seems that NOT NULL constraint should behave the same as CHECK constraints in this regard, i.e., we should not heed NO INHERIT in this case. I have made these changes and added some tests, and will be posting a v5 shortly. -- Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/ <inflex> really, I see PHP as like a strange amalgamation of C, Perl, Shell <crab> inflex: you know that "amalgam" means "mixture with mercury", more or less, right? <crab> i.e., "deadly poison" ^ permalink raw reply [nested|flat] 6+ messages in thread
* Re: not null constraints, again @ 2024-09-24 19:07 Alvaro Herrera <[email protected]> parent: Alvaro Herrera <[email protected]> 0 siblings, 0 replies; 6+ messages in thread From: Alvaro Herrera @ 2024-09-24 19:07 UTC (permalink / raw) To: jian he <[email protected]>; +Cc: Tender Wang <[email protected]>; Pg Hackers <[email protected]> On 2024-Sep-24, Alvaro Herrera wrote: > I have made these changes and added some tests, and will be posting a v5 > shortly. I ran the coverage report and found a couple of ereports are not covered by any tests. I'm adding those. May add more tomorrow, after looking at the coverage report some more. I should give a try at running Andres' differential coverage report[1] at some point ... [1] https://postgr.es/m/20240414223305.m3i5eju6zylabvln%40awork3.anarazel.de -- Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/ Attachments: [text/x-diff] v5-0001-Catalog-not-null-constraints.patch (294.9K, ../../[email protected]/2-v5-0001-Catalog-not-null-constraints.patch) download | inline diff: From cbe1e2804e8f3d5b815f53d74ab9654a8cb362a7 Mon Sep 17 00:00:00 2001 From: Alvaro Herrera <[email protected]> Date: Tue, 24 Sep 2024 21:05:58 +0200 Subject: [PATCH v5] Catalog not-null constraints --- contrib/sepgsql/expected/alter.out | 3 - contrib/test_decoding/expected/ddl.out | 12 + doc/src/sgml/catalogs.sgml | 12 +- doc/src/sgml/ddl.sgml | 65 +- doc/src/sgml/ref/alter_table.sgml | 16 +- doc/src/sgml/ref/create_table.sgml | 8 +- src/backend/catalog/heap.c | 370 ++++- src/backend/catalog/information_schema.sql | 71 +- src/backend/catalog/pg_constraint.c | 250 ++++ src/backend/commands/tablecmds.c | 1297 +++++++++++------ src/backend/commands/typecmds.c | 4 + src/backend/nodes/makefuncs.c | 24 + src/backend/optimizer/util/plancat.c | 2 + src/backend/parser/gram.y | 23 +- src/backend/parser/parse_utilcmd.c | 252 ++-- src/backend/utils/adt/ruleutils.c | 22 + src/bin/pg_dump/common.c | 31 +- src/bin/pg_dump/pg_dump.c | 270 +++- src/bin/pg_dump/pg_dump.h | 8 +- src/bin/pg_dump/t/002_pg_dump.pl | 6 +- src/bin/psql/describe.c | 44 + src/bin/psql/t/010_tab_completion.pl | 8 +- src/include/catalog/heap.h | 8 +- src/include/catalog/pg_constraint.h | 7 + src/include/nodes/makefuncs.h | 1 + src/include/nodes/parsenodes.h | 10 +- .../test_ddl_deparse/expected/alter_table.out | 17 +- .../expected/create_table.out | 2 - .../test_ddl_deparse/test_ddl_deparse.c | 3 - src/test/regress/expected/alter_table.out | 31 +- src/test/regress/expected/cluster.out | 7 +- src/test/regress/expected/constraints.out | 362 +++++ src/test/regress/expected/create_table.out | 35 +- .../regress/expected/create_table_like.out | 34 +- src/test/regress/expected/foreign_data.out | 110 +- src/test/regress/expected/foreign_key.out | 16 +- .../regress/expected/generated_stored.out | 2 + src/test/regress/expected/identity.out | 6 +- src/test/regress/expected/index_including.out | 4 +- src/test/regress/expected/indexing.out | 56 +- src/test/regress/expected/inherit.out | 600 ++++++++ src/test/regress/expected/publication.out | 15 + .../regress/expected/replica_identity.out | 24 + src/test/regress/expected/rowsecurity.out | 2 + src/test/regress/sql/alter_table.sql | 13 +- src/test/regress/sql/constraints.sql | 122 ++ src/test/regress/sql/create_table_like.sql | 5 +- src/test/regress/sql/index_including.sql | 4 +- src/test/regress/sql/indexing.sql | 3 +- src/test/regress/sql/inherit.sql | 273 ++++ src/test/regress/sql/replica_identity.sql | 15 + 51 files changed, 3775 insertions(+), 810 deletions(-) diff --git a/contrib/sepgsql/expected/alter.out b/contrib/sepgsql/expected/alter.out index ae43537505..1462cfa3cb 100644 --- a/contrib/sepgsql/expected/alter.out +++ b/contrib/sepgsql/expected/alter.out @@ -164,7 +164,6 @@ LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_re LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_2.b" permissive=0 ALTER TABLE regtest_table ALTER b DROP NOT NULL; LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table.b" permissive=0 -LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_2.b" permissive=0 ALTER TABLE regtest_table ALTER b SET STATISTICS -1; LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table.b" permissive=0 LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_2.b" permissive=0 @@ -249,8 +248,6 @@ LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_re LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_ptable_1_tens.p" permissive=0 ALTER TABLE regtest_ptable ALTER p DROP NOT NULL; LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_ptable.p" permissive=0 -LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table_part.p" permissive=0 -LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_ptable_1_tens.p" permissive=0 ALTER TABLE regtest_ptable ALTER p SET STATISTICS -1; LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_ptable.p" permissive=0 LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table_part.p" permissive=0 diff --git a/contrib/test_decoding/expected/ddl.out b/contrib/test_decoding/expected/ddl.out index 5713b8ab1c..bcd1f74b2b 100644 --- a/contrib/test_decoding/expected/ddl.out +++ b/contrib/test_decoding/expected/ddl.out @@ -492,6 +492,9 @@ WITH (user_catalog_table = true) options | text[] | | | | extended | | Indexes: "replication_metadata_pkey" PRIMARY KEY, btree (id) +Not-null constraints: + "replication_metadata_id_not_null" NOT NULL "id" + "replication_metadata_relation_not_null" NOT NULL "relation" Options: user_catalog_table=true INSERT INTO replication_metadata(relation, options) @@ -506,6 +509,9 @@ ALTER TABLE replication_metadata RESET (user_catalog_table); options | text[] | | | | extended | | Indexes: "replication_metadata_pkey" PRIMARY KEY, btree (id) +Not-null constraints: + "replication_metadata_id_not_null" NOT NULL "id" + "replication_metadata_relation_not_null" NOT NULL "relation" INSERT INTO replication_metadata(relation, options) VALUES ('bar', ARRAY['a', 'b']); @@ -519,6 +525,9 @@ ALTER TABLE replication_metadata SET (user_catalog_table = true); options | text[] | | | | extended | | Indexes: "replication_metadata_pkey" PRIMARY KEY, btree (id) +Not-null constraints: + "replication_metadata_id_not_null" NOT NULL "id" + "replication_metadata_relation_not_null" NOT NULL "relation" Options: user_catalog_table=true INSERT INTO replication_metadata(relation, options) @@ -538,6 +547,9 @@ ALTER TABLE replication_metadata SET (user_catalog_table = false); rewritemeornot | integer | | | | plain | | Indexes: "replication_metadata_pkey" PRIMARY KEY, btree (id) +Not-null constraints: + "replication_metadata_id_not_null" NOT NULL "id" + "replication_metadata_relation_not_null" NOT NULL "relation" Options: user_catalog_table=false INSERT INTO replication_metadata(relation, options) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index bfb97865e1..1ff5c639ad 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1271,7 +1271,7 @@ <structfield>attnotnull</structfield> <type>bool</type> </para> <para> - This represents a not-null constraint. + This column has a not-null constraint. </para></entry> </row> @@ -2502,14 +2502,10 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </indexterm> <para> - The catalog <structname>pg_constraint</structname> stores check, primary - key, unique, foreign key, and exclusion constraints on tables, as well as - not-null constraints on domains. + The catalog <structname>pg_constraint</structname> stores check, not-null, + primary key, unique, foreign key, and exclusion constraints on tables. (Column constraints are not treated specially. Every column constraint is equivalent to some table constraint.) - Not-null constraints on relations are represented in the - <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link> - catalog, not here. </para> <para> @@ -2571,7 +2567,7 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <para> <literal>c</literal> = check constraint, <literal>f</literal> = foreign key constraint, - <literal>n</literal> = not-null constraint (domains only), + <literal>n</literal> = not-null constraint, <literal>p</literal> = primary key constraint, <literal>u</literal> = unique constraint, <literal>t</literal> = constraint trigger, diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 8ab0ddb112..1ad80203c7 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -768,18 +768,39 @@ CREATE TABLE products ( name text <emphasis>NOT NULL</emphasis>, price numeric ); +</programlisting> + An explicit constraint name can also be specified, for example: +<programlisting> +CREATE TABLE products ( + product_no integer NOT NULL, + name text <emphasis>CONSTRAINT products_name_not_null</emphasis> NOT NULL, + price numeric +); </programlisting> </para> <para> - A not-null constraint is always written as a column constraint. A - not-null constraint is functionally equivalent to creating a check + A not-null constraint is usually written as a column constraint. The + syntax for writing it as a table constraint is +<programlisting> +CREATE TABLE products ( + product_no integer, + name text, + price numeric, + <emphasis>NOT NULL product_no</emphasis>, + <emphasis>NOT NULL name</emphasis> +); +</programlisting> + But this syntax is not standard and mainly intended for use by + <application>pg_dump</application>. + </para> + + <para> + A not-null constraint is functionally equivalent to creating a check constraint <literal>CHECK (<replaceable>column_name</replaceable> IS NOT NULL)</literal>, but in <productname>PostgreSQL</productname> creating an explicit - not-null constraint is more efficient. The drawback is that you - cannot give explicit names to not-null constraints created this - way. + not-null constraint is more efficient. </para> <para> @@ -796,6 +817,10 @@ CREATE TABLE products ( order the constraints are checked. </para> + <para> + However, a column can have at most one explicit not-null constraint. + </para> + <para> The <literal>NOT NULL</literal> constraint has an inverse: the <literal>NULL</literal> constraint. This does not mean that the @@ -989,7 +1014,7 @@ CREATE TABLE example ( <para> A table can have at most one primary key. (There can be any number - of unique and not-null constraints, which are functionally almost the + of unique constraints, which combined with not-null constraints are functionally almost the same thing, but only one can be identified as the primary key.) Relational database theory dictates that every table must have a primary key. This rule is @@ -1649,11 +1674,16 @@ ALTER TABLE products ADD CHECK (name <> ''); ALTER TABLE products ADD CONSTRAINT some_name UNIQUE (product_no); ALTER TABLE products ADD FOREIGN KEY (product_group_id) REFERENCES product_groups; </programlisting> - To add a not-null constraint, which cannot be written as a table - constraint, use this syntax: + </para> + + <para> + To add a not-null constraint, which is normally not written as a table + constraint, this special syntax is available: <programlisting> ALTER TABLE products ALTER COLUMN product_no SET NOT NULL; </programlisting> + This command silently does nothing if the column already has a + not-null constraint. </para> <para> @@ -1694,12 +1724,15 @@ ALTER TABLE products DROP CONSTRAINT some_name; </para> <para> - This works the same for all constraint types except not-null - constraints. To drop a not-null constraint use: + Simplified syntax is available to drop a not-null constraint: <programlisting> ALTER TABLE products ALTER COLUMN product_no DROP NOT NULL; </programlisting> - (Recall that not-null constraints do not have names.) + This mirrors the <literal>SET NOT NULL</literal> syntax for adding a + not-null constraint. This command will silently do nothing if the column + does not have a not-null constraint. (Recall that a column can have at + most one not-null constraint, so it is never ambiguous which constraint + this command acts on.) </para> </sect2> @@ -4443,12 +4476,10 @@ ALTER INDEX measurement_city_id_logdate_key <para> Both <literal>CHECK</literal> and <literal>NOT NULL</literal> constraints of a partitioned table are always inherited by all its - partitions. <literal>CHECK</literal> constraints that are marked - <literal>NO INHERIT</literal> are not allowed to be created on - partitioned tables. - You cannot drop a <literal>NOT NULL</literal> constraint on a - partition's column if the same constraint is present in the parent - table. + partitions; it is not allowed to create <literal>NO INHERIT</literal> + constraints of those types. + You cannot drop a constraint of those types if the same constraint + is present in the parent table. </para> </listitem> diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 1a49f321cf..9b5d719416 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -98,7 +98,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM <phrase>and <replaceable class="parameter">column_constraint</replaceable> is:</phrase> [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ] -{ NOT NULL | +{ NOT NULL [ NO INHERIT ] | NULL | CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] | DEFAULT <replaceable>default_expr</replaceable> | @@ -114,6 +114,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM [ 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> | 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> ) ] | @@ -1024,6 +1025,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM attached are marked <literal>NO INHERIT</literal>, the command will fail; such constraints must be recreated without the <literal>NO INHERIT</literal> clause. + By contrast, a <literal>NOT NULL</literal> constraint that was created + as <literal>NO INHERIT</literal> will be changed to a normal inheriting + one during attach. </para> <para> @@ -1789,11 +1793,17 @@ ALTER TABLE measurement <title>Compatibility</title> <para> - The forms <literal>ADD</literal> (without <literal>USING INDEX</literal>), + The forms <literal>ADD [COLUMN]</literal>, <literal>DROP [COLUMN]</literal>, <literal>DROP IDENTITY</literal>, <literal>RESTART</literal>, <literal>SET DEFAULT</literal>, <literal>SET DATA TYPE</literal> (without <literal>USING</literal>), <literal>SET GENERATED</literal>, and <literal>SET <replaceable>sequence_option</replaceable></literal> - conform with the SQL standard. The other forms are + conform with the SQL standard. + The form <literal>ADD <replaceable>table_constraint</replaceable></literal> + conforms with the SQL standard when the <literal>USING INDEX</literal> and + <literal>NOT VALID</literal> clauses are omitted and the constraint type is + one of <literal>CHECK</literal>, <literal>UNIQUE</literal>, <literal>PRIMARY KEY</literal>, + or <literal>REFERENCES</literal>. + The other forms are <productname>PostgreSQL</productname> extensions of the SQL standard. Also, the ability to specify more than one manipulation in a single <command>ALTER TABLE</command> command is an extension. diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index c1855b8d82..f9a098e77a 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -77,6 +77,7 @@ 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">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> | PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</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> ) ] | @@ -2394,13 +2395,6 @@ CREATE TABLE cities_partdef constraint, and index names must be unique across all relations within the same schema. </para> - - <para> - Currently, <productname>PostgreSQL</productname> does not record names - for not-null constraints at all, so they are not - subject to the uniqueness restriction. This might change in a future - release. - </para> </refsect2> <refsect2> diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 78e59384d1..cec6762663 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -2171,6 +2171,56 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr, return constrOid; } +/* + * Store a not-null constraint for the given relation + * + * The OID of the new constraint is returned. + */ +static Oid +StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum, + bool is_validated, bool is_local, int inhcount, + bool is_no_inherit) +{ + Oid constrOid; + + Assert(attnum > InvalidAttrNumber); + + constrOid = + CreateConstraintEntry(nnname, + RelationGetNamespace(rel), + CONSTRAINT_NOTNULL, + false, + false, + is_validated, + InvalidOid, + RelationGetRelid(rel), + &attnum, + 1, + 1, + InvalidOid, /* not a domain constraint */ + InvalidOid, /* no associated index */ + InvalidOid, /* Foreign key fields */ + NULL, + NULL, + NULL, + NULL, + 0, + ' ', + ' ', + NULL, + 0, + ' ', + NULL, /* not an exclusion constraint */ + NULL, + NULL, + is_local, + inhcount, + is_no_inherit, + false, + false); + return constrOid; +} + /* * Store defaults and constraints (passed as a list of CookedConstraint). * @@ -2215,6 +2265,14 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal) is_internal); numchecks++; break; + + case CONSTR_NOTNULL: + con->conoid = + StoreRelNotNull(rel, con->name, con->attnum, + !con->skip_validation, con->is_local, + con->inhcount, con->is_no_inherit); + break; + default: elog(ERROR, "unrecognized constraint type: %d", (int) con->contype); @@ -2243,7 +2301,7 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal) * cooked CHECK constraints * * All entries in newColDefaults will be processed. Entries in newConstraints - * will be processed only if they are CONSTR_CHECK type. + * will be processed only if they are CONSTR_CHECK or CONSTR_NOTNULL types. * * Returns a list of CookedConstraint nodes that shows the cooked form of * the default and constraint expressions added to the relation. @@ -2272,6 +2330,7 @@ AddRelationNewConstraints(Relation rel, ParseNamespaceItem *nsitem; int numchecks; List *checknames; + List *nnnames; Node *expr; CookedConstraint *cooked; @@ -2356,6 +2415,7 @@ AddRelationNewConstraints(Relation rel, */ numchecks = numoldchecks; checknames = NIL; + nnnames = NIL; foreach_node(Constraint, cdef, newConstraints) { Oid constrOid; @@ -2410,7 +2470,7 @@ AddRelationNewConstraints(Relation rel, * Check against pre-existing constraints. If we are allowed * to merge with an existing constraint, there's no more to do * here. (We omit the duplicate constraint from the result, - * which is what ATAddCheckConstraint wants.) + * which is what ATAddCheckNNConstraint wants.) */ if (MergeWithExistingConstraint(rel, ccname, expr, allow_merge, is_local, @@ -2479,6 +2539,76 @@ AddRelationNewConstraints(Relation rel, cooked->is_no_inherit = cdef->is_no_inherit; cookedConstraints = lappend(cookedConstraints, cooked); } + else if (cdef->contype == CONSTR_NOTNULL) + { + CookedConstraint *nncooked; + AttrNumber colnum; + char *nnname; + + /* Determine which column to modify */ + colnum = get_attnum(RelationGetRelid(rel), strVal(linitial(cdef->keys))); + if (colnum == InvalidAttrNumber) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + strVal(linitial(cdef->keys)), RelationGetRelationName(rel))); + if (colnum < InvalidAttrNumber) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot add not-null constraint on system column \"%s\"", + strVal(linitial(cdef->keys)))); + + /* + * If the column already has a not-null constraint, we don't want + * to add another one; just adjust inheritance status as needed. + */ + if (AdjustNotNullInheritance(RelationGetRelid(rel), colnum, + cdef->inhcount, is_local, cdef->is_no_inherit)) + continue; + + /* + * If a constraint name is specified, check that it isn't already + * used. Otherwise, choose a non-conflicting one ourselves. + */ + if (cdef->conname) + { + if (ConstraintNameIsUsed(CONSTRAINT_RELATION, + RelationGetRelid(rel), + cdef->conname)) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("constraint \"%s\" for relation \"%s\" already exists", + cdef->conname, RelationGetRelationName(rel))); + nnname = cdef->conname; + } + else + nnname = ChooseConstraintName(RelationGetRelationName(rel), + strVal(linitial(cdef->keys)), + "not_null", + RelationGetNamespace(rel), + nnnames); + nnnames = lappend(nnnames, nnname); + + constrOid = + StoreRelNotNull(rel, nnname, colnum, + cdef->initially_valid, + cdef->inhcount == 0, + cdef->inhcount, + cdef->is_no_inherit); + + nncooked = (CookedConstraint *) palloc(sizeof(CookedConstraint)); + nncooked->contype = CONSTR_NOTNULL; + nncooked->conoid = constrOid; + nncooked->name = nnname; + nncooked->attnum = colnum; + nncooked->expr = NULL; + nncooked->skip_validation = cdef->skip_validation; + nncooked->is_local = is_local; + nncooked->inhcount = cdef->inhcount; + nncooked->is_no_inherit = cdef->is_no_inherit; + + cookedConstraints = lappend(cookedConstraints, nncooked); + } } /* @@ -2648,6 +2778,242 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr, return found; } +/* + * Create the not-null constraints when creating a new relation + * + * These come from two sources: the 'constraints' list (of Constraint) is + * specified directly by the user; the 'old_notnulls' list (of + * CookedConstraint) comes from inheritance. We create one constraint + * for each column, giving priority to user-specified ones, and setting + * inhcount according to how many parents cause each column to get a + * not-null constraint. If a user-specified name clashes with another + * user-specified name, an error is raised. + * + * Returns a list of AttrNumber for columns that need to have the attnotnull + * flag set. + */ +List * +AddRelationNotNullConstraints(Relation rel, List *constraints, + List *old_notnulls) +{ + List *givennames; + List *nnnames; + List *nncols = NIL; + + /* + * We track two lists of names: nnnames keeps all the constraint names, + * givennames tracks user-generated names. The distinction is important, + * because we must raise error for user-generated name conflicts, but for + * system-generated name conflicts we just generate another. + */ + nnnames = NIL; + givennames = NIL; + + /* + * First, create all not-null constraints that are directly specified by + * the user. Note that inheritance might have given us another source for + * each, so we must scan the old_notnulls list and increment inhcount for + * each element with identical attnum. We delete from there any element + * that we process. + * + * We don't use foreach() here because we have two nested loops over the + * constraint list, with possible element deletions in the inner one. If + * we used foreach_delete_current() it could only fix up the state of one + * of the loops, so it seems cleaner to use looping over list indexes for + * both loops. Note that any deletion will happen beyond where the outer + * loop is, so its index never needs adjustment. + */ + for (int outerpos = 0; outerpos < list_length(constraints); outerpos++) + { + Constraint *constr; + AttrNumber attnum; + char *conname; + int inhcount = 0; + + constr = list_nth_node(Constraint, constraints, outerpos); + + Assert(constr->contype == CONSTR_NOTNULL); + + attnum = get_attnum(RelationGetRelid(rel), + strVal(linitial(constr->keys))); + if (attnum == InvalidAttrNumber) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + strVal(linitial(constr->keys)), + RelationGetRelationName(rel))); + if (attnum < InvalidAttrNumber) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot add not-null constraint on system column \"%s\"", + strVal(linitial(constr->keys)))); + + /* + * A column can only have one not-null constraint, so discard any + * additional ones that appear for columns we already saw; but check + * that the NO INHERIT flags match. + */ + for (int restpos = outerpos + 1; restpos < list_length(constraints);) + { + Constraint *other; + + other = list_nth_node(Constraint, constraints, restpos); + if (strcmp(strVal(linitial(constr->keys)), + strVal(linitial(other->keys))) == 0) + { + if (other->is_no_inherit != constr->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting NO INHERIT declaration for not-null constraint on column \"%s\"", + strVal(linitial(constr->keys)))); + /* XXX do we need to verify any other fields? */ + constraints = list_delete_nth_cell(constraints, restpos); + } + else + restpos++; + } + + /* + * Search in the list of inherited constraints for any entries on the + * same column. + */ + foreach_ptr(CookedConstraint, old, old_notnulls) + { + if (old->attnum == attnum) + { + /* + * If we get a constraint from the parent, having a local NO + * INHERIT one doesn't work. + */ + if (constr->is_no_inherit) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot define not-null constraint on column \"%s\" with NO INHERIT", + strVal(linitial(constr->keys))), + errdetail("The column has an inherited not-null constraint."))); + + inhcount++; + old_notnulls = foreach_delete_current(old_notnulls, old); + } + } + + /* + * Determine a constraint name, which may have been specified by the + * user, or raise an error if a conflict exists with another + * user-specified name. + */ + if (constr->conname) + { + foreach_ptr(char, thisname, givennames) + { + if (strcmp(thisname, constr->conname) == 0) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("constraint \"%s\" for relation \"%s\" already exists", + constr->conname, + RelationGetRelationName(rel))); + } + + conname = constr->conname; + givennames = lappend(givennames, conname); + } + else + conname = ChooseConstraintName(RelationGetRelationName(rel), + get_attname(RelationGetRelid(rel), + attnum, false), + "not_null", + RelationGetNamespace(rel), + nnnames); + nnnames = lappend(nnnames, conname); + + StoreRelNotNull(rel, conname, + attnum, true, true, + inhcount, constr->is_no_inherit); + + nncols = lappend_int(nncols, attnum); + } + + /* + * If any column remains in the old_notnulls list, we must create a not- + * null constraint marked not-local. Because multiple parents could + * specify a not-null constraint for the same column, we must count how + * many there are and add to the original inhcount accordingly, deleting + * elements we've already processed. + * + * We don't use foreach() here because we have two nested loops over the + * constraint list, with possible element deletions in the inner one. If + * we used foreach_delete_current() it could only fix up the state of one + * of the loops, so it seems cleaner to use looping over list indexes for + * both loops. Note that any deletion will happen beyond where the outer + * loop is, so its index never needs adjustment. + */ + for (int outerpos = 0; outerpos < list_length(old_notnulls); outerpos++) + { + CookedConstraint *cooked; + char *conname = NULL; + int inhcount = 1; + + cooked = (CookedConstraint *) list_nth(old_notnulls, outerpos); + Assert(cooked->contype == CONSTR_NOTNULL); + Assert(cooked->name); + + /* + * Preserve the first non-conflicting constraint name we come across. + */ + if (conname == NULL) + conname = cooked->name; + + for (int restpos = outerpos + 1; restpos < list_length(old_notnulls);) + { + CookedConstraint *other; + + other = (CookedConstraint *) list_nth(old_notnulls, restpos); + Assert(other->name); + if (other->attnum == cooked->attnum) + { + if (conname == NULL) + conname = other->name; + + inhcount++; + old_notnulls = list_delete_nth_cell(old_notnulls, restpos); + } + else + restpos++; + } + + /* If we got a name, make sure it isn't one we've already used */ + if (conname != NULL) + { + foreach_ptr(char, thisname, nnnames) + { + if (strcmp(thisname, conname) == 0) + { + conname = NULL; + break; + } + } + } + + /* and choose a name, if needed */ + if (conname == NULL) + conname = ChooseConstraintName(RelationGetRelationName(rel), + get_attname(RelationGetRelid(rel), + cooked->attnum, false), + "not_null", + RelationGetNamespace(rel), + nnnames); + nnnames = lappend(nnnames, conname); + + /* ignore the origin constraint's is_local and inhcount */ + StoreRelNotNull(rel, conname, cooked->attnum, true, + false, inhcount, false); + + nncols = lappend_int(nncols, cooked->attnum); + } + + return nncols; +} + /* * Update the count of constraints in the relation's pg_class tuple. * diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql index c4145131ce..76c78c0d18 100644 --- a/src/backend/catalog/information_schema.sql +++ b/src/backend/catalog/information_schema.sql @@ -440,9 +440,8 @@ CREATE VIEW check_constraints AS WHERE pg_has_role(coalesce(c.relowner, t.typowner), 'USAGE') AND con.contype = 'c' - UNION - -- not-null constraints on domains - + UNION ALL + -- not-null constraints SELECT current_database()::information_schema.sql_identifier AS constraint_catalog, rs.nspname::information_schema.sql_identifier AS constraint_schema, con.conname::information_schema.sql_identifier AS constraint_name, @@ -453,24 +452,7 @@ CREATE VIEW check_constraints AS LEFT JOIN pg_type t ON t.oid = con.contypid LEFT JOIN pg_attribute at ON (con.conrelid = at.attrelid AND con.conkey[1] = at.attnum) WHERE pg_has_role(coalesce(c.relowner, t.typowner), 'USAGE'::text) - AND con.contype = 'n' - - UNION - -- not-null constraints on relations - - SELECT CAST(current_database() AS sql_identifier) AS constraint_catalog, - CAST(n.nspname AS sql_identifier) AS constraint_schema, - CAST(CAST(n.oid AS text) || '_' || CAST(r.oid AS text) || '_' || CAST(a.attnum AS text) || '_not_null' AS sql_identifier) AS constraint_name, -- XXX - CAST(a.attname || ' IS NOT NULL' AS character_data) - AS check_clause - FROM pg_namespace n, pg_class r, pg_attribute a - WHERE n.oid = r.relnamespace - AND r.oid = a.attrelid - AND a.attnum > 0 - AND NOT a.attisdropped - AND a.attnotnull - AND r.relkind IN ('r', 'p') - AND pg_has_role(r.relowner, 'USAGE'); + AND con.contype = 'n'; GRANT SELECT ON check_constraints TO PUBLIC; @@ -839,6 +821,20 @@ CREATE VIEW constraint_column_usage AS UNION ALL + /* not-null constraints */ + SELECT DISTINCT nr.nspname, r.relname, r.relowner, a.attname, nc.nspname, c.conname + FROM pg_namespace nr, pg_class r, pg_attribute a, pg_namespace nc, pg_constraint c + WHERE nr.oid = r.relnamespace + AND r.oid = a.attrelid + AND r.oid = c.conrelid + AND a.attnum = c.conkey[1] + AND c.connamespace = nc.oid + AND c.contype = 'n' + AND r.relkind in ('r', 'p') + AND not a.attisdropped + + UNION ALL + /* unique/primary key/foreign key constraints */ SELECT nr.nspname, r.relname, r.relowner, a.attname, nc.nspname, c.conname FROM pg_namespace nr, pg_class r, pg_attribute a, pg_namespace nc, @@ -1839,6 +1835,7 @@ CREATE VIEW table_constraints AS CAST(r.relname AS sql_identifier) AS table_name, CAST( CASE c.contype WHEN 'c' THEN 'CHECK' + WHEN 'n' THEN 'CHECK' WHEN 'f' THEN 'FOREIGN KEY' WHEN 'p' THEN 'PRIMARY KEY' WHEN 'u' THEN 'UNIQUE' END @@ -1863,38 +1860,6 @@ CREATE VIEW table_constraints AS AND c.contype NOT IN ('t', 'x') -- ignore nonstandard constraints AND r.relkind IN ('r', 'p') AND (NOT pg_is_other_temp_schema(nr.oid)) - AND (pg_has_role(r.relowner, 'USAGE') - -- SELECT privilege omitted, per SQL standard - OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') - OR has_any_column_privilege(r.oid, 'INSERT, UPDATE, REFERENCES') ) - - UNION ALL - - -- not-null constraints - - SELECT CAST(current_database() AS sql_identifier) AS constraint_catalog, - CAST(nr.nspname AS sql_identifier) AS constraint_schema, - CAST(CAST(nr.oid AS text) || '_' || CAST(r.oid AS text) || '_' || CAST(a.attnum AS text) || '_not_null' AS sql_identifier) AS constraint_name, -- XXX - CAST(current_database() AS sql_identifier) AS table_catalog, - CAST(nr.nspname AS sql_identifier) AS table_schema, - CAST(r.relname AS sql_identifier) AS table_name, - CAST('CHECK' AS character_data) AS constraint_type, - CAST('NO' AS yes_or_no) AS is_deferrable, - CAST('NO' AS yes_or_no) AS initially_deferred, - CAST('YES' AS yes_or_no) AS enforced, - CAST(NULL AS yes_or_no) AS nulls_distinct - - FROM pg_namespace nr, - pg_class r, - pg_attribute a - - WHERE nr.oid = r.relnamespace - AND r.oid = a.attrelid - AND a.attnotnull - AND a.attnum > 0 - AND NOT a.attisdropped - AND r.relkind IN ('r', 'p') - AND (NOT pg_is_other_temp_schema(nr.oid)) AND (pg_has_role(r.relowner, 'USAGE') -- SELECT privilege omitted, per SQL standard OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index 1e2df031a8..0d460b9b7f 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -19,8 +19,10 @@ #include "access/htup_details.h" #include "access/sysattr.h" #include "access/table.h" +#include "access/xact.h" #include "catalog/catalog.h" #include "catalog/dependency.h" +#include "catalog/heap.h" #include "catalog/indexing.h" #include "catalog/objectaccess.h" #include "catalog/pg_constraint.h" @@ -563,6 +565,78 @@ ChooseConstraintName(const char *name1, const char *name2, return conname; } +/* + * Find and return a copy of the pg_constraint tuple that implements a + * validated not-null constraint for the given column of the given relation. + * If no such constraint exists, return NULL. + * + * XXX This would be easier if we had pg_attribute.notnullconstr with the OID + * of the constraint that implements the not-null constraint for that column. + * I'm not sure it's worth the catalog bloat and de-normalization, however. + */ +HeapTuple +findNotNullConstraintAttnum(Oid relid, AttrNumber attnum) +{ + Relation pg_constraint; + HeapTuple conTup, + retval = NULL; + SysScanDesc scan; + ScanKeyData key; + + pg_constraint = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + scan = systable_beginscan(pg_constraint, ConstraintRelidTypidNameIndexId, + true, NULL, 1, &key); + + while (HeapTupleIsValid(conTup = systable_getnext(scan))) + { + Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup); + AttrNumber conkey; + + /* + * We're looking for a NOTNULL constraint that's marked validated, + * with the column we're looking for as the sole element in conkey. + */ + if (con->contype != CONSTRAINT_NOTNULL) + continue; + if (!con->convalidated) + continue; + + conkey = extractNotNullColumn(conTup); + if (conkey != attnum) + continue; + + /* Found it */ + retval = heap_copytuple(conTup); + break; + } + + systable_endscan(scan); + table_close(pg_constraint, AccessShareLock); + + return retval; +} + +/* + * Find and return the pg_constraint tuple that implements a validated + * not-null constraint for the given column of the given relation. If + * no such column or no such constraint exists, return NULL. + */ +HeapTuple +findNotNullConstraint(Oid relid, const char *colname) +{ + AttrNumber attnum; + + attnum = get_attnum(relid, colname); + if (attnum <= InvalidAttrNumber) + return NULL; + + return findNotNullConstraintAttnum(relid, attnum); +} + /* * Find and return the pg_constraint tuple that implements a validated * not-null constraint for the given domain. @@ -607,6 +681,182 @@ findDomainNotNullConstraint(Oid typid) return retval; } +/* + * Given a pg_constraint tuple for a not-null constraint, return the column + * number it is for. + */ +AttrNumber +extractNotNullColumn(HeapTuple constrTup) +{ + AttrNumber colnum; + Datum adatum; + ArrayType *arr; + + /* only tuples for not-null constraints should be given */ + Assert(((Form_pg_constraint) GETSTRUCT(constrTup))->contype == CONSTRAINT_NOTNULL); + + adatum = SysCacheGetAttrNotNull(CONSTROID, constrTup, + Anum_pg_constraint_conkey); + arr = DatumGetArrayTypeP(adatum); /* ensure not toasted */ + if (ARR_NDIM(arr) != 1 || + ARR_HASNULL(arr) || + ARR_ELEMTYPE(arr) != INT2OID || + ARR_DIMS(arr)[0] != 1) + elog(ERROR, "conkey is not a 1-D smallint array"); + + memcpy(&colnum, ARR_DATA_PTR(arr), sizeof(AttrNumber)); + Assert(colnum > 0 && colnum <= MaxAttrNumber); + + if ((Pointer) arr != DatumGetPointer(adatum)) + pfree(arr); /* free de-toasted copy, if any */ + + return colnum; +} + +/* + * AdjustNotNullInheritance + * Adjust inheritance count for a single not-null constraint + * + * If no not-null constraint is found for the column, return false. + * Caller can create one. + * + * If the constraint does exist and matches the requested inheritability + * status, adjust its inheritance count and islocal status as requested, and + * return true. If the inheritability status doesn't match, an error is + * raised. + */ +bool +AdjustNotNullInheritance(Oid relid, AttrNumber attnum, int count, + bool is_local, bool is_no_inherit) +{ + HeapTuple tup; + + Assert(count == 0 || count == 1); + + tup = findNotNullConstraintAttnum(relid, attnum); + if (HeapTupleIsValid(tup)) + { + Relation pg_constraint; + Form_pg_constraint conform; + bool changed = false; + + pg_constraint = table_open(ConstraintRelationId, RowExclusiveLock); + conform = (Form_pg_constraint) GETSTRUCT(tup); + + /* + * If the NO INHERIT flag we're asked for doesn't match what the + * existing constraint has, throw an error. + */ + if (is_no_inherit != conform->connoinherit) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"", + NameStr(conform->conname), get_rel_name(relid))); + + if (count > 0) + { + conform->coninhcount += count; + changed = true; + } + if (is_local) + { + conform->conislocal = true; + changed = true; + } + + if (changed) + CatalogTupleUpdate(pg_constraint, &tup->t_self, tup); + + table_close(pg_constraint, RowExclusiveLock); + + return true; + } + + return false; +} + +/* + * RelationGetNotNullConstraints + * Return the list of not-null constraints for the given rel + * + * Caller can request cooked constraints, or raw. + * + * This is seldom needed, so we just scan pg_constraint each time. + * + * 'include_noinh' determines whether to include NO INHERIT constraints or not. + */ +List * +RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh) +{ + List *notnulls = NIL; + Relation constrRel; + HeapTuple htup; + SysScanDesc conscan; + ScanKeyData skey; + + constrRel = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&skey, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + conscan = systable_beginscan(constrRel, ConstraintRelidTypidNameIndexId, true, + NULL, 1, &skey); + + while (HeapTupleIsValid(htup = systable_getnext(conscan))) + { + Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(htup); + AttrNumber colnum; + + if (conForm->contype != CONSTRAINT_NOTNULL) + continue; + if (conForm->connoinherit && !include_noinh) + continue; + + colnum = extractNotNullColumn(htup); + + if (cooked) + { + CookedConstraint *cooked; + + cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint)); + + cooked->contype = CONSTR_NOTNULL; + cooked->name = pstrdup(NameStr(conForm->conname)); + cooked->attnum = colnum; + cooked->expr = NULL; + cooked->skip_validation = false; + cooked->is_local = true; + cooked->inhcount = 0; + cooked->is_no_inherit = conForm->connoinherit; + + notnulls = lappend(notnulls, cooked); + } + else + { + Constraint *constr; + + constr = makeNode(Constraint); + constr->contype = CONSTR_NOTNULL; + constr->conname = pstrdup(NameStr(conForm->conname)); + constr->deferrable = false; + constr->initdeferred = false; + constr->location = -1; + constr->keys = list_make1(makeString(get_attname(relid, colnum, + false))); + constr->skip_validation = false; + constr->initially_valid = true; + constr->is_no_inherit = conForm->connoinherit; + notnulls = lappend(notnulls, constr); + } + } + + systable_endscan(conscan); + table_close(constrRel, AccessShareLock); + + return notnulls; +} + + /* * Delete a single constraint record. */ diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index d27e6cf345..952785b332 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -362,7 +362,8 @@ static void truncate_check_activity(Relation rel); static void RangeVarCallbackForTruncate(const RangeVar *relation, Oid relId, Oid oldRelId, void *arg); static List *MergeAttributes(List *columns, const List *supers, char relpersistence, - bool is_partition, List **supconstr); + bool is_partition, List **supconstr, + List **supnotnulls); static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr); static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef); static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef); @@ -447,16 +448,14 @@ static bool check_for_column_name_collision(Relation rel, const char *colname, 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); -static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode); -static void ATPrepSetNotNull(List **wqueue, Relation rel, - AlterTableCmd *cmd, bool recurse, bool recursing, - LOCKMODE lockmode, - AlterTableUtilityContext *context); -static ObjectAddress ATExecSetNotNull(AlteredTableInfo *tab, Relation rel, - const char *colName, LOCKMODE lockmode); -static void ATExecCheckNotNull(AlteredTableInfo *tab, Relation rel, - const char *colName, LOCKMODE lockmode); +static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse, + LOCKMODE lockmode); +static void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum, + LOCKMODE lockmode); +static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel, + char *constrname, char *colName, + bool recurse, bool recursing, + LOCKMODE lockmode); static bool NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr); static bool ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *provenConstraint); @@ -488,6 +487,9 @@ static ObjectAddress ATExecDropColumn(List **wqueue, Relation rel, const char *c bool recurse, bool recursing, bool missing_ok, LOCKMODE lockmode, ObjectAddresses *addrs); +static void ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd, + bool recurse, LOCKMODE lockmode, + AlterTableUtilityContext *context); static ObjectAddress ATExecAddIndex(AlteredTableInfo *tab, Relation rel, IndexStmt *stmt, bool is_rebuild, LOCKMODE lockmode); static ObjectAddress ATExecAddStatistics(AlteredTableInfo *tab, Relation rel, @@ -499,11 +501,11 @@ static ObjectAddress ATExecAddConstraint(List **wqueue, static char *ChooseForeignKeyConstraintNameAddition(List *colnames); static ObjectAddress ATExecAddIndexConstraint(AlteredTableInfo *tab, Relation rel, IndexStmt *stmt, LOCKMODE lockmode); -static ObjectAddress ATAddCheckConstraint(List **wqueue, - AlteredTableInfo *tab, Relation rel, - Constraint *constr, - bool recurse, bool recursing, bool is_readd, - LOCKMODE lockmode); +static ObjectAddress ATAddCheckNNConstraint(List **wqueue, + AlteredTableInfo *tab, Relation rel, + Constraint *constr, + bool recurse, bool recursing, bool is_readd, + LOCKMODE lockmode); static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, Constraint *fkconstraint, bool recurse, bool recursing, @@ -560,9 +562,12 @@ static void GetForeignKeyCheckTriggers(Relation trigrel, Oid *insertTriggerOid, Oid *updateTriggerOid); static void ATExecDropConstraint(Relation rel, const char *constrName, - DropBehavior behavior, - bool recurse, bool recursing, + DropBehavior behavior, bool recurse, bool missing_ok, LOCKMODE lockmode); +static ObjectAddress dropconstraint_internal(Relation rel, + HeapTuple constraintTup, DropBehavior behavior, + bool recurse, bool recursing, + bool missing_ok, LOCKMODE lockmode); static void ATPrepAlterColumnType(List **wqueue, AlteredTableInfo *tab, Relation rel, bool recurse, bool recursing, @@ -660,6 +665,7 @@ static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl); static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, Relation partitionTbl); +static void verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partIdx); static List *GetParentedForeignKeyRefs(Relation partition); static void ATDetachCheckNoForeignKeyRefs(Relation partition); static char GetAttributeCompression(Oid atttypid, const char *compression); @@ -697,8 +703,10 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, TupleDesc descriptor; List *inheritOids; List *old_constraints; + List *old_notnulls; List *rawDefaults; List *cookedDefaults; + List *nncols; Datum reloptions; ListCell *listptr; AttrNumber attnum; @@ -883,12 +891,13 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, MergeAttributes(stmt->tableElts, inheritOids, stmt->relation->relpersistence, stmt->partbound != NULL, - &old_constraints); + &old_constraints, &old_notnulls); /* * Create a tuple descriptor from the relation schema. Note that this - * deals with column names, types, and not-null constraints, but not - * default values or CHECK constraints; we handle those below. + * deals with column names, types, and in-descriptor NOT NULL flags, but + * not default values, NOT NULL or CHECK constraints; we handle those + * below. */ descriptor = BuildDescForRelation(stmt->tableElts); @@ -1260,6 +1269,17 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, AddRelationNewConstraints(rel, NIL, stmt->constraints, true, true, false, queryString); + /* + * Finally, merge the not-null constraints that are declared directly with + * those that come from parent relations (making sure to count inheritance + * appropriately for each), create them, and set the attnotnull flag on + * columns that don't yet have it. + */ + nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints, + old_notnulls); + foreach_int(attrnum, nncols) + set_attnotnull(NULL, rel, attrnum, NoLock); + ObjectAddressSet(address, RelationRelationId, relationId); /* @@ -2391,6 +2411,8 @@ storage_name(char c) * Output arguments: * 'supconstr' receives a list of constraints belonging to the parents, * updated as necessary to be valid for the child. + * 'supnotnulls' receives a list of CookedConstraints that corresponds to + * constraints coming from inheritance parents. * * Return value: * Completed schema list. @@ -2421,7 +2443,10 @@ storage_name(char c) * * Constraints (including not-null constraints) for the child table * are the union of all relevant constraints, from both the child schema - * and parent tables. + * and parent tables. In addition, in legacy inheritance, each column that + * appears in a primary key in any of the parents also gets a NOT NULL + * constraint (partitioning doesn't need this, because the PK itself gets + * inherited.) * * The default value for a child column is defined as: * (1) If the child schema specifies a default, that value is used. @@ -2440,10 +2465,11 @@ storage_name(char c) */ static List * MergeAttributes(List *columns, const List *supers, char relpersistence, - bool is_partition, List **supconstr) + bool is_partition, List **supconstr, List **supnotnulls) { List *inh_columns = NIL; List *constraints = NIL; + List *nnconstraints = NIL; bool have_bogus_defaults = false; int child_attno; static Node bogus_marker = {0}; /* marks conflicting defaults */ @@ -2554,8 +2580,10 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, AttrMap *newattmap; List *inherited_defaults; List *cols_with_defaults; + List *nnconstrs; ListCell *lc1; ListCell *lc2; + Bitmapset *nncols = NULL; /* caller already got lock */ relation = table_open(parent, NoLock); @@ -2643,6 +2671,15 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, /* We can't process inherited defaults until newattmap is complete. */ inherited_defaults = cols_with_defaults = NIL; + /* + * Request attnotnull on columns that have a not-null constraint + * that's not marked NO INHERIT. + */ + nnconstrs = RelationGetNotNullConstraints(RelationGetRelid(relation), + true, false); + foreach_ptr(CookedConstraint, cc, nnconstrs) + nncols = bms_add_member(nncols, cc->attnum); + for (AttrNumber parent_attno = 1; parent_attno <= tupleDesc->natts; parent_attno++) { @@ -2664,7 +2701,6 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, */ newdef = makeColumnDef(attributeName, attribute->atttypid, attribute->atttypmod, attribute->attcollation); - newdef->is_not_null = attribute->attnotnull; newdef->storage = attribute->attstorage; newdef->generated = attribute->attgenerated; if (CompressionMethodIsValid(attribute->attcompression)) @@ -2712,6 +2748,12 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, mergeddef = newdef; } + /* + * mark attnotnull if parent has it + */ + if (bms_is_member(parent_attno, nncols)) + mergeddef->is_not_null = true; + /* * Locate default/generation expression if any */ @@ -2823,6 +2865,19 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, } } + /* + * Also copy the not-null constraints from this parent. The + * attnotnull markings were already installed above. + */ + foreach_ptr(CookedConstraint, nn, nnconstrs) + { + Assert(nn->contype == CONSTR_NOTNULL); + + nn->attnum = newattmap->attnums[nn->attnum - 1]; + + nnconstraints = lappend(nnconstraints, nn); + } + free_attrmap(newattmap); /* @@ -2893,8 +2948,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, /* * Now that we have the column definition list for a partition, we can * check whether the columns referenced in the column constraint specs - * actually exist. Also, we merge parent's not-null constraints and - * defaults into each corresponding column definition. + * actually exist. Also, merge column defaults. */ if (is_partition) { @@ -2911,7 +2965,6 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, if (strcmp(coldef->colname, restdef->colname) == 0) { found = true; - coldef->is_not_null |= restdef->is_not_null; /* * Check for conflicts related to generated columns. @@ -3000,6 +3053,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, } *supconstr = constraints; + *supnotnulls = nnconstraints; return columns; } @@ -3280,11 +3334,6 @@ MergeInheritedAttribute(List *inh_columns, format_type_with_typemod(prevtypeid, prevtypmod), format_type_with_typemod(newtypeid, newtypmod)))); - /* - * Merge of not-null constraints = OR 'em together - */ - prevdef->is_not_null |= newdef->is_not_null; - /* * Must have the same collation */ @@ -3920,7 +3969,10 @@ rename_constraint_internal(Oid myrelid, constraintOid); con = (Form_pg_constraint) GETSTRUCT(tuple); - if (myrelid && con->contype == CONSTRAINT_CHECK && !con->connoinherit) + if (myrelid && + (con->contype == CONSTRAINT_CHECK || + con->contype == CONSTRAINT_NOTNULL) && + !con->connoinherit) { if (recurse) { @@ -4675,15 +4727,6 @@ AlterTableGetLockLevel(List *cmds) cmd_lockmode = ShareUpdateExclusiveLock; break; - case AT_CheckNotNull: - - /* - * This only examines the table's schema; but lock must be - * strong enough to prevent concurrent DROP NOT NULL. - */ - cmd_lockmode = AccessShareLock; - break; - default: /* oops */ elog(ERROR, "unrecognized alter table type: %d", (int) cmd->subtype); @@ -4852,23 +4895,17 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, case AT_DropNotNull: /* ALTER COLUMN DROP NOT NULL */ ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - ATPrepDropNotNull(rel, recurse, recursing); - ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context); + /* Set up recursion for phase 2; no other prep needed */ + if (recurse) + cmd->recurse = true; pass = AT_PASS_DROP; break; case AT_SetNotNull: /* ALTER COLUMN SET NOT NULL */ ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - /* Need command-specific recursion decision */ - ATPrepSetNotNull(wqueue, rel, cmd, recurse, recursing, - lockmode, context); - pass = AT_PASS_COL_ATTRS; - break; - case AT_CheckNotNull: /* check column is already marked NOT NULL */ - ATSimplePermissions(cmd->subtype, rel, - ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context); - /* No command-specific prep needed */ + /* Set up recursion for phase 2; no other prep needed */ + if (recurse) + cmd->recurse = true; pass = AT_PASS_COL_ATTRS; break; case AT_SetExpression: /* ALTER COLUMN SET EXPRESSION */ @@ -4933,10 +4970,13 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, case AT_AddConstraint: /* ADD CONSTRAINT */ ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - /* Recursion occurs during execution phase */ - /* No command-specific prep needed except saving recurse flag */ + ATPrepAddPrimaryKey(wqueue, rel, cmd, recurse, lockmode, context); if (recurse) + { + /* recurses at exec time; lock descendants and set flag */ + (void) find_all_inheritors(RelationGetRelid(rel), lockmode, NULL); cmd->recurse = true; + } pass = AT_PASS_ADD_CONSTR; break; case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */ @@ -5252,13 +5292,11 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, address = ATExecDropIdentity(rel, cmd->name, cmd->missing_ok, lockmode, cmd->recurse, false); break; case AT_DropNotNull: /* ALTER COLUMN DROP NOT NULL */ - address = ATExecDropNotNull(rel, cmd->name, lockmode); + address = ATExecDropNotNull(rel, cmd->name, cmd->recurse, lockmode); break; case AT_SetNotNull: /* ALTER COLUMN SET NOT NULL */ - address = ATExecSetNotNull(tab, rel, cmd->name, lockmode); - break; - case AT_CheckNotNull: /* check column is already marked NOT NULL */ - ATExecCheckNotNull(tab, rel, cmd->name, lockmode); + address = ATExecSetNotNull(wqueue, rel, NULL, cmd->name, + cmd->recurse, false, lockmode); break; case AT_SetExpression: address = ATExecSetExpression(tab, rel, cmd->name, cmd->def, lockmode); @@ -5341,7 +5379,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, break; case AT_DropConstraint: /* DROP CONSTRAINT */ ATExecDropConstraint(rel, cmd->name, cmd->behavior, - cmd->recurse, false, + cmd->recurse, cmd->missing_ok, lockmode); break; case AT_AlterColumnType: /* ALTER COLUMN TYPE */ @@ -5604,21 +5642,10 @@ ATParseTransformCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, */ switch (cmd2->subtype) { - case AT_SetNotNull: - /* Need command-specific recursion decision */ - ATPrepSetNotNull(wqueue, rel, cmd2, - recurse, false, - lockmode, context); - pass = AT_PASS_COL_ATTRS; - break; case AT_AddIndex: - /* This command never recurses */ - /* No command-specific prep needed */ pass = AT_PASS_ADD_INDEX; break; case AT_AddIndexConstraint: - /* This command never recurses */ - /* No command-specific prep needed */ pass = AT_PASS_ADD_INDEXCONSTR; break; case AT_AddConstraint: @@ -5627,6 +5654,9 @@ ATParseTransformCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, cmd2->recurse = true; switch (castNode(Constraint, cmd2->def)->contype) { + case CONSTR_NOTNULL: + pass = AT_PASS_COL_ATTRS; + break; case CONSTR_PRIMARY: case CONSTR_UNIQUE: case CONSTR_EXCLUSION: @@ -6287,6 +6317,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode) RelationGetRelationName(oldrel)), errtableconstraint(oldrel, con->name))); break; + case CONSTR_NOTNULL: case CONSTR_FOREIGN: /* Nothing to do here */ break; @@ -6400,8 +6431,6 @@ alter_table_type_to_string(AlterTableType cmdtype) return "ALTER COLUMN ... SET EXPRESSION"; case AT_DropExpression: return "ALTER COLUMN ... DROP EXPRESSION"; - case AT_CheckNotNull: - return NULL; /* not real grammar */ case AT_SetStatistics: return "ALTER COLUMN ... SET STATISTICS"; case AT_SetOptions: @@ -7492,40 +7521,19 @@ add_column_collation_dependency(Oid relid, int32 attnum, Oid collid) /* * ALTER TABLE ALTER COLUMN DROP NOT NULL - */ - -static void -ATPrepDropNotNull(Relation rel, bool recurse, bool recursing) -{ - /* - * If the parent is a partitioned table, like check constraints, we do not - * support removing the NOT NULL while partitions exist. - */ - if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - { - PartitionDesc partdesc = RelationGetPartitionDesc(rel, true); - - Assert(partdesc != NULL); - if (partdesc->nparts > 0 && !recurse && !recursing) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("cannot remove constraint from only the partitioned table when partitions exist"), - errhint("Do not specify the ONLY keyword."))); - } -} - -/* + * * Return the address of the modified column. If the column was already * nullable, InvalidObjectAddress is returned. */ static ObjectAddress -ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) +ATExecDropNotNull(Relation rel, const char *colName, bool recurse, + LOCKMODE lockmode) { HeapTuple tuple; + HeapTuple conTup; Form_pg_attribute attTup; AttrNumber attnum; Relation attr_rel; - List *indexoidlist; ObjectAddress address; /* @@ -7541,6 +7549,15 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) colName, RelationGetRelationName(rel)))); attTup = (Form_pg_attribute) GETSTRUCT(tuple); attnum = attTup->attnum; + ObjectAddressSubSet(address, RelationRelationId, + RelationGetRelid(rel), attnum); + + /* If the column is already nullable there's nothing to do. */ + if (!attTup->attnotnull) + { + table_close(attr_rel, RowExclusiveLock); + return InvalidObjectAddress; + } /* Prevent them from altering a system attribute */ if (attnum <= 0) @@ -7556,60 +7573,37 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) colName, RelationGetRelationName(rel)))); /* - * Check that the attribute is not in a primary key or in an index used as - * a replica identity. - * - * Note: we'll throw error even if the pkey index is not valid. + * It's not OK to remove a constraint only for the parent and leave it in + * the children, so disallow that. */ - - /* Loop over all indexes on the relation */ - indexoidlist = RelationGetIndexList(rel); - - foreach_oid(indexoid, indexoidlist) + if (!recurse) { - HeapTuple indexTuple; - Form_pg_index indexStruct; - - indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid)); - if (!HeapTupleIsValid(indexTuple)) - elog(ERROR, "cache lookup failed for index %u", indexoid); - indexStruct = (Form_pg_index) GETSTRUCT(indexTuple); - - /* - * If the index is not a primary key or an index used as replica - * identity, skip the check. - */ - if (indexStruct->indisprimary || indexStruct->indisreplident) + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) { - /* - * Loop over each attribute in the primary key or the index used - * as replica identity and see if it matches the to-be-altered - * attribute. - */ - for (int i = 0; i < indexStruct->indnkeyatts; i++) - { - if (indexStruct->indkey.values[i] == attnum) - { - if (indexStruct->indisprimary) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("column \"%s\" is in a primary key", - colName))); - else - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("column \"%s\" is in index used as replica identity", - colName))); - } - } - } + PartitionDesc partdesc; - ReleaseSysCache(indexTuple); + partdesc = RelationGetPartitionDesc(rel, true); + + if (partdesc->nparts > 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot remove constraint from only the partitioned table when partitions exist"), + errhint("Do not specify the ONLY keyword.")); + } + else if (rel->rd_rel->relhassubclass && + find_inheritance_children(RelationGetRelid(rel), NoLock) != NIL) + { + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("not-null constraint on column \"%s\" must be removed in child tables too", + colName), + errhint("Do not specify the ONLY keyword.")); + } } - list_free(indexoidlist); - - /* If rel is partition, shouldn't drop NOT NULL if parent has the same */ + /* + * If rel is partition, shouldn't drop NOT NULL if parent has the same. + */ if (rel->rd_rel->relispartition) { Oid parentId = get_partition_parent(RelationGetRelid(rel), false); @@ -7627,19 +7621,18 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) } /* - * Okay, actually perform the catalog change ... if needed + * Find the constraint that makes this column NOT NULL, and drop it. + * dropconstraint_internal() resets attnotnull. */ - if (attTup->attnotnull) - { - attTup->attnotnull = false; + conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum); + if (conTup == NULL) + elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"", + colName, RelationGetRelationName(rel)); - CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple); - - ObjectAddressSubSet(address, RelationRelationId, - RelationGetRelid(rel), attnum); - } - else - address = InvalidObjectAddress; + /* The normal case: we have a pg_constraint row, remove it */ + dropconstraint_internal(rel, conTup, DROP_RESTRICT, recurse, false, + false, lockmode); + heap_freetuple(conTup); InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), attnum); @@ -7650,104 +7643,93 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) } /* - * ALTER TABLE ALTER COLUMN SET NOT NULL + * Helper to set pg_attribute.attnotnull if it isn't set, and to tell phase 3 + * to verify it. + * + * When called to alter an existing table, 'wqueue' must be given so that we + * can queue a check that existing tuples pass the constraint. When called + * from table creation, 'wqueue' should be passed as NULL. */ - static void -ATPrepSetNotNull(List **wqueue, Relation rel, - AlterTableCmd *cmd, bool recurse, bool recursing, - LOCKMODE lockmode, AlterTableUtilityContext *context) +set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum, + LOCKMODE lockmode) { - /* - * If we're already recursing, there's nothing to do; the topmost - * invocation of ATSimpleRecursion already visited all children. - */ - if (recursing) - return; + Oid reloid = RelationGetRelid(rel); + HeapTuple tuple; + Form_pg_attribute attForm; - /* - * If the target column is already marked NOT NULL, we can skip recursing - * to children, because their columns should already be marked NOT NULL as - * well. But there's no point in checking here unless the relation has - * some children; else we can just wait till execution to check. (If it - * does have children, however, this can save taking per-child locks - * unnecessarily. This greatly improves concurrency in some parallel - * restore scenarios.) - * - * Unfortunately, we can only apply this optimization to partitioned - * tables, because traditional inheritance doesn't enforce that child - * columns be NOT NULL when their parent is. (That's a bug that should - * get fixed someday.) - */ - if (rel->rd_rel->relhassubclass && - rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + CheckAlterTableIsSafe(rel); + + tuple = SearchSysCacheCopyAttNum(reloid, attnum); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, reloid); + attForm = (Form_pg_attribute) GETSTRUCT(tuple); + if (!attForm->attnotnull) { - HeapTuple tuple; - bool attnotnull; + Relation attr_rel; - tuple = SearchSysCacheAttName(RelationGetRelid(rel), cmd->name); + attr_rel = table_open(AttributeRelationId, RowExclusiveLock); - /* Might as well throw the error now, if name is bad */ - if (!HeapTupleIsValid(tuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", - cmd->name, RelationGetRelationName(rel)))); + attForm->attnotnull = true; + CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple); - attnotnull = ((Form_pg_attribute) GETSTRUCT(tuple))->attnotnull; - ReleaseSysCache(tuple); - if (attnotnull) - return; + if (wqueue && !NotNullImpliedByRelConstraints(rel, attForm)) + { + AlteredTableInfo *tab; + + tab = ATGetQueueEntry(wqueue, rel); + tab->verify_new_notnull = true; + } + + CommandCounterIncrement(); + + table_close(attr_rel, RowExclusiveLock); } - /* - * If we have ALTER TABLE ONLY ... SET NOT NULL on a partitioned table, - * apply ALTER TABLE ... CHECK NOT NULL to every child. Otherwise, use - * normal recursion logic. - */ - if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && - !recurse) - { - AlterTableCmd *newcmd = makeNode(AlterTableCmd); - - newcmd->subtype = AT_CheckNotNull; - newcmd->name = pstrdup(cmd->name); - ATSimpleRecursion(wqueue, rel, newcmd, true, lockmode, context); - } - else - ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context); + heap_freetuple(tuple); } /* - * Return the address of the modified column. If the column was already NOT - * NULL, InvalidObjectAddress is returned. + * ALTER TABLE ALTER COLUMN SET NOT NULL + * + * Add a not-null constraint to a single table and its children. Returns + * the address of the constraint added to the parent relation, if one gets + * added, or InvalidObjectAddress otherwise. + * + * We must recurse to child tables during execution, rather than using + * ALTER TABLE's normal prep-time recursion. */ static ObjectAddress -ATExecSetNotNull(AlteredTableInfo *tab, Relation rel, - const char *colName, LOCKMODE lockmode) +ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName, + bool recurse, bool recursing, LOCKMODE lockmode) { HeapTuple tuple; - Form_pg_attribute attTup; AttrNumber attnum; - Relation attr_rel; ObjectAddress address; + Constraint *constraint; + CookedConstraint *ccon; + List *cooked; + bool is_no_inherit = false; - /* - * lookup the attribute - */ - attr_rel = table_open(AttributeRelationId, RowExclusiveLock); + /* Guard against stack overflow due to overly deep inheritance tree. */ + check_stack_depth(); - tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName); + /* At top level, permission check was done in ATPrepCmd, else do it */ + if (recursing) + { + ATSimplePermissions(AT_AddConstraint, rel, + ATT_PARTITIONED_TABLE | ATT_TABLE | ATT_FOREIGN_TABLE); + Assert(conName != NULL); + } - if (!HeapTupleIsValid(tuple)) + attnum = get_attnum(RelationGetRelid(rel), colName); + if (attnum == InvalidAttrNumber) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), errmsg("column \"%s\" of relation \"%s\" does not exist", colName, RelationGetRelationName(rel)))); - attTup = (Form_pg_attribute) GETSTRUCT(tuple); - attnum = attTup->attnum; - /* Prevent them from altering a system attribute */ if (attnum <= 0) ereport(ERROR, @@ -7755,81 +7737,129 @@ ATExecSetNotNull(AlteredTableInfo *tab, Relation rel, errmsg("cannot alter system column \"%s\"", colName))); - /* - * Okay, actually perform the catalog change ... if needed - */ - if (!attTup->attnotnull) + /* See if there's already a constraint */ + tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum); + if (HeapTupleIsValid(tuple)) { - attTup->attnotnull = true; - - CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple); + Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple); + bool changed = false; /* - * Ordinarily phase 3 must ensure that no NULLs exist in columns that - * are set NOT NULL; however, if we can find a constraint which proves - * this then we can skip that. We needn't bother looking if we've - * already found that we must verify some other not-null constraint. + * Don't let a NO INHERIT constraint be changed into inherit. */ - if (!tab->verify_new_notnull && !NotNullImpliedByRelConstraints(rel, attTup)) + if (conForm->connoinherit && recurse) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"", + NameStr(conForm->conname), + RelationGetRelationName(rel))); + + /* + * If we find an appropriate constraint, we're almost done, but just + * need to change some properties on it: if we're recursing, increment + * coninhcount; if not, set conislocal if not already set. + */ + if (recursing) { - /* Tell Phase 3 it needs to test the constraint */ - tab->verify_new_notnull = true; + conForm->coninhcount++; + changed = true; + } + else if (!conForm->conislocal) + { + conForm->conislocal = true; + changed = true; } - ObjectAddressSubSet(address, RelationRelationId, - RelationGetRelid(rel), attnum); + if (changed) + { + Relation constr_rel; + + constr_rel = table_open(ConstraintRelationId, RowExclusiveLock); + + CatalogTupleUpdate(constr_rel, &tuple->t_self, tuple); + ObjectAddressSet(address, ConstraintRelationId, conForm->oid); + table_close(constr_rel, RowExclusiveLock); + } + + if (changed) + return address; + else + return InvalidObjectAddress; } - else - address = InvalidObjectAddress; + + /* + * If we're asked not to recurse, and children exist, raise an error for + * partitioned tables. For inheritance, we act as if NO INHERIT had been + * specified. + */ + if (!recurse && + find_inheritance_children(RelationGetRelid(rel), + NoLock) != NIL) + { + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraint must be added to child tables too"), + errhint("Do not specify the ONLY keyword.")); + else + is_no_inherit = true; + } + + /* + * No constraint exists; we must add one. First determine a name to use, + * if we haven't already. + */ + if (!recursing) + { + Assert(conName == NULL); + conName = ChooseConstraintName(RelationGetRelationName(rel), + colName, "not_null", + RelationGetNamespace(rel), + NIL); + } + + constraint = makeNotNullConstraint(makeString(colName)); + constraint->is_no_inherit = is_no_inherit; + constraint->inhcount = recursing ? 1 : 0; + constraint->conname = conName; + + /* and do it */ + cooked = AddRelationNewConstraints(rel, NIL, list_make1(constraint), + false, !recursing, false, NULL); + ccon = linitial(cooked); + ObjectAddressSet(address, ConstraintRelationId, ccon->conoid); InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), attnum); - table_close(attr_rel, RowExclusiveLock); + /* Mark pg_attribute.attnotnull for the column */ + set_attnotnull(wqueue, rel, attnum, lockmode); + + /* + * Recurse to propagate the constraint to children that don't have one. + */ + if (recurse) + { + List *children; + + children = find_inheritance_children(RelationGetRelid(rel), + lockmode); + + foreach_oid(childoid, children) + { + Relation childrel = table_open(childoid, NoLock); + + CommandCounterIncrement(); + + ATExecSetNotNull(wqueue, childrel, conName, colName, + recurse, true, lockmode); + table_close(childrel, NoLock); + } + } return address; } -/* - * ALTER TABLE ALTER COLUMN CHECK NOT NULL - * - * This doesn't exist in the grammar, but we generate AT_CheckNotNull - * commands against the partitions of a partitioned table if the user - * writes ALTER TABLE ONLY ... SET NOT NULL on the partitioned table, - * or tries to create a primary key on it (which internally creates - * AT_SetNotNull on the partitioned table). Such a command doesn't - * allow us to actually modify any partition, but we want to let it - * go through if the partitions are already properly marked. - * - * In future, this might need to adjust the child table's state, likely - * by incrementing an inheritance count for the attnotnull constraint. - * For now we need only check for the presence of the flag. - */ -static void -ATExecCheckNotNull(AlteredTableInfo *tab, Relation rel, - const char *colName, LOCKMODE lockmode) -{ - HeapTuple tuple; - - tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName); - - if (!HeapTupleIsValid(tuple)) - ereport(ERROR, - errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", - colName, RelationGetRelationName(rel))); - - if (!((Form_pg_attribute) GETSTRUCT(tuple))->attnotnull) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("constraint must be added to child tables too"), - errdetail("Column \"%s\" of relation \"%s\" is not already NOT NULL.", - colName, RelationGetRelationName(rel)), - errhint("Do not specify the ONLY keyword."))); - - ReleaseSysCache(tuple); -} - /* * NotNullImpliedByRelConstraints * Does rel's existing constraints imply NOT NULL for the given attribute? @@ -9134,6 +9164,72 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName, return object; } +/* + * Prepare to add a primary key on table, by adding not-null constraints + * on all columns. + */ +static void +ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd, + bool recurse, LOCKMODE lockmode, + AlterTableUtilityContext *context) +{ + ListCell *lc; + Constraint *pkconstr; + + pkconstr = castNode(Constraint, cmd->def); + if (pkconstr->contype != CONSTR_PRIMARY) + return; + + /* + * If not recursing, we must ensure that all children have a NOT NULL + * constraint on the columns, and error out if not. + */ + if (!recurse) + { + List *children; + + children = find_inheritance_children(RelationGetRelid(rel), + lockmode); + foreach_oid(childrelid, children) + { + foreach(lc, pkconstr->keys) + { + HeapTuple tup; + Form_pg_attribute attrForm; + char *attname = strVal(lfirst(lc)); + + tup = SearchSysCacheAttName(childrelid, attname); + if (!tup) + elog(ERROR, "cache lookup failed for attribute %s of relation %u", + attname, childrelid); + attrForm = (Form_pg_attribute) GETSTRUCT(tup); + if (!attrForm->attnotnull) + ereport(ERROR, + errmsg("column \"%s\" of table \"%s\" is not marked NOT NULL", + attname, get_rel_name(childrelid))); + ReleaseSysCache(tup); + } + } + } + + /* Insert not-null constraints in the queue for the PK columns */ + foreach(lc, pkconstr->keys) + { + AlterTableCmd *newcmd; + Constraint *nnconstr; + + nnconstr = makeNotNullConstraint(lfirst(lc)); + nnconstr->inhcount = 0; + + newcmd = makeNode(AlterTableCmd); + newcmd->subtype = AT_AddConstraint; + newcmd->recurse = true; + newcmd->def = (Node *) nnconstr; + + ATPrepCmd(wqueue, rel, newcmd, true, false, lockmode, context); + } +} + /* * ALTER TABLE ADD INDEX * @@ -9329,17 +9425,18 @@ ATExecAddConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, Assert(IsA(newConstraint, Constraint)); /* - * Currently, we only expect to see CONSTR_CHECK and CONSTR_FOREIGN nodes - * arriving here (see the preprocessing done in parse_utilcmd.c). Use a - * switch anyway to make it easier to add more code later. + * Currently, we only expect to see CONSTR_CHECK, CONSTR_NOTNULL and + * CONSTR_FOREIGN nodes arriving here (see the preprocessing done in + * parse_utilcmd.c). */ switch (newConstraint->contype) { case CONSTR_CHECK: + case CONSTR_NOTNULL: address = - ATAddCheckConstraint(wqueue, tab, rel, - newConstraint, recurse, false, is_readd, - lockmode); + ATAddCheckNNConstraint(wqueue, tab, rel, + newConstraint, recurse, false, is_readd, + lockmode); break; case CONSTR_FOREIGN: @@ -9420,9 +9517,9 @@ ChooseForeignKeyConstraintNameAddition(List *colnames) } /* - * Add a check constraint to a single table and its children. Returns the - * address of the constraint added to the parent relation, if one gets added, - * or InvalidObjectAddress otherwise. + * Add a check or not-null constraint to a single table and its children. + * Returns the address of the constraint added to the parent relation, + * if one gets added, or InvalidObjectAddress otherwise. * * Subroutine for ATExecAddConstraint. * @@ -9435,9 +9532,9 @@ ChooseForeignKeyConstraintNameAddition(List *colnames) * the parent table and pass that down. */ static ObjectAddress -ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, - Constraint *constr, bool recurse, bool recursing, - bool is_readd, LOCKMODE lockmode) +ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, + Constraint *constr, bool recurse, bool recursing, + bool is_readd, LOCKMODE lockmode) { List *newcons; ListCell *lcon; @@ -9445,6 +9542,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, ListCell *child; ObjectAddress address = InvalidObjectAddress; + /* Guard against stack overflow due to overly deep inheritance tree. */ + check_stack_depth(); + /* At top level, permission check was done in ATPrepCmd, else do it */ if (recursing) ATSimplePermissions(AT_AddConstraint, rel, @@ -9476,7 +9576,7 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, { CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon); - if (!ccon->skip_validation) + if (!ccon->skip_validation && ccon->contype != CONSTR_NOTNULL) { NewConstraint *newcon; @@ -9492,11 +9592,18 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, if (constr->conname == NULL) constr->conname = ccon->name; + /* + * If adding a not-null constraint, set the pg_attribute flag and tell + * phase 3 to verify existing rows, if needed. + */ + if (constr->contype == CONSTR_NOTNULL) + set_attnotnull(wqueue, rel, ccon->attnum, lockmode); + ObjectAddressSet(address, ConstraintRelationId, ccon->conoid); } /* At this point we must have a locked-down name to use */ - Assert(constr->conname != NULL); + Assert(newcons == NIL || constr->conname != NULL); /* Advance command counter in case same table is visited multiple times */ CommandCounterIncrement(); @@ -9526,7 +9633,7 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, /* * Check if ONLY was specified with ALTER TABLE. If so, allow the - * constraint creation only if there are no children currently. Error out + * constraint creation only if there are no children currently. Error out * otherwise. */ if (!recurse && children != NIL) @@ -9534,6 +9641,12 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("constraint must be added to child tables too"))); + /* + * The constraint must appear as inherited in children, so create a + * modified constraint object to use. + */ + constr = copyObject(constr); + constr->inhcount = 1; foreach(child, children) { Oid childrelid = lfirst_oid(child); @@ -9547,9 +9660,13 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, /* Find or create work queue entry for this table */ childtab = ATGetQueueEntry(wqueue, childrel); - /* Recurse to child */ - ATAddCheckConstraint(wqueue, childtab, childrel, - constr, recurse, true, is_readd, lockmode); + /* + * Recurse to child. XXX if we didn't create a constraint on the + * parent because it already existed, and we do create one on a child, + * should we return that child's constraint ObjectAddress here? + */ + ATAddCheckNNConstraint(wqueue, childtab, childrel, + constr, recurse, true, is_readd, lockmode); table_close(childrel, NoLock); } @@ -12579,24 +12696,14 @@ createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid, */ static void ATExecDropConstraint(Relation rel, const char *constrName, - DropBehavior behavior, - bool recurse, bool recursing, + DropBehavior behavior, bool recurse, bool missing_ok, LOCKMODE lockmode) { - List *children; Relation conrel; - Form_pg_constraint con; SysScanDesc scan; ScanKeyData skey[3]; HeapTuple tuple; bool found = false; - bool is_no_inherit_constraint = false; - char contype; - - /* At top level, permission check was done in ATPrepCmd, else do it */ - if (recursing) - ATSimplePermissions(AT_DropConstraint, rel, - ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); conrel = table_open(ConstraintRelationId, RowExclusiveLock); @@ -12621,47 +12728,8 @@ ATExecDropConstraint(Relation rel, const char *constrName, /* There can be at most one matching row */ if (HeapTupleIsValid(tuple = systable_getnext(scan))) { - ObjectAddress conobj; - - con = (Form_pg_constraint) GETSTRUCT(tuple); - - /* Don't drop inherited constraints */ - if (con->coninhcount > 0 && !recursing) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("cannot drop inherited constraint \"%s\" of relation \"%s\"", - constrName, RelationGetRelationName(rel)))); - - is_no_inherit_constraint = con->connoinherit; - contype = con->contype; - - /* - * If it's a foreign-key constraint, we'd better lock the referenced - * table and check that that's not in use, just as we've already done - * for the constrained table (else we might, eg, be dropping a trigger - * that has unfired events). But we can/must skip that in the - * self-referential case. - */ - if (contype == CONSTRAINT_FOREIGN && - con->confrelid != RelationGetRelid(rel)) - { - Relation frel; - - /* Must match lock taken by RemoveTriggerById: */ - frel = table_open(con->confrelid, AccessExclusiveLock); - CheckAlterTableIsSafe(frel); - table_close(frel, NoLock); - } - - /* - * Perform the actual constraint deletion - */ - conobj.classId = ConstraintRelationId; - conobj.objectId = con->oid; - conobj.objectSubId = 0; - - performDeletion(&conobj, behavior, 0); - + dropconstraint_internal(rel, tuple, behavior, recurse, false, + missing_ok, lockmode); found = true; } @@ -12670,31 +12738,155 @@ ATExecDropConstraint(Relation rel, const char *constrName, if (!found) { if (!missing_ok) - { ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("constraint \"%s\" of relation \"%s\" does not exist", - constrName, RelationGetRelationName(rel)))); - } + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + constrName, RelationGetRelationName(rel))); else - { ereport(NOTICE, - (errmsg("constraint \"%s\" of relation \"%s\" does not exist, skipping", - constrName, RelationGetRelationName(rel)))); - table_close(conrel, RowExclusiveLock); - return; + errmsg("constraint \"%s\" of relation \"%s\" does not exist, skipping", + constrName, RelationGetRelationName(rel))); + } + + table_close(conrel, RowExclusiveLock); +} + +/* + * Remove a constraint, using its pg_constraint tuple + * + * Implementation for ALTER TABLE DROP CONSTRAINT and ALTER TABLE ALTER COLUMN + * DROP NOT NULL. + * + * Returns the address of the constraint being removed. + */ +static ObjectAddress +dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior behavior, + bool recurse, bool recursing, bool missing_ok, + LOCKMODE lockmode) +{ + Relation conrel; + Form_pg_constraint con; + ObjectAddress conobj; + List *children; + bool is_no_inherit_constraint = false; + char *constrName; + char *colname = NULL; + + /* Guard against stack overflow due to overly deep inheritance tree. */ + check_stack_depth(); + + /* At top level, permission check was done in ATPrepCmd, else do it */ + if (recursing) + ATSimplePermissions(AT_DropConstraint, rel, + ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); + + conrel = table_open(ConstraintRelationId, RowExclusiveLock); + + con = (Form_pg_constraint) GETSTRUCT(constraintTup); + constrName = NameStr(con->conname); + + /* Don't allow drop of inherited constraints */ + if (con->coninhcount > 0 && !recursing) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot drop inherited constraint \"%s\" of relation \"%s\"", + constrName, RelationGetRelationName(rel)))); + + /* + * Reset pg_constraint.attnotnull, if this is a not-null constraint. + * + * While doing that, we're in a good position to disallow dropping a not- + * null constraint underneath a primary key, a replica identity index, or + * a generated identity column. + */ + if (con->contype == CONSTRAINT_NOTNULL) + { + Relation attrel = table_open(AttributeRelationId, RowExclusiveLock); + AttrNumber attnum = extractNotNullColumn(constraintTup); + Bitmapset *pkattrs; + Bitmapset *irattrs; + HeapTuple atttup; + Form_pg_attribute attForm; + + /* save column name for recursion step */ + colname = get_attname(RelationGetRelid(rel), attnum, false); + + /* Disallow if it's in the primary key */ + pkattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_PRIMARY_KEY); + if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, pkattrs)) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is in a primary key", + get_attname(RelationGetRelid(rel), attnum, false))); + + /* Disallow if it's in the replica identity */ + irattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_IDENTITY_KEY); + if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, irattrs)) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is in index used as replica identity", + get_attname(RelationGetRelid(rel), attnum, false))); + + /* Disallow if it's a GENERATED AS IDENTITY column */ + atttup = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum); + if (!HeapTupleIsValid(atttup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, RelationGetRelid(rel)); + attForm = (Form_pg_attribute) GETSTRUCT(atttup); + if (attForm->attidentity != '\0') + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("column \"%s\" of relation \"%s\" is an identity column", + get_attname(RelationGetRelid(rel), attnum, + false), + RelationGetRelationName(rel))); + + /* All good -- reset attnotnull if needed */ + if (attForm->attnotnull) + { + attForm->attnotnull = false; + CatalogTupleUpdate(attrel, &atttup->t_self, atttup); } + + table_close(attrel, RowExclusiveLock); + } + + is_no_inherit_constraint = con->connoinherit; + + /* + * If it's a foreign-key constraint, we'd better lock the referenced table + * and check that that's not in use, just as we've already done for the + * constrained table (else we might, eg, be dropping a trigger that has + * unfired events). But we can/must skip that in the self-referential + * case. + */ + if (con->contype == CONSTRAINT_FOREIGN && + con->confrelid != RelationGetRelid(rel)) + { + Relation frel; + + /* Must match lock taken by RemoveTriggerById: */ + frel = table_open(con->confrelid, AccessExclusiveLock); + CheckAlterTableIsSafe(frel); + table_close(frel, NoLock); } /* - * For partitioned tables, non-CHECK inherited constraints are dropped via - * the dependency mechanism, so we're done here. + * Perform the actual constraint deletion */ - if (contype != CONSTRAINT_CHECK && + ObjectAddressSet(conobj, ConstraintRelationId, con->oid); + performDeletion(&conobj, behavior, 0); + + /* + * For partitioned tables, non-CHECK, non-NOT-NULL inherited constraints + * are dropped via the dependency mechanism, so we're done here. + */ + if (con->contype != CONSTRAINT_CHECK && + con->contype != CONSTRAINT_NOTNULL && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) { table_close(conrel, RowExclusiveLock); - return; + return conobj; } /* @@ -12722,48 +12914,65 @@ ATExecDropConstraint(Relation rel, const char *constrName, foreach_oid(childrelid, children) { Relation childrel; - HeapTuple copy_tuple; + HeapTuple tuple; + Form_pg_constraint childcon; /* find_inheritance_children already got lock */ childrel = table_open(childrelid, NoLock); CheckAlterTableIsSafe(childrel); - ScanKeyInit(&skey[0], - Anum_pg_constraint_conrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(childrelid)); - ScanKeyInit(&skey[1], - Anum_pg_constraint_contypid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(InvalidOid)); - ScanKeyInit(&skey[2], - Anum_pg_constraint_conname, - BTEqualStrategyNumber, F_NAMEEQ, - CStringGetDatum(constrName)); - scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, - true, NULL, 3, skey); + /* + * We search for not-null constraints by column name, and others by + * constraint name. + */ + if (con->contype == CONSTRAINT_NOTNULL) + { + tuple = findNotNullConstraint(childrelid, colname); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u", + colname, RelationGetRelid(childrel)); + } + else + { + SysScanDesc scan; + ScanKeyData skey[3]; - /* There can be at most one matching row */ - if (!HeapTupleIsValid(tuple = systable_getnext(scan))) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("constraint \"%s\" of relation \"%s\" does not exist", - constrName, - RelationGetRelationName(childrel)))); + ScanKeyInit(&skey[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(childrelid)); + ScanKeyInit(&skey[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + ScanKeyInit(&skey[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(constrName)); + scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, + true, NULL, 3, skey); + /* There can only be one, so no need to loop */ + tuple = systable_getnext(scan); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + constrName, + RelationGetRelationName(childrel)))); + tuple = heap_copytuple(tuple); + systable_endscan(scan); + } - copy_tuple = heap_copytuple(tuple); + childcon = (Form_pg_constraint) GETSTRUCT(tuple); - systable_endscan(scan); + /* Right now only CHECK and not-null constraints can be inherited */ + if (childcon->contype != CONSTRAINT_CHECK && + childcon->contype != CONSTRAINT_NOTNULL) + elog(ERROR, "inherited constraint is not a CHECK or not-null constraint"); - con = (Form_pg_constraint) GETSTRUCT(copy_tuple); - - /* Right now only CHECK constraints can be inherited */ - if (con->contype != CONSTRAINT_CHECK) - elog(ERROR, "inherited constraint is not a CHECK constraint"); - - if (con->coninhcount <= 0) /* shouldn't happen */ + if (childcon->coninhcount <= 0) /* shouldn't happen */ elog(ERROR, "relation %u has non-inherited constraint \"%s\"", - childrelid, constrName); + childrelid, NameStr(childcon->conname)); if (recurse) { @@ -12771,18 +12980,18 @@ ATExecDropConstraint(Relation rel, const char *constrName, * If the child constraint has other definition sources, just * decrement its inheritance count; if not, recurse to delete it. */ - if (con->coninhcount == 1 && !con->conislocal) + if (childcon->coninhcount == 1 && !childcon->conislocal) { /* Time to delete this child constraint, too */ - ATExecDropConstraint(childrel, constrName, behavior, - true, true, - false, lockmode); + dropconstraint_internal(childrel, tuple, behavior, + recurse, true, missing_ok, + lockmode); } else { /* Child constraint must survive my deletion */ - con->coninhcount--; - CatalogTupleUpdate(conrel, ©_tuple->t_self, copy_tuple); + childcon->coninhcount--; + CatalogTupleUpdate(conrel, &tuple->t_self, tuple); /* Make update visible */ CommandCounterIncrement(); @@ -12791,25 +13000,29 @@ ATExecDropConstraint(Relation rel, const char *constrName, else { /* - * If we were told to drop ONLY in this table (no recursion), we - * need to mark the inheritors' constraints as locally defined - * rather than inherited. + * If we were told to drop ONLY in this table (no recursion) and + * there are no further parents for this constraint, we need to + * mark the inheritors' constraints as locally defined rather than + * inherited. */ - con->coninhcount--; - con->conislocal = true; + childcon->coninhcount--; + if (childcon->coninhcount == 0) + childcon->conislocal = true; - CatalogTupleUpdate(conrel, ©_tuple->t_self, copy_tuple); + CatalogTupleUpdate(conrel, &tuple->t_self, tuple); /* Make update visible */ CommandCounterIncrement(); } - heap_freetuple(copy_tuple); + heap_freetuple(tuple); table_close(childrel, NoLock); } table_close(conrel, RowExclusiveLock); + + return conobj; } /* @@ -13758,10 +13971,26 @@ RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab) char *defstring = pg_get_constraintdef_command(conoid); Oid indoid; - tab->changedConstraintOids = lappend_oid(tab->changedConstraintOids, - conoid); - tab->changedConstraintDefs = lappend(tab->changedConstraintDefs, - defstring); + /* + * It is critical to create not-null constraints ahead of primary key + * indexes; otherwise, the not-null constraint would be created by the + * primary key, and the constraint name would be wrong. + */ + if (get_constraint_type(conoid) == CONSTRAINT_NOTNULL) + { + tab->changedConstraintOids = lcons_oid(conoid, + tab->changedConstraintOids); + tab->changedConstraintDefs = lcons(defstring, + tab->changedConstraintDefs); + } + else + { + + tab->changedConstraintOids = lappend_oid(tab->changedConstraintOids, + conoid); + tab->changedConstraintDefs = lappend(tab->changedConstraintDefs, + defstring); + } /* * For the index of a constraint, if any, remember if it is used for @@ -13924,9 +14153,10 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) /* * If the constraint is inherited (only), we don't want to inject a - * new definition here; it'll get recreated when ATAddCheckConstraint - * recurses from adding the parent table's constraint. But we had to - * carry the info this far so that we can drop the constraint below. + * new definition here; it'll get recreated when + * ATAddCheckNNConstraint recurses from adding the parent table's + * constraint. But we had to carry the info this far so that we can + * drop the constraint below. */ if (!conislocal) continue; @@ -14165,23 +14395,21 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd, tab->subcmds[AT_PASS_OLD_CONSTR] = lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); - /* recreate any comment on the constraint */ - RebuildConstraintComment(tab, - AT_PASS_OLD_CONSTR, - oldId, - rel, - NIL, - con->conname); - } - else if (cmd->subtype == AT_SetNotNull) - { /* - * The parser will create AT_SetNotNull subcommands for - * columns of PRIMARY KEY indexes/constraints, but we need - * not do anything with them here, because the columns' - * NOT NULL marks will already have been propagated into - * the new table definition. + * Recreate any comment on the constraint. If we have + * recreated a primary key, then transformTableConstraint + * has added an unnamed not-null constraint here; skip + * this in that case. */ + if (con->conname) + RebuildConstraintComment(tab, + AT_PASS_OLD_CONSTR, + oldId, + rel, + NIL, + con->conname); + else + Assert(con->contype == CONSTR_NOTNULL); } else elog(ERROR, "unexpected statement subtype: %d", @@ -15935,14 +16163,24 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart RelationGetRelationName(child_rel), parent_attname))); /* - * Check child doesn't discard NOT NULL property. (Other - * constraints are checked elsewhere.) + * If the parent has a not-null constraint that's not NO INHERIT, + * make sure the child has one too. + * + * Other constraints are checked elsewhere. */ if (parent_att->attnotnull && !child_att->attnotnull) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("column \"%s\" in child table must be marked NOT NULL", - parent_attname))); + { + HeapTuple contup; + + contup = findNotNullConstraintAttnum(RelationGetRelid(parent_rel), + parent_att->attnum); + if (HeapTupleIsValid(contup) && + !((Form_pg_constraint) GETSTRUCT(contup))->connoinherit) + ereport(ERROR, + errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL", + parent_attname, RelationGetRelationName(child_rel))); + } /* * Child column must be generated if and only if parent column is. @@ -16043,7 +16281,8 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) HeapTuple child_tuple; bool found = false; - if (parent_con->contype != CONSTRAINT_CHECK) + if (parent_con->contype != CONSTRAINT_CHECK && + parent_con->contype != CONSTRAINT_NOTNULL) continue; /* if the parent's constraint is marked NO INHERIT, it's not inherited */ @@ -16063,21 +16302,50 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) Form_pg_constraint child_con = (Form_pg_constraint) GETSTRUCT(child_tuple); HeapTuple child_copy; - if (child_con->contype != CONSTRAINT_CHECK) + if (child_con->contype != parent_con->contype) continue; - if (strcmp(NameStr(parent_con->conname), - NameStr(child_con->conname)) != 0) - continue; + /* + * CHECK constraint are matched by name, NOT NULL ones by + * attribute number + */ + if (child_con->contype == CONSTRAINT_CHECK) + { + if (strcmp(NameStr(parent_con->conname), + NameStr(child_con->conname)) != 0) + continue; + } + else if (child_con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(parent_tuple); + AttrNumber child_attno = extractNotNullColumn(child_tuple); - if (!constraints_equivalent(parent_tuple, child_tuple, RelationGetDescr(constraintrel))) + if (strcmp(get_attname(parent_relid, parent_attno, false), + get_attname(RelationGetRelid(child_rel), child_attno, + false)) != 0) + continue; + } + + if (child_con->contype == CONSTRAINT_CHECK && + !constraints_equivalent(parent_tuple, child_tuple, RelationGetDescr(constraintrel))) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("child table \"%s\" has different definition for check constraint \"%s\"", RelationGetRelationName(child_rel), NameStr(parent_con->conname)))); - /* If the child constraint is "no inherit" then cannot merge */ - if (child_con->connoinherit) + /* + * If the CHECK child constraint is "no inherit" then cannot + * merge. + * + * This is not desirable for not-null constraints, mostly because + * it breaks our pg_upgrade strategy, but it also makes sense on + * its own: if a child has its own not-null constraint and then + * acquires a parent with the same constraint, then we start to + * enforce that constraint for all the descendants of that child + * too, if any. + */ + if (child_con->contype == CONSTRAINT_CHECK && + child_con->connoinherit) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("constraint \"%s\" conflicts with non-inherited constraint on child table \"%s\"", @@ -16105,6 +16373,29 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("too many inheritance parents")); + /* Also reset connoinherit for not-null, if the child has that */ + if (child_con->contype == CONSTRAINT_NOTNULL && + child_con->connoinherit) + { + /* + * If the child has children, it's not possible to turn a NO + * INHERIT constraint into an inheritable one: we would need + * to recurse to create constraints in those children, but + * this is not a good place to do that. + */ + if (child_rel->rd_rel->relhassubclass) + ereport(ERROR, + errmsg("cannot add NOT NULL constraint to column \"%s\" of relation \"%s\" with inheritance children", + get_attname(RelationGetRelid(child_rel), + extractNotNullColumn(child_tuple), + false), + RelationGetRelationName(child_rel)), + errdetail("Existing constraint \"%s\" is marked NO INHERIT.", + NameStr(child_con->conname))); + + child_con->connoinherit = false; + } + /* * In case of partitions, an inherited constraint must be * inherited only once since it cannot have multiple parents and @@ -16126,10 +16417,21 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) systable_endscan(child_scan); if (!found) + { + if (parent_con->contype == CONSTRAINT_NOTNULL) + ereport(ERROR, + errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL", + get_attname(parent_relid, + extractNotNullColumn(parent_tuple), + false), + RelationGetRelationName(child_rel))); + ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("child table is missing constraint \"%s\"", NameStr(parent_con->conname)))); + } } systable_endscan(parent_scan); @@ -16167,6 +16469,11 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) /* Off to RemoveInheritance() where most of the work happens */ RemoveInheritance(rel, parent_rel, false); + /* + * If the parent has a primary key, then we decrement counts for all NOT + * NULL constraints + */ + ObjectAddressSet(address, RelationRelationId, RelationGetRelid(parent_rel)); @@ -16275,6 +16582,7 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) HeapTuple attributeTuple, constraintTuple; List *connames; + List *nncolumns; bool found; bool is_partitioning; @@ -16343,6 +16651,8 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) * this, we first need a list of the names of the parent's check * constraints. (We cheat a bit by only checking for name matches, * assuming that the expressions will match.) + * + * For NOT NULL columns, we store column numbers to match. */ catalogRelation = table_open(ConstraintRelationId, RowExclusiveLock); ScanKeyInit(&key[0], @@ -16353,6 +16663,7 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) true, NULL, 1, key); connames = NIL; + nncolumns = NIL; while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) { @@ -16360,6 +16671,8 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) if (con->contype == CONSTRAINT_CHECK) connames = lappend(connames, pstrdup(NameStr(con->conname))); + if (con->contype == CONSTRAINT_NOTNULL) + nncolumns = lappend_int(nncolumns, extractNotNullColumn(constraintTuple)); } systable_endscan(scan); @@ -16375,20 +16688,39 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) { Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple); - bool match; + bool match = false; - if (con->contype != CONSTRAINT_CHECK) - continue; - - match = false; - foreach_ptr(char, chkname, connames) + /* + * Match CHECK constraints by name, not-null constraints by column + * number, and ignore all others. + */ + if (con->contype == CONSTRAINT_CHECK) { - if (strcmp(NameStr(con->conname), chkname) == 0) + foreach_ptr(char, chkname, connames) { - match = true; - break; + if (con->contype == CONSTRAINT_CHECK && + strcmp(NameStr(con->conname), chkname) == 0) + { + match = true; + break; + } } } + else if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber child_attno = extractNotNullColumn(constraintTuple); + + foreach_int(prevattno, nncolumns) + { + if (prevattno == child_attno) + { + match = true; + break; + } + } + } + else + continue; if (match) { @@ -18948,7 +19280,8 @@ AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel) /* * If no suitable index was found in the partition-to-be, create one - * now. + * now. Note that if this is a PK, not-null constraints must already + * exist. */ if (!found) { @@ -19589,7 +19922,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name) * DetachAddConstraintIfNeeded * Subroutine for ATExecDetachPartition. Create a constraint that * takes the place of the partition constraint, but avoid creating - * a dupe if an constraint already exists which implies the needed + * a dupe if a constraint already exists which implies the needed * constraint. */ static void @@ -19622,8 +19955,8 @@ DetachAddConstraintIfNeeded(List **wqueue, Relation partRel) n->initially_valid = true; n->skip_validation = true; /* It's a re-add, since it nominally already exists */ - ATAddCheckConstraint(wqueue, tab, partRel, n, - true, false, true, ShareUpdateExclusiveLock); + ATAddCheckNNConstraint(wqueue, tab, partRel, n, + true, false, true, ShareUpdateExclusiveLock); } } @@ -19892,6 +20225,13 @@ ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name) RelationGetRelationName(partIdx)))); } + /* + * If it's a primary key, make sure the columns in the partition are + * NOT NULL. + */ + if (parentIdx->rd_index->indisprimary) + verifyPartitionIndexNotNull(childInfo, partTbl); + /* All good -- do it */ IndexSetParentIndex(partIdx, RelationGetRelid(parentIdx)); if (OidIsValid(constraintOid)) @@ -20035,6 +20375,29 @@ validatePartitionedIndex(Relation partedIdx, Relation partedTbl) } } +/* + * When attaching an index as a partition of a partitioned index which is a + * primary key, verify that all the columns in the partition are marked NOT + * NULL. + */ +static void +verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition) +{ + for (int i = 0; i < iinfo->ii_NumIndexKeyAttrs; i++) + { + Form_pg_attribute att = TupleDescAttr(RelationGetDescr(partition), + iinfo->ii_IndexAttrNumbers[i] - 1); + + if (!att->attnotnull) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("invalid primary key definition"), + errdetail("Column \"%s\" of relation \"%s\" is not marked NOT NULL.", + NameStr(att->attname), + RelationGetRelationName(partition))); + } +} + /* * Return an OID list of constraints that reference the given relation * that are marked as having a parent constraints. diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 2a6550de90..859e2191f0 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -944,6 +944,10 @@ DefineDomain(CreateDomainStmt *stmt) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("conflicting NULL/NOT NULL constraints"))); + if (constr->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("not-null constraints for domains cannot be marked NO INHERIT")); typNotNull = true; nullDefined = true; break; diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 9cac3c1c27..fbce911b06 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -436,6 +436,30 @@ makeRangeVar(char *schemaname, char *relname, int location) return r; } +/* + * makeNotNullConstraint - + * creates a Constraint node for NOT NULL constraints + */ +Constraint * +makeNotNullConstraint(String *colname) +{ + Constraint *notnull; + + notnull = makeNode(Constraint); + notnull->contype = CONSTR_NOTNULL; + notnull->conname = NULL; + notnull->is_no_inherit = false; + notnull->inhcount = 0; + notnull->deferrable = false; + notnull->initdeferred = false; + notnull->location = -1; + notnull->keys = list_make1(colname); + notnull->skip_validation = false; + notnull->initially_valid = true; + + return notnull; +} + /* * makeTypeName - * build a TypeName node for an unqualified name. diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index b913f91ff0..37b0ca2e43 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -1698,6 +1698,8 @@ relation_excluded_by_constraints(PlannerInfo *root, * Currently, attnotnull constraints must be treated as NO INHERIT unless * this is a partitioned table. In future we might track their * inheritance status more accurately, allowing this to be refined. + * + * XXX do we need/want to change this? */ include_notnull = (!rte->inh || rte->relkind == RELKIND_PARTITIONED_TABLE); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index b1d4642c59..ba72cac9c0 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -3900,12 +3900,15 @@ ColConstraint: * or be part of a_expr NOT LIKE or similar constructs). */ ColConstraintElem: - NOT NULL_P + NOT NULL_P opt_no_inherit { Constraint *n = makeNode(Constraint); n->contype = CONSTR_NOTNULL; n->location = @1; + n->is_no_inherit = $3; + n->skip_validation = false; + n->initially_valid = true; $$ = (Node *) n; } | NULL_P @@ -4142,6 +4145,20 @@ ConstraintElem: n->initially_valid = !n->skip_validation; $$ = (Node *) n; } + | NOT NULL_P ColId ConstraintAttributeSpec + { + Constraint *n = makeNode(Constraint); + + n->contype = CONSTR_NOTNULL; + n->location = @1; + n->keys = list_make1(makeString($3)); + /* no NOT VALID support yet */ + processCASbits($4, @4, "NOT NULL", + NULL, NULL, NULL, + &n->is_no_inherit, yyscanner); + n->initially_valid = true; + $$ = (Node *) n; + } | UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace ConstraintAttributeSpec { @@ -4309,10 +4326,10 @@ DomainConstraintElem: n->contype = CONSTR_NOTNULL; n->location = @1; n->keys = list_make1(makeString("value")); - /* no NOT VALID support yet */ + /* no NOT VALID, NO INHERIT support */ processCASbits($3, @3, "NOT NULL", NULL, NULL, NULL, - &n->is_no_inherit, yyscanner); + NULL, yyscanner); n->initially_valid = true; $$ = (Node *) n; } diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 1e15ce10b4..eac78449d1 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -81,6 +81,7 @@ typedef struct bool isalter; /* true if altering existing table */ List *columns; /* ColumnDef items */ List *ckconstraints; /* CHECK constraints */ + List *nnconstraints; /* NOT NULL constraints */ List *fkconstraints; /* FOREIGN KEY constraints */ List *ixconstraints; /* index-creating constraints */ List *likeclauses; /* LIKE clauses that need post-processing */ @@ -240,6 +241,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) cxt.isalter = false; cxt.columns = NIL; cxt.ckconstraints = NIL; + cxt.nnconstraints = NIL; cxt.fkconstraints = NIL; cxt.ixconstraints = NIL; cxt.likeclauses = NIL; @@ -303,6 +305,32 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) Assert(stmt->constraints == NIL); + /* + * Before processing index constraints, which could include a primary key, + * we must scan all not-null constraints to propagate the is_not_null flag + * to each corresponding ColumnDef. This is necessary because table-level + * not-null constraints have not been marked in each ColumnDef, and the PK + * processing code needs to know whether one constraint has already been + * declared in order not to declare a redundant one. + */ + foreach_node(Constraint, nn, cxt.nnconstraints) + { + char *colname = strVal(linitial(nn->keys)); + + foreach_node(ColumnDef, cd, cxt.columns) + { + /* not our column? */ + if (strcmp(cd->colname, colname) != 0) + continue; + /* Already marked not-null? Nothing to do */ + if (cd->is_not_null) + break; + /* Bingo, we're done for this constraint */ + cd->is_not_null = true; + break; + } + } + /* * Postprocess constraints that give rise to index definitions. */ @@ -340,6 +368,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) */ stmt->tableElts = cxt.columns; stmt->constraints = cxt.ckconstraints; + stmt->nnconstraints = cxt.nnconstraints; result = lappend(cxt.blist, stmt); result = list_concat(result, cxt.alist); @@ -566,6 +595,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) bool saw_default; bool saw_identity; bool saw_generated; + bool need_notnull = false; ListCell *clist; cxt->columns = lappend(cxt->columns, column); @@ -663,10 +693,8 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) constraint->cooked_expr = NULL; column->constraints = lappend(column->constraints, constraint); - constraint = makeNode(Constraint); - constraint->contype = CONSTR_NOTNULL; - constraint->location = -1; - column->constraints = lappend(column->constraints, constraint); + /* have a not-null constraint added later */ + need_notnull = true; } /* Process column constraints, if any... */ @@ -684,7 +712,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) switch (constraint->contype) { case CONSTR_NULL: - if (saw_nullable && column->is_not_null) + if ((saw_nullable && column->is_not_null) || need_notnull) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("conflicting NULL/NOT NULL declarations for column \"%s\" of table \"%s\"", @@ -696,6 +724,14 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) break; case CONSTR_NOTNULL: + if (cxt->ispartitioned && constraint->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("not-null constraints on partitioned tables cannot be NO INHERIT")); + + /* + * Disallow conflicting [NOT] NULL markings + */ if (saw_nullable && !column->is_not_null) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), @@ -703,8 +739,25 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) column->colname, cxt->relation->relname), parser_errposition(cxt->pstate, constraint->location))); - column->is_not_null = true; - saw_nullable = true; + /* Ignore redundant NOT NULL markings */ + + /* + * If this is the first time we see this column being marked + * not null, add the constraint entry; and get rid of any + * previous markings to mark the column NOT NULL. + */ + if (!column->is_not_null) + { + column->is_not_null = true; + saw_nullable = true; + + constraint->keys = list_make1(makeString(column->colname)); + cxt->nnconstraints = lappend(cxt->nnconstraints, constraint); + + /* Don't need this anymore, if we had it */ + need_notnull = false; + } + break; case CONSTR_DEFAULT: @@ -754,16 +807,19 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) column->identity = constraint->generated_when; saw_identity = true; - /* An identity column is implicitly NOT NULL */ - if (saw_nullable && !column->is_not_null) + /* + * Identity columns are always NOT NULL, but we may have a + * constraint already. + */ + if (!saw_nullable) + need_notnull = true; + else if (!column->is_not_null) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("conflicting NULL/NOT NULL declarations for column \"%s\" of table \"%s\"", column->colname, cxt->relation->relname), parser_errposition(cxt->pstate, constraint->location))); - column->is_not_null = true; - saw_nullable = true; break; } @@ -790,6 +846,16 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) break; case CONSTR_PRIMARY: + /* primary key columns need a NOT NULL constraint */ + if (!saw_nullable) + need_notnull = true; + else if (!column->is_not_null) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting NULL/NOT NULL declarations for column \"%s\" of table \"%s\"", + column->colname, cxt->relation->relname), + parser_errposition(cxt->pstate, + constraint->location))); if (cxt->isforeign) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -869,6 +935,18 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) constraint->location))); } + /* + * If we need a not-null constraint for PRIMARY KEY, SERIAL or IDENTITY, + * and one was not explicitly specified, add one now. + */ + if (need_notnull && !(saw_nullable && column->is_not_null)) + { + column->is_not_null = true; + cxt->nnconstraints = + lappend(cxt->nnconstraints, + makeNotNullConstraint(makeString(column->colname))); + } + /* * If needed, generate ALTER FOREIGN TABLE ALTER COLUMN statement to add * per-column foreign data wrapper options to this column after creation. @@ -938,6 +1016,15 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint) cxt->ckconstraints = lappend(cxt->ckconstraints, constraint); break; + case CONSTR_NOTNULL: + if (cxt->ispartitioned && constraint->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("not-null constraints on partitioned tables cannot be NO INHERIT")); + + cxt->nnconstraints = lappend(cxt->nnconstraints, constraint); + break; + case CONSTR_FOREIGN: if (cxt->isforeign) ereport(ERROR, @@ -949,7 +1036,6 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint) break; case CONSTR_NULL: - case CONSTR_NOTNULL: case CONSTR_DEFAULT: case CONSTR_ATTR_DEFERRABLE: case CONSTR_ATTR_NOT_DEFERRABLE: @@ -1053,14 +1139,10 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla continue; /* - * Create a new column, which is marked as NOT inherited. - * - * For constraints, ONLY the not-null constraint is inherited by the - * new column definition per SQL99. + * Create a new column definition */ def = makeColumnDef(NameStr(attribute->attname), attribute->atttypid, attribute->atttypmod, attribute->attcollation); - def->is_not_null = attribute->attnotnull; /* * Add to column list @@ -1129,14 +1211,28 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla } } + /* + * Reproduce not-null constraints, if any, by copying them. We do this + * regardless of options given. + */ + if (tupleDesc->constr && tupleDesc->constr->has_not_null) + { + List *lst; + + lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false, + true); + cxt->nnconstraints = list_concat(cxt->nnconstraints, lst); + } + /* * We cannot yet deal with defaults, CHECK constraints, indexes, or * statistics, since we don't yet know what column numbers the copied * columns will have in the finished table. If any of those options are * specified, add the LIKE clause to cxt->likeclauses so that - * expandTableLikeClause will be called after we do know that. Also, - * remember the relation OID so that expandTableLikeClause is certain to - * open the same table. + * expandTableLikeClause will be called after we do know that. + * + * In order for this to work, we remember the relation OID so that + * expandTableLikeClause is certain to open the same table. */ if (table_like_clause->options & (CREATE_TABLE_LIKE_DEFAULTS | @@ -1506,8 +1602,8 @@ transformOfType(CreateStmtContext *cxt, TypeName *ofTypename) * with the index there. * * Unlike transformIndexConstraint, we don't make any effort to force primary - * key columns to be NOT NULL. The larger cloning process this is part of - * should have cloned their NOT NULL status separately (and DefineIndex will + * key columns to be not-null. The larger cloning process this is part of + * should have cloned their not-null status separately (and DefineIndex will * complain if that fails to happen). */ IndexStmt * @@ -2066,10 +2162,12 @@ transformIndexConstraints(CreateStmtContext *cxt) ListCell *lc; /* - * Run through the constraints that need to generate an index. For PRIMARY - * KEY, mark each column as NOT NULL and create an index. For UNIQUE or - * EXCLUDE, create an index as for PRIMARY KEY, but do not insist on NOT - * NULL. + * Run through the constraints that need to generate an index, and do so. + * + * For PRIMARY KEY, in addition we set each column's attnotnull flag true. + * We do not create a separate not-null constraint, as that would be + * redundant: the PRIMARY KEY constraint itself fulfills that role. Other + * constraint types don't need any not-null markings. */ foreach(lc, cxt->ixconstraints) { @@ -2143,9 +2241,7 @@ transformIndexConstraints(CreateStmtContext *cxt) } /* - * Now append all the IndexStmts to cxt->alist. If we generated an ALTER - * TABLE SET NOT NULL statement to support a primary key, it's already in - * cxt->alist. + * Now append all the IndexStmts to cxt->alist. */ cxt->alist = list_concat(cxt->alist, finalindexlist); } @@ -2153,18 +2249,15 @@ transformIndexConstraints(CreateStmtContext *cxt) /* * transformIndexConstraint * Transform one UNIQUE, PRIMARY KEY, or EXCLUDE constraint for - * transformIndexConstraints. + * transformIndexConstraints. An IndexStmt is returned. * - * We return an IndexStmt. For a PRIMARY KEY constraint, we additionally - * produce not-null constraints, either by marking ColumnDefs in cxt->columns - * as is_not_null or by adding an ALTER TABLE SET NOT NULL command to - * cxt->alist. + * For a PRIMARY KEY constraint, we additionally create not-null constraints + * for columns that don't already have them. */ static IndexStmt * transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) { IndexStmt *index; - List *notnullcmds = NIL; ListCell *lc; index = makeNode(IndexStmt); @@ -2384,6 +2477,11 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) errdetail("Cannot create a primary key or unique constraint using such an index."), parser_errposition(cxt->pstate, constraint->location))); + /* Ensure these columns get a NOT NULL constraint */ + cxt->nnconstraints = + lappend(cxt->nnconstraints, + makeNotNullConstraint(makeString(attname))); + constraint->keys = lappend(constraint->keys, makeString(attname)); } else @@ -2422,7 +2520,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) * For UNIQUE and PRIMARY KEY, we just have a list of column names. * * Make sure referenced keys exist. If we are making a PRIMARY KEY index, - * also make sure they are NOT NULL. For WITHOUT OVERLAPS constraints, we + * also make sure they are not-null. For WITHOUT OVERLAPS constraints, we * make sure the last part is a range or multirange. */ else @@ -2431,7 +2529,6 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) { char *key = strVal(lfirst(lc)); bool found = false; - bool forced_not_null = false; ColumnDef *column = NULL; ListCell *columns; IndexElem *iparam; @@ -2454,15 +2551,17 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) { /* * column is defined in the new table. For PRIMARY KEY, we - * can apply the not-null constraint cheaply here ... unless - * the column is marked is_from_type, in which case marking it - * here would be ineffective (see MergeAttributes). + * can apply the not-null constraint cheaply here. Note that + * this isn't effective in ALTER TABLE, unless the column is + * being added in the same command. */ if (constraint->contype == CONSTR_PRIMARY && - !column->is_from_type) + !column->is_not_null) { column->is_not_null = true; - forced_not_null = true; + cxt->nnconstraints = + lappend(cxt->nnconstraints, + makeNotNullConstraint(makeString(key))); } } else if (SystemAttributeByName(key) != NULL) @@ -2470,7 +2569,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) /* * column will be a system column in the new table, so accept * it. System columns can't ever be null, so no need to worry - * about PRIMARY/not-null constraint. + * about PRIMARY/NOT NULL constraint. */ found = true; } @@ -2507,13 +2606,9 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) found = true; typid = inhattr->atttypid; - /* - * It's tempting to set forced_not_null if the - * parent column is already NOT NULL, but that - * seems unsafe because the column's NOT NULL - * marking might disappear between now and - * execution. Do the runtime check to be safe. - */ + cxt->nnconstraints = + lappend(cxt->nnconstraints, + makeNotNullConstraint(makeString(pstrdup(inhname)))); break; } } @@ -2611,18 +2706,6 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; index->indexParams = lappend(index->indexParams, iparam); - /* - * For a primary-key column, also create an item for ALTER TABLE - * SET NOT NULL if we couldn't ensure it via is_not_null above. - */ - if (constraint->contype == CONSTR_PRIMARY && !forced_not_null) - { - AlterTableCmd *notnullcmd = makeNode(AlterTableCmd); - - notnullcmd->subtype = AT_SetNotNull; - notnullcmd->name = pstrdup(key); - notnullcmds = lappend(notnullcmds, notnullcmd); - } } if (constraint->without_overlaps) @@ -2741,22 +2824,6 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) index->indexIncludingParams = lappend(index->indexIncludingParams, iparam); } - /* - * If we found anything that requires run-time SET NOT NULL, build a full - * ALTER TABLE command for that and add it to cxt->alist. - */ - if (notnullcmds) - { - AlterTableStmt *alterstmt = makeNode(AlterTableStmt); - - alterstmt->relation = copyObject(cxt->relation); - alterstmt->cmds = notnullcmds; - alterstmt->objtype = OBJECT_TABLE; - alterstmt->missing_ok = false; - - cxt->alist = lappend(cxt->alist, alterstmt); - } - return index; } @@ -3395,6 +3462,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, cxt.isalter = true; cxt.columns = NIL; cxt.ckconstraints = NIL; + cxt.nnconstraints = NIL; cxt.fkconstraints = NIL; cxt.ixconstraints = NIL; cxt.likeclauses = NIL; @@ -3644,9 +3712,8 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, Node *istmt = (Node *) lfirst(l); /* - * We assume here that cxt.alist contains only IndexStmts and possibly - * ALTER TABLE SET NOT NULL statements generated from primary key - * constraints. We absorb the subcommands of the latter directly. + * We assume here that cxt.alist contains only IndexStmts generated + * from primary key constraints. */ if (IsA(istmt, IndexStmt)) { @@ -3658,30 +3725,31 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, newcmd->def = (Node *) idxstmt; newcmds = lappend(newcmds, newcmd); } - else if (IsA(istmt, AlterTableStmt)) - { - AlterTableStmt *alterstmt = (AlterTableStmt *) istmt; - - newcmds = list_concat(newcmds, alterstmt->cmds); - } else elog(ERROR, "unexpected stmt type %d", (int) nodeTag(istmt)); } cxt.alist = NIL; - /* Append any CHECK or FK constraints to the commands list */ - foreach(l, cxt.ckconstraints) + /* Append any CHECK, NOT NULL or FK constraints to the commands list */ + foreach_node(Constraint, def, cxt.ckconstraints) { newcmd = makeNode(AlterTableCmd); newcmd->subtype = AT_AddConstraint; - newcmd->def = (Node *) lfirst_node(Constraint, l); + newcmd->def = (Node *) def; newcmds = lappend(newcmds, newcmd); } - foreach(l, cxt.fkconstraints) + foreach_node(Constraint, def, cxt.nnconstraints) { newcmd = makeNode(AlterTableCmd); newcmd->subtype = AT_AddConstraint; - newcmd->def = (Node *) lfirst_node(Constraint, l); + newcmd->def = (Node *) def; + newcmds = lappend(newcmds, newcmd); + } + foreach_node(Constraint, def, cxt.fkconstraints) + { + newcmd = makeNode(AlterTableCmd); + newcmd->subtype = AT_AddConstraint; + newcmd->def = (Node *) def; newcmds = lappend(newcmds, newcmd); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 2177d17e27..a39068d1bf 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2516,6 +2516,28 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, conForm->connoinherit ? " NO INHERIT" : ""); break; } + case CONSTRAINT_NOTNULL: + { + if (conForm->conrelid) + { + AttrNumber attnum; + + attnum = extractNotNullColumn(tup); + + appendStringInfo(&buf, "NOT NULL %s", + quote_identifier(get_attname(conForm->conrelid, + attnum, false))); + if (((Form_pg_constraint) GETSTRUCT(tup))->connoinherit) + appendStringInfoString(&buf, " NO INHERIT"); + } + else if (conForm->contypid) + { + /* conkey is null for domain not-null constraints */ + appendStringInfoString(&buf, "NOT NULL"); + } + break; + } + case CONSTRAINT_TRIGGER: /* diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index c323b5bd3d..ff2a0cde8d 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -85,7 +85,8 @@ static catalogid_hash *catalogIdHash = NULL; static void flagInhTables(Archive *fout, TableInfo *tblinfo, int numTables, InhInfo *inhinfo, int numInherits); static void flagInhIndexes(Archive *fout, TableInfo *tblinfo, int numTables); -static void flagInhAttrs(Archive *fout, TableInfo *tblinfo, int numTables); +static void flagInhAttrs(Archive *fout, DumpOptions *dopt, TableInfo *tblinfo, + int numTables); static int strInArray(const char *pattern, char **arr, int arr_size); static IndxInfo *findIndexByOid(Oid oid); @@ -206,7 +207,7 @@ getSchemaData(Archive *fout, int *numTablesPtr) getTableAttrs(fout, tblinfo, numTables); pg_log_info("flagging inherited columns in subtables"); - flagInhAttrs(fout, tblinfo, numTables); + flagInhAttrs(fout, fout->dopt, tblinfo, numTables); pg_log_info("reading partitioning data"); getPartitioningInfo(fout); @@ -454,7 +455,8 @@ flagInhIndexes(Archive *fout, TableInfo tblinfo[], int numTables) * What we need to do here is: * * - Detect child columns that inherit NOT NULL bits from their parents, so - * that we needn't specify that again for the child. + * that we needn't specify that again for the child. (Versions >= 18 no + * longer need this.) * * - Detect child columns that have DEFAULT NULL when their parents had some * non-null default. In this case, we make up a dummy AttrDefInfo object so @@ -474,9 +476,8 @@ flagInhIndexes(Archive *fout, TableInfo tblinfo[], int numTables) * modifies tblinfo */ static void -flagInhAttrs(Archive *fout, TableInfo *tblinfo, int numTables) +flagInhAttrs(Archive *fout, DumpOptions *dopt, TableInfo *tblinfo, int numTables) { - DumpOptions *dopt = fout->dopt; int i, j, k; @@ -538,7 +539,16 @@ flagInhAttrs(Archive *fout, TableInfo *tblinfo, int numTables) { AttrDefInfo *parentDef = parent->attrdefs[inhAttrInd]; - foundNotNull |= parent->notnull[inhAttrInd]; + /* + * Account for each parent having a not-null constraint + * not marked NO INHERIT on this column. In versions 18 + * and later, we don't need this. + */ + if (fout->remoteVersion < 180000 && + parent->notnull_constrs[inhAttrInd] != NULL && + !parent->notnull_noinh[inhAttrInd]) + foundNotNull = true; + foundDefault |= (parentDef != NULL && strcmp(parentDef->adef_expr, "NULL") != 0 && !parent->attgenerated[inhAttrInd]); @@ -556,8 +566,13 @@ flagInhAttrs(Archive *fout, TableInfo *tblinfo, int numTables) } } - /* Remember if we found inherited NOT NULL */ - tbinfo->inhNotNull[j] = foundNotNull; + /* + * In versions < 18, for lack of a better system, we arbitrarily + * decide that a not-null constraint is not locally defined if at + * least one of the parents has it. + */ + if (fout->remoteVersion < 180000 && !foundNotNull) + tbinfo->notnull_islocal[j] = false; /* * Manufacture a DEFAULT NULL clause if necessary. This breaks diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 130b80775d..4d6a1fbdd9 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -345,6 +345,10 @@ static void getTableData(DumpOptions *dopt, TableInfo *tblinfo, int numTables, c static void makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo); static void buildMatViewRefreshDependencies(Archive *fout); static void getTableDataFKConstraints(void); +static void determineNotNullFlags(Archive *fout, PGresult *res, int r, + TableInfo *tbinfo, int j, + int i_notnull_name, int i_notnull_noinherit, + int i_notnull_islocal); static char *format_function_arguments(const FuncInfo *finfo, const char *funcargs, bool is_agg); static char *format_function_signature(Archive *fout, @@ -8736,7 +8740,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) int i_attlen; int i_attalign; int i_attislocal; - int i_attnotnull; + int i_notnull_name; + int i_notnull_noinherit; + int i_notnull_islocal; int i_attoptions; int i_attcollation; int i_attcompression; @@ -8746,13 +8752,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) /* * We want to perform just one query against pg_attribute, and then just - * one against pg_attrdef (for DEFAULTs) and one against pg_constraint - * (for CHECK constraints). However, we mustn't try to select every row - * of those catalogs and then sort it out on the client side, because some - * of the server-side functions we need would be unsafe to apply to tables - * we don't have lock on. Hence, we build an array of the OIDs of tables - * we care about (and now have lock on!), and use a WHERE clause to - * constrain which rows are selected. + * one against pg_attrdef (for DEFAULTs) and two against pg_constraint + * (for CHECK constraints and for NOT NULL constraints). However, we + * mustn't try to select every row of those catalogs and then sort it out + * on the client side, because some of the server-side functions we need + * would be unsafe to apply to tables we don't have lock on. Hence, we + * build an array of the OIDs of tables we care about (and now have lock + * on!), and use a WHERE clause to constrain which rows are selected. */ appendPQExpBufferChar(tbloids, '{'); appendPQExpBufferChar(checkoids, '{'); @@ -8799,7 +8805,6 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) "a.attstattarget,\n" "a.attstorage,\n" "t.typstorage,\n" - "a.attnotnull,\n" "a.atthasdef,\n" "a.attisdropped,\n" "a.attlen,\n" @@ -8816,6 +8821,30 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) "ORDER BY option_name" "), E',\n ') AS attfdwoptions,\n"); + /* + * Find out any NOT NULL markings for each column. In 18 and up we read + * pg_constraint to obtain the constraint name. notnull_noinherit is set + * according to the NO INHERIT property. For versions prior to 18, we + * store an empty string as the name when a constraint is marked as + * attnotnull (this cues dumpTableSchema to print the NOT NULL clause + * without a name); also, such cases are never NO INHERIT. + * + * We track in notnull_islocal whether the constraint was defined directly + * in this table or via an ancestor, for binary upgrade. flagInhAttrs + * might modify this later for servers older than 18; it's also in charge + * of determining the correct inhcount. + */ + if (fout->remoteVersion >= 180000) + appendPQExpBufferStr(q, + "co.conname AS notnull_name,\n" + "co.connoinherit AS notnull_noinherit,\n" + "co.conislocal AS notnull_islocal,\n"); + else + appendPQExpBufferStr(q, + "CASE WHEN a.attnotnull THEN '' ELSE NULL END AS notnull_name,\n" + "false AS notnull_noinherit,\n" + "a.attislocal AS notnull_islocal,\n"); + if (fout->remoteVersion >= 140000) appendPQExpBufferStr(q, "a.attcompression AS attcompression,\n"); @@ -8850,11 +8879,25 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n" "JOIN pg_catalog.pg_attribute a ON (src.tbloid = a.attrelid) " "LEFT JOIN pg_catalog.pg_type t " - "ON (a.atttypid = t.oid)\n" - "WHERE a.attnum > 0::pg_catalog.int2\n" - "ORDER BY a.attrelid, a.attnum", + "ON (a.atttypid = t.oid)\n", tbloids->data); + /* + * In versions 18 and up, we need pg_constraint for explicit NOT NULL + * entries. Also, we need to know if the NOT NULL for each column is + * backing a primary key. + */ + if (fout->remoteVersion >= 180000) + appendPQExpBufferStr(q, + " LEFT JOIN pg_catalog.pg_constraint co ON " + "(a.attrelid = co.conrelid\n" + " AND co.contype = 'n' AND " + "co.conkey = array[a.attnum])\n"); + + appendPQExpBufferStr(q, + "WHERE a.attnum > 0::pg_catalog.int2\n" + "ORDER BY a.attrelid, a.attnum"); + res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK); ntups = PQntuples(res); @@ -8872,7 +8915,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) i_attlen = PQfnumber(res, "attlen"); i_attalign = PQfnumber(res, "attalign"); i_attislocal = PQfnumber(res, "attislocal"); - i_attnotnull = PQfnumber(res, "attnotnull"); + i_notnull_name = PQfnumber(res, "notnull_name"); + i_notnull_noinherit = PQfnumber(res, "notnull_noinherit"); + i_notnull_islocal = PQfnumber(res, "notnull_islocal"); i_attoptions = PQfnumber(res, "attoptions"); i_attcollation = PQfnumber(res, "attcollation"); i_attcompression = PQfnumber(res, "attcompression"); @@ -8937,8 +8982,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) tbinfo->attcompression = (char *) pg_malloc(numatts * sizeof(char)); tbinfo->attfdwoptions = (char **) pg_malloc(numatts * sizeof(char *)); tbinfo->attmissingval = (char **) pg_malloc(numatts * sizeof(char *)); - tbinfo->notnull = (bool *) pg_malloc(numatts * sizeof(bool)); - tbinfo->inhNotNull = (bool *) pg_malloc(numatts * sizeof(bool)); + tbinfo->notnull_constrs = (char **) pg_malloc(numatts * sizeof(char *)); + tbinfo->notnull_noinh = (bool *) pg_malloc(numatts * sizeof(bool)); + tbinfo->notnull_islocal = (bool *) pg_malloc(numatts * sizeof(bool)); tbinfo->attrdefs = (AttrDefInfo **) pg_malloc(numatts * sizeof(AttrDefInfo *)); hasdefaults = false; @@ -8962,7 +9008,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen)); tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign)); tbinfo->attislocal[j] = (PQgetvalue(res, r, i_attislocal)[0] == 't'); - tbinfo->notnull[j] = (PQgetvalue(res, r, i_attnotnull)[0] == 't'); + + /* Handle not-null constraint name and flags */ + determineNotNullFlags(fout, res, r, + tbinfo, j, + i_notnull_name, i_notnull_noinherit, + i_notnull_islocal); + tbinfo->attoptions[j] = pg_strdup(PQgetvalue(res, r, i_attoptions)); tbinfo->attcollation[j] = atooid(PQgetvalue(res, r, i_attcollation)); tbinfo->attcompression[j] = *(PQgetvalue(res, r, i_attcompression)); @@ -8971,8 +9023,6 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) tbinfo->attrdefs[j] = NULL; /* fix below */ if (PQgetvalue(res, r, i_atthasdef)[0] == 't') hasdefaults = true; - /* these flags will be set in flagInhAttrs() */ - tbinfo->inhNotNull[j] = false; } if (hasdefaults) @@ -9253,6 +9303,110 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) destroyPQExpBuffer(checkoids); } +/* + * Based on the getTableAttrs query's row corresponding to one column, set + * the name and flags to handle a not-null constraint for that column in + * the tbinfo struct. + * + * Result row 'r' is for tbinfo's attribute 'j'. + * + * There are three possibilities: + * 1) the column has no not-null constraints. In that case, ->notnull_constrs + * (the constraint name) remains NULL. + * 2) The column has a constraint with no name (this is the case when + * constraints come from pre-18 servers). In this case, ->notnull_constrs + * is set to the empty string; dumpTableSchema will print just "NOT NULL". + * 3) The column has a constraint with a known name; in that case + * notnull_constrs carries that name and dumpTableSchema will print + * "CONSTRAINT the_name NOT NULL". However, if the name is the default + * (table_column_not_null), there's no need to print that name in the dump, + * so notnull_constrs is set to the empty string and it behaves as the case + * above. + * + * In a child table that inherits from a parent already containing NOT NULL + * constraints and the columns in the child don't have their own NOT NULL + * declarations, we suppress printing constraints in the child: the + * constraints are acquired at the point where the child is attached to the + * parent. This is tracked in ->notnull_inh (which is set in flagInhAttrs for + * servers pre-18). + * + * Any of these constraints might have the NO INHERIT bit. If so we set + * ->notnull_noinh and NO INHERIT will be printed by dumpTableSchema. + * + * In case 3 above, the name comparison is a bit of a hack; it actually fails + * to do the right thing in all but the trivial case. However, the downside + * of getting it wrong is simply that the name is printed rather than + * suppressed, so it's not a big deal. + */ +static void +determineNotNullFlags(Archive *fout, PGresult *res, int r, + TableInfo *tbinfo, int j, + int i_notnull_name, int i_notnull_noinherit, + int i_notnull_islocal) +{ + DumpOptions *dopt = fout->dopt; + + /* + * notnull_noinh is straight from the query result. notnull_islocal also, + * though flagInhAttrs may change that one later in versions < 18. + */ + tbinfo->notnull_noinh[j] = PQgetvalue(res, r, i_notnull_noinherit)[0] == 't'; + tbinfo->notnull_islocal[j] = PQgetvalue(res, r, i_notnull_islocal)[0] == 't'; + + /* + * Determine a constraint name to use. If the column is not marked not- + * null, we set NULL which cues ... to do nothing. An empty string says + * to print an unnamed NOT NULL, and anything else is a constraint name to + * use. + */ + if (fout->remoteVersion < 180000) + { + /* + * < 18 doesn't have not-null names, so an unnamed constraint is + * sufficient. + */ + if (PQgetisnull(res, r, i_notnull_name)) + tbinfo->notnull_constrs[j] = NULL; + else + tbinfo->notnull_constrs[j] = ""; + } + else + { + if (PQgetisnull(res, r, i_notnull_name)) + tbinfo->notnull_constrs[j] = NULL; + else + { + /* + * In binary upgrade of inheritance child tables, must have a + * constraint name that we can UPDATE later. + */ + if (dopt->binary_upgrade && + !tbinfo->ispartition && + !tbinfo->notnull_islocal) + { + tbinfo->notnull_constrs[j] = + pstrdup(PQgetvalue(res, r, i_notnull_name)); + } + else + { + char *default_name; + + /* XXX should match ChooseConstraintName better */ + default_name = psprintf("%s_%s_not_null", tbinfo->dobj.name, + tbinfo->attnames[j]); + if (strcmp(default_name, + PQgetvalue(res, r, i_notnull_name)) == 0) + tbinfo->notnull_constrs[j] = ""; + else + { + tbinfo->notnull_constrs[j] = + pstrdup(PQgetvalue(res, r, i_notnull_name)); + } + } + } + } +} + /* * Test whether a column should be printed as part of table's CREATE TABLE. * Column number is zero-based. @@ -15950,13 +16104,14 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) !tbinfo->attrdefs[j]->separate); /* - * Not Null constraint --- suppress if inherited, except - * if partition, or in binary-upgrade case where that - * won't work. + * Not Null constraint --- print it if it is locally + * defined, or if binary upgrade. (In the latter case, we + * reset conislocal below.) */ - print_notnull = (tbinfo->notnull[j] && - (!tbinfo->inhNotNull[j] || - tbinfo->ispartition || dopt->binary_upgrade)); + print_notnull = (tbinfo->notnull_constrs[j] != NULL && + (tbinfo->notnull_islocal[j] || + dopt->binary_upgrade || + tbinfo->ispartition)); /* * Skip column if fully defined by reloftype, except in @@ -16014,7 +16169,16 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) if (print_notnull) - appendPQExpBufferStr(q, " NOT NULL"); + { + if (tbinfo->notnull_constrs[j][0] == '\0') + appendPQExpBufferStr(q, " NOT NULL"); + else + appendPQExpBuffer(q, " CONSTRAINT %s NOT NULL", + fmtId(tbinfo->notnull_constrs[j])); + + if (tbinfo->notnull_noinh[j]) + appendPQExpBufferStr(q, " NO INHERIT"); + } /* Add collation if not default for the type */ if (OidIsValid(tbinfo->attcollation[j])) @@ -16269,6 +16433,41 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) if (!firstitem) appendPQExpBufferStr(q, ");\n"); + /* + * Fix up not-null constraints that come from inheritance. As + * above, do the pg_constraint manipulations in a single SQL + * command. + */ + firstitem = true; + for (j = 0; j < tbinfo->numatts; j++) + { + /* + * If a not-null constraint comes from inheritance, reset + * conislocal. The inhcount is fixed by ALTER TABLE INHERIT, + * below. + */ + if (tbinfo->notnull_constrs[j] != NULL && + tbinfo->notnull_constrs[j][0] != '\0' && + !tbinfo->notnull_islocal[j]) + { + if (firstitem) + { + appendPQExpBufferStr(q, "UPDATE pg_catalog.pg_constraint\n" + "SET conislocal = false\n" + "WHERE contype = 'n' AND conrelid = "); + appendStringLiteralAH(q, qualrelname, fout); + appendPQExpBufferStr(q, "::pg_catalog.regclass AND\n" + "conname IN ("); + firstitem = false; + } + else + appendPQExpBufferStr(q, ", "); + appendStringLiteralAH(q, tbinfo->notnull_constrs[j], fout); + } + } + if (!firstitem) + appendPQExpBufferStr(q, ");\n"); + /* * Add inherited CHECK constraints, if any. * @@ -16408,11 +16607,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) * we have to mark it separately. */ if (!shouldPrintColumn(dopt, tbinfo, j) && - tbinfo->notnull[j] && !tbinfo->inhNotNull[j]) - appendPQExpBuffer(q, - "ALTER %sTABLE ONLY %s ALTER COLUMN %s SET NOT NULL;\n", - foreign, qualrelname, - fmtId(tbinfo->attnames[j])); + tbinfo->notnull_constrs[j] != NULL && + (tbinfo->notnull_islocal[j] && !tbinfo->ispartition && !dopt->binary_upgrade)) + { + /* No constraint name desired? */ + if (tbinfo->notnull_constrs[j][0] == '\0') + appendPQExpBuffer(q, + "ALTER %sTABLE ONLY %s ALTER COLUMN %s SET NOT NULL;\n", + foreign, qualrelname, + fmtId(tbinfo->attnames[j])); + else + appendPQExpBuffer(q, + "ALTER %sTABLE ONLY %s ADD CONSTRAINT %s NOT NULL %s;\n", + foreign, qualrelname, + tbinfo->notnull_constrs[j], + fmtId(tbinfo->attnames[j])); + } /* * Dump per-column statistics information. We only issue an ALTER diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 9f907ed5ad..533868e162 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -347,8 +347,12 @@ typedef struct _tableInfo char *attcompression; /* per-attribute compression method */ char **attfdwoptions; /* per-attribute fdw options */ char **attmissingval; /* per attribute missing value */ - bool *notnull; /* not-null constraints on attributes */ - bool *inhNotNull; /* true if NOT NULL is inherited */ + char **notnull_constrs; /* NOT NULL constraint names. If null, + * there isn't one on this column. If + * empty string, unnamed constraint + * (pre-v17) */ + bool *notnull_noinh; /* NOT NULL is NO INHERIT */ + bool *notnull_islocal; /* true if NOT NULL has local definition */ struct _attrDefInfo **attrdefs; /* DEFAULT expressions */ struct _constraintInfo *checkexprs; /* CHECK constraints */ bool needs_override; /* has GENERATED ALWAYS AS IDENTITY */ diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index ab6c830491..1094534598 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -3340,8 +3340,8 @@ my %tests = ( FOR VALUES FROM (\'2006-02-01\') TO (\'2006-03-01\');', regexp => qr/^ \QCREATE TABLE dump_test_second_schema.measurement_y2006m2 (\E\n - \s+\Qcity_id integer DEFAULT nextval('dump_test.measurement_city_id_seq'::regclass) NOT NULL,\E\n - \s+\Qlogdate date NOT NULL,\E\n + \s+\Qcity_id integer DEFAULT nextval('dump_test.measurement_city_id_seq'::regclass) CONSTRAINT measurement_city_id_not_null NOT NULL,\E\n + \s+\Qlogdate date CONSTRAINT measurement_logdate_not_null NOT NULL,\E\n \s+\Qpeaktemp integer,\E\n \s+\Qunitsales integer DEFAULT 0,\E\n \s+\QCONSTRAINT measurement_peaktemp_check CHECK ((peaktemp >= '-460'::integer)),\E\n @@ -3749,7 +3749,7 @@ my %tests = ( ) INHERITS (dump_test.test_inheritance_parent);', regexp => qr/^ \QCREATE TABLE dump_test.test_inheritance_child (\E\n - \s+\Qcol1 integer,\E\n + \s+\Qcol1 integer NOT NULL,\E\n \s+\QCONSTRAINT test_inheritance_child CHECK ((col2 >= 142857))\E\n \)\n \QINHERITS (dump_test.test_inheritance_parent);\E\n diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index faabecbc76..9cef61244c 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -3058,6 +3058,50 @@ describeOneTableDetails(const char *schemaname, } PQclear(result); } + + /* + * If verbose, print NOT NULL constraints. + */ + if (verbose) + { + printfPQExpBuffer(&buf, + "SELECT c.conname, a.attname, c.connoinherit,\n" + " c.conislocal, c.coninhcount <> 0\n" + "FROM pg_catalog.pg_constraint c JOIN\n" + " pg_catalog.pg_attribute a ON\n" + " (a.attrelid = c.conrelid AND a.attnum = c.conkey[1])\n" + "WHERE c.contype = 'n' AND\n" + " c.conrelid = '%s'::pg_catalog.regclass\n" + "ORDER BY a.attnum", + oid); + + result = PSQLexec(buf.data); + if (!result) + goto error_return; + else + tuples = PQntuples(result); + + if (tuples > 0) + printTableAddFooter(&cont, _("Not-null constraints:")); + + /* Might be an empty set - that's ok */ + for (i = 0; i < tuples; i++) + { + bool islocal = PQgetvalue(result, i, 3)[0] == 't'; + bool inherited = PQgetvalue(result, i, 4)[0] == 't'; + + printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s", + PQgetvalue(result, i, 0), + PQgetvalue(result, i, 1), + PQgetvalue(result, i, 2)[0] == 't' ? + " NO INHERIT" : + islocal && inherited ? _(" (local, inherited)") : + inherited ? _(" (inherited)") : ""); + + printTableAddFooter(&cont, buf.data); + } + PQclear(result); + } } /* Get view_def if table is a view or materialized view */ diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl index fd645896c9..f3e6c8beaa 100644 --- a/src/bin/psql/t/010_tab_completion.pl +++ b/src/bin/psql/t/010_tab_completion.pl @@ -39,7 +39,7 @@ $node->start; # set up a few database objects $node->safe_psql('postgres', - "CREATE TABLE tab1 (c1 int primary key, c2 text);\n" + "CREATE TABLE tab1 (c1 int primary key constraint foo not null, c2 text);\n" . "CREATE TABLE mytab123 (f1 int, f2 text);\n" . "CREATE TABLE mytab246 (f1 int, f2 text);\n" . "CREATE TABLE \"mixedName\" (f1 int, f2 text);\n" @@ -209,21 +209,21 @@ clear_query(); # check interpretation of referenced names check_completion( - "alter table tab1 drop constraint \t", + "alter table tab1 drop constraint t\t", qr/tab1_pkey /, "complete index name for referenced table"); clear_query(); check_completion( - "alter table TAB1 drop constraint \t", + "alter table TAB1 drop constraint t\t", qr/tab1_pkey /, "complete index name for referenced table, with downcasing"); clear_query(); check_completion( - "alter table public.\"tab1\" drop constraint \t", + "alter table public.\"tab1\" drop constraint t\t", qr/tab1_pkey /, "complete index name for referenced table, with schema and quoting"); diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h index c512824cd1..e446d49b3e 100644 --- a/src/include/catalog/heap.h +++ b/src/include/catalog/heap.h @@ -34,10 +34,11 @@ typedef struct RawColumnDefault typedef struct CookedConstraint { - ConstrType contype; /* CONSTR_DEFAULT or CONSTR_CHECK */ + ConstrType contype; /* CONSTR_DEFAULT, CONSTR_CHECK, + * CONSTR_NOTNULL */ Oid conoid; /* constr OID if created, otherwise Invalid */ char *name; /* name, or NULL if none */ - AttrNumber attnum; /* which attr (only for DEFAULT) */ + AttrNumber attnum; /* which attr (only for NOTNULL, DEFAULT) */ Node *expr; /* transformed default or check expr */ bool skip_validation; /* skip validation? (only for CHECK) */ bool is_local; /* constraint has local (non-inherited) def */ @@ -113,6 +114,9 @@ extern List *AddRelationNewConstraints(Relation rel, bool is_local, bool is_internal, const char *queryString); +extern List *AddRelationNotNullConstraints(Relation rel, + List *constraints, + List *old_notnulls); extern void RelationClearMissing(Relation rel); extern void SetAttrMissing(Oid relid, char *attname, char *value); diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h index 115217a616..2dc9750c6d 100644 --- a/src/include/catalog/pg_constraint.h +++ b/src/include/catalog/pg_constraint.h @@ -257,7 +257,14 @@ extern char *ChooseConstraintName(const char *name1, const char *name2, const char *label, Oid namespaceid, List *others); +extern HeapTuple findNotNullConstraintAttnum(Oid relid, AttrNumber attnum); +extern HeapTuple findNotNullConstraint(Oid relid, const char *colname); extern HeapTuple findDomainNotNullConstraint(Oid typid); +extern AttrNumber extractNotNullColumn(HeapTuple constrTup); +extern bool AdjustNotNullInheritance(Oid relid, AttrNumber attnum, int count, + bool is_local, bool is_no_inherit); +extern List *RelationGetNotNullConstraints(Oid relid, bool cooked, + bool include_noinh); extern void RemoveConstraintById(Oid conId); extern void RenameConstraintById(Oid conId, const char *newname); diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 0765e5c57b..028f8815d1 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -68,6 +68,7 @@ extern RelabelType *makeRelabelType(Expr *arg, Oid rtype, int32 rtypmod, Oid rcollid, CoercionForm rformat); extern RangeVar *makeRangeVar(char *schemaname, char *relname, int location); +extern Constraint *makeNotNullConstraint(String *colname); extern TypeName *makeTypeName(char *typnam); extern TypeName *makeTypeNameFromNameList(List *names); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index e62ce1b753..3572f6da6c 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2354,7 +2354,6 @@ typedef enum AlterTableType AT_SetNotNull, /* alter column set not null */ AT_SetExpression, /* alter column set expression */ AT_DropExpression, /* alter column drop expression */ - AT_CheckNotNull, /* check column is already marked not null */ AT_SetStatistics, /* alter column set statistics */ AT_SetOptions, /* alter column set ( options ) */ AT_ResetOptions, /* alter column reset ( options ) */ @@ -2637,10 +2636,10 @@ 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 - * Constraint nodes (in fact, only CONSTR_CHECK nodes, in the present - * implementation). + * 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. * ---------------------- */ @@ -2655,6 +2654,7 @@ typedef struct CreateStmt PartitionSpec *partspec; /* PARTITION BY clause */ TypeName *ofTypename; /* OF typename */ List *constraints; /* constraints (list of Constraint nodes) */ + List *nnconstraints; /* NOT NULL constraints (ditto) */ List *options; /* options from WITH clause */ OnCommitAction oncommit; /* what do we do at COMMIT? */ char *tablespacename; /* table space to use, or NULL */ diff --git a/src/test/modules/test_ddl_deparse/expected/alter_table.out b/src/test/modules/test_ddl_deparse/expected/alter_table.out index 6daa186a84..50d0354a34 100644 --- a/src/test/modules/test_ddl_deparse/expected/alter_table.out +++ b/src/test/modules/test_ddl_deparse/expected/alter_table.out @@ -28,6 +28,7 @@ ALTER TABLE parent ADD COLUMN b serial; NOTICE: DDL test: type simple, tag CREATE SEQUENCE NOTICE: DDL test: type alter table, tag ALTER TABLE NOTICE: subcommand: type ADD COLUMN (and recurse) desc column b of table parent +NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint parent_b_not_null on table parent NOTICE: DDL test: type simple, tag ALTER SEQUENCE ALTER TABLE parent RENAME COLUMN b TO c; NOTICE: DDL test: type simple, tag ALTER TABLE @@ -57,24 +58,17 @@ NOTICE: subcommand: type DETACH PARTITION desc table part2 DROP TABLE part2; ALTER TABLE part ADD PRIMARY KEY (a); NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type SET NOT NULL desc column a of table part -NOTICE: subcommand: type SET NOT NULL desc column a of table part1 +NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint part_a_not_null on table part NOTICE: subcommand: type ADD INDEX desc index part_pkey ALTER TABLE parent ALTER COLUMN a SET NOT NULL; NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type SET NOT NULL desc column a of table parent -NOTICE: subcommand: type SET NOT NULL desc column a of table child -NOTICE: subcommand: type SET NOT NULL desc column a of table grandchild +NOTICE: subcommand: type SET NOT NULL (and recurse) desc constraint parent_a_not_null on table parent ALTER TABLE parent ALTER COLUMN a DROP NOT NULL; NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type DROP NOT NULL desc column a of table parent -NOTICE: subcommand: type DROP NOT NULL desc column a of table child -NOTICE: subcommand: type DROP NOT NULL desc column a of table grandchild +NOTICE: subcommand: type DROP NOT NULL (and recurse) desc column a of table parent ALTER TABLE parent ALTER COLUMN a SET NOT NULL; NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type SET NOT NULL desc column a of table parent -NOTICE: subcommand: type SET NOT NULL desc column a of table child -NOTICE: subcommand: type SET NOT NULL desc column a of table grandchild +NOTICE: subcommand: type SET NOT NULL (and recurse) desc constraint parent_a_not_null on table parent ALTER TABLE parent ALTER COLUMN a ADD GENERATED ALWAYS AS IDENTITY; NOTICE: DDL test: type simple, tag CREATE SEQUENCE NOTICE: DDL test: type simple, tag ALTER SEQUENCE @@ -116,6 +110,7 @@ NOTICE: DDL test: type alter table, tag ALTER TABLE NOTICE: subcommand: type ALTER COLUMN SET TYPE desc column c of table parent NOTICE: subcommand: type ALTER COLUMN SET TYPE desc column c of table child NOTICE: subcommand: type ALTER COLUMN SET TYPE desc column c of table grandchild +NOTICE: subcommand: type (re) ADD CONSTRAINT desc constraint parent_b_not_null on table parent NOTICE: subcommand: type (re) ADD STATS desc statistics object parent_stat ALTER TABLE parent ALTER COLUMN c SET DEFAULT 0; NOTICE: DDL test: type alter table, tag ALTER TABLE diff --git a/src/test/modules/test_ddl_deparse/expected/create_table.out b/src/test/modules/test_ddl_deparse/expected/create_table.out index 2178ce83e9..14915f661a 100644 --- a/src/test/modules/test_ddl_deparse/expected/create_table.out +++ b/src/test/modules/test_ddl_deparse/expected/create_table.out @@ -85,8 +85,6 @@ CREATE TABLE employees OF employee_type ( salary WITH OPTIONS DEFAULT 1000 ); NOTICE: DDL test: type simple, tag CREATE TABLE -NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type SET NOT NULL desc column name of table employees NOTICE: DDL test: type simple, tag CREATE INDEX -- Inheritance CREATE TABLE person ( 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 2758ae82d7..3922dc0f33 100644 --- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c +++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c @@ -135,9 +135,6 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS) case AT_DropExpression: strtype = "DROP EXPRESSION"; break; - case AT_CheckNotNull: - strtype = "CHECK NOT NULL"; - break; case AT_SetStatistics: strtype = "SET STATS"; break; diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index 3b3b0738d7..c864fa29dc 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -1216,20 +1216,6 @@ alter table only parent alter a set not null; ERROR: column "a" of relation "parent" contains null values alter table child alter a set not null; ERROR: column "a" of relation "child" contains null values -delete from parent; -alter table only parent alter a set not null; -insert into parent values (NULL); -ERROR: null value in column "a" of relation "parent" violates not-null constraint -DETAIL: Failing row contains (null). -alter table child alter a set not null; -insert into child (a, b) values (NULL, 'foo'); -ERROR: null value in column "a" of relation "child" violates not-null constraint -DETAIL: Failing row contains (null, foo). -delete from child; -alter table child alter a set not null; -insert into child (a, b) values (NULL, 'foo'); -ERROR: null value in column "a" of relation "child" violates not-null constraint -DETAIL: Failing row contains (null, foo). drop table child; drop table parent; -- test setting and removing default values @@ -3386,6 +3372,7 @@ DROP TABLE tt9; -- Check that comments on constraints and indexes are not lost at ALTER TABLE. CREATE TABLE comment_test ( id int, + constraint id_notnull_constraint not null id, positive_col int CHECK (positive_col > 0), indexed_col int, CONSTRAINT comment_test_pk PRIMARY KEY (id)); @@ -3394,6 +3381,7 @@ COMMENT ON COLUMN comment_test.id IS 'Column ''id'' on comment_test'; COMMENT ON INDEX comment_test_index IS 'Simple index on comment_test'; COMMENT ON CONSTRAINT comment_test_positive_col_check ON comment_test IS 'CHECK constraint on comment_test.positive_col'; COMMENT ON CONSTRAINT comment_test_pk ON comment_test IS 'PRIMARY KEY constraint of comment_test'; +COMMENT ON CONSTRAINT id_notnull_constraint ON comment_test IS 'NOT NULL constraint of comment_test'; COMMENT ON INDEX comment_test_pk IS 'Index backing the PRIMARY KEY of comment_test'; SELECT col_description('comment_test'::regclass, 1) as comment; comment @@ -3413,7 +3401,8 @@ SELECT conname as constraint, obj_description(oid, 'pg_constraint') as comment F ---------------------------------+----------------------------------------------- comment_test_pk | PRIMARY KEY constraint of comment_test comment_test_positive_col_check | CHECK constraint on comment_test.positive_col -(2 rows) + id_notnull_constraint | NOT NULL constraint of comment_test +(3 rows) -- Change the datatype of all the columns. ALTER TABLE is optimized to not -- rebuild an index if the new data type is binary compatible with the old @@ -3444,7 +3433,8 @@ SELECT conname as constraint, obj_description(oid, 'pg_constraint') as comment F ---------------------------------+----------------------------------------------- comment_test_pk | PRIMARY KEY constraint of comment_test comment_test_positive_col_check | CHECK constraint on comment_test.positive_col -(2 rows) + id_notnull_constraint | NOT NULL constraint of comment_test +(3 rows) -- Check compatibility for foreign keys and comments. This is done -- separately as rebuilding the column type of the parent leads @@ -3860,6 +3850,9 @@ CREATE TABLE atnotnull1 (); ALTER TABLE atnotnull1 ADD COLUMN a INT, ALTER a SET NOT NULL; +ALTER TABLE atnotnull1 + ADD COLUMN b INT, + ADD NOT NULL b; ALTER TABLE atnotnull1 ADD COLUMN c INT, ADD PRIMARY KEY (c); @@ -3868,9 +3861,14 @@ ALTER TABLE atnotnull1 Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+---------+-----------+----------+---------+---------+--------------+------------- a | integer | | not null | | plain | | + b | integer | | not null | | plain | | c | integer | | not null | | plain | | Indexes: "atnotnull1_pkey" PRIMARY KEY, btree (c) +Not-null constraints: + "atnotnull1_a_not_null" NOT NULL "a" + "atnotnull1_b_not_null" NOT NULL "b" + "atnotnull1_c_not_null" NOT NULL "c" -- cannot drop column that is part of the partition key CREATE TABLE partitioned ( @@ -4405,7 +4403,6 @@ ERROR: cannot alter inherited column "b" -- partitions exist ALTER TABLE ONLY list_parted2 ALTER b SET NOT NULL; ERROR: constraint must be added to child tables too -DETAIL: Column "b" of relation "part_2" is not already NOT NULL. HINT: Do not specify the ONLY keyword. ALTER TABLE ONLY list_parted2 ADD CONSTRAINT check_b CHECK (b <> 'zz'); ERROR: constraint must be added to child tables too diff --git a/src/test/regress/expected/cluster.out b/src/test/regress/expected/cluster.out index a13aafff0b..4d40a6809a 100644 --- a/src/test/regress/expected/cluster.out +++ b/src/test/regress/expected/cluster.out @@ -247,11 +247,12 @@ ERROR: insert or update on table "clstr_tst" violates foreign key constraint "c DETAIL: Key (b)=(1111) is not present in table "clstr_tst_s". SELECT conname FROM pg_constraint WHERE conrelid = 'clstr_tst'::regclass ORDER BY 1; - conname ----------------- + conname +---------------------- + clstr_tst_a_not_null clstr_tst_con clstr_tst_pkey -(2 rows) +(3 rows) SELECT relname, relkind, EXISTS(SELECT 1 FROM pg_class WHERE oid = c.reltoastrelid) AS hastoast diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out index cf0b80d616..f6b71fca54 100644 --- a/src/test/regress/expected/constraints.out +++ b/src/test/regress/expected/constraints.out @@ -6,6 +6,7 @@ -- - PRIMARY KEY clauses -- - UNIQUE clauses -- - EXCLUDE clauses +-- - NOT NULL clauses -- -- directory paths are passed to us in environment variables \getenv abs_srcdir PG_ABS_SRCDIR @@ -797,6 +798,367 @@ ALTER TABLE deferred_excl ADD EXCLUDE (f1 WITH =); ERROR: could not create exclusion constraint "deferred_excl_f1_excl" DETAIL: Key (f1)=(3) conflicts with key (f1)=(3). DROP TABLE deferred_excl; +-- verify constraints created for NOT NULL clauses +CREATE TABLE notnull_tbl1 (a INTEGER NOT NULL NOT NULL); +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl1_a_not_null" NOT NULL "a" + +-- no-op +ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl1_a_not_null" NOT NULL "a" + +-- duplicate name +ALTER TABLE notnull_tbl1 ADD COLUMN b INT CONSTRAINT notnull_tbl1_a_not_null NOT NULL; +ERROR: constraint "notnull_tbl1_a_not_null" for relation "notnull_tbl1" already exists +-- DROP NOT NULL gets rid of both the attnotnull flag and the constraint itself +ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + +-- SET NOT NULL puts both back +ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl1_a_not_null" NOT NULL "a" + +-- Doing it twice doesn't create a redundant constraint +ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; +select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass; + conname | contype | conkey +-------------------------+---------+-------- + notnull_tbl1_a_not_null | n | {1} +(1 row) + +-- Using the "table constraint" syntax also works +ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL; +ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "foobar" NOT NULL "a" + +DROP TABLE notnull_tbl1; +-- NOT NULL NO INHERIT +CREATE TABLE ATACC1 (a int, not null a no inherit); +CREATE TABLE ATACC2 () INHERITS (ATACC1); +\d+ ATACC2 + Table "public.atacc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: atacc1 + +DROP TABLE ATACC1, ATACC2; +CREATE TABLE ATACC1 (a int); +ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT; +CREATE TABLE ATACC2 () INHERITS (ATACC1); +\d+ ATACC2 + Table "public.atacc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: atacc1 + +DROP TABLE ATACC1, ATACC2; +CREATE TABLE ATACC1 (a int); +CREATE TABLE ATACC2 () INHERITS (ATACC1); +ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT; +\d+ ATACC2 + Table "public.atacc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: atacc1 + +DROP TABLE ATACC1, ATACC2; +-- NOT NULL NO INHERIT is not possible on partitioned tables +CREATE TABLE ATACC1 (a int NOT NULL NO INHERIT) PARTITION BY LIST (a); +ERROR: not-null constraints on partitioned tables cannot be NO INHERIT +CREATE TABLE ATACC1 (a int, NOT NULL a NO INHERIT) PARTITION BY LIST (a); +ERROR: not-null constraints on partitioned tables cannot be NO INHERIT +-- it's not possible to override a no-inherit constraint with an inheritable one +CREATE TABLE ATACC2 (a int, CONSTRAINT a_is_not_null NOT NULL a NO INHERIT); +CREATE TABLE ATACC1 (a int); +CREATE TABLE ATACC3 (a int) INHERITS (ATACC2); +NOTICE: merging column "a" with inherited definition +ALTER TABLE ATACC2 INHERIT ATACC1; +-- can't override +ALTER TABLE ATACC1 ADD CONSTRAINT ditto NOT NULL a; +ERROR: cannot change NO INHERIT status of NOT NULL constraint "a_is_not_null" on relation "atacc2" +-- dropping the NO INHERIT constraint allows this to work +ALTER TABLE ATACC2 DROP CONSTRAINT a_is_not_null; +ALTER TABLE ATACC1 ADD CONSTRAINT ditto NOT NULL a; +\d+ ATACC3 + Table "public.atacc3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "ditto" NOT NULL "a" (inherited) +Inherits: atacc2 + +DROP TABLE ATACC1, ATACC2, ATACC3; +-- Can't have two constraints with the same name +CREATE TABLE notnull_tbl2 (a INTEGER CONSTRAINT blah NOT NULL, b INTEGER CONSTRAINT blah NOT NULL); +ERROR: constraint "blah" for relation "notnull_tbl2" already exists +-- can't drop not-null in primary key +CREATE TABLE notnull_tbl2 (a INTEGER PRIMARY KEY); +ALTER TABLE notnull_tbl2 ALTER a DROP NOT NULL; +ERROR: column "a" is in a primary key +DROP TABLE notnull_tbl2; +CREATE TABLE notnull_tbl3 (a INTEGER NOT NULL, CHECK (a IS NOT NULL)); +ALTER TABLE notnull_tbl3 ALTER A DROP NOT NULL; +ALTER TABLE notnull_tbl3 ADD b int, ADD CONSTRAINT pk PRIMARY KEY (a, b); +\d notnull_tbl3 + Table "public.notnull_tbl3" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | not null | + b | integer | | not null | +Indexes: + "pk" PRIMARY KEY, btree (a, b) +Check constraints: + "notnull_tbl3_a_check" CHECK (a IS NOT NULL) + +ALTER TABLE notnull_tbl3 DROP CONSTRAINT pk; +\d notnull_tbl3 + Table "public.notnull_tbl3" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | not null | + b | integer | | not null | +Check constraints: + "notnull_tbl3_a_check" CHECK (a IS NOT NULL) + +-- Primary keys cause not-null constraints to be created. +CREATE TABLE cnn_pk (a int, b int); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +ALTER TABLE cnn_pk ADD CONSTRAINT cnn_primarykey PRIMARY KEY (b); +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Indexes: + "cnn_primarykey" PRIMARY KEY, btree (b) +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +DROP TABLE cnn_pk, cnn_pk_child; +-- As above, but create the primary key ahead of time +CREATE TABLE cnn_pk (a int, b int, CONSTRAINT cnn_primarykey PRIMARY KEY (b)); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Indexes: + "cnn_primarykey" PRIMARY KEY, btree (b) +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +DROP TABLE cnn_pk, cnn_pk_child; +-- As above, but create the primary key using a UNIQUE index +CREATE TABLE cnn_pk (a int, b int); +CREATE UNIQUE INDEX cnn_uq ON cnn_pk (b); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +ALTER TABLE cnn_pk ADD CONSTRAINT cnn_primarykey PRIMARY KEY USING INDEX cnn_uq; +NOTICE: ALTER TABLE / ADD CONSTRAINT USING INDEX will rename index "cnn_uq" to "cnn_primarykey" +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Indexes: + "cnn_primarykey" PRIMARY KEY, btree (b) +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +DROP TABLE cnn_pk, cnn_pk_child; +-- Ensure partitions are scanned for null values when adding a PK +create table cnn2_parted(a int) partition by list (a); +create table cnn_part1 partition of cnn2_parted for values in (1, null); +insert into cnn_part1 values (null); +alter table cnn2_parted add primary key (a); +ERROR: column "a" of relation "cnn_part1" contains null values +drop table cnn2_parted; +-- columns in regular and LIKE inheritance should be marked not-nullable +-- for primary keys, even if those are deferred +CREATE TABLE notnull_tbl4 (a INTEGER PRIMARY KEY INITIALLY DEFERRED); +CREATE TABLE notnull_tbl4_lk (LIKE notnull_tbl4); +CREATE TABLE notnull_tbl4_lk2 (LIKE notnull_tbl4 INCLUDING INDEXES); +CREATE TABLE notnull_tbl4_lk3 (LIKE notnull_tbl4 INCLUDING INDEXES, CONSTRAINT a_nn NOT NULL a); +CREATE TABLE notnull_tbl4_cld () INHERITS (notnull_tbl4); +CREATE TABLE notnull_tbl4_cld2 (PRIMARY KEY (a) DEFERRABLE) INHERITS (notnull_tbl4); +CREATE TABLE notnull_tbl4_cld3 (PRIMARY KEY (a) DEFERRABLE, CONSTRAINT a_nn NOT NULL a) INHERITS (notnull_tbl4); +\d+ notnull_tbl4 + Table "public.notnull_tbl4" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_pkey" PRIMARY KEY, btree (a) DEFERRABLE INITIALLY DEFERRED +Not-null constraints: + "notnull_tbl4_a_not_null" NOT NULL "a" +Child tables: notnull_tbl4_cld, + notnull_tbl4_cld2, + notnull_tbl4_cld3 + +\d+ notnull_tbl4_lk + Table "public.notnull_tbl4_lk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl4_a_not_null" NOT NULL "a" + +\d+ notnull_tbl4_lk2 + Table "public.notnull_tbl4_lk2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_lk2_pkey" PRIMARY KEY, btree (a) DEFERRABLE INITIALLY DEFERRED +Not-null constraints: + "notnull_tbl4_a_not_null" NOT NULL "a" + +\d+ notnull_tbl4_lk3 + Table "public.notnull_tbl4_lk3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_lk3_pkey" PRIMARY KEY, btree (a) DEFERRABLE INITIALLY DEFERRED +Not-null constraints: + "notnull_tbl4_a_not_null" NOT NULL "a" + +\d+ notnull_tbl4_cld + Table "public.notnull_tbl4_cld" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl4_a_not_null" NOT NULL "a" (inherited) +Inherits: notnull_tbl4 + +\d+ notnull_tbl4_cld2 + Table "public.notnull_tbl4_cld2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_cld2_pkey" PRIMARY KEY, btree (a) DEFERRABLE +Not-null constraints: + "notnull_tbl4_cld2_a_not_null" NOT NULL "a" (local, inherited) +Inherits: notnull_tbl4 + +\d+ notnull_tbl4_cld3 + Table "public.notnull_tbl4_cld3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_cld3_pkey" PRIMARY KEY, btree (a) DEFERRABLE +Not-null constraints: + "a_nn" NOT NULL "a" (local, inherited) +Inherits: notnull_tbl4 + +-- leave these tables around for pg_upgrade testing -- Comments -- Setup a low-level role to enforce non-superuser checks. CREATE ROLE regress_constraint_comments; diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out index 284a7fb85c..344d05233a 100644 --- a/src/test/regress/expected/create_table.out +++ b/src/test/regress/expected/create_table.out @@ -759,21 +759,23 @@ CREATE TABLE part_b PARTITION OF parted ( NOTICE: merging constraint "check_a" with inherited definition -- conislocal should be false for any merged constraints, true otherwise SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname; - conname | conislocal | coninhcount ----------+------------+------------- - check_a | f | 1 - check_b | t | 0 -(2 rows) + conname | conislocal | coninhcount +-------------------+------------+------------- + check_a | f | 1 + part_b_b_not_null | t | 1 + check_b | t | 0 +(3 rows) -- Once check_b is added to the parent, it should be made non-local for part_b ALTER TABLE parted ADD CONSTRAINT check_b CHECK (b >= 0); NOTICE: merging constraint "check_b" with inherited definition SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname; - conname | conislocal | coninhcount ----------+------------+------------- - check_a | f | 1 - check_b | f | 1 -(2 rows) + conname | conislocal | coninhcount +-------------------+------------+------------- + check_a | f | 1 + check_b | f | 1 + part_b_b_not_null | t | 1 +(3 rows) -- Neither check_a nor check_b are droppable from part_b ALTER TABLE part_b DROP CONSTRAINT check_a; @@ -785,9 +787,10 @@ ERROR: cannot drop inherited constraint "check_b" of relation "part_b" -- be local constraints. ALTER TABLE parted DROP CONSTRAINT check_a, DROP CONSTRAINT check_b; SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname; - conname | conislocal | coninhcount ----------+------------+------------- -(0 rows) + conname | conislocal | coninhcount +-------------------+------------+------------- + part_b_b_not_null | t | 1 +(1 row) -- specify PARTITION BY for a partition CREATE TABLE fail_part_col_not_found PARTITION OF parted FOR VALUES IN ('c') PARTITION BY RANGE (c); @@ -851,6 +854,8 @@ drop table test_part_coll_posix; b | integer | | not null | 1 | plain | | Partition of: parted FOR VALUES IN ('b') Partition constraint: ((a IS NOT NULL) AND (a = 'b'::text)) +Not-null constraints: + "part_b_b_not_null" NOT NULL "b" (local, inherited) -- Both partition bound and partition key in describe output \d+ part_c @@ -862,6 +867,8 @@ Partition constraint: ((a IS NOT NULL) AND (a = 'b'::text)) Partition of: parted FOR VALUES IN ('c') Partition constraint: ((a IS NOT NULL) AND (a = 'c'::text)) Partition key: RANGE (b) +Not-null constraints: + "part_c_b_not_null" NOT NULL "b" (local, inherited) Partitions: part_c_1_10 FOR VALUES FROM (1) TO (10) -- a level-2 partition's constraint will include the parent's expressions @@ -873,6 +880,8 @@ Partitions: part_c_1_10 FOR VALUES FROM (1) TO (10) b | integer | | not null | 0 | plain | | Partition of: part_c FOR VALUES FROM (1) TO (10) Partition constraint: ((a IS NOT NULL) AND (a = 'c'::text) AND (b IS NOT NULL) AND (b >= 1) AND (b < 10)) +Not-null constraints: + "part_c_b_not_null" NOT NULL "b" (inherited) -- Show partition count in the parent's describe output -- Tempted to include \d+ output listing partitions with bound info but diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out index 6bfc6d040f..d091da5a1e 100644 --- a/src/test/regress/expected/create_table_like.out +++ b/src/test/regress/expected/create_table_like.out @@ -348,6 +348,8 @@ CREATE TABLE ctlt12_storage (LIKE ctlt1 INCLUDING STORAGE, LIKE ctlt2 INCLUDING a | text | | not null | | main | | b | text | | | | extended | | c | text | | | | external | | +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" CREATE TABLE ctlt12_comments (LIKE ctlt1 INCLUDING COMMENTS, LIKE ctlt2 INCLUDING COMMENTS); \d+ ctlt12_comments @@ -357,6 +359,8 @@ CREATE TABLE ctlt12_comments (LIKE ctlt1 INCLUDING COMMENTS, LIKE ctlt2 INCLUDIN a | text | | not null | | extended | | A b | text | | | | extended | | B c | text | | | | extended | | C +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" CREATE TABLE ctlt1_inh (LIKE ctlt1 INCLUDING CONSTRAINTS INCLUDING COMMENTS) INHERITS (ctlt1); NOTICE: merging column "a" with inherited definition @@ -370,6 +374,8 @@ NOTICE: merging constraint "ctlt1_a_check" with inherited definition b | text | | | | extended | | B Check constraints: "ctlt1_a_check" CHECK (length(a) > 2) +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" (local, inherited) Inherits: ctlt1 SELECT description FROM pg_description, pg_constraint c WHERE classoid = 'pg_constraint'::regclass AND objoid = c.oid AND c.conrelid = 'ctlt1_inh'::regclass; @@ -391,6 +397,8 @@ Check constraints: "ctlt1_a_check" CHECK (length(a) > 2) "ctlt3_a_check" CHECK (length(a) < 5) "ctlt3_c_check" CHECK (length(c) < 7) +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" (inherited) Inherits: ctlt1, ctlt3 @@ -409,6 +417,8 @@ Check constraints: "ctlt1_a_check" CHECK (length(a) > 2) "ctlt3_a_check" CHECK (length(a) < 5) "ctlt3_c_check" CHECK (length(c) < 7) +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" (inherited) Inherits: ctlt1 SELECT description FROM pg_description, pg_constraint c WHERE classoid = 'pg_constraint'::regclass AND objoid = c.oid AND c.conrelid = 'ctlt13_like'::regclass; @@ -433,6 +443,8 @@ Check constraints: Statistics objects: "public.ctlt_all_a_b_stat" ON a, b FROM ctlt_all "public.ctlt_all_expr_stat" ON (a || b) FROM ctlt_all +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" SELECT c.relname, objsubid, description FROM pg_description, pg_index i, pg_class c WHERE classoid = 'pg_class'::regclass AND objoid = i.indexrelid AND c.oid = i.indexrelid AND i.indrelid = 'ctlt_all'::regclass ORDER BY c.relname, objsubid; relname | objsubid | description @@ -473,6 +485,8 @@ Check constraints: Statistics objects: "public.pg_attrdef_a_b_stat" ON a, b FROM public.pg_attrdef "public.pg_attrdef_expr_stat" ON (a || b) FROM public.pg_attrdef +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" DROP TABLE public.pg_attrdef; -- Check that LIKE isn't confused when new table masks the old, either @@ -495,20 +509,28 @@ Check constraints: Statistics objects: "ctl_schema.ctlt1_a_b_stat" ON a, b FROM ctlt1 "ctl_schema.ctlt1_expr_stat" ON (a || b) FROM ctlt1 +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" ROLLBACK; DROP TABLE ctlt1, ctlt2, ctlt3, ctlt4, ctlt12_storage, ctlt12_comments, ctlt1_inh, ctlt13_inh, ctlt13_like, ctlt_all, ctla, ctlb CASCADE; NOTICE: drop cascades to table inhe -- LIKE must respect NO INHERIT property of constraints -CREATE TABLE noinh_con_copy (a int CHECK (a > 0) NO INHERIT); +CREATE TABLE noinh_con_copy (a int CHECK (a > 0) NO INHERIT, b int not null, + c int not null no inherit); CREATE TABLE noinh_con_copy1 (LIKE noinh_con_copy INCLUDING CONSTRAINTS); -\d noinh_con_copy1 - Table "public.noinh_con_copy1" - Column | Type | Collation | Nullable | Default ---------+---------+-----------+----------+--------- - a | integer | | | +\d+ noinh_con_copy1 + Table "public.noinh_con_copy1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | + c | integer | | not null | | plain | | Check constraints: "noinh_con_copy_a_check" CHECK (a > 0) NO INHERIT +Not-null constraints: + "noinh_con_copy_b_not_null" NOT NULL "b" + "noinh_con_copy_c_not_null" NOT NULL "c" NO INHERIT -- fail, as partitioned tables don't allow NO INHERIT constraints CREATE TABLE noinh_con_copy1_parted (LIKE noinh_con_copy INCLUDING ALL) diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out index 6ed50fdcfa..cce49e509a 100644 --- a/src/test/regress/expected/foreign_data.out +++ b/src/test/regress/expected/foreign_data.out @@ -742,6 +742,8 @@ COMMENT ON COLUMN ft1.c1 IS 'ft1.c1'; Check constraints: "ft1_c2_check" CHECK (c2 <> ''::text) "ft1_c3_check" CHECK (c3 >= '01-01-1994'::date AND c3 <= '01-31-1994'::date) +Not-null constraints: + "ft1_c1_not_null" NOT NULL "c1" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -864,6 +866,9 @@ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET STORAGE PLAIN; Check constraints: "ft1_c2_check" CHECK (c2 <> ''::text) "ft1_c3_check" CHECK (c3 >= '01-01-1994'::date AND c3 <= '01-31-1994'::date) +Not-null constraints: + "ft1_c1_not_null" NOT NULL "c1" + "ft1_c6_not_null" NOT NULL "c6" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1409,6 +1414,8 @@ CREATE FOREIGN TABLE ft2 () INHERITS (fd_pt1) c1 | integer | | not null | | plain | | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1418,6 +1425,8 @@ Child tables: ft2, FOREIGN c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1430,6 +1439,8 @@ DROP FOREIGN TABLE ft2; c1 | integer | | not null | | plain | | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" CREATE FOREIGN TABLE ft2 ( c1 integer NOT NULL, @@ -1443,6 +1454,8 @@ CREATE FOREIGN TABLE ft2 ( c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1454,6 +1467,8 @@ ALTER FOREIGN TABLE ft2 INHERIT fd_pt1; c1 | integer | | not null | | plain | | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1463,6 +1478,8 @@ Child tables: ft2, FOREIGN c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1484,6 +1501,8 @@ NOTICE: merging column "c3" with inherited definition c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1497,6 +1516,8 @@ Child tables: ct3, c1 | integer | | not null | | plain | | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (inherited) Inherits: ft2 \d+ ft3 @@ -1506,6 +1527,8 @@ Inherits: ft2 c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft3_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 Inherits: ft2 @@ -1527,6 +1550,9 @@ ALTER TABLE fd_pt1 ADD COLUMN c8 integer; c6 | integer | | | | plain | | c7 | integer | | not null | | plain | | c8 | integer | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" + "fd_pt1_c7_not_null" NOT NULL "c7" Child tables: ft2, FOREIGN \d+ ft2 @@ -1541,6 +1567,9 @@ Child tables: ft2, FOREIGN c6 | integer | | | | | plain | | c7 | integer | | not null | | | plain | | c8 | integer | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) + "fd_pt1_c7_not_null" NOT NULL "c7" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1559,6 +1588,9 @@ Child tables: ct3, c6 | integer | | | | plain | | c7 | integer | | not null | | plain | | c8 | integer | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (inherited) + "fd_pt1_c7_not_null" NOT NULL "c7" (inherited) Inherits: ft2 \d+ ft3 @@ -1573,6 +1605,9 @@ Inherits: ft2 c6 | integer | | | | | plain | | c7 | integer | | not null | | | plain | | c8 | integer | | | | | plain | | +Not-null constraints: + "ft3_c1_not_null" NOT NULL "c1" (local, inherited) + "fd_pt1_c7_not_null" NOT NULL "c7" (inherited) Server: s0 Inherits: ft2 @@ -1601,6 +1636,9 @@ ALTER TABLE fd_pt1 ALTER COLUMN c8 SET STORAGE EXTERNAL; c6 | integer | | not null | | plain | | c7 | integer | | | | plain | | c8 | text | | | | external | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" + "fd_pt1_c6_not_null" NOT NULL "c6" Child tables: ft2, FOREIGN \d+ ft2 @@ -1615,6 +1653,9 @@ Child tables: ft2, FOREIGN c6 | integer | | not null | | | plain | | c7 | integer | | | | | plain | | c8 | text | | | | | external | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) + "fd_pt1_c6_not_null" NOT NULL "c6" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1634,6 +1675,8 @@ ALTER TABLE fd_pt1 DROP COLUMN c8; c1 | integer | | not null | | plain | 10000 | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1643,6 +1686,8 @@ Child tables: ft2, FOREIGN c1 | integer | | not null | | | plain | 10000 | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1657,11 +1702,12 @@ SELECT relname, conname, contype, conislocal, coninhcount, connoinherit FROM pg_class AS pc JOIN pg_constraint AS pgc ON (conrelid = pc.oid) WHERE pc.relname = 'fd_pt1' ORDER BY 1,2; - relname | conname | contype | conislocal | coninhcount | connoinherit ----------+------------+---------+------------+-------------+-------------- - fd_pt1 | fd_pt1chk1 | c | t | 0 | t - fd_pt1 | fd_pt1chk2 | c | t | 0 | f -(2 rows) + relname | conname | contype | conislocal | coninhcount | connoinherit +---------+--------------------+---------+------------+-------------+-------------- + fd_pt1 | fd_pt1_c1_not_null | n | t | 0 | f + fd_pt1 | fd_pt1chk1 | c | t | 0 | t + fd_pt1 | fd_pt1chk2 | c | t | 0 | f +(3 rows) -- child does not inherit NO INHERIT constraints \d+ fd_pt1 @@ -1674,6 +1720,8 @@ SELECT relname, conname, contype, conislocal, coninhcount, connoinherit Check constraints: "fd_pt1chk1" CHECK (c1 > 0) NO INHERIT "fd_pt1chk2" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1685,6 +1733,8 @@ Child tables: ft2, FOREIGN c3 | date | | | | | plain | | Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1721,6 +1771,8 @@ ALTER FOREIGN TABLE ft2 INHERIT fd_pt1; Check constraints: "fd_pt1chk1" CHECK (c1 > 0) NO INHERIT "fd_pt1chk2" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1732,6 +1784,8 @@ Child tables: ft2, FOREIGN c3 | date | | | | | plain | | Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1751,6 +1805,8 @@ ALTER TABLE fd_pt1 ADD CONSTRAINT fd_pt1chk3 CHECK (c2 <> '') NOT VALID; c3 | date | | | | plain | | Check constraints: "fd_pt1chk3" CHECK (c2 <> ''::text) NOT VALID +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1763,6 +1819,8 @@ Child tables: ft2, FOREIGN Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) "fd_pt1chk3" CHECK (c2 <> ''::text) NOT VALID +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1778,6 +1836,8 @@ ALTER TABLE fd_pt1 VALIDATE CONSTRAINT fd_pt1chk3; c3 | date | | | | plain | | Check constraints: "fd_pt1chk3" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1790,6 +1850,8 @@ Child tables: ft2, FOREIGN Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) "fd_pt1chk3" CHECK (c2 <> ''::text) +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1809,6 +1871,8 @@ ALTER TABLE fd_pt1 RENAME CONSTRAINT fd_pt1chk3 TO f2_check; f3 | date | | | | plain | | Check constraints: "f2_check" CHECK (f2 <> ''::text) +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "f1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1821,6 +1885,8 @@ Child tables: ft2, FOREIGN Check constraints: "f2_check" CHECK (f2 <> ''::text) "fd_pt1chk2" CHECK (f2 <> ''::text) +Not-null constraints: + "ft2_c1_not_null" NOT NULL "f1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1867,6 +1933,8 @@ CREATE FOREIGN TABLE fd_pt2_1 PARTITION OF fd_pt2 FOR VALUES IN (1) c2 | text | | | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN \d+ fd_pt2_1 @@ -1878,6 +1946,8 @@ Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN c3 | date | | | | | plain | | Partition of: fd_pt2 FOR VALUES IN (1) Partition constraint: ((c1 IS NOT NULL) AND (c1 = 1)) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1897,6 +1967,8 @@ CREATE FOREIGN TABLE fd_pt2_1 ( c2 | text | | | | | extended | | c3 | date | | | | | plain | | c4 | character(1) | | | | | extended | | +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1912,6 +1984,8 @@ DROP FOREIGN TABLE fd_pt2_1; c2 | text | | | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" Number of partitions: 0 CREATE FOREIGN TABLE fd_pt2_1 ( @@ -1926,6 +2000,8 @@ CREATE FOREIGN TABLE fd_pt2_1 ( c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1939,6 +2015,8 @@ ALTER TABLE fd_pt2 ATTACH PARTITION fd_pt2_1 FOR VALUES IN (1); c2 | text | | | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN \d+ fd_pt2_1 @@ -1950,6 +2028,8 @@ Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN c3 | date | | | | | plain | | Partition of: fd_pt2 FOR VALUES IN (1) Partition constraint: ((c1 IS NOT NULL) AND (c1 = 1)) +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1967,6 +2047,8 @@ ALTER TABLE fd_pt2_1 ADD CONSTRAINT p21chk CHECK (c2 <> ''); c2 | text | | | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN \d+ fd_pt2_1 @@ -1980,6 +2062,9 @@ Partition of: fd_pt2 FOR VALUES IN (1) Partition constraint: ((c1 IS NOT NULL) AND (c1 = 1)) Check constraints: "p21chk" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" (inherited) + "fd_pt2_1_c3_not_null" NOT NULL "c3" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1997,6 +2082,9 @@ ALTER TABLE fd_pt2 ALTER c2 SET NOT NULL; c2 | text | | not null | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" + "fd_pt2_c2_not_null" NOT NULL "c2" Number of partitions: 0 \d+ fd_pt2_1 @@ -2008,11 +2096,14 @@ Number of partitions: 0 c3 | date | | not null | | | plain | | Check constraints: "p21chk" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" + "fd_pt2_1_c3_not_null" NOT NULL "c3" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') ALTER TABLE fd_pt2 ATTACH PARTITION fd_pt2_1 FOR VALUES IN (1); -- ERROR -ERROR: column "c2" in child table must be marked NOT NULL +ERROR: column "c2" in child table "fd_pt2_1" must be marked NOT NULL ALTER FOREIGN TABLE fd_pt2_1 ALTER c2 SET NOT NULL; ALTER TABLE fd_pt2 ATTACH PARTITION fd_pt2_1 FOR VALUES IN (1); ALTER TABLE fd_pt2 DETACH PARTITION fd_pt2_1; @@ -2027,6 +2118,9 @@ ALTER TABLE fd_pt2 ADD CONSTRAINT fd_pt2chk1 CHECK (c1 > 0); Partition key: LIST (c1) Check constraints: "fd_pt2chk1" CHECK (c1 > 0) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" + "fd_pt2_c2_not_null" NOT NULL "c2" Number of partitions: 0 \d+ fd_pt2_1 @@ -2038,6 +2132,10 @@ Number of partitions: 0 c3 | date | | not null | | | plain | | Check constraints: "p21chk" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" + "fd_pt2_1_c2_not_null" NOT NULL "c2" + "fd_pt2_1_c3_not_null" NOT NULL "c3" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 8c04a24b37..17c84e0cfb 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -2053,13 +2053,19 @@ ORDER BY co.contype, cr.relname, co.conname, p.conname; part33_self_fk | parted_self_fk_id_abc_fkey | f | t | parted_self_fk_id_abc_fkey | t | parted_self_fk part3_self_fk | parted_self_fk_id_abc_fkey | f | t | parted_self_fk_id_abc_fkey | t | parted_self_fk parted_self_fk | parted_self_fk_id_abc_fkey | f | t | | | parted_self_fk + part1_self_fk | part1_self_fk_id_not_null | n | t | | | + part2_self_fk | parted_self_fk_id_not_null | n | t | | | + part32_self_fk | part3_self_fk_id_not_null | n | t | | | + part33_self_fk | part33_self_fk_id_not_null | n | t | | | + part3_self_fk | part3_self_fk_id_not_null | n | t | | | + parted_self_fk | parted_self_fk_id_not_null | n | t | | | part1_self_fk | part1_self_fk_pkey | p | t | parted_self_fk_pkey | t | part2_self_fk | part2_self_fk_pkey | p | t | parted_self_fk_pkey | t | part32_self_fk | part32_self_fk_pkey | p | t | part3_self_fk_pkey | t | part33_self_fk | part33_self_fk_pkey | p | t | part3_self_fk_pkey | t | part3_self_fk | part3_self_fk_pkey | p | t | parted_self_fk_pkey | t | parted_self_fk | parted_self_fk_pkey | p | t | | | -(12 rows) +(18 rows) -- detach and re-attach multiple times just to ensure everything is kosher ALTER TABLE parted_self_fk DETACH PARTITION part2_self_fk; @@ -2082,13 +2088,19 @@ ORDER BY co.contype, cr.relname, co.conname, p.conname; part33_self_fk | parted_self_fk_id_abc_fkey | f | t | parted_self_fk_id_abc_fkey | t | parted_self_fk part3_self_fk | parted_self_fk_id_abc_fkey | f | t | parted_self_fk_id_abc_fkey | t | parted_self_fk parted_self_fk | parted_self_fk_id_abc_fkey | f | t | | | parted_self_fk + part1_self_fk | part1_self_fk_id_not_null | n | t | | | + part2_self_fk | parted_self_fk_id_not_null | n | t | | | + part32_self_fk | part3_self_fk_id_not_null | n | t | | | + part33_self_fk | part33_self_fk_id_not_null | n | t | | | + part3_self_fk | part3_self_fk_id_not_null | n | t | | | + parted_self_fk | parted_self_fk_id_not_null | n | t | | | part1_self_fk | part1_self_fk_pkey | p | t | parted_self_fk_pkey | t | part2_self_fk | part2_self_fk_pkey | p | t | parted_self_fk_pkey | t | part32_self_fk | part32_self_fk_pkey | p | t | part3_self_fk_pkey | t | part33_self_fk | part33_self_fk_pkey | p | t | part3_self_fk_pkey | t | part3_self_fk | part3_self_fk_pkey | p | t | parted_self_fk_pkey | t | parted_self_fk | parted_self_fk_pkey | p | t | | | -(12 rows) +(18 rows) -- Leave this table around, for pg_upgrade/pg_dump tests -- Test creating a constraint at the parent that already exists in partitions. diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_stored.out index 8ea8a3a92d..0d037d48ca 100644 --- a/src/test/regress/expected/generated_stored.out +++ b/src/test/regress/expected/generated_stored.out @@ -321,6 +321,8 @@ NOTICE: merging column "b" with inherited definition a | integer | | not null | | plain | | b | integer | | | generated always as (a * 22) stored | plain | | x | integer | | | | plain | | +Not-null constraints: + "gtest1_a_not_null" NOT NULL "a" (inherited) Inherits: gtest1 CREATE TABLE gtestxx_1 (a int NOT NULL, b int); diff --git a/src/test/regress/expected/identity.out b/src/test/regress/expected/identity.out index f14bfccfb1..2a2b777c89 100644 --- a/src/test/regress/expected/identity.out +++ b/src/test/regress/expected/identity.out @@ -578,6 +578,10 @@ TABLE itest8; f3 | integer | | not null | generated by default as identity | plain | | f4 | bigint | | not null | generated always as identity | plain | | f5 | bigint | | | | plain | | +Not-null constraints: + "itest8_f2_not_null" NOT NULL "f2" + "itest8_f3_not_null" NOT NULL "f3" + "itest8_f4_not_null" NOT NULL "f4" \d itest8_f2_seq Sequence "public.itest8_f2_seq" @@ -618,7 +622,7 @@ INSERT into pitest1_p1 (f1, f2) VALUES ('2016-07-3', 'from pitest1_p1'); CREATE TABLE pitest1_p2 (f3 bigint, f2 text, f1 date NOT NULL); INSERT INTO pitest1_p2 (f1, f2, f3) VALUES ('2016-08-2', 'before attaching', 100); ALTER TABLE pitest1 ATTACH PARTITION pitest1_p2 FOR VALUES FROM ('2016-08-01') TO ('2016-09-01'); -- requires NOT NULL constraint -ERROR: column "f3" in child table must be marked NOT NULL +ERROR: column "f3" in child table "pitest1_p2" must be marked NOT NULL ALTER TABLE pitest1_p2 ALTER COLUMN f3 SET NOT NULL; ALTER TABLE pitest1 ATTACH PARTITION pitest1_p2 FOR VALUES FROM ('2016-08-01') TO ('2016-09-01'); INSERT INTO pitest1_p2 (f1, f2) VALUES ('2016-08-3', 'from pitest1_p2'); diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out index ea8b2454bf..4e8fe49c8c 100644 --- a/src/test/regress/expected/index_including.out +++ b/src/test/regress/expected/index_including.out @@ -113,7 +113,7 @@ SELECT indexrelid::regclass, indnatts, indnkeyatts, indisunique, indisprimary, i covering | 4 | 2 | t | t | 1 2 3 4 | 1978 1978 (1 row) -SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid AND contype = 'p'; pg_get_constraintdef | conname | conkey ---------------------------------------+----------+-------- PRIMARY KEY (c1, c2) INCLUDE (c3, c4) | covering | {1,2} @@ -191,7 +191,7 @@ SELECT indexrelid::regclass, indnatts, indnkeyatts, indisunique, indisprimary, i tbl_pkey | 4 | 2 | t | t | 1 2 3 4 | 1978 1978 (1 row) -SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid AND contype = 'p'; pg_get_constraintdef | conname | conkey ---------------------------------------+----------+-------- PRIMARY KEY (c1, c2) INCLUDE (c3, c4) | tbl_pkey | {1,2} diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out index f25723da92..036bc9e887 100644 --- a/src/test/regress/expected/indexing.out +++ b/src/test/regress/expected/indexing.out @@ -1117,15 +1117,27 @@ alter table idxpart attach partition idxpart3 for values from (20, 20) to (30, 3 select conname, contype, conrelid::regclass, conindid::regclass, conkey from pg_constraint where conrelid::regclass::text like 'idxpart%' order by conrelid::regclass::text, conname; - conname | contype | conrelid | conindid | conkey -----------------+---------+-----------+----------------+-------- - idxpart_pkey | p | idxpart | idxpart_pkey | {1,2} - idxpart1_pkey | p | idxpart1 | idxpart1_pkey | {1,2} - idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2} - idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2} - idxpart22_pkey | p | idxpart22 | idxpart22_pkey | {1,2} - idxpart3_pkey | p | idxpart3 | idxpart3_pkey | {2,1} -(6 rows) + conname | contype | conrelid | conindid | conkey +---------------------+---------+-----------+----------------+-------- + idxpart_a_not_null | n | idxpart | - | {1} + idxpart_b_not_null | n | idxpart | - | {2} + idxpart_pkey | p | idxpart | idxpart_pkey | {1,2} + idxpart1_pkey | p | idxpart1 | idxpart1_pkey | {1,2} + idxpart_a_not_null | n | idxpart1 | - | {1} + idxpart_b_not_null | n | idxpart1 | - | {2} + idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2} + idxpart_a_not_null | n | idxpart2 | - | {1} + idxpart_b_not_null | n | idxpart2 | - | {2} + idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2} + idxpart_a_not_null | n | idxpart21 | - | {1} + idxpart_b_not_null | n | idxpart21 | - | {2} + idxpart22_pkey | p | idxpart22 | idxpart22_pkey | {1,2} + idxpart_a_not_null | n | idxpart22 | - | {1} + idxpart_b_not_null | n | idxpart22 | - | {2} + idxpart3_a_not_null | n | idxpart3 | - | {2} + idxpart3_b_not_null | n | idxpart3 | - | {1} + idxpart3_pkey | p | idxpart3 | idxpart3_pkey | {2,1} +(18 rows) drop table idxpart; -- Verify that multi-layer partitioning honors the requirement that all @@ -1150,13 +1162,19 @@ create table idxpart2 partition of idxpart for values from (0) to (1000) partiti create table idxpart21 partition of idxpart2 for values from (0) to (1000); select conname, contype, conrelid::regclass, conindid::regclass, conkey from pg_constraint where conrelid::regclass::text like 'idxpart%' - order by conname; - conname | contype | conrelid | conindid | conkey -----------------+---------+-----------+----------------+-------- - idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2} - idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2} - idxpart_pkey | p | idxpart | idxpart_pkey | {1,2} -(3 rows) + order by conrelid::regclass::text, conname; + conname | contype | conrelid | conindid | conkey +--------------------+---------+-----------+----------------+-------- + idxpart_a_not_null | n | idxpart | - | {1} + idxpart_b_not_null | n | idxpart | - | {2} + idxpart_pkey | p | idxpart | idxpart_pkey | {1,2} + idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2} + idxpart_a_not_null | n | idxpart2 | - | {1} + idxpart_b_not_null | n | idxpart2 | - | {2} + idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2} + idxpart_a_not_null | n | idxpart21 | - | {1} + idxpart_b_not_null | n | idxpart21 | - | {2} +(9 rows) drop table idxpart; -- If a partitioned table has a unique/PK constraint, then it's not possible @@ -1259,14 +1277,10 @@ create table idxpart0 (like idxpart); alter table idxpart0 add unique (a); alter table idxpart attach partition idxpart0 default; alter table only idxpart add primary key (a); -- fail, no not-null constraint -ERROR: constraint must be added to child tables too -DETAIL: Column "a" of relation "idxpart0" is not already NOT NULL. -HINT: Do not specify the ONLY keyword. +ERROR: column "a" of table "idxpart0" is not marked NOT NULL alter table idxpart0 alter column a set not null; alter table only idxpart add primary key (a); -- now it works alter index idxpart_pkey attach partition idxpart0_a_key; -alter table idxpart0 alter column a drop not null; -- fail, pkey needs it -ERROR: column "a" is marked NOT NULL in parent table drop table idxpart; -- if a partition has a unique index without a constraint, does not attach -- automatically; creates a new index instead. diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out index dbb748a2d2..f506052764 100644 --- a/src/test/regress/expected/inherit.out +++ b/src/test/regress/expected/inherit.out @@ -1252,6 +1252,8 @@ Indexes: "test_primary_constraints_pkey" PRIMARY KEY, btree (id) Referenced by: TABLE "test_foreign_constraints" CONSTRAINT "test_foreign_constraints_id1_fkey" FOREIGN KEY (id1) REFERENCES test_primary_constraints(id) +Not-null constraints: + "test_primary_constraints_id_not_null" NOT NULL "id" \d+ test_foreign_constraints Table "public.test_foreign_constraints" @@ -2025,6 +2027,604 @@ select * from cnullparent where f1 = 2; drop table cnullparent cascade; NOTICE: drop cascades to table cnullchild -- +-- Test inheritance of NOT NULL constraints +-- +create table pp1 (f1 int); +create table cc1 (f2 text, f3 int) inherits (pp1); +create table cc2 (f4 float) inherits (pp1,cc1); +NOTICE: merging multiple inherited definitions of column "f1" +create table cc3 () inherits (pp1,cc1,cc2); +NOTICE: merging multiple inherited definitions of column "f1" +NOTICE: merging multiple inherited definitions of column "f1" +NOTICE: merging multiple inherited definitions of column "f2" +NOTICE: merging multiple inherited definitions of column "f3" +alter table pp1 alter f1 set not null; +\d+ cc3 + Table "public.cc3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) +Inherits: pp1, + cc1, + cc2 + +alter table cc3 no inherit pp1; +alter table cc3 no inherit cc1; +alter table cc3 no inherit cc2; +\d+ cc3 + Table "public.cc3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" + +drop table cc3; +-- named NOT NULL constraint +alter table cc1 add column a2 int constraint nn not null; +\d+ cc1 + Table "public.cc1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + a2 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) + "nn" NOT NULL "a2" +Inherits: pp1 +Child tables: cc2 + +\d+ cc2 + Table "public.cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | + a2 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) + "nn" NOT NULL "a2" (inherited) +Inherits: pp1, + cc1 + +alter table pp1 alter column f1 set not null; +\d+ pp1 + Table "public.pp1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" +Child tables: cc1, + cc2 + +\d+ cc1 + Table "public.cc1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + a2 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) + "nn" NOT NULL "a2" +Inherits: pp1 +Child tables: cc2 + +\d+ cc2 + Table "public.cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | + a2 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) + "nn" NOT NULL "a2" (inherited) +Inherits: pp1, + cc1 + +-- cannot create table with inconsistent NO INHERIT constraint +create table cc3 (a2 int not null no inherit) inherits (cc1); +NOTICE: moving and merging column "a2" with inherited definition +DETAIL: User-specified column moved to the position of the inherited column. +ERROR: cannot define not-null constraint on column "a2" with NO INHERIT +DETAIL: The column has an inherited not-null constraint. +-- change NO INHERIT status of inherited constraint: no dice, it's inherited +alter table cc2 add not null a2 no inherit; +ERROR: cannot change NO INHERIT status of NOT NULL constraint "nn" on relation "cc2" +-- remove constraint from cc2: no dice, it's inherited +alter table cc2 alter column a2 drop not null; +ERROR: cannot drop inherited constraint "nn" of relation "cc2" +-- remove constraint from cc1, should succeed +alter table cc1 alter column a2 drop not null; +\d+ cc1 + Table "public.cc1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + a2 | integer | | | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) +Inherits: pp1 +Child tables: cc2 + +-- same for cc2 +alter table cc2 alter column f1 drop not null; +ERROR: cannot drop inherited constraint "pp1_f1_not_null" of relation "cc2" +\d+ cc2 + Table "public.cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | + a2 | integer | | | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) +Inherits: pp1, + cc1 + +-- remove from cc1, should fail again +alter table cc1 alter column f1 drop not null; +ERROR: cannot drop inherited constraint "pp1_f1_not_null" of relation "cc1" +-- remove from pp1, should succeed +alter table pp1 alter column f1 drop not null; +\d+ pp1 + Table "public.pp1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | | | plain | | +Child tables: cc1, + cc2 + +alter table pp1 add primary key (f1); +-- Leave these tables around, for pg_upgrade testing +-- test that inhcount is updated correctly through multiple inheritance +create table inh_pp1 (f1 int); +create table inh_cc1 (f2 text, f3 int) inherits (inh_pp1); +create table inh_cc2(f4 float) inherits(inh_pp1,inh_cc1); +NOTICE: merging multiple inherited definitions of column "f1" +alter table inh_pp1 alter column f1 set not null; +alter table inh_cc2 no inherit inh_pp1; +alter table inh_cc2 no inherit inh_cc1; +\d+ inh_cc2 + Table "public.inh_cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | +Not-null constraints: + "inh_pp1_f1_not_null" NOT NULL "f1" + +drop table inh_pp1, inh_cc1, inh_cc2; +create table inh_pp1 (f1 int not null); +create table inh_cc1 (f2 text, f3 int) inherits (inh_pp1); +create table inh_cc2(f4 float) inherits(inh_pp1,inh_cc1); +NOTICE: merging multiple inherited definitions of column "f1" +alter table inh_pp1 alter column f1 drop not null; +\d+ inh_cc2 + Table "public.inh_cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | +Inherits: inh_pp1, + inh_cc1 + +drop table inh_pp1, inh_cc1, inh_cc2; +-- Test a not-null addition that must walk down the hierarchy +CREATE TABLE inh_parent (); +CREATE TABLE inh_child (i int) INHERITS (inh_parent); +CREATE TABLE inh_grandchild () INHERITS (inh_parent, inh_child); +ALTER TABLE inh_parent ADD COLUMN i int NOT NULL; +NOTICE: merging definition of column "i" for child "inh_child" +NOTICE: merging definition of column "i" for child "inh_grandchild" +drop table inh_parent, inh_child, inh_grandchild; +-- Test the same constraint name for different columns in different parents +create table inh_parent1(a int constraint nn not null); +create table inh_parent2(b int constraint nn not null); +create table inh_child () inherits (inh_parent1, inh_parent2); +\d+ inh_child + Table "public.inh_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "nn" NOT NULL "a" (inherited) + "inh_child_b_not_null" NOT NULL "b" (inherited) +Inherits: inh_parent1, + inh_parent2 + +drop table inh_parent1, inh_parent2, inh_child; +-- Test multiple parents with overlapping primary keys +create table inh_parent1(a int, b int, c int, primary key (a, b)); +create table inh_parent2(d int, e int, b int, primary key (d, b)); +create table inh_child() inherits (inh_parent1, inh_parent2); +NOTICE: merging multiple inherited definitions of column "b" +select conrelid::regclass, conname, contype, conkey, + coninhcount, conislocal, connoinherit + from pg_constraint where contype in ('n','p') and + conrelid::regclass::text in ('inh_child', 'inh_parent1', 'inh_parent2') + order by 1, 2; + conrelid | conname | contype | conkey | coninhcount | conislocal | connoinherit +-------------+------------------------+---------+--------+-------------+------------+-------------- + inh_parent1 | inh_parent1_a_not_null | n | {1} | 0 | t | f + inh_parent1 | inh_parent1_b_not_null | n | {2} | 0 | t | f + inh_parent1 | inh_parent1_pkey | p | {1,2} | 0 | t | t + inh_parent2 | inh_parent2_b_not_null | n | {3} | 0 | t | f + inh_parent2 | inh_parent2_d_not_null | n | {1} | 0 | t | f + inh_parent2 | inh_parent2_pkey | p | {1,3} | 0 | t | t + inh_child | inh_parent1_a_not_null | n | {1} | 1 | f | f + inh_child | inh_parent1_b_not_null | n | {2} | 2 | f | f + inh_child | inh_parent2_d_not_null | n | {4} | 1 | f | f +(9 rows) + +\d+ inh_child + Table "public.inh_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | + b | integer | | not null | | plain | | + c | integer | | | | plain | | + d | integer | | not null | | plain | | + e | integer | | | | plain | | +Not-null constraints: + "inh_parent1_a_not_null" NOT NULL "a" (inherited) + "inh_parent1_b_not_null" NOT NULL "b" (inherited) + "inh_parent2_d_not_null" NOT NULL "d" (inherited) +Inherits: inh_parent1, + inh_parent2 + +drop table inh_parent1, inh_parent2, inh_child; +-- NOT NULL NO INHERIT +create table inh_nn_parent(a int); +create table inh_nn_child() inherits (inh_nn_parent); +alter table inh_nn_parent add not null a no inherit; +create table inh_nn_child2() inherits (inh_nn_parent); +select conrelid::regclass, conname, contype, conkey, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal, connoinherit + from pg_constraint where contype = 'n' and + conrelid::regclass::text like 'inh\_nn\_%' + order by 2, 1; + conrelid | conname | contype | conkey | attname | coninhcount | conislocal | connoinherit +---------------+--------------------------+---------+--------+---------+-------------+------------+-------------- + inh_nn_parent | inh_nn_parent_a_not_null | n | {1} | a | 0 | t | t +(1 row) + +\d+ inh_nn* + Table "public.inh_nn_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: inh_nn_parent + + Table "public.inh_nn_child2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: inh_nn_parent + + Table "public.inh_nn_parent" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "inh_nn_parent_a_not_null" NOT NULL "a" NO INHERIT +Child tables: inh_nn_child, + inh_nn_child2 + +drop table inh_nn_parent, inh_nn_child, inh_nn_child2; +CREATE TABLE inh_nn_parent (a int, NOT NULL a NO INHERIT); +CREATE TABLE inh_nn_child() INHERITS (inh_nn_parent); +ALTER TABLE inh_nn_parent ADD CONSTRAINT nna NOT NULL a; +ERROR: cannot change NO INHERIT status of NOT NULL constraint "inh_nn_parent_a_not_null" on relation "inh_nn_parent" +ALTER TABLE inh_nn_parent ALTER a SET NOT NULL; +ERROR: cannot change NO INHERIT status of NOT NULL constraint "inh_nn_parent_a_not_null" on relation "inh_nn_parent" +DROP TABLE inh_nn_parent cascade; +NOTICE: drop cascades to table inh_nn_child +-- Adding a PK at the top level of a hierarchy should cause all descendants +-- to be checked for nulls, even past a no-inherit constraint +CREATE TABLE inh_nn_lvl1 (a int); +CREATE TABLE inh_nn_lvl2 () INHERITS (inh_nn_lvl1); +CREATE TABLE inh_nn_lvl3 (CONSTRAINT foo NOT NULL a NO INHERIT) INHERITS (inh_nn_lvl2); +ALTER TABLE inh_nn_lvl1 ADD PRIMARY KEY (a); +ERROR: cannot change NO INHERIT status of NOT NULL constraint "foo" on relation "inh_nn_lvl3" +DROP TABLE inh_nn_lvl1, inh_nn_lvl2, inh_nn_lvl3; +-- Disallow specifying conflicting NO INHERIT flags for the same constraint +CREATE TABLE inh_nn1 (a int primary key, b int, not null a no inherit); +ERROR: conflicting NO INHERIT declaration for not-null constraint on column "a" +CREATE TABLE inh_nn1 (a int not null); +CREATE TABLE inh_nn2 (a int not null no inherit) INHERITS (inh_nn1); +NOTICE: merging column "a" with inherited definition +ERROR: cannot define not-null constraint on column "a" with NO INHERIT +DETAIL: The column has an inherited not-null constraint. +CREATE TABLE inh_nn3 (a int not null, b int, not null a no inherit); +ERROR: conflicting NO INHERIT declaration for not-null constraint on column "a" +CREATE TABLE inh_nn4 (a int not null no inherit, b int, not null a); +ERROR: conflicting NO INHERIT declaration for not-null constraint on column "a" +DROP TABLE inh_nn1, inh_nn2, inh_nn3, inh_nn4; +ERROR: table "inh_nn2" does not exist +-- +-- test inherit/deinherit +-- +create table inh_parent(f1 int); +create table inh_child1(f1 int not null); +create table inh_child2(f1 int); +-- inh_child1 should have not null constraint +alter table inh_child1 inherit inh_parent; +-- should fail, missing NOT NULL constraint +alter table inh_child2 inherit inh_child1; +ERROR: column "f1" in child table "inh_child2" must be marked NOT NULL +alter table inh_child2 alter column f1 set not null; +alter table inh_child2 inherit inh_child1; +-- add NOT NULL constraint recursively +alter table inh_parent alter column f1 set not null; +\d+ inh_parent + Table "public.inh_parent" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_parent_f1_not_null" NOT NULL "f1" +Child tables: inh_child1 + +\d+ inh_child1 + Table "public.inh_child1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_child1_f1_not_null" NOT NULL "f1" (local, inherited) +Inherits: inh_parent +Child tables: inh_child2 + +\d+ inh_child2 + Table "public.inh_child2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_child2_f1_not_null" NOT NULL "f1" (local, inherited) +Inherits: inh_child1 + +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass) + order by 2, 1; + conrelid | conname | contype | coninhcount | conislocal +------------+------------------------+---------+-------------+------------ + inh_child1 | inh_child1_f1_not_null | n | 1 | t + inh_child2 | inh_child2_f1_not_null | n | 1 | t + inh_parent | inh_parent_f1_not_null | n | 0 | t +(3 rows) + +-- +-- test deinherit procedure +-- +-- deinherit inh_child1 +create table inh_child3 () inherits (inh_child1); +alter table inh_child1 no inherit inh_parent; +\d+ inh_parent + Table "public.inh_parent" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_parent_f1_not_null" NOT NULL "f1" + +\d+ inh_child1 + Table "public.inh_child1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_child1_f1_not_null" NOT NULL "f1" +Child tables: inh_child2, + inh_child3 + +\d+ inh_child2 + Table "public.inh_child2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_child2_f1_not_null" NOT NULL "f1" (local, inherited) +Inherits: inh_child1 + +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass::text in ('inh_parent', 'inh_child1', 'inh_child2', 'inh_child3') + order by 2, 1; + conrelid | conname | contype | coninhcount | conislocal +------------+------------------------+---------+-------------+------------ + inh_child1 | inh_child1_f1_not_null | n | 0 | t + inh_child3 | inh_child1_f1_not_null | n | 1 | f + inh_child2 | inh_child2_f1_not_null | n | 1 | t + inh_parent | inh_parent_f1_not_null | n | 0 | t +(4 rows) + +drop table inh_parent, inh_child1, inh_child2, inh_child3; +-- Can turn a NO INHERIT constraint on children into normal, but only if +-- there aren't children +create table inh_parent (a int not null); +create table inh_child (a int not null no inherit); +create table inh_grandchild () inherits (inh_child); +alter table inh_child inherit inh_parent; -- nope +ERROR: cannot add NOT NULL constraint to column "a" of relation "inh_child" with inheritance children +DETAIL: Existing constraint "inh_child_a_not_null" is marked NO INHERIT. +drop table inh_child, inh_grandchild; +create table inh_child (a int not null no inherit); +alter table inh_child inherit inh_parent; -- now it works +\d+ inh_child + Table "public.inh_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "inh_child_a_not_null" NOT NULL "a" (local, inherited) +Inherits: inh_parent + +drop table inh_parent, inh_child; +-- ALTER TABLE INHERIT ensures that the child has not-null constraints +create table inh_parent (a int not null); +create table inh_child (a int); +alter table inh_child inherit inh_parent; -- nope +ERROR: column "a" in child table "inh_child" must be marked NOT NULL +drop table inh_parent, inh_child; +-- a PK in parent must have a not-null in child that it can mark inherited +create table inh_parent (a int primary key); +create table inh_child (a int primary key); +alter table inh_child inherit inh_parent; -- nope +alter table inh_child alter a set not null; +alter table inh_child inherit inh_parent; -- now it works +ERROR: relation "inh_parent" would be inherited from more than once +-- don't interfere with other types of constraints +alter table inh_parent add constraint inh_parent_excl exclude ((1) with =); +alter table inh_parent add constraint inh_parent_uq unique (a); +alter table inh_parent add constraint inh_parent_fk foreign key (a) references inh_parent (a); +create table inh_child2 () inherits (inh_parent); +create table inh_child3 (like inh_parent); +alter table inh_child3 inherit inh_parent; +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint + where conrelid::regclass::text in ('inh_parent', 'inh_child', 'inh_child2', 'inh_child3') + order by 2, 1; + conrelid | conname | contype | coninhcount | conislocal +------------+-----------------------+---------+-------------+------------ + inh_child | inh_child_a_not_null | n | 1 | t + inh_child | inh_child_pkey | p | 0 | t + inh_parent | inh_parent_a_not_null | n | 0 | t + inh_child2 | inh_parent_a_not_null | n | 1 | f + inh_child3 | inh_parent_a_not_null | n | 1 | t + inh_parent | inh_parent_excl | x | 0 | t + inh_parent | inh_parent_fk | f | 0 | t + inh_parent | inh_parent_pkey | p | 0 | t + inh_parent | inh_parent_uq | u | 0 | t +(9 rows) + +drop table inh_parent, inh_child, inh_child2, inh_child3; +-- +-- test multi inheritance tree +-- +create table inh_parent(f1 int not null); +create table inh_child1() inherits(inh_parent); +create table inh_child2() inherits(inh_parent); +create table inh_child3() inherits(inh_child1, inh_child2); +NOTICE: merging multiple inherited definitions of column "f1" +-- show constraint info +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass, 'inh_child3'::regclass) + order by 2, conrelid::regclass::text; + conrelid | conname | contype | coninhcount | conislocal +------------+------------------------+---------+-------------+------------ + inh_child1 | inh_parent_f1_not_null | n | 1 | f + inh_child2 | inh_parent_f1_not_null | n | 1 | f + inh_child3 | inh_parent_f1_not_null | n | 2 | f + inh_parent | inh_parent_f1_not_null | n | 0 | t +(4 rows) + +drop table inh_parent cascade; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table inh_child1 +drop cascades to table inh_child2 +drop cascades to table inh_child3 +-- test child table with inherited columns and +-- with explicitly specified not null constraints +create table inh_parent_1(f1 int); +create table inh_parent_2(f2 text); +create table inh_child(f1 int not null, f2 text not null) inherits(inh_parent_1, inh_parent_2); +NOTICE: merging column "f1" with inherited definition +NOTICE: merging column "f2" with inherited definition +-- show constraint info +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent_1'::regclass, 'inh_parent_2'::regclass, 'inh_child'::regclass) + order by 2, conrelid::regclass::text; + conrelid | conname | contype | coninhcount | conislocal +-----------+-----------------------+---------+-------------+------------ + inh_child | inh_child_f1_not_null | n | 0 | t + inh_child | inh_child_f2_not_null | n | 0 | t +(2 rows) + +-- also drops inh_child table +drop table inh_parent_1 cascade; +NOTICE: drop cascades to table inh_child +drop table inh_parent_2; +-- test multi layer inheritance tree +create table inh_p1(f1 int not null); +create table inh_p2(f1 int not null); +create table inh_p3(f2 int); +create table inh_p4(f1 int not null, f3 text not null); +create table inh_multiparent() inherits(inh_p1, inh_p2, inh_p3, inh_p4); +NOTICE: merging multiple inherited definitions of column "f1" +NOTICE: merging multiple inherited definitions of column "f1" +-- constraint on f1 should have three parents +select conrelid::regclass, contype, conname, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass in ('inh_p1', 'inh_p2', 'inh_p3', 'inh_p4', + 'inh_multiparent') + order by conrelid::regclass::text, conname; + conrelid | contype | conname | attname | coninhcount | conislocal +-----------------+---------+--------------------+---------+-------------+------------ + inh_multiparent | n | inh_p1_f1_not_null | f1 | 3 | f + inh_multiparent | n | inh_p4_f3_not_null | f3 | 1 | f + inh_p1 | n | inh_p1_f1_not_null | f1 | 0 | t + inh_p2 | n | inh_p2_f1_not_null | f1 | 0 | t + inh_p4 | n | inh_p4_f1_not_null | f1 | 0 | t + inh_p4 | n | inh_p4_f3_not_null | f3 | 0 | t +(6 rows) + +create table inh_multiparent2 (a int not null, f1 int) inherits(inh_p3, inh_multiparent); +NOTICE: merging multiple inherited definitions of column "f2" +NOTICE: merging column "f1" with inherited definition +select conrelid::regclass, contype, conname, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass in ('inh_p3', 'inh_multiparent', 'inh_multiparent2') + order by conrelid::regclass::text, conname; + conrelid | contype | conname | attname | coninhcount | conislocal +------------------+---------+-----------------------------+---------+-------------+------------ + inh_multiparent | n | inh_p1_f1_not_null | f1 | 3 | f + inh_multiparent | n | inh_p4_f3_not_null | f3 | 1 | f + inh_multiparent2 | n | inh_multiparent2_a_not_null | a | 0 | t + inh_multiparent2 | n | inh_p1_f1_not_null | f1 | 1 | f + inh_multiparent2 | n | inh_p4_f3_not_null | f3 | 1 | f +(5 rows) + +drop table inh_p1, inh_p2, inh_p3, inh_p4 cascade; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table inh_multiparent +drop cascades to table inh_multiparent2 +-- -- Mixed ownership inheritance tree -- create role regress_alice; diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out index 660245ed0c..7b79e8c64a 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -193,6 +193,8 @@ Indexes: "testpub_tbl2_pkey" PRIMARY KEY, btree (id) Publications: "testpub_foralltables" +Not-null constraints: + "testpub_tbl2_id_not_null" NOT NULL "id" \dRp+ testpub_foralltables Publication testpub_foralltables @@ -771,6 +773,8 @@ Indexes: "testpub_tbl7_pkey" PRIMARY KEY, btree (a) Publications: "testpub_fortable" (a, b) +Not-null constraints: + "testpub_tbl7_a_not_null" NOT NULL "a" -- ok: the column list is the same, we should skip this table (or at least not fail) ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, b); @@ -785,6 +789,8 @@ Indexes: "testpub_tbl7_pkey" PRIMARY KEY, btree (a) Publications: "testpub_fortable" (a, b) +Not-null constraints: + "testpub_tbl7_a_not_null" NOT NULL "a" -- ok: the column list changes, make sure the catalog gets updated ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, c); @@ -799,6 +805,8 @@ Indexes: "testpub_tbl7_pkey" PRIMARY KEY, btree (a) Publications: "testpub_fortable" (a, c) +Not-null constraints: + "testpub_tbl7_a_not_null" NOT NULL "a" -- column list for partitioned tables has to cover replica identities for -- all child relations @@ -935,6 +943,9 @@ Indexes: "testpub_tbl_both_filters_pkey" PRIMARY KEY, btree (a, c) REPLICA IDENTITY Publications: "testpub_both_filters" (a, c) WHERE (c <> 1) +Not-null constraints: + "testpub_tbl_both_filters_a_not_null" NOT NULL "a" + "testpub_tbl_both_filters_c_not_null" NOT NULL "c" DROP TABLE testpub_tbl_both_filters; DROP PUBLICATION testpub_both_filters; @@ -1164,6 +1175,8 @@ Publications: "testpib_ins_trunct" "testpub_default" "testpub_fortbl" +Not-null constraints: + "testpub_tbl1_id_not_null" NOT NULL "id" \dRp+ testpub_default Publication testpub_default @@ -1189,6 +1202,8 @@ Indexes: Publications: "testpib_ins_trunct" "testpub_fortbl" +Not-null constraints: + "testpub_tbl1_id_not_null" NOT NULL "id" -- verify relation cache invalidation when a primary key is added using -- an existing index diff --git a/src/test/regress/expected/replica_identity.out b/src/test/regress/expected/replica_identity.out index e9d7315a9c..b9b8dde018 100644 --- a/src/test/regress/expected/replica_identity.out +++ b/src/test/regress/expected/replica_identity.out @@ -174,6 +174,10 @@ Indexes: "test_replica_identity_partial" UNIQUE, btree (keya, keyb) WHERE keyb <> '3'::text "test_replica_identity_unique_defer" UNIQUE CONSTRAINT, btree (keya, keyb) DEFERRABLE "test_replica_identity_unique_nondefer" UNIQUE CONSTRAINT, btree (keya, keyb) +Not-null constraints: + "test_replica_identity_id_not_null" NOT NULL "id" + "test_replica_identity_keya_not_null" NOT NULL "keya" + "test_replica_identity_keyb_not_null" NOT NULL "keyb" Replica Identity: FULL ALTER TABLE test_replica_identity REPLICA IDENTITY NOTHING; @@ -231,6 +235,9 @@ Indexes: -- used as replica identity. ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL; ERROR: column "id" is in index used as replica identity +-- but it's OK when the identity is FULL +ALTER TABLE test_replica_identity3 REPLICA IDENTITY FULL; +ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL; -- -- Test that replica identity can be set on an index that's not yet valid. -- (This matches the way pg_dump will try to dump a partitioned table.) @@ -253,6 +260,8 @@ ALTER TABLE ONLY test_replica_identity4_1 Partition key: LIST (id) Indexes: "test_replica_identity4_pkey" PRIMARY KEY, btree (id) INVALID REPLICA IDENTITY +Not-null constraints: + "test_replica_identity4_id_not_null" NOT NULL "id" Partitions: test_replica_identity4_1 FOR VALUES IN (1) ALTER INDEX test_replica_identity4_pkey @@ -265,11 +274,26 @@ ALTER INDEX test_replica_identity4_pkey Partition key: LIST (id) Indexes: "test_replica_identity4_pkey" PRIMARY KEY, btree (id) REPLICA IDENTITY +Not-null constraints: + "test_replica_identity4_id_not_null" NOT NULL "id" Partitions: test_replica_identity4_1 FOR VALUES IN (1) +-- Dropping the primary key is not allowed if that would leave the replica +-- identity as nullable +CREATE TABLE test_replica_identity5 (a int not null, b int, c int, + PRIMARY KEY (b, c)); +CREATE UNIQUE INDEX test_replica_identity5_a_b_key ON test_replica_identity5 (a, b); +ALTER TABLE test_replica_identity5 REPLICA IDENTITY USING INDEX test_replica_identity5_a_b_key; +ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey; +ALTER TABLE test_replica_identity5 ALTER b SET NOT NULL; +ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey; +ERROR: constraint "test_replica_identity5_pkey" of relation "test_replica_identity5" does not exist +ALTER TABLE test_replica_identity5 ALTER b DROP NOT NULL; +ERROR: column "b" is in index used as replica identity DROP TABLE test_replica_identity; DROP TABLE test_replica_identity2; DROP TABLE test_replica_identity3; DROP TABLE test_replica_identity4; +DROP TABLE test_replica_identity5; DROP TABLE test_replica_identity_othertable; DROP TABLE test_replica_identity_t3; diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out index 319190855b..4ccf98d8e9 100644 --- a/src/test/regress/expected/rowsecurity.out +++ b/src/test/regress/expected/rowsecurity.out @@ -955,6 +955,8 @@ Policies: POLICY "pp1r" AS RESTRICTIVE TO regress_rls_dave USING ((cid < 55)) +Not-null constraints: + "part_document_dlevel_not_null" NOT NULL "dlevel" Partitions: part_document_fiction FOR VALUES FROM (11) TO (12), part_document_nonfiction FOR VALUES FROM (99) TO (100), part_document_satire FOR VALUES FROM (55) TO (56) diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index 453799abed..bc695efc96 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -920,14 +920,6 @@ insert into parent values (NULL); insert into child (a, b) values (NULL, 'foo'); alter table only parent alter a set not null; alter table child alter a set not null; -delete from parent; -alter table only parent alter a set not null; -insert into parent values (NULL); -alter table child alter a set not null; -insert into child (a, b) values (NULL, 'foo'); -delete from child; -alter table child alter a set not null; -insert into child (a, b) values (NULL, 'foo'); drop table child; drop table parent; @@ -2127,6 +2119,7 @@ DROP TABLE tt9; -- Check that comments on constraints and indexes are not lost at ALTER TABLE. CREATE TABLE comment_test ( id int, + constraint id_notnull_constraint not null id, positive_col int CHECK (positive_col > 0), indexed_col int, CONSTRAINT comment_test_pk PRIMARY KEY (id)); @@ -2136,6 +2129,7 @@ COMMENT ON COLUMN comment_test.id IS 'Column ''id'' on comment_test'; COMMENT ON INDEX comment_test_index IS 'Simple index on comment_test'; COMMENT ON CONSTRAINT comment_test_positive_col_check ON comment_test IS 'CHECK constraint on comment_test.positive_col'; COMMENT ON CONSTRAINT comment_test_pk ON comment_test IS 'PRIMARY KEY constraint of comment_test'; +COMMENT ON CONSTRAINT id_notnull_constraint ON comment_test IS 'NOT NULL constraint of comment_test'; COMMENT ON INDEX comment_test_pk IS 'Index backing the PRIMARY KEY of comment_test'; SELECT col_description('comment_test'::regclass, 1) as comment; @@ -2349,6 +2343,9 @@ CREATE TABLE atnotnull1 (); ALTER TABLE atnotnull1 ADD COLUMN a INT, ALTER a SET NOT NULL; +ALTER TABLE atnotnull1 + ADD COLUMN b INT, + ADD NOT NULL b; ALTER TABLE atnotnull1 ADD COLUMN c INT, ADD PRIMARY KEY (c); diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql index e3e3bea709..7190512a10 100644 --- a/src/test/regress/sql/constraints.sql +++ b/src/test/regress/sql/constraints.sql @@ -6,6 +6,7 @@ -- - PRIMARY KEY clauses -- - UNIQUE clauses -- - EXCLUDE clauses +-- - NOT NULL clauses -- -- directory paths are passed to us in environment variables @@ -597,6 +598,127 @@ ALTER TABLE deferred_excl ADD EXCLUDE (f1 WITH =); DROP TABLE deferred_excl; +-- verify constraints created for NOT NULL clauses +CREATE TABLE notnull_tbl1 (a INTEGER NOT NULL NOT NULL); +\d+ notnull_tbl1 +-- no-op +ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; +\d+ notnull_tbl1 +-- duplicate name +ALTER TABLE notnull_tbl1 ADD COLUMN b INT CONSTRAINT notnull_tbl1_a_not_null NOT NULL; +-- DROP NOT NULL gets rid of both the attnotnull flag and the constraint itself +ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL; +\d+ notnull_tbl1 +-- SET NOT NULL puts both back +ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; +\d+ notnull_tbl1 +-- Doing it twice doesn't create a redundant constraint +ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; +select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass; +-- Using the "table constraint" syntax also works +ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL; +ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a; +\d+ notnull_tbl1 +DROP TABLE notnull_tbl1; + +-- NOT NULL NO INHERIT +CREATE TABLE ATACC1 (a int, not null a no inherit); +CREATE TABLE ATACC2 () INHERITS (ATACC1); +\d+ ATACC2 +DROP TABLE ATACC1, ATACC2; +CREATE TABLE ATACC1 (a int); +ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT; +CREATE TABLE ATACC2 () INHERITS (ATACC1); +\d+ ATACC2 +DROP TABLE ATACC1, ATACC2; +CREATE TABLE ATACC1 (a int); +CREATE TABLE ATACC2 () INHERITS (ATACC1); +ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT; +\d+ ATACC2 +DROP TABLE ATACC1, ATACC2; + +-- NOT NULL NO INHERIT is not possible on partitioned tables +CREATE TABLE ATACC1 (a int NOT NULL NO INHERIT) PARTITION BY LIST (a); +CREATE TABLE ATACC1 (a int, NOT NULL a NO INHERIT) PARTITION BY LIST (a); + +-- it's not possible to override a no-inherit constraint with an inheritable one +CREATE TABLE ATACC2 (a int, CONSTRAINT a_is_not_null NOT NULL a NO INHERIT); +CREATE TABLE ATACC1 (a int); +CREATE TABLE ATACC3 (a int) INHERITS (ATACC2); +ALTER TABLE ATACC2 INHERIT ATACC1; +-- can't override +ALTER TABLE ATACC1 ADD CONSTRAINT ditto NOT NULL a; +-- dropping the NO INHERIT constraint allows this to work +ALTER TABLE ATACC2 DROP CONSTRAINT a_is_not_null; +ALTER TABLE ATACC1 ADD CONSTRAINT ditto NOT NULL a; +\d+ ATACC3 +DROP TABLE ATACC1, ATACC2, ATACC3; + +-- Can't have two constraints with the same name +CREATE TABLE notnull_tbl2 (a INTEGER CONSTRAINT blah NOT NULL, b INTEGER CONSTRAINT blah NOT NULL); + +-- can't drop not-null in primary key +CREATE TABLE notnull_tbl2 (a INTEGER PRIMARY KEY); +ALTER TABLE notnull_tbl2 ALTER a DROP NOT NULL; +DROP TABLE notnull_tbl2; + +CREATE TABLE notnull_tbl3 (a INTEGER NOT NULL, CHECK (a IS NOT NULL)); +ALTER TABLE notnull_tbl3 ALTER A DROP NOT NULL; +ALTER TABLE notnull_tbl3 ADD b int, ADD CONSTRAINT pk PRIMARY KEY (a, b); +\d notnull_tbl3 +ALTER TABLE notnull_tbl3 DROP CONSTRAINT pk; +\d notnull_tbl3 + +-- Primary keys cause not-null constraints to be created. +CREATE TABLE cnn_pk (a int, b int); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +ALTER TABLE cnn_pk ADD CONSTRAINT cnn_primarykey PRIMARY KEY (b); +\d+ cnn_pk* +ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; +\d+ cnn_pk* +DROP TABLE cnn_pk, cnn_pk_child; + +-- As above, but create the primary key ahead of time +CREATE TABLE cnn_pk (a int, b int, CONSTRAINT cnn_primarykey PRIMARY KEY (b)); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +\d+ cnn_pk* +ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; +\d+ cnn_pk* +DROP TABLE cnn_pk, cnn_pk_child; + +-- As above, but create the primary key using a UNIQUE index +CREATE TABLE cnn_pk (a int, b int); +CREATE UNIQUE INDEX cnn_uq ON cnn_pk (b); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +ALTER TABLE cnn_pk ADD CONSTRAINT cnn_primarykey PRIMARY KEY USING INDEX cnn_uq; +\d+ cnn_pk* +DROP TABLE cnn_pk, cnn_pk_child; + +-- Ensure partitions are scanned for null values when adding a PK +create table cnn2_parted(a int) partition by list (a); +create table cnn_part1 partition of cnn2_parted for values in (1, null); +insert into cnn_part1 values (null); +alter table cnn2_parted add primary key (a); +drop table cnn2_parted; + +-- columns in regular and LIKE inheritance should be marked not-nullable +-- for primary keys, even if those are deferred +CREATE TABLE notnull_tbl4 (a INTEGER PRIMARY KEY INITIALLY DEFERRED); +CREATE TABLE notnull_tbl4_lk (LIKE notnull_tbl4); +CREATE TABLE notnull_tbl4_lk2 (LIKE notnull_tbl4 INCLUDING INDEXES); +CREATE TABLE notnull_tbl4_lk3 (LIKE notnull_tbl4 INCLUDING INDEXES, CONSTRAINT a_nn NOT NULL a); +CREATE TABLE notnull_tbl4_cld () INHERITS (notnull_tbl4); +CREATE TABLE notnull_tbl4_cld2 (PRIMARY KEY (a) DEFERRABLE) INHERITS (notnull_tbl4); +CREATE TABLE notnull_tbl4_cld3 (PRIMARY KEY (a) DEFERRABLE, CONSTRAINT a_nn NOT NULL a) INHERITS (notnull_tbl4); +\d+ notnull_tbl4 +\d+ notnull_tbl4_lk +\d+ notnull_tbl4_lk2 +\d+ notnull_tbl4_lk3 +\d+ notnull_tbl4_cld +\d+ notnull_tbl4_cld2 +\d+ notnull_tbl4_cld3 +-- leave these tables around for pg_upgrade testing + -- Comments -- Setup a low-level role to enforce non-superuser checks. CREATE ROLE regress_constraint_comments; diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql index 04008a027b..dea8942c71 100644 --- a/src/test/regress/sql/create_table_like.sql +++ b/src/test/regress/sql/create_table_like.sql @@ -194,9 +194,10 @@ ROLLBACK; DROP TABLE ctlt1, ctlt2, ctlt3, ctlt4, ctlt12_storage, ctlt12_comments, ctlt1_inh, ctlt13_inh, ctlt13_like, ctlt_all, ctla, ctlb CASCADE; -- LIKE must respect NO INHERIT property of constraints -CREATE TABLE noinh_con_copy (a int CHECK (a > 0) NO INHERIT); +CREATE TABLE noinh_con_copy (a int CHECK (a > 0) NO INHERIT, b int not null, + c int not null no inherit); CREATE TABLE noinh_con_copy1 (LIKE noinh_con_copy INCLUDING CONSTRAINTS); -\d noinh_con_copy1 +\d+ noinh_con_copy1 -- fail, as partitioned tables don't allow NO INHERIT constraints CREATE TABLE noinh_con_copy1_parted (LIKE noinh_con_copy INCLUDING ALL) diff --git a/src/test/regress/sql/index_including.sql b/src/test/regress/sql/index_including.sql index 11c95974ec..43bb6ea585 100644 --- a/src/test/regress/sql/index_including.sql +++ b/src/test/regress/sql/index_including.sql @@ -68,7 +68,7 @@ DROP TABLE tbl; CREATE TABLE tbl (c1 int,c2 int, c3 int, c4 box, CONSTRAINT covering PRIMARY KEY(c1,c2) INCLUDE(c3,c4)); SELECT indexrelid::regclass, indnatts, indnkeyatts, indisunique, indisprimary, indkey, indclass FROM pg_index WHERE indrelid = 'tbl'::regclass::oid; -SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid AND contype = 'p'; -- ensure that constraint works INSERT INTO tbl SELECT 1, 2, 3*x, box('4,4,4,4') FROM generate_series(1,10) AS x; INSERT INTO tbl SELECT 1, NULL, 3*x, box('4,4,4,4') FROM generate_series(1,10) AS x; @@ -95,7 +95,7 @@ DROP TABLE tbl; CREATE TABLE tbl (c1 int,c2 int, c3 int, c4 box, PRIMARY KEY(c1,c2) INCLUDE(c3,c4)); SELECT indexrelid::regclass, indnatts, indnkeyatts, indisunique, indisprimary, indkey, indclass FROM pg_index WHERE indrelid = 'tbl'::regclass::oid; -SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid AND contype = 'p'; -- ensure that constraint works INSERT INTO tbl SELECT 1, 2, 3*x, box('4,4,4,4') FROM generate_series(1,10) AS x; INSERT INTO tbl SELECT 1, NULL, 3*x, box('4,4,4,4') FROM generate_series(1,10) AS x; diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql index 5f1f4b80c9..7165d343cd 100644 --- a/src/test/regress/sql/indexing.sql +++ b/src/test/regress/sql/indexing.sql @@ -592,7 +592,7 @@ create table idxpart2 partition of idxpart for values from (0) to (1000) partiti create table idxpart21 partition of idxpart2 for values from (0) to (1000); select conname, contype, conrelid::regclass, conindid::regclass, conkey from pg_constraint where conrelid::regclass::text like 'idxpart%' - order by conname; + order by conrelid::regclass::text, conname; drop table idxpart; -- If a partitioned table has a unique/PK constraint, then it's not possible @@ -671,7 +671,6 @@ alter table only idxpart add primary key (a); -- fail, no not-null constraint alter table idxpart0 alter column a set not null; alter table only idxpart add primary key (a); -- now it works alter index idxpart_pkey attach partition idxpart0_a_key; -alter table idxpart0 alter column a drop not null; -- fail, pkey needs it drop table idxpart; -- if a partition has a unique index without a constraint, does not attach diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql index e3bcfdb181..038e80b621 100644 --- a/src/test/regress/sql/inherit.sql +++ b/src/test/regress/sql/inherit.sql @@ -759,6 +759,279 @@ select * from cnullparent; select * from cnullparent where f1 = 2; drop table cnullparent cascade; +-- +-- Test inheritance of NOT NULL constraints +-- +create table pp1 (f1 int); +create table cc1 (f2 text, f3 int) inherits (pp1); +create table cc2 (f4 float) inherits (pp1,cc1); +create table cc3 () inherits (pp1,cc1,cc2); +alter table pp1 alter f1 set not null; +\d+ cc3 +alter table cc3 no inherit pp1; +alter table cc3 no inherit cc1; +alter table cc3 no inherit cc2; +\d+ cc3 +drop table cc3; + +-- named NOT NULL constraint +alter table cc1 add column a2 int constraint nn not null; +\d+ cc1 +\d+ cc2 +alter table pp1 alter column f1 set not null; +\d+ pp1 +\d+ cc1 +\d+ cc2 + +-- cannot create table with inconsistent NO INHERIT constraint +create table cc3 (a2 int not null no inherit) inherits (cc1); + +-- change NO INHERIT status of inherited constraint: no dice, it's inherited +alter table cc2 add not null a2 no inherit; + +-- remove constraint from cc2: no dice, it's inherited +alter table cc2 alter column a2 drop not null; + +-- remove constraint from cc1, should succeed +alter table cc1 alter column a2 drop not null; +\d+ cc1 + +-- same for cc2 +alter table cc2 alter column f1 drop not null; +\d+ cc2 + +-- remove from cc1, should fail again +alter table cc1 alter column f1 drop not null; + +-- remove from pp1, should succeed +alter table pp1 alter column f1 drop not null; +\d+ pp1 + +alter table pp1 add primary key (f1); +-- Leave these tables around, for pg_upgrade testing + +-- test that inhcount is updated correctly through multiple inheritance +create table inh_pp1 (f1 int); +create table inh_cc1 (f2 text, f3 int) inherits (inh_pp1); +create table inh_cc2(f4 float) inherits(inh_pp1,inh_cc1); +alter table inh_pp1 alter column f1 set not null; +alter table inh_cc2 no inherit inh_pp1; +alter table inh_cc2 no inherit inh_cc1; +\d+ inh_cc2 +drop table inh_pp1, inh_cc1, inh_cc2; + +create table inh_pp1 (f1 int not null); +create table inh_cc1 (f2 text, f3 int) inherits (inh_pp1); +create table inh_cc2(f4 float) inherits(inh_pp1,inh_cc1); +alter table inh_pp1 alter column f1 drop not null; +\d+ inh_cc2 +drop table inh_pp1, inh_cc1, inh_cc2; + + +-- Test a not-null addition that must walk down the hierarchy +CREATE TABLE inh_parent (); +CREATE TABLE inh_child (i int) INHERITS (inh_parent); +CREATE TABLE inh_grandchild () INHERITS (inh_parent, inh_child); +ALTER TABLE inh_parent ADD COLUMN i int NOT NULL; +drop table inh_parent, inh_child, inh_grandchild; + +-- Test the same constraint name for different columns in different parents +create table inh_parent1(a int constraint nn not null); +create table inh_parent2(b int constraint nn not null); +create table inh_child () inherits (inh_parent1, inh_parent2); +\d+ inh_child +drop table inh_parent1, inh_parent2, inh_child; + +-- Test multiple parents with overlapping primary keys +create table inh_parent1(a int, b int, c int, primary key (a, b)); +create table inh_parent2(d int, e int, b int, primary key (d, b)); +create table inh_child() inherits (inh_parent1, inh_parent2); +select conrelid::regclass, conname, contype, conkey, + coninhcount, conislocal, connoinherit + from pg_constraint where contype in ('n','p') and + conrelid::regclass::text in ('inh_child', 'inh_parent1', 'inh_parent2') + order by 1, 2; +\d+ inh_child +drop table inh_parent1, inh_parent2, inh_child; + +-- NOT NULL NO INHERIT +create table inh_nn_parent(a int); +create table inh_nn_child() inherits (inh_nn_parent); +alter table inh_nn_parent add not null a no inherit; +create table inh_nn_child2() inherits (inh_nn_parent); +select conrelid::regclass, conname, contype, conkey, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal, connoinherit + from pg_constraint where contype = 'n' and + conrelid::regclass::text like 'inh\_nn\_%' + order by 2, 1; +\d+ inh_nn* +drop table inh_nn_parent, inh_nn_child, inh_nn_child2; + +CREATE TABLE inh_nn_parent (a int, NOT NULL a NO INHERIT); +CREATE TABLE inh_nn_child() INHERITS (inh_nn_parent); +ALTER TABLE inh_nn_parent ADD CONSTRAINT nna NOT NULL a; +ALTER TABLE inh_nn_parent ALTER a SET NOT NULL; +DROP TABLE inh_nn_parent cascade; + +-- Adding a PK at the top level of a hierarchy should cause all descendants +-- to be checked for nulls, even past a no-inherit constraint +CREATE TABLE inh_nn_lvl1 (a int); +CREATE TABLE inh_nn_lvl2 () INHERITS (inh_nn_lvl1); +CREATE TABLE inh_nn_lvl3 (CONSTRAINT foo NOT NULL a NO INHERIT) INHERITS (inh_nn_lvl2); +ALTER TABLE inh_nn_lvl1 ADD PRIMARY KEY (a); +DROP TABLE inh_nn_lvl1, inh_nn_lvl2, inh_nn_lvl3; + +-- Disallow specifying conflicting NO INHERIT flags for the same constraint +CREATE TABLE inh_nn1 (a int primary key, b int, not null a no inherit); +CREATE TABLE inh_nn1 (a int not null); +CREATE TABLE inh_nn2 (a int not null no inherit) INHERITS (inh_nn1); +CREATE TABLE inh_nn3 (a int not null, b int, not null a no inherit); +CREATE TABLE inh_nn4 (a int not null no inherit, b int, not null a); +DROP TABLE inh_nn1, inh_nn2, inh_nn3, inh_nn4; + +-- +-- test inherit/deinherit +-- +create table inh_parent(f1 int); +create table inh_child1(f1 int not null); +create table inh_child2(f1 int); + +-- inh_child1 should have not null constraint +alter table inh_child1 inherit inh_parent; + +-- should fail, missing NOT NULL constraint +alter table inh_child2 inherit inh_child1; + +alter table inh_child2 alter column f1 set not null; +alter table inh_child2 inherit inh_child1; + +-- add NOT NULL constraint recursively +alter table inh_parent alter column f1 set not null; + +\d+ inh_parent +\d+ inh_child1 +\d+ inh_child2 + +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass) + order by 2, 1; + +-- +-- test deinherit procedure +-- + +-- deinherit inh_child1 +create table inh_child3 () inherits (inh_child1); +alter table inh_child1 no inherit inh_parent; +\d+ inh_parent +\d+ inh_child1 +\d+ inh_child2 +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass::text in ('inh_parent', 'inh_child1', 'inh_child2', 'inh_child3') + order by 2, 1; +drop table inh_parent, inh_child1, inh_child2, inh_child3; + +-- Can turn a NO INHERIT constraint on children into normal, but only if +-- there aren't children +create table inh_parent (a int not null); +create table inh_child (a int not null no inherit); +create table inh_grandchild () inherits (inh_child); +alter table inh_child inherit inh_parent; -- nope +drop table inh_child, inh_grandchild; +create table inh_child (a int not null no inherit); +alter table inh_child inherit inh_parent; -- now it works +\d+ inh_child +drop table inh_parent, inh_child; + +-- ALTER TABLE INHERIT ensures that the child has not-null constraints +create table inh_parent (a int not null); +create table inh_child (a int); +alter table inh_child inherit inh_parent; -- nope +drop table inh_parent, inh_child; + +-- a PK in parent must have a not-null in child that it can mark inherited +create table inh_parent (a int primary key); +create table inh_child (a int primary key); +alter table inh_child inherit inh_parent; -- nope +alter table inh_child alter a set not null; +alter table inh_child inherit inh_parent; -- now it works + +-- don't interfere with other types of constraints +alter table inh_parent add constraint inh_parent_excl exclude ((1) with =); +alter table inh_parent add constraint inh_parent_uq unique (a); +alter table inh_parent add constraint inh_parent_fk foreign key (a) references inh_parent (a); +create table inh_child2 () inherits (inh_parent); +create table inh_child3 (like inh_parent); +alter table inh_child3 inherit inh_parent; +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint + where conrelid::regclass::text in ('inh_parent', 'inh_child', 'inh_child2', 'inh_child3') + order by 2, 1; + +drop table inh_parent, inh_child, inh_child2, inh_child3; + +-- +-- test multi inheritance tree +-- +create table inh_parent(f1 int not null); +create table inh_child1() inherits(inh_parent); +create table inh_child2() inherits(inh_parent); +create table inh_child3() inherits(inh_child1, inh_child2); + +-- show constraint info +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass, 'inh_child3'::regclass) + order by 2, conrelid::regclass::text; + +drop table inh_parent cascade; + +-- test child table with inherited columns and +-- with explicitly specified not null constraints +create table inh_parent_1(f1 int); +create table inh_parent_2(f2 text); +create table inh_child(f1 int not null, f2 text not null) inherits(inh_parent_1, inh_parent_2); + +-- show constraint info +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent_1'::regclass, 'inh_parent_2'::regclass, 'inh_child'::regclass) + order by 2, conrelid::regclass::text; + +-- also drops inh_child table +drop table inh_parent_1 cascade; +drop table inh_parent_2; + +-- test multi layer inheritance tree +create table inh_p1(f1 int not null); +create table inh_p2(f1 int not null); +create table inh_p3(f2 int); +create table inh_p4(f1 int not null, f3 text not null); + +create table inh_multiparent() inherits(inh_p1, inh_p2, inh_p3, inh_p4); + +-- constraint on f1 should have three parents +select conrelid::regclass, contype, conname, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass in ('inh_p1', 'inh_p2', 'inh_p3', 'inh_p4', + 'inh_multiparent') + order by conrelid::regclass::text, conname; + +create table inh_multiparent2 (a int not null, f1 int) inherits(inh_p3, inh_multiparent); +select conrelid::regclass, contype, conname, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass in ('inh_p3', 'inh_multiparent', 'inh_multiparent2') + order by conrelid::regclass::text, conname; + +drop table inh_p1, inh_p2, inh_p3, inh_p4 cascade; + -- -- Mixed ownership inheritance tree -- diff --git a/src/test/regress/sql/replica_identity.sql b/src/test/regress/sql/replica_identity.sql index 039cca25e8..30daec05b7 100644 --- a/src/test/regress/sql/replica_identity.sql +++ b/src/test/regress/sql/replica_identity.sql @@ -100,6 +100,9 @@ ALTER TABLE test_replica_identity3 ALTER COLUMN id TYPE bigint; -- ALTER TABLE DROP NOT NULL is not allowed for columns part of an index -- used as replica identity. ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL; +-- but it's OK when the identity is FULL +ALTER TABLE test_replica_identity3 REPLICA IDENTITY FULL; +ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL; -- -- Test that replica identity can be set on an index that's not yet valid. @@ -120,9 +123,21 @@ ALTER INDEX test_replica_identity4_pkey ATTACH PARTITION test_replica_identity4_1_pkey; \d+ test_replica_identity4 +-- Dropping the primary key is not allowed if that would leave the replica +-- identity as nullable +CREATE TABLE test_replica_identity5 (a int not null, b int, c int, + PRIMARY KEY (b, c)); +CREATE UNIQUE INDEX test_replica_identity5_a_b_key ON test_replica_identity5 (a, b); +ALTER TABLE test_replica_identity5 REPLICA IDENTITY USING INDEX test_replica_identity5_a_b_key; +ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey; +ALTER TABLE test_replica_identity5 ALTER b SET NOT NULL; +ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey; +ALTER TABLE test_replica_identity5 ALTER b DROP NOT NULL; + DROP TABLE test_replica_identity; DROP TABLE test_replica_identity2; DROP TABLE test_replica_identity3; DROP TABLE test_replica_identity4; +DROP TABLE test_replica_identity5; DROP TABLE test_replica_identity_othertable; DROP TABLE test_replica_identity_t3; -- 2.39.2 ^ permalink raw reply [nested|flat] 6+ messages in thread
end of thread, other threads:[~2024-09-24 19:07 UTC | newest] Thread overview: 6+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2021-03-11 09:01 [PATCH v54 3/7] Make archiver process an auxiliary process Kyotaro Horiguchi <[email protected]> 2024-09-20 21:15 Re: not null constraints, again Alvaro Herrera <[email protected]> 2024-09-23 06:40 ` Re: not null constraints, again Tender Wang <[email protected]> 2024-09-24 03:22 ` Re: not null constraints, again jian he <[email protected]> 2024-09-24 16:59 ` Re: not null constraints, again Alvaro Herrera <[email protected]> 2024-09-24 19:07 ` Re: not null constraints, again Alvaro Herrera <[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