diff --git a/contrib/sepgsql/expected/alter.out b/contrib/sepgsql/expected/alter.out index ae43537505..1462cfa3cb 100644 --- a/contrib/sepgsql/expected/alter.out +++ b/contrib/sepgsql/expected/alter.out @@ -164,7 +164,6 @@ LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_re LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_2.b" permissive=0 ALTER TABLE regtest_table ALTER b DROP NOT NULL; LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table.b" permissive=0 -LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_2.b" permissive=0 ALTER TABLE regtest_table ALTER b SET STATISTICS -1; LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table.b" permissive=0 LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_2.b" permissive=0 @@ -249,8 +248,6 @@ LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_re LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_ptable_1_tens.p" permissive=0 ALTER TABLE regtest_ptable ALTER p DROP NOT NULL; LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_ptable.p" permissive=0 -LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table_part.p" permissive=0 -LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_ptable_1_tens.p" permissive=0 ALTER TABLE regtest_ptable ALTER p SET STATISTICS -1; LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_ptable.p" permissive=0 LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table_part.p" permissive=0 diff --git a/contrib/test_decoding/expected/ddl.out b/contrib/test_decoding/expected/ddl.out index 5713b8ab1c..bcd1f74b2b 100644 --- a/contrib/test_decoding/expected/ddl.out +++ b/contrib/test_decoding/expected/ddl.out @@ -492,6 +492,9 @@ WITH (user_catalog_table = true) options | text[] | | | | extended | | Indexes: "replication_metadata_pkey" PRIMARY KEY, btree (id) +Not-null constraints: + "replication_metadata_id_not_null" NOT NULL "id" + "replication_metadata_relation_not_null" NOT NULL "relation" Options: user_catalog_table=true INSERT INTO replication_metadata(relation, options) @@ -506,6 +509,9 @@ ALTER TABLE replication_metadata RESET (user_catalog_table); options | text[] | | | | extended | | Indexes: "replication_metadata_pkey" PRIMARY KEY, btree (id) +Not-null constraints: + "replication_metadata_id_not_null" NOT NULL "id" + "replication_metadata_relation_not_null" NOT NULL "relation" INSERT INTO replication_metadata(relation, options) VALUES ('bar', ARRAY['a', 'b']); @@ -519,6 +525,9 @@ ALTER TABLE replication_metadata SET (user_catalog_table = true); options | text[] | | | | extended | | Indexes: "replication_metadata_pkey" PRIMARY KEY, btree (id) +Not-null constraints: + "replication_metadata_id_not_null" NOT NULL "id" + "replication_metadata_relation_not_null" NOT NULL "relation" Options: user_catalog_table=true INSERT INTO replication_metadata(relation, options) @@ -538,6 +547,9 @@ ALTER TABLE replication_metadata SET (user_catalog_table = false); rewritemeornot | integer | | | | plain | | Indexes: "replication_metadata_pkey" PRIMARY KEY, btree (id) +Not-null constraints: + "replication_metadata_id_not_null" NOT NULL "id" + "replication_metadata_relation_not_null" NOT NULL "relation" Options: user_catalog_table=false INSERT INTO replication_metadata(relation, options) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 964c819a02..c180ed7abb 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1271,7 +1271,7 @@ attnotnull bool - This represents a not-null constraint. + This column has a not-null constraint. @@ -2502,14 +2502,10 @@ SCRAM-SHA-256$<iteration count>:&l - The catalog pg_constraint stores check, primary - key, unique, foreign key, and exclusion constraints on tables, as well as - not-null constraints on domains. + The catalog pg_constraint stores check, not-null, + primary key, unique, foreign key, and exclusion constraints on tables. (Column constraints are not treated specially. Every column constraint is equivalent to some table constraint.) - Not-null constraints on relations are represented in the - pg_attribute - catalog, not here. @@ -2571,7 +2567,7 @@ SCRAM-SHA-256$<iteration count>:&l c = check constraint, f = foreign key constraint, - n = not-null constraint (domains only), + n = not-null constraint, p = primary key constraint, u = unique constraint, t = constraint trigger, diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 898b6ddc8d..3c56610d2a 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -771,18 +771,39 @@ CREATE TABLE products ( name text NOT NULL, price numeric ); + + An explicit constraint name can also be specified, for example: + +CREATE TABLE products ( + product_no integer NOT NULL, + name text CONSTRAINT products_name_not_null NOT NULL, + price numeric +); - A not-null constraint is always written as a column constraint. A - not-null constraint is functionally equivalent to creating a check + A not-null constraint is usually written as a column constraint. The + syntax for writing it as a table constraint is + +CREATE TABLE products ( + product_no integer, + name text, + price numeric, + NOT NULL product_no, + NOT NULL name +); + + But this syntax is not standard and mainly intended for use by + pg_dump. + + + + A not-null constraint is functionally equivalent to creating a check constraint CHECK (column_name IS NOT NULL), but in PostgreSQL creating an explicit - not-null constraint is more efficient. The drawback is that you - cannot give explicit names to not-null constraints created this - way. + not-null constraint is more efficient. @@ -799,6 +820,10 @@ CREATE TABLE products ( order the constraints are checked. + + However, a column can have at most one explicit not-null constraint. + + The NOT NULL constraint has an inverse: the NULL constraint. This does not mean that the @@ -992,7 +1017,7 @@ CREATE TABLE example ( A table can have at most one primary key. (There can be any number - of unique and not-null constraints, which are functionally almost the + of unique constraints, which combined with not-null constraints are functionally almost the same thing, but only one can be identified as the primary key.) Relational database theory dictates that every table must have a primary key. This rule is @@ -1652,11 +1677,16 @@ ALTER TABLE products ADD CHECK (name <> ''); ALTER TABLE products ADD CONSTRAINT some_name UNIQUE (product_no); ALTER TABLE products ADD FOREIGN KEY (product_group_id) REFERENCES product_groups; - To add a not-null constraint, which cannot be written as a table - constraint, use this syntax: + + + + To add a not-null constraint, which is normally not written as a table + constraint, this special syntax is available: ALTER TABLE products ALTER COLUMN product_no SET NOT NULL; + This command silently does nothing if the column already has a + not-null constraint. @@ -1697,12 +1727,15 @@ ALTER TABLE products DROP CONSTRAINT some_name; - This works the same for all constraint types except not-null - constraints. To drop a not-null constraint use: + Simplified syntax is available to drop a not-null constraint: ALTER TABLE products ALTER COLUMN product_no DROP NOT NULL; - (Recall that not-null constraints do not have names.) + This mirrors the SET NOT NULL syntax for adding a + not-null constraint. This command will silently do nothing if the column + does not have a not-null constraint. (Recall that a column can have at + most one not-null constraint, so it is never ambiguous which constraint + this command acts on.) @@ -4446,12 +4479,10 @@ ALTER INDEX measurement_city_id_logdate_key Both CHECK and NOT NULL constraints of a partitioned table are always inherited by all its - partitions. CHECK constraints that are marked - NO INHERIT are not allowed to be created on - partitioned tables. - You cannot drop a NOT NULL constraint on a - partition's column if the same constraint is present in the parent - table. + partitions; it is not allowed to create NO INHERIT + constraints of those types. + You cannot drop a constraint of those types if the same constraint + is present in the parent table. diff --git a/doc/src/sgml/ref/alter_foreign_table.sgml b/doc/src/sgml/ref/alter_foreign_table.sgml index 3cb6f08fcf..e2da3cc719 100644 --- a/doc/src/sgml/ref/alter_foreign_table.sgml +++ b/doc/src/sgml/ref/alter_foreign_table.sgml @@ -173,7 +173,8 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name This form adds a new constraint to a foreign table, using the same syntax as CREATE FOREIGN TABLE. - Currently only CHECK constraints are supported. + Currently only CHECK and NOT NULL + constraints are supported. @@ -182,7 +183,8 @@ ALTER FOREIGN TABLE [ IF EXISTS ] nameCREATE FOREIGN TABLE.) - If the constraint is marked NOT VALID, then it isn't + If the constraint is marked NOT VALID (allowed only for + the CHECK case), then it isn't assumed to hold, but is only recorded for possible future use. diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 61a0fb3dec..6098ebed43 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -98,7 +98,7 @@ WITH ( MODULUS numeric_literal, REM and column_constraint is: [ CONSTRAINT constraint_name ] -{ NOT NULL | +{ NOT NULL [ NO INHERIT ] | NULL | CHECK ( expression ) [ NO INHERIT ] | DEFAULT default_expr | @@ -114,6 +114,7 @@ WITH ( MODULUS numeric_literal, REM [ CONSTRAINT constraint_name ] { CHECK ( expression ) [ NO INHERIT ] | + NOT NULL column_name [ NO INHERIT ] | UNIQUE [ NULLS [ NOT ] DISTINCT ] ( column_name [, ... ] ) index_parameters | PRIMARY KEY ( column_name [, ... ] ) index_parameters | EXCLUDE [ USING index_method ] ( exclude_element WITH operator [, ... ] ) index_parameters [ WHERE ( predicate ) ] | @@ -849,19 +850,16 @@ WITH ( MODULUS numeric_literal, REM table. Subsequently, queries against the parent will include records of the target table. To be added as a child, the target table must already contain all the same columns as the parent (it could have - additional columns, too). The columns must have matching data types, - and if they have NOT NULL constraints in the parent - then they must also have NOT NULL constraints in the - child. + additional columns, too). The columns must have matching data types. - There must also be matching child-table constraints for all - CHECK constraints of the parent, except those - marked non-inheritable (that is, created with ALTER TABLE ... ADD CONSTRAINT ... NO INHERIT) - in the parent, which are ignored; all child-table constraints matched - must not be marked non-inheritable. - Currently + In addition, all CHECK and NOT NULL + constraints on the parent must also exist on the child, except those + marked non-inheritable (that is, created with + ALTER TABLE ... ADD CONSTRAINT ... NO INHERIT), which + are ignored. All child-table constraints matched must not be marked + non-inheritable. Currently UNIQUE, PRIMARY KEY, and FOREIGN KEY constraints are not considered, but this might change in the future. @@ -1793,11 +1791,17 @@ ALTER TABLE measurement Compatibility - The forms ADD (without USING INDEX), + The forms ADD [COLUMN], DROP [COLUMN], DROP IDENTITY, RESTART, SET DEFAULT, SET DATA TYPE (without USING), SET GENERATED, and SET sequence_option - conform with the SQL standard. The other forms are + conform with the SQL standard. + The form ADD table_constraint + conforms with the SQL standard when the USING INDEX and + NOT VALID clauses are omitted and the constraint type is + one of CHECK, UNIQUE, PRIMARY KEY, + or REFERENCES. + The other forms are PostgreSQL extensions of the SQL standard. Also, the ability to specify more than one manipulation in a single ALTER TABLE command is an extension. diff --git a/doc/src/sgml/ref/create_foreign_table.sgml b/doc/src/sgml/ref/create_foreign_table.sgml index dc4b907599..fc81ba3c49 100644 --- a/doc/src/sgml/ref/create_foreign_table.sgml +++ b/doc/src/sgml/ref/create_foreign_table.sgml @@ -43,7 +43,7 @@ CREATE FOREIGN TABLE [ IF NOT EXISTS ] table_name where column_constraint is: [ CONSTRAINT constraint_name ] -{ NOT NULL | +{ NOT NULL [ NO INHERIT ] | NULL | CHECK ( expression ) [ NO INHERIT ] | DEFAULT default_expr | @@ -52,6 +52,7 @@ CREATE FOREIGN TABLE [ IF NOT EXISTS ] table_name and table_constraint is: [ CONSTRAINT constraint_name ] + NOT NULL column_name [ NO INHERIT ] | CHECK ( expression ) [ NO INHERIT ] and partition_bound_spec is: @@ -203,11 +204,16 @@ WITH ( MODULUS numeric_literal, REM - NOT NULL + NOT NULL [ NO INHERIT ] The column is not allowed to contain null values. + + + A constraint marked with NO INHERIT will not propagate to + child tables. + diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index 83859bac76..dd83b07d65 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -61,7 +61,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI where column_constraint is: [ CONSTRAINT constraint_name ] -{ NOT NULL | +{ NOT NULL [ NO INHERIT ] | NULL | CHECK ( expression ) [ NO INHERIT ] | DEFAULT default_expr | @@ -77,6 +77,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI [ CONSTRAINT constraint_name ] { CHECK ( expression ) [ NO INHERIT ] | + NOT NULL column_name [ NO INHERIT ] | UNIQUE [ NULLS [ NOT ] DISTINCT ] ( column_name [, ... ] [, column_name WITHOUT OVERLAPS ] ) index_parameters | PRIMARY KEY ( column_name [, ... ] [, column_name WITHOUT OVERLAPS ] ) index_parameters | EXCLUDE [ USING index_method ] ( exclude_element WITH operator [, ... ] ) index_parameters [ WHERE ( predicate ) ] | @@ -818,11 +819,16 @@ WITH ( MODULUS numeric_literal, REM - NOT NULL + NOT NULL [ NO INHERIT ] The column is not allowed to contain null values. + + + A constraint marked with NO INHERIT will not propagate to + child tables. + @@ -2398,13 +2404,6 @@ CREATE TABLE cities_partdef constraint, and index names must be unique across all relations within the same schema. - - - Currently, PostgreSQL does not record names - for not-null constraints at all, so they are not - subject to the uniqueness restriction. This might change in a future - release. - diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index c54a543c53..003af4bf21 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -2173,6 +2173,56 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr, return constrOid; } +/* + * Store a not-null constraint for the given relation + * + * The OID of the new constraint is returned. + */ +static Oid +StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum, + bool is_validated, bool is_local, int inhcount, + bool is_no_inherit) +{ + Oid constrOid; + + Assert(attnum > InvalidAttrNumber); + + constrOid = + CreateConstraintEntry(nnname, + RelationGetNamespace(rel), + CONSTRAINT_NOTNULL, + false, + false, + is_validated, + InvalidOid, + RelationGetRelid(rel), + &attnum, + 1, + 1, + InvalidOid, /* not a domain constraint */ + InvalidOid, /* no associated index */ + InvalidOid, /* Foreign key fields */ + NULL, + NULL, + NULL, + NULL, + 0, + ' ', + ' ', + NULL, + 0, + ' ', + NULL, /* not an exclusion constraint */ + NULL, + NULL, + is_local, + inhcount, + is_no_inherit, + false, + false); + return constrOid; +} + /* * Store defaults and constraints (passed as a list of CookedConstraint). * @@ -2217,6 +2267,14 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal) is_internal); numchecks++; break; + + case CONSTR_NOTNULL: + con->conoid = + StoreRelNotNull(rel, con->name, con->attnum, + !con->skip_validation, con->is_local, + con->inhcount, con->is_no_inherit); + break; + default: elog(ERROR, "unrecognized constraint type: %d", (int) con->contype); @@ -2245,7 +2303,7 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal) * cooked CHECK constraints * * All entries in newColDefaults will be processed. Entries in newConstraints - * will be processed only if they are CONSTR_CHECK type. + * will be processed only if they are CONSTR_CHECK or CONSTR_NOTNULL types. * * Returns a list of CookedConstraint nodes that shows the cooked form of * the default and constraint expressions added to the relation. @@ -2274,6 +2332,7 @@ AddRelationNewConstraints(Relation rel, ParseNamespaceItem *nsitem; int numchecks; List *checknames; + List *nnnames; Node *expr; CookedConstraint *cooked; @@ -2358,6 +2417,7 @@ AddRelationNewConstraints(Relation rel, */ numchecks = numoldchecks; checknames = NIL; + nnnames = NIL; foreach_node(Constraint, cdef, newConstraints) { Oid constrOid; @@ -2412,7 +2472,7 @@ AddRelationNewConstraints(Relation rel, * Check against pre-existing constraints. If we are allowed * to merge with an existing constraint, there's no more to do * here. (We omit the duplicate constraint from the result, - * which is what ATAddCheckConstraint wants.) + * which is what ATAddCheckNNConstraint wants.) */ if (MergeWithExistingConstraint(rel, ccname, expr, allow_merge, is_local, @@ -2481,6 +2541,77 @@ AddRelationNewConstraints(Relation rel, cooked->is_no_inherit = cdef->is_no_inherit; cookedConstraints = lappend(cookedConstraints, cooked); } + else if (cdef->contype == CONSTR_NOTNULL) + { + CookedConstraint *nncooked; + AttrNumber colnum; + int16 inhcount = is_local ? 0 : 1; + char *nnname; + + /* Determine which column to modify */ + colnum = get_attnum(RelationGetRelid(rel), strVal(linitial(cdef->keys))); + if (colnum == InvalidAttrNumber) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + strVal(linitial(cdef->keys)), RelationGetRelationName(rel))); + if (colnum < InvalidAttrNumber) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot add not-null constraint on system column \"%s\"", + strVal(linitial(cdef->keys)))); + + /* + * If the column already has a not-null constraint, we don't want + * to add another one; just adjust inheritance status as needed. + */ + if (AdjustNotNullInheritance(RelationGetRelid(rel), colnum, + is_local, cdef->is_no_inherit)) + continue; + + /* + * If a constraint name is specified, check that it isn't already + * used. Otherwise, choose a non-conflicting one ourselves. + */ + if (cdef->conname) + { + if (ConstraintNameIsUsed(CONSTRAINT_RELATION, + RelationGetRelid(rel), + cdef->conname)) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("constraint \"%s\" for relation \"%s\" already exists", + cdef->conname, RelationGetRelationName(rel))); + nnname = cdef->conname; + } + else + nnname = ChooseConstraintName(RelationGetRelationName(rel), + strVal(linitial(cdef->keys)), + "not_null", + RelationGetNamespace(rel), + nnnames); + nnnames = lappend(nnnames, nnname); + + constrOid = + StoreRelNotNull(rel, nnname, colnum, + cdef->initially_valid, + is_local, + inhcount, + cdef->is_no_inherit); + + nncooked = (CookedConstraint *) palloc(sizeof(CookedConstraint)); + nncooked->contype = CONSTR_NOTNULL; + nncooked->conoid = constrOid; + nncooked->name = nnname; + nncooked->attnum = colnum; + nncooked->expr = NULL; + nncooked->skip_validation = cdef->skip_validation; + nncooked->is_local = is_local; + nncooked->inhcount = inhcount; + nncooked->is_no_inherit = cdef->is_no_inherit; + + cookedConstraints = lappend(cookedConstraints, nncooked); + } } /* @@ -2648,6 +2779,262 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr, return found; } +/* + * Create the not-null constraints when creating a new relation + * + * These come from two sources: the 'constraints' list (of Constraint) is + * specified directly by the user; the 'old_notnulls' list (of + * CookedConstraint) comes from inheritance. We create one constraint + * for each column, giving priority to user-specified ones, and setting + * inhcount according to how many parents cause each column to get a + * not-null constraint. If a user-specified name clashes with another + * user-specified name, an error is raised. + * + * Returns a list of AttrNumber for columns that need to have the attnotnull + * flag set. + */ +List * +AddRelationNotNullConstraints(Relation rel, List *constraints, + List *old_notnulls) +{ + List *givennames; + List *nnnames; + List *nncols = NIL; + + /* + * We track two lists of names: nnnames keeps all the constraint names, + * givennames tracks user-generated names. The distinction is important, + * because we must raise error for user-generated name conflicts, but for + * system-generated name conflicts we just generate another. + */ + nnnames = NIL; + givennames = NIL; + + /* + * First, create all not-null constraints that are directly specified by + * the user. Note that inheritance might have given us another source for + * each, so we must scan the old_notnulls list and increment inhcount for + * each element with identical attnum. We delete from there any element + * that we process. + * + * We don't use foreach() here because we have two nested loops over the + * constraint list, with possible element deletions in the inner one. If + * we used foreach_delete_current() it could only fix up the state of one + * of the loops, so it seems cleaner to use looping over list indexes for + * both loops. Note that any deletion will happen beyond where the outer + * loop is, so its index never needs adjustment. + */ + for (int outerpos = 0; outerpos < list_length(constraints); outerpos++) + { + Constraint *constr; + AttrNumber attnum; + char *conname; + int inhcount = 0; + + constr = list_nth_node(Constraint, constraints, outerpos); + + Assert(constr->contype == CONSTR_NOTNULL); + + attnum = get_attnum(RelationGetRelid(rel), + strVal(linitial(constr->keys))); + if (attnum == InvalidAttrNumber) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + strVal(linitial(constr->keys)), + RelationGetRelationName(rel))); + if (attnum < InvalidAttrNumber) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot add not-null constraint on system column \"%s\"", + strVal(linitial(constr->keys)))); + + /* + * A column can only have one not-null constraint, so discard any + * additional ones that appear for columns we already saw; but check + * that the NO INHERIT flags match. + */ + for (int restpos = outerpos + 1; restpos < list_length(constraints);) + { + Constraint *other; + + other = list_nth_node(Constraint, constraints, restpos); + if (strcmp(strVal(linitial(constr->keys)), + strVal(linitial(other->keys))) == 0) + { + if (other->is_no_inherit != constr->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting NO INHERIT declaration for not-null constraint on column \"%s\"", + strVal(linitial(constr->keys)))); + + /* + * Preserve constraint name if one is specified, but raise an + * error if conflicting ones are specified. + */ + if (other->conname) + { + if (!constr->conname) + constr->conname = pstrdup(other->conname); + else if (strcmp(constr->conname, other->conname) != 0) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting not-null constraint names \"%s\" and \"%s\"", + constr->conname, other->conname)); + } + + /* XXX do we need to verify any other fields? */ + constraints = list_delete_nth_cell(constraints, restpos); + } + else + restpos++; + } + + /* + * Search in the list of inherited constraints for any entries on the + * same column; determine an inheritance count from that. Also, if at + * least one parent has a constraint for this column, then we must not + * accept a user specification for a NO INHERIT one. Any constraint + * from parents that we process here is deleted from the list: we no + * longer need to process it in the loop below. + */ + foreach_ptr(CookedConstraint, old, old_notnulls) + { + if (old->attnum == attnum) + { + /* + * If we get a constraint from the parent, having a local NO + * INHERIT one doesn't work. + */ + if (constr->is_no_inherit) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot define not-null constraint on column \"%s\" with NO INHERIT", + strVal(linitial(constr->keys))), + errdetail("The column has an inherited not-null constraint."))); + + inhcount++; + old_notnulls = foreach_delete_current(old_notnulls, old); + } + } + + /* + * Determine a constraint name, which may have been specified by the + * user, or raise an error if a conflict exists with another + * user-specified name. + */ + if (constr->conname) + { + foreach_ptr(char, thisname, givennames) + { + if (strcmp(thisname, constr->conname) == 0) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("constraint \"%s\" for relation \"%s\" already exists", + constr->conname, + RelationGetRelationName(rel))); + } + + conname = constr->conname; + givennames = lappend(givennames, conname); + } + else + conname = ChooseConstraintName(RelationGetRelationName(rel), + get_attname(RelationGetRelid(rel), + attnum, false), + "not_null", + RelationGetNamespace(rel), + nnnames); + nnnames = lappend(nnnames, conname); + + StoreRelNotNull(rel, conname, + attnum, true, true, + inhcount, constr->is_no_inherit); + + nncols = lappend_int(nncols, attnum); + } + + /* + * If any column remains in the old_notnulls list, we must create a not- + * null constraint marked not-local for that column. Because multiple + * parents could specify a not-null constraint for the same column, we + * must count how many there are and set an appropriate inhcount + * accordingly, deleting elements we've already processed. + * + * We don't use foreach() here because we have two nested loops over the + * constraint list, with possible element deletions in the inner one. If + * we used foreach_delete_current() it could only fix up the state of one + * of the loops, so it seems cleaner to use looping over list indexes for + * both loops. Note that any deletion will happen beyond where the outer + * loop is, so its index never needs adjustment. + */ + for (int outerpos = 0; outerpos < list_length(old_notnulls); outerpos++) + { + CookedConstraint *cooked; + char *conname = NULL; + int inhcount = 1; + + cooked = (CookedConstraint *) list_nth(old_notnulls, outerpos); + Assert(cooked->contype == CONSTR_NOTNULL); + Assert(cooked->name); + + /* + * Preserve the first non-conflicting constraint name we come across. + */ + if (conname == NULL) + conname = cooked->name; + + for (int restpos = outerpos + 1; restpos < list_length(old_notnulls);) + { + CookedConstraint *other; + + other = (CookedConstraint *) list_nth(old_notnulls, restpos); + Assert(other->name); + if (other->attnum == cooked->attnum) + { + if (conname == NULL) + conname = other->name; + + inhcount++; + old_notnulls = list_delete_nth_cell(old_notnulls, restpos); + } + else + restpos++; + } + + /* If we got a name, make sure it isn't one we've already used */ + if (conname != NULL) + { + foreach_ptr(char, thisname, nnnames) + { + if (strcmp(thisname, conname) == 0) + { + conname = NULL; + break; + } + } + } + + /* and choose a name, if needed */ + if (conname == NULL) + conname = ChooseConstraintName(RelationGetRelationName(rel), + get_attname(RelationGetRelid(rel), + cooked->attnum, false), + "not_null", + RelationGetNamespace(rel), + nnnames); + nnnames = lappend(nnnames, conname); + + /* ignore the origin constraint's is_local and inhcount */ + StoreRelNotNull(rel, conname, cooked->attnum, true, + false, inhcount, false); + + nncols = lappend_int(nncols, cooked->attnum); + } + + return nncols; +} + /* * Update the count of constraints in the relation's pg_class tuple. * diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql index c4145131ce..76c78c0d18 100644 --- a/src/backend/catalog/information_schema.sql +++ b/src/backend/catalog/information_schema.sql @@ -440,9 +440,8 @@ CREATE VIEW check_constraints AS WHERE pg_has_role(coalesce(c.relowner, t.typowner), 'USAGE') AND con.contype = 'c' - UNION - -- not-null constraints on domains - + UNION ALL + -- not-null constraints SELECT current_database()::information_schema.sql_identifier AS constraint_catalog, rs.nspname::information_schema.sql_identifier AS constraint_schema, con.conname::information_schema.sql_identifier AS constraint_name, @@ -453,24 +452,7 @@ CREATE VIEW check_constraints AS LEFT JOIN pg_type t ON t.oid = con.contypid LEFT JOIN pg_attribute at ON (con.conrelid = at.attrelid AND con.conkey[1] = at.attnum) WHERE pg_has_role(coalesce(c.relowner, t.typowner), 'USAGE'::text) - AND con.contype = 'n' - - UNION - -- not-null constraints on relations - - SELECT CAST(current_database() AS sql_identifier) AS constraint_catalog, - CAST(n.nspname AS sql_identifier) AS constraint_schema, - CAST(CAST(n.oid AS text) || '_' || CAST(r.oid AS text) || '_' || CAST(a.attnum AS text) || '_not_null' AS sql_identifier) AS constraint_name, -- XXX - CAST(a.attname || ' IS NOT NULL' AS character_data) - AS check_clause - FROM pg_namespace n, pg_class r, pg_attribute a - WHERE n.oid = r.relnamespace - AND r.oid = a.attrelid - AND a.attnum > 0 - AND NOT a.attisdropped - AND a.attnotnull - AND r.relkind IN ('r', 'p') - AND pg_has_role(r.relowner, 'USAGE'); + AND con.contype = 'n'; GRANT SELECT ON check_constraints TO PUBLIC; @@ -839,6 +821,20 @@ CREATE VIEW constraint_column_usage AS UNION ALL + /* not-null constraints */ + SELECT DISTINCT nr.nspname, r.relname, r.relowner, a.attname, nc.nspname, c.conname + FROM pg_namespace nr, pg_class r, pg_attribute a, pg_namespace nc, pg_constraint c + WHERE nr.oid = r.relnamespace + AND r.oid = a.attrelid + AND r.oid = c.conrelid + AND a.attnum = c.conkey[1] + AND c.connamespace = nc.oid + AND c.contype = 'n' + AND r.relkind in ('r', 'p') + AND not a.attisdropped + + UNION ALL + /* unique/primary key/foreign key constraints */ SELECT nr.nspname, r.relname, r.relowner, a.attname, nc.nspname, c.conname FROM pg_namespace nr, pg_class r, pg_attribute a, pg_namespace nc, @@ -1839,6 +1835,7 @@ CREATE VIEW table_constraints AS CAST(r.relname AS sql_identifier) AS table_name, CAST( CASE c.contype WHEN 'c' THEN 'CHECK' + WHEN 'n' THEN 'CHECK' WHEN 'f' THEN 'FOREIGN KEY' WHEN 'p' THEN 'PRIMARY KEY' WHEN 'u' THEN 'UNIQUE' END @@ -1863,38 +1860,6 @@ CREATE VIEW table_constraints AS AND c.contype NOT IN ('t', 'x') -- ignore nonstandard constraints AND r.relkind IN ('r', 'p') AND (NOT pg_is_other_temp_schema(nr.oid)) - AND (pg_has_role(r.relowner, 'USAGE') - -- SELECT privilege omitted, per SQL standard - OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') - OR has_any_column_privilege(r.oid, 'INSERT, UPDATE, REFERENCES') ) - - UNION ALL - - -- not-null constraints - - SELECT CAST(current_database() AS sql_identifier) AS constraint_catalog, - CAST(nr.nspname AS sql_identifier) AS constraint_schema, - CAST(CAST(nr.oid AS text) || '_' || CAST(r.oid AS text) || '_' || CAST(a.attnum AS text) || '_not_null' AS sql_identifier) AS constraint_name, -- XXX - CAST(current_database() AS sql_identifier) AS table_catalog, - CAST(nr.nspname AS sql_identifier) AS table_schema, - CAST(r.relname AS sql_identifier) AS table_name, - CAST('CHECK' AS character_data) AS constraint_type, - CAST('NO' AS yes_or_no) AS is_deferrable, - CAST('NO' AS yes_or_no) AS initially_deferred, - CAST('YES' AS yes_or_no) AS enforced, - CAST(NULL AS yes_or_no) AS nulls_distinct - - FROM pg_namespace nr, - pg_class r, - pg_attribute a - - WHERE nr.oid = r.relnamespace - AND r.oid = a.attrelid - AND a.attnotnull - AND a.attnum > 0 - AND NOT a.attisdropped - AND r.relkind IN ('r', 'p') - AND (NOT pg_is_other_temp_schema(nr.oid)) AND (pg_has_role(r.relowner, 'USAGE') -- SELECT privilege omitted, per SQL standard OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index 54f3fb50a5..e953000c01 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -21,6 +21,7 @@ #include "access/table.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" @@ -564,6 +565,78 @@ ChooseConstraintName(const char *name1, const char *name2, return conname; } +/* + * Find and return a copy of the pg_constraint tuple that implements a + * validated not-null constraint for the given column of the given relation. + * If no such constraint exists, return NULL. + * + * XXX This would be easier if we had pg_attribute.notnullconstr with the OID + * of the constraint that implements the not-null constraint for that column. + * I'm not sure it's worth the catalog bloat and de-normalization, however. + */ +HeapTuple +findNotNullConstraintAttnum(Oid relid, AttrNumber attnum) +{ + Relation pg_constraint; + HeapTuple conTup, + retval = NULL; + SysScanDesc scan; + ScanKeyData key; + + pg_constraint = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + scan = systable_beginscan(pg_constraint, ConstraintRelidTypidNameIndexId, + true, NULL, 1, &key); + + while (HeapTupleIsValid(conTup = systable_getnext(scan))) + { + Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup); + AttrNumber conkey; + + /* + * We're looking for a NOTNULL constraint that's marked validated, + * with the column we're looking for as the sole element in conkey. + */ + if (con->contype != CONSTRAINT_NOTNULL) + continue; + if (!con->convalidated) + continue; + + conkey = extractNotNullColumn(conTup); + if (conkey != attnum) + continue; + + /* Found it */ + retval = heap_copytuple(conTup); + break; + } + + systable_endscan(scan); + table_close(pg_constraint, AccessShareLock); + + return retval; +} + +/* + * Find and return the pg_constraint tuple that implements a validated + * not-null constraint for the given column of the given relation. If + * no such column or no such constraint exists, return NULL. + */ +HeapTuple +findNotNullConstraint(Oid relid, const char *colname) +{ + AttrNumber attnum; + + attnum = get_attnum(relid, colname); + if (attnum <= InvalidAttrNumber) + return NULL; + + return findNotNullConstraintAttnum(relid, attnum); +} + /* * Find and return the pg_constraint tuple that implements a validated * not-null constraint for the given domain. @@ -608,6 +681,185 @@ findDomainNotNullConstraint(Oid typid) return retval; } +/* + * Given a pg_constraint tuple for a not-null constraint, return the column + * number it is for. + */ +AttrNumber +extractNotNullColumn(HeapTuple constrTup) +{ + AttrNumber colnum; + Datum adatum; + ArrayType *arr; + + /* only tuples for not-null constraints should be given */ + Assert(((Form_pg_constraint) GETSTRUCT(constrTup))->contype == CONSTRAINT_NOTNULL); + + adatum = SysCacheGetAttrNotNull(CONSTROID, constrTup, + Anum_pg_constraint_conkey); + arr = DatumGetArrayTypeP(adatum); /* ensure not toasted */ + if (ARR_NDIM(arr) != 1 || + ARR_HASNULL(arr) || + ARR_ELEMTYPE(arr) != INT2OID || + ARR_DIMS(arr)[0] != 1) + elog(ERROR, "conkey is not a 1-D smallint array"); + + memcpy(&colnum, ARR_DATA_PTR(arr), sizeof(AttrNumber)); + Assert(colnum > 0 && colnum <= MaxAttrNumber); + + if ((Pointer) arr != DatumGetPointer(adatum)) + pfree(arr); /* free de-toasted copy, if any */ + + return colnum; +} + +/* + * AdjustNotNullInheritance + * Adjust inheritance status for a single not-null constraint + * + * 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. + */ +bool +AdjustNotNullInheritance(Oid relid, AttrNumber attnum, + bool is_local, bool is_no_inherit) +{ + HeapTuple tup; + + tup = findNotNullConstraintAttnum(relid, attnum); + if (HeapTupleIsValid(tup)) + { + Relation pg_constraint; + Form_pg_constraint conform; + bool changed = false; + + pg_constraint = table_open(ConstraintRelationId, RowExclusiveLock); + conform = (Form_pg_constraint) GETSTRUCT(tup); + + /* + * If the NO INHERIT flag we're asked for doesn't match what the + * existing constraint has, throw an error. + */ + if (is_no_inherit != conform->connoinherit) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"", + NameStr(conform->conname), get_rel_name(relid))); + + if (!is_local) + { + if (pg_add_s16_overflow(conform->coninhcount, 1, + &conform->coninhcount)) + ereport(ERROR, + errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("too many inheritance parents")); + changed = true; + } + else if (!conform->conislocal) + { + conform->conislocal = true; + changed = true; + } + + if (changed) + CatalogTupleUpdate(pg_constraint, &tup->t_self, tup); + + table_close(pg_constraint, RowExclusiveLock); + + return true; + } + + return false; +} + +/* + * RelationGetNotNullConstraints + * Return the list of not-null constraints for the given rel + * + * Caller can request cooked constraints, or raw. + * + * This is seldom needed, so we just scan pg_constraint each time. + * + * 'include_noinh' determines whether to include NO INHERIT constraints or not. + */ +List * +RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh) +{ + List *notnulls = NIL; + Relation constrRel; + HeapTuple htup; + SysScanDesc conscan; + ScanKeyData skey; + + constrRel = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&skey, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + conscan = systable_beginscan(constrRel, ConstraintRelidTypidNameIndexId, true, + NULL, 1, &skey); + + while (HeapTupleIsValid(htup = systable_getnext(conscan))) + { + Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(htup); + AttrNumber colnum; + + if (conForm->contype != CONSTRAINT_NOTNULL) + continue; + if (conForm->connoinherit && !include_noinh) + continue; + + colnum = extractNotNullColumn(htup); + + if (cooked) + { + CookedConstraint *cooked; + + cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint)); + + cooked->contype = CONSTR_NOTNULL; + cooked->conoid = conForm->oid; + cooked->name = pstrdup(NameStr(conForm->conname)); + cooked->attnum = colnum; + cooked->expr = NULL; + cooked->skip_validation = false; + cooked->is_local = true; + cooked->inhcount = 0; + cooked->is_no_inherit = conForm->connoinherit; + + notnulls = lappend(notnulls, cooked); + } + else + { + Constraint *constr; + + constr = makeNode(Constraint); + constr->contype = CONSTR_NOTNULL; + constr->conname = pstrdup(NameStr(conForm->conname)); + constr->deferrable = false; + constr->initdeferred = false; + constr->location = -1; + constr->keys = list_make1(makeString(get_attname(relid, colnum, + false))); + constr->skip_validation = false; + constr->initially_valid = true; + constr->is_no_inherit = conForm->connoinherit; + notnulls = lappend(notnulls, constr); + } + } + + systable_endscan(conscan); + table_close(constrRel, AccessShareLock); + + return notnulls; +} + + /* * Delete a single constraint record. */ diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index eaa8142427..ccd9645e7d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -371,7 +371,8 @@ static void truncate_check_activity(Relation rel); static void RangeVarCallbackForTruncate(const RangeVar *relation, Oid relId, Oid oldRelId, void *arg); static List *MergeAttributes(List *columns, const List *supers, char relpersistence, - bool is_partition, List **supconstr); + bool is_partition, List **supconstr, + List **supnotnulls); static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr); static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef); static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef); @@ -456,15 +457,14 @@ static bool check_for_column_name_collision(Relation rel, const char *colname, bool if_not_exists); static void add_column_datatype_dependency(Oid relid, int32 attnum, Oid typid); static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid); -static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode); -static void ATPrepSetNotNull(List **wqueue, Relation rel, - AlterTableCmd *cmd, bool recurse, bool recursing, - LOCKMODE lockmode, - AlterTableUtilityContext *context); -static ObjectAddress ATExecSetNotNull(AlteredTableInfo *tab, Relation rel, - const char *colName, LOCKMODE lockmode); -static void ATExecCheckNotNull(AlteredTableInfo *tab, Relation rel, - const char *colName, LOCKMODE lockmode); +static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse, + LOCKMODE lockmode); +static void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum, + LOCKMODE lockmode); +static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel, + char *constrname, char *colName, + bool recurse, bool recursing, + LOCKMODE lockmode); static bool NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr); static bool ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *provenConstraint); @@ -496,6 +496,9 @@ static ObjectAddress ATExecDropColumn(List **wqueue, Relation rel, const char *c bool recurse, bool recursing, bool missing_ok, LOCKMODE lockmode, ObjectAddresses *addrs); +static void ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd, + bool recurse, LOCKMODE lockmode, + AlterTableUtilityContext *context); static ObjectAddress ATExecAddIndex(AlteredTableInfo *tab, Relation rel, IndexStmt *stmt, bool is_rebuild, LOCKMODE lockmode); static ObjectAddress ATExecAddStatistics(AlteredTableInfo *tab, Relation rel, @@ -507,11 +510,11 @@ static ObjectAddress ATExecAddConstraint(List **wqueue, static char *ChooseForeignKeyConstraintNameAddition(List *colnames); static ObjectAddress ATExecAddIndexConstraint(AlteredTableInfo *tab, Relation rel, IndexStmt *stmt, LOCKMODE lockmode); -static ObjectAddress ATAddCheckConstraint(List **wqueue, - AlteredTableInfo *tab, Relation rel, - Constraint *constr, - bool recurse, bool recursing, bool is_readd, - LOCKMODE lockmode); +static ObjectAddress ATAddCheckNNConstraint(List **wqueue, + AlteredTableInfo *tab, Relation rel, + Constraint *constr, + bool recurse, bool recursing, bool is_readd, + LOCKMODE lockmode); static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, Constraint *fkconstraint, bool recurse, bool recursing, @@ -577,9 +580,12 @@ static void GetForeignKeyCheckTriggers(Relation trigrel, Oid *insertTriggerOid, Oid *updateTriggerOid); static void ATExecDropConstraint(Relation rel, const char *constrName, - DropBehavior behavior, - bool recurse, bool recursing, + DropBehavior behavior, bool recurse, bool missing_ok, LOCKMODE lockmode); +static ObjectAddress dropconstraint_internal(Relation rel, + HeapTuple constraintTup, DropBehavior behavior, + bool recurse, bool recursing, + bool missing_ok, LOCKMODE lockmode); static void ATPrepAlterColumnType(List **wqueue, AlteredTableInfo *tab, Relation rel, bool recurse, bool recursing, @@ -677,6 +683,7 @@ static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl); static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, Relation partitionTbl); +static void verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partIdx); static List *GetParentedForeignKeyRefs(Relation partition); static void ATDetachCheckNoForeignKeyRefs(Relation partition); static char GetAttributeCompression(Oid atttypid, const char *compression); @@ -714,8 +721,10 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, TupleDesc descriptor; List *inheritOids; List *old_constraints; + List *old_notnulls; List *rawDefaults; List *cookedDefaults; + List *nncols; Datum reloptions; ListCell *listptr; AttrNumber attnum; @@ -906,12 +915,13 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, MergeAttributes(stmt->tableElts, inheritOids, stmt->relation->relpersistence, stmt->partbound != NULL, - &old_constraints); + &old_constraints, &old_notnulls); /* * Create a tuple descriptor from the relation schema. Note that this - * deals with column names, types, and not-null constraints, but not - * default values or CHECK constraints; we handle those below. + * deals with column names, types, and in-descriptor NOT NULL flags, but + * not default values, NOT NULL or CHECK constraints; we handle those + * below. */ descriptor = BuildDescForRelation(stmt->tableElts); @@ -1283,6 +1293,17 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, AddRelationNewConstraints(rel, NIL, stmt->constraints, true, true, false, queryString); + /* + * Finally, merge the not-null constraints that are declared directly with + * those that come from parent relations (making sure to count inheritance + * appropriately for each), create them, and set the attnotnull flag on + * columns that don't yet have it. + */ + nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints, + old_notnulls); + foreach_int(attrnum, nncols) + set_attnotnull(NULL, rel, attrnum, NoLock); + ObjectAddressSet(address, RelationRelationId, relationId); /* @@ -2414,6 +2435,8 @@ storage_name(char c) * Output arguments: * 'supconstr' receives a list of constraints belonging to the parents, * updated as necessary to be valid for the child. + * 'supnotnulls' receives a list of CookedConstraints that corresponds to + * constraints coming from inheritance parents. * * Return value: * Completed schema list. @@ -2444,7 +2467,10 @@ storage_name(char c) * * Constraints (including not-null constraints) for the child table * are the union of all relevant constraints, from both the child schema - * and parent tables. + * and parent tables. In addition, in legacy inheritance, each column that + * appears in a primary key in any of the parents also gets a NOT NULL + * constraint (partitioning doesn't need this, because the PK itself gets + * inherited.) * * The default value for a child column is defined as: * (1) If the child schema specifies a default, that value is used. @@ -2463,10 +2489,11 @@ storage_name(char c) */ static List * MergeAttributes(List *columns, const List *supers, char relpersistence, - bool is_partition, List **supconstr) + bool is_partition, List **supconstr, List **supnotnulls) { List *inh_columns = NIL; List *constraints = NIL; + List *nnconstraints = NIL; bool have_bogus_defaults = false; int child_attno; static Node bogus_marker = {0}; /* marks conflicting defaults */ @@ -2577,8 +2604,10 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, AttrMap *newattmap; List *inherited_defaults; List *cols_with_defaults; + List *nnconstrs; ListCell *lc1; ListCell *lc2; + Bitmapset *nncols = NULL; /* caller already got lock */ relation = table_open(parent, NoLock); @@ -2666,6 +2695,15 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, /* We can't process inherited defaults until newattmap is complete. */ inherited_defaults = cols_with_defaults = NIL; + /* + * Request attnotnull on columns that have a not-null constraint + * that's not marked NO INHERIT. + */ + nnconstrs = RelationGetNotNullConstraints(RelationGetRelid(relation), + true, false); + foreach_ptr(CookedConstraint, cc, nnconstrs) + nncols = bms_add_member(nncols, cc->attnum); + for (AttrNumber parent_attno = 1; parent_attno <= tupleDesc->natts; parent_attno++) { @@ -2687,7 +2725,6 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, */ newdef = makeColumnDef(attributeName, attribute->atttypid, attribute->atttypmod, attribute->attcollation); - newdef->is_not_null = attribute->attnotnull; newdef->storage = attribute->attstorage; newdef->generated = attribute->attgenerated; if (CompressionMethodIsValid(attribute->attcompression)) @@ -2735,6 +2772,12 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, mergeddef = newdef; } + /* + * mark attnotnull if parent has it + */ + if (bms_is_member(parent_attno, nncols)) + mergeddef->is_not_null = true; + /* * Locate default/generation expression if any */ @@ -2846,6 +2889,19 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, } } + /* + * Also copy the not-null constraints from this parent. The + * attnotnull markings were already installed above. + */ + foreach_ptr(CookedConstraint, nn, nnconstrs) + { + Assert(nn->contype == CONSTR_NOTNULL); + + nn->attnum = newattmap->attnums[nn->attnum - 1]; + + nnconstraints = lappend(nnconstraints, nn); + } + free_attrmap(newattmap); /* @@ -2916,8 +2972,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, /* * Now that we have the column definition list for a partition, we can * check whether the columns referenced in the column constraint specs - * actually exist. Also, we merge parent's not-null constraints and - * defaults into each corresponding column definition. + * actually exist. Also, merge column defaults. */ if (is_partition) { @@ -2934,7 +2989,6 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, if (strcmp(coldef->colname, restdef->colname) == 0) { found = true; - coldef->is_not_null |= restdef->is_not_null; /* * Check for conflicts related to generated columns. @@ -3023,6 +3077,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, } *supconstr = constraints; + *supnotnulls = nnconstraints; return columns; } @@ -3303,11 +3358,6 @@ MergeInheritedAttribute(List *inh_columns, format_type_with_typemod(prevtypeid, prevtypmod), format_type_with_typemod(newtypeid, newtypmod)))); - /* - * Merge of not-null constraints = OR 'em together - */ - prevdef->is_not_null |= newdef->is_not_null; - /* * Must have the same collation */ @@ -3946,7 +3996,10 @@ rename_constraint_internal(Oid myrelid, constraintOid); con = (Form_pg_constraint) GETSTRUCT(tuple); - if (myrelid && con->contype == CONSTRAINT_CHECK && !con->connoinherit) + if (myrelid && + (con->contype == CONSTRAINT_CHECK || + con->contype == CONSTRAINT_NOTNULL) && + !con->connoinherit) { if (recurse) { @@ -4704,15 +4757,6 @@ AlterTableGetLockLevel(List *cmds) cmd_lockmode = ShareUpdateExclusiveLock; break; - case AT_CheckNotNull: - - /* - * This only examines the table's schema; but lock must be - * strong enough to prevent concurrent DROP NOT NULL. - */ - cmd_lockmode = AccessShareLock; - break; - default: /* oops */ elog(ERROR, "unrecognized alter table type: %d", (int) cmd->subtype); @@ -4881,22 +4925,17 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, case AT_DropNotNull: /* ALTER COLUMN DROP NOT NULL */ ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context); + /* Set up recursion for phase 2; no other prep needed */ + if (recurse) + cmd->recurse = true; pass = AT_PASS_DROP; break; case AT_SetNotNull: /* ALTER COLUMN SET NOT NULL */ ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - /* Need command-specific recursion decision */ - ATPrepSetNotNull(wqueue, rel, cmd, recurse, recursing, - lockmode, context); - pass = AT_PASS_COL_ATTRS; - break; - case AT_CheckNotNull: /* check column is already marked NOT NULL */ - ATSimplePermissions(cmd->subtype, rel, - ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context); - /* No command-specific prep needed */ + /* Set up recursion for phase 2; no other prep needed */ + if (recurse) + cmd->recurse = true; pass = AT_PASS_COL_ATTRS; break; case AT_SetExpression: /* ALTER COLUMN SET EXPRESSION */ @@ -4961,10 +5000,13 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, case AT_AddConstraint: /* ADD CONSTRAINT */ ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - /* Recursion occurs during execution phase */ - /* No command-specific prep needed except saving recurse flag */ + ATPrepAddPrimaryKey(wqueue, rel, cmd, recurse, lockmode, context); if (recurse) + { + /* recurses at exec time; lock descendants and set flag */ + (void) find_all_inheritors(RelationGetRelid(rel), lockmode, NULL); cmd->recurse = true; + } pass = AT_PASS_ADD_CONSTR; break; case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */ @@ -5279,13 +5321,11 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, address = ATExecDropIdentity(rel, cmd->name, cmd->missing_ok, lockmode, cmd->recurse, false); break; case AT_DropNotNull: /* ALTER COLUMN DROP NOT NULL */ - address = ATExecDropNotNull(rel, cmd->name, lockmode); + address = ATExecDropNotNull(rel, cmd->name, cmd->recurse, lockmode); break; case AT_SetNotNull: /* ALTER COLUMN SET NOT NULL */ - address = ATExecSetNotNull(tab, rel, cmd->name, lockmode); - break; - case AT_CheckNotNull: /* check column is already marked NOT NULL */ - ATExecCheckNotNull(tab, rel, cmd->name, lockmode); + address = ATExecSetNotNull(wqueue, rel, NULL, cmd->name, + cmd->recurse, false, lockmode); break; case AT_SetExpression: address = ATExecSetExpression(tab, rel, cmd->name, cmd->def, lockmode); @@ -5368,7 +5408,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, break; case AT_DropConstraint: /* DROP CONSTRAINT */ ATExecDropConstraint(rel, cmd->name, cmd->behavior, - cmd->recurse, false, + cmd->recurse, cmd->missing_ok, lockmode); break; case AT_AlterColumnType: /* ALTER COLUMN TYPE */ @@ -5631,21 +5671,10 @@ ATParseTransformCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, */ switch (cmd2->subtype) { - case AT_SetNotNull: - /* Need command-specific recursion decision */ - ATPrepSetNotNull(wqueue, rel, cmd2, - recurse, false, - lockmode, context); - pass = AT_PASS_COL_ATTRS; - break; case AT_AddIndex: - /* This command never recurses */ - /* No command-specific prep needed */ pass = AT_PASS_ADD_INDEX; break; case AT_AddIndexConstraint: - /* This command never recurses */ - /* No command-specific prep needed */ pass = AT_PASS_ADD_INDEXCONSTR; break; case AT_AddConstraint: @@ -5654,6 +5683,9 @@ ATParseTransformCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, cmd2->recurse = true; switch (castNode(Constraint, cmd2->def)->contype) { + case CONSTR_NOTNULL: + pass = AT_PASS_COL_ATTRS; + break; case CONSTR_PRIMARY: case CONSTR_UNIQUE: case CONSTR_EXCLUSION: @@ -6093,8 +6125,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode) /* * 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, and - * heap_attisnull is a pretty cheap test anyway. + * is a bit of overkill but it minimizes risk of bugs. */ for (i = 0; i < newTupDesc->natts; i++) { @@ -6314,6 +6345,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode) RelationGetRelationName(oldrel)), errtableconstraint(oldrel, con->name))); break; + case CONSTR_NOTNULL: case CONSTR_FOREIGN: /* Nothing to do here */ break; @@ -6427,8 +6459,6 @@ alter_table_type_to_string(AlterTableType cmdtype) return "ALTER COLUMN ... SET EXPRESSION"; case AT_DropExpression: return "ALTER COLUMN ... DROP EXPRESSION"; - case AT_CheckNotNull: - return NULL; /* not real grammar */ case AT_SetStatistics: return "ALTER COLUMN ... SET STATISTICS"; case AT_SetOptions: @@ -7524,13 +7554,14 @@ add_column_collation_dependency(Oid relid, int32 attnum, Oid collid) * nullable, InvalidObjectAddress is returned. */ static ObjectAddress -ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) +ATExecDropNotNull(Relation rel, const char *colName, bool recurse, + LOCKMODE lockmode) { HeapTuple tuple; + HeapTuple conTup; Form_pg_attribute attTup; AttrNumber attnum; Relation attr_rel; - List *indexoidlist; ObjectAddress address; /* @@ -7546,6 +7577,15 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) colName, RelationGetRelationName(rel)))); attTup = (Form_pg_attribute) GETSTRUCT(tuple); attnum = attTup->attnum; + ObjectAddressSubSet(address, RelationRelationId, + RelationGetRelid(rel), attnum); + + /* If the column is already nullable there's nothing to do. */ + if (!attTup->attnotnull) + { + table_close(attr_rel, RowExclusiveLock); + return InvalidObjectAddress; + } /* Prevent them from altering a system attribute */ if (attnum <= 0) @@ -7561,60 +7601,8 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) colName, RelationGetRelationName(rel)))); /* - * Check that the attribute is not in a primary key or in an index used as - * a replica identity. - * - * Note: we'll throw error even if the pkey index is not valid. + * If rel is partition, shouldn't drop NOT NULL if parent has the same. */ - - /* Loop over all indexes on the relation */ - indexoidlist = RelationGetIndexList(rel); - - foreach_oid(indexoid, indexoidlist) - { - HeapTuple indexTuple; - Form_pg_index indexStruct; - - indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid)); - if (!HeapTupleIsValid(indexTuple)) - elog(ERROR, "cache lookup failed for index %u", indexoid); - indexStruct = (Form_pg_index) GETSTRUCT(indexTuple); - - /* - * If the index is not a primary key or an index used as replica - * identity, skip the check. - */ - if (indexStruct->indisprimary || indexStruct->indisreplident) - { - /* - * Loop over each attribute in the primary key or the index used - * as replica identity and see if it matches the to-be-altered - * attribute. - */ - for (int i = 0; i < indexStruct->indnkeyatts; i++) - { - if (indexStruct->indkey.values[i] == attnum) - { - if (indexStruct->indisprimary) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("column \"%s\" is in a primary key", - colName))); - else - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("column \"%s\" is in index used as replica identity", - colName))); - } - } - } - - ReleaseSysCache(indexTuple); - } - - list_free(indexoidlist); - - /* If rel is partition, shouldn't drop NOT NULL if parent has the same */ if (rel->rd_rel->relispartition) { Oid parentId = get_partition_parent(RelationGetRelid(rel), false); @@ -7632,19 +7620,18 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) } /* - * Okay, actually perform the catalog change ... if needed + * Find the constraint that makes this column NOT NULL, and drop it. + * dropconstraint_internal() resets attnotnull. */ - if (attTup->attnotnull) - { - attTup->attnotnull = false; + conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum); + if (conTup == NULL) + elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"", + colName, RelationGetRelationName(rel)); - CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple); - - ObjectAddressSubSet(address, RelationRelationId, - RelationGetRelid(rel), attnum); - } - else - address = InvalidObjectAddress; + /* The normal case: we have a pg_constraint row, remove it */ + dropconstraint_internal(rel, conTup, DROP_RESTRICT, recurse, false, + false, lockmode); + heap_freetuple(conTup); InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), attnum); @@ -7655,104 +7642,105 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode) } /* - * ALTER TABLE ALTER COLUMN SET NOT NULL + * Helper to set pg_attribute.attnotnull if it isn't set, and to tell phase 3 + * to verify it. + * + * When called to alter an existing table, 'wqueue' must be given so that we + * can queue a check that existing tuples pass the constraint. When called + * from table creation, 'wqueue' should be passed as NULL. */ - static void -ATPrepSetNotNull(List **wqueue, Relation rel, - AlterTableCmd *cmd, bool recurse, bool recursing, - LOCKMODE lockmode, AlterTableUtilityContext *context) +set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum, + LOCKMODE lockmode) { + Form_pg_attribute attr; + + CheckAlterTableIsSafe(rel); + /* - * If we're already recursing, there's nothing to do; the topmost - * invocation of ATSimpleRecursion already visited all children. + * Exit quickly by testing attnotnull from the tupledesc's copy of the + * attribute. */ - if (recursing) + attr = TupleDescAttr(RelationGetDescr(rel), attnum - 1); + if (attr->attisdropped) return; - /* - * If the target column is already marked NOT NULL, we can skip recursing - * to children, because their columns should already be marked NOT NULL as - * well. But there's no point in checking here unless the relation has - * some children; else we can just wait till execution to check. (If it - * does have children, however, this can save taking per-child locks - * unnecessarily. This greatly improves concurrency in some parallel - * restore scenarios.) - * - * Unfortunately, we can only apply this optimization to partitioned - * tables, because traditional inheritance doesn't enforce that child - * columns be NOT NULL when their parent is. (That's a bug that should - * get fixed someday.) - */ - if (rel->rd_rel->relhassubclass && - rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + if (!attr->attnotnull) { + Relation attr_rel; HeapTuple tuple; - bool attnotnull; - tuple = SearchSysCacheAttName(RelationGetRelid(rel), cmd->name); + attr_rel = table_open(AttributeRelationId, RowExclusiveLock); - /* Might as well throw the error now, if name is bad */ + tuple = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum); if (!HeapTupleIsValid(tuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", - cmd->name, RelationGetRelationName(rel)))); + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, RelationGetRelid(rel)); - attnotnull = ((Form_pg_attribute) GETSTRUCT(tuple))->attnotnull; - ReleaseSysCache(tuple); - if (attnotnull) - return; + 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)) + { + AlteredTableInfo *tab; + + tab = ATGetQueueEntry(wqueue, rel); + tab->verify_new_notnull = true; + } + + CommandCounterIncrement(); + + table_close(attr_rel, RowExclusiveLock); + heap_freetuple(tuple); } - - /* - * If we have ALTER TABLE ONLY ... SET NOT NULL on a partitioned table, - * apply ALTER TABLE ... CHECK NOT NULL to every child. Otherwise, use - * normal recursion logic. - */ - if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && - !recurse) - { - AlterTableCmd *newcmd = makeNode(AlterTableCmd); - - newcmd->subtype = AT_CheckNotNull; - newcmd->name = pstrdup(cmd->name); - ATSimpleRecursion(wqueue, rel, newcmd, true, lockmode, context); - } - else - ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context); } /* - * Return the address of the modified column. If the column was already NOT - * NULL, InvalidObjectAddress is returned. + * ALTER TABLE ALTER COLUMN SET NOT NULL + * + * Add a not-null constraint to a single table and its children. Returns + * the address of the constraint added to the parent relation, if one gets + * added, or InvalidObjectAddress otherwise. + * + * We must recurse to child tables during execution, rather than using + * ALTER TABLE's normal prep-time recursion. */ static ObjectAddress -ATExecSetNotNull(AlteredTableInfo *tab, Relation rel, - const char *colName, LOCKMODE lockmode) +ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName, + bool recurse, bool recursing, LOCKMODE lockmode) { HeapTuple tuple; - Form_pg_attribute attTup; AttrNumber attnum; - Relation attr_rel; ObjectAddress address; + Constraint *constraint; + CookedConstraint *ccon; + List *cooked; + bool is_no_inherit = false; - /* - * lookup the attribute - */ - attr_rel = table_open(AttributeRelationId, RowExclusiveLock); + /* Guard against stack overflow due to overly deep inheritance tree. */ + check_stack_depth(); - tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName); + /* At top level, permission check was done in ATPrepCmd, else do it */ + if (recursing) + { + ATSimplePermissions(AT_AddConstraint, rel, + ATT_PARTITIONED_TABLE | ATT_TABLE | ATT_FOREIGN_TABLE); + Assert(conName != NULL); + } - if (!HeapTupleIsValid(tuple)) + attnum = get_attnum(RelationGetRelid(rel), colName); + if (attnum == InvalidAttrNumber) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), errmsg("column \"%s\" of relation \"%s\" does not exist", colName, RelationGetRelationName(rel)))); - attTup = (Form_pg_attribute) GETSTRUCT(tuple); - attnum = attTup->attnum; - /* Prevent them from altering a system attribute */ if (attnum <= 0) ereport(ERROR, @@ -7760,81 +7748,132 @@ ATExecSetNotNull(AlteredTableInfo *tab, Relation rel, errmsg("cannot alter system column \"%s\"", colName))); - /* - * Okay, actually perform the catalog change ... if needed - */ - if (!attTup->attnotnull) + /* See if there's already a constraint */ + tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum); + if (HeapTupleIsValid(tuple)) { - attTup->attnotnull = true; - - CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple); + Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple); + bool changed = false; /* - * Ordinarily phase 3 must ensure that no NULLs exist in columns that - * are set NOT NULL; however, if we can find a constraint which proves - * this then we can skip that. We needn't bother looking if we've - * already found that we must verify some other not-null constraint. + * Don't let a NO INHERIT constraint be changed into inherit. */ - if (!tab->verify_new_notnull && !NotNullImpliedByRelConstraints(rel, attTup)) + if (conForm->connoinherit && recurse) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"", + NameStr(conForm->conname), + RelationGetRelationName(rel))); + + /* + * If we find an appropriate constraint, we're almost done, but just + * need to change some properties on it: if we're recursing, increment + * coninhcount; if not, set conislocal if not already set. + */ + if (recursing) { - /* Tell Phase 3 it needs to test the constraint */ - tab->verify_new_notnull = true; + if (pg_add_s16_overflow(conForm->coninhcount, 1, + &conForm->coninhcount)) + ereport(ERROR, + errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("too many inheritance parents")); + changed = true; + } + else if (!conForm->conislocal) + { + conForm->conislocal = true; + changed = true; } - ObjectAddressSubSet(address, RelationRelationId, - RelationGetRelid(rel), attnum); + if (changed) + { + Relation constr_rel; + + constr_rel = table_open(ConstraintRelationId, RowExclusiveLock); + + CatalogTupleUpdate(constr_rel, &tuple->t_self, tuple); + ObjectAddressSet(address, ConstraintRelationId, conForm->oid); + table_close(constr_rel, RowExclusiveLock); + } + + if (changed) + return address; + else + return InvalidObjectAddress; } - else - address = InvalidObjectAddress; + + /* + * If we're asked not to recurse, and children exist, raise an error for + * partitioned tables. For inheritance, we act as if NO INHERIT had been + * specified. + */ + if (!recurse && + find_inheritance_children(RelationGetRelid(rel), + NoLock) != NIL) + { + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraint must be added to child tables too"), + errhint("Do not specify the ONLY keyword.")); + else + is_no_inherit = true; + } + + /* + * No constraint exists; we must add one. First determine a name to use, + * if we haven't already. + */ + if (!recursing) + { + Assert(conName == NULL); + conName = ChooseConstraintName(RelationGetRelationName(rel), + colName, "not_null", + RelationGetNamespace(rel), + NIL); + } + + constraint = makeNotNullConstraint(makeString(colName)); + constraint->is_no_inherit = is_no_inherit; + constraint->conname = conName; + + /* and do it */ + cooked = AddRelationNewConstraints(rel, NIL, list_make1(constraint), + false, !recursing, false, NULL); + ccon = linitial(cooked); + ObjectAddressSet(address, ConstraintRelationId, ccon->conoid); InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), attnum); - table_close(attr_rel, RowExclusiveLock); + /* Mark pg_attribute.attnotnull for the column */ + set_attnotnull(wqueue, rel, attnum, lockmode); + + /* + * Recurse to propagate the constraint to children that don't have one. + */ + if (recurse) + { + List *children; + + children = find_inheritance_children(RelationGetRelid(rel), + lockmode); + + foreach_oid(childoid, children) + { + Relation childrel = table_open(childoid, NoLock); + + CommandCounterIncrement(); + + ATExecSetNotNull(wqueue, childrel, conName, colName, + recurse, true, lockmode); + table_close(childrel, NoLock); + } + } return address; } -/* - * ALTER TABLE ALTER COLUMN CHECK NOT NULL - * - * This doesn't exist in the grammar, but we generate AT_CheckNotNull - * commands against the partitions of a partitioned table if the user - * writes ALTER TABLE ONLY ... SET NOT NULL on the partitioned table, - * or tries to create a primary key on it (which internally creates - * AT_SetNotNull on the partitioned table). Such a command doesn't - * allow us to actually modify any partition, but we want to let it - * go through if the partitions are already properly marked. - * - * In future, this might need to adjust the child table's state, likely - * by incrementing an inheritance count for the attnotnull constraint. - * For now we need only check for the presence of the flag. - */ -static void -ATExecCheckNotNull(AlteredTableInfo *tab, Relation rel, - const char *colName, LOCKMODE lockmode) -{ - HeapTuple tuple; - - tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName); - - if (!HeapTupleIsValid(tuple)) - ereport(ERROR, - errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", - colName, RelationGetRelationName(rel))); - - if (!((Form_pg_attribute) GETSTRUCT(tuple))->attnotnull) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("constraint must be added to child tables too"), - errdetail("Column \"%s\" of relation \"%s\" is not already NOT NULL.", - colName, RelationGetRelationName(rel)), - errhint("Do not specify the ONLY keyword."))); - - ReleaseSysCache(tuple); -} - /* * NotNullImpliedByRelConstraints * Does rel's existing constraints imply NOT NULL for the given attribute? @@ -9139,6 +9178,71 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName, return object; } +/* + * Prepare to add a primary key on table, by adding not-null constraints + * on all columns. + */ +static void +ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd, + bool recurse, LOCKMODE lockmode, + AlterTableUtilityContext *context) +{ + ListCell *lc; + Constraint *pkconstr; + + pkconstr = castNode(Constraint, cmd->def); + if (pkconstr->contype != CONSTR_PRIMARY) + return; + + /* + * If not recursing, we must ensure that all children have a NOT NULL + * constraint on the columns, and error out if not. + */ + if (!recurse) + { + List *children; + + children = find_inheritance_children(RelationGetRelid(rel), + lockmode); + foreach_oid(childrelid, children) + { + foreach(lc, pkconstr->keys) + { + HeapTuple tup; + Form_pg_attribute attrForm; + char *attname = strVal(lfirst(lc)); + + tup = SearchSysCacheAttName(childrelid, attname); + if (!tup) + elog(ERROR, "cache lookup failed for attribute %s of relation %u", + attname, childrelid); + attrForm = (Form_pg_attribute) GETSTRUCT(tup); + if (!attrForm->attnotnull) + ereport(ERROR, + errmsg("column \"%s\" of table \"%s\" is not marked NOT NULL", + attname, get_rel_name(childrelid))); + ReleaseSysCache(tup); + } + } + } + + /* Insert not-null constraints in the queue for the PK columns */ + foreach(lc, pkconstr->keys) + { + AlterTableCmd *newcmd; + Constraint *nnconstr; + + nnconstr = makeNotNullConstraint(lfirst(lc)); + + newcmd = makeNode(AlterTableCmd); + newcmd->subtype = AT_AddConstraint; + newcmd->recurse = true; + newcmd->def = (Node *) nnconstr; + + ATPrepCmd(wqueue, rel, newcmd, true, false, lockmode, context); + } +} + /* * ALTER TABLE ADD INDEX * @@ -9334,17 +9438,18 @@ ATExecAddConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, Assert(IsA(newConstraint, Constraint)); /* - * Currently, we only expect to see CONSTR_CHECK and CONSTR_FOREIGN nodes - * arriving here (see the preprocessing done in parse_utilcmd.c). Use a - * switch anyway to make it easier to add more code later. + * Currently, we only expect to see CONSTR_CHECK, CONSTR_NOTNULL and + * CONSTR_FOREIGN nodes arriving here (see the preprocessing done in + * parse_utilcmd.c). */ switch (newConstraint->contype) { case CONSTR_CHECK: + case CONSTR_NOTNULL: address = - ATAddCheckConstraint(wqueue, tab, rel, - newConstraint, recurse, false, is_readd, - lockmode); + ATAddCheckNNConstraint(wqueue, tab, rel, + newConstraint, recurse, false, is_readd, + lockmode); break; case CONSTR_FOREIGN: @@ -9425,9 +9530,9 @@ ChooseForeignKeyConstraintNameAddition(List *colnames) } /* - * Add a check constraint to a single table and its children. Returns the - * address of the constraint added to the parent relation, if one gets added, - * or InvalidObjectAddress otherwise. + * Add a check or not-null constraint to a single table and its children. + * Returns the address of the constraint added to the parent relation, + * if one gets added, or InvalidObjectAddress otherwise. * * Subroutine for ATExecAddConstraint. * @@ -9440,9 +9545,9 @@ ChooseForeignKeyConstraintNameAddition(List *colnames) * the parent table and pass that down. */ static ObjectAddress -ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, - Constraint *constr, bool recurse, bool recursing, - bool is_readd, LOCKMODE lockmode) +ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, + Constraint *constr, bool recurse, bool recursing, + bool is_readd, LOCKMODE lockmode) { List *newcons; ListCell *lcon; @@ -9450,6 +9555,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, ListCell *child; ObjectAddress address = InvalidObjectAddress; + /* Guard against stack overflow due to overly deep inheritance tree. */ + check_stack_depth(); + /* At top level, permission check was done in ATPrepCmd, else do it */ if (recursing) ATSimplePermissions(AT_AddConstraint, rel, @@ -9481,7 +9589,7 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, { CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon); - if (!ccon->skip_validation) + if (!ccon->skip_validation && ccon->contype != CONSTR_NOTNULL) { NewConstraint *newcon; @@ -9497,11 +9605,18 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, if (constr->conname == NULL) constr->conname = ccon->name; + /* + * If adding a not-null constraint, set the pg_attribute flag and tell + * phase 3 to verify existing rows, if needed. + */ + if (constr->contype == CONSTR_NOTNULL) + set_attnotnull(wqueue, rel, ccon->attnum, lockmode); + ObjectAddressSet(address, ConstraintRelationId, ccon->conoid); } /* At this point we must have a locked-down name to use */ - Assert(constr->conname != NULL); + Assert(newcons == NIL || constr->conname != NULL); /* Advance command counter in case same table is visited multiple times */ CommandCounterIncrement(); @@ -9531,7 +9646,7 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, /* * Check if ONLY was specified with ALTER TABLE. If so, allow the - * constraint creation only if there are no children currently. Error out + * constraint creation only if there are no children currently. Error out * otherwise. */ if (!recurse && children != NIL) @@ -9539,6 +9654,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("constraint must be added to child tables too"))); + /* + * Recurse to create the constraint on each child. + */ foreach(child, children) { Oid childrelid = lfirst_oid(child); @@ -9552,9 +9670,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, /* Find or create work queue entry for this table */ childtab = ATGetQueueEntry(wqueue, childrel); - /* Recurse to child */ - ATAddCheckConstraint(wqueue, childtab, childrel, - constr, recurse, true, is_readd, lockmode); + /* Recurse to this child */ + ATAddCheckNNConstraint(wqueue, childtab, childrel, + constr, recurse, true, is_readd, lockmode); table_close(childrel, NoLock); } @@ -12667,24 +12785,14 @@ createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid, */ static void ATExecDropConstraint(Relation rel, const char *constrName, - DropBehavior behavior, - bool recurse, bool recursing, + DropBehavior behavior, bool recurse, bool missing_ok, LOCKMODE lockmode) { - List *children; Relation conrel; - Form_pg_constraint con; SysScanDesc scan; ScanKeyData skey[3]; HeapTuple tuple; bool found = false; - bool is_no_inherit_constraint = false; - char contype; - - /* At top level, permission check was done in ATPrepCmd, else do it */ - if (recursing) - ATSimplePermissions(AT_DropConstraint, rel, - ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); conrel = table_open(ConstraintRelationId, RowExclusiveLock); @@ -12709,47 +12817,8 @@ ATExecDropConstraint(Relation rel, const char *constrName, /* There can be at most one matching row */ if (HeapTupleIsValid(tuple = systable_getnext(scan))) { - ObjectAddress conobj; - - con = (Form_pg_constraint) GETSTRUCT(tuple); - - /* Don't drop inherited constraints */ - if (con->coninhcount > 0 && !recursing) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("cannot drop inherited constraint \"%s\" of relation \"%s\"", - constrName, RelationGetRelationName(rel)))); - - is_no_inherit_constraint = con->connoinherit; - contype = con->contype; - - /* - * If it's a foreign-key constraint, we'd better lock the referenced - * table and check that that's not in use, just as we've already done - * for the constrained table (else we might, eg, be dropping a trigger - * that has unfired events). But we can/must skip that in the - * self-referential case. - */ - if (contype == CONSTRAINT_FOREIGN && - con->confrelid != RelationGetRelid(rel)) - { - Relation frel; - - /* Must match lock taken by RemoveTriggerById: */ - frel = table_open(con->confrelid, AccessExclusiveLock); - CheckAlterTableIsSafe(frel); - table_close(frel, NoLock); - } - - /* - * Perform the actual constraint deletion - */ - conobj.classId = ConstraintRelationId; - conobj.objectId = con->oid; - conobj.objectSubId = 0; - - performDeletion(&conobj, behavior, 0); - + dropconstraint_internal(rel, tuple, behavior, recurse, false, + missing_ok, lockmode); found = true; } @@ -12758,31 +12827,180 @@ ATExecDropConstraint(Relation rel, const char *constrName, if (!found) { if (!missing_ok) - { ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("constraint \"%s\" of relation \"%s\" does not exist", - constrName, RelationGetRelationName(rel)))); - } + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + constrName, RelationGetRelationName(rel))); else - { ereport(NOTICE, - (errmsg("constraint \"%s\" of relation \"%s\" does not exist, skipping", - constrName, RelationGetRelationName(rel)))); - table_close(conrel, RowExclusiveLock); - return; + errmsg("constraint \"%s\" of relation \"%s\" does not exist, skipping", + constrName, RelationGetRelationName(rel))); + } + + table_close(conrel, RowExclusiveLock); +} + +/* + * Remove a constraint, using its pg_constraint tuple + * + * Implementation for ALTER TABLE DROP CONSTRAINT and ALTER TABLE ALTER COLUMN + * DROP NOT NULL. + * + * Returns the address of the constraint being removed. + */ +static ObjectAddress +dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior behavior, + bool recurse, bool recursing, bool missing_ok, + LOCKMODE lockmode) +{ + Relation conrel; + Form_pg_constraint con; + ObjectAddress conobj; + List *children; + bool is_no_inherit_constraint = false; + char *constrName; + char *colname = NULL; + + /* Guard against stack overflow due to overly deep inheritance tree. */ + check_stack_depth(); + + /* At top level, permission check was done in ATPrepCmd, else do it */ + if (recursing) + ATSimplePermissions(AT_DropConstraint, rel, + ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); + + conrel = table_open(ConstraintRelationId, RowExclusiveLock); + + con = (Form_pg_constraint) GETSTRUCT(constraintTup); + constrName = NameStr(con->conname); + + /* Don't allow drop of inherited constraints */ + if (con->coninhcount > 0 && !recursing) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot drop inherited constraint \"%s\" of relation \"%s\"", + constrName, RelationGetRelationName(rel)))); + + /* + * Reset pg_constraint.attnotnull, if this is a not-null constraint. + * + * While doing that, we're in a good position to disallow dropping a not- + * null constraint underneath a primary key, a replica identity index, or + * a generated identity column. + */ + if (con->contype == CONSTRAINT_NOTNULL) + { + Relation attrel = table_open(AttributeRelationId, RowExclusiveLock); + AttrNumber attnum = extractNotNullColumn(constraintTup); + Bitmapset *pkattrs; + Bitmapset *irattrs; + HeapTuple atttup; + Form_pg_attribute attForm; + + /* save column name for recursion step */ + colname = get_attname(RelationGetRelid(rel), attnum, false); + + /* + * Disallow if it's in the primary key. For partitioned tables we + * cannot rely solely on RelationGetIndexAttrBitmap, because it'll + * return NULL if the primary key is invalid; but we still need to + * protect not-null constraints under such a constraint, so check the + * slow way. + */ + pkattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_PRIMARY_KEY); + + if (pkattrs == NULL && + rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + Oid pkindex = RelationGetPrimaryKeyIndex(rel, true); + + if (OidIsValid(pkindex)) + { + Relation pk = relation_open(pkindex, AccessShareLock); + + pkattrs = NULL; + for (int i = 0; i < pk->rd_index->indnkeyatts; i++) + pkattrs = bms_add_member(pkattrs, pk->rd_index->indkey.values[i]); + + relation_close(pk, AccessShareLock); + } } + + if (pkattrs && + bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, pkattrs)) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is in a primary key", + get_attname(RelationGetRelid(rel), attnum, false))); + + /* Disallow if it's in the replica identity */ + irattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_IDENTITY_KEY); + if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, irattrs)) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is in index used as replica identity", + get_attname(RelationGetRelid(rel), attnum, false))); + + /* Disallow if it's a GENERATED AS IDENTITY column */ + atttup = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum); + if (!HeapTupleIsValid(atttup)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, RelationGetRelid(rel)); + attForm = (Form_pg_attribute) GETSTRUCT(atttup); + if (attForm->attidentity != '\0') + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("column \"%s\" of relation \"%s\" is an identity column", + get_attname(RelationGetRelid(rel), attnum, + false), + RelationGetRelationName(rel))); + + /* All good -- reset attnotnull if needed */ + if (attForm->attnotnull) + { + attForm->attnotnull = false; + CatalogTupleUpdate(attrel, &atttup->t_self, atttup); + } + + table_close(attrel, RowExclusiveLock); + } + + is_no_inherit_constraint = con->connoinherit; + + /* + * If it's a foreign-key constraint, we'd better lock the referenced table + * and check that that's not in use, just as we've already done for the + * constrained table (else we might, eg, be dropping a trigger that has + * unfired events). But we can/must skip that in the self-referential + * case. + */ + if (con->contype == CONSTRAINT_FOREIGN && + con->confrelid != RelationGetRelid(rel)) + { + Relation frel; + + /* Must match lock taken by RemoveTriggerById: */ + frel = table_open(con->confrelid, AccessExclusiveLock); + CheckAlterTableIsSafe(frel); + table_close(frel, NoLock); } /* - * For partitioned tables, non-CHECK inherited constraints are dropped via - * the dependency mechanism, so we're done here. + * Perform the actual constraint deletion */ - if (contype != CONSTRAINT_CHECK && + ObjectAddressSet(conobj, ConstraintRelationId, con->oid); + performDeletion(&conobj, behavior, 0); + + /* + * For partitioned tables, non-CHECK, non-NOT-NULL inherited constraints + * are dropped via the dependency mechanism, so we're done here. + */ + if (con->contype != CONSTRAINT_CHECK && + con->contype != CONSTRAINT_NOTNULL && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) { table_close(conrel, RowExclusiveLock); - return; + return conobj; } /* @@ -12798,48 +13016,65 @@ ATExecDropConstraint(Relation rel, const char *constrName, foreach_oid(childrelid, children) { Relation childrel; - HeapTuple copy_tuple; + HeapTuple tuple; + Form_pg_constraint childcon; /* find_inheritance_children already got lock */ childrel = table_open(childrelid, NoLock); CheckAlterTableIsSafe(childrel); - ScanKeyInit(&skey[0], - Anum_pg_constraint_conrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(childrelid)); - ScanKeyInit(&skey[1], - Anum_pg_constraint_contypid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(InvalidOid)); - ScanKeyInit(&skey[2], - Anum_pg_constraint_conname, - BTEqualStrategyNumber, F_NAMEEQ, - CStringGetDatum(constrName)); - scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, - true, NULL, 3, skey); + /* + * We search for not-null constraints by column name, and others by + * constraint name. + */ + if (con->contype == CONSTRAINT_NOTNULL) + { + tuple = findNotNullConstraint(childrelid, colname); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u", + colname, RelationGetRelid(childrel)); + } + else + { + SysScanDesc scan; + ScanKeyData skey[3]; - /* There can be at most one matching row */ - if (!HeapTupleIsValid(tuple = systable_getnext(scan))) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("constraint \"%s\" of relation \"%s\" does not exist", - constrName, - RelationGetRelationName(childrel)))); + ScanKeyInit(&skey[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(childrelid)); + ScanKeyInit(&skey[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + ScanKeyInit(&skey[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(constrName)); + scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, + true, NULL, 3, skey); + /* There can only be one, so no need to loop */ + tuple = systable_getnext(scan); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + constrName, + RelationGetRelationName(childrel)))); + tuple = heap_copytuple(tuple); + systable_endscan(scan); + } - copy_tuple = heap_copytuple(tuple); + childcon = (Form_pg_constraint) GETSTRUCT(tuple); - systable_endscan(scan); + /* Right now only CHECK and not-null constraints can be inherited */ + if (childcon->contype != CONSTRAINT_CHECK && + childcon->contype != CONSTRAINT_NOTNULL) + elog(ERROR, "inherited constraint is not a CHECK or not-null constraint"); - con = (Form_pg_constraint) GETSTRUCT(copy_tuple); - - /* Right now only CHECK constraints can be inherited */ - if (con->contype != CONSTRAINT_CHECK) - elog(ERROR, "inherited constraint is not a CHECK constraint"); - - if (con->coninhcount <= 0) /* shouldn't happen */ + if (childcon->coninhcount <= 0) /* shouldn't happen */ elog(ERROR, "relation %u has non-inherited constraint \"%s\"", - childrelid, constrName); + childrelid, NameStr(childcon->conname)); if (recurse) { @@ -12847,18 +13082,18 @@ ATExecDropConstraint(Relation rel, const char *constrName, * If the child constraint has other definition sources, just * decrement its inheritance count; if not, recurse to delete it. */ - if (con->coninhcount == 1 && !con->conislocal) + if (childcon->coninhcount == 1 && !childcon->conislocal) { /* Time to delete this child constraint, too */ - ATExecDropConstraint(childrel, constrName, behavior, - true, true, - false, lockmode); + dropconstraint_internal(childrel, tuple, behavior, + recurse, true, missing_ok, + lockmode); } else { /* Child constraint must survive my deletion */ - con->coninhcount--; - CatalogTupleUpdate(conrel, ©_tuple->t_self, copy_tuple); + childcon->coninhcount--; + CatalogTupleUpdate(conrel, &tuple->t_self, tuple); /* Make update visible */ CommandCounterIncrement(); @@ -12867,25 +13102,29 @@ ATExecDropConstraint(Relation rel, const char *constrName, else { /* - * If we were told to drop ONLY in this table (no recursion), we - * need to mark the inheritors' constraints as locally defined - * rather than inherited. + * If we were told to drop ONLY in this table (no recursion) and + * there are no further parents for this constraint, we need to + * mark the inheritors' constraints as locally defined rather than + * inherited. */ - con->coninhcount--; - con->conislocal = true; + childcon->coninhcount--; + if (childcon->coninhcount == 0) + childcon->conislocal = true; - CatalogTupleUpdate(conrel, ©_tuple->t_self, copy_tuple); + CatalogTupleUpdate(conrel, &tuple->t_self, tuple); /* Make update visible */ CommandCounterIncrement(); } - heap_freetuple(copy_tuple); + heap_freetuple(tuple); table_close(childrel, NoLock); } table_close(conrel, RowExclusiveLock); + + return conobj; } /* @@ -13834,10 +14073,26 @@ RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab) char *defstring = pg_get_constraintdef_command(conoid); Oid indoid; - tab->changedConstraintOids = lappend_oid(tab->changedConstraintOids, - conoid); - tab->changedConstraintDefs = lappend(tab->changedConstraintDefs, - defstring); + /* + * It is critical to create not-null constraints ahead of primary key + * indexes; otherwise, the not-null constraint would be created by the + * primary key, and the constraint name would be wrong. + */ + if (get_constraint_type(conoid) == CONSTRAINT_NOTNULL) + { + tab->changedConstraintOids = lcons_oid(conoid, + tab->changedConstraintOids); + tab->changedConstraintDefs = lcons(defstring, + tab->changedConstraintDefs); + } + else + { + + tab->changedConstraintOids = lappend_oid(tab->changedConstraintOids, + conoid); + tab->changedConstraintDefs = lappend(tab->changedConstraintDefs, + defstring); + } /* * For the index of a constraint, if any, remember if it is used for @@ -14000,9 +14255,10 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) /* * If the constraint is inherited (only), we don't want to inject a - * new definition here; it'll get recreated when ATAddCheckConstraint - * recurses from adding the parent table's constraint. But we had to - * carry the info this far so that we can drop the constraint below. + * new definition here; it'll get recreated when + * ATAddCheckNNConstraint recurses from adding the parent table's + * constraint. But we had to carry the info this far so that we can + * drop the constraint below. */ if (!conislocal) continue; @@ -14241,23 +14497,21 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd, tab->subcmds[AT_PASS_OLD_CONSTR] = lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd); - /* recreate any comment on the constraint */ - RebuildConstraintComment(tab, - AT_PASS_OLD_CONSTR, - oldId, - rel, - NIL, - con->conname); - } - else if (cmd->subtype == AT_SetNotNull) - { /* - * The parser will create AT_SetNotNull subcommands for - * columns of PRIMARY KEY indexes/constraints, but we need - * not do anything with them here, because the columns' - * NOT NULL marks will already have been propagated into - * the new table definition. + * Recreate any comment on the constraint. If we have + * recreated a primary key, then transformTableConstraint + * has added an unnamed not-null constraint here; skip + * this in that case. */ + if (con->conname) + RebuildConstraintComment(tab, + AT_PASS_OLD_CONSTR, + oldId, + rel, + NIL, + con->conname); + else + Assert(con->contype == CONSTR_NOTNULL); } else elog(ERROR, "unexpected statement subtype: %d", @@ -16012,14 +16266,24 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart RelationGetRelationName(child_rel), parent_attname))); /* - * Check child doesn't discard NOT NULL property. (Other - * constraints are checked elsewhere.) + * If the parent has a not-null constraint that's not NO INHERIT, + * make sure the child has one too. + * + * Other constraints are checked elsewhere. */ if (parent_att->attnotnull && !child_att->attnotnull) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("column \"%s\" in child table must be marked NOT NULL", - parent_attname))); + { + HeapTuple contup; + + contup = findNotNullConstraintAttnum(RelationGetRelid(parent_rel), + parent_att->attnum); + if (HeapTupleIsValid(contup) && + !((Form_pg_constraint) GETSTRUCT(contup))->connoinherit) + ereport(ERROR, + errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL", + parent_attname, RelationGetRelationName(child_rel))); + } /* * Child column must be generated if and only if parent column is. @@ -16101,6 +16365,7 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) ScanKeyData parent_key; HeapTuple parent_tuple; Oid parent_relid = RelationGetRelid(parent_rel); + AttrMap *attmap; constraintrel = table_open(ConstraintRelationId, RowExclusiveLock); @@ -16112,21 +16377,32 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) parent_scan = systable_beginscan(constraintrel, ConstraintRelidTypidNameIndexId, true, NULL, 1, &parent_key); + attmap = build_attrmap_by_name(RelationGetDescr(parent_rel), + RelationGetDescr(child_rel), + true); + while (HeapTupleIsValid(parent_tuple = systable_getnext(parent_scan))) { Form_pg_constraint parent_con = (Form_pg_constraint) GETSTRUCT(parent_tuple); SysScanDesc child_scan; ScanKeyData child_key; HeapTuple child_tuple; + AttrNumber parent_attno; bool found = false; - if (parent_con->contype != CONSTRAINT_CHECK) + if (parent_con->contype != CONSTRAINT_CHECK && + parent_con->contype != CONSTRAINT_NOTNULL) continue; /* if the parent's constraint is marked NO INHERIT, it's not inherited */ if (parent_con->connoinherit) continue; + if (parent_con->contype == CONSTRAINT_NOTNULL) + parent_attno = extractNotNullColumn(parent_tuple); + else + parent_attno = InvalidAttrNumber; + /* Search for a child constraint matching this one */ ScanKeyInit(&child_key, Anum_pg_constraint_conrelid, @@ -16140,20 +16416,46 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) Form_pg_constraint child_con = (Form_pg_constraint) GETSTRUCT(child_tuple); HeapTuple child_copy; - if (child_con->contype != CONSTRAINT_CHECK) + if (child_con->contype != parent_con->contype) continue; - if (strcmp(NameStr(parent_con->conname), - NameStr(child_con->conname)) != 0) - continue; + /* + * CHECK constraint are matched by constraint name, NOT NULL ones + * by attribute number. + */ + if (child_con->contype == CONSTRAINT_CHECK) + { + if (strcmp(NameStr(parent_con->conname), + NameStr(child_con->conname)) != 0) + continue; + } + else if (child_con->contype == CONSTRAINT_NOTNULL) + { + Form_pg_attribute parent_attr; + Form_pg_attribute child_attr; + AttrNumber child_attno; - if (!constraints_equivalent(parent_tuple, child_tuple, RelationGetDescr(constraintrel))) + parent_attr = TupleDescAttr(parent_rel->rd_att, parent_attno - 1); + child_attno = extractNotNullColumn(child_tuple); + if (parent_attno != attmap->attnums[child_attno - 1]) + continue; + + child_attr = TupleDescAttr(child_rel->rd_att, child_attno - 1); + /* there shouldn't be constraints on dropped columns */ + if (parent_attr->attisdropped || child_attr->attisdropped) + elog(ERROR, "found not-null constraint on dropped columns"); + } + + if (child_con->contype == CONSTRAINT_CHECK && + !constraints_equivalent(parent_tuple, child_tuple, RelationGetDescr(constraintrel))) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("child table \"%s\" has different definition for check constraint \"%s\"", RelationGetRelationName(child_rel), NameStr(parent_con->conname)))); - /* If the child constraint is "no inherit" then cannot merge */ + /* + * If the child constraint is "no inherit" then cannot merge + */ if (child_con->connoinherit) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), @@ -16204,10 +16506,21 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel) systable_endscan(child_scan); if (!found) + { + if (parent_con->contype == CONSTRAINT_NOTNULL) + ereport(ERROR, + errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL", + get_attname(parent_relid, + extractNotNullColumn(parent_tuple), + false), + RelationGetRelationName(child_rel))); + ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("child table is missing constraint \"%s\"", NameStr(parent_con->conname)))); + } } systable_endscan(parent_scan); @@ -16352,7 +16665,9 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) ScanKeyData key[3]; HeapTuple attributeTuple, constraintTuple; + AttrMap *attmap; List *connames; + List *nncolumns; bool found; bool is_partitioning; @@ -16417,11 +16732,18 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) table_close(catalogRelation, RowExclusiveLock); /* - * Likewise, find inherited check constraints and disinherit them. To do - * this, we first need a list of the names of the parent's check - * constraints. (We cheat a bit by only checking for name matches, + * Likewise, find inherited check and not-null constraints and disinherit + * them. To do this, we first need a list of the names of the parent's + * check constraints. (We cheat a bit by only checking for name matches, * assuming that the expressions will match.) + * + * For NOT NULL columns, we store column numbers to match, mapping them in + * to the child rel's attribute numbers. */ + attmap = build_attrmap_by_name(RelationGetDescr(child_rel), + RelationGetDescr(parent_rel), + false); + catalogRelation = table_open(ConstraintRelationId, RowExclusiveLock); ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, @@ -16431,18 +16753,28 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) true, NULL, 1, key); connames = NIL; + nncolumns = NIL; while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) { Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple); + if (con->connoinherit) + continue; + if (con->contype == CONSTRAINT_CHECK) connames = lappend(connames, pstrdup(NameStr(con->conname))); + if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber parent_attno = extractNotNullColumn(constraintTuple); + + nncolumns = lappend_int(nncolumns, attmap->attnums[parent_attno - 1]); + } } systable_endscan(scan); - /* Now scan the child's constraints */ + /* Now scan the child's constraints to find matches */ ScanKeyInit(&key[0], Anum_pg_constraint_conrelid, BTEqualStrategyNumber, F_OIDEQ, @@ -16453,20 +16785,41 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) while (HeapTupleIsValid(constraintTuple = systable_getnext(scan))) { Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple); - bool match; + bool match = false; - if (con->contype != CONSTRAINT_CHECK) - continue; - - match = false; - foreach_ptr(char, chkname, connames) + /* + * Match CHECK constraints by name, not-null constraints by column + * number, and ignore all others. + */ + if (con->contype == CONSTRAINT_CHECK) { - if (strcmp(NameStr(con->conname), chkname) == 0) + foreach_ptr(char, chkname, connames) { - match = true; - break; + if (con->contype == CONSTRAINT_CHECK && + strcmp(NameStr(con->conname), chkname) == 0) + { + match = true; + connames = foreach_delete_current(connames, chkname); + break; + } } } + else if (con->contype == CONSTRAINT_NOTNULL) + { + AttrNumber child_attno = extractNotNullColumn(constraintTuple); + + foreach_int(prevattno, nncolumns) + { + if (prevattno == child_attno) + { + match = true; + nncolumns = foreach_delete_current(nncolumns, prevattno); + break; + } + } + } + else + continue; if (match) { @@ -16487,6 +16840,12 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) } } + /* We should have matched all constraints */ + if (connames != NIL || nncolumns != NIL) + elog(ERROR, "%d unmatched constraints while removing inheritance from \"%s\" to \"%s\"", + list_length(connames) + list_length(nncolumns), + RelationGetRelationName(child_rel), RelationGetRelationName(parent_rel)); + systable_endscan(scan); table_close(catalogRelation, RowExclusiveLock); @@ -19039,7 +19398,8 @@ AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel) /* * If no suitable index was found in the partition-to-be, create one - * now. + * now. Note that if this is a PK, not-null constraints must already + * exist. */ if (!found) { @@ -19737,7 +20097,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name) * DetachAddConstraintIfNeeded * Subroutine for ATExecDetachPartition. Create a constraint that * takes the place of the partition constraint, but avoid creating - * a dupe if an constraint already exists which implies the needed + * a dupe if a constraint already exists which implies the needed * constraint. */ static void @@ -19770,8 +20130,8 @@ DetachAddConstraintIfNeeded(List **wqueue, Relation partRel) n->initially_valid = true; n->skip_validation = true; /* It's a re-add, since it nominally already exists */ - ATAddCheckConstraint(wqueue, tab, partRel, n, - true, false, true, ShareUpdateExclusiveLock); + ATAddCheckNNConstraint(wqueue, tab, partRel, n, + true, false, true, ShareUpdateExclusiveLock); } } @@ -20040,6 +20400,13 @@ ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name) RelationGetRelationName(partIdx)))); } + /* + * If it's a primary key, make sure the columns in the partition are + * NOT NULL. + */ + if (parentIdx->rd_index->indisprimary) + verifyPartitionIndexNotNull(childInfo, partTbl); + /* All good -- do it */ IndexSetParentIndex(partIdx, RelationGetRelid(parentIdx)); if (OidIsValid(constraintOid)) @@ -20183,6 +20550,29 @@ validatePartitionedIndex(Relation partedIdx, Relation partedTbl) } } +/* + * When attaching an index as a partition of a partitioned index which is a + * primary key, verify that all the columns in the partition are marked NOT + * NULL. + */ +static void +verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition) +{ + for (int i = 0; i < iinfo->ii_NumIndexKeyAttrs; i++) + { + Form_pg_attribute att = TupleDescAttr(RelationGetDescr(partition), + iinfo->ii_IndexAttrNumbers[i] - 1); + + if (!att->attnotnull) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("invalid primary key definition"), + errdetail("Column \"%s\" of relation \"%s\" is not marked NOT NULL.", + NameStr(att->attname), + RelationGetRelationName(partition))); + } +} + /* * Return an OID list of constraints that reference the given relation * that are marked as having a parent constraints. diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 2a6550de90..859e2191f0 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -944,6 +944,10 @@ DefineDomain(CreateDomainStmt *stmt) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("conflicting NULL/NOT NULL constraints"))); + if (constr->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("not-null constraints for domains cannot be marked NO INHERIT")); typNotNull = true; nullDefined = true; break; diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 9cac3c1c27..7e5df7bea4 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -436,6 +436,29 @@ makeRangeVar(char *schemaname, char *relname, int location) return r; } +/* + * makeNotNullConstraint - + * creates a Constraint node for NOT NULL constraints + */ +Constraint * +makeNotNullConstraint(String *colname) +{ + Constraint *notnull; + + notnull = makeNode(Constraint); + notnull->contype = CONSTR_NOTNULL; + notnull->conname = NULL; + notnull->is_no_inherit = false; + notnull->deferrable = false; + notnull->initdeferred = false; + notnull->location = -1; + notnull->keys = list_make1(colname); + notnull->skip_validation = false; + notnull->initially_valid = true; + + return notnull; +} + /* * makeTypeName - * build a TypeName node for an unqualified name. diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index b913f91ff0..37b0ca2e43 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -1698,6 +1698,8 @@ relation_excluded_by_constraints(PlannerInfo *root, * Currently, attnotnull constraints must be treated as NO INHERIT unless * this is a partitioned table. In future we might track their * inheritance status more accurately, allowing this to be refined. + * + * XXX do we need/want to change this? */ include_notnull = (!rte->inh || rte->relkind == RELKIND_PARTITIONED_TABLE); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 89fdb94c23..67eb96396a 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -3908,12 +3908,15 @@ ColConstraint: * or be part of a_expr NOT LIKE or similar constructs). */ ColConstraintElem: - NOT NULL_P + NOT NULL_P opt_no_inherit { Constraint *n = makeNode(Constraint); n->contype = CONSTR_NOTNULL; n->location = @1; + n->is_no_inherit = $3; + n->skip_validation = false; + n->initially_valid = true; $$ = (Node *) n; } | NULL_P @@ -4150,6 +4153,20 @@ ConstraintElem: n->initially_valid = !n->skip_validation; $$ = (Node *) n; } + | NOT NULL_P ColId ConstraintAttributeSpec + { + Constraint *n = makeNode(Constraint); + + n->contype = CONSTR_NOTNULL; + n->location = @1; + n->keys = list_make1(makeString($3)); + /* no NOT VALID support yet */ + processCASbits($4, @4, "NOT NULL", + NULL, NULL, NULL, + &n->is_no_inherit, yyscanner); + n->initially_valid = true; + $$ = (Node *) n; + } | UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace ConstraintAttributeSpec { @@ -4317,10 +4334,10 @@ DomainConstraintElem: n->contype = CONSTR_NOTNULL; n->location = @1; n->keys = list_make1(makeString("value")); - /* no NOT VALID support yet */ + /* no NOT VALID, NO INHERIT support */ processCASbits($3, @3, "NOT NULL", NULL, NULL, NULL, - &n->is_no_inherit, yyscanner); + NULL, yyscanner); n->initially_valid = true; $$ = (Node *) n; } diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 1e15ce10b4..0f324ee4e3 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -81,6 +81,7 @@ typedef struct bool isalter; /* true if altering existing table */ List *columns; /* ColumnDef items */ List *ckconstraints; /* CHECK constraints */ + List *nnconstraints; /* NOT NULL constraints */ List *fkconstraints; /* FOREIGN KEY constraints */ List *ixconstraints; /* index-creating constraints */ List *likeclauses; /* LIKE clauses that need post-processing */ @@ -240,6 +241,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) cxt.isalter = false; cxt.columns = NIL; cxt.ckconstraints = NIL; + cxt.nnconstraints = NIL; cxt.fkconstraints = NIL; cxt.ixconstraints = NIL; cxt.likeclauses = NIL; @@ -303,6 +305,32 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) Assert(stmt->constraints == NIL); + /* + * Before processing index constraints, which could include a primary key, + * we must scan all not-null constraints to propagate the is_not_null flag + * to each corresponding ColumnDef. This is necessary because table-level + * not-null constraints have not been marked in each ColumnDef, and the PK + * processing code needs to know whether one constraint has already been + * declared in order not to declare a redundant one. + */ + foreach_node(Constraint, nn, cxt.nnconstraints) + { + char *colname = strVal(linitial(nn->keys)); + + foreach_node(ColumnDef, cd, cxt.columns) + { + /* not our column? */ + if (strcmp(cd->colname, colname) != 0) + continue; + /* Already marked not-null? Nothing to do */ + if (cd->is_not_null) + break; + /* Bingo, we're done for this constraint */ + cd->is_not_null = true; + break; + } + } + /* * Postprocess constraints that give rise to index definitions. */ @@ -340,6 +368,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) */ stmt->tableElts = cxt.columns; stmt->constraints = cxt.ckconstraints; + stmt->nnconstraints = cxt.nnconstraints; result = lappend(cxt.blist, stmt); result = list_concat(result, cxt.alist); @@ -566,7 +595,9 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) bool saw_default; bool saw_identity; bool saw_generated; - ListCell *clist; + bool need_notnull = false; + bool disallow_noinherit_notnull = false; + Constraint *notnull_constraint = NULL; cxt->columns = lappend(cxt->columns, column); @@ -663,28 +694,54 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) constraint->cooked_expr = NULL; column->constraints = lappend(column->constraints, constraint); - constraint = makeNode(Constraint); - constraint->contype = CONSTR_NOTNULL; - constraint->location = -1; - column->constraints = lappend(column->constraints, constraint); + /* have a not-null constraint added later */ + need_notnull = true; + disallow_noinherit_notnull = true; } /* Process column constraints, if any... */ transformConstraintAttrs(cxt, column->constraints); + /* + * First, scan the column's constraints to see if a not-null constraint + * that we add must be prevented from being NO INHERIT. This should be + * enforced only for PRIMARY KEY, not IDENTITY or SERIAL. However, if the + * not-null constraint is specified as a table constraint rather than as a + * column constraint, AddRelationNotNullConstraints would raise an error + * if a NO INHERIT mismatch is found. To avoid inconsistently disallowing + * it in the table constraint case but not the column constraint case, we + * disallow it here as well. Maybe AddRelationNotNullConstraints can be + * improved someday, so that it doesn't complain, and then we can remove + * the restriction for SERIAL and IDENTITY here as well. + */ + if (!disallow_noinherit_notnull) + { + foreach_node(Constraint, constraint, column->constraints) + { + switch (constraint->contype) + { + case CONSTR_IDENTITY: + case CONSTR_PRIMARY: + disallow_noinherit_notnull = true; + break; + default: + break; + } + } + } + + /* Now scan them again to do full processing */ saw_nullable = false; saw_default = false; saw_identity = false; saw_generated = false; - foreach(clist, column->constraints) + foreach_node(Constraint, constraint, column->constraints) { - Constraint *constraint = lfirst_node(Constraint, clist); - switch (constraint->contype) { case CONSTR_NULL: - if (saw_nullable && column->is_not_null) + if ((saw_nullable && column->is_not_null) || need_notnull) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("conflicting NULL/NOT NULL declarations for column \"%s\" of table \"%s\"", @@ -696,6 +753,12 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) break; case CONSTR_NOTNULL: + if (cxt->ispartitioned && constraint->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("not-null constraints on partitioned tables cannot be NO INHERIT")); + + /* Disallow conflicting [NOT] NULL markings */ if (saw_nullable && !column->is_not_null) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), @@ -703,8 +766,52 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) column->colname, cxt->relation->relname), parser_errposition(cxt->pstate, constraint->location))); - column->is_not_null = true; - saw_nullable = true; + + if (disallow_noinherit_notnull && constraint->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting NO INHERIT declarations for not-null constraints on column \"%s\"", + column->colname)); + + /* + * If this is the first time we see this column being marked + * not-null, add the constraint entry and keep track of it. + * Also, remove previous markings that we need one. + * + * If this is a redundant not-null specification, just check + * that it doesn't conflict with what was specified earlier. + * + * Any conflicts with table constraints will be further + * checked in AddRelationNotNullConstraints(). + */ + if (!column->is_not_null) + { + column->is_not_null = true; + saw_nullable = true; + need_notnull = false; + + constraint->keys = list_make1(makeString(column->colname)); + notnull_constraint = constraint; + cxt->nnconstraints = lappend(cxt->nnconstraints, constraint); + } + else if (notnull_constraint) + { + if (constraint->conname && + notnull_constraint->conname && + strcmp(notnull_constraint->conname, constraint->conname) != 0) + elog(ERROR, "conflicting not-null constraint names \"%s\" and \"%s\"", + notnull_constraint->conname, constraint->conname); + + if (notnull_constraint->is_no_inherit != constraint->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting NO INHERIT declarations for not-null constraints on column \"%s\"", + column->colname)); + + if (!notnull_constraint->conname && constraint->conname) + notnull_constraint->conname = constraint->conname; + } + break; case CONSTR_DEFAULT: @@ -754,16 +861,19 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) column->identity = constraint->generated_when; saw_identity = true; - /* An identity column is implicitly NOT NULL */ - if (saw_nullable && !column->is_not_null) + /* + * Identity columns are always NOT NULL, but we may have a + * constraint already. + */ + if (!saw_nullable) + need_notnull = true; + else if (!column->is_not_null) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("conflicting NULL/NOT NULL declarations for column \"%s\" of table \"%s\"", column->colname, cxt->relation->relname), parser_errposition(cxt->pstate, constraint->location))); - column->is_not_null = true; - saw_nullable = true; break; } @@ -790,6 +900,15 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) break; case CONSTR_PRIMARY: + if (saw_nullable && !column->is_not_null) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting NULL/NOT NULL declarations for column \"%s\" of table \"%s\"", + column->colname, cxt->relation->relname), + parser_errposition(cxt->pstate, + constraint->location))); + need_notnull = true; + if (cxt->isforeign) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -869,6 +988,17 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) constraint->location))); } + /* + * If we need a not-null constraint for PRIMARY KEY, SERIAL or IDENTITY, + * and one was not explicitly specified, add one now. + */ + if (need_notnull && !(saw_nullable && column->is_not_null)) + { + column->is_not_null = true; + notnull_constraint = makeNotNullConstraint(makeString(column->colname)); + cxt->nnconstraints = lappend(cxt->nnconstraints, notnull_constraint); + } + /* * If needed, generate ALTER FOREIGN TABLE ALTER COLUMN statement to add * per-column foreign data wrapper options to this column after creation. @@ -938,6 +1068,15 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint) cxt->ckconstraints = lappend(cxt->ckconstraints, constraint); break; + case CONSTR_NOTNULL: + if (cxt->ispartitioned && constraint->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("not-null constraints on partitioned tables cannot be NO INHERIT")); + + cxt->nnconstraints = lappend(cxt->nnconstraints, constraint); + break; + case CONSTR_FOREIGN: if (cxt->isforeign) ereport(ERROR, @@ -949,7 +1088,6 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint) break; case CONSTR_NULL: - case CONSTR_NOTNULL: case CONSTR_DEFAULT: case CONSTR_ATTR_DEFERRABLE: case CONSTR_ATTR_NOT_DEFERRABLE: @@ -1053,14 +1191,10 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla continue; /* - * Create a new column, which is marked as NOT inherited. - * - * For constraints, ONLY the not-null constraint is inherited by the - * new column definition per SQL99. + * Create a new column definition */ def = makeColumnDef(NameStr(attribute->attname), attribute->atttypid, attribute->atttypmod, attribute->attcollation); - def->is_not_null = attribute->attnotnull; /* * Add to column list @@ -1129,14 +1263,28 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla } } + /* + * Reproduce not-null constraints, if any, by copying them. We do this + * regardless of options given. + */ + if (tupleDesc->constr && tupleDesc->constr->has_not_null) + { + List *lst; + + lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false, + true); + cxt->nnconstraints = list_concat(cxt->nnconstraints, lst); + } + /* * We cannot yet deal with defaults, CHECK constraints, indexes, or * statistics, since we don't yet know what column numbers the copied * columns will have in the finished table. If any of those options are * specified, add the LIKE clause to cxt->likeclauses so that - * expandTableLikeClause will be called after we do know that. Also, - * remember the relation OID so that expandTableLikeClause is certain to - * open the same table. + * expandTableLikeClause will be called after we do know that. + * + * In order for this to work, we remember the relation OID so that + * expandTableLikeClause is certain to open the same table. */ if (table_like_clause->options & (CREATE_TABLE_LIKE_DEFAULTS | @@ -1506,8 +1654,8 @@ transformOfType(CreateStmtContext *cxt, TypeName *ofTypename) * with the index there. * * Unlike transformIndexConstraint, we don't make any effort to force primary - * key columns to be NOT NULL. The larger cloning process this is part of - * should have cloned their NOT NULL status separately (and DefineIndex will + * key columns to be not-null. The larger cloning process this is part of + * should have cloned their not-null status separately (and DefineIndex will * complain if that fails to happen). */ IndexStmt * @@ -2066,10 +2214,10 @@ transformIndexConstraints(CreateStmtContext *cxt) ListCell *lc; /* - * Run through the constraints that need to generate an index. For PRIMARY - * KEY, mark each column as NOT NULL and create an index. For UNIQUE or - * EXCLUDE, create an index as for PRIMARY KEY, but do not insist on NOT - * NULL. + * Run through the constraints that need to generate an index, and do so. + * + * For PRIMARY KEY, this queues not-null constraints for each column, if + * needed. */ foreach(lc, cxt->ixconstraints) { @@ -2143,9 +2291,7 @@ transformIndexConstraints(CreateStmtContext *cxt) } /* - * Now append all the IndexStmts to cxt->alist. If we generated an ALTER - * TABLE SET NOT NULL statement to support a primary key, it's already in - * cxt->alist. + * Now append all the IndexStmts to cxt->alist. */ cxt->alist = list_concat(cxt->alist, finalindexlist); } @@ -2153,18 +2299,15 @@ transformIndexConstraints(CreateStmtContext *cxt) /* * transformIndexConstraint * Transform one UNIQUE, PRIMARY KEY, or EXCLUDE constraint for - * transformIndexConstraints. + * transformIndexConstraints. An IndexStmt is returned. * - * We return an IndexStmt. For a PRIMARY KEY constraint, we additionally - * produce not-null constraints, either by marking ColumnDefs in cxt->columns - * as is_not_null or by adding an ALTER TABLE SET NOT NULL command to - * cxt->alist. + * For a PRIMARY KEY constraint, we additionally create not-null constraints + * for columns that don't already have them. */ static IndexStmt * transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) { IndexStmt *index; - List *notnullcmds = NIL; ListCell *lc; index = makeNode(IndexStmt); @@ -2384,6 +2527,12 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) errdetail("Cannot create a primary key or unique constraint using such an index."), parser_errposition(cxt->pstate, constraint->location))); + /* If a PK, ensure the columns get not null constraints */ + if (constraint->contype == CONSTR_PRIMARY) + cxt->nnconstraints = + lappend(cxt->nnconstraints, + makeNotNullConstraint(makeString(attname))); + constraint->keys = lappend(constraint->keys, makeString(attname)); } else @@ -2422,7 +2571,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) * For UNIQUE and PRIMARY KEY, we just have a list of column names. * * Make sure referenced keys exist. If we are making a PRIMARY KEY index, - * also make sure they are NOT NULL. For WITHOUT OVERLAPS constraints, we + * also make sure they are not-null. For WITHOUT OVERLAPS constraints, we * make sure the last part is a range or multirange. */ else @@ -2431,7 +2580,6 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) { char *key = strVal(lfirst(lc)); bool found = false; - bool forced_not_null = false; ColumnDef *column = NULL; ListCell *columns; IndexElem *iparam; @@ -2453,24 +2601,51 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) if (found) { /* - * column is defined in the new table. For PRIMARY KEY, we - * can apply the not-null constraint cheaply here ... unless - * the column is marked is_from_type, in which case marking it - * here would be ineffective (see MergeAttributes). + * column is defined in the new table. For CREATE TABLE with + * a PRIMARY KEY, we can apply the not-null constraint cheaply + * here. If the not-null constraint already exists, we can + * (albeit not so cheaply) verify that it's not a NO INHERIT + * constraint. + * + * Note that ALTER TABLE never needs either check, because + * those constraints have already been added by + * ATPrepAddPrimaryKey. */ if (constraint->contype == CONSTR_PRIMARY && - !column->is_from_type) + !cxt->isalter) { - column->is_not_null = true; - forced_not_null = true; + if (column->is_not_null) + { + foreach_node(Constraint, nn, cxt->nnconstraints) + { + if (strcmp(strVal(linitial(nn->keys)), key) == 0) + { + if (nn->is_no_inherit) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting NO INHERIT declaration for not-null constraint on column \"%s\"", + key)); + break; + } + } + } + else + { + column->is_not_null = true; + cxt->nnconstraints = + lappend(cxt->nnconstraints, + makeNotNullConstraint(makeString(key))); + } } + else if (constraint->contype == CONSTR_PRIMARY) + Assert(column->is_not_null); } else if (SystemAttributeByName(key) != NULL) { /* * column will be a system column in the new table, so accept * it. System columns can't ever be null, so no need to worry - * about PRIMARY/not-null constraint. + * about PRIMARY/NOT NULL constraint. */ found = true; } @@ -2507,13 +2682,10 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) found = true; typid = inhattr->atttypid; - /* - * It's tempting to set forced_not_null if the - * parent column is already NOT NULL, but that - * seems unsafe because the column's NOT NULL - * marking might disappear between now and - * execution. Do the runtime check to be safe. - */ + if (constraint->contype == CONSTR_PRIMARY) + cxt->nnconstraints = + lappend(cxt->nnconstraints, + makeNotNullConstraint(makeString(pstrdup(inhname)))); break; } } @@ -2610,19 +2782,6 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) iparam->ordering = SORTBY_DEFAULT; iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; index->indexParams = lappend(index->indexParams, iparam); - - /* - * For a primary-key column, also create an item for ALTER TABLE - * SET NOT NULL if we couldn't ensure it via is_not_null above. - */ - if (constraint->contype == CONSTR_PRIMARY && !forced_not_null) - { - AlterTableCmd *notnullcmd = makeNode(AlterTableCmd); - - notnullcmd->subtype = AT_SetNotNull; - notnullcmd->name = pstrdup(key); - notnullcmds = lappend(notnullcmds, notnullcmd); - } } if (constraint->without_overlaps) @@ -2741,22 +2900,6 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) index->indexIncludingParams = lappend(index->indexIncludingParams, iparam); } - /* - * If we found anything that requires run-time SET NOT NULL, build a full - * ALTER TABLE command for that and add it to cxt->alist. - */ - if (notnullcmds) - { - AlterTableStmt *alterstmt = makeNode(AlterTableStmt); - - alterstmt->relation = copyObject(cxt->relation); - alterstmt->cmds = notnullcmds; - alterstmt->objtype = OBJECT_TABLE; - alterstmt->missing_ok = false; - - cxt->alist = lappend(cxt->alist, alterstmt); - } - return index; } @@ -3395,6 +3538,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, cxt.isalter = true; cxt.columns = NIL; cxt.ckconstraints = NIL; + cxt.nnconstraints = NIL; cxt.fkconstraints = NIL; cxt.ixconstraints = NIL; cxt.likeclauses = NIL; @@ -3644,9 +3788,8 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, Node *istmt = (Node *) lfirst(l); /* - * We assume here that cxt.alist contains only IndexStmts and possibly - * ALTER TABLE SET NOT NULL statements generated from primary key - * constraints. We absorb the subcommands of the latter directly. + * We assume here that cxt.alist contains only IndexStmts generated + * from primary key constraints. */ if (IsA(istmt, IndexStmt)) { @@ -3658,30 +3801,31 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, newcmd->def = (Node *) idxstmt; newcmds = lappend(newcmds, newcmd); } - else if (IsA(istmt, AlterTableStmt)) - { - AlterTableStmt *alterstmt = (AlterTableStmt *) istmt; - - newcmds = list_concat(newcmds, alterstmt->cmds); - } else elog(ERROR, "unexpected stmt type %d", (int) nodeTag(istmt)); } cxt.alist = NIL; - /* Append any CHECK or FK constraints to the commands list */ - foreach(l, cxt.ckconstraints) + /* Append any CHECK, NOT NULL or FK constraints to the commands list */ + foreach_node(Constraint, def, cxt.ckconstraints) { newcmd = makeNode(AlterTableCmd); newcmd->subtype = AT_AddConstraint; - newcmd->def = (Node *) lfirst_node(Constraint, l); + newcmd->def = (Node *) def; newcmds = lappend(newcmds, newcmd); } - foreach(l, cxt.fkconstraints) + foreach_node(Constraint, def, cxt.nnconstraints) { newcmd = makeNode(AlterTableCmd); newcmd->subtype = AT_AddConstraint; - newcmd->def = (Node *) lfirst_node(Constraint, l); + newcmd->def = (Node *) def; + newcmds = lappend(newcmds, newcmd); + } + foreach_node(Constraint, def, cxt.fkconstraints) + { + newcmd = makeNode(AlterTableCmd); + newcmd->subtype = AT_AddConstraint; + newcmd->def = (Node *) def; newcmds = lappend(newcmds, newcmd); } diff --git a/src/backend/replication/logical/relation.c b/src/backend/replication/logical/relation.c index f139e7b01e..f5a0ef2bd9 100644 --- a/src/backend/replication/logical/relation.c +++ b/src/backend/replication/logical/relation.c @@ -855,7 +855,7 @@ GetRelationIdentityOrPK(Relation rel) idxoid = RelationGetReplicaIndex(rel); if (!OidIsValid(idxoid)) - idxoid = RelationGetPrimaryKeyIndex(rel); + idxoid = RelationGetPrimaryKeyIndex(rel, false); return idxoid; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 2177d17e27..a39068d1bf 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2516,6 +2516,28 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, conForm->connoinherit ? " NO INHERIT" : ""); break; } + case CONSTRAINT_NOTNULL: + { + if (conForm->conrelid) + { + AttrNumber attnum; + + attnum = extractNotNullColumn(tup); + + appendStringInfo(&buf, "NOT NULL %s", + quote_identifier(get_attname(conForm->conrelid, + attnum, false))); + if (((Form_pg_constraint) GETSTRUCT(tup))->connoinherit) + appendStringInfoString(&buf, " NO INHERIT"); + } + else if (conForm->contypid) + { + /* conkey is null for domain not-null constraints */ + appendStringInfoString(&buf, "NOT NULL"); + } + break; + } + case CONSTRAINT_TRIGGER: /* diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 5bbb654a5d..342467fd18 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -4817,18 +4817,38 @@ RelationGetIndexList(Relation relation) result = lappend_oid(result, index->indexrelid); /* - * Invalid, non-unique, non-immediate or predicate indexes aren't - * interesting for either oid indexes or replication identity indexes, - * so don't check them. + * Non-unique or predicate indexes aren't interesting for either oid + * indexes or replication identity indexes, so don't check them. + * Deferred ones are not useful for replication identity either; but + * we do include them if they are PKs. */ - if (!index->indisvalid || !index->indisunique || - !index->indimmediate || + if (!index->indisunique || !heap_attisnull(htup, Anum_pg_index_indpred, NULL)) continue; - /* remember primary key index if any */ - if (index->indisprimary) + /* + * Remember primary key index, if any. For regular tables we do this + * only if the index is valid; but for partitioned tables, then we do + * it even if it's invalid. + * + * The reason for returning invalid primary keys for partitioned + * tables is that we need it to prevent drop of not-null constraints + * that may underlie such a primary key, which is only a problem for + * partitioned tables. + */ + if (index->indisprimary && + (index->indisvalid || + relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)) + { pkeyIndex = index->indexrelid; + pkdeferrable = !index->indimmediate; + } + + if (!index->indimmediate) + continue; + + if (!index->indisvalid) + continue; /* remember explicitly chosen replica index */ if (index->indisreplident) @@ -4952,10 +4972,10 @@ RelationGetStatExtList(Relation relation) * RelationGetPrimaryKeyIndex -- get OID of the relation's primary key index * * Returns InvalidOid if there is no such index, or if the primary key is - * DEFERRABLE. + * DEFERRABLE and the caller isn't OK with that. */ Oid -RelationGetPrimaryKeyIndex(Relation relation) +RelationGetPrimaryKeyIndex(Relation relation, bool deferrable_ok) { List *ilist; @@ -4967,7 +4987,11 @@ RelationGetPrimaryKeyIndex(Relation relation) Assert(relation->rd_indexvalid); } - return relation->rd_ispkdeferrable ? InvalidOid : relation->rd_pkindex; + if (deferrable_ok) + return relation->rd_pkindex; + else if (relation->rd_ispkdeferrable) + return InvalidOid; + return relation->rd_pkindex; } /* diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index 9b2d34e281..33d323085f 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -83,7 +83,8 @@ static catalogid_hash *catalogIdHash = NULL; static void flagInhTables(Archive *fout, TableInfo *tblinfo, int numTables, InhInfo *inhinfo, int numInherits); static void flagInhIndexes(Archive *fout, TableInfo *tblinfo, int numTables); -static void flagInhAttrs(Archive *fout, TableInfo *tblinfo, int numTables); +static void flagInhAttrs(Archive *fout, DumpOptions *dopt, TableInfo *tblinfo, + int numTables); static int strInArray(const char *pattern, char **arr, int arr_size); static IndxInfo *findIndexByOid(Oid oid); @@ -204,7 +205,7 @@ getSchemaData(Archive *fout, int *numTablesPtr) getTableAttrs(fout, tblinfo, numTables); pg_log_info("flagging inherited columns in subtables"); - flagInhAttrs(fout, tblinfo, numTables); + flagInhAttrs(fout, fout->dopt, tblinfo, numTables); pg_log_info("reading partitioning data"); getPartitioningInfo(fout); @@ -452,7 +453,8 @@ flagInhIndexes(Archive *fout, TableInfo tblinfo[], int numTables) * What we need to do here is: * * - Detect child columns that inherit NOT NULL bits from their parents, so - * that we needn't specify that again for the child. + * that we needn't specify that again for the child. (Versions >= 18 no + * longer need this.) * * - Detect child columns that have DEFAULT NULL when their parents had some * non-null default. In this case, we make up a dummy AttrDefInfo object so @@ -472,9 +474,8 @@ flagInhIndexes(Archive *fout, TableInfo tblinfo[], int numTables) * modifies tblinfo */ static void -flagInhAttrs(Archive *fout, TableInfo *tblinfo, int numTables) +flagInhAttrs(Archive *fout, DumpOptions *dopt, TableInfo *tblinfo, int numTables) { - DumpOptions *dopt = fout->dopt; int i, j, k; @@ -536,7 +537,15 @@ flagInhAttrs(Archive *fout, TableInfo *tblinfo, int numTables) { AttrDefInfo *parentDef = parent->attrdefs[inhAttrInd]; - foundNotNull |= parent->notnull[inhAttrInd]; + /* + * Account for each parent having a not-null constraint. + * In versions 18 and later, we don't need this (and those + * didn't have NO INHERIT.) + */ + if (fout->remoteVersion < 180000 && + parent->notnull_constrs[inhAttrInd] != NULL) + foundNotNull = true; + foundDefault |= (parentDef != NULL && strcmp(parentDef->adef_expr, "NULL") != 0 && !parent->attgenerated[inhAttrInd]); @@ -554,8 +563,13 @@ flagInhAttrs(Archive *fout, TableInfo *tblinfo, int numTables) } } - /* Remember if we found inherited NOT NULL */ - tbinfo->inhNotNull[j] = foundNotNull; + /* + * In versions < 18, for lack of a better system, we arbitrarily + * decide that a not-null constraint is not locally defined if at + * least one of the parents has it. + */ + if (fout->remoteVersion < 180000 && foundNotNull) + tbinfo->notnull_islocal[j] = false; /* * Manufacture a DEFAULT NULL clause if necessary. This breaks diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index b7b822da62..a8c141b689 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -343,6 +343,10 @@ static void getTableData(DumpOptions *dopt, TableInfo *tblinfo, int numTables, c static void makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo); static void buildMatViewRefreshDependencies(Archive *fout); static void getTableDataFKConstraints(void); +static void determineNotNullFlags(Archive *fout, PGresult *res, int r, + TableInfo *tbinfo, int j, + int i_notnull_name, int i_notnull_noinherit, + int i_notnull_islocal); static char *format_function_arguments(const FuncInfo *finfo, const char *funcargs, bool is_agg); static char *format_function_signature(Archive *fout, @@ -8751,7 +8755,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) int i_attlen; int i_attalign; int i_attislocal; - int i_attnotnull; + int i_notnull_name; + int i_notnull_noinherit; + int i_notnull_islocal; int i_attoptions; int i_attcollation; int i_attcompression; @@ -8761,13 +8767,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) /* * We want to perform just one query against pg_attribute, and then just - * one against pg_attrdef (for DEFAULTs) and one against pg_constraint - * (for CHECK constraints). However, we mustn't try to select every row - * of those catalogs and then sort it out on the client side, because some - * of the server-side functions we need would be unsafe to apply to tables - * we don't have lock on. Hence, we build an array of the OIDs of tables - * we care about (and now have lock on!), and use a WHERE clause to - * constrain which rows are selected. + * one against pg_attrdef (for DEFAULTs) and two against pg_constraint + * (for CHECK constraints and for NOT NULL constraints). However, we + * mustn't try to select every row of those catalogs and then sort it out + * on the client side, because some of the server-side functions we need + * would be unsafe to apply to tables we don't have lock on. Hence, we + * build an array of the OIDs of tables we care about (and now have lock + * on!), and use a WHERE clause to constrain which rows are selected. */ appendPQExpBufferChar(tbloids, '{'); appendPQExpBufferChar(checkoids, '{'); @@ -8814,7 +8820,6 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) "a.attstattarget,\n" "a.attstorage,\n" "t.typstorage,\n" - "a.attnotnull,\n" "a.atthasdef,\n" "a.attisdropped,\n" "a.attlen,\n" @@ -8831,6 +8836,30 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) "ORDER BY option_name" "), E',\n ') AS attfdwoptions,\n"); + /* + * Find out any NOT NULL markings for each column. In 18 and up we read + * pg_constraint to obtain the constraint name. notnull_noinherit is set + * according to the NO INHERIT property. For versions prior to 18, we + * store an empty string as the name when a constraint is marked as + * attnotnull (this cues dumpTableSchema to print the NOT NULL clause + * without a name); also, such cases are never NO INHERIT. + * + * We track in notnull_islocal whether the constraint was defined directly + * in this table or via an ancestor, for binary upgrade. flagInhAttrs + * might modify this later for servers older than 18; it's also in charge + * of determining the correct inhcount. + */ + if (fout->remoteVersion >= 180000) + appendPQExpBufferStr(q, + "co.conname AS notnull_name,\n" + "co.connoinherit AS notnull_noinherit,\n" + "co.conislocal AS notnull_islocal,\n"); + else + appendPQExpBufferStr(q, + "CASE WHEN a.attnotnull THEN '' ELSE NULL END AS notnull_name,\n" + "false AS notnull_noinherit,\n" + "a.attislocal AS notnull_islocal,\n"); + if (fout->remoteVersion >= 140000) appendPQExpBufferStr(q, "a.attcompression AS attcompression,\n"); @@ -8865,11 +8894,25 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n" "JOIN pg_catalog.pg_attribute a ON (src.tbloid = a.attrelid) " "LEFT JOIN pg_catalog.pg_type t " - "ON (a.atttypid = t.oid)\n" - "WHERE a.attnum > 0::pg_catalog.int2\n" - "ORDER BY a.attrelid, a.attnum", + "ON (a.atttypid = t.oid)\n", tbloids->data); + /* + * In versions 18 and up, we need pg_constraint for explicit NOT NULL + * entries. Also, we need to know if the NOT NULL for each column is + * backing a primary key. + */ + if (fout->remoteVersion >= 180000) + appendPQExpBufferStr(q, + " LEFT JOIN pg_catalog.pg_constraint co ON " + "(a.attrelid = co.conrelid\n" + " AND co.contype = 'n' AND " + "co.conkey = array[a.attnum])\n"); + + appendPQExpBufferStr(q, + "WHERE a.attnum > 0::pg_catalog.int2\n" + "ORDER BY a.attrelid, a.attnum"); + res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK); ntups = PQntuples(res); @@ -8887,7 +8930,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) i_attlen = PQfnumber(res, "attlen"); i_attalign = PQfnumber(res, "attalign"); i_attislocal = PQfnumber(res, "attislocal"); - i_attnotnull = PQfnumber(res, "attnotnull"); + i_notnull_name = PQfnumber(res, "notnull_name"); + i_notnull_noinherit = PQfnumber(res, "notnull_noinherit"); + i_notnull_islocal = PQfnumber(res, "notnull_islocal"); i_attoptions = PQfnumber(res, "attoptions"); i_attcollation = PQfnumber(res, "attcollation"); i_attcompression = PQfnumber(res, "attcompression"); @@ -8952,8 +8997,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) tbinfo->attcompression = (char *) pg_malloc(numatts * sizeof(char)); tbinfo->attfdwoptions = (char **) pg_malloc(numatts * sizeof(char *)); tbinfo->attmissingval = (char **) pg_malloc(numatts * sizeof(char *)); - tbinfo->notnull = (bool *) pg_malloc(numatts * sizeof(bool)); - tbinfo->inhNotNull = (bool *) pg_malloc(numatts * sizeof(bool)); + tbinfo->notnull_constrs = (char **) pg_malloc(numatts * sizeof(char *)); + tbinfo->notnull_noinh = (bool *) pg_malloc(numatts * sizeof(bool)); + tbinfo->notnull_islocal = (bool *) pg_malloc(numatts * sizeof(bool)); tbinfo->attrdefs = (AttrDefInfo **) pg_malloc(numatts * sizeof(AttrDefInfo *)); hasdefaults = false; @@ -8977,7 +9023,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen)); tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign)); tbinfo->attislocal[j] = (PQgetvalue(res, r, i_attislocal)[0] == 't'); - tbinfo->notnull[j] = (PQgetvalue(res, r, i_attnotnull)[0] == 't'); + + /* Handle not-null constraint name and flags */ + determineNotNullFlags(fout, res, r, + tbinfo, j, + i_notnull_name, i_notnull_noinherit, + i_notnull_islocal); + tbinfo->attoptions[j] = pg_strdup(PQgetvalue(res, r, i_attoptions)); tbinfo->attcollation[j] = atooid(PQgetvalue(res, r, i_attcollation)); tbinfo->attcompression[j] = *(PQgetvalue(res, r, i_attcompression)); @@ -8986,8 +9038,6 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) tbinfo->attrdefs[j] = NULL; /* fix below */ if (PQgetvalue(res, r, i_atthasdef)[0] == 't') hasdefaults = true; - /* these flags will be set in flagInhAttrs() */ - tbinfo->inhNotNull[j] = false; } if (hasdefaults) @@ -9268,6 +9318,110 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) destroyPQExpBuffer(checkoids); } +/* + * Based on the getTableAttrs query's row corresponding to one column, set + * the name and flags to handle a not-null constraint for that column in + * the tbinfo struct. + * + * Result row 'r' is for tbinfo's attribute 'j'. + * + * There are three possibilities: + * 1) the column has no not-null constraints. In that case, ->notnull_constrs + * (the constraint name) remains NULL. + * 2) The column has a constraint with no name (this is the case when + * constraints come from pre-18 servers). In this case, ->notnull_constrs + * is set to the empty string; dumpTableSchema will print just "NOT NULL". + * 3) The column has a constraint with a known name; in that case + * notnull_constrs carries that name and dumpTableSchema will print + * "CONSTRAINT the_name NOT NULL". However, if the name is the default + * (table_column_not_null), there's no need to print that name in the dump, + * so notnull_constrs is set to the empty string and it behaves as the case + * above. + * + * In a child table that inherits from a parent already containing NOT NULL + * constraints and the columns in the child don't have their own NOT NULL + * declarations, we suppress printing constraints in the child: the + * constraints are acquired at the point where the child is attached to the + * parent. This is tracked in ->notnull_inh (which is set in flagInhAttrs for + * servers pre-18). + * + * Any of these constraints might have the NO INHERIT bit. If so we set + * ->notnull_noinh and NO INHERIT will be printed by dumpTableSchema. + * + * In case 3 above, the name comparison is a bit of a hack; it actually fails + * to do the right thing in all but the trivial case. However, the downside + * of getting it wrong is simply that the name is printed rather than + * suppressed, so it's not a big deal. + */ +static void +determineNotNullFlags(Archive *fout, PGresult *res, int r, + TableInfo *tbinfo, int j, + int i_notnull_name, int i_notnull_noinherit, + int i_notnull_islocal) +{ + DumpOptions *dopt = fout->dopt; + + /* + * notnull_noinh is straight from the query result. notnull_islocal also, + * though flagInhAttrs may change that one later in versions < 18. + */ + tbinfo->notnull_noinh[j] = PQgetvalue(res, r, i_notnull_noinherit)[0] == 't'; + tbinfo->notnull_islocal[j] = PQgetvalue(res, r, i_notnull_islocal)[0] == 't'; + + /* + * Determine a constraint name to use. If the column is not marked not- + * null, we set NULL which cues ... to do nothing. An empty string says + * to print an unnamed NOT NULL, and anything else is a constraint name to + * use. + */ + if (fout->remoteVersion < 180000) + { + /* + * < 18 doesn't have not-null names, so an unnamed constraint is + * sufficient. + */ + if (PQgetisnull(res, r, i_notnull_name)) + tbinfo->notnull_constrs[j] = NULL; + else + tbinfo->notnull_constrs[j] = ""; + } + else + { + if (PQgetisnull(res, r, i_notnull_name)) + tbinfo->notnull_constrs[j] = NULL; + else + { + /* + * In binary upgrade of inheritance child tables, must have a + * constraint name that we can UPDATE later. + */ + if (dopt->binary_upgrade && + !tbinfo->ispartition && + !tbinfo->notnull_islocal) + { + tbinfo->notnull_constrs[j] = + pstrdup(PQgetvalue(res, r, i_notnull_name)); + } + else + { + char *default_name; + + /* XXX should match ChooseConstraintName better */ + default_name = psprintf("%s_%s_not_null", tbinfo->dobj.name, + tbinfo->attnames[j]); + if (strcmp(default_name, + PQgetvalue(res, r, i_notnull_name)) == 0) + tbinfo->notnull_constrs[j] = ""; + else + { + tbinfo->notnull_constrs[j] = + pstrdup(PQgetvalue(res, r, i_notnull_name)); + } + } + } + } +} + /* * Test whether a column should be printed as part of table's CREATE TABLE. * Column number is zero-based. @@ -15970,13 +16124,14 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) !tbinfo->attrdefs[j]->separate); /* - * Not Null constraint --- suppress if inherited, except - * if partition, or in binary-upgrade case where that - * won't work. + * Not Null constraint --- print it if it is locally + * defined, or if binary upgrade. (In the latter case, we + * reset conislocal below.) */ - print_notnull = (tbinfo->notnull[j] && - (!tbinfo->inhNotNull[j] || - tbinfo->ispartition || dopt->binary_upgrade)); + print_notnull = (tbinfo->notnull_constrs[j] != NULL && + (tbinfo->notnull_islocal[j] || + dopt->binary_upgrade || + tbinfo->ispartition)); /* * Skip column if fully defined by reloftype, except in @@ -16032,9 +16187,22 @@ 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) - appendPQExpBufferStr(q, " NOT NULL"); + { + if (tbinfo->notnull_constrs[j][0] == '\0') + appendPQExpBufferStr(q, " NOT NULL"); + else + appendPQExpBuffer(q, " CONSTRAINT %s NOT NULL", + fmtId(tbinfo->notnull_constrs[j])); + + if (tbinfo->notnull_noinh[j]) + appendPQExpBufferStr(q, " NO INHERIT"); + } /* Add collation if not default for the type */ if (OidIsValid(tbinfo->attcollation[j])) @@ -16212,6 +16380,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) tbinfo->relkind == RELKIND_PARTITIONED_TABLE)) { bool firstitem; + bool firstitem_extra; /* * Drop any dropped columns. Merge the pg_attribute manipulations @@ -16289,6 +16458,71 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) if (!firstitem) appendPQExpBufferStr(q, ");\n"); + /* + * Fix up not-null constraints that come from inheritance. As + * above, do the pg_constraint manipulations in a single SQL + * command. (Actually, two in special cases, if we're doing an + * upgrade from < 18). + */ + firstitem = true; + firstitem_extra = true; + resetPQExpBuffer(extra); + for (j = 0; j < tbinfo->numatts; j++) + { + /* + * If a not-null constraint comes from inheritance, reset + * conislocal. The inhcount is fixed by ALTER TABLE INHERIT, + * below. Special hack: in versions < 18, columns with no + * local definition need their constraint to be matched by + * column number in conkeys instead of by contraint name, + * because the latter is not available. (We distinguish the + * case because the constraint name is the empty string.) + */ + if (tbinfo->notnull_constrs[j] != NULL && + !tbinfo->notnull_islocal[j]) + { + if (tbinfo->notnull_constrs[j][0] != '\0') + { + if (firstitem) + { + appendPQExpBufferStr(q, "UPDATE pg_catalog.pg_constraint\n" + "SET conislocal = false\n" + "WHERE contype = 'n' AND conrelid = "); + appendStringLiteralAH(q, qualrelname, fout); + appendPQExpBufferStr(q, "::pg_catalog.regclass AND\n" + "conname IN ("); + firstitem = false; + } + else + appendPQExpBufferStr(q, ", "); + appendStringLiteralAH(q, tbinfo->notnull_constrs[j], fout); + } + else + { + if (firstitem_extra) + { + appendPQExpBufferStr(extra, "UPDATE pg_catalog.pg_constraint\n" + "SET conislocal = false\n" + "WHERE contype = 'n' AND conrelid = "); + appendStringLiteralAH(extra, qualrelname, fout); + appendPQExpBufferStr(extra, "::pg_catalog.regclass AND\n" + "conkey IN ("); + firstitem_extra = false; + } + else + appendPQExpBufferStr(extra, ", "); + appendPQExpBuffer(extra, "'{%d}'", j + 1); + } + } + } + if (!firstitem) + appendPQExpBufferStr(q, ");\n"); + if (!firstitem_extra) + appendPQExpBufferStr(extra, ");\n"); + + if (extra->len > 0) + appendBinaryPQExpBuffer(q, extra->data, extra->len); + /* * Add inherited CHECK constraints, if any. * @@ -16428,11 +16662,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) * we have to mark it separately. */ if (!shouldPrintColumn(dopt, tbinfo, j) && - tbinfo->notnull[j] && !tbinfo->inhNotNull[j]) - appendPQExpBuffer(q, - "ALTER %sTABLE ONLY %s ALTER COLUMN %s SET NOT NULL;\n", - foreign, qualrelname, - fmtId(tbinfo->attnames[j])); + tbinfo->notnull_constrs[j] != NULL && + (tbinfo->notnull_islocal[j] && !tbinfo->ispartition && !dopt->binary_upgrade)) + { + /* No constraint name desired? */ + if (tbinfo->notnull_constrs[j][0] == '\0') + appendPQExpBuffer(q, + "ALTER %sTABLE ONLY %s ALTER COLUMN %s SET NOT NULL;\n", + foreign, qualrelname, + fmtId(tbinfo->attnames[j])); + else + appendPQExpBuffer(q, + "ALTER %sTABLE ONLY %s ADD CONSTRAINT %s NOT NULL %s;\n", + foreign, qualrelname, + tbinfo->notnull_constrs[j], + fmtId(tbinfo->attnames[j])); + } /* * Dump per-column statistics information. We only issue an ALTER diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index c1552ead45..d65f558565 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -347,8 +347,12 @@ typedef struct _tableInfo char *attcompression; /* per-attribute compression method */ char **attfdwoptions; /* per-attribute fdw options */ char **attmissingval; /* per attribute missing value */ - bool *notnull; /* not-null constraints on attributes */ - bool *inhNotNull; /* true if NOT NULL is inherited */ + char **notnull_constrs; /* NOT NULL constraint names. If null, + * there isn't one on this column. If + * empty string, unnamed constraint + * (pre-v17) */ + bool *notnull_noinh; /* NOT NULL is NO INHERIT */ + bool *notnull_islocal; /* true if NOT NULL has local definition */ struct _attrDefInfo **attrdefs; /* DEFAULT expressions */ struct _constraintInfo *checkexprs; /* CHECK constraints */ bool needs_override; /* has GENERATED ALWAYS AS IDENTITY */ diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 213904440f..aa1564cd45 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -3350,8 +3350,8 @@ my %tests = ( FOR VALUES FROM (\'2006-02-01\') TO (\'2006-03-01\');', regexp => qr/^ \QCREATE TABLE dump_test_second_schema.measurement_y2006m2 (\E\n - \s+\Qcity_id integer DEFAULT nextval('dump_test.measurement_city_id_seq'::regclass) NOT NULL,\E\n - \s+\Qlogdate date NOT NULL,\E\n + \s+\Qcity_id integer DEFAULT nextval('dump_test.measurement_city_id_seq'::regclass) CONSTRAINT measurement_city_id_not_null NOT NULL,\E\n + \s+\Qlogdate date CONSTRAINT measurement_logdate_not_null NOT NULL,\E\n \s+\Qpeaktemp integer,\E\n \s+\Qunitsales integer DEFAULT 0,\E\n \s+\QCONSTRAINT measurement_peaktemp_check CHECK ((peaktemp >= '-460'::integer)),\E\n @@ -3759,7 +3759,7 @@ my %tests = ( ) INHERITS (dump_test.test_inheritance_parent);', regexp => qr/^ \QCREATE TABLE dump_test.test_inheritance_child (\E\n - \s+\Qcol1 integer,\E\n + \s+\Qcol1 integer NOT NULL,\E\n \s+\QCONSTRAINT test_inheritance_child CHECK ((col2 >= 142857))\E\n \)\n \QINHERITS (dump_test.test_inheritance_parent);\E\n diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index bbe632cc79..5bfebad64d 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -3053,6 +3053,50 @@ describeOneTableDetails(const char *schemaname, } PQclear(result); } + + /* + * If verbose, print NOT NULL constraints. + */ + if (verbose) + { + printfPQExpBuffer(&buf, + "SELECT c.conname, a.attname, c.connoinherit,\n" + " c.conislocal, c.coninhcount <> 0\n" + "FROM pg_catalog.pg_constraint c JOIN\n" + " pg_catalog.pg_attribute a ON\n" + " (a.attrelid = c.conrelid AND a.attnum = c.conkey[1])\n" + "WHERE c.contype = 'n' AND\n" + " c.conrelid = '%s'::pg_catalog.regclass\n" + "ORDER BY a.attnum", + oid); + + result = PSQLexec(buf.data); + if (!result) + goto error_return; + else + tuples = PQntuples(result); + + if (tuples > 0) + printTableAddFooter(&cont, _("Not-null constraints:")); + + /* Might be an empty set - that's ok */ + for (i = 0; i < tuples; i++) + { + bool islocal = PQgetvalue(result, i, 3)[0] == 't'; + bool inherited = PQgetvalue(result, i, 4)[0] == 't'; + + printfPQExpBuffer(&buf, " \"%s\" NOT NULL \"%s\"%s", + PQgetvalue(result, i, 0), + PQgetvalue(result, i, 1), + PQgetvalue(result, i, 2)[0] == 't' ? + " NO INHERIT" : + islocal && inherited ? _(" (local, inherited)") : + inherited ? _(" (inherited)") : ""); + + printTableAddFooter(&cont, buf.data); + } + PQclear(result); + } } /* Get view_def if table is a view or materialized view */ diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl index fd645896c9..f3e6c8beaa 100644 --- a/src/bin/psql/t/010_tab_completion.pl +++ b/src/bin/psql/t/010_tab_completion.pl @@ -39,7 +39,7 @@ $node->start; # set up a few database objects $node->safe_psql('postgres', - "CREATE TABLE tab1 (c1 int primary key, c2 text);\n" + "CREATE TABLE tab1 (c1 int primary key constraint foo not null, c2 text);\n" . "CREATE TABLE mytab123 (f1 int, f2 text);\n" . "CREATE TABLE mytab246 (f1 int, f2 text);\n" . "CREATE TABLE \"mixedName\" (f1 int, f2 text);\n" @@ -209,21 +209,21 @@ clear_query(); # check interpretation of referenced names check_completion( - "alter table tab1 drop constraint \t", + "alter table tab1 drop constraint t\t", qr/tab1_pkey /, "complete index name for referenced table"); clear_query(); check_completion( - "alter table TAB1 drop constraint \t", + "alter table TAB1 drop constraint t\t", qr/tab1_pkey /, "complete index name for referenced table, with downcasing"); clear_query(); check_completion( - "alter table public.\"tab1\" drop constraint \t", + "alter table public.\"tab1\" drop constraint t\t", qr/tab1_pkey /, "complete index name for referenced table, with schema and quoting"); diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 2abc523f5c..86436e0356 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202411071 +#define CATALOG_VERSION_NO 202411081 #endif diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h index d6a2c79129..8c278f202b 100644 --- a/src/include/catalog/heap.h +++ b/src/include/catalog/heap.h @@ -34,10 +34,11 @@ typedef struct RawColumnDefault typedef struct CookedConstraint { - ConstrType contype; /* CONSTR_DEFAULT or CONSTR_CHECK */ + ConstrType contype; /* CONSTR_DEFAULT, CONSTR_CHECK, + * CONSTR_NOTNULL */ Oid conoid; /* constr OID if created, otherwise Invalid */ char *name; /* name, or NULL if none */ - AttrNumber attnum; /* which attr (only for DEFAULT) */ + AttrNumber attnum; /* which attr (only for NOTNULL, DEFAULT) */ Node *expr; /* transformed default or check expr */ bool skip_validation; /* skip validation? (only for CHECK) */ bool is_local; /* constraint has local (non-inherited) def */ @@ -113,6 +114,9 @@ extern List *AddRelationNewConstraints(Relation rel, bool is_local, bool is_internal, const char *queryString); +extern List *AddRelationNotNullConstraints(Relation rel, + List *constraints, + List *old_notnulls); extern void RelationClearMissing(Relation rel); extern void SetAttrMissing(Oid relid, char *attname, char *value); diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h index 35788315bc..4b4476738a 100644 --- a/src/include/catalog/pg_constraint.h +++ b/src/include/catalog/pg_constraint.h @@ -257,7 +257,14 @@ extern char *ChooseConstraintName(const char *name1, const char *name2, const char *label, Oid namespaceid, List *others); +extern HeapTuple findNotNullConstraintAttnum(Oid relid, AttrNumber attnum); +extern HeapTuple findNotNullConstraint(Oid relid, const char *colname); extern HeapTuple findDomainNotNullConstraint(Oid typid); +extern AttrNumber extractNotNullColumn(HeapTuple constrTup); +extern bool AdjustNotNullInheritance(Oid relid, AttrNumber attnum, + bool is_local, bool is_no_inherit); +extern List *RelationGetNotNullConstraints(Oid relid, bool cooked, + bool include_noinh); extern void RemoveConstraintById(Oid conId); extern void RenameConstraintById(Oid conId, const char *newname); diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 0765e5c57b..028f8815d1 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -68,6 +68,7 @@ extern RelabelType *makeRelabelType(Expr *arg, Oid rtype, int32 rtypmod, Oid rcollid, CoercionForm rformat); extern RangeVar *makeRangeVar(char *schemaname, char *relname, int location); +extern Constraint *makeNotNullConstraint(String *colname); extern TypeName *makeTypeName(char *typnam); extern TypeName *makeTypeNameFromNameList(List *names); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 0d96db5638..0f9462493e 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2365,7 +2365,6 @@ typedef enum AlterTableType AT_SetNotNull, /* alter column set not null */ AT_SetExpression, /* alter column set expression */ AT_DropExpression, /* alter column drop expression */ - AT_CheckNotNull, /* check column is already marked not null */ AT_SetStatistics, /* alter column set statistics */ AT_SetOptions, /* alter column set ( options ) */ AT_ResetOptions, /* alter column reset ( options ) */ @@ -2663,10 +2662,10 @@ typedef struct VariableShowStmt * Create Table Statement * * NOTE: in the raw gram.y output, ColumnDef and Constraint nodes are - * intermixed in tableElts, and constraints is NIL. After parse analysis, - * tableElts contains just ColumnDefs, and constraints contains just - * Constraint nodes (in fact, only CONSTR_CHECK nodes, in the present - * implementation). + * intermixed in tableElts, and constraints and nnconstraints are NIL. After + * parse analysis, tableElts contains just ColumnDefs, nnconstraints contains + * Constraint nodes of CONSTR_NOTNULL type from various sources, and + * constraints contains just CONSTR_CHECK Constraint nodes. * ---------------------- */ @@ -2681,6 +2680,7 @@ typedef struct CreateStmt PartitionSpec *partspec; /* PARTITION BY clause */ TypeName *ofTypename; /* OF typename */ List *constraints; /* constraints (list of Constraint nodes) */ + List *nnconstraints; /* NOT NULL constraints (ditto) */ List *options; /* options from WITH clause */ OnCommitAction oncommit; /* what do we do at COMMIT? */ char *tablespacename; /* table space to use, or NULL */ diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h index 18c32ea700..8d23959e95 100644 --- a/src/include/utils/relcache.h +++ b/src/include/utils/relcache.h @@ -46,7 +46,7 @@ extern void RelationClose(Relation relation); extern List *RelationGetFKeyList(Relation relation); extern List *RelationGetIndexList(Relation relation); extern List *RelationGetStatExtList(Relation relation); -extern Oid RelationGetPrimaryKeyIndex(Relation relation); +extern Oid RelationGetPrimaryKeyIndex(Relation relation, bool deferrable_ok); extern Oid RelationGetReplicaIndex(Relation relation); extern List *RelationGetIndexExpressions(Relation relation); extern List *RelationGetDummyIndexExpressions(Relation relation); diff --git a/src/test/modules/test_ddl_deparse/expected/alter_table.out b/src/test/modules/test_ddl_deparse/expected/alter_table.out index 6daa186a84..50d0354a34 100644 --- a/src/test/modules/test_ddl_deparse/expected/alter_table.out +++ b/src/test/modules/test_ddl_deparse/expected/alter_table.out @@ -28,6 +28,7 @@ ALTER TABLE parent ADD COLUMN b serial; NOTICE: DDL test: type simple, tag CREATE SEQUENCE NOTICE: DDL test: type alter table, tag ALTER TABLE NOTICE: subcommand: type ADD COLUMN (and recurse) desc column b of table parent +NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint parent_b_not_null on table parent NOTICE: DDL test: type simple, tag ALTER SEQUENCE ALTER TABLE parent RENAME COLUMN b TO c; NOTICE: DDL test: type simple, tag ALTER TABLE @@ -57,24 +58,17 @@ NOTICE: subcommand: type DETACH PARTITION desc table part2 DROP TABLE part2; ALTER TABLE part ADD PRIMARY KEY (a); NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type SET NOT NULL desc column a of table part -NOTICE: subcommand: type SET NOT NULL desc column a of table part1 +NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint part_a_not_null on table part NOTICE: subcommand: type ADD INDEX desc index part_pkey ALTER TABLE parent ALTER COLUMN a SET NOT NULL; NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type SET NOT NULL desc column a of table parent -NOTICE: subcommand: type SET NOT NULL desc column a of table child -NOTICE: subcommand: type SET NOT NULL desc column a of table grandchild +NOTICE: subcommand: type SET NOT NULL (and recurse) desc constraint parent_a_not_null on table parent ALTER TABLE parent ALTER COLUMN a DROP NOT NULL; NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type DROP NOT NULL desc column a of table parent -NOTICE: subcommand: type DROP NOT NULL desc column a of table child -NOTICE: subcommand: type DROP NOT NULL desc column a of table grandchild +NOTICE: subcommand: type DROP NOT NULL (and recurse) desc column a of table parent ALTER TABLE parent ALTER COLUMN a SET NOT NULL; NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type SET NOT NULL desc column a of table parent -NOTICE: subcommand: type SET NOT NULL desc column a of table child -NOTICE: subcommand: type SET NOT NULL desc column a of table grandchild +NOTICE: subcommand: type SET NOT NULL (and recurse) desc constraint parent_a_not_null on table parent ALTER TABLE parent ALTER COLUMN a ADD GENERATED ALWAYS AS IDENTITY; NOTICE: DDL test: type simple, tag CREATE SEQUENCE NOTICE: DDL test: type simple, tag ALTER SEQUENCE @@ -116,6 +110,7 @@ NOTICE: DDL test: type alter table, tag ALTER TABLE NOTICE: subcommand: type ALTER COLUMN SET TYPE desc column c of table parent NOTICE: subcommand: type ALTER COLUMN SET TYPE desc column c of table child NOTICE: subcommand: type ALTER COLUMN SET TYPE desc column c of table grandchild +NOTICE: subcommand: type (re) ADD CONSTRAINT desc constraint parent_b_not_null on table parent NOTICE: subcommand: type (re) ADD STATS desc statistics object parent_stat ALTER TABLE parent ALTER COLUMN c SET DEFAULT 0; NOTICE: DDL test: type alter table, tag ALTER TABLE diff --git a/src/test/modules/test_ddl_deparse/expected/create_table.out b/src/test/modules/test_ddl_deparse/expected/create_table.out index 2178ce83e9..14915f661a 100644 --- a/src/test/modules/test_ddl_deparse/expected/create_table.out +++ b/src/test/modules/test_ddl_deparse/expected/create_table.out @@ -85,8 +85,6 @@ CREATE TABLE employees OF employee_type ( salary WITH OPTIONS DEFAULT 1000 ); NOTICE: DDL test: type simple, tag CREATE TABLE -NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type SET NOT NULL desc column name of table employees NOTICE: DDL test: type simple, tag CREATE INDEX -- Inheritance CREATE TABLE person ( diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c index 97cf52d133..98d237ac68 100644 --- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c +++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c @@ -134,9 +134,6 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS) case AT_DropExpression: strtype = "DROP EXPRESSION"; break; - case AT_CheckNotNull: - strtype = "CHECK NOT NULL"; - break; case AT_SetStatistics: strtype = "SET STATS"; break; diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index 332cb16917..2212c8dbb5 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -1216,20 +1216,6 @@ alter table only parent alter a set not null; ERROR: column "a" of relation "parent" contains null values alter table child alter a set not null; ERROR: column "a" of relation "child" contains null values -delete from parent; -alter table only parent alter a set not null; -insert into parent values (NULL); -ERROR: null value in column "a" of relation "parent" violates not-null constraint -DETAIL: Failing row contains (null). -alter table child alter a set not null; -insert into child (a, b) values (NULL, 'foo'); -ERROR: null value in column "a" of relation "child" violates not-null constraint -DETAIL: Failing row contains (null, foo). -delete from child; -alter table child alter a set not null; -insert into child (a, b) values (NULL, 'foo'); -ERROR: null value in column "a" of relation "child" violates not-null constraint -DETAIL: Failing row contains (null, foo). drop table child; drop table parent; -- test setting and removing default values @@ -3385,6 +3371,7 @@ DROP TABLE tt9; -- Check that comments on constraints and indexes are not lost at ALTER TABLE. CREATE TABLE comment_test ( id int, + constraint id_notnull_constraint not null id, positive_col int CHECK (positive_col > 0), indexed_col int, CONSTRAINT comment_test_pk PRIMARY KEY (id)); @@ -3393,6 +3380,7 @@ COMMENT ON COLUMN comment_test.id IS 'Column ''id'' on comment_test'; COMMENT ON INDEX comment_test_index IS 'Simple index on comment_test'; COMMENT ON CONSTRAINT comment_test_positive_col_check ON comment_test IS 'CHECK constraint on comment_test.positive_col'; COMMENT ON CONSTRAINT comment_test_pk ON comment_test IS 'PRIMARY KEY constraint of comment_test'; +COMMENT ON CONSTRAINT id_notnull_constraint ON comment_test IS 'NOT NULL constraint of comment_test'; COMMENT ON INDEX comment_test_pk IS 'Index backing the PRIMARY KEY of comment_test'; SELECT col_description('comment_test'::regclass, 1) as comment; comment @@ -3412,7 +3400,8 @@ SELECT conname as constraint, obj_description(oid, 'pg_constraint') as comment F ---------------------------------+----------------------------------------------- comment_test_pk | PRIMARY KEY constraint of comment_test comment_test_positive_col_check | CHECK constraint on comment_test.positive_col -(2 rows) + id_notnull_constraint | NOT NULL constraint of comment_test +(3 rows) -- Change the datatype of all the columns. ALTER TABLE is optimized to not -- rebuild an index if the new data type is binary compatible with the old @@ -3443,7 +3432,8 @@ SELECT conname as constraint, obj_description(oid, 'pg_constraint') as comment F ---------------------------------+----------------------------------------------- comment_test_pk | PRIMARY KEY constraint of comment_test comment_test_positive_col_check | CHECK constraint on comment_test.positive_col -(2 rows) + id_notnull_constraint | NOT NULL constraint of comment_test +(3 rows) -- Check compatibility for foreign keys and comments. This is done -- separately as rebuilding the column type of the parent leads @@ -3859,6 +3849,9 @@ CREATE TABLE atnotnull1 (); ALTER TABLE atnotnull1 ADD COLUMN a INT, ALTER a SET NOT NULL; +ALTER TABLE atnotnull1 + ADD COLUMN b INT, + ADD NOT NULL b; ALTER TABLE atnotnull1 ADD COLUMN c INT, ADD PRIMARY KEY (c); @@ -3867,9 +3860,14 @@ ALTER TABLE atnotnull1 Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+---------+-----------+----------+---------+---------+--------------+------------- a | integer | | not null | | plain | | + b | integer | | not null | | plain | | c | integer | | not null | | plain | | Indexes: "atnotnull1_pkey" PRIMARY KEY, btree (c) +Not-null constraints: + "atnotnull1_a_not_null" NOT NULL "a" + "atnotnull1_b_not_null" NOT NULL "b" + "atnotnull1_c_not_null" NOT NULL "c" -- cannot drop column that is part of the partition key CREATE TABLE partitioned ( @@ -4028,6 +4026,14 @@ SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_1'::reg f | 1 (1 row) +-- check that NOT NULL NO INHERIT cannot be merged to a normal NOT NULL +CREATE TABLE part_fail (a int NOT NULL NO INHERIT, + b char(2) COLLATE "C", + CONSTRAINT check_a CHECK (a > 0) +); +ALTER TABLE list_parted ATTACH PARTITION part_fail FOR VALUES IN (2); +ERROR: constraint "part_fail_a_not_null" conflicts with non-inherited constraint on child table "part_fail" +DROP TABLE part_fail; -- check that the new partition won't overlap with an existing partition CREATE TABLE fail_part (LIKE part_1 INCLUDING CONSTRAINTS); ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); @@ -4404,7 +4410,6 @@ ERROR: cannot alter inherited column "b" -- partitions exist ALTER TABLE ONLY list_parted2 ALTER b SET NOT NULL; ERROR: constraint must be added to child tables too -DETAIL: Column "b" of relation "part_2" is not already NOT NULL. HINT: Do not specify the ONLY keyword. ALTER TABLE ONLY list_parted2 ADD CONSTRAINT check_b CHECK (b <> 'zz'); ERROR: constraint must be added to child tables too @@ -4424,6 +4429,8 @@ Partition of: list_parted2 FOR VALUES IN (2) Partition constraint: ((a IS NOT NULL) AND (a = 2)) Check constraints: "check_b" CHECK (b <> 'zz'::bpchar) +Not-null constraints: + "list_parted2_b_not_null" NOT NULL "b" -- It's alright though, if no partitions are yet created CREATE TABLE parted_no_parts (a int) PARTITION BY LIST (a); @@ -4436,6 +4443,12 @@ ALTER TABLE part_2 ALTER b DROP NOT NULL; ERROR: column "b" is marked NOT NULL in parent table ALTER TABLE part_2 DROP CONSTRAINT check_a2; ERROR: cannot drop inherited constraint "check_a2" of relation "part_2" +-- can't drop NOT NULL from under an invalid PK +CREATE TABLE list_parted3 (a int NOT NULL) PARTITION BY LIST (a); +CREATE TABLE list_parted3_1 PARTITION OF list_parted3 FOR VALUES IN (1); +ALTER TABLE ONLY list_parted3 ADD PRIMARY KEY (a); +ALTER TABLE ONLY list_parted3 DROP CONSTRAINT list_parted3_a_not_null; +ERROR: column "a" is in a primary key -- Doesn't make sense to add NO INHERIT constraints on partitioned tables ALTER TABLE list_parted2 add constraint check_b2 check (b <> 'zz') NO INHERIT; ERROR: cannot add NO INHERIT constraint to partitioned table "list_parted2" @@ -4462,7 +4475,7 @@ SELECT * FROM list_parted; (0 rows) -- cleanup -DROP TABLE list_parted, list_parted2, range_parted; +DROP TABLE list_parted, list_parted2, range_parted, list_parted3; DROP TABLE fail_def_part; DROP TABLE hash_parted; -- more tests for certain multi-level partitioning scenarios diff --git a/src/test/regress/expected/cluster.out b/src/test/regress/expected/cluster.out index a13aafff0b..4d40a6809a 100644 --- a/src/test/regress/expected/cluster.out +++ b/src/test/regress/expected/cluster.out @@ -247,11 +247,12 @@ ERROR: insert or update on table "clstr_tst" violates foreign key constraint "c DETAIL: Key (b)=(1111) is not present in table "clstr_tst_s". SELECT conname FROM pg_constraint WHERE conrelid = 'clstr_tst'::regclass ORDER BY 1; - conname ----------------- + conname +---------------------- + clstr_tst_a_not_null clstr_tst_con clstr_tst_pkey -(2 rows) +(3 rows) SELECT relname, relkind, EXISTS(SELECT 1 FROM pg_class WHERE oid = c.reltoastrelid) AS hastoast diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out index cf0b80d616..71200c90ed 100644 --- a/src/test/regress/expected/constraints.out +++ b/src/test/regress/expected/constraints.out @@ -6,6 +6,7 @@ -- - PRIMARY KEY clauses -- - UNIQUE clauses -- - EXCLUDE clauses +-- - NOT NULL clauses -- -- directory paths are passed to us in environment variables \getenv abs_srcdir PG_ABS_SRCDIR @@ -797,6 +798,530 @@ ALTER TABLE deferred_excl ADD EXCLUDE (f1 WITH =); ERROR: could not create exclusion constraint "deferred_excl_f1_excl" DETAIL: Key (f1)=(3) conflicts with key (f1)=(3). DROP TABLE deferred_excl; +-- verify constraints created for NOT NULL clauses +CREATE TABLE notnull_tbl1 (a INTEGER NOT NULL NOT NULL); +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl1_a_not_null" NOT NULL "a" + +-- no-op +ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl1_a_not_null" NOT NULL "a" + +-- duplicate name +ALTER TABLE notnull_tbl1 ADD COLUMN b INT CONSTRAINT notnull_tbl1_a_not_null NOT NULL; +ERROR: constraint "notnull_tbl1_a_not_null" for relation "notnull_tbl1" already exists +-- DROP NOT NULL gets rid of both the attnotnull flag and the constraint itself +ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + +-- SET NOT NULL puts both back +ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl1_a_not_null" NOT NULL "a" + +-- Doing it twice doesn't create a redundant constraint +ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; +select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass; + conname | contype | conkey +-------------------------+---------+-------- + notnull_tbl1_a_not_null | n | {1} +(1 row) + +-- Using the "table constraint" syntax also works +ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL; +ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "foobar" NOT NULL "a" + +DROP TABLE notnull_tbl1; +-- 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 +-- handle this separately from column constraints. +create table notnull_tbl1 (a int primary key constraint foo not null); +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl1_pkey" PRIMARY KEY, btree (a) +Not-null constraints: + "foo" NOT NULL "a" + +create table notnull_tbl2 (a serial, constraint foo not null a); +\d+ notnull_tbl2 + Table "public.notnull_tbl2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+-----------------------------------------+---------+--------------+------------- + a | integer | | not null | nextval('notnull_tbl2_a_seq'::regclass) | plain | | +Not-null constraints: + "foo" NOT NULL "a" + +create table notnull_tbl3 (constraint foo not null a, a int generated by default as identity); +\d+ notnull_tbl3 + Table "public.notnull_tbl3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+----------------------------------+---------+--------------+------------- + a | integer | | not null | generated by default as identity | plain | | +Not-null constraints: + "foo" NOT NULL "a" + +create table notnull_tbl4 (a int not null constraint foo not null); +\d+ notnull_tbl4 + Table "public.notnull_tbl4" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "foo" NOT NULL "a" + +create table notnull_tbl5 (a int constraint foo not null constraint foo not null); +\d+ notnull_tbl5 + Table "public.notnull_tbl5" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "foo" NOT NULL "a" + +create table notnull_tbl6 (like notnull_tbl1, constraint foo not null a); +\d+ notnull_tbl6 + Table "public.notnull_tbl6" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "foo" NOT NULL "a" + +drop table notnull_tbl2, notnull_tbl3, notnull_tbl4, notnull_tbl5, notnull_tbl6; +-- error cases: +create table notnull_tbl_fail (a serial constraint foo not null constraint bar not null); +ERROR: conflicting not-null constraint names "foo" and "bar" +create table notnull_tbl_fail (a serial constraint foo not null no inherit constraint foo not null); +ERROR: conflicting NO INHERIT declarations for not-null constraints on column "a" +create table notnull_tbl_fail (a int constraint foo not null, constraint foo not null a no inherit); +ERROR: conflicting NO INHERIT declaration for not-null constraint on column "a" +create table notnull_tbl_fail (a serial constraint foo not null, constraint bar not null a); +ERROR: conflicting not-null constraint names "foo" and "bar" +create table notnull_tbl_fail (a serial, constraint foo not null a, constraint bar not null a); +ERROR: conflicting not-null constraint names "foo" and "bar" +create table notnull_tbl_fail (a serial, constraint foo not null a no inherit); +ERROR: conflicting NO INHERIT declaration for not-null constraint on column "a" +create table notnull_tbl_fail (a serial not null no inherit); +ERROR: conflicting NO INHERIT declarations for not-null constraints on column "a" +create table notnull_tbl_fail (like notnull_tbl1, constraint foo2 not null a); +ERROR: conflicting not-null constraint names "foo" and "foo2" +create table notnull_tbl_fail (a int primary key constraint foo not null no inherit); +ERROR: conflicting NO INHERIT declarations for not-null constraints on column "a" +create table notnull_tbl_fail (a int not null no inherit primary key); +ERROR: conflicting NO INHERIT declarations for not-null constraints on column "a" +create table notnull_tbl_fail (a int primary key, not null a no inherit); +ERROR: conflicting NO INHERIT declaration for not-null constraint on column "a" +create table notnull_tbl_fail (a int, primary key(a), not null a no inherit); +ERROR: conflicting NO INHERIT declaration for not-null constraint on column "a" +create table notnull_tbl_fail (a int generated by default as identity, constraint foo not null a no inherit); +ERROR: conflicting NO INHERIT declaration for not-null constraint on column "a" +create table notnull_tbl_fail (a int generated by default as identity not null no inherit); +ERROR: conflicting NO INHERIT declarations for not-null constraints on column "a" +drop table notnull_tbl1; +-- NOT NULL NO INHERIT +CREATE TABLE ATACC1 (a int, NOT NULL a NO INHERIT); +CREATE TABLE ATACC2 () INHERITS (ATACC1); +\d+ ATACC2 + Table "public.atacc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: atacc1 + +DROP TABLE ATACC1, ATACC2; +CREATE TABLE ATACC1 (a int); +ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT; +CREATE TABLE ATACC2 () INHERITS (ATACC1); +\d+ ATACC2 + Table "public.atacc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: atacc1 + +DROP TABLE ATACC1, ATACC2; +CREATE TABLE ATACC1 (a int); +CREATE TABLE ATACC2 () INHERITS (ATACC1); +ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT; +\d+ ATACC2 + Table "public.atacc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: atacc1 + +CREATE TABLE ATACC3 (PRIMARY KEY (a)) INHERITS (ATACC1); +\d+ ATACC3 + Table "public.atacc3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "atacc3_pkey" PRIMARY KEY, btree (a) +Not-null constraints: + "atacc3_a_not_null" NOT NULL "a" +Inherits: atacc1 + +DROP TABLE ATACC1, ATACC2, ATACC3; +-- NOT NULL NO INHERIT is not possible on partitioned tables +CREATE TABLE ATACC1 (a int NOT NULL NO INHERIT) PARTITION BY LIST (a); +ERROR: not-null constraints on partitioned tables cannot be NO INHERIT +CREATE TABLE ATACC1 (a int, NOT NULL a NO INHERIT) PARTITION BY LIST (a); +ERROR: not-null constraints on partitioned tables cannot be NO INHERIT +-- it's not possible to override a no-inherit constraint with an inheritable one +CREATE TABLE ATACC2 (a int, CONSTRAINT a_is_not_null NOT NULL a NO INHERIT); +CREATE TABLE ATACC1 (a int); +CREATE TABLE ATACC3 (a int) INHERITS (ATACC2); +NOTICE: merging column "a" with inherited definition +ALTER TABLE ATACC2 INHERIT ATACC1; +-- can't override +ALTER TABLE ATACC1 ADD CONSTRAINT ditto NOT NULL a; +ERROR: cannot change NO INHERIT status of NOT NULL constraint "a_is_not_null" on relation "atacc2" +-- dropping the NO INHERIT constraint allows this to work +ALTER TABLE ATACC2 DROP CONSTRAINT a_is_not_null; +ALTER TABLE ATACC1 ADD CONSTRAINT ditto NOT NULL a; +\d+ ATACC3 + Table "public.atacc3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "ditto" NOT NULL "a" (inherited) +Inherits: atacc2 + +DROP TABLE ATACC1, ATACC2, ATACC3; +-- Can't have two constraints with the same name +CREATE TABLE notnull_tbl2 (a INTEGER CONSTRAINT blah NOT NULL, b INTEGER CONSTRAINT blah NOT NULL); +ERROR: constraint "blah" for relation "notnull_tbl2" already exists +-- can't drop not-null in primary key +CREATE TABLE notnull_tbl2 (a INTEGER PRIMARY KEY); +ALTER TABLE notnull_tbl2 ALTER a DROP NOT NULL; +ERROR: column "a" is in a primary key +DROP TABLE notnull_tbl2; +CREATE TABLE notnull_tbl3 (a INTEGER NOT NULL, CHECK (a IS NOT NULL)); +ALTER TABLE notnull_tbl3 ALTER A DROP NOT NULL; +ALTER TABLE notnull_tbl3 ADD b int, ADD CONSTRAINT pk PRIMARY KEY (a, b); +\d notnull_tbl3 + Table "public.notnull_tbl3" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | not null | + b | integer | | not null | +Indexes: + "pk" PRIMARY KEY, btree (a, b) +Check constraints: + "notnull_tbl3_a_check" CHECK (a IS NOT NULL) + +ALTER TABLE notnull_tbl3 DROP CONSTRAINT pk; +\d notnull_tbl3 + Table "public.notnull_tbl3" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | not null | + b | integer | | not null | +Check constraints: + "notnull_tbl3_a_check" CHECK (a IS NOT NULL) + +-- Primary keys cause not-null constraints to be created. +CREATE TABLE cnn_pk (a int, b int); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +ALTER TABLE cnn_pk ADD CONSTRAINT cnn_primarykey PRIMARY KEY (b); +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Indexes: + "cnn_primarykey" PRIMARY KEY, btree (b) +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +DROP TABLE cnn_pk, cnn_pk_child; +-- As above, but create the primary key ahead of time +CREATE TABLE cnn_pk (a int, b int, CONSTRAINT cnn_primarykey PRIMARY KEY (b)); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Indexes: + "cnn_primarykey" PRIMARY KEY, btree (b) +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +DROP TABLE cnn_pk, cnn_pk_child; +-- As above, but create the primary key using a UNIQUE index +CREATE TABLE cnn_pk (a int, b int); +CREATE UNIQUE INDEX cnn_uq ON cnn_pk (b); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +ALTER TABLE cnn_pk ADD CONSTRAINT cnn_primarykey PRIMARY KEY USING INDEX cnn_uq; +NOTICE: ALTER TABLE / ADD CONSTRAINT USING INDEX will rename index "cnn_uq" to "cnn_primarykey" +\d+ cnn_pk* + Table "public.cnn_pk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Indexes: + "cnn_primarykey" PRIMARY KEY, btree (b) +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" +Child tables: cnn_pk_child + + Table "public.cnn_pk_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "cnn_pk_b_not_null" NOT NULL "b" (inherited) +Inherits: cnn_pk + +DROP TABLE cnn_pk, cnn_pk_child; +-- Unique constraints don't give raise to not-null constraints, however. +create table cnn_uq (a int); +alter table cnn_uq add unique (a); +\d+ cnn_uq + Table "public.cnn_uq" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Indexes: + "cnn_uq_a_key" UNIQUE CONSTRAINT, btree (a) + +drop table cnn_uq; +create table cnn_uq (a int); +create unique index cnn_uq_idx on cnn_uq (a); +alter table cnn_uq add unique using index cnn_uq_idx; +\d+ cnn_uq + Table "public.cnn_uq" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Indexes: + "cnn_uq_idx" UNIQUE CONSTRAINT, btree (a) + +-- Ensure partitions are scanned for null values when adding a PK +create table cnn2_parted(a int) partition by list (a); +create table cnn_part1 partition of cnn2_parted for values in (1, null); +insert into cnn_part1 values (null); +alter table cnn2_parted add primary key (a); +ERROR: column "a" of relation "cnn_part1" contains null values +drop table cnn2_parted; +-- columns in regular and LIKE inheritance should be marked not-nullable +-- for primary keys, even if those are deferred +CREATE TABLE notnull_tbl4 (a INTEGER PRIMARY KEY INITIALLY DEFERRED); +CREATE TABLE notnull_tbl4_lk (LIKE notnull_tbl4); +CREATE TABLE notnull_tbl4_lk2 (LIKE notnull_tbl4 INCLUDING INDEXES); +CREATE TABLE notnull_tbl4_lk3 (LIKE notnull_tbl4 INCLUDING INDEXES, NOT NULL a); +ALTER TABLE notnull_tbl4_lk3 RENAME CONSTRAINT notnull_tbl4_a_not_null TO a_nn; +CREATE TABLE notnull_tbl4_cld () INHERITS (notnull_tbl4); +CREATE TABLE notnull_tbl4_cld2 (PRIMARY KEY (a) DEFERRABLE) INHERITS (notnull_tbl4); +CREATE TABLE notnull_tbl4_cld3 (PRIMARY KEY (a) DEFERRABLE, CONSTRAINT a_nn NOT NULL a) INHERITS (notnull_tbl4); +\d+ notnull_tbl4 + Table "public.notnull_tbl4" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_pkey" PRIMARY KEY, btree (a) DEFERRABLE INITIALLY DEFERRED +Not-null constraints: + "notnull_tbl4_a_not_null" NOT NULL "a" +Child tables: notnull_tbl4_cld, + notnull_tbl4_cld2, + notnull_tbl4_cld3 + +\d+ notnull_tbl4_lk + Table "public.notnull_tbl4_lk" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl4_a_not_null" NOT NULL "a" + +\d+ notnull_tbl4_lk2 + Table "public.notnull_tbl4_lk2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_lk2_pkey" PRIMARY KEY, btree (a) DEFERRABLE INITIALLY DEFERRED +Not-null constraints: + "notnull_tbl4_a_not_null" NOT NULL "a" + +\d+ notnull_tbl4_lk3 + Table "public.notnull_tbl4_lk3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_lk3_pkey" PRIMARY KEY, btree (a) DEFERRABLE INITIALLY DEFERRED +Not-null constraints: + "a_nn" NOT NULL "a" + +\d+ notnull_tbl4_cld + Table "public.notnull_tbl4_cld" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl4_a_not_null" NOT NULL "a" (inherited) +Inherits: notnull_tbl4 + +\d+ notnull_tbl4_cld2 + Table "public.notnull_tbl4_cld2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_cld2_pkey" PRIMARY KEY, btree (a) DEFERRABLE +Not-null constraints: + "notnull_tbl4_cld2_a_not_null" NOT NULL "a" (local, inherited) +Inherits: notnull_tbl4 + +\d+ notnull_tbl4_cld3 + Table "public.notnull_tbl4_cld3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Indexes: + "notnull_tbl4_cld3_pkey" PRIMARY KEY, btree (a) DEFERRABLE +Not-null constraints: + "a_nn" NOT NULL "a" (local, inherited) +Inherits: notnull_tbl4 + +-- leave these tables around for pg_upgrade testing +-- It's possible to remove a constraint from parents without affecting children +CREATE TABLE notnull_tbl5 (a int CONSTRAINT ann NOT NULL, + b int CONSTRAINT bnn NOT NULL); +CREATE TABLE notnull_tbl5_child () INHERITS (notnull_tbl5); +ALTER TABLE ONLY notnull_tbl5 DROP CONSTRAINT ann; +ALTER TABLE ONLY notnull_tbl5 ALTER b DROP NOT NULL; +\d+ notnull_tbl5_child + Table "public.notnull_tbl5_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "ann" NOT NULL "a" + "bnn" NOT NULL "b" +Inherits: notnull_tbl5 + +CREATE TABLE notnull_tbl6 (a int CONSTRAINT ann NOT NULL, + b int CONSTRAINT bnn NOT NULL, check (a > 0)) PARTITION BY LIST (a); +CREATE TABLE notnull_tbl6_1 PARTITION OF notnull_tbl6 FOR VALUES IN (1); +ALTER TABLE ONLY notnull_tbl6 DROP CONSTRAINT ann; +ALTER TABLE ONLY notnull_tbl6 ALTER b DROP NOT NULL; +\d+ notnull_tbl6_1 + Table "public.notnull_tbl6_1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | + b | integer | | not null | | plain | | +Partition of: notnull_tbl6 FOR VALUES IN (1) +Partition constraint: ((a IS NOT NULL) AND (a = 1)) +Check constraints: + "notnull_tbl6_a_check" CHECK (a > 0) +Not-null constraints: + "ann" NOT NULL "a" + "bnn" NOT NULL "b" + -- Comments -- Setup a low-level role to enforce non-superuser checks. CREATE ROLE regress_constraint_comments; diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out index 57a24050ab..76604705a9 100644 --- a/src/test/regress/expected/create_table.out +++ b/src/test/regress/expected/create_table.out @@ -771,21 +771,23 @@ CREATE TABLE part_b PARTITION OF parted ( NOTICE: merging constraint "check_a" with inherited definition -- conislocal should be false for any merged constraints, true otherwise SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname; - conname | conislocal | coninhcount ----------+------------+------------- - check_a | f | 1 - check_b | t | 0 -(2 rows) + conname | conislocal | coninhcount +-------------------+------------+------------- + check_a | f | 1 + part_b_b_not_null | t | 1 + check_b | t | 0 +(3 rows) -- Once check_b is added to the parent, it should be made non-local for part_b ALTER TABLE parted ADD CONSTRAINT check_b CHECK (b >= 0); NOTICE: merging constraint "check_b" with inherited definition SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname; - conname | conislocal | coninhcount ----------+------------+------------- - check_a | f | 1 - check_b | f | 1 -(2 rows) + conname | conislocal | coninhcount +-------------------+------------+------------- + check_a | f | 1 + check_b | f | 1 + part_b_b_not_null | t | 1 +(3 rows) -- Neither check_a nor check_b are droppable from part_b ALTER TABLE part_b DROP CONSTRAINT check_a; @@ -797,9 +799,10 @@ ERROR: cannot drop inherited constraint "check_b" of relation "part_b" -- be local constraints. ALTER TABLE parted DROP CONSTRAINT check_a, DROP CONSTRAINT check_b; SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname; - conname | conislocal | coninhcount ----------+------------+------------- -(0 rows) + conname | conislocal | coninhcount +-------------------+------------+------------- + part_b_b_not_null | t | 1 +(1 row) -- specify PARTITION BY for a partition CREATE TABLE fail_part_col_not_found PARTITION OF parted FOR VALUES IN ('c') PARTITION BY RANGE (c); @@ -863,6 +866,8 @@ drop table test_part_coll_posix; b | integer | | not null | 1 | plain | | Partition of: parted FOR VALUES IN ('b') Partition constraint: ((a IS NOT NULL) AND (a = 'b'::text)) +Not-null constraints: + "part_b_b_not_null" NOT NULL "b" (local, inherited) -- Both partition bound and partition key in describe output \d+ part_c @@ -874,6 +879,8 @@ Partition constraint: ((a IS NOT NULL) AND (a = 'b'::text)) Partition of: parted FOR VALUES IN ('c') Partition constraint: ((a IS NOT NULL) AND (a = 'c'::text)) Partition key: RANGE (b) +Not-null constraints: + "part_c_b_not_null" NOT NULL "b" (local, inherited) Partitions: part_c_1_10 FOR VALUES FROM (1) TO (10) -- a level-2 partition's constraint will include the parent's expressions @@ -885,6 +892,8 @@ Partitions: part_c_1_10 FOR VALUES FROM (1) TO (10) b | integer | | not null | 0 | plain | | Partition of: part_c FOR VALUES FROM (1) TO (10) Partition constraint: ((a IS NOT NULL) AND (a = 'c'::text) AND (b IS NOT NULL) AND (b >= 1) AND (b < 10)) +Not-null constraints: + "part_c_b_not_null" NOT NULL "b" (inherited) -- Show partition count in the parent's describe output -- Tempted to include \d+ output listing partitions with bound info but diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out index 6bfc6d040f..d091da5a1e 100644 --- a/src/test/regress/expected/create_table_like.out +++ b/src/test/regress/expected/create_table_like.out @@ -348,6 +348,8 @@ CREATE TABLE ctlt12_storage (LIKE ctlt1 INCLUDING STORAGE, LIKE ctlt2 INCLUDING a | text | | not null | | main | | b | text | | | | extended | | c | text | | | | external | | +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" CREATE TABLE ctlt12_comments (LIKE ctlt1 INCLUDING COMMENTS, LIKE ctlt2 INCLUDING COMMENTS); \d+ ctlt12_comments @@ -357,6 +359,8 @@ CREATE TABLE ctlt12_comments (LIKE ctlt1 INCLUDING COMMENTS, LIKE ctlt2 INCLUDIN a | text | | not null | | extended | | A b | text | | | | extended | | B c | text | | | | extended | | C +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" CREATE TABLE ctlt1_inh (LIKE ctlt1 INCLUDING CONSTRAINTS INCLUDING COMMENTS) INHERITS (ctlt1); NOTICE: merging column "a" with inherited definition @@ -370,6 +374,8 @@ NOTICE: merging constraint "ctlt1_a_check" with inherited definition b | text | | | | extended | | B Check constraints: "ctlt1_a_check" CHECK (length(a) > 2) +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" (local, inherited) Inherits: ctlt1 SELECT description FROM pg_description, pg_constraint c WHERE classoid = 'pg_constraint'::regclass AND objoid = c.oid AND c.conrelid = 'ctlt1_inh'::regclass; @@ -391,6 +397,8 @@ Check constraints: "ctlt1_a_check" CHECK (length(a) > 2) "ctlt3_a_check" CHECK (length(a) < 5) "ctlt3_c_check" CHECK (length(c) < 7) +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" (inherited) Inherits: ctlt1, ctlt3 @@ -409,6 +417,8 @@ Check constraints: "ctlt1_a_check" CHECK (length(a) > 2) "ctlt3_a_check" CHECK (length(a) < 5) "ctlt3_c_check" CHECK (length(c) < 7) +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" (inherited) Inherits: ctlt1 SELECT description FROM pg_description, pg_constraint c WHERE classoid = 'pg_constraint'::regclass AND objoid = c.oid AND c.conrelid = 'ctlt13_like'::regclass; @@ -433,6 +443,8 @@ Check constraints: Statistics objects: "public.ctlt_all_a_b_stat" ON a, b FROM ctlt_all "public.ctlt_all_expr_stat" ON (a || b) FROM ctlt_all +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" SELECT c.relname, objsubid, description FROM pg_description, pg_index i, pg_class c WHERE classoid = 'pg_class'::regclass AND objoid = i.indexrelid AND c.oid = i.indexrelid AND i.indrelid = 'ctlt_all'::regclass ORDER BY c.relname, objsubid; relname | objsubid | description @@ -473,6 +485,8 @@ Check constraints: Statistics objects: "public.pg_attrdef_a_b_stat" ON a, b FROM public.pg_attrdef "public.pg_attrdef_expr_stat" ON (a || b) FROM public.pg_attrdef +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" DROP TABLE public.pg_attrdef; -- Check that LIKE isn't confused when new table masks the old, either @@ -495,20 +509,28 @@ Check constraints: Statistics objects: "ctl_schema.ctlt1_a_b_stat" ON a, b FROM ctlt1 "ctl_schema.ctlt1_expr_stat" ON (a || b) FROM ctlt1 +Not-null constraints: + "ctlt1_a_not_null" NOT NULL "a" ROLLBACK; DROP TABLE ctlt1, ctlt2, ctlt3, ctlt4, ctlt12_storage, ctlt12_comments, ctlt1_inh, ctlt13_inh, ctlt13_like, ctlt_all, ctla, ctlb CASCADE; NOTICE: drop cascades to table inhe -- LIKE must respect NO INHERIT property of constraints -CREATE TABLE noinh_con_copy (a int CHECK (a > 0) NO INHERIT); +CREATE TABLE noinh_con_copy (a int CHECK (a > 0) NO INHERIT, b int not null, + c int not null no inherit); CREATE TABLE noinh_con_copy1 (LIKE noinh_con_copy INCLUDING CONSTRAINTS); -\d noinh_con_copy1 - Table "public.noinh_con_copy1" - Column | Type | Collation | Nullable | Default ---------+---------+-----------+----------+--------- - a | integer | | | +\d+ noinh_con_copy1 + Table "public.noinh_con_copy1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | + b | integer | | not null | | plain | | + c | integer | | not null | | plain | | Check constraints: "noinh_con_copy_a_check" CHECK (a > 0) NO INHERIT +Not-null constraints: + "noinh_con_copy_b_not_null" NOT NULL "b" + "noinh_con_copy_c_not_null" NOT NULL "c" NO INHERIT -- fail, as partitioned tables don't allow NO INHERIT constraints CREATE TABLE noinh_con_copy1_parted (LIKE noinh_con_copy INCLUDING ALL) diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out index 6ed50fdcfa..cce49e509a 100644 --- a/src/test/regress/expected/foreign_data.out +++ b/src/test/regress/expected/foreign_data.out @@ -742,6 +742,8 @@ COMMENT ON COLUMN ft1.c1 IS 'ft1.c1'; Check constraints: "ft1_c2_check" CHECK (c2 <> ''::text) "ft1_c3_check" CHECK (c3 >= '01-01-1994'::date AND c3 <= '01-31-1994'::date) +Not-null constraints: + "ft1_c1_not_null" NOT NULL "c1" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -864,6 +866,9 @@ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET STORAGE PLAIN; Check constraints: "ft1_c2_check" CHECK (c2 <> ''::text) "ft1_c3_check" CHECK (c3 >= '01-01-1994'::date AND c3 <= '01-31-1994'::date) +Not-null constraints: + "ft1_c1_not_null" NOT NULL "c1" + "ft1_c6_not_null" NOT NULL "c6" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1409,6 +1414,8 @@ CREATE FOREIGN TABLE ft2 () INHERITS (fd_pt1) c1 | integer | | not null | | plain | | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1418,6 +1425,8 @@ Child tables: ft2, FOREIGN c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1430,6 +1439,8 @@ DROP FOREIGN TABLE ft2; c1 | integer | | not null | | plain | | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" CREATE FOREIGN TABLE ft2 ( c1 integer NOT NULL, @@ -1443,6 +1454,8 @@ CREATE FOREIGN TABLE ft2 ( c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1454,6 +1467,8 @@ ALTER FOREIGN TABLE ft2 INHERIT fd_pt1; c1 | integer | | not null | | plain | | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1463,6 +1478,8 @@ Child tables: ft2, FOREIGN c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1484,6 +1501,8 @@ NOTICE: merging column "c3" with inherited definition c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1497,6 +1516,8 @@ Child tables: ct3, c1 | integer | | not null | | plain | | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (inherited) Inherits: ft2 \d+ ft3 @@ -1506,6 +1527,8 @@ Inherits: ft2 c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft3_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 Inherits: ft2 @@ -1527,6 +1550,9 @@ ALTER TABLE fd_pt1 ADD COLUMN c8 integer; c6 | integer | | | | plain | | c7 | integer | | not null | | plain | | c8 | integer | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" + "fd_pt1_c7_not_null" NOT NULL "c7" Child tables: ft2, FOREIGN \d+ ft2 @@ -1541,6 +1567,9 @@ Child tables: ft2, FOREIGN c6 | integer | | | | | plain | | c7 | integer | | not null | | | plain | | c8 | integer | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) + "fd_pt1_c7_not_null" NOT NULL "c7" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1559,6 +1588,9 @@ Child tables: ct3, c6 | integer | | | | plain | | c7 | integer | | not null | | plain | | c8 | integer | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (inherited) + "fd_pt1_c7_not_null" NOT NULL "c7" (inherited) Inherits: ft2 \d+ ft3 @@ -1573,6 +1605,9 @@ Inherits: ft2 c6 | integer | | | | | plain | | c7 | integer | | not null | | | plain | | c8 | integer | | | | | plain | | +Not-null constraints: + "ft3_c1_not_null" NOT NULL "c1" (local, inherited) + "fd_pt1_c7_not_null" NOT NULL "c7" (inherited) Server: s0 Inherits: ft2 @@ -1601,6 +1636,9 @@ ALTER TABLE fd_pt1 ALTER COLUMN c8 SET STORAGE EXTERNAL; c6 | integer | | not null | | plain | | c7 | integer | | | | plain | | c8 | text | | | | external | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" + "fd_pt1_c6_not_null" NOT NULL "c6" Child tables: ft2, FOREIGN \d+ ft2 @@ -1615,6 +1653,9 @@ Child tables: ft2, FOREIGN c6 | integer | | not null | | | plain | | c7 | integer | | | | | plain | | c8 | text | | | | | external | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) + "fd_pt1_c6_not_null" NOT NULL "c6" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1634,6 +1675,8 @@ ALTER TABLE fd_pt1 DROP COLUMN c8; c1 | integer | | not null | | plain | 10000 | c2 | text | | | | extended | | c3 | date | | | | plain | | +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1643,6 +1686,8 @@ Child tables: ft2, FOREIGN c1 | integer | | not null | | | plain | 10000 | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1657,11 +1702,12 @@ SELECT relname, conname, contype, conislocal, coninhcount, connoinherit FROM pg_class AS pc JOIN pg_constraint AS pgc ON (conrelid = pc.oid) WHERE pc.relname = 'fd_pt1' ORDER BY 1,2; - relname | conname | contype | conislocal | coninhcount | connoinherit ----------+------------+---------+------------+-------------+-------------- - fd_pt1 | fd_pt1chk1 | c | t | 0 | t - fd_pt1 | fd_pt1chk2 | c | t | 0 | f -(2 rows) + relname | conname | contype | conislocal | coninhcount | connoinherit +---------+--------------------+---------+------------+-------------+-------------- + fd_pt1 | fd_pt1_c1_not_null | n | t | 0 | f + fd_pt1 | fd_pt1chk1 | c | t | 0 | t + fd_pt1 | fd_pt1chk2 | c | t | 0 | f +(3 rows) -- child does not inherit NO INHERIT constraints \d+ fd_pt1 @@ -1674,6 +1720,8 @@ SELECT relname, conname, contype, conislocal, coninhcount, connoinherit Check constraints: "fd_pt1chk1" CHECK (c1 > 0) NO INHERIT "fd_pt1chk2" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1685,6 +1733,8 @@ Child tables: ft2, FOREIGN c3 | date | | | | | plain | | Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1721,6 +1771,8 @@ ALTER FOREIGN TABLE ft2 INHERIT fd_pt1; Check constraints: "fd_pt1chk1" CHECK (c1 > 0) NO INHERIT "fd_pt1chk2" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1732,6 +1784,8 @@ Child tables: ft2, FOREIGN c3 | date | | | | | plain | | Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1751,6 +1805,8 @@ ALTER TABLE fd_pt1 ADD CONSTRAINT fd_pt1chk3 CHECK (c2 <> '') NOT VALID; c3 | date | | | | plain | | Check constraints: "fd_pt1chk3" CHECK (c2 <> ''::text) NOT VALID +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1763,6 +1819,8 @@ Child tables: ft2, FOREIGN Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) "fd_pt1chk3" CHECK (c2 <> ''::text) NOT VALID +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1778,6 +1836,8 @@ ALTER TABLE fd_pt1 VALIDATE CONSTRAINT fd_pt1chk3; c3 | date | | | | plain | | Check constraints: "fd_pt1chk3" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "c1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1790,6 +1850,8 @@ Child tables: ft2, FOREIGN Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) "fd_pt1chk3" CHECK (c2 <> ''::text) +Not-null constraints: + "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1809,6 +1871,8 @@ ALTER TABLE fd_pt1 RENAME CONSTRAINT fd_pt1chk3 TO f2_check; f3 | date | | | | plain | | Check constraints: "f2_check" CHECK (f2 <> ''::text) +Not-null constraints: + "fd_pt1_c1_not_null" NOT NULL "f1" Child tables: ft2, FOREIGN \d+ ft2 @@ -1821,6 +1885,8 @@ Child tables: ft2, FOREIGN Check constraints: "f2_check" CHECK (f2 <> ''::text) "fd_pt1chk2" CHECK (f2 <> ''::text) +Not-null constraints: + "ft2_c1_not_null" NOT NULL "f1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') Inherits: fd_pt1 @@ -1867,6 +1933,8 @@ CREATE FOREIGN TABLE fd_pt2_1 PARTITION OF fd_pt2 FOR VALUES IN (1) c2 | text | | | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN \d+ fd_pt2_1 @@ -1878,6 +1946,8 @@ Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN c3 | date | | | | | plain | | Partition of: fd_pt2 FOR VALUES IN (1) Partition constraint: ((c1 IS NOT NULL) AND (c1 = 1)) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1897,6 +1967,8 @@ CREATE FOREIGN TABLE fd_pt2_1 ( c2 | text | | | | | extended | | c3 | date | | | | | plain | | c4 | character(1) | | | | | extended | | +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1912,6 +1984,8 @@ DROP FOREIGN TABLE fd_pt2_1; c2 | text | | | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" Number of partitions: 0 CREATE FOREIGN TABLE fd_pt2_1 ( @@ -1926,6 +2000,8 @@ CREATE FOREIGN TABLE fd_pt2_1 ( c1 | integer | | not null | | | plain | | c2 | text | | | | | extended | | c3 | date | | | | | plain | | +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1939,6 +2015,8 @@ ALTER TABLE fd_pt2 ATTACH PARTITION fd_pt2_1 FOR VALUES IN (1); c2 | text | | | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN \d+ fd_pt2_1 @@ -1950,6 +2028,8 @@ Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN c3 | date | | | | | plain | | Partition of: fd_pt2 FOR VALUES IN (1) Partition constraint: ((c1 IS NOT NULL) AND (c1 = 1)) +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1967,6 +2047,8 @@ ALTER TABLE fd_pt2_1 ADD CONSTRAINT p21chk CHECK (c2 <> ''); c2 | text | | | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN \d+ fd_pt2_1 @@ -1980,6 +2062,9 @@ Partition of: fd_pt2 FOR VALUES IN (1) Partition constraint: ((c1 IS NOT NULL) AND (c1 = 1)) Check constraints: "p21chk" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" (inherited) + "fd_pt2_1_c3_not_null" NOT NULL "c3" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') @@ -1997,6 +2082,9 @@ ALTER TABLE fd_pt2 ALTER c2 SET NOT NULL; c2 | text | | not null | | extended | | c3 | date | | | | plain | | Partition key: LIST (c1) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" + "fd_pt2_c2_not_null" NOT NULL "c2" Number of partitions: 0 \d+ fd_pt2_1 @@ -2008,11 +2096,14 @@ Number of partitions: 0 c3 | date | | not null | | | plain | | Check constraints: "p21chk" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" + "fd_pt2_1_c3_not_null" NOT NULL "c3" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') ALTER TABLE fd_pt2 ATTACH PARTITION fd_pt2_1 FOR VALUES IN (1); -- ERROR -ERROR: column "c2" in child table must be marked NOT NULL +ERROR: column "c2" in child table "fd_pt2_1" must be marked NOT NULL ALTER FOREIGN TABLE fd_pt2_1 ALTER c2 SET NOT NULL; ALTER TABLE fd_pt2 ATTACH PARTITION fd_pt2_1 FOR VALUES IN (1); ALTER TABLE fd_pt2 DETACH PARTITION fd_pt2_1; @@ -2027,6 +2118,9 @@ ALTER TABLE fd_pt2 ADD CONSTRAINT fd_pt2chk1 CHECK (c1 > 0); Partition key: LIST (c1) Check constraints: "fd_pt2chk1" CHECK (c1 > 0) +Not-null constraints: + "fd_pt2_c1_not_null" NOT NULL "c1" + "fd_pt2_c2_not_null" NOT NULL "c2" Number of partitions: 0 \d+ fd_pt2_1 @@ -2038,6 +2132,10 @@ Number of partitions: 0 c3 | date | | not null | | | plain | | Check constraints: "p21chk" CHECK (c2 <> ''::text) +Not-null constraints: + "fd_pt2_1_c1_not_null" NOT NULL "c1" + "fd_pt2_1_c2_not_null" NOT NULL "c2" + "fd_pt2_1_c3_not_null" NOT NULL "c3" Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 9bcc849573..a5165270c2 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -2053,13 +2053,19 @@ ORDER BY co.contype, cr.relname, co.conname, p.conname; part33_self_fk | parted_self_fk_id_abc_fkey | f | t | parted_self_fk_id_abc_fkey | t | parted_self_fk part3_self_fk | parted_self_fk_id_abc_fkey | f | t | parted_self_fk_id_abc_fkey | t | parted_self_fk parted_self_fk | parted_self_fk_id_abc_fkey | f | t | | | parted_self_fk + part1_self_fk | part1_self_fk_id_not_null | n | t | | | + part2_self_fk | parted_self_fk_id_not_null | n | t | | | + part32_self_fk | part3_self_fk_id_not_null | n | t | | | + part33_self_fk | part33_self_fk_id_not_null | n | t | | | + part3_self_fk | part3_self_fk_id_not_null | n | t | | | + parted_self_fk | parted_self_fk_id_not_null | n | t | | | part1_self_fk | part1_self_fk_pkey | p | t | parted_self_fk_pkey | t | part2_self_fk | part2_self_fk_pkey | p | t | parted_self_fk_pkey | t | part32_self_fk | part32_self_fk_pkey | p | t | part3_self_fk_pkey | t | part33_self_fk | part33_self_fk_pkey | p | t | part3_self_fk_pkey | t | part3_self_fk | part3_self_fk_pkey | p | t | parted_self_fk_pkey | t | parted_self_fk | parted_self_fk_pkey | p | t | | | -(12 rows) +(18 rows) -- detach and re-attach multiple times just to ensure everything is kosher ALTER TABLE parted_self_fk DETACH PARTITION part2_self_fk; @@ -2082,13 +2088,19 @@ ORDER BY co.contype, cr.relname, co.conname, p.conname; part33_self_fk | parted_self_fk_id_abc_fkey | f | t | parted_self_fk_id_abc_fkey | t | parted_self_fk part3_self_fk | parted_self_fk_id_abc_fkey | f | t | parted_self_fk_id_abc_fkey | t | parted_self_fk parted_self_fk | parted_self_fk_id_abc_fkey | f | t | | | parted_self_fk + part1_self_fk | part1_self_fk_id_not_null | n | t | | | + part2_self_fk | parted_self_fk_id_not_null | n | t | | | + part32_self_fk | part3_self_fk_id_not_null | n | t | | | + part33_self_fk | part33_self_fk_id_not_null | n | t | | | + part3_self_fk | part3_self_fk_id_not_null | n | t | | | + parted_self_fk | parted_self_fk_id_not_null | n | t | | | part1_self_fk | part1_self_fk_pkey | p | t | parted_self_fk_pkey | t | part2_self_fk | part2_self_fk_pkey | p | t | parted_self_fk_pkey | t | part32_self_fk | part32_self_fk_pkey | p | t | part3_self_fk_pkey | t | part33_self_fk | part33_self_fk_pkey | p | t | part3_self_fk_pkey | t | part3_self_fk | part3_self_fk_pkey | p | t | parted_self_fk_pkey | t | parted_self_fk | parted_self_fk_pkey | p | t | | | -(12 rows) +(18 rows) -- Leave this table around, for pg_upgrade/pg_dump tests -- Test creating a constraint at the parent that already exists in partitions. diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_stored.out index 8ea8a3a92d..0d037d48ca 100644 --- a/src/test/regress/expected/generated_stored.out +++ b/src/test/regress/expected/generated_stored.out @@ -321,6 +321,8 @@ NOTICE: merging column "b" with inherited definition a | integer | | not null | | plain | | b | integer | | | generated always as (a * 22) stored | plain | | x | integer | | | | plain | | +Not-null constraints: + "gtest1_a_not_null" NOT NULL "a" (inherited) Inherits: gtest1 CREATE TABLE gtestxx_1 (a int NOT NULL, b int); diff --git a/src/test/regress/expected/identity.out b/src/test/regress/expected/identity.out index f14bfccfb1..2a2b777c89 100644 --- a/src/test/regress/expected/identity.out +++ b/src/test/regress/expected/identity.out @@ -578,6 +578,10 @@ TABLE itest8; f3 | integer | | not null | generated by default as identity | plain | | f4 | bigint | | not null | generated always as identity | plain | | f5 | bigint | | | | plain | | +Not-null constraints: + "itest8_f2_not_null" NOT NULL "f2" + "itest8_f3_not_null" NOT NULL "f3" + "itest8_f4_not_null" NOT NULL "f4" \d itest8_f2_seq Sequence "public.itest8_f2_seq" @@ -618,7 +622,7 @@ INSERT into pitest1_p1 (f1, f2) VALUES ('2016-07-3', 'from pitest1_p1'); CREATE TABLE pitest1_p2 (f3 bigint, f2 text, f1 date NOT NULL); INSERT INTO pitest1_p2 (f1, f2, f3) VALUES ('2016-08-2', 'before attaching', 100); ALTER TABLE pitest1 ATTACH PARTITION pitest1_p2 FOR VALUES FROM ('2016-08-01') TO ('2016-09-01'); -- requires NOT NULL constraint -ERROR: column "f3" in child table must be marked NOT NULL +ERROR: column "f3" in child table "pitest1_p2" must be marked NOT NULL ALTER TABLE pitest1_p2 ALTER COLUMN f3 SET NOT NULL; ALTER TABLE pitest1 ATTACH PARTITION pitest1_p2 FOR VALUES FROM ('2016-08-01') TO ('2016-09-01'); INSERT INTO pitest1_p2 (f1, f2) VALUES ('2016-08-3', 'from pitest1_p2'); diff --git a/src/test/regress/expected/index_including.out b/src/test/regress/expected/index_including.out index ea8b2454bf..4e8fe49c8c 100644 --- a/src/test/regress/expected/index_including.out +++ b/src/test/regress/expected/index_including.out @@ -113,7 +113,7 @@ SELECT indexrelid::regclass, indnatts, indnkeyatts, indisunique, indisprimary, i covering | 4 | 2 | t | t | 1 2 3 4 | 1978 1978 (1 row) -SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid AND contype = 'p'; pg_get_constraintdef | conname | conkey ---------------------------------------+----------+-------- PRIMARY KEY (c1, c2) INCLUDE (c3, c4) | covering | {1,2} @@ -191,7 +191,7 @@ SELECT indexrelid::regclass, indnatts, indnkeyatts, indisunique, indisprimary, i tbl_pkey | 4 | 2 | t | t | 1 2 3 4 | 1978 1978 (1 row) -SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid AND contype = 'p'; pg_get_constraintdef | conname | conkey ---------------------------------------+----------+-------- PRIMARY KEY (c1, c2) INCLUDE (c3, c4) | tbl_pkey | {1,2} diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out index 69becce19b..bcf1db11d7 100644 --- a/src/test/regress/expected/indexing.out +++ b/src/test/regress/expected/indexing.out @@ -1117,15 +1117,27 @@ alter table idxpart attach partition idxpart3 for values from (20, 20) to (30, 3 select conname, contype, conrelid::regclass, conindid::regclass, conkey from pg_constraint where conrelid::regclass::text like 'idxpart%' order by conrelid::regclass::text, conname; - conname | contype | conrelid | conindid | conkey -----------------+---------+-----------+----------------+-------- - idxpart_pkey | p | idxpart | idxpart_pkey | {1,2} - idxpart1_pkey | p | idxpart1 | idxpart1_pkey | {1,2} - idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2} - idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2} - idxpart22_pkey | p | idxpart22 | idxpart22_pkey | {1,2} - idxpart3_pkey | p | idxpart3 | idxpart3_pkey | {2,1} -(6 rows) + conname | contype | conrelid | conindid | conkey +---------------------+---------+-----------+----------------+-------- + idxpart_a_not_null | n | idxpart | - | {1} + idxpart_b_not_null | n | idxpart | - | {2} + idxpart_pkey | p | idxpart | idxpart_pkey | {1,2} + idxpart1_pkey | p | idxpart1 | idxpart1_pkey | {1,2} + idxpart_a_not_null | n | idxpart1 | - | {1} + idxpart_b_not_null | n | idxpart1 | - | {2} + idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2} + idxpart_a_not_null | n | idxpart2 | - | {1} + idxpart_b_not_null | n | idxpart2 | - | {2} + idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2} + idxpart_a_not_null | n | idxpart21 | - | {1} + idxpart_b_not_null | n | idxpart21 | - | {2} + idxpart22_pkey | p | idxpart22 | idxpart22_pkey | {1,2} + idxpart_a_not_null | n | idxpart22 | - | {1} + idxpart_b_not_null | n | idxpart22 | - | {2} + idxpart3_a_not_null | n | idxpart3 | - | {2} + idxpart3_b_not_null | n | idxpart3 | - | {1} + idxpart3_pkey | p | idxpart3 | idxpart3_pkey | {2,1} +(18 rows) drop table idxpart; -- Verify that multi-layer partitioning honors the requirement that all @@ -1150,13 +1162,19 @@ create table idxpart2 partition of idxpart for values from (0) to (1000) partiti create table idxpart21 partition of idxpart2 for values from (0) to (1000); select conname, contype, conrelid::regclass, conindid::regclass, conkey from pg_constraint where conrelid::regclass::text like 'idxpart%' - order by conname; - conname | contype | conrelid | conindid | conkey -----------------+---------+-----------+----------------+-------- - idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2} - idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2} - idxpart_pkey | p | idxpart | idxpart_pkey | {1,2} -(3 rows) + order by conrelid::regclass::text, conname; + conname | contype | conrelid | conindid | conkey +--------------------+---------+-----------+----------------+-------- + idxpart_a_not_null | n | idxpart | - | {1} + idxpart_b_not_null | n | idxpart | - | {2} + idxpart_pkey | p | idxpart | idxpart_pkey | {1,2} + idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2} + idxpart_a_not_null | n | idxpart2 | - | {1} + idxpart_b_not_null | n | idxpart2 | - | {2} + idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2} + idxpart_a_not_null | n | idxpart21 | - | {1} + idxpart_b_not_null | n | idxpart21 | - | {2} +(9 rows) drop table idxpart; -- If a partitioned table has a unique/PK constraint, then it's not possible @@ -1259,14 +1277,10 @@ create table idxpart0 (like idxpart); alter table idxpart0 add unique (a); alter table idxpart attach partition idxpart0 default; alter table only idxpart add primary key (a); -- fail, no not-null constraint -ERROR: constraint must be added to child tables too -DETAIL: Column "a" of relation "idxpart0" is not already NOT NULL. -HINT: Do not specify the ONLY keyword. +ERROR: column "a" of table "idxpart0" is not marked NOT NULL alter table idxpart0 alter column a set not null; alter table only idxpart add primary key (a); -- now it works alter index idxpart_pkey attach partition idxpart0_a_key; -alter table idxpart0 alter column a drop not null; -- fail, pkey needs it -ERROR: column "a" is marked NOT NULL in parent table drop table idxpart; -- if a partition has a unique index without a constraint, does not attach -- automatically; creates a new index instead. diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out index 482fc47933..bb81f6d2b4 100644 --- a/src/test/regress/expected/inherit.out +++ b/src/test/regress/expected/inherit.out @@ -539,6 +539,9 @@ CREATE TEMP TABLE z (b TEXT, PRIMARY KEY(aa, b)) inherits (a); INSERT INTO z VALUES (NULL, 'text'); -- should fail ERROR: null value in column "aa" of relation "z" violates not-null constraint DETAIL: Failing row contains (null, text). +-- ... but not UNIQUE. +CREATE TEMP TABLE z2 (b TEXT, UNIQUE(aa, b)) inherits (a); +INSERT INTO z2 VALUES (NULL, 'text'); -- should work -- Check inherited UPDATE with first child excluded create table some_tab (f1 int, f2 int, f3 int, check (f1 < 10) no inherit); create table some_tab_child () inherits(some_tab); @@ -1252,6 +1255,8 @@ Indexes: "test_primary_constraints_pkey" PRIMARY KEY, btree (id) Referenced by: TABLE "test_foreign_constraints" CONSTRAINT "test_foreign_constraints_id1_fkey" FOREIGN KEY (id1) REFERENCES test_primary_constraints(id) +Not-null constraints: + "test_primary_constraints_id_not_null" NOT NULL "id" \d+ test_foreign_constraints Table "public.test_foreign_constraints" @@ -2054,6 +2059,613 @@ select * from cnullparent where f1 = 2; drop table cnullparent cascade; NOTICE: drop cascades to table cnullchild -- +-- Test inheritance of NOT NULL constraints +-- +create table pp1 (f1 int); +create table cc1 (f2 text, f3 int) inherits (pp1); +create table cc2 (f4 float) inherits (pp1,cc1); +NOTICE: merging multiple inherited definitions of column "f1" +create table cc3 () inherits (pp1,cc1,cc2); +NOTICE: merging multiple inherited definitions of column "f1" +NOTICE: merging multiple inherited definitions of column "f1" +NOTICE: merging multiple inherited definitions of column "f2" +NOTICE: merging multiple inherited definitions of column "f3" +alter table pp1 alter f1 set not null; +\d+ cc3 + Table "public.cc3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) +Inherits: pp1, + cc1, + cc2 + +alter table cc3 no inherit pp1; +alter table cc3 no inherit cc1; +alter table cc3 no inherit cc2; +\d+ cc3 + Table "public.cc3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" + +drop table cc3; +-- named NOT NULL constraint +alter table cc1 add column a2 int constraint nn not null; +\d+ cc1 + Table "public.cc1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + a2 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) + "nn" NOT NULL "a2" +Inherits: pp1 +Child tables: cc2 + +\d+ cc2 + Table "public.cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | + a2 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) + "nn" NOT NULL "a2" (inherited) +Inherits: pp1, + cc1 + +alter table pp1 alter column f1 set not null; +\d+ pp1 + Table "public.pp1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" +Child tables: cc1, + cc2 + +\d+ cc1 + Table "public.cc1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + a2 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) + "nn" NOT NULL "a2" +Inherits: pp1 +Child tables: cc2 + +\d+ cc2 + Table "public.cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | + a2 | integer | | not null | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) + "nn" NOT NULL "a2" (inherited) +Inherits: pp1, + cc1 + +-- cannot create table with inconsistent NO INHERIT constraint +create table cc3 (a2 int not null no inherit) inherits (cc1); +NOTICE: moving and merging column "a2" with inherited definition +DETAIL: User-specified column moved to the position of the inherited column. +ERROR: cannot define not-null constraint on column "a2" with NO INHERIT +DETAIL: The column has an inherited not-null constraint. +-- change NO INHERIT status of inherited constraint: no dice, it's inherited +alter table cc2 add not null a2 no inherit; +ERROR: cannot change NO INHERIT status of NOT NULL constraint "nn" on relation "cc2" +-- remove constraint from cc2: no dice, it's inherited +alter table cc2 alter column a2 drop not null; +ERROR: cannot drop inherited constraint "nn" of relation "cc2" +-- remove constraint from cc1, should succeed +alter table cc1 alter column a2 drop not null; +\d+ cc1 + Table "public.cc1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + a2 | integer | | | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) +Inherits: pp1 +Child tables: cc2 + +-- same for cc2 +alter table cc2 alter column f1 drop not null; +ERROR: cannot drop inherited constraint "pp1_f1_not_null" of relation "cc2" +\d+ cc2 + Table "public.cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | + a2 | integer | | | | plain | | +Not-null constraints: + "pp1_f1_not_null" NOT NULL "f1" (inherited) +Inherits: pp1, + cc1 + +-- remove from cc1, should fail again +alter table cc1 alter column f1 drop not null; +ERROR: cannot drop inherited constraint "pp1_f1_not_null" of relation "cc1" +-- remove from pp1, should succeed +alter table pp1 alter column f1 drop not null; +\d+ pp1 + Table "public.pp1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | | | plain | | +Child tables: cc1, + cc2 + +alter table pp1 add primary key (f1); +-- Leave these tables around, for pg_upgrade testing +-- test that removing inheritance of NOT NULL NO INHERIT works correctly +create table inh_parent (f1 int not null no inherit, f2 int not null no inherit); +create table inh_child (f1 int not null no inherit, f2 int); +alter table inh_child inherit inh_parent; +alter table inh_child no inherit inh_parent; +\d+ inh_child + Table "public.inh_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | integer | | | | plain | | +Not-null constraints: + "inh_child_f1_not_null" NOT NULL "f1" NO INHERIT + +drop table inh_parent, inh_child; +-- test that inhcount is updated correctly through multiple inheritance +create table inh_pp1 (f1 int); +create table inh_cc1 (f2 text, f3 int) inherits (inh_pp1); +create table inh_cc2(f4 float) inherits(inh_pp1,inh_cc1); +NOTICE: merging multiple inherited definitions of column "f1" +alter table inh_pp1 alter column f1 set not null; +alter table inh_cc2 no inherit inh_pp1; +alter table inh_cc2 no inherit inh_cc1; +\d+ inh_cc2 + Table "public.inh_cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | not null | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | +Not-null constraints: + "inh_pp1_f1_not_null" NOT NULL "f1" + +drop table inh_pp1, inh_cc1, inh_cc2; +create table inh_pp1 (f1 int not null); +create table inh_cc1 (f2 text, f3 int) inherits (inh_pp1); +create table inh_cc2(f4 float) inherits(inh_pp1,inh_cc1); +NOTICE: merging multiple inherited definitions of column "f1" +alter table inh_pp1 alter column f1 drop not null; +\d+ inh_cc2 + Table "public.inh_cc2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+------------------+-----------+----------+---------+----------+--------------+------------- + f1 | integer | | | | plain | | + f2 | text | | | | extended | | + f3 | integer | | | | plain | | + f4 | double precision | | | | plain | | +Inherits: inh_pp1, + inh_cc1 + +drop table inh_pp1, inh_cc1, inh_cc2; +-- Test a not-null addition that must walk down the hierarchy +CREATE TABLE inh_parent (); +CREATE TABLE inh_child (i int) INHERITS (inh_parent); +CREATE TABLE inh_grandchild () INHERITS (inh_parent, inh_child); +ALTER TABLE inh_parent ADD COLUMN i int NOT NULL; +NOTICE: merging definition of column "i" for child "inh_child" +NOTICE: merging definition of column "i" for child "inh_grandchild" +drop table inh_parent, inh_child, inh_grandchild; +-- Test the same constraint name for different columns in different parents +create table inh_parent1(a int constraint nn not null); +create table inh_parent2(b int constraint nn not null); +create table inh_child1 () inherits (inh_parent1, inh_parent2); +\d+ inh_child1 + Table "public.inh_child1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "nn" NOT NULL "a" (inherited) + "inh_child1_b_not_null" NOT NULL "b" (inherited) +Inherits: inh_parent1, + inh_parent2 + +create table inh_child2 (constraint foo not null a) inherits (inh_parent1, inh_parent2); +alter table inh_child2 no inherit inh_parent2; +\d+ inh_child2 + Table "public.inh_child2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | + b | integer | | not null | | plain | | +Not-null constraints: + "foo" NOT NULL "a" (local, inherited) + "nn" NOT NULL "b" +Inherits: inh_parent1 + +drop table inh_parent1, inh_parent2, inh_child1, inh_child2; +-- Test multiple parents with overlapping primary keys +create table inh_parent1(a int, b int, c int, primary key (a, b)); +create table inh_parent2(d int, e int, b int, primary key (d, b)); +create table inh_child() inherits (inh_parent1, inh_parent2); +NOTICE: merging multiple inherited definitions of column "b" +select conrelid::regclass, conname, contype, conkey, + coninhcount, conislocal, connoinherit + from pg_constraint where contype in ('n','p') and + conrelid::regclass::text in ('inh_child', 'inh_parent1', 'inh_parent2') + order by 1, 2; + conrelid | conname | contype | conkey | coninhcount | conislocal | connoinherit +-------------+------------------------+---------+--------+-------------+------------+-------------- + inh_parent1 | inh_parent1_a_not_null | n | {1} | 0 | t | f + inh_parent1 | inh_parent1_b_not_null | n | {2} | 0 | t | f + inh_parent1 | inh_parent1_pkey | p | {1,2} | 0 | t | t + inh_parent2 | inh_parent2_b_not_null | n | {3} | 0 | t | f + inh_parent2 | inh_parent2_d_not_null | n | {1} | 0 | t | f + inh_parent2 | inh_parent2_pkey | p | {1,3} | 0 | t | t + inh_child | inh_parent1_a_not_null | n | {1} | 1 | f | f + inh_child | inh_parent1_b_not_null | n | {2} | 2 | f | f + inh_child | inh_parent2_d_not_null | n | {4} | 1 | f | f +(9 rows) + +\d+ inh_child + Table "public.inh_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | + b | integer | | not null | | plain | | + c | integer | | | | plain | | + d | integer | | not null | | plain | | + e | integer | | | | plain | | +Not-null constraints: + "inh_parent1_a_not_null" NOT NULL "a" (inherited) + "inh_parent1_b_not_null" NOT NULL "b" (inherited) + "inh_parent2_d_not_null" NOT NULL "d" (inherited) +Inherits: inh_parent1, + inh_parent2 + +drop table inh_parent1, inh_parent2, inh_child; +-- NOT NULL NO INHERIT +create table inh_nn_parent(a int); +create table inh_nn_child() inherits (inh_nn_parent); +alter table inh_nn_parent add not null a no inherit; +create table inh_nn_child2() inherits (inh_nn_parent); +select conrelid::regclass, conname, contype, conkey, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal, connoinherit + from pg_constraint where contype = 'n' and + conrelid::regclass::text like 'inh\_nn\_%' + order by 2, 1; + conrelid | conname | contype | conkey | attname | coninhcount | conislocal | connoinherit +---------------+--------------------------+---------+--------+---------+-------------+------------+-------------- + inh_nn_parent | inh_nn_parent_a_not_null | n | {1} | a | 0 | t | t +(1 row) + +\d+ inh_nn* + Table "public.inh_nn_child" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: inh_nn_parent + + Table "public.inh_nn_child2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | | | plain | | +Inherits: inh_nn_parent + + Table "public.inh_nn_parent" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + a | integer | | not null | | plain | | +Not-null constraints: + "inh_nn_parent_a_not_null" NOT NULL "a" NO INHERIT +Child tables: inh_nn_child, + inh_nn_child2 + +drop table inh_nn_parent, inh_nn_child, inh_nn_child2; +CREATE TABLE inh_nn_parent (a int, NOT NULL a NO INHERIT); +CREATE TABLE inh_nn_child() INHERITS (inh_nn_parent); +ALTER TABLE inh_nn_parent ADD CONSTRAINT nna NOT NULL a; +ERROR: cannot change NO INHERIT status of NOT NULL constraint "inh_nn_parent_a_not_null" on relation "inh_nn_parent" +ALTER TABLE inh_nn_parent ALTER a SET NOT NULL; +ERROR: cannot change NO INHERIT status of NOT NULL constraint "inh_nn_parent_a_not_null" on relation "inh_nn_parent" +DROP TABLE inh_nn_parent cascade; +NOTICE: drop cascades to table inh_nn_child +-- Adding a PK at the top level of a hierarchy should cause all descendants +-- to be checked for nulls, even past a no-inherit constraint +CREATE TABLE inh_nn_lvl1 (a int); +CREATE TABLE inh_nn_lvl2 () INHERITS (inh_nn_lvl1); +CREATE TABLE inh_nn_lvl3 (CONSTRAINT foo NOT NULL a NO INHERIT) INHERITS (inh_nn_lvl2); +ALTER TABLE inh_nn_lvl1 ADD PRIMARY KEY (a); +ERROR: cannot change NO INHERIT status of NOT NULL constraint "foo" on relation "inh_nn_lvl3" +DROP TABLE inh_nn_lvl1, inh_nn_lvl2, inh_nn_lvl3; +-- Disallow specifying conflicting NO INHERIT flags for the same constraint +CREATE TABLE inh_nn1 (a int primary key, b int, not null a no inherit); +ERROR: conflicting NO INHERIT declaration for not-null constraint on column "a" +CREATE TABLE inh_nn1 (a int not null); +CREATE TABLE inh_nn2 (a int not null no inherit) INHERITS (inh_nn1); +NOTICE: merging column "a" with inherited definition +ERROR: cannot define not-null constraint on column "a" with NO INHERIT +DETAIL: The column has an inherited not-null constraint. +CREATE TABLE inh_nn3 (a int not null, b int, not null a no inherit); +ERROR: conflicting NO INHERIT declaration for not-null constraint on column "a" +CREATE TABLE inh_nn4 (a int not null no inherit, b int, not null a); +ERROR: conflicting NO INHERIT declaration for not-null constraint on column "a" +DROP TABLE inh_nn1, inh_nn2, inh_nn3, inh_nn4; +ERROR: table "inh_nn2" does not exist +-- +-- test inherit/deinherit +-- +create table inh_parent(f1 int); +create table inh_child1(f1 int not null); +create table inh_child2(f1 int); +-- inh_child1 should have not null constraint +alter table inh_child1 inherit inh_parent; +-- should fail, missing NOT NULL constraint +alter table inh_child2 inherit inh_child1; +ERROR: column "f1" in child table "inh_child2" must be marked NOT NULL +alter table inh_child2 alter column f1 set not null; +alter table inh_child2 inherit inh_child1; +-- add NOT NULL constraint recursively +alter table inh_parent alter column f1 set not null; +\d+ inh_parent + Table "public.inh_parent" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_parent_f1_not_null" NOT NULL "f1" +Child tables: inh_child1 + +\d+ inh_child1 + Table "public.inh_child1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_child1_f1_not_null" NOT NULL "f1" (local, inherited) +Inherits: inh_parent +Child tables: inh_child2 + +\d+ inh_child2 + Table "public.inh_child2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_child2_f1_not_null" NOT NULL "f1" (local, inherited) +Inherits: inh_child1 + +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass) + order by 2, 1; + conrelid | conname | contype | coninhcount | conislocal +------------+------------------------+---------+-------------+------------ + inh_child1 | inh_child1_f1_not_null | n | 1 | t + inh_child2 | inh_child2_f1_not_null | n | 1 | t + inh_parent | inh_parent_f1_not_null | n | 0 | t +(3 rows) + +-- +-- test deinherit procedure +-- +-- deinherit inh_child1 +create table inh_child3 () inherits (inh_child1); +alter table inh_child1 no inherit inh_parent; +\d+ inh_parent + Table "public.inh_parent" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_parent_f1_not_null" NOT NULL "f1" + +\d+ inh_child1 + Table "public.inh_child1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_child1_f1_not_null" NOT NULL "f1" +Child tables: inh_child2, + inh_child3 + +\d+ inh_child2 + Table "public.inh_child2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + f1 | integer | | not null | | plain | | +Not-null constraints: + "inh_child2_f1_not_null" NOT NULL "f1" (local, inherited) +Inherits: inh_child1 + +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass::text in ('inh_parent', 'inh_child1', 'inh_child2', 'inh_child3') + order by 2, 1; + conrelid | conname | contype | coninhcount | conislocal +------------+------------------------+---------+-------------+------------ + inh_child1 | inh_child1_f1_not_null | n | 0 | t + inh_child3 | inh_child1_f1_not_null | n | 1 | f + inh_child2 | inh_child2_f1_not_null | n | 1 | t + inh_parent | inh_parent_f1_not_null | n | 0 | t +(4 rows) + +drop table inh_parent, inh_child1, inh_child2, inh_child3; +-- ALTER TABLE INHERIT ensures that the child has not-null constraints +create table inh_parent (a int not null); +create table inh_child (a int); +alter table inh_child inherit inh_parent; -- nope +ERROR: column "a" in child table "inh_child" must be marked NOT NULL +drop table inh_parent, inh_child; +-- Can't merge a NO INHERIT constraint with a normal one +create table inh_parent (a int not null); +create table inh_child (a int not null no inherit); +alter table inh_child inherit inh_parent; +ERROR: constraint "inh_child_a_not_null" conflicts with non-inherited constraint on child table "inh_child" +drop table inh_parent, inh_child; +-- don't interfere with other types of constraints +create table inh_parent (a int primary key); +create table inh_child (a int primary key) inherits (inh_parent); +NOTICE: merging column "a" with inherited definition +alter table inh_parent add constraint inh_parent_excl exclude ((1) with =); +alter table inh_parent add constraint inh_parent_uq unique (a); +alter table inh_parent add constraint inh_parent_fk foreign key (a) references inh_parent (a); +create table inh_child2 () inherits (inh_parent); +create table inh_child3 (like inh_parent); +alter table inh_child3 inherit inh_parent; +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint + where conrelid::regclass::text in ('inh_parent', 'inh_child', 'inh_child2', 'inh_child3') + order by 2, 1; + conrelid | conname | contype | coninhcount | conislocal +------------+-----------------------+---------+-------------+------------ + inh_child | inh_child_a_not_null | n | 1 | t + inh_child | inh_child_pkey | p | 0 | t + inh_parent | inh_parent_a_not_null | n | 0 | t + inh_child2 | inh_parent_a_not_null | n | 1 | f + inh_child3 | inh_parent_a_not_null | n | 1 | t + inh_parent | inh_parent_excl | x | 0 | t + inh_parent | inh_parent_fk | f | 0 | t + inh_parent | inh_parent_pkey | p | 0 | t + inh_parent | inh_parent_uq | u | 0 | t +(9 rows) + +drop table inh_parent, inh_child, inh_child2, inh_child3; +-- +-- test multi inheritance tree +-- +create table inh_parent(f1 int not null); +create table inh_child1() inherits(inh_parent); +create table inh_child2() inherits(inh_parent); +create table inh_child3() inherits(inh_child1, inh_child2); +NOTICE: merging multiple inherited definitions of column "f1" +-- show constraint info +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass, 'inh_child3'::regclass) + order by 2, conrelid::regclass::text; + conrelid | conname | contype | coninhcount | conislocal +------------+------------------------+---------+-------------+------------ + inh_child1 | inh_parent_f1_not_null | n | 1 | f + inh_child2 | inh_parent_f1_not_null | n | 1 | f + inh_child3 | inh_parent_f1_not_null | n | 2 | f + inh_parent | inh_parent_f1_not_null | n | 0 | t +(4 rows) + +drop table inh_parent cascade; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table inh_child1 +drop cascades to table inh_child2 +drop cascades to table inh_child3 +-- test child table with inherited columns and +-- with explicitly specified not null constraints +create table inh_parent_1(f1 int); +create table inh_parent_2(f2 text); +create table inh_child(f1 int not null, f2 text not null) inherits(inh_parent_1, inh_parent_2); +NOTICE: merging column "f1" with inherited definition +NOTICE: merging column "f2" with inherited definition +-- show constraint info +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent_1'::regclass, 'inh_parent_2'::regclass, 'inh_child'::regclass) + order by 2, conrelid::regclass::text; + conrelid | conname | contype | coninhcount | conislocal +-----------+-----------------------+---------+-------------+------------ + inh_child | inh_child_f1_not_null | n | 0 | t + inh_child | inh_child_f2_not_null | n | 0 | t +(2 rows) + +-- also drops inh_child table +drop table inh_parent_1 cascade; +NOTICE: drop cascades to table inh_child +drop table inh_parent_2; +-- test multi layer inheritance tree +create table inh_p1(f1 int not null); +create table inh_p2(f1 int not null); +create table inh_p3(f2 int); +create table inh_p4(f1 int not null, f3 text not null); +create table inh_multiparent() inherits(inh_p1, inh_p2, inh_p3, inh_p4); +NOTICE: merging multiple inherited definitions of column "f1" +NOTICE: merging multiple inherited definitions of column "f1" +-- constraint on f1 should have three parents +select conrelid::regclass, contype, conname, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass in ('inh_p1', 'inh_p2', 'inh_p3', 'inh_p4', + 'inh_multiparent') + order by conrelid::regclass::text, conname; + conrelid | contype | conname | attname | coninhcount | conislocal +-----------------+---------+--------------------+---------+-------------+------------ + inh_multiparent | n | inh_p1_f1_not_null | f1 | 3 | f + inh_multiparent | n | inh_p4_f3_not_null | f3 | 1 | f + inh_p1 | n | inh_p1_f1_not_null | f1 | 0 | t + inh_p2 | n | inh_p2_f1_not_null | f1 | 0 | t + inh_p4 | n | inh_p4_f1_not_null | f1 | 0 | t + inh_p4 | n | inh_p4_f3_not_null | f3 | 0 | t +(6 rows) + +create table inh_multiparent2 (a int not null, f1 int) inherits(inh_p3, inh_multiparent); +NOTICE: merging multiple inherited definitions of column "f2" +NOTICE: merging column "f1" with inherited definition +select conrelid::regclass, contype, conname, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass in ('inh_p3', 'inh_multiparent', 'inh_multiparent2') + order by conrelid::regclass::text, conname; + conrelid | contype | conname | attname | coninhcount | conislocal +------------------+---------+-----------------------------+---------+-------------+------------ + inh_multiparent | n | inh_p1_f1_not_null | f1 | 3 | f + inh_multiparent | n | inh_p4_f3_not_null | f3 | 1 | f + inh_multiparent2 | n | inh_multiparent2_a_not_null | a | 0 | t + inh_multiparent2 | n | inh_p1_f1_not_null | f1 | 1 | f + inh_multiparent2 | n | inh_p4_f3_not_null | f3 | 1 | f +(5 rows) + +drop table inh_p1, inh_p2, inh_p3, inh_p4 cascade; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table inh_multiparent +drop cascades to table inh_multiparent2 +-- -- Mixed ownership inheritance tree -- create role regress_alice; diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out index a8949ffc2c..5de2d64d01 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -199,6 +199,8 @@ Indexes: "testpub_tbl2_pkey" PRIMARY KEY, btree (id) Publications: "testpub_foralltables" +Not-null constraints: + "testpub_tbl2_id_not_null" NOT NULL "id" \dRp+ testpub_foralltables Publication testpub_foralltables @@ -777,6 +779,8 @@ Indexes: "testpub_tbl7_pkey" PRIMARY KEY, btree (a) Publications: "testpub_fortable" (a, b) +Not-null constraints: + "testpub_tbl7_a_not_null" NOT NULL "a" -- ok: the column list is the same, we should skip this table (or at least not fail) ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, b); @@ -791,6 +795,8 @@ Indexes: "testpub_tbl7_pkey" PRIMARY KEY, btree (a) Publications: "testpub_fortable" (a, b) +Not-null constraints: + "testpub_tbl7_a_not_null" NOT NULL "a" -- ok: the column list changes, make sure the catalog gets updated ALTER PUBLICATION testpub_fortable SET TABLE testpub_tbl7 (a, c); @@ -805,6 +811,8 @@ Indexes: "testpub_tbl7_pkey" PRIMARY KEY, btree (a) Publications: "testpub_fortable" (a, c) +Not-null constraints: + "testpub_tbl7_a_not_null" NOT NULL "a" -- column list for partitioned tables has to cover replica identities for -- all child relations @@ -941,6 +949,9 @@ Indexes: "testpub_tbl_both_filters_pkey" PRIMARY KEY, btree (a, c) REPLICA IDENTITY Publications: "testpub_both_filters" (a, c) WHERE (c <> 1) +Not-null constraints: + "testpub_tbl_both_filters_a_not_null" NOT NULL "a" + "testpub_tbl_both_filters_c_not_null" NOT NULL "c" DROP TABLE testpub_tbl_both_filters; DROP PUBLICATION testpub_both_filters; @@ -1170,6 +1181,8 @@ Publications: "testpib_ins_trunct" "testpub_default" "testpub_fortbl" +Not-null constraints: + "testpub_tbl1_id_not_null" NOT NULL "id" \dRp+ testpub_default Publication testpub_default @@ -1195,6 +1208,8 @@ Indexes: Publications: "testpib_ins_trunct" "testpub_fortbl" +Not-null constraints: + "testpub_tbl1_id_not_null" NOT NULL "id" -- verify relation cache invalidation when a primary key is added using -- an existing index diff --git a/src/test/regress/expected/replica_identity.out b/src/test/regress/expected/replica_identity.out index e9d7315a9c..b9b8dde018 100644 --- a/src/test/regress/expected/replica_identity.out +++ b/src/test/regress/expected/replica_identity.out @@ -174,6 +174,10 @@ Indexes: "test_replica_identity_partial" UNIQUE, btree (keya, keyb) WHERE keyb <> '3'::text "test_replica_identity_unique_defer" UNIQUE CONSTRAINT, btree (keya, keyb) DEFERRABLE "test_replica_identity_unique_nondefer" UNIQUE CONSTRAINT, btree (keya, keyb) +Not-null constraints: + "test_replica_identity_id_not_null" NOT NULL "id" + "test_replica_identity_keya_not_null" NOT NULL "keya" + "test_replica_identity_keyb_not_null" NOT NULL "keyb" Replica Identity: FULL ALTER TABLE test_replica_identity REPLICA IDENTITY NOTHING; @@ -231,6 +235,9 @@ Indexes: -- used as replica identity. ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL; ERROR: column "id" is in index used as replica identity +-- but it's OK when the identity is FULL +ALTER TABLE test_replica_identity3 REPLICA IDENTITY FULL; +ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL; -- -- Test that replica identity can be set on an index that's not yet valid. -- (This matches the way pg_dump will try to dump a partitioned table.) @@ -253,6 +260,8 @@ ALTER TABLE ONLY test_replica_identity4_1 Partition key: LIST (id) Indexes: "test_replica_identity4_pkey" PRIMARY KEY, btree (id) INVALID REPLICA IDENTITY +Not-null constraints: + "test_replica_identity4_id_not_null" NOT NULL "id" Partitions: test_replica_identity4_1 FOR VALUES IN (1) ALTER INDEX test_replica_identity4_pkey @@ -265,11 +274,26 @@ ALTER INDEX test_replica_identity4_pkey Partition key: LIST (id) Indexes: "test_replica_identity4_pkey" PRIMARY KEY, btree (id) REPLICA IDENTITY +Not-null constraints: + "test_replica_identity4_id_not_null" NOT NULL "id" Partitions: test_replica_identity4_1 FOR VALUES IN (1) +-- Dropping the primary key is not allowed if that would leave the replica +-- identity as nullable +CREATE TABLE test_replica_identity5 (a int not null, b int, c int, + PRIMARY KEY (b, c)); +CREATE UNIQUE INDEX test_replica_identity5_a_b_key ON test_replica_identity5 (a, b); +ALTER TABLE test_replica_identity5 REPLICA IDENTITY USING INDEX test_replica_identity5_a_b_key; +ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey; +ALTER TABLE test_replica_identity5 ALTER b SET NOT NULL; +ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey; +ERROR: constraint "test_replica_identity5_pkey" of relation "test_replica_identity5" does not exist +ALTER TABLE test_replica_identity5 ALTER b DROP NOT NULL; +ERROR: column "b" is in index used as replica identity DROP TABLE test_replica_identity; DROP TABLE test_replica_identity2; DROP TABLE test_replica_identity3; DROP TABLE test_replica_identity4; +DROP TABLE test_replica_identity5; DROP TABLE test_replica_identity_othertable; DROP TABLE test_replica_identity_t3; diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out index 319190855b..4ccf98d8e9 100644 --- a/src/test/regress/expected/rowsecurity.out +++ b/src/test/regress/expected/rowsecurity.out @@ -955,6 +955,8 @@ Policies: POLICY "pp1r" AS RESTRICTIVE TO regress_rls_dave USING ((cid < 55)) +Not-null constraints: + "part_document_dlevel_not_null" NOT NULL "dlevel" Partitions: part_document_fiction FOR VALUES FROM (11) TO (12), part_document_nonfiction FOR VALUES FROM (99) TO (100), part_document_satire FOR VALUES FROM (55) TO (56) diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index c5dd43a15c..637e3dac38 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -920,14 +920,6 @@ insert into parent values (NULL); insert into child (a, b) values (NULL, 'foo'); alter table only parent alter a set not null; alter table child alter a set not null; -delete from parent; -alter table only parent alter a set not null; -insert into parent values (NULL); -alter table child alter a set not null; -insert into child (a, b) values (NULL, 'foo'); -delete from child; -alter table child alter a set not null; -insert into child (a, b) values (NULL, 'foo'); drop table child; drop table parent; @@ -2125,6 +2117,7 @@ DROP TABLE tt9; -- Check that comments on constraints and indexes are not lost at ALTER TABLE. CREATE TABLE comment_test ( id int, + constraint id_notnull_constraint not null id, positive_col int CHECK (positive_col > 0), indexed_col int, CONSTRAINT comment_test_pk PRIMARY KEY (id)); @@ -2134,6 +2127,7 @@ COMMENT ON COLUMN comment_test.id IS 'Column ''id'' on comment_test'; COMMENT ON INDEX comment_test_index IS 'Simple index on comment_test'; COMMENT ON CONSTRAINT comment_test_positive_col_check ON comment_test IS 'CHECK constraint on comment_test.positive_col'; COMMENT ON CONSTRAINT comment_test_pk ON comment_test IS 'PRIMARY KEY constraint of comment_test'; +COMMENT ON CONSTRAINT id_notnull_constraint ON comment_test IS 'NOT NULL constraint of comment_test'; COMMENT ON INDEX comment_test_pk IS 'Index backing the PRIMARY KEY of comment_test'; SELECT col_description('comment_test'::regclass, 1) as comment; @@ -2347,6 +2341,9 @@ CREATE TABLE atnotnull1 (); ALTER TABLE atnotnull1 ADD COLUMN a INT, ALTER a SET NOT NULL; +ALTER TABLE atnotnull1 + ADD COLUMN b INT, + ADD NOT NULL b; ALTER TABLE atnotnull1 ADD COLUMN c INT, ADD PRIMARY KEY (c); @@ -2487,6 +2484,14 @@ ALTER TABLE list_parted ATTACH PARTITION part_1 FOR VALUES IN (1); SELECT attislocal, attinhcount FROM pg_attribute WHERE attrelid = 'part_1'::regclass AND attnum > 0; SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_1'::regclass AND conname = 'check_a'; +-- check that NOT NULL NO INHERIT cannot be merged to a normal NOT NULL +CREATE TABLE part_fail (a int NOT NULL NO INHERIT, + b char(2) COLLATE "C", + CONSTRAINT check_a CHECK (a > 0) +); +ALTER TABLE list_parted ATTACH PARTITION part_fail FOR VALUES IN (2); +DROP TABLE part_fail; + -- check that the new partition won't overlap with an existing partition CREATE TABLE fail_part (LIKE part_1 INCLUDING CONSTRAINTS); ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); @@ -2837,6 +2842,12 @@ ALTER TABLE list_parted2 ALTER b SET NOT NULL, ADD CONSTRAINT check_a2 CHECK (a ALTER TABLE part_2 ALTER b DROP NOT NULL; ALTER TABLE part_2 DROP CONSTRAINT check_a2; +-- can't drop NOT NULL from under an invalid PK +CREATE TABLE list_parted3 (a int NOT NULL) PARTITION BY LIST (a); +CREATE TABLE list_parted3_1 PARTITION OF list_parted3 FOR VALUES IN (1); +ALTER TABLE ONLY list_parted3 ADD PRIMARY KEY (a); +ALTER TABLE ONLY list_parted3 DROP CONSTRAINT list_parted3_a_not_null; + -- Doesn't make sense to add NO INHERIT constraints on partitioned tables ALTER TABLE list_parted2 add constraint check_b2 check (b <> 'zz') NO INHERIT; @@ -2857,7 +2868,7 @@ ALTER TABLE list_parted DROP COLUMN b; SELECT * FROM list_parted; -- cleanup -DROP TABLE list_parted, list_parted2, range_parted; +DROP TABLE list_parted, list_parted2, range_parted, list_parted3; DROP TABLE fail_def_part; DROP TABLE hash_parted; diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql index e3e3bea709..e607eb1fdd 100644 --- a/src/test/regress/sql/constraints.sql +++ b/src/test/regress/sql/constraints.sql @@ -6,6 +6,7 @@ -- - PRIMARY KEY clauses -- - UNIQUE clauses -- - EXCLUDE clauses +-- - NOT NULL clauses -- -- directory paths are passed to us in environment variables @@ -597,6 +598,190 @@ ALTER TABLE deferred_excl ADD EXCLUDE (f1 WITH =); DROP TABLE deferred_excl; +-- verify constraints created for NOT NULL clauses +CREATE TABLE notnull_tbl1 (a INTEGER NOT NULL NOT NULL); +\d+ notnull_tbl1 +-- no-op +ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; +\d+ notnull_tbl1 +-- duplicate name +ALTER TABLE notnull_tbl1 ADD COLUMN b INT CONSTRAINT notnull_tbl1_a_not_null NOT NULL; +-- DROP NOT NULL gets rid of both the attnotnull flag and the constraint itself +ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL; +\d+ notnull_tbl1 +-- SET NOT NULL puts both back +ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; +\d+ notnull_tbl1 +-- Doing it twice doesn't create a redundant constraint +ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; +select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass; +-- Using the "table constraint" syntax also works +ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL; +ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a; +\d+ notnull_tbl1 +DROP TABLE notnull_tbl1; + +-- 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 +-- handle this separately from column constraints. +create table notnull_tbl1 (a int primary key constraint foo not null); +\d+ notnull_tbl1 +create table notnull_tbl2 (a serial, constraint foo not null a); +\d+ notnull_tbl2 +create table notnull_tbl3 (constraint foo not null a, a int generated by default as identity); +\d+ notnull_tbl3 +create table notnull_tbl4 (a int not null constraint foo not null); +\d+ notnull_tbl4 +create table notnull_tbl5 (a int constraint foo not null constraint foo not null); +\d+ notnull_tbl5 +create table notnull_tbl6 (like notnull_tbl1, constraint foo not null a); +\d+ notnull_tbl6 +drop table notnull_tbl2, notnull_tbl3, notnull_tbl4, notnull_tbl5, notnull_tbl6; + +-- error cases: +create table notnull_tbl_fail (a serial constraint foo not null constraint bar not null); +create table notnull_tbl_fail (a serial constraint foo not null no inherit constraint foo not null); +create table notnull_tbl_fail (a int constraint foo not null, constraint foo not null a no inherit); +create table notnull_tbl_fail (a serial constraint foo not null, constraint bar not null a); +create table notnull_tbl_fail (a serial, constraint foo not null a, constraint bar not null a); +create table notnull_tbl_fail (a serial, constraint foo not null a no inherit); +create table notnull_tbl_fail (a serial not null no inherit); +create table notnull_tbl_fail (like notnull_tbl1, constraint foo2 not null a); +create table notnull_tbl_fail (a int primary key constraint foo not null no inherit); +create table notnull_tbl_fail (a int not null no inherit primary key); +create table notnull_tbl_fail (a int primary key, not null a no inherit); +create table notnull_tbl_fail (a int, primary key(a), not null a no inherit); +create table notnull_tbl_fail (a int generated by default as identity, constraint foo not null a no inherit); +create table notnull_tbl_fail (a int generated by default as identity not null no inherit); + +drop table notnull_tbl1; + +-- NOT NULL NO INHERIT +CREATE TABLE ATACC1 (a int, NOT NULL a NO INHERIT); +CREATE TABLE ATACC2 () INHERITS (ATACC1); +\d+ ATACC2 +DROP TABLE ATACC1, ATACC2; +CREATE TABLE ATACC1 (a int); +ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT; +CREATE TABLE ATACC2 () INHERITS (ATACC1); +\d+ ATACC2 +DROP TABLE ATACC1, ATACC2; +CREATE TABLE ATACC1 (a int); +CREATE TABLE ATACC2 () INHERITS (ATACC1); +ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT; +\d+ ATACC2 +CREATE TABLE ATACC3 (PRIMARY KEY (a)) INHERITS (ATACC1); +\d+ ATACC3 +DROP TABLE ATACC1, ATACC2, ATACC3; + +-- NOT NULL NO INHERIT is not possible on partitioned tables +CREATE TABLE ATACC1 (a int NOT NULL NO INHERIT) PARTITION BY LIST (a); +CREATE TABLE ATACC1 (a int, NOT NULL a NO INHERIT) PARTITION BY LIST (a); + +-- it's not possible to override a no-inherit constraint with an inheritable one +CREATE TABLE ATACC2 (a int, CONSTRAINT a_is_not_null NOT NULL a NO INHERIT); +CREATE TABLE ATACC1 (a int); +CREATE TABLE ATACC3 (a int) INHERITS (ATACC2); +ALTER TABLE ATACC2 INHERIT ATACC1; +-- can't override +ALTER TABLE ATACC1 ADD CONSTRAINT ditto NOT NULL a; +-- dropping the NO INHERIT constraint allows this to work +ALTER TABLE ATACC2 DROP CONSTRAINT a_is_not_null; +ALTER TABLE ATACC1 ADD CONSTRAINT ditto NOT NULL a; +\d+ ATACC3 +DROP TABLE ATACC1, ATACC2, ATACC3; + +-- Can't have two constraints with the same name +CREATE TABLE notnull_tbl2 (a INTEGER CONSTRAINT blah NOT NULL, b INTEGER CONSTRAINT blah NOT NULL); + +-- can't drop not-null in primary key +CREATE TABLE notnull_tbl2 (a INTEGER PRIMARY KEY); +ALTER TABLE notnull_tbl2 ALTER a DROP NOT NULL; +DROP TABLE notnull_tbl2; + +CREATE TABLE notnull_tbl3 (a INTEGER NOT NULL, CHECK (a IS NOT NULL)); +ALTER TABLE notnull_tbl3 ALTER A DROP NOT NULL; +ALTER TABLE notnull_tbl3 ADD b int, ADD CONSTRAINT pk PRIMARY KEY (a, b); +\d notnull_tbl3 +ALTER TABLE notnull_tbl3 DROP CONSTRAINT pk; +\d notnull_tbl3 + +-- Primary keys cause not-null constraints to be created. +CREATE TABLE cnn_pk (a int, b int); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +ALTER TABLE cnn_pk ADD CONSTRAINT cnn_primarykey PRIMARY KEY (b); +\d+ cnn_pk* +ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; +\d+ cnn_pk* +DROP TABLE cnn_pk, cnn_pk_child; + +-- As above, but create the primary key ahead of time +CREATE TABLE cnn_pk (a int, b int, CONSTRAINT cnn_primarykey PRIMARY KEY (b)); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +\d+ cnn_pk* +ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; +\d+ cnn_pk* +DROP TABLE cnn_pk, cnn_pk_child; + +-- As above, but create the primary key using a UNIQUE index +CREATE TABLE cnn_pk (a int, b int); +CREATE UNIQUE INDEX cnn_uq ON cnn_pk (b); +CREATE TABLE cnn_pk_child () INHERITS (cnn_pk); +ALTER TABLE cnn_pk ADD CONSTRAINT cnn_primarykey PRIMARY KEY USING INDEX cnn_uq; +\d+ cnn_pk* +DROP TABLE cnn_pk, cnn_pk_child; + +-- Unique constraints don't give raise to not-null constraints, however. +create table cnn_uq (a int); +alter table cnn_uq add unique (a); +\d+ cnn_uq +drop table cnn_uq; +create table cnn_uq (a int); +create unique index cnn_uq_idx on cnn_uq (a); +alter table cnn_uq add unique using index cnn_uq_idx; +\d+ cnn_uq + +-- Ensure partitions are scanned for null values when adding a PK +create table cnn2_parted(a int) partition by list (a); +create table cnn_part1 partition of cnn2_parted for values in (1, null); +insert into cnn_part1 values (null); +alter table cnn2_parted add primary key (a); +drop table cnn2_parted; + +-- columns in regular and LIKE inheritance should be marked not-nullable +-- for primary keys, even if those are deferred +CREATE TABLE notnull_tbl4 (a INTEGER PRIMARY KEY INITIALLY DEFERRED); +CREATE TABLE notnull_tbl4_lk (LIKE notnull_tbl4); +CREATE TABLE notnull_tbl4_lk2 (LIKE notnull_tbl4 INCLUDING INDEXES); +CREATE TABLE notnull_tbl4_lk3 (LIKE notnull_tbl4 INCLUDING INDEXES, NOT NULL a); +ALTER TABLE notnull_tbl4_lk3 RENAME CONSTRAINT notnull_tbl4_a_not_null TO a_nn; +CREATE TABLE notnull_tbl4_cld () INHERITS (notnull_tbl4); +CREATE TABLE notnull_tbl4_cld2 (PRIMARY KEY (a) DEFERRABLE) INHERITS (notnull_tbl4); +CREATE TABLE notnull_tbl4_cld3 (PRIMARY KEY (a) DEFERRABLE, CONSTRAINT a_nn NOT NULL a) INHERITS (notnull_tbl4); +\d+ notnull_tbl4 +\d+ notnull_tbl4_lk +\d+ notnull_tbl4_lk2 +\d+ notnull_tbl4_lk3 +\d+ notnull_tbl4_cld +\d+ notnull_tbl4_cld2 +\d+ notnull_tbl4_cld3 +-- leave these tables around for pg_upgrade testing + +-- It's possible to remove a constraint from parents without affecting children +CREATE TABLE notnull_tbl5 (a int CONSTRAINT ann NOT NULL, + b int CONSTRAINT bnn NOT NULL); +CREATE TABLE notnull_tbl5_child () INHERITS (notnull_tbl5); +ALTER TABLE ONLY notnull_tbl5 DROP CONSTRAINT ann; +ALTER TABLE ONLY notnull_tbl5 ALTER b DROP NOT NULL; +\d+ notnull_tbl5_child +CREATE TABLE notnull_tbl6 (a int CONSTRAINT ann NOT NULL, + b int CONSTRAINT bnn NOT NULL, check (a > 0)) PARTITION BY LIST (a); +CREATE TABLE notnull_tbl6_1 PARTITION OF notnull_tbl6 FOR VALUES IN (1); +ALTER TABLE ONLY notnull_tbl6 DROP CONSTRAINT ann; +ALTER TABLE ONLY notnull_tbl6 ALTER b DROP NOT NULL; +\d+ notnull_tbl6_1 + -- Comments -- Setup a low-level role to enforce non-superuser checks. CREATE ROLE regress_constraint_comments; diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql index 04008a027b..dea8942c71 100644 --- a/src/test/regress/sql/create_table_like.sql +++ b/src/test/regress/sql/create_table_like.sql @@ -194,9 +194,10 @@ ROLLBACK; DROP TABLE ctlt1, ctlt2, ctlt3, ctlt4, ctlt12_storage, ctlt12_comments, ctlt1_inh, ctlt13_inh, ctlt13_like, ctlt_all, ctla, ctlb CASCADE; -- LIKE must respect NO INHERIT property of constraints -CREATE TABLE noinh_con_copy (a int CHECK (a > 0) NO INHERIT); +CREATE TABLE noinh_con_copy (a int CHECK (a > 0) NO INHERIT, b int not null, + c int not null no inherit); CREATE TABLE noinh_con_copy1 (LIKE noinh_con_copy INCLUDING CONSTRAINTS); -\d noinh_con_copy1 +\d+ noinh_con_copy1 -- fail, as partitioned tables don't allow NO INHERIT constraints CREATE TABLE noinh_con_copy1_parted (LIKE noinh_con_copy INCLUDING ALL) diff --git a/src/test/regress/sql/index_including.sql b/src/test/regress/sql/index_including.sql index 11c95974ec..43bb6ea585 100644 --- a/src/test/regress/sql/index_including.sql +++ b/src/test/regress/sql/index_including.sql @@ -68,7 +68,7 @@ DROP TABLE tbl; CREATE TABLE tbl (c1 int,c2 int, c3 int, c4 box, CONSTRAINT covering PRIMARY KEY(c1,c2) INCLUDE(c3,c4)); SELECT indexrelid::regclass, indnatts, indnkeyatts, indisunique, indisprimary, indkey, indclass FROM pg_index WHERE indrelid = 'tbl'::regclass::oid; -SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid AND contype = 'p'; -- ensure that constraint works INSERT INTO tbl SELECT 1, 2, 3*x, box('4,4,4,4') FROM generate_series(1,10) AS x; INSERT INTO tbl SELECT 1, NULL, 3*x, box('4,4,4,4') FROM generate_series(1,10) AS x; @@ -95,7 +95,7 @@ DROP TABLE tbl; CREATE TABLE tbl (c1 int,c2 int, c3 int, c4 box, PRIMARY KEY(c1,c2) INCLUDE(c3,c4)); SELECT indexrelid::regclass, indnatts, indnkeyatts, indisunique, indisprimary, indkey, indclass FROM pg_index WHERE indrelid = 'tbl'::regclass::oid; -SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'tbl'::regclass::oid AND contype = 'p'; -- ensure that constraint works INSERT INTO tbl SELECT 1, 2, 3*x, box('4,4,4,4') FROM generate_series(1,10) AS x; INSERT INTO tbl SELECT 1, NULL, 3*x, box('4,4,4,4') FROM generate_series(1,10) AS x; diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql index 04834441db..b5cb01c2d7 100644 --- a/src/test/regress/sql/indexing.sql +++ b/src/test/regress/sql/indexing.sql @@ -592,7 +592,7 @@ create table idxpart2 partition of idxpart for values from (0) to (1000) partiti create table idxpart21 partition of idxpart2 for values from (0) to (1000); select conname, contype, conrelid::regclass, conindid::regclass, conkey from pg_constraint where conrelid::regclass::text like 'idxpart%' - order by conname; + order by conrelid::regclass::text, conname; drop table idxpart; -- If a partitioned table has a unique/PK constraint, then it's not possible @@ -671,7 +671,6 @@ alter table only idxpart add primary key (a); -- fail, no not-null constraint alter table idxpart0 alter column a set not null; alter table only idxpart add primary key (a); -- now it works alter index idxpart_pkey attach partition idxpart0_a_key; -alter table idxpart0 alter column a drop not null; -- fail, pkey needs it drop table idxpart; -- if a partition has a unique index without a constraint, does not attach diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql index 51251b0e51..f51c70d6b0 100644 --- a/src/test/regress/sql/inherit.sql +++ b/src/test/regress/sql/inherit.sql @@ -96,6 +96,9 @@ SELECT relname, d.* FROM ONLY d, pg_class where d.tableoid = pg_class.oid; -- Confirm PRIMARY KEY adds NOT NULL constraint to child table CREATE TEMP TABLE z (b TEXT, PRIMARY KEY(aa, b)) inherits (a); INSERT INTO z VALUES (NULL, 'text'); -- should fail +-- ... but not UNIQUE. +CREATE TEMP TABLE z2 (b TEXT, UNIQUE(aa, b)) inherits (a); +INSERT INTO z2 VALUES (NULL, 'text'); -- should work -- Check inherited UPDATE with first child excluded create table some_tab (f1 int, f2 int, f3 int, check (f1 < 10) no inherit); @@ -767,6 +770,281 @@ select * from cnullparent; select * from cnullparent where f1 = 2; drop table cnullparent cascade; +-- +-- Test inheritance of NOT NULL constraints +-- +create table pp1 (f1 int); +create table cc1 (f2 text, f3 int) inherits (pp1); +create table cc2 (f4 float) inherits (pp1,cc1); +create table cc3 () inherits (pp1,cc1,cc2); +alter table pp1 alter f1 set not null; +\d+ cc3 +alter table cc3 no inherit pp1; +alter table cc3 no inherit cc1; +alter table cc3 no inherit cc2; +\d+ cc3 +drop table cc3; + +-- named NOT NULL constraint +alter table cc1 add column a2 int constraint nn not null; +\d+ cc1 +\d+ cc2 +alter table pp1 alter column f1 set not null; +\d+ pp1 +\d+ cc1 +\d+ cc2 + +-- cannot create table with inconsistent NO INHERIT constraint +create table cc3 (a2 int not null no inherit) inherits (cc1); + +-- change NO INHERIT status of inherited constraint: no dice, it's inherited +alter table cc2 add not null a2 no inherit; + +-- remove constraint from cc2: no dice, it's inherited +alter table cc2 alter column a2 drop not null; + +-- remove constraint from cc1, should succeed +alter table cc1 alter column a2 drop not null; +\d+ cc1 + +-- same for cc2 +alter table cc2 alter column f1 drop not null; +\d+ cc2 + +-- remove from cc1, should fail again +alter table cc1 alter column f1 drop not null; + +-- remove from pp1, should succeed +alter table pp1 alter column f1 drop not null; +\d+ pp1 + +alter table pp1 add primary key (f1); +-- Leave these tables around, for pg_upgrade testing + +-- test that removing inheritance of NOT NULL NO INHERIT works correctly +create table inh_parent (f1 int not null no inherit, f2 int not null no inherit); +create table inh_child (f1 int not null no inherit, f2 int); +alter table inh_child inherit inh_parent; +alter table inh_child no inherit inh_parent; +\d+ inh_child +drop table inh_parent, inh_child; + +-- test that inhcount is updated correctly through multiple inheritance +create table inh_pp1 (f1 int); +create table inh_cc1 (f2 text, f3 int) inherits (inh_pp1); +create table inh_cc2(f4 float) inherits(inh_pp1,inh_cc1); +alter table inh_pp1 alter column f1 set not null; +alter table inh_cc2 no inherit inh_pp1; +alter table inh_cc2 no inherit inh_cc1; +\d+ inh_cc2 +drop table inh_pp1, inh_cc1, inh_cc2; + +create table inh_pp1 (f1 int not null); +create table inh_cc1 (f2 text, f3 int) inherits (inh_pp1); +create table inh_cc2(f4 float) inherits(inh_pp1,inh_cc1); +alter table inh_pp1 alter column f1 drop not null; +\d+ inh_cc2 +drop table inh_pp1, inh_cc1, inh_cc2; + + +-- Test a not-null addition that must walk down the hierarchy +CREATE TABLE inh_parent (); +CREATE TABLE inh_child (i int) INHERITS (inh_parent); +CREATE TABLE inh_grandchild () INHERITS (inh_parent, inh_child); +ALTER TABLE inh_parent ADD COLUMN i int NOT NULL; +drop table inh_parent, inh_child, inh_grandchild; + +-- Test the same constraint name for different columns in different parents +create table inh_parent1(a int constraint nn not null); +create table inh_parent2(b int constraint nn not null); +create table inh_child1 () inherits (inh_parent1, inh_parent2); +\d+ inh_child1 + +create table inh_child2 (constraint foo not null a) inherits (inh_parent1, inh_parent2); +alter table inh_child2 no inherit inh_parent2; +\d+ inh_child2 + +drop table inh_parent1, inh_parent2, inh_child1, inh_child2; + +-- Test multiple parents with overlapping primary keys +create table inh_parent1(a int, b int, c int, primary key (a, b)); +create table inh_parent2(d int, e int, b int, primary key (d, b)); +create table inh_child() inherits (inh_parent1, inh_parent2); +select conrelid::regclass, conname, contype, conkey, + coninhcount, conislocal, connoinherit + from pg_constraint where contype in ('n','p') and + conrelid::regclass::text in ('inh_child', 'inh_parent1', 'inh_parent2') + order by 1, 2; +\d+ inh_child +drop table inh_parent1, inh_parent2, inh_child; + +-- NOT NULL NO INHERIT +create table inh_nn_parent(a int); +create table inh_nn_child() inherits (inh_nn_parent); +alter table inh_nn_parent add not null a no inherit; +create table inh_nn_child2() inherits (inh_nn_parent); +select conrelid::regclass, conname, contype, conkey, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal, connoinherit + from pg_constraint where contype = 'n' and + conrelid::regclass::text like 'inh\_nn\_%' + order by 2, 1; +\d+ inh_nn* +drop table inh_nn_parent, inh_nn_child, inh_nn_child2; + +CREATE TABLE inh_nn_parent (a int, NOT NULL a NO INHERIT); +CREATE TABLE inh_nn_child() INHERITS (inh_nn_parent); +ALTER TABLE inh_nn_parent ADD CONSTRAINT nna NOT NULL a; +ALTER TABLE inh_nn_parent ALTER a SET NOT NULL; +DROP TABLE inh_nn_parent cascade; + +-- Adding a PK at the top level of a hierarchy should cause all descendants +-- to be checked for nulls, even past a no-inherit constraint +CREATE TABLE inh_nn_lvl1 (a int); +CREATE TABLE inh_nn_lvl2 () INHERITS (inh_nn_lvl1); +CREATE TABLE inh_nn_lvl3 (CONSTRAINT foo NOT NULL a NO INHERIT) INHERITS (inh_nn_lvl2); +ALTER TABLE inh_nn_lvl1 ADD PRIMARY KEY (a); +DROP TABLE inh_nn_lvl1, inh_nn_lvl2, inh_nn_lvl3; + +-- Disallow specifying conflicting NO INHERIT flags for the same constraint +CREATE TABLE inh_nn1 (a int primary key, b int, not null a no inherit); +CREATE TABLE inh_nn1 (a int not null); +CREATE TABLE inh_nn2 (a int not null no inherit) INHERITS (inh_nn1); +CREATE TABLE inh_nn3 (a int not null, b int, not null a no inherit); +CREATE TABLE inh_nn4 (a int not null no inherit, b int, not null a); +DROP TABLE inh_nn1, inh_nn2, inh_nn3, inh_nn4; + +-- +-- test inherit/deinherit +-- +create table inh_parent(f1 int); +create table inh_child1(f1 int not null); +create table inh_child2(f1 int); + +-- inh_child1 should have not null constraint +alter table inh_child1 inherit inh_parent; + +-- should fail, missing NOT NULL constraint +alter table inh_child2 inherit inh_child1; + +alter table inh_child2 alter column f1 set not null; +alter table inh_child2 inherit inh_child1; + +-- add NOT NULL constraint recursively +alter table inh_parent alter column f1 set not null; + +\d+ inh_parent +\d+ inh_child1 +\d+ inh_child2 + +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass) + order by 2, 1; + +-- +-- test deinherit procedure +-- + +-- deinherit inh_child1 +create table inh_child3 () inherits (inh_child1); +alter table inh_child1 no inherit inh_parent; +\d+ inh_parent +\d+ inh_child1 +\d+ inh_child2 +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass::text in ('inh_parent', 'inh_child1', 'inh_child2', 'inh_child3') + order by 2, 1; +drop table inh_parent, inh_child1, inh_child2, inh_child3; + +-- ALTER TABLE INHERIT ensures that the child has not-null constraints +create table inh_parent (a int not null); +create table inh_child (a int); +alter table inh_child inherit inh_parent; -- nope +drop table inh_parent, inh_child; + +-- Can't merge a NO INHERIT constraint with a normal one +create table inh_parent (a int not null); +create table inh_child (a int not null no inherit); +alter table inh_child inherit inh_parent; +drop table inh_parent, inh_child; + +-- don't interfere with other types of constraints +create table inh_parent (a int primary key); +create table inh_child (a int primary key) inherits (inh_parent); +alter table inh_parent add constraint inh_parent_excl exclude ((1) with =); +alter table inh_parent add constraint inh_parent_uq unique (a); +alter table inh_parent add constraint inh_parent_fk foreign key (a) references inh_parent (a); +create table inh_child2 () inherits (inh_parent); +create table inh_child3 (like inh_parent); +alter table inh_child3 inherit inh_parent; +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint + where conrelid::regclass::text in ('inh_parent', 'inh_child', 'inh_child2', 'inh_child3') + order by 2, 1; + +drop table inh_parent, inh_child, inh_child2, inh_child3; + +-- +-- test multi inheritance tree +-- +create table inh_parent(f1 int not null); +create table inh_child1() inherits(inh_parent); +create table inh_child2() inherits(inh_parent); +create table inh_child3() inherits(inh_child1, inh_child2); + +-- show constraint info +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass, 'inh_child3'::regclass) + order by 2, conrelid::regclass::text; + +drop table inh_parent cascade; + +-- test child table with inherited columns and +-- with explicitly specified not null constraints +create table inh_parent_1(f1 int); +create table inh_parent_2(f2 text); +create table inh_child(f1 int not null, f2 text not null) inherits(inh_parent_1, inh_parent_2); + +-- show constraint info +select conrelid::regclass, conname, contype, coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid in ('inh_parent_1'::regclass, 'inh_parent_2'::regclass, 'inh_child'::regclass) + order by 2, conrelid::regclass::text; + +-- also drops inh_child table +drop table inh_parent_1 cascade; +drop table inh_parent_2; + +-- test multi layer inheritance tree +create table inh_p1(f1 int not null); +create table inh_p2(f1 int not null); +create table inh_p3(f2 int); +create table inh_p4(f1 int not null, f3 text not null); + +create table inh_multiparent() inherits(inh_p1, inh_p2, inh_p3, inh_p4); + +-- constraint on f1 should have three parents +select conrelid::regclass, contype, conname, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass in ('inh_p1', 'inh_p2', 'inh_p3', 'inh_p4', + 'inh_multiparent') + order by conrelid::regclass::text, conname; + +create table inh_multiparent2 (a int not null, f1 int) inherits(inh_p3, inh_multiparent); +select conrelid::regclass, contype, conname, + (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]), + coninhcount, conislocal + from pg_constraint where contype = 'n' and + conrelid::regclass in ('inh_p3', 'inh_multiparent', 'inh_multiparent2') + order by conrelid::regclass::text, conname; + +drop table inh_p1, inh_p2, inh_p3, inh_p4 cascade; + -- -- Mixed ownership inheritance tree -- diff --git a/src/test/regress/sql/replica_identity.sql b/src/test/regress/sql/replica_identity.sql index 039cca25e8..30daec05b7 100644 --- a/src/test/regress/sql/replica_identity.sql +++ b/src/test/regress/sql/replica_identity.sql @@ -100,6 +100,9 @@ ALTER TABLE test_replica_identity3 ALTER COLUMN id TYPE bigint; -- ALTER TABLE DROP NOT NULL is not allowed for columns part of an index -- used as replica identity. ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL; +-- but it's OK when the identity is FULL +ALTER TABLE test_replica_identity3 REPLICA IDENTITY FULL; +ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL; -- -- Test that replica identity can be set on an index that's not yet valid. @@ -120,9 +123,21 @@ ALTER INDEX test_replica_identity4_pkey ATTACH PARTITION test_replica_identity4_1_pkey; \d+ test_replica_identity4 +-- Dropping the primary key is not allowed if that would leave the replica +-- identity as nullable +CREATE TABLE test_replica_identity5 (a int not null, b int, c int, + PRIMARY KEY (b, c)); +CREATE UNIQUE INDEX test_replica_identity5_a_b_key ON test_replica_identity5 (a, b); +ALTER TABLE test_replica_identity5 REPLICA IDENTITY USING INDEX test_replica_identity5_a_b_key; +ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey; +ALTER TABLE test_replica_identity5 ALTER b SET NOT NULL; +ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey; +ALTER TABLE test_replica_identity5 ALTER b DROP NOT NULL; + DROP TABLE test_replica_identity; DROP TABLE test_replica_identity2; DROP TABLE test_replica_identity3; DROP TABLE test_replica_identity4; +DROP TABLE test_replica_identity5; DROP TABLE test_replica_identity_othertable; DROP TABLE test_replica_identity_t3;