public inbox for [email protected]
help / color / mirror / Atom feed[PATCH v4 2/2] Do COPY FROM encoding conversion/verification in larger chunks.
40+ messages / 7 participants
[nested] [flat]
* [PATCH v4 2/2] Do COPY FROM encoding conversion/verification in larger chunks.
@ 2021-02-07 18:09 Heikki Linnakangas <[email protected]>
0 siblings, 0 replies; 40+ messages in thread
From: Heikki Linnakangas @ 2021-02-07 18:09 UTC (permalink / raw)
NOTE: This changes behavior in one corner-case: if client and server
encodings are the same single-byte encoding (e.g. latin1), previously the
input would not be checked for zero bytes ('\0'). Any fields containing
zero bytes would be truncated at the zero. But if encoding conversion was
needed, the conversion routine would throw an error on the zero. After
this commit, the input is always checked for zeros.
---
src/backend/commands/copyfrom.c | 80 ++--
src/backend/commands/copyfromparse.c | 515 +++++++++++++++++------
src/include/commands/copyfrom_internal.h | 62 +--
src/include/mb/pg_wchar.h | 22 +-
4 files changed, 500 insertions(+), 179 deletions(-)
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index c39cc736ed2..37bbf7dc293 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -3,6 +3,12 @@
* copyfrom.c
* COPY <table> FROM file/program/client
*
+ * This file contains routines needed to efficiently load tuples into a
+ * table. That includes looking up the correct partition, firing triggers,
+ * calling the table AM function to insert the data, and updating indexes.
+ * Reading data from the input file or client and parsing it into Datums
+ * is handled in copyfromparse.c.
+ *
* Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
@@ -23,6 +29,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "access/xlog.h"
+#include "catalog/namespace.h"
#include "commands/copy.h"
#include "commands/copyfrom_internal.h"
#include "commands/progress.h"
@@ -87,7 +94,7 @@ typedef struct CopyMultiInsertInfo
List *multiInsertBuffers; /* List of tracked CopyMultiInsertBuffers */
int bufferedTuples; /* number of tuples buffered over all buffers */
int bufferedBytes; /* number of bytes from all buffered tuples */
- CopyFromState cstate; /* Copy state for this CopyMultiInsertInfo */
+ CopyFromState cstate; /* Copy state for this CopyMultiInsertInfo */
EState *estate; /* Executor state used for COPY */
CommandId mycid; /* Command Id used for COPY */
int ti_options; /* table insert options */
@@ -107,7 +114,7 @@ static void ClosePipeFromProgram(CopyFromState cstate);
void
CopyFromErrorCallback(void *arg)
{
- CopyFromState cstate = (CopyFromState) arg;
+ CopyFromState cstate = (CopyFromState) arg;
char curlineno_str[32];
snprintf(curlineno_str, sizeof(curlineno_str), UINT64_FORMAT,
@@ -149,15 +156,9 @@ CopyFromErrorCallback(void *arg)
/*
* Error is relevant to a particular line.
*
- * If line_buf still contains the correct line, and it's already
- * transcoded, print it. If it's still in a foreign encoding, it's
- * quite likely that the error is precisely a failure to do
- * encoding conversion (ie, bad data). We dare not try to convert
- * it, and at present there's no way to regurgitate it without
- * conversion. So we have to punt and just report the line number.
+ * If line_buf still contains the correct line, print it.
*/
- if (cstate->line_buf_valid &&
- (cstate->line_buf_converted || !cstate->need_transcoding))
+ if (cstate->line_buf_valid)
{
char *lineval;
@@ -300,7 +301,7 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
MemoryContext oldcontext;
int i;
uint64 save_cur_lineno;
- CopyFromState cstate = miinfo->cstate;
+ CopyFromState cstate = miinfo->cstate;
EState *estate = miinfo->estate;
CommandId mycid = miinfo->mycid;
int ti_options = miinfo->ti_options;
@@ -1186,7 +1187,7 @@ BeginCopyFrom(ParseState *pstate,
List *attnamelist,
List *options)
{
- CopyFromState cstate;
+ CopyFromState cstate;
bool pipe = (filename == NULL);
TupleDesc tupDesc;
AttrNumber num_phys_attrs,
@@ -1214,7 +1215,7 @@ BeginCopyFrom(ParseState *pstate,
oldcontext = MemoryContextSwitchTo(cstate->copycontext);
/* Extract options from the statement node tree */
- ProcessCopyOptions(pstate, &cstate->opts, true /* is_from */, options);
+ ProcessCopyOptions(pstate, &cstate->opts, true /* is_from */ , options);
/* Process the target relation */
cstate->rel = rel;
@@ -1305,15 +1306,20 @@ BeginCopyFrom(ParseState *pstate,
cstate->file_encoding = cstate->opts.file_encoding;
/*
- * Set up encoding conversion info. Even if the file and server encodings
- * are the same, we must apply pg_any_to_server() to validate data in
- * multibyte encodings.
+ * Look up encoding conversion function.
*/
- cstate->need_transcoding =
- (cstate->file_encoding != GetDatabaseEncoding() ||
- pg_database_encoding_max_length() > 1);
- /* See Multibyte encoding comment above */
- cstate->encoding_embeds_ascii = PG_ENCODING_IS_CLIENT_ONLY(cstate->file_encoding);
+ if (cstate->file_encoding == GetDatabaseEncoding() ||
+ cstate->file_encoding == PG_SQL_ASCII ||
+ GetDatabaseEncoding() == PG_SQL_ASCII)
+ {
+ cstate->need_transcoding = false;
+ }
+ else
+ {
+ cstate->need_transcoding = true;
+ cstate->conversion_proc = FindDefaultConversionProc(cstate->file_encoding,
+ GetDatabaseEncoding());
+ }
cstate->copy_src = COPY_FILE; /* default */
@@ -1324,7 +1330,6 @@ BeginCopyFrom(ParseState *pstate,
oldcontext = MemoryContextSwitchTo(cstate->copycontext);
/* Initialize state variables */
- cstate->reached_eof = false;
cstate->eol_type = EOL_UNKNOWN;
cstate->cur_relname = RelationGetRelationName(cstate->rel);
cstate->cur_lineno = 0;
@@ -1332,19 +1337,36 @@ BeginCopyFrom(ParseState *pstate,
cstate->cur_attval = NULL;
/*
- * Set up variables to avoid per-attribute overhead. attribute_buf and
- * raw_buf are used in both text and binary modes, but we use line_buf
- * only in text mode.
+ * Allocate buffers for the input pipeline.
+ *
+ * attribute_buf and raw_buf are used in both text and binary modes, but
+ * input_buf and line_buf only in text mode.
*/
- initStringInfo(&cstate->attribute_buf);
- cstate->raw_buf = (char *) palloc(RAW_BUF_SIZE + 1);
+ cstate->raw_buf = palloc(RAW_BUF_SIZE);
cstate->raw_buf_index = cstate->raw_buf_len = 0;
+ cstate->raw_reached_eof = false;
+
if (!cstate->opts.binary)
{
+ /*
+ * If encoding conversion is needed, we need another buffer to hold
+ * the converted input data. Otherwise, we can just point input_buf
+ * to the same buffer as raw_buf.
+ */
+ if (cstate->need_transcoding)
+ {
+ cstate->input_buf = (char *) palloc(INPUT_BUF_SIZE + 1);
+ cstate->input_buf_index = cstate->input_buf_len = 0;
+ }
+ else
+ cstate->input_buf = cstate->raw_buf;
+ cstate->input_reached_eof = false;
+
initStringInfo(&cstate->line_buf);
- cstate->line_buf_converted = false;
}
+ initStringInfo(&cstate->attribute_buf);
+
/* Assign range table, we'll need it in CopyFrom. */
if (pstate)
cstate->range_table = pstate->p_rtable;
@@ -1563,7 +1585,7 @@ ClosePipeFromProgram(CopyFromState cstate)
* should not report that as an error. Otherwise, SIGPIPE indicates a
* problem.
*/
- if (!cstate->reached_eof &&
+ if (!cstate->raw_reached_eof &&
wait_result_is_signal(pclose_rc, SIGPIPE))
return;
diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c
index 315b16fd7af..e4f85c88792 100644
--- a/src/backend/commands/copyfromparse.c
+++ b/src/backend/commands/copyfromparse.c
@@ -3,6 +3,49 @@
* copyfromparse.c
* Parse CSV/text/binary format for COPY FROM.
*
+ * This file contains routines to parse the text, CSV and binary input
+ * formats. The main entry point is NextCopyFrom(), which parses the
+ * next input line and returns it as Datums.
+ *
+ * In text/CSV mode, the parsing happens in multiple stages:
+ *
+ * [data source] --> raw_buf --> input_buf --> line_buf --> attribute_buf
+ * 1. 2. 3. 4.
+ *
+ * 1. CopyLoadRawBuf() reads raw data from the input file or client, and
+ * places it into 'raw_buf'.
+ *
+ * 2. CopyConvertBuf() calls the encoding conversion function to convert
+ * the data in 'raw_buf' from client to server encoding, placing the
+ * converted result in 'input_buf'.
+ *
+ * 3. CopyReadLine() parses the data in 'input_buf', one line at a time.
+ * It is responsible for finding the next newline marker, taking quote and
+ * escape characters into account according to the COPY options. The line
+ * is copied into 'line_buf', with quotes and escape characters still intact.
+ *
+ * 4. CopyReadAttributesText/CSV() function takes the input line from
+ * 'line_buf', and splits it into fields, unescaping the data as required.
+ * The fields are stored in 'attribute_buf', and 'raw_fields' array holds
+ * pointers to each field.
+ *
+ * If encoding conversion is not required, a shortcut is taken in step 2 to
+ * avoid copying the data unnecessarily. The 'input_buf' pointer is set to
+ * point directly to 'raw_buf', so that CopyLoadRawBuf() loads the raw data
+ * directly into 'input_buf'. CopyConvertBuf() then merely validates that
+ * the data is valid in the current encoding.
+ *
+ * In binary mode, the pipeline is much simpler. Input is loaded into
+ * into 'raw_buf', and encoding conversion is done in the datatype-specific
+ * receive functions, if required. 'input_buf' and 'line_buf' are not used,
+ * but 'attribute_buf' is used as a temporary buffer to hold one attribute's
+ * data when it's passed the receive function.
+ *
+ * 'raw_buf' is always 64 kB in size (RAW_BUF_SIZE). 'input_buf' is also
+ * 64 kB (INPUT_BUF_SIZE), if encoding conversion is required. 'line_buf'
+ * and 'attribute_buf' are expanded on demand, to hold the longest line
+ * encountered so far.
+ *
* Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
@@ -35,7 +78,7 @@
#define OCTVALUE(c) ((c) - '0')
/*
- * These macros centralize code used to process line_buf and raw_buf buffers.
+ * These macros centralize code used to process line_buf and input_buf buffers.
* They are macros because they often do continue/break control and to avoid
* function call overhead in tight COPY loops.
*
@@ -53,9 +96,9 @@
#define IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(extralen) \
if (1) \
{ \
- if (raw_buf_ptr + (extralen) >= copy_buf_len && !hit_eof) \
+ if (input_buf_ptr + (extralen) >= copy_buf_len && !hit_eof) \
{ \
- raw_buf_ptr = prev_raw_ptr; /* undo fetch */ \
+ input_buf_ptr = prev_raw_ptr; /* undo fetch */ \
need_data = true; \
continue; \
} \
@@ -65,10 +108,10 @@ if (1) \
#define IF_NEED_REFILL_AND_EOF_BREAK(extralen) \
if (1) \
{ \
- if (raw_buf_ptr + (extralen) >= copy_buf_len && hit_eof) \
+ if (input_buf_ptr + (extralen) >= copy_buf_len && hit_eof) \
{ \
if (extralen) \
- raw_buf_ptr = copy_buf_len; /* consume the partial character */ \
+ input_buf_ptr = copy_buf_len; /* consume the partial character */ \
/* backslash just before EOF, treat as data char */ \
result = true; \
break; \
@@ -77,17 +120,17 @@ if (1) \
/*
* Transfer any approved data to line_buf; must do this to be sure
- * there is some room in raw_buf.
+ * there is some room in input_buf.
*/
#define REFILL_LINEBUF \
if (1) \
{ \
- if (raw_buf_ptr > cstate->raw_buf_index) \
+ if (input_buf_ptr > cstate->input_buf_index) \
{ \
appendBinaryStringInfo(&cstate->line_buf, \
- cstate->raw_buf + cstate->raw_buf_index, \
- raw_buf_ptr - cstate->raw_buf_index); \
- cstate->raw_buf_index = raw_buf_ptr; \
+ cstate->input_buf + cstate->input_buf_index, \
+ input_buf_ptr - cstate->input_buf_index); \
+ cstate->input_buf_index = input_buf_ptr; \
} \
} else ((void) 0)
@@ -95,7 +138,7 @@ if (1) \
#define NO_END_OF_COPY_GOTO \
if (1) \
{ \
- raw_buf_ptr = prev_raw_ptr + 1; \
+ input_buf_ptr = prev_raw_ptr + 1; \
goto not_end_of_copy; \
} else ((void) 0)
@@ -118,7 +161,7 @@ static int CopyGetData(CopyFromState cstate, void *databuf,
int minread, int maxread);
static inline bool CopyGetInt32(CopyFromState cstate, int32 *val);
static inline bool CopyGetInt16(CopyFromState cstate, int16 *val);
-static bool CopyLoadRawBuf(CopyFromState cstate);
+static void CopyLoadInputBuf(CopyFromState cstate);
static int CopyReadBinaryData(CopyFromState cstate, char *dest, int nbytes);
void
@@ -226,7 +269,7 @@ CopyGetData(CopyFromState cstate, void *databuf, int minread, int maxread)
(errcode_for_file_access(),
errmsg("could not read from COPY file: %m")));
if (bytesread == 0)
- cstate->reached_eof = true;
+ cstate->raw_reached_eof = true;
break;
case COPY_OLD_FE:
@@ -247,7 +290,7 @@ CopyGetData(CopyFromState cstate, void *databuf, int minread, int maxread)
bytesread = minread;
break;
case COPY_NEW_FE:
- while (maxread > 0 && bytesread < minread && !cstate->reached_eof)
+ while (maxread > 0 && bytesread < minread && !cstate->raw_reached_eof)
{
int avail;
@@ -275,7 +318,7 @@ CopyGetData(CopyFromState cstate, void *databuf, int minread, int maxread)
break;
case 'c': /* CopyDone */
/* COPY IN correctly terminated by frontend */
- cstate->reached_eof = true;
+ cstate->raw_reached_eof = true;
return bytesread;
case 'f': /* CopyFail */
ereport(ERROR,
@@ -361,34 +404,303 @@ CopyGetInt16(CopyFromState cstate, int16 *val)
/*
- * CopyLoadRawBuf loads some more data into raw_buf
+ * Perform encoding conversion on data in 'raw_buf', writing the converted
+ * data into 'input_buf'.
*
- * Returns true if able to obtain at least one more byte, else false.
+ * On entry, there must be some data to convert in 'raw_buf'.
+ */
+static void
+CopyConvertBuf(CopyFromState cstate)
+{
+ /*
+ * If the file and server encoding are the same, no encoding conversion is
+ * required. However, we still need to verify that the input is valid for
+ * the encoding.
+ */
+ if (!cstate->need_transcoding)
+ {
+ /*
+ * When conversion is not required, input_buf and raw_buf are the
+ * same. raw_buf_len is the total number of bytes in the buffer, and
+ * input_buf_len tracks how many of those bytes have already been
+ * verified.
+ */
+ int preverifiedlen = cstate->input_buf_len;
+ int unverifiedlen = cstate->raw_buf_len - cstate->input_buf_len;
+ int nverified;
+
+ if (unverifiedlen == 0)
+ {
+ /*
+ * If no more raw data is coming, report the EOF to the caller.
+ */
+ if (cstate->raw_reached_eof)
+ cstate->input_reached_eof = true;
+ return;
+ }
+
+ /*
+ * Verify the new data, including any residual unverified bytes from
+ * previous round.
+ */
+ nverified = pg_encoding_verifymbstr(cstate->file_encoding,
+ cstate->raw_buf + preverifiedlen,
+ unverifiedlen);
+ if (nverified == 0)
+ {
+ /*
+ * Could not verify anything.
+ *
+ * If there is no more raw input data coming, it means that there
+ * was an incomplete multi-byte sequence at the end. Also, if
+ * there's "enough" input left, we should be able to verify at
+ * least one character, and a failure to do so means that we've
+ * hit an invalid byte sequence.
+ */
+ if (cstate->raw_reached_eof || unverifiedlen >= pg_database_encoding_max_length())
+ cstate->input_reached_error = true;
+ return;
+ }
+ cstate->input_buf_len += nverified;
+ }
+ else
+ {
+ /*
+ * Encoding conversion is needed.
+ */
+ int nbytes;
+ unsigned char *src;
+ int srclen;
+ unsigned char *dst;
+ int dstlen;
+ int convertedlen;
+
+ if (RAW_BUF_BYTES(cstate) == 0)
+ {
+ /*
+ * If no more raw data is coming, report the EOF to the caller.
+ */
+ if (cstate->raw_reached_eof)
+ cstate->input_reached_eof = true;
+ return;
+ }
+
+ /*
+ * First, copy down any unprocessed data.
+ */
+ nbytes = INPUT_BUF_BYTES(cstate);
+ if (nbytes > 0 && cstate->input_buf_index > 0)
+ memmove(cstate->input_buf, cstate->input_buf + cstate->input_buf_index,
+ nbytes);
+ cstate->input_buf_index = 0;
+ cstate->input_buf_len = nbytes;
+ cstate->input_buf[nbytes] = '\0';
+
+ src = (unsigned char *) cstate->raw_buf + cstate->raw_buf_index;
+ srclen = cstate->raw_buf_len - cstate->raw_buf_index;
+ dst = (unsigned char *) cstate->input_buf + cstate->input_buf_len;
+ dstlen = INPUT_BUF_SIZE - cstate->input_buf_len + 1;
+
+ /*
+ * Do the conversion. This might stop short, if there is an invalid
+ * byte sequence in the input. We'll convert as much as we can in
+ * that case.
+ *
+ * Note: Even if we hit an invalid byte sequence, we don't report the
+ * error until all the valid bytes have been consumed. The input
+ * might contain an end-of-input marker (\.), and we don't want to
+ * report an error if the invalid byte sequence is after the
+ * end-of-input marker. We might unnecessarily convert some data
+ * after the end-of-input marker as long as it's valid for the
+ * encoding, but that's harmless.
+ */
+ convertedlen = pg_do_encoding_conversion_buf(cstate->conversion_proc,
+ cstate->file_encoding,
+ GetDatabaseEncoding(),
+ src, srclen,
+ dst, dstlen,
+ true);
+ if (convertedlen == 0)
+ {
+ /*
+ * Could not convert anything. If there is no more raw input data
+ * coming, it means that there was an incomplete multi-byte
+ * sequence at the end. Also, if there is plenty of input left,
+ * we should be able to convert at least one character, so a
+ * failure to do so must mean that we've hit a byte sequence
+ * that's invalid.
+ */
+ if (cstate->raw_reached_eof || srclen >= MAX_CONVERSION_INPUT_LENGTH)
+ cstate->input_reached_error = true;
+ return;
+ }
+ cstate->raw_buf_index += convertedlen;
+ cstate->input_buf_len += strlen((char *) dst);
+ }
+}
+
+/*
+ * Report an encoding or conversion error.
+ */
+static void
+CopyConversionError(CopyFromState cstate)
+{
+ Assert(cstate->raw_buf_len > 0);
+ Assert(cstate->input_reached_error);
+
+ if (!cstate->need_transcoding)
+ {
+ /*
+ * Everything up to input_buf_len was successfully verified, and
+ * input_buf_len points to the invalid or incomplete character.
+ */
+ report_invalid_encoding(cstate->file_encoding,
+ cstate->raw_buf + cstate->input_buf_len,
+ cstate->raw_buf_len - cstate->input_buf_len);
+ }
+ else
+ {
+ /*
+ * raw_buf_index points to the invalid or untranslatable character. We
+ * let the conversion routine report the error, because it can provide
+ * a more specific error message than we could here. An earlier call
+ * to the conversion routine in CopyConvertBuf() detected that there
+ * is an error, now we call the conversion routine again with
+ * noError=false, to have it throw the error.
+ */
+ unsigned char *src;
+ int srclen;
+ unsigned char *dst;
+ int dstlen;
+
+ src = (unsigned char *) cstate->raw_buf + cstate->raw_buf_index;
+ srclen = cstate->raw_buf_len - cstate->raw_buf_index;
+ dst = (unsigned char *) cstate->input_buf + cstate->input_buf_len;
+ dstlen = INPUT_BUF_SIZE - cstate->input_buf_len + 1;
+
+ (void) pg_do_encoding_conversion_buf(cstate->conversion_proc,
+ cstate->file_encoding,
+ GetDatabaseEncoding(),
+ src, srclen,
+ dst, dstlen,
+ false);
+
+ /*
+ * The conversion routine should have reported an error, so this
+ * should not be reached.
+ */
+ elog(ERROR, "encoding conversion failed without error");
+ }
+}
+
+/*
+ * Load more data from data source to raw_buf.
*
* If RAW_BUF_BYTES(cstate) > 0, the unprocessed bytes are moved to the start
- * of the buffer and then we load more data after that. This case occurs only
- * when a multibyte character crosses a bufferload boundary.
+ * of the buffer and then we load more data after that.
*/
-static bool
+static void
CopyLoadRawBuf(CopyFromState cstate)
{
- int nbytes = RAW_BUF_BYTES(cstate);
+ int nbytes;
int inbytes;
- /* Copy down the unprocessed data if any. */
- if (nbytes > 0)
+ /*
+ * In text mode, if encoding conversion is not required, raw_buf and
+ * input_buf point to the same buffer. Their len/index better agree, too.
+ */
+ if (cstate->raw_buf == cstate->input_buf)
+ {
+ Assert(!cstate->need_transcoding);
+ Assert(cstate->raw_buf_index == cstate->input_buf_index);
+ Assert(cstate->input_buf_len <= cstate->raw_buf_len);
+ }
+
+ /*
+ * Copy down the unprocessed data if any.
+ */
+ nbytes = RAW_BUF_BYTES(cstate);
+ if (nbytes > 0 && cstate->raw_buf_index > 0)
memmove(cstate->raw_buf, cstate->raw_buf + cstate->raw_buf_index,
nbytes);
+ cstate->raw_buf_len -= cstate->raw_buf_index;
+ cstate->raw_buf_index = 0;
+
+ /*
+ * If raw_buf and input_buf are in fact the same buffer, adjust the
+ * input_buf variables, too.
+ */
+ if (cstate->raw_buf == cstate->input_buf)
+ {
+ cstate->input_buf_len -= cstate->input_buf_index;
+ cstate->input_buf_index = 0;
+ }
- inbytes = CopyGetData(cstate, cstate->raw_buf + nbytes,
- 1, RAW_BUF_SIZE - nbytes);
+ /* Load more data */
+ inbytes = CopyGetData(cstate, cstate->raw_buf + cstate->raw_buf_len,
+ 1, RAW_BUF_SIZE - cstate->raw_buf_len);
nbytes += inbytes;
cstate->raw_buf[nbytes] = '\0';
- cstate->raw_buf_index = 0;
cstate->raw_buf_len = nbytes;
+
cstate->bytes_processed += inbytes;
pgstat_progress_update_param(PROGRESS_COPY_BYTES_PROCESSED, cstate->bytes_processed);
- return (inbytes > 0);
+
+ if (inbytes == 0)
+ cstate->raw_reached_eof = true;
+}
+
+/*
+ * CopyLoadInputBuf loads some more data into input_buf
+ *
+ * On return, at least one more input character is loaded into
+ * input_buf, or input_reached_eof is set.
+ *
+ * If INPUT_BUF_BYTES(cstate) > 0, the unprocessed bytes are moved to the start
+ * of the buffer and then we load more data after that.
+ */
+static void
+CopyLoadInputBuf(CopyFromState cstate)
+{
+ int nbytes = INPUT_BUF_BYTES(cstate);
+
+ /*
+ * The caller has updated input_buf_index to indicate how much of the
+ * input has been consumed and isn't needed anymore. If input_buf is the
+ * same physical area as raw_buf, updated raw_buf_index accordingly.
+ */
+ if (cstate->raw_buf == cstate->input_buf)
+ {
+ Assert(!cstate->need_transcoding);
+ Assert(cstate->input_buf_index >= cstate->raw_buf_index);
+ cstate->raw_buf_index = cstate->input_buf_index;
+ }
+
+ for (;;)
+ {
+ /* If we now have some unconverted data, try to convert it. */
+ CopyConvertBuf(cstate);
+
+ /* If we now have some more input bytes ready, return them */
+ if (INPUT_BUF_BYTES(cstate) > nbytes)
+ return;
+
+ /*
+ * If we reached an invalid byte sequence, or we're at an incomplete
+ * multi-byte character but there is no more raw input data, report
+ * conversion error.
+ */
+ if (cstate->input_reached_error)
+ CopyConversionError(cstate);
+
+ /* no more input, and everything has been converted */
+ if (cstate->input_reached_eof)
+ break;
+
+ /* Try to load more raw data */
+ Assert(!cstate->raw_reached_eof);
+ CopyLoadRawBuf(cstate);
+ }
}
/*
@@ -423,7 +735,8 @@ CopyReadBinaryData(CopyFromState cstate, char *dest, int nbytes)
/* Load more data if buffer is empty. */
if (RAW_BUF_BYTES(cstate) == 0)
{
- if (!CopyLoadRawBuf(cstate))
+ CopyLoadRawBuf(cstate);
+ if (cstate->raw_reached_eof)
break; /* EOF */
}
@@ -697,10 +1010,7 @@ CopyReadLine(CopyFromState cstate)
bool result;
resetStringInfo(&cstate->line_buf);
- cstate->line_buf_valid = true;
-
- /* Mark that encoding conversion hasn't occurred yet */
- cstate->line_buf_converted = false;
+ cstate->line_buf_valid = false;
/* Parse data and transfer into line_buf */
result = CopyReadLineText(cstate);
@@ -714,10 +1024,17 @@ CopyReadLine(CopyFromState cstate)
*/
if (cstate->copy_src == COPY_NEW_FE)
{
+ int inbytes;
+
do
{
- cstate->raw_buf_index = cstate->raw_buf_len;
- } while (CopyLoadRawBuf(cstate));
+ inbytes = CopyGetData(cstate, cstate->input_buf,
+ 1, INPUT_BUF_SIZE);
+ } while (inbytes > 0);
+ cstate->input_buf_index = 0;
+ cstate->input_buf_len = 0;
+ cstate->raw_buf_index = 0;
+ cstate->raw_buf_len = 0;
}
}
else
@@ -754,25 +1071,8 @@ CopyReadLine(CopyFromState cstate)
}
}
- /* Done reading the line. Convert it to server encoding. */
- if (cstate->need_transcoding)
- {
- char *cvt;
-
- cvt = pg_any_to_server(cstate->line_buf.data,
- cstate->line_buf.len,
- cstate->file_encoding);
- if (cvt != cstate->line_buf.data)
- {
- /* transfer converted data back to line_buf */
- resetStringInfo(&cstate->line_buf);
- appendBinaryStringInfo(&cstate->line_buf, cvt, strlen(cvt));
- pfree(cvt);
- }
- }
-
/* Now it's safe to use the buffer in error messages */
- cstate->line_buf_converted = true;
+ cstate->line_buf_valid = true;
return result;
}
@@ -783,13 +1083,12 @@ CopyReadLine(CopyFromState cstate)
static bool
CopyReadLineText(CopyFromState cstate)
{
- char *copy_raw_buf;
- int raw_buf_ptr;
+ char *copy_input_buf;
+ int input_buf_ptr;
int copy_buf_len;
bool need_data = false;
bool hit_eof = false;
bool result = false;
- char mblen_str[2];
/* CSV variables */
bool first_char_in_line = true;
@@ -807,8 +1106,6 @@ CopyReadLineText(CopyFromState cstate)
escapec = '\0';
}
- mblen_str[1] = '\0';
-
/*
* The objective of this loop is to transfer the entire next input line
* into line_buf. Hence, we only care for detecting newlines (\r and/or
@@ -821,18 +1118,25 @@ CopyReadLineText(CopyFromState cstate)
* These four characters, and the CSV escape and quote characters, are
* assumed the same in frontend and backend encodings.
*
- * For speed, we try to move data from raw_buf to line_buf in chunks
- * rather than one character at a time. raw_buf_ptr points to the next
- * character to examine; any characters from raw_buf_index to raw_buf_ptr
- * have been determined to be part of the line, but not yet transferred to
- * line_buf.
+ * The input has already been converted to the database encoding. All
+ * supported server encodings have the property that all bytes in a
+ * multi-byte sequence have the high bit set, so a multibyte character
+ * cannot contain any newline or escape characters embedded in the
+ * multibyte sequence. Therefore, we can process the input byte-by-byte,
+ * regardless of the encoding.
*
- * For a little extra speed within the loop, we copy raw_buf and
- * raw_buf_len into local variables.
+ * For speed, we try to move data from input_buf to line_buf in chunks
+ * rather than one character at a time. input_buf_ptr points to the next
+ * character to examine; any characters from input_buf_index to
+ * input_buf_ptr have been determined to be part of the line, but not yet
+ * transferred to line_buf.
+ *
+ * For a little extra speed within the loop, we copy input_buf and
+ * input_buf_len into local variables.
*/
- copy_raw_buf = cstate->raw_buf;
- raw_buf_ptr = cstate->raw_buf_index;
- copy_buf_len = cstate->raw_buf_len;
+ copy_input_buf = cstate->input_buf;
+ input_buf_ptr = cstate->input_buf_index;
+ copy_buf_len = cstate->input_buf_len;
for (;;)
{
@@ -849,24 +1153,24 @@ CopyReadLineText(CopyFromState cstate)
* cstate->copy_src != COPY_OLD_FE, but it hardly seems worth it,
* considering the size of the buffer.
*/
- if (raw_buf_ptr >= copy_buf_len || need_data)
+ if (input_buf_ptr >= copy_buf_len || need_data)
{
REFILL_LINEBUF;
/*
- * Try to read some more data. This will certainly reset
- * raw_buf_index to zero, and raw_buf_ptr must go with it.
+ * Try to read some more data.
*/
- if (!CopyLoadRawBuf(cstate))
- hit_eof = true;
- raw_buf_ptr = 0;
- copy_buf_len = cstate->raw_buf_len;
+ CopyLoadInputBuf(cstate);
+ /* update our local variables */
+ hit_eof = cstate->input_reached_eof;
+ input_buf_ptr = cstate->input_buf_index;
+ copy_buf_len = cstate->input_buf_len;
/*
* If we are completely out of data, break out of the loop,
* reporting EOF.
*/
- if (copy_buf_len <= 0)
+ if (INPUT_BUF_BYTES(cstate) <= 0)
{
result = true;
break;
@@ -875,8 +1179,8 @@ CopyReadLineText(CopyFromState cstate)
}
/* OK to fetch a character */
- prev_raw_ptr = raw_buf_ptr;
- c = copy_raw_buf[raw_buf_ptr++];
+ prev_raw_ptr = input_buf_ptr;
+ c = copy_input_buf[input_buf_ptr++];
if (cstate->opts.csv_mode)
{
@@ -930,16 +1234,16 @@ CopyReadLineText(CopyFromState cstate)
* If need more data, go back to loop top to load it.
*
* Note that if we are at EOF, c will wind up as '\0' because
- * of the guaranteed pad of raw_buf.
+ * of the guaranteed pad of input_buf.
*/
IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(0);
/* get next char */
- c = copy_raw_buf[raw_buf_ptr];
+ c = copy_input_buf[input_buf_ptr];
if (c == '\n')
{
- raw_buf_ptr++; /* eat newline */
+ input_buf_ptr++; /* eat newline */
cstate->eol_type = EOL_CRNL; /* in case not set yet */
}
else
@@ -1006,14 +1310,14 @@ CopyReadLineText(CopyFromState cstate)
/* -----
* get next character
* Note: we do not change c so if it isn't \., we can fall
- * through and continue processing for file encoding.
+ * through and continue processing.
* -----
*/
- c2 = copy_raw_buf[raw_buf_ptr];
+ c2 = copy_input_buf[input_buf_ptr];
if (c2 == '.')
{
- raw_buf_ptr++; /* consume the '.' */
+ input_buf_ptr++; /* consume the '.' */
/*
* Note: if we loop back for more data here, it does not
@@ -1025,7 +1329,7 @@ CopyReadLineText(CopyFromState cstate)
/* Get the next character */
IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(0);
/* if hit_eof, c2 will become '\0' */
- c2 = copy_raw_buf[raw_buf_ptr++];
+ c2 = copy_input_buf[input_buf_ptr++];
if (c2 == '\n')
{
@@ -1050,7 +1354,7 @@ CopyReadLineText(CopyFromState cstate)
/* Get the next character */
IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(0);
/* if hit_eof, c2 will become '\0' */
- c2 = copy_raw_buf[raw_buf_ptr++];
+ c2 = copy_input_buf[input_buf_ptr++];
if (c2 != '\r' && c2 != '\n')
{
@@ -1075,11 +1379,11 @@ CopyReadLineText(CopyFromState cstate)
* Transfer only the data before the \. into line_buf, then
* discard the data and the \. sequence.
*/
- if (prev_raw_ptr > cstate->raw_buf_index)
+ if (prev_raw_ptr > cstate->input_buf_index)
appendBinaryStringInfo(&cstate->line_buf,
- cstate->raw_buf + cstate->raw_buf_index,
- prev_raw_ptr - cstate->raw_buf_index);
- cstate->raw_buf_index = raw_buf_ptr;
+ cstate->input_buf + cstate->input_buf_index,
+ prev_raw_ptr - cstate->input_buf_index);
+ cstate->input_buf_index = input_buf_ptr;
result = true; /* report EOF */
break;
}
@@ -1095,15 +1399,8 @@ CopyReadLineText(CopyFromState cstate)
* backslashes are not special, so we want to process the
* character after the backslash just like a normal character,
* so we don't increment in those cases.
- *
- * Set 'c' to skip whole character correctly in multi-byte
- * encodings. If we don't have the whole character in the
- * buffer yet, we might loop back to process it, after all,
- * but that's OK because multi-byte characters cannot have any
- * special meaning.
*/
- raw_buf_ptr++;
- c = c2;
+ input_buf_ptr++;
}
}
@@ -1114,30 +1411,6 @@ CopyReadLineText(CopyFromState cstate)
* value, while in non-CSV mode, \. cannot be a data value.
*/
not_end_of_copy:
-
- /*
- * Process all bytes of a multi-byte character as a group.
- *
- * We only support multi-byte sequences where the first byte has the
- * high-bit set, so as an optimization we can avoid this block
- * entirely if it is not set.
- */
- if (cstate->encoding_embeds_ascii && IS_HIGHBIT_SET(c))
- {
- int mblen;
-
- /*
- * It is enough to look at the first byte in all our encodings, to
- * get the length. (GB18030 is a bit special, but still works for
- * our purposes; see comment in pg_gb18030_mblen())
- */
- mblen_str[0] = c;
- mblen = pg_encoding_mblen(cstate->file_encoding, mblen_str);
-
- IF_NEED_REFILL_AND_NOT_EOF_CONTINUE(mblen - 1);
- IF_NEED_REFILL_AND_EOF_BREAK(mblen - 1);
- raw_buf_ptr += mblen - 1;
- }
first_char_in_line = false;
} /* end of outer loop */
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index e37942df391..5c110ce5b92 100644
--- a/src/include/commands/copyfrom_internal.h
+++ b/src/include/commands/copyfrom_internal.h
@@ -53,17 +53,6 @@ typedef enum CopyInsertMethod
/*
* This struct contains all the state variables used throughout a COPY FROM
* operation.
- *
- * Multi-byte encodings: all supported client-side encodings encode multi-byte
- * characters by having the first byte's high bit set. Subsequent bytes of the
- * character can have the high bit not set. When scanning data in such an
- * encoding to look for a match to a single-byte (ie ASCII) character, we must
- * use the full pg_encoding_mblen() machinery to skip over multibyte
- * characters, else we might find a false match to a trailing byte. In
- * supported server encodings, there is no possibility of a false match, and
- * it's faster to make useless comparisons to trailing bytes than it is to
- * invoke pg_encoding_mblen() to skip over them. encoding_embeds_ascii is true
- * when we have to do it the hard way.
*/
typedef struct CopyFromStateData
{
@@ -71,13 +60,11 @@ typedef struct CopyFromStateData
CopySource copy_src; /* type of copy source */
FILE *copy_file; /* used if copy_src == COPY_FILE */
StringInfo fe_msgbuf; /* used if copy_src == COPY_NEW_FE */
- bool reached_eof; /* true if we read to end of copy data (not
- * all copy_src types maintain this) */
EolType eol_type; /* EOL type of input */
int file_encoding; /* file or remote side's character encoding */
bool need_transcoding; /* file encoding diff from server? */
- bool encoding_embeds_ascii; /* ASCII can be non-first byte? */
+ Oid conversion_proc;
/* parameters from the COPY command */
Relation rel; /* relation to copy from */
@@ -132,31 +119,52 @@ typedef struct CopyFromStateData
/*
* Similarly, line_buf holds the whole input line being processed. The
- * input cycle is first to read the whole line into line_buf, convert it
- * to server encoding there, and then extract the individual attribute
- * fields into attribute_buf. line_buf is preserved unmodified so that we
- * can display it in error messages if appropriate. (In binary mode,
- * line_buf is not used.)
+ * input cycle is first to read the whole line into line_buf, and then
+ * extract the individual attribute fields into attribute_buf. line_buf
+ * is preserved unmodified so that we can display it in error messages if
+ * appropriate. (In binary mode, line_buf is not used.)
*/
StringInfoData line_buf;
- bool line_buf_converted; /* converted to server encoding? */
bool line_buf_valid; /* contains the row being processed? */
/*
- * Finally, raw_buf holds raw data read from the data source (file or
- * client connection). In text mode, CopyReadLine parses this data
- * sufficiently to locate line boundaries, then transfers the data to
- * line_buf and converts it. In binary mode, CopyReadBinaryData fetches
- * appropriate amounts of data from this buffer. In both modes, we
- * guarantee that there is a \0 at raw_buf[raw_buf_len].
+ * input_buf holds input data, already converted to database encoding.
+ *
+ * In text mode, CopyReadLine parses this data sufficiently to locate line
+ * boundaries, then transfers the data to line_buf. In binary mode,
+ * CopyReadBinaryData fetches appropriate amounts of data from this
+ * buffer. In both modes, we guarantee that there is a \0 at
+ * input_buf[input_buf_len].
+ */
+#define INPUT_BUF_SIZE 65536 /* we palloc INPUT_BUF_SIZE+1 bytes */
+ char *input_buf;
+ int input_buf_index; /* next byte to process */
+ int input_buf_len; /* total # of bytes stored */
+ bool input_reached_eof; /* true if we reached EOF */
+ bool input_reached_error; /* true if a conversion error happened */
+ /* Shorthand for number of unconsumed bytes available in input_buf */
+#define INPUT_BUF_BYTES(cstate) ((cstate)->input_buf_len - (cstate)->input_buf_index)
+
+ /*
+ * raw_buf holds raw input data read from the data source (file or client
+ * connection), not yet converted to the database encoding. Like with
+ * 'input_buf', we guarantee that there is a \0 at raw_buf[raw_buf_len].
+ *
+ * If the encoding conversion is not required, the input data is read
+ * directly into 'input_buf', and raw_buf is not used. In that case,
+ * input_buf_len tracks the number of bytes verified to be valid in the
+ * encoding, and raw_buf_len is the total # of bytes stored in the buffer.
*/
#define RAW_BUF_SIZE 65536 /* we palloc RAW_BUF_SIZE+1 bytes */
char *raw_buf;
int raw_buf_index; /* next byte to process */
int raw_buf_len; /* total # of bytes stored */
- uint64 bytes_processed;/* number of bytes processed so far */
+ bool raw_reached_eof; /* true if we reached EOF */
+
/* Shorthand for number of unconsumed bytes available in raw_buf */
#define RAW_BUF_BYTES(cstate) ((cstate)->raw_buf_len - (cstate)->raw_buf_index)
+
+ uint64 bytes_processed; /* number of bytes processed so far */
} CopyFromStateData;
extern void ReceiveCopyBegin(CopyFromState cstate);
diff --git a/src/include/mb/pg_wchar.h b/src/include/mb/pg_wchar.h
index bbce9071dfc..2377934faf6 100644
--- a/src/include/mb/pg_wchar.h
+++ b/src/include/mb/pg_wchar.h
@@ -306,15 +306,33 @@ typedef enum pg_enc
/*
* When converting strings between different encodings, we assume that space
- * for converted result is 4-to-1 growth in the worst case. The rate for
+ * for converted result is 4-to-1 growth in the worst case. The rate for
* currently supported encoding pairs are within 3 (SJIS JIS X0201 half width
- * kanna -> UTF8 is the worst case). So "4" should be enough for the moment.
+ * kana -> UTF8 is the worst case). So "4" should be enough for the moment.
*
* Note that this is not the same as the maximum character width in any
* particular encoding.
*/
#define MAX_CONVERSION_GROWTH 4
+/*
+ * Maximum byte length of a string that's required in any encoding to convert
+ * at least one character to any other encoding. In other words, if you feed
+ * MAX_CONVERSION_INPUT_LENGTH bytes to any encoding conversion function, it
+ * is guaranteed to be able to convert something without needing more input
+ * (assuming the input is valid).
+ *
+ * Currently, the maximum case is the conversion UTF8 -> SJIS JIS X0201 half
+ * width kana, where a pair of UTF-8 characters is converted into a single
+ * SHIFT_JIS_2004 character (the reverse of the worst case for
+ * MAX_CONVERSION_GROWTH). It needs 6 bytes of input. In theory, a
+ * user-defined conversion function might have more complicated cases, although
+ * for the reverse mapping you would probably also need to bump up
+ * MAX_CONVERSION_GROWTH. But there is no need to be stingy here, so make it
+ * generous.
+ */
+#define MAX_CONVERSION_INPUT_LENGTH 16
+
/*
* Maximum byte length of the string equivalent to any one Unicode code point,
* in any backend encoding. The current value assumes that a 4-byte UTF-8
--
2.30.0
--------------1D68F301AB7A25998BA3365F--
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
@ 2025-03-10 11:26 Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: Rushabh Lathia @ 2025-03-10 11:26 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: pgsql-hackers
On Thu, Feb 27, 2025 at 3:25 PM Rushabh Lathia <[email protected]>
wrote:
> Thank Alvaro for the fixup patch.
>
>
>
>
> On Fri, Feb 21, 2025 at 11:43 AM Alvaro Herrera <[email protected]>
> wrote:
>
>> Hello,
>>
>> Thanks!
>>
>> I noticed a typo 'constrint' in several places; fixed in the attached.
>>
>> I discovered that this sequence, taken from added regression tests,
>>
>> CREATE TABLE notnull_tbl1 (a int);
>> ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent not null a not valid;
>> CREATE TABLE notnull_chld (a int);
>> ALTER TABLE notnull_chld ADD CONSTRAINT nn_child not null a not valid;
>> ALTER TABLE notnull_chld INHERIT notnull_tbl1;
>> ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
>>
>> does mark the constraint as validated in the child, but only in
>> pg_constraint -- pg_attribute continues to be marked as 'i', so if you
>> try to use it for a PK, it fails:
>>
>> alter table notnull_chld add constraint foo primary key (a);
>> ERROR: column "a" of table "notnull_chld" is marked as NOT VALID NOT
>> NULL constrint
>>
>> I thought that was going to be a quick fix, so I tried to do so; since
>> we already have a function 'set_attnotnull', I thought it was the
>> perfect tool to changing attnotnull. However, it's not great, because
>> since that part of the code is already doing the validation, I don't
>> want it to queue the validation again, so the API needs a tweak; I
>> changed it to receiving separately which new value to update attnotnull
>> to, and whether to queue validation. With that change it works
>> correctly, but it is a bit ugly at the callers' side. Maybe it works to
>> pass two booleans instead? Please have a look at whether that can be
>> improved.
>>
>
> I haven't given much thought to improving the API, but I'll look into it
> now. Also,
> I'm considering renaming AdjustNotNullInheritance() since it now also
> checks for invalid NOT NULL constraints. What do you think?
>
I adjusted the set_attnotnull() API and removed the added queue_validation
parameter. Rather, the function start using wqueue input parameter as a
check.
If wqueue is NULL, skip the queue_validation. Attaching patch here, but not
sure how clear it is, in term of understanding the API. Your
thoughts/inputs?
Looking further for AdjustNotNullInheritance() I don't see need of rename
this
API as it's just adding new error check now for an existing NOT NULL
constraint.
JFYI, I can reproduce the failure Ashutosh Bapat reported, while running
the pg_upgrade test through meson commands. I am investigating that
further.
Thanks,
Rushabh Lathia
Attachments:
[application/octet-stream] 0004-Adjust-set_attnotnull-API-input-parameters.patch (3.3K, ../../CAGPqQf2C+KyLkRQ3ToBx2BC-URG2LQDQ1LX0tu7_jvzTJ_caMg@mail.gmail.com/3-0004-Adjust-set_attnotnull-API-input-parameters.patch)
download | inline diff:
From 0d8f3ff345d25d3d19ef1e3afb7374c45acef44b Mon Sep 17 00:00:00 2001
From: Rushabh Lathia <[email protected]>
Date: Mon, 10 Mar 2025 16:40:02 +0530
Subject: [PATCH 4/4] Adjust set_attnotnull() API input parameters.
---
src/backend/commands/tablecmds.c | 18 ++++++------------
1 file changed, 6 insertions(+), 12 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ef9ec42e624..a1730abd318 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -471,8 +471,7 @@ static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid)
static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
LOCKMODE lockmode);
static void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
- char newvalue, bool queue_validation,
- LOCKMODE lockmode);
+ char newvalue, LOCKMODE lockmode);
static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel,
char *constrname, char *colName,
bool recurse, bool recursing,
@@ -1319,8 +1318,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints,
old_notnulls);
foreach_int(attrnum, nncols)
- set_attnotnull(NULL, rel, attrnum, ATTRIBUTE_NOTNULL_TRUE,
- false, NoLock);
+ set_attnotnull(NULL, rel, attrnum, ATTRIBUTE_NOTNULL_TRUE, NoLock);
ObjectAddressSet(address, RelationRelationId, relationId);
@@ -7718,13 +7716,11 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
*/
static void
set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
- char newvalue, bool queue_validation, LOCKMODE lockmode)
+ char newvalue, LOCKMODE lockmode)
{
Form_pg_attribute attr;
- Assert(!queue_validation || wqueue != NULL);
-
CheckAlterTableIsSafe(rel);
/*
@@ -7756,7 +7752,7 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
* Queue later validation of this constraint, if necessary and
* requested by caller.
*/
- if (queue_validation &&
+ if (wqueue &&
newvalue == ATTRIBUTE_NOTNULL_TRUE &&
!NotNullImpliedByRelConstraints(rel, attr))
{
@@ -7940,7 +7936,7 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
constraint->skip_validation ?
ATTRIBUTE_NOTNULL_INVALID :
ATTRIBUTE_NOTNULL_TRUE,
- !constraint->skip_validation, lockmode);
+ lockmode);
/*
* Recurse to propagate the constraint to children that don't have one.
@@ -9800,7 +9796,6 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ccon->skip_validation ?
ATTRIBUTE_NOTNULL_INVALID :
ATTRIBUTE_NOTNULL_TRUE,
- !ccon->skip_validation,
lockmode);
ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
@@ -12550,8 +12545,7 @@ QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
/* Also flip attnotnull */
- set_attnotnull(wqueue, rel, attnum, ATTRIBUTE_NOTNULL_TRUE, false,
- lockmode);
+ set_attnotnull(NULL, rel, attnum, ATTRIBUTE_NOTNULL_TRUE, lockmode);
InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
--
2.43.0
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
@ 2025-03-10 11:50 ` Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-10 11:50 UTC (permalink / raw)
To: Rushabh Lathia <[email protected]>; +Cc: pgsql-hackers
On 2025-Mar-10, Rushabh Lathia wrote:
> I adjusted the set_attnotnull() API and removed the added
> queue_validation parameter. Rather, the function start using wqueue
> input parameter as a check.
> If wqueue is NULL, skip the queue_validation. Attaching patch here,
> but not sure how clear it is, in term of understanding the API. Your
> thoughts/inputs?
Yeah, I think this makes sense, if it supports all the cases correctly.
(If in your code you have any calls of set_attnotnull that pass a NULL
wqueue during ALTER TABLE in order to avoid queueing a check, you had
better have comments to explain this.)
Kindly do not send patches standalone, because the CFbot doesn't
understand that it needs to apply it on top of the three previous
patches. You need to send all 4 patches every time. You can see the
result here:
https://commitfest.postgresql.org/patch/5554/
If you click on the "need rebase!" label you'll see that it tried to
apply patch 0004 to the top of the master branch, and that obviously
failed. (FWIW if you click on the "summary" label you'll also see that
the patch has been failing the CI tests since Feb 27.)
> Looking further for AdjustNotNullInheritance() I don't see need of
> rename this API as it's just adding new error check now for an
> existing NOT NULL constraint.
OK.
> JFYI, I can reproduce the failure Ashutosh Bapat reported, while
> running the pg_upgrade test through meson commands. I am
> investigating that further.
Ah good, thanks.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"Industry suffers from the managerial dogma that for the sake of stability
and continuity, the company should be independent of the competence of
individual employees." (E. Dijkstra)
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-03-11 10:15 ` Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: Rushabh Lathia @ 2025-03-11 10:15 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: pgsql-hackers
On Mon, Mar 10, 2025 at 5:20 PM Alvaro Herrera <[email protected]>
wrote:
> On 2025-Mar-10, Rushabh Lathia wrote:
>
> > I adjusted the set_attnotnull() API and removed the added
> > queue_validation parameter. Rather, the function start using wqueue
> > input parameter as a check.
> > If wqueue is NULL, skip the queue_validation. Attaching patch here,
> > but not sure how clear it is, in term of understanding the API. Your
> > thoughts/inputs?
>
> Yeah, I think this makes sense, if it supports all the cases correctly.
> (If in your code you have any calls of set_attnotnull that pass a NULL
> wqueue during ALTER TABLE in order to avoid queueing a check, you had
> better have comments to explain this.)
>
Done.
>
> Kindly do not send patches standalone, because the CFbot doesn't
> understand that it needs to apply it on top of the three previous
> patches. You need to send all 4 patches every time. You can see the
> result here:
> https://commitfest.postgresql.org/patch/5554/
> If you click on the "need rebase!" label you'll see that it tried to
> apply patch 0004 to the top of the master branch, and that obviously
> failed. (FWIW if you click on the "summary" label you'll also see that
> the patch has been failing the CI tests since Feb 27.)
>
Sure, attaching the rebased patch here.
>
> > Looking further for AdjustNotNullInheritance() I don't see need of
> > rename this API as it's just adding new error check now for an
> > existing NOT NULL constraint.
>
> OK.
>
> > JFYI, I can reproduce the failure Ashutosh Bapat reported, while
> > running the pg_upgrade test through meson commands. I am
> > investigating that further.
>
> Ah good, thanks.
>
Somehow, I am now not able to reproduce after the clean build. Yesterday
I was able to reproduce, so I was happy, but again trying to analyze the
issue
when I start with the
Thanks,
Rushabh Lathia
www.EnterpriseDB.com
Attachments:
[application/octet-stream] 0002-Support-NOT-VALID-and-VALIDATE-CONSTRAINT-for-named-.patch (47.8K, ../../CAGPqQf31ctDvcbJE7J3in++iCRbf7-Rv07F7dpnu-_GyLWFz+w@mail.gmail.com/3-0002-Support-NOT-VALID-and-VALIDATE-CONSTRAINT-for-named-.patch)
download | inline diff:
From aee75c8f0b5b7e1f5ccd83ab62a631611fab72f9 Mon Sep 17 00:00:00 2001
From: Rushabh Lathia <[email protected]>
Date: Mon, 10 Feb 2025 16:22:30 +0530
Subject: [PATCH 2/3] Support NOT VALID and VALIDATE CONSTRAINT for named NOT
NULL constraints.
Commit also add support for pg_dump to dump NOT VALID named NOT NULL
constraints.
---
src/backend/bootstrap/bootstrap.c | 4 +-
src/backend/catalog/heap.c | 2 +-
src/backend/catalog/pg_constraint.c | 31 ++-
src/backend/commands/tablecmds.c | 249 ++++++++++++++++++----
src/backend/executor/execMain.c | 3 +-
src/backend/parser/gram.y | 4 +-
src/bin/pg_dump/pg_dump.c | 169 +++++++++++++--
src/bin/pg_dump/pg_dump.h | 1 +
src/bin/pg_dump/t/002_pg_dump.pl | 17 ++
src/bin/psql/describe.c | 10 +-
src/include/catalog/pg_attribute.h | 1 +
src/include/catalog/pg_constraint.h | 3 +-
src/test/regress/expected/constraints.out | 153 +++++++++++++
src/test/regress/sql/constraints.sql | 85 ++++++++
14 files changed, 659 insertions(+), 73 deletions(-)
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 1e95dc32f46..919972dc409 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -582,10 +582,8 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
attrtypes[attnum]->atttypmod = -1;
attrtypes[attnum]->attislocal = true;
- /* set default to false */
attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
-
if (nullness == BOOTCOL_NULL_FORCE_NOT_NULL)
{
attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
@@ -699,7 +697,7 @@ InsertOneNull(int i)
{
elog(DEBUG4, "inserting column %d NULL", i);
Assert(i >= 0 && i < MAXATTR);
- if (TupleDescAttr(boot_reldesc->rd_att, i)->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
+ if (TupleDescAttr(boot_reldesc->rd_att, i)->attnotnull != ATTRIBUTE_NOTNULL_FALSE)
elog(ERROR,
"NULL value specified for not-null column \"%s\" of relation \"%s\"",
NameStr(TupleDescAttr(boot_reldesc->rd_att, i)->attname),
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 8c2d2cdcdfe..6458f78c216 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2626,7 +2626,7 @@ AddRelationNewConstraints(Relation rel,
* to add another one; just adjust inheritance status as needed.
*/
if (AdjustNotNullInheritance(RelationGetRelid(rel), colnum,
- is_local, cdef->is_no_inherit))
+ cdef->conname, is_local, cdef->is_no_inherit))
continue;
/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..e14041b34ca 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -574,7 +574,7 @@ ChooseConstraintName(const char *name1, const char *name2,
/*
* 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.
+ * 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
@@ -604,13 +604,11 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
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.
+ * We're looking for a NOTNULL constraint 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)
@@ -628,9 +626,9 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
}
/*
- * 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.
+ * Find and return the pg_constraint tuple that implements a
+ * 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)
@@ -726,10 +724,13 @@ extractNotNullColumn(HeapTuple constrTup)
* conislocal/coninhcount and return true.
* In the latter case, if is_local is true we flip conislocal true, or do
* nothing if it's already true; otherwise we increment coninhcount by 1.
+ *
+ * Function add a check for existing INVALID not-null constraint on
+ * the table column.
*/
bool
AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
- bool is_local, bool is_no_inherit)
+ const char *conname, bool is_local, bool is_no_inherit)
{
HeapTuple tup;
@@ -753,6 +754,18 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"",
NameStr(conform->conname), get_rel_name(relid)));
+ /*
+ * Throw an error if an invalid NOT NULL constraint exists on the
+ * table column and an attempt is made to add another valid NOT NULL
+ * constraint.
+ */
+ if (is_local && !conform->convalidated && conname)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("Invalid NOT NULL constraint \"%s\" exist on relation \"%s\"",
+ NameStr(conform->conname), get_rel_name(relid)),
+ errhint("Do VALIDATE CONSTRAINT"));
+
if (!is_local)
{
if (pg_add_s16_overflow(conform->coninhcount, 1,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3c81e48cac1..6a757c7c374 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -410,6 +410,9 @@ static void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation
static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
char *constrName, HeapTuple contuple,
bool recurse, bool recursing, LOCKMODE lockmode);
+static void QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple,
+ bool recurse, bool recursing, LOCKMODE lockmode);
static int transformColumnNameList(Oid relId, List *colList,
int16 *attnums, Oid *atttypids, Oid *attcollids);
static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
@@ -473,7 +476,7 @@ static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid)
static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
LOCKMODE lockmode);
static void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
- LOCKMODE lockmode);
+ char newvalue, LOCKMODE lockmode);
static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel,
char *constrname, char *colName,
bool recurse, bool recursing,
@@ -710,6 +713,7 @@ static List *GetParentedForeignKeyRefs(Relation partition);
static void ATDetachCheckNoForeignKeyRefs(Relation partition);
static char GetAttributeCompression(Oid atttypid, const char *compression);
static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static bool check_for_invalid_notnull(Oid relid, const char *attname);
/* ----------------------------------------------------------------
@@ -1315,7 +1319,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints,
old_notnulls);
foreach_int(attrnum, nncols)
- set_attnotnull(NULL, rel, attrnum, NoLock);
+ set_attnotnull(NULL, rel, attrnum, ATTRIBUTE_NOTNULL_TRUE, NoLock);
ObjectAddressSet(address, RelationRelationId, relationId);
@@ -6191,7 +6195,8 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
{
Form_pg_attribute attr = TupleDescAttr(newTupDesc, i);
- if (attr->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ if ((attr->attnotnull == ATTRIBUTE_NOTNULL_TRUE ||
+ attr->attnotnull == ATTRIBUTE_NOTNULL_INVALID) &&
!attr->attisdropped)
notnull_attrs = lappend_int(notnull_attrs, i);
}
@@ -7743,10 +7748,11 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
*/
static void
set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
- LOCKMODE lockmode)
+ char newvalue, LOCKMODE lockmode)
{
Form_pg_attribute attr;
+
CheckAlterTableIsSafe(rel);
/*
@@ -7757,7 +7763,7 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
if (attr->attisdropped)
return;
- if (attr->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
+ if (attr->attnotnull != newvalue)
{
Relation attr_rel;
HeapTuple tuple;
@@ -7770,15 +7776,17 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
attnum, RelationGetRelid(rel));
attr = (Form_pg_attribute) GETSTRUCT(tuple);
- Assert(attr->attnotnull == ATTRIBUTE_NOTNULL_FALSE);
- attr->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
+ attr->attnotnull = newvalue;
+
CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
/*
- * If the nullness isn't already proven by validated constraints, have
- * ALTER TABLE phase 3 test for it.
+ * Queue later validation of this constraint, if necessary and
+ * requested by caller.
*/
- if (wqueue && !NotNullImpliedByRelConstraints(rel, attr))
+ if (wqueue &&
+ newvalue == ATTRIBUTE_NOTNULL_TRUE &&
+ !NotNullImpliedByRelConstraints(rel, attr))
{
AlteredTableInfo *tab;
@@ -7884,6 +7892,15 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
conForm->conislocal = true;
changed = true;
}
+ else if (!conForm->convalidated)
+ {
+ /*
+ * flip attnotnull and convalidated, and also validate the
+ * constraint.
+ */
+ return ATExecValidateConstraint(wqueue, rel, conForm->conname.data,
+ recurse, recursing, lockmode);
+ }
if (changed)
{
@@ -7946,8 +7963,12 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel), attnum);
- /* Mark pg_attribute.attnotnull for the column */
- set_attnotnull(wqueue, rel, attnum, lockmode);
+ /* Mark pg_attribute.attnotnull for the column and request validation */
+ set_attnotnull(wqueue, rel, attnum,
+ constraint->skip_validation ?
+ ATTRIBUTE_NOTNULL_INVALID :
+ ATTRIBUTE_NOTNULL_TRUE,
+ lockmode);
/*
* Recurse to propagate the constraint to children that don't have one.
@@ -9387,12 +9408,19 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
}
/* Insert not-null constraints in the queue for the PK columns */
- foreach(lc, pkconstr->keys)
+ foreach_node(String, colname, pkconstr->keys)
{
AlterTableCmd *newcmd;
Constraint *nnconstr;
- nnconstr = makeNotNullConstraint(lfirst(lc));
+ /* Verify that the not-null constraint has been validated */
+ if (check_for_invalid_notnull(RelationGetRelid(rel), strVal(colname)))
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("column \"%s\" of table \"%s\" is marked as NOT VALID NOT NULL constraint",
+ strVal(colname), RelationGetRelationName(rel)));
+
+ nnconstr = makeNotNullConstraint(colname);
newcmd = makeNode(AlterTableCmd);
newcmd->subtype = AT_AddConstraint;
@@ -9403,6 +9431,30 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
}
}
+/*
+ * This function checks for any invalid NOT NULL constraint on the given
+ * relation and attribute name. It returns true if found, false otherwise.
+ */
+static bool
+check_for_invalid_notnull(Oid relid, const char *attname)
+{
+ HeapTuple tuple;
+ bool retval = false;
+
+ tuple = SearchSysCache2(ATTNAME,
+ ObjectIdGetDatum(relid),
+ CStringGetDatum(attname));
+ if (!HeapTupleIsValid(tuple))
+ return false;
+ if (!((Form_pg_attribute) GETSTRUCT(tuple))->attisdropped &&
+ ((Form_pg_attribute) GETSTRUCT(tuple))->attnotnull == ATTRIBUTE_NOTNULL_INVALID)
+ retval = true;
+
+ ReleaseSysCache(tuple);
+
+ return retval;
+}
+
/*
* ALTER TABLE ADD INDEX
*
@@ -9770,7 +9822,11 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* phase 3 to verify existing rows, if needed.
*/
if (constr->contype == CONSTR_NOTNULL)
- set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
+ set_attnotnull(wqueue, rel, ccon->attnum,
+ ccon->skip_validation ?
+ ATTRIBUTE_NOTNULL_INVALID :
+ ATTRIBUTE_NOTNULL_TRUE,
+ lockmode);
ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
}
@@ -12371,10 +12427,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
con = (Form_pg_constraint) GETSTRUCT(tuple);
if (con->contype != CONSTRAINT_FOREIGN &&
- con->contype != CONSTRAINT_CHECK)
+ con->contype != CONSTRAINT_CHECK &&
+ con->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
+ errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key, not-null, or check constraint",
constrName, RelationGetRelationName(rel))));
if (!con->conenforced)
@@ -12393,6 +12450,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
QueueCheckConstraintValidation(wqueue, conrel, rel, constrName,
tuple, recurse, recursing, lockmode);
}
+ else if (con->contype == CONSTRAINT_NOTNULL)
+ {
+ QueueNNConstraintValidation(wqueue, conrel, rel,
+ tuple, recurse, recursing, lockmode);
+ }
ObjectAddressSet(address, ConstraintRelationId, con->oid);
}
@@ -12522,9 +12584,7 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
AlteredTableInfo *tab;
HeapTuple copyTuple;
Form_pg_constraint copy_con;
-
List *children = NIL;
- ListCell *child;
NewConstraint *newcon;
Datum val;
char *conbin;
@@ -12533,24 +12593,19 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
Assert(con->contype == CONSTRAINT_CHECK);
/*
- * If we're recursing, the parent has already done this, so skip it. Also,
- * if the constraint is a NO INHERIT constraint, we shouldn't try to look
- * for it in the children.
- */
- if (!recursing && !con->connoinherit)
- children = find_all_inheritors(RelationGetRelid(rel),
- lockmode, NULL);
-
- /*
- * For CHECK constraints, we must ensure that we only mark the constraint
- * as validated on the parent if it's already validated on the children.
+ * For constraints that aren't NO INHERIT, we must ensure that we only
+ * mark the constraint as validated on the parent if it's already
+ * validated on the children.
+ *
+ * If we're recursing, the parent has already done this, so skip it.
*
* We recurse before validating on the parent, to reduce risk of
* deadlocks.
*/
- foreach(child, children)
+ if (!recursing && !con->connoinherit)
+ children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+ foreach_oid(childoid, children)
{
- Oid childoid = lfirst_oid(child);
Relation childrel;
if (childoid == RelationGetRelid(rel))
@@ -12609,6 +12664,114 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
heap_freetuple(copyTuple);
}
+/*
+ * QueueNNConstraintValidation
+ *
+ * Add an entry to the wqueue to validate the given not-null constraint in
+ * Phase 3 and update the convalidated field in the pg_constraint catalog for
+ * the specified relation and all its inheriting children.
+ */
+static void
+QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple, bool recurse, bool recursing,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint con;
+ AlteredTableInfo *tab;
+ HeapTuple copyTuple;
+ Form_pg_constraint copy_con;
+ List *children = NIL;
+ AttrNumber attnum;
+ char *colname;
+
+ con = (Form_pg_constraint) GETSTRUCT(contuple);
+ Assert(con->contype == CONSTRAINT_NOTNULL);
+
+ attnum = extractNotNullColumn(contuple);
+
+ /*
+ * For constraints that aren't NO INHERIT, we must ensure that we only
+ * mark the constraint as validated on the parent if it's already
+ * validated on the children.
+ *
+ * If we're recursing, the parent has already done this, so skip it.
+ *
+ * We recurse before validating on the parent, to reduce risk of
+ * deadlocks.
+ */
+ if (!recursing && !con->connoinherit)
+ children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+
+ colname = get_attname(RelationGetRelid(rel), attnum, false);
+ foreach_oid(childoid, children)
+ {
+ Relation childrel;
+ HeapTuple contup;
+ Form_pg_constraint childcon;
+ char *conname;
+
+ if (childoid == RelationGetRelid(rel))
+ continue;
+
+ /*
+ * If we are told not to recurse, there had better not be any child
+ * tables, because we can't mark the constraint on the parent valid
+ * unless it is valid for all child tables.
+ */
+ if (!recurse)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+ errmsg("constraint must be validated on child tables too"));
+
+ /*
+ * The column on child might have a different attnum, so search by
+ * column name.
+ */
+ contup = findNotNullConstraint(childoid, colname);
+ if (!contup)
+ elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
+ colname, get_rel_name(childoid));
+ childcon = (Form_pg_constraint) GETSTRUCT(contup);
+ if (childcon->convalidated)
+ continue;
+
+ /* find_all_inheritors already got lock */
+ childrel = table_open(childoid, NoLock);
+ conname = pstrdup(NameStr(childcon->conname));
+
+ /* XXX improve ATExecValidateConstraint API to avoid double search */
+ ATExecValidateConstraint(wqueue, childrel, conname,
+ false, true, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->verify_new_notnull = true;
+
+ /*
+ * Invalidate relcache so that others see the new validated constraint.
+ */
+ CacheInvalidateRelcache(rel);
+
+ /*
+ * Now update the catalogs, while we have the door open.
+ */
+ copyTuple = heap_copytuple(contuple);
+ copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+ copy_con->convalidated = true;
+ CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
+
+ /*
+ * Also flip attnotnull. call function with wqueue as NULL to
+ * bypass validation, as it has already been performed.
+ */
+ set_attnotnull(NULL, rel, attnum, ATTRIBUTE_NOTNULL_TRUE, lockmode);
+
+ InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
+
+ heap_freetuple(copyTuple);
+}
+
/*
* transformColumnNameList - transform list of column names
*
@@ -13480,7 +13643,7 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
RelationGetRelationName(rel)));
/* All good -- reset attnotnull if needed */
- if (attForm->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
+ if (attForm->attnotnull != ATTRIBUTE_NOTNULL_FALSE)
{
attForm->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
CatalogTupleUpdate(attrel, &atttup->t_self, atttup);
@@ -16816,12 +16979,20 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
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)));
+ if (HeapTupleIsValid(contup))
+ {
+ Form_pg_constraint childcon;
+
+ childcon = (Form_pg_constraint) GETSTRUCT(contup);
+ if (!childcon->connoinherit)
+ ereport(ERROR,
+ errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL",
+ parent_attname, RelationGetRelationName(child_rel)));
+
+ if (!childcon->convalidated)
+ elog(WARNING, "found an invalid constraint");
+ }
}
/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index d582231ab60..4b0446b00d8 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -2070,7 +2070,8 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
{
Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
- if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ if ((att->attnotnull == ATTRIBUTE_NOTNULL_TRUE ||
+ att->attnotnull == ATTRIBUTE_NOTNULL_INVALID) &&
slot_attisnull(slot, attrChk))
{
char *val_desc;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..715eb51300b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4224,9 +4224,9 @@ ConstraintElem:
n->keys = list_make1(makeString($3));
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
- NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
- n->initially_valid = true;
+ n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
| UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c371570501a..1a946461219 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8876,6 +8876,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQExpBuffer q = createPQExpBuffer();
PQExpBuffer tbloids = createPQExpBuffer();
PQExpBuffer checkoids = createPQExpBuffer();
+ PQExpBuffer invalidnotnulloids = createPQExpBuffer();
PGresult *res;
int ntups;
int curtblindx;
@@ -8892,6 +8893,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
int i_attlen;
int i_attalign;
int i_attislocal;
+ int i_notnull_valid;
int i_notnull_name;
int i_notnull_noinherit;
int i_notnull_islocal;
@@ -8988,11 +8990,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
*/
if (fout->remoteVersion >= 180000)
appendPQExpBufferStr(q,
+ "CASE WHEN a.attnotnull = 'i' THEN 'f' ELSE 't' END AS notnull_valid,\n"
"co.conname AS notnull_name,\n"
"co.connoinherit AS notnull_noinherit,\n"
"co.conislocal AS notnull_islocal,\n");
else
appendPQExpBufferStr(q,
+ "'t' AS notnull_valid,\n"
"CASE WHEN a.attnotnull THEN '' ELSE NULL END AS notnull_name,\n"
"false AS notnull_noinherit,\n"
"a.attislocal AS notnull_islocal,\n");
@@ -9067,6 +9071,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
i_attlen = PQfnumber(res, "attlen");
i_attalign = PQfnumber(res, "attalign");
i_attislocal = PQfnumber(res, "attislocal");
+ i_notnull_valid = PQfnumber(res, "notnull_valid");
i_notnull_name = PQfnumber(res, "notnull_name");
i_notnull_noinherit = PQfnumber(res, "notnull_noinherit");
i_notnull_islocal = PQfnumber(res, "notnull_islocal");
@@ -9086,6 +9091,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
* r is handled by the inner loop.
*/
curtblindx = -1;
+ appendPQExpBufferChar(invalidnotnulloids, '{');
for (int r = 0; r < ntups;)
{
Oid attrelid = atooid(PQgetvalue(res, r, i_attrelid));
@@ -9134,6 +9140,7 @@ 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_valid = (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));
@@ -9160,12 +9167,29 @@ 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_valid[j] = (PQgetvalue(res, r, i_notnull_valid)[0] == 't');
- /* Handle not-null constraint name and flags */
- determineNotNullFlags(fout, res, r,
- tbinfo, j,
- i_notnull_name, i_notnull_noinherit,
- i_notnull_islocal);
+ /*
+ * Dump the invalid NOT NULL constraint like the Check constraints
+ */
+ if (tbinfo->notnull_valid[j])
+ /* Handle not-null constraint name and flags */
+ determineNotNullFlags(fout, res, r,
+ tbinfo, j,
+ i_notnull_name, i_notnull_noinherit,
+ i_notnull_islocal);
+ else if (!PQgetisnull(res, r, i_notnull_name))
+ {
+ /*
+ * Add the entry into invalidnotnull list so NOT NULL
+ * constraint get dump as separate constraints.
+ */
+ if (invalidnotnulloids->len > 1) /* do we have more than
+ * the '{'? */
+ appendPQExpBufferChar(invalidnotnulloids, ',');
+ appendPQExpBuffer(invalidnotnulloids, "%u", tbinfo->dobj.catId.oid);
+
+ }
tbinfo->attoptions[j] = pg_strdup(PQgetvalue(res, r, i_attoptions));
tbinfo->attcollation[j] = atooid(PQgetvalue(res, r, i_attcollation));
@@ -9187,6 +9211,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
}
PQclear(res);
+ appendPQExpBufferChar(invalidnotnulloids, '}');
/*
* Now get info about column defaults. This is skipped for a data-only
@@ -9450,6 +9475,128 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQclear(res);
}
+ /*
+ * Get info about table INVALID NOT NULL constraints. This is skipped for
+ * a data-only dump, as it is only needed for table schemas.
+ */
+ if (dopt->dumpSchema && invalidnotnulloids->len > 2)
+ {
+ ConstraintInfo *constrs;
+ int numConstrs;
+ int i_tableoid;
+ int i_oid;
+ int i_conrelid;
+ int i_conname;
+ int i_consrc;
+ int i_conislocal;
+ int i_convalidated;
+
+ pg_log_info("finding table invalid not null constraints");
+
+ resetPQExpBuffer(q);
+ appendPQExpBuffer(q,
+ "SELECT c.tableoid, c.oid, conrelid, conname, "
+ "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
+ "conislocal, convalidated "
+ "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+ "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
+ "WHERE contype = 'n' AND convalidated = 'f'"
+ "ORDER BY c.conrelid, c.conname",
+ invalidnotnulloids->data);
+
+ res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
+
+ numConstrs = PQntuples(res);
+ constrs = (ConstraintInfo *) pg_malloc(numConstrs * sizeof(ConstraintInfo));
+
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_oid = PQfnumber(res, "oid");
+ i_conrelid = PQfnumber(res, "conrelid");
+ i_conname = PQfnumber(res, "conname");
+ i_consrc = PQfnumber(res, "consrc");
+ i_conislocal = PQfnumber(res, "conislocal");
+ i_convalidated = PQfnumber(res, "convalidated");
+
+ /* As above, this loop iterates once per table, not once per row */
+ curtblindx = -1;
+ for (int j = 0; j < numConstrs;)
+ {
+ Oid conrelid = atooid(PQgetvalue(res, j, i_conrelid));
+ TableInfo *tbinfo = NULL;
+ int numcons;
+
+ /* Count rows for this table */
+ for (numcons = 1; numcons < numConstrs - j; numcons++)
+ if (atooid(PQgetvalue(res, j + numcons, i_conrelid)) != conrelid)
+ break;
+
+ /*
+ * Locate the associated TableInfo; we rely on tblinfo[] being in
+ * OID order.
+ */
+ while (++curtblindx < numTables)
+ {
+ tbinfo = &tblinfo[curtblindx];
+ if (tbinfo->dobj.catId.oid == conrelid)
+ break;
+ }
+ if (curtblindx >= numTables)
+ pg_fatal("unrecognized table OID %u", conrelid);
+
+
+ tbinfo->checkexprs = constrs + j;
+
+ for (int c = 0; c < numcons; c++, j++)
+ {
+ bool validated = PQgetvalue(res, j, i_convalidated)[0] == 't';
+
+ constrs[j].dobj.objType = DO_CONSTRAINT;
+ constrs[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
+ constrs[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&constrs[j].dobj);
+ constrs[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_conname));
+ constrs[j].dobj.namespace = tbinfo->dobj.namespace;
+ constrs[j].contable = tbinfo;
+ constrs[j].condomain = NULL;
+ constrs[j].contype = 'n';
+ constrs[j].condef = pg_strdup(PQgetvalue(res, j, i_consrc));
+ constrs[j].confrelid = InvalidOid;
+ constrs[j].conindex = 0;
+ constrs[j].condeferrable = false;
+ constrs[j].condeferred = false;
+ constrs[j].conislocal = (PQgetvalue(res, j, i_conislocal)[0] == 't');
+
+ /*
+ * An unvalidated constraint needs to be dumped separately, so
+ * that potentially-violating existing data is loaded before
+ * the constraint.
+ */
+ constrs[j].separate = !validated;
+
+ constrs[j].dobj.dump = tbinfo->dobj.dump;
+
+ /*
+ * Mark the constraint as needing to appear before the table
+ * --- this is so that any other dependencies of the
+ * constraint will be emitted before we try to create the
+ * table. If the constraint is to be dumped separately, it
+ * will be dumped after data is loaded anyway, so don't do it.
+ * (There's an automatic dependency in the opposite direction
+ * anyway, so don't need to add one manually here.)
+ */
+ if (!constrs[j].separate)
+ addObjectDependency(&tbinfo->dobj,
+ constrs[j].dobj.dumpId);
+
+ /*
+ * We will detect later whether the constraint must be split
+ * out from the table definition.
+ */
+ }
+ }
+ PQclear(res);
+ }
+
destroyPQExpBuffer(q);
destroyPQExpBuffer(tbloids);
destroyPQExpBuffer(checkoids);
@@ -16543,7 +16690,8 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
* defined, or if binary upgrade. (In the latter case, we
* reset conislocal below.)
*/
- print_notnull = (tbinfo->notnull_constrs[j] != NULL &&
+ print_notnull = (tbinfo->notnull_valid[j] &&
+ tbinfo->notnull_constrs[j] != NULL &&
(tbinfo->notnull_islocal[j] ||
dopt->binary_upgrade ||
tbinfo->ispartition));
@@ -16605,11 +16753,6 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
tbinfo->attrdefs[j]->adef_expr);
}
- print_notnull = (tbinfo->notnull_constrs[j] != NULL &&
- (tbinfo->notnull_islocal[j] ||
- dopt->binary_upgrade ||
- tbinfo->ispartition));
-
if (print_notnull)
{
if (tbinfo->notnull_constrs[j][0] == '\0')
@@ -17931,9 +18074,9 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
.createStmt = q->data,
.dropStmt = delq->data));
}
- else if (coninfo->contype == 'c' && tbinfo)
+ else if ((coninfo->contype == 'c' || coninfo->contype == 'n') && tbinfo)
{
- /* CHECK constraint on a table */
+ /* CHECK/INVALID_NOTNULL constraint on a table */
/* Ignore if not to be dumped separately, or if it was inherited */
if (coninfo->separate && coninfo->conislocal)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bbdb30b5f54..90335d2bb0b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -361,6 +361,7 @@ typedef struct _tableInfo
char *attcompression; /* per-attribute compression method */
char **attfdwoptions; /* per-attribute fdw options */
char **attmissingval; /* per attribute missing value */
+ bool *notnull_valid; /* NOT NULL status */
char **notnull_constrs; /* NOT NULL constraint names. If null,
* there isn't one on this column. If
* empty string, unnamed constraint
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index c7bffc1b045..5b5b756e512 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1099,6 +1099,23 @@ my %tests = (
},
},
+ 'CONSTRAINT NOT NULL / INVALID' => {
+ create_sql => 'CREATE TABLE dump_test.test_table_nn (
+ col1 int);
+ ALTER TABLE dump_test.test_table_nn ADD CONSTRAINT nn NOT NULL col1 NOT VALID;',
+ regexp => qr/^
+ \QALTER TABLE dump_test.test_table_nn\E \n^\s+
+ \QADD CONSTRAINT nn NOT NULL col1 NOT VALID;\E
+ /xm,
+ like => {
+ %full_runs, %dump_test_schema_runs, section_post_data => 1,
+ },
+ unlike => {
+ exclude_dump_test_schema => 1,
+ only_dump_measurement => 1,
+ },
+ },
+
'CONSTRAINT PRIMARY KEY / WITHOUT OVERLAPS' => {
create_sql => 'CREATE TABLE dump_test.test_table_tpk (
col1 int4range,
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..f48b24ed38c 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2107,7 +2107,7 @@ describeOneTableDetails(const char *schemaname,
printTableAddCell(&cont, PQgetvalue(res, i, attcoll_col), false, false);
printTableAddCell(&cont,
- strcmp(PQgetvalue(res, i, attnotnull_col), "t") == 0 ? "not null" : "",
+ strcmp(PQgetvalue(res, i, attnotnull_col), "f") == 0 ? "" : "not null",
false, false);
identity = PQgetvalue(res, i, attidentity_col);
@@ -3114,7 +3114,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.conname, a.attname, c.connoinherit,\n"
- " c.conislocal, c.coninhcount <> 0\n"
+ " c.conislocal, c.coninhcount <> 0, c.convalidated \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"
@@ -3138,13 +3138,15 @@ describeOneTableDetails(const char *schemaname,
bool islocal = PQgetvalue(result, i, 3)[0] == 't';
bool inherited = PQgetvalue(result, i, 4)[0] == 't';
- printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s",
+ printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s%s",
PQgetvalue(result, i, 0),
PQgetvalue(result, i, 1),
PQgetvalue(result, i, 2)[0] == 't' ?
" NO INHERIT" :
islocal && inherited ? _(" (local, inherited)") :
- inherited ? _(" (inherited)") : "");
+ inherited ? _(" (inherited)") : "",
+ PQgetvalue(result, i, 5)[0] == 'f' ?
+ " NOT VALID " : "");
printTableAddFooter(&cont, buf.data);
}
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index b51a2670958..a01a0fa96d2 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -229,6 +229,7 @@ MAKE_SYSCACHE(ATTNUM, pg_attribute_relid_attnum_index, 128);
#define ATTRIBUTE_NOTNULL_TRUE 't'
#define ATTRIBUTE_NOTNULL_FALSE 'f'
+#define ATTRIBUTE_NOTNULL_INVALID 'i'
#endif /* EXPOSE_TO_CLIENT_CODE */
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 6da164e7e4d..0e01ff1bbbd 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -264,7 +264,8 @@ 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,
- bool is_local, bool is_no_inherit);
+ const char *conname, bool is_local,
+ bool is_no_inherit);
extern List *RelationGetNotNullConstraints(Oid relid, bool cooked,
bool include_noinh);
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..b52578bf8d3 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -896,6 +896,159 @@ Not-null constraints:
"foobar" NOT NULL "a"
DROP TABLE notnull_tbl1;
+-- verify NOT NULL VALID/NOT VALID
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1);
+INSERT INTO notnull_tbl1 VALUES (NULL, 2);
+INSERT INTO notnull_tbl1 VALUES (300, 3);
+-- Below statement should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID;
+\d+ notnull_tbl1
+ Table "public.notnull_tbl1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | not null | | plain | |
+ b | integer | | | | plain | |
+Not-null constraints:
+ "nn" NOT NULL "a" NOT VALID
+
+-- Try to insert new record with NULL, should throw an error
+INSERT INTO notnull_tbl1 VALUES (NULL, 4);
+ERROR: null value in column "a" of relation "notnull_tbl1" violates not-null constraint
+DETAIL: Failing row contains (null, 4).
+-- SELECT NULL values for COLUMN a, should return 2 records.
+SELECT * FROM notnull_tbl1 WHERE a is NULL;
+ a | b
+---+---
+ | 1
+ | 2
+(2 rows)
+
+-- UPDATE the one of the NULL values
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+-- DELETE the record (NULL, 2)
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1;
+ a | b
+-----+---
+ 300 | 3
+ 100 | 1
+(2 rows)
+
+-- Try to add primary key on table column marked as NOT VALID NOT NULL
+-- constraint. This should throw an error.
+ALTER TABLE notnull_tbl1 add primary key (a);
+ERROR: column "a" of table "notnull_tbl1" is marked as NOT VALID NOT NULL constraint
+-- INHERITS table having NOT VALID NOT NULL constraints.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+NOTICE: merging column "a" with inherited definition
+NOTICE: merging column "b" with inherited definition
+-- Child table NOT NULL constraints should be valid.
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid = 'notnull_tbl1_child'::regclass;
+ conname | convalidated
+---------+--------------
+ nn | t
+(1 row)
+
+DROP TABLE notnull_tbl1_child;
+ALTER TABLE notnull_tbl1 validate constraint nn;
+\d+ notnull_tbl1
+ Table "public.notnull_tbl1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | not null | | plain | |
+ b | integer | | | | plain | |
+Not-null constraints:
+ "nn" NOT NULL "a"
+
+DROP TABLE notnull_tbl1;
+-- Test the different Not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent not null a not valid;
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child not null a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+-- This statement should validate not null constraint for parent as well as
+-- child.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
+in ('notnull_tbl1'::regclass, 'notnull_chld'::regclass);
+ conname | convalidated
+-----------+--------------
+ nn_parent | t
+ nn_child | t
+(2 rows)
+
+DROP TABLE notnull_tbl1 CASCADE;
+NOTICE: drop cascades to table notnull_chld
+-- test to throw an error when trying to add another NOT NULL
+-- on the table column with INVALID NOT NULL constraint.
+CREATE TABLE notnull_tbl1 (a INTEGER);
+INSERT INTO notnull_tbl1 VALUES ( NULL );
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid;
+-- Should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+ERROR: Invalid NOT NULL constraint "nn1" exist on relation "notnull_tbl1"
+HINT: Do VALIDATE CONSTRAINT
+-- This should throw an error while validating the NOT NULL
+-- constraint.
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+-- After deleting null values, SET NOT NULL should work
+DELETE FROM notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
+in ('notnull_tbl1'::regclass);
+ conname | convalidated
+---------+--------------
+ nn1 | t
+(1 row)
+
+DROP TABLE notnull_tbl1;
+-- Test invalid not null on inheritance table.
+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;
+NOTICE: merging definition of column "i" for child "inh_child"
+NOTICE: merging definition of column "i" for child "inh_grandchild"
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ALTER i SET NOT NULL;
+SELECT conrelid::regclass, conname, convalidated, coninhcount FROM pg_catalog.pg_constraint WHERE conrelid
+in ('inh_parent'::regclass, 'inh_child'::regclass, 'inh_grandchild'::regclass) ORDER BY 1;
+ conrelid | conname | convalidated | coninhcount
+----------------+---------+--------------+-------------
+ inh_parent | nn | t | 0
+ inh_child | nn | t | 1
+ inh_grandchild | nn | t | 2
+(3 rows)
+
+drop table inh_parent, inh_child, inh_grandchild;
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 2);
+INSERT INTO notnull_tbl1_upg VALUES (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY RANGE (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID;
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES FROM (0) TO (10);
+CREATE TABLE notnull_tbl1_2 PARTITION OF notnull_tbl1 FOR VALUES FROM (20) TO (30);
+-- Parent table NOT NULL constraints will be marked as validated false, where
+-- for child table it will be true
+SELECT conrelid::regclass, conname, convalidated FROM pg_catalog.pg_constraint WHERE
+conrelid IN ('notnull_tbl1'::regclass, 'notnull_tbl1_1'::regclass, 'notnull_tbl1_2'::regclass);
+ conrelid | conname | convalidated
+----------------+-------------+--------------
+ notnull_tbl1 | notnull_con | f
+ notnull_tbl1_1 | notnull_con | t
+ notnull_tbl1_2 | notnull_con | t
+(3 rows)
+
+DROP TABLE notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2;
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 21ce4177de4..7aff17e6764 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -640,6 +640,91 @@ ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a;
\d+ notnull_tbl1
DROP TABLE notnull_tbl1;
+-- verify NOT NULL VALID/NOT VALID
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1);
+INSERT INTO notnull_tbl1 VALUES (NULL, 2);
+INSERT INTO notnull_tbl1 VALUES (300, 3);
+-- Below statement should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID;
+\d+ notnull_tbl1
+-- Try to insert new record with NULL, should throw an error
+INSERT INTO notnull_tbl1 VALUES (NULL, 4);
+-- SELECT NULL values for COLUMN a, should return 2 records.
+SELECT * FROM notnull_tbl1 WHERE a is NULL;
+-- UPDATE the one of the NULL values
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+-- DELETE the record (NULL, 2)
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1;
+-- Try to add primary key on table column marked as NOT VALID NOT NULL
+-- constraint. This should throw an error.
+ALTER TABLE notnull_tbl1 add primary key (a);
+-- INHERITS table having NOT VALID NOT NULL constraints.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+-- Child table NOT NULL constraints should be valid.
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid = 'notnull_tbl1_child'::regclass;
+DROP TABLE notnull_tbl1_child;
+ALTER TABLE notnull_tbl1 validate constraint nn;
+\d+ notnull_tbl1
+DROP TABLE notnull_tbl1;
+-- Test the different Not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent not null a not valid;
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child not null a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+-- This statement should validate not null constraint for parent as well as
+-- child.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
+in ('notnull_tbl1'::regclass, 'notnull_chld'::regclass);
+DROP TABLE notnull_tbl1 CASCADE;
+-- test to throw an error when trying to add another NOT NULL
+-- on the table column with INVALID NOT NULL constraint.
+CREATE TABLE notnull_tbl1 (a INTEGER);
+INSERT INTO notnull_tbl1 VALUES ( NULL );
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid;
+-- Should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+-- This should throw an error while validating the NOT NULL
+-- constraint.
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+-- After deleting null values, SET NOT NULL should work
+DELETE FROM notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
+in ('notnull_tbl1'::regclass);
+DROP TABLE notnull_tbl1;
+-- Test invalid not null on inheritance table.
+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;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ALTER i SET NOT NULL;
+SELECT conrelid::regclass, conname, convalidated, coninhcount FROM pg_catalog.pg_constraint WHERE conrelid
+in ('inh_parent'::regclass, 'inh_child'::regclass, 'inh_grandchild'::regclass) ORDER BY 1;
+drop table inh_parent, inh_child, inh_grandchild;
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 2);
+INSERT INTO notnull_tbl1_upg VALUES (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY RANGE (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID;
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES FROM (0) TO (10);
+CREATE TABLE notnull_tbl1_2 PARTITION OF notnull_tbl1 FOR VALUES FROM (20) TO (30);
+-- Parent table NOT NULL constraints will be marked as validated false, where
+-- for child table it will be true
+SELECT conrelid::regclass, conname, convalidated FROM pg_catalog.pg_constraint WHERE
+conrelid IN ('notnull_tbl1'::regclass, 'notnull_tbl1_1'::regclass, 'notnull_tbl1_2'::regclass);
+DROP TABLE notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2;
+
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
--
2.43.0
[application/octet-stream] 0001-Convert-pg_attribut.attnotnull-to-char-type.patch (19.6K, ../../CAGPqQf31ctDvcbJE7J3in++iCRbf7-Rv07F7dpnu-_GyLWFz+w@mail.gmail.com/4-0001-Convert-pg_attribut.attnotnull-to-char-type.patch)
download | inline diff:
From 8e909c049dabe9d6ed559e14f076714ad7a43d8f Mon Sep 17 00:00:00 2001
From: Rushabh Lathia <[email protected]>
Date: Mon, 10 Feb 2025 15:39:16 +0530
Subject: [PATCH 1/3] Convert pg_attribut.attnotnull to char type.
This commit change the pg_attribut.attnotnull to char type. Now
attnotnull holds three values, where attnotnull can be either
TRUE, FALSE and INVALID. INVALID is when name not null constrint
is NOT VALIDATED.
---
src/backend/access/common/tupdesc.c | 12 ++++----
src/backend/bootstrap/bootstrap.c | 13 ++++----
src/backend/catalog/heap.c | 16 +++++-----
src/backend/catalog/index.c | 2 +-
src/backend/catalog/indexing.c | 3 +-
src/backend/catalog/information_schema.sql | 4 +--
src/backend/commands/tablecmds.c | 35 ++++++++++++----------
src/backend/executor/execMain.c | 3 +-
src/backend/jit/llvm/llvmjit_deform.c | 10 ++++---
src/backend/optimizer/util/plancat.c | 5 ++--
src/include/access/tupdesc.h | 2 +-
src/include/catalog/pg_attribute.h | 5 +++-
12 files changed, 63 insertions(+), 47 deletions(-)
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index ed2195f14b2..58698812c0b 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -251,7 +251,7 @@ CreateTupleDescCopy(TupleDesc tupdesc)
{
Form_pg_attribute att = TupleDescAttr(desc, i);
- att->attnotnull = false;
+ att->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -297,7 +297,7 @@ CreateTupleDescTruncatedCopy(TupleDesc tupdesc, int natts)
{
Form_pg_attribute att = TupleDescAttr(desc, i);
- att->attnotnull = false;
+ att->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -417,7 +417,7 @@ TupleDescCopy(TupleDesc dst, TupleDesc src)
{
Form_pg_attribute att = TupleDescAttr(dst, i);
- att->attnotnull = false;
+ att->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -463,7 +463,7 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno,
dstAtt->attnum = dstAttno;
/* since we're not copying constraints or defaults, clear these */
- dstAtt->attnotnull = false;
+ dstAtt->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
dstAtt->atthasdef = false;
dstAtt->atthasmissing = false;
dstAtt->attidentity = '\0';
@@ -840,7 +840,7 @@ TupleDescInitEntry(TupleDesc desc,
att->attnum = attributeNumber;
att->attndims = attdim;
- att->attnotnull = false;
+ att->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -903,7 +903,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
att->attnum = attributeNumber;
att->attndims = attdim;
- att->attnotnull = false;
+ att->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 6db864892d0..1e95dc32f46 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -582,14 +582,17 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
attrtypes[attnum]->atttypmod = -1;
attrtypes[attnum]->attislocal = true;
+ /* set default to false */
+ attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
+
if (nullness == BOOTCOL_NULL_FORCE_NOT_NULL)
{
- attrtypes[attnum]->attnotnull = true;
+ attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
}
else if (nullness == BOOTCOL_NULL_FORCE_NULL)
{
- attrtypes[attnum]->attnotnull = false;
+ attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
}
else
{
@@ -608,11 +611,11 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
for (i = 0; i < attnum; i++)
{
if (attrtypes[i]->attlen <= 0 ||
- !attrtypes[i]->attnotnull)
+ attrtypes[i]->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
break;
}
if (i == attnum)
- attrtypes[attnum]->attnotnull = true;
+ attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
}
}
}
@@ -696,7 +699,7 @@ InsertOneNull(int i)
{
elog(DEBUG4, "inserting column %d NULL", i);
Assert(i >= 0 && i < MAXATTR);
- if (TupleDescAttr(boot_reldesc->rd_att, i)->attnotnull)
+ if (TupleDescAttr(boot_reldesc->rd_att, i)->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
elog(ERROR,
"NULL value specified for not-null column \"%s\" of relation \"%s\"",
NameStr(TupleDescAttr(boot_reldesc->rd_att, i)->attname),
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..8c2d2cdcdfe 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -150,7 +150,7 @@ static const FormData_pg_attribute a1 = {
.attbyval = false,
.attalign = TYPALIGN_SHORT,
.attstorage = TYPSTORAGE_PLAIN,
- .attnotnull = true,
+ .attnotnull = ATTRIBUTE_NOTNULL_TRUE,
.attislocal = true,
};
@@ -163,7 +163,7 @@ static const FormData_pg_attribute a2 = {
.attbyval = true,
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
- .attnotnull = true,
+ .attnotnull = ATTRIBUTE_NOTNULL_TRUE,
.attislocal = true,
};
@@ -176,7 +176,7 @@ static const FormData_pg_attribute a3 = {
.attbyval = true,
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
- .attnotnull = true,
+ .attnotnull = ATTRIBUTE_NOTNULL_TRUE,
.attislocal = true,
};
@@ -189,7 +189,7 @@ static const FormData_pg_attribute a4 = {
.attbyval = true,
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
- .attnotnull = true,
+ .attnotnull = ATTRIBUTE_NOTNULL_TRUE,
.attislocal = true,
};
@@ -202,7 +202,7 @@ static const FormData_pg_attribute a5 = {
.attbyval = true,
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
- .attnotnull = true,
+ .attnotnull = ATTRIBUTE_NOTNULL_TRUE,
.attislocal = true,
};
@@ -221,7 +221,7 @@ static const FormData_pg_attribute a6 = {
.attbyval = true,
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
- .attnotnull = true,
+ .attnotnull = ATTRIBUTE_NOTNULL_TRUE,
.attislocal = true,
};
@@ -752,7 +752,7 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
slot[slotCount]->tts_values[Anum_pg_attribute_attalign - 1] = CharGetDatum(attrs->attalign);
slot[slotCount]->tts_values[Anum_pg_attribute_attstorage - 1] = CharGetDatum(attrs->attstorage);
slot[slotCount]->tts_values[Anum_pg_attribute_attcompression - 1] = CharGetDatum(attrs->attcompression);
- slot[slotCount]->tts_values[Anum_pg_attribute_attnotnull - 1] = BoolGetDatum(attrs->attnotnull);
+ slot[slotCount]->tts_values[Anum_pg_attribute_attnotnull - 1] = CharGetDatum(attrs->attnotnull);
slot[slotCount]->tts_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(attrs->atthasdef);
slot[slotCount]->tts_values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(attrs->atthasmissing);
slot[slotCount]->tts_values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(attrs->attidentity);
@@ -1713,7 +1713,7 @@ RemoveAttributeById(Oid relid, AttrNumber attnum)
attStruct->atttypid = InvalidOid;
/* Remove any not-null constraint the column may have */
- attStruct->attnotnull = false;
+ attStruct->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
/* Unset this so no one tries to look up the generation expression */
attStruct->attgenerated = '\0';
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 739a92bdcc1..6b0c16bf59d 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -262,7 +262,7 @@ index_check_primary_key(Relation heapRel,
attnum, RelationGetRelid(heapRel));
attform = (Form_pg_attribute) GETSTRUCT(atttuple);
- if (!attform->attnotnull)
+ if (attform->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("primary key column \"%s\" is not marked NOT NULL",
diff --git a/src/backend/catalog/indexing.c b/src/backend/catalog/indexing.c
index 25c4b6bdc87..a2211d75566 100644
--- a/src/backend/catalog/indexing.c
+++ b/src/backend/catalog/indexing.c
@@ -207,7 +207,8 @@ CatalogTupleCheckConstraints(Relation heapRel, HeapTuple tup)
{
Form_pg_attribute thisatt = TupleDescAttr(tupdesc, attnum);
- Assert(!(thisatt->attnotnull && att_isnull(attnum, bp)));
+ Assert(!(thisatt->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ att_isnull(attnum, bp)));
}
}
}
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index a7bffca93d1..4f59bc46e74 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -292,7 +292,7 @@ CREATE VIEW attributes AS
CAST(a.attname AS sql_identifier) AS attribute_name,
CAST(a.attnum AS cardinal_number) AS ordinal_position,
CAST(pg_get_expr(ad.adbin, ad.adrelid) AS character_data) AS attribute_default,
- CAST(CASE WHEN a.attnotnull OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END
+ CAST(CASE WHEN a.attnotnull = 't' OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END
AS yes_or_no)
AS is_nullable, -- This column was apparently removed between SQL:2003 and SQL:2008.
@@ -671,7 +671,7 @@ CREATE VIEW columns AS
CAST(a.attname AS sql_identifier) AS column_name,
CAST(a.attnum AS cardinal_number) AS ordinal_position,
CAST(CASE WHEN a.attgenerated = '' THEN pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS column_default,
- CAST(CASE WHEN a.attnotnull OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END
+ CAST(CASE WHEN a.attnotnull = 't' OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END
AS yes_or_no)
AS is_nullable,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 18ff8956577..3c81e48cac1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1399,7 +1399,10 @@ BuildDescForRelation(const List *columns)
TupleDescInitEntryCollation(desc, attnum, attcollation);
/* Fill in additional stuff not handled by TupleDescInitEntry */
- att->attnotnull = entry->is_not_null;
+ if (entry->is_not_null)
+ att->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
+ else
+ att->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
att->attislocal = entry->is_local;
att->attinhcount = entry->inhcount;
att->attidentity = entry->identity;
@@ -6188,7 +6191,8 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
{
Form_pg_attribute attr = TupleDescAttr(newTupDesc, i);
- if (attr->attnotnull && !attr->attisdropped)
+ if (attr->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ !attr->attisdropped)
notnull_attrs = lappend_int(notnull_attrs, i);
}
if (notnull_attrs)
@@ -7669,7 +7673,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
RelationGetRelid(rel), attnum);
/* If the column is already nullable there's nothing to do. */
- if (!attTup->attnotnull)
+ if (attTup->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
{
table_close(attr_rel, RowExclusiveLock);
return InvalidObjectAddress;
@@ -7699,7 +7703,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
AttrNumber parent_attnum;
parent_attnum = get_attnum(parentId, colName);
- if (TupleDescAttr(tupDesc, parent_attnum - 1)->attnotnull)
+ if (TupleDescAttr(tupDesc, parent_attnum - 1)->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("column \"%s\" is marked NOT NULL in parent table",
@@ -7753,7 +7757,7 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
if (attr->attisdropped)
return;
- if (!attr->attnotnull)
+ if (attr->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
{
Relation attr_rel;
HeapTuple tuple;
@@ -7766,8 +7770,8 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
attnum, RelationGetRelid(rel));
attr = (Form_pg_attribute) GETSTRUCT(tuple);
- Assert(!attr->attnotnull);
- attr->attnotnull = true;
+ Assert(attr->attnotnull == ATTRIBUTE_NOTNULL_FALSE);
+ attr->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
/*
@@ -8172,7 +8176,7 @@ ATExecAddIdentity(Relation rel, const char *colName,
* to an existing column that is not NOT NULL would create a state that
* cannot be reproduced without contortions.
*/
- if (!attTup->attnotnull)
+ if (attTup->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("column \"%s\" of relation \"%s\" must be declared NOT NULL before identity can be added",
@@ -9373,7 +9377,7 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
elog(ERROR, "cache lookup failed for attribute %s of relation %u",
attname, childrelid);
attrForm = (Form_pg_attribute) GETSTRUCT(tup);
- if (!attrForm->attnotnull)
+ if (attrForm->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
ereport(ERROR,
errmsg("column \"%s\" of table \"%s\" is not marked NOT NULL",
attname, get_rel_name(childrelid)));
@@ -13476,9 +13480,9 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
RelationGetRelationName(rel)));
/* All good -- reset attnotnull if needed */
- if (attForm->attnotnull)
+ if (attForm->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
{
- attForm->attnotnull = false;
+ attForm->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
CatalogTupleUpdate(attrel, &atttup->t_self, atttup);
}
@@ -16805,7 +16809,8 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
*
* Other constraints are checked elsewhere.
*/
- if (parent_att->attnotnull && !child_att->attnotnull)
+ if (parent_att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ child_att->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
{
HeapTuple contup;
@@ -17848,7 +17853,7 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
RelationGetRelationName(indexRel), attno)));
attr = TupleDescAttr(rel->rd_att, attno - 1);
- if (!attr->attnotnull)
+ if (attr->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("index \"%s\" cannot be used as replica identity because column \"%s\" is nullable",
@@ -19326,7 +19331,7 @@ PartConstraintImpliedByRelConstraint(Relation scanrel,
{
Form_pg_attribute att = TupleDescAttr(scanrel->rd_att, i - 1);
- if (att->attnotnull && !att->attisdropped)
+ if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE && !att->attisdropped)
{
NullTest *ntest = makeNode(NullTest);
@@ -21158,7 +21163,7 @@ verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition)
Form_pg_attribute att = TupleDescAttr(RelationGetDescr(partition),
iinfo->ii_IndexAttrNumbers[i] - 1);
- if (!att->attnotnull)
+ if (att->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
ereport(ERROR,
errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("invalid primary key definition"),
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 0493b7d5365..d582231ab60 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -2070,7 +2070,8 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
{
Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
- if (att->attnotnull && slot_attisnull(slot, attrChk))
+ if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ slot_attisnull(slot, attrChk))
{
char *val_desc;
Relation orig_rel = rel;
diff --git a/src/backend/jit/llvm/llvmjit_deform.c b/src/backend/jit/llvm/llvmjit_deform.c
index 5d169c7a40b..e6444bc2c91 100644
--- a/src/backend/jit/llvm/llvmjit_deform.c
+++ b/src/backend/jit/llvm/llvmjit_deform.c
@@ -123,7 +123,7 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
* combination of attisdropped && attnotnull combination shouldn't
* exist.
*/
- if (att->attnotnull &&
+ if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
!att->atthasmissing &&
!att->attisdropped)
guaranteed_column_number = attnum;
@@ -438,7 +438,7 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
* into account, because if they're present the heaptuple's natts
* would have indicated that a slot_getmissingattrs() is needed.
*/
- if (!att->attnotnull)
+ if (att->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
{
LLVMBasicBlockRef b_ifnotnull;
LLVMBasicBlockRef b_ifnull;
@@ -604,7 +604,8 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
known_alignment = -1;
attguaranteedalign = false;
}
- else if (att->attnotnull && attguaranteedalign && known_alignment >= 0)
+ else if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ attguaranteedalign && known_alignment >= 0)
{
/*
* If the offset to the column was previously known, a NOT NULL &
@@ -614,7 +615,8 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
Assert(att->attlen > 0);
known_alignment += att->attlen;
}
- else if (att->attnotnull && (att->attlen % alignto) == 0)
+ else if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ (att->attlen % alignto) == 0)
{
/*
* After a NOT NULL fixed-width column with a length that is a
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 71abb01f655..e322098cfb2 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -177,7 +177,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
{
CompactAttribute *attr = TupleDescCompactAttr(relation->rd_att, i);
- if (attr->attnotnull)
+ if (attr->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
{
rel->notnullattnums = bms_add_member(rel->notnullattnums,
i + 1);
@@ -1355,7 +1355,8 @@ get_relation_constraints(PlannerInfo *root,
{
Form_pg_attribute att = TupleDescAttr(relation->rd_att, i - 1);
- if (att->attnotnull && !att->attisdropped)
+ if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ !att->attisdropped)
{
NullTest *ntest = makeNode(NullTest);
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 396eeb7a0bb..1eba67ca8e9 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -76,7 +76,7 @@ typedef struct CompactAttribute
bool atthasmissing; /* as FormData_pg_attribute.atthasmissing */
bool attisdropped; /* as FormData_pg_attribute.attisdropped */
bool attgenerated; /* FormData_pg_attribute.attgenerated != '\0' */
- bool attnotnull; /* as FormData_pg_attribute.attnotnull */
+ char attnotnull; /* as FormData_pg_attribute.attnotnull */
uint8 attalignby; /* alignment requirement in bytes */
} CompactAttribute;
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index deaa515fe53..b51a2670958 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -118,7 +118,7 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
char attcompression BKI_DEFAULT('\0');
/* This flag represents the "NOT NULL" constraint */
- bool attnotnull;
+ char attnotnull;
/* Has DEFAULT value or not */
bool atthasdef BKI_DEFAULT(f);
@@ -227,6 +227,9 @@ MAKE_SYSCACHE(ATTNUM, pg_attribute_relid_attnum_index, 128);
#define ATTRIBUTE_GENERATED_STORED 's'
#define ATTRIBUTE_GENERATED_VIRTUAL 'v'
+#define ATTRIBUTE_NOTNULL_TRUE 't'
+#define ATTRIBUTE_NOTNULL_FALSE 'f'
+
#endif /* EXPOSE_TO_CLIENT_CODE */
#endif /* PG_ATTRIBUTE_H */
--
2.43.0
[application/octet-stream] 0003-Documentation-changes.patch (3.0K, ../../CAGPqQf31ctDvcbJE7J3in++iCRbf7-Rv07F7dpnu-_GyLWFz+w@mail.gmail.com/5-0003-Documentation-changes.patch)
download | inline diff:
From b093a8a109fa7aec8caf792d9e3521fcebada86f Mon Sep 17 00:00:00 2001
From: Rushabh Lathia <[email protected]>
Date: Thu, 27 Feb 2025 15:22:56 +0530
Subject: [PATCH 3/3] Documentation changes
---
doc/src/sgml/catalogs.sgml | 5 +++--
doc/src/sgml/ref/alter_table.sgml | 4 ++--
src/bin/psql/tab-complete.in.c | 2 +-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..d6066d7dba1 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1257,10 +1257,11 @@
<row>
<entry role="catalog_table_entry"><para role="column_definition">
- <structfield>attnotnull</structfield> <type>bool</type>
+ <structfield>attnotnull</structfield> <type>char</type>
</para>
<para>
- This column has a not-null constraint.
+ The value 't' indicates that a not-null constraint exists for the
+ column, 'i' for an invalid constraint and 'f' for none.
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index dceb7a7593c..288cd3339f8 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -99,7 +99,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 [ NO INHERIT ] |
+{ NOT NULL [ NOT VALID ] [ NO INHERIT ] |
NULL |
CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
DEFAULT <replaceable>default_expr</replaceable> |
@@ -458,7 +458,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<para>
This form adds a new constraint to a table using the same constraint
syntax as <link linkend="sql-createtable"><command>CREATE TABLE</command></link>, plus the option <literal>NOT
- VALID</literal>, which is currently only allowed for foreign key
+ VALID</literal>, which is currently only allowed for foreign key, NOT NULL constraints
and CHECK constraints.
</para>
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8432be641ac..1c3295d949a 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2728,7 +2728,7 @@ match_previous_words(int pattern_id,
COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
/* ALTER TABLE xxx ADD CONSTRAINT yyy */
else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny))
- COMPLETE_WITH("CHECK", "UNIQUE", "PRIMARY KEY", "EXCLUDE", "FOREIGN KEY");
+ COMPLETE_WITH("CHECK", "UNIQUE", "PRIMARY KEY", "EXCLUDE", "FOREIGN KEY", "NOT NULL");
/* ALTER TABLE xxx ADD [CONSTRAINT yyy] (PRIMARY KEY|UNIQUE) */
else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "PRIMARY", "KEY") ||
Matches("ALTER", "TABLE", MatchAny, "ADD", "UNIQUE") ||
--
2.43.0
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
@ 2025-03-12 08:27 ` Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: Ashutosh Bapat @ 2025-03-12 08:27 UTC (permalink / raw)
To: Rushabh Lathia <[email protected]>; +Cc: Alvaro Herrera <[email protected]>; pgsql-hackers
On Tue, Mar 11, 2025 at 3:46 PM Rushabh Lathia <[email protected]> wrote:
>>
>> > JFYI, I can reproduce the failure Ashutosh Bapat reported, while
>> > running the pg_upgrade test through meson commands. I am
>> > investigating that further.
>>
>> Ah good, thanks.
>
>
> Somehow, I am now not able to reproduce after the clean build. Yesterday
> I was able to reproduce, so I was happy, but again trying to analyze the issue
> when I start with the
If the test passes for you, can you please try the patches at [1] on
top of your patches? Please apply those, set and export environment
variable PG_TEST_EXTRA=regress_dump_test, and run 002_pg_upgrade test?
I intended to do this but can not do it since the test always fails
with your patches applied.
[1] https://www.postgresql.org/message-id/CAExHW5uQoyOddBKLBBJpfxXqqok%3DBTeMvt5OpnM6gw0SroiUUw%40mail.g...
--
Best Wishes,
Ashutosh Bapat
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
@ 2025-03-12 09:50 ` Alvaro Herrera <[email protected]>
2025-03-12 10:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
0 siblings, 2 replies; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-12 09:50 UTC (permalink / raw)
To: Ashutosh Bapat <[email protected]>; +Cc: Rushabh Lathia <[email protected]>; pgsql-hackers
On 2025-Mar-12, Ashutosh Bapat wrote:
> If the test passes for you, can you please try the patches at [1] on
> top of your patches? Please apply those, set and export environment
> variable PG_TEST_EXTRA=regress_dump_test, and run 002_pg_upgrade test?
> I intended to do this but can not do it since the test always fails
> with your patches applied.
Oh, I need to enable a PG_TEST_EXTRA option in order for the test to
run? FFS. That explains why the tests passed just fine for me.
I'll re-run.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"La conclusión que podemos sacar de esos estudios es que
no podemos sacar ninguna conclusión de ellos" (Tanenbaum)
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-03-12 10:04 ` Rushabh Lathia <[email protected]>
1 sibling, 0 replies; 40+ messages in thread
From: Rushabh Lathia @ 2025-03-12 10:04 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: Ashutosh Bapat <[email protected]>; pgsql-hackers
On Wed, Mar 12, 2025 at 3:20 PM Alvaro Herrera <[email protected]>
wrote:
> On 2025-Mar-12, Ashutosh Bapat wrote:
>
> > If the test passes for you, can you please try the patches at [1] on
> > top of your patches? Please apply those, set and export environment
> > variable PG_TEST_EXTRA=regress_dump_test, and run 002_pg_upgrade test?
> > I intended to do this but can not do it since the test always fails
> > with your patches applied.
>
> Oh, I need to enable a PG_TEST_EXTRA option in order for the test to
> run? FFS. That explains why the tests passed just fine for me.
> I'll re-run.
>
> --
> Álvaro Herrera Breisgau, Deutschland —
> https://www.EnterpriseDB.com/
> "La conclusión que podemos sacar de esos estudios es que
> no podemos sacar ninguna conclusión de ellos" (Tanenbaum)
>
I can reproduce the issue and will be sending the patch soon.
Thanks Alvaro, Ashutosh.
Rushabh Lathia
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-03-12 10:08 ` Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
1 sibling, 1 reply; 40+ messages in thread
From: Ashutosh Bapat @ 2025-03-12 10:08 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: Rushabh Lathia <[email protected]>; pgsql-hackers
On Wed, Mar 12, 2025 at 3:20 PM Alvaro Herrera <[email protected]> wrote:
>
> On 2025-Mar-12, Ashutosh Bapat wrote:
>
> > If the test passes for you, can you please try the patches at [1] on
> > top of your patches? Please apply those, set and export environment
> > variable PG_TEST_EXTRA=regress_dump_test, and run 002_pg_upgrade test?
> > I intended to do this but can not do it since the test always fails
> > with your patches applied.
>
> Oh, I need to enable a PG_TEST_EXTRA option in order for the test to
> run? FFS. That explains why the tests passed just fine for me.
> I'll re-run.
You need PG_TEST_EXTRA to run the testcase I am adding there. Rest of
the testcases run without PG_TEST_EXTRA.
--
Best Wishes,
Ashutosh Bapat
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
@ 2025-03-12 10:22 ` Rushabh Lathia <[email protected]>
2025-03-12 11:51 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
0 siblings, 2 replies; 40+ messages in thread
From: Rushabh Lathia @ 2025-03-12 10:22 UTC (permalink / raw)
To: Ashutosh Bapat <[email protected]>; Alvaro Herrera <[email protected]>; +Cc: pgsql-hackers
Hi Alvaro,
Here are the latest patches, which includes the regression fix.
Thanks,
On Wed, Mar 12, 2025 at 3:38 PM Ashutosh Bapat <[email protected]>
wrote:
> On Wed, Mar 12, 2025 at 3:20 PM Alvaro Herrera <[email protected]>
> wrote:
> >
> > On 2025-Mar-12, Ashutosh Bapat wrote:
> >
> > > If the test passes for you, can you please try the patches at [1] on
> > > top of your patches? Please apply those, set and export environment
> > > variable PG_TEST_EXTRA=regress_dump_test, and run 002_pg_upgrade test?
> > > I intended to do this but can not do it since the test always fails
> > > with your patches applied.
> >
> > Oh, I need to enable a PG_TEST_EXTRA option in order for the test to
> > run? FFS. That explains why the tests passed just fine for me.
> > I'll re-run.
>
> You need PG_TEST_EXTRA to run the testcase I am adding there. Rest of
> the testcases run without PG_TEST_EXTRA.
>
> --
> Best Wishes,
> Ashutosh Bapat
>
--
Rushabh Lathia
Attachments:
[application/octet-stream] 0001-Convert-pg_attribut.attnotnull-to-char-type.patch (19.6K, ../../CAGPqQf2so0xrznT9sFjFFROnDgpbnAQbs2vg71QdC295afjm5Q@mail.gmail.com/3-0001-Convert-pg_attribut.attnotnull-to-char-type.patch)
download | inline diff:
From 547d79773cd88a94810d7d4c2e005f63316d9601 Mon Sep 17 00:00:00 2001
From: Rushabh Lathia <[email protected]>
Date: Mon, 10 Feb 2025 15:39:16 +0530
Subject: [PATCH 1/3] Convert pg_attribut.attnotnull to char type.
This commit change the pg_attribut.attnotnull to char type. Now
attnotnull holds three values, where attnotnull can be either
TRUE, FALSE and INVALID. INVALID is when name not null constrint
is NOT VALIDATED.
---
src/backend/access/common/tupdesc.c | 12 ++++----
src/backend/bootstrap/bootstrap.c | 13 ++++----
src/backend/catalog/heap.c | 16 +++++-----
src/backend/catalog/index.c | 2 +-
src/backend/catalog/indexing.c | 3 +-
src/backend/catalog/information_schema.sql | 4 +--
src/backend/commands/tablecmds.c | 35 ++++++++++++----------
src/backend/executor/execMain.c | 3 +-
src/backend/jit/llvm/llvmjit_deform.c | 10 ++++---
src/backend/optimizer/util/plancat.c | 5 ++--
src/include/access/tupdesc.h | 2 +-
src/include/catalog/pg_attribute.h | 5 +++-
12 files changed, 63 insertions(+), 47 deletions(-)
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index ed2195f14b2..58698812c0b 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -251,7 +251,7 @@ CreateTupleDescCopy(TupleDesc tupdesc)
{
Form_pg_attribute att = TupleDescAttr(desc, i);
- att->attnotnull = false;
+ att->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -297,7 +297,7 @@ CreateTupleDescTruncatedCopy(TupleDesc tupdesc, int natts)
{
Form_pg_attribute att = TupleDescAttr(desc, i);
- att->attnotnull = false;
+ att->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -417,7 +417,7 @@ TupleDescCopy(TupleDesc dst, TupleDesc src)
{
Form_pg_attribute att = TupleDescAttr(dst, i);
- att->attnotnull = false;
+ att->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -463,7 +463,7 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno,
dstAtt->attnum = dstAttno;
/* since we're not copying constraints or defaults, clear these */
- dstAtt->attnotnull = false;
+ dstAtt->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
dstAtt->atthasdef = false;
dstAtt->atthasmissing = false;
dstAtt->attidentity = '\0';
@@ -840,7 +840,7 @@ TupleDescInitEntry(TupleDesc desc,
att->attnum = attributeNumber;
att->attndims = attdim;
- att->attnotnull = false;
+ att->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -903,7 +903,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
att->attnum = attributeNumber;
att->attndims = attdim;
- att->attnotnull = false;
+ att->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 6db864892d0..1e95dc32f46 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -582,14 +582,17 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
attrtypes[attnum]->atttypmod = -1;
attrtypes[attnum]->attislocal = true;
+ /* set default to false */
+ attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
+
if (nullness == BOOTCOL_NULL_FORCE_NOT_NULL)
{
- attrtypes[attnum]->attnotnull = true;
+ attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
}
else if (nullness == BOOTCOL_NULL_FORCE_NULL)
{
- attrtypes[attnum]->attnotnull = false;
+ attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
}
else
{
@@ -608,11 +611,11 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
for (i = 0; i < attnum; i++)
{
if (attrtypes[i]->attlen <= 0 ||
- !attrtypes[i]->attnotnull)
+ attrtypes[i]->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
break;
}
if (i == attnum)
- attrtypes[attnum]->attnotnull = true;
+ attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
}
}
}
@@ -696,7 +699,7 @@ InsertOneNull(int i)
{
elog(DEBUG4, "inserting column %d NULL", i);
Assert(i >= 0 && i < MAXATTR);
- if (TupleDescAttr(boot_reldesc->rd_att, i)->attnotnull)
+ if (TupleDescAttr(boot_reldesc->rd_att, i)->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
elog(ERROR,
"NULL value specified for not-null column \"%s\" of relation \"%s\"",
NameStr(TupleDescAttr(boot_reldesc->rd_att, i)->attname),
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..8c2d2cdcdfe 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -150,7 +150,7 @@ static const FormData_pg_attribute a1 = {
.attbyval = false,
.attalign = TYPALIGN_SHORT,
.attstorage = TYPSTORAGE_PLAIN,
- .attnotnull = true,
+ .attnotnull = ATTRIBUTE_NOTNULL_TRUE,
.attislocal = true,
};
@@ -163,7 +163,7 @@ static const FormData_pg_attribute a2 = {
.attbyval = true,
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
- .attnotnull = true,
+ .attnotnull = ATTRIBUTE_NOTNULL_TRUE,
.attislocal = true,
};
@@ -176,7 +176,7 @@ static const FormData_pg_attribute a3 = {
.attbyval = true,
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
- .attnotnull = true,
+ .attnotnull = ATTRIBUTE_NOTNULL_TRUE,
.attislocal = true,
};
@@ -189,7 +189,7 @@ static const FormData_pg_attribute a4 = {
.attbyval = true,
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
- .attnotnull = true,
+ .attnotnull = ATTRIBUTE_NOTNULL_TRUE,
.attislocal = true,
};
@@ -202,7 +202,7 @@ static const FormData_pg_attribute a5 = {
.attbyval = true,
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
- .attnotnull = true,
+ .attnotnull = ATTRIBUTE_NOTNULL_TRUE,
.attislocal = true,
};
@@ -221,7 +221,7 @@ static const FormData_pg_attribute a6 = {
.attbyval = true,
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
- .attnotnull = true,
+ .attnotnull = ATTRIBUTE_NOTNULL_TRUE,
.attislocal = true,
};
@@ -752,7 +752,7 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
slot[slotCount]->tts_values[Anum_pg_attribute_attalign - 1] = CharGetDatum(attrs->attalign);
slot[slotCount]->tts_values[Anum_pg_attribute_attstorage - 1] = CharGetDatum(attrs->attstorage);
slot[slotCount]->tts_values[Anum_pg_attribute_attcompression - 1] = CharGetDatum(attrs->attcompression);
- slot[slotCount]->tts_values[Anum_pg_attribute_attnotnull - 1] = BoolGetDatum(attrs->attnotnull);
+ slot[slotCount]->tts_values[Anum_pg_attribute_attnotnull - 1] = CharGetDatum(attrs->attnotnull);
slot[slotCount]->tts_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(attrs->atthasdef);
slot[slotCount]->tts_values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(attrs->atthasmissing);
slot[slotCount]->tts_values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(attrs->attidentity);
@@ -1713,7 +1713,7 @@ RemoveAttributeById(Oid relid, AttrNumber attnum)
attStruct->atttypid = InvalidOid;
/* Remove any not-null constraint the column may have */
- attStruct->attnotnull = false;
+ attStruct->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
/* Unset this so no one tries to look up the generation expression */
attStruct->attgenerated = '\0';
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 739a92bdcc1..6b0c16bf59d 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -262,7 +262,7 @@ index_check_primary_key(Relation heapRel,
attnum, RelationGetRelid(heapRel));
attform = (Form_pg_attribute) GETSTRUCT(atttuple);
- if (!attform->attnotnull)
+ if (attform->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("primary key column \"%s\" is not marked NOT NULL",
diff --git a/src/backend/catalog/indexing.c b/src/backend/catalog/indexing.c
index 25c4b6bdc87..a2211d75566 100644
--- a/src/backend/catalog/indexing.c
+++ b/src/backend/catalog/indexing.c
@@ -207,7 +207,8 @@ CatalogTupleCheckConstraints(Relation heapRel, HeapTuple tup)
{
Form_pg_attribute thisatt = TupleDescAttr(tupdesc, attnum);
- Assert(!(thisatt->attnotnull && att_isnull(attnum, bp)));
+ Assert(!(thisatt->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ att_isnull(attnum, bp)));
}
}
}
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index a7bffca93d1..4f59bc46e74 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -292,7 +292,7 @@ CREATE VIEW attributes AS
CAST(a.attname AS sql_identifier) AS attribute_name,
CAST(a.attnum AS cardinal_number) AS ordinal_position,
CAST(pg_get_expr(ad.adbin, ad.adrelid) AS character_data) AS attribute_default,
- CAST(CASE WHEN a.attnotnull OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END
+ CAST(CASE WHEN a.attnotnull = 't' OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END
AS yes_or_no)
AS is_nullable, -- This column was apparently removed between SQL:2003 and SQL:2008.
@@ -671,7 +671,7 @@ CREATE VIEW columns AS
CAST(a.attname AS sql_identifier) AS column_name,
CAST(a.attnum AS cardinal_number) AS ordinal_position,
CAST(CASE WHEN a.attgenerated = '' THEN pg_get_expr(ad.adbin, ad.adrelid) END AS character_data) AS column_default,
- CAST(CASE WHEN a.attnotnull OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END
+ CAST(CASE WHEN a.attnotnull = 't' OR (t.typtype = 'd' AND t.typnotnull) THEN 'NO' ELSE 'YES' END
AS yes_or_no)
AS is_nullable,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 18ff8956577..3c81e48cac1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1399,7 +1399,10 @@ BuildDescForRelation(const List *columns)
TupleDescInitEntryCollation(desc, attnum, attcollation);
/* Fill in additional stuff not handled by TupleDescInitEntry */
- att->attnotnull = entry->is_not_null;
+ if (entry->is_not_null)
+ att->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
+ else
+ att->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
att->attislocal = entry->is_local;
att->attinhcount = entry->inhcount;
att->attidentity = entry->identity;
@@ -6188,7 +6191,8 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
{
Form_pg_attribute attr = TupleDescAttr(newTupDesc, i);
- if (attr->attnotnull && !attr->attisdropped)
+ if (attr->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ !attr->attisdropped)
notnull_attrs = lappend_int(notnull_attrs, i);
}
if (notnull_attrs)
@@ -7669,7 +7673,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
RelationGetRelid(rel), attnum);
/* If the column is already nullable there's nothing to do. */
- if (!attTup->attnotnull)
+ if (attTup->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
{
table_close(attr_rel, RowExclusiveLock);
return InvalidObjectAddress;
@@ -7699,7 +7703,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
AttrNumber parent_attnum;
parent_attnum = get_attnum(parentId, colName);
- if (TupleDescAttr(tupDesc, parent_attnum - 1)->attnotnull)
+ if (TupleDescAttr(tupDesc, parent_attnum - 1)->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("column \"%s\" is marked NOT NULL in parent table",
@@ -7753,7 +7757,7 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
if (attr->attisdropped)
return;
- if (!attr->attnotnull)
+ if (attr->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
{
Relation attr_rel;
HeapTuple tuple;
@@ -7766,8 +7770,8 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
attnum, RelationGetRelid(rel));
attr = (Form_pg_attribute) GETSTRUCT(tuple);
- Assert(!attr->attnotnull);
- attr->attnotnull = true;
+ Assert(attr->attnotnull == ATTRIBUTE_NOTNULL_FALSE);
+ attr->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
/*
@@ -8172,7 +8176,7 @@ ATExecAddIdentity(Relation rel, const char *colName,
* to an existing column that is not NOT NULL would create a state that
* cannot be reproduced without contortions.
*/
- if (!attTup->attnotnull)
+ if (attTup->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("column \"%s\" of relation \"%s\" must be declared NOT NULL before identity can be added",
@@ -9373,7 +9377,7 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
elog(ERROR, "cache lookup failed for attribute %s of relation %u",
attname, childrelid);
attrForm = (Form_pg_attribute) GETSTRUCT(tup);
- if (!attrForm->attnotnull)
+ if (attrForm->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
ereport(ERROR,
errmsg("column \"%s\" of table \"%s\" is not marked NOT NULL",
attname, get_rel_name(childrelid)));
@@ -13476,9 +13480,9 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
RelationGetRelationName(rel)));
/* All good -- reset attnotnull if needed */
- if (attForm->attnotnull)
+ if (attForm->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
{
- attForm->attnotnull = false;
+ attForm->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
CatalogTupleUpdate(attrel, &atttup->t_self, atttup);
}
@@ -16805,7 +16809,8 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
*
* Other constraints are checked elsewhere.
*/
- if (parent_att->attnotnull && !child_att->attnotnull)
+ if (parent_att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ child_att->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
{
HeapTuple contup;
@@ -17848,7 +17853,7 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
RelationGetRelationName(indexRel), attno)));
attr = TupleDescAttr(rel->rd_att, attno - 1);
- if (!attr->attnotnull)
+ if (attr->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("index \"%s\" cannot be used as replica identity because column \"%s\" is nullable",
@@ -19326,7 +19331,7 @@ PartConstraintImpliedByRelConstraint(Relation scanrel,
{
Form_pg_attribute att = TupleDescAttr(scanrel->rd_att, i - 1);
- if (att->attnotnull && !att->attisdropped)
+ if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE && !att->attisdropped)
{
NullTest *ntest = makeNode(NullTest);
@@ -21158,7 +21163,7 @@ verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition)
Form_pg_attribute att = TupleDescAttr(RelationGetDescr(partition),
iinfo->ii_IndexAttrNumbers[i] - 1);
- if (!att->attnotnull)
+ if (att->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
ereport(ERROR,
errcode(ERRCODE_INVALID_TABLE_DEFINITION),
errmsg("invalid primary key definition"),
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 0493b7d5365..d582231ab60 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -2070,7 +2070,8 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
{
Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
- if (att->attnotnull && slot_attisnull(slot, attrChk))
+ if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ slot_attisnull(slot, attrChk))
{
char *val_desc;
Relation orig_rel = rel;
diff --git a/src/backend/jit/llvm/llvmjit_deform.c b/src/backend/jit/llvm/llvmjit_deform.c
index 5d169c7a40b..e6444bc2c91 100644
--- a/src/backend/jit/llvm/llvmjit_deform.c
+++ b/src/backend/jit/llvm/llvmjit_deform.c
@@ -123,7 +123,7 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
* combination of attisdropped && attnotnull combination shouldn't
* exist.
*/
- if (att->attnotnull &&
+ if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
!att->atthasmissing &&
!att->attisdropped)
guaranteed_column_number = attnum;
@@ -438,7 +438,7 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
* into account, because if they're present the heaptuple's natts
* would have indicated that a slot_getmissingattrs() is needed.
*/
- if (!att->attnotnull)
+ if (att->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
{
LLVMBasicBlockRef b_ifnotnull;
LLVMBasicBlockRef b_ifnull;
@@ -604,7 +604,8 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
known_alignment = -1;
attguaranteedalign = false;
}
- else if (att->attnotnull && attguaranteedalign && known_alignment >= 0)
+ else if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ attguaranteedalign && known_alignment >= 0)
{
/*
* If the offset to the column was previously known, a NOT NULL &
@@ -614,7 +615,8 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
Assert(att->attlen > 0);
known_alignment += att->attlen;
}
- else if (att->attnotnull && (att->attlen % alignto) == 0)
+ else if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ (att->attlen % alignto) == 0)
{
/*
* After a NOT NULL fixed-width column with a length that is a
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 71abb01f655..e322098cfb2 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -177,7 +177,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
{
CompactAttribute *attr = TupleDescCompactAttr(relation->rd_att, i);
- if (attr->attnotnull)
+ if (attr->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
{
rel->notnullattnums = bms_add_member(rel->notnullattnums,
i + 1);
@@ -1355,7 +1355,8 @@ get_relation_constraints(PlannerInfo *root,
{
Form_pg_attribute att = TupleDescAttr(relation->rd_att, i - 1);
- if (att->attnotnull && !att->attisdropped)
+ if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ !att->attisdropped)
{
NullTest *ntest = makeNode(NullTest);
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 396eeb7a0bb..1eba67ca8e9 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -76,7 +76,7 @@ typedef struct CompactAttribute
bool atthasmissing; /* as FormData_pg_attribute.atthasmissing */
bool attisdropped; /* as FormData_pg_attribute.attisdropped */
bool attgenerated; /* FormData_pg_attribute.attgenerated != '\0' */
- bool attnotnull; /* as FormData_pg_attribute.attnotnull */
+ char attnotnull; /* as FormData_pg_attribute.attnotnull */
uint8 attalignby; /* alignment requirement in bytes */
} CompactAttribute;
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index deaa515fe53..b51a2670958 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -118,7 +118,7 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
char attcompression BKI_DEFAULT('\0');
/* This flag represents the "NOT NULL" constraint */
- bool attnotnull;
+ char attnotnull;
/* Has DEFAULT value or not */
bool atthasdef BKI_DEFAULT(f);
@@ -227,6 +227,9 @@ MAKE_SYSCACHE(ATTNUM, pg_attribute_relid_attnum_index, 128);
#define ATTRIBUTE_GENERATED_STORED 's'
#define ATTRIBUTE_GENERATED_VIRTUAL 'v'
+#define ATTRIBUTE_NOTNULL_TRUE 't'
+#define ATTRIBUTE_NOTNULL_FALSE 'f'
+
#endif /* EXPOSE_TO_CLIENT_CODE */
#endif /* PG_ATTRIBUTE_H */
--
2.43.0
[application/octet-stream] 0002-Support-NOT-VALID-and-VALIDATE-CONSTRAINT-for-named-.patch (48.3K, ../../CAGPqQf2so0xrznT9sFjFFROnDgpbnAQbs2vg71QdC295afjm5Q@mail.gmail.com/4-0002-Support-NOT-VALID-and-VALIDATE-CONSTRAINT-for-named-.patch)
download | inline diff:
From 4d790fef210116b15ad0ec028405f735501cc144 Mon Sep 17 00:00:00 2001
From: Rushabh Lathia <[email protected]>
Date: Mon, 10 Feb 2025 16:22:30 +0530
Subject: [PATCH 2/3] Support NOT VALID and VALIDATE CONSTRAINT for named NOT
NULL constraints.
Commit also add support for pg_dump to dump NOT VALID named NOT NULL
constraints.
---
src/backend/bootstrap/bootstrap.c | 4 +-
src/backend/catalog/heap.c | 2 +-
src/backend/catalog/pg_constraint.c | 31 ++-
src/backend/commands/tablecmds.c | 249 ++++++++++++++++++----
src/backend/executor/execMain.c | 3 +-
src/backend/parser/gram.y | 4 +-
src/bin/pg_dump/pg_dump.c | 172 +++++++++++++--
src/bin/pg_dump/pg_dump.h | 1 +
src/bin/pg_dump/t/002_pg_dump.pl | 17 ++
src/bin/psql/describe.c | 10 +-
src/include/catalog/pg_attribute.h | 1 +
src/include/catalog/pg_constraint.h | 3 +-
src/test/regress/expected/constraints.out | 153 +++++++++++++
src/test/regress/sql/constraints.sql | 85 ++++++++
14 files changed, 661 insertions(+), 74 deletions(-)
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 1e95dc32f46..919972dc409 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -582,10 +582,8 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
attrtypes[attnum]->atttypmod = -1;
attrtypes[attnum]->attislocal = true;
- /* set default to false */
attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
-
if (nullness == BOOTCOL_NULL_FORCE_NOT_NULL)
{
attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
@@ -699,7 +697,7 @@ InsertOneNull(int i)
{
elog(DEBUG4, "inserting column %d NULL", i);
Assert(i >= 0 && i < MAXATTR);
- if (TupleDescAttr(boot_reldesc->rd_att, i)->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
+ if (TupleDescAttr(boot_reldesc->rd_att, i)->attnotnull != ATTRIBUTE_NOTNULL_FALSE)
elog(ERROR,
"NULL value specified for not-null column \"%s\" of relation \"%s\"",
NameStr(TupleDescAttr(boot_reldesc->rd_att, i)->attname),
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 8c2d2cdcdfe..6458f78c216 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2626,7 +2626,7 @@ AddRelationNewConstraints(Relation rel,
* to add another one; just adjust inheritance status as needed.
*/
if (AdjustNotNullInheritance(RelationGetRelid(rel), colnum,
- is_local, cdef->is_no_inherit))
+ cdef->conname, is_local, cdef->is_no_inherit))
continue;
/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..e14041b34ca 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -574,7 +574,7 @@ ChooseConstraintName(const char *name1, const char *name2,
/*
* 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.
+ * 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
@@ -604,13 +604,11 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
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.
+ * We're looking for a NOTNULL constraint 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)
@@ -628,9 +626,9 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
}
/*
- * 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.
+ * Find and return the pg_constraint tuple that implements a
+ * 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)
@@ -726,10 +724,13 @@ extractNotNullColumn(HeapTuple constrTup)
* conislocal/coninhcount and return true.
* In the latter case, if is_local is true we flip conislocal true, or do
* nothing if it's already true; otherwise we increment coninhcount by 1.
+ *
+ * Function add a check for existing INVALID not-null constraint on
+ * the table column.
*/
bool
AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
- bool is_local, bool is_no_inherit)
+ const char *conname, bool is_local, bool is_no_inherit)
{
HeapTuple tup;
@@ -753,6 +754,18 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"",
NameStr(conform->conname), get_rel_name(relid)));
+ /*
+ * Throw an error if an invalid NOT NULL constraint exists on the
+ * table column and an attempt is made to add another valid NOT NULL
+ * constraint.
+ */
+ if (is_local && !conform->convalidated && conname)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("Invalid NOT NULL constraint \"%s\" exist on relation \"%s\"",
+ NameStr(conform->conname), get_rel_name(relid)),
+ errhint("Do VALIDATE CONSTRAINT"));
+
if (!is_local)
{
if (pg_add_s16_overflow(conform->coninhcount, 1,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3c81e48cac1..6a757c7c374 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -410,6 +410,9 @@ static void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation
static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
char *constrName, HeapTuple contuple,
bool recurse, bool recursing, LOCKMODE lockmode);
+static void QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple,
+ bool recurse, bool recursing, LOCKMODE lockmode);
static int transformColumnNameList(Oid relId, List *colList,
int16 *attnums, Oid *atttypids, Oid *attcollids);
static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
@@ -473,7 +476,7 @@ static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid)
static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
LOCKMODE lockmode);
static void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
- LOCKMODE lockmode);
+ char newvalue, LOCKMODE lockmode);
static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel,
char *constrname, char *colName,
bool recurse, bool recursing,
@@ -710,6 +713,7 @@ static List *GetParentedForeignKeyRefs(Relation partition);
static void ATDetachCheckNoForeignKeyRefs(Relation partition);
static char GetAttributeCompression(Oid atttypid, const char *compression);
static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static bool check_for_invalid_notnull(Oid relid, const char *attname);
/* ----------------------------------------------------------------
@@ -1315,7 +1319,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints,
old_notnulls);
foreach_int(attrnum, nncols)
- set_attnotnull(NULL, rel, attrnum, NoLock);
+ set_attnotnull(NULL, rel, attrnum, ATTRIBUTE_NOTNULL_TRUE, NoLock);
ObjectAddressSet(address, RelationRelationId, relationId);
@@ -6191,7 +6195,8 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
{
Form_pg_attribute attr = TupleDescAttr(newTupDesc, i);
- if (attr->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ if ((attr->attnotnull == ATTRIBUTE_NOTNULL_TRUE ||
+ attr->attnotnull == ATTRIBUTE_NOTNULL_INVALID) &&
!attr->attisdropped)
notnull_attrs = lappend_int(notnull_attrs, i);
}
@@ -7743,10 +7748,11 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
*/
static void
set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
- LOCKMODE lockmode)
+ char newvalue, LOCKMODE lockmode)
{
Form_pg_attribute attr;
+
CheckAlterTableIsSafe(rel);
/*
@@ -7757,7 +7763,7 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
if (attr->attisdropped)
return;
- if (attr->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
+ if (attr->attnotnull != newvalue)
{
Relation attr_rel;
HeapTuple tuple;
@@ -7770,15 +7776,17 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
attnum, RelationGetRelid(rel));
attr = (Form_pg_attribute) GETSTRUCT(tuple);
- Assert(attr->attnotnull == ATTRIBUTE_NOTNULL_FALSE);
- attr->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
+ attr->attnotnull = newvalue;
+
CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
/*
- * If the nullness isn't already proven by validated constraints, have
- * ALTER TABLE phase 3 test for it.
+ * Queue later validation of this constraint, if necessary and
+ * requested by caller.
*/
- if (wqueue && !NotNullImpliedByRelConstraints(rel, attr))
+ if (wqueue &&
+ newvalue == ATTRIBUTE_NOTNULL_TRUE &&
+ !NotNullImpliedByRelConstraints(rel, attr))
{
AlteredTableInfo *tab;
@@ -7884,6 +7892,15 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
conForm->conislocal = true;
changed = true;
}
+ else if (!conForm->convalidated)
+ {
+ /*
+ * flip attnotnull and convalidated, and also validate the
+ * constraint.
+ */
+ return ATExecValidateConstraint(wqueue, rel, conForm->conname.data,
+ recurse, recursing, lockmode);
+ }
if (changed)
{
@@ -7946,8 +7963,12 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel), attnum);
- /* Mark pg_attribute.attnotnull for the column */
- set_attnotnull(wqueue, rel, attnum, lockmode);
+ /* Mark pg_attribute.attnotnull for the column and request validation */
+ set_attnotnull(wqueue, rel, attnum,
+ constraint->skip_validation ?
+ ATTRIBUTE_NOTNULL_INVALID :
+ ATTRIBUTE_NOTNULL_TRUE,
+ lockmode);
/*
* Recurse to propagate the constraint to children that don't have one.
@@ -9387,12 +9408,19 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
}
/* Insert not-null constraints in the queue for the PK columns */
- foreach(lc, pkconstr->keys)
+ foreach_node(String, colname, pkconstr->keys)
{
AlterTableCmd *newcmd;
Constraint *nnconstr;
- nnconstr = makeNotNullConstraint(lfirst(lc));
+ /* Verify that the not-null constraint has been validated */
+ if (check_for_invalid_notnull(RelationGetRelid(rel), strVal(colname)))
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("column \"%s\" of table \"%s\" is marked as NOT VALID NOT NULL constraint",
+ strVal(colname), RelationGetRelationName(rel)));
+
+ nnconstr = makeNotNullConstraint(colname);
newcmd = makeNode(AlterTableCmd);
newcmd->subtype = AT_AddConstraint;
@@ -9403,6 +9431,30 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
}
}
+/*
+ * This function checks for any invalid NOT NULL constraint on the given
+ * relation and attribute name. It returns true if found, false otherwise.
+ */
+static bool
+check_for_invalid_notnull(Oid relid, const char *attname)
+{
+ HeapTuple tuple;
+ bool retval = false;
+
+ tuple = SearchSysCache2(ATTNAME,
+ ObjectIdGetDatum(relid),
+ CStringGetDatum(attname));
+ if (!HeapTupleIsValid(tuple))
+ return false;
+ if (!((Form_pg_attribute) GETSTRUCT(tuple))->attisdropped &&
+ ((Form_pg_attribute) GETSTRUCT(tuple))->attnotnull == ATTRIBUTE_NOTNULL_INVALID)
+ retval = true;
+
+ ReleaseSysCache(tuple);
+
+ return retval;
+}
+
/*
* ALTER TABLE ADD INDEX
*
@@ -9770,7 +9822,11 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* phase 3 to verify existing rows, if needed.
*/
if (constr->contype == CONSTR_NOTNULL)
- set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
+ set_attnotnull(wqueue, rel, ccon->attnum,
+ ccon->skip_validation ?
+ ATTRIBUTE_NOTNULL_INVALID :
+ ATTRIBUTE_NOTNULL_TRUE,
+ lockmode);
ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
}
@@ -12371,10 +12427,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
con = (Form_pg_constraint) GETSTRUCT(tuple);
if (con->contype != CONSTRAINT_FOREIGN &&
- con->contype != CONSTRAINT_CHECK)
+ con->contype != CONSTRAINT_CHECK &&
+ con->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
+ errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key, not-null, or check constraint",
constrName, RelationGetRelationName(rel))));
if (!con->conenforced)
@@ -12393,6 +12450,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
QueueCheckConstraintValidation(wqueue, conrel, rel, constrName,
tuple, recurse, recursing, lockmode);
}
+ else if (con->contype == CONSTRAINT_NOTNULL)
+ {
+ QueueNNConstraintValidation(wqueue, conrel, rel,
+ tuple, recurse, recursing, lockmode);
+ }
ObjectAddressSet(address, ConstraintRelationId, con->oid);
}
@@ -12522,9 +12584,7 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
AlteredTableInfo *tab;
HeapTuple copyTuple;
Form_pg_constraint copy_con;
-
List *children = NIL;
- ListCell *child;
NewConstraint *newcon;
Datum val;
char *conbin;
@@ -12533,24 +12593,19 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
Assert(con->contype == CONSTRAINT_CHECK);
/*
- * If we're recursing, the parent has already done this, so skip it. Also,
- * if the constraint is a NO INHERIT constraint, we shouldn't try to look
- * for it in the children.
- */
- if (!recursing && !con->connoinherit)
- children = find_all_inheritors(RelationGetRelid(rel),
- lockmode, NULL);
-
- /*
- * For CHECK constraints, we must ensure that we only mark the constraint
- * as validated on the parent if it's already validated on the children.
+ * For constraints that aren't NO INHERIT, we must ensure that we only
+ * mark the constraint as validated on the parent if it's already
+ * validated on the children.
+ *
+ * If we're recursing, the parent has already done this, so skip it.
*
* We recurse before validating on the parent, to reduce risk of
* deadlocks.
*/
- foreach(child, children)
+ if (!recursing && !con->connoinherit)
+ children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+ foreach_oid(childoid, children)
{
- Oid childoid = lfirst_oid(child);
Relation childrel;
if (childoid == RelationGetRelid(rel))
@@ -12609,6 +12664,114 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
heap_freetuple(copyTuple);
}
+/*
+ * QueueNNConstraintValidation
+ *
+ * Add an entry to the wqueue to validate the given not-null constraint in
+ * Phase 3 and update the convalidated field in the pg_constraint catalog for
+ * the specified relation and all its inheriting children.
+ */
+static void
+QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple, bool recurse, bool recursing,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint con;
+ AlteredTableInfo *tab;
+ HeapTuple copyTuple;
+ Form_pg_constraint copy_con;
+ List *children = NIL;
+ AttrNumber attnum;
+ char *colname;
+
+ con = (Form_pg_constraint) GETSTRUCT(contuple);
+ Assert(con->contype == CONSTRAINT_NOTNULL);
+
+ attnum = extractNotNullColumn(contuple);
+
+ /*
+ * For constraints that aren't NO INHERIT, we must ensure that we only
+ * mark the constraint as validated on the parent if it's already
+ * validated on the children.
+ *
+ * If we're recursing, the parent has already done this, so skip it.
+ *
+ * We recurse before validating on the parent, to reduce risk of
+ * deadlocks.
+ */
+ if (!recursing && !con->connoinherit)
+ children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+
+ colname = get_attname(RelationGetRelid(rel), attnum, false);
+ foreach_oid(childoid, children)
+ {
+ Relation childrel;
+ HeapTuple contup;
+ Form_pg_constraint childcon;
+ char *conname;
+
+ if (childoid == RelationGetRelid(rel))
+ continue;
+
+ /*
+ * If we are told not to recurse, there had better not be any child
+ * tables, because we can't mark the constraint on the parent valid
+ * unless it is valid for all child tables.
+ */
+ if (!recurse)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+ errmsg("constraint must be validated on child tables too"));
+
+ /*
+ * The column on child might have a different attnum, so search by
+ * column name.
+ */
+ contup = findNotNullConstraint(childoid, colname);
+ if (!contup)
+ elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
+ colname, get_rel_name(childoid));
+ childcon = (Form_pg_constraint) GETSTRUCT(contup);
+ if (childcon->convalidated)
+ continue;
+
+ /* find_all_inheritors already got lock */
+ childrel = table_open(childoid, NoLock);
+ conname = pstrdup(NameStr(childcon->conname));
+
+ /* XXX improve ATExecValidateConstraint API to avoid double search */
+ ATExecValidateConstraint(wqueue, childrel, conname,
+ false, true, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->verify_new_notnull = true;
+
+ /*
+ * Invalidate relcache so that others see the new validated constraint.
+ */
+ CacheInvalidateRelcache(rel);
+
+ /*
+ * Now update the catalogs, while we have the door open.
+ */
+ copyTuple = heap_copytuple(contuple);
+ copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+ copy_con->convalidated = true;
+ CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
+
+ /*
+ * Also flip attnotnull. call function with wqueue as NULL to
+ * bypass validation, as it has already been performed.
+ */
+ set_attnotnull(NULL, rel, attnum, ATTRIBUTE_NOTNULL_TRUE, lockmode);
+
+ InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
+
+ heap_freetuple(copyTuple);
+}
+
/*
* transformColumnNameList - transform list of column names
*
@@ -13480,7 +13643,7 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
RelationGetRelationName(rel)));
/* All good -- reset attnotnull if needed */
- if (attForm->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
+ if (attForm->attnotnull != ATTRIBUTE_NOTNULL_FALSE)
{
attForm->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
CatalogTupleUpdate(attrel, &atttup->t_self, atttup);
@@ -16816,12 +16979,20 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
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)));
+ if (HeapTupleIsValid(contup))
+ {
+ Form_pg_constraint childcon;
+
+ childcon = (Form_pg_constraint) GETSTRUCT(contup);
+ if (!childcon->connoinherit)
+ ereport(ERROR,
+ errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL",
+ parent_attname, RelationGetRelationName(child_rel)));
+
+ if (!childcon->convalidated)
+ elog(WARNING, "found an invalid constraint");
+ }
}
/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index d582231ab60..4b0446b00d8 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -2070,7 +2070,8 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
{
Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
- if (att->attnotnull == ATTRIBUTE_NOTNULL_TRUE &&
+ if ((att->attnotnull == ATTRIBUTE_NOTNULL_TRUE ||
+ att->attnotnull == ATTRIBUTE_NOTNULL_INVALID) &&
slot_attisnull(slot, attrChk))
{
char *val_desc;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..715eb51300b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4224,9 +4224,9 @@ ConstraintElem:
n->keys = list_make1(makeString($3));
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
- NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
- n->initially_valid = true;
+ n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
| UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c371570501a..4a5299d7c5c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8876,6 +8876,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQExpBuffer q = createPQExpBuffer();
PQExpBuffer tbloids = createPQExpBuffer();
PQExpBuffer checkoids = createPQExpBuffer();
+ PQExpBuffer invalidnotnulloids = createPQExpBuffer();
PGresult *res;
int ntups;
int curtblindx;
@@ -8892,6 +8893,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
int i_attlen;
int i_attalign;
int i_attislocal;
+ int i_notnull_valid;
int i_notnull_name;
int i_notnull_noinherit;
int i_notnull_islocal;
@@ -8988,11 +8990,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
*/
if (fout->remoteVersion >= 180000)
appendPQExpBufferStr(q,
+ "CASE WHEN a.attnotnull = 'i' THEN 'f' ELSE 't' END AS notnull_valid,\n"
"co.conname AS notnull_name,\n"
"co.connoinherit AS notnull_noinherit,\n"
"co.conislocal AS notnull_islocal,\n");
else
appendPQExpBufferStr(q,
+ "'t' AS notnull_valid,\n"
"CASE WHEN a.attnotnull THEN '' ELSE NULL END AS notnull_name,\n"
"false AS notnull_noinherit,\n"
"a.attislocal AS notnull_islocal,\n");
@@ -9067,6 +9071,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
i_attlen = PQfnumber(res, "attlen");
i_attalign = PQfnumber(res, "attalign");
i_attislocal = PQfnumber(res, "attislocal");
+ i_notnull_valid = PQfnumber(res, "notnull_valid");
i_notnull_name = PQfnumber(res, "notnull_name");
i_notnull_noinherit = PQfnumber(res, "notnull_noinherit");
i_notnull_islocal = PQfnumber(res, "notnull_islocal");
@@ -9086,6 +9091,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
* r is handled by the inner loop.
*/
curtblindx = -1;
+ appendPQExpBufferChar(invalidnotnulloids, '{');
for (int r = 0; r < ntups;)
{
Oid attrelid = atooid(PQgetvalue(res, r, i_attrelid));
@@ -9134,6 +9140,7 @@ 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_valid = (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));
@@ -9160,12 +9167,29 @@ 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_valid[j] = (PQgetvalue(res, r, i_notnull_valid)[0] == 't');
- /* Handle not-null constraint name and flags */
- determineNotNullFlags(fout, res, r,
- tbinfo, j,
- i_notnull_name, i_notnull_noinherit,
- i_notnull_islocal);
+ /*
+ * Dump the invalid NOT NULL constraint like the Check constraints
+ */
+ if (tbinfo->notnull_valid[j])
+ /* Handle not-null constraint name and flags */
+ determineNotNullFlags(fout, res, r,
+ tbinfo, j,
+ i_notnull_name, i_notnull_noinherit,
+ i_notnull_islocal);
+ else if (!PQgetisnull(res, r, i_notnull_name))
+ {
+ /*
+ * Add the entry into invalidnotnull list so NOT NULL
+ * constraint get dump as separate constraints.
+ */
+ if (invalidnotnulloids->len > 1) /* do we have more than
+ * the '{'? */
+ appendPQExpBufferChar(invalidnotnulloids, ',');
+ appendPQExpBuffer(invalidnotnulloids, "%u", tbinfo->dobj.catId.oid);
+
+ }
tbinfo->attoptions[j] = pg_strdup(PQgetvalue(res, r, i_attoptions));
tbinfo->attcollation[j] = atooid(PQgetvalue(res, r, i_attcollation));
@@ -9187,6 +9211,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
}
PQclear(res);
+ appendPQExpBufferChar(invalidnotnulloids, '}');
/*
* Now get info about column defaults. This is skipped for a data-only
@@ -9450,6 +9475,128 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQclear(res);
}
+ /*
+ * Get info about table INVALID NOT NULL constraints. This is skipped for
+ * a data-only dump, as it is only needed for table schemas.
+ */
+ if (dopt->dumpSchema && invalidnotnulloids->len > 2)
+ {
+ ConstraintInfo *constrs;
+ int numConstrs;
+ int i_tableoid;
+ int i_oid;
+ int i_conrelid;
+ int i_conname;
+ int i_consrc;
+ int i_conislocal;
+ int i_convalidated;
+
+ pg_log_info("finding table invalid not null constraints");
+
+ resetPQExpBuffer(q);
+ appendPQExpBuffer(q,
+ "SELECT c.tableoid, c.oid, conrelid, conname, "
+ "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
+ "conislocal, convalidated "
+ "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+ "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
+ "WHERE contype = 'n' AND convalidated = 'f'"
+ "ORDER BY c.conrelid, c.conname",
+ invalidnotnulloids->data);
+
+ res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
+
+ numConstrs = PQntuples(res);
+ constrs = (ConstraintInfo *) pg_malloc(numConstrs * sizeof(ConstraintInfo));
+
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_oid = PQfnumber(res, "oid");
+ i_conrelid = PQfnumber(res, "conrelid");
+ i_conname = PQfnumber(res, "conname");
+ i_consrc = PQfnumber(res, "consrc");
+ i_conislocal = PQfnumber(res, "conislocal");
+ i_convalidated = PQfnumber(res, "convalidated");
+
+ /* As above, this loop iterates once per table, not once per row */
+ curtblindx = -1;
+ for (int j = 0; j < numConstrs;)
+ {
+ Oid conrelid = atooid(PQgetvalue(res, j, i_conrelid));
+ TableInfo *tbinfo = NULL;
+ int numcons;
+
+ /* Count rows for this table */
+ for (numcons = 1; numcons < numConstrs - j; numcons++)
+ if (atooid(PQgetvalue(res, j + numcons, i_conrelid)) != conrelid)
+ break;
+
+ /*
+ * Locate the associated TableInfo; we rely on tblinfo[] being in
+ * OID order.
+ */
+ while (++curtblindx < numTables)
+ {
+ tbinfo = &tblinfo[curtblindx];
+ if (tbinfo->dobj.catId.oid == conrelid)
+ break;
+ }
+ if (curtblindx >= numTables)
+ pg_fatal("unrecognized table OID %u", conrelid);
+
+
+ tbinfo->checkexprs = constrs + j;
+
+ for (int c = 0; c < numcons; c++, j++)
+ {
+ bool validated = PQgetvalue(res, j, i_convalidated)[0] == 't';
+
+ constrs[j].dobj.objType = DO_CONSTRAINT;
+ constrs[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
+ constrs[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&constrs[j].dobj);
+ constrs[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_conname));
+ constrs[j].dobj.namespace = tbinfo->dobj.namespace;
+ constrs[j].contable = tbinfo;
+ constrs[j].condomain = NULL;
+ constrs[j].contype = 'n';
+ constrs[j].condef = pg_strdup(PQgetvalue(res, j, i_consrc));
+ constrs[j].confrelid = InvalidOid;
+ constrs[j].conindex = 0;
+ constrs[j].condeferrable = false;
+ constrs[j].condeferred = false;
+ constrs[j].conislocal = (PQgetvalue(res, j, i_conislocal)[0] == 't');
+
+ /*
+ * An unvalidated constraint needs to be dumped separately, so
+ * that potentially-violating existing data is loaded before
+ * the constraint.
+ */
+ constrs[j].separate = !validated;
+
+ constrs[j].dobj.dump = tbinfo->dobj.dump;
+
+ /*
+ * Mark the constraint as needing to appear before the table
+ * --- this is so that any other dependencies of the
+ * constraint will be emitted before we try to create the
+ * table. If the constraint is to be dumped separately, it
+ * will be dumped after data is loaded anyway, so don't do it.
+ * (There's an automatic dependency in the opposite direction
+ * anyway, so don't need to add one manually here.)
+ */
+ if (!constrs[j].separate)
+ addObjectDependency(&tbinfo->dobj,
+ constrs[j].dobj.dumpId);
+
+ /*
+ * We will detect later whether the constraint must be split
+ * out from the table definition.
+ */
+ }
+ }
+ PQclear(res);
+ }
+
destroyPQExpBuffer(q);
destroyPQExpBuffer(tbloids);
destroyPQExpBuffer(checkoids);
@@ -16543,7 +16690,8 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
* defined, or if binary upgrade. (In the latter case, we
* reset conislocal below.)
*/
- print_notnull = (tbinfo->notnull_constrs[j] != NULL &&
+ print_notnull = (tbinfo->notnull_valid[j] &&
+ tbinfo->notnull_constrs[j] != NULL &&
(tbinfo->notnull_islocal[j] ||
dopt->binary_upgrade ||
tbinfo->ispartition));
@@ -16605,11 +16753,6 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
tbinfo->attrdefs[j]->adef_expr);
}
- print_notnull = (tbinfo->notnull_constrs[j] != NULL &&
- (tbinfo->notnull_islocal[j] ||
- dopt->binary_upgrade ||
- tbinfo->ispartition));
-
if (print_notnull)
{
if (tbinfo->notnull_constrs[j][0] == '\0')
@@ -16928,7 +17071,8 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
* because the latter is not available. (We distinguish the
* case because the constraint name is the empty string.)
*/
- if (tbinfo->notnull_constrs[j] != NULL &&
+ if (tbinfo->notnull_valid[j] &&
+ tbinfo->notnull_constrs[j] != NULL &&
!tbinfo->notnull_islocal[j])
{
if (tbinfo->notnull_constrs[j][0] != '\0')
@@ -17931,9 +18075,9 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
.createStmt = q->data,
.dropStmt = delq->data));
}
- else if (coninfo->contype == 'c' && tbinfo)
+ else if ((coninfo->contype == 'c' || coninfo->contype == 'n') && tbinfo)
{
- /* CHECK constraint on a table */
+ /* CHECK/INVALID_NOTNULL constraint on a table */
/* Ignore if not to be dumped separately, or if it was inherited */
if (coninfo->separate && coninfo->conislocal)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bbdb30b5f54..90335d2bb0b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -361,6 +361,7 @@ typedef struct _tableInfo
char *attcompression; /* per-attribute compression method */
char **attfdwoptions; /* per-attribute fdw options */
char **attmissingval; /* per attribute missing value */
+ bool *notnull_valid; /* NOT NULL status */
char **notnull_constrs; /* NOT NULL constraint names. If null,
* there isn't one on this column. If
* empty string, unnamed constraint
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index c7bffc1b045..5b5b756e512 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1099,6 +1099,23 @@ my %tests = (
},
},
+ 'CONSTRAINT NOT NULL / INVALID' => {
+ create_sql => 'CREATE TABLE dump_test.test_table_nn (
+ col1 int);
+ ALTER TABLE dump_test.test_table_nn ADD CONSTRAINT nn NOT NULL col1 NOT VALID;',
+ regexp => qr/^
+ \QALTER TABLE dump_test.test_table_nn\E \n^\s+
+ \QADD CONSTRAINT nn NOT NULL col1 NOT VALID;\E
+ /xm,
+ like => {
+ %full_runs, %dump_test_schema_runs, section_post_data => 1,
+ },
+ unlike => {
+ exclude_dump_test_schema => 1,
+ only_dump_measurement => 1,
+ },
+ },
+
'CONSTRAINT PRIMARY KEY / WITHOUT OVERLAPS' => {
create_sql => 'CREATE TABLE dump_test.test_table_tpk (
col1 int4range,
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..f48b24ed38c 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2107,7 +2107,7 @@ describeOneTableDetails(const char *schemaname,
printTableAddCell(&cont, PQgetvalue(res, i, attcoll_col), false, false);
printTableAddCell(&cont,
- strcmp(PQgetvalue(res, i, attnotnull_col), "t") == 0 ? "not null" : "",
+ strcmp(PQgetvalue(res, i, attnotnull_col), "f") == 0 ? "" : "not null",
false, false);
identity = PQgetvalue(res, i, attidentity_col);
@@ -3114,7 +3114,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.conname, a.attname, c.connoinherit,\n"
- " c.conislocal, c.coninhcount <> 0\n"
+ " c.conislocal, c.coninhcount <> 0, c.convalidated \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"
@@ -3138,13 +3138,15 @@ describeOneTableDetails(const char *schemaname,
bool islocal = PQgetvalue(result, i, 3)[0] == 't';
bool inherited = PQgetvalue(result, i, 4)[0] == 't';
- printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s",
+ printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s%s",
PQgetvalue(result, i, 0),
PQgetvalue(result, i, 1),
PQgetvalue(result, i, 2)[0] == 't' ?
" NO INHERIT" :
islocal && inherited ? _(" (local, inherited)") :
- inherited ? _(" (inherited)") : "");
+ inherited ? _(" (inherited)") : "",
+ PQgetvalue(result, i, 5)[0] == 'f' ?
+ " NOT VALID " : "");
printTableAddFooter(&cont, buf.data);
}
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index b51a2670958..a01a0fa96d2 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -229,6 +229,7 @@ MAKE_SYSCACHE(ATTNUM, pg_attribute_relid_attnum_index, 128);
#define ATTRIBUTE_NOTNULL_TRUE 't'
#define ATTRIBUTE_NOTNULL_FALSE 'f'
+#define ATTRIBUTE_NOTNULL_INVALID 'i'
#endif /* EXPOSE_TO_CLIENT_CODE */
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 6da164e7e4d..0e01ff1bbbd 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -264,7 +264,8 @@ 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,
- bool is_local, bool is_no_inherit);
+ const char *conname, bool is_local,
+ bool is_no_inherit);
extern List *RelationGetNotNullConstraints(Oid relid, bool cooked,
bool include_noinh);
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..b52578bf8d3 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -896,6 +896,159 @@ Not-null constraints:
"foobar" NOT NULL "a"
DROP TABLE notnull_tbl1;
+-- verify NOT NULL VALID/NOT VALID
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1);
+INSERT INTO notnull_tbl1 VALUES (NULL, 2);
+INSERT INTO notnull_tbl1 VALUES (300, 3);
+-- Below statement should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID;
+\d+ notnull_tbl1
+ Table "public.notnull_tbl1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | not null | | plain | |
+ b | integer | | | | plain | |
+Not-null constraints:
+ "nn" NOT NULL "a" NOT VALID
+
+-- Try to insert new record with NULL, should throw an error
+INSERT INTO notnull_tbl1 VALUES (NULL, 4);
+ERROR: null value in column "a" of relation "notnull_tbl1" violates not-null constraint
+DETAIL: Failing row contains (null, 4).
+-- SELECT NULL values for COLUMN a, should return 2 records.
+SELECT * FROM notnull_tbl1 WHERE a is NULL;
+ a | b
+---+---
+ | 1
+ | 2
+(2 rows)
+
+-- UPDATE the one of the NULL values
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+-- DELETE the record (NULL, 2)
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1;
+ a | b
+-----+---
+ 300 | 3
+ 100 | 1
+(2 rows)
+
+-- Try to add primary key on table column marked as NOT VALID NOT NULL
+-- constraint. This should throw an error.
+ALTER TABLE notnull_tbl1 add primary key (a);
+ERROR: column "a" of table "notnull_tbl1" is marked as NOT VALID NOT NULL constraint
+-- INHERITS table having NOT VALID NOT NULL constraints.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+NOTICE: merging column "a" with inherited definition
+NOTICE: merging column "b" with inherited definition
+-- Child table NOT NULL constraints should be valid.
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid = 'notnull_tbl1_child'::regclass;
+ conname | convalidated
+---------+--------------
+ nn | t
+(1 row)
+
+DROP TABLE notnull_tbl1_child;
+ALTER TABLE notnull_tbl1 validate constraint nn;
+\d+ notnull_tbl1
+ Table "public.notnull_tbl1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | not null | | plain | |
+ b | integer | | | | plain | |
+Not-null constraints:
+ "nn" NOT NULL "a"
+
+DROP TABLE notnull_tbl1;
+-- Test the different Not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent not null a not valid;
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child not null a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+-- This statement should validate not null constraint for parent as well as
+-- child.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
+in ('notnull_tbl1'::regclass, 'notnull_chld'::regclass);
+ conname | convalidated
+-----------+--------------
+ nn_parent | t
+ nn_child | t
+(2 rows)
+
+DROP TABLE notnull_tbl1 CASCADE;
+NOTICE: drop cascades to table notnull_chld
+-- test to throw an error when trying to add another NOT NULL
+-- on the table column with INVALID NOT NULL constraint.
+CREATE TABLE notnull_tbl1 (a INTEGER);
+INSERT INTO notnull_tbl1 VALUES ( NULL );
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid;
+-- Should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+ERROR: Invalid NOT NULL constraint "nn1" exist on relation "notnull_tbl1"
+HINT: Do VALIDATE CONSTRAINT
+-- This should throw an error while validating the NOT NULL
+-- constraint.
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+-- After deleting null values, SET NOT NULL should work
+DELETE FROM notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
+in ('notnull_tbl1'::regclass);
+ conname | convalidated
+---------+--------------
+ nn1 | t
+(1 row)
+
+DROP TABLE notnull_tbl1;
+-- Test invalid not null on inheritance table.
+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;
+NOTICE: merging definition of column "i" for child "inh_child"
+NOTICE: merging definition of column "i" for child "inh_grandchild"
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ALTER i SET NOT NULL;
+SELECT conrelid::regclass, conname, convalidated, coninhcount FROM pg_catalog.pg_constraint WHERE conrelid
+in ('inh_parent'::regclass, 'inh_child'::regclass, 'inh_grandchild'::regclass) ORDER BY 1;
+ conrelid | conname | convalidated | coninhcount
+----------------+---------+--------------+-------------
+ inh_parent | nn | t | 0
+ inh_child | nn | t | 1
+ inh_grandchild | nn | t | 2
+(3 rows)
+
+drop table inh_parent, inh_child, inh_grandchild;
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 2);
+INSERT INTO notnull_tbl1_upg VALUES (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY RANGE (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID;
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES FROM (0) TO (10);
+CREATE TABLE notnull_tbl1_2 PARTITION OF notnull_tbl1 FOR VALUES FROM (20) TO (30);
+-- Parent table NOT NULL constraints will be marked as validated false, where
+-- for child table it will be true
+SELECT conrelid::regclass, conname, convalidated FROM pg_catalog.pg_constraint WHERE
+conrelid IN ('notnull_tbl1'::regclass, 'notnull_tbl1_1'::regclass, 'notnull_tbl1_2'::regclass);
+ conrelid | conname | convalidated
+----------------+-------------+--------------
+ notnull_tbl1 | notnull_con | f
+ notnull_tbl1_1 | notnull_con | t
+ notnull_tbl1_2 | notnull_con | t
+(3 rows)
+
+DROP TABLE notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2;
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 21ce4177de4..7aff17e6764 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -640,6 +640,91 @@ ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a;
\d+ notnull_tbl1
DROP TABLE notnull_tbl1;
+-- verify NOT NULL VALID/NOT VALID
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1);
+INSERT INTO notnull_tbl1 VALUES (NULL, 2);
+INSERT INTO notnull_tbl1 VALUES (300, 3);
+-- Below statement should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID;
+\d+ notnull_tbl1
+-- Try to insert new record with NULL, should throw an error
+INSERT INTO notnull_tbl1 VALUES (NULL, 4);
+-- SELECT NULL values for COLUMN a, should return 2 records.
+SELECT * FROM notnull_tbl1 WHERE a is NULL;
+-- UPDATE the one of the NULL values
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+-- DELETE the record (NULL, 2)
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1;
+-- Try to add primary key on table column marked as NOT VALID NOT NULL
+-- constraint. This should throw an error.
+ALTER TABLE notnull_tbl1 add primary key (a);
+-- INHERITS table having NOT VALID NOT NULL constraints.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+-- Child table NOT NULL constraints should be valid.
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid = 'notnull_tbl1_child'::regclass;
+DROP TABLE notnull_tbl1_child;
+ALTER TABLE notnull_tbl1 validate constraint nn;
+\d+ notnull_tbl1
+DROP TABLE notnull_tbl1;
+-- Test the different Not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent not null a not valid;
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child not null a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+-- This statement should validate not null constraint for parent as well as
+-- child.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
+in ('notnull_tbl1'::regclass, 'notnull_chld'::regclass);
+DROP TABLE notnull_tbl1 CASCADE;
+-- test to throw an error when trying to add another NOT NULL
+-- on the table column with INVALID NOT NULL constraint.
+CREATE TABLE notnull_tbl1 (a INTEGER);
+INSERT INTO notnull_tbl1 VALUES ( NULL );
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid;
+-- Should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+-- This should throw an error while validating the NOT NULL
+-- constraint.
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+-- After deleting null values, SET NOT NULL should work
+DELETE FROM notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
+in ('notnull_tbl1'::regclass);
+DROP TABLE notnull_tbl1;
+-- Test invalid not null on inheritance table.
+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;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ALTER i SET NOT NULL;
+SELECT conrelid::regclass, conname, convalidated, coninhcount FROM pg_catalog.pg_constraint WHERE conrelid
+in ('inh_parent'::regclass, 'inh_child'::regclass, 'inh_grandchild'::regclass) ORDER BY 1;
+drop table inh_parent, inh_child, inh_grandchild;
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 2);
+INSERT INTO notnull_tbl1_upg VALUES (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY RANGE (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID;
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES FROM (0) TO (10);
+CREATE TABLE notnull_tbl1_2 PARTITION OF notnull_tbl1 FOR VALUES FROM (20) TO (30);
+-- Parent table NOT NULL constraints will be marked as validated false, where
+-- for child table it will be true
+SELECT conrelid::regclass, conname, convalidated FROM pg_catalog.pg_constraint WHERE
+conrelid IN ('notnull_tbl1'::regclass, 'notnull_tbl1_1'::regclass, 'notnull_tbl1_2'::regclass);
+DROP TABLE notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2;
+
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
--
2.43.0
[application/octet-stream] 0003-Documentation-changes.patch (3.0K, ../../CAGPqQf2so0xrznT9sFjFFROnDgpbnAQbs2vg71QdC295afjm5Q@mail.gmail.com/5-0003-Documentation-changes.patch)
download | inline diff:
From d6fd85b2142a6ba30da03b85b93ef3563e92eec8 Mon Sep 17 00:00:00 2001
From: Rushabh Lathia <[email protected]>
Date: Thu, 27 Feb 2025 15:22:56 +0530
Subject: [PATCH 3/3] Documentation changes
---
doc/src/sgml/catalogs.sgml | 5 +++--
doc/src/sgml/ref/alter_table.sgml | 4 ++--
src/bin/psql/tab-complete.in.c | 2 +-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..d6066d7dba1 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1257,10 +1257,11 @@
<row>
<entry role="catalog_table_entry"><para role="column_definition">
- <structfield>attnotnull</structfield> <type>bool</type>
+ <structfield>attnotnull</structfield> <type>char</type>
</para>
<para>
- This column has a not-null constraint.
+ The value 't' indicates that a not-null constraint exists for the
+ column, 'i' for an invalid constraint and 'f' for none.
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index dceb7a7593c..288cd3339f8 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -99,7 +99,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 [ NO INHERIT ] |
+{ NOT NULL [ NOT VALID ] [ NO INHERIT ] |
NULL |
CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
DEFAULT <replaceable>default_expr</replaceable> |
@@ -458,7 +458,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<para>
This form adds a new constraint to a table using the same constraint
syntax as <link linkend="sql-createtable"><command>CREATE TABLE</command></link>, plus the option <literal>NOT
- VALID</literal>, which is currently only allowed for foreign key
+ VALID</literal>, which is currently only allowed for foreign key, NOT NULL constraints
and CHECK constraints.
</para>
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8432be641ac..1c3295d949a 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2728,7 +2728,7 @@ match_previous_words(int pattern_id,
COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes);
/* ALTER TABLE xxx ADD CONSTRAINT yyy */
else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny))
- COMPLETE_WITH("CHECK", "UNIQUE", "PRIMARY KEY", "EXCLUDE", "FOREIGN KEY");
+ COMPLETE_WITH("CHECK", "UNIQUE", "PRIMARY KEY", "EXCLUDE", "FOREIGN KEY", "NOT NULL");
/* ALTER TABLE xxx ADD [CONSTRAINT yyy] (PRIMARY KEY|UNIQUE) */
else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "PRIMARY", "KEY") ||
Matches("ALTER", "TABLE", MatchAny, "ADD", "UNIQUE") ||
--
2.43.0
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
@ 2025-03-12 11:51 ` Ashutosh Bapat <[email protected]>
2025-03-12 11:57 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
1 sibling, 1 reply; 40+ messages in thread
From: Ashutosh Bapat @ 2025-03-12 11:51 UTC (permalink / raw)
To: Rushabh Lathia <[email protected]>; +Cc: Alvaro Herrera <[email protected]>; pgsql-hackers
On Wed, Mar 12, 2025 at 3:52 PM Rushabh Lathia <[email protected]> wrote:
>
>
> Hi Alvaro,
>
> Here are the latest patches, which includes the regression fix.
>
The 002_pg_upgrade test passes with and without my patches now. But
then the tests added here do not leave behind any parent-child table.
Previously we have found problems in dumping and restoring constraints
in an inheritance hierarchy. I think the test should leave behind all
the combinations of parent and child NOT NULL constraints so that
0002_pg_upgrade can test those.
We should add more scenarios for constraint inheritance. E.g.
#CREATE TABLE notnull_tbl1 (a int);
#ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent not null a not valid;
#CREATE TABLE notnull_chld (a int);
#ALTER TABLE notnull_chld ADD CONSTRAINT nn_child not null a;
#ALTER TABLE notnull_chld INHERIT notnull_tbl1;
#SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
in ('notnull_tbl1'::regclass, 'notnull_chld'::regclass);
conname | convalidated
-----------+--------------
nn_parent | f
nn_child | t
(2 rows)
Is it expected that a child may have VALID constraint but parent has
not valid constraint?
Same case with partitioned table. We should leave partitioned table
hierarchy behind for 002_pg_upgrade to test. And we need tests to test
scenarios where a partitioned table has valid constraint but we try to
change constraint on a partition to not valid and vice versa. I think
we shouldn't allow such assymetry in partitioned table hierarchy and
having a test would be better.
--
Best Wishes,
Ashutosh Bapat
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 11:51 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
@ 2025-03-12 11:57 ` Alvaro Herrera <[email protected]>
0 siblings, 0 replies; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-12 11:57 UTC (permalink / raw)
To: Ashutosh Bapat <[email protected]>; +Cc: Rushabh Lathia <[email protected]>; pgsql-hackers
On 2025-Mar-12, Ashutosh Bapat wrote:
> The 002_pg_upgrade test passes with and without my patches now. But
> then the tests added here do not leave behind any parent-child table.
> Previously we have found problems in dumping and restoring constraints
> in an inheritance hierarchy. I think the test should leave behind all
> the combinations of parent and child NOT NULL constraints so that
> 0002_pg_upgrade can test those.
I agree.
> Is it expected that a child may have VALID constraint but parent has
> not valid constraint?
Sure. Consider: if the parent has an unvalidated constraint, we cannot
make any assertions about the state of its children. The children may
have additional constraints of their own -- in this case, a child can
have a validated constraint even though the parent has none, or only an
unvalidatec constraint.
But the opposite is not allowed: if you know something to be true about
a parent table (to wit: that no row in it is NULL), then this must
necessarily apply to its children as well. Therefore, if there's a
valid constraint in the parent, then all children must have the same
constraint, and all such constraints must be known valid.
> Same case with partitioned table. We should leave partitioned table
> hierarchy behind for 002_pg_upgrade to test. And we need tests to test
> scenarios where a partitioned table has valid constraint but we try to
> change constraint on a partition to not valid and vice versa. I think
> we shouldn't allow such assymetry in partitioned table hierarchy and
> having a test would be better.
Agreed.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
@ 2025-03-12 18:20 ` Alvaro Herrera <[email protected]>
2025-03-13 08:37 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
1 sibling, 2 replies; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-12 18:20 UTC (permalink / raw)
To: Rushabh Lathia <[email protected]>; +Cc: Ashutosh Bapat <[email protected]>; pgsql-hackers
On 2025-Mar-12, Rushabh Lathia wrote:
> Hi Alvaro,
>
> Here are the latest patches, which includes the regression fix.
Thank you.
Taking a step back after discussing this with some colleagues, I need to
contradict what I said at the start of this thread. There's a worry
that changing pg_attribute.attnotnull in the way I initially suggested
might not be such a great idea after all. I did a quick search using
codesearch.debian.net for applications reading that column and thinking
about how they would react to this change; I think in the end it's going
to be quite disastrous. We would break a vast number of these apps, and
there are probably countless other apps and frameworks that we would
also break. Everybody would hate us forever. Upgrading to Postgres 18
would become as bad an experience as the drastic change of implicit
casts to text in 8.3. Nothing else in the intervening 17 years strikes
me as so problematic as this change would be.
So I think we may need to go back and somehow leave pg_attribute alone,
to avoid breaking the whole world. Maybe it would be sufficient to
change the CompactAttribute->attnotnull to no longer be a boolean, but
the new char representation instead. I'm not sure if this would
actually work.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-03-13 08:37 ` Rushabh Lathia <[email protected]>
2025-03-17 11:14 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
1 sibling, 1 reply; 40+ messages in thread
From: Rushabh Lathia @ 2025-03-13 08:37 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: Ashutosh Bapat <[email protected]>; pgsql-hackers
On Wed, Mar 12, 2025 at 11:50 PM Alvaro Herrera <[email protected]>
wrote:
> On 2025-Mar-12, Rushabh Lathia wrote:
>
> > Hi Alvaro,
> >
> > Here are the latest patches, which includes the regression fix.
>
> Thank you.
>
> Taking a step back after discussing this with some colleagues, I need to
> contradict what I said at the start of this thread. There's a worry
> that changing pg_attribute.attnotnull in the way I initially suggested
> might not be such a great idea after all. I did a quick search using
> codesearch.debian.net for applications reading that column and thinking
> about how they would react to this change; I think in the end it's going
> to be quite disastrous. We would break a vast number of these apps, and
> there are probably countless other apps and frameworks that we would
> also break. Everybody would hate us forever. Upgrading to Postgres 18
> would become as bad an experience as the drastic change of implicit
> casts to text in 8.3. Nothing else in the intervening 17 years strikes
> me as so problematic as this change would be.
>
> So I think we may need to go back and somehow leave pg_attribute alone,
> to avoid breaking the whole world. Maybe it would be sufficient to
> change the CompactAttribute->attnotnull to no longer be a boolean, but
> the new char representation instead. I'm not sure if this would
> actually work.
>
Thank you for your feedback. I understand that this change could be
problematic
for existing applications, as attnotnull is a highly generic catalog
column. I will
revisit the patch, make the necessary adjustments, and submit a revised
version
accordingly.
I appreciate your insights and will try to submit the new patch.
Regards,
Rushabh Lathia
www.EnterpriseDB.com
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-13 08:37 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
@ 2025-03-17 11:14 ` jian he <[email protected]>
2025-03-17 11:23 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: jian he @ 2025-03-17 11:14 UTC (permalink / raw)
To: Rushabh Lathia <[email protected]>; +Cc: Alvaro Herrera <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
hi.
I played around with it.
current syntax, we don't need to deal with column constraint grammar.
like the following can fail directly:
create table t0(a int constraint nn not null a not valid);
we only support table constraint cases like:
alter table lp add constraint nc1 not null a not valid;
since CREATE TABLE with invalid constraint does not make sense, so
we can just issue a warning. like:
create table t0(a int, constraint nn not null a not valid);
WARNING: CREATE TABLE NOT NULL NOT VALID CONSTRAINT WILL SET TO VALID
the wording needs to change...
for not null not valid syntax, we only need to support:
ALTER TABLE ADD CONSTRAINT conname NOT NULL column_name NOT VALID
ALTER TABLE ADD NOT NULL column_name NOT VALID
ALTER TABLE VALIDATE CONSTRAINT conname
The attached is what I came up with:
--------------------------------------------------------------------
ALTER TABLE ADD CONSTRAINT conname NOT NULL column_name NOT VALID
will create an invalidated check constraint. like
ALTER TABLE ADD CONSTRAINT conname CHECK (column_name IS NOT NULL) NOT VALID
when you validate the not-null constraint (internally it's a check constraint)
it will drop the check constraint and install a not-null constraint
with the same name.
drop a check constraint, it will call RemoveConstraintById.
within RemoveConstraintById it will lock pg_constraint.conrelid in
AccessExclusiveLock mode,
which is not ideal, because
ALTER TABLE VALIDATE CONSTRAINT only needs ShareUpdateExclusiveLock.
so we have to find a way to release that AccessExclusiveLock.
because we have converted a not-null constraint to a check constraint,
we need to somehow distinguish this case,
so pg_constraint adds another column: coninternaltype.
(the naming is not good, i guess)
because we dropped a invalid check constraint, but the inherited
constraint cannot be dropped.
so this ALTER TABLE VALIDATE CONSTRAINT will not work for partitions,
but it will work for the root partitioned table. (same logic for table
inheritance).
----------------------------------
demo:
create table t(a int);
alter table t add constraint nc1 not null a not valid;
\d t
Table "public.t"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | |
Check constraints:
"nc1" CHECK (a IS NOT NULL) NOT VALID
insert into t default values;
ERROR: new row for relation "t" violates check constraint "nc1"
DETAIL: Failing row contains (null).
alter table t validate constraint nc1;
\d+ t
Table "public.t"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
a | integer | | not null | | plain |
| |
Not-null constraints:
"nc1" NOT NULL "a"
Access method: heap
-------------------------------------------------------------------
some regress tests added.
need more polishing, but overall it works as the above described.
not sure if this idea is crazy or not,
what do you think?
Attachments:
[text/x-patch] v2-0001-not-null-not-valid-implementation.patch (25.2K, ../../CACJufxEOMEuG5gY-x9tHoa+BZx7_+rJnuh5eSWx3r2tBMThu3w@mail.gmail.com/2-v2-0001-not-null-not-valid-implementation.patch)
download | inline diff:
From 5a66a8da2a8c5fef88d7730e127900e5b83e0cde Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
Date: Sun, 16 Mar 2025 20:50:53 +0800
Subject: [PATCH v4 1/1] not null not valid implementation
---
src/backend/catalog/heap.c | 14 +-
src/backend/catalog/index.c | 1 +
src/backend/catalog/pg_constraint.c | 3 +
src/backend/commands/tablecmds.c | 168 +++++++++++++++++++++-
src/backend/commands/trigger.c | 1 +
src/backend/commands/typecmds.c | 2 +
src/backend/parser/gram.y | 5 +-
src/backend/parser/parse_utilcmd.c | 63 +++++++-
src/include/catalog/heap.h | 1 +
src/include/catalog/pg_constraint.h | 3 +
src/include/nodes/parsenodes.h | 1 +
src/test/regress/expected/constraints.out | 63 ++++++++
src/test/regress/sql/constraints.sql | 41 ++++++
13 files changed, 355 insertions(+), 11 deletions(-)
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..6dc0267bd5a 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -104,7 +104,7 @@ static ObjectAddress AddNewRelationType(const char *typeName,
static void RelationRemoveInheritance(Oid relid);
static Oid StoreRelCheck(Relation rel, const char *ccname, Node *expr,
bool is_enforced, bool is_validated, bool is_local,
- int16 inhcount, bool is_no_inherit, bool is_internal);
+ int16 inhcount, bool is_no_inherit, char coninternaltype, bool is_internal);
static void StoreConstraints(Relation rel, List *cooked_constraints,
bool is_internal);
static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
@@ -2136,7 +2136,7 @@ SetAttrMissing(Oid relid, char *attname, char *value)
static Oid
StoreRelCheck(Relation rel, const char *ccname, Node *expr,
bool is_enforced, bool is_validated, bool is_local,
- int16 inhcount, bool is_no_inherit, bool is_internal)
+ int16 inhcount, bool is_no_inherit, char coninternaltype, bool is_internal)
{
char *ccbin;
List *varList;
@@ -2228,6 +2228,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
inhcount, /* coninhcount */
is_no_inherit, /* connoinherit */
false, /* conperiod */
+ coninternaltype, /* coninternaltype */
is_internal); /* internally constructed? */
pfree(ccbin);
@@ -2282,6 +2283,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
inhcount,
is_no_inherit,
false,
+ '\0', /* coninternaltype */
false);
return constrOid;
}
@@ -2327,7 +2329,9 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
StoreRelCheck(rel, con->name, con->expr,
con->is_enforced, !con->skip_validation,
con->is_local, con->inhcount,
- con->is_no_inherit, is_internal);
+ con->is_no_inherit,
+ con->coninternaltype,
+ is_internal);
numchecks++;
break;
@@ -2460,6 +2464,7 @@ AddRelationNewConstraints(Relation rel,
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
cooked->is_no_inherit = false;
+ cooked->coninternaltype = '\0';
cookedConstraints = lappend(cookedConstraints, cooked);
}
@@ -2579,6 +2584,7 @@ AddRelationNewConstraints(Relation rel,
StoreRelCheck(rel, ccname, expr, cdef->is_enforced,
cdef->initially_valid, is_local,
is_local ? 0 : 1, cdef->is_no_inherit,
+ cdef->contype_internal,
is_internal);
numchecks++;
@@ -2594,6 +2600,7 @@ AddRelationNewConstraints(Relation rel,
cooked->is_local = is_local;
cooked->inhcount = is_local ? 0 : 1;
cooked->is_no_inherit = cdef->is_no_inherit;
+ cooked->coninternaltype = cdef->contype_internal;
cookedConstraints = lappend(cookedConstraints, cooked);
}
else if (cdef->contype == CONSTR_NOTNULL)
@@ -2670,6 +2677,7 @@ AddRelationNewConstraints(Relation rel,
nncooked->is_local = is_local;
nncooked->inhcount = inhcount;
nncooked->is_no_inherit = cdef->is_no_inherit;
+ nncooked->coninternaltype = '\0';
cookedConstraints = lappend(cookedConstraints, nncooked);
}
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 739a92bdcc1..e5bc679a850 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1986,6 +1986,7 @@ index_constraint_create(Relation heapRelation,
inhcount,
noinherit,
is_without_overlaps,
+ '\0', /* coninternaltype */
is_internal);
/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..3f3a276e3aa 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -80,6 +80,7 @@ CreateConstraintEntry(const char *constraintName,
int16 conInhCount,
bool conNoInherit,
bool conPeriod,
+ char coninternaltype,
bool is_internal)
{
Relation conDesc;
@@ -202,6 +203,7 @@ CreateConstraintEntry(const char *constraintName,
values[Anum_pg_constraint_coninhcount - 1] = Int16GetDatum(conInhCount);
values[Anum_pg_constraint_connoinherit - 1] = BoolGetDatum(conNoInherit);
values[Anum_pg_constraint_conperiod - 1] = BoolGetDatum(conPeriod);
+ values[Anum_pg_constraint_coninternaltype - 1] = CharGetDatum(coninternaltype);
if (conkeyArray)
values[Anum_pg_constraint_conkey - 1] = PointerGetDatum(conkeyArray);
@@ -834,6 +836,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
cooked->is_local = true;
cooked->inhcount = 0;
cooked->is_no_inherit = conForm->connoinherit;
+ cooked->coninternaltype = '\0';
notnulls = lappend(notnulls, cooked);
}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 129c97fdf28..b3df5b913a5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -604,6 +604,9 @@ static void GetForeignKeyCheckTriggers(Relation trigrel,
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior, bool recurse,
bool missing_ok, LOCKMODE lockmode);
+static void DropConstraintByOid(Relation rel, Oid constroid,
+ DropBehavior behavior, bool recurse,
+ bool missing_ok, LOCKMODE lockmode);
static ObjectAddress dropconstraint_internal(Relation rel,
HeapTuple constraintTup, DropBehavior behavior,
bool recurse, bool recursing,
@@ -991,6 +994,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
cooked->is_local = true; /* not used for defaults */
cooked->inhcount = 0; /* ditto */
cooked->is_no_inherit = false;
+ cooked->coninternaltype = '\0';
cookedDefaults = lappend(cookedDefaults, cooked);
}
}
@@ -10581,6 +10585,7 @@ addFkConstraint(addFkConstraintSides fkside,
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
with_period, /* conPeriod */
+ '\0', /* coninternaltype */
is_internal); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -12337,6 +12342,9 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
HeapTuple tuple;
Form_pg_constraint con;
ObjectAddress address;
+ Oid constroid = InvalidOid;
+ AttrNumber attnum = -1;
+ char *conname = NULL;
conrel = table_open(ConstraintRelationId, RowExclusiveLock);
@@ -12367,7 +12375,8 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
con = (Form_pg_constraint) GETSTRUCT(tuple);
if (con->contype != CONSTRAINT_FOREIGN &&
- con->contype != CONSTRAINT_CHECK)
+ con->contype != CONSTRAINT_CHECK &&
+ con->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
@@ -12383,14 +12392,30 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
if (con->contype == CONSTRAINT_FOREIGN)
{
QueueFKConstraintValidation(wqueue, conrel, rel, tuple, lockmode);
+ ObjectAddressSet(address, ConstraintRelationId, con->oid);
}
- else if (con->contype == CONSTRAINT_CHECK)
+ else if (con->contype == CONSTRAINT_CHECK && con->coninternaltype == '\0')
{
QueueCheckConstraintValidation(wqueue, conrel, rel, constrName,
tuple, recurse, recursing, lockmode);
+ ObjectAddressSet(address, ConstraintRelationId, con->oid);
+ }
+ else if (con->contype == CONSTRAINT_CHECK && con->coninternaltype == CONSTRAINT_NOTNULL)
+ {
+ Datum adatum;
+ ArrayType *arr;
+ adatum = SysCacheGetAttrNotNull(CONSTROID, tuple,
+ Anum_pg_constraint_conkey);
+ arr = DatumGetArrayTypeP(adatum); /* ensure not toasted */
+ if (ARR_NDIM(arr) != 1 ||
+ ARR_DIMS(arr)[0] != 1 ||
+ ARR_HASNULL(arr) ||
+ ARR_ELEMTYPE(arr) != INT2OID)
+ elog(ERROR, "confkey is not a 1-D smallint array");
+ memcpy(&attnum, ARR_DATA_PTR(arr), 1 * sizeof(int16));
+ constroid = con->oid;
+ conname = pstrdup(NameStr(con->conname));
}
-
- ObjectAddressSet(address, ConstraintRelationId, con->oid);
}
else
address = InvalidObjectAddress; /* already validated */
@@ -12399,6 +12424,80 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
table_close(conrel, RowExclusiveLock);
+ if (OidIsValid(constroid))
+ {
+ AlteredTableInfo *tab;
+ Constraint *n;
+ String *val;
+ List *children;
+
+ if (rel->rd_rel->relispartition)
+ {
+ char *relname;
+ List *ancestors = NIL;
+ Oid rootrelid = InvalidOid;
+
+ ancestors = get_partition_ancestors(RelationGetRelid(rel));
+ Assert(ancestors != NIL);
+
+ rootrelid = llast_oid(ancestors);
+ list_free(ancestors);
+
+ relname = get_rel_name(rootrelid);
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot validate not-null constraint from partition \"%s\"",
+ RelationGetRelationName(rel)),
+ errhint("You might need validate not-null constraint through root partition \"%s\"", relname));
+ }
+ else if (has_superclass(RelationGetRelid(rel)))
+ {
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot validate not-null constraint on inheritance children \"%s\"",
+ RelationGetRelationName(rel)));
+ }
+
+ children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+ DropConstraintByOid(rel, constroid,
+ DROP_CASCADE, true,
+ false, lockmode);
+
+ tab = ATGetQueueEntry(wqueue, rel);
+ n = makeNode(Constraint);
+ n->contype = CONSTR_NOTNULL;
+ n->conname = conname;
+ n->location = -1;
+ n->is_no_inherit = false;
+ n->raw_expr = NULL;
+ n->is_enforced = true;
+ n->initially_valid = true;
+ n->skip_validation = false;
+ val = makeString(get_attname(RelationGetRelid(rel), attnum,
+ false));
+ n->keys = list_make1(val);
+ ATAddCheckNNConstraint(wqueue, tab, rel, n,
+ recurse, recursing, false, lockmode);
+
+ /*
+ * DropConstraintByOid will call RemoveConstraintById.
+ * Inside RemoveConstraintById, the relation to which this constraint belongs
+ * will be locked in AccessExclusiveLock mode.
+ *
+ * ATAddCheckNNConstraint will install not-null constraint for rel and it's children.
+ * ALTER TABLE VALIDATE CONSTRAINT generally take ShareUpdateExclusiveLock lock.
+ * removing the logical equivalent CHECK constraint will not impact the data integrity.
+ * thus here release the AccessExclusiveLock on rel and its children.
+ */
+ foreach_oid(childrelid, children)
+ UnlockRelationOid(childrelid, AccessExclusiveLock);
+
+ if (CheckRelationLockedByMe(rel, AccessExclusiveLock, true))
+ elog(ERROR, "relation \"%s\" was lock in AccessExclusiveLock", RelationGetRelationName(rel));
+
+ address = InvalidObjectAddress;
+ }
+
return address;
}
@@ -13298,6 +13397,67 @@ createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
parentUpdTrigger, false);
}
+
+
+/*
+ * DROP CONSTRAINT by Oid
+ *
+ * Like DROP COLUMN, we can't use the normal ALTER TABLE recursion mechanism.
+ * The constroid is the OID of the constraint to be dropped. If the constraint
+ * has any inherited child constraints, it will recursively drop them too.
+ */
+static void
+DropConstraintByOid(Relation rel, Oid constroid,
+ DropBehavior behavior, bool recurse,
+ bool missing_ok, LOCKMODE lockmode)
+{
+ Relation conrel;
+ SysScanDesc scan;
+ ScanKeyData skey[1];
+ HeapTuple tuple;
+ bool found = false;
+ char *constrName = NULL;
+ Form_pg_constraint con;
+
+ conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+ /*
+ * Find and drop the target constraint
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_constraint_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(constroid));
+ scan = systable_beginscan(conrel, ConstraintOidIndexId,
+ true, NULL, 1, skey);
+
+ /* There can be at most one matching row */
+ if (HeapTupleIsValid(tuple = systable_getnext(scan)))
+ {
+ con = (Form_pg_constraint) GETSTRUCT(tuple);
+ constrName = pstrdup(NameStr(con->conname));
+ dropconstraint_internal(rel, tuple, behavior, recurse, false,
+ missing_ok, lockmode);
+ found = true;
+ }
+
+ systable_endscan(scan);
+
+ if (!found)
+ {
+ if (!missing_ok)
+ ereport(ERROR,
+ 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);
+}
+
/*
* ALTER TABLE DROP CONSTRAINT
*
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index c9f61130c69..e99b91d3840 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -839,6 +839,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
0, /* inhcount */
true, /* noinherit */
false, /* conperiod */
+ '\0', /* coninternaltype */
isInternal); /* is_internal */
}
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 3cb3ca1cca1..457d47b7ba4 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3606,6 +3606,7 @@ domainAddCheckConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
0, /* inhcount */
false, /* connoinherit */
false, /* conperiod */
+ '\0', /* coninternaltype */
false); /* is_internal */
if (constrAddr)
ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid);
@@ -3714,6 +3715,7 @@ domainAddNotNullConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
0, /* inhcount */
false, /* connoinherit */
false, /* conperiod */
+ '\0', /* coninternaltype */
false); /* is_internal */
if (constrAddr)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..0af53b6fbf3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4222,11 +4222,10 @@ ConstraintElem:
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, NULL,
+ NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
- n->initially_valid = true;
+ n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
| UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index abbe1bb45a3..d5fb9b70c9b 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1090,7 +1090,68 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("not-null constraints on partitioned tables cannot be NO INHERIT"));
- cxt->nnconstraints = lappend(cxt->nnconstraints, constraint);
+ if (constraint->skip_validation && (strcmp(cxt->stmtType, "ALTER TABLE") == 0))
+ {
+ Constraint *constr;
+ NullTest *nnulltest;
+ AttrNumber colnum;
+ TupleDesc tupdesc;
+ Form_pg_attribute attr;
+
+ constr = makeNode(Constraint);
+ constr->contype = CONSTR_CHECK;
+ if (constraint->conname)
+ constr->conname = pstrdup(constraint->conname);
+ constr->deferrable = false;
+ constr->initdeferred = false;
+ constr->is_enforced = true;
+ constr->skip_validation = true;
+ constr->initially_valid = false;
+ constr->is_no_inherit = false;
+ constr->nulls_not_distinct = false;
+ constr->location = constraint->location;
+ constr->contype_internal = CONSTRAINT_NOTNULL;
+
+ colnum = get_attnum(RelationGetRelid(cxt->rel), strVal(linitial(constraint->keys)));
+ if (colnum == InvalidAttrNumber)
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column \"%s\" of relation \"%s\" does not exist",
+ strVal(linitial(constraint->keys)), RelationGetRelationName(cxt->rel)));
+ if (colnum < InvalidAttrNumber)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot add not-null constraint on system column \"%s\"",
+ strVal(linitial(constraint->keys))));
+
+ tupdesc = RelationGetDescr(cxt->rel);
+ attr = TupleDescAttr(tupdesc, colnum - 1);
+
+ nnulltest = makeNode(NullTest);
+ nnulltest->arg = (Expr *) makeVar(1,
+ attr->attnum,
+ attr->atttypid,
+ attr->atttypmod,
+ attr->attcollation,
+ 0);
+
+ nnulltest->nulltesttype = IS_NOT_NULL;
+ nnulltest->argisrow = false;
+ nnulltest->location = constraint->location;
+ constr->cooked_expr = nodeToString(nnulltest);
+
+ cxt->ckconstraints = lappend(cxt->ckconstraints, constr);
+ }
+ else if (constraint->skip_validation && (strcmp(cxt->stmtType, "CREATE TABLE") == 0))
+ {
+ constraint->skip_validation = false;
+ constraint->initially_valid = true;
+ ereport(WARNING,
+ errmsg("CREATE TABLE NOT NULL NOT VALID CONSTRAINT WILL SET TO VALID"));
+ cxt->nnconstraints = lappend(cxt->nnconstraints, constraint);
+ }
+ else
+ cxt->nnconstraints = lappend(cxt->nnconstraints, constraint);
break;
case CONSTR_FOREIGN:
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index dbd339e9df4..70b61b12464 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -42,6 +42,7 @@ typedef struct CookedConstraint
Node *expr; /* transformed default or check expr */
bool is_enforced; /* is enforced? (only for CHECK) */
bool skip_validation; /* skip validation? (only for CHECK) */
+ char coninternaltype; /* (only for NOT NULL) */
bool is_local; /* constraint has local (non-inherited) def */
int16 inhcount; /* number of times constraint is inherited */
bool is_no_inherit; /* constraint has local def and cannot be
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 6da164e7e4d..5740a277d8f 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -114,6 +114,8 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
*/
bool conperiod;
+ char coninternaltype; /* constraint internal type; see codes below */
+
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/*
@@ -250,6 +252,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
int16 conInhCount,
bool conNoInherit,
bool conPeriod,
+ char coninternaltype,
bool is_internal);
extern bool ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 23c9e3c5abf..01a72589084 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2824,6 +2824,7 @@ typedef struct Constraint
* untransformed parse tree */
char *cooked_expr; /* CHECK or DEFAULT expression, as
* nodeToString representation */
+ char contype_internal; /**/
char generated_when; /* ALWAYS or BY DEFAULT */
char generated_kind; /* STORED or VIRTUAL */
bool nulls_not_distinct; /* null treatment for UNIQUE constraints */
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..cdba544bb8e 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -1392,3 +1392,66 @@ DROP TABLE constraint_comments_tbl;
DROP DOMAIN constraint_comments_dom;
DROP ROLE regress_constraint_comments;
DROP ROLE regress_constraint_comments_noaccess;
+-- test cases for not null not valid and validate it.
+create table t0_ok(a int, constraint nn not null a not valid);
+WARNING: CREATE TABLE NOT NULL NOT VALID CONSTRAINT WILL SET TO VALID
+create table t_notnull_notvalid(a int);
+insert into t_notnull_notvalid values(null);
+alter table t_notnull_notvalid add constraint nc1 not null a not valid;
+\d t_notnull_notvalid
+ Table "public.t_notnull_notvalid"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+Check constraints:
+ "nc1" CHECK (a IS NOT NULL) NOT VALID
+
+alter table t_notnull_notvalid validate constraint nc1; --fail.
+ERROR: column "a" of relation "t_notnull_notvalid" contains null values
+truncate t_notnull_notvalid;
+insert into t_notnull_notvalid values(1);
+alter table t_notnull_notvalid validate constraint nc1; --ok
+\d+ t_notnull_notvalid
+ Table "public.t_notnull_notvalid"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | not null | | plain | |
+Not-null constraints:
+ "nc1" NOT NULL "a"
+
+create table lp (a int) partition by list (a);
+create table lp_default partition of lp default;
+create table lp_ef partition of lp for values in (1,2,3,4);
+create table lp_g partition of lp for values in (5,6,7,8);
+create table lp_null partition of lp for values in (null);
+alter table lp add constraint nc1 not null a not valid;
+insert into lp select g from generate_Series(2, 9) g;
+insert into lp_null select NULL; --fail
+ERROR: new row for relation "lp_null" violates check constraint "nc1"
+DETAIL: Failing row contains (null).
+alter table lp_default validate constraint nc1; --fail
+ERROR: cannot validate not-null constraint from partition "lp_default"
+HINT: You might need validate not-null constraint through root partition "lp"
+alter table lp_g validate constraint nc1; --fail
+ERROR: cannot validate not-null constraint from partition "lp_g"
+HINT: You might need validate not-null constraint through root partition "lp"
+alter table lp validate constraint nc1; --ok.
+alter table lp validate constraint nc1; --ok.
+\d lp
+ Partitioned table "public.lp"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | not null |
+Partition key: LIST (a)
+Number of partitions: 4 (Use \d+ to list them.)
+
+\d lp_default
+ Table "public.lp_default"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | not null |
+Partition of: lp DEFAULT
+
+drop table t0_ok;
+drop table lp;
+drop table t_notnull_notvalid;
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 21ce4177de4..06c3895e745 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -836,3 +836,44 @@ DROP DOMAIN constraint_comments_dom;
DROP ROLE regress_constraint_comments;
DROP ROLE regress_constraint_comments_noaccess;
+
+
+-- test cases for not null not valid and validate it.
+create table t0_ok(a int, constraint nn not null a not valid);
+
+create table t_notnull_notvalid(a int);
+insert into t_notnull_notvalid values(null);
+alter table t_notnull_notvalid add constraint nc1 not null a not valid;
+\d t_notnull_notvalid
+alter table t_notnull_notvalid validate constraint nc1; --fail.
+
+truncate t_notnull_notvalid;
+insert into t_notnull_notvalid values(1);
+alter table t_notnull_notvalid validate constraint nc1; --ok
+\d+ t_notnull_notvalid
+
+
+create table lp (a int) partition by list (a);
+create table lp_default partition of lp default;
+create table lp_ef partition of lp for values in (1,2,3,4);
+create table lp_g partition of lp for values in (5,6,7,8);
+create table lp_null partition of lp for values in (null);
+alter table lp add constraint nc1 not null a not valid;
+insert into lp select g from generate_Series(2, 9) g;
+
+insert into lp_null select NULL; --fail
+alter table lp_default validate constraint nc1; --fail
+alter table lp_g validate constraint nc1; --fail
+
+alter table lp validate constraint nc1; --ok.
+alter table lp validate constraint nc1; --ok.
+
+\d lp
+\d lp_default
+
+
+
+drop table t0_ok;
+drop table lp;
+drop table t_notnull_notvalid;
+
--
2.34.1
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-13 08:37 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-17 11:14 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
@ 2025-03-17 11:23 ` Alvaro Herrera <[email protected]>
2025-03-20 09:54 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-17 11:23 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On 2025-Mar-17, jian he wrote:
> hi.
> I played around with it.
>
> current syntax, we don't need to deal with column constraint grammar.
> like the following can fail directly:
> create table t0(a int constraint nn not null a not valid);
> we only support table constraint cases like:
> alter table lp add constraint nc1 not null a not valid;
>
> since CREATE TABLE with invalid constraint does not make sense, so
> we can just issue a warning. like:
> create table t0(a int, constraint nn not null a not valid);
> WARNING: CREATE TABLE NOT NULL NOT VALID CONSTRAINT WILL SET TO VALID
> the wording needs to change...
Yeah, we discussed this elsewhere. I have an alpha-quality patch for
that, but I wasn't too sure about it ...
[1] https://postgr.es/m/CACJufxEQcHNhN6M18JY1mQcgQq9Gn9ofMeop47SdFDE5B8wbug@mail.gmail.com
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-13 08:37 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-17 11:14 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-17 11:23 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-03-20 09:54 ` Rushabh Lathia <[email protected]>
2025-03-20 10:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-20 10:34 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-20 12:19 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
0 siblings, 3 replies; 40+ messages in thread
From: Rushabh Lathia @ 2025-03-20 09:54 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: jian he <[email protected]>; pgsql-hackers
Hi Alvaro,
Thank you for the offline discussion.
As we all agree, changing the attnotnull datatype would not be a good idea
since it is
a commonly used catalog column, and many applications and extensions depend
on it.
Attached is another version of the patch (WIP), where I have introduced a
new catalog column,
pg_attribute.attinvalidnotnull (boolean). This column will default to FALSE
but will be set to TRUE
when an INVALID NOT NULL constraint is created. With this approach, we can
avoid performing
extra scans on the catalog table to identify INVALID NOT NULL constraints,
ensuring there is no
performance impact.
Also updated the pg_dump implementation patch and attaching the same here.
Thanks,
On Mon, Mar 17, 2025 at 3:23 PM Alvaro Herrera <[email protected]>
wrote:
> On 2025-Mar-17, jian he wrote:
>
> > hi.
> > I played around with it.
> >
> > current syntax, we don't need to deal with column constraint grammar.
> > like the following can fail directly:
> > create table t0(a int constraint nn not null a not valid);
> > we only support table constraint cases like:
> > alter table lp add constraint nc1 not null a not valid;
> >
> > since CREATE TABLE with invalid constraint does not make sense, so
> > we can just issue a warning. like:
> > create table t0(a int, constraint nn not null a not valid);
> > WARNING: CREATE TABLE NOT NULL NOT VALID CONSTRAINT WILL SET TO VALID
> > the wording needs to change...
>
> Yeah, we discussed this elsewhere. I have an alpha-quality patch for
> that, but I wasn't too sure about it ...
>
> [1]
> https://postgr.es/m/CACJufxEQcHNhN6M18JY1mQcgQq9Gn9ofMeop47SdFDE5B8wbug@mail.gmail.com
>
> --
> Álvaro Herrera 48°01'N 7°57'E —
> https://www.EnterpriseDB.com/
>
--
Rushabh Lathia
Attachments:
[application/octet-stream] 0002-Support-for-pg_dump.patch (11.0K, ../../CAGPqQf1x2xMG2d19bqyFDEPTni7h=8T-Gi+_oLRGGGAMzy9tZQ@mail.gmail.com/3-0002-Support-for-pg_dump.patch)
download | inline diff:
From 110b2f60bdd53d3b4eb31dc71c9ffb83afac59ea Mon Sep 17 00:00:00 2001
From: Rushabh Lathia <[email protected]>
Date: Thu, 20 Mar 2025 11:39:56 +0530
Subject: [PATCH 2/2] Support for pg_dump.
---
src/bin/pg_dump/pg_dump.c | 167 ++++++++++++++++++++++++++++++++++++--
src/bin/pg_dump/pg_dump.h | 1 +
2 files changed, 159 insertions(+), 9 deletions(-)
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c371570501a..96b7d181f91 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8876,6 +8876,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQExpBuffer q = createPQExpBuffer();
PQExpBuffer tbloids = createPQExpBuffer();
PQExpBuffer checkoids = createPQExpBuffer();
+ PQExpBuffer invalidnotnulloids = createPQExpBuffer();
PGresult *res;
int ntups;
int curtblindx;
@@ -8892,6 +8893,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
int i_attlen;
int i_attalign;
int i_attislocal;
+ int i_notnull_validated;
int i_notnull_name;
int i_notnull_noinherit;
int i_notnull_islocal;
@@ -8988,11 +8990,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
*/
if (fout->remoteVersion >= 180000)
appendPQExpBufferStr(q,
+ "co.convalidated as notnull_validated,\n"
"co.conname AS notnull_name,\n"
"co.connoinherit AS notnull_noinherit,\n"
"co.conislocal AS notnull_islocal,\n");
else
appendPQExpBufferStr(q,
+ "false as notnull_validated,\n"
"CASE WHEN a.attnotnull THEN '' ELSE NULL END AS notnull_name,\n"
"false AS notnull_noinherit,\n"
"a.attislocal AS notnull_islocal,\n");
@@ -9067,6 +9071,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
i_attlen = PQfnumber(res, "attlen");
i_attalign = PQfnumber(res, "attalign");
i_attislocal = PQfnumber(res, "attislocal");
+ i_notnull_validated = PQfnumber(res, "notnull_validated");
i_notnull_name = PQfnumber(res, "notnull_name");
i_notnull_noinherit = PQfnumber(res, "notnull_noinherit");
i_notnull_islocal = PQfnumber(res, "notnull_islocal");
@@ -9086,6 +9091,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
* r is handled by the inner loop.
*/
curtblindx = -1;
+ appendPQExpBufferChar(invalidnotnulloids, '{');
for (int r = 0; r < ntups;)
{
Oid attrelid = atooid(PQgetvalue(res, r, i_attrelid));
@@ -9135,6 +9141,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
tbinfo->attfdwoptions = (char **) pg_malloc(numatts * sizeof(char *));
tbinfo->attmissingval = (char **) pg_malloc(numatts * sizeof(char *));
tbinfo->notnull_constrs = (char **) pg_malloc(numatts * sizeof(char *));
+ tbinfo->notnull_validated = (bool *) pg_malloc(numatts * sizeof(bool));
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 *));
@@ -9160,6 +9167,28 @@ 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_validated[j] = (PQgetvalue(res, r, i_notnull_validated)[0] == 't');
+
+ /*
+ * Dump the invalid NOT NULL constraint like the Check constraints
+ */
+ if (tbinfo->notnull_validated[j])
+ /* Handle not-null constraint name and flags */
+ determineNotNullFlags(fout, res, r,
+ tbinfo, j,
+ i_notnull_name, i_notnull_noinherit,
+ i_notnull_islocal);
+ else if (!PQgetisnull(res, r, i_notnull_name))
+ {
+ /*
+ * Add the entry into invalidnotnull list so NOT NULL
+ * constraint get dump as separate constraints.
+ */
+ if (invalidnotnulloids->len > 1) /* do we have more than
+ * the '{'? */
+ appendPQExpBufferChar(invalidnotnulloids, ',');
+ appendPQExpBuffer(invalidnotnulloids, "%u", tbinfo->dobj.catId.oid);
+ }
/* Handle not-null constraint name and flags */
determineNotNullFlags(fout, res, r,
@@ -9187,6 +9216,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
}
PQclear(res);
+ appendPQExpBufferChar(invalidnotnulloids, '}');
/*
* Now get info about column defaults. This is skipped for a data-only
@@ -9450,6 +9480,128 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQclear(res);
}
+ /*
+ * Get info about table INVALID NOT NULL constraints. This is skipped for
+ * a data-only dump, as it is only needed for table schemas.
+ */
+ if (dopt->dumpSchema && invalidnotnulloids->len > 2)
+ {
+ ConstraintInfo *constrs;
+ int numConstrs;
+ int i_tableoid;
+ int i_oid;
+ int i_conrelid;
+ int i_conname;
+ int i_consrc;
+ int i_conislocal;
+ int i_convalidated;
+
+ pg_log_info("finding table invalid not null constraints");
+
+ resetPQExpBuffer(q);
+ appendPQExpBuffer(q,
+ "SELECT c.tableoid, c.oid, conrelid, conname, "
+ "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
+ "conislocal, convalidated "
+ "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+ "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
+ "WHERE contype = 'n' AND convalidated = 'f'"
+ "ORDER BY c.conrelid, c.conname",
+ invalidnotnulloids->data);
+
+ res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
+
+ numConstrs = PQntuples(res);
+ constrs = (ConstraintInfo *) pg_malloc(numConstrs * sizeof(ConstraintInfo));
+
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_oid = PQfnumber(res, "oid");
+ i_conrelid = PQfnumber(res, "conrelid");
+ i_conname = PQfnumber(res, "conname");
+ i_consrc = PQfnumber(res, "consrc");
+ i_conislocal = PQfnumber(res, "conislocal");
+ i_convalidated = PQfnumber(res, "convalidated");
+
+ /* As above, this loop iterates once per table, not once per row */
+ curtblindx = -1;
+ for (int j = 0; j < numConstrs;)
+ {
+ Oid conrelid = atooid(PQgetvalue(res, j, i_conrelid));
+ TableInfo *tbinfo = NULL;
+ int numcons;
+
+ /* Count rows for this table */
+ for (numcons = 1; numcons < numConstrs - j; numcons++)
+ if (atooid(PQgetvalue(res, j + numcons, i_conrelid)) != conrelid)
+ break;
+
+ /*
+ * Locate the associated TableInfo; we rely on tblinfo[] being in
+ * OID order.
+ */
+ while (++curtblindx < numTables)
+ {
+ tbinfo = &tblinfo[curtblindx];
+ if (tbinfo->dobj.catId.oid == conrelid)
+ break;
+ }
+ if (curtblindx >= numTables)
+ pg_fatal("unrecognized table OID %u", conrelid);
+
+
+ tbinfo->checkexprs = constrs + j;
+
+ for (int c = 0; c < numcons; c++, j++)
+ {
+ bool validated = PQgetvalue(res, j, i_convalidated)[0] == 't';
+
+ constrs[j].dobj.objType = DO_CONSTRAINT;
+ constrs[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
+ constrs[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&constrs[j].dobj);
+ constrs[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_conname));
+ constrs[j].dobj.namespace = tbinfo->dobj.namespace;
+ constrs[j].contable = tbinfo;
+ constrs[j].condomain = NULL;
+ constrs[j].contype = 'n';
+ constrs[j].condef = pg_strdup(PQgetvalue(res, j, i_consrc));
+ constrs[j].confrelid = InvalidOid;
+ constrs[j].conindex = 0;
+ constrs[j].condeferrable = false;
+ constrs[j].condeferred = false;
+ constrs[j].conislocal = (PQgetvalue(res, j, i_conislocal)[0] == 't');
+
+ /*
+ * An unvalidated constraint needs to be dumped separately, so
+ * that potentially-violating existing data is loaded before
+ * the constraint.
+ */
+ constrs[j].separate = !validated;
+
+ constrs[j].dobj.dump = tbinfo->dobj.dump;
+
+ /*
+ * Mark the constraint as needing to appear before the table
+ * --- this is so that any other dependencies of the
+ * constraint will be emitted before we try to create the
+ * table. If the constraint is to be dumped separately, it
+ * will be dumped after data is loaded anyway, so don't do it.
+ * (There's an automatic dependency in the opposite direction
+ * anyway, so don't need to add one manually here.)
+ */
+ if (!constrs[j].separate)
+ addObjectDependency(&tbinfo->dobj,
+ constrs[j].dobj.dumpId);
+
+ /*
+ * We will detect later whether the constraint must be split
+ * out from the table definition.
+ */
+ }
+ }
+ PQclear(res);
+ }
+
destroyPQExpBuffer(q);
destroyPQExpBuffer(tbloids);
destroyPQExpBuffer(checkoids);
@@ -16543,7 +16695,8 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
* defined, or if binary upgrade. (In the latter case, we
* reset conislocal below.)
*/
- print_notnull = (tbinfo->notnull_constrs[j] != NULL &&
+ print_notnull = (tbinfo->notnull_validated[j] &&
+ tbinfo->notnull_constrs[j] != NULL &&
(tbinfo->notnull_islocal[j] ||
dopt->binary_upgrade ||
tbinfo->ispartition));
@@ -16605,11 +16758,6 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
tbinfo->attrdefs[j]->adef_expr);
}
- print_notnull = (tbinfo->notnull_constrs[j] != NULL &&
- (tbinfo->notnull_islocal[j] ||
- dopt->binary_upgrade ||
- tbinfo->ispartition));
-
if (print_notnull)
{
if (tbinfo->notnull_constrs[j][0] == '\0')
@@ -16928,7 +17076,8 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
* because the latter is not available. (We distinguish the
* case because the constraint name is the empty string.)
*/
- if (tbinfo->notnull_constrs[j] != NULL &&
+ if (tbinfo->notnull_validated[j] &&
+ tbinfo->notnull_constrs[j] != NULL &&
!tbinfo->notnull_islocal[j])
{
if (tbinfo->notnull_constrs[j][0] != '\0')
@@ -17931,9 +18080,9 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
.createStmt = q->data,
.dropStmt = delq->data));
}
- else if (coninfo->contype == 'c' && tbinfo)
+ else if ((coninfo->contype == 'c' || coninfo->contype == 'n') && tbinfo)
{
- /* CHECK constraint on a table */
+ /* CHECK/INVALID_NOTNULL constraint on a table */
/* Ignore if not to be dumped separately, or if it was inherited */
if (coninfo->separate && coninfo->conislocal)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bbdb30b5f54..676bf2c53c1 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -365,6 +365,7 @@ typedef struct _tableInfo
* there isn't one on this column. If
* empty string, unnamed constraint
* (pre-v17) */
+ bool *notnull_validated; /* NOT NULL is VALIDATED */
bool *notnull_noinh; /* NOT NULL is NO INHERIT */
bool *notnull_islocal; /* true if NOT NULL has local definition */
struct _attrDefInfo **attrdefs; /* DEFAULT expressions */
--
2.43.0
[application/octet-stream] 0001-Support-NOT-VALID-and-VALIDATE-CONSTRAINT-for-named-.patch (34.5K, ../../CAGPqQf1x2xMG2d19bqyFDEPTni7h=8T-Gi+_oLRGGGAMzy9tZQ@mail.gmail.com/4-0001-Support-NOT-VALID-and-VALIDATE-CONSTRAINT-for-named-.patch)
download | inline diff:
From 02a3fbe6fedadde45374fc9cbb73c32b7dd3fce3 Mon Sep 17 00:00:00 2001
From: Rushabh Lathia <[email protected]>
Date: Thu, 20 Mar 2025 11:22:46 +0530
Subject: [PATCH 1/2] Support NOT VALID and VALIDATE CONSTRAINT for named NOT
NULL constraints.
Commit also add support for pg_dump to dump NOT VALID named NOT NULL
constraints.
---
src/backend/access/common/tupdesc.c | 6 +
src/backend/bootstrap/bootstrap.c | 1 +
src/backend/catalog/heap.c | 3 +-
src/backend/catalog/pg_constraint.c | 17 +-
src/backend/commands/tablecmds.c | 216 +++++++++++++++++++++-
src/backend/optimizer/util/plancat.c | 3 +-
src/backend/parser/gram.y | 4 +-
src/bin/psql/describe.c | 7 +-
src/include/access/tupdesc.h | 2 +
src/include/catalog/pg_attribute.h | 3 +
src/include/catalog/pg_constraint.h | 3 +-
src/test/regress/expected/constraints.out | 153 +++++++++++++++
src/test/regress/sql/constraints.sql | 85 +++++++++
13 files changed, 484 insertions(+), 19 deletions(-)
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index ed2195f14b2..ed269d26090 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -75,6 +75,7 @@ populate_compact_attribute_internal(Form_pg_attribute src,
dst->attisdropped = src->attisdropped;
dst->attgenerated = (src->attgenerated != '\0');
dst->attnotnull = src->attnotnull;
+ dst->attinvalidnotnull = src->attinvalidnotnull;
switch (src->attalign)
{
@@ -252,6 +253,7 @@ CreateTupleDescCopy(TupleDesc tupdesc)
Form_pg_attribute att = TupleDescAttr(desc, i);
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -298,6 +300,7 @@ CreateTupleDescTruncatedCopy(TupleDesc tupdesc, int natts)
Form_pg_attribute att = TupleDescAttr(desc, i);
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -418,6 +421,7 @@ TupleDescCopy(TupleDesc dst, TupleDesc src)
Form_pg_attribute att = TupleDescAttr(dst, i);
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -464,6 +468,7 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno,
/* since we're not copying constraints or defaults, clear these */
dstAtt->attnotnull = false;
+ dstAtt->attinvalidnotnull = false;
dstAtt->atthasdef = false;
dstAtt->atthasmissing = false;
dstAtt->attidentity = '\0';
@@ -841,6 +846,7 @@ TupleDescInitEntry(TupleDesc desc,
att->attndims = attdim;
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 6db864892d0..9077ed46d33 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -582,6 +582,7 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
attrtypes[attnum]->atttypmod = -1;
attrtypes[attnum]->attislocal = true;
+ attrtypes[attnum]->attinvalidnotnull = false;
if (nullness == BOOTCOL_NULL_FORCE_NOT_NULL)
{
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..3a258ab4e69 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -753,6 +753,7 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
slot[slotCount]->tts_values[Anum_pg_attribute_attstorage - 1] = CharGetDatum(attrs->attstorage);
slot[slotCount]->tts_values[Anum_pg_attribute_attcompression - 1] = CharGetDatum(attrs->attcompression);
slot[slotCount]->tts_values[Anum_pg_attribute_attnotnull - 1] = BoolGetDatum(attrs->attnotnull);
+ slot[slotCount]->tts_values[Anum_pg_attribute_attinvalidnotnull - 1] = BoolGetDatum(attrs->attinvalidnotnull);
slot[slotCount]->tts_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(attrs->atthasdef);
slot[slotCount]->tts_values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(attrs->atthasmissing);
slot[slotCount]->tts_values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(attrs->attidentity);
@@ -2626,7 +2627,7 @@ AddRelationNewConstraints(Relation rel,
* to add another one; just adjust inheritance status as needed.
*/
if (AdjustNotNullInheritance(RelationGetRelid(rel), colnum,
- is_local, cdef->is_no_inherit))
+ cdef->conname, is_local, cdef->is_no_inherit))
continue;
/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..a1200ac8abb 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -609,8 +609,6 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
*/
if (con->contype != CONSTRAINT_NOTNULL)
continue;
- if (!con->convalidated)
- continue;
conkey = extractNotNullColumn(conTup);
if (conkey != attnum)
@@ -729,7 +727,8 @@ extractNotNullColumn(HeapTuple constrTup)
*/
bool
AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
- bool is_local, bool is_no_inherit)
+ const char *conname, bool is_local,
+ bool is_no_inherit)
{
HeapTuple tup;
@@ -753,6 +752,18 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"",
NameStr(conform->conname), get_rel_name(relid)));
+ /*
+ * Throw an error if an invalid NOT NULL constraint exists on the
+ * table column and an attempt is made to add another valid NOT NULL
+ * constraint.
+ */
+ if (is_local && !conform->convalidated && conname)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("Invalid NOT NULL constraint \"%s\" exist on relation \"%s\"",
+ NameStr(conform->conname), get_rel_name(relid)),
+ errhint("Do VALIDATE CONSTRAINT"));
+
if (!is_local)
{
if (pg_add_s16_overflow(conform->coninhcount, 1,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 18ff8956577..5f9d02ebe84 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -410,6 +410,9 @@ static void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation
static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
char *constrName, HeapTuple contuple,
bool recurse, bool recursing, LOCKMODE lockmode);
+static void QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple, bool recurse, bool recursing,
+ LOCKMODE lockmode);
static int transformColumnNameList(Oid relId, List *colList,
int16 *attnums, Oid *atttypids, Oid *attcollids);
static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
@@ -473,7 +476,9 @@ static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid)
static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
LOCKMODE lockmode);
static void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
- LOCKMODE lockmode);
+ bool skip_validation, LOCKMODE lockmode);
+static void set_attinvalidnotnull(Relation rel, AttrNumber attnum,
+ bool attinvalidnotnull);
static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel,
char *constrname, char *colName,
bool recurse, bool recursing,
@@ -710,6 +715,7 @@ static List *GetParentedForeignKeyRefs(Relation partition);
static void ATDetachCheckNoForeignKeyRefs(Relation partition);
static char GetAttributeCompression(Oid atttypid, const char *compression);
static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static bool check_for_invalid_notnull(Oid relid, const char *attname);
/* ----------------------------------------------------------------
@@ -1315,7 +1321,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints,
old_notnulls);
foreach_int(attrnum, nncols)
- set_attnotnull(NULL, rel, attrnum, NoLock);
+ set_attnotnull(NULL, rel, attrnum, false, NoLock);
ObjectAddressSet(address, RelationRelationId, relationId);
@@ -7729,6 +7735,45 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
return address;
}
+static void
+set_attinvalidnotnull(Relation rel, AttrNumber attnum,
+ bool attinvalidnotnull)
+{
+ Form_pg_attribute attr;
+
+ CheckAlterTableIsSafe(rel);
+
+ /*
+ * Exit quickly by testing attnotnull from the tupledesc's copy of the
+ * attribute.
+ */
+ attr = TupleDescAttr(RelationGetDescr(rel), attnum - 1);
+ if (attr->attisdropped)
+ return;
+
+ if (attr->attinvalidnotnull != attinvalidnotnull)
+ {
+ Relation attr_rel;
+ HeapTuple tuple;
+
+ attr_rel = table_open(AttributeRelationId, RowExclusiveLock);
+
+ tuple = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, RelationGetRelid(rel));
+
+ attr = (Form_pg_attribute) GETSTRUCT(tuple);
+ attr->attinvalidnotnull = attinvalidnotnull;
+
+ CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
+ CommandCounterIncrement();
+
+ table_close(attr_rel, RowExclusiveLock);
+ heap_freetuple(tuple);
+ }
+}
+
/*
* Helper to set pg_attribute.attnotnull if it isn't set, and to tell phase 3
* to verify it.
@@ -7739,7 +7784,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
*/
static void
set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
- LOCKMODE lockmode)
+ bool skip_validation, LOCKMODE lockmode)
{
Form_pg_attribute attr;
@@ -7768,20 +7813,23 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
attr = (Form_pg_attribute) GETSTRUCT(tuple);
Assert(!attr->attnotnull);
attr->attnotnull = true;
- CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
/*
* If the nullness isn't already proven by validated constraints, have
* ALTER TABLE phase 3 test for it.
*/
- if (wqueue && !NotNullImpliedByRelConstraints(rel, attr))
+ if (!skip_validation &&
+ wqueue && !NotNullImpliedByRelConstraints(rel, attr))
{
AlteredTableInfo *tab;
tab = ATGetQueueEntry(wqueue, rel);
tab->verify_new_notnull = true;
}
+ else
+ attr->attinvalidnotnull = true;
+ CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
CommandCounterIncrement();
table_close(attr_rel, RowExclusiveLock);
@@ -7880,6 +7928,15 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
conForm->conislocal = true;
changed = true;
}
+ else if (!conForm->convalidated)
+ {
+ /*
+ * flip attnotnull and convalidated, and also validate the
+ * constraint.
+ */
+ return ATExecValidateConstraint(wqueue, rel, conForm->conname.data,
+ recurse, recursing, lockmode);
+ }
if (changed)
{
@@ -7943,7 +8000,7 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
RelationGetRelid(rel), attnum);
/* Mark pg_attribute.attnotnull for the column */
- set_attnotnull(wqueue, rel, attnum, lockmode);
+ set_attnotnull(wqueue, rel, attnum, constraint->skip_validation, lockmode);
/*
* Recurse to propagate the constraint to children that don't have one.
@@ -9388,6 +9445,13 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
AlterTableCmd *newcmd;
Constraint *nnconstr;
+ /* Verify that the not-null constraint has been validated */
+ if (check_for_invalid_notnull(RelationGetRelid(rel), strVal(lfirst(lc))))
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("column \"%s\" of table \"%s\" is marked as NOT VALID NOT NULL constraint",
+ strVal(lfirst(lc)), RelationGetRelationName(rel)));
+
nnconstr = makeNotNullConstraint(lfirst(lc));
newcmd = makeNode(AlterTableCmd);
@@ -9399,6 +9463,28 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
}
}
+/*
+ * This function checks for any invalid NOT NULL constraint on the given
+ * relation and attribute name. It returns true if found, false otherwise.
+ */
+static bool
+check_for_invalid_notnull(Oid relid, const char *attname)
+{
+ HeapTuple tuple;
+ bool retval = false;
+
+ tuple = findNotNullConstraint(relid, attname);
+ if (!HeapTupleIsValid(tuple))
+ return false;
+
+ if (((Form_pg_constraint) GETSTRUCT(tuple))->convalidated == false)
+ retval = true;
+
+ heap_freetuple(tuple);
+
+ return retval;
+}
+
/*
* ALTER TABLE ADD INDEX
*
@@ -9766,7 +9852,7 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* phase 3 to verify existing rows, if needed.
*/
if (constr->contype == CONSTR_NOTNULL)
- set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
+ set_attnotnull(wqueue, rel, ccon->attnum, ccon->skip_validation, lockmode);
ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
}
@@ -12367,7 +12453,8 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
con = (Form_pg_constraint) GETSTRUCT(tuple);
if (con->contype != CONSTRAINT_FOREIGN &&
- con->contype != CONSTRAINT_CHECK)
+ con->contype != CONSTRAINT_CHECK &&
+ con->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
@@ -12389,6 +12476,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
QueueCheckConstraintValidation(wqueue, conrel, rel, constrName,
tuple, recurse, recursing, lockmode);
}
+ else if (con->contype == CONSTRAINT_NOTNULL)
+ {
+ QueueNNConstraintValidation(wqueue, conrel, rel,
+ tuple, recurse, recursing, lockmode);
+ }
ObjectAddressSet(address, ConstraintRelationId, con->oid);
}
@@ -12605,6 +12697,114 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
heap_freetuple(copyTuple);
}
+/*
+ * QueueNNConstraintValidation
+ *
+ * Add an entry to the wqueue to validate the given not-null constraint in
+ * Phase 3 and update the convalidated field in the pg_constraint catalog for
+ * the specified relation and all its inheriting children.
+ */
+static void
+QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple, bool recurse, bool recursing,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint con;
+ AlteredTableInfo *tab;
+ HeapTuple copyTuple;
+ Form_pg_constraint copy_con;
+ List *children = NIL;
+ AttrNumber attnum;
+ char *colname;
+
+ con = (Form_pg_constraint) GETSTRUCT(contuple);
+ Assert(con->contype == CONSTRAINT_NOTNULL);
+
+ attnum = extractNotNullColumn(contuple);
+
+ /*
+ * For constraints that aren't NO INHERIT, we must ensure that we only
+ * mark the constraint as validated on the parent if it's already
+ * validated on the children.
+ *
+ * If we're recursing, the parent has already done this, so skip it.
+ *
+ * We recurse before validating on the parent, to reduce risk of
+ * deadlocks.
+ */
+ if (!recursing && !con->connoinherit)
+ children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+
+ colname = get_attname(RelationGetRelid(rel), attnum, false);
+ foreach_oid(childoid, children)
+ {
+ Relation childrel;
+ HeapTuple contup;
+ Form_pg_constraint childcon;
+ char *conname;
+
+ if (childoid == RelationGetRelid(rel))
+ continue;
+
+ /*
+ * If we are told not to recurse, there had better not be any child
+ * tables, because we can't mark the constraint on the parent valid
+ * unless it is valid for all child tables.
+ */
+ if (!recurse)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+ errmsg("constraint must be validated on child tables too"));
+
+ /*
+ * The column on child might have a different attnum, so search by
+ * column name.
+ */
+ contup = findNotNullConstraint(childoid, colname);
+ if (!contup)
+ elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
+ colname, get_rel_name(childoid));
+ childcon = (Form_pg_constraint) GETSTRUCT(contup);
+ if (childcon->convalidated)
+ continue;
+
+ /* find_all_inheritors already got lock */
+ childrel = table_open(childoid, NoLock);
+ conname = pstrdup(NameStr(childcon->conname));
+
+ /* XXX improve ATExecValidateConstraint API to avoid double search */
+ ATExecValidateConstraint(wqueue, childrel, conname,
+ false, true, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->verify_new_notnull = true;
+
+ /*
+ * Invalidate relcache so that others see the new validated constraint.
+ */
+ CacheInvalidateRelcache(rel);
+
+ /*
+ * Now update the catalogs, while we have the door open.
+ */
+ copyTuple = heap_copytuple(contuple);
+ copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+ copy_con->convalidated = true;
+ CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
+
+ /*
+ * Also flip attnotnull. call function with wqueue as NULL to
+ * bypass validation, as it has already been performed.
+ */
+ set_attinvalidnotnull(rel, attnum, false);
+
+ InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
+
+ heap_freetuple(copyTuple);
+}
+
/*
* transformColumnNameList - transform list of column names
*
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 71abb01f655..5cc6980eced 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -28,6 +28,7 @@
#include "catalog/catalog.h"
#include "catalog/heap.h"
#include "catalog/pg_am.h"
+#include "catalog/pg_constraint.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_statistic_ext_data.h"
@@ -177,7 +178,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
{
CompactAttribute *attr = TupleDescCompactAttr(relation->rd_att, i);
- if (attr->attnotnull)
+ if (attr->attnotnull && !attr->attinvalidnotnull)
{
rel->notnullattnums = bms_add_member(rel->notnullattnums,
i + 1);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..715eb51300b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4224,9 +4224,9 @@ ConstraintElem:
n->keys = list_make1(makeString($3));
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
- NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
- n->initially_valid = true;
+ n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
| UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..b7e444f7489 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3114,7 +3114,7 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.conname, a.attname, c.connoinherit,\n"
- " c.conislocal, c.coninhcount <> 0\n"
+ " c.conislocal, c.coninhcount <> 0, c.convalidated \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"
@@ -3138,13 +3138,14 @@ describeOneTableDetails(const char *schemaname,
bool islocal = PQgetvalue(result, i, 3)[0] == 't';
bool inherited = PQgetvalue(result, i, 4)[0] == 't';
- printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s",
+ printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s%s",
PQgetvalue(result, i, 0),
PQgetvalue(result, i, 1),
PQgetvalue(result, i, 2)[0] == 't' ?
" NO INHERIT" :
islocal && inherited ? _(" (local, inherited)") :
- inherited ? _(" (inherited)") : "");
+ inherited ? _(" (inherited)") : "",
+ PQgetvalue(result, i, 5)[0] == 'f' ? " NOT VALID" : "");
printTableAddFooter(&cont, buf.data);
}
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 396eeb7a0bb..fcea828e713 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -45,6 +45,7 @@ typedef struct TupleConstr
bool has_not_null;
bool has_generated_stored;
bool has_generated_virtual;
+ bool has_invalid_not_null;
} TupleConstr;
/*
@@ -77,6 +78,7 @@ typedef struct CompactAttribute
bool attisdropped; /* as FormData_pg_attribute.attisdropped */
bool attgenerated; /* FormData_pg_attribute.attgenerated != '\0' */
bool attnotnull; /* as FormData_pg_attribute.attnotnull */
+ bool attinvalidnotnull; /* as FormData_pg_attribute.attinvalidnotnull */
uint8 attalignby; /* alignment requirement in bytes */
} CompactAttribute;
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index deaa515fe53..bac4267d21d 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -120,6 +120,9 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
/* This flag represents the "NOT NULL" constraint */
bool attnotnull;
+ /* This flag represents the "INVALID NOT NULL" constraint */
+ bool attinvalidnotnull BKI_DEFAULT(f);
+
/* Has DEFAULT value or not */
bool atthasdef BKI_DEFAULT(f);
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 6da164e7e4d..0e01ff1bbbd 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -264,7 +264,8 @@ 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,
- bool is_local, bool is_no_inherit);
+ const char *conname, bool is_local,
+ bool is_no_inherit);
extern List *RelationGetNotNullConstraints(Oid relid, bool cooked,
bool include_noinh);
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..8e22a492c7d 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -896,6 +896,159 @@ Not-null constraints:
"foobar" NOT NULL "a"
DROP TABLE notnull_tbl1;
+-- verify NOT NULL VALID/NOT VALID
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1);
+INSERT INTO notnull_tbl1 VALUES (NULL, 2);
+INSERT INTO notnull_tbl1 VALUES (300, 3);
+-- Below statement should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID;
+\d+ notnull_tbl1
+ Table "public.notnull_tbl1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | not null | | plain | |
+ b | integer | | | | plain | |
+Not-null constraints:
+ "nn" NOT NULL "a" NOT VALID
+
+-- Try to insert new record with NULL, should throw an error
+INSERT INTO notnull_tbl1 VALUES (NULL, 4);
+ERROR: null value in column "a" of relation "notnull_tbl1" violates not-null constraint
+DETAIL: Failing row contains (null, 4).
+-- SELECT NULL values for COLUMN a, should return 2 records.
+SELECT * FROM notnull_tbl1 WHERE a is NULL;
+ a | b
+---+---
+ | 1
+ | 2
+(2 rows)
+
+-- UPDATE the one of the NULL values
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+-- DELETE the record (NULL, 2)
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1;
+ a | b
+-----+---
+ 300 | 3
+ 100 | 1
+(2 rows)
+
+-- Try to add primary key on table column marked as NOT VALID NOT NULL
+-- constraint. This should throw an error.
+ALTER TABLE notnull_tbl1 add primary key (a);
+ERROR: column "a" of table "notnull_tbl1" is marked as NOT VALID NOT NULL constraint
+-- INHERITS table having NOT VALID NOT NULL constraints.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+NOTICE: merging column "a" with inherited definition
+NOTICE: merging column "b" with inherited definition
+-- Child table NOT NULL constraints should be valid.
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid = 'notnull_tbl1_child'::regclass;
+ conname | convalidated
+---------+--------------
+ nn | t
+(1 row)
+
+DROP TABLE notnull_tbl1_child;
+ALTER TABLE notnull_tbl1 validate constraint nn;
+\d+ notnull_tbl1
+ Table "public.notnull_tbl1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | not null | | plain | |
+ b | integer | | | | plain | |
+Not-null constraints:
+ "nn" NOT NULL "a"
+
+DROP TABLE notnull_tbl1;
+-- Test the different Not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent not null a not valid;
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child not null a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+-- This statement should validate not null constraint for parent as well as
+-- child.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
+in ('notnull_tbl1'::regclass, 'notnull_chld'::regclass);
+ conname | convalidated
+-----------+--------------
+ nn_parent | t
+ nn_child | t
+(2 rows)
+
+DROP TABLE notnull_tbl1 CASCADE;
+NOTICE: drop cascades to table notnull_chld
+-- test to throw an error when trying to add another NOT NULL
+-- on the table column with INVALID NOT NULL constraint.
+CREATE TABLE notnull_tbl1 (a INTEGER);
+INSERT INTO notnull_tbl1 VALUES ( NULL );
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid;
+-- Should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+ERROR: Invalid NOT NULL constraint "nn1" exist on relation "notnull_tbl1"
+HINT: Do VALIDATE CONSTRAINT
+-- This should throw an error while validating the NOT NULL
+-- constraint.
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+-- After deleting null values, SET NOT NULL should work
+DELETE FROM notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
+in ('notnull_tbl1'::regclass);
+ conname | convalidated
+---------+--------------
+ nn1 | t
+(1 row)
+
+DROP TABLE notnull_tbl1;
+-- Test invalid not null on inheritance table.
+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;
+NOTICE: merging definition of column "i" for child "inh_child"
+NOTICE: merging definition of column "i" for child "inh_grandchild"
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ALTER i SET NOT NULL;
+SELECT conrelid::regclass, conname, convalidated, coninhcount FROM pg_catalog.pg_constraint WHERE conrelid
+in ('inh_parent'::regclass, 'inh_child'::regclass, 'inh_grandchild'::regclass) ORDER BY 1;
+ conrelid | conname | convalidated | coninhcount
+----------------+---------+--------------+-------------
+ inh_parent | nn | t | 0
+ inh_child | nn | t | 1
+ inh_grandchild | nn | t | 2
+(3 rows)
+
+drop table inh_parent, inh_child, inh_grandchild;
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 2);
+INSERT INTO notnull_tbl1_upg VALUES (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY RANGE (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID;
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES FROM (0) TO (10);
+CREATE TABLE notnull_tbl1_2 PARTITION OF notnull_tbl1 FOR VALUES FROM (20) TO (30);
+-- Parent table NOT NULL constraints will be marked as validated false, where
+-- for child table it will be true
+SELECT conrelid::regclass, conname, convalidated FROM pg_catalog.pg_constraint WHERE
+conrelid IN ('notnull_tbl1'::regclass, 'notnull_tbl1_1'::regclass, 'notnull_tbl1_2'::regclass);
+ conrelid | conname | convalidated
+----------------+-------------+--------------
+ notnull_tbl1 | notnull_con | f
+ notnull_tbl1_1 | notnull_con | t
+ notnull_tbl1_2 | notnull_con | t
+(3 rows)
+
+DROP TABLE notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2;
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 21ce4177de4..7aff17e6764 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -640,6 +640,91 @@ ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a;
\d+ notnull_tbl1
DROP TABLE notnull_tbl1;
+-- verify NOT NULL VALID/NOT VALID
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1);
+INSERT INTO notnull_tbl1 VALUES (NULL, 2);
+INSERT INTO notnull_tbl1 VALUES (300, 3);
+-- Below statement should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID;
+\d+ notnull_tbl1
+-- Try to insert new record with NULL, should throw an error
+INSERT INTO notnull_tbl1 VALUES (NULL, 4);
+-- SELECT NULL values for COLUMN a, should return 2 records.
+SELECT * FROM notnull_tbl1 WHERE a is NULL;
+-- UPDATE the one of the NULL values
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+-- DELETE the record (NULL, 2)
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1;
+-- Try to add primary key on table column marked as NOT VALID NOT NULL
+-- constraint. This should throw an error.
+ALTER TABLE notnull_tbl1 add primary key (a);
+-- INHERITS table having NOT VALID NOT NULL constraints.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+-- Child table NOT NULL constraints should be valid.
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid = 'notnull_tbl1_child'::regclass;
+DROP TABLE notnull_tbl1_child;
+ALTER TABLE notnull_tbl1 validate constraint nn;
+\d+ notnull_tbl1
+DROP TABLE notnull_tbl1;
+-- Test the different Not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent not null a not valid;
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child not null a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+-- This statement should validate not null constraint for parent as well as
+-- child.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
+in ('notnull_tbl1'::regclass, 'notnull_chld'::regclass);
+DROP TABLE notnull_tbl1 CASCADE;
+-- test to throw an error when trying to add another NOT NULL
+-- on the table column with INVALID NOT NULL constraint.
+CREATE TABLE notnull_tbl1 (a INTEGER);
+INSERT INTO notnull_tbl1 VALUES ( NULL );
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid;
+-- Should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+-- This should throw an error while validating the NOT NULL
+-- constraint.
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+-- After deleting null values, SET NOT NULL should work
+DELETE FROM notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
+in ('notnull_tbl1'::regclass);
+DROP TABLE notnull_tbl1;
+-- Test invalid not null on inheritance table.
+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;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ALTER i SET NOT NULL;
+SELECT conrelid::regclass, conname, convalidated, coninhcount FROM pg_catalog.pg_constraint WHERE conrelid
+in ('inh_parent'::regclass, 'inh_child'::regclass, 'inh_grandchild'::regclass) ORDER BY 1;
+drop table inh_parent, inh_child, inh_grandchild;
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 2);
+INSERT INTO notnull_tbl1_upg VALUES (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY RANGE (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID;
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES FROM (0) TO (10);
+CREATE TABLE notnull_tbl1_2 PARTITION OF notnull_tbl1 FOR VALUES FROM (20) TO (30);
+-- Parent table NOT NULL constraints will be marked as validated false, where
+-- for child table it will be true
+SELECT conrelid::regclass, conname, convalidated FROM pg_catalog.pg_constraint WHERE
+conrelid IN ('notnull_tbl1'::regclass, 'notnull_tbl1_1'::regclass, 'notnull_tbl1_2'::regclass);
+DROP TABLE notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2;
+
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
--
2.43.0
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-13 08:37 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-17 11:14 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-17 11:23 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-20 09:54 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
@ 2025-03-20 10:26 ` Alvaro Herrera <[email protected]>
2 siblings, 0 replies; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-20 10:26 UTC (permalink / raw)
To: Rushabh Lathia <[email protected]>; +Cc: jian he <[email protected]>; pgsql-hackers
Hello
On 2025-Mar-20, Rushabh Lathia wrote:
> Attached is another version of the patch (WIP), where I have
> introduced a new catalog column, pg_attribute.attinvalidnotnull
> (boolean). This column will default to FALSE but will be set to TRUE
> when an INVALID NOT NULL constraint is created. With this approach,
> we can avoid performing extra scans on the catalog table to identify
> INVALID NOT NULL constraints, ensuring there is no performance impact.
I find this a reasonable way forward. We won't impact any applications
that are currently relying on the boolean semantics of attnotnull, while
still allowing the backend to know the real status of the constraint
without having to scan pg_constraint for it.
I also checked the struct layout, which is currently
struct FormData_pg_attribute {
Oid attrelid; /* 0 4 */
NameData attname; /* 4 64 */
/* --- cacheline 1 boundary (64 bytes) was 4 bytes ago --- */
Oid atttypid; /* 68 4 */
int16 attlen; /* 72 2 */
int16 attnum; /* 74 2 */
int32 atttypmod; /* 76 4 */
int16 attndims; /* 80 2 */
_Bool attbyval; /* 82 1 */
char attalign; /* 83 1 */
char attstorage; /* 84 1 */
char attcompression; /* 85 1 */
_Bool attnotnull; /* 86 1 */
_Bool atthasdef; /* 87 1 */
_Bool atthasmissing; /* 88 1 */
char attidentity; /* 89 1 */
char attgenerated; /* 90 1 */
_Bool attisdropped; /* 91 1 */
_Bool attislocal; /* 92 1 */
/* XXX 1 byte hole, try to pack */
int16 attinhcount; /* 94 2 */
Oid attcollation; /* 96 4 */
/* size: 100, cachelines: 2, members: 20 */
/* sum members: 99, holes: 1, sum holes: 1 */
/* last cacheline: 36 bytes */
};
Since there's a one-byte hole after the lot of bools/chars, the new bool
will use it and this won't enlarge the storage, neither in memory nor on
disk.
I have not reviewed this patch in detail yet.
Thank you!
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-13 08:37 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-17 11:14 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-17 11:23 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-20 09:54 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
@ 2025-03-20 10:34 ` jian he <[email protected]>
2025-03-20 11:07 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2 siblings, 1 reply; 40+ messages in thread
From: jian he @ 2025-03-20 10:34 UTC (permalink / raw)
To: Rushabh Lathia <[email protected]>; +Cc: Alvaro Herrera <[email protected]>; pgsql-hackers
On Thu, Mar 20, 2025 at 5:54 PM Rushabh Lathia <[email protected]> wrote:
>
hi. looking at the regress tests.
+-- verify NOT NULL VALID/NOT VALID
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1);
+INSERT INTO notnull_tbl1 VALUES (NULL, 2);
+INSERT INTO notnull_tbl1 VALUES (300, 3);
+-- Below statement should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID;
+\d+ notnull_tbl1
+ Table "public.notnull_tbl1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats
target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | not null | | plain | |
+ b | integer | | | | plain | |
+Not-null constraints:
+ "nn" NOT NULL "a" NOT VALID
as I mentioned in an offline discussion.
as you can see the output of `\d+ notnull_tbl1`
That means the pg_attribute.attnotnull definition is changed.
current description in
https://www.postgresql.org/docs/current/catalog-pg-attribute.html
attnotnull bool
This represents a not-null constraint.
now the "attnotnull" column means:
This represents a not-null constraint, it may not be validated.
This attribute column may already contain NULLs on it.
so doc/src/sgml/catalogs.sgml the following part will need to adjust
accordingly.
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>attnotnull</structfield> <type>bool</type>
</para>
<para>
This column has a not-null constraint.
</para></entry>
</row>
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-13 08:37 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-17 11:14 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-17 11:23 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-20 09:54 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-20 10:34 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
@ 2025-03-20 11:07 ` Alvaro Herrera <[email protected]>
0 siblings, 0 replies; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-20 11:07 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Rushabh Lathia <[email protected]>; pgsql-hackers
On 2025-Mar-20, jian he wrote:
> as you can see the output of `\d+ notnull_tbl1`
> That means the pg_attribute.attnotnull definition is changed.
That's correct, it changed in that way. I propose for the new docs:
> <row>
> <entry role="catalog_table_entry"><para role="column_definition">
> <structfield>attnotnull</structfield> <type>bool</type>
> </para>
> <para>
> This column has a not-null constraint.
> </para></entry>
> </row>
"This column has a possibly unvalidated not-null constraint". The
description for the new column would say "the not-null constraint for
this column is validated" (or the opposite). My recommendation is to
rename the other column from attinvalidnotnull (Rushabh's proposal) to
"attnotnullvalid" and invert the sense of the boolean. I think that's
less confusing.
Thanks
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"If you want to have good ideas, you must have many ideas. Most of them
will be wrong, and what you have to learn is which ones to throw away."
(Linus Pauling)
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-13 08:37 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-17 11:14 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-17 11:23 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-20 09:54 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
@ 2025-03-20 12:19 ` Ashutosh Bapat <[email protected]>
2025-03-20 14:59 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2 siblings, 1 reply; 40+ messages in thread
From: Ashutosh Bapat @ 2025-03-20 12:19 UTC (permalink / raw)
To: Rushabh Lathia <[email protected]>; +Cc: Alvaro Herrera <[email protected]>; jian he <[email protected]>; pgsql-hackers
On Thu, Mar 20, 2025 at 3:25 PM Rushabh Lathia <[email protected]> wrote:
>
> Hi Alvaro,
>
> Thank you for the offline discussion.
>
> As we all agree, changing the attnotnull datatype would not be a good idea since it is
> a commonly used catalog column, and many applications and extensions depend on it.
>
> Attached is another version of the patch (WIP), where I have introduced a new catalog column,
> pg_attribute.attinvalidnotnull (boolean). This column will default to FALSE but will be set to TRUE
> when an INVALID NOT NULL constraint is created. With this approach, we can avoid performing
> extra scans on the catalog table to identify INVALID NOT NULL constraints, ensuring there is no
> performance impact.
>
> Also updated the pg_dump implementation patch and attaching the same here.
>
These patches do not address comments discussed in [1]. Since there
was a change in design, I am assuming that those will be addressed
once the design change is accepted.
[1] https://www.postgresql.org/message-id/[email protected]
--
Best Wishes,
Ashutosh Bapat
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-13 08:37 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-17 11:14 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-17 11:23 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-20 09:54 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-20 12:19 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
@ 2025-03-20 14:59 ` jian he <[email protected]>
2025-03-20 15:52 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: jian he @ 2025-03-20 14:59 UTC (permalink / raw)
To: Ashutosh Bapat <[email protected]>; +Cc: Rushabh Lathia <[email protected]>; Alvaro Herrera <[email protected]>; pgsql-hackers
On Thu, Mar 20, 2025 at 8:20 PM Ashutosh Bapat
<[email protected]> wrote:
>
> On Thu, Mar 20, 2025 at 3:25 PM Rushabh Lathia <[email protected]> wrote:
> >
> > Hi Alvaro,
> >
> > Thank you for the offline discussion.
> >
> > As we all agree, changing the attnotnull datatype would not be a good idea since it is
> > a commonly used catalog column, and many applications and extensions depend on it.
> >
> > Attached is another version of the patch (WIP), where I have introduced a new catalog column,
> > pg_attribute.attinvalidnotnull (boolean). This column will default to FALSE but will be set to TRUE
> > when an INVALID NOT NULL constraint is created. With this approach, we can avoid performing
> > extra scans on the catalog table to identify INVALID NOT NULL constraints, ensuring there is no
> > performance impact.
> >
> > Also updated the pg_dump implementation patch and attaching the same here.
> >
>
> These patches do not address comments discussed in [1]. Since there
> was a change in design, I am assuming that those will be addressed
> once the design change is accepted.
>
> [1] https://www.postgresql.org/message-id/[email protected]
> Is it expected that a child may have VALID constraint but parent has
> not valid constraint?
but the MergeConstraintsIntoExisting logic is when
ALTER TABLE ATTACH PARTITION,
it expects the child table to also have an equivalent constraint
definition on it.
see MergeConstraintsIntoExisting:
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("child table is missing constraint \"%s\"",
NameStr(parent_con->conname))));
So I decided not to support it.
main idea:
NOT NULL NOT VALID
* one column one NOT NULL, if you want to change status, it's not
allowed, it will error out, give you hints.
* it will logically be equivalent to CHECK(x IS NOT NULL) NOT VALID.
* it can only be added using ALTER TABLE, not with CREATE TABLE (a
warning will be issued)
* pg_attribute.attinvalidnotnull meaning: this attnum has a
(convalidated == false) NOT NULL pg_constraint entry to it.
* if attnotnull is true, then attinvalidnotnull should be false.
Conversely, if attinvalidnotnull is true, then attnotnull should be false.
* an invalid not-null cannot be used while adding a primary key.
* if attinvalidnotnull is true, this column can not accept NULL values,
but the existing column value may contain NULLs, we need to
VALIDATE the not-null constraint to check if this column exists NULL
values or not.
* partitioned table can not have NOT NULL NOT VALID.
Attachments:
[text/x-patch] v3-0001-NOT-NULL-NOT-VALID.patch (47.7K, ../../CACJufxG0A-_ppTBzGT=L99AFv2XF+xbx19H7XaQTQ_rZGYcj-w@mail.gmail.com/2-v3-0001-NOT-NULL-NOT-VALID.patch)
download | inline diff:
From 0106f2ded55a699324369412de859ab93f374960 Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
Date: Thu, 20 Mar 2025 22:43:08 +0800
Subject: [PATCH v3 1/1] NOT NULL NOT VALID
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* it will logically be equivalent to CHECK(x IS NOT NULL) NOT VALID.
* it can only be added using ALTER TABLE, not with CREATE TABLE (a warning will issued)
* if attnotnull is true, then attinvalidnotnull should be false.
Conversely, if attinvalidnotnull is true, then attnotnull should be false.
* an invalid not-null cannot be used while adding a primary key.
* if attinvalidnotnull is true, this column can not accept NULL values,
but the existing column value may contain NULLs, we need to
VALIDATE the not-null constraint to check if this column exists NULL values or not.
* TODO: currently, partitioned table can not mark as NO VALID. maybe it's doable.
partition with not valid not-null will error out while attach to the partitioned table.
---
doc/src/sgml/catalogs.sgml | 10 +
src/backend/access/common/tupdesc.c | 11 ++
src/backend/bootstrap/bootstrap.c | 1 +
src/backend/catalog/heap.c | 8 +-
src/backend/catalog/pg_constraint.c | 45 ++++-
src/backend/commands/tablecmds.c | 211 +++++++++++++++++++++-
src/backend/executor/execMain.c | 130 ++++++++-----
src/backend/parser/gram.y | 4 +-
src/backend/parser/parse_utilcmd.c | 7 +-
src/backend/utils/cache/relcache.c | 3 +
src/bin/psql/describe.c | 9 +-
src/include/access/tupdesc.h | 1 +
src/include/catalog/pg_attribute.h | 3 +
src/include/catalog/pg_constraint.h | 8 +-
src/test/regress/expected/constraints.out | 128 +++++++++++++
src/test/regress/sql/constraints.sql | 92 ++++++++++
16 files changed, 593 insertions(+), 78 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..299d2a46f4e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1264,6 +1264,16 @@
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>attinvalidnotnull</structfield> <type>bool</type>
+ </para>
+ <para>
+ This column has a attinvalidnotnull not-null constraint.
+ If <structfield>attnotnull</structfield> is true, this has to be false.
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>atthasdef</structfield> <type>bool</type>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index ed2195f14b2..634cfddff23 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -252,6 +252,7 @@ CreateTupleDescCopy(TupleDesc tupdesc)
Form_pg_attribute att = TupleDescAttr(desc, i);
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -298,6 +299,7 @@ CreateTupleDescTruncatedCopy(TupleDesc tupdesc, int natts)
Form_pg_attribute att = TupleDescAttr(desc, i);
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -341,6 +343,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
TupleConstr *cpy = (TupleConstr *) palloc0(sizeof(TupleConstr));
cpy->has_not_null = constr->has_not_null;
+ cpy->has_invalid_not_null = constr->has_invalid_not_null;
cpy->has_generated_stored = constr->has_generated_stored;
cpy->has_generated_virtual = constr->has_generated_virtual;
@@ -418,6 +421,7 @@ TupleDescCopy(TupleDesc dst, TupleDesc src)
Form_pg_attribute att = TupleDescAttr(dst, i);
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -464,6 +468,7 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno,
/* since we're not copying constraints or defaults, clear these */
dstAtt->attnotnull = false;
+ dstAtt->attinvalidnotnull = false;
dstAtt->atthasdef = false;
dstAtt->atthasmissing = false;
dstAtt->attidentity = '\0';
@@ -613,6 +618,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
return false;
if (attr1->attnotnull != attr2->attnotnull)
return false;
+ if (attr1->attinvalidnotnull != attr2->attinvalidnotnull)
+ return false;
if (attr1->atthasdef != attr2->atthasdef)
return false;
if (attr1->attidentity != attr2->attidentity)
@@ -639,6 +646,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
return false;
if (constr1->has_not_null != constr2->has_not_null)
return false;
+ if (constr1->has_invalid_not_null != constr2->has_invalid_not_null)
+ return false;
if (constr1->has_generated_stored != constr2->has_generated_stored)
return false;
if (constr1->has_generated_virtual != constr2->has_generated_virtual)
@@ -841,6 +850,7 @@ TupleDescInitEntry(TupleDesc desc,
att->attndims = attdim;
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -904,6 +914,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
att->attndims = attdim;
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 6db864892d0..9077ed46d33 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -582,6 +582,7 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
attrtypes[attnum]->atttypmod = -1;
attrtypes[attnum]->attislocal = true;
+ attrtypes[attnum]->attinvalidnotnull = false;
if (nullness == BOOTCOL_NULL_FORCE_NOT_NULL)
{
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..e3ae6993c95 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -753,6 +753,7 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
slot[slotCount]->tts_values[Anum_pg_attribute_attstorage - 1] = CharGetDatum(attrs->attstorage);
slot[slotCount]->tts_values[Anum_pg_attribute_attcompression - 1] = CharGetDatum(attrs->attcompression);
slot[slotCount]->tts_values[Anum_pg_attribute_attnotnull - 1] = BoolGetDatum(attrs->attnotnull);
+ slot[slotCount]->tts_values[Anum_pg_attribute_attinvalidnotnull - 1] = BoolGetDatum(attrs->attinvalidnotnull);
slot[slotCount]->tts_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(attrs->atthasdef);
slot[slotCount]->tts_values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(attrs->atthasmissing);
slot[slotCount]->tts_values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(attrs->attidentity);
@@ -2621,12 +2622,17 @@ AddRelationNewConstraints(Relation rel,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("not-null constraints are not supported on virtual generated columns"));
+ if (cdef->initially_valid)
+ Assert(!cdef->skip_validation);
+ else
+ Assert(cdef->skip_validation);
+
/*
* 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,
- is_local, cdef->is_no_inherit))
+ is_local, cdef->is_no_inherit, cdef->skip_validation))
continue;
/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..64f2e04859a 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -576,13 +576,14 @@ ChooseConstraintName(const char *name1, const char *name2,
* 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.
+ * if include_invalid is true, it may return an invalid not-null tuple.
*
* 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)
+findNotNullConstraintAttnum(Oid relid, AttrNumber attnum, bool include_invalid)
{
Relation pg_constraint;
HeapTuple conTup,
@@ -609,7 +610,7 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
*/
if (con->contype != CONSTRAINT_NOTNULL)
continue;
- if (!con->convalidated)
+ if (!con->convalidated && !include_invalid)
continue;
conkey = extractNotNullColumn(conTup);
@@ -631,9 +632,10 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
* 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.
+ * if include_invalid is true, it may return an invalid not-null tuple.
*/
HeapTuple
-findNotNullConstraint(Oid relid, const char *colname)
+findNotNullConstraint(Oid relid, const char *colname, bool include_invalid)
{
AttrNumber attnum;
@@ -641,7 +643,7 @@ findNotNullConstraint(Oid relid, const char *colname)
if (attnum <= InvalidAttrNumber)
return NULL;
- return findNotNullConstraintAttnum(relid, attnum);
+ return findNotNullConstraintAttnum(relid, attnum, include_invalid);
}
/*
@@ -729,11 +731,11 @@ extractNotNullColumn(HeapTuple constrTup)
*/
bool
AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
- bool is_local, bool is_no_inherit)
+ bool is_local, bool is_no_inherit, bool is_notvalid)
{
HeapTuple tup;
- tup = findNotNullConstraintAttnum(relid, attnum);
+ tup = findNotNullConstraintAttnum(relid, attnum, true);
if (HeapTupleIsValid(tup))
{
Relation pg_constraint;
@@ -753,6 +755,27 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"",
NameStr(conform->conname), get_rel_name(relid)));
+ /*
+ * Throw an error if an invalid NOT NULL constraint exists on the
+ * table column and an attempt is made to add another valid NOT NULL
+ * constraint.
+ */
+ if (is_notvalid && conform->convalidated)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot change VALID status of NOT NULL constraint \"%s\" on relation \"%s\"",
+ NameStr(conform->conname), get_rel_name(relid)),
+ errhint("You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint \"%s\"",
+ NameStr(conform->conname)));
+
+ if (!is_notvalid && !conform->convalidated)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot change VALID status of NOT NULL constraint \"%s\" on relation \"%s\"",
+ NameStr(conform->conname), get_rel_name(relid)),
+ errhint("You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint \"%s\"",
+ NameStr(conform->conname)));
+
if (!is_local)
{
if (pg_add_s16_overflow(conform->coninhcount, 1,
@@ -788,9 +811,10 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
* This is seldom needed, so we just scan pg_constraint each time.
*
* 'include_noinh' determines whether to include NO INHERIT constraints or not.
+ * 'include_notvalid' determines whether to include NO VALID constraints or not.
*/
List *
-RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
+RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh, bool include_notvalid)
{
List *notnulls = NIL;
Relation constrRel;
@@ -816,6 +840,9 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
if (conForm->connoinherit && !include_noinh)
continue;
+ if (!conForm->convalidated && !include_notvalid)
+ continue;
+
colnum = extractNotNullColumn(htup);
if (cooked)
@@ -830,7 +857,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
cooked->attnum = colnum;
cooked->expr = NULL;
cooked->is_enforced = true;
- cooked->skip_validation = false;
+ cooked->skip_validation = !conForm->convalidated;
cooked->is_local = true;
cooked->inhcount = 0;
cooked->is_no_inherit = conForm->connoinherit;
@@ -850,7 +877,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
constr->keys = list_make1(makeString(get_attname(relid, colnum,
false)));
constr->is_enforced = true;
- constr->skip_validation = false;
+ constr->skip_validation = !conForm->convalidated;
constr->initially_valid = true;
constr->is_no_inherit = conForm->connoinherit;
notnulls = lappend(notnulls, constr);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 129c97fdf28..b1828ec0945 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -410,6 +410,9 @@ static void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation
static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
char *constrName, HeapTuple contuple,
bool recurse, bool recursing, LOCKMODE lockmode);
+static void QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple, bool recurse, bool recursing,
+ LOCKMODE lockmode);
static int transformColumnNameList(Oid relId, List *colList,
int16 *attnums, Oid *atttypids, Oid *attcollids);
static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
@@ -2713,10 +2716,10 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
/*
* Request attnotnull on columns that have a not-null constraint
- * that's not marked NO INHERIT.
+ * that's not marked NO INHERIT. but we will include NOT VALID
*/
nnconstrs = RelationGetNotNullConstraints(RelationGetRelid(relation),
- true, false);
+ true, false, true);
foreach_ptr(CookedConstraint, cc, nnconstrs)
nncols = bms_add_member(nncols, cc->attnum);
@@ -7711,7 +7714,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
* Find the constraint that makes this column NOT NULL, and drop it.
* dropconstraint_internal() resets attnotnull.
*/
- conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
+ conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum, false);
if (conTup == NULL)
elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
colName, RelationGetRelationName(rel));
@@ -7729,6 +7732,45 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
return address;
}
+static void
+set_attinvalidnotnull(Relation rel, AttrNumber attnum,
+ bool attinvalidnotnull)
+{
+ Form_pg_attribute attr;
+
+ CheckAlterTableIsSafe(rel);
+
+ /*
+ * Exit quickly by testing attnotnull from the tupledesc's copy of the
+ * attribute.
+ */
+ attr = TupleDescAttr(RelationGetDescr(rel), attnum - 1);
+ if (attr->attisdropped)
+ return;
+
+ if (attr->attinvalidnotnull != attinvalidnotnull)
+ {
+ Relation attr_rel;
+ HeapTuple tuple;
+
+ attr_rel = table_open(AttributeRelationId, RowExclusiveLock);
+
+ tuple = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, RelationGetRelid(rel));
+
+ attr = (Form_pg_attribute) GETSTRUCT(tuple);
+ attr->attinvalidnotnull = attinvalidnotnull;
+
+ CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
+ CommandCounterIncrement();
+
+ table_close(attr_rel, RowExclusiveLock);
+ heap_freetuple(tuple);
+ }
+}
+
/*
* Helper to set pg_attribute.attnotnull if it isn't set, and to tell phase 3
* to verify it.
@@ -7845,7 +7887,7 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
colName, RelationGetRelationName(rel))));
/* See if there's already a constraint */
- tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
+ tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum, true);
if (HeapTupleIsValid(tuple))
{
Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
@@ -7880,6 +7922,19 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
conForm->conislocal = true;
changed = true;
}
+ else if (!conForm->convalidated)
+ {
+ /*
+ * Don't let a NO VALID constraint be changed into VALID.
+ * Only way to validate a not-nul constraint is through ALTER TABLE VALIDATE CONSTRAINT
+ */
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot change VALID status of NOT NULL constraint \"%s\" on relation \"%s\"",
+ NameStr(conForm->conname), get_rel_name(RelationGetRelid(rel))),
+ errhint("You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint \"%s\"",
+ NameStr(conForm->conname)));
+ }
if (changed)
{
@@ -9387,6 +9442,21 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
{
AlterTableCmd *newcmd;
Constraint *nnconstr;
+ HeapTuple tuple;
+
+ tuple = findNotNullConstraint(RelationGetRelid(rel), strVal(lfirst(lc)), true);
+ if (tuple != NULL)
+ {
+ Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
+ if (!conForm->convalidated)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("column \"%s\" of table \"%s\" is marked as NOT VALID NOT NULL constraint",
+ strVal(lfirst(lc)), RelationGetRelationName(rel)),
+ errhint("You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint \"%s\"",
+ NameStr(conForm->conname)));
+ heap_freetuple(tuple);
+ }
nnconstr = makeNotNullConstraint(lfirst(lc));
@@ -9765,9 +9835,12 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* 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)
+ if (constr->contype == CONSTR_NOTNULL && !constr->skip_validation)
set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
+ if (constr->contype == CONSTR_NOTNULL && constr->skip_validation)
+ set_attinvalidnotnull(rel, ccon->attnum, lockmode);
+
ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
}
@@ -12176,7 +12249,7 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
HeapTuple childtup;
Form_pg_constraint childcon;
- childtup = findNotNullConstraint(childoid, colName);
+ childtup = findNotNullConstraint(childoid, colName, false);
if (!childtup)
elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
colName, childoid);
@@ -12367,10 +12440,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
con = (Form_pg_constraint) GETSTRUCT(tuple);
if (con->contype != CONSTRAINT_FOREIGN &&
- con->contype != CONSTRAINT_CHECK)
+ con->contype != CONSTRAINT_CHECK &&
+ con->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
+ errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint or not-null constraint",
constrName, RelationGetRelationName(rel))));
if (!con->conenforced)
@@ -12389,6 +12463,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
QueueCheckConstraintValidation(wqueue, conrel, rel, constrName,
tuple, recurse, recursing, lockmode);
}
+ else if (con->contype == CONSTRAINT_NOTNULL)
+ {
+ QueueNNConstraintValidation(wqueue, conrel, rel,
+ tuple, recurse, recursing, lockmode);
+ }
ObjectAddressSet(address, ConstraintRelationId, con->oid);
}
@@ -12605,6 +12684,117 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
heap_freetuple(copyTuple);
}
+/*
+ * QueueNNConstraintValidation
+ *
+ * Add an entry to the wqueue to validate the given not-null constraint in
+ * Phase 3 and update the convalidated field in the pg_constraint catalog for
+ * the specified relation and all its inheriting children.
+ */
+static void
+QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple, bool recurse, bool recursing,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint con;
+ AlteredTableInfo *tab;
+ HeapTuple copyTuple;
+ Form_pg_constraint copy_con;
+ List *children = NIL;
+ AttrNumber attnum;
+ char *colname;
+
+ con = (Form_pg_constraint) GETSTRUCT(contuple);
+ Assert(con->contype == CONSTRAINT_NOTNULL);
+
+ attnum = extractNotNullColumn(contuple);
+
+ /*
+ * For constraints that aren't NO INHERIT, we must ensure that we only
+ * mark the constraint as validated on the parent if it's already
+ * validated on the children.
+ *
+ * If we're recursing, the parent has already done this, so skip it.
+ *
+ * We recurse before validating on the parent, to reduce risk of
+ * deadlocks.
+ */
+ if (!recursing && !con->connoinherit)
+ children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+
+ colname = get_attname(RelationGetRelid(rel), attnum, false);
+ foreach_oid(childoid, children)
+ {
+ Relation childrel;
+ HeapTuple contup;
+ Form_pg_constraint childcon;
+ char *conname;
+
+ if (childoid == RelationGetRelid(rel))
+ continue;
+
+ /*
+ * If we are told not to recurse, there had better not be any child
+ * tables, because we can't mark the constraint on the parent valid
+ * unless it is valid for all child tables.
+ */
+ if (!recurse)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+ errmsg("constraint must be validated on child tables too"));
+
+ /*
+ * The column on child might have a different attnum, so search by
+ * column name.
+ */
+ contup = findNotNullConstraint(childoid, colname, true);
+ if (!contup)
+ elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
+ colname, get_rel_name(childoid));
+ childcon = (Form_pg_constraint) GETSTRUCT(contup);
+ if (childcon->convalidated)
+ continue;
+
+ /* find_all_inheritors already got lock */
+ childrel = table_open(childoid, NoLock);
+ conname = pstrdup(NameStr(childcon->conname));
+
+ /* XXX improve ATExecValidateConstraint API to avoid double search */
+ ATExecValidateConstraint(wqueue, childrel, conname,
+ false, true, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->verify_new_notnull = true;
+
+ set_attnotnull(NULL, rel, attnum, lockmode);
+
+ /*
+ * Invalidate relcache so that others see the new validated constraint.
+ */
+ CacheInvalidateRelcache(rel);
+
+
+ /*
+ * Now update the catalogs, while we have the door open.
+ */
+ copyTuple = heap_copytuple(contuple);
+ copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+ copy_con->convalidated = true;
+ CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
+
+ /*
+ * Also flip attnotnull. call function with wqueue as NULL to
+ * bypass validation, as it has already been performed.
+ */
+ set_attinvalidnotnull(rel, attnum, false);
+
+ InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
+
+ heap_freetuple(copyTuple);
+}
+
/*
* transformColumnNameList - transform list of column names
*
@@ -13549,7 +13739,7 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
*/
if (con->contype == CONSTRAINT_NOTNULL)
{
- tuple = findNotNullConstraint(childrelid, colname);
+ tuple = findNotNullConstraint(childrelid, colname, false);
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
colname, RelationGetRelid(childrel));
@@ -16813,8 +17003,9 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
{
HeapTuple contup;
+ Assert(!parent_att->attinvalidnotnull);
contup = findNotNullConstraintAttnum(RelationGetRelid(parent_rel),
- parent_att->attnum);
+ parent_att->attnum, false);
if (HeapTupleIsValid(contup) &&
!((Form_pg_constraint) GETSTRUCT(contup))->connoinherit)
ereport(ERROR,
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index e9bd98c7738..06226150f94 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -93,6 +93,9 @@ static bool ExecCheckPermissionsModified(Oid relOid, Oid userid,
static void ExecCheckXactReadOnly(PlannedStmt *plannedstmt);
static void EvalPlanQualStart(EPQState *epqstate, Plan *planTree);
+static void ReportNotNullViolationError(ResultRelInfo *resultRelInfo,
+ TupleTableSlot *slot,
+ EState *estate, int attrChk);
/* end of local decls */
@@ -2071,57 +2074,21 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
if (att->attnotnull && slot_attisnull(slot, attrChk))
- {
- char *val_desc;
- Relation orig_rel = rel;
- TupleDesc orig_tupdesc = RelationGetDescr(rel);
+ ReportNotNullViolationError(resultRelInfo, slot, estate, attrChk);
+ }
+ }
- /*
- * If the tuple has been routed, it's been converted to the
- * partition's rowtype, which might differ from the root
- * table's. We must convert it back to the root table's
- * rowtype so that val_desc shown error message matches the
- * input tuple.
- */
- if (resultRelInfo->ri_RootResultRelInfo)
- {
- ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
- AttrMap *map;
+ if (constr->has_invalid_not_null)
+ {
+ int natts = tupdesc->natts;
+ int attrChk;
- tupdesc = RelationGetDescr(rootrel->ri_RelationDesc);
- /* a reverse map */
- map = build_attrmap_by_name_if_req(orig_tupdesc,
- tupdesc,
- false);
+ for (attrChk = 1; attrChk <= natts; attrChk++)
+ {
+ Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
- /*
- * Partition-specific slot's tupdesc can't be changed, so
- * allocate a new one.
- */
- if (map != NULL)
- slot = execute_attr_map_slot(map, slot,
- MakeTupleTableSlot(tupdesc, &TTSOpsVirtual));
- modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate),
- ExecGetUpdatedCols(rootrel, estate));
- rel = rootrel->ri_RelationDesc;
- }
- else
- modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, estate),
- ExecGetUpdatedCols(resultRelInfo, estate));
- val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
- slot,
- tupdesc,
- modifiedCols,
- 64);
-
- ereport(ERROR,
- (errcode(ERRCODE_NOT_NULL_VIOLATION),
- errmsg("null value in column \"%s\" of relation \"%s\" violates not-null constraint",
- NameStr(att->attname),
- RelationGetRelationName(orig_rel)),
- val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
- errtablecol(orig_rel, attrChk)));
- }
+ if (att->attinvalidnotnull && slot_attisnull(slot, attrChk))
+ ReportNotNullViolationError(resultRelInfo, slot, estate, attrChk);
}
}
@@ -2176,6 +2143,73 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
}
}
+
+/*
+ * Report a violation of a not-null constraint that was already detected.
+ */
+static void
+ReportNotNullViolationError(ResultRelInfo *resultRelInfo, TupleTableSlot *slot,
+ EState *estate, int attrChk)
+{
+ Bitmapset *modifiedCols;
+ char *val_desc;
+
+ Relation rel = resultRelInfo->ri_RelationDesc;
+ Relation orig_rel = rel;
+
+ TupleDesc tupdesc = RelationGetDescr(rel);
+ TupleDesc orig_tupdesc = RelationGetDescr(rel);
+ Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
+
+ Assert(attrChk > 0);
+
+ /*
+ * If the tuple has been routed, it's been converted to the partition's
+ * rowtype, which might differ from the root table's. We must convert it
+ * back to the root table's rowtype so that val_desc shown error message
+ * matches the input tuple.
+ */
+ if (resultRelInfo->ri_RootResultRelInfo)
+ {
+ ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
+ AttrMap *map;
+
+ tupdesc = RelationGetDescr(rootrel->ri_RelationDesc);
+ /* a reverse map */
+ map = build_attrmap_by_name_if_req(orig_tupdesc,
+ tupdesc,
+ false);
+
+ /*
+ * Partition-specific slot's tupdesc can't be changed, so allocate a
+ * new one.
+ */
+ if (map != NULL)
+ slot = execute_attr_map_slot(map, slot,
+ MakeTupleTableSlot(tupdesc, &TTSOpsVirtual));
+ modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate),
+ ExecGetUpdatedCols(rootrel, estate));
+ rel = rootrel->ri_RelationDesc;
+ }
+ else
+ modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, estate),
+ ExecGetUpdatedCols(resultRelInfo, estate));
+
+ val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
+ slot,
+ tupdesc,
+ modifiedCols,
+ 64);
+ ereport(ERROR,
+ errcode(ERRCODE_NOT_NULL_VIOLATION),
+ errmsg("null value in column \"%s\" of relation \"%s\" violates not-null constraint",
+ NameStr(att->attname),
+ RelationGetRelationName(orig_rel)),
+ val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
+ errtablecol(orig_rel, attrChk));
+}
+
+
/*
* ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs
* of the specified kind.
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..715eb51300b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4224,9 +4224,9 @@ ConstraintElem:
n->keys = list_make1(makeString($3));
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
- NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
- n->initially_valid = true;
+ n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
| UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 896a7f2c59b..49fa91ea139 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1089,6 +1089,11 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
ereport(ERROR,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("not-null constraints on partitioned tables cannot be NO INHERIT"));
+ /* XXX TODO, this can be done */
+ if (cxt->ispartitioned && constraint->skip_validation)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("not-null constraints on partitioned tables cannot be NO VALID"));
cxt->nnconstraints = lappend(cxt->nnconstraints, constraint);
break;
@@ -1291,7 +1296,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
List *lst;
lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false,
- true);
+ true, false);
cxt->nnconstraints = list_concat(cxt->nnconstraints, lst);
}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9f54a9e72b7..d5cd93e55c3 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -593,6 +593,8 @@ RelationBuildTupleDesc(Relation relation)
/* Update constraint/default info */
if (attp->attnotnull)
constr->has_not_null = true;
+ if (attp->attinvalidnotnull)
+ constr->has_invalid_not_null = true;
if (attp->attgenerated == ATTRIBUTE_GENERATED_STORED)
constr->has_generated_stored = true;
if (attp->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
@@ -678,6 +680,7 @@ RelationBuildTupleDesc(Relation relation)
* Set up constraint/default info
*/
if (constr->has_not_null ||
+ constr->has_invalid_not_null ||
constr->has_generated_stored ||
constr->has_generated_virtual ||
ndef > 0 ||
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..09f8f92a59c 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3114,7 +3114,8 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.conname, a.attname, c.connoinherit,\n"
- " c.conislocal, c.coninhcount <> 0\n"
+ " c.conislocal, c.coninhcount <> 0,\n"
+ " c.convalidated\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"
@@ -3137,14 +3138,16 @@ describeOneTableDetails(const char *schemaname,
{
bool islocal = PQgetvalue(result, i, 3)[0] == 't';
bool inherited = PQgetvalue(result, i, 4)[0] == 't';
+ bool validated = PQgetvalue(result, i, 5)[0] == 't';
- printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s",
+ printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s%s",
PQgetvalue(result, i, 0),
PQgetvalue(result, i, 1),
PQgetvalue(result, i, 2)[0] == 't' ?
" NO INHERIT" :
islocal && inherited ? _(" (local, inherited)") :
- inherited ? _(" (inherited)") : "");
+ inherited ? _(" (inherited)") : "",
+ !validated ? " NO VALID": "");
printTableAddFooter(&cont, buf.data);
}
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 396eeb7a0bb..cc619477b63 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -43,6 +43,7 @@ typedef struct TupleConstr
uint16 num_defval;
uint16 num_check;
bool has_not_null;
+ bool has_invalid_not_null;
bool has_generated_stored;
bool has_generated_virtual;
} TupleConstr;
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index deaa515fe53..9998b4abdef 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -120,6 +120,9 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
/* This flag represents the "NOT NULL" constraint */
bool attnotnull;
+ /* This flag represents the "NOT NULL NOT VALID" constraint */
+ bool attinvalidnotnull BKI_DEFAULT(f);
+
/* Has DEFAULT value or not */
bool atthasdef BKI_DEFAULT(f);
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 6da164e7e4d..f00a8e25d5d 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -259,14 +259,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 findNotNullConstraintAttnum(Oid relid, AttrNumber attnum, bool include_invalid);
+extern HeapTuple findNotNullConstraint(Oid relid, const char *colname, bool include_invalid);
extern HeapTuple findDomainNotNullConstraint(Oid typid);
extern AttrNumber extractNotNullColumn(HeapTuple constrTup);
extern bool AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
- bool is_local, bool is_no_inherit);
+ bool is_local, bool is_no_inherit, bool is_notvalid);
extern List *RelationGetNotNullConstraints(Oid relid, bool cooked,
- bool include_noinh);
+ bool include_noinh, bool include_notvalid);
extern void RemoveConstraintById(Oid conId);
extern void RenameConstraintById(Oid conId, const char *newname);
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..c8363a44523 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -896,6 +896,134 @@ Not-null constraints:
"foobar" NOT NULL "a"
DROP TABLE notnull_tbl1;
+-------tests for NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM pg_constraint
+WHERE conrelid = ANY($1)
+ORDER BY 1;
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1),(NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; --ok
+\d+ notnull_tbl1
+ Table "public.notnull_tbl1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | | | plain | |
+ b | integer | | | | plain | |
+Not-null constraints:
+ "nn" NOT NULL "a" NO VALID
+
+INSERT INTO notnull_tbl1 VALUES (NULL, 4); --error
+ERROR: null value in column "a" of relation "notnull_tbl1" violates not-null constraint
+DETAIL: Failing row contains (null, 4).
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1 ORDER BY a, b;
+ a | b
+-----+---
+ 100 | 1
+ 300 | 3
+(2 rows)
+
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+ERROR: column "a" of table "notnull_tbl1" is marked as NOT VALID NOT NULL constraint
+HINT: You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+-- INHERITS table having NOT VALID NOT NULL constraints.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+NOTICE: merging column "a" with inherited definition
+NOTICE: merging column "b" with inherited definition
+-- Child table NOT NULL constraints should be valid.
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+ conrelid | conname | convalidated | coninhcount
+--------------------+---------+--------------+-------------
+ notnull_tbl1 | nn | f | 0
+ notnull_tbl1_child | nn | t | 1
+(2 rows)
+
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+---now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1_child;
+DROP TABLE notnull_tbl1;
+-- Test the different Not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+ conrelid | conname | convalidated | coninhcount
+--------------+-----------+--------------+-------------
+ notnull_tbl1 | nn_parent | t | 0
+ notnull_chld | nn_child | t | 1
+(2 rows)
+
+DROP TABLE notnull_tbl1 CASCADE;
+NOTICE: drop cascades to table notnull_chld
+-- test to throw an error when trying to add another NOT NULL
+-- on the table column with INVALID NOT NULL constraint.
+CREATE TABLE notnull_tbl1 (a INTEGER);
+INSERT INTO notnull_tbl1 VALUES ( NULL );
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid;
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid no inherit; --error
+ERROR: cannot change NO INHERIT status of NOT NULL constraint "nn1" on relation "notnull_tbl1"
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error can't change not-null status
+ERROR: cannot change VALID status of NOT NULL constraint "nn1" on relation "notnull_tbl1"
+HINT: You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn1"
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; --error can't change not-null status
+ERROR: cannot change VALID status of NOT NULL constraint "nn1" on relation "notnull_tbl1"
+HINT: You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn1"
+DROP TABLE notnull_tbl1;
+-- Test invalid not null on inheritance table.
+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;
+NOTICE: merging definition of column "i" for child "inh_child"
+NOTICE: merging definition of column "i" for child "inh_grandchild"
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --error
+ERROR: cannot change VALID status of NOT NULL constraint "nn" on relation "inh_parent"
+HINT: You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+ERROR: cannot change VALID status of NOT NULL constraint "nn" on relation "inh_child"
+HINT: You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+ conrelid | conname | convalidated | coninhcount
+----------------+---------+--------------+-------------
+ inh_parent | nn | f | 0
+ inh_child | nn | f | 1
+ inh_grandchild | nn | f | 2
+(3 rows)
+
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY RANGE (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --error
+ERROR: not-null constraints on partitioned tables cannot be NO VALID
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a; --ok
+CREATE TABLE notnull_tbl1_1(a int, b int);
+INSERT INTO notnull_tbl1_1 DEFAULT VALUES;
+ALTER TABLE notnull_tbl1_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_1 FOR VALUES FROM (0) TO (10); --error
+ERROR: column "a" in child table "notnull_tbl1_1" must be marked NOT NULL
+TRUNCATE notnull_tbl1_1;
+ALTER TABLE notnull_tbl1_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_1 FOR VALUES FROM (0) TO (10); --ok
+DROP TABLE notnull_tbl1, notnull_tbl1_1;
+DEALLOCATE get_nnconstraint_info;
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 21ce4177de4..b708dded7d6 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -640,6 +640,98 @@ ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a;
\d+ notnull_tbl1
DROP TABLE notnull_tbl1;
+-------tests for NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM pg_constraint
+WHERE conrelid = ANY($1)
+ORDER BY 1;
+
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1),(NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; --ok
+\d+ notnull_tbl1
+
+INSERT INTO notnull_tbl1 VALUES (NULL, 4); --error
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1 ORDER BY a, b;
+
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+-- INHERITS table having NOT VALID NOT NULL constraints.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+-- Child table NOT NULL constraints should be valid.
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+---now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1_child;
+DROP TABLE notnull_tbl1;
+
+
+-- Test the different Not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+DROP TABLE notnull_tbl1 CASCADE;
+
+
+-- test to throw an error when trying to add another NOT NULL
+-- on the table column with INVALID NOT NULL constraint.
+CREATE TABLE notnull_tbl1 (a INTEGER);
+INSERT INTO notnull_tbl1 VALUES ( NULL );
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid;
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid no inherit; --error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error can't change not-null status
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; --error can't change not-null status
+DROP TABLE notnull_tbl1;
+
+
+-- Test invalid not null on inheritance table.
+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;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --error
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+
+
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY RANGE (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a; --ok
+
+CREATE TABLE notnull_tbl1_1(a int, b int);
+INSERT INTO notnull_tbl1_1 DEFAULT VALUES;
+ALTER TABLE notnull_tbl1_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_1 FOR VALUES FROM (0) TO (10); --error
+TRUNCATE notnull_tbl1_1;
+
+ALTER TABLE notnull_tbl1_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_1 FOR VALUES FROM (0) TO (10); --ok
+
+DROP TABLE notnull_tbl1, notnull_tbl1_1;
+
+DEALLOCATE get_nnconstraint_info;
+
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
+
+
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
--
2.34.1
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-13 08:37 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-17 11:14 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-17 11:23 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-20 09:54 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-20 12:19 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-20 14:59 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
@ 2025-03-20 15:52 ` Alvaro Herrera <[email protected]>
2025-03-21 14:36 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-20 15:52 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Ashutosh Bapat <[email protected]>; Rushabh Lathia <[email protected]>; pgsql-hackers
On 2025-Mar-20, jian he wrote:
> > Is it expected that a child may have VALID constraint but parent has
> > not valid constraint?
>
> but the MergeConstraintsIntoExisting logic is when
> ALTER TABLE ATTACH PARTITION,
> it expects the child table to also have an equivalent constraint
> definition on it.
> see MergeConstraintsIntoExisting:
> ereport(ERROR,
> (errcode(ERRCODE_DATATYPE_MISMATCH),
> errmsg("child table is missing constraint \"%s\"",
> NameStr(parent_con->conname))));
>
> So I decided not to support it.
> * partitioned table can not have NOT NULL NOT VALID.
I'm not sure I understand what you're saying here. I think a
partitioned table can be allowed to have a NOT VALID constraint. BUT if
it does, then all the children must have a corresponding constraint. The
constraint on children may be valid or may be invalid; the parent
doesn't force the issue one way or the other. But it has to exist.
Also, if you run ALTER TABLE VALIDATE CONSTRAINT on the parent, then at
that point you have to validate that all those corresponding constraints on
the children are also validated.
> * one column one NOT NULL, if you want to change status, it's not
> allowed, it will error out, give you hints.
I think we discussed this already. If you say
ALTER TABLE .. ALTER COLUMN .. SET NOT NULL
and an invalid constraint exists, then we can simply validate that
constraint.
However, if you say
ALTER TABLE .. ADD CONSTRAINT foobar NOT NULL col;
and an invalid constraint exists whose name is different from foobar,
then we should raise an error, because the user's requirement that the
constraint is named foobar cannot be satisfied. If the constraint is
named foobar, OR if the user doesn't specify a constraint name
ALTER TABLE .. ADD NOT NULL col;
then it's okay to validate that constraint without raising an error.
The important thing being that the user requirement is satisfied.
> * it can only be added using ALTER TABLE, not with CREATE TABLE (a
> warning will be issued)
I think the issue of adding constraints with NOT VALID during CREATE
TABLE is the topic of another thread. We already silently ignore the
NOT VALID markers during CREATE TABLE for other types of constraints.
> * pg_attribute.attinvalidnotnull meaning: this attnum has a
> (convalidated == false) NOT NULL pg_constraint entry to it.
> * if attnotnull is true, then attinvalidnotnull should be false.
> Conversely, if attinvalidnotnull is true, then attnotnull should be false.
I don't like this. It seems baroque and it will break existing
applications, because they currently query for attnotnull and assume
that inserting a null value will work, but in reality it will fail
because attinvalidnotnull is true (meaning an invalid constraint exists,
which prevents inserting nulls).
I think the idea should be: attnotnull means that a constraint exists;
it doesn't imply anything regarding the constraint being valid or not.
attnotnullvalid will indicate whether the constraint is valid; this
column can only be true if attnotnull is already true.
> * an invalid not-null cannot be used while adding a primary key.
Check.
> * if attinvalidnotnull is true, this column can not accept NULL values,
> but the existing column value may contain NULLs, we need to
> VALIDATE the not-null constraint to check if this column exists NULL
> values or not.
Check.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-13 08:37 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-17 11:14 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-17 11:23 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-20 09:54 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-20 12:19 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-20 14:59 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-20 15:52 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-03-21 14:36 ` jian he <[email protected]>
2025-03-21 14:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: jian he @ 2025-03-21 14:36 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: Ashutosh Bapat <[email protected]>; Rushabh Lathia <[email protected]>; pgsql-hackers
On Thu, Mar 20, 2025 at 11:53 PM Alvaro Herrera <[email protected]> wrote:
>
> On 2025-Mar-20, jian he wrote:
>
> > > Is it expected that a child may have VALID constraint but parent has
> > > not valid constraint?
> >
> > but the MergeConstraintsIntoExisting logic is when
> > ALTER TABLE ATTACH PARTITION,
> > it expects the child table to also have an equivalent constraint
> > definition on it.
> > see MergeConstraintsIntoExisting:
> > ereport(ERROR,
> > (errcode(ERRCODE_DATATYPE_MISMATCH),
> > errmsg("child table is missing constraint \"%s\"",
> > NameStr(parent_con->conname))));
> >
> > So I decided not to support it.
>
> > * partitioned table can not have NOT NULL NOT VALID.
>
> I'm not sure I understand what you're saying here. I think a
> partitioned table can be allowed to have a NOT VALID constraint. BUT if
> it does, then all the children must have a corresponding constraint. The
> constraint on children may be valid or may be invalid; the parent
> doesn't force the issue one way or the other. But it has to exist.
> Also, if you run ALTER TABLE VALIDATE CONSTRAINT on the parent, then at
> that point you have to validate that all those corresponding constraints on
> the children are also validated.
* if partitioned table have valid not-null, then partition with
invalid not-null can not attach to the partition tree.
if partitioned table have not valid not-null, we *can* attach a
valid not-null to the partition tree.
(inheritance hierarchy behaves the same).
this part does not require a lot of code changes.
However, to make the pg_dump working with partitioned table we need to
tweak AdjustNotNullInheritance a little bit.
>
> > * one column one NOT NULL, if you want to change status, it's not
> > allowed, it will error out, give you hints.
>
> I think we discussed this already. If you say
> ALTER TABLE .. ALTER COLUMN .. SET NOT NULL
> and an invalid constraint exists, then we can simply validate that
> constraint.
>
> However, if you say
> ALTER TABLE .. ADD CONSTRAINT foobar NOT NULL col;
> and an invalid constraint exists whose name is different from foobar,
> then we should raise an error, because the user's requirement that the
> constraint is named foobar cannot be satisfied. If the constraint is
> named foobar, OR if the user doesn't specify a constraint name
> ALTER TABLE .. ADD NOT NULL col;
> then it's okay to validate that constraint without raising an error.
> The important thing being that the user requirement is satisfied.
>
i changed this accordingly.
ALTER TABLE .. ALTER COLUMN .. SET NOT NULL
will validate not-null and set attnotnull, attinvalidnotnull accordingly.
> > * pg_attribute.attinvalidnotnull meaning: this attnum has a
> > (convalidated == false) NOT NULL pg_constraint entry to it.
> > * if attnotnull is true, then attinvalidnotnull should be false.
> > Conversely, if attinvalidnotnull is true, then attnotnull should be false.
>
> I don't like this. It seems baroque and it will break existing
> applications, because they currently query for attnotnull and assume
> that inserting a null value will work, but in reality it will fail
> because attinvalidnotnull is true (meaning an invalid constraint exists,
> which prevents inserting nulls).
>
> I think the idea should be: attnotnull means that a constraint exists;
> it doesn't imply anything regarding the constraint being valid or not.
> attnotnullvalid will indicate whether the constraint is valid; this
> column can only be true if attnotnull is already true.
>
i basically model NOT NULL NOT VALID == CHECK (x IS NOT NULL).
i think your idea may need more refactoring?
all the "if (attr->attnotnull" need change to "if (attr->attnotnull &&
attr->attnotnullvalid)"
or am i missing something?
Anyway, I will just share my idea first, and will explore your idea later.
in my attached patch, you will only create an not-null not valid
pg_constraint entry
If `if (constr->contype == CONSTR_NOTNULL && constr->skip_validation)`
in ATAddCheckNNConstraint conditions are satisfied.
imho, my approach is less bug-prone, almost no need to refactor current code.
we can even add a assert in InsertPgAttributeTuples:
Assert(!attrs->attinvalidnotnull);
new patch attached:
* Rushabh's pg_dump relation code incorporated into a single one patch.
* pg_dump works fine, mainly by tweak AdjustNotNullInheritance
following the same logic in MergeConstraintsIntoExisting.
if not do it, pg_constraint.conislocal meta info will be wrong.
Attachments:
[text/x-patch] v4-0001-NOT-NULL-NOT-VALID.patch (62.9K, ../../CACJufxGcH34SjCuee+7OJXjmYmWgceuOyo3U-fo30hpxP6vQ_g@mail.gmail.com/2-v4-0001-NOT-NULL-NOT-VALID.patch)
download | inline diff:
From 4c60297c019bebfd5cdcfbfdde3b4f57e168626c Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
Date: Fri, 21 Mar 2025 22:10:42 +0800
Subject: [PATCH v4 1/1] NOT NULL NOT VALID
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* it will logically be equivalent to CHECK(x IS NOT NULL) NOT VALID.
* it can only be added using ALTER TABLE. entry point is
ATAddCheckNNConstraint. when we add a constraint, it will either valid or not valid.
``
if (constr->contype == CONSTR_NOTNULL)
{
if (!constr->skip_validation)
set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
else
set_attinvalidnotnull(rel, ccon->attnum, true);
}
``
we will only change the state attinvalidnotnull in QueueNNConstraintValidation,
which we will validate the constraint, so attinvalidnotnull should be false.
when we insert a pg_attribute tuple, attinvalidnotnull will always false,
we can even add a assert in InsertPgAttributeTuples: Assert(!attrs->attinvalidnotnull);
* if attnotnull is true, then attinvalidnotnull should be false.
Conversely, if attinvalidnotnull is true, then attnotnull should be false.
* an invalid not-null cannot be used while adding a primary key.
* if attinvalidnotnull is true, this column can not accept NULL values,
but the existing column value may contain NULLs, we need to
VALIDATE the not-null constraint to check if this column exists NULL values or not.
* if partitioned table have valid not-null, then can not attach a not valid to partition tree.
if partitioned table have not valid not-null, we *can* attach a valid not-null to partition tree.
(inheritance hierarchy behave the same).
i tested loaclally, pg_dump will works fine with partitioned table, mainly by tweak
AdjustNotNullInheritance.
normal table not null not valid pg_dump also works fine.
discussion: https://postgr.es/m/CAGPqQf0KitkNack4F5CFkFi-9Dqvp29Ro=EpcWt=4_hs-Rt+bQ@mail.gmail.com
---
doc/src/sgml/catalogs.sgml | 10 +
doc/src/sgml/ref/alter_table.sgml | 2 +
src/backend/access/common/tupdesc.c | 11 ++
src/backend/bootstrap/bootstrap.c | 1 +
src/backend/catalog/heap.c | 11 +-
src/backend/catalog/pg_constraint.c | 57 +++++-
src/backend/commands/tablecmds.c | 214 ++++++++++++++++++++--
src/backend/executor/execMain.c | 130 ++++++++-----
src/backend/parser/gram.y | 4 +-
src/backend/parser/parse_utilcmd.c | 14 +-
src/backend/utils/cache/relcache.c | 3 +
src/bin/pg_dump/pg_dump.c | 155 +++++++++++++++-
src/bin/pg_dump/pg_dump.h | 1 +
src/bin/psql/describe.c | 9 +-
src/include/access/tupdesc.h | 1 +
src/include/catalog/pg_attribute.h | 3 +
src/include/catalog/pg_constraint.h | 10 +-
src/test/regress/expected/constraints.out | 169 +++++++++++++++++
src/test/regress/sql/constraints.sql | 114 ++++++++++++
19 files changed, 827 insertions(+), 92 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..299d2a46f4e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1264,6 +1264,16 @@
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>attinvalidnotnull</structfield> <type>bool</type>
+ </para>
+ <para>
+ This column has a attinvalidnotnull not-null constraint.
+ If <structfield>attnotnull</structfield> is true, this has to be false.
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>atthasdef</structfield> <type>bool</type>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 4f15b89a98f..75d90654275 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -243,6 +243,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
entire table; however, if a valid <literal>CHECK</literal> constraint is
found which proves no <literal>NULL</literal> can exist, then the
table scan is skipped.
+ If a column is marked as <literal>NOT NULL NOT VALID</literal>,
+ <literal>SET NOT NULL</literal> will change it to a validated <literal>NOT NULL</literal> constraint.
</para>
<para>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index ed2195f14b2..634cfddff23 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -252,6 +252,7 @@ CreateTupleDescCopy(TupleDesc tupdesc)
Form_pg_attribute att = TupleDescAttr(desc, i);
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -298,6 +299,7 @@ CreateTupleDescTruncatedCopy(TupleDesc tupdesc, int natts)
Form_pg_attribute att = TupleDescAttr(desc, i);
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -341,6 +343,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
TupleConstr *cpy = (TupleConstr *) palloc0(sizeof(TupleConstr));
cpy->has_not_null = constr->has_not_null;
+ cpy->has_invalid_not_null = constr->has_invalid_not_null;
cpy->has_generated_stored = constr->has_generated_stored;
cpy->has_generated_virtual = constr->has_generated_virtual;
@@ -418,6 +421,7 @@ TupleDescCopy(TupleDesc dst, TupleDesc src)
Form_pg_attribute att = TupleDescAttr(dst, i);
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -464,6 +468,7 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno,
/* since we're not copying constraints or defaults, clear these */
dstAtt->attnotnull = false;
+ dstAtt->attinvalidnotnull = false;
dstAtt->atthasdef = false;
dstAtt->atthasmissing = false;
dstAtt->attidentity = '\0';
@@ -613,6 +618,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
return false;
if (attr1->attnotnull != attr2->attnotnull)
return false;
+ if (attr1->attinvalidnotnull != attr2->attinvalidnotnull)
+ return false;
if (attr1->atthasdef != attr2->atthasdef)
return false;
if (attr1->attidentity != attr2->attidentity)
@@ -639,6 +646,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
return false;
if (constr1->has_not_null != constr2->has_not_null)
return false;
+ if (constr1->has_invalid_not_null != constr2->has_invalid_not_null)
+ return false;
if (constr1->has_generated_stored != constr2->has_generated_stored)
return false;
if (constr1->has_generated_virtual != constr2->has_generated_virtual)
@@ -841,6 +850,7 @@ TupleDescInitEntry(TupleDesc desc,
att->attndims = attdim;
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -904,6 +914,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
att->attndims = attdim;
att->attnotnull = false;
+ att->attinvalidnotnull = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 6db864892d0..9077ed46d33 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -582,6 +582,7 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
attrtypes[attnum]->atttypmod = -1;
attrtypes[attnum]->attislocal = true;
+ attrtypes[attnum]->attinvalidnotnull = false;
if (nullness == BOOTCOL_NULL_FORCE_NOT_NULL)
{
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..9b2315a27e7 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -753,6 +753,8 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
slot[slotCount]->tts_values[Anum_pg_attribute_attstorage - 1] = CharGetDatum(attrs->attstorage);
slot[slotCount]->tts_values[Anum_pg_attribute_attcompression - 1] = CharGetDatum(attrs->attcompression);
slot[slotCount]->tts_values[Anum_pg_attribute_attnotnull - 1] = BoolGetDatum(attrs->attnotnull);
+ slot[slotCount]->tts_values[Anum_pg_attribute_attinvalidnotnull - 1] = BoolGetDatum(attrs->attinvalidnotnull);
+ Assert(!attrs->attinvalidnotnull); /*test demo only*/
slot[slotCount]->tts_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(attrs->atthasdef);
slot[slotCount]->tts_values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(attrs->atthasmissing);
slot[slotCount]->tts_values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(attrs->attidentity);
@@ -2621,12 +2623,17 @@ AddRelationNewConstraints(Relation rel,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("not-null constraints are not supported on virtual generated columns"));
+ if (cdef->initially_valid)
+ Assert(!cdef->skip_validation);
+ else
+ Assert(cdef->skip_validation);
+
/*
* 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,
- is_local, cdef->is_no_inherit))
+ if (AdjustNotNullInheritance(rel, colnum,
+ is_local, cdef->is_no_inherit, cdef->skip_validation))
continue;
/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..9b4d0427877 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -576,13 +576,14 @@ ChooseConstraintName(const char *name1, const char *name2,
* 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.
+ * if include_invalid is true, it may return an invalid not-null tuple.
*
* 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)
+findNotNullConstraintAttnum(Oid relid, AttrNumber attnum, bool include_invalid)
{
Relation pg_constraint;
HeapTuple conTup,
@@ -609,7 +610,7 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
*/
if (con->contype != CONSTRAINT_NOTNULL)
continue;
- if (!con->convalidated)
+ if (!con->convalidated && !include_invalid)
continue;
conkey = extractNotNullColumn(conTup);
@@ -631,9 +632,10 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
* 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.
+ * if include_invalid is true, it may return an invalid not-null tuple.
*/
HeapTuple
-findNotNullConstraint(Oid relid, const char *colname)
+findNotNullConstraint(Oid relid, const char *colname, bool include_invalid)
{
AttrNumber attnum;
@@ -641,7 +643,7 @@ findNotNullConstraint(Oid relid, const char *colname)
if (attnum <= InvalidAttrNumber)
return NULL;
- return findNotNullConstraintAttnum(relid, attnum);
+ return findNotNullConstraintAttnum(relid, attnum, include_invalid);
}
/*
@@ -728,12 +730,13 @@ extractNotNullColumn(HeapTuple constrTup)
* nothing if it's already true; otherwise we increment coninhcount by 1.
*/
bool
-AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
- bool is_local, bool is_no_inherit)
+AdjustNotNullInheritance(Relation rel, AttrNumber attnum,
+ bool is_local, bool is_no_inherit, bool is_notvalid)
{
HeapTuple tup;
+ Oid relid = RelationGetRelid(rel);
- tup = findNotNullConstraintAttnum(relid, attnum);
+ tup = findNotNullConstraintAttnum(relid, attnum, true);
if (HeapTupleIsValid(tup))
{
Relation pg_constraint;
@@ -753,6 +756,36 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"",
NameStr(conform->conname), get_rel_name(relid)));
+ /*
+ * Throw an error if we want change the NOT NULL constraint from NOT
+ * VALID to VALID.
+ */
+ if (!is_notvalid && !conform->convalidated)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot change VALID status of NOT NULL constraint \"%s\" on relation \"%s\"",
+ NameStr(conform->conname), get_rel_name(relid)),
+ errhint("You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint \"%s\"",
+ NameStr(conform->conname)));
+
+ /* VALID TO NOT VALID seems no usage, so just issue a warning */
+ if (is_notvalid && conform->convalidated)
+ {
+ if (is_local)
+ ereport(WARNING,
+ errmsg("NOT NULL constraint \"%s\" on relation \"%s\" is already valid",
+ NameStr(conform->conname), get_rel_name(relid)));
+ else if (rel->rd_rel->relispartition)
+ {
+ /*
+ * In case of partitions, an inherited not null constraint
+ * is never considered local. See MergeConstraintsIntoExisting also.
+ */
+ conform->conislocal = false;
+ changed =true;
+ }
+ }
+
if (!is_local)
{
if (pg_add_s16_overflow(conform->coninhcount, 1,
@@ -788,9 +821,10 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
* This is seldom needed, so we just scan pg_constraint each time.
*
* 'include_noinh' determines whether to include NO INHERIT constraints or not.
+ * 'include_notvalid' determines whether to include NO VALID constraints or not.
*/
List *
-RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
+RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh, bool include_notvalid)
{
List *notnulls = NIL;
Relation constrRel;
@@ -816,6 +850,9 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
if (conForm->connoinherit && !include_noinh)
continue;
+ if (!conForm->convalidated && !include_notvalid)
+ continue;
+
colnum = extractNotNullColumn(htup);
if (cooked)
@@ -830,7 +867,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
cooked->attnum = colnum;
cooked->expr = NULL;
cooked->is_enforced = true;
- cooked->skip_validation = false;
+ cooked->skip_validation = !conForm->convalidated;
cooked->is_local = true;
cooked->inhcount = 0;
cooked->is_no_inherit = conForm->connoinherit;
@@ -850,7 +887,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
constr->keys = list_make1(makeString(get_attname(relid, colnum,
false)));
constr->is_enforced = true;
- constr->skip_validation = false;
+ constr->skip_validation = !conForm->convalidated;
constr->initially_valid = true;
constr->is_no_inherit = conForm->connoinherit;
notnulls = lappend(notnulls, constr);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 129c97fdf28..94acf07b63d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -410,6 +410,9 @@ static void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation
static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
char *constrName, HeapTuple contuple,
bool recurse, bool recursing, LOCKMODE lockmode);
+static void QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple, bool recurse, bool recursing,
+ LOCKMODE lockmode);
static int transformColumnNameList(Oid relId, List *colList,
int16 *attnums, Oid *atttypids, Oid *attcollids);
static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
@@ -2713,10 +2716,10 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
/*
* Request attnotnull on columns that have a not-null constraint
- * that's not marked NO INHERIT.
+ * that's not marked NO INHERIT. but we will include NOT VALID
*/
nnconstrs = RelationGetNotNullConstraints(RelationGetRelid(relation),
- true, false);
+ true, false, true);
foreach_ptr(CookedConstraint, cc, nnconstrs)
nncols = bms_add_member(nncols, cc->attnum);
@@ -7711,7 +7714,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
* Find the constraint that makes this column NOT NULL, and drop it.
* dropconstraint_internal() resets attnotnull.
*/
- conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
+ conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum, false);
if (conTup == NULL)
elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
colName, RelationGetRelationName(rel));
@@ -7729,6 +7732,44 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
return address;
}
+/*
+ * update pg_attribute.attinvalidnotnull
+*/
+static void
+set_attinvalidnotnull(Relation rel, AttrNumber attnum,
+ bool attinvalidnotnull)
+{
+ Form_pg_attribute attr;
+
+ CheckAlterTableIsSafe(rel);
+
+ attr = TupleDescAttr(RelationGetDescr(rel), attnum - 1);
+ if (attr->attisdropped)
+ return;
+
+ if (attr->attinvalidnotnull != attinvalidnotnull)
+ {
+ Relation attr_rel;
+ HeapTuple tuple;
+
+ attr_rel = table_open(AttributeRelationId, RowExclusiveLock);
+
+ tuple = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, RelationGetRelid(rel));
+
+ attr = (Form_pg_attribute) GETSTRUCT(tuple);
+ attr->attinvalidnotnull = attinvalidnotnull;
+
+ CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
+ CommandCounterIncrement();
+
+ table_close(attr_rel, RowExclusiveLock);
+ heap_freetuple(tuple);
+ }
+}
+
/*
* Helper to set pg_attribute.attnotnull if it isn't set, and to tell phase 3
* to verify it.
@@ -7845,7 +7886,7 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
colName, RelationGetRelationName(rel))));
/* See if there's already a constraint */
- tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
+ tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum, true);
if (HeapTupleIsValid(tuple))
{
Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
@@ -7880,6 +7921,16 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
conForm->conislocal = true;
changed = true;
}
+ else if (!conForm->convalidated)
+ {
+
+ /*
+ * flip attnotnull and convalidated, and also validate the
+ * constraint.
+ */
+ return ATExecValidateConstraint(wqueue, rel, NameStr(conForm->conname),
+ recurse, recursing, lockmode);
+ }
if (changed)
{
@@ -9387,6 +9438,22 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
{
AlterTableCmd *newcmd;
Constraint *nnconstr;
+ HeapTuple tuple;
+
+ tuple = findNotNullConstraint(RelationGetRelid(rel), strVal(lfirst(lc)), true);
+ if (tuple != NULL)
+ {
+ Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
+ if (!conForm->convalidated)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("primary key require an validated NOT NULL constraint"),
+ errdetail("Column \"%s\" of table \"%s\" is marked as NOT VALID NOT NULL constraint",
+ strVal(lfirst(lc)), RelationGetRelationName(rel)),
+ errhint("You may try ALTER TABLE VALIDATE CONSTRAINT to validate constraint \"%s\"",
+ NameStr(conForm->conname)));
+ heap_freetuple(tuple);
+ }
nnconstr = makeNotNullConstraint(lfirst(lc));
@@ -9762,11 +9829,19 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
constr->conname = ccon->name;
/*
- * If adding a not-null constraint, set the pg_attribute flag and tell
+ * If adding a valid not-null constraint, set the pg_attribute flag and tell
* phase 3 to verify existing rows, if needed.
+ * However if we are adding a invalid not-null constraint, then we only
+ * need set the pg_attribute.attinvalidnotnull. phase 3 don't need to
+ * verify existing rows.
*/
if (constr->contype == CONSTR_NOTNULL)
- set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
+ {
+ if (!constr->skip_validation)
+ set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
+ else
+ set_attinvalidnotnull(rel, ccon->attnum, true);
+ }
ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
}
@@ -12176,7 +12251,7 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
HeapTuple childtup;
Form_pg_constraint childcon;
- childtup = findNotNullConstraint(childoid, colName);
+ childtup = findNotNullConstraint(childoid, colName, false);
if (!childtup)
elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
colName, childoid);
@@ -12367,10 +12442,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
con = (Form_pg_constraint) GETSTRUCT(tuple);
if (con->contype != CONSTRAINT_FOREIGN &&
- con->contype != CONSTRAINT_CHECK)
+ con->contype != CONSTRAINT_CHECK &&
+ con->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
+ errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint or not-null constraint",
constrName, RelationGetRelationName(rel))));
if (!con->conenforced)
@@ -12389,6 +12465,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
QueueCheckConstraintValidation(wqueue, conrel, rel, constrName,
tuple, recurse, recursing, lockmode);
}
+ else if (con->contype == CONSTRAINT_NOTNULL)
+ {
+ QueueNNConstraintValidation(wqueue, conrel, rel,
+ tuple, recurse, recursing, lockmode);
+ }
ObjectAddressSet(address, ConstraintRelationId, con->oid);
}
@@ -12605,6 +12686,116 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
heap_freetuple(copyTuple);
}
+/*
+ * QueueNNConstraintValidation
+ *
+ * Add an entry to the wqueue to validate the given not-null constraint in
+ * Phase 3 and update the convalidated field in the pg_constraint catalog for
+ * the specified relation and all its inheriting children.
+ */
+static void
+QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple, bool recurse, bool recursing,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint con;
+ AlteredTableInfo *tab;
+ HeapTuple copyTuple;
+ Form_pg_constraint copy_con;
+ List *children = NIL;
+ AttrNumber attnum;
+ char *colname;
+
+ con = (Form_pg_constraint) GETSTRUCT(contuple);
+ Assert(con->contype == CONSTRAINT_NOTNULL);
+
+ attnum = extractNotNullColumn(contuple);
+
+ /*
+ * For constraints that aren't NO INHERIT, we must ensure that we only
+ * mark the constraint as validated on the parent if it's already
+ * validated on the children.
+ *
+ * If we're recursing, the parent has already done this, so skip it.
+ *
+ * We recurse before validating on the parent, to reduce risk of
+ * deadlocks.
+ */
+ if (!recursing && !con->connoinherit)
+ children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+
+ colname = get_attname(RelationGetRelid(rel), attnum, false);
+ foreach_oid(childoid, children)
+ {
+ Relation childrel;
+ HeapTuple contup;
+ Form_pg_constraint childcon;
+ char *conname;
+
+ if (childoid == RelationGetRelid(rel))
+ continue;
+
+ /*
+ * If we are told not to recurse, there had better not be any child
+ * tables, because we can't mark the constraint on the parent valid
+ * unless it is valid for all child tables.
+ */
+ if (!recurse)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+ errmsg("constraint must be validated on child tables too"));
+
+ /*
+ * The column on child might have a different attnum, so search by
+ * column name.
+ */
+ contup = findNotNullConstraint(childoid, colname, true);
+ if (!contup)
+ elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
+ colname, get_rel_name(childoid));
+ childcon = (Form_pg_constraint) GETSTRUCT(contup);
+ if (childcon->convalidated)
+ continue;
+
+ /* find_all_inheritors already got lock */
+ childrel = table_open(childoid, NoLock);
+ conname = pstrdup(NameStr(childcon->conname));
+
+ /* XXX improve ATExecValidateConstraint API to avoid double search */
+ ATExecValidateConstraint(wqueue, childrel, conname,
+ false, true, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->verify_new_notnull = true;
+
+ set_attnotnull(NULL, rel, attnum, lockmode);
+
+ /*
+ * Invalidate relcache so that others see the new validated constraint.
+ */
+ CacheInvalidateRelcache(rel);
+
+ /*
+ * Now update the catalogs, while we have the door open.
+ */
+ copyTuple = heap_copytuple(contuple);
+ copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+ copy_con->convalidated = true;
+ CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
+
+ /*
+ * set attinvalidnotnull to false. set_attnotnull already tell phase 3 to
+ * vertify not-null status.
+ */
+ set_attinvalidnotnull(rel, attnum, false);
+
+ InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
+
+ heap_freetuple(copyTuple);
+}
+
/*
* transformColumnNameList - transform list of column names
*
@@ -13549,7 +13740,7 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
*/
if (con->contype == CONSTRAINT_NOTNULL)
{
- tuple = findNotNullConstraint(childrelid, colname);
+ tuple = findNotNullConstraint(childrelid, colname, false);
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
colname, RelationGetRelid(childrel));
@@ -16813,8 +17004,9 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
{
HeapTuple contup;
+ Assert(!parent_att->attinvalidnotnull);
contup = findNotNullConstraintAttnum(RelationGetRelid(parent_rel),
- parent_att->attnum);
+ parent_att->attnum, false);
if (HeapTupleIsValid(contup) &&
!((Form_pg_constraint) GETSTRUCT(contup))->connoinherit)
ereport(ERROR,
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index e9bd98c7738..06226150f94 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -93,6 +93,9 @@ static bool ExecCheckPermissionsModified(Oid relOid, Oid userid,
static void ExecCheckXactReadOnly(PlannedStmt *plannedstmt);
static void EvalPlanQualStart(EPQState *epqstate, Plan *planTree);
+static void ReportNotNullViolationError(ResultRelInfo *resultRelInfo,
+ TupleTableSlot *slot,
+ EState *estate, int attrChk);
/* end of local decls */
@@ -2071,57 +2074,21 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
if (att->attnotnull && slot_attisnull(slot, attrChk))
- {
- char *val_desc;
- Relation orig_rel = rel;
- TupleDesc orig_tupdesc = RelationGetDescr(rel);
+ ReportNotNullViolationError(resultRelInfo, slot, estate, attrChk);
+ }
+ }
- /*
- * If the tuple has been routed, it's been converted to the
- * partition's rowtype, which might differ from the root
- * table's. We must convert it back to the root table's
- * rowtype so that val_desc shown error message matches the
- * input tuple.
- */
- if (resultRelInfo->ri_RootResultRelInfo)
- {
- ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
- AttrMap *map;
+ if (constr->has_invalid_not_null)
+ {
+ int natts = tupdesc->natts;
+ int attrChk;
- tupdesc = RelationGetDescr(rootrel->ri_RelationDesc);
- /* a reverse map */
- map = build_attrmap_by_name_if_req(orig_tupdesc,
- tupdesc,
- false);
+ for (attrChk = 1; attrChk <= natts; attrChk++)
+ {
+ Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
- /*
- * Partition-specific slot's tupdesc can't be changed, so
- * allocate a new one.
- */
- if (map != NULL)
- slot = execute_attr_map_slot(map, slot,
- MakeTupleTableSlot(tupdesc, &TTSOpsVirtual));
- modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate),
- ExecGetUpdatedCols(rootrel, estate));
- rel = rootrel->ri_RelationDesc;
- }
- else
- modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, estate),
- ExecGetUpdatedCols(resultRelInfo, estate));
- val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
- slot,
- tupdesc,
- modifiedCols,
- 64);
-
- ereport(ERROR,
- (errcode(ERRCODE_NOT_NULL_VIOLATION),
- errmsg("null value in column \"%s\" of relation \"%s\" violates not-null constraint",
- NameStr(att->attname),
- RelationGetRelationName(orig_rel)),
- val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
- errtablecol(orig_rel, attrChk)));
- }
+ if (att->attinvalidnotnull && slot_attisnull(slot, attrChk))
+ ReportNotNullViolationError(resultRelInfo, slot, estate, attrChk);
}
}
@@ -2176,6 +2143,73 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
}
}
+
+/*
+ * Report a violation of a not-null constraint that was already detected.
+ */
+static void
+ReportNotNullViolationError(ResultRelInfo *resultRelInfo, TupleTableSlot *slot,
+ EState *estate, int attrChk)
+{
+ Bitmapset *modifiedCols;
+ char *val_desc;
+
+ Relation rel = resultRelInfo->ri_RelationDesc;
+ Relation orig_rel = rel;
+
+ TupleDesc tupdesc = RelationGetDescr(rel);
+ TupleDesc orig_tupdesc = RelationGetDescr(rel);
+ Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
+
+ Assert(attrChk > 0);
+
+ /*
+ * If the tuple has been routed, it's been converted to the partition's
+ * rowtype, which might differ from the root table's. We must convert it
+ * back to the root table's rowtype so that val_desc shown error message
+ * matches the input tuple.
+ */
+ if (resultRelInfo->ri_RootResultRelInfo)
+ {
+ ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
+ AttrMap *map;
+
+ tupdesc = RelationGetDescr(rootrel->ri_RelationDesc);
+ /* a reverse map */
+ map = build_attrmap_by_name_if_req(orig_tupdesc,
+ tupdesc,
+ false);
+
+ /*
+ * Partition-specific slot's tupdesc can't be changed, so allocate a
+ * new one.
+ */
+ if (map != NULL)
+ slot = execute_attr_map_slot(map, slot,
+ MakeTupleTableSlot(tupdesc, &TTSOpsVirtual));
+ modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate),
+ ExecGetUpdatedCols(rootrel, estate));
+ rel = rootrel->ri_RelationDesc;
+ }
+ else
+ modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, estate),
+ ExecGetUpdatedCols(resultRelInfo, estate));
+
+ val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
+ slot,
+ tupdesc,
+ modifiedCols,
+ 64);
+ ereport(ERROR,
+ errcode(ERRCODE_NOT_NULL_VIOLATION),
+ errmsg("null value in column \"%s\" of relation \"%s\" violates not-null constraint",
+ NameStr(att->attname),
+ RelationGetRelationName(orig_rel)),
+ val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
+ errtablecol(orig_rel, attrChk));
+}
+
+
/*
* ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs
* of the specified kind.
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..715eb51300b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4224,9 +4224,9 @@ ConstraintElem:
n->keys = list_make1(makeString($3));
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
- NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
- n->initially_valid = true;
+ n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
| UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 896a7f2c59b..59d689640c6 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -329,6 +329,18 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
cd->is_not_null = true;
break;
}
+ if (!cxt.isforeign && !nn->initially_valid)
+ {
+ nn->initially_valid = true;
+ nn->skip_validation = false;
+ /*
+ * we do not support NOT NULL NOT VALID for column constraint,
+ * nn->conname should not be NULL.
+ */
+ ereport(WARNING,
+ errcode(ERRCODE_WARNING),
+ errmsg("Ignoring NOT VALID flag for NOT NULL constraint \"%s\"", nn->conname));
+ }
}
/*
@@ -1291,7 +1303,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
List *lst;
lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false,
- true);
+ true, false);
cxt->nnconstraints = list_concat(cxt->nnconstraints, lst);
}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9f54a9e72b7..d5cd93e55c3 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -593,6 +593,8 @@ RelationBuildTupleDesc(Relation relation)
/* Update constraint/default info */
if (attp->attnotnull)
constr->has_not_null = true;
+ if (attp->attinvalidnotnull)
+ constr->has_invalid_not_null = true;
if (attp->attgenerated == ATTRIBUTE_GENERATED_STORED)
constr->has_generated_stored = true;
if (attp->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
@@ -678,6 +680,7 @@ RelationBuildTupleDesc(Relation relation)
* Set up constraint/default info
*/
if (constr->has_not_null ||
+ constr->has_invalid_not_null ||
constr->has_generated_stored ||
constr->has_generated_virtual ||
ndef > 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 428ed2d60fc..1d67dcac124 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8884,6 +8884,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQExpBuffer q = createPQExpBuffer();
PQExpBuffer tbloids = createPQExpBuffer();
PQExpBuffer checkoids = createPQExpBuffer();
+ PQExpBuffer invalidnotnulloids = createPQExpBuffer();
PGresult *res;
int ntups;
int curtblindx;
@@ -8903,6 +8904,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
int i_notnull_name;
int i_notnull_noinherit;
int i_notnull_islocal;
+ int i_notnull_validated;
int i_attoptions;
int i_attcollation;
int i_attcompression;
@@ -8998,12 +9000,14 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
appendPQExpBufferStr(q,
"co.conname AS notnull_name,\n"
"co.connoinherit AS notnull_noinherit,\n"
- "co.conislocal AS notnull_islocal,\n");
+ "co.conislocal AS notnull_islocal,\n"
+ "co.convalidated as notnull_validated,\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");
+ "a.attislocal AS notnull_islocal,\n"
+ "true as notnull_validated\n");
if (fout->remoteVersion >= 140000)
appendPQExpBufferStr(q,
@@ -9078,6 +9082,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
i_notnull_name = PQfnumber(res, "notnull_name");
i_notnull_noinherit = PQfnumber(res, "notnull_noinherit");
i_notnull_islocal = PQfnumber(res, "notnull_islocal");
+ i_notnull_validated = PQfnumber(res, "notnull_validated");
i_attoptions = PQfnumber(res, "attoptions");
i_attcollation = PQfnumber(res, "attcollation");
i_attcompression = PQfnumber(res, "attcompression");
@@ -9094,6 +9099,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
* r is handled by the inner loop.
*/
curtblindx = -1;
+ appendPQExpBufferChar(invalidnotnulloids, '{');
for (int r = 0; r < ntups;)
{
Oid attrelid = atooid(PQgetvalue(res, r, i_attrelid));
@@ -9145,6 +9151,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
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->notnull_validated = (bool *) pg_malloc(numatts * sizeof(bool));
tbinfo->attrdefs = (AttrDefInfo **) pg_malloc(numatts * sizeof(AttrDefInfo *));
hasdefaults = false;
@@ -9168,12 +9175,33 @@ 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_validated[j] = (PQgetvalue(res, r, i_notnull_validated)[0] == 't');
- /* Handle not-null constraint name and flags */
- determineNotNullFlags(fout, res, r,
- tbinfo, j,
- i_notnull_name, i_notnull_noinherit,
- i_notnull_islocal);
+ if (tbinfo->notnull_validated[j])
+ {
+ /* Handle not-null constraint name and flags */
+ determineNotNullFlags(fout, res, r,
+ tbinfo, j,
+ i_notnull_name, i_notnull_noinherit,
+ i_notnull_islocal);
+ }
+ else
+ {
+ tbinfo->notnull_constrs[j] = NULL;
+
+ /*
+ * if column notnull_validated is false, it can also mean that
+ * column don't have not-null constraint at all. if column have
+ * NOT NULL NOT VALID then Add the entry into invalidnotnulloids
+ * list so it can dumped as seperate constraint.
+ */
+ if (!PQgetisnull(res, r, i_notnull_name))
+ {
+ if (invalidnotnulloids->len > 1)
+ appendPQExpBufferChar(invalidnotnulloids, ',');
+ appendPQExpBuffer(invalidnotnulloids, "%u", tbinfo->dobj.catId.oid);
+ }
+ }
tbinfo->attoptions[j] = pg_strdup(PQgetvalue(res, r, i_attoptions));
tbinfo->attcollation[j] = atooid(PQgetvalue(res, r, i_attcollation));
@@ -9193,6 +9221,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
appendPQExpBuffer(tbloids, "%u", tbinfo->dobj.catId.oid);
}
}
+ appendPQExpBufferChar(invalidnotnulloids, '}');
PQclear(res);
@@ -9326,6 +9355,110 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQclear(res);
}
+ /*
+ * Get info about table NOT NULL NOT VALID constraints. This is skipped for
+ * a data-only dump, as it is only needed for table schemas.
+ */
+ if (dopt->dumpSchema && invalidnotnulloids->len > 2)
+ {
+ ConstraintInfo *constrs;
+ int numConstrs;
+ int i_tableoid;
+ int i_oid;
+ int i_conrelid;
+ int i_conname;
+ int i_consrc;
+ int i_conislocal;
+ int i_convalidated;
+
+ pg_log_info("finding table invalid not null constraints");
+
+ resetPQExpBuffer(q);
+ appendPQExpBuffer(q,
+ "SELECT c.tableoid, c.oid, conrelid, conname, "
+ "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
+ "conislocal, convalidated "
+ "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+ "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
+ "WHERE contype = 'n' AND NOT convalidated "
+ "ORDER BY c.conrelid, c.conname",
+ invalidnotnulloids->data);
+
+ res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
+
+ numConstrs = PQntuples(res);
+ constrs = (ConstraintInfo *) pg_malloc(numConstrs * sizeof(ConstraintInfo));
+
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_oid = PQfnumber(res, "oid");
+ i_conrelid = PQfnumber(res, "conrelid");
+ i_conname = PQfnumber(res, "conname");
+ i_consrc = PQfnumber(res, "consrc");
+ i_conislocal = PQfnumber(res, "conislocal");
+ i_convalidated = PQfnumber(res, "convalidated");
+
+ /* As above, this loop iterates once per table, not once per row */
+ curtblindx = -1;
+ for (int j = 0; j < numConstrs;)
+ {
+ Oid conrelid = atooid(PQgetvalue(res, j, i_conrelid));
+ TableInfo *tbinfo = NULL;
+ int numcons;
+
+ /* Count rows for this table */
+ for (numcons = 1; numcons < numConstrs - j; numcons++)
+ if (atooid(PQgetvalue(res, j + numcons, i_conrelid)) != conrelid)
+ break;
+
+ /*
+ * Locate the associated TableInfo; we rely on tblinfo[] being in
+ * OID order.
+ */
+ while (++curtblindx < numTables)
+ {
+ tbinfo = &tblinfo[curtblindx];
+ if (tbinfo->dobj.catId.oid == conrelid)
+ break;
+ }
+ if (curtblindx >= numTables)
+ pg_fatal("unrecognized table OID %u", conrelid);
+
+ tbinfo->checkexprs = constrs + j;
+
+ for (int c = 0; c < numcons; c++, j++)
+ {
+ bool validated = PQgetvalue(res, j, i_convalidated)[0] == 't';
+
+ Assert(!validated);
+ constrs[j].dobj.objType = DO_CONSTRAINT;
+ constrs[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
+ constrs[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&constrs[j].dobj);
+ constrs[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_conname));
+ constrs[j].dobj.namespace = tbinfo->dobj.namespace;
+ constrs[j].contable = tbinfo;
+ constrs[j].condomain = NULL;
+ constrs[j].contype = 'n';
+ constrs[j].condef = pg_strdup(PQgetvalue(res, j, i_consrc));
+ constrs[j].confrelid = InvalidOid;
+ constrs[j].conindex = 0;
+ constrs[j].condeferrable = false;
+ constrs[j].condeferred = false;
+ constrs[j].conislocal = (PQgetvalue(res, j, i_conislocal)[0] == 't');
+
+ /*
+ * NOT NULL NOT VALID must dumped separately as an ALTER TABLE
+ * ADD CONSTRAINT entry. because CREATE TABLE can not create
+ * invalid not null constraint.
+ */
+ constrs[j].separate = !validated;
+
+ constrs[j].dobj.dump = tbinfo->dobj.dump;
+ }
+ }
+ PQclear(res);
+ }
+
/*
* Get info about table CHECK constraints. This is skipped for a
* data-only dump, as it is only needed for table schemas.
@@ -16620,6 +16753,8 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
if (print_notnull)
{
+ Assert(tbinfo->notnull_validated[j]);
+
if (tbinfo->notnull_constrs[j][0] == '\0')
appendPQExpBufferStr(q, " NOT NULL");
else
@@ -17939,9 +18074,9 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
.createStmt = q->data,
.dropStmt = delq->data));
}
- else if (coninfo->contype == 'c' && tbinfo)
+ else if ((coninfo->contype == 'c' || coninfo->contype == 'n') && tbinfo)
{
- /* CHECK constraint on a table */
+ /* CHECK and INVALID NOT NULL constraint on a table */
/* Ignore if not to be dumped separately, or if it was inherited */
if (coninfo->separate && coninfo->conislocal)
@@ -17965,7 +18100,7 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
ARCHIVE_OPTS(.tag = tag,
.namespace = tbinfo->dobj.namespace->dobj.name,
.owner = tbinfo->rolname,
- .description = "CHECK CONSTRAINT",
+ .description = coninfo->contype == 'c' ? "CHECK CONSTRAINT" : "CONSTRAINT",
.section = SECTION_POST_DATA,
.createStmt = q->data,
.dropStmt = delq->data));
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bbdb30b5f54..e0c9bbd64a2 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -367,6 +367,7 @@ typedef struct _tableInfo
* (pre-v17) */
bool *notnull_noinh; /* NOT NULL is NO INHERIT */
bool *notnull_islocal; /* true if NOT NULL has local definition */
+ bool *notnull_validated; /* true if NOT NULL is validated */
struct _attrDefInfo **attrdefs; /* DEFAULT expressions */
struct _constraintInfo *checkexprs; /* CHECK constraints */
bool needs_override; /* has GENERATED ALWAYS AS IDENTITY */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..09f8f92a59c 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3114,7 +3114,8 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.conname, a.attname, c.connoinherit,\n"
- " c.conislocal, c.coninhcount <> 0\n"
+ " c.conislocal, c.coninhcount <> 0,\n"
+ " c.convalidated\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"
@@ -3137,14 +3138,16 @@ describeOneTableDetails(const char *schemaname,
{
bool islocal = PQgetvalue(result, i, 3)[0] == 't';
bool inherited = PQgetvalue(result, i, 4)[0] == 't';
+ bool validated = PQgetvalue(result, i, 5)[0] == 't';
- printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s",
+ printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s%s",
PQgetvalue(result, i, 0),
PQgetvalue(result, i, 1),
PQgetvalue(result, i, 2)[0] == 't' ?
" NO INHERIT" :
islocal && inherited ? _(" (local, inherited)") :
- inherited ? _(" (inherited)") : "");
+ inherited ? _(" (inherited)") : "",
+ !validated ? " NO VALID": "");
printTableAddFooter(&cont, buf.data);
}
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 396eeb7a0bb..cc619477b63 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -43,6 +43,7 @@ typedef struct TupleConstr
uint16 num_defval;
uint16 num_check;
bool has_not_null;
+ bool has_invalid_not_null;
bool has_generated_stored;
bool has_generated_virtual;
} TupleConstr;
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index deaa515fe53..9998b4abdef 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -120,6 +120,9 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
/* This flag represents the "NOT NULL" constraint */
bool attnotnull;
+ /* This flag represents the "NOT NULL NOT VALID" constraint */
+ bool attinvalidnotnull BKI_DEFAULT(f);
+
/* Has DEFAULT value or not */
bool atthasdef BKI_DEFAULT(f);
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 6da164e7e4d..fe27dde16fa 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -259,14 +259,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 findNotNullConstraintAttnum(Oid relid, AttrNumber attnum, bool include_invalid);
+extern HeapTuple findNotNullConstraint(Oid relid, const char *colname, bool include_invalid);
extern HeapTuple findDomainNotNullConstraint(Oid typid);
extern AttrNumber extractNotNullColumn(HeapTuple constrTup);
-extern bool AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
- bool is_local, bool is_no_inherit);
+extern bool AdjustNotNullInheritance(Relation rel, AttrNumber attnum,
+ bool is_local, bool is_no_inherit, bool is_notvalid);
extern List *RelationGetNotNullConstraints(Oid relid, bool cooked,
- bool include_noinh);
+ bool include_noinh, bool include_notvalid);
extern void RemoveConstraintById(Oid conId);
extern void RenameConstraintById(Oid conId, const char *newname);
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..d57d7e09984 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -896,6 +896,175 @@ Not-null constraints:
"foobar" NOT NULL "a"
DROP TABLE notnull_tbl1;
+-------tests for NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM pg_constraint
+WHERE conrelid = ANY($1)
+ORDER BY 1;
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1),(NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; --ok
+\d+ notnull_tbl1
+ Table "public.notnull_tbl1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | | | plain | |
+ b | integer | | | | plain | |
+Not-null constraints:
+ "nn" NOT NULL "a" NO VALID
+
+INSERT INTO notnull_tbl1 VALUES (NULL, 4); --error, invalid not-null constraint forbiden new null values
+ERROR: null value in column "a" of relation "notnull_tbl1" violates not-null constraint
+DETAIL: Failing row contains (null, 4).
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid no inherit; --error
+ERROR: cannot change NO INHERIT status of NOT NULL constraint "nn" on relation "notnull_tbl1"
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error can't change not-null status
+ERROR: cannot change VALID status of NOT NULL constraint "nn" on relation "notnull_tbl1"
+HINT: You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; --validate it then error out
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1 ORDER BY a, b;
+ a | b
+-----+---
+ 100 | 1
+ 300 | 3
+(2 rows)
+
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+ERROR: primary key require an validated NOT NULL constraint
+DETAIL: Column "a" of table "notnull_tbl1" is marked as NOT VALID NOT NULL constraint
+HINT: You may try ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+-- however child table NOT NULL constraints should be valid.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+NOTICE: merging column "a" with inherited definition
+NOTICE: merging column "b" with inherited definition
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+ conrelid | conname | convalidated | coninhcount
+--------------------+---------+--------------+-------------
+ notnull_tbl1 | nn | f | 0
+ notnull_tbl1_child | nn | t | 1
+(2 rows)
+
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+---now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1_child;
+DROP TABLE notnull_tbl1;
+-- Test the different not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+-- parent have valid not null constraint then child table cannot have invalid one
+CREATE TABLE notnull_chld0 (a int);
+ALTER TABLE notnull_chld0 ADD CONSTRAINT nn_chld0 NOT NULL a;
+ALTER TABLE notnull_tbl1 INHERIT notnull_chld0; --error
+ERROR: column "a" in child table "notnull_tbl1" must be marked NOT NULL
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+ conrelid | conname | convalidated | coninhcount
+--------------+-----------+--------------+-------------
+ notnull_tbl1 | nn_parent | t | 0
+ notnull_chld | nn_child | t | 1
+(2 rows)
+
+DROP TABLE notnull_chld0;
+DROP TABLE notnull_chld;
+DROP TABLE notnull_tbl1;
+-- Test invalid not null on inheritance table.
+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;
+NOTICE: merging definition of column "i" for child "inh_child"
+NOTICE: merging definition of column "i" for child "inh_grandchild"
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i; --error
+ERROR: cannot change VALID status of NOT NULL constraint "nn" on relation "inh_parent"
+HINT: You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+ERROR: cannot change VALID status of NOT NULL constraint "nn" on relation "inh_child"
+HINT: You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+ conrelid | conname | convalidated | coninhcount
+----------------+---------+--------------+-------------
+ inh_parent | nn | f | 0
+ inh_child | nn | f | 1
+ inh_grandchild | nn | f | 2
+(3 rows)
+
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --ok
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be valid now.
+ conrelid | conname | convalidated | coninhcount
+----------------+---------+--------------+-------------
+ inh_parent | nn | t | 0
+ inh_child | nn | t | 1
+ inh_grandchild | nn | t | 2
+(3 rows)
+
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+DROP TABLE notnull_tbl1;
+ERROR: table "notnull_tbl1" does not exist
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --ok
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES IN (1,2);
+CREATE TABLE notnull_tbl1_2(a int, CONSTRAINT nn2 NOT NULL a, b int);
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_2 FOR VALUES IN (3,4);
+CREATE TABLE notnull_tbl1_3(a int, b int);
+INSERT INTO notnull_tbl1_3 values(NULL,1);
+ALTER TABLE notnull_tbl1_3 add CONSTRAINT nn3 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_3 FOR VALUES IN (NULL,5);
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+ conrelid | conname | convalidated | coninhcount
+----------------+-------------+--------------+-------------
+ notnull_tbl1 | notnull_con | f | 0
+ notnull_tbl1_1 | notnull_con | t | 1
+ notnull_tbl1_2 | nn2 | t | 1
+ notnull_tbl1_3 | nn3 | f | 1
+(4 rows)
+
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --error, table already have null values
+ERROR: column "a" of relation "notnull_tbl1_3" contains null values
+ALTER TABLE notnull_tbl1_3 VALIDATE CONSTRAINT nn3; --error
+ERROR: column "a" of relation "notnull_tbl1_3" contains null values
+TRUNCATE notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --OK
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+ conrelid | conname | convalidated | coninhcount
+----------------+-------------+--------------+-------------
+ notnull_tbl1 | notnull_con | t | 0
+ notnull_tbl1_1 | notnull_con | t | 1
+ notnull_tbl1_2 | nn2 | t | 1
+ notnull_tbl1_3 | nn3 | t | 1
+(4 rows)
+
+DROP TABLE notnull_tbl1;
+-----partitioned table have not-null, then the partitions can not be NOT NULL NOT VALID.
+CREATE TABLE pp_nn (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE pp_nn ADD CONSTRAINT pp_nn_notnull NOT NULL a;
+CREATE TABLE pp_nn_1(a int, b int);
+ALTER TABLE pp_nn_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --error
+ERROR: column "a" in child table "pp_nn_1" must be marked NOT NULL
+ALTER TABLE pp_nn_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --ok
+DROP TABLE pp_nn;
+DEALLOCATE get_nnconstraint_info;
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 21ce4177de4..976e8e6941c 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -640,6 +640,120 @@ ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a;
\d+ notnull_tbl1
DROP TABLE notnull_tbl1;
+-------tests for NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM pg_constraint
+WHERE conrelid = ANY($1)
+ORDER BY 1;
+
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1),(NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; --ok
+\d+ notnull_tbl1
+
+INSERT INTO notnull_tbl1 VALUES (NULL, 4); --error, invalid not-null constraint forbiden new null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid no inherit; --error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error can't change not-null status
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; --validate it then error out
+
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1 ORDER BY a, b;
+
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+-- however child table NOT NULL constraints should be valid.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+---now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1_child;
+DROP TABLE notnull_tbl1;
+
+
+-- Test the different not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+
+-- parent have valid not null constraint then child table cannot have invalid one
+CREATE TABLE notnull_chld0 (a int);
+ALTER TABLE notnull_chld0 ADD CONSTRAINT nn_chld0 NOT NULL a;
+ALTER TABLE notnull_tbl1 INHERIT notnull_chld0; --error
+
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+DROP TABLE notnull_chld0;
+DROP TABLE notnull_chld;
+DROP TABLE notnull_tbl1;
+
+
+-- Test invalid not null on inheritance table.
+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;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i; --error
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --ok
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be valid now.
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+
+
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+DROP TABLE notnull_tbl1;
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --ok
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES IN (1,2);
+CREATE TABLE notnull_tbl1_2(a int, CONSTRAINT nn2 NOT NULL a, b int);
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_2 FOR VALUES IN (3,4);
+
+CREATE TABLE notnull_tbl1_3(a int, b int);
+INSERT INTO notnull_tbl1_3 values(NULL,1);
+ALTER TABLE notnull_tbl1_3 add CONSTRAINT nn3 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_3 FOR VALUES IN (NULL,5);
+
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --error, table already have null values
+ALTER TABLE notnull_tbl1_3 VALIDATE CONSTRAINT nn3; --error
+
+TRUNCATE notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --OK
+
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+DROP TABLE notnull_tbl1;
+
+-----partitioned table have not-null, then the partitions can not be NOT NULL NOT VALID.
+CREATE TABLE pp_nn (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE pp_nn ADD CONSTRAINT pp_nn_notnull NOT NULL a;
+
+CREATE TABLE pp_nn_1(a int, b int);
+ALTER TABLE pp_nn_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --error
+ALTER TABLE pp_nn_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --ok
+DROP TABLE pp_nn;
+
+DEALLOCATE get_nnconstraint_info;
+
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
+
+
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
--
2.34.1
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-13 08:37 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-17 11:14 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-17 11:23 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-20 09:54 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-20 12:19 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-20 14:59 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-20 15:52 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 14:36 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
@ 2025-03-21 14:50 ` Alvaro Herrera <[email protected]>
0 siblings, 0 replies; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-21 14:50 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Ashutosh Bapat <[email protected]>; Rushabh Lathia <[email protected]>; pgsql-hackers
On 2025-Mar-21, jian he wrote:
> * if partitioned table have valid not-null, then partition with
> invalid not-null can not attach to the partition tree.
Correct.
> if partitioned table have not valid not-null, we *can* attach a
> valid not-null to the partition tree.
Also correct.
> (inheritance hierarchy behaves the same).
Good -- it should! :-)
> this part does not require a lot of code changes.
> However, to make the pg_dump working with partitioned table we need to
> tweak AdjustNotNullInheritance a little bit.
Hmm, well, modifying a function to suite what we need it to do is part
of code patching :-)
> i changed this accordingly.
> ALTER TABLE .. ALTER COLUMN .. SET NOT NULL
> will validate not-null and set attnotnull, attinvalidnotnull accordingly.
Okay.
> i basically model NOT NULL NOT VALID == CHECK (x IS NOT NULL).
> i think your idea may need more refactoring?
> all the "if (attr->attnotnull" need change to "if (attr->attnotnull &&
> attr->attnotnullvalid)"
> or am i missing something?
In some places, yes we will need to change like that. However, many
places do not need to change like that. In particular, (most?) client
applications do not necessarily need that change, and to me, that's the
most important part, because we do not control external applications,
and --as I said upthread-- we do not have the luxury of breaking them.
> Anyway, I will just share my idea first, and will explore your idea later.
Thank you.
> in my attached patch, you will only create an not-null not valid
> pg_constraint entry
> If `if (constr->contype == CONSTR_NOTNULL && constr->skip_validation)`
> in ATAddCheckNNConstraint conditions are satisfied.
>
>
> imho, my approach is less bug-prone, almost no need to refactor current code.
I'll give this a look ... probably won't have time today ... however,
IMO the consideration of external applications (ORMs, LibreOffice, admin
GUIs, etc) not breaking is the most important thing to keep in mind.
You can go over the code found by codesearch.debian.net when searching
for `attnotnull` to see the sort of code that would be affected.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"¿Qué importan los años? Lo que realmente importa es comprobar que
a fin de cuentas la mejor edad de la vida es estar vivo" (Mafalda)
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-03-21 15:26 ` Robert Haas <[email protected]>
2025-03-21 18:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
1 sibling, 1 reply; 40+ messages in thread
From: Robert Haas @ 2025-03-21 15:26 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On Wed, Mar 12, 2025 at 2:20 PM Alvaro Herrera <[email protected]> wrote:
> Taking a step back after discussing this with some colleagues, I need to
> contradict what I said at the start of this thread. There's a worry
> that changing pg_attribute.attnotnull in the way I initially suggested
> might not be such a great idea after all. I did a quick search using
> codesearch.debian.net for applications reading that column and thinking
> about how they would react to this change; I think in the end it's going
> to be quite disastrous. We would break a vast number of these apps, and
> there are probably countless other apps and frameworks that we would
> also break. Everybody would hate us forever. Upgrading to Postgres 18
> would become as bad an experience as the drastic change of implicit
> casts to text in 8.3. Nothing else in the intervening 17 years strikes
> me as so problematic as this change would be.
I don't agree with this conclusion. The 8.3 casting changes were
problematic because any piece of SQL you'd ever written could have
problems. This change will only break queries that look at the
attnotnull column. While there may be quite a few of those, it can't
possibly be of the same order. I think it's routine that changing the
name or type of system catalog columns breaks things for a few people
(e.g. procpid->pid, or relistemp->relpersistence) and we sometimes get
complaints about that, but at least you can grep for it and it's
mostly going to affect admin tools rather than all the queries
everywhere.
That's not to say that adding a second bool column instead of changing
the existing column's data type is necessarily the wrong way to go.
But I think you're overestimating the blast radius by quite a lot.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
@ 2025-03-21 18:04 ` Alvaro Herrera <[email protected]>
2025-03-24 01:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-24 14:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
0 siblings, 2 replies; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-21 18:04 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On 2025-Mar-21, Robert Haas wrote:
> I don't agree with this conclusion.
Uhm.
> The 8.3 casting changes were problematic because any piece of SQL
> you'd ever written could have problems.
Okay, this much I agree with.
> This change will only break queries that look at the attnotnull
> column. While there may be quite a few of those, it can't possibly be
> of the same order. I think it's routine that changing the name or type
> of system catalog columns breaks things for a few people (e.g.
> procpid->pid, or relistemp->relpersistence) and we sometimes get
> complaints about that, but at least you can grep for it and it's
> mostly going to affect admin tools rather than all the queries
> everywhere.
In several of the cases that I checked, the application just tests the
returned value for boolean truth. If we change the column from boolean
to char, they would stop working properly because both the 't' and the
'f' char values would test as true. But suppose we were to rename the
column; that would cause developers to have to examine the code to
determine how to react. That might even be good, because we're end up
in a situation were no application uses outdated assumptions about
nullness in a column. However, consider the rationale given in
https://postgr.es/m/[email protected]
that removing attndims would break PHP -- after that discussion, we
decided against removing the column, even though it's completely
useless, because we don't want to break PHP. You know, removing
attnotnull would break PHP in exactly the same way, or maybe in some
worse way. I don't see how can we reach a different conclusion for this
change that for that one.
> That's not to say that adding a second bool column instead of changing
> the existing column's data type is necessarily the wrong way to go.
> But I think you're overestimating the blast radius by quite a lot.
I am just going by some truth established by previous discussion.
If we agree to remove attnotnull or to change the way it works, then we
can also remove attndims.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-21 18:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-03-24 01:27 ` jian he <[email protected]>
2025-03-27 19:25 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
1 sibling, 1 reply; 40+ messages in thread
From: jian he @ 2025-03-24 01:27 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: Robert Haas <[email protected]>; Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
hi.
you may like the attached. it's based on your idea: attnotnullvalid.
I came across a case, not sure if it's a bug.
CREATE TABLE ttchk (a INTEGER);
ALTER TABLE ttchk ADD CONSTRAINT cc check (a is NOT NULL) NOT VALID;
CREATE TABLE ttchk_child(a INTEGER) INHERITS(ttchk);
ttchk_child's constraint cc will default to valid,
but pg_dump && pg_restore will make ttchk_child's constraint invalid.
since it's an existing behavior, so not-null constraint will align with it.
--------------------------------------------------------------------
-----the following text is copied from the commit message------------
NOT NULL NOT VALID
* TODO: In doc/src/sgml/ref/alter_table.sgml, under the
<title>Compatibility</title> section,
clarify how the "NOT NULL NOT VALID" syntax conforms with the standard.
* TODO: Should CREATE TABLE LIKE copy an existing invalid not-null
constraint to the new table,
and if so, the new table's not-null will be marked as valid.
description entry of pg_attribute.attnotnullvalid:
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>attnotnullvalid</structfield> <type>bool</type>
+ </para>
+ <para>
+ The not-null constraint validity status of the column.
+ If true, it means this column has a valid not-null constraint,
+ false means this column doesn't have a not-null constraint or
has an unvalidated one.
+ If <structfield>attnotnull</structfield> is false, this must be false.
</para></entry>
* attnotnull means that a not-null constraint exists; it doesn't imply anything
regarding the constraint being valid or not.
attnotnullvalid will indicate whether the constraint is valid; this column can
only be true if attnotnull is already true.
attnotnullvalid only added to FormData_pg_attribute, didn't add to
CompactAttribute.
mainly because invalid not-null is not being commonly used.
TupleDesc->TupleConstr->has_not_null now also represents invalid not-null
constraint.
* For table in pg_catalog schema, if that column attnotnull attribute is true,
then attnotnullvalid attribute is also true. Similarly, if
attnotnull is false,
then attnotnullvalid is false. I added an SQL check at the end of
src/test/regress/sql/constraints.sql (not sure it's necessary)
* CREATE TABLE specifying not valid not-null constraint will be set to valid,
a warning is issued within function transformCreateStmt.
that means InsertPgAttributeTuples can not insert attribute
that is (attnotnull && !attnotnullvalid).
I added an Assert in InsertPgAttributeTuples.
(also added to other places, to demo i didn't mess something, maybe
it's necessary).
* table rewrite won't validate invalid not-null constraint, that is aligned
with check constraint.
* attnotnullvalid mainly changed in these two places:
1. ATAddCheckNNConstraint, if you specified "NOT NULL NOT VALID", it
will change
it from false to false, but will set attnotnull to true.
2. QueueNNConstraintValidation, subroutine of ATExecValidateConstraint.
when validing an not valid not-null constraint, toggle it from
false to true,
also set attnotnull to true.
* A partitioned table can have an invalid NOT NULL constraint while its
partitions have a valid one, but not the other way around.
but pg_dump/pg_restore may not preserve the constraint name properly, but
that's fine for not-null constraint, i think.
* regular table invalid not null constraint pg_dump also works fine.
Attachments:
[text/x-patch] v5-0001-NOT-NULL-NOT-VALID.patch (67.8K, ../../CACJufxECVsdWSC4J0wo2LF-+QoacsfX_Scv-NGzQxWjzPF1coA@mail.gmail.com/2-v5-0001-NOT-NULL-NOT-VALID.patch)
download | inline diff:
From fc4bf954772d25dfbf60774429d875f78e4fd69e Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
Date: Mon, 24 Mar 2025 09:21:10 +0800
Subject: [PATCH v5 1/1] NOT NULL NOT VALID
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* TODO: In doc/src/sgml/ref/alter_table.sgml, under the <title>Compatibility</title> section,
clarify how the "NOT NULL NOT VALID" syntax conforms with the standard.
* TODO: Should CREATE TABLE LIKE copy an existing invalid not-null constraint to the new table,
and if so, the new table's not-null will be marked as valid.
* attnotnull means that a not-null constraint exists; it doesn't imply anything
regarding the constraint being valid or not.
attnotnullvalid will indicate whether the constraint is valid; this column can
only be true if attnotnull is already true.
attnotnullvalid only added to FormData_pg_attribute, didn't add to CompactAttribute.
mainly because invalid not-null is not being commonly used.
TupleDesc->TupleConstr->has_not_null now also represents invalid not-null
constraint.
* For table in pg_catalog schema, if that column attnotnull attribute is true,
then attnotnullvalid attribute is also true. Similarly, if attnotnull is false,
then attnotnullvalid is false. I added an SQL check at the end of
src/test/regress/sql/constraints.sql (not sure it's necessary)
* CREATE TABLE specifying not valid not-null constraint will be set to valid,
a warning is issued within function transformCreateStmt.
that means InsertPgAttributeTuples can not insert attribute
that is (attnotnull && !attnotnullvalid).
I added an Assert in InsertPgAttributeTuples.
* table rewrite won't validate invalid not-null constraint, that is aligned
with check constraint.
* attnotnullvalid, two places toggled it from true to false.
1. ATAddCheckNNConstraint, if you specified "NOT NULL NOT VALID", it will change
it from false to false, but will set attnotnull to true.
2. QueueNNConstraintValidation, subroutine of ATExecValidateConstraint.
when validing an not valid not-null constraint, toggle it from false to true,
also set attnotnull to true.
* A partitioned table can have an invalid NOT NULL constraint while its
partitions have a valid one, but not the other way around.
but pg_dump/pg_restore may not preserve the constraint name properly, but
that's fine for not-null constraint.
* regular table invalid not null constraint pg_dump also works fine.
discussion: https://postgr.es/m/CAGPqQf0KitkNack4F5CFkFi-9Dqvp29Ro=EpcWt=4_hs-Rt+bQ@mail.gmail.com
---
contrib/postgres_fdw/postgres_fdw.c | 1 +
doc/src/sgml/catalogs.sgml | 14 +-
doc/src/sgml/ref/alter_table.sgml | 8 +-
src/backend/access/common/tupdesc.c | 8 +
src/backend/bootstrap/bootstrap.c | 9 +
src/backend/catalog/genbki.pl | 2 +
src/backend/catalog/heap.c | 20 +-
src/backend/catalog/pg_constraint.c | 57 +++++-
src/backend/commands/tablecmds.c | 222 ++++++++++++++++++++--
src/backend/executor/execMain.c | 1 +
src/backend/optimizer/util/plancat.c | 6 +-
src/backend/parser/gram.y | 4 +-
src/backend/parser/parse_utilcmd.c | 16 +-
src/backend/utils/cache/catcache.c | 1 +
src/backend/utils/cache/relcache.c | 6 +-
src/bin/pg_dump/pg_dump.c | 157 ++++++++++++++-
src/bin/pg_dump/pg_dump.h | 3 +-
src/bin/psql/describe.c | 9 +-
src/include/access/tupdesc.h | 2 +-
src/include/catalog/pg_attribute.h | 3 +
src/include/catalog/pg_constraint.h | 10 +-
src/test/regress/expected/constraints.out | 183 ++++++++++++++++++
src/test/regress/sql/constraints.sql | 125 ++++++++++++
23 files changed, 810 insertions(+), 57 deletions(-)
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 6beae0fa37f..92dd1afd47f 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -5572,6 +5572,7 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
" LEFT JOIN pg_attribute a ON "
" attrelid = c.oid AND attnum > 0 "
" AND NOT attisdropped "
+ " AND attnotnullvalid "
" LEFT JOIN pg_attrdef ad ON "
" adrelid = c.oid AND adnum = attnum ");
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..4f42c5bdc32 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1260,7 +1260,19 @@
<structfield>attnotnull</structfield> <type>bool</type>
</para>
<para>
- This column has a not-null constraint.
+ This column has a possibly unvalidated not-null constraint
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>attnotnullvalid</structfield> <type>bool</type>
+ </para>
+ <para>
+ The not-null constraint validity status of the column.
+ If true, it means this column has a valid not-null constraint,
+ false means this column doesn't have a not-null constraint or has an unvalidated one.
+ If <structfield>attnotnull</structfield> is false, this must be false.
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 4f15b89a98f..9a2c0f5bfa0 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -243,6 +243,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
entire table; however, if a valid <literal>CHECK</literal> constraint is
found which proves no <literal>NULL</literal> can exist, then the
table scan is skipped.
+ If a column has a invalid not-null constraint, <literal>SET NOT NULL</literal>
+ will change it to a validated not-null constraint.
</para>
<para>
@@ -458,8 +460,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<para>
This form adds a new constraint to a table using the same constraint
syntax as <link linkend="sql-createtable"><command>CREATE TABLE</command></link>, plus the option <literal>NOT
- VALID</literal>, which is currently only allowed for foreign key
- and CHECK constraints.
+ VALID</literal>, which is currently only allowed for foreign key,
+ CHECK constraints and not-null constraints.
</para>
<para>
@@ -586,7 +588,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>VALIDATE CONSTRAINT</literal></term>
<listitem>
<para>
- This form validates a foreign key or check constraint that was
+ This form validates a foreign key or check constraint or not-null constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
satisfied. Nothing happens if the constraint is already marked valid.
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index ed2195f14b2..86cb23d58ab 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -252,6 +252,7 @@ CreateTupleDescCopy(TupleDesc tupdesc)
Form_pg_attribute att = TupleDescAttr(desc, i);
att->attnotnull = false;
+ att->attnotnullvalid = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -298,6 +299,7 @@ CreateTupleDescTruncatedCopy(TupleDesc tupdesc, int natts)
Form_pg_attribute att = TupleDescAttr(desc, i);
att->attnotnull = false;
+ att->attnotnullvalid = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -418,6 +420,7 @@ TupleDescCopy(TupleDesc dst, TupleDesc src)
Form_pg_attribute att = TupleDescAttr(dst, i);
att->attnotnull = false;
+ att->attnotnullvalid = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -464,6 +467,7 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno,
/* since we're not copying constraints or defaults, clear these */
dstAtt->attnotnull = false;
+ dstAtt->attnotnullvalid = false;
dstAtt->atthasdef = false;
dstAtt->atthasmissing = false;
dstAtt->attidentity = '\0';
@@ -613,6 +617,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
return false;
if (attr1->attnotnull != attr2->attnotnull)
return false;
+ if (attr1->attnotnullvalid != attr2->attnotnullvalid)
+ return false;
if (attr1->atthasdef != attr2->atthasdef)
return false;
if (attr1->attidentity != attr2->attidentity)
@@ -841,6 +847,7 @@ TupleDescInitEntry(TupleDesc desc,
att->attndims = attdim;
att->attnotnull = false;
+ att->attnotnullvalid = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -904,6 +911,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
att->attndims = attdim;
att->attnotnull = false;
+ att->attnotnullvalid = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 6db864892d0..b18dc1d5ee6 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -615,6 +615,15 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
attrtypes[attnum]->attnotnull = true;
}
}
+
+ /*
+ * If a system catalog column is attnotnull then attnotnullvalid is true,
+ * otherwise false.
+ */
+ if (attrtypes[attnum]->attnotnull == true)
+ attrtypes[attnum]->attnotnullvalid = true;
+ else
+ attrtypes[attnum]->attnotnullvalid = false;
}
diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index df3231fcd41..b8651a9d865 100644
--- a/src/backend/catalog/genbki.pl
+++ b/src/backend/catalog/genbki.pl
@@ -986,6 +986,8 @@ sub morph_row_for_pgattr
$row->{attnotnull} = 'f';
}
+ $row->{attnotnullvalid} = $row->{attnotnull} eq 't' ? 't' : 'f';
+
Catalog::AddDefaultValues($row, $pgattr_schema, 'pg_attribute');
return;
}
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..bed22db755a 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -151,6 +151,7 @@ static const FormData_pg_attribute a1 = {
.attalign = TYPALIGN_SHORT,
.attstorage = TYPSTORAGE_PLAIN,
.attnotnull = true,
+ .attnotnullvalid = true,
.attislocal = true,
};
@@ -164,6 +165,7 @@ static const FormData_pg_attribute a2 = {
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
.attnotnull = true,
+ .attnotnullvalid = true,
.attislocal = true,
};
@@ -177,6 +179,7 @@ static const FormData_pg_attribute a3 = {
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
.attnotnull = true,
+ .attnotnullvalid = true,
.attislocal = true,
};
@@ -190,6 +193,7 @@ static const FormData_pg_attribute a4 = {
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
.attnotnull = true,
+ .attnotnullvalid = true,
.attislocal = true,
};
@@ -203,6 +207,7 @@ static const FormData_pg_attribute a5 = {
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
.attnotnull = true,
+ .attnotnullvalid = true,
.attislocal = true,
};
@@ -222,6 +227,7 @@ static const FormData_pg_attribute a6 = {
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
.attnotnull = true,
+ .attnotnullvalid = true,
.attislocal = true,
};
@@ -753,6 +759,11 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
slot[slotCount]->tts_values[Anum_pg_attribute_attstorage - 1] = CharGetDatum(attrs->attstorage);
slot[slotCount]->tts_values[Anum_pg_attribute_attcompression - 1] = CharGetDatum(attrs->attcompression);
slot[slotCount]->tts_values[Anum_pg_attribute_attnotnull - 1] = BoolGetDatum(attrs->attnotnull);
+ slot[slotCount]->tts_values[Anum_pg_attribute_attnotnullvalid - 1] = BoolGetDatum(attrs->attnotnullvalid);
+ if (!attrs->attnotnull)
+ Assert(!attrs->attnotnullvalid);
+ else
+ Assert(attrs->attnotnullvalid);
slot[slotCount]->tts_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(attrs->atthasdef);
slot[slotCount]->tts_values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(attrs->atthasmissing);
slot[slotCount]->tts_values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(attrs->attidentity);
@@ -2621,12 +2632,17 @@ AddRelationNewConstraints(Relation rel,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("not-null constraints are not supported on virtual generated columns"));
+ if (cdef->initially_valid)
+ Assert(!cdef->skip_validation);
+ else
+ Assert(cdef->skip_validation);
+
/*
* 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,
- is_local, cdef->is_no_inherit))
+ if (AdjustNotNullInheritance(rel, colnum,
+ is_local, cdef->is_no_inherit, cdef->skip_validation))
continue;
/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..5005ea7e6f1 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -576,13 +576,14 @@ ChooseConstraintName(const char *name1, const char *name2,
* 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.
+ * if include_invalid is true, it may return an invalid not-null tuple.
*
* 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)
+findNotNullConstraintAttnum(Oid relid, AttrNumber attnum, bool include_invalid)
{
Relation pg_constraint;
HeapTuple conTup,
@@ -609,7 +610,7 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
*/
if (con->contype != CONSTRAINT_NOTNULL)
continue;
- if (!con->convalidated)
+ if (!con->convalidated && !include_invalid)
continue;
conkey = extractNotNullColumn(conTup);
@@ -631,9 +632,10 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
* 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.
+ * if include_invalid is true, it may return an invalid not-null tuple.
*/
HeapTuple
-findNotNullConstraint(Oid relid, const char *colname)
+findNotNullConstraint(Oid relid, const char *colname, bool include_invalid)
{
AttrNumber attnum;
@@ -641,7 +643,7 @@ findNotNullConstraint(Oid relid, const char *colname)
if (attnum <= InvalidAttrNumber)
return NULL;
- return findNotNullConstraintAttnum(relid, attnum);
+ return findNotNullConstraintAttnum(relid, attnum, include_invalid);
}
/*
@@ -728,12 +730,13 @@ extractNotNullColumn(HeapTuple constrTup)
* nothing if it's already true; otherwise we increment coninhcount by 1.
*/
bool
-AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
- bool is_local, bool is_no_inherit)
+AdjustNotNullInheritance(Relation rel, AttrNumber attnum,
+ bool is_local, bool is_no_inherit, bool is_notvalid)
{
HeapTuple tup;
+ Oid relid = RelationGetRelid(rel);
- tup = findNotNullConstraintAttnum(relid, attnum);
+ tup = findNotNullConstraintAttnum(relid, attnum, true);
if (HeapTupleIsValid(tup))
{
Relation pg_constraint;
@@ -753,6 +756,36 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"",
NameStr(conform->conname), get_rel_name(relid)));
+ /*
+ * Throw an error if we want change the NOT NULL constraint from NOT
+ * VALID to VALID.
+ */
+ if (!is_notvalid && !conform->convalidated)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("cannot change invalid constraint \"%s\" on relation \"%s\" to valid",
+ NameStr(conform->conname), get_rel_name(relid)),
+ errhint("You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint \"%s\"",
+ NameStr(conform->conname)));
+
+ /* VALID TO NOT VALID seems no usage, so just issue a warning */
+ if (is_notvalid && conform->convalidated)
+ {
+ if (is_local)
+ ereport(WARNING,
+ errmsg("NOT NULL constraint \"%s\" on relation \"%s\" is already valid",
+ NameStr(conform->conname), get_rel_name(relid)));
+ else if (rel->rd_rel->relispartition)
+ {
+ /*
+ * In case of partitions, an inherited not null constraint
+ * is never considered local. See MergeConstraintsIntoExisting also.
+ */
+ conform->conislocal = false;
+ changed =true;
+ }
+ }
+
if (!is_local)
{
if (pg_add_s16_overflow(conform->coninhcount, 1,
@@ -788,9 +821,10 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
* This is seldom needed, so we just scan pg_constraint each time.
*
* 'include_noinh' determines whether to include NO INHERIT constraints or not.
+ * 'include_notvalid' determines whether to include NO VALID constraints or not.
*/
List *
-RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
+RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh, bool include_notvalid)
{
List *notnulls = NIL;
Relation constrRel;
@@ -816,6 +850,9 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
if (conForm->connoinherit && !include_noinh)
continue;
+ if (!conForm->convalidated && !include_notvalid)
+ continue;
+
colnum = extractNotNullColumn(htup);
if (cooked)
@@ -830,7 +867,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
cooked->attnum = colnum;
cooked->expr = NULL;
cooked->is_enforced = true;
- cooked->skip_validation = false;
+ cooked->skip_validation = !conForm->convalidated;
cooked->is_local = true;
cooked->inhcount = 0;
cooked->is_no_inherit = conForm->connoinherit;
@@ -850,7 +887,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
constr->keys = list_make1(makeString(get_attname(relid, colnum,
false)));
constr->is_enforced = true;
- constr->skip_validation = false;
+ constr->skip_validation = !conForm->convalidated;
constr->initially_valid = true;
constr->is_no_inherit = conForm->connoinherit;
notnulls = lappend(notnulls, constr);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1202544ebd0..fc77e15793e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -410,6 +410,9 @@ static void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation
static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
char *constrName, HeapTuple contuple,
bool recurse, bool recursing, LOCKMODE lockmode);
+static void QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple, bool recurse, bool recursing,
+ LOCKMODE lockmode);
static int transformColumnNameList(Oid relId, List *colList,
int16 *attnums, Oid *atttypids, Oid *attcollids);
static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
@@ -474,6 +477,7 @@ static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool r
LOCKMODE lockmode);
static void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
LOCKMODE lockmode);
+static void set_attnotnullinvalid(Relation rel, AttrNumber attnum, bool is_valid);
static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel,
char *constrname, char *colName,
bool recurse, bool recursing,
@@ -1400,6 +1404,12 @@ BuildDescForRelation(const List *columns)
/* Fill in additional stuff not handled by TupleDescInitEntry */
att->attnotnull = entry->is_not_null;
+
+ if (att->attnotnull)
+ att->attnotnullvalid = true;
+ else
+ att->attnotnullvalid = false;
+
att->attislocal = entry->is_local;
att->attinhcount = entry->inhcount;
att->attidentity = entry->identity;
@@ -2713,10 +2723,10 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
/*
* Request attnotnull on columns that have a not-null constraint
- * that's not marked NO INHERIT.
+ * that's not marked NO INHERIT. but we will include NOT VALID
*/
nnconstrs = RelationGetNotNullConstraints(RelationGetRelid(relation),
- true, false);
+ true, false, true);
foreach_ptr(CookedConstraint, cc, nnconstrs)
nncols = bms_add_member(nncols, cc->attnum);
@@ -6183,12 +6193,14 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
* If we are rebuilding the tuples OR if we added any new but not
* verified not-null constraints, check all not-null constraints. This
* is a bit of overkill but it minimizes risk of bugs.
+ * But we don't need check invalid not-null constraint! this is aligned
+ * with check constraint behavior.
*/
for (i = 0; i < newTupDesc->natts; i++)
{
Form_pg_attribute attr = TupleDescAttr(newTupDesc, i);
- if (attr->attnotnull && !attr->attisdropped)
+ if (attr->attnotnull && attr->attnotnullvalid && !attr->attisdropped)
notnull_attrs = lappend_int(notnull_attrs, attr->attnum);
}
if (notnull_attrs)
@@ -7669,6 +7681,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
/* If the column is already nullable there's nothing to do. */
if (!attTup->attnotnull)
{
+ // Assert(!attTup->attnotnullvalid);
table_close(attr_rel, RowExclusiveLock);
return InvalidObjectAddress;
}
@@ -7709,7 +7722,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
* Find the constraint that makes this column NOT NULL, and drop it.
* dropconstraint_internal() resets attnotnull.
*/
- conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
+ conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum, false);
if (conTup == NULL)
elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
colName, RelationGetRelationName(rel));
@@ -7766,6 +7779,7 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
attr = (Form_pg_attribute) GETSTRUCT(tuple);
Assert(!attr->attnotnull);
attr->attnotnull = true;
+ attr->attnotnullvalid = true;
CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
/*
@@ -7787,6 +7801,42 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
}
}
+/*
+ * currently we have two commands can change attnotnullvalid, attnotnull status:
+ * ALTER TABLE VALIDATE CONSTRAINT, ALTER TABLE ADD NOT NULL NOT VALID. In both
+ * case, we need set attnotnull to true, set attnotnullvalid based on "is_valid".
+*/
+static void
+set_attnotnullinvalid(Relation rel, AttrNumber attnum, bool is_valid)
+{
+ Form_pg_attribute attr;
+ Relation attr_rel;
+ HeapTuple tuple;
+
+ CheckAlterTableIsSafe(rel);
+
+ attr = TupleDescAttr(RelationGetDescr(rel), attnum - 1);
+ if (attr->attisdropped)
+ return;
+
+ attr_rel = table_open(AttributeRelationId, RowExclusiveLock);
+
+ tuple = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+ attnum, RelationGetRelid(rel));
+
+ attr = (Form_pg_attribute) GETSTRUCT(tuple);
+ attr->attnotnullvalid = is_valid;
+ attr->attnotnull = true;
+
+ CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
+ CommandCounterIncrement();
+
+ table_close(attr_rel, RowExclusiveLock);
+ heap_freetuple(tuple);
+}
+
/*
* ALTER TABLE ALTER COLUMN SET NOT NULL
*
@@ -7843,7 +7893,7 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
colName, RelationGetRelationName(rel))));
/* See if there's already a constraint */
- tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
+ tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum, true);
if (HeapTupleIsValid(tuple))
{
Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
@@ -7878,6 +7928,16 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
conForm->conislocal = true;
changed = true;
}
+ else if (!conForm->convalidated)
+ {
+
+ /*
+ * flip attnotnull and convalidated, and also validate the
+ * constraint.
+ */
+ return ATExecValidateConstraint(wqueue, rel, NameStr(conForm->conname),
+ recurse, recursing, lockmode);
+ }
if (changed)
{
@@ -9385,6 +9445,24 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
{
AlterTableCmd *newcmd;
Constraint *nnconstr;
+ HeapTuple tuple;
+
+ tuple = findNotNullConstraint(RelationGetRelid(rel), strVal(lfirst(lc)), true);
+ if (tuple != NULL)
+ {
+ Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
+ if (!conForm->convalidated)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("primary key require an validated NOT NULL constraint"),
+ errdetail("NOT NULL constraint \"%s\" on Column \"%s\" of table \"%s\" is not validated",
+ NameStr(conForm->conname),
+ strVal(lfirst(lc)),
+ RelationGetRelationName(rel)),
+ errhint("You may try ALTER TABLE VALIDATE CONSTRAINT to validate it \"%s\"",
+ NameStr(conForm->conname)));
+ heap_freetuple(tuple);
+ }
nnconstr = makeNotNullConstraint(lfirst(lc));
@@ -9760,11 +9838,19 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
constr->conname = ccon->name;
/*
- * If adding a not-null constraint, set the pg_attribute flag and tell
+ * If adding a valid not-null constraint, set the pg_attribute flag and tell
* phase 3 to verify existing rows, if needed.
+ * However if we are adding a invalid not-null constraint, then we only
+ * need set the pg_attribute.attnotnullvalid to false, phase 3 don't need to
+ * verify existing rows.
*/
if (constr->contype == CONSTR_NOTNULL)
- set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
+ {
+ if (!constr->skip_validation)
+ set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
+ else
+ set_attnotnullinvalid(rel, ccon->attnum, false);
+ }
ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
}
@@ -12174,7 +12260,7 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
HeapTuple childtup;
Form_pg_constraint childcon;
- childtup = findNotNullConstraint(childoid, colName);
+ childtup = findNotNullConstraint(childoid, colName, false);
if (!childtup)
elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
colName, childoid);
@@ -12365,10 +12451,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
con = (Form_pg_constraint) GETSTRUCT(tuple);
if (con->contype != CONSTRAINT_FOREIGN &&
- con->contype != CONSTRAINT_CHECK)
+ con->contype != CONSTRAINT_CHECK &&
+ con->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
+ errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint or not-null constraint",
constrName, RelationGetRelationName(rel))));
if (!con->conenforced)
@@ -12387,6 +12474,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
QueueCheckConstraintValidation(wqueue, conrel, rel, constrName,
tuple, recurse, recursing, lockmode);
}
+ else if (con->contype == CONSTRAINT_NOTNULL)
+ {
+ QueueNNConstraintValidation(wqueue, conrel, rel,
+ tuple, recurse, recursing, lockmode);
+ }
ObjectAddressSet(address, ConstraintRelationId, con->oid);
}
@@ -12603,6 +12695,106 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
heap_freetuple(copyTuple);
}
+/*
+ * QueueNNConstraintValidation
+ *
+ * Add an entry to the wqueue to validate the given not-null constraint in
+ * Phase 3 and update the convalidated field in the pg_constraint catalog for
+ * the specified relation and all its inheriting children.
+ */
+static void
+QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple, bool recurse, bool recursing,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint con;
+ AlteredTableInfo *tab;
+ HeapTuple copyTuple;
+ Form_pg_constraint copy_con;
+ List *children = NIL;
+ AttrNumber attnum;
+ char *colname;
+
+ con = (Form_pg_constraint) GETSTRUCT(contuple);
+ Assert(con->contype == CONSTRAINT_NOTNULL);
+
+ attnum = extractNotNullColumn(contuple);
+
+ /*
+ * If we're recursing, the parent has already done this, so skip it.
+ *
+ * We recurse before validating on the parent, to reduce risk of
+ * deadlocks.
+ */
+ if (!recursing && !con->connoinherit)
+ children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+
+ colname = get_attname(RelationGetRelid(rel), attnum, false);
+ foreach_oid(childoid, children)
+ {
+ Relation childrel;
+ HeapTuple contup;
+ Form_pg_constraint childcon;
+ char *conname;
+
+ if (childoid == RelationGetRelid(rel))
+ continue;
+
+ /*
+ * If we are told not to recurse, there had better not be any child
+ * tables, because we can't mark the constraint on the parent valid
+ * unless it is valid for all child tables.
+ */
+ if (!recurse)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+ errmsg("constraint must be validated on child tables too"));
+
+ /*
+ * The column on child might have a different attnum, so search by
+ * column name.
+ */
+ contup = findNotNullConstraint(childoid, colname, true);
+ if (!contup)
+ elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
+ colname, get_rel_name(childoid));
+ childcon = (Form_pg_constraint) GETSTRUCT(contup);
+ if (childcon->convalidated)
+ continue;
+
+ /* find_all_inheritors already got lock */
+ childrel = table_open(childoid, NoLock);
+ conname = pstrdup(NameStr(childcon->conname));
+
+ /* XXX improve ATExecValidateConstraint API to avoid double search */
+ ATExecValidateConstraint(wqueue, childrel, conname,
+ false, true, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ set_attnotnullinvalid(rel, attnum, true);
+
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->verify_new_notnull = true;
+
+ /*
+ * Invalidate relcache so that others see the new validated constraint.
+ */
+ CacheInvalidateRelcache(rel);
+
+ /*
+ * Now update the catalogs, while we have the door open.
+ */
+ copyTuple = heap_copytuple(contuple);
+ copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+ copy_con->convalidated = true;
+ CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
+
+ InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
+
+ heap_freetuple(copyTuple);
+}
+
/*
* transformColumnNameList - transform list of column names
*
@@ -13473,10 +13665,11 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
false),
RelationGetRelationName(rel)));
- /* All good -- reset attnotnull if needed */
+ /* All good -- reset attnotnull and attnotnullvalid if needed */
if (attForm->attnotnull)
{
attForm->attnotnull = false;
+ attForm->attnotnullvalid = false;
CatalogTupleUpdate(attrel, &atttup->t_self, atttup);
}
@@ -13547,7 +13740,7 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
*/
if (con->contype == CONSTRAINT_NOTNULL)
{
- tuple = findNotNullConstraint(childrelid, colname);
+ tuple = findNotNullConstraint(childrelid, colname, false);
if (!HeapTupleIsValid(tuple))
elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
colname, RelationGetRelid(childrel));
@@ -16812,7 +17005,7 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
HeapTuple contup;
contup = findNotNullConstraintAttnum(RelationGetRelid(parent_rel),
- parent_att->attnum);
+ parent_att->attnum, true);
if (HeapTupleIsValid(contup) &&
!((Form_pg_constraint) GETSTRUCT(contup))->connoinherit)
ereport(ERROR,
@@ -19328,7 +19521,8 @@ PartConstraintImpliedByRelConstraint(Relation scanrel,
{
Form_pg_attribute att = TupleDescAttr(scanrel->rd_att, i - 1);
- if (att->attnotnull && !att->attisdropped)
+ /* invalid not-null constraint must be ignored */
+ if (att->attnotnull && att->attnotnullvalid && !att->attisdropped)
{
NullTest *ntest = makeNode(NullTest);
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index e9bd98c7738..3be2e4f4639 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -2070,6 +2070,7 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
{
Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
+ /* not valid not-null constraint also checked */
if (att->attnotnull && slot_attisnull(slot, attrChk))
{
char *val_desc;
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 0489ad36644..c8790264c8d 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -175,9 +175,9 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
{
for (int i = 0; i < relation->rd_att->natts; i++)
{
- CompactAttribute *attr = TupleDescCompactAttr(relation->rd_att, i);
+ Form_pg_attribute attr = TupleDescAttr(relation->rd_att, i);
- if (attr->attnotnull)
+ if (attr->attnotnull && attr->attnotnullvalid)
{
rel->notnullattnums = bms_add_member(rel->notnullattnums,
i + 1);
@@ -1355,7 +1355,7 @@ get_relation_constraints(PlannerInfo *root,
{
Form_pg_attribute att = TupleDescAttr(relation->rd_att, i - 1);
- if (att->attnotnull && !att->attisdropped)
+ if (att->attnotnull && att->attnotnullvalid && !att->attisdropped)
{
NullTest *ntest = makeNode(NullTest);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..715eb51300b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4224,9 +4224,9 @@ ConstraintElem:
n->keys = list_make1(makeString($3));
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
- NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
- n->initially_valid = true;
+ n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
| UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 896a7f2c59b..aaaee388f5d 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -329,6 +329,20 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
cd->is_not_null = true;
break;
}
+ if (!cxt.isforeign && !nn->initially_valid)
+ {
+ nn->initially_valid = true;
+ nn->skip_validation = false;
+
+ /*
+ * not-null constraint created via CREATE TABLE will always be
+ * valid. since there is no data there while CREATE TABLE, make it
+ * invalid does not make sense
+ */
+ ereport(WARNING,
+ errcode(ERRCODE_WARNING),
+ errmsg("Ignoring NOT VALID flag for NOT NULL constraint on column \"%s\"", colname));
+ }
}
/*
@@ -1291,7 +1305,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
List *lst;
lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false,
- true);
+ true, false);
cxt->nnconstraints = list_concat(cxt->nnconstraints, lst);
}
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 9ad7681f155..70c11529b90 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1142,6 +1142,7 @@ CatalogCacheInitializeCache(CatCache *cache)
keytype = attr->atttypid;
/* cache key columns should always be NOT NULL */
Assert(attr->attnotnull);
+ Assert(attr->attnotnullvalid);
}
else
{
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9f54a9e72b7..84fbabd5344 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -590,7 +590,10 @@ RelationBuildTupleDesc(Relation relation)
populate_compact_attribute(relation->rd_att, attnum - 1);
- /* Update constraint/default info */
+ /*
+ * Update constraint/default info
+ * has_not_null also include invalid not-null constraint
+ */
if (attp->attnotnull)
constr->has_not_null = true;
if (attp->attgenerated == ATTRIBUTE_GENERATED_STORED)
@@ -3573,6 +3576,7 @@ RelationBuildLocalRelation(const char *relname,
datt->attidentity = satt->attidentity;
datt->attgenerated = satt->attgenerated;
datt->attnotnull = satt->attnotnull;
+ datt->attnotnullvalid = satt->attnotnullvalid;
has_not_null |= satt->attnotnull;
populate_compact_attribute(rel->rd_att, i);
}
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 428ed2d60fc..42755bdfd7d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8884,6 +8884,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQExpBuffer q = createPQExpBuffer();
PQExpBuffer tbloids = createPQExpBuffer();
PQExpBuffer checkoids = createPQExpBuffer();
+ PQExpBuffer invalidnotnulloids = createPQExpBuffer();
PGresult *res;
int ntups;
int curtblindx;
@@ -8903,6 +8904,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
int i_notnull_name;
int i_notnull_noinherit;
int i_notnull_islocal;
+ int i_notnull_validated;
int i_attoptions;
int i_attcollation;
int i_attcompression;
@@ -8998,12 +9000,14 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
appendPQExpBufferStr(q,
"co.conname AS notnull_name,\n"
"co.connoinherit AS notnull_noinherit,\n"
- "co.conislocal AS notnull_islocal,\n");
+ "co.conislocal AS notnull_islocal,\n"
+ "co.convalidated as notnull_validated,\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");
+ "a.attislocal AS notnull_islocal,\n"
+ "true as notnull_validated\n");
if (fout->remoteVersion >= 140000)
appendPQExpBufferStr(q,
@@ -9078,6 +9082,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
i_notnull_name = PQfnumber(res, "notnull_name");
i_notnull_noinherit = PQfnumber(res, "notnull_noinherit");
i_notnull_islocal = PQfnumber(res, "notnull_islocal");
+ i_notnull_validated = PQfnumber(res, "notnull_validated");
i_attoptions = PQfnumber(res, "attoptions");
i_attcollation = PQfnumber(res, "attcollation");
i_attcompression = PQfnumber(res, "attcompression");
@@ -9094,6 +9099,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
* r is handled by the inner loop.
*/
curtblindx = -1;
+ appendPQExpBufferChar(invalidnotnulloids, '{');
for (int r = 0; r < ntups;)
{
Oid attrelid = atooid(PQgetvalue(res, r, i_attrelid));
@@ -9145,6 +9151,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
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->notnull_validated = (bool *) pg_malloc(numatts * sizeof(bool));
tbinfo->attrdefs = (AttrDefInfo **) pg_malloc(numatts * sizeof(AttrDefInfo *));
hasdefaults = false;
@@ -9168,12 +9175,32 @@ 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_validated[j] = (PQgetvalue(res, r, i_notnull_validated)[0] == 't');
- /* Handle not-null constraint name and flags */
- determineNotNullFlags(fout, res, r,
- tbinfo, j,
- i_notnull_name, i_notnull_noinherit,
- i_notnull_islocal);
+ if (tbinfo->notnull_validated[j])
+ {
+ /* Handle not-null constraint name and flags */
+ determineNotNullFlags(fout, res, r,
+ tbinfo, j,
+ i_notnull_name, i_notnull_noinherit,
+ i_notnull_islocal);
+ }
+ else
+ {
+ tbinfo->notnull_constrs[j] = NULL;
+
+ /*
+ * if column notnull_validated is false, it means either column
+ * don't have not-null constraint at all, or the existing one is
+ * invalid. here we onlu dump the invalid not-null.
+ */
+ if (!PQgetisnull(res, r, i_notnull_name))
+ {
+ if (invalidnotnulloids->len > 1)
+ appendPQExpBufferChar(invalidnotnulloids, ',');
+ appendPQExpBuffer(invalidnotnulloids, "%u", tbinfo->dobj.catId.oid);
+ }
+ }
tbinfo->attoptions[j] = pg_strdup(PQgetvalue(res, r, i_attoptions));
tbinfo->attcollation[j] = atooid(PQgetvalue(res, r, i_attcollation));
@@ -9193,6 +9220,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
appendPQExpBuffer(tbloids, "%u", tbinfo->dobj.catId.oid);
}
}
+ appendPQExpBufferChar(invalidnotnulloids, '}');
PQclear(res);
@@ -9326,6 +9354,112 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQclear(res);
}
+ /*
+ * Get info about table NOT NULL NOT VALID constraints. This is skipped for
+ * a data-only dump, as it is only needed for table schemas.
+ */
+ if (dopt->dumpSchema && invalidnotnulloids->len > 2)
+ {
+ ConstraintInfo *constrs;
+ int numConstrs;
+ int i_tableoid;
+ int i_oid;
+ int i_conrelid;
+ int i_conname;
+ int i_consrc;
+ int i_conislocal;
+ int i_convalidated;
+
+ pg_log_info("finding table invalid not null constraints");
+
+ resetPQExpBuffer(q);
+ appendPQExpBuffer(q,
+ "SELECT c.tableoid, c.oid, conrelid, conname, "
+ "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
+ "conislocal, convalidated "
+ "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+ "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
+ "WHERE contype = 'n' AND NOT convalidated "
+ "ORDER BY c.conrelid, c.conname",
+ invalidnotnulloids->data);
+
+ res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
+
+ numConstrs = PQntuples(res);
+ constrs = (ConstraintInfo *) pg_malloc(numConstrs * sizeof(ConstraintInfo));
+
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_oid = PQfnumber(res, "oid");
+ i_conrelid = PQfnumber(res, "conrelid");
+ i_conname = PQfnumber(res, "conname");
+ i_consrc = PQfnumber(res, "consrc");
+ i_conislocal = PQfnumber(res, "conislocal");
+ i_convalidated = PQfnumber(res, "convalidated");
+
+ /* As above, this loop iterates once per table, not once per row */
+ curtblindx = -1;
+ for (int j = 0; j < numConstrs;)
+ {
+ Oid conrelid = atooid(PQgetvalue(res, j, i_conrelid));
+ TableInfo *tbinfo = NULL;
+ int numcons;
+
+ /* Count rows for this table */
+ for (numcons = 1; numcons < numConstrs - j; numcons++)
+ if (atooid(PQgetvalue(res, j + numcons, i_conrelid)) != conrelid)
+ break;
+
+ /*
+ * Locate the associated TableInfo; we rely on tblinfo[] being in
+ * OID order.
+ */
+ while (++curtblindx < numTables)
+ {
+ tbinfo = &tblinfo[curtblindx];
+ if (tbinfo->dobj.catId.oid == conrelid)
+ break;
+ }
+ if (curtblindx >= numTables)
+ pg_fatal("unrecognized table OID %u", conrelid);
+
+ tbinfo->checkexprs = constrs + j;
+
+ for (int c = 0; c < numcons; c++, j++)
+ {
+ bool validated = PQgetvalue(res, j, i_convalidated)[0] == 't';
+
+ Assert(!validated);
+ constrs[j].dobj.objType = DO_CONSTRAINT;
+ constrs[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
+ constrs[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&constrs[j].dobj);
+ constrs[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_conname));
+ constrs[j].dobj.namespace = tbinfo->dobj.namespace;
+ constrs[j].contable = tbinfo;
+ constrs[j].condomain = NULL;
+ constrs[j].contype = 'n';
+ constrs[j].condef = pg_strdup(PQgetvalue(res, j, i_consrc));
+ constrs[j].confrelid = InvalidOid;
+ constrs[j].conindex = 0;
+ constrs[j].condeferrable = false;
+ constrs[j].condeferred = false;
+ constrs[j].conislocal = (PQgetvalue(res, j, i_conislocal)[0] == 't');
+
+ /*
+ * invalid not-null constraint must dumped separately as an
+ * ALTER TABLE ADD CONSTRAINT entry. because CREATE TABLE can
+ * not create invalid not null constraint. it's also required
+ * for potentially-violating existing data is loaded before the
+ * constraint.
+ */
+ constrs[j].separate = !validated;
+
+ constrs[j].dobj.dump = tbinfo->dobj.dump;
+ }
+ }
+ PQclear(res);
+ }
+
/*
* Get info about table CHECK constraints. This is skipped for a
* data-only dump, as it is only needed for table schemas.
@@ -16620,6 +16754,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
if (print_notnull)
{
+ /* column constraint should all be validated not-null */
+ Assert(tbinfo->notnull_validated[j]);
+
if (tbinfo->notnull_constrs[j][0] == '\0')
appendPQExpBufferStr(q, " NOT NULL");
else
@@ -17939,9 +18076,9 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
.createStmt = q->data,
.dropStmt = delq->data));
}
- else if (coninfo->contype == 'c' && tbinfo)
+ else if ((coninfo->contype == 'c' || coninfo->contype == 'n') && tbinfo)
{
- /* CHECK constraint on a table */
+ /* CHECK and invalid not-null constraint on a table */
/* Ignore if not to be dumped separately, or if it was inherited */
if (coninfo->separate && coninfo->conislocal)
@@ -17965,7 +18102,7 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
ARCHIVE_OPTS(.tag = tag,
.namespace = tbinfo->dobj.namespace->dobj.name,
.owner = tbinfo->rolname,
- .description = "CHECK CONSTRAINT",
+ .description = coninfo->contype == 'c' ? "CHECK CONSTRAINT" : "CONSTRAINT",
.section = SECTION_POST_DATA,
.createStmt = q->data,
.dropStmt = delq->data));
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bbdb30b5f54..c2d8c3e601a 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -367,8 +367,9 @@ typedef struct _tableInfo
* (pre-v17) */
bool *notnull_noinh; /* NOT NULL is NO INHERIT */
bool *notnull_islocal; /* true if NOT NULL has local definition */
+ bool *notnull_validated; /* true if NOT NULL is validated */
struct _attrDefInfo **attrdefs; /* DEFAULT expressions */
- struct _constraintInfo *checkexprs; /* CHECK constraints */
+ struct _constraintInfo *checkexprs; /* CHECK constraints XXX: also include invalid not-null constraint */
bool needs_override; /* has GENERATED ALWAYS AS IDENTITY */
char *amname; /* relation access method */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..09f8f92a59c 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3114,7 +3114,8 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.conname, a.attname, c.connoinherit,\n"
- " c.conislocal, c.coninhcount <> 0\n"
+ " c.conislocal, c.coninhcount <> 0,\n"
+ " c.convalidated\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"
@@ -3137,14 +3138,16 @@ describeOneTableDetails(const char *schemaname,
{
bool islocal = PQgetvalue(result, i, 3)[0] == 't';
bool inherited = PQgetvalue(result, i, 4)[0] == 't';
+ bool validated = PQgetvalue(result, i, 5)[0] == 't';
- printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s",
+ printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s%s",
PQgetvalue(result, i, 0),
PQgetvalue(result, i, 1),
PQgetvalue(result, i, 2)[0] == 't' ?
" NO INHERIT" :
islocal && inherited ? _(" (local, inherited)") :
- inherited ? _(" (inherited)") : "");
+ inherited ? _(" (inherited)") : "",
+ !validated ? " NO VALID": "");
printTableAddFooter(&cont, buf.data);
}
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 396eeb7a0bb..55418350505 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -42,7 +42,7 @@ typedef struct TupleConstr
struct AttrMissing *missing; /* missing attributes values, NULL if none */
uint16 num_defval;
uint16 num_check;
- bool has_not_null;
+ bool has_not_null; /* this includes invalid not-null !!! */
bool has_generated_stored;
bool has_generated_virtual;
} TupleConstr;
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index deaa515fe53..398b6e324c8 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -120,6 +120,9 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
/* This flag represents the "NOT NULL" constraint */
bool attnotnull;
+ /* false means "NOT NULL NOT VALID". true means XXX (need to consider attnotnull) */
+ bool attnotnullvalid;
+
/* Has DEFAULT value or not */
bool atthasdef BKI_DEFAULT(f);
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 6da164e7e4d..fe27dde16fa 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -259,14 +259,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 findNotNullConstraintAttnum(Oid relid, AttrNumber attnum, bool include_invalid);
+extern HeapTuple findNotNullConstraint(Oid relid, const char *colname, bool include_invalid);
extern HeapTuple findDomainNotNullConstraint(Oid typid);
extern AttrNumber extractNotNullColumn(HeapTuple constrTup);
-extern bool AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
- bool is_local, bool is_no_inherit);
+extern bool AdjustNotNullInheritance(Relation rel, AttrNumber attnum,
+ bool is_local, bool is_no_inherit, bool is_notvalid);
extern List *RelationGetNotNullConstraints(Oid relid, bool cooked,
- bool include_noinh);
+ bool include_noinh, bool include_notvalid);
extern void RemoveConstraintById(Oid conId);
extern void RenameConstraintById(Oid conId, const char *newname);
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..7cf69fa7dd3 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -896,6 +896,175 @@ Not-null constraints:
"foobar" NOT NULL "a"
DROP TABLE notnull_tbl1;
+-------tests for NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM pg_constraint
+WHERE conrelid = ANY($1)
+ORDER BY 1;
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1),(NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; --ok
+\d+ notnull_tbl1
+ Table "public.notnull_tbl1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | not null | | plain | |
+ b | integer | | | | plain | |
+Not-null constraints:
+ "nn" NOT NULL "a" NO VALID
+
+INSERT INTO notnull_tbl1 VALUES (NULL, 4); --error, invalid not-null constraint forbiden new null values
+ERROR: null value in column "a" of relation "notnull_tbl1" violates not-null constraint
+DETAIL: Failing row contains (null, 4).
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid no inherit; --error
+ERROR: cannot change NO INHERIT status of NOT NULL constraint "nn" on relation "notnull_tbl1"
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error can't change not-null status
+ERROR: cannot change invalid constraint "nn" on relation "notnull_tbl1" to valid
+HINT: You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; --validate it then error out
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1 ORDER BY a, b;
+ a | b
+-----+---
+ 100 | 1
+ 300 | 3
+(2 rows)
+
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+ERROR: primary key require an validated NOT NULL constraint
+DETAIL: NOT NULL constraint "nn" on Column "a" of table "notnull_tbl1" is not validated
+HINT: You may try ALTER TABLE VALIDATE CONSTRAINT to validate it "nn"
+-- however child table NOT NULL constraints should be valid.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+NOTICE: merging column "a" with inherited definition
+NOTICE: merging column "b" with inherited definition
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+ conrelid | conname | convalidated | coninhcount
+--------------------+---------+--------------+-------------
+ notnull_tbl1 | nn | f | 0
+ notnull_tbl1_child | nn | t | 1
+(2 rows)
+
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+---now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1_child;
+DROP TABLE notnull_tbl1;
+-- Test the different not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+-- parent have valid not null constraint then child table cannot have invalid one
+CREATE TABLE notnull_chld0 (a int);
+ALTER TABLE notnull_chld0 ADD CONSTRAINT nn_chld0 NOT NULL a;
+ALTER TABLE notnull_tbl1 INHERIT notnull_chld0; --error
+ERROR: constraint "nn_parent" conflicts with NOT VALID constraint on child table "notnull_tbl1"
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+ conrelid | conname | convalidated | coninhcount
+--------------+-----------+--------------+-------------
+ notnull_tbl1 | nn_parent | t | 0
+ notnull_chld | nn_child | t | 1
+(2 rows)
+
+DROP TABLE notnull_chld0;
+DROP TABLE notnull_chld;
+DROP TABLE notnull_tbl1;
+-- Test invalid not null on inheritance table.
+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;
+NOTICE: merging definition of column "i" for child "inh_child"
+NOTICE: merging definition of column "i" for child "inh_grandchild"
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i; --error
+ERROR: cannot change invalid constraint "nn" on relation "inh_parent" to valid
+HINT: You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+ERROR: cannot change invalid constraint "nn" on relation "inh_child" to valid
+HINT: You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+ conrelid | conname | convalidated | coninhcount
+----------------+---------+--------------+-------------
+ inh_parent | nn | f | 0
+ inh_child | nn | f | 1
+ inh_grandchild | nn | f | 2
+(3 rows)
+
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --ok
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be valid now.
+ conrelid | conname | convalidated | coninhcount
+----------------+---------+--------------+-------------
+ inh_parent | nn | t | 0
+ inh_child | nn | t | 1
+ inh_grandchild | nn | t | 2
+(3 rows)
+
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+DROP TABLE notnull_tbl1;
+ERROR: table "notnull_tbl1" does not exist
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --ok
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES IN (1,2);
+CREATE TABLE notnull_tbl1_2(a int, CONSTRAINT nn2 NOT NULL a, b int);
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_2 FOR VALUES IN (3,4);
+CREATE TABLE notnull_tbl1_3(a int, b int);
+INSERT INTO notnull_tbl1_3 values(NULL,1);
+ALTER TABLE notnull_tbl1_3 add CONSTRAINT nn3 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_3 FOR VALUES IN (NULL,5);
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+ conrelid | conname | convalidated | coninhcount
+----------------+-------------+--------------+-------------
+ notnull_tbl1 | notnull_con | f | 0
+ notnull_tbl1_1 | notnull_con | t | 1
+ notnull_tbl1_2 | nn2 | t | 1
+ notnull_tbl1_3 | nn3 | f | 1
+(4 rows)
+
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --error, table already have null values
+ERROR: column "a" of relation "notnull_tbl1_3" contains null values
+ALTER TABLE notnull_tbl1_3 VALIDATE CONSTRAINT nn3; --error
+ERROR: column "a" of relation "notnull_tbl1_3" contains null values
+TRUNCATE notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --OK
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+ conrelid | conname | convalidated | coninhcount
+----------------+-------------+--------------+-------------
+ notnull_tbl1 | notnull_con | t | 0
+ notnull_tbl1_1 | notnull_con | t | 1
+ notnull_tbl1_2 | nn2 | t | 1
+ notnull_tbl1_3 | nn3 | t | 1
+(4 rows)
+
+DROP TABLE notnull_tbl1;
+-----partitioned table have not-null, then the partitions can not be NOT NULL NOT VALID.
+CREATE TABLE pp_nn (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE pp_nn ADD CONSTRAINT pp_nn_notnull NOT NULL a;
+CREATE TABLE pp_nn_1(a int, b int);
+ALTER TABLE pp_nn_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --error
+ERROR: constraint "nn1" conflicts with NOT VALID constraint on child table "pp_nn_1"
+ALTER TABLE pp_nn_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --ok
+DROP TABLE pp_nn;
+DEALLOCATE get_nnconstraint_info;
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
@@ -1392,3 +1561,17 @@ DROP TABLE constraint_comments_tbl;
DROP DOMAIN constraint_comments_dom;
DROP ROLE regress_constraint_comments;
DROP ROLE regress_constraint_comments_noaccess;
+--sanity check attnotnull and attnotnullvalid.
+select pc.relname, pa.attname, pa.attnum, pa.attnotnull, pa.attnotnullvalid
+from pg_attribute pa join pg_class pc
+on pa.attrelid = pc.oid
+where ( (pa.attnotnull and not pa.attnotnullvalid) or
+ (not pa.attnotnull and pa.attnotnullvalid) )
+and pc.relnamespace in
+ ('pg_catalog'::regnamespace,
+ 'pg_toast'::regnamespace,
+ 'information_schema'::regnamespace);
+ relname | attname | attnum | attnotnull | attnotnullvalid
+---------+---------+--------+------------+-----------------
+(0 rows)
+
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 21ce4177de4..34ad33f2d55 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -640,6 +640,120 @@ ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a;
\d+ notnull_tbl1
DROP TABLE notnull_tbl1;
+-------tests for NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM pg_constraint
+WHERE conrelid = ANY($1)
+ORDER BY 1;
+
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1),(NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; --ok
+\d+ notnull_tbl1
+
+INSERT INTO notnull_tbl1 VALUES (NULL, 4); --error, invalid not-null constraint forbiden new null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid no inherit; --error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error can't change not-null status
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; --validate it then error out
+
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1 ORDER BY a, b;
+
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+-- however child table NOT NULL constraints should be valid.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+---now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1_child;
+DROP TABLE notnull_tbl1;
+
+
+-- Test the different not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+
+-- parent have valid not null constraint then child table cannot have invalid one
+CREATE TABLE notnull_chld0 (a int);
+ALTER TABLE notnull_chld0 ADD CONSTRAINT nn_chld0 NOT NULL a;
+ALTER TABLE notnull_tbl1 INHERIT notnull_chld0; --error
+
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+DROP TABLE notnull_chld0;
+DROP TABLE notnull_chld;
+DROP TABLE notnull_tbl1;
+
+
+-- Test invalid not null on inheritance table.
+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;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i; --error
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --ok
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be valid now.
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+
+
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+DROP TABLE notnull_tbl1;
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --ok
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES IN (1,2);
+CREATE TABLE notnull_tbl1_2(a int, CONSTRAINT nn2 NOT NULL a, b int);
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_2 FOR VALUES IN (3,4);
+
+CREATE TABLE notnull_tbl1_3(a int, b int);
+INSERT INTO notnull_tbl1_3 values(NULL,1);
+ALTER TABLE notnull_tbl1_3 add CONSTRAINT nn3 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_3 FOR VALUES IN (NULL,5);
+
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --error, table already have null values
+ALTER TABLE notnull_tbl1_3 VALIDATE CONSTRAINT nn3; --error
+
+TRUNCATE notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --OK
+
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+DROP TABLE notnull_tbl1;
+
+-----partitioned table have not-null, then the partitions can not be NOT NULL NOT VALID.
+CREATE TABLE pp_nn (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE pp_nn ADD CONSTRAINT pp_nn_notnull NOT NULL a;
+
+CREATE TABLE pp_nn_1(a int, b int);
+ALTER TABLE pp_nn_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --error
+ALTER TABLE pp_nn_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --ok
+DROP TABLE pp_nn;
+
+DEALLOCATE get_nnconstraint_info;
+
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
+
+
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
@@ -836,3 +950,14 @@ DROP DOMAIN constraint_comments_dom;
DROP ROLE regress_constraint_comments;
DROP ROLE regress_constraint_comments_noaccess;
+
+--sanity check attnotnull and attnotnullvalid.
+select pc.relname, pa.attname, pa.attnum, pa.attnotnull, pa.attnotnullvalid
+from pg_attribute pa join pg_class pc
+on pa.attrelid = pc.oid
+where ( (pa.attnotnull and not pa.attnotnullvalid) or
+ (not pa.attnotnull and pa.attnotnullvalid) )
+and pc.relnamespace in
+ ('pg_catalog'::regnamespace,
+ 'pg_toast'::regnamespace,
+ 'information_schema'::regnamespace);
\ No newline at end of file
--
2.34.1
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-21 18:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-24 01:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
@ 2025-03-27 19:25 ` Alvaro Herrera <[email protected]>
2025-03-28 08:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-27 19:25 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Robert Haas <[email protected]>; Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On 2025-Mar-24, jian he wrote:
> hi.
> you may like the attached. it's based on your idea: attnotnullvalid.
This is quite close to what I was thinking, yeah. I noticed a couple of
bugs however, and ended up cleaning up the whole thing. Here's what I
have so far. I'm not sure the pg_dump bits are okay (apart from what
you report below) -- I think it's losing the constraint names, which is
of course unacceptable.
I think the warnings about creating constraints as valid when
originating as invalid are unnecessary at this point. We should add
those, or not, for all constraint types, not just not-null. That's IMO
a separate discussion.
> I came across a case, not sure if it's a bug.
> CREATE TABLE ttchk (a INTEGER);
> ALTER TABLE ttchk ADD CONSTRAINT cc check (a is NOT NULL) NOT VALID;
> CREATE TABLE ttchk_child(a INTEGER) INHERITS(ttchk);
> ttchk_child's constraint cc will default to valid,
> but pg_dump && pg_restore will make ttchk_child's constraint invalid.
> since it's an existing behavior, so not-null constraint will align with it.
Hmm, yes, such pg_dump behavior would be incorrect. I'll give the
pg_dump code another look tomorrow.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
Attachments:
[text/x-diff] notnull-notvalid.patch (71.5K, ../../[email protected]/2-notnull-notvalid.patch)
download | inline diff:
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index ac14c06c715..e9296e2d4bd 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -5549,7 +5549,15 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
"SELECT relname, "
" attname, "
" format_type(atttypid, atttypmod), "
- " attnotnull, "
+ " attnotnull, ");
+
+ /* NOT VALID NOT NULL columns are supported since Postgres 18 */
+ if (PQserverVersion(conn) >= 180000)
+ appendStringInfoString(&buf, "attnotnullvalid, ");
+ else
+ appendStringInfoString(&buf, "attnotnull AS attnotnullvalid, ");
+
+ appendStringInfoString(&buf,
" pg_get_expr(adbin, adrelid), ");
/* Generated columns are supported since Postgres 12 */
@@ -5651,6 +5659,7 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
char *attname;
char *typename;
char *attnotnull;
+ char *attnotnullvalid;
char *attgenerated;
char *attdefault;
char *collname;
@@ -5663,14 +5672,15 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
attname = PQgetvalue(res, i, 1);
typename = PQgetvalue(res, i, 2);
attnotnull = PQgetvalue(res, i, 3);
- attdefault = PQgetisnull(res, i, 4) ? NULL :
- PQgetvalue(res, i, 4);
- attgenerated = PQgetisnull(res, i, 5) ? NULL :
+ attnotnullvalid = PQgetvalue(res, i, 4);
+ attdefault = PQgetisnull(res, i, 5) ? NULL :
PQgetvalue(res, i, 5);
- collname = PQgetisnull(res, i, 6) ? NULL :
+ attgenerated = PQgetisnull(res, i, 6) ? NULL :
PQgetvalue(res, i, 6);
- collnamespace = PQgetisnull(res, i, 7) ? NULL :
+ collname = PQgetisnull(res, i, 7) ? NULL :
PQgetvalue(res, i, 7);
+ collnamespace = PQgetisnull(res, i, 8) ? NULL :
+ PQgetvalue(res, i, 8);
if (first_item)
first_item = false;
@@ -5714,7 +5724,11 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
/* Add NOT NULL if needed */
if (import_not_null && attnotnull[0] == 't')
+ {
appendStringInfoString(&buf, " NOT NULL");
+ if (attnotnullvalid[0] == 'f')
+ appendStringInfoString(&buf, " NOT VALID");
+ }
}
while (++i < numrows &&
strcmp(PQgetvalue(res, i, 0), tablename) == 0);
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..f33f1ea1b57 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1260,7 +1260,16 @@
<structfield>attnotnull</structfield> <type>bool</type>
</para>
<para>
- This column has a not-null constraint.
+ This column has a (possibly unvalidated) not-null constraint.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>attnotnullvalid</structfield> <type>bool</type>
+ </para>
+ <para>
+ Whether the not-null constraint, if one exists, has been validated.
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 11d1bc7dbe1..344387cd99d 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -243,6 +243,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
entire table; however, if a valid <literal>CHECK</literal> constraint is
found which proves no <literal>NULL</literal> can exist, then the
table scan is skipped.
+ If a column has a invalid not-null constraint,
+ <literal>SET NOT NULL</literal> validates it.
</para>
<para>
@@ -458,8 +460,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<para>
This form adds a new constraint to a table using the same constraint
syntax as <link linkend="sql-createtable"><command>CREATE TABLE</command></link>, plus the option <literal>NOT
- VALID</literal>, which is currently only allowed for foreign key
- and CHECK constraints.
+ VALID</literal>, which is currently only allowed for foreign key,
+ <literal>CHECK</literal> constraints and not-null constraints.
</para>
<para>
@@ -586,7 +588,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>VALIDATE CONSTRAINT</literal></term>
<listitem>
<para>
- This form validates a foreign key or check constraint that was
+ This form validates a foreign key, check, or not-null constraint that was
previously created as <literal>NOT VALID</literal>, by scanning the
table to ensure there are no rows for which the constraint is not
satisfied. Nothing happens if the constraint is already marked valid.
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index ed2195f14b2..320de2cbda0 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -74,7 +74,8 @@ populate_compact_attribute_internal(Form_pg_attribute src,
dst->atthasmissing = src->atthasmissing;
dst->attisdropped = src->attisdropped;
dst->attgenerated = (src->attgenerated != '\0');
- dst->attnotnull = src->attnotnull;
+ dst->attnullability = !src->attnotnull ? ATTNULLABLE_NONE :
+ src->attnotnullvalid ? ATTNULLABLE_VALID : ATTNULLABLE_INVALID;
switch (src->attalign)
{
@@ -252,6 +253,7 @@ CreateTupleDescCopy(TupleDesc tupdesc)
Form_pg_attribute att = TupleDescAttr(desc, i);
att->attnotnull = false;
+ att->attnotnullvalid = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -298,6 +300,7 @@ CreateTupleDescTruncatedCopy(TupleDesc tupdesc, int natts)
Form_pg_attribute att = TupleDescAttr(desc, i);
att->attnotnull = false;
+ att->attnotnullvalid = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -418,6 +421,7 @@ TupleDescCopy(TupleDesc dst, TupleDesc src)
Form_pg_attribute att = TupleDescAttr(dst, i);
att->attnotnull = false;
+ att->attnotnullvalid = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -464,6 +468,7 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno,
/* since we're not copying constraints or defaults, clear these */
dstAtt->attnotnull = false;
+ dstAtt->attnotnullvalid = false;
dstAtt->atthasdef = false;
dstAtt->atthasmissing = false;
dstAtt->attidentity = '\0';
@@ -613,6 +618,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
return false;
if (attr1->attnotnull != attr2->attnotnull)
return false;
+ if (attr1->attnotnullvalid != attr2->attnotnullvalid)
+ return false;
if (attr1->atthasdef != attr2->atthasdef)
return false;
if (attr1->attidentity != attr2->attidentity)
@@ -841,6 +848,7 @@ TupleDescInitEntry(TupleDesc desc,
att->attndims = attdim;
att->attnotnull = false;
+ att->attnotnullvalid = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
@@ -904,6 +912,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
att->attndims = attdim;
att->attnotnull = false;
+ att->attnotnullvalid = false;
att->atthasdef = false;
att->atthasmissing = false;
att->attidentity = '\0';
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 6db864892d0..44ec9e58520 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -615,6 +615,9 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
attrtypes[attnum]->attnotnull = true;
}
}
+
+ /* Not-null constraints on system catalogs are always valid. */
+ attrtypes[attnum]->attnotnullvalid = attrtypes[attnum]->attnotnull;
}
diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index df3231fcd41..bdf55d7dc8d 100644
--- a/src/backend/catalog/genbki.pl
+++ b/src/backend/catalog/genbki.pl
@@ -986,6 +986,9 @@ sub morph_row_for_pgattr
$row->{attnotnull} = 'f';
}
+ # Not-null constraints on system catalogs are always valid.
+ $row->{attnotnullvalid} = $row->{attnotnull};
+
Catalog::AddDefaultValues($row, $pgattr_schema, 'pg_attribute');
return;
}
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..d3a814986e9 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -151,6 +151,7 @@ static const FormData_pg_attribute a1 = {
.attalign = TYPALIGN_SHORT,
.attstorage = TYPSTORAGE_PLAIN,
.attnotnull = true,
+ .attnotnullvalid = true,
.attislocal = true,
};
@@ -164,6 +165,7 @@ static const FormData_pg_attribute a2 = {
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
.attnotnull = true,
+ .attnotnullvalid = true,
.attislocal = true,
};
@@ -177,6 +179,7 @@ static const FormData_pg_attribute a3 = {
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
.attnotnull = true,
+ .attnotnullvalid = true,
.attislocal = true,
};
@@ -190,6 +193,7 @@ static const FormData_pg_attribute a4 = {
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
.attnotnull = true,
+ .attnotnullvalid = true,
.attislocal = true,
};
@@ -203,6 +207,7 @@ static const FormData_pg_attribute a5 = {
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
.attnotnull = true,
+ .attnotnullvalid = true,
.attislocal = true,
};
@@ -222,6 +227,7 @@ static const FormData_pg_attribute a6 = {
.attalign = TYPALIGN_INT,
.attstorage = TYPSTORAGE_PLAIN,
.attnotnull = true,
+ .attnotnullvalid = true,
.attislocal = true,
};
@@ -753,6 +759,7 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
slot[slotCount]->tts_values[Anum_pg_attribute_attstorage - 1] = CharGetDatum(attrs->attstorage);
slot[slotCount]->tts_values[Anum_pg_attribute_attcompression - 1] = CharGetDatum(attrs->attcompression);
slot[slotCount]->tts_values[Anum_pg_attribute_attnotnull - 1] = BoolGetDatum(attrs->attnotnull);
+ slot[slotCount]->tts_values[Anum_pg_attribute_attnotnullvalid - 1] = BoolGetDatum(attrs->attnotnullvalid);
slot[slotCount]->tts_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(attrs->atthasdef);
slot[slotCount]->tts_values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(attrs->atthasmissing);
slot[slotCount]->tts_values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(attrs->attidentity);
@@ -1714,6 +1721,7 @@ RemoveAttributeById(Oid relid, AttrNumber attnum)
/* Remove any not-null constraint the column may have */
attStruct->attnotnull = false;
+ attStruct->attnotnullvalid = false;
/* Unset this so no one tries to look up the generation expression */
attStruct->attgenerated = '\0';
@@ -2621,12 +2629,17 @@ AddRelationNewConstraints(Relation rel,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("not-null constraints are not supported on virtual generated columns"));
+ Assert(cdef->initially_valid != cdef->skip_validation);
+
/*
* If the column already has a not-null constraint, we don't want
- * to add another one; just adjust inheritance status as needed.
+ * to add another one; adjust inheritance status as needed. This
+ * also checks whether the existing constraint matches the
+ * requested validity.
*/
- if (AdjustNotNullInheritance(RelationGetRelid(rel), colnum,
- is_local, cdef->is_no_inherit))
+ if (AdjustNotNullInheritance(rel, colnum, is_local,
+ cdef->is_no_inherit,
+ cdef->skip_validation))
continue;
/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..81e975e1238 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -574,8 +574,8 @@ ChooseConstraintName(const char *name1, const char *name2,
/*
* 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.
+ * (possibly not valid) 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.
@@ -604,13 +604,11 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
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.
+ * We're looking for a NOTNULL constraint 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)
@@ -628,9 +626,10 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
}
/*
- * 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.
+ * Find and return a copy of the pg_constraint tuple that implements a
+ * (possibly not valid) 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)
@@ -721,19 +720,23 @@ extractNotNullColumn(HeapTuple constrTup)
*
* If no not-null constraint is found for the column, return false.
* Caller can create one.
+ *
* If a constraint exists but the connoinherit flag is not what the caller
- * wants, throw an error about the incompatibility. Otherwise, we adjust
- * conislocal/coninhcount and return true.
- * In the latter case, if is_local is true we flip conislocal true, or do
- * nothing if it's already true; otherwise we increment coninhcount by 1.
+ * wants, throw an error about the incompatibility. If the desired
+ * constraint is valid but the existing constraint is not valid, also
+ * throw an error about that (the opposite case is acceptable).
+ *
+ * If everything checks out, we adjust conislocal/coninhcount and return
+ * true. If is_local is true we flip conislocal true, or do nothing if
+ * it's already true; otherwise we increment coninhcount by 1.
*/
bool
-AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
- bool is_local, bool is_no_inherit)
+AdjustNotNullInheritance(Relation rel, AttrNumber attnum,
+ bool is_local, bool is_no_inherit, bool is_notvalid)
{
HeapTuple tup;
- tup = findNotNullConstraintAttnum(relid, attnum);
+ tup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
if (HeapTupleIsValid(tup))
{
Relation pg_constraint;
@@ -751,7 +754,18 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
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)));
+ NameStr(conform->conname), RelationGetRelationName(rel)));
+
+ /*
+ * Throw an error if the existing constraint is NOT VALID and caller
+ * wants a valid one.
+ */
+ if (!is_notvalid && !conform->convalidated)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("incompatible NOT VALID constraint \"%s\" on relation \"%s\"",
+ NameStr(conform->conname), RelationGetRelationName(rel)),
+ errhint("You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to validate it."));
if (!is_local)
{
@@ -830,7 +844,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
cooked->attnum = colnum;
cooked->expr = NULL;
cooked->is_enforced = true;
- cooked->skip_validation = false;
+ cooked->skip_validation = !conForm->convalidated;
cooked->is_local = true;
cooked->inhcount = 0;
cooked->is_no_inherit = conForm->connoinherit;
@@ -850,7 +864,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
constr->keys = list_make1(makeString(get_attname(relid, colnum,
false)));
constr->is_enforced = true;
- constr->skip_validation = false;
+ constr->skip_validation = !conForm->convalidated;
constr->initially_valid = true;
constr->is_no_inherit = conForm->connoinherit;
notnulls = lappend(notnulls, constr);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index afb25007613..bb5bd0f6d61 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -419,6 +419,9 @@ static void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation
static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
char *constrName, HeapTuple contuple,
bool recurse, bool recursing, LOCKMODE lockmode);
+static void QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple, bool recurse, bool recursing,
+ LOCKMODE lockmode);
static int transformColumnNameList(Oid relId, List *colList,
int16 *attnums, Oid *atttypids, Oid *attcollids);
static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
@@ -482,7 +485,7 @@ static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid)
static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
LOCKMODE lockmode);
static void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
- LOCKMODE lockmode);
+ bool is_valid, bool queue_validation);
static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel,
char *constrname, char *colName,
bool recurse, bool recursing,
@@ -1324,7 +1327,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints,
old_notnulls);
foreach_int(attrnum, nncols)
- set_attnotnull(NULL, rel, attrnum, NoLock);
+ set_attnotnull(NULL, rel, attrnum, true, true);
ObjectAddressSet(address, RelationRelationId, relationId);
@@ -1408,7 +1411,7 @@ BuildDescForRelation(const List *columns)
TupleDescInitEntryCollation(desc, attnum, attcollation);
/* Fill in additional stuff not handled by TupleDescInitEntry */
- att->attnotnull = entry->is_not_null;
+ att->attnotnull = att->attnotnullvalid = entry->is_not_null;
att->attislocal = entry->is_local;
att->attinhcount = entry->inhcount;
att->attidentity = entry->identity;
@@ -2722,7 +2725,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
/*
* Request attnotnull on columns that have a not-null constraint
- * that's not marked NO INHERIT.
+ * that's not marked NO INHERIT (even if not valid).
*/
nnconstrs = RelationGetNotNullConstraints(RelationGetRelid(relation),
true, false);
@@ -6190,14 +6193,16 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
{
/*
* If we are rebuilding the tuples OR if we added any new but not
- * verified not-null constraints, check all not-null constraints. This
- * is a bit of overkill but it minimizes risk of bugs.
+ * verified not-null constraints, check all valid not-null
+ * constraints. This is a bit of overkill but it minimizes risk of
+ * bugs.
*/
for (i = 0; i < newTupDesc->natts; i++)
{
Form_pg_attribute attr = TupleDescAttr(newTupDesc, i);
- if (attr->attnotnull && !attr->attisdropped)
+ if (attr->attnotnull && attr->attnotnullvalid &&
+ !attr->attisdropped)
notnull_attrs = lappend_int(notnull_attrs, attr->attnum);
}
if (notnull_attrs)
@@ -7716,7 +7721,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
/*
* Find the constraint that makes this column NOT NULL, and drop it.
- * dropconstraint_internal() resets attnotnull.
+ * dropconstraint_internal() resets attnotnull/attnotnullvalid.
*/
conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
if (conTup == NULL)
@@ -7746,7 +7751,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
*/
static void
set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
- LOCKMODE lockmode)
+ bool is_valid, bool queue_validation)
{
Form_pg_attribute attr;
@@ -7760,7 +7765,7 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
if (attr->attisdropped)
return;
- if (!attr->attnotnull)
+ if (!attr->attnotnull || (is_valid && !attr->attnotnullvalid))
{
Relation attr_rel;
HeapTuple tuple;
@@ -7773,15 +7778,17 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
attnum, RelationGetRelid(rel));
attr = (Form_pg_attribute) GETSTRUCT(tuple);
- Assert(!attr->attnotnull);
+
attr->attnotnull = true;
+ attr->attnotnullvalid = is_valid;
CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
/*
* If the nullness isn't already proven by validated constraints, have
* ALTER TABLE phase 3 test for it.
*/
- if (wqueue && !NotNullImpliedByRelConstraints(rel, attr))
+ if (queue_validation && wqueue &&
+ !NotNullImpliedByRelConstraints(rel, attr))
{
AlteredTableInfo *tab;
@@ -7887,6 +7894,15 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
conForm->conislocal = true;
changed = true;
}
+ else if (!conForm->convalidated)
+ {
+ /*
+ * Flip attnotnull and convalidated, and also validate the
+ * constraint.
+ */
+ return ATExecValidateConstraint(wqueue, rel, NameStr(conForm->conname),
+ recurse, recursing, lockmode);
+ }
if (changed)
{
@@ -7949,8 +7965,8 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
InvokeObjectPostAlterHook(RelationRelationId,
RelationGetRelid(rel), attnum);
- /* Mark pg_attribute.attnotnull for the column */
- set_attnotnull(wqueue, rel, attnum, lockmode);
+ /* Mark pg_attribute.attnotnull for the column and queue validation */
+ set_attnotnull(wqueue, rel, attnum, true, true);
/*
* Recurse to propagate the constraint to children that don't have one.
@@ -9389,12 +9405,45 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
}
}
- /* Insert not-null constraints in the queue for the PK columns */
+ /* Verify that columns are not-null, or request that they be made so */
foreach(lc, pkconstr->keys)
{
AlterTableCmd *newcmd;
Constraint *nnconstr;
+ HeapTuple tuple;
+ /*
+ * First check if a suitable constraint exists. If it does, we don't
+ * need to request another one. We do need to bail out if it's not
+ * valid, though.
+ */
+ tuple = findNotNullConstraint(RelationGetRelid(rel), strVal(lfirst(lc)));
+ if (tuple != NULL)
+ {
+ Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
+
+ /* a NO INHERIT constraint is no good */
+ if (conForm->connoinherit)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("NO INHERIT not-null constraint is incompatible with primary key"),
+ errhint("You will need to use ALTER TABLE ... ALTER CONSTRAINT ... INHERIT."));
+
+ /* an unvalidated constraint is no good */
+ if (!conForm->convalidated)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("not-null constraint \"%s\" of table \"%s\" has not been validated",
+ NameStr(conForm->conname),
+ RelationGetRelationName(rel)),
+ errhint("You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to validate it."));
+
+ /* All good with this one; don't request another */
+ heap_freetuple(tuple);
+ continue;
+ }
+
+ /* This column is not already not-null, so add it to the queue */
nnconstr = makeNotNullConstraint(lfirst(lc));
newcmd = makeNode(AlterTableCmd);
@@ -9769,11 +9818,15 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
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 adding a valid not-null constraint, set the pg_attribute flag
+ * and tell phase 3 to verify existing rows, if needed. For an
+ * invalid constraint, just set attnotnull and attnotnullvalid,
+ * without queueing verification.
*/
if (constr->contype == CONSTR_NOTNULL)
- set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
+ set_attnotnull(wqueue, rel, ccon->attnum,
+ !constr->skip_validation,
+ !constr->skip_validation);
ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
}
@@ -12437,10 +12490,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
con = (Form_pg_constraint) GETSTRUCT(tuple);
if (con->contype != CONSTRAINT_FOREIGN &&
- con->contype != CONSTRAINT_CHECK)
+ con->contype != CONSTRAINT_CHECK &&
+ con->contype != CONSTRAINT_NOTNULL)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
+ errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key, check, or not-null constraint",
constrName, RelationGetRelationName(rel))));
if (!con->conenforced)
@@ -12459,6 +12513,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
QueueCheckConstraintValidation(wqueue, conrel, rel, constrName,
tuple, recurse, recursing, lockmode);
}
+ else if (con->contype == CONSTRAINT_NOTNULL)
+ {
+ QueueNNConstraintValidation(wqueue, conrel, rel,
+ tuple, recurse, recursing, lockmode);
+ }
ObjectAddressSet(address, ConstraintRelationId, con->oid);
}
@@ -12675,6 +12734,109 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
heap_freetuple(copyTuple);
}
+/*
+ * QueueNNConstraintValidation
+ *
+ * Add an entry to the wqueue to validate the given not-null constraint in
+ * Phase 3 and update the convalidated field in the pg_constraint catalog for
+ * the specified relation and all its inheriting children.
+ */
+static void
+QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+ HeapTuple contuple, bool recurse, bool recursing,
+ LOCKMODE lockmode)
+{
+ Form_pg_constraint con;
+ AlteredTableInfo *tab;
+ HeapTuple copyTuple;
+ Form_pg_constraint copy_con;
+ List *children = NIL;
+ AttrNumber attnum;
+ char *colname;
+
+ con = (Form_pg_constraint) GETSTRUCT(contuple);
+ Assert(con->contype == CONSTRAINT_NOTNULL);
+
+ attnum = extractNotNullColumn(contuple);
+
+ /*
+ * If we're recursing, we've already done this for parent, so skip it.
+ * Also, if the constraint is a NO INHERIT constraint, we shouldn't try to
+ * look for it in the children.
+ *
+ * We recurse before validating on the parent, to reduce risk of
+ * deadlocks.
+ */
+ if (!recursing && !con->connoinherit)
+ children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+
+ colname = get_attname(RelationGetRelid(rel), attnum, false);
+ foreach_oid(childoid, children)
+ {
+ Relation childrel;
+ HeapTuple contup;
+ Form_pg_constraint childcon;
+ char *conname;
+
+ if (childoid == RelationGetRelid(rel))
+ continue;
+
+ /*
+ * If we are told not to recurse, there had better not be any child
+ * tables, because we can't mark the constraint on the parent valid
+ * unless it is valid for all child tables.
+ */
+ if (!recurse)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+ errmsg("constraint must be validated on child tables too"));
+
+ /*
+ * The column on child might have a different attnum, so search by
+ * column name.
+ */
+ contup = findNotNullConstraint(childoid, colname);
+ if (!contup)
+ elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
+ colname, get_rel_name(childoid));
+ childcon = (Form_pg_constraint) GETSTRUCT(contup);
+ if (childcon->convalidated)
+ continue;
+
+ /* find_all_inheritors already got lock */
+ childrel = table_open(childoid, NoLock);
+ conname = pstrdup(NameStr(childcon->conname));
+
+ /* XXX improve ATExecValidateConstraint API to avoid double search */
+ ATExecValidateConstraint(wqueue, childrel, conname,
+ false, true, lockmode);
+ table_close(childrel, NoLock);
+ }
+
+ /* Set the flags appropriately without queueing another validation */
+ set_attnotnull(NULL, rel, attnum, true, false);
+
+ tab = ATGetQueueEntry(wqueue, rel);
+ tab->verify_new_notnull = true;
+
+ /*
+ * Invalidate relcache so that others see the new validated constraint.
+ */
+ CacheInvalidateRelcache(rel);
+
+ /*
+ * Now update the catalogs, while we have the door open.
+ */
+ copyTuple = heap_copytuple(contuple);
+ copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+ copy_con->convalidated = true;
+ CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple);
+
+ InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
+
+ heap_freetuple(copyTuple);
+}
+
/*
* transformColumnNameList - transform list of column names
*
@@ -13543,10 +13705,11 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
false),
RelationGetRelationName(rel)));
- /* All good -- reset attnotnull if needed */
+ /* All good -- reset attnotnull and attnotnullvalid if needed */
if (attForm->attnotnull)
{
attForm->attnotnull = false;
+ attForm->attnotnullvalid = false;
CatalogTupleUpdate(attrel, &atttup->t_self, atttup);
}
@@ -16897,19 +17060,25 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
if (parent_att->attgenerated && !child_att->attgenerated)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("column \"%s\" in child table must be a generated column", parent_attname)));
+ errmsg("column \"%s\" in child table must be a generated column",
+ parent_attname)));
if (child_att->attgenerated && !parent_att->attgenerated)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("column \"%s\" in child table must not be a generated column", parent_attname)));
+ errmsg("column \"%s\" in child table must not be a generated column",
+ parent_attname)));
- if (parent_att->attgenerated && child_att->attgenerated && child_att->attgenerated != parent_att->attgenerated)
+ if (parent_att->attgenerated && child_att->attgenerated &&
+ child_att->attgenerated != parent_att->attgenerated)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("column \"%s\" inherits from generated column of different kind", parent_attname),
+ errmsg("column \"%s\" inherits from generated column of different kind",
+ parent_attname),
errdetail("Parent column is %s, child column is %s.",
- parent_att->attgenerated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL",
- child_att->attgenerated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL")));
+ parent_att->attgenerated == ATTRIBUTE_GENERATED_STORED ?
+ "STORED" : "VIRTUAL",
+ child_att->attgenerated == ATTRIBUTE_GENERATED_STORED ?
+ "STORED" : "VIRTUAL")));
/*
* Regular inheritance children are independent enough not to
@@ -19398,7 +19567,8 @@ PartConstraintImpliedByRelConstraint(Relation scanrel,
{
Form_pg_attribute att = TupleDescAttr(scanrel->rd_att, i - 1);
- if (att->attnotnull && !att->attisdropped)
+ /* invalid not-null constraint must be ignored */
+ if (att->attnotnull && att->attnotnullvalid && !att->attisdropped)
{
NullTest *ntest = makeNode(NullTest);
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index e9bd98c7738..3be2e4f4639 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -2070,6 +2070,7 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
{
Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
+ /* not valid not-null constraint also checked */
if (att->attnotnull && slot_attisnull(slot, attrChk))
{
char *val_desc;
diff --git a/src/backend/jit/llvm/llvmjit_deform.c b/src/backend/jit/llvm/llvmjit_deform.c
index 5d169c7a40b..c562edd094b 100644
--- a/src/backend/jit/llvm/llvmjit_deform.c
+++ b/src/backend/jit/llvm/llvmjit_deform.c
@@ -123,7 +123,7 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
* combination of attisdropped && attnotnull combination shouldn't
* exist.
*/
- if (att->attnotnull &&
+ if (att->attnullability == ATTNULLABLE_VALID &&
!att->atthasmissing &&
!att->attisdropped)
guaranteed_column_number = attnum;
@@ -438,7 +438,7 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
* into account, because if they're present the heaptuple's natts
* would have indicated that a slot_getmissingattrs() is needed.
*/
- if (!att->attnotnull)
+ if (att->attnullability != ATTNULLABLE_VALID)
{
LLVMBasicBlockRef b_ifnotnull;
LLVMBasicBlockRef b_ifnull;
@@ -604,7 +604,8 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
known_alignment = -1;
attguaranteedalign = false;
}
- else if (att->attnotnull && attguaranteedalign && known_alignment >= 0)
+ else if (att->attnullability == ATTNULLABLE_VALID &&
+ attguaranteedalign && known_alignment >= 0)
{
/*
* If the offset to the column was previously known, a NOT NULL &
@@ -614,7 +615,8 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
Assert(att->attlen > 0);
known_alignment += att->attlen;
}
- else if (att->attnotnull && (att->attlen % alignto) == 0)
+ else if (att->attnullability == ATTNULLABLE_VALID &&
+ (att->attlen % alignto) == 0)
{
/*
* After a NOT NULL fixed-width column with a length that is a
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 0489ad36644..44b65b99413 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -177,7 +177,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
{
CompactAttribute *attr = TupleDescCompactAttr(relation->rd_att, i);
- if (attr->attnotnull)
+ if (attr->attnullability == ATTNULLABLE_VALID)
{
rel->notnullattnums = bms_add_member(rel->notnullattnums,
i + 1);
@@ -1355,7 +1355,7 @@ get_relation_constraints(PlannerInfo *root,
{
Form_pg_attribute att = TupleDescAttr(relation->rd_att, i - 1);
- if (att->attnotnull && !att->attisdropped)
+ if (att->attnotnull /* && att->attnotnullvalid */ && !att->attisdropped)
{
NullTest *ntest = makeNode(NullTest);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0fc502a3a40..e6d3c56df86 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4214,9 +4214,9 @@ ConstraintElem:
n->keys = list_make1(makeString($3));
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
- NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
- n->initially_valid = true;
+ n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
}
| UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 896a7f2c59b..92f422540c2 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -329,6 +329,20 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
cd->is_not_null = true;
break;
}
+ if (!cxt.isforeign && !nn->initially_valid)
+ {
+ nn->initially_valid = true;
+ nn->skip_validation = false;
+
+ /*
+ * not-null constraint created via CREATE TABLE will always be
+ * valid. since there is no data there while CREATE TABLE, make
+ * it invalid does not make sense
+ */
+ ereport(WARNING,
+ errcode(ERRCODE_WARNING),
+ errmsg("Ignoring NOT VALID flag for NOT NULL constraint on column \"%s\"", colname));
+ }
}
/*
diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 9ad7681f155..70c11529b90 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1142,6 +1142,7 @@ CatalogCacheInitializeCache(CatCache *cache)
keytype = attr->atttypid;
/* cache key columns should always be NOT NULL */
Assert(attr->attnotnull);
+ Assert(attr->attnotnullvalid);
}
else
{
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9f54a9e72b7..ea035f6feb8 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -590,7 +590,10 @@ RelationBuildTupleDesc(Relation relation)
populate_compact_attribute(relation->rd_att, attnum - 1);
- /* Update constraint/default info */
+ /*
+ * Update constraint/default info has_not_null also include invalid
+ * not-null constraint
+ */
if (attp->attnotnull)
constr->has_not_null = true;
if (attp->attgenerated == ATTRIBUTE_GENERATED_STORED)
@@ -3573,6 +3576,7 @@ RelationBuildLocalRelation(const char *relname,
datt->attidentity = satt->attidentity;
datt->attgenerated = satt->attgenerated;
datt->attnotnull = satt->attnotnull;
+ datt->attnotnullvalid = satt->attnotnullvalid;
has_not_null |= satt->attnotnull;
populate_compact_attribute(rel->rd_att, i);
}
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e41e645f649..4c370889c52 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8919,6 +8919,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQExpBuffer q = createPQExpBuffer();
PQExpBuffer tbloids = createPQExpBuffer();
PQExpBuffer checkoids = createPQExpBuffer();
+ PQExpBuffer invalidnotnulloids = createPQExpBuffer();
PGresult *res;
int ntups;
int curtblindx;
@@ -8938,6 +8939,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
int i_notnull_name;
int i_notnull_noinherit;
int i_notnull_islocal;
+ int i_notnull_validated;
int i_attoptions;
int i_attcollation;
int i_attcompression;
@@ -9018,11 +9020,12 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
/*
* 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.
+ * pg_constraint to obtain the constraint name. notnull_validated and
+ * notnull_noinherit are set according to the NOT VALID and NO INHERIT
+ * properties, respectively. 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 nor NOT VALID.
*
* We track in notnull_islocal whether the constraint was defined directly
* in this table or via an ancestor, for binary upgrade. flagInhAttrs
@@ -9032,11 +9035,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
if (fout->remoteVersion >= 180000)
appendPQExpBufferStr(q,
"co.conname AS notnull_name,\n"
+ "co.convalidated AS notnull_validated,\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"
+ "a.attnotnull AS notnull_validated\n"
"false AS notnull_noinherit,\n"
"a.attislocal AS notnull_islocal,\n");
@@ -9111,6 +9116,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
i_attalign = PQfnumber(res, "attalign");
i_attislocal = PQfnumber(res, "attislocal");
i_notnull_name = PQfnumber(res, "notnull_name");
+ i_notnull_validated = PQfnumber(res, "notnull_validated");
i_notnull_noinherit = PQfnumber(res, "notnull_noinherit");
i_notnull_islocal = PQfnumber(res, "notnull_islocal");
i_attoptions = PQfnumber(res, "attoptions");
@@ -9129,6 +9135,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
* r is handled by the inner loop.
*/
curtblindx = -1;
+ appendPQExpBufferChar(invalidnotnulloids, '{');
for (int r = 0; r < ntups;)
{
Oid attrelid = atooid(PQgetvalue(res, r, i_attrelid));
@@ -9178,6 +9185,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
tbinfo->attfdwoptions = (char **) pg_malloc(numatts * sizeof(char *));
tbinfo->attmissingval = (char **) pg_malloc(numatts * sizeof(char *));
tbinfo->notnull_constrs = (char **) pg_malloc(numatts * sizeof(char *));
+ tbinfo->notnull_validated = (bool *) pg_malloc(numatts * sizeof(bool));
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 *));
@@ -9203,12 +9211,33 @@ 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_validated[j] = (PQgetvalue(res, r, i_notnull_validated)[0] == 't');
- /* Handle not-null constraint name and flags */
- determineNotNullFlags(fout, res, r,
- tbinfo, j,
- i_notnull_name, i_notnull_noinherit,
- i_notnull_islocal);
+ if (tbinfo->notnull_validated[j])
+ {
+ /* Handle not-null constraint name and flags */
+ determineNotNullFlags(fout, res, r,
+ tbinfo, j,
+ i_notnull_name, i_notnull_noinherit,
+ i_notnull_islocal);
+ }
+ else
+ {
+ tbinfo->notnull_constrs[j] = NULL;
+
+ /*
+ * if column notnull_validated is false, it means either
+ * column don't have not-null constraint at all, or the
+ * existing one is invalid. here we onlu dump the invalid
+ * not-null.
+ */
+ if (!PQgetisnull(res, r, i_notnull_name))
+ {
+ if (invalidnotnulloids->len > 1)
+ appendPQExpBufferChar(invalidnotnulloids, ',');
+ appendPQExpBuffer(invalidnotnulloids, "%u", tbinfo->dobj.catId.oid);
+ }
+ }
tbinfo->attoptions[j] = pg_strdup(PQgetvalue(res, r, i_attoptions));
tbinfo->attcollation[j] = atooid(PQgetvalue(res, r, i_attcollation));
@@ -9228,6 +9257,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
appendPQExpBuffer(tbloids, "%u", tbinfo->dobj.catId.oid);
}
}
+ appendPQExpBufferChar(invalidnotnulloids, '}');
PQclear(res);
@@ -9361,6 +9391,112 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
PQclear(res);
}
+ /*
+ * Get info about table NOT NULL NOT VALID constraints. This is skipped
+ * for a data-only dump, as it is only needed for table schemas.
+ */
+ if (dopt->dumpSchema && invalidnotnulloids->len > 2)
+ {
+ ConstraintInfo *constrs;
+ int numConstrs;
+ int i_tableoid;
+ int i_oid;
+ int i_conrelid;
+ int i_conname;
+ int i_consrc;
+ int i_conislocal;
+ int i_convalidated;
+
+ pg_log_info("finding table invalid not null constraints");
+
+ resetPQExpBuffer(q);
+ appendPQExpBuffer(q,
+ "SELECT c.tableoid, c.oid, conrelid, conname, "
+ "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
+ "conislocal, convalidated "
+ "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+ "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
+ "WHERE contype = 'n' AND NOT convalidated "
+ "ORDER BY c.conrelid, c.conname",
+ invalidnotnulloids->data);
+
+ res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
+
+ numConstrs = PQntuples(res);
+ constrs = (ConstraintInfo *) pg_malloc(numConstrs * sizeof(ConstraintInfo));
+
+ i_tableoid = PQfnumber(res, "tableoid");
+ i_oid = PQfnumber(res, "oid");
+ i_conrelid = PQfnumber(res, "conrelid");
+ i_conname = PQfnumber(res, "conname");
+ i_consrc = PQfnumber(res, "consrc");
+ i_conislocal = PQfnumber(res, "conislocal");
+ i_convalidated = PQfnumber(res, "convalidated");
+
+ /* As above, this loop iterates once per table, not once per row */
+ curtblindx = -1;
+ for (int j = 0; j < numConstrs;)
+ {
+ Oid conrelid = atooid(PQgetvalue(res, j, i_conrelid));
+ TableInfo *tbinfo = NULL;
+ int numcons;
+
+ /* Count rows for this table */
+ for (numcons = 1; numcons < numConstrs - j; numcons++)
+ if (atooid(PQgetvalue(res, j + numcons, i_conrelid)) != conrelid)
+ break;
+
+ /*
+ * Locate the associated TableInfo; we rely on tblinfo[] being in
+ * OID order.
+ */
+ while (++curtblindx < numTables)
+ {
+ tbinfo = &tblinfo[curtblindx];
+ if (tbinfo->dobj.catId.oid == conrelid)
+ break;
+ }
+ if (curtblindx >= numTables)
+ pg_fatal("unrecognized table OID %u", conrelid);
+
+ tbinfo->checkexprs = constrs + j;
+
+ for (int c = 0; c < numcons; c++, j++)
+ {
+ bool validated = PQgetvalue(res, j, i_convalidated)[0] == 't';
+
+ Assert(!validated);
+ constrs[j].dobj.objType = DO_CONSTRAINT;
+ constrs[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
+ constrs[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+ AssignDumpId(&constrs[j].dobj);
+ constrs[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_conname));
+ constrs[j].dobj.namespace = tbinfo->dobj.namespace;
+ constrs[j].contable = tbinfo;
+ constrs[j].condomain = NULL;
+ constrs[j].contype = 'n';
+ constrs[j].condef = pg_strdup(PQgetvalue(res, j, i_consrc));
+ constrs[j].confrelid = InvalidOid;
+ constrs[j].conindex = 0;
+ constrs[j].condeferrable = false;
+ constrs[j].condeferred = false;
+ constrs[j].conislocal = (PQgetvalue(res, j, i_conislocal)[0] == 't');
+
+ /*
+ * invalid not-null constraint must dumped separately as an
+ * ALTER TABLE ADD CONSTRAINT entry. because CREATE TABLE can
+ * not create invalid not null constraint. it's also required
+ * for potentially-violating existing data is loaded before
+ * the constraint.
+ */
+ constrs[j].separate = !validated;
+
+ constrs[j].dobj.dump = tbinfo->dobj.dump;
+ }
+ }
+ PQclear(res);
+ }
+
/*
* Get info about table CHECK constraints. This is skipped for a
* data-only dump, as it is only needed for table schemas.
@@ -16658,6 +16794,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
if (print_notnull)
{
+ /* column constraint should all be validated not-null */
+ Assert(tbinfo->notnull_validated[j]);
+
if (tbinfo->notnull_constrs[j][0] == '\0')
appendPQExpBufferStr(q, " NOT NULL");
else
@@ -17977,13 +18116,20 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
.createStmt = q->data,
.dropStmt = delq->data));
}
- else if (coninfo->contype == 'c' && tbinfo)
+ else if ((coninfo->contype == 'c' || coninfo->contype == 'n') && tbinfo)
{
- /* CHECK constraint on a table */
+ /* CHECK or invalid not-null constraint on a table */
/* Ignore if not to be dumped separately, or if it was inherited */
if (coninfo->separate && coninfo->conislocal)
{
+ const char *keyword;
+
+ if (coninfo->contype == 'c')
+ keyword = "CHECK CONSTRAINT";
+ else
+ keyword = "INVALID NOT NULL CONSTRAINT";
+
/* not ONLY since we want it to propagate to children */
appendPQExpBuffer(q, "ALTER %sTABLE %s\n", foreign,
fmtQualifiedDumpable(tbinfo));
@@ -18003,7 +18149,7 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
ARCHIVE_OPTS(.tag = tag,
.namespace = tbinfo->dobj.namespace->dobj.name,
.owner = tbinfo->rolname,
- .description = "CHECK CONSTRAINT",
+ .description = keyword,
.section = SECTION_POST_DATA,
.createStmt = q->data,
.dropStmt = delq->data));
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bbdb30b5f54..aec0f38a353 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -365,10 +365,11 @@ typedef struct _tableInfo
* there isn't one on this column. If
* empty string, unnamed constraint
* (pre-v17) */
+ bool *notnull_validated; /* true if NOT NULL is valid */
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 */
+ struct _constraintInfo *checkexprs; /* CHECK, invalid not-null constraints */
bool needs_override; /* has GENERATED ALWAYS AS IDENTITY */
char *amname; /* relation access method */
@@ -496,6 +497,8 @@ typedef struct _evttriggerInfo
* use a different objType for foreign key constraints, to make it easier
* to sort them the way we want.
*
+ * Not-null constraints don't need this, unless they are NOT VALID.
+ *
* Note: condeferrable and condeferred are currently only valid for
* unique/primary-key constraints. Otherwise that info is in condef.
*/
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index bf565afcc4e..8ba47572ed3 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3114,7 +3114,8 @@ describeOneTableDetails(const char *schemaname,
{
printfPQExpBuffer(&buf,
"SELECT c.conname, a.attname, c.connoinherit,\n"
- " c.conislocal, c.coninhcount <> 0\n"
+ " c.conislocal, c.coninhcount <> 0,\n"
+ " c.convalidated\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"
@@ -3137,14 +3138,16 @@ describeOneTableDetails(const char *schemaname,
{
bool islocal = PQgetvalue(result, i, 3)[0] == 't';
bool inherited = PQgetvalue(result, i, 4)[0] == 't';
+ bool validated = PQgetvalue(result, i, 5)[0] == 't';
- printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s",
+ printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s%s",
PQgetvalue(result, i, 0),
PQgetvalue(result, i, 1),
PQgetvalue(result, i, 2)[0] == 't' ?
" NO INHERIT" :
islocal && inherited ? _(" (local, inherited)") :
- inherited ? _(" (inherited)") : "");
+ inherited ? _(" (inherited)") : "",
+ !validated ? " NOT VALID" : "");
printTableAddFooter(&cont, buf.data);
}
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 396eeb7a0bb..83b33ffc8a4 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -42,7 +42,7 @@ typedef struct TupleConstr
struct AttrMissing *missing; /* missing attributes values, NULL if none */
uint16 num_defval;
uint16 num_check;
- bool has_not_null;
+ bool has_not_null; /* this includes invalid not-null !!! */
bool has_generated_stored;
bool has_generated_virtual;
} TupleConstr;
@@ -76,10 +76,15 @@ typedef struct CompactAttribute
bool atthasmissing; /* as FormData_pg_attribute.atthasmissing */
bool attisdropped; /* as FormData_pg_attribute.attisdropped */
bool attgenerated; /* FormData_pg_attribute.attgenerated != '\0' */
- bool attnotnull; /* as FormData_pg_attribute.attnotnull */
+ char attnullability; /* attnotnull + attnotnullvalid */
uint8 attalignby; /* alignment requirement in bytes */
} CompactAttribute;
+/* Valid values for CompactAttribute->attnullability */
+#define ATTNULLABLE_VALID 'v' /* valid constraint exists */
+#define ATTNULLABLE_INVALID 'i' /* constraint exists, marked invalid */
+#define ATTNULLABLE_NONE 'f' /* no constraint exists */
+
/*
* This struct is passed around within the backend to describe the structure
* of tuples. For tuples coming from on-disk relations, the information is
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index deaa515fe53..9b1236e7a90 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -117,9 +117,12 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
*/
char attcompression BKI_DEFAULT('\0');
- /* This flag represents the "NOT NULL" constraint */
+ /* Whether a not-null constraint exists for the column */
bool attnotnull;
+ /* Whether the not-null constraint, if it exists, is valid */
+ bool attnotnullvalid;
+
/* Has DEFAULT value or not */
bool atthasdef BKI_DEFAULT(f);
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 6da164e7e4d..cf27d53e848 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -263,8 +263,8 @@ 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,
- bool is_local, bool is_no_inherit);
+extern bool AdjustNotNullInheritance(Relation rel, AttrNumber attnum,
+ bool is_local, bool is_no_inherit, bool is_notvalid);
extern List *RelationGetNotNullConstraints(Oid relid, bool cooked,
bool include_noinh);
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..b8398efcdeb 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -896,6 +896,189 @@ Not-null constraints:
"foobar" NOT NULL "a"
DROP TABLE notnull_tbl1;
+-- test NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM pg_constraint
+WHERE conrelid = ANY($1)
+ORDER BY 1;
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; -- error
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; -- ok
+\d+ notnull_tbl1
+ Table "public.notnull_tbl1"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | not null | | plain | |
+ b | integer | | | | plain | |
+Not-null constraints:
+ "nn" NOT NULL "a" NOT VALID
+
+-- even an invalid not-null forbids new nulls:
+INSERT INTO notnull_tbl1 VALUES (NULL, 4);
+ERROR: null value in column "a" of relation "notnull_tbl1" violates not-null constraint
+DETAIL: Failing row contains (null, 4).
+-- a constraint already exists:
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a NOT VALID NO INHERIT;
+ERROR: cannot change NO INHERIT status of NOT NULL constraint "nn" on relation "notnull_tbl1"
+-- Can't change constraint validity this way:
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+ERROR: incompatible NOT VALID constraint "nn" on relation "notnull_tbl1"
+HINT: You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to validate it.
+-- This can change the validity, but must fail if there are nulls
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+ERROR: not-null constraint "nn" of table "notnull_tbl1" has not been validated
+HINT: You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to validate it.
+-- Creating a child table should mark the constraint there as valid
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS (notnull_tbl1);
+NOTICE: merging column "a" with inherited definition
+NOTICE: merging column "b" with inherited definition
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+ conrelid | conname | convalidated | coninhcount
+--------------------+---------+--------------+-------------
+ notnull_tbl1 | nn | f | 0
+ notnull_tbl1_child | nn | t | 1
+(2 rows)
+
+-- Can't make it valid if there are nulls
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+ERROR: column "a" of relation "notnull_tbl1" contains null values
+-- Make the constraint valid
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+--- now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1, notnull_tbl1_child;
+-- dropping an invalid constraint is possible
+CREATE TABLE notnull_tbl1 (a int, b int);
+ALTER TABLE notnull_tbl1 ADD NOT NULL a NOT VALID,
+ ADD NOT NULL b NOT VALID;
+ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL;
+ALTER TABLE notnull_tbl1 DROP CONSTRAINT notnull_tbl1_b_not_null;
+DROP TABLE notnull_tbl1;
+-- ALTER .. NO INHERIT works for invalid constraints
+CREATE TABLE notnull_tbl1 (a int);
+CREATE TABLE notnull_tbl1_chld () INHERITS (notnull_tbl1);
+ALTER TABLE notnull_tbl1 ADD NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ALTER CONSTRAINT notnull_tbl1_a_not_null NO INHERIT;
+-- DROP CONSTRAINT recurses correctly on invalid constraints
+ALTER TABLE notnull_tbl1 ALTER CONSTRAINT notnull_tbl1_a_not_null INHERIT;
+ALTER TABLE notnull_tbl1 DROP CONSTRAINT notnull_tbl1_a_not_null;
+DROP TABLE notnull_tbl1, notnull_tbl1_chld;
+-- Test the different not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+-- parent have valid not null constraint then child table cannot have invalid one
+CREATE TABLE notnull_chld0 (a int);
+ALTER TABLE notnull_chld0 ADD CONSTRAINT nn_chld0 NOT NULL a;
+ALTER TABLE notnull_tbl1 INHERIT notnull_chld0; --error
+ERROR: constraint "nn_parent" conflicts with NOT VALID constraint on child table "notnull_tbl1"
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+ conrelid | conname | convalidated | coninhcount
+--------------+-----------+--------------+-------------
+ notnull_tbl1 | nn_parent | t | 0
+ notnull_chld | nn_child | t | 1
+(2 rows)
+
+DROP TABLE notnull_chld0;
+DROP TABLE notnull_chld;
+DROP TABLE notnull_tbl1;
+-- Test invalid not null on inheritance table.
+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;
+NOTICE: merging definition of column "i" for child "inh_child"
+NOTICE: merging definition of column "i" for child "inh_grandchild"
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i; --error
+ERROR: incompatible NOT VALID constraint "nn" on relation "inh_parent"
+HINT: You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to validate it.
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+ERROR: incompatible NOT VALID constraint "nn" on relation "inh_child"
+HINT: You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to validate it.
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+ conrelid | conname | convalidated | coninhcount
+----------------+---------+--------------+-------------
+ inh_parent | nn | f | 0
+ inh_child | nn | f | 1
+ inh_grandchild | nn | f | 2
+(3 rows)
+
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --ok
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be valid now.
+ conrelid | conname | convalidated | coninhcount
+----------------+---------+--------------+-------------
+ inh_parent | nn | t | 0
+ inh_child | nn | t | 1
+ inh_grandchild | nn | t | 2
+(3 rows)
+
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+DROP TABLE notnull_tbl1;
+ERROR: table "notnull_tbl1" does not exist
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --ok
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES IN (1,2);
+CREATE TABLE notnull_tbl1_2(a int, CONSTRAINT nn2 NOT NULL a, b int);
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_2 FOR VALUES IN (3,4);
+CREATE TABLE notnull_tbl1_3(a int, b int);
+INSERT INTO notnull_tbl1_3 values(NULL,1);
+ALTER TABLE notnull_tbl1_3 add CONSTRAINT nn3 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_3 FOR VALUES IN (NULL,5);
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+ conrelid | conname | convalidated | coninhcount
+----------------+-------------+--------------+-------------
+ notnull_tbl1 | notnull_con | f | 0
+ notnull_tbl1_1 | notnull_con | t | 1
+ notnull_tbl1_2 | nn2 | t | 1
+ notnull_tbl1_3 | nn3 | f | 1
+(4 rows)
+
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --error, table already have null values
+ERROR: column "a" of relation "notnull_tbl1_3" contains null values
+ALTER TABLE notnull_tbl1_3 VALIDATE CONSTRAINT nn3; --error
+ERROR: column "a" of relation "notnull_tbl1_3" contains null values
+TRUNCATE notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --OK
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+ conrelid | conname | convalidated | coninhcount
+----------------+-------------+--------------+-------------
+ notnull_tbl1 | notnull_con | t | 0
+ notnull_tbl1_1 | notnull_con | t | 1
+ notnull_tbl1_2 | nn2 | t | 1
+ notnull_tbl1_3 | nn3 | t | 1
+(4 rows)
+
+DROP TABLE notnull_tbl1;
+-----partitioned table have not-null, then the partitions can not be NOT NULL NOT VALID.
+CREATE TABLE pp_nn (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE pp_nn ADD CONSTRAINT pp_nn_notnull NOT NULL a;
+CREATE TABLE pp_nn_1(a int, b int);
+ALTER TABLE pp_nn_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --error
+ERROR: constraint "nn1" conflicts with NOT VALID constraint on child table "pp_nn_1"
+ALTER TABLE pp_nn_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --ok
+DROP TABLE pp_nn;
+DEALLOCATE get_nnconstraint_info;
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
@@ -1392,3 +1575,17 @@ DROP TABLE constraint_comments_tbl;
DROP DOMAIN constraint_comments_dom;
DROP ROLE regress_constraint_comments;
DROP ROLE regress_constraint_comments_noaccess;
+--sanity check attnotnull and attnotnullvalid.
+select pc.relname, pa.attname, pa.attnum, pa.attnotnull, pa.attnotnullvalid
+from pg_attribute pa join pg_class pc
+on pa.attrelid = pc.oid
+where ( (pa.attnotnull and not pa.attnotnullvalid) or
+ (not pa.attnotnull and pa.attnotnullvalid) )
+and pc.relnamespace in
+ ('pg_catalog'::regnamespace,
+ 'pg_toast'::regnamespace,
+ 'information_schema'::regnamespace);
+ relname | attname | attnum | attnotnull | attnotnullvalid
+---------+---------+--------+------------+-----------------
+(0 rows)
+
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 21ce4177de4..fec1213aa49 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -640,6 +640,146 @@ ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a;
\d+ notnull_tbl1
DROP TABLE notnull_tbl1;
+-- test NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM pg_constraint
+WHERE conrelid = ANY($1)
+ORDER BY 1;
+
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; -- error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; -- ok
+\d+ notnull_tbl1
+
+-- even an invalid not-null forbids new nulls:
+INSERT INTO notnull_tbl1 VALUES (NULL, 4);
+
+-- a constraint already exists:
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a NOT VALID NO INHERIT;
+
+-- Can't change constraint validity this way:
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+
+-- This can change the validity, but must fail if there are nulls
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+
+-- Creating a child table should mark the constraint there as valid
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS (notnull_tbl1);
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+
+-- Can't make it valid if there are nulls
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+-- Make the constraint valid
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+
+--- now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1, notnull_tbl1_child;
+
+-- dropping an invalid constraint is possible
+CREATE TABLE notnull_tbl1 (a int, b int);
+ALTER TABLE notnull_tbl1 ADD NOT NULL a NOT VALID,
+ ADD NOT NULL b NOT VALID;
+ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL;
+ALTER TABLE notnull_tbl1 DROP CONSTRAINT notnull_tbl1_b_not_null;
+DROP TABLE notnull_tbl1;
+
+-- ALTER .. NO INHERIT works for invalid constraints
+CREATE TABLE notnull_tbl1 (a int);
+CREATE TABLE notnull_tbl1_chld () INHERITS (notnull_tbl1);
+ALTER TABLE notnull_tbl1 ADD NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ALTER CONSTRAINT notnull_tbl1_a_not_null NO INHERIT;
+
+-- DROP CONSTRAINT recurses correctly on invalid constraints
+ALTER TABLE notnull_tbl1 ALTER CONSTRAINT notnull_tbl1_a_not_null INHERIT;
+ALTER TABLE notnull_tbl1 DROP CONSTRAINT notnull_tbl1_a_not_null;
+DROP TABLE notnull_tbl1, notnull_tbl1_chld;
+
+-- Test the different not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+
+-- parent have valid not null constraint then child table cannot have invalid one
+CREATE TABLE notnull_chld0 (a int);
+ALTER TABLE notnull_chld0 ADD CONSTRAINT nn_chld0 NOT NULL a;
+ALTER TABLE notnull_tbl1 INHERIT notnull_chld0; --error
+
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+DROP TABLE notnull_chld0;
+DROP TABLE notnull_chld;
+DROP TABLE notnull_tbl1;
+
+
+-- Test invalid not null on inheritance table.
+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;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i; --error
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --ok
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be valid now.
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+
+
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+DROP TABLE notnull_tbl1;
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --ok
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES IN (1,2);
+CREATE TABLE notnull_tbl1_2(a int, CONSTRAINT nn2 NOT NULL a, b int);
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_2 FOR VALUES IN (3,4);
+
+CREATE TABLE notnull_tbl1_3(a int, b int);
+INSERT INTO notnull_tbl1_3 values(NULL,1);
+ALTER TABLE notnull_tbl1_3 add CONSTRAINT nn3 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_3 FOR VALUES IN (NULL,5);
+
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --error, table already have null values
+ALTER TABLE notnull_tbl1_3 VALIDATE CONSTRAINT nn3; --error
+
+TRUNCATE notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --OK
+
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+DROP TABLE notnull_tbl1;
+
+-----partitioned table have not-null, then the partitions can not be NOT NULL NOT VALID.
+CREATE TABLE pp_nn (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE pp_nn ADD CONSTRAINT pp_nn_notnull NOT NULL a;
+
+CREATE TABLE pp_nn_1(a int, b int);
+ALTER TABLE pp_nn_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --error
+ALTER TABLE pp_nn_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --ok
+DROP TABLE pp_nn;
+
+DEALLOCATE get_nnconstraint_info;
+
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
+
+
-- Verify that constraint names and NO INHERIT are properly considered when
-- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
-- and that conflicting cases are rejected. Mind that table constraints
@@ -836,3 +976,14 @@ DROP DOMAIN constraint_comments_dom;
DROP ROLE regress_constraint_comments;
DROP ROLE regress_constraint_comments_noaccess;
+
+--sanity check attnotnull and attnotnullvalid.
+select pc.relname, pa.attname, pa.attnum, pa.attnotnull, pa.attnotnullvalid
+from pg_attribute pa join pg_class pc
+on pa.attrelid = pc.oid
+where ( (pa.attnotnull and not pa.attnotnullvalid) or
+ (not pa.attnotnull and pa.attnotnullvalid) )
+and pc.relnamespace in
+ ('pg_catalog'::regnamespace,
+ 'pg_toast'::regnamespace,
+ 'information_schema'::regnamespace);
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-21 18:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-24 01:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-27 19:25 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-03-28 08:50 ` jian he <[email protected]>
2025-03-28 18:42 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: jian he @ 2025-03-28 08:50 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: Robert Haas <[email protected]>; Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On Fri, Mar 28, 2025 at 3:25 AM Alvaro Herrera <[email protected]> wrote:
>
> On 2025-Mar-24, jian he wrote:
>
> > hi.
> > you may like the attached. it's based on your idea: attnotnullvalid.
>
> This is quite close to what I was thinking, yeah. I noticed a couple of
> bugs however, and ended up cleaning up the whole thing. Here's what I
> have so far. I'm not sure the pg_dump bits are okay (apart from what
> you report below) -- I think it's losing the constraint names, which is
> of course unacceptable.
>
hi.
/*
* Update constraint/default info has_not_null also include invalid
* not-null constraint
*/
this comment needs a period. it should be:
/*
* Update constraint/default info. has_not_null also include invalid
* not-null constraint
*/
gram.y:
/* no NOT VALID support yet */
processCASbits($4, @4, "NOT NULL",
NULL, NULL, NULL, &n->skip_validation,
&n->is_no_inherit, yyscanner);
n->initially_valid = !n->skip_validation;
$$ = (Node *) n;
comment "/* no NOT VALID support yet */" should be removed?
get_relation_constraints:
- if (att->attnotnull && !att->attisdropped)
+ if (att->attnotnull /* && att->attnotnullvalid */ && !att->attisdropped)
looking at how we deal with check constraints,
i think we need
+ if (att->attnotnull && att->attnotnullvalid && !att->attisdropped)
set_attnotnull comments should say something about attnotnullvalid?
since it will change pg_attribute.attnotnullvalid
ATPrepAddPrimaryKey
+ if (!conForm->convalidated)
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("not-null constraint \"%s\" of table \"%s\" has not been validated",
+ NameStr(conForm->conname),
+ RelationGetRelationName(rel)),
+ errhint("You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to
validate it."));
I think the error message is less helpful.
Overall, I think we should say that:
to add the primary key on column x requires a validated not-null
constraint on column x.
------------------------------------------------------------------------
i think your patch messed up with pg_constraint.conislocal.
for example:
CREATE TABLE parted (id bigint default 1,id_abc bigint) PARTITION BY LIST (id);
alter TABLE parted add CONSTRAINT dummy_constr not null id not valid;
CREATE TABLE parted_1 (id bigint default 1,id_abc bigint);
alter TABLE parted_1 add CONSTRAINT dummy_constr not null id;
ALTER TABLE parted ATTACH PARTITION parted_1 FOR VALUES IN ('1');
select conrelid::regclass, conname, conislocal
from pg_constraint where conname = 'dummy_constr';
conrelid | conname | conislocal
----------+--------------+------------
parted | dummy_constr | t
parted_1 | dummy_constr | f
(2 rows)
if you do pg_dump, and execute the pg_dump output
pg_dump --no-statistics --clean --table-and-children=*parted*
--no-owner --verbose --column-inserts --file=dump.sql --no-acl
select conrelid::regclass, conname, conislocal
from pg_constraint where conname = 'dummy_constr';
output is
conrelid | conname | conislocal
----------+--------------+------------
parted | dummy_constr | t
parted_1 | dummy_constr | t
(2 rows)
because pg_dump will produce
CREATE TABLE public.parted (id bigint DEFAULT 1, id_abc bigint )
PARTITION BY LIST (id);
CREATE TABLE public.parted_1 ( id bigint DEFAULT 1 CONSTRAINT
dummy_constr NOT NULL, id_abc bigint);
ALTER TABLE public.parted ADD CONSTRAINT dummy_constr NOT NULL id NOT VALID;
the third sql command didn't override the parted_1 constraint
dummy_constr conislocal value.
you may check my code change in AdjustNotNullInheritance
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-21 18:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-24 01:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-27 19:25 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-28 08:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
@ 2025-03-28 18:42 ` Alvaro Herrera <[email protected]>
2025-03-31 08:45 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-31 17:21 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
0 siblings, 2 replies; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-28 18:42 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Robert Haas <[email protected]>; Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On 2025-Mar-28, jian he wrote:
> i think your patch messed up with pg_constraint.conislocal.
> for example:
>
> CREATE TABLE parted (id bigint default 1,id_abc bigint) PARTITION BY LIST (id);
> alter TABLE parted add CONSTRAINT dummy_constr not null id not valid;
> CREATE TABLE parted_1 (id bigint default 1,id_abc bigint);
> alter TABLE parted_1 add CONSTRAINT dummy_constr not null id;
> ALTER TABLE parted ATTACH PARTITION parted_1 FOR VALUES IN ('1');
>
> select conrelid::regclass, conname, conislocal
> from pg_constraint where conname = 'dummy_constr';
>
> conrelid | conname | conislocal
> ----------+--------------+------------
> parted | dummy_constr | t
> parted_1 | dummy_constr | f
> (2 rows)
>
>
> if you do pg_dump, and execute the pg_dump output
> pg_dump --no-statistics --clean --table-and-children=*parted*
> --no-owner --verbose --column-inserts --file=dump.sql --no-acl
>
> select conrelid::regclass, conname, conislocal
> from pg_constraint where conname = 'dummy_constr';
> output is
>
> conrelid | conname | conislocal
> ----------+--------------+------------
> parted | dummy_constr | t
> parted_1 | dummy_constr | t
> (2 rows)
Interesting. Yeah, I removed the code you had there because it was
super weird, had no comments, and removing it had zero effect (no tests
failed), so I thought it was useless. But apparently something is going
on here that's not what we want.
To fix this, we could say that pg_dump should realize the difference and
dump in a different way ... however I think that'd require looking at
conislocal differently, which I definitely don't want to mess with.
Maybe the real problem here is that making the (valid) child constraint
no longer local when the parent constraint is not valid is not sensible,
precisely because pg_dump won't be able to produce good output. That
sounds more workable to me ... except that we'd have to ensure that
validating the parent constraint would turn the child constraints as not
local anymore, which might be a bit weird. But maybe not weirder than
the other approach.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"I must say, I am absolutely impressed with what pgsql's implementation of
VALUES allows me to do. It's kind of ridiculous how much "work" goes away in
my code. Too bad I can't do this at work (Oracle 8/9)." (Tom Allison)
http://archives.postgresql.org/pgsql-general/2007-06/msg00016.php
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-21 18:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-24 01:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-27 19:25 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-28 08:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-28 18:42 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-03-31 08:45 ` jian he <[email protected]>
2025-03-31 12:25 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
1 sibling, 1 reply; 40+ messages in thread
From: jian he @ 2025-03-31 08:45 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: Robert Haas <[email protected]>; Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On Sat, Mar 29, 2025 at 2:42 AM Alvaro Herrera <[email protected]> wrote:
>
> On 2025-Mar-28, jian he wrote:
>
> > i think your patch messed up with pg_constraint.conislocal.
> > for example:
> >
> > CREATE TABLE parted (id bigint default 1,id_abc bigint) PARTITION BY LIST (id);
> > alter TABLE parted add CONSTRAINT dummy_constr not null id not valid;
> > CREATE TABLE parted_1 (id bigint default 1,id_abc bigint);
> > alter TABLE parted_1 add CONSTRAINT dummy_constr not null id;
> > ALTER TABLE parted ATTACH PARTITION parted_1 FOR VALUES IN ('1');
> >
> > select conrelid::regclass, conname, conislocal
> > from pg_constraint where conname = 'dummy_constr';
> >
> > conrelid | conname | conislocal
> > ----------+--------------+------------
> > parted | dummy_constr | t
> > parted_1 | dummy_constr | f
> > (2 rows)
> >
> >
> > if you do pg_dump, and execute the pg_dump output
> > pg_dump --no-statistics --clean --table-and-children=*parted*
> > --no-owner --verbose --column-inserts --file=dump.sql --no-acl
> >
> > select conrelid::regclass, conname, conislocal
> > from pg_constraint where conname = 'dummy_constr';
> > output is
> >
> > conrelid | conname | conislocal
> > ----------+--------------+------------
> > parted | dummy_constr | t
> > parted_1 | dummy_constr | t
> > (2 rows)
>
> Interesting. Yeah, I removed the code you had there because it was
> super weird, had no comments, and removing it had zero effect (no tests
> failed), so I thought it was useless. But apparently something is going
> on here that's not what we want.
>
my change in AdjustNotNullInheritance is copied from
MergeConstraintsIntoExisting
``
/*
* OK, bump the child constraint's inheritance count. (If we fail
* later on, this change will just roll back.)
*/
child_copy = heap_copytuple(child_tuple);
child_con = (Form_pg_constraint) GETSTRUCT(child_copy);
if (pg_add_s16_overflow(child_con->coninhcount, 1,
&child_con->coninhcount))
ereport(ERROR,
errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("too many inheritance parents"));
/*
* In case of partitions, an inherited constraint must be
* inherited only once since it cannot have multiple parents and
* it is never considered local.
*/
if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
Assert(child_con->coninhcount == 1);
child_con->conislocal = false;
}
``
if you look at MergeConstraintsIntoExisting, then it won't be weird.
AdjustNotNullInheritance is kind of doing the same thing as
MergeConstraintsIntoExisting, I think.
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-21 18:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-24 01:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-27 19:25 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-28 08:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-28 18:42 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-31 08:45 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
@ 2025-03-31 12:25 ` jian he <[email protected]>
2025-03-31 13:10 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: jian he @ 2025-03-31 12:25 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: Robert Haas <[email protected]>; Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
hi.
in notnull-notvalid.patch
+ if (coninfo->contype == 'c')
+ keyword = "CHECK CONSTRAINT";
+ else
+ keyword = "INVALID NOT NULL CONSTRAINT";
we have a new TocEntry->desc kind.
so the following related code within src/bin/pg_dump also needs change
----------------
if (strcmp(te->desc, "CONSTRAINT") == 0 ||
strcmp(te->desc, "CHECK CONSTRAINT") == 0 ||
strcmp(te->desc, "FK CONSTRAINT") == 0)
strcpy(buffer, "DROP CONSTRAINT");
else
snprintf(buffer, sizeof(buffer), "DROP %s",
te->desc);
----------------
else if (strcmp(te->desc, "CONSTRAINT") == 0 ||
strcmp(te->desc, "CHECK CONSTRAINT") == 0 ||
strcmp(te->desc, "FK CONSTRAINT") == 0 ||
strcmp(te->desc, "INDEX") == 0 ||
strcmp(te->desc, "RULE") == 0 ||
strcmp(te->desc, "TRIGGER") == 0)
te->section = SECTION_POST_DATA;
----------------
/* these object types don't have separate owners */
else if (strcmp(type, "CAST") == 0 ||
strcmp(type, "CHECK CONSTRAINT") == 0 ||
strcmp(type, "CONSTRAINT") == 0 ||
strcmp(type, "DATABASE PROPERTIES") == 0 ||
strcmp(type, "DEFAULT") == 0 ||
strcmp(type, "FK CONSTRAINT") == 0 ||
strcmp(type, "INDEX") == 0 ||
strcmp(type, "RULE") == 0 ||
strcmp(type, "TRIGGER") == 0 ||
strcmp(type, "ROW SECURITY") == 0 ||
strcmp(type, "POLICY") == 0 ||
strcmp(type, "USER MAPPING") == 0)
{
/* do nothing */
}
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-21 18:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-24 01:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-27 19:25 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-28 08:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-28 18:42 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-31 08:45 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-31 12:25 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
@ 2025-03-31 13:10 ` Alvaro Herrera <[email protected]>
0 siblings, 0 replies; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-31 13:10 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Robert Haas <[email protected]>; Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On 2025-Mar-31, jian he wrote:
> hi.
> in notnull-notvalid.patch
>
> + if (coninfo->contype == 'c')
> + keyword = "CHECK CONSTRAINT";
> + else
> + keyword = "INVALID NOT NULL CONSTRAINT";
> we have a new TocEntry->desc kind.
Yeah, I wasn't sure that this change made much actual sense. I think it
may be better to stick with just CONSTRAINT. There probably isn't
sufficient reason to have a different ->desc value.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-21 18:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-24 01:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-27 19:25 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-28 08:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-28 18:42 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-03-31 17:21 ` Robert Haas <[email protected]>
1 sibling, 0 replies; 40+ messages in thread
From: Robert Haas @ 2025-03-31 17:21 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: jian he <[email protected]>; Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On Fri, Mar 28, 2025 at 2:42 PM Alvaro Herrera <[email protected]> wrote:
> Maybe the real problem here is that making the (valid) child constraint
> no longer local when the parent constraint is not valid is not sensible,
> precisely because pg_dump won't be able to produce good output. That
> sounds more workable to me ... except that we'd have to ensure that
> validating the parent constraint would turn the child constraints as not
> local anymore, which might be a bit weird. But maybe not weirder than
> the other approach.
It seems like a bad idea to make conislocal and coninhcount have
anything to do with whether the constraint is valid. We need those to
mean what they have traditionally meant just to make the correct
things happen when the constraint is dropped, either directly from the
child or at some ancestor of that child. If something is dropped at an
ancestor table, we cascade down the tree and decrement coninhcount. If
something is dropped at a child table, we can set conislocal=false.
The constraint goes away, IIUC, when coninhcount=0 and
conislocal=false, which directly corresponds to "this constraint no
longer has a remaining definition either locally or by inheritance". I
don't see how you can change much of anything here without breaking
the existing structure. Validity could have separate tracking of some
sort, possibly as elaborate as convalidlocal and convalidinhcount, but
I don't think it can get away with redefining the tracking that we
already have for existence.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-21 18:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-03-24 14:22 ` Robert Haas <[email protected]>
2025-03-24 16:29 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
1 sibling, 1 reply; 40+ messages in thread
From: Robert Haas @ 2025-03-24 14:22 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On Fri, Mar 21, 2025 at 2:04 PM Alvaro Herrera <[email protected]> wrote:
> In several of the cases that I checked, the application just tests the
> returned value for boolean truth. If we change the column from boolean
> to char, they would stop working properly because both the 't' and the
> 'f' char values would test as true. But suppose we were to rename the
> column; that would cause developers to have to examine the code to
> determine how to react. That might even be good, because we're end up
> in a situation were no application uses outdated assumptions about
> nullness in a column.
Yes.
> However, consider the rationale given in
> https://postgr.es/m/[email protected]
> that removing attndims would break PHP -- after that discussion, we
> decided against removing the column, even though it's completely
> useless, because we don't want to break PHP. You know, removing
> attnotnull would break PHP in exactly the same way, or maybe in some
> worse way. I don't see how can we reach a different conclusion for this
> change that for that one.
Well, that discussion seems awfully weird to me. I can recall plenty
of cases where we've changed stuff in the system catalog and rebuffed
complaints from people who were grumpy about having to adjust their
queries. I don't really understand what makes that case different.
I mean, maybe there's an argument that some changes are more
disruptive than others. For instance, if removing attndims would force
drivers to run extra more complicated queries to learn whether a
certain type is an array type, one could argue that taking it away
without providing some alternative is breaking the drivers in some
fundamental way. I'm not sure whether that's a real problem, but there
is no such argument to be made here. There's no proposal on the table
to entirely remove any information from pg_attribute -- only to turn
something that is presently two-valued into something three-valued.
So, it will still be possible for it to learn the same facts that it
can learn today, it will just perhaps need to be adjusted in terms of
exactly how it does that. But if that is prohibited then what made it
OK all the other times we've done it?
Again, I'm not 100% positive that changing the Boolean column to a
three-valued column is the right way forward, but it does have the
advantage of saving a byte, and the width of system catalog tables has
been a periodic concern. Also, relpersistence is an example of a
seemingly very similar kind of change - turning a Boolean column into
a char column so it can support three values. Do we with the benefit
of hindsight consider that to have been a mistake? I don't, because I
think that PostgreSQL development will be paralyzed if we prohibit
such changes, but opinions might vary.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-21 18:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-24 14:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
@ 2025-03-24 16:29 ` Alvaro Herrera <[email protected]>
2025-03-24 17:47 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: Alvaro Herrera @ 2025-03-24 16:29 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On 2025-Mar-24, Robert Haas wrote:
> I mean, maybe there's an argument that some changes are more
> disruptive than others. For instance, if removing attndims would force
> drivers to run extra more complicated queries to learn whether a
> certain type is an array type, one could argue that taking it away
> without providing some alternative is breaking the drivers in some
> fundamental way. I'm not sure whether that's a real problem, but there
> is no such argument to be made here. There's no proposal on the table
> to entirely remove any information from pg_attribute -- only to turn
> something that is presently two-valued into something three-valued.
> So, it will still be possible for it to learn the same facts that it
> can learn today, it will just perhaps need to be adjusted in terms of
> exactly how it does that. But if that is prohibited then what made it
> OK all the other times we've done it?
I think use of attnotnull is much more prevalent than that of fields
we've removed or changed previously. In pg_attribute we recently
removed attcacheoff, which pretty much nobody uses; going back to 2016 I
couldn't find any other removal or type change. In pg_class, I can see
that we got rid of reltoastidxid in 2013, but that one was quite
esoteric and I doubt anybody would be interested in that. One commit
that I see removed a lot of columns is 739adf32eecf, but that was in
2002.
One we removed not so long ago was protransform, which was turned into
prosupport by commit 1fb57af92069 in 2019. But I'm sure that
protransform was much less used by external code, because it's very
specialized, so there weren't so many complaints. We removed
proisagg/proiswindow to turn them into prokind with commit fd1a421fe661
(2018), and you can find plenty of user complaints about that.
Use of attnotnull is very widespread.
> Again, I'm not 100% positive that changing the Boolean column to a
> three-valued column is the right way forward, but it does have the
> advantage of saving a byte, and the width of system catalog tables has
> been a periodic concern.
In this case, as I already said, the new boolean column would go in
what's currently padding space, so there's no widening taking place.
> Also, relpersistence is an example of a seemingly very similar kind of
> change - turning a Boolean column into a char column so it can support
> three values. Do we with the benefit of hindsight consider that to
> have been a mistake?
Are you talking about 5f7b58fad8f4 "Generalize concept of temporary
relations to "relation persistence"." from 2010? That was 15 years ago,
and maybe it was not a mistake because the ecosystem around Postgres was
different then, but also relistemp/relpersistence is not as widely used
as attnotnull. There were complaints regarding that change though:
https://www.postgresql.org/message-id/flat/[email protected]
> I don't, because I think that PostgreSQL development will be paralyzed
> if we prohibit such changes, but opinions might vary.
It's very rare that we need to make catalog changes like this, so I
think you're being overly dramatic. This is not going to paralyze the
development. But I agree that we should still allow ourselves to change
system schema, even if we'd break a few things here and there, as long
as it's not the world.
Please don't think that I'm in favor of doing it the difficult way for
no reason. Previous versions of the patch (changing attnotnull to
char), from the Postgres core code perspective, were pretty much ready
-- we're having this discussion only to avoid the breakage. I could
save us a bunch of time here and just push those patches, but I think
the responsible thing here (the one we're less likely to regret) is not
to.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-21 18:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-24 14:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-24 16:29 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-03-24 17:47 ` Robert Haas <[email protected]>
0 siblings, 0 replies; 40+ messages in thread
From: Robert Haas @ 2025-03-24 17:47 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On Mon, Mar 24, 2025 at 12:29 PM Alvaro Herrera <[email protected]> wrote:
> > Again, I'm not 100% positive that changing the Boolean column to a
> > three-valued column is the right way forward, but it does have the
> > advantage of saving a byte, and the width of system catalog tables has
> > been a periodic concern.
>
> In this case, as I already said, the new boolean column would go in
> what's currently padding space, so there's no widening taking place.
IMHO, there will almost certainly be more single-byte columns at some
point in the future, so I feel like using up padding space is not much
different than actual widening over the long term.
> Please don't think that I'm in favor of doing it the difficult way for
> no reason. Previous versions of the patch (changing attnotnull to
> char), from the Postgres core code perspective, were pretty much ready
> -- we're having this discussion only to avoid the breakage. I could
> save us a bunch of time here and just push those patches, but I think
> the responsible thing here (the one we're less likely to regret) is not
> to.
Fair enough. I think I've said what I want to say, and I'm not here to
substitute my judgement for yours.
--
Robert Haas
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
@ 2025-04-03 08:01 Peter Eisentraut <[email protected]>
2025-04-03 08:07 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: Peter Eisentraut @ 2025-04-03 08:01 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; jian he <[email protected]>; +Cc: Robert Haas <[email protected]>; Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
It occurred to me that we will also want to have NOT NULL NOT ENFORCED
constraints eventually. As we have discussed elsewhere, the NOT
ENFORCED state is closely related to the NOT VALID state. So that
should probably be considered in the design here.
Reading up on this again now, I'm confused about putting the NOT VALID
state for not-null constraints into pg_attribute. We have catalogued
not-null constraints now, so we can put metadata for them into
pg_constraint! And we have NOT VALID and NOT ENFORCED flags in
pg_constraint already.
So what is the purpose of the attnotnullvalid field? In the latest
posted patch, I don't see this column used in the executor for the
actual constraint checking. So is this all merely for clients to
understand the constraint metadata? If we add more metadata for
not-null constraints, do we need to add a new pg_attribute flag for each
one? That doesn't seem right.
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-04-03 08:01 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Peter Eisentraut <[email protected]>
@ 2025-04-03 08:07 ` Alvaro Herrera <[email protected]>
2025-04-03 15:03 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Peter Eisentraut <[email protected]>
0 siblings, 1 reply; 40+ messages in thread
From: Alvaro Herrera @ 2025-04-03 08:07 UTC (permalink / raw)
To: Peter Eisentraut <[email protected]>; +Cc: jian he <[email protected]>; Robert Haas <[email protected]>; Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On 2025-Apr-03, Peter Eisentraut wrote:
> It occurred to me that we will also want to have NOT NULL NOT ENFORCED
> constraints eventually. As we have discussed elsewhere, the NOT
> ENFORCED state is closely related to the NOT VALID state. So that
> should probably be considered in the design here.
Yeah, I don't think there's time to shoehorn NOT ENFORCED status for
not-null constraints. I'd guess that it'd take at least a couple of
weeks to make that work.
> Reading up on this again now, I'm confused about putting the NOT VALID
> state for not-null constraints into pg_attribute. We have catalogued
> not-null constraints now, so we can put metadata for them into
> pg_constraint! And we have NOT VALID and NOT ENFORCED flags in
> pg_constraint already.
>
> So what is the purpose of the attnotnullvalid field? In the latest posted
> patch, I don't see this column used in the executor for the actual
> constraint checking. So is this all merely for clients to understand the
> constraint metadata? If we add more metadata for not-null constraints, do
> we need to add a new pg_attribute flag for each one? That doesn't seem
> right.
The new flag is there for quick access by get_relation_info. We could
easily not have it otherwise, because clients don't need it, but its
lack would probably make planning measurably slower because it'd have to
do syscache access for every single not-null constraint to figure out if
it's valid or not.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"Hay quien adquiere la mala costumbre de ser infeliz" (M. A. Evans)
^ permalink raw reply [nested|flat] 40+ messages in thread
* Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints
2025-04-03 08:01 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Peter Eisentraut <[email protected]>
2025-04-03 08:07 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
@ 2025-04-03 15:03 ` Peter Eisentraut <[email protected]>
0 siblings, 0 replies; 40+ messages in thread
From: Peter Eisentraut @ 2025-04-03 15:03 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: jian he <[email protected]>; Robert Haas <[email protected]>; Rushabh Lathia <[email protected]>; Ashutosh Bapat <[email protected]>; pgsql-hackers
On 03.04.25 10:07, Alvaro Herrera wrote:
> The new flag is there for quick access by get_relation_info. We could
> easily not have it otherwise, because clients don't need it, but its
> lack would probably make planning measurably slower because it'd have to
> do syscache access for every single not-null constraint to figure out if
> it's valid or not.
In the v6 patch, you are adding a attnullability field to the
CompactAttribute in the tuple descriptor and use that in
get_relation_info(). That seems like the right approach, because then
you're doing all that preprocessing about which constraint is active in
the relcache. So I don't see where the extra pg_attribute field
attnotnullvalid is getting used.
^ permalink raw reply [nested|flat] 40+ messages in thread
end of thread, other threads:[~2025-04-03 15:03 UTC | newest]
Thread overview: 40+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2021-02-07 18:09 [PATCH v4 2/2] Do COPY FROM encoding conversion/verification in larger chunks. Heikki Linnakangas <[email protected]>
2025-03-10 11:26 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-10 11:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-11 10:15 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 08:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 09:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 10:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 10:08 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 10:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-12 11:51 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-12 11:57 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-12 18:20 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-13 08:37 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-17 11:14 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-17 11:23 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-20 09:54 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Rushabh Lathia <[email protected]>
2025-03-20 10:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-20 10:34 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-20 11:07 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-20 12:19 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Ashutosh Bapat <[email protected]>
2025-03-20 14:59 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-20 15:52 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 14:36 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-21 14:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-21 15:26 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-21 18:04 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-24 01:27 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-27 19:25 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-28 08:50 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-28 18:42 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-31 08:45 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-31 12:25 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints jian he <[email protected]>
2025-03-31 13:10 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-31 17:21 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-24 14:22 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-03-24 16:29 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-03-24 17:47 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Robert Haas <[email protected]>
2025-04-03 08:01 Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Peter Eisentraut <[email protected]>
2025-04-03 08:07 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Alvaro Herrera <[email protected]>
2025-04-03 15:03 ` Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints Peter Eisentraut <[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