From 8ea7585d7619f80f171b4314f741ab1ca561a5af Mon Sep 17 00:00:00 2001 From: amit Date: Thu, 29 Jun 2017 13:50:27 +0900 Subject: [PATCH 2/2] Relax some rules about unbounded range partition bounds Currently, unbounded means - or + infinity depending on whether it appears in FROM or TO, respectively. That rule limits the usefulness of being able to specify unboundedness for range partition columns at all. For example, in case of multi-column partition key, one cannot specify the same bound value as both a partition's upper bound and the next partition's lower bound to establish them as contiguous non-overlapping partitions, if there is unbounded value in the suffix of the bound tuple, because such a specification would be considered to be overlapping per the current rule. Adjust syntax to allow an explicit direction to be specified for unbounded values --- "unbounded below" signifies -infinity, whereas "unbounded above" signifies +infinity. Adjust the logic in get_qual_for_range() to consider the possibility that an infinity in a lower bound could really be +infinity and one in an upper bound could be -infinity. Add tests for the new syntax, tuple-routing, and partition constraint checking for the new use cases allowed by the new syntax. --- doc/src/sgml/ref/create_table.sgml | 16 +++---- src/backend/catalog/partition.c | 74 ++++++++++++++++++++---------- src/backend/parser/gram.y | 37 +++++++++------ src/backend/parser/parse_utilcmd.c | 42 +++++++++++------ src/backend/utils/adt/ruleutils.c | 22 ++++++++- src/include/nodes/parsenodes.h | 8 ++-- src/include/parser/kwlist.h | 2 + src/test/regress/expected/create_table.out | 65 ++++++++++++++++++++------ src/test/regress/expected/insert.out | 49 ++++++++++++++++++++ src/test/regress/sql/create_table.sql | 36 +++++++++++---- src/test/regress/sql/insert.sql | 22 +++++++++ 11 files changed, 285 insertions(+), 88 deletions(-) diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index b15c19d3d0..d146291d9a 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -87,8 +87,8 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI and partition_bound_spec is: IN ( { numeric_literal | string_literal | NULL } [, ...] ) | -FROM ( { numeric_literal | string_literal | UNBOUNDED } [, ...] ) - TO ( { numeric_literal | string_literal | UNBOUNDED } [, ...] ) +FROM ( { numeric_literal | string_literal | [ + | - ] infinity } [, ...] ) + TO ( { numeric_literal | string_literal | [ + | - ] infinity } [, ...] ) index_parameters in UNIQUE, PRIMARY KEY, and EXCLUDE constraints are: @@ -300,13 +300,11 @@ FROM ( { numeric_literal | - Writing UNBOUNDED in FROM - signifies -infinity as the lower bound of the - corresponding column, whereas when written in TO, - it signifies +infinity as the upper bound. - All items following an UNBOUNDED item within - a FROM or TO list must also - be UNBOUNDED. + In the absence of an explicit sign, infinity specified in the + FROM list signifies -infinity, whereas +infinity + if specified in the TO. Note that one cannot + specify a finite value after specifying (+/-) infinity in either + the FROM or the TO list. diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c index 96760a0f05..9f74bc9ce7 100644 --- a/src/backend/catalog/partition.c +++ b/src/backend/catalog/partition.c @@ -377,15 +377,13 @@ RelationBuildPartitionDesc(Relation rel) } /* - * If either of them has infinite element, we can't equate - * them. Even when both are infinite, they'd have - * opposite signs, because only one of cur and prev is a - * lower bound). + * If either of them has infinite element, we can't invoke + * the comparison procedure. */ if (cur->content[j] != RANGE_DATUM_FINITE || prev->content[j] != RANGE_DATUM_FINITE) { - is_distinct = true; + is_distinct = (cur->content[j] != prev->content[j]); break; } cmpval = FunctionCall2Coll(&key->partsupfunc[j], @@ -1396,7 +1394,7 @@ get_qual_for_list(PartitionKey key, PartitionBoundSpec *spec) * * Constructs an Expr for the key column (returned in *keyCol) and Consts * for the lower and upper range limits (returned in *lower_val and - * *upper_val). For UNBOUNDED limits, NULL is returned instead of a Const. + * *upper_val). For infinite limits, NULL is returned instead of a Const. * All of these structures are freshly palloc'd. * * *partexprs_item points to the cell containing the next expression in @@ -1468,17 +1466,21 @@ get_range_key_properties(PartitionKey key, int keynum, * AND * (b < bu) OR (b = bu AND c < cu)) * - * If cu happens to be UNBOUNDED, we need not emit any expression for it, so - * the last line would be: + * If cl happens to be -infinity, we need not emit any expression for it, so + * the AND sub-expression corresponding to the lower bound would be: * - * (b < bu) OR (b = bu), which is simplified to (b <= bu) + * (b > bl) OR (b = bl), which is simplified to (b >= bl) * - * In most common cases with only one partition column, say a, the following - * expression tree will be generated: a IS NOT NULL AND a >= al AND a < au + * But if cl were to be +infinity (because the range partition bound syntax + * allows that), no row with b == bl would qualify for this partition, because + * c >= +infinity will never be true. So, the expression would simply be: + * b > bl. Similarly, if cu happens to be -infinity, the AND sub-expression + * for the upper bound would be b < bu, because no row with b = bu would + * qualify for this partition. * - * If all values of both lower and upper bounds are UNBOUNDED, the partition - * does not really have a constraint, except the IS NOT NULL constraint for - * partition keys. + * In most common cases with only one partition column, say a, the following + * expression tree will be generated: a IS NOT NULL AND a >= al AND a < au, + * provided both al and au are finite values. * * If we end up with an empty result list, we return a single-member list * containing a constant TRUE, because callers expect a non-empty list. @@ -1662,15 +1664,24 @@ get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec) if (need_next_lower_arm && lower_val) { uint16 strategy; + DefElem *inf; + bool next_is_neg_inf = false; + + if (ldatum_next && ldatum_next->infinite) + { + inf = castNode(DefElem, ldatum_next->value); + if (strcmp(inf->defname, "negative_infinity") == 0) + next_is_neg_inf = true; + } /* * For the non-last columns of this arm, use the EQ operator. - * For the last or the last finite-valued column, use GE. + * For the last or the last finite-valued column (provided the + * next infinite column is actually -infinity), use GE. */ if (j - i < current_or_arm) strategy = BTEqualStrategyNumber; - else if ((ldatum_next && ldatum_next->infinite) || - j == key->partnatts - 1) + else if (next_is_neg_inf || j == key->partnatts - 1) strategy = BTGreaterEqualStrategyNumber; else strategy = BTGreaterStrategyNumber; @@ -1685,14 +1696,24 @@ get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec) if (need_next_upper_arm && upper_val) { uint16 strategy; + DefElem *inf; + bool next_is_pos_inf = false; + + if (udatum_next && udatum_next->infinite) + { + inf = castNode(DefElem, udatum_next->value); + if (strcmp(inf->defname, "positive_infinity") == 0) + next_is_pos_inf = true; + } /* * For the non-last columns of this arm, use the EQ operator. - * For the last finite-valued column, use LE. + * For the last finite-valued column (provided the next + * infinite column is actually +infinity), use LE. */ if (j - i < current_or_arm) strategy = BTEqualStrategyNumber; - else if (udatum_next && udatum_next->infinite) + else if (next_is_pos_inf) strategy = BTLessEqualStrategyNumber; else strategy = BTLessStrategyNumber; @@ -2097,12 +2118,19 @@ make_one_range_bound(PartitionKey key, int index, List *datums, bool lower) foreach(lc, datums) { PartitionRangeDatum *datum = castNode(PartitionRangeDatum, lfirst(lc)); + DefElem *inf; /* What's contained in this range datum? */ - bound->content[i] = !datum->infinite - ? RANGE_DATUM_FINITE - : (lower ? RANGE_DATUM_NEG_INF - : RANGE_DATUM_POS_INF); + if (!datum->infinite) + bound->content[i] = RANGE_DATUM_FINITE; + else + { + inf = castNode(DefElem, datum->value); + if (strcmp(inf->defname, "negative_infinity") == 0) + bound->content[i] = RANGE_DATUM_NEG_INF; + else + bound->content[i] = RANGE_DATUM_POS_INF; + } if (bound->content[i] == RANGE_DATUM_FINITE) { diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0f3998ff89..989fd8099b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -601,11 +601,11 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ /* ordinary key words in alphabetical order */ -%token ABORT_P ABSOLUTE_P ACCESS ACTION ADD_P ADMIN AFTER +%token ABORT_P ABSOLUTE_P ABOVE ACCESS ACTION ADD_P ADMIN AFTER AGGREGATE ALL ALSO ALTER ALWAYS ANALYSE ANALYZE AND ANY ARRAY AS ASC ASSERTION ASSIGNMENT ASYMMETRIC AT ATTACH ATTRIBUTE AUTHORIZATION - BACKWARD BEFORE BEGIN_P BETWEEN BIGINT BINARY BIT + BACKWARD BEFORE BEGIN_P BELOW BETWEEN BIGINT BINARY BIT BOOLEAN_P BOTH BY CACHE CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P @@ -2681,6 +2681,23 @@ partbound_datum: Sconst { $$ = makeStringConst($1, @1); } | NumericOnly { $$ = makeAConst($1, @1); } | NULL_P { $$ = makeNullAConst(@1); } + | UNBOUNDED BELOW + { + $$ = (Node *) makeDefElem("negative_infinity", NULL, @1); + } + | UNBOUNDED ABOVE + { + $$ = (Node *) makeDefElem("positive_infinity", NULL, @1); + } + /* + * If a user does not specify the direction, whether it's the + * negative or the positive infinity is determined during the parse + * analysis by seeing which of FROM and TO lists the bound is in. + */ + | UNBOUNDED + { + $$ = (Node *) makeDefElem("infinity", NULL, @1); + } ; partbound_datum_list: @@ -2696,21 +2713,11 @@ range_datum_list: ; PartitionRangeDatum: - UNBOUNDED - { - PartitionRangeDatum *n = makeNode(PartitionRangeDatum); - - n->infinite = true; - n->value = NULL; - n->location = @1; - - $$ = (Node *) n; - } - | partbound_datum + partbound_datum { PartitionRangeDatum *n = makeNode(PartitionRangeDatum); - n->infinite = false; + n->infinite = IsA($1, DefElem); n->value = $1; n->location = @1; @@ -14606,6 +14613,7 @@ ColLabel: IDENT { $$ = $1; } */ unreserved_keyword: ABORT_P + | ABOVE | ABSOLUTE_P | ACCESS | ACTION @@ -14624,6 +14632,7 @@ unreserved_keyword: | BACKWARD | BEFORE | BEGIN_P + | BELOW | BY | CACHE | CALLED diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index ee5f3a3a52..2734677855 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -3365,7 +3365,7 @@ transformPartitionBound(ParseState *pstate, Relation parent, *cell2; int i, j; - bool seen_unbounded; + bool seen_infinity; if (spec->strategy != PARTITION_STRATEGY_RANGE) ereport(ERROR, @@ -3383,35 +3383,51 @@ transformPartitionBound(ParseState *pstate, Relation parent, errmsg("TO must specify exactly one value per partitioning column"))); /* - * Check that no finite value follows an UNBOUNDED item in either of - * lower and upper bound lists. + * Check that no finite value follows an infinity value in either of + * lower and upper bound lists. While at it, convert the "infinite" + * elements in the lowerdatums list into "negative_infinity" and those + * in the upperdatums list into "positive_infinity". */ - seen_unbounded = false; + seen_infinity = false; foreach(cell1, spec->lowerdatums) { PartitionRangeDatum *ldatum = castNode(PartitionRangeDatum, lfirst(cell1)); if (ldatum->infinite) - seen_unbounded = true; - else if (seen_unbounded) + { + DefElem *inf; + + seen_infinity = true; + inf = castNode(DefElem, ldatum->value); + if (strcmp(inf->defname, "infinity") == 0) + inf->defname = pstrdup("negative_infinity"); + } + else if (seen_infinity) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("cannot specify finite value after UNBOUNDED"), + errmsg("cannot specify finite value after infinity"), parser_errposition(pstate, exprLocation((Node *) ldatum)))); } - seen_unbounded = false; + seen_infinity = false; foreach(cell1, spec->upperdatums) { PartitionRangeDatum *rdatum = castNode(PartitionRangeDatum, lfirst(cell1)); if (rdatum->infinite) - seen_unbounded = true; - else if (seen_unbounded) + { + DefElem *inf; + + seen_infinity = true; + inf = castNode(DefElem, rdatum->value); + if (strcmp(inf->defname, "infinity") == 0) + inf->defname = pstrdup("positive_infinity"); + } + else if (seen_infinity) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("cannot specify finite value after UNBOUNDED"), + errmsg("cannot specify finite value after infinity"), parser_errposition(pstate, exprLocation((Node *) rdatum)))); } @@ -3444,7 +3460,7 @@ transformPartitionBound(ParseState *pstate, Relation parent, coltype = get_partition_col_typid(key, i); coltypmod = get_partition_col_typmod(key, i); - if (ldatum->value) + if (ldatum->value && !IsA(ldatum->value, DefElem)) { con = castNode(A_Const, ldatum->value); value = transformPartitionBoundValue(pstate, con, @@ -3458,7 +3474,7 @@ transformPartitionBound(ParseState *pstate, Relation parent, ldatum->value = (Node *) value; } - if (rdatum->value) + if (rdatum->value && !IsA(rdatum->value, DefElem)) { con = castNode(A_Const, rdatum->value); value = transformPartitionBoundValue(pstate, con, diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 18d9e27d1e..b445991bf7 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -8681,7 +8681,16 @@ get_rule_expr(Node *node, deparse_context *context, appendStringInfoString(buf, sep); if (datum->infinite) - appendStringInfoString(buf, "UNBOUNDED"); + { + DefElem *inf; + + inf = castNode(DefElem, datum->value); + if (strcmp(inf->defname, + "negative_infinity") == 0) + appendStringInfoString(buf, "unbounded below"); + else + appendStringInfoString(buf, "unbounded above"); + } else { Const *val = castNode(Const, datum->value); @@ -8699,7 +8708,16 @@ get_rule_expr(Node *node, deparse_context *context, appendStringInfoString(buf, sep); if (datum->infinite) - appendStringInfoString(buf, "UNBOUNDED"); + { + DefElem *inf; + + inf = castNode(DefElem, datum->value); + if (strcmp(inf->defname, + "negative_infinity") == 0) + appendStringInfoString(buf, "unbounded below"); + else + appendStringInfoString(buf, "unbounded above"); + } else { Const *val = castNode(Const, datum->value); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 1d96169d34..5fea805717 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -811,14 +811,16 @@ typedef struct PartitionBoundSpec /* * PartitionRangeDatum - can be either a value or UNBOUNDED * - * "value" is an A_Const in raw grammar output, a Const after analysis + * "value" is an A_Const in raw grammar output, a Const after analysis, if + * it represents a finite value. For (-/+) infinity, it contains a suitably + * decorated DefElem instead, both before and after analysis. */ typedef struct PartitionRangeDatum { NodeTag type; - bool infinite; /* true if UNBOUNDED */ - Node *value; /* null if UNBOUNDED */ + bool infinite; /* true if (-/+) infinity */ + Node *value; int location; /* token location, or -1 if unknown */ } PartitionRangeDatum; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f50e45e886..7b71bb39e1 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -27,6 +27,7 @@ /* name, value, category */ PG_KEYWORD("abort", ABORT_P, UNRESERVED_KEYWORD) +PG_KEYWORD("above", ABOVE, UNRESERVED_KEYWORD) PG_KEYWORD("absolute", ABSOLUTE_P, UNRESERVED_KEYWORD) PG_KEYWORD("access", ACCESS, UNRESERVED_KEYWORD) PG_KEYWORD("action", ACTION, UNRESERVED_KEYWORD) @@ -55,6 +56,7 @@ PG_KEYWORD("authorization", AUTHORIZATION, TYPE_FUNC_NAME_KEYWORD) PG_KEYWORD("backward", BACKWARD, UNRESERVED_KEYWORD) PG_KEYWORD("before", BEFORE, UNRESERVED_KEYWORD) PG_KEYWORD("begin", BEGIN_P, UNRESERVED_KEYWORD) +PG_KEYWORD("below", BELOW, UNRESERVED_KEYWORD) PG_KEYWORD("between", BETWEEN, COL_NAME_KEYWORD) PG_KEYWORD("bigint", BIGINT, COL_NAME_KEYWORD) PG_KEYWORD("binary", BINARY, TYPE_FUNC_NAME_KEYWORD) diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out index b6f794e1c2..39a68b454b 100644 --- a/src/test/regress/expected/create_table.out +++ b/src/test/regress/expected/create_table.out @@ -514,11 +514,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 --- cannot specify finite values after UNBOUNDED has been specified +-- 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); -ERROR: cannot specify finite value after UNBOUNDED -LINE 1: ...ge_parted_multicol FOR VALUES FROM (1, UNBOUNDED, 1) TO (UNB... +CREATE TABLE fail_part PARTITION OF range_parted_multicol FOR VALUES FROM (1, unbounded, 1) TO (unbounded, 1, 1); +ERROR: cannot specify finite value after infinity +LINE 1: ...ge_parted_multicol FOR VALUES FROM (1, unbounded, 1) TO (unb... ^ DROP TABLE range_parted_multicol; -- check if compatible with the specified parent @@ -604,10 +604,45 @@ CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, un CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, 20); ERROR: partition "fail_part" would overlap partition "part12" -- cannot create a partition that says column b is allowed to range --- from -infinity to +infinity, while there exist partitions that have --- more specific ranges +-- from unbounded below to unbounded above, while there exist partitions that +-- have more specific ranges CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, unbounded); ERROR: partition "fail_part" would overlap partition "part10" +-- check that range partition bound syntax allows unbounded values to be +-- specified with direction in arbitrary ways. +create table multicol_range (a text, b numeric) partition by range (a, b); +create table multicol_range_1 partition of multicol_range for values from ('a', unbounded) to ('b', unbounded below); +create table multicol_range_2 partition of multicol_range for values from ('b', unbounded) to ('c', unbounded below); +-- Accepts 'd', but not strings lexically greater than 'd' +create table multicol_range_3 partition of multicol_range for values from ('c', unbounded) to ('d', unbounded); +\d+ multicol_range_3 + Table "public.multicol_range_3" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+----------+--------------+------------- + a | text | | | | extended | | + b | numeric | | | | main | | +Partition of: multicol_range FOR VALUES FROM ('c', unbounded below) TO ('d', unbounded above) +Partition constraint: ((a IS NOT NULL) AND (b IS NOT NULL) AND (a >= 'c'::text) AND (a <= 'd'::text)) + +-- Does not accept 'd'; only strings lexically greater than 'd' +create table multicol_range_4 partition of multicol_range for values from ('d', unbounded above) to (unbounded, unbounded); +\d+ multicol_range_4 + Table "public.multicol_range_4" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+----------+--------------+------------- + a | text | | | | extended | | + b | numeric | | | | main | | +Partition of: multicol_range FOR VALUES FROM ('d', unbounded above) TO (unbounded above, unbounded above) +Partition constraint: ((a IS NOT NULL) AND (b IS NOT NULL) AND (a > 'd'::text)) + +-- Some combinations would make the resulting range effectivly empty, which +-- are promptly rejected +create table multicol_range_err partition of multicol_range for values from ('e', unbounded above) to (unbounded below, unbounded); +ERROR: cannot create range partition with empty range +create table multicol_range_err partition of multicol_range for values from ('e', unbounded) to ('e', unbounded below); +ERROR: cannot create range partition with empty range +create table multicol_range_err partition of multicol_range for values from (unbounded above, unbounded) to (unbounded below, unbounded); +ERROR: cannot create range partition with empty range -- check schema propagation from parent CREATE TABLE parted ( a text, @@ -708,7 +743,7 @@ Number of partitions: 3 (Use \d+ to list them.) -- check that we get the expected partition constraints CREATE TABLE range_parted4 (a int, b int, c int) PARTITION BY RANGE (abs(a), abs(b), c); -CREATE TABLE unbounded_range_part PARTITION OF range_parted4 FOR VALUES FROM (UNBOUNDED, UNBOUNDED, UNBOUNDED) TO (UNBOUNDED, UNBOUNDED, UNBOUNDED); +CREATE TABLE unbounded_range_part PARTITION OF range_parted4 FOR VALUES FROM (unbounded, unbounded, unbounded) TO (unbounded, unbounded, unbounded); \d+ unbounded_range_part Table "public.unbounded_range_part" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description @@ -716,11 +751,11 @@ CREATE TABLE unbounded_range_part PARTITION OF range_parted4 FOR VALUES FROM (UN a | integer | | | | plain | | b | integer | | | | plain | | c | integer | | | | plain | | -Partition of: range_parted4 FOR VALUES FROM (UNBOUNDED, UNBOUNDED, UNBOUNDED) TO (UNBOUNDED, UNBOUNDED, UNBOUNDED) +Partition of: range_parted4 FOR VALUES FROM (unbounded below, unbounded below, unbounded below) TO (unbounded above, unbounded above, unbounded above) Partition constraint: ((abs(a) IS NOT NULL) AND (abs(b) IS NOT NULL) AND (c IS NOT NULL)) DROP TABLE unbounded_range_part; -CREATE TABLE range_parted4_1 PARTITION OF range_parted4 FOR VALUES FROM (UNBOUNDED, UNBOUNDED, UNBOUNDED) TO (1, UNBOUNDED, UNBOUNDED); +CREATE TABLE range_parted4_1 PARTITION OF range_parted4 FOR VALUES FROM (unbounded, unbounded, unbounded) TO (1, unbounded, unbounded); \d+ range_parted4_1 Table "public.range_parted4_1" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description @@ -728,10 +763,10 @@ CREATE TABLE range_parted4_1 PARTITION OF range_parted4 FOR VALUES FROM (UNBOUND a | integer | | | | plain | | b | integer | | | | plain | | c | integer | | | | plain | | -Partition of: range_parted4 FOR VALUES FROM (UNBOUNDED, UNBOUNDED, UNBOUNDED) TO (1, UNBOUNDED, UNBOUNDED) +Partition of: range_parted4 FOR VALUES FROM (unbounded below, unbounded below, unbounded below) TO (1, unbounded above, unbounded above) Partition constraint: ((abs(a) IS NOT NULL) AND (abs(b) IS NOT NULL) AND (c IS NOT NULL) AND (abs(a) <= 1)) -CREATE TABLE range_parted4_2 PARTITION OF range_parted4 FOR VALUES FROM (3, 4, 5) TO (6, 7, UNBOUNDED); +CREATE TABLE range_parted4_2 PARTITION OF range_parted4 FOR VALUES FROM (3, 4, 5) TO (6, 7, unbounded); \d+ range_parted4_2 Table "public.range_parted4_2" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description @@ -739,10 +774,10 @@ CREATE TABLE range_parted4_2 PARTITION OF range_parted4 FOR VALUES FROM (3, 4, 5 a | integer | | | | plain | | b | integer | | | | plain | | c | integer | | | | plain | | -Partition of: range_parted4 FOR VALUES FROM (3, 4, 5) TO (6, 7, UNBOUNDED) +Partition of: range_parted4 FOR VALUES FROM (3, 4, 5) TO (6, 7, unbounded above) Partition constraint: ((abs(a) IS NOT NULL) AND (abs(b) IS NOT NULL) AND (c IS NOT NULL) AND ((abs(a) > 3) OR ((abs(a) = 3) AND (abs(b) > 4)) OR ((abs(a) = 3) AND (abs(b) = 4) AND (c >= 5))) AND ((abs(a) < 6) OR ((abs(a) = 6) AND (abs(b) <= 7)))) -CREATE TABLE range_parted4_3 PARTITION OF range_parted4 FOR VALUES FROM (6, 8, UNBOUNDED) TO (9, UNBOUNDED, UNBOUNDED); +CREATE TABLE range_parted4_3 PARTITION OF range_parted4 FOR VALUES FROM (6, 8, unbounded) TO (9, unbounded, unbounded); \d+ range_parted4_3 Table "public.range_parted4_3" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description @@ -750,12 +785,12 @@ CREATE TABLE range_parted4_3 PARTITION OF range_parted4 FOR VALUES FROM (6, 8, U a | integer | | | | plain | | b | integer | | | | plain | | c | integer | | | | plain | | -Partition of: range_parted4 FOR VALUES FROM (6, 8, UNBOUNDED) TO (9, UNBOUNDED, UNBOUNDED) +Partition of: range_parted4 FOR VALUES FROM (6, 8, unbounded below) TO (9, unbounded above, unbounded above) Partition constraint: ((abs(a) IS NOT NULL) AND (abs(b) IS NOT NULL) AND (c IS NOT NULL) AND ((abs(a) > 6) OR ((abs(a) = 6) AND (abs(b) >= 8))) AND (abs(a) <= 9)) DROP TABLE range_parted4; -- cleanup -DROP TABLE parted, list_parted, range_parted, list_parted2, range_parted2, range_parted3; +DROP TABLE parted, list_parted, range_parted, list_parted2, range_parted2, range_parted3, multicol_range; -- comments on partitioned tables columns CREATE TABLE parted_col_comment (a int, b text) PARTITION BY LIST (a); COMMENT ON TABLE parted_col_comment IS 'Am partitioned table'; diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out index d1153f410b..77e71df121 100644 --- a/src/test/regress/expected/insert.out +++ b/src/test/regress/expected/insert.out @@ -508,3 +508,52 @@ ERROR: new row for relation "brtrigpartcon1" violates partition constraint DETAIL: Failing row contains (2, hi there). drop table brtrigpartcon; drop function brtrigpartcon1trigf(); +-- check that tuple-routing works and partition constraint are enforced +-- properly when unbounded values are specified with arbitrary signs in +-- FROM and TO of range partition bounds. +create table blogs (a text, b numeric) partition by range (a, b); +create table blogs_a partition of blogs for values from ('a', unbounded) to ('b', unbounded below); +create table blogs_b partition of blogs for values from ('b', unbounded) to ('c', unbounded below); +-- Note that this one admits just the letter 'd' +create table blogs_c_and_d partition of blogs for values from ('c', unbounded) to ('d', unbounded); +-- This one allows strings lexically greater than letter 'd' +create table blogs_d_to_z partition of blogs for values from ('d', unbounded above) to (unbounded, unbounded); +\d+ blogs + Table "public.blogs" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+----------+--------------+------------- + a | text | | | | extended | | + b | numeric | | | | main | | +Partition key: RANGE (a, b) +Partitions: blogs_a FOR VALUES FROM ('a', unbounded below) TO ('b', unbounded below), + blogs_b FOR VALUES FROM ('b', unbounded below) TO ('c', unbounded below), + blogs_c_and_d FOR VALUES FROM ('c', unbounded below) TO ('d', unbounded above), + blogs_d_to_z FOR VALUES FROM ('d', unbounded above) TO (unbounded above, unbounded above) + +insert into blogs values ('and there it was', 0), ('because thats why', -1), ('come on', 12345), ('d', 132), ('drag', 908.0), ('zappa the legend', 1); +select tableoid::regclass::text, * from blogs order by 1; + tableoid | a | b +---------------+-------------------+------- + blogs_a | and there it was | 0 + blogs_b | because thats why | -1 + blogs_c_and_d | come on | 12345 + blogs_c_and_d | d | 132 + blogs_d_to_z | drag | 908.0 + blogs_d_to_z | zappa the legend | 1 +(6 rows) + +-- indmissible +insert into blogs_a values ('b', 1); +ERROR: new row for relation "blogs_a" violates partition constraint +DETAIL: Failing row contains (b, 1). +insert into blogs_b values ('c', 1); +ERROR: new row for relation "blogs_b" violates partition constraint +DETAIL: Failing row contains (c, 1). +insert into blogs_c_and_d values ('dr', 1); +ERROR: new row for relation "blogs_c_and_d" violates partition constraint +DETAIL: Failing row contains (dr, 1). +insert into blogs_d_to_z values ('d', 1); +ERROR: new row for relation "blogs_d_to_z" violates partition constraint +DETAIL: Failing row contains (d, 1). +-- cleanup +drop table blogs; diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql index cb7aa5bbc6..a25ba23320 100644 --- a/src/test/regress/sql/create_table.sql +++ b/src/test/regress/sql/create_table.sql @@ -485,9 +485,9 @@ 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); --- cannot specify finite values after UNBOUNDED has been specified +-- 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); +CREATE TABLE fail_part PARTITION OF range_parted_multicol FOR VALUES FROM (1, unbounded, 1) TO (unbounded, 1, 1); DROP TABLE range_parted_multicol; -- check if compatible with the specified parent @@ -566,10 +566,28 @@ CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, un CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, 20); -- cannot create a partition that says column b is allowed to range --- from -infinity to +infinity, while there exist partitions that have --- more specific ranges +-- from unbounded below to unbounded above, while there exist partitions that +-- have more specific ranges CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, unbounded); +-- check that range partition bound syntax allows unbounded values to be +-- specified with direction in arbitrary ways. +create table multicol_range (a text, b numeric) partition by range (a, b); +create table multicol_range_1 partition of multicol_range for values from ('a', unbounded) to ('b', unbounded below); +create table multicol_range_2 partition of multicol_range for values from ('b', unbounded) to ('c', unbounded below); +-- Accepts 'd', but not strings lexically greater than 'd' +create table multicol_range_3 partition of multicol_range for values from ('c', unbounded) to ('d', unbounded); +\d+ multicol_range_3 +-- Does not accept 'd'; only strings lexically greater than 'd' +create table multicol_range_4 partition of multicol_range for values from ('d', unbounded above) to (unbounded, unbounded); +\d+ multicol_range_4 + +-- Some combinations would make the resulting range effectivly empty, which +-- are promptly rejected +create table multicol_range_err partition of multicol_range for values from ('e', unbounded above) to (unbounded below, unbounded); +create table multicol_range_err partition of multicol_range for values from ('e', unbounded) to ('e', unbounded below); +create table multicol_range_err partition of multicol_range for values from (unbounded above, unbounded) to (unbounded below, unbounded); + -- check schema propagation from parent CREATE TABLE parted ( @@ -626,19 +644,19 @@ CREATE TABLE part_c_1_10 PARTITION OF part_c FOR VALUES FROM (1) TO (10); -- check that we get the expected partition constraints CREATE TABLE range_parted4 (a int, b int, c int) PARTITION BY RANGE (abs(a), abs(b), c); -CREATE TABLE unbounded_range_part PARTITION OF range_parted4 FOR VALUES FROM (UNBOUNDED, UNBOUNDED, UNBOUNDED) TO (UNBOUNDED, UNBOUNDED, UNBOUNDED); +CREATE TABLE unbounded_range_part PARTITION OF range_parted4 FOR VALUES FROM (unbounded, unbounded, unbounded) TO (unbounded, unbounded, unbounded); \d+ unbounded_range_part DROP TABLE unbounded_range_part; -CREATE TABLE range_parted4_1 PARTITION OF range_parted4 FOR VALUES FROM (UNBOUNDED, UNBOUNDED, UNBOUNDED) TO (1, UNBOUNDED, UNBOUNDED); +CREATE TABLE range_parted4_1 PARTITION OF range_parted4 FOR VALUES FROM (unbounded, unbounded, unbounded) TO (1, unbounded, unbounded); \d+ range_parted4_1 -CREATE TABLE range_parted4_2 PARTITION OF range_parted4 FOR VALUES FROM (3, 4, 5) TO (6, 7, UNBOUNDED); +CREATE TABLE range_parted4_2 PARTITION OF range_parted4 FOR VALUES FROM (3, 4, 5) TO (6, 7, unbounded); \d+ range_parted4_2 -CREATE TABLE range_parted4_3 PARTITION OF range_parted4 FOR VALUES FROM (6, 8, UNBOUNDED) TO (9, UNBOUNDED, UNBOUNDED); +CREATE TABLE range_parted4_3 PARTITION OF range_parted4 FOR VALUES FROM (6, 8, unbounded) TO (9, unbounded, unbounded); \d+ range_parted4_3 DROP TABLE range_parted4; -- cleanup -DROP TABLE parted, list_parted, range_parted, list_parted2, range_parted2, range_parted3; +DROP TABLE parted, list_parted, range_parted, list_parted2, range_parted2, range_parted3, multicol_range; -- comments on partitioned tables columns CREATE TABLE parted_col_comment (a int, b text) PARTITION BY LIST (a); diff --git a/src/test/regress/sql/insert.sql b/src/test/regress/sql/insert.sql index 83c3ad8f53..047a35e87b 100644 --- a/src/test/regress/sql/insert.sql +++ b/src/test/regress/sql/insert.sql @@ -342,3 +342,25 @@ insert into brtrigpartcon values (1, 'hi there'); insert into brtrigpartcon1 values (1, 'hi there'); drop table brtrigpartcon; drop function brtrigpartcon1trigf(); + +-- check that tuple-routing works and partition constraint are enforced +-- properly when unbounded values are specified with arbitrary signs in +-- FROM and TO of range partition bounds. +create table blogs (a text, b numeric) partition by range (a, b); +create table blogs_a partition of blogs for values from ('a', unbounded) to ('b', unbounded below); +create table blogs_b partition of blogs for values from ('b', unbounded) to ('c', unbounded below); +-- Note that this one admits just the letter 'd' +create table blogs_c_and_d partition of blogs for values from ('c', unbounded) to ('d', unbounded); +-- This one allows strings lexically greater than letter 'd' +create table blogs_d_to_z partition of blogs for values from ('d', unbounded above) to (unbounded, unbounded); +\d+ blogs +insert into blogs values ('and there it was', 0), ('because thats why', -1), ('come on', 12345), ('d', 132), ('drag', 908.0), ('zappa the legend', 1); +select tableoid::regclass::text, * from blogs order by 1; +-- indmissible +insert into blogs_a values ('b', 1); +insert into blogs_b values ('c', 1); +insert into blogs_c_and_d values ('dr', 1); +insert into blogs_d_to_z values ('d', 1); + +-- cleanup +drop table blogs; -- 2.11.0