mirror of
https://github.com/postgres/postgres.git
synced 2025-11-24 00:23:06 +03:00
Catalog not-null constraints
We now create contype='n' pg_constraint rows for not-null constraints.
We propagate these constraints to other tables during operations such as
adding inheritance relationships, creating and attaching partitions and
creating tables LIKE other tables. We also spawn not-null constraints
for inheritance child tables when their parents have primary keys.
These related constraints mostly follow the well-known rules of
conislocal and coninhcount that we have for CHECK constraints, with some
adaptations: for example, as opposed to CHECK constraints, we don't
match not-null ones by name when descending a hierarchy to alter it,
instead matching by column name that they apply to. This means we don't
require the constraint names to be identical across a hierarchy.
For now, we omit them for system catalogs. Maybe this is worth
reconsidering. We don't support NOT VALID nor DEFERRABLE clauses
either; these can be added as separate features later (this patch is
already large and complicated enough.)
psql shows these constraints in \d+.
pg_dump requires some ad-hoc hacks, particularly when dumping a primary
key. We now create one "throwaway" not-null constraint for each column
in the PK together with the CREATE TABLE command, and once the PK is
created, all those throwaway constraints are removed. This avoids
having to check each tuple for nullness when the dump restores the
primary key creation.
pg_upgrading from an older release requires a somewhat brittle procedure
to create a constraint state that matches what would be created if the
database were being created fresh in Postgres 17. I have tested all the
scenarios I could think of, and it works correctly as far as I can tell,
but I could have neglected weird cases.
This patch has been very long in the making. The first patch was
written by Bernd Helmle in 2010 to add a new pg_constraint.contype value
('n'), which I (Álvaro) then hijacked in 2011 and 2012, until that one
was killed by the realization that we ought to use contype='c' instead:
manufactured CHECK constraints. However, later SQL standard
development, as well as nonobvious emergent properties of that design
(mostly, failure to distinguish them from "normal" CHECK constraints as
well as the performance implication of having to test the CHECK
expression) led us to reconsider this choice, so now the current
implementation uses contype='n' again. During Postgres 16 this had
already been introduced by commit e056c557ae, but there were some
problems mainly with the pg_upgrade procedure that couldn't be fixed in
reasonable time, so it was reverted.
In 2016 Vitaly Burovoy also worked on this feature[1] but found no
consensus for his proposed approach, which was claimed to be closer to
the letter of the standard, requiring an additional pg_attribute column
to track the OID of the not-null constraint for that column.
[1] https://postgr.es/m/CAKOSWNkN6HSyatuys8xZxzRCR-KL1OkHS5-b9qd9bf1Rad3PLA@mail.gmail.com
Author: Álvaro Herrera <alvherre@alvh.no-ip.org>
Author: Bernd Helmle <mailings@oopsware.de>
Reviewed-by: Justin Pryzby <pryzby@telsasoft.com>
Reviewed-by: Peter Eisentraut <peter.eisentraut@enterprisedb.com>
Reviewed-by: Dean Rasheed <dean.a.rasheed@gmail.com>
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
#include "access/xact.h"
|
||||
#include "catalog/catalog.h"
|
||||
#include "catalog/dependency.h"
|
||||
#include "catalog/heap.h"
|
||||
#include "catalog/indexing.h"
|
||||
#include "catalog/objectaccess.h"
|
||||
#include "catalog/pg_constraint.h"
|
||||
@@ -562,6 +563,291 @@ ChooseConstraintName(const char *name1, const char *name2,
|
||||
return conname;
|
||||
}
|
||||
|
||||
/*
|
||||
* Find and return the pg_constraint tuple that implements a validated
|
||||
* not-null constraint for the given column of the given relation.
|
||||
*
|
||||
* XXX This would be easier if we had pg_attribute.notnullconstr with the OID
|
||||
* of the constraint that implements the not-null constraint for that column.
|
||||
* I'm not sure it's worth the catalog bloat and de-normalization, however.
|
||||
*/
|
||||
HeapTuple
|
||||
findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
|
||||
{
|
||||
Relation pg_constraint;
|
||||
HeapTuple conTup,
|
||||
retval = NULL;
|
||||
SysScanDesc scan;
|
||||
ScanKeyData key;
|
||||
|
||||
pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
|
||||
ScanKeyInit(&key,
|
||||
Anum_pg_constraint_conrelid,
|
||||
BTEqualStrategyNumber, F_OIDEQ,
|
||||
ObjectIdGetDatum(relid));
|
||||
scan = systable_beginscan(pg_constraint, ConstraintRelidTypidNameIndexId,
|
||||
true, NULL, 1, &key);
|
||||
|
||||
while (HeapTupleIsValid(conTup = systable_getnext(scan)))
|
||||
{
|
||||
Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
|
||||
AttrNumber conkey;
|
||||
|
||||
/*
|
||||
* We're looking for a NOTNULL constraint that's marked validated,
|
||||
* with the column we're looking for as the sole element in conkey.
|
||||
*/
|
||||
if (con->contype != CONSTRAINT_NOTNULL)
|
||||
continue;
|
||||
if (!con->convalidated)
|
||||
continue;
|
||||
|
||||
conkey = extractNotNullColumn(conTup);
|
||||
if (conkey != attnum)
|
||||
continue;
|
||||
|
||||
/* Found it */
|
||||
retval = heap_copytuple(conTup);
|
||||
break;
|
||||
}
|
||||
|
||||
systable_endscan(scan);
|
||||
table_close(pg_constraint, AccessShareLock);
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*
|
||||
* Find and return the pg_constraint tuple that implements a validated
|
||||
* not-null constraint for the given column of the given relation.
|
||||
*/
|
||||
HeapTuple
|
||||
findNotNullConstraint(Oid relid, const char *colname)
|
||||
{
|
||||
AttrNumber attnum = get_attnum(relid, colname);
|
||||
|
||||
return findNotNullConstraintAttnum(relid, attnum);
|
||||
}
|
||||
|
||||
/*
|
||||
* Given a pg_constraint tuple for a not-null constraint, return the column
|
||||
* number it is for.
|
||||
*/
|
||||
AttrNumber
|
||||
extractNotNullColumn(HeapTuple constrTup)
|
||||
{
|
||||
AttrNumber colnum;
|
||||
Datum adatum;
|
||||
ArrayType *arr;
|
||||
|
||||
/* only tuples for not-null constraints should be given */
|
||||
Assert(((Form_pg_constraint) GETSTRUCT(constrTup))->contype == CONSTRAINT_NOTNULL);
|
||||
|
||||
adatum = SysCacheGetAttrNotNull(CONSTROID, constrTup,
|
||||
Anum_pg_constraint_conkey);
|
||||
arr = DatumGetArrayTypeP(adatum); /* ensure not toasted */
|
||||
if (ARR_NDIM(arr) != 1 ||
|
||||
ARR_HASNULL(arr) ||
|
||||
ARR_ELEMTYPE(arr) != INT2OID ||
|
||||
ARR_DIMS(arr)[0] != 1)
|
||||
elog(ERROR, "conkey is not a 1-D smallint array");
|
||||
|
||||
memcpy(&colnum, ARR_DATA_PTR(arr), sizeof(AttrNumber));
|
||||
|
||||
if ((Pointer) arr != DatumGetPointer(adatum))
|
||||
pfree(arr); /* free de-toasted copy, if any */
|
||||
|
||||
return colnum;
|
||||
}
|
||||
|
||||
/*
|
||||
* AdjustNotNullInheritance1
|
||||
* Adjust inheritance count for a single not-null constraint
|
||||
*
|
||||
* Adjust inheritance count, and possibly islocal status, for the not-null
|
||||
* constraint row of the given column, if it exists, and return true.
|
||||
* If no not-null constraint is found for the column, return false.
|
||||
*/
|
||||
bool
|
||||
AdjustNotNullInheritance1(Oid relid, AttrNumber attnum, int count)
|
||||
{
|
||||
HeapTuple tup;
|
||||
|
||||
tup = findNotNullConstraintAttnum(relid, attnum);
|
||||
if (HeapTupleIsValid(tup))
|
||||
{
|
||||
Relation pg_constraint;
|
||||
Form_pg_constraint conform;
|
||||
|
||||
pg_constraint = table_open(ConstraintRelationId, RowExclusiveLock);
|
||||
conform = (Form_pg_constraint) GETSTRUCT(tup);
|
||||
if (count > 0)
|
||||
conform->coninhcount += count;
|
||||
|
||||
/* sanity check */
|
||||
if (conform->coninhcount < 0)
|
||||
elog(ERROR, "invalid inhcount %d for constraint \"%s\" on relation \"%s\"",
|
||||
conform->coninhcount, NameStr(conform->conname),
|
||||
get_rel_name(relid));
|
||||
|
||||
/*
|
||||
* If the constraints are no longer inherited, mark them local. It's
|
||||
* arguable that we should drop them instead, but it's hard to see
|
||||
* that being better. The user can drop it manually later.
|
||||
*/
|
||||
if (conform->coninhcount == 0)
|
||||
conform->conislocal = true;
|
||||
|
||||
CatalogTupleUpdate(pg_constraint, &tup->t_self, tup);
|
||||
|
||||
table_close(pg_constraint, RowExclusiveLock);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* AdjustNotNullInheritance
|
||||
* Adjust not-null constraints' inhcount/islocal for
|
||||
* ALTER TABLE [NO] INHERITS
|
||||
*
|
||||
* Mark the NOT NULL constraints for the given relation columns as
|
||||
* inherited, so that they can't be dropped.
|
||||
*
|
||||
* Caller must have checked beforehand that attnotnull was set for all
|
||||
* columns. However, some of those could be set because of a primary
|
||||
* key, so throw a proper user-visible error if one is not found.
|
||||
*/
|
||||
void
|
||||
AdjustNotNullInheritance(Oid relid, Bitmapset *columns, int count)
|
||||
{
|
||||
Relation pg_constraint;
|
||||
int attnum;
|
||||
|
||||
pg_constraint = table_open(ConstraintRelationId, RowExclusiveLock);
|
||||
|
||||
/*
|
||||
* Scan the set of columns and bump inhcount for each.
|
||||
*/
|
||||
attnum = -1;
|
||||
while ((attnum = bms_next_member(columns, attnum)) >= 0)
|
||||
{
|
||||
HeapTuple tup;
|
||||
Form_pg_constraint conform;
|
||||
|
||||
tup = findNotNullConstraintAttnum(relid, attnum);
|
||||
if (!HeapTupleIsValid(tup))
|
||||
ereport(ERROR,
|
||||
errcode(ERRCODE_DATATYPE_MISMATCH),
|
||||
errmsg("column \"%s\" in child table must be marked NOT NULL",
|
||||
get_attname(relid, attnum,
|
||||
false)));
|
||||
|
||||
conform = (Form_pg_constraint) GETSTRUCT(tup);
|
||||
conform->coninhcount += count;
|
||||
if (conform->coninhcount < 0)
|
||||
elog(ERROR, "invalid inhcount %d for constraint \"%s\" on relation \"%s\"",
|
||||
conform->coninhcount, NameStr(conform->conname),
|
||||
get_rel_name(relid));
|
||||
|
||||
/*
|
||||
* If the constraints are no longer inherited, mark them local. It's
|
||||
* arguable that we should drop them instead, but it's hard to see
|
||||
* that being better. The user can drop it manually later.
|
||||
*/
|
||||
if (conform->coninhcount == 0)
|
||||
conform->conislocal = true;
|
||||
|
||||
CatalogTupleUpdate(pg_constraint, &tup->t_self, tup);
|
||||
}
|
||||
|
||||
table_close(pg_constraint, RowExclusiveLock);
|
||||
}
|
||||
|
||||
/*
|
||||
* RelationGetNotNullConstraints
|
||||
* Return the list of not-null constraints for the given rel
|
||||
*
|
||||
* Caller can request cooked constraints, or raw.
|
||||
*
|
||||
* This is seldom needed, so we just scan pg_constraint each time.
|
||||
*
|
||||
* XXX This is only used to create derived tables, so NO INHERIT constraints
|
||||
* are always skipped.
|
||||
*/
|
||||
List *
|
||||
RelationGetNotNullConstraints(Oid relid, bool cooked)
|
||||
{
|
||||
List *notnulls = NIL;
|
||||
Relation constrRel;
|
||||
HeapTuple htup;
|
||||
SysScanDesc conscan;
|
||||
ScanKeyData skey;
|
||||
|
||||
constrRel = table_open(ConstraintRelationId, AccessShareLock);
|
||||
ScanKeyInit(&skey,
|
||||
Anum_pg_constraint_conrelid,
|
||||
BTEqualStrategyNumber, F_OIDEQ,
|
||||
ObjectIdGetDatum(relid));
|
||||
conscan = systable_beginscan(constrRel, ConstraintRelidTypidNameIndexId, true,
|
||||
NULL, 1, &skey);
|
||||
|
||||
while (HeapTupleIsValid(htup = systable_getnext(conscan)))
|
||||
{
|
||||
Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(htup);
|
||||
AttrNumber colnum;
|
||||
|
||||
if (conForm->contype != CONSTRAINT_NOTNULL)
|
||||
continue;
|
||||
if (conForm->connoinherit)
|
||||
continue;
|
||||
|
||||
colnum = extractNotNullColumn(htup);
|
||||
|
||||
if (cooked)
|
||||
{
|
||||
CookedConstraint *cooked;
|
||||
|
||||
cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint));
|
||||
|
||||
cooked->contype = CONSTR_NOTNULL;
|
||||
cooked->name = pstrdup(NameStr(conForm->conname));
|
||||
cooked->attnum = colnum;
|
||||
cooked->expr = NULL;
|
||||
cooked->skip_validation = false;
|
||||
cooked->is_local = true;
|
||||
cooked->inhcount = 0;
|
||||
cooked->is_no_inherit = conForm->connoinherit;
|
||||
|
||||
notnulls = lappend(notnulls, cooked);
|
||||
}
|
||||
else
|
||||
{
|
||||
Constraint *constr;
|
||||
|
||||
constr = makeNode(Constraint);
|
||||
constr->contype = CONSTR_NOTNULL;
|
||||
constr->conname = pstrdup(NameStr(conForm->conname));
|
||||
constr->deferrable = false;
|
||||
constr->initdeferred = false;
|
||||
constr->location = -1;
|
||||
constr->keys = list_make1(makeString(get_attname(relid, colnum,
|
||||
false)));
|
||||
constr->skip_validation = false;
|
||||
constr->initially_valid = true;
|
||||
notnulls = lappend(notnulls, constr);
|
||||
}
|
||||
}
|
||||
|
||||
systable_endscan(conscan);
|
||||
table_close(constrRel, AccessShareLock);
|
||||
|
||||
return notnulls;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Delete a single constraint record.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user