diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c index 393d44f..56df4b5 100644 --- a/src/backend/catalog/partition.c +++ b/src/backend/catalog/partition.c @@ -35,6 +35,7 @@ #include "nodes/parsenodes.h" #include "optimizer/clauses.h" #include "optimizer/planmain.h" +#include "optimizer/prep.h" #include "optimizer/var.h" #include "rewrite/rewriteManip.h" #include "storage/lmgr.h" @@ -88,9 +89,12 @@ typedef struct PartitionBoundInfoData * partitioned table) */ int null_index; /* Index of the null-accepting partition; -1 * if there isn't one */ + int default_index; /* Index of the default partition if any; -1 + * if there isn't one */ } PartitionBoundInfoData; #define partition_bound_accepts_nulls(bi) ((bi)->null_index != -1) +#define partition_bound_has_default(bi) ((bi)->default_index != -1) /* * When qsort'ing partition bounds after reading from the catalog, each bound @@ -121,14 +125,15 @@ static int32 qsort_partition_rbound_cmp(const void *a, const void *b, static Oid get_partition_operator(PartitionKey key, int col, StrategyNumber strategy, bool *need_relabel); static Expr *make_partition_op_expr(PartitionKey key, int keynum, - uint16 strategy, Expr *arg1, Expr *arg2); + uint16 strategy, Expr *arg1, Expr *arg2, + bool is_default); static void get_range_key_properties(PartitionKey key, int keynum, PartitionRangeDatum *ldatum, PartitionRangeDatum *udatum, ListCell **partexprs_item, Expr **keyCol, Const **lower_val, Const **upper_val); -static List *get_qual_for_list(PartitionKey key, PartitionBoundSpec *spec); +static List *get_qual_for_list(Relation parent, PartitionBoundSpec *spec); static List *get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec); static List *generate_partition_qual(Relation rel); @@ -147,6 +152,8 @@ static int32 partition_bound_cmp(PartitionKey key, static int partition_bound_bsearch(PartitionKey key, PartitionBoundInfo boundinfo, void *probe, bool probe_is_bound, bool *is_equal); +static void check_default_allows_bound(Relation parent, + PartitionBoundSpec *new_spec); /* * RelationBuildPartitionDesc @@ -174,6 +181,7 @@ RelationBuildPartitionDesc(Relation rel) /* List partitioning specific */ PartitionListValue **all_values = NULL; int null_index = -1; + int default_index = -1; /* Range partitioning specific */ PartitionRangeBound **rbounds = NULL; @@ -254,6 +262,19 @@ RelationBuildPartitionDesc(Relation rel) if (spec->strategy != PARTITION_STRATEGY_LIST) elog(ERROR, "invalid strategy in partition bound spec"); + /* + * In case of default partition, just note the index, we do not + * add this to non_null_values list. + */ + c = list_head(spec->listdatums); + if ((list_length(spec->listdatums) == 1) && + isDefaultPartitionBound((Node *) lfirst(c))) + { + default_index = i; + i++; + continue; + } + foreach(c, spec->listdatums) { Const *val = castNode(Const, lfirst(c)); @@ -506,6 +527,17 @@ RelationBuildPartitionDesc(Relation rel) else boundinfo->null_index = -1; + /* Assign mapping index for default partition. */ + if (default_index != -1) + { + Assert(default_index >= 0 && + mapping[default_index] == -1); + mapping[default_index] = next_index++; + boundinfo->default_index = mapping[default_index]; + } + else + boundinfo->default_index = -1; + /* All partition must now have a valid mapping */ Assert(next_index == nparts); break; @@ -561,6 +593,12 @@ RelationBuildPartitionDesc(Relation rel) } } boundinfo->indexes[i] = -1; + + /* + * Currently range partition do not have default partition + * support. + */ + boundinfo->default_index = -1; break; } @@ -610,6 +648,9 @@ partition_bounds_equal(PartitionKey key, if (b1->null_index != b2->null_index) return false; + if (b1->default_index != b2->default_index) + return false; + for (i = 0; i < b1->ndatums; i++) { int j; @@ -672,6 +713,7 @@ check_new_partition_bound(char *relname, Relation parent, { PartitionKey key = RelationGetPartitionKey(parent); PartitionDesc partdesc = RelationGetPartitionDesc(parent); + PartitionBoundInfo boundinfo = partdesc->boundinfo; ParseState *pstate = make_parsestate(NULL); int with = -1; bool overlap = false; @@ -684,13 +726,29 @@ check_new_partition_bound(char *relname, Relation parent, if (partdesc->nparts > 0) { - PartitionBoundInfo boundinfo = partdesc->boundinfo; ListCell *cell; Assert(boundinfo && boundinfo->strategy == PARTITION_STRATEGY_LIST && (boundinfo->ndatums > 0 || - partition_bound_accepts_nulls(boundinfo))); + partition_bound_accepts_nulls(boundinfo) || + partition_bound_has_default(boundinfo))); + + /* + * Default partition cannot be added if there already + * exists one. + */ + cell = list_head(spec->listdatums); + if (cell && isDefaultPartitionBound((Node *) lfirst(cell))) + { + if (partition_bound_has_default(boundinfo)) + { + overlap = true; + with = boundinfo->default_index; + } + + break; + } foreach(cell, spec->listdatums) { @@ -838,6 +896,139 @@ check_new_partition_bound(char *relname, Relation parent, relname, get_rel_name(partdesc->oids[with])), parser_errposition(pstate, spec->location))); } + + /* + * If a default partition exists, it's partition constraint will change + * after the addition of this new partition such that it won't allow any + * row that qualifies for this new partition. So, check if the existing + * data in the default partition satisfies this *would be* default + * partition constraint. + */ + if (partdesc->nparts > 0 && partition_bound_has_default(boundinfo)) + check_default_allows_bound(parent, spec); +} + +/* + * check_default_allows_bound + * + * Checks if there exists any row in default partition that passes the check + * for constraints of new partition, if any reports an error. + */ +static void +check_default_allows_bound(Relation parent, PartitionBoundSpec *new_spec) +{ + Relation default_rel; + int default_index; + List *new_part_constraints = NIL; + List *all_parts; + ListCell *lc; + + /* Currently default partition is supported only for LIST partition. */ + if (new_spec->strategy != PARTITION_STRATEGY_LIST) + return; + + new_part_constraints = get_qual_for_list(parent, new_spec); + new_part_constraints = (List *) eval_const_expressions(NULL, + (Node *) new_part_constraints); + new_part_constraints = + (List *) canonicalize_qual((Expr *) new_part_constraints); + new_part_constraints = list_make1(make_ands_explicit(new_part_constraints)); + + /* Generate the constraint and default execution states. */ + default_index = parent->rd_partdesc->boundinfo->default_index; + default_rel = heap_open(parent->rd_partdesc->oids[default_index], + AccessExclusiveLock); + + if (default_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + all_parts = find_all_inheritors(RelationGetRelid(default_rel), + AccessExclusiveLock, NULL); + else + all_parts = list_make1_oid(RelationGetRelid(default_rel)); + + foreach(lc, all_parts) + { + Oid part_relid = lfirst_oid(lc); + Relation part_rel; + Expr *constr; + Expr *partition_constraint; + EState *estate; + HeapTuple tuple; + ExprState *partqualstate = NULL; + Snapshot snapshot; + TupleDesc tupdesc; + ExprContext *econtext; + HeapScanDesc scan; + MemoryContext oldCxt; + TupleTableSlot *tupslot; + + /* Lock already taken above. */ + if (part_relid != RelationGetRelid(default_rel)) + part_rel = heap_open(part_relid, NoLock); + else + part_rel = default_rel; + + /* + * Skip if it's a partitioned table. Only RELKIND_RELATION relations + * (ie, leaf partitions) need to be scanned. + */ + if (part_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + heap_close(part_rel, NoLock); + continue; + } + + tupdesc = CreateTupleDescCopy(RelationGetDescr(part_rel)); + constr = linitial(new_part_constraints); + partition_constraint = (Expr *) map_partition_varattnos((List *) constr, + 1, part_rel, parent); + estate = CreateExecutorState(); + + /* Build expression execution states for partition check quals */ + partqualstate = ExecPrepareExpr(partition_constraint, estate); + + econtext = GetPerTupleExprContext(estate); + snapshot = RegisterSnapshot(GetLatestSnapshot()); + scan = heap_beginscan(part_rel, snapshot, 0, NULL); + tupslot = MakeSingleTupleTableSlot(tupdesc); + + /* + * 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) + { + char *val_desc; + + ExecStoreTuple(tuple, tupslot, InvalidBuffer, false); + econtext->ecxt_scantuple = tupslot; + + if (partqualstate && ExecCheck(partqualstate, econtext)) + { + val_desc = + ExecBuildSlotValueDescription(RelationGetRelid(part_rel), + tupslot, tupdesc, NULL, 64); + + ereport(ERROR, + (errcode(ERRCODE_CHECK_VIOLATION), + errmsg("new default partition constraint is violated by some row"), + val_desc ? + errdetail("Violating row contains %s.", val_desc) : 0)); + } + + ResetExprContext(econtext); + CHECK_FOR_INTERRUPTS(); + } + + CacheInvalidateRelcache(part_rel); + MemoryContextSwitchTo(oldCxt); + heap_endscan(scan); + UnregisterSnapshot(snapshot); + ExecDropSingleTupleTableSlot(tupslot); + FreeExecutorState(estate); + heap_close(part_rel, NoLock); + } } /* @@ -886,6 +1077,16 @@ get_partition_parent(Oid relid) } /* + * Returns true if the partition bound is default. + */ +bool +isDefaultPartitionBound(Node *value) +{ + return (IsA(value, DefElem) && + !strcmp(castNode(DefElem, value)->defname, "default_partition")); +} + +/* * get_qual_from_partbound * Given a parser node for partition bound, return the list of executable * expressions as partition constraint @@ -903,7 +1104,7 @@ get_qual_from_partbound(Relation rel, Relation parent, { case PARTITION_STRATEGY_LIST: Assert(spec->strategy == PARTITION_STRATEGY_LIST); - my_qual = get_qual_for_list(key, spec); + my_qual = get_qual_for_list(parent, spec); break; case PARTITION_STRATEGY_RANGE: @@ -1233,7 +1434,7 @@ get_partition_operator(PartitionKey key, int col, StrategyNumber strategy, */ static Expr * make_partition_op_expr(PartitionKey key, int keynum, - uint16 strategy, Expr *arg1, Expr *arg2) + uint16 strategy, Expr *arg1, Expr *arg2, bool is_default) { Oid operoid; bool need_relabel = false; @@ -1263,11 +1464,17 @@ make_partition_op_expr(PartitionKey key, int keynum, { ScalarArrayOpExpr *saopexpr; + if (is_default && + ((operoid = get_negator(operoid)) == InvalidOid)) + ereport(ERROR, (errcode(ERRCODE_RESTRICT_VIOLATION), + errmsg("DEFAULT partition cannot be used without negator of operator %s", + get_opname(operoid)))); + /* Build leftop = ANY (rightop) */ saopexpr = makeNode(ScalarArrayOpExpr); saopexpr->opno = operoid; saopexpr->opfuncid = get_opcode(operoid); - saopexpr->useOr = true; + saopexpr->useOr = !is_default; saopexpr->inputcollid = key->partcollation[keynum]; saopexpr->args = list_make2(arg1, arg2); saopexpr->location = -1; @@ -1299,10 +1506,11 @@ make_partition_op_expr(PartitionKey key, int keynum, * Returns a list of expressions to use as a list partition's constraint. */ static List * -get_qual_for_list(PartitionKey key, PartitionBoundSpec *spec) +get_qual_for_list(Relation parent, PartitionBoundSpec *spec) { + PartitionKey key = RelationGetPartitionKey(parent); List *result; - List *datums = NIL; + List *datums = NIL; ArrayExpr *arr; Expr *opexpr; ListCell *cell; @@ -1310,6 +1518,8 @@ get_qual_for_list(PartitionKey key, PartitionBoundSpec *spec) bool list_has_null = false; NullTest *nulltest1 = NULL, *nulltest2 = NULL; + bool is_default = + isDefaultPartitionBound((Node *) linitial(spec->listdatums)); /* Left operand is either a simple Var or arbitrary expression */ if (key->partattrs[0] != 0) @@ -1323,20 +1533,63 @@ get_qual_for_list(PartitionKey key, PartitionBoundSpec *spec) keyCol = (Expr *) copyObject(linitial(key->partexprs)); /* - * Create a list of datums without NULL; as we handle it separately - * below. + * In case of default partition for list, the partition constraint is + * basically any value that is not equal to any of the values in + * boundinfo->datums array. So, construct a list of constants from + * boundinfo->datums to pass to function make_partition_op_expr via + * ArrayExpr, which would return an negated expression for default + * partition. */ - foreach (cell, spec->listdatums) + if (is_default) { - Const *val = castNode(Const, lfirst(cell)); + int i; + int ndatums = 0; + MemoryContext oldcxt; + PartitionDesc pdesc = RelationGetPartitionDesc(parent); + PartitionBoundInfo boundinfo = pdesc->boundinfo; + + ndatums = (pdesc->nparts > 0) ? boundinfo->ndatums : 0; + oldcxt = MemoryContextSwitchTo(CacheMemoryContext); + + for (i = 0; i < ndatums; i++) + { + Const *val; + + /* Construct const from datum */ + val = makeConst(key->parttypid[0], + key->parttypmod[0], + key->parttypcoll[0], + key->parttyplen[0], + *boundinfo->datums[i], + false, /* isnull */ + true /* byval */ ); + + datums = lappend(datums, val); + } - if (val->constisnull) + if (boundinfo && partition_bound_accepts_nulls(boundinfo)) list_has_null = true; - else - datums = lappend(datums, lfirst(cell)); + + MemoryContextSwitchTo(oldcxt); + } + else + { + /* + * Create a list of datums without NULL; as we handle it separately + * below. + */ + foreach (cell, spec->listdatums) + { + Const *val = castNode(Const, lfirst(cell)); + + if (val->constisnull) + list_has_null = true; + else + datums = lappend(datums, lfirst(cell)); + } } - if (!list_has_null) + if ((!list_has_null && !is_default) || (list_has_null && is_default)) { /* * Gin up a col IS NOT NULL test that will be AND'd with other @@ -1373,7 +1626,7 @@ get_qual_for_list(PartitionKey key, PartitionBoundSpec *spec) /* Generate the main expression, i.e., keyCol = ANY (arr) */ opexpr = make_partition_op_expr(key, 0, BTEqualStrategyNumber, - keyCol, (Expr *) arr); + keyCol, (Expr *) arr, is_default); if (nulltest1) result = list_make2(nulltest1, opexpr); @@ -1587,7 +1840,7 @@ get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec) oldcxt = MemoryContextSwitchTo(estate->es_query_cxt); test_expr = make_partition_op_expr(key, i, BTEqualStrategyNumber, (Expr *) lower_val, - (Expr *) upper_val); + (Expr *) upper_val, false); fix_opfuncids((Node *) test_expr); test_exprstate = ExecInitExpr(test_expr, NULL); test_result = ExecEvalExprSwitchContext(test_exprstate, @@ -1611,7 +1864,7 @@ get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec) /* Equal, so generate keyCol = lower_val expression */ result = lappend(result, make_partition_op_expr(key, i, BTEqualStrategyNumber, - keyCol, (Expr *) lower_val)); + keyCol, (Expr *) lower_val, false)); i++; } @@ -1671,7 +1924,8 @@ get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec) make_partition_op_expr(key, j, strategy, keyCol, - (Expr *) lower_val)); + (Expr *) lower_val, + false)); } if (need_next_upper_arm && upper_val) @@ -1693,7 +1947,8 @@ get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec) make_partition_op_expr(key, j, strategy, keyCol, - (Expr *) upper_val)); + (Expr *) upper_val, + false)); } @@ -1983,11 +2238,13 @@ get_partition_for_tuple(PartitionDispatch *pd, } } + cur_index = -1; /* - * A null partition key is only acceptable if null-accepting list - * partition exists. + * A null partition key is acceptable if null-accepting list partition + * or a default partition exists. Check if there exists a null + * accepting partition, else this will be handled later by default + * partition if it exists. */ - cur_index = -1; if (isnull[0] && partition_bound_accepts_nulls(partdesc->boundinfo)) cur_index = partdesc->boundinfo->null_index; else if (!isnull[0]) @@ -2023,16 +2280,28 @@ get_partition_for_tuple(PartitionDispatch *pd, } /* - * cur_index < 0 means we failed to find a partition of this parent. - * cur_index >= 0 means we either found the leaf partition, or the - * next parent to find a partition of. + * cur_index < 0 means we could not find a non-default partition of this + * parent. cur_index >= 0 means we either found the leaf partition, or + * the next parent to find a partition of. */ if (cur_index < 0) { - result = -1; - *failed_at = parent; - *failed_slot = slot; - break; + if (partition_bound_has_default(partdesc->boundinfo)) + { + result = parent->indexes[partdesc->boundinfo->default_index]; + + if (result >= 0) + break; + else + parent = pd[-result]; + } + else + { + result = -1; + *failed_at = parent; + *failed_slot = slot; + break; + } } else if (parent->indexes[cur_index] >= 0) { diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 4a899f1..dae592a 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -92,11 +92,6 @@ static bool ExecCheckRTEPermsModified(Oid relOid, Oid userid, Bitmapset *modifiedCols, AclMode requiredPerms); static void ExecCheckXactReadOnly(PlannedStmt *plannedstmt); -static char *ExecBuildSlotValueDescription(Oid reloid, - TupleTableSlot *slot, - TupleDesc tupdesc, - Bitmapset *modifiedCols, - int maxfieldlen); static char *ExecBuildSlotPartitionKeyDescription(Relation rel, Datum *values, bool *isnull, @@ -2174,7 +2169,7 @@ ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo, * column involved, that subset will be returned with a key identifying which * columns they are. */ -static char * +char * ExecBuildSlotValueDescription(Oid reloid, TupleTableSlot *slot, TupleDesc tupdesc, diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index ff5b1be..c394c90 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2675,6 +2675,23 @@ ForValues: $$ = n; } + + /* + * A default partition, that can be partition of either LIST or RANGE + * partitioned table. + * Currently this is supported only for LIST partition. + */ + | DEFAULT + { + PartitionBoundSpec *n = makeNode(PartitionBoundSpec); + + n->listdatums = + list_make1(makeDefElem("default_partition", NULL, @1)); + n->location = @1; + + $$ = n; + } + ; partbound_datum: @@ -2716,6 +2733,7 @@ PartitionRangeDatum: $$ = (Node *) n; } + ; /***************************************************************************** diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 9134fb9..bbc0dd6 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -33,6 +33,7 @@ #include "catalog/heap.h" #include "catalog/index.h" #include "catalog/namespace.h" +#include "catalog/partition.h" #include "catalog/pg_am.h" #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" @@ -3291,10 +3292,29 @@ transformPartitionBound(ParseState *pstate, Relation parent, int32 coltypmod; if (spec->strategy != PARTITION_STRATEGY_LIST) - ereport(ERROR, - (errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("invalid bound specification for a list partition"), - parser_errposition(pstate, exprLocation((Node *) spec)))); + { + /* + * In case of default partition, parser had no way to identify the + * partition strategy. Assign the parent strategy to default + * partition bound spec. + */ + if ((list_length(spec->listdatums) == 1) && + isDefaultPartitionBound((Node *) linitial(spec->listdatums))) + { + result_spec->strategy = PARTITION_STRATEGY_LIST; + + /* + * Default partition datums do not need any more + * transformations. + */ + return result_spec; + } + else + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("invalid bound specification for a list partition"), + parser_errposition(pstate, exprLocation((Node *) spec)))); + } /* Get the only column's name in case we need to output an error */ if (key->partattrs[0] != 0) diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 824d757..47810ae 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -8652,6 +8652,18 @@ get_rule_expr(Node *node, deparse_context *context, case PARTITION_STRATEGY_LIST: Assert(spec->listdatums != NIL); + /* + * If the boundspec is of Default partition, it does + * not have list of datums, but has only one node to + * indicate its a default partition. + */ + if (isDefaultPartitionBound( + (Node *) linitial(spec->listdatums))) + { + appendStringInfoString(buf, "DEFAULT"); + break; + } + appendStringInfoString(buf, "FOR VALUES IN ("); sep = ""; foreach(cell, spec->listdatums) diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 2abd087..b01ba07 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -2044,7 +2044,7 @@ psql_completion(const char *text, int start, int end) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, ""); /* Limited completion support for partition bound specification */ else if (TailMatches3("ATTACH", "PARTITION", MatchAny)) - COMPLETE_WITH_CONST("FOR VALUES"); + COMPLETE_WITH_LIST2("FOR VALUES", "DEFAULT"); else if (TailMatches2("FOR", "VALUES")) COMPLETE_WITH_LIST2("FROM (", "IN ("); @@ -2483,7 +2483,7 @@ psql_completion(const char *text, int start, int end) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_partitioned_tables, ""); /* Limited completion support for partition bound specification */ else if (TailMatches3("PARTITION", "OF", MatchAny)) - COMPLETE_WITH_CONST("FOR VALUES"); + COMPLETE_WITH_LIST2("FOR VALUES", "DEFAULT"); /* CREATE TABLESPACE */ else if (Matches3("CREATE", "TABLESPACE", MatchAny)) diff --git a/src/include/catalog/partition.h b/src/include/catalog/partition.h index 0a1e468..3a80bd9 100644 --- a/src/include/catalog/partition.h +++ b/src/include/catalog/partition.h @@ -83,6 +83,7 @@ extern List *map_partition_varattnos(List *expr, int target_varno, Relation partrel, Relation parent); extern List *RelationGetPartitionQual(Relation rel); extern Expr *get_partition_qual_relid(Oid relid); +extern bool isDefaultPartitionBound(Node *value); /* For tuple routing */ extern PartitionDispatch *RelationGetPartitionDispatchInfo(Relation rel, diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index 8cc5f3a..8bd574c 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -537,5 +537,10 @@ extern void CheckCmdReplicaIdentity(Relation rel, CmdType cmd); extern void CheckSubscriptionRelkind(char relkind, const char *nspname, const char *relname); +char *ExecBuildSlotValueDescription(Oid reloid, + TupleTableSlot *slot, + TupleDesc tupdesc, + Bitmapset *modifiedCols, + int maxfieldlen); #endif /* EXECUTOR_H */ diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index 8aadbb8..b800173 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -3273,6 +3273,11 @@ SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_1'::reg CREATE TABLE fail_part (LIKE part_1 INCLUDING CONSTRAINTS); ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1); ERROR: partition "fail_part" would overlap partition "part_1" +-- attaching default partition overlaps if a default partition already exists +CREATE TABLE part_def1 PARTITION OF list_parted DEFAULT; +CREATE TABLE part_def2 (LIKE part_1 INCLUDING CONSTRAINTS); +ALTER TABLE list_parted ATTACH PARTITION part_def2 DEFAULT; +ERROR: partition "part_def2" would overlap partition "part_def1" -- check validation when attaching list partitions CREATE TABLE list_parted2 ( a int, @@ -3286,6 +3291,16 @@ ERROR: partition constraint is violated by some row -- should be ok after deleting the bad row DELETE FROM part_2; ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2); +-- check partition cannot be attached if default has some row for its values +CREATE TABLE list_parted2_def PARTITION OF list_parted2 DEFAULT; +INSERT INTO list_parted2_def VALUES (11, 'z'); +CREATE TABLE part_3 (LIKE list_parted2); +ALTER TABLE list_parted2 ATTACH PARTITION part_3 FOR VALUES IN (11); +ERROR: new default partition constraint is violated by some row +DETAIL: Violating row contains (11, z). +-- should be ok after deleting the bad row +DELETE FROM list_parted2_def WHERE a = 11; +ALTER TABLE list_parted2 ATTACH PARTITION part_3 FOR VALUES IN (11); -- adding constraints that describe the desired partition constraint -- (or more restrictive) will help skip the validation scan CREATE TABLE part_3_4 ( @@ -3338,9 +3353,22 @@ ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5); ERROR: partition constraint is violated by some row -- delete the faulting row and also add a constraint to skip the scan DELETE FROM part_5_a WHERE a NOT IN (3); -ALTER TABLE part_5 ADD CONSTRAINT check_a CHECK (a IN (5)), ALTER a SET NOT NULL; -ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5); +ALTER TABLE part_5 ADD CONSTRAINT check_a CHECK (a IN (5, 55)), ALTER a SET NOT NULL; +ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5, 55); INFO: partition constraint for table "part_5" is implied by existing constraints +-- check that leaf partitons of default partition are scanned when +-- attaching a partitioned table. +ALTER TABLE part_5 DROP CONSTRAINT check_a; +CREATE TABLE part5_def PARTITION OF part_5 DEFAULT PARTITION BY LIST(a); +CREATE TABLE part5_def_p1 PARTITION OF part5_def FOR VALUES IN (44, 55); +INSERT INTO part5_def_p1 VALUES (55, 'y'); +CREATE TABLE part5_p1 (LIKE part_5); +ALTER TABLE part_5 ATTACH PARTITION part5_p1 FOR VALUES IN ('y'); +ERROR: new default partition constraint is violated by some row +DETAIL: Violating row contains (55, y). +-- should be ok after deleting the bad row +DELETE FROM part5_def_p1 WHERE b = 'y'; +ALTER TABLE part_5 ATTACH PARTITION part5_p1 FOR VALUES IN ('y'); -- check that the table being attached is not already a partition ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2); ERROR: "part_2" is already a partition diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out index 5136506..92068ee 100644 --- a/src/test/regress/expected/create_table.out +++ b/src/test/regress/expected/create_table.out @@ -449,6 +449,7 @@ CREATE TABLE list_parted ( CREATE TABLE part_1 PARTITION OF list_parted FOR VALUES IN ('1'); CREATE TABLE part_2 PARTITION OF list_parted FOR VALUES IN (2); CREATE TABLE part_null PARTITION OF list_parted FOR VALUES IN (null); +CREATE TABLE part_default PARTITION OF list_parted DEFAULT; CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN (int '1'); ERROR: syntax error at or near "int" LINE 1: ... fail_part PARTITION OF list_parted FOR VALUES IN (int '1'); @@ -457,6 +458,8 @@ CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN ('1'::int); ERROR: syntax error at or near "::" LINE 1: ...fail_part PARTITION OF list_parted FOR VALUES IN ('1'::int); ^ +CREATE TABLE fail_default_part PARTITION OF list_parted DEFAULT; +ERROR: partition "fail_default_part" would overlap partition "part_default" -- syntax does not allow empty list of values for list partitions CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN (); ERROR: syntax error at or near ")" @@ -514,6 +517,11 @@ ERROR: TO must specify exactly one value per partitioning column -- cannot specify null values in range bounds CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (unbounded); ERROR: cannot specify NULL in range bound +-- range partition cannot have default partition +CREATE TABLE fail_part PARTITION OF range_parted DEFAULT; +ERROR: invalid bound specification for a range partition +LINE 1: CREATE TABLE fail_part PARTITION OF range_parted DEFAULT; + ^ -- cannot specify finite values after UNBOUNDED has been specified CREATE TABLE range_parted_multicol (a int, b int, c int) PARTITION BY RANGE (a, b, c); CREATE TABLE fail_part PARTITION OF range_parted_multicol FOR VALUES FROM (1, UNBOUNDED, 1) TO (UNBOUNDED, 1, 1); @@ -565,10 +573,16 @@ CREATE TABLE list_parted2 ( ) PARTITION BY LIST (a); CREATE TABLE part_null_z PARTITION OF list_parted2 FOR VALUES IN (null, 'z'); CREATE TABLE part_ab PARTITION OF list_parted2 FOR VALUES IN ('a', 'b'); +CREATE TABLE list_parted2_def PARTITION OF list_parted2 DEFAULT; CREATE TABLE fail_part PARTITION OF list_parted2 FOR VALUES IN (null); ERROR: partition "fail_part" would overlap partition "part_null_z" CREATE TABLE fail_part PARTITION OF list_parted2 FOR VALUES IN ('b', 'c'); ERROR: partition "fail_part" would overlap partition "part_ab" +-- check default partition overlap +INSERT INTO list_parted2 VALUES('X'); +CREATE TABLE fail_part PARTITION OF list_parted2 FOR VALUES IN ('W', 'X', 'Y'); +ERROR: new default partition constraint is violated by some row +DETAIL: Violating row contains (X). CREATE TABLE range_parted2 ( a int ) PARTITION BY RANGE (a); diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out index 8b0752a..841f7c3 100644 --- a/src/test/regress/expected/insert.out +++ b/src/test/regress/expected/insert.out @@ -202,6 +202,7 @@ create table list_parted ( create table part_aa_bb partition of list_parted FOR VALUES IN ('aa', 'bb'); create table part_cc_dd partition of list_parted FOR VALUES IN ('cc', 'dd'); create table part_null partition of list_parted FOR VALUES IN (null); +create table part_default partition of list_parted default; -- fail insert into part_aa_bb values ('cc', 1); ERROR: new row for relation "part_aa_bb" violates partition constraint @@ -212,24 +213,46 @@ DETAIL: Failing row contains (AAa, 1). insert into part_aa_bb values (null); ERROR: new row for relation "part_aa_bb" violates partition constraint DETAIL: Failing row contains (null, null). +insert into part_default values ('aa', 2); +ERROR: new row for relation "part_default" violates partition constraint +DETAIL: Failing row contains (aa, 2). +insert into part_default values (null, 2); +ERROR: new row for relation "part_default" violates partition constraint +DETAIL: Failing row contains (null, 2). -- ok insert into part_cc_dd values ('cC', 1); insert into part_null values (null, 0); +insert into part_default values ('Zz', 2); -- check in case of multi-level partitioned table create table part_ee_ff partition of list_parted for values in ('ee', 'ff') partition by range (b); create table part_ee_ff1 partition of part_ee_ff for values from (1) to (10); create table part_ee_ff2 partition of part_ee_ff for values from (10) to (20); +-- check in case of multi-level default partitioned table +drop table part_default; +create table part_default partition of list_parted default partition by range(b); +create table part_default_p1 partition of part_default for values from (20) to (30); +create table part_default_p2 partition of part_default for values from (30) to (40); -- fail insert into part_ee_ff1 values ('EE', 11); ERROR: new row for relation "part_ee_ff1" violates partition constraint DETAIL: Failing row contains (EE, 11). +insert into part_default_p2 values ('gg', 43); +ERROR: new row for relation "part_default_p2" violates partition constraint +DETAIL: Failing row contains (gg, 43). -- fail (even the parent's, ie, part_ee_ff's partition constraint applies) insert into part_ee_ff1 values ('cc', 1); ERROR: new row for relation "part_ee_ff1" violates partition constraint DETAIL: Failing row contains (cc, 1). +insert into part_default values ('gg', 43); +ERROR: no partition of relation "part_default" found for row +DETAIL: Partition key of the failing row contains (b) = (43). -- ok insert into part_ee_ff1 values ('ff', 1); insert into part_ee_ff2 values ('ff', 11); +insert into part_default_p1 values ('cd', 25); +insert into part_default_p2 values ('gg', 35); +insert into list_parted values ('ab', 21); +drop table part_default; -- Check tuple routing for partitioned tables -- fail insert into range_parted values ('a', 0); diff --git a/src/test/regress/expected/update.out b/src/test/regress/expected/update.out index 9366f04..0f41e7e 100644 --- a/src/test/regress/expected/update.out +++ b/src/test/regress/expected/update.out @@ -218,5 +218,24 @@ ERROR: new row for relation "part_b_10_b_20" violates partition constraint DETAIL: Failing row contains (b, 9). -- ok update range_parted set b = b + 1 where b = 10; +create table list_parted ( + a text, + b int +) partition by list (a); +create table list_part1 partition of list_parted for values in ('a', 'b'); +create table list_default partition of list_parted default; +insert into list_part1 values ('a', 1); +insert into list_default values ('d', 10); +-- fail +update list_part1 set a = 'c' where a = 'a'; +ERROR: new row for relation "list_part1" violates partition constraint +DETAIL: Failing row contains (c, 1). +update list_default set a = 'a' where a = 'd'; +ERROR: new row for relation "list_default" violates partition constraint +DETAIL: Failing row contains (a, 10). +-- ok +update list_part1 set a = 'b' where a = 'a'; +update list_default set a = 'x' where a = 'd'; -- cleanup drop table range_parted; +drop table list_parted; diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index c41b487..aa1a4e3 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -2095,6 +2095,10 @@ SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_1'::reg -- 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); +-- attaching default partition overlaps if a default partition already exists +CREATE TABLE part_def1 PARTITION OF list_parted DEFAULT; +CREATE TABLE part_def2 (LIKE part_1 INCLUDING CONSTRAINTS); +ALTER TABLE list_parted ATTACH PARTITION part_def2 DEFAULT; -- check validation when attaching list partitions CREATE TABLE list_parted2 ( @@ -2111,6 +2115,15 @@ ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2); DELETE FROM part_2; ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2); +-- check partition cannot be attached if default has some row for its values +CREATE TABLE list_parted2_def PARTITION OF list_parted2 DEFAULT; +INSERT INTO list_parted2_def VALUES (11, 'z'); +CREATE TABLE part_3 (LIKE list_parted2); +ALTER TABLE list_parted2 ATTACH PARTITION part_3 FOR VALUES IN (11); +-- should be ok after deleting the bad row +DELETE FROM list_parted2_def WHERE a = 11; +ALTER TABLE list_parted2 ATTACH PARTITION part_3 FOR VALUES IN (11); + -- adding constraints that describe the desired partition constraint -- (or more restrictive) will help skip the validation scan CREATE TABLE part_3_4 ( @@ -2169,9 +2182,20 @@ ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5); -- delete the faulting row and also add a constraint to skip the scan DELETE FROM part_5_a WHERE a NOT IN (3); -ALTER TABLE part_5 ADD CONSTRAINT check_a CHECK (a IN (5)), ALTER a SET NOT NULL; -ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5); - +ALTER TABLE part_5 ADD CONSTRAINT check_a CHECK (a IN (5, 55)), ALTER a SET NOT NULL; +ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5, 55); + +-- check that leaf partitons of default partition are scanned when +-- attaching a partitioned table. +ALTER TABLE part_5 DROP CONSTRAINT check_a; +CREATE TABLE part5_def PARTITION OF part_5 DEFAULT PARTITION BY LIST(a); +CREATE TABLE part5_def_p1 PARTITION OF part5_def FOR VALUES IN (44, 55); +INSERT INTO part5_def_p1 VALUES (55, 'y'); +CREATE TABLE part5_p1 (LIKE part_5); +ALTER TABLE part_5 ATTACH PARTITION part5_p1 FOR VALUES IN ('y'); +-- should be ok after deleting the bad row +DELETE FROM part5_def_p1 WHERE b = 'y'; +ALTER TABLE part_5 ATTACH PARTITION part5_p1 FOR VALUES IN ('y'); -- check that the table being attached is not already a partition ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2); diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql index cb7aa5b..f44c0e0 100644 --- a/src/test/regress/sql/create_table.sql +++ b/src/test/regress/sql/create_table.sql @@ -439,8 +439,10 @@ CREATE TABLE list_parted ( CREATE TABLE part_1 PARTITION OF list_parted FOR VALUES IN ('1'); CREATE TABLE part_2 PARTITION OF list_parted FOR VALUES IN (2); CREATE TABLE part_null PARTITION OF list_parted FOR VALUES IN (null); +CREATE TABLE part_default PARTITION OF list_parted DEFAULT; CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN (int '1'); CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN ('1'::int); +CREATE TABLE fail_default_part PARTITION OF list_parted DEFAULT; -- syntax does not allow empty list of values for list partitions CREATE TABLE fail_part PARTITION OF list_parted FOR VALUES IN (); @@ -484,6 +486,8 @@ CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a') TO ('z', -- cannot specify null values in range bounds CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (unbounded); +-- range partition cannot have default partition +CREATE TABLE fail_part PARTITION OF range_parted DEFAULT; -- cannot specify finite values after UNBOUNDED has been specified CREATE TABLE range_parted_multicol (a int, b int, c int) PARTITION BY RANGE (a, b, c); @@ -529,9 +533,13 @@ CREATE TABLE list_parted2 ( ) PARTITION BY LIST (a); CREATE TABLE part_null_z PARTITION OF list_parted2 FOR VALUES IN (null, 'z'); CREATE TABLE part_ab PARTITION OF list_parted2 FOR VALUES IN ('a', 'b'); +CREATE TABLE list_parted2_def PARTITION OF list_parted2 DEFAULT; CREATE TABLE fail_part PARTITION OF list_parted2 FOR VALUES IN (null); CREATE TABLE fail_part PARTITION OF list_parted2 FOR VALUES IN ('b', 'c'); +-- check default partition overlap +INSERT INTO list_parted2 VALUES('X'); +CREATE TABLE fail_part PARTITION OF list_parted2 FOR VALUES IN ('W', 'X', 'Y'); CREATE TABLE range_parted2 ( a int diff --git a/src/test/regress/sql/insert.sql b/src/test/regress/sql/insert.sql index db8967b..a9f2289 100644 --- a/src/test/regress/sql/insert.sql +++ b/src/test/regress/sql/insert.sql @@ -118,27 +118,42 @@ create table list_parted ( create table part_aa_bb partition of list_parted FOR VALUES IN ('aa', 'bb'); create table part_cc_dd partition of list_parted FOR VALUES IN ('cc', 'dd'); create table part_null partition of list_parted FOR VALUES IN (null); +create table part_default partition of list_parted default; -- fail insert into part_aa_bb values ('cc', 1); insert into part_aa_bb values ('AAa', 1); insert into part_aa_bb values (null); +insert into part_default values ('aa', 2); +insert into part_default values (null, 2); -- ok insert into part_cc_dd values ('cC', 1); insert into part_null values (null, 0); +insert into part_default values ('Zz', 2); -- check in case of multi-level partitioned table create table part_ee_ff partition of list_parted for values in ('ee', 'ff') partition by range (b); create table part_ee_ff1 partition of part_ee_ff for values from (1) to (10); create table part_ee_ff2 partition of part_ee_ff for values from (10) to (20); +-- check in case of multi-level default partitioned table +drop table part_default; +create table part_default partition of list_parted default partition by range(b); +create table part_default_p1 partition of part_default for values from (20) to (30); +create table part_default_p2 partition of part_default for values from (30) to (40); -- fail insert into part_ee_ff1 values ('EE', 11); +insert into part_default_p2 values ('gg', 43); -- fail (even the parent's, ie, part_ee_ff's partition constraint applies) insert into part_ee_ff1 values ('cc', 1); +insert into part_default values ('gg', 43); -- ok insert into part_ee_ff1 values ('ff', 1); insert into part_ee_ff2 values ('ff', 11); +insert into part_default_p1 values ('cd', 25); +insert into part_default_p2 values ('gg', 35); +insert into list_parted values ('ab', 21); +drop table part_default; -- Check tuple routing for partitioned tables diff --git a/src/test/regress/sql/update.sql b/src/test/regress/sql/update.sql index 6637119..02840a4 100644 --- a/src/test/regress/sql/update.sql +++ b/src/test/regress/sql/update.sql @@ -125,5 +125,22 @@ update range_parted set b = b - 1 where b = 10; -- ok update range_parted set b = b + 1 where b = 10; +create table list_parted ( + a text, + b int +) partition by list (a); +create table list_part1 partition of list_parted for values in ('a', 'b'); +create table list_default partition of list_parted default; +insert into list_part1 values ('a', 1); +insert into list_default values ('d', 10); + +-- fail +update list_part1 set a = 'c' where a = 'a'; +update list_default set a = 'a' where a = 'd'; +-- ok +update list_part1 set a = 'b' where a = 'a'; +update list_default set a = 'x' where a = 'd'; + -- cleanup drop table range_parted; +drop table list_parted;