From 4724bea119e674700dc8e7e4557702bdaf32a5c4 Mon Sep 17 00:00:00 2001 From: Alvaro Herrera Date: Tue, 31 May 2011 12:38:52 -0400 Subject: [PATCH] Enable CHECK constraints to be declared NOT VALID This means that they can initially be added to a large existing table without checking its initial contents, but new tuples must comply to them; a separate pass invoked by ALTER TABLE / VALIDATE can verify existing data and ensure it complies with the constraint, at which point it is marked validated and becomes a normal part of the table ecosystem. This patch was sponsored by Enova Financial. --- doc/src/sgml/catalogs.sgml | 2 +- doc/src/sgml/ref/alter_table.sgml | 4 +- src/backend/catalog/heap.c | 13 +- src/backend/commands/tablecmds.c | 220 ++++++++++++++++++++++++----- src/backend/parser/gram.y | 13 ++ src/include/catalog/heap.h | 1 + src/include/nodes/parsenodes.h | 2 + src/test/regress/expected/alter_table.out | 36 +++++ src/test/regress/sql/alter_table.sql | 29 ++++ 9 files changed, 278 insertions(+), 42 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 8504555..6216734 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1898,7 +1898,7 @@ convalidated bool - Has the constraint been validated? Can only be false for foreign keys + Has the constraint been validated? Can only be false for foreign keys and CHECK constraints diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 4e02438..6128b8e 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -240,7 +240,7 @@ ALTER TABLE name This form adds a new constraint to a table using the same syntax as - . Newly added foreign key constraints can + . Newly added foreign key and CHECK constraints can also be defined as NOT VALID to avoid the potentially lengthy initial check that must otherwise be performed. Constraint checks are skipped at create table time, so @@ -253,7 +253,7 @@ ALTER TABLE name VALIDATE CONSTRAINT - This form validates a foreign key constraint that was previously created + This form validates a foreign key or CHECK constraint that was previously created as NOT VALID. Constraints already marked valid do not cause an error response. diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 71c9931..669d15a 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -97,7 +97,7 @@ static Oid AddNewRelationType(const char *typeName, Oid new_array_type); static void RelationRemoveInheritance(Oid relid); static void StoreRelCheck(Relation rel, char *ccname, Node *expr, - bool is_local, int inhcount); + bool is_validated, bool is_local, int inhcount); static void StoreConstraints(Relation rel, List *cooked_constraints); static bool MergeWithExistingConstraint(Relation rel, char *ccname, Node *expr, bool allow_merge, bool is_local); @@ -1829,7 +1829,7 @@ StoreAttrDefault(Relation rel, AttrNumber attnum, Node *expr) */ static void StoreRelCheck(Relation rel, char *ccname, Node *expr, - bool is_local, int inhcount) + bool is_validated, bool is_local, int inhcount) { char *ccbin; char *ccsrc; @@ -1890,7 +1890,7 @@ StoreRelCheck(Relation rel, char *ccname, Node *expr, CONSTRAINT_CHECK, /* Constraint Type */ false, /* Is Deferrable */ false, /* Is Deferred */ - true, /* Is Validated */ + is_validated, RelationGetRelid(rel), /* relation */ attNos, /* attrs in the constraint */ keycount, /* # attrs in the constraint */ @@ -1950,7 +1950,7 @@ StoreConstraints(Relation rel, List *cooked_constraints) StoreAttrDefault(rel, con->attnum, con->expr); break; case CONSTR_CHECK: - StoreRelCheck(rel, con->name, con->expr, + StoreRelCheck(rel, con->name, con->expr, !con->skip_validation, con->is_local, con->inhcount); numchecks++; break; @@ -2064,6 +2064,7 @@ AddRelationNewConstraints(Relation rel, cooked->name = NULL; cooked->attnum = colDef->attnum; cooked->expr = expr; + cooked->skip_validation = false; cooked->is_local = is_local; cooked->inhcount = is_local ? 0 : 1; cookedConstraints = lappend(cookedConstraints, cooked); @@ -2177,7 +2178,8 @@ AddRelationNewConstraints(Relation rel, /* * OK, store it. */ - StoreRelCheck(rel, ccname, expr, is_local, is_local ? 0 : 1); + StoreRelCheck(rel, ccname, expr, !cdef->skip_validation, is_local, + is_local ? 0 : 1); numchecks++; @@ -2186,6 +2188,7 @@ AddRelationNewConstraints(Relation rel, cooked->name = ccname; cooked->attnum = 0; cooked->expr = expr; + cooked->skip_validation = cdef->skip_validation; cooked->is_local = is_local; cooked->inhcount = is_local ? 0 : 1; cookedConstraints = lappend(cookedConstraints, cooked); diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index bdbcdff..5d15bff 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -257,7 +257,8 @@ static void AlterIndexNamespaces(Relation classRel, Relation rel, static void AlterSeqNamespaces(Relation classRel, Relation rel, Oid oldNspOid, Oid newNspOid, const char *newNspName, LOCKMODE lockmode); -static void ATExecValidateConstraint(Relation rel, const char *constrName); +static void ATExecValidateConstraint(Relation rel, const char *constrName, + bool recurse, bool recursing, LOCKMODE lockmode); static int transformColumnNameList(Oid relId, List *colList, int16 *attnums, Oid *atttypids); static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid, @@ -268,6 +269,8 @@ static Oid transformFkeyCheckAttrs(Relation pkrel, int numattrs, int16 *attnums, Oid *opclasses); static void checkFkeyPermissions(Relation rel, int16 *attnums, int natts); +static void validateCheckConstraint(char *conname, Relation rel, + HeapTuple constrtup); static void validateForeignKeyConstraint(char *conname, Relation rel, Relation pkrel, Oid pkindOid, Oid constraintOid); @@ -559,6 +562,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId) cooked->name = NULL; cooked->attnum = attnum; cooked->expr = colDef->cooked_default; + cooked->skip_validation = false; cooked->is_local = true; /* not used for defaults */ cooked->inhcount = 0; /* ditto */ cookedDefaults = lappend(cookedDefaults, cooked); @@ -1558,6 +1562,7 @@ MergeAttributes(List *schema, List *supers, char relpersistence, cooked->name = pstrdup(name); cooked->attnum = 0; /* not used for constraints */ cooked->expr = expr; + cooked->skip_validation = false; cooked->is_local = false; cooked->inhcount = 1; constraints = lappend(constraints, cooked); @@ -2922,7 +2927,11 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, ATPrepAddInherit(rel); pass = AT_PASS_MISC; break; - case AT_ValidateConstraint: + case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */ + ATSimplePermissions(rel, ATT_TABLE); + /* Recursion occurs during execution phase */ + pass = AT_PASS_MISC; + break; case AT_EnableTrig: /* ENABLE TRIGGER variants */ case AT_EnableAlwaysTrig: case AT_EnableReplicaTrig: @@ -3087,8 +3096,8 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */ ATExecAddIndexConstraint(tab, rel, (IndexStmt *) cmd->def, lockmode); break; - case AT_ValidateConstraint: - ATExecValidateConstraint(rel, cmd->name); + case AT_ValidateConstraint: /* VALIDATE CONSTRAINT */ + ATExecValidateConstraint(rel, cmd->name, true, false, lockmode); break; case AT_DropConstraint: /* DROP CONSTRAINT */ ATExecDropConstraint(rel, cmd->name, cmd->behavior, @@ -5361,19 +5370,23 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, list_make1(copyObject(constr)), recursing, !recursing); - /* Add each constraint to Phase 3's queue */ + /* Add each to-be-validated constraint to Phase 3's queue */ foreach(lcon, newcons) { CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon); - NewConstraint *newcon; - newcon = (NewConstraint *) palloc0(sizeof(NewConstraint)); - newcon->name = ccon->name; - newcon->contype = ccon->contype; - /* ExecQual wants implicit-AND format */ - newcon->qual = (Node *) make_ands_implicit((Expr *) ccon->expr); + if (!ccon->skip_validation) + { + NewConstraint *newcon; - tab->constraints = lappend(tab->constraints, newcon); + newcon = (NewConstraint *) palloc0(sizeof(NewConstraint)); + newcon->name = ccon->name; + newcon->contype = ccon->contype; + /* ExecQual wants implicit-AND format */ + newcon->qual = (Node *) make_ands_implicit((Expr *) ccon->expr); + + tab->constraints = lappend(tab->constraints, newcon); + } /* Save the actually assigned name if it was defaulted */ if (constr->conname == NULL) @@ -5732,9 +5745,15 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel, /* * ALTER TABLE VALIDATE CONSTRAINT + * + * XXX The reason we handle recursion here rather than at Phase 1 is because + * there's no good way to skip recursing when handling foreign keys: there is + * no need to lock children in that case, yet we wouldn't be able to avoid + * doing so at that level. */ static void -ATExecValidateConstraint(Relation rel, const char *constrName) +ATExecValidateConstraint(Relation rel, const char *constrName, bool recurse, + bool recursing, LOCKMODE lockmode) { Relation conrel; SysScanDesc scan; @@ -5758,8 +5777,7 @@ ATExecValidateConstraint(Relation rel, const char *constrName) while (HeapTupleIsValid(tuple = systable_getnext(scan))) { con = (Form_pg_constraint) GETSTRUCT(tuple); - if (con->contype == CONSTRAINT_FOREIGN && - strcmp(NameStr(con->conname), constrName) == 0) + if (strcmp(NameStr(con->conname), constrName) == 0) { found = true; break; @@ -5769,39 +5787,104 @@ ATExecValidateConstraint(Relation rel, const char *constrName) if (!found) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("foreign key constraint \"%s\" of relation \"%s\" does not exist", + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + constrName, RelationGetRelationName(rel)))); + + if (con->contype != CONSTRAINT_FOREIGN && + con->contype != CONSTRAINT_CHECK) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint", constrName, RelationGetRelationName(rel)))); if (!con->convalidated) { - Oid conid = HeapTupleGetOid(tuple); - HeapTuple copyTuple = heap_copytuple(tuple); - Form_pg_constraint copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); - Relation refrel; + HeapTuple copyTuple; + Form_pg_constraint copy_con; - /* - * Triggers are already in place on both tables, so a concurrent write - * that alters the result here is not possible. Normally we can run a - * query here to do the validation, which would only require - * AccessShareLock. In some cases, it is possible that we might need - * to fire triggers to perform the check, so we take a lock at - * RowShareLock level just in case. - */ - refrel = heap_open(con->confrelid, RowShareLock); + if (con->contype == CONSTRAINT_FOREIGN) + { + Oid conid = HeapTupleGetOid(tuple); + Relation refrel; + + /* + * Triggers are already in place on both tables, so a concurrent write + * that alters the result here is not possible. Normally we can run a + * query here to do the validation, which would only require + * AccessShareLock. In some cases, it is possible that we might need + * to fire triggers to perform the check, so we take a lock at + * RowShareLock level just in case. + */ + refrel = heap_open(con->confrelid, RowShareLock); + + validateForeignKeyConstraint((char *) constrName, rel, refrel, + con->conindid, + conid); + heap_close(refrel, NoLock); + + /* + * Foreign keys do not inherit, so we purposedly ignore the + * recursion bit here + */ + } + else if (con->contype == CONSTRAINT_CHECK) + { + /* + * For CHECK constraints, we must ensure that we only mark the + * constraint as validated on the parent if it's already validated + * on the children. + * + * We recurse before validating on the parent, to reduce risk of + * deadlocks. + */ + if (recurse && !recursing) + { + List *children; + ListCell *child; + + children = find_all_inheritors(RelationGetRelid(rel), + lockmode, NULL); + + /* + * If we are told not to recurse, there had better not be any + * child tables; else the addition would put them out of step. + * + * XXX Is this necessary? + */ + if (children && !recurse) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraint must be validated on child tables too"))); - validateForeignKeyConstraint((char *) constrName, rel, refrel, - con->conindid, - conid); + foreach(child, children) + { + Oid childoid = lfirst_oid(child); + Relation childrel; + + if (childoid == RelationGetRelid(rel)) + continue; + + /* find_all_inheritors already got lock */ + childrel = heap_open(childoid, NoLock); + + ATExecValidateConstraint(childrel, constrName, false, + true, lockmode); + heap_close(childrel, NoLock); + } + } + + validateCheckConstraint((char *) constrName, rel, tuple); + } /* * Now update the catalog, while we have the door open. */ + copyTuple = heap_copytuple(tuple); + copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); copy_con->convalidated = true; simple_heap_update(conrel, ©Tuple->t_self, copyTuple); CatalogUpdateIndexes(conrel, copyTuple); heap_freetuple(copyTuple); - - heap_close(refrel, NoLock); } systable_endscan(scan); @@ -6107,6 +6190,75 @@ checkFkeyPermissions(Relation rel, int16 *attnums, int natts) } /* + * Scan the existing rows in a table to verify they meet a proposed + * CHECK constraint. + * + * The caller must have opened and locked the relation appropriately. + */ +static void +validateCheckConstraint(char *conname, Relation rel, HeapTuple constrtup) +{ + EState *estate; + Datum val; + char *conbin; + Expr *origexpr; + List *exprstate; + TupleDesc tupdesc; + HeapScanDesc scan; + HeapTuple tuple; + ExprContext *econtext; + MemoryContext oldcxt; + TupleTableSlot *slot; + bool isnull; + + estate = CreateExecutorState(); + /* + * XXX this tuple doesn't really come from a syscache, but this doesn't + * matter to SysCacheGetAttr, because it only wants to be able to fetch the + * tupdesc + */ + val = SysCacheGetAttr(CONSTROID, constrtup, Anum_pg_constraint_conbin, + &isnull); + if (isnull) + elog(ERROR, "null conbin for constraint %u", + HeapTupleGetOid(constrtup)); + conbin = TextDatumGetCString(val); + origexpr = (Expr *) stringToNode(conbin); + exprstate = (List *) ExecPrepareExpr((Expr *) make_ands_implicit((Expr *) origexpr), estate); + + econtext = GetPerTupleExprContext(estate); + tupdesc = RelationGetDescr(rel); + slot = MakeSingleTupleTableSlot(tupdesc); + econtext->ecxt_scantuple = slot; + + scan = heap_beginscan(rel, SnapshotNow, 0, NULL); + + /* + * Switch to per-tuple memory context and reset it for each tuple + * produced, so we don't leak memory. + */ + oldcxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + ExecStoreTuple(tuple, slot, InvalidBuffer, false); + + if (!ExecQual(exprstate, econtext, true)) + ereport(ERROR, + (errcode(ERRCODE_CHECK_VIOLATION), + errmsg("check constraint \"%s\" is violated by some row", + conname))); + + ResetExprContext(econtext); + } + + MemoryContextSwitchTo(oldcxt); + heap_endscan(scan); + ExecDropSingleTupleTableSlot(slot); + FreeExecutorState(estate); +} + +/* * Scan the existing rows in a table to verify they meet a proposed FK * constraint. * diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 1d39674..f59cc5f 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2746,6 +2746,8 @@ ConstraintElem: n->location = @1; n->raw_expr = $3; n->cooked_expr = NULL; + n->skip_validation = false; + n->initially_valid = true; if ($5 != 0) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -2753,6 +2755,17 @@ ConstraintElem: parser_errposition(@5))); $$ = (Node *)n; } + | CHECK '(' a_expr ')' NOT VALID + { + Constraint *n = makeNode(Constraint); + n->contype = CONSTR_CHECK; + n->location = @1; + n->raw_expr = $3; + n->cooked_expr = NULL; + n->skip_validation = true; + n->initially_valid = false; + $$ = (Node *)n; + } | UNIQUE '(' columnList ')' opt_definition OptConsTableSpace ConstraintAttributeSpec { diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h index c95e913..0b7b190 100644 --- a/src/include/catalog/heap.h +++ b/src/include/catalog/heap.h @@ -30,6 +30,7 @@ typedef struct CookedConstraint char *name; /* name, or NULL if none */ AttrNumber attnum; /* which attr (only for 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 */ int inhcount; /* number of times constraint is inherited */ } CookedConstraint; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index ee1881b..bf1d13b 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1543,6 +1543,8 @@ typedef struct Constraint char fk_matchtype; /* FULL, PARTIAL, UNSPECIFIED */ char fk_upd_action; /* ON UPDATE action */ char fk_del_action; /* ON DELETE action */ + + /* Fields used for constraints that allow a NOT VALID specification */ bool skip_validation; /* skip validation of existing rows? */ bool initially_valid; /* start the new constraint as valid */ } Constraint; diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index 26e7bfd..2041ad4 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -196,10 +196,46 @@ DELETE FROM tmp3 where a=5; -- Try (and succeed) and repeat to show it works on already valid constraint ALTER TABLE tmp3 validate constraint tmpconstr; ALTER TABLE tmp3 validate constraint tmpconstr; +-- Try a non-verified CHECK constraint +ALTER TABLE tmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail +ERROR: check constraint "b_greater_than_ten" is violated by some row +ALTER TABLE tmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds +ALTER TABLE tmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails +ERROR: check constraint "b_greater_than_ten" is violated by some row +DELETE FROM tmp3 WHERE NOT b > 10; +ALTER TABLE tmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds +ALTER TABLE tmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds +-- Test inherited NOT VALID CHECK constraints +select * from tmp3; + a | b +---+---- + 1 | 20 +(1 row) + +CREATE TABLE tmp6 () INHERITS (tmp3); +CREATE TABLE tmp7 () INHERITS (tmp3); +INSERT INTO tmp6 VALUES (6, 30), (7, 16); +ALTER TABLE tmp3 ADD CONSTRAINT b_le_20 CHECK (b <= 20) NOT VALID; +ALTER TABLE tmp3 VALIDATE CONSTRAINT b_le_20; -- fails +ERROR: check constraint "b_le_20" is violated by some row +DELETE FROM tmp6 WHERE b > 20; +ALTER TABLE tmp3 VALIDATE CONSTRAINT b_le_20; -- succeeds +-- An already validated constraint must not be revalidated +CREATE FUNCTION boo(int) RETURNS int IMMUTABLE STRICT LANGUAGE plpgsql AS $$ BEGIN RAISE NOTICE 'boo: %', $1; RETURN $1; END; $$; +INSERT INTO tmp7 VALUES (8, 18); +ALTER TABLE tmp7 ADD CONSTRAINT identity CHECK (b = boo(b)); +NOTICE: boo: 18 +ALTER TABLE tmp3 ADD CONSTRAINT IDENTITY check (b = boo(b)) NOT VALID; +NOTICE: merging constraint "identity" with inherited definition +ALTER TABLE tmp3 VALIDATE CONSTRAINT identity; +NOTICE: boo: 16 +NOTICE: boo: 20 -- Try (and fail) to create constraint from tmp5(a) to tmp4(a) - unique constraint on -- tmp4 is a,b ALTER TABLE tmp5 add constraint tmpconstr foreign key(a) references tmp4(a) match full; ERROR: there is no unique constraint matching given keys for referenced table "tmp4" +DROP TABLE tmp7; +DROP TABLE tmp6; DROP TABLE tmp5; DROP TABLE tmp4; DROP TABLE tmp3; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index 0ed16fb..da82d6e 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -236,12 +236,41 @@ DELETE FROM tmp3 where a=5; ALTER TABLE tmp3 validate constraint tmpconstr; ALTER TABLE tmp3 validate constraint tmpconstr; +-- Try a non-verified CHECK constraint +ALTER TABLE tmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10); -- fail +ALTER TABLE tmp3 ADD CONSTRAINT b_greater_than_ten CHECK (b > 10) NOT VALID; -- succeeds +ALTER TABLE tmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- fails +DELETE FROM tmp3 WHERE NOT b > 10; +ALTER TABLE tmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds +ALTER TABLE tmp3 VALIDATE CONSTRAINT b_greater_than_ten; -- succeeds + +-- Test inherited NOT VALID CHECK constraints +select * from tmp3; +CREATE TABLE tmp6 () INHERITS (tmp3); +CREATE TABLE tmp7 () INHERITS (tmp3); + +INSERT INTO tmp6 VALUES (6, 30), (7, 16); +ALTER TABLE tmp3 ADD CONSTRAINT b_le_20 CHECK (b <= 20) NOT VALID; +ALTER TABLE tmp3 VALIDATE CONSTRAINT b_le_20; -- fails +DELETE FROM tmp6 WHERE b > 20; +ALTER TABLE tmp3 VALIDATE CONSTRAINT b_le_20; -- succeeds + +-- An already validated constraint must not be revalidated +CREATE FUNCTION boo(int) RETURNS int IMMUTABLE STRICT LANGUAGE plpgsql AS $$ BEGIN RAISE NOTICE 'boo: %', $1; RETURN $1; END; $$; +INSERT INTO tmp7 VALUES (8, 18); +ALTER TABLE tmp7 ADD CONSTRAINT identity CHECK (b = boo(b)); +ALTER TABLE tmp3 ADD CONSTRAINT IDENTITY check (b = boo(b)) NOT VALID; +ALTER TABLE tmp3 VALIDATE CONSTRAINT identity; -- Try (and fail) to create constraint from tmp5(a) to tmp4(a) - unique constraint on -- tmp4 is a,b ALTER TABLE tmp5 add constraint tmpconstr foreign key(a) references tmp4(a) match full; +DROP TABLE tmp7; + +DROP TABLE tmp6; + DROP TABLE tmp5; DROP TABLE tmp4; -- 1.7.4.4