Multi column range partition table

Started by amul sulover 8 years ago41 messages
#1amul sul
sulamul@gmail.com

Hi,

While working on the another patch, I came across the case where
I need an auto generated partition for a mutil-column range partitioned
table having following range bound:

PARTITION p1 FROM (UNBOUNDED, UNBOUNDED) TO (10, 10)
PARTITION p2 FROM (10, 10) TO (10, UNBOUNDED)
PARTITION p3 FROM (10, UNBOUNDED) TO (20, 10)
PARTITION p4 FROM (20, 10) TO (20, UNBOUNDED)
PARTITION p5 FROM (20, UNBOUNDED) TO (UNBOUNDED, UNBOUNDED)

In this, a lower bound of the partition is an upper bound of the
previous partition.

While trying to create p3 partition with (10, UNBOUNDED) to (20, 10) bound,
got an overlap partition error.

Here is the SQL to reproduced this error:

CREATE TABLE range_parted ( i1 int, i2 int ) PARTITION BY RANGE (i1, i2);
CREATE TABLE p1 PARTITION OF range_parted FOR VALUES FROM (UNBOUNDED,
UNBOUNDED) TO (10, 10);
CREATE TABLE p2 PARTITION OF range_parted FOR VALUES FROM (10, 10) TO
(10, UNBOUNDED);
CREATE TABLE p3 PARTITION OF tab1 FOR VALUES FROM (10, UNBOUNDED) TO (20, 10);

ERROR: partition "p3" would overlap partition "tab1_p_10_10"

This happened because of UNBOUNDED handling, where it is a negative infinite
if it is in FROM clause. Wondering can't we explicitly treat this as
a positive infinite value, can we?

Thoughts/Comments?

Regards,
Amul

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#2Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: amul sul (#1)
Re: Multi column range partition table

On 2017/06/22 20:48, amul sul wrote:

Hi,

While working on the another patch, I came across the case where
I need an auto generated partition for a mutil-column range partitioned
table having following range bound:

PARTITION p1 FROM (UNBOUNDED, UNBOUNDED) TO (10, 10)
PARTITION p2 FROM (10, 10) TO (10, UNBOUNDED)
PARTITION p3 FROM (10, UNBOUNDED) TO (20, 10)
PARTITION p4 FROM (20, 10) TO (20, UNBOUNDED)
PARTITION p5 FROM (20, UNBOUNDED) TO (UNBOUNDED, UNBOUNDED)

In this, a lower bound of the partition is an upper bound of the
previous partition.

While trying to create p3 partition with (10, UNBOUNDED) to (20, 10) bound,
got an overlap partition error.

Here is the SQL to reproduced this error:

CREATE TABLE range_parted ( i1 int, i2 int ) PARTITION BY RANGE (i1, i2);
CREATE TABLE p1 PARTITION OF range_parted FOR VALUES FROM (UNBOUNDED,
UNBOUNDED) TO (10, 10);
CREATE TABLE p2 PARTITION OF range_parted FOR VALUES FROM (10, 10) TO
(10, UNBOUNDED);
CREATE TABLE p3 PARTITION OF tab1 FOR VALUES FROM (10, UNBOUNDED) TO (20, 10);

ERROR: partition "p3" would overlap partition "tab1_p_10_10"

This happened because of UNBOUNDED handling, where it is a negative infinite
if it is in FROM clause. Wondering can't we explicitly treat this as
a positive infinite value, can we?

No, we cannot. What would be greater than (or equal to) +infinite?
Nothing. So, even if you will want p3 to accept (10, 9890148), it won't
because 9890148 is not >= +infinite. It will accept only the rows where
the first column is > 10 (second column is not checked in that case).

You will have to define p3 as follows:

CREATE TABLE p3 PARTITION OF tab1 FOR VALUES FROM (11, UNBOUNDED) TO (20, 10);

It's fine to use the previous partition's upper bound as the lower bound
of the current partition, if the former does contain an UNBOUNDED value,
because whereas a finite value divides the range into two parts (assigned
to the two partitions respectively), an UNBOUNDED value does not. The
latter represents an abstract end of the range (either on the positive
side or the negative).

Does that make sense?

Thanks,
Amit

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#3amul sul
sulamul@gmail.com
In reply to: Amit Langote (#2)
Re: Multi column range partition table

On Fri, Jun 23, 2017 at 6:58 AM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/06/22 20:48, amul sul wrote:

Hi,

While working on the another patch, I came across the case where
I need an auto generated partition for a mutil-column range partitioned
table having following range bound:

PARTITION p1 FROM (UNBOUNDED, UNBOUNDED) TO (10, 10)
PARTITION p2 FROM (10, 10) TO (10, UNBOUNDED)
PARTITION p3 FROM (10, UNBOUNDED) TO (20, 10)
PARTITION p4 FROM (20, 10) TO (20, UNBOUNDED)
PARTITION p5 FROM (20, UNBOUNDED) TO (UNBOUNDED, UNBOUNDED)

In this, a lower bound of the partition is an upper bound of the
previous partition.

While trying to create p3 partition with (10, UNBOUNDED) to (20, 10) bound,
got an overlap partition error.

Here is the SQL to reproduced this error:

CREATE TABLE range_parted ( i1 int, i2 int ) PARTITION BY RANGE (i1, i2);
CREATE TABLE p1 PARTITION OF range_parted FOR VALUES FROM (UNBOUNDED,
UNBOUNDED) TO (10, 10);
CREATE TABLE p2 PARTITION OF range_parted FOR VALUES FROM (10, 10) TO
(10, UNBOUNDED);
CREATE TABLE p3 PARTITION OF tab1 FOR VALUES FROM (10, UNBOUNDED) TO (20, 10);

ERROR: partition "p3" would overlap partition "tab1_p_10_10"

This happened because of UNBOUNDED handling, where it is a negative infinite
if it is in FROM clause. Wondering can't we explicitly treat this as
a positive infinite value, can we?

No, we cannot. What would be greater than (or equal to) +infinite?
Nothing. So, even if you will want p3 to accept (10, 9890148), it won't
because 9890148 is not >= +infinite. It will accept only the rows where
the first column is > 10 (second column is not checked in that case).

You will have to define p3 as follows:

CREATE TABLE p3 PARTITION OF tab1 FOR VALUES FROM (11, UNBOUNDED) TO (20, 10);

What if the partition key column is FLOAT ?

Regards,
Amul

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#4Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: amul sul (#3)
Re: Multi column range partition table

On 2017/06/23 13:42, amul sul wrote:

On Fri, Jun 23, 2017 at 6:58 AM, Amit Langote wrote:

On 2017/06/22 20:48, amul sul wrote:

This happened because of UNBOUNDED handling, where it is a negative infinite
if it is in FROM clause. Wondering can't we explicitly treat this as
a positive infinite value, can we?

No, we cannot. What would be greater than (or equal to) +infinite?
Nothing. So, even if you will want p3 to accept (10, 9890148), it won't
because 9890148 is not >= +infinite. It will accept only the rows where
the first column is > 10 (second column is not checked in that case).

You will have to define p3 as follows:

CREATE TABLE p3 PARTITION OF tab1 FOR VALUES FROM (11, UNBOUNDED) TO (20, 10);

What if the partition key column is FLOAT ?

I would say use a value such that the btfloat4cmp (or btfloat8cmp) will
tell it to be greater than 10.

Of course, we can't write what I just said in the user-level
documentation, because the fact that we use system- or user-defined btree
comparison proc (btfloat4/8cmp) for partitioning may be irrelevant to the
users. Although, we do mention in the documentation that we use btree
operator class specified semantics for partitioning. In any case,
defining your partitioning or indexing on raw float type column(s) is
prone to semantic caveats, I'd think.

Also, there was interesting exchange on this topic during the patch
development [1]/messages/by-id/CA+TgmoaucSqQ=dJFhaojSpb1706MQYo1Tfn_3tWv6CVWhAOdrQ@mail.gmail.com. Excerpt:

"Same for ranges of floating-point numbers, which are also probably an
unlikely candidate for a partitioning key anyway."

Thanks,
Amit

[1]: /messages/by-id/CA+TgmoaucSqQ=dJFhaojSpb1706MQYo1Tfn_3tWv6CVWhAOdrQ@mail.gmail.com
/messages/by-id/CA+TgmoaucSqQ=dJFhaojSpb1706MQYo1Tfn_3tWv6CVWhAOdrQ@mail.gmail.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#5Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Amit Langote (#2)
Re: Multi column range partition table

On Fri, Jun 23, 2017 at 6:58 AM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/06/22 20:48, amul sul wrote:

Hi,

While working on the another patch, I came across the case where
I need an auto generated partition for a mutil-column range partitioned
table having following range bound:

PARTITION p1 FROM (UNBOUNDED, UNBOUNDED) TO (10, 10)
PARTITION p2 FROM (10, 10) TO (10, UNBOUNDED)
PARTITION p3 FROM (10, UNBOUNDED) TO (20, 10)
PARTITION p4 FROM (20, 10) TO (20, UNBOUNDED)
PARTITION p5 FROM (20, UNBOUNDED) TO (UNBOUNDED, UNBOUNDED)

In this, a lower bound of the partition is an upper bound of the
previous partition.

While trying to create p3 partition with (10, UNBOUNDED) to (20, 10) bound,
got an overlap partition error.

Here is the SQL to reproduced this error:

CREATE TABLE range_parted ( i1 int, i2 int ) PARTITION BY RANGE (i1, i2);
CREATE TABLE p1 PARTITION OF range_parted FOR VALUES FROM (UNBOUNDED,
UNBOUNDED) TO (10, 10);
CREATE TABLE p2 PARTITION OF range_parted FOR VALUES FROM (10, 10) TO
(10, UNBOUNDED);
CREATE TABLE p3 PARTITION OF tab1 FOR VALUES FROM (10, UNBOUNDED) TO (20, 10);

ERROR: partition "p3" would overlap partition "tab1_p_10_10"

This happened because of UNBOUNDED handling, where it is a negative infinite
if it is in FROM clause. Wondering can't we explicitly treat this as
a positive infinite value, can we?

The way we have designed our syntax, we don't have a way to tell that
p3 comes after p2 and they have no gap between those. But I don't
think that's your question. What you are struggling with is a way to
specify a lower bound (10, +infinity) so that anything with i1 > 10
would go to partition 3.

No, we cannot. What would be greater than (or equal to) +infinite?
Nothing. So, even if you will want p3 to accept (10, 9890148), it won't
because 9890148 is not >= +infinite. It will accept only the rows where
the first column is > 10 (second column is not checked in that case).

You will have to define p3 as follows:

CREATE TABLE p3 PARTITION OF tab1 FOR VALUES FROM (11, UNBOUNDED) TO (20, 10);

That's not exactly same as specifying (10, +infinity) in case i1 is a
float. A user can not precisely tell what would be the acceptable
value just greater than 10.

An UNBOUNDED in the lower bound is always considered as -infinity for
that data type. There is no way to specify a lower bound which has
+infinity in it. +infinite as a lower bounds for the first key may not
make sense (although that means that the partition will always be
empty), but it does make sense for keys after the first as Amul has
explained below.

The question is do we have support for that and if not, will we
consider it for v10 or v11 and how.

It's fine to use the previous partition's upper bound as the lower bound
of the current partition, if the former does contain an UNBOUNDED value,
because whereas a finite value divides the range into two parts (assigned
to the two partitions respectively), an UNBOUNDED value does not. The
latter represents an abstract end of the range (either on the positive
side or the negative).

Not exactly for second key onwards.

--
Best Wishes,
Ashutosh Bapat
EnterpriseDB Corporation
The Postgres Database Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#6Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Ashutosh Bapat (#5)
Re: Multi column range partition table

On 23 June 2017 at 08:01, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

The way we have designed our syntax, we don't have a way to tell that
p3 comes after p2 and they have no gap between those. But I don't
think that's your question. What you are struggling with is a way to
specify a lower bound (10, +infinity) so that anything with i1 > 10
would go to partition 3.

I think actually there is a fundamental problem here, which arises
because UNBOUNDED has 2 different meanings depending on context, and
thus it is not possible in general to specify the start of one range
to be equal to the end of the previous range, as is necessary to get
contiguous non-overlapping ranges.

Note that this isn't just a problem for floating point datatypes
either, it also applies to other types such as strings. For example,
given a partition over (text, int) types defined with the following
values:

FROM ('a', UNBOUNDED) TO ('b', UNBOUNDED)

which is equivalent to

FROM ('a', -INFINITY) TO ('b', +INFINITY)

where should the next range start?

Even if you were to find a way to specify "the next string after 'b'",
it wouldn't exactly be pretty. The problem is that the above partition
corresponds to "all the strings starting with 'a', plus the string
'b', which is pretty ugly. A neater way to define the pair of ranges
in this case would be:

FROM ('a', -INFINITY) TO ('b', -INFINITY)
FROM ('b', -INFINITY) TO ('c', -INFINITY)

since then all strings starting with 'a' would fall into the first
partition and all the strings starting with 'b' would fall into the
second one.

Currently, when there are 2 partition columns, the partition
constraint is defined as

(a is not null) and (b is not null)
and
(a > al or (a = al and b >= bl))
and
(a < au or (a = au and b < bu))

if the upper bound bu were allowed to be -INFINITY (something that
should probably be forbidden unless the previous column's upper bound
were finite), then this would simplify to

(a is not null) and (b is not null)
and
(a > al or (a = al and b >= bl))
and
(a < au)

and in the example above, where al is -INFINITY, it would further simplify to

(a is not null) and (b is not null)
and
(a >= al)
and
(a < au)

There would also be a similar simplification possible if the lower
bound of a partition column were allowed to be +INFINITY.

So, I think that having UNBOUNDED represent both -INFINITY and
+INFINITY depending on context is a design flaw, and that we need to
allow both -INFINITY and +INFINITY as upper and lower bounds (provided
they are preceded by a column with a finite bound). I think that, in
general, that's the only way to allow contiguous non-overlapping
partitions to be defined on multiple columns.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#7Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Dean Rasheed (#6)
2 attachment(s)
Re: Multi column range partition table

On 2017/06/23 17:00, Dean Rasheed wrote:

On 23 June 2017 at 08:01, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

The way we have designed our syntax, we don't have a way to tell that
p3 comes after p2 and they have no gap between those. But I don't
think that's your question. What you are struggling with is a way to
specify a lower bound (10, +infinity) so that anything with i1 > 10
would go to partition 3.

I think actually there is a fundamental problem here, which arises
because UNBOUNDED has 2 different meanings depending on context, and
thus it is not possible in general to specify the start of one range
to be equal to the end of the previous range, as is necessary to get
contiguous non-overlapping ranges.

Okay, I thought about this a bit more and I think I realize that this
arbitrary-sounding restriction of allowing only -infinity in FROM and
+infinity in TO limits the usefulness of the feature to specify infinite
bounds at all.

Note that this isn't just a problem for floating point datatypes
either, it also applies to other types such as strings. For example,
given a partition over (text, int) types defined with the following
values:

FROM ('a', UNBOUNDED) TO ('b', UNBOUNDED)

which is equivalent to

FROM ('a', -INFINITY) TO ('b', +INFINITY)

where should the next range start?

Even if you were to find a way to specify "the next string after 'b'",
it wouldn't exactly be pretty. The problem is that the above partition
corresponds to "all the strings starting with 'a', plus the string
'b', which is pretty ugly. A neater way to define the pair of ranges
in this case would be:

FROM ('a', -INFINITY) TO ('b', -INFINITY)
FROM ('b', -INFINITY) TO ('c', -INFINITY)

since then all strings starting with 'a' would fall into the first
partition and all the strings starting with 'b' would fall into the
second one.

I agree that a valid use case like the one above is awkward to express
currently.

Currently, when there are 2 partition columns, the partition
constraint is defined as

(a is not null) and (b is not null)
and
(a > al or (a = al and b >= bl))
and
(a < au or (a = au and b < bu))

if the upper bound bu were allowed to be -INFINITY (something that
should probably be forbidden unless the previous column's upper bound
were finite), then this would simplify to

(a is not null) and (b is not null)
and
(a > al or (a = al and b >= bl))
and
(a < au)

and in the example above, where al is -INFINITY, it would further simplify to

(a is not null) and (b is not null)
and
(a >= al)
and
(a < au)

There would also be a similar simplification possible if the lower
bound of a partition column were allowed to be +INFINITY.

Yep.

So, I think that having UNBOUNDED represent both -INFINITY and
+INFINITY depending on context is a design flaw, and that we need to
allow both -INFINITY and +INFINITY as upper and lower bounds (provided
they are preceded by a column with a finite bound). I think that, in
general, that's the only way to allow contiguous non-overlapping
partitions to be defined on multiple columns.

Alright, I spent some time implementing a patch to allow specifying
-infinity and +infinity in arbitrary ways. Of course, it prevents
nonsensical inputs with appropriate error messages.

When implementing the same, I initially thought that the only grammar
modification required is to allow specifying a sign before the unbounded
keyword, but thought it sounded strange to call the actual bound values
-unbounded and +unbounded.  While the keyword "unbounded" describes the
property of being unbounded, actual values are really -infinity and
+infinity.  So, I decided to instead modify the grammar to accept
-infinity and +infinity in the FROM and TO lists.  The sign is optional
and in its absence, infinity in FROM means -infinity and vice versa.  This
decision may be seen as controversial, now that we are actually in beta,
if we decide to go with this patch at all.

Some adjustments were required in the logic in partition.c that depended
on the old assumption that all infinite values in the lower bound meant
-infinity and vice versa. That includes get_qual_for_range() being able
to simplify the partition constraint as Dean mentioned in his email.

When testing the patch, I realized that the current code in
check_new_partition_bound() that checks for range partition overlap had a
latent bug that resulted in false positives for the new cases that the new
less restrictive syntax allowed. I spent some time simplifying that code
while also fixing the aforementioned bug. It's implemented in the
attached patch 0001.

0002 is the patch that implements the new syntax.

It's possible that this won't be considered a PG 10 open item but a new
feature and so PG 11 material, as Ashutosh also wondered.

Thanks,
Amit

Attachments:

0001-Simplify-code-that-checks-range-partition-overlap.patchtext/plain; charset=UTF-8; name=0001-Simplify-code-that-checks-range-partition-overlap.patchDownload
From 3487d63069a2ba7cb205ded31be235bcf08fc0fa Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Thu, 29 Jun 2017 16:39:07 +0900
Subject: [PATCH 1/2] Simplify code that checks range partition overlap

While making the logic a sightly easier to reason about, a latent
bug in the logic was fixed in this simplification process, whereby
false positives would occur (partitions that don't actually overlap
would be concluded to overlap).
---
 src/backend/catalog/partition.c            | 64 ++++++++++--------------------
 src/test/regress/expected/create_table.out |  2 +-
 2 files changed, 23 insertions(+), 43 deletions(-)

diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c
index f8c55b1fe7..457b2fef66 100644
--- a/src/backend/catalog/partition.c
+++ b/src/backend/catalog/partition.c
@@ -753,68 +753,48 @@ check_new_partition_bound(char *relname, Relation parent,
 						   boundinfo->strategy == PARTITION_STRATEGY_RANGE);
 
 					/*
-					 * Firstly, find the greatest range bound that is less
-					 * than or equal to the new lower bound.
+					 * Find the greatest range bound among those of the
+					 * existing partitions that is less than or equal to the
+					 * new lower bound.
 					 */
 					off1 = partition_bound_bsearch(key, boundinfo, lower, true,
 												   &equal);
 
 					/*
-					 * off1 == -1 means that all existing bounds are greater
-					 * than the new lower bound.  In that case and the case
-					 * where no partition is defined between the bounds at
-					 * off1 and off1 + 1, we have a "gap" in the range that
-					 * could be occupied by the new partition.  We confirm if
-					 * so by checking whether the new upper bound is confined
-					 * within the gap.
+					 * If there is a "gap" in the range immediately on the
+					 * right of the returned bound (one at off1, which the new
+					 * lower bound is greater than or equal to), then the
+					 * new partition could occupy the same.  Only if its upper
+					 * bound is also confined within the gap.
 					 */
-					if (!equal && boundinfo->indexes[off1 + 1] < 0)
+					if (boundinfo->indexes[off1 + 1] < 0)
 					{
+						equal = false;
 						off2 = partition_bound_bsearch(key, boundinfo, upper,
 													   true, &equal);
-
 						/*
-						 * If the new upper bound is returned to be equal to
-						 * the bound at off2, the latter must be the upper
-						 * bound of some partition with which the new
-						 * partition clearly overlaps.
-						 *
-						 * Also, if bound at off2 is not same as the one
-						 * returned for the new lower bound (IOW, off1 !=
-						 * off2), then the new partition overlaps at least one
-						 * partition.
+						 * If the new upper bound turns out to be crossing
+						 * over the available gap, then the new partition
+						 * overlaps with the partition(s) on the right.
+						 * However, because a partition's upper bound can be
+						 * equal to the lower bound of the partition
+						 * immediately on the the right, discount that case.
 						 */
-						if (equal || off1 != off2)
+						if (off2 > off1 + 1 || ((off2 == off1 + 1) && !equal))
 						{
 							overlap = true;
 
 							/*
-							 * The bound at off2 could be the lower bound of
-							 * the partition with which the new partition
-							 * overlaps.  In that case, use the upper bound
-							 * (that is, the bound at off2 + 1) to get the
-							 * index of that partition.
+							 * Although the new upper bound might have crossed
+							 * over multiple partitions on the right, we only
+							 * ever report the immediately adjacent one.
 							 */
-							if (boundinfo->indexes[off2] < 0)
-								with = boundinfo->indexes[off2 + 1];
-							else
-								with = boundinfo->indexes[off2];
+							with = boundinfo->indexes[off1 + 2];
 						}
 					}
 					else
 					{
-						/*
-						 * Equal has been set to true and there is no "gap"
-						 * between the bound at off1 and that at off1 + 1, so
-						 * the new partition will overlap some partition. In
-						 * the former case, the new lower bound is found to be
-						 * equal to the bound at off1, which could only ever
-						 * be true if the latter is the lower bound of some
-						 * partition.  It's clear in such a case that the new
-						 * partition overlaps that partition, whose index we
-						 * get using its upper bound (that is, using the bound
-						 * at off1 + 1).
-						 */
+						/* There is no gap. */
 						overlap = true;
 						with = boundinfo->indexes[off1 + 1];
 					}
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index fb8745be04..b6f794e1c2 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -589,7 +589,7 @@ CREATE TABLE part3 PARTITION OF range_parted2 FOR VALUES FROM (30) TO (40);
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (30);
 ERROR:  partition "fail_part" would overlap partition "part2"
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (50);
-ERROR:  partition "fail_part" would overlap partition "part3"
+ERROR:  partition "fail_part" would overlap partition "part2"
 -- now check for multi-column range partition key
 CREATE TABLE range_parted3 (
 	a int,
-- 
2.11.0

0002-Relax-some-rules-about-unbounded-range-partition-bou.patchtext/plain; charset=UTF-8; name=0002-Relax-some-rules-about-unbounded-range-partition-bou.patchDownload
From 52574b20e53e5da94ffe256ae30b0bb19503659a Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
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 with the current rule.

Adjust syntax to allow an explicit sign to be specified for unbounded
values.  Also, replace the 'unbounded' keyword with 'infinity', because
that's what seems to make sense with a sign.

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 relaxed syntax.
---
 doc/src/sgml/ref/create_table.sgml         | 16 +++---
 src/backend/catalog/partition.c            | 74 +++++++++++++++++++---------
 src/backend/parser/gram.y                  | 36 ++++++++------
 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                |  1 +
 src/test/regress/expected/create_table.out | 79 +++++++++++++++++++++---------
 src/test/regress/expected/inherit.out      |  6 +--
 src/test/regress/expected/insert.out       | 61 ++++++++++++++++++++---
 src/test/regress/sql/create_table.sql      | 50 +++++++++++++------
 src/test/regress/sql/inherit.sql           |  6 +--
 src/test/regress/sql/insert.sql            | 34 ++++++++++---
 13 files changed, 315 insertions(+), 120 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
 <phrase>and <replaceable class="PARAMETER">partition_bound_spec</replaceable> is:</phrase>
 
 IN ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | NULL } [, ...] ) |
-FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | UNBOUNDED } [, ...] )
-  TO ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | UNBOUNDED } [, ...] )
+FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | [ + | - ] infinity } [, ...] )
+  TO ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | [ + | - ] infinity } [, ...] )
 
 <phrase><replaceable class="PARAMETER">index_parameters</replaceable> in <literal>UNIQUE</literal>, <literal>PRIMARY KEY</literal>, and <literal>EXCLUDE</literal> constraints are:</phrase>
 
@@ -300,13 +300,11 @@ FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replace
      </para>
 
      <para>
-      Writing <literal>UNBOUNDED</literal> in <literal>FROM</literal>
-      signifies <literal>-infinity</literal> as the lower bound of the
-      corresponding column, whereas when written in <literal>TO</literal>,
-      it signifies <literal>+infinity</literal> as the upper bound.
-      All items following an <literal>UNBOUNDED</literal> item within
-      a <literal>FROM</literal> or <literal>TO</literal> list must also
-      be <literal>UNBOUNDED</literal>.
+      In the absence of an explicit sign, infinity specified in the
+      <literal>FROM</literal> list signifies -infinity, whereas +infinity
+      if specified in the <literal>TO</literal>.  Note that one cannot
+      specify a finite value after specifying (+/-) infinity in either
+      the <literal>FROM</literal> or the <literal>TO</literal> list.
      </para>
 
      <para>
diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c
index 457b2fef66..0e2b60e5a8 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],
@@ -1392,7 +1390,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
@@ -1464,17 +1462,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.
@@ -1658,15 +1660,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;
@@ -1681,14 +1692,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;
@@ -2093,12 +2114,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..111f35ce8b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -634,8 +634,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	HANDLER HAVING HEADER_P HOLD HOUR_P
 
 	IDENTITY_P IF_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P
-	INCLUDING INCREMENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P
-	INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER
+	INCLUDING INCREMENT INDEX INDEXES INFINITY INHERIT INHERITS INITIALLY
+	INLINE_P INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER
 	INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION
 
 	JOIN
@@ -2681,6 +2681,23 @@ partbound_datum:
 			Sconst			{ $$ = makeStringConst($1, @1); }
 			| NumericOnly	{ $$ = makeAConst($1, @1); }
 			| NULL_P		{ $$ = makeNullAConst(@1); }
+			| '-' INFINITY
+				{
+					$$ = (Node *) makeDefElem("negative_infinity", NULL, @1);
+				}
+			| '+' INFINITY
+				{
+					$$ = (Node *) makeDefElem("positive_infinity", NULL, @1);
+				}
+			/*
+			 * If a user skips the sign, 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.
+			 */
+			| INFINITY
+				{
+					$$ = (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;
 
@@ -14714,6 +14721,7 @@ unreserved_keyword:
 			| INCREMENT
 			| INDEX
 			| INDEXES
+			| INFINITY
 			| INHERIT
 			| INHERITS
 			| INLINE_P
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..e093e503b3 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, "-infinity");
+								else
+									appendStringInfoString(buf, "+infinity");
+							}
 							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, "-infinity");
+								else
+									appendStringInfoString(buf, "+infinity");
+							}
 							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..dff0af69bd 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -198,6 +198,7 @@ PG_KEYWORD("including", INCLUDING, UNRESERVED_KEYWORD)
 PG_KEYWORD("increment", INCREMENT, UNRESERVED_KEYWORD)
 PG_KEYWORD("index", INDEX, UNRESERVED_KEYWORD)
 PG_KEYWORD("indexes", INDEXES, UNRESERVED_KEYWORD)
+PG_KEYWORD("infinity", INFINITY, UNRESERVED_KEYWORD)
 PG_KEYWORD("inherit", INHERIT, UNRESERVED_KEYWORD)
 PG_KEYWORD("inherits", INHERITS, UNRESERVED_KEYWORD)
 PG_KEYWORD("initially", INITIALLY, RESERVED_KEYWORD)
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index b6f794e1c2..ab5665b0c8 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -512,13 +512,13 @@ ERROR:  FROM must specify exactly one value per partitioning column
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a') TO ('z', 1);
 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);
+CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (infinity);
 ERROR:  cannot specify NULL in range bound
--- cannot specify finite values after UNBOUNDED has been specified
+-- cannot specify finite values after infinity 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, infinity, 1) TO (infinity, 1, 1);
+ERROR:  cannot specify finite value after infinity
+LINE 1: ...nge_parted_multicol FOR VALUES FROM (1, infinity, 1) TO (inf...
                                                              ^
 DROP TABLE range_parted_multicol;
 -- check if compatible with the specified parent
@@ -578,11 +578,11 @@ ERROR:  cannot create range partition with empty range
 -- note that the range '[1, 1)' has no elements
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (1);
 ERROR:  cannot create range partition with empty range
-CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (1);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (2);
+CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (infinity) TO (1);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (infinity) TO (2);
 ERROR:  partition "fail_part" would overlap partition "part0"
 CREATE TABLE part1 PARTITION OF range_parted2 FOR VALUES FROM (1) TO (10);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (infinity);
 ERROR:  partition "fail_part" would overlap partition "part1"
 CREATE TABLE part2 PARTITION OF range_parted2 FOR VALUES FROM (20) TO (30);
 CREATE TABLE part3 PARTITION OF range_parted2 FOR VALUES FROM (30) TO (40);
@@ -595,19 +595,54 @@ CREATE TABLE range_parted3 (
 	a int,
 	b int
 ) PARTITION BY RANGE (a, (b+1));
-CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, unbounded);
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, 1);
+CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, infinity) TO (0, infinity);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, infinity) TO (0, 1);
 ERROR:  partition "fail_part" would overlap partition "part00"
-CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, 1);
+CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, infinity) TO (1, 1);
 CREATE TABLE part11 PARTITION OF range_parted3 FOR VALUES FROM (1, 1) TO (1, 10);
-CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, unbounded);
+CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, infinity);
 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
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, infinity) TO (1, infinity);
 ERROR:  partition "fail_part" would overlap partition "part10"
+-- check that range partition bound syntax allows -/+ to be specified for
+-- infinity values 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', infinity) to ('b', -infinity);
+create table multicol_range_2 partition of multicol_range for values from ('b', infinity) to ('c', -infinity);
+-- Accepts 'd', but not strings lexically greater than 'd'
+create table multicol_range_3 partition of multicol_range for values from ('c', infinity) to ('d', infinity);
+\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', -infinity) TO ('d', +infinity)
+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', +infinity) to (infinity, infinity);
+\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', +infinity) TO (+infinity, +infinity)
+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', +infinity) to (-infinity, infinity);
+ERROR:  cannot create range partition with empty range
+create table multicol_range_err partition of multicol_range for values from ('e', infinity) to ('e', -infinity);
+ERROR:  cannot create range partition with empty range
+create table multicol_range_err partition of multicol_range for values from (+infinity, infinity) to (-infinity, infinity);
+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 (infinity, infinity, infinity) TO (infinity, infinity, infinity);
 \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 (-infinity, -infinity, -infinity) TO (+infinity, +infinity, +infinity)
 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 (infinity, infinity, infinity) TO (1, infinity, infinity);
 \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 (-infinity, -infinity, -infinity) TO (1, +infinity, +infinity)
 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, infinity);
 \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, +infinity)
 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, infinity) TO (9, infinity, infinity);
 \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, -infinity) TO (9, +infinity, +infinity)
 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/inherit.out b/src/test/regress/expected/inherit.out
index 35d182d599..924a24f97b 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1718,7 +1718,7 @@ create table part_10_20_cd partition of part_10_20 for values in ('cd');
 create table part_21_30 partition of range_list_parted for values from (21) to (30) partition by list (b);
 create table part_21_30_ab partition of part_21_30 for values in ('ab');
 create table part_21_30_cd partition of part_21_30 for values in ('cd');
-create table part_40_inf partition of range_list_parted for values from (40) to (unbounded) partition by list (b);
+create table part_40_inf partition of range_list_parted for values from (40) to (+infinity) partition by list (b);
 create table part_40_inf_ab partition of part_40_inf for values in ('ab');
 create table part_40_inf_cd partition of part_40_inf for values in ('cd');
 create table part_40_inf_null partition of part_40_inf for values in (null);
@@ -1831,12 +1831,12 @@ drop table range_list_parted;
 -- check that constraint exclusion is able to cope with the partition
 -- constraint emitted for multi-column range partitioned tables
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, 1, 1);
+create table mcrparted0 partition of mcrparted for values from (-infinity, -infinity, -infinity) to (1, 1, 1);
 create table mcrparted1 partition of mcrparted for values from (1, 1, 1) to (10, 5, 10);
 create table mcrparted2 partition of mcrparted for values from (10, 5, 10) to (10, 10, 10);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
 create table mcrparted4 partition of mcrparted for values from (20, 10, 10) to (20, 20, 20);
-create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (+infinity, +infinity, +infinity);
 explain (costs off) select * from mcrparted where a = 0;	-- scans mcrparted0
           QUERY PLAN          
 ------------------------------
diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out
index d1153f410b..6047c5b0a6 100644
--- a/src/test/regress/expected/insert.out
+++ b/src/test/regress/expected/insert.out
@@ -288,7 +288,7 @@ select tableoid::regclass, * from list_parted;
 
 -- some more tests to exercise tuple-routing with multi-level partitioning
 create table part_gg partition of list_parted for values in ('gg') partition by range (b);
-create table part_gg1 partition of part_gg for values from (unbounded) to (1);
+create table part_gg1 partition of part_gg for values from (infinity) to (1);
 create table part_gg2 partition of part_gg for values from (1) to (10) partition by range (b);
 create table part_gg2_1 partition of part_gg2 for values from (1) to (5);
 create table part_gg2_2 partition of part_gg2 for values from (5) to (10);
@@ -439,12 +439,12 @@ drop table key_desc, key_desc_1;
 -- check multi-column range partitioning expression enforces the same
 -- constraint as what tuple-routing would determine it to be
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, unbounded, unbounded);
-create table mcrparted1 partition of mcrparted for values from (2, 1, unbounded) to (10, 5, 10);
-create table mcrparted2 partition of mcrparted for values from (10, 6, unbounded) to (10, unbounded, unbounded);
+create table mcrparted0 partition of mcrparted for values from (infinity, infinity, infinity) to (1, infinity, infinity);
+create table mcrparted1 partition of mcrparted for values from (2, 1, infinity) to (10, 5, 10);
+create table mcrparted2 partition of mcrparted for values from (10, 6, infinity) to (10, infinity, infinity);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
-create table mcrparted4 partition of mcrparted for values from (21, unbounded, unbounded) to (30, 20, unbounded);
-create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted4 partition of mcrparted for values from (21, infinity, infinity) to (30, 20, infinity);
+create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (infinity, infinity, infinity);
 -- routed to mcrparted0
 insert into mcrparted values (0, 1, 1);
 insert into mcrparted0 values (0, 1, 1);
@@ -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 infinity 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', infinity) to ('b', -infinity);
+create table blogs_b partition of blogs for values from ('b', infinity) to ('c', -infinity);
+-- Note that this one admits just the letter 'd'
+create table blogs_c_and_d partition of blogs for values from ('c', infinity) to ('d', infinity);
+-- This one allows strings lexically greater than letter 'd'
+create table blogs_d_to_z partition of blogs for values from ('d', +infinity) to (infinity, infinity);
+\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', -infinity) TO ('b', -infinity),
+            blogs_b FOR VALUES FROM ('b', -infinity) TO ('c', -infinity),
+            blogs_c_and_d FOR VALUES FROM ('c', -infinity) TO ('d', +infinity),
+            blogs_d_to_z FOR VALUES FROM ('d', +infinity) TO (+infinity, +infinity)
+
+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..f0bdc7116c 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -483,11 +483,11 @@ CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a', 1) TO ('z
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a') TO ('z', 1);
 
 -- cannot specify null values in range bounds
-CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (infinity);
 
--- cannot specify finite values after UNBOUNDED has been specified
+-- cannot specify finite values after infinity 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, infinity, 1) TO (infinity, 1, 1);
 DROP TABLE range_parted_multicol;
 
 -- check if compatible with the specified parent
@@ -542,10 +542,10 @@ CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (0);
 -- note that the range '[1, 1)' has no elements
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (1);
 
-CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (1);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (2);
+CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (infinity) TO (1);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (infinity) TO (2);
 CREATE TABLE part1 PARTITION OF range_parted2 FOR VALUES FROM (1) TO (10);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (infinity);
 CREATE TABLE part2 PARTITION OF range_parted2 FOR VALUES FROM (20) TO (30);
 CREATE TABLE part3 PARTITION OF range_parted2 FOR VALUES FROM (30) TO (40);
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (30);
@@ -557,18 +557,36 @@ CREATE TABLE range_parted3 (
 	b int
 ) PARTITION BY RANGE (a, (b+1));
 
-CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, unbounded);
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, 1);
+CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, infinity) TO (0, infinity);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, infinity) TO (0, 1);
 
-CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, 1);
+CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, infinity) TO (1, 1);
 CREATE TABLE part11 PARTITION OF range_parted3 FOR VALUES FROM (1, 1) TO (1, 10);
-CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, unbounded);
+CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, infinity);
 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
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, infinity) TO (1, infinity);
+
+-- check that range partition bound syntax allows -/+ to be specified for
+-- infinity values 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', infinity) to ('b', -infinity);
+create table multicol_range_2 partition of multicol_range for values from ('b', infinity) to ('c', -infinity);
+-- Accepts 'd', but not strings lexically greater than 'd'
+create table multicol_range_3 partition of multicol_range for values from ('c', infinity) to ('d', infinity);
+\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', +infinity) to (infinity, infinity);
+\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', +infinity) to (-infinity, infinity);
+create table multicol_range_err partition of multicol_range for values from ('e', infinity) to ('e', -infinity);
+create table multicol_range_err partition of multicol_range for values from (+infinity, infinity) to (-infinity, infinity);
 
 -- check schema propagation from parent
 
@@ -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 (infinity, infinity, infinity) TO (infinity, infinity, infinity);
 \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 (infinity, infinity, infinity) TO (1, infinity, infinity);
 \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, infinity);
 \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, infinity) TO (9, infinity, infinity);
 \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/inherit.sql b/src/test/regress/sql/inherit.sql
index 70fe971d51..663ecc8190 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -623,7 +623,7 @@ create table part_10_20_cd partition of part_10_20 for values in ('cd');
 create table part_21_30 partition of range_list_parted for values from (21) to (30) partition by list (b);
 create table part_21_30_ab partition of part_21_30 for values in ('ab');
 create table part_21_30_cd partition of part_21_30 for values in ('cd');
-create table part_40_inf partition of range_list_parted for values from (40) to (unbounded) partition by list (b);
+create table part_40_inf partition of range_list_parted for values from (40) to (+infinity) partition by list (b);
 create table part_40_inf_ab partition of part_40_inf for values in ('ab');
 create table part_40_inf_cd partition of part_40_inf for values in ('cd');
 create table part_40_inf_null partition of part_40_inf for values in (null);
@@ -647,12 +647,12 @@ drop table range_list_parted;
 -- check that constraint exclusion is able to cope with the partition
 -- constraint emitted for multi-column range partitioned tables
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, 1, 1);
+create table mcrparted0 partition of mcrparted for values from (-infinity, -infinity, -infinity) to (1, 1, 1);
 create table mcrparted1 partition of mcrparted for values from (1, 1, 1) to (10, 5, 10);
 create table mcrparted2 partition of mcrparted for values from (10, 5, 10) to (10, 10, 10);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
 create table mcrparted4 partition of mcrparted for values from (20, 10, 10) to (20, 20, 20);
-create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (+infinity, +infinity, +infinity);
 explain (costs off) select * from mcrparted where a = 0;	-- scans mcrparted0
 explain (costs off) select * from mcrparted where a = 10 and abs(b) < 5;	-- scans mcrparted1
 explain (costs off) select * from mcrparted where a = 10 and abs(b) = 5;	-- scans mcrparted1, mcrparted2
diff --git a/src/test/regress/sql/insert.sql b/src/test/regress/sql/insert.sql
index 83c3ad8f53..dd3baab93a 100644
--- a/src/test/regress/sql/insert.sql
+++ b/src/test/regress/sql/insert.sql
@@ -169,7 +169,7 @@ select tableoid::regclass, * from list_parted;
 
 -- some more tests to exercise tuple-routing with multi-level partitioning
 create table part_gg partition of list_parted for values in ('gg') partition by range (b);
-create table part_gg1 partition of part_gg for values from (unbounded) to (1);
+create table part_gg1 partition of part_gg for values from (infinity) to (1);
 create table part_gg2 partition of part_gg for values from (1) to (10) partition by range (b);
 create table part_gg2_1 partition of part_gg2 for values from (1) to (5);
 create table part_gg2_2 partition of part_gg2 for values from (5) to (10);
@@ -293,12 +293,12 @@ drop table key_desc, key_desc_1;
 -- check multi-column range partitioning expression enforces the same
 -- constraint as what tuple-routing would determine it to be
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, unbounded, unbounded);
-create table mcrparted1 partition of mcrparted for values from (2, 1, unbounded) to (10, 5, 10);
-create table mcrparted2 partition of mcrparted for values from (10, 6, unbounded) to (10, unbounded, unbounded);
+create table mcrparted0 partition of mcrparted for values from (infinity, infinity, infinity) to (1, infinity, infinity);
+create table mcrparted1 partition of mcrparted for values from (2, 1, infinity) to (10, 5, 10);
+create table mcrparted2 partition of mcrparted for values from (10, 6, infinity) to (10, infinity, infinity);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
-create table mcrparted4 partition of mcrparted for values from (21, unbounded, unbounded) to (30, 20, unbounded);
-create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted4 partition of mcrparted for values from (21, infinity, infinity) to (30, 20, infinity);
+create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (infinity, infinity, infinity);
 
 -- routed to mcrparted0
 insert into mcrparted values (0, 1, 1);
@@ -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 infinity 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', infinity) to ('b', -infinity);
+create table blogs_b partition of blogs for values from ('b', infinity) to ('c', -infinity);
+-- Note that this one admits just the letter 'd'
+create table blogs_c_and_d partition of blogs for values from ('c', infinity) to ('d', infinity);
+-- This one allows strings lexically greater than letter 'd'
+create table blogs_d_to_z partition of blogs for values from ('d', +infinity) to (infinity, infinity);
+\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

#8Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Amit Langote (#7)
Re: Multi column range partition table

On Fri, Jun 30, 2017 at 1:36 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

Alright, I spent some time implementing a patch to allow specifying
-infinity and +infinity in arbitrary ways. Of course, it prevents
nonsensical inputs with appropriate error messages.

I don't think -infinity and +infinity are the right terms. For a
string or character data type there is no -infinity and +infinity.
Similarly for enums. We need to extend UNBOUNDED somehow to indicate
the end of a given type in the given direction. I thought about
UNBOUNDED LEFT/RIGHT but then whether LEFT indicates -ve side or +side
would cause confusion. Also LEFT/RIGHT may work for a single
dimensional datatype but not for multi-dimensional spaces. How about
MINIMUM/MAXIMUM or UNBOUNDED MIN/MAX to indicate the extremities.

--
Best Wishes,
Ashutosh Bapat
EnterpriseDB Corporation
The Postgres Database Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#9Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Amit Langote (#7)
1 attachment(s)
Re: Multi column range partition table

On 30 June 2017 at 09:06, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

When testing the patch, I realized that the current code in
check_new_partition_bound() that checks for range partition overlap had a
latent bug that resulted in false positives for the new cases that the new
less restrictive syntax allowed. I spent some time simplifying that code
while also fixing the aforementioned bug. It's implemented in the
attached patch 0001.

I haven't had time to look at 0002 yet, but looking at 0001, I'm not
convinced that this really represents much of a simplification, but I
do prefer the way it now consistently reports the first overlapping
partition in the error message.

I'm not entirely convinced by this change either:

-                        if (equal || off1 != off2)
+                        if (off2 > off1 + 1 || ((off2 == off1 + 1) && !equal))

Passing probe_is_bound = true to partition_bound_bsearch() will I
think cause it to return equal = false when the upper bound of one
partition equals the lower bound of another, so relying on the "equal"
flag here seems a bit dubious. I think I can just about convince
myself that it works, but not for the reasons stated in the comments.

It also seems unnecessary for this code to be doing 2 binary searches.
I think a better simplification would be to just do one binary search
to find the gap that the lower bound fits in, and then test the upper
bound of the new partition against the lower bound of the next
partition (if there is one), as in the attached patch.

Regards,
Dean

Attachments:

simplify-new-range-partition-bounds-check.patchtext/x-patch; charset=US-ASCII; name=simplify-new-range-partition-bounds-check.patchDownload
diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c
new file mode 100644
index 7da2058..96760a0
--- a/src/backend/catalog/partition.c
+++ b/src/backend/catalog/partition.c
@@ -745,78 +745,62 @@ check_new_partition_bound(char *relname,
 				if (partdesc->nparts > 0)
 				{
 					PartitionBoundInfo boundinfo = partdesc->boundinfo;
-					int			off1,
-								off2;
-					bool		equal = false;
+					int			offset;
+					bool		equal;
 
 					Assert(boundinfo && boundinfo->ndatums > 0 &&
 						   boundinfo->strategy == PARTITION_STRATEGY_RANGE);
 
 					/*
-					 * Firstly, find the greatest range bound that is less
-					 * than or equal to the new lower bound.
+					 * Test whether the new lower bound (which is treated
+					 * inclusively as part of the new partition) lies inside an
+					 * existing partition, or in a gap.
+					 *
+					 * If it's in a gap, the next index value will be -1 (the
+					 * lower bound of the next partition).  This is also true
+					 * if there is no next partition, since the index array is
+					 * initialised with an extra -1 at the end.
+					 *
+					 * Note that this also allows for the possibility that the
+					 * new lower bound equals an existing upper bound.
 					 */
-					off1 = partition_bound_bsearch(key, boundinfo, lower, true,
-												   &equal);
+					offset = partition_bound_bsearch(key, boundinfo, lower,
+													 true, &equal);
 
-					/*
-					 * off1 == -1 means that all existing bounds are greater
-					 * than the new lower bound.  In that case and the case
-					 * where no partition is defined between the bounds at
-					 * off1 and off1 + 1, we have a "gap" in the range that
-					 * could be occupied by the new partition.  We confirm if
-					 * so by checking whether the new upper bound is confined
-					 * within the gap.
-					 */
-					if (!equal && boundinfo->indexes[off1 + 1] < 0)
+					if (boundinfo->indexes[offset + 1] < 0)
 					{
-						off2 = partition_bound_bsearch(key, boundinfo, upper,
-													   true, &equal);
-
 						/*
-						 * If the new upper bound is returned to be equal to
-						 * the bound at off2, the latter must be the upper
-						 * bound of some partition with which the new
-						 * partition clearly overlaps.
-						 *
-						 * Also, if bound at off2 is not same as the one
-						 * returned for the new lower bound (IOW, off1 !=
-						 * off2), then the new partition overlaps at least one
-						 * partition.
+						 * Check that the new partition will fit in the gap.
+						 * For it to fit, the new upper bound must be less than
+						 * or equal to the lower bound of the next partition,
+						 * if there is one.
 						 */
-						if (equal || off1 != off2)
+						if (offset + 1 < boundinfo->ndatums)
 						{
-							overlap = true;
+							int32		cmpval;
 
-							/*
-							 * The bound at off2 could be the lower bound of
-							 * the partition with which the new partition
-							 * overlaps.  In that case, use the upper bound
-							 * (that is, the bound at off2 + 1) to get the
-							 * index of that partition.
-							 */
-							if (boundinfo->indexes[off2] < 0)
-								with = boundinfo->indexes[off2 + 1];
-							else
-								with = boundinfo->indexes[off2];
+							cmpval = partition_bound_cmp(key, boundinfo,
+														 offset + 1, upper,
+														 true);
+							if (cmpval < 0)
+							{
+								/*
+								 * The new partition overlaps with the existing
+								 * partition between offset + 1 and offset + 2.
+								 */
+								overlap = true;
+								with = boundinfo->indexes[offset + 2];
+							}
 						}
 					}
 					else
 					{
 						/*
-						 * Equal has been set to true and there is no "gap"
-						 * between the bound at off1 and that at off1 + 1, so
-						 * the new partition will overlap some partition. In
-						 * the former case, the new lower bound is found to be
-						 * equal to the bound at off1, which could only ever
-						 * be true if the latter is the lower bound of some
-						 * partition.  It's clear in such a case that the new
-						 * partition overlaps that partition, whose index we
-						 * get using its upper bound (that is, using the bound
-						 * at off1 + 1).
+						 * The new partition overlaps with the existing
+						 * partition between offset and offset + 1.
 						 */
 						overlap = true;
-						with = boundinfo->indexes[off1 + 1];
+						with = boundinfo->indexes[offset + 1];
 					}
 				}
 
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
new file mode 100644
index fb8745b..b6f794e
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -589,7 +589,7 @@ CREATE TABLE part3 PARTITION OF range_pa
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (30);
 ERROR:  partition "fail_part" would overlap partition "part2"
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (50);
-ERROR:  partition "fail_part" would overlap partition "part3"
+ERROR:  partition "fail_part" would overlap partition "part2"
 -- now check for multi-column range partition key
 CREATE TABLE range_parted3 (
 	a int,
#10Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Ashutosh Bapat (#8)
Re: Multi column range partition table

On 30 June 2017 at 10:04, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

On Fri, Jun 30, 2017 at 1:36 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

Alright, I spent some time implementing a patch to allow specifying
-infinity and +infinity in arbitrary ways. Of course, it prevents
nonsensical inputs with appropriate error messages.

I don't think -infinity and +infinity are the right terms. For a
string or character data type there is no -infinity and +infinity.
Similarly for enums. We need to extend UNBOUNDED somehow to indicate
the end of a given type in the given direction. I thought about
UNBOUNDED LEFT/RIGHT but then whether LEFT indicates -ve side or +side
would cause confusion. Also LEFT/RIGHT may work for a single
dimensional datatype but not for multi-dimensional spaces. How about
MINIMUM/MAXIMUM or UNBOUNDED MIN/MAX to indicate the extremities.

Yes, I think you're right. Also, some datatypes include values that
are equal to +/-infinity, which would then behave differently from
unbounded as range bounds, so it wouldn't be a good idea to overload
that term.

My first thought was UNBOUNDED ABOVE/BELOW, because that matches the
terminology already in use of upper and lower bounds.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#11Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Dean Rasheed (#9)
Re: Multi column range partition table

Hi Dean,

Thanks a lot for the review.

On 2017/07/03 1:59, Dean Rasheed wrote:

On 30 June 2017 at 09:06, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

When testing the patch, I realized that the current code in
check_new_partition_bound() that checks for range partition overlap had a
latent bug that resulted in false positives for the new cases that the new
less restrictive syntax allowed. I spent some time simplifying that code
while also fixing the aforementioned bug. It's implemented in the
attached patch 0001.

I haven't had time to look at 0002 yet, but looking at 0001, I'm not
convinced that this really represents much of a simplification, but I
do prefer the way it now consistently reports the first overlapping
partition in the error message.

I'm not entirely convinced by this change either:

-                        if (equal || off1 != off2)
+                        if (off2 > off1 + 1 || ((off2 == off1 + 1) && !equal))

Passing probe_is_bound = true to partition_bound_bsearch() will I
think cause it to return equal = false when the upper bound of one
partition equals the lower bound of another, so relying on the "equal"
flag here seems a bit dubious. I think I can just about convince
myself that it works, but not for the reasons stated in the comments.

You are right. What's actually happening in the case where I was thinking
equal would be set to true is that off2 ends up being equal to off1, so
the second arm of that || is not checked at all.

It also seems unnecessary for this code to be doing 2 binary searches.
I think a better simplification would be to just do one binary search
to find the gap that the lower bound fits in, and then test the upper
bound of the new partition against the lower bound of the next
partition (if there is one), as in the attached patch.

I agree. The patch looks good to me.

Thanks again.

Regards,
Amit

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#12Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Dean Rasheed (#10)
Re: Multi column range partition table

On 2017/07/03 2:15, Dean Rasheed wrote:

On 30 June 2017 at 10:04, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

On Fri, Jun 30, 2017 at 1:36 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

Alright, I spent some time implementing a patch to allow specifying
-infinity and +infinity in arbitrary ways. Of course, it prevents
nonsensical inputs with appropriate error messages.

I don't think -infinity and +infinity are the right terms. For a
string or character data type there is no -infinity and +infinity.
Similarly for enums. We need to extend UNBOUNDED somehow to indicate
the end of a given type in the given direction. I thought about
UNBOUNDED LEFT/RIGHT but then whether LEFT indicates -ve side or +side
would cause confusion. Also LEFT/RIGHT may work for a single
dimensional datatype but not for multi-dimensional spaces. How about
MINIMUM/MAXIMUM or UNBOUNDED MIN/MAX to indicate the extremities.

Yes, I think you're right. Also, some datatypes include values that
are equal to +/-infinity, which would then behave differently from
unbounded as range bounds, so it wouldn't be a good idea to overload
that term.

Agree with you both that using (+/-) infinity may not be a good idea after
all.

My first thought was UNBOUNDED ABOVE/BELOW, because that matches the
terminology already in use of upper and lower bounds.

I was starting to like the Ashutosh's suggested UNBOUNDED MIN/MAX syntax,
but could you clarify your comment that ABOVE/BELOW is the terminology
already in use of upper and lower bounds? I couldn't find ABOVE/BELOW in
our existing syntax anywhere that uses the upper/lower bound notion, so
was confused a little bit.

Also, I assume UNBOUNDED ABOVE signifies positive infinity and vice versa.

Thanks,
Amit

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#13Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Amit Langote (#12)
2 attachment(s)
Re: Multi column range partition table

On 2017/07/03 14:00, Amit Langote wrote:

On 2017/07/03 2:15, Dean Rasheed wrote:

On 30 June 2017 at 10:04, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

On Fri, Jun 30, 2017 at 1:36 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

Alright, I spent some time implementing a patch to allow specifying
-infinity and +infinity in arbitrary ways. Of course, it prevents
nonsensical inputs with appropriate error messages.

I don't think -infinity and +infinity are the right terms. For a
string or character data type there is no -infinity and +infinity.
Similarly for enums. We need to extend UNBOUNDED somehow to indicate
the end of a given type in the given direction. I thought about
UNBOUNDED LEFT/RIGHT but then whether LEFT indicates -ve side or +side
would cause confusion. Also LEFT/RIGHT may work for a single
dimensional datatype but not for multi-dimensional spaces. How about
MINIMUM/MAXIMUM or UNBOUNDED MIN/MAX to indicate the extremities.

Yes, I think you're right. Also, some datatypes include values that
are equal to +/-infinity, which would then behave differently from
unbounded as range bounds, so it wouldn't be a good idea to overload
that term.

Agree with you both that using (+/-) infinity may not be a good idea after
all.

My first thought was UNBOUNDED ABOVE/BELOW, because that matches the
terminology already in use of upper and lower bounds.

I was starting to like the Ashutosh's suggested UNBOUNDED MIN/MAX syntax,
but could you clarify your comment that ABOVE/BELOW is the terminology
already in use of upper and lower bounds? I couldn't find ABOVE/BELOW in
our existing syntax anywhere that uses the upper/lower bound notion, so
was confused a little bit.

Also, I assume UNBOUNDED ABOVE signifies positive infinity and vice versa.

Anyway, here's the revised version of the syntax patch that implements
ABOVE/BELOW extension to UNBOUNDED specification.

0001 is the patch that Dean posted [1]/messages/by-id/CAEZATCVcBCBZsMcHj37TF+dcsjCtKZdZ_FAaJjaFMvfoXRqZMg@mail.gmail.com as a replacement for what I earlier
posted for simplifying range partition overlap check.

0002 is the UNBOUNDED syntax extension patch.

Thanks,
Amit

[1]: /messages/by-id/CAEZATCVcBCBZsMcHj37TF+dcsjCtKZdZ_FAaJjaFMvfoXRqZMg@mail.gmail.com
/messages/by-id/CAEZATCVcBCBZsMcHj37TF+dcsjCtKZdZ_FAaJjaFMvfoXRqZMg@mail.gmail.com

Attachments:

0001-Dean-s-patch-to-simply-range-partition-overlap-check.patchtext/plain; charset=UTF-8; name=0001-Dean-s-patch-to-simply-range-partition-overlap-check.patchDownload
From e99ee071125b0026e69c5a49cee1865bf380883b Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Mon, 3 Jul 2017 10:52:45 +0900
Subject: [PATCH 1/2] Dean's patch to simply range partition overlap check

---
 src/backend/catalog/partition.c            | 90 ++++++++++++------------------
 src/test/regress/expected/create_table.out |  2 +-
 2 files changed, 38 insertions(+), 54 deletions(-)

diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c
index 7da2058f15..96760a0f05 100644
--- a/src/backend/catalog/partition.c
+++ b/src/backend/catalog/partition.c
@@ -745,78 +745,62 @@ check_new_partition_bound(char *relname, Relation parent,
 				if (partdesc->nparts > 0)
 				{
 					PartitionBoundInfo boundinfo = partdesc->boundinfo;
-					int			off1,
-								off2;
-					bool		equal = false;
+					int			offset;
+					bool		equal;
 
 					Assert(boundinfo && boundinfo->ndatums > 0 &&
 						   boundinfo->strategy == PARTITION_STRATEGY_RANGE);
 
 					/*
-					 * Firstly, find the greatest range bound that is less
-					 * than or equal to the new lower bound.
+					 * Test whether the new lower bound (which is treated
+					 * inclusively as part of the new partition) lies inside an
+					 * existing partition, or in a gap.
+					 *
+					 * If it's in a gap, the next index value will be -1 (the
+					 * lower bound of the next partition).  This is also true
+					 * if there is no next partition, since the index array is
+					 * initialised with an extra -1 at the end.
+					 *
+					 * Note that this also allows for the possibility that the
+					 * new lower bound equals an existing upper bound.
 					 */
-					off1 = partition_bound_bsearch(key, boundinfo, lower, true,
-												   &equal);
+					offset = partition_bound_bsearch(key, boundinfo, lower,
+													 true, &equal);
 
-					/*
-					 * off1 == -1 means that all existing bounds are greater
-					 * than the new lower bound.  In that case and the case
-					 * where no partition is defined between the bounds at
-					 * off1 and off1 + 1, we have a "gap" in the range that
-					 * could be occupied by the new partition.  We confirm if
-					 * so by checking whether the new upper bound is confined
-					 * within the gap.
-					 */
-					if (!equal && boundinfo->indexes[off1 + 1] < 0)
+					if (boundinfo->indexes[offset + 1] < 0)
 					{
-						off2 = partition_bound_bsearch(key, boundinfo, upper,
-													   true, &equal);
-
 						/*
-						 * If the new upper bound is returned to be equal to
-						 * the bound at off2, the latter must be the upper
-						 * bound of some partition with which the new
-						 * partition clearly overlaps.
-						 *
-						 * Also, if bound at off2 is not same as the one
-						 * returned for the new lower bound (IOW, off1 !=
-						 * off2), then the new partition overlaps at least one
-						 * partition.
+						 * Check that the new partition will fit in the gap.
+						 * For it to fit, the new upper bound must be less than
+						 * or equal to the lower bound of the next partition,
+						 * if there is one.
 						 */
-						if (equal || off1 != off2)
+						if (offset + 1 < boundinfo->ndatums)
 						{
-							overlap = true;
+							int32		cmpval;
 
-							/*
-							 * The bound at off2 could be the lower bound of
-							 * the partition with which the new partition
-							 * overlaps.  In that case, use the upper bound
-							 * (that is, the bound at off2 + 1) to get the
-							 * index of that partition.
-							 */
-							if (boundinfo->indexes[off2] < 0)
-								with = boundinfo->indexes[off2 + 1];
-							else
-								with = boundinfo->indexes[off2];
+							cmpval = partition_bound_cmp(key, boundinfo,
+														 offset + 1, upper,
+														 true);
+							if (cmpval < 0)
+							{
+								/*
+								 * The new partition overlaps with the existing
+								 * partition between offset + 1 and offset + 2.
+								 */
+								overlap = true;
+								with = boundinfo->indexes[offset + 2];
+							}
 						}
 					}
 					else
 					{
 						/*
-						 * Equal has been set to true and there is no "gap"
-						 * between the bound at off1 and that at off1 + 1, so
-						 * the new partition will overlap some partition. In
-						 * the former case, the new lower bound is found to be
-						 * equal to the bound at off1, which could only ever
-						 * be true if the latter is the lower bound of some
-						 * partition.  It's clear in such a case that the new
-						 * partition overlaps that partition, whose index we
-						 * get using its upper bound (that is, using the bound
-						 * at off1 + 1).
+						 * The new partition overlaps with the existing
+						 * partition between offset and offset + 1.
 						 */
 						overlap = true;
-						with = boundinfo->indexes[off1 + 1];
+						with = boundinfo->indexes[offset + 1];
 					}
 				}
 
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index fb8745be04..b6f794e1c2 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -589,7 +589,7 @@ CREATE TABLE part3 PARTITION OF range_parted2 FOR VALUES FROM (30) TO (40);
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (30);
 ERROR:  partition "fail_part" would overlap partition "part2"
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (50);
-ERROR:  partition "fail_part" would overlap partition "part3"
+ERROR:  partition "fail_part" would overlap partition "part2"
 -- now check for multi-column range partition key
 CREATE TABLE range_parted3 (
 	a int,
-- 
2.11.0

0002-Relax-some-rules-about-unbounded-range-partition-bou.patchtext/plain; charset=UTF-8; name=0002-Relax-some-rules-about-unbounded-range-partition-bou.patchDownload
From 8ea7585d7619f80f171b4314f741ab1ca561a5af Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
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
 <phrase>and <replaceable class="PARAMETER">partition_bound_spec</replaceable> is:</phrase>
 
 IN ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | NULL } [, ...] ) |
-FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | UNBOUNDED } [, ...] )
-  TO ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | UNBOUNDED } [, ...] )
+FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | [ + | - ] infinity } [, ...] )
+  TO ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | [ + | - ] infinity } [, ...] )
 
 <phrase><replaceable class="PARAMETER">index_parameters</replaceable> in <literal>UNIQUE</literal>, <literal>PRIMARY KEY</literal>, and <literal>EXCLUDE</literal> constraints are:</phrase>
 
@@ -300,13 +300,11 @@ FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replace
      </para>
 
      <para>
-      Writing <literal>UNBOUNDED</literal> in <literal>FROM</literal>
-      signifies <literal>-infinity</literal> as the lower bound of the
-      corresponding column, whereas when written in <literal>TO</literal>,
-      it signifies <literal>+infinity</literal> as the upper bound.
-      All items following an <literal>UNBOUNDED</literal> item within
-      a <literal>FROM</literal> or <literal>TO</literal> list must also
-      be <literal>UNBOUNDED</literal>.
+      In the absence of an explicit sign, infinity specified in the
+      <literal>FROM</literal> list signifies -infinity, whereas +infinity
+      if specified in the <literal>TO</literal>.  Note that one cannot
+      specify a finite value after specifying (+/-) infinity in either
+      the <literal>FROM</literal> or the <literal>TO</literal> list.
      </para>
 
      <para>
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 <keyword> ABORT_P ABSOLUTE_P ACCESS ACTION ADD_P ADMIN AFTER
+%token <keyword> 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

#14Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Amit Langote (#12)
Re: Multi column range partition table

On 3 July 2017 at 06:00, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/07/03 2:15, Dean Rasheed wrote:

My first thought was UNBOUNDED ABOVE/BELOW, because that matches the
terminology already in use of upper and lower bounds.

I was starting to like the Ashutosh's suggested UNBOUNDED MIN/MAX syntax,
but could you clarify your comment that ABOVE/BELOW is the terminology
already in use of upper and lower bounds? I couldn't find ABOVE/BELOW in
our existing syntax anywhere that uses the upper/lower bound notion, so
was confused a little bit.

I just meant that the words "above" and "below" more closely match the
already-used terms "upper" and "lower" for the bounds, so that
terminology seemed more consistent, e.g. "UNBOUNDED ABOVE" => no upper
bound.

Also, I assume UNBOUNDED ABOVE signifies positive infinity and vice versa.

Right.

I'm not particularly wedded to that terminology. I always find naming
things hard, so if anyone can think of anything better, let's hear it.

The bigger question is do we want this for PG10? If so, time is
getting tight. My feeling is that we do, because otherwise we'd be
changing the syntax in PG11 of a feature only just released in PG10,
and I think the current syntax is flawed, so it would be better not to
have it in any public release. I'd feel better hearing from the
original committer though.

Meanwhile, I'll continue trying to review the latest patches...

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#15Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Dean Rasheed (#14)
Re: Multi column range partition table

On Mon, Jul 3, 2017 at 2:06 PM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

On 3 July 2017 at 06:00, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/07/03 2:15, Dean Rasheed wrote:

My first thought was UNBOUNDED ABOVE/BELOW, because that matches the
terminology already in use of upper and lower bounds.

I was starting to like the Ashutosh's suggested UNBOUNDED MIN/MAX syntax,
but could you clarify your comment that ABOVE/BELOW is the terminology
already in use of upper and lower bounds? I couldn't find ABOVE/BELOW in
our existing syntax anywhere that uses the upper/lower bound notion, so
was confused a little bit.

I just meant that the words "above" and "below" more closely match the
already-used terms "upper" and "lower" for the bounds, so that
terminology seemed more consistent, e.g. "UNBOUNDED ABOVE" => no upper
bound.

Also, I assume UNBOUNDED ABOVE signifies positive infinity and vice versa.

Right.

I'm not particularly wedded to that terminology. I always find naming
things hard, so if anyone can think of anything better, let's hear it.

Yet another option: UNBOUNDED UPPER/LOWER.

--
Best Wishes,
Ashutosh Bapat
EnterpriseDB Corporation
The Postgres Database Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#16Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Dean Rasheed (#14)
2 attachment(s)
Re: Multi column range partition table

Hi Dean,

On 2017/07/03 17:36, Dean Rasheed wrote:

On 3 July 2017 at 06:00, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/07/03 2:15, Dean Rasheed wrote:

My first thought was UNBOUNDED ABOVE/BELOW, because that matches the
terminology already in use of upper and lower bounds.

I was starting to like the Ashutosh's suggested UNBOUNDED MIN/MAX syntax,
but could you clarify your comment that ABOVE/BELOW is the terminology
already in use of upper and lower bounds? I couldn't find ABOVE/BELOW in
our existing syntax anywhere that uses the upper/lower bound notion, so
was confused a little bit.

I just meant that the words "above" and "below" more closely match the
already-used terms "upper" and "lower" for the bounds, so that
terminology seemed more consistent, e.g. "UNBOUNDED ABOVE" => no upper
bound.

Also, I assume UNBOUNDED ABOVE signifies positive infinity and vice versa.

Right.

I see, thanks for clarifying.

I'm not particularly wedded to that terminology. I always find naming
things hard, so if anyone can think of anything better, let's hear it.

The bigger question is do we want this for PG10? If so, time is
getting tight. My feeling is that we do, because otherwise we'd be
changing the syntax in PG11 of a feature only just released in PG10,
and I think the current syntax is flawed, so it would be better not to
have it in any public release. I'd feel better hearing from the
original committer though.

The way I have extended the syntax in the posted patch, ABOVE/BELOW (or
whatever we decide instead) are optional. UNBOUNDED without the
ABOVE/BELOW specifications implicitly means UNBOUNDED ABOVE if in FROM and
vice versa, which seems to me like sensible default behavior and what's
already present in PG 10.

Do you think ABOVE/BELOW shouldn't really be optional?

Meanwhile, I'll continue trying to review the latest patches...

I had forgotten to update the CREATE TABLE documentation in 0002 to
reflect the syntax extension. Fixed in the attached latest patch.

Thanks,
Amit

Attachments:

0001-Dean-s-patch-to-simply-range-partition-overlap-check.patchtext/plain; charset=UTF-8; name=0001-Dean-s-patch-to-simply-range-partition-overlap-check.patchDownload
From e99ee071125b0026e69c5a49cee1865bf380883b Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Mon, 3 Jul 2017 10:52:45 +0900
Subject: [PATCH 1/2] Dean's patch to simply range partition overlap check

---
 src/backend/catalog/partition.c            | 90 ++++++++++++------------------
 src/test/regress/expected/create_table.out |  2 +-
 2 files changed, 38 insertions(+), 54 deletions(-)

diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c
index 7da2058f15..96760a0f05 100644
--- a/src/backend/catalog/partition.c
+++ b/src/backend/catalog/partition.c
@@ -745,78 +745,62 @@ check_new_partition_bound(char *relname, Relation parent,
 				if (partdesc->nparts > 0)
 				{
 					PartitionBoundInfo boundinfo = partdesc->boundinfo;
-					int			off1,
-								off2;
-					bool		equal = false;
+					int			offset;
+					bool		equal;
 
 					Assert(boundinfo && boundinfo->ndatums > 0 &&
 						   boundinfo->strategy == PARTITION_STRATEGY_RANGE);
 
 					/*
-					 * Firstly, find the greatest range bound that is less
-					 * than or equal to the new lower bound.
+					 * Test whether the new lower bound (which is treated
+					 * inclusively as part of the new partition) lies inside an
+					 * existing partition, or in a gap.
+					 *
+					 * If it's in a gap, the next index value will be -1 (the
+					 * lower bound of the next partition).  This is also true
+					 * if there is no next partition, since the index array is
+					 * initialised with an extra -1 at the end.
+					 *
+					 * Note that this also allows for the possibility that the
+					 * new lower bound equals an existing upper bound.
 					 */
-					off1 = partition_bound_bsearch(key, boundinfo, lower, true,
-												   &equal);
+					offset = partition_bound_bsearch(key, boundinfo, lower,
+													 true, &equal);
 
-					/*
-					 * off1 == -1 means that all existing bounds are greater
-					 * than the new lower bound.  In that case and the case
-					 * where no partition is defined between the bounds at
-					 * off1 and off1 + 1, we have a "gap" in the range that
-					 * could be occupied by the new partition.  We confirm if
-					 * so by checking whether the new upper bound is confined
-					 * within the gap.
-					 */
-					if (!equal && boundinfo->indexes[off1 + 1] < 0)
+					if (boundinfo->indexes[offset + 1] < 0)
 					{
-						off2 = partition_bound_bsearch(key, boundinfo, upper,
-													   true, &equal);
-
 						/*
-						 * If the new upper bound is returned to be equal to
-						 * the bound at off2, the latter must be the upper
-						 * bound of some partition with which the new
-						 * partition clearly overlaps.
-						 *
-						 * Also, if bound at off2 is not same as the one
-						 * returned for the new lower bound (IOW, off1 !=
-						 * off2), then the new partition overlaps at least one
-						 * partition.
+						 * Check that the new partition will fit in the gap.
+						 * For it to fit, the new upper bound must be less than
+						 * or equal to the lower bound of the next partition,
+						 * if there is one.
 						 */
-						if (equal || off1 != off2)
+						if (offset + 1 < boundinfo->ndatums)
 						{
-							overlap = true;
+							int32		cmpval;
 
-							/*
-							 * The bound at off2 could be the lower bound of
-							 * the partition with which the new partition
-							 * overlaps.  In that case, use the upper bound
-							 * (that is, the bound at off2 + 1) to get the
-							 * index of that partition.
-							 */
-							if (boundinfo->indexes[off2] < 0)
-								with = boundinfo->indexes[off2 + 1];
-							else
-								with = boundinfo->indexes[off2];
+							cmpval = partition_bound_cmp(key, boundinfo,
+														 offset + 1, upper,
+														 true);
+							if (cmpval < 0)
+							{
+								/*
+								 * The new partition overlaps with the existing
+								 * partition between offset + 1 and offset + 2.
+								 */
+								overlap = true;
+								with = boundinfo->indexes[offset + 2];
+							}
 						}
 					}
 					else
 					{
 						/*
-						 * Equal has been set to true and there is no "gap"
-						 * between the bound at off1 and that at off1 + 1, so
-						 * the new partition will overlap some partition. In
-						 * the former case, the new lower bound is found to be
-						 * equal to the bound at off1, which could only ever
-						 * be true if the latter is the lower bound of some
-						 * partition.  It's clear in such a case that the new
-						 * partition overlaps that partition, whose index we
-						 * get using its upper bound (that is, using the bound
-						 * at off1 + 1).
+						 * The new partition overlaps with the existing
+						 * partition between offset and offset + 1.
 						 */
 						overlap = true;
-						with = boundinfo->indexes[off1 + 1];
+						with = boundinfo->indexes[offset + 1];
 					}
 				}
 
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index fb8745be04..b6f794e1c2 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -589,7 +589,7 @@ CREATE TABLE part3 PARTITION OF range_parted2 FOR VALUES FROM (30) TO (40);
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (30);
 ERROR:  partition "fail_part" would overlap partition "part2"
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (50);
-ERROR:  partition "fail_part" would overlap partition "part3"
+ERROR:  partition "fail_part" would overlap partition "part2"
 -- now check for multi-column range partition key
 CREATE TABLE range_parted3 (
 	a int,
-- 
2.11.0

0002-Relax-some-rules-about-unbounded-range-partition-bou.patchtext/plain; charset=UTF-8; name=0002-Relax-some-rules-about-unbounded-range-partition-bou.patchDownload
From 0d4e1a0f91ed6ad7a02338dd7a952ffab57a47f9 Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
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 with the current rule.

Adjust syntax to allow an explicit sign to be specified for unbounded
values.  Also, replace the 'unbounded' keyword with 'infinity', because
that's what seems to make sense with a sign.

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 relaxed syntax.
---
 doc/src/sgml/ref/create_table.sgml         | 22 +++++----
 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, 291 insertions(+), 88 deletions(-)

diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index b15c19d3d0..5d717f17b4 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
 <phrase>and <replaceable class="PARAMETER">partition_bound_spec</replaceable> is:</phrase>
 
 IN ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | NULL } [, ...] ) |
-FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | UNBOUNDED } [, ...] )
-  TO ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | UNBOUNDED } [, ...] )
+FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | UNBOUNDED [ ABOVE | BELOW ] } [, ...] )
+  TO ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | UNBOUNDED [ ABOVE | BELOW ] } [, ...] )
 
 <phrase><replaceable class="PARAMETER">index_parameters</replaceable> in <literal>UNIQUE</literal>, <literal>PRIMARY KEY</literal>, and <literal>EXCLUDE</literal> constraints are:</phrase>
 
@@ -300,13 +300,17 @@ FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replace
      </para>
 
      <para>
-      Writing <literal>UNBOUNDED</literal> in <literal>FROM</literal>
-      signifies <literal>-infinity</literal> as the lower bound of the
-      corresponding column, whereas when written in <literal>TO</literal>,
-      it signifies <literal>+infinity</literal> as the upper bound.
-      All items following an <literal>UNBOUNDED</literal> item within
-      a <literal>FROM</literal> or <literal>TO</literal> list must also
-      be <literal>UNBOUNDED</literal>.
+      When creating a range partition, <literal>UNBOUNDED ABOVE</literal> in
+      either <literal>FROM</literal> or <literal>TO</literal> means that the
+      corresponding partitioning column does not have an upper bound, while
+      <literal>UNBOUNDED BELOW</literal> means that the corresponding
+      partitioning column does not have a lower bound.  Without explicit
+      <literal>ABOVE</literal> / <literal>BELOW</literal> specification, it
+      is assumed to mean <literal>UNBOUNDED ABOVE</literal> if specified in
+      <literal>FROM</literal> and <literal>UNBOUNDED BELOW</literal> if
+      specified in <literal>TO</literal>.  Note that one cannot specify a
+      finite value after a <literal>UNBOUNDED [ ABOVE | BELOW ]</literal>
+      specification in either <literal>FROM</literal> or <literal>TO</literal>.
      </para>
 
      <para>
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 <keyword> ABORT_P ABSOLUTE_P ACCESS ACTION ADD_P ADMIN AFTER
+%token <keyword> 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

#17Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Amit Langote (#16)
Re: Multi column range partition table

On 3 July 2017 at 10:32, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/07/03 17:36, Dean Rasheed wrote:

The bigger question is do we want this for PG10? If so, time is
getting tight. My feeling is that we do, because otherwise we'd be
changing the syntax in PG11 of a feature only just released in PG10,
and I think the current syntax is flawed, so it would be better not to
have it in any public release. I'd feel better hearing from the
original committer though.

The way I have extended the syntax in the posted patch, ABOVE/BELOW (or
whatever we decide instead) are optional. UNBOUNDED without the
ABOVE/BELOW specifications implicitly means UNBOUNDED ABOVE if in FROM and
vice versa, which seems to me like sensible default behavior and what's
already present in PG 10.

Do you think ABOVE/BELOW shouldn't really be optional?

Hmm, I'm not so sure about that.

The more I think about this, the more I think that the current design
is broken, and that introducing UNBOUNDED ABOVE/BELOW is just a
sticking plaster to cover that up. Yes, it allows nicer multi-column
ranges to be defined, as demonstrated upthread. But, it also allows
some pretty counterintuitive things like making the lower bound
exclusive and the upper bound inclusive.

I think that's actually the real problem with the current design. If I
have a single-column partition like

(col) FROM (x) TO (y)

it's pretty clear that's a simple range, inclusive at the lower end
and exclusive at the upper end:

(x) <= (col) < (y)

If I now make that a 2-column partition, but leave the second column
unbounded:

(col1,col2) FROM (x,UNBOUNDED) TO (y,UNBOUNDED)

my initial expectation would have been for that to mean the same
thing, i.e.,

(x) <= (col1) < (y)

but that only happens if "UNBOUNDED" means negative infinity in both
places. That then starts to give the sort of desirable properties
you'd expect, like using the same expression for the lower bound of
one partition as the upper bound of another makes the two partitions
contiguous.

But of course, that's not exactly a pretty design either, because then
you'd be saying that UNBOUNDED means positive infinity if it's the
upper bound of the first column, and negative infinity if it's the
lower bound of the first column or either bound of any other column.

Another aspect of the current design I don't like is that you have to
keep repeating UNBOUNDED [ABOVE/BELOW], for each of the rest of the
columns in the bound, and anything else is an error. That's a pretty
verbose way of saying "the rest of the columns are unbounded".

So the more I think about this, the more I think that a cleaner design
would be as follows:

1). Don't allow UNBOUNDED, except in the first column, where it can
keep it's current meaning.

2). Allow the partition bounds to have fewer columns than the
partition definition, and have that mean the same as it would have
meant if you were partitioning by that many columns. So, for
example, if you were partitioning by (col1,col2), you'd be allowed
to define a partition like so:

FROM (x) TO (y)

and it would mean

x <= col1 < y

Or you'd be able to define a partition like

FROM (x1,x2) TO (y)

which would mean

(col1 > x1) OR (col1 = x1 AND col2 >= x2) AND col1 < y

3). Don't allow any value after UNBOUNDED (i.e., only specify
UNBOUNDED once in a partition bound).

This design has a few neat properties:

- Lower bounds are always inclusive and upper bounds are always
exclusive.

- If the expression for the lower bound of one partition is the same
as the expression for the upper bound of another, the 2 partitions
are contiguous, making it easy to define a covering set of
partitions.

- It's much easier to understand what a bound of "(x)" means than
"(x,UNBOUNDED [ABOVE/BELOW])"

- It's much less verbose, and there's no needless repetition.

Of course, it's pretty late in the day to be proposing this kind of
redesign, but I fear that if we don't tackle it now, it will just be
harder to deal with in the future.

Actually, a quick, simple hacky implementation might be to just fill
in any omitted values in a partition bound with negative infinity
internally, and when printing a bound, omit any values after an
infinite value. But really, I think we'd want to tidy up the
implementation, and I think a number of things would actually get much
simpler. For example, get_qual_for_range() could simply stop when it
reached the end of the list of values for the bound, and it wouldn't
need to worry about an unbounded value following a bounded one.

Thoughts?

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#18Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Dean Rasheed (#17)
2 attachment(s)
Re: Multi column range partition table

Hi Dean,

On 2017/07/04 16:49, Dean Rasheed wrote:

On 3 July 2017 at 10:32, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/07/03 17:36, Dean Rasheed wrote:

The bigger question is do we want this for PG10? If so, time is
getting tight. My feeling is that we do, because otherwise we'd be
changing the syntax in PG11 of a feature only just released in PG10,
and I think the current syntax is flawed, so it would be better not to
have it in any public release. I'd feel better hearing from the
original committer though.

The way I have extended the syntax in the posted patch, ABOVE/BELOW (or
whatever we decide instead) are optional. UNBOUNDED without the
ABOVE/BELOW specifications implicitly means UNBOUNDED ABOVE if in FROM and
vice versa, which seems to me like sensible default behavior and what's
already present in PG 10.

Do you think ABOVE/BELOW shouldn't really be optional?

Hmm, I'm not so sure about that.

The more I think about this, the more I think that the current design
is broken, and that introducing UNBOUNDED ABOVE/BELOW is just a
sticking plaster to cover that up. Yes, it allows nicer multi-column
ranges to be defined, as demonstrated upthread. But, it also allows
some pretty counterintuitive things like making the lower bound
exclusive and the upper bound inclusive.

Yes, I kind of got that impression from the example, but wasn't able to
reach the same conclusion as yours that it stems from the underlying
design issues; I thought we'd just have to document them as caveats, but
that doesn't really sound nice. Thanks for pointing that out.

I think that's actually the real problem with the current design. If I
have a single-column partition like

(col) FROM (x) TO (y)

it's pretty clear that's a simple range, inclusive at the lower end
and exclusive at the upper end:

(x) <= (col) < (y)

If I now make that a 2-column partition, but leave the second column
unbounded:

(col1,col2) FROM (x,UNBOUNDED) TO (y,UNBOUNDED)

my initial expectation would have been for that to mean the same
thing, i.e.,

(x) <= (col1) < (y)

but that only happens if "UNBOUNDED" means negative infinity in both
places. That then starts to give the sort of desirable properties
you'd expect, like using the same expression for the lower bound of
one partition as the upper bound of another makes the two partitions
contiguous.

But of course, that's not exactly a pretty design either, because then
you'd be saying that UNBOUNDED means positive infinity if it's the
upper bound of the first column, and negative infinity if it's the
lower bound of the first column or either bound of any other column.

Initially, I didn't understand the part where you said FROM (x, UNBOUNDED)
TO (y, UNBOUNDED) would mean the same thing as (x) <= (col1) < (y),
because row comparison logic that underlying multi-column range partition
key comparisons appears to me to contradict the same. But, maybe it's
thinking about the implementation details like this that's clouding my
judgement about the correctness or the intuitiveness of the current design.

Another aspect of the current design I don't like is that you have to
keep repeating UNBOUNDED [ABOVE/BELOW], for each of the rest of the
columns in the bound, and anything else is an error. That's a pretty
verbose way of saying "the rest of the columns are unbounded".

So the more I think about this, the more I think that a cleaner design
would be as follows:

1). Don't allow UNBOUNDED, except in the first column, where it can
keep it's current meaning.

2). Allow the partition bounds to have fewer columns than the
partition definition, and have that mean the same as it would have
meant if you were partitioning by that many columns. So, for
example, if you were partitioning by (col1,col2), you'd be allowed
to define a partition like so:

FROM (x) TO (y)

and it would mean

x <= col1 < y

Or you'd be able to define a partition like

FROM (x1,x2) TO (y)

which would mean

(col1 > x1) OR (col1 = x1 AND col2 >= x2) AND col1 < y

3). Don't allow any value after UNBOUNDED (i.e., only specify
UNBOUNDED once in a partition bound).

I assume we don't need the ability of specifying ABOVE/BELOW in this design.

In retrospect, that sounds like something that was implemented in the
earlier versions of the patch, whereby there was no ability to specify
UNBOUNDED on a per-column basis. So the syntax was:

FROM { (x [, ...]) | UNBOUNDED } TO { (y [, ...]) | UNBOUNDED }

But, it was pointed out to me [1]/messages/by-id/CA+TgmoYJcUTcN7vVgg54GHtffH11JJWYZnfF4KiRxjV-iaACQg@mail.gmail.com that that doesn't address the use case,
for example, where part1 goes up to (10, 10) and part2 goes from (10, 10)
up to (10, unbounded).

The new design will limit the usage of unbounded range partitions at the
tail ends.

This design has a few neat properties:

- Lower bounds are always inclusive and upper bounds are always
exclusive.

- If the expression for the lower bound of one partition is the same
as the expression for the upper bound of another, the 2 partitions
are contiguous, making it easy to define a covering set of
partitions.

- It's much easier to understand what a bound of "(x)" means than
"(x,UNBOUNDED [ABOVE/BELOW])"

- It's much less verbose, and there's no needless repetition.

They all sound good to me.

Of course, it's pretty late in the day to be proposing this kind of
redesign, but I fear that if we don't tackle it now, it will just be
harder to deal with in the future.

Actually, a quick, simple hacky implementation might be to just fill
in any omitted values in a partition bound with negative infinity
internally, and when printing a bound, omit any values after an
infinite value. But really, I think we'd want to tidy up the
implementation, and I think a number of things would actually get much
simpler. For example, get_qual_for_range() could simply stop when it
reached the end of the list of values for the bound, and it wouldn't
need to worry about an unbounded value following a bounded one.

Thoughts?

I cooked up a patch for the "hacky" implementation for now, just as you
described in the above paragraph. Will you be willing to give it a look?
I will also think about the non-hacky way of implementing this.

0001 is your patch to tidy up check_new_partition_bound() (must be
applied before 0002)

0002 is the patch to implement the range partition syntax redesign that
you outlined above

Thanks again.

Regards,
Amit

[1]: /messages/by-id/CA+TgmoYJcUTcN7vVgg54GHtffH11JJWYZnfF4KiRxjV-iaACQg@mail.gmail.com
/messages/by-id/CA+TgmoYJcUTcN7vVgg54GHtffH11JJWYZnfF4KiRxjV-iaACQg@mail.gmail.com

Attachments:

0001-Dean-s-patch-to-simply-range-partition-overlap-check.patchtext/plain; charset=UTF-8; name=0001-Dean-s-patch-to-simply-range-partition-overlap-check.patchDownload
From b8c832f8ca80291b953379c6195f5c90a5d05c91 Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Mon, 3 Jul 2017 10:52:45 +0900
Subject: [PATCH 1/2] Dean's patch to simply range partition overlap check

---
 src/backend/catalog/partition.c            | 90 ++++++++++++------------------
 src/test/regress/expected/create_table.out |  2 +-
 2 files changed, 38 insertions(+), 54 deletions(-)

diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c
index 7da2058f15..96760a0f05 100644
--- a/src/backend/catalog/partition.c
+++ b/src/backend/catalog/partition.c
@@ -745,78 +745,62 @@ check_new_partition_bound(char *relname, Relation parent,
 				if (partdesc->nparts > 0)
 				{
 					PartitionBoundInfo boundinfo = partdesc->boundinfo;
-					int			off1,
-								off2;
-					bool		equal = false;
+					int			offset;
+					bool		equal;
 
 					Assert(boundinfo && boundinfo->ndatums > 0 &&
 						   boundinfo->strategy == PARTITION_STRATEGY_RANGE);
 
 					/*
-					 * Firstly, find the greatest range bound that is less
-					 * than or equal to the new lower bound.
+					 * Test whether the new lower bound (which is treated
+					 * inclusively as part of the new partition) lies inside an
+					 * existing partition, or in a gap.
+					 *
+					 * If it's in a gap, the next index value will be -1 (the
+					 * lower bound of the next partition).  This is also true
+					 * if there is no next partition, since the index array is
+					 * initialised with an extra -1 at the end.
+					 *
+					 * Note that this also allows for the possibility that the
+					 * new lower bound equals an existing upper bound.
 					 */
-					off1 = partition_bound_bsearch(key, boundinfo, lower, true,
-												   &equal);
+					offset = partition_bound_bsearch(key, boundinfo, lower,
+													 true, &equal);
 
-					/*
-					 * off1 == -1 means that all existing bounds are greater
-					 * than the new lower bound.  In that case and the case
-					 * where no partition is defined between the bounds at
-					 * off1 and off1 + 1, we have a "gap" in the range that
-					 * could be occupied by the new partition.  We confirm if
-					 * so by checking whether the new upper bound is confined
-					 * within the gap.
-					 */
-					if (!equal && boundinfo->indexes[off1 + 1] < 0)
+					if (boundinfo->indexes[offset + 1] < 0)
 					{
-						off2 = partition_bound_bsearch(key, boundinfo, upper,
-													   true, &equal);
-
 						/*
-						 * If the new upper bound is returned to be equal to
-						 * the bound at off2, the latter must be the upper
-						 * bound of some partition with which the new
-						 * partition clearly overlaps.
-						 *
-						 * Also, if bound at off2 is not same as the one
-						 * returned for the new lower bound (IOW, off1 !=
-						 * off2), then the new partition overlaps at least one
-						 * partition.
+						 * Check that the new partition will fit in the gap.
+						 * For it to fit, the new upper bound must be less than
+						 * or equal to the lower bound of the next partition,
+						 * if there is one.
 						 */
-						if (equal || off1 != off2)
+						if (offset + 1 < boundinfo->ndatums)
 						{
-							overlap = true;
+							int32		cmpval;
 
-							/*
-							 * The bound at off2 could be the lower bound of
-							 * the partition with which the new partition
-							 * overlaps.  In that case, use the upper bound
-							 * (that is, the bound at off2 + 1) to get the
-							 * index of that partition.
-							 */
-							if (boundinfo->indexes[off2] < 0)
-								with = boundinfo->indexes[off2 + 1];
-							else
-								with = boundinfo->indexes[off2];
+							cmpval = partition_bound_cmp(key, boundinfo,
+														 offset + 1, upper,
+														 true);
+							if (cmpval < 0)
+							{
+								/*
+								 * The new partition overlaps with the existing
+								 * partition between offset + 1 and offset + 2.
+								 */
+								overlap = true;
+								with = boundinfo->indexes[offset + 2];
+							}
 						}
 					}
 					else
 					{
 						/*
-						 * Equal has been set to true and there is no "gap"
-						 * between the bound at off1 and that at off1 + 1, so
-						 * the new partition will overlap some partition. In
-						 * the former case, the new lower bound is found to be
-						 * equal to the bound at off1, which could only ever
-						 * be true if the latter is the lower bound of some
-						 * partition.  It's clear in such a case that the new
-						 * partition overlaps that partition, whose index we
-						 * get using its upper bound (that is, using the bound
-						 * at off1 + 1).
+						 * The new partition overlaps with the existing
+						 * partition between offset and offset + 1.
 						 */
 						overlap = true;
-						with = boundinfo->indexes[off1 + 1];
+						with = boundinfo->indexes[offset + 1];
 					}
 				}
 
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index fb8745be04..b6f794e1c2 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -589,7 +589,7 @@ CREATE TABLE part3 PARTITION OF range_parted2 FOR VALUES FROM (30) TO (40);
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (30);
 ERROR:  partition "fail_part" would overlap partition "part2"
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (50);
-ERROR:  partition "fail_part" would overlap partition "part3"
+ERROR:  partition "fail_part" would overlap partition "part2"
 -- now check for multi-column range partition key
 CREATE TABLE range_parted3 (
 	a int,
-- 
2.11.0

0002-Range-partition-bound-specification-syntax-overhaul.patchtext/plain; charset=UTF-8; name=0002-Range-partition-bound-specification-syntax-overhaul.patchDownload
From 6cf7bc6a620517b4d1e61f75ebf3675d5106c94d Mon Sep 17 00:00:00 2001
From: amit <amitlangote09@gmail.com>
Date: Wed, 5 Jul 2017 13:01:21 +0900
Subject: [PATCH 2/2] Range partition bound specification syntax overhaul

---
 doc/src/sgml/ref/create_table.sgml         |  14 +--
 src/backend/catalog/partition.c            |  69 ++++++++-----
 src/backend/parser/parse_utilcmd.c         | 160 ++++++++++++++++++++---------
 src/backend/utils/adt/ruleutils.c          |  35 +++++--
 src/test/regress/expected/create_table.out |  77 ++++++++------
 src/test/regress/expected/inherit.out      |   4 +-
 src/test/regress/expected/insert.out       |  10 +-
 src/test/regress/sql/create_table.sql      |  49 +++++----
 src/test/regress/sql/inherit.sql           |   4 +-
 src/test/regress/sql/insert.sql            |  10 +-
 10 files changed, 280 insertions(+), 152 deletions(-)

diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index b15c19d3d0..4eefe24115 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -300,13 +300,13 @@ FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replace
      </para>
 
      <para>
-      Writing <literal>UNBOUNDED</literal> in <literal>FROM</literal>
-      signifies <literal>-infinity</literal> as the lower bound of the
-      corresponding column, whereas when written in <literal>TO</literal>,
-      it signifies <literal>+infinity</literal> as the upper bound.
-      All items following an <literal>UNBOUNDED</literal> item within
-      a <literal>FROM</literal> or <literal>TO</literal> list must also
-      be <literal>UNBOUNDED</literal>.
+      <literal>UNBOUNDED</literal> can be specified only for the first column
+      of the partition key and it must not be followed by values for the
+      remaining columns.  Specifying <literal>UNBOUNDED</literal> in
+      <literal>FROM</literal> signifies <literal>-infinity</literal> as the
+      lower bound of the corresponding column, whereas when specified in
+      <literal>TO</literal>, it signifies <literal>+infinity</literal> as the
+      upper bound.
      </para>
 
      <para>
diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c
index 96760a0f05..4a788719e5 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],
@@ -1468,17 +1466,14 @@ 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 cu happens to be -infinity (if a user didn't specify cu, it's treated
+ * as such), the last line effectively becomes:
  *
- *	(b < bu) OR (b = bu), which is simplified to (b <= bu)
+ *	(b < bu) OR (b = bu AND FALSE) which is simplied to (b < bu)
  *
  * 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
- *
- * 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.
+ * 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.
@@ -1688,12 +1683,10 @@ get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec)
 
 				/*
 				 * For the non-last columns of this arm, use the EQ operator.
-				 * For the last finite-valued column, use LE.
+				 * and LE for the last column.
 				 */
 				if (j - i < current_or_arm)
 					strategy = BTEqualStrategyNumber;
-				else if (udatum_next && udatum_next->infinite)
-					strategy = BTLessEqualStrategyNumber;
 				else
 					strategy = BTLessStrategyNumber;
 
@@ -2099,17 +2092,26 @@ make_one_range_bound(PartitionKey key, int index, List *datums, bool lower)
 		PartitionRangeDatum *datum = castNode(PartitionRangeDatum, lfirst(lc));
 
 		/* 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 (bound->content[i] == RANGE_DATUM_FINITE)
+		if (datum->infinite)
+		{
+			/*
+			 * Infinite datums for partitioning column other than the first
+			 * column are to be interpreted as negative infinite.
+			 */
+			if (i == 0)
+				bound->content[i] = lower ? RANGE_DATUM_NEG_INF
+										  : RANGE_DATUM_POS_INF;
+			else
+				bound->content[i] = RANGE_DATUM_NEG_INF;
+		}
+		else
 		{
 			Const	   *val = castNode(Const, datum->value);
 
 			if (val->constisnull)
 				elog(ERROR, "invalid range bound datum");
+
+			bound->content[i] = RANGE_DATUM_FINITE;
 			bound->datums[i] = val->constvalue;
 		}
 
@@ -2151,17 +2153,28 @@ partition_rbound_cmp(PartitionKey key,
 	{
 		/*
 		 * First, handle cases involving infinity, which don't require
-		 * invoking the comparison proc.
+		 * invoking the comparison proc.  Infinite datums in only the first
+		 * column are significant as a user-specified boundary condition.
+		 * Its presence in non-first columns is an internal implementation
+		 * detail and both content1[i] and content2[i] would contain
+		 * RANGE_DATUM_NEG_INF in that case.  We tie-break by considering
+		 * the lower bound as the greater of the two.
 		 */
 		if (content1[i] != RANGE_DATUM_FINITE &&
 			content2[i] != RANGE_DATUM_FINITE)
+			return i == 0 ? (content1[i] < content2[i] ? -1 : 1)
+						  : (lower1 ? 1 : -1);
 
 			/*
-			 * Both are infinity, so they are equal unless one is negative
-			 * infinity and other positive (or vice versa)
-			 */
-			return content1[i] == content2[i] ? 0
-				: (content1[i] < content2[i] ? -1 : 1);
+			 * Both are infinity.  If both have the same sign, we know that
+			 * they come from the upper bound and the lower bound of two
+			 * contiguous partitions, respectively.  We tie-break in this
+			 * case by considering the lower bound as the greater one of the
+			 * two.  If they have different signs, then the answer is obvious.
+			 *
+			return content1[i] == content2[i]
+						? (lower1 ? 1 : -1)
+						: (content1[i] < content2[i] ? -1 : 1);*/
 		else if (content1[i] != RANGE_DATUM_FINITE)
 			return content1[i] == RANGE_DATUM_NEG_INF ? -1 : 1;
 		else if (content2[i] != RANGE_DATUM_FINITE)
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index ee5f3a3a52..34390661cd 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3361,8 +3361,7 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 	}
 	else if (strategy == PARTITION_STRATEGY_RANGE)
 	{
-		ListCell   *cell1,
-				   *cell2;
+		ListCell   *cell;
 		int			i,
 					j;
 		bool		seen_unbounded;
@@ -3373,61 +3372,45 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 					 errmsg("invalid bound specification for a range partition"),
 					 parser_errposition(pstate, exprLocation((Node *) spec))));
 
-		if (list_length(spec->lowerdatums) != partnatts)
+		if (list_length(spec->lowerdatums) > partnatts)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("FROM must specify exactly one value per partitioning column")));
-		if (list_length(spec->upperdatums) != partnatts)
+					 errmsg("FROM cannot contain more values than the number of partitioning columns"),
+					 errdetail_plural("\"%s\" has %d partitioning column.",
+									  "\"%s\" has %d partitioning columns.",
+									  partnatts,
+									  RelationGetRelationName(parent),
+									  partnatts)));
+		if (list_length(spec->upperdatums) > partnatts)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 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.
-		 */
-		seen_unbounded = false;
-		foreach(cell1, spec->lowerdatums)
-		{
-			PartitionRangeDatum *ldatum = castNode(PartitionRangeDatum,
-												   lfirst(cell1));
-
-			if (ldatum->infinite)
-				seen_unbounded = true;
-			else if (seen_unbounded)
-				ereport(ERROR,
-						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("cannot specify finite value after UNBOUNDED"),
-						 parser_errposition(pstate, exprLocation((Node *) ldatum))));
-		}
-		seen_unbounded = false;
-		foreach(cell1, spec->upperdatums)
-		{
-			PartitionRangeDatum *rdatum = castNode(PartitionRangeDatum,
-												   lfirst(cell1));
-
-			if (rdatum->infinite)
-				seen_unbounded = true;
-			else if (seen_unbounded)
-				ereport(ERROR,
-						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("cannot specify finite value after UNBOUNDED"),
-						 parser_errposition(pstate, exprLocation((Node *) rdatum))));
-		}
+					 errmsg("TO cannot contain more values than the number of partitioning columns"),
+					 errdetail_plural("\"%s\" has %d partitioning column.",
+									  "\"%s\" has %d partitioning columns.",
+									  partnatts,
+									  RelationGetRelationName(parent),
+									  partnatts)));
 
 		/* Transform all the constants */
+		result_spec->lowerdatums = NIL;
 		i = j = 0;
-		result_spec->lowerdatums = result_spec->upperdatums = NIL;
-		forboth(cell1, spec->lowerdatums, cell2, spec->upperdatums)
+		seen_unbounded = false;
+		foreach(cell, spec->lowerdatums)
 		{
-			PartitionRangeDatum *ldatum = (PartitionRangeDatum *) lfirst(cell1);
-			PartitionRangeDatum *rdatum = (PartitionRangeDatum *) lfirst(cell2);
+			PartitionRangeDatum *ldatum = castNode(PartitionRangeDatum,
+												   lfirst(cell));
 			char	   *colname;
 			Oid			coltype;
 			int32		coltypmod;
 			A_Const    *con;
 			Const	   *value;
 
+			if (seen_unbounded)
+				ereport(ERROR,
+						(errcode(ERRCODE_DATATYPE_MISMATCH),
+						 errmsg("cannot specify a value after UNBOUNDED"),
+						 parser_errposition(pstate, exprLocation((Node *) ldatum))));
+
 			/* Get the column's name in case we need to output an error */
 			if (key->partattrs[i] != 0)
 				colname = get_relid_attribute_name(RelationGetRelid(parent),
@@ -3457,6 +3440,72 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 				ldatum = copyObject(ldatum);	/* don't scribble on input */
 				ldatum->value = (Node *) value;
 			}
+			else if (i > 0)
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_DATATYPE_MISMATCH),
+						 errmsg("cannot specify UNBOUNDED for columns other than the first column"),
+						 parser_errposition(pstate, exprLocation((Node *) ldatum))));
+			}
+			else
+				seen_unbounded = true;
+
+			result_spec->lowerdatums = lappend(result_spec->lowerdatums,
+											   ldatum);
+			++i;
+		}
+
+		/*
+		 * Fill in infinite datums for the remaining columns.  Note that
+		 * infinite datums for non-first columns are always interpreted as
+		 * negative infinity by the interal partitioning code.
+		 */
+		for (j = i; j < partnatts; j++)
+		{
+			PartitionRangeDatum *datum = makeNode(PartitionRangeDatum);
+
+			datum->infinite = true;
+			datum->value = NULL;
+			datum->location = -1;
+			result_spec->lowerdatums = lappend(result_spec->lowerdatums,
+											   datum);
+		}
+		Assert(list_length(result_spec->lowerdatums) == partnatts);
+
+		result_spec->upperdatums = NIL;
+		i = j = 0;
+		seen_unbounded = false;
+		foreach(cell, spec->upperdatums)
+		{
+			PartitionRangeDatum *rdatum = castNode(PartitionRangeDatum,
+												   lfirst(cell));
+			char	   *colname;
+			Oid			coltype;
+			int32		coltypmod;
+			A_Const    *con;
+			Const	   *value;
+
+			if (seen_unbounded)
+				ereport(ERROR,
+						(errcode(ERRCODE_DATATYPE_MISMATCH),
+						 errmsg("cannot specify a value after UNBOUNDED"),
+						 parser_errposition(pstate, exprLocation((Node *) rdatum))));
+
+			/* Get the column's name in case we need to output an error */
+			if (key->partattrs[i] != 0)
+				colname = get_relid_attribute_name(RelationGetRelid(parent),
+												   key->partattrs[i]);
+			else
+			{
+				colname = deparse_expression((Node *) list_nth(partexprs, j),
+											 deparse_context_for(RelationGetRelationName(parent),
+																 RelationGetRelid(parent)),
+											 false, false);
+				++j;
+			}
+			/* Need its type data too */
+			coltype = get_partition_col_typid(key, i);
+			coltypmod = get_partition_col_typmod(key, i);
 
 			if (rdatum->value)
 			{
@@ -3471,14 +3520,33 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 				rdatum = copyObject(rdatum);	/* don't scribble on input */
 				rdatum->value = (Node *) value;
 			}
+			else if (i > 0)
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_DATATYPE_MISMATCH),
+						 errmsg("cannot specify UNBOUNDED for columns other than the first column"),
+						 parser_errposition(pstate, exprLocation((Node *) rdatum))));
+			}
+			else
+				seen_unbounded = true;
 
-			result_spec->lowerdatums = lappend(result_spec->lowerdatums,
-											   ldatum);
 			result_spec->upperdatums = lappend(result_spec->upperdatums,
 											   rdatum);
-
 			++i;
 		}
+
+		/* See the comment above. */
+		for (j = i; j < partnatts; j++)
+		{
+			PartitionRangeDatum *datum = makeNode(PartitionRangeDatum);
+
+			datum->infinite = true;
+			datum->value = NULL;
+			datum->location = -1;
+			result_spec->upperdatums = lappend(result_spec->upperdatums,
+											   datum);
+		}
+		Assert(list_length(result_spec->upperdatums) == partnatts);
 	}
 	else
 		elog(ERROR, "unexpected partition strategy: %d", (int) strategy);
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 18d9e27d1e..25be1bad52 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -8646,6 +8646,7 @@ get_rule_expr(Node *node, deparse_context *context,
 				PartitionBoundSpec *spec = (PartitionBoundSpec *) node;
 				ListCell   *cell;
 				char	   *sep;
+				int			i;
 
 				switch (spec->strategy)
 				{
@@ -8674,39 +8675,57 @@ get_rule_expr(Node *node, deparse_context *context,
 
 						appendStringInfoString(buf, "FOR VALUES FROM (");
 						sep = "";
+						i = 0;
 						foreach(cell, spec->lowerdatums)
 						{
 							PartitionRangeDatum *datum =
 							castNode(PartitionRangeDatum, lfirst(cell));
 
-							appendStringInfoString(buf, sep);
-							if (datum->infinite)
+							/*
+							 * We display UNBOUNDED only if it's known that
+							 * it was user-entered infinite datum, which is
+							 * allowed only for the first paritioning column.
+							 * Any other infinite datums have been internally
+							 * added by the system which we don't want to
+							 * display in the deparsed output.
+							 */
+							if (datum->infinite && i == 0)
+							{
 								appendStringInfoString(buf, "UNBOUNDED");
-							else
+								break;
+							}
+							else if (!datum->infinite)
 							{
 								Const	   *val = castNode(Const, datum->value);
 
+								appendStringInfoString(buf, sep);
 								get_const_expr(val, context, -1);
+								sep = ", ";
 							}
-							sep = ", ";
+							i++;
 						}
 						appendStringInfoString(buf, ") TO (");
 						sep = "";
+						i = 0;
 						foreach(cell, spec->upperdatums)
 						{
 							PartitionRangeDatum *datum =
 							castNode(PartitionRangeDatum, lfirst(cell));
 
-							appendStringInfoString(buf, sep);
-							if (datum->infinite)
+							if (datum->infinite && i == 0)
+							{
 								appendStringInfoString(buf, "UNBOUNDED");
-							else
+								break;		/* See the comment above. */
+							}
+							else if (!datum->infinite)
 							{
 								Const	   *val = castNode(Const, datum->value);
 
+								appendStringInfoString(buf, sep);
 								get_const_expr(val, context, -1);
+								sep = ", ";
 							}
-							sep = ", ";
+							i++;
 						}
 						appendStringInfoString(buf, ")");
 						break;
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index b6f794e1c2..beba88541e 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -505,22 +505,44 @@ CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES IN ('a');
 ERROR:  invalid bound specification for a range partition
 LINE 1: ...BLE fail_part PARTITION OF range_parted FOR VALUES IN ('a');
                                                               ^
--- each of start and end bounds must have same number of values as the
+-- each of start and end bounds cannot contain more values than the
 -- length of the partition key
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a', 1) TO ('z');
-ERROR:  FROM must specify exactly one value per partitioning column
+ERROR:  FROM cannot contain more values than the number of partitioning columns
+DETAIL:  "range_parted" has 1 partitioning column.
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a') TO ('z', 1);
-ERROR:  TO must specify exactly one value per partitioning column
+ERROR:  TO cannot contain more values than the number of partitioning columns
+DETAIL:  "range_parted" has 1 partitioning column.
+CREATE TABLE multicol_range_parted (
+	a int,
+	b int,
+	c int
+) partition by range (a, b, c);
+CREATE TABLE fail_part PARTITION OF multicol_range_parted FOR VALUES FROM (1) TO (1, 10, 9, 10, 23);
+ERROR:  TO cannot contain more values than the number of partitioning columns
+DETAIL:  "multicol_range_parted" has 3 partitioning columns.
+-- although they are allowed to contain fewer
+CREATE TABLE multicol_range_parted1 PARTITION OF multicol_range_parted FOR VALUES FROM (1) TO (1, 10);
+CREATE TABLE multicol_range_parted2 PARTITION OF multicol_range_parted FOR VALUES FROM (1, 10) TO (1, 10, 1);
 -- cannot specify null values in range bounds
-CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (unbounded);
+CREATE TABLE fail_part PARTITION OF multicol_range_parted FOR VALUES FROM (NULL) TO (UNBOUNDED);
 ERROR:  cannot specify NULL in range bound
--- 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...
+-- cannot specify UNBOUNDED for non-first columns
+CREATE TABLE fail_part PARTITION OF multicol_range_parted FOR VALUES FROM (1, UNBOUNDED, 1) TO (UNBOUNDED, 1, 1);
+ERROR:  cannot specify UNBOUNDED for columns other than the first column
+LINE 1: ...TION OF multicol_range_parted FOR VALUES FROM (1, UNBOUNDED,...
+                                                             ^
+CREATE TABLE multicol_range_parted0 PARTITION OF multicol_range_parted FOR VALUES FROM (UNBOUNDED) TO (1);
+-- cannot specify any values after unbounded
+CREATE TABLE fail_part PARTITION OF multicol_range_parted FOR VALUES FROM (UNBOUNDED, 1) TO (1);
+ERROR:  cannot specify a value after UNBOUNDED
+LINE 1: ...multicol_range_parted FOR VALUES FROM (UNBOUNDED, 1) TO (1);
+                                                             ^
+CREATE TABLE fail_part PARTITION OF multicol_range_parted FOR VALUES FROM (UNBOUNDED, UNBOUNDED) TO (1);
+ERROR:  cannot specify a value after UNBOUNDED
+LINE 1: ...multicol_range_parted FOR VALUES FROM (UNBOUNDED, UNBOUNDED)...
                                                              ^
-DROP TABLE range_parted_multicol;
+DROP TABLE multicol_range_parted;
 -- check if compatible with the specified parent
 -- cannot create as partition of a non-partitioned table
 CREATE TABLE unparted (
@@ -595,19 +617,14 @@ CREATE TABLE range_parted3 (
 	a int,
 	b int
 ) PARTITION BY RANGE (a, (b+1));
-CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, unbounded);
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, 1);
+CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (unbounded) TO (0);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (-1) TO (0);
 ERROR:  partition "fail_part" would overlap partition "part00"
-CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, 1);
+CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (0) TO (1, 1);
 CREATE TABLE part11 PARTITION OF range_parted3 FOR VALUES FROM (1, 1) TO (1, 10);
-CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, unbounded);
+CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (unbounded);
 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
-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 schema propagation from parent
 CREATE TABLE parted (
 	a text,
@@ -708,7 +725,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) TO (UNBOUNDED);
 \d+ unbounded_range_part
                            Table "public.unbounded_range_part"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -716,11 +733,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) TO (UNBOUNDED)
 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) TO (1);
 \d+ range_parted4_1
                               Table "public.range_parted4_1"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -728,10 +745,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 constraint: ((abs(a) IS NOT NULL) AND (abs(b) IS NOT NULL) AND (c IS NOT NULL) AND (abs(a) <= 1))
+Partition of: range_parted4 FOR VALUES FROM (UNBOUNDED) TO (1)
+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);
 \d+ range_parted4_2
                               Table "public.range_parted4_2"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -739,10 +756,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 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))))
+Partition of: range_parted4 FOR VALUES FROM (3, 4, 5) TO (6, 7)
+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, 7) TO (9);
 \d+ range_parted4_3
                               Table "public.range_parted4_3"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -750,8 +767,8 @@ 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 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))
+Partition of: range_parted4 FOR VALUES FROM (6, 7) TO (9)
+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) >= 7))) AND (abs(a) < 9))
 
 DROP TABLE range_parted4;
 -- cleanup
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 35d182d599..cd9072ec09 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1831,12 +1831,12 @@ drop table range_list_parted;
 -- check that constraint exclusion is able to cope with the partition
 -- constraint emitted for multi-column range partitioned tables
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, 1, 1);
+create table mcrparted0 partition of mcrparted for values from (unbounded) to (1, 1, 1);
 create table mcrparted1 partition of mcrparted for values from (1, 1, 1) to (10, 5, 10);
 create table mcrparted2 partition of mcrparted for values from (10, 5, 10) to (10, 10, 10);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
 create table mcrparted4 partition of mcrparted for values from (20, 10, 10) to (20, 20, 20);
-create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (unbounded);
 explain (costs off) select * from mcrparted where a = 0;	-- scans mcrparted0
           QUERY PLAN          
 ------------------------------
diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out
index d1153f410b..0424a75738 100644
--- a/src/test/regress/expected/insert.out
+++ b/src/test/regress/expected/insert.out
@@ -439,12 +439,12 @@ drop table key_desc, key_desc_1;
 -- check multi-column range partitioning expression enforces the same
 -- constraint as what tuple-routing would determine it to be
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, unbounded, unbounded);
-create table mcrparted1 partition of mcrparted for values from (2, 1, unbounded) to (10, 5, 10);
-create table mcrparted2 partition of mcrparted for values from (10, 6, unbounded) to (10, unbounded, unbounded);
+create table mcrparted0 partition of mcrparted for values from (unbounded) to (1);
+create table mcrparted1 partition of mcrparted for values from (2, 1) to (10, 5, 10);
+create table mcrparted2 partition of mcrparted for values from (10, 6) to (11);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
-create table mcrparted4 partition of mcrparted for values from (21, unbounded, unbounded) to (30, 20, unbounded);
-create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted4 partition of mcrparted for values from (21) to (30, 20);
+create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (unbounded);
 -- routed to mcrparted0
 insert into mcrparted values (0, 1, 1);
 insert into mcrparted0 values (0, 1, 1);
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index cb7aa5bbc6..30ea2f0834 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -477,18 +477,34 @@ CREATE TABLE range_parted (
 
 -- trying to specify list for range partitioned table
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES IN ('a');
--- each of start and end bounds must have same number of values as the
+
+-- each of start and end bounds cannot contain more values than the
 -- length of the partition key
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a', 1) TO ('z');
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a') TO ('z', 1);
 
+CREATE TABLE multicol_range_parted (
+	a int,
+	b int,
+	c int
+) partition by range (a, b, c);
+CREATE TABLE fail_part PARTITION OF multicol_range_parted FOR VALUES FROM (1) TO (1, 10, 9, 10, 23);
+-- although they are allowed to contain fewer
+CREATE TABLE multicol_range_parted1 PARTITION OF multicol_range_parted FOR VALUES FROM (1) TO (1, 10);
+CREATE TABLE multicol_range_parted2 PARTITION OF multicol_range_parted FOR VALUES FROM (1, 10) TO (1, 10, 1);
+
 -- cannot specify null values in range bounds
-CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (unbounded);
+CREATE TABLE fail_part PARTITION OF multicol_range_parted FOR VALUES FROM (NULL) TO (UNBOUNDED);
 
--- 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);
-DROP TABLE range_parted_multicol;
+-- cannot specify UNBOUNDED for non-first columns
+CREATE TABLE fail_part PARTITION OF multicol_range_parted FOR VALUES FROM (1, UNBOUNDED, 1) TO (UNBOUNDED, 1, 1);
+CREATE TABLE multicol_range_parted0 PARTITION OF multicol_range_parted FOR VALUES FROM (UNBOUNDED) TO (1);
+
+-- cannot specify any values after unbounded
+CREATE TABLE fail_part PARTITION OF multicol_range_parted FOR VALUES FROM (UNBOUNDED, 1) TO (1);
+CREATE TABLE fail_part PARTITION OF multicol_range_parted FOR VALUES FROM (UNBOUNDED, UNBOUNDED) TO (1);
+
+DROP TABLE multicol_range_parted;
 
 -- check if compatible with the specified parent
 
@@ -557,19 +573,14 @@ CREATE TABLE range_parted3 (
 	b int
 ) PARTITION BY RANGE (a, (b+1));
 
-CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, unbounded);
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, 1);
+CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (unbounded) TO (0);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (-1) TO (0);
 
-CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, 1);
+CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (0) TO (1, 1);
 CREATE TABLE part11 PARTITION OF range_parted3 FOR VALUES FROM (1, 1) TO (1, 10);
-CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, unbounded);
+CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (unbounded);
 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
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, unbounded);
-
 -- check schema propagation from parent
 
 CREATE TABLE parted (
@@ -626,14 +637,14 @@ 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) TO (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) TO (1);
 \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);
 \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, 7) TO (9);
 \d+ range_parted4_3
 DROP TABLE range_parted4;
 
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index 70fe971d51..5e86dc3f31 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -647,12 +647,12 @@ drop table range_list_parted;
 -- check that constraint exclusion is able to cope with the partition
 -- constraint emitted for multi-column range partitioned tables
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, 1, 1);
+create table mcrparted0 partition of mcrparted for values from (unbounded) to (1, 1, 1);
 create table mcrparted1 partition of mcrparted for values from (1, 1, 1) to (10, 5, 10);
 create table mcrparted2 partition of mcrparted for values from (10, 5, 10) to (10, 10, 10);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
 create table mcrparted4 partition of mcrparted for values from (20, 10, 10) to (20, 20, 20);
-create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (unbounded);
 explain (costs off) select * from mcrparted where a = 0;	-- scans mcrparted0
 explain (costs off) select * from mcrparted where a = 10 and abs(b) < 5;	-- scans mcrparted1
 explain (costs off) select * from mcrparted where a = 10 and abs(b) = 5;	-- scans mcrparted1, mcrparted2
diff --git a/src/test/regress/sql/insert.sql b/src/test/regress/sql/insert.sql
index 83c3ad8f53..c974aa9ed4 100644
--- a/src/test/regress/sql/insert.sql
+++ b/src/test/regress/sql/insert.sql
@@ -293,12 +293,12 @@ drop table key_desc, key_desc_1;
 -- check multi-column range partitioning expression enforces the same
 -- constraint as what tuple-routing would determine it to be
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, unbounded, unbounded);
-create table mcrparted1 partition of mcrparted for values from (2, 1, unbounded) to (10, 5, 10);
-create table mcrparted2 partition of mcrparted for values from (10, 6, unbounded) to (10, unbounded, unbounded);
+create table mcrparted0 partition of mcrparted for values from (unbounded) to (1);
+create table mcrparted1 partition of mcrparted for values from (2, 1) to (10, 5, 10);
+create table mcrparted2 partition of mcrparted for values from (10, 6) to (11);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
-create table mcrparted4 partition of mcrparted for values from (21, unbounded, unbounded) to (30, 20, unbounded);
-create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted4 partition of mcrparted for values from (21) to (30, 20);
+create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (unbounded);
 
 -- routed to mcrparted0
 insert into mcrparted values (0, 1, 1);
-- 
2.11.0

#19Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Amit Langote (#18)
1 attachment(s)
Re: Multi column range partition table

On 5 July 2017 at 10:43, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

So the more I think about this, the more I think that a cleaner design
would be as follows:

1). Don't allow UNBOUNDED, except in the first column, where it can
keep it's current meaning.

2). Allow the partition bounds to have fewer columns than the
partition definition, and have that mean the same as it would have
meant if you were partitioning by that many columns. So, for
example, if you were partitioning by (col1,col2), you'd be allowed
to define a partition like so:

FROM (x) TO (y)

and it would mean

x <= col1 < y

Or you'd be able to define a partition like

FROM (x1,x2) TO (y)

which would mean

(col1 > x1) OR (col1 = x1 AND col2 >= x2) AND col1 < y

3). Don't allow any value after UNBOUNDED (i.e., only specify
UNBOUNDED once in a partition bound).

I assume we don't need the ability of specifying ABOVE/BELOW in this design.

Yes that's right.

In retrospect, that sounds like something that was implemented in the
earlier versions of the patch, whereby there was no ability to specify
UNBOUNDED on a per-column basis. So the syntax was:

FROM { (x [, ...]) | UNBOUNDED } TO { (y [, ...]) | UNBOUNDED }

Yes, that's where I ended up too.

But, it was pointed out to me [1] that that doesn't address the use case,
for example, where part1 goes up to (10, 10) and part2 goes from (10, 10)
up to (10, unbounded).

The new design will limit the usage of unbounded range partitions at the
tail ends.

True, but I don't think that's really a problem. When the first column
is a discrete type, an upper bound of (10, unbounded) can be rewritten
as (11) in the new design. When it's a continuous type, e.g. floating
point, it can no longer be represented, because (10.0, unbounded)
really means (col1 <= 10.0). But we've already decided not to support
anything other than inclusive lower bounds and exclusive upper bounds,
so allowing this upper bound goes against that design choice.

Of course, it's pretty late in the day to be proposing this kind of
redesign, but I fear that if we don't tackle it now, it will just be
harder to deal with in the future.

Actually, a quick, simple hacky implementation might be to just fill
in any omitted values in a partition bound with negative infinity
internally, and when printing a bound, omit any values after an
infinite value. But really, I think we'd want to tidy up the
implementation, and I think a number of things would actually get much
simpler. For example, get_qual_for_range() could simply stop when it
reached the end of the list of values for the bound, and it wouldn't
need to worry about an unbounded value following a bounded one.

Thoughts?

I cooked up a patch for the "hacky" implementation for now, just as you
described in the above paragraph. Will you be willing to give it a look?
I will also think about the non-hacky way of implementing this.

OK, I'll take a look.

Meanwhile, I already had a go at the "non-hacky" implementation (WIP
patch attached). The more I worked on it, the simpler things got,
which I think is a good sign.

Part-way through, I realised that the PartitionRangeDatum Node type is
no longer needed, because each bound value is now necessarily finite,
so the lowerdatums and upperdatums lists in a PartitionBoundSpec can
now be made into lists of Const nodes, making them match the
listdatums field used for LIST partitioning, and then a whole lot of
related code gets simplified.

It needed a little bit more code in partition.c to track individual
bound sizes, but there were a number of other places that could be
simplified, so overall this represents a reduction in the code size
and complexity.

It's not complete (e.g., no doc updates yet), but it passes all the
tests, and so far seems to work as I would expect.

Regards,
Dean

Attachments:

refactor-unbounded-range-partitions.patchtext/x-patch; charset=US-ASCII; name=refactor-unbounded-range-partitions.patchDownload
diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c
new file mode 100644
index 7da2058..aade9f5
--- a/src/backend/catalog/partition.c
+++ b/src/backend/catalog/partition.c
@@ -66,23 +66,26 @@
  * is an upper bound.
  */
 
-/* Ternary value to represent what's contained in a range bound datum */
-typedef enum RangeDatumContent
+/* Type of an individual bound of a range partition */
+typedef enum RangeBoundKind
 {
-	RANGE_DATUM_FINITE = 0,		/* actual datum stored elsewhere */
-	RANGE_DATUM_NEG_INF,		/* negative infinity */
-	RANGE_DATUM_POS_INF			/* positive infinity */
-} RangeDatumContent;
+	RANGE_BOUND_FINITE = 0,		/* actual bound stored in the datums array */
+	RANGE_BOUND_NEG_INF,		/* negative infinity; NULL datums array */
+	RANGE_BOUND_POS_INF			/* positive infinity; NULL datums array */
+} RangeBoundKind;
 
 typedef struct PartitionBoundInfoData
 {
 	char		strategy;		/* list or range bounds? */
 	int			ndatums;		/* Length of the datums following array */
-	Datum	  **datums;			/* Array of datum-tuples with key->partnatts
-								 * datums each */
-	RangeDatumContent **content;	/* what's contained in each range bound
-									 * datum? (see the above enum); NULL for
+	Datum	  **datums;			/* Array of datum-tuples with up to
+								 * key->partnatts datums each */
+	RangeBoundKind *rbound_kind;	/* The type of each range bound; one per
+									 * member of the datums array; NULL for
 									 * list partitioned tables */
+	int		   *rbound_ndatums;	/* The number of datums in each range bound;
+								 * one per member of the datums array; NULL
+								 * for list partitioned tables */
 	int		   *indexes;		/* Partition indexes; one entry per member of
 								 * the datums array (plus one if range
 								 * partitioned table) */
@@ -108,8 +111,11 @@ typedef struct PartitionListValue
 typedef struct PartitionRangeBound
 {
 	int			index;
-	Datum	   *datums;			/* range bound datums */
-	RangeDatumContent *content; /* what's contained in each datum? */
+	RangeBoundKind kind;		/* type of range bound */
+	int			ndatums;		/* number of range bound datums; 0 for
+								 * unbounded ranges */
+	Datum	   *datums;			/* range bound datums; NULL for unbounded
+								 * ranges */
 	bool		lower;			/* this is the lower (vs upper) bound */
 } PartitionRangeBound;
 
@@ -123,11 +129,8 @@ static Oid get_partition_operator(Partit
 static Expr *make_partition_op_expr(PartitionKey key, int keynum,
 					   uint16 strategy, Expr *arg1, Expr *arg2);
 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);
+						 Expr **keyCol);
 static List *get_qual_for_list(PartitionKey key, PartitionBoundSpec *spec);
 static List *get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec);
 static List *generate_partition_qual(Relation rel);
@@ -135,11 +138,11 @@ static List *generate_partition_qual(Rel
 static PartitionRangeBound *make_one_range_bound(PartitionKey key, int index,
 					 List *datums, bool lower);
 static int32 partition_rbound_cmp(PartitionKey key,
-					 Datum *datums1, RangeDatumContent *content1, bool lower1,
-					 PartitionRangeBound *b2);
+					 Datum *datums1, int ndatums1, RangeBoundKind kind1,
+					 bool lower1, PartitionRangeBound *b2);
 static int32 partition_rbound_datum_cmp(PartitionKey key,
-						   Datum *rb_datums, RangeDatumContent *rb_content,
-						   Datum *tuple_datums);
+						   Datum *rb_datums, int rb_ndatums,
+						   RangeBoundKind rb_kind, Datum *tuple_datums);
 
 static int32 partition_bound_cmp(PartitionKey key,
 					PartitionBoundInfo boundinfo,
@@ -366,36 +369,27 @@ RelationBuildPartitionDesc(Relation rel)
 				int			j;
 
 				/* Is current bound is distinct from the previous? */
-				for (j = 0; j < key->partnatts; j++)
+				if (prev == NULL ||
+					cur->kind != prev->kind ||
+					cur->ndatums != prev->ndatums)
 				{
-					Datum		cmpval;
-
-					if (prev == NULL)
+					is_distinct = true;
+				}
+				else
+				{
+					for (j = 0; j < cur->ndatums; j++)
 					{
-						is_distinct = true;
-						break;
-					}
+						Datum		cmpval;
 
-					/*
-					 * 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 (cur->content[j] != RANGE_DATUM_FINITE ||
-						prev->content[j] != RANGE_DATUM_FINITE)
-					{
-						is_distinct = true;
-						break;
-					}
-					cmpval = FunctionCall2Coll(&key->partsupfunc[j],
-											   key->partcollation[j],
-											   cur->datums[j],
-											   prev->datums[j]);
-					if (DatumGetInt32(cmpval) != 0)
-					{
-						is_distinct = true;
-						break;
+						cmpval = FunctionCall2Coll(&key->partsupfunc[j],
+												   key->partcollation[j],
+												   cur->datums[j],
+												   prev->datums[j]);
+						if (DatumGetInt32(cmpval) != 0)
+						{
+							is_distinct = true;
+							break;
+						}
 					}
 				}
 
@@ -454,8 +448,8 @@ RelationBuildPartitionDesc(Relation rel)
 			palloc0(sizeof(PartitionBoundInfoData));
 		boundinfo->strategy = key->strategy;
 		boundinfo->ndatums = ndatums;
-		boundinfo->null_index = -1;
 		boundinfo->datums = (Datum **) palloc0(ndatums * sizeof(Datum *));
+		boundinfo->null_index = -1;
 
 		/* Initialize mapping array with invalid values */
 		mapping = (int *) palloc(sizeof(int) * nparts);
@@ -512,8 +506,11 @@ RelationBuildPartitionDesc(Relation rel)
 
 			case PARTITION_STRATEGY_RANGE:
 				{
-					boundinfo->content = (RangeDatumContent **) palloc(ndatums *
-																	   sizeof(RangeDatumContent *));
+					boundinfo->rbound_kind = (RangeBoundKind *)
+											 palloc(ndatums *
+													sizeof(RangeBoundKind));
+					boundinfo->rbound_ndatums = (int *) palloc(ndatums *
+															   sizeof(int));
 					boundinfo->indexes = (int *) palloc((ndatums + 1) *
 														sizeof(int));
 
@@ -521,20 +518,20 @@ RelationBuildPartitionDesc(Relation rel)
 					{
 						int			j;
 
-						boundinfo->datums[i] = (Datum *) palloc(key->partnatts *
-																sizeof(Datum));
-						boundinfo->content[i] = (RangeDatumContent *)
-							palloc(key->partnatts *
-								   sizeof(RangeDatumContent));
-						for (j = 0; j < key->partnatts; j++)
+						boundinfo->rbound_kind[i] = rbounds[i]->kind;
+						boundinfo->rbound_ndatums[i] = rbounds[i]->ndatums;
+						if (rbounds[i]->ndatums > 0)
 						{
-							if (rbounds[i]->content[j] == RANGE_DATUM_FINITE)
+							boundinfo->datums[i] = (Datum *) palloc(rbounds[i]->ndatums *
+																	sizeof(Datum));
+
+							for (j = 0; j < rbounds[i]->ndatums; j++)
+							{
 								boundinfo->datums[i][j] =
 									datumCopy(rbounds[i]->datums[j],
 											  key->parttypbyval[j],
 											  key->parttyplen[j]);
-							/* Remember, we are storing the tri-state value. */
-							boundinfo->content[i][j] = rbounds[i]->content[j];
+							}
 						}
 
 						/*
@@ -611,25 +608,30 @@ partition_bounds_equal(PartitionKey key,
 
 	for (i = 0; i < b1->ndatums; i++)
 	{
+		int			ndatums;
 		int			j;
 
-		for (j = 0; j < key->partnatts; j++)
+		if (key->strategy == PARTITION_STRATEGY_RANGE)
 		{
-			/* For range partitions, the bounds might not be finite. */
-			if (b1->content != NULL)
-			{
-				/*
-				 * A finite bound always differs from an infinite bound, and
-				 * different kinds of infinities differ from each other.
-				 */
-				if (b1->content[i][j] != b2->content[i][j])
-					return false;
+			/* Fields only used for range partitions */
+			if (b1->rbound_kind[i] != b2->rbound_kind[i])
+				return false;
 
-				/* Non-finite bounds are equal without further examination. */
-				if (b1->content[i][j] != RANGE_DATUM_FINITE)
-					continue;
-			}
+			if (b1->rbound_ndatums[i] != b2->rbound_ndatums[i])
+				return false;
 
+			/* Number of datums can vary by bound */
+			ndatums = b1->rbound_ndatums[i];
+		}
+		else
+			/* Number of datums is the same for each bound */
+			ndatums = key->partnatts;
+
+		if (b1->indexes[i] != b2->indexes[i])
+			return false;
+
+		for (j = 0; j < ndatums; j++)
+		{
 			/*
 			 * Compare the actual values. Note that it would be both incorrect
 			 * and unsafe to invoke the comparison operator derived from the
@@ -646,9 +648,6 @@ partition_bounds_equal(PartitionKey key,
 							  key->parttyplen[j]))
 				return false;
 		}
-
-		if (b1->indexes[i] != b2->indexes[i])
-			return false;
 	}
 
 	/* There are ndatums+1 indexes in case of range partitions */
@@ -735,8 +734,8 @@ check_new_partition_bound(char *relname,
 				 * First check if the resulting range would be empty with
 				 * specified lower and upper bounds
 				 */
-				if (partition_rbound_cmp(key, lower->datums, lower->content, true,
-										 upper) >= 0)
+				if (partition_rbound_cmp(key, lower->datums, lower->ndatums,
+										 lower->kind, true, upper) >= 0)
 					ereport(ERROR,
 							(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
 							 errmsg("cannot create range partition with empty range"),
@@ -745,78 +744,62 @@ check_new_partition_bound(char *relname,
 				if (partdesc->nparts > 0)
 				{
 					PartitionBoundInfo boundinfo = partdesc->boundinfo;
-					int			off1,
-								off2;
-					bool		equal = false;
+					int			offset;
+					bool		equal;
 
 					Assert(boundinfo && boundinfo->ndatums > 0 &&
 						   boundinfo->strategy == PARTITION_STRATEGY_RANGE);
 
 					/*
-					 * Firstly, find the greatest range bound that is less
-					 * than or equal to the new lower bound.
+					 * Test whether the new lower bound (which is treated
+					 * inclusively as part of the new partition) lies inside an
+					 * existing partition, or in a gap.
+					 *
+					 * If it's in a gap, the next index value will be -1 (the
+					 * lower bound of the next partition).  This is also true
+					 * if there is no next partition, since the index array is
+					 * initialised with an extra -1 at the end.
+					 *
+					 * Note that this also allows for the possibility that the
+					 * new lower bound equals an existing upper bound.
 					 */
-					off1 = partition_bound_bsearch(key, boundinfo, lower, true,
-												   &equal);
+					offset = partition_bound_bsearch(key, boundinfo, lower,
+													 true, &equal);
 
-					/*
-					 * off1 == -1 means that all existing bounds are greater
-					 * than the new lower bound.  In that case and the case
-					 * where no partition is defined between the bounds at
-					 * off1 and off1 + 1, we have a "gap" in the range that
-					 * could be occupied by the new partition.  We confirm if
-					 * so by checking whether the new upper bound is confined
-					 * within the gap.
-					 */
-					if (!equal && boundinfo->indexes[off1 + 1] < 0)
+					if (boundinfo->indexes[offset + 1] < 0)
 					{
-						off2 = partition_bound_bsearch(key, boundinfo, upper,
-													   true, &equal);
-
 						/*
-						 * If the new upper bound is returned to be equal to
-						 * the bound at off2, the latter must be the upper
-						 * bound of some partition with which the new
-						 * partition clearly overlaps.
-						 *
-						 * Also, if bound at off2 is not same as the one
-						 * returned for the new lower bound (IOW, off1 !=
-						 * off2), then the new partition overlaps at least one
-						 * partition.
+						 * Check that the new partition will fit in the gap.
+						 * For it to fit, the new upper bound must be less than
+						 * or equal to the lower bound of the next partition,
+						 * if there is one.
 						 */
-						if (equal || off1 != off2)
+						if (offset + 1 < boundinfo->ndatums)
 						{
-							overlap = true;
+							int32		cmpval;
 
-							/*
-							 * The bound at off2 could be the lower bound of
-							 * the partition with which the new partition
-							 * overlaps.  In that case, use the upper bound
-							 * (that is, the bound at off2 + 1) to get the
-							 * index of that partition.
-							 */
-							if (boundinfo->indexes[off2] < 0)
-								with = boundinfo->indexes[off2 + 1];
-							else
-								with = boundinfo->indexes[off2];
+							cmpval = partition_bound_cmp(key, boundinfo,
+														 offset + 1, upper,
+														 true);
+							if (cmpval < 0)
+							{
+								/*
+								 * The new partition overlaps with the existing
+								 * partition between offset + 1 and offset + 2.
+								 */
+								overlap = true;
+								with = boundinfo->indexes[offset + 2];
+							}
 						}
 					}
 					else
 					{
 						/*
-						 * Equal has been set to true and there is no "gap"
-						 * between the bound at off1 and that at off1 + 1, so
-						 * the new partition will overlap some partition. In
-						 * the former case, the new lower bound is found to be
-						 * equal to the bound at off1, which could only ever
-						 * be true if the latter is the lower bound of some
-						 * partition.  It's clear in such a case that the new
-						 * partition overlaps that partition, whose index we
-						 * get using its upper bound (that is, using the bound
-						 * at off1 + 1).
+						 * The new partition overlaps with the existing
+						 * partition between offset and offset + 1.
 						 */
 						overlap = true;
-						with = boundinfo->indexes[off1 + 1];
+						with = boundinfo->indexes[offset + 1];
 					}
 				}
 
@@ -1410,21 +1393,15 @@ get_qual_for_list(PartitionKey key, Part
  * This is a subroutine for get_qual_for_range, and its API is pretty
  * specialized to that caller.
  *
- * 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.
- * All of these structures are freshly palloc'd.
+ * Constructs an Expr for the key column (returned in *keyCol).
  *
  * *partexprs_item points to the cell containing the next expression in
  * the key->partexprs list, or NULL.  It may be advanced upon return.
  */
 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)
+						 Expr **keyCol)
 {
 	/* Get partition key expression for this column */
 	if (key->partattrs[keynum] != 0)
@@ -1443,17 +1420,6 @@ get_range_key_properties(PartitionKey ke
 		*keyCol = copyObject(lfirst(*partexprs_item));
 		*partexprs_item = lnext(*partexprs_item);
 	}
-
-	/* Get appropriate Const nodes for the bounds */
-	if (!ldatum->infinite)
-		*lower_val = castNode(Const, copyObject(ldatum->value));
-	else
-		*lower_val = NULL;
-
-	if (!udatum->infinite)
-		*upper_val = castNode(Const, copyObject(udatum->value));
-	else
-		*upper_val = NULL;
 }
 
 /*
@@ -1484,17 +1450,16 @@ get_range_key_properties(PartitionKey ke
  *		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:
- *
- *	(b < bu) OR (b = bu), which is simplified to (b <= bu)
+ * The upper and lower bound tuples may also contain fewer values than the
+ * number of columns in the partition key, in which case the relevant
+ * expression is shortened.
  *
  * 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
  *
- * 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.
+ * If either bound is UNBOUNDED, the matching bound tuples list is empty and
+ * the partition does not really have a constraint, except the IS NOT NULL
+ * constraint for partition keys.
  *
  * 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.
@@ -1509,8 +1474,6 @@ get_qual_for_range(PartitionKey key, Par
 			   *partexprs_item_saved;
 	int			i,
 				j;
-	PartitionRangeDatum *ldatum,
-			   *udatum;
 	Expr	   *keyCol;
 	Const	   *lower_val,
 			   *upper_val;
@@ -1582,8 +1545,8 @@ get_qual_for_range(PartitionKey key, Par
 		Datum		test_result;
 		bool		isNull;
 
-		ldatum = castNode(PartitionRangeDatum, lfirst(cell1));
-		udatum = castNode(PartitionRangeDatum, lfirst(cell2));
+		lower_val = castNode(Const, lfirst(cell1));
+		upper_val = castNode(Const, lfirst(cell2));
 
 		/*
 		 * Since get_range_key_properties() modifies partexprs_item, and we
@@ -1592,18 +1555,7 @@ get_qual_for_range(PartitionKey key, Par
 		 */
 		partexprs_item_saved = partexprs_item;
 
-		get_range_key_properties(key, i, ldatum, udatum,
-								 &partexprs_item,
-								 &keyCol,
-								 &lower_val, &upper_val);
-
-		/*
-		 * If either or both of lower_val and upper_val is NULL, they are
-		 * unequal, because being NULL means the column is unbounded in the
-		 * respective direction.
-		 */
-		if (!lower_val || !upper_val)
-			break;
+		get_range_key_properties(key, i, &partexprs_item, &keyCol);
 
 		/* Create the test expression */
 		estate = CreateExecutorState();
@@ -1643,7 +1595,7 @@ get_qual_for_range(PartitionKey key, Par
 	lower_or_start_datum = cell1;
 	upper_or_start_datum = cell2;
 
-	/* OR will have as many arms as there are key columns left. */
+	/* OR will have up to as many arms as there are key columns left. */
 	num_or_arms = key->partnatts - i;
 	current_or_arm = 0;
 	lower_or_arms = upper_or_arms = NIL;
@@ -1653,27 +1605,23 @@ get_qual_for_range(PartitionKey key, Par
 		List	   *lower_or_arm_args = NIL,
 				   *upper_or_arm_args = NIL;
 
-		/* Restart scan of columns from the i'th one */
+		/*
+		 * Restart scan of columns from the i'th one.  We cannot use forboth
+		 * here because the upper and lower bounds may have different numbers
+		 * of values.
+		 */
 		j = i;
 		partexprs_item = partexprs_item_saved;
 
-		for_both_cell(cell1, lower_or_start_datum, cell2, upper_or_start_datum)
+		for (cell1 = lower_or_start_datum, cell2 = upper_or_start_datum;
+			 cell1 != NULL || cell2 != NULL;
+			 cell1 = cell1 ? lnext(cell1) : NULL,
+			 cell2 = cell2 ? lnext(cell2) : NULL)
 		{
-			PartitionRangeDatum *ldatum_next = NULL,
-					   *udatum_next = NULL;
+			lower_val = cell1 ? castNode(Const, lfirst(cell1)) : NULL;
+			upper_val = cell2 ? castNode(Const, lfirst(cell2)) : NULL;
 
-			ldatum = castNode(PartitionRangeDatum, lfirst(cell1));
-			if (lnext(cell1))
-				ldatum_next = castNode(PartitionRangeDatum,
-									   lfirst(lnext(cell1)));
-			udatum = castNode(PartitionRangeDatum, lfirst(cell2));
-			if (lnext(cell2))
-				udatum_next = castNode(PartitionRangeDatum,
-									   lfirst(lnext(cell2)));
-			get_range_key_properties(key, j, ldatum, udatum,
-									 &partexprs_item,
-									 &keyCol,
-									 &lower_val, &upper_val);
+			get_range_key_properties(key, j, &partexprs_item, &keyCol);
 
 			if (need_next_lower_arm && lower_val)
 			{
@@ -1681,12 +1629,11 @@ get_qual_for_range(PartitionKey key, Par
 
 				/*
 				 * 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 column of the bound, use GE. Otherwise use GT.
 				 */
 				if (j - i < current_or_arm)
 					strategy = BTEqualStrategyNumber;
-				else if ((ldatum_next && ldatum_next->infinite) ||
-						 j == key->partnatts - 1)
+				else if (lnext(cell1) == NULL)
 					strategy = BTGreaterEqualStrategyNumber;
 				else
 					strategy = BTGreaterStrategyNumber;
@@ -1704,12 +1651,10 @@ get_qual_for_range(PartitionKey key, Par
 
 				/*
 				 * For the non-last columns of this arm, use the EQ operator.
-				 * For the last finite-valued column, use LE.
+				 * Otherwise use LT.
 				 */
 				if (j - i < current_or_arm)
 					strategy = BTEqualStrategyNumber;
-				else if (udatum_next && udatum_next->infinite)
-					strategy = BTLessEqualStrategyNumber;
 				else
 					strategy = BTLessStrategyNumber;
 
@@ -1729,11 +1674,11 @@ get_qual_for_range(PartitionKey key, Par
 			if (j - i > current_or_arm)
 			{
 				/*
-				 * We need not emit the next arm if the new column that will
-				 * be considered is unbounded.
+				 * We need not emit the next arm if there are no bounds on any
+				 * of the remaining columns.
 				 */
-				need_next_lower_arm = ldatum_next && !ldatum_next->infinite;
-				need_next_upper_arm = udatum_next && !udatum_next->infinite;
+				need_next_lower_arm = (cell1 && lnext(cell1));
+				need_next_upper_arm = (cell2 && lnext(cell2));
 				break;
 			}
 		}
@@ -2091,9 +2036,8 @@ qsort_partition_list_value_cmp(const voi
 /*
  * make_one_range_bound
  *
- * Return a PartitionRangeBound given a list of PartitionRangeDatum elements
- * and a flag telling whether the bound is lower or not.  Made into a function
- * because there are multiple sites that want to use this facility.
+ * Return a PartitionRangeBound given a list of Const bound values and a flag
+ * indicating whether the bound is a lower bound or not.
  */
 static PartitionRangeBound *
 make_one_range_bound(PartitionKey key, int index, List *datums, bool lower)
@@ -2104,31 +2048,30 @@ make_one_range_bound(PartitionKey key, i
 
 	bound = (PartitionRangeBound *) palloc0(sizeof(PartitionRangeBound));
 	bound->index = index;
-	bound->datums = (Datum *) palloc0(key->partnatts * sizeof(Datum));
-	bound->content = (RangeDatumContent *) palloc0(key->partnatts *
-												   sizeof(RangeDatumContent));
 	bound->lower = lower;
 
-	i = 0;
-	foreach(lc, datums)
+	/* Handle unbounded ranges, which have an empty list of bound values */
+	if (!datums)
 	{
-		PartitionRangeDatum *datum = castNode(PartitionRangeDatum, lfirst(lc));
-
-		/* What's contained in this range datum? */
-		bound->content[i] = !datum->infinite
-			? RANGE_DATUM_FINITE
-			: (lower ? RANGE_DATUM_NEG_INF
-			   : RANGE_DATUM_POS_INF);
+		bound->kind = lower ? RANGE_BOUND_NEG_INF : RANGE_BOUND_POS_INF;
+		bound->ndatums = 0;
+		bound->datums = NULL;
+		return bound;
+	}
 
-		if (bound->content[i] == RANGE_DATUM_FINITE)
-		{
-			Const	   *val = castNode(Const, datum->value);
+	/* Otherwise it's a finite bound; store the datum values in an array */
+	bound->kind = RANGE_BOUND_FINITE;
+	bound->ndatums = list_length(datums);
+	bound->datums = (Datum *) palloc0(bound->ndatums * sizeof(Datum));
 
-			if (val->constisnull)
-				elog(ERROR, "invalid range bound datum");
-			bound->datums[i] = val->constvalue;
-		}
+	i = 0;
+	foreach(lc, datums)
+	{
+		Const	   *val = castNode(Const, lfirst(lc));
 
+		if (val->constisnull)
+			elog(ERROR, "invalid range bound datum");
+		bound->datums[i] = val->constvalue;
 		i++;
 	}
 
@@ -2143,100 +2086,113 @@ qsort_partition_rbound_cmp(const void *a
 	PartitionRangeBound *b2 = (*(PartitionRangeBound *const *) b);
 	PartitionKey key = (PartitionKey) arg;
 
-	return partition_rbound_cmp(key, b1->datums, b1->content, b1->lower, b2);
+	return partition_rbound_cmp(key, b1->datums, b1->ndatums, b1->kind,
+								b1->lower, b2);
 }
 
 /*
  * partition_rbound_cmp
  *
  * Return for two range bounds whether the 1st one (specified in datum1,
- * content1, and lower1) is <=, =, >= the bound specified in *b2
+ * ndatums1, kind1, and lower1) is <, =, > the bound specified in *b2
  */
 static int32
 partition_rbound_cmp(PartitionKey key,
-					 Datum *datums1, RangeDatumContent *content1, bool lower1,
-					 PartitionRangeBound *b2)
+					 Datum *datums1, int ndatums1, RangeBoundKind kind1,
+					 bool lower1, PartitionRangeBound *b2)
 {
-	int32		cmpval = 0;		/* placate compiler */
 	int			i;
 	Datum	   *datums2 = b2->datums;
-	RangeDatumContent *content2 = b2->content;
 	bool		lower2 = b2->lower;
 
-	for (i = 0; i < key->partnatts; i++)
-	{
-		/*
-		 * First, handle cases involving infinity, which don't require
-		 * invoking the comparison proc.
-		 */
-		if (content1[i] != RANGE_DATUM_FINITE &&
-			content2[i] != RANGE_DATUM_FINITE)
+	/*
+	 * Handle cases involving infinity, which don't require any values to be
+	 * compared.
+	 */
+	if (kind1 == RANGE_BOUND_NEG_INF)
+		return b2->kind == RANGE_BOUND_NEG_INF ? 0 : -1;
+	else if (kind1 == RANGE_BOUND_POS_INF)
+		return b2->kind == RANGE_BOUND_POS_INF ? 0 : 1;
+	else if (b2->kind != RANGE_BOUND_FINITE)
+		return b2->kind == RANGE_BOUND_NEG_INF ? 1 : -1;
 
-			/*
-			 * Both are infinity, so they are equal unless one is negative
-			 * infinity and other positive (or vice versa)
-			 */
-			return content1[i] == content2[i] ? 0
-				: (content1[i] < content2[i] ? -1 : 1);
-		else if (content1[i] != RANGE_DATUM_FINITE)
-			return content1[i] == RANGE_DATUM_NEG_INF ? -1 : 1;
-		else if (content2[i] != RANGE_DATUM_FINITE)
-			return content2[i] == RANGE_DATUM_NEG_INF ? 1 : -1;
+	/* Compare values from the two range bounds */
+	for (i = 0; i < Min(ndatums1, b2->ndatums); i++)
+	{
+		int32		cmpval;
 
 		cmpval = DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[i],
 												 key->partcollation[i],
 												 datums1[i],
 												 datums2[i]));
 		if (cmpval != 0)
-			break;
+			return cmpval;
 	}
 
 	/*
-	 * If the comparison is anything other than equal, we're done. If they
-	 * compare equal though, we still have to consider whether the boundaries
-	 * are inclusive or exclusive.  Exclusive one is considered smaller of the
-	 * two.
+	 * If one range has more values than the other, treat that range as
+	 * larger, since all the shared values are now known to be equal.
 	 */
-	if (cmpval == 0 && lower1 != lower2)
-		cmpval = lower1 ? 1 : -1;
+	if (ndatums1 > b2->ndatums)
+		return 1;
+	else if (ndatums1 < b2->ndatums)
+		return -1;
 
-	return cmpval;
+	/*
+	 * The two bounds are equal as far as their values are concerned.  If one
+	 * is a lower (inclusive) bound and the other is an upper (exclusive)
+	 * bound, treat the upper bound as smaller since it belongs to a partition
+	 * that holds smaller values.
+	 */
+	if (lower1)
+		return lower2 ? 0 : 1;
+	else
+		return lower2 ? -1 : 0;
 }
 
 /*
  * partition_rbound_datum_cmp
  *
- * Return whether range bound (specified in rb_datums, rb_content, and
- * rb_lower) <=, =, >= partition key of tuple (tuple_datums)
+ * Return whether range bound (specified in rb_datums, rb_ndatums, rb_kind,
+ * and rb_lower) <, =, > partition key of tuple (tuple_datums)
  */
 static int32
 partition_rbound_datum_cmp(PartitionKey key,
-						   Datum *rb_datums, RangeDatumContent *rb_content,
-						   Datum *tuple_datums)
+						   Datum *rb_datums, int rb_ndatums,
+						   RangeBoundKind rb_kind, Datum *tuple_datums)
 {
 	int			i;
-	int32		cmpval = -1;
 
-	for (i = 0; i < key->partnatts; i++)
+	/* Handle UNBOUNDED ranges */
+	if (rb_kind == RANGE_BOUND_NEG_INF)
+		return -1;
+	if (rb_kind == RANGE_BOUND_POS_INF)
+		return 1;
+
+	/* Compare the range bound values with the tuple values */
+	for (i = 0; i < rb_ndatums; i++)
 	{
-		if (rb_content[i] != RANGE_DATUM_FINITE)
-			return rb_content[i] == RANGE_DATUM_NEG_INF ? -1 : 1;
+		int32		cmpval;
 
 		cmpval = DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[i],
 												 key->partcollation[i],
 												 rb_datums[i],
 												 tuple_datums[i]));
 		if (cmpval != 0)
-			break;
+			return cmpval;
 	}
 
-	return cmpval;
+	/*
+	 * All the tuple values are the same as the range bound values, but there
+	 * may be more values in the tuple, in which case it is larger.
+	 */
+	return key->partnatts > rb_ndatums ? -1 : 0;
 }
 
 /*
  * partition_bound_cmp
  *
- * Return whether the bound at offset in boundinfo is <=, =, >= the argument
+ * Return whether the bound at offset in boundinfo is <, =, > the argument
  * specified in *probe.
  */
 static int32
@@ -2257,7 +2213,8 @@ partition_bound_cmp(PartitionKey key, Pa
 
 		case PARTITION_STRATEGY_RANGE:
 			{
-				RangeDatumContent *content = boundinfo->content[offset];
+				RangeBoundKind kind = boundinfo->rbound_kind[offset];
+				int			ndatums = boundinfo->rbound_ndatums[offset];
 
 				if (probe_is_bound)
 				{
@@ -2268,13 +2225,13 @@ partition_bound_cmp(PartitionKey key, Pa
 					 */
 					bool		lower = boundinfo->indexes[offset] < 0;
 
-					cmpval = partition_rbound_cmp(key,
-												  bound_datums, content, lower,
+					cmpval = partition_rbound_cmp(key, bound_datums, ndatums,
+												  kind, lower,
 												  (PartitionRangeBound *) probe);
 				}
 				else
-					cmpval = partition_rbound_datum_cmp(key,
-														bound_datums, content,
+					cmpval = partition_rbound_datum_cmp(key, bound_datums,
+														ndatums, kind,
 														(Datum *) probe);
 				break;
 			}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index 67ac814..c4036a9
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4453,18 +4453,6 @@ _copyPartitionBoundSpec(const PartitionB
 	return newnode;
 }
 
-static PartitionRangeDatum *
-_copyPartitionRangeDatum(const PartitionRangeDatum *from)
-{
-	PartitionRangeDatum *newnode = makeNode(PartitionRangeDatum);
-
-	COPY_SCALAR_FIELD(infinite);
-	COPY_NODE_FIELD(value);
-	COPY_LOCATION_FIELD(location);
-
-	return newnode;
-}
-
 static PartitionCmd *
 _copyPartitionCmd(const PartitionCmd *from)
 {
@@ -5519,9 +5507,6 @@ copyObjectImpl(const void *from)
 		case T_PartitionBoundSpec:
 			retval = _copyPartitionBoundSpec(from);
 			break;
-		case T_PartitionRangeDatum:
-			retval = _copyPartitionRangeDatum(from);
-			break;
 		case T_PartitionCmd:
 			retval = _copyPartitionCmd(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index 91d64b7..5d0ccc1
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2847,16 +2847,6 @@ _equalPartitionBoundSpec(const Partition
 }
 
 static bool
-_equalPartitionRangeDatum(const PartitionRangeDatum *a, const PartitionRangeDatum *b)
-{
-	COMPARE_SCALAR_FIELD(infinite);
-	COMPARE_NODE_FIELD(value);
-	COMPARE_LOCATION_FIELD(location);
-
-	return true;
-}
-
-static bool
 _equalPartitionCmd(const PartitionCmd *a, const PartitionCmd *b)
 {
 	COMPARE_NODE_FIELD(name);
@@ -3669,9 +3659,6 @@ equal(const void *a, const void *b)
 		case T_PartitionBoundSpec:
 			retval = _equalPartitionBoundSpec(a, b);
 			break;
-		case T_PartitionRangeDatum:
-			retval = _equalPartitionRangeDatum(a, b);
-			break;
 		case T_PartitionCmd:
 			retval = _equalPartitionCmd(a, b);
 			break;
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index 97ba25f..12e9d07
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1541,9 +1541,6 @@ exprLocation(const Node *expr)
 		case T_PartitionBoundSpec:
 			loc = ((const PartitionBoundSpec *) expr)->location;
 			break;
-		case T_PartitionRangeDatum:
-			loc = ((const PartitionRangeDatum *) expr)->location;
-			break;
 		default:
 			/* for any other node type it's just unknown... */
 			loc = -1;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index 3a23f0b..52332a7
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -3550,16 +3550,6 @@ _outPartitionBoundSpec(StringInfo str, c
 	WRITE_LOCATION_FIELD(location);
 }
 
-static void
-_outPartitionRangeDatum(StringInfo str, const PartitionRangeDatum *node)
-{
-	WRITE_NODE_TYPE("PARTITIONRANGEDATUM");
-
-	WRITE_BOOL_FIELD(infinite);
-	WRITE_NODE_FIELD(value);
-	WRITE_LOCATION_FIELD(location);
-}
-
 /*
  * outNode -
  *	  converts a Node into ascii string and append it to 'str'
@@ -4196,9 +4186,6 @@ outNode(StringInfo str, const void *obj)
 			case T_PartitionBoundSpec:
 				_outPartitionBoundSpec(str, obj);
 				break;
-			case T_PartitionRangeDatum:
-				_outPartitionRangeDatum(str, obj);
-				break;
 
 			default:
 
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index 2988e8b..005fc6d
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -2382,21 +2382,6 @@ _readPartitionBoundSpec(void)
 }
 
 /*
- * _readPartitionRangeDatum
- */
-static PartitionRangeDatum *
-_readPartitionRangeDatum(void)
-{
-	READ_LOCALS(PartitionRangeDatum);
-
-	READ_BOOL_FIELD(infinite);
-	READ_NODE_FIELD(value);
-	READ_LOCATION_FIELD(location);
-
-	READ_DONE();
-}
-
-/*
  * parseNodeString
  *
  * Given a character string representing a node tree, parseNodeString creates
@@ -2638,8 +2623,6 @@ parseNodeString(void)
 		return_value = _readExtensibleNode();
 	else if (MATCH("PARTITIONBOUNDSPEC", 18))
 		return_value = _readPartitionBoundSpec();
-	else if (MATCH("PARTITIONRANGEDATUM", 19))
-		return_value = _readPartitionRangeDatum();
 	else
 	{
 		elog(ERROR, "badly formatted node string \"%.32s\"...", token);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
new file mode 100644
index 0f3998f..0e2e651
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -576,8 +576,8 @@ static Node *makeRecursiveViewSelect(cha
 %type <partelem>	part_elem
 %type <list>		part_params
 %type <partboundspec> ForValues
-%type <node>		partbound_datum PartitionRangeDatum
-%type <list>		partbound_datum_list range_datum_list
+%type <node>		partbound_datum
+%type <list>		partbound_datum_list range_bound
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -2664,13 +2664,13 @@ ForValues:
 				}
 
 			/* a RANGE partition */
-			| FOR VALUES FROM '(' range_datum_list ')' TO '(' range_datum_list ')'
+			| FOR VALUES FROM range_bound TO range_bound
 				{
 					PartitionBoundSpec *n = makeNode(PartitionBoundSpec);
 
 					n->strategy = PARTITION_STRATEGY_RANGE;
-					n->lowerdatums = $5;
-					n->upperdatums = $9;
+					n->lowerdatums = $4;
+					n->upperdatums = $6;
 					n->location = @3;
 
 					$$ = n;
@@ -2689,33 +2689,9 @@ partbound_datum_list:
 												{ $$ = lappend($1, $3); }
 		;
 
-range_datum_list:
-			PartitionRangeDatum					{ $$ = list_make1($1); }
-			| range_datum_list ',' PartitionRangeDatum
-												{ $$ = lappend($1, $3); }
-		;
-
-PartitionRangeDatum:
-			UNBOUNDED
-				{
-					PartitionRangeDatum *n = makeNode(PartitionRangeDatum);
-
-					n->infinite = true;
-					n->value = NULL;
-					n->location = @1;
-
-					$$ = (Node *) n;
-				}
-			| partbound_datum
-				{
-					PartitionRangeDatum *n = makeNode(PartitionRangeDatum);
-
-					n->infinite = false;
-					n->value = $1;
-					n->location = @1;
-
-					$$ = (Node *) n;
-				}
+range_bound:
+			UNBOUNDED							{ $$ = NIL; }
+			| '(' partbound_datum_list ')'		{ $$ = $2; }
 		;
 
 /*****************************************************************************
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
new file mode 100644
index ee5f3a3..252baa6
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3361,11 +3361,9 @@ transformPartitionBound(ParseState *psta
 	}
 	else if (strategy == PARTITION_STRATEGY_RANGE)
 	{
-		ListCell   *cell1,
-				   *cell2;
+		ListCell   *cell;
 		int			i,
 					j;
-		bool		seen_unbounded;
 
 		if (spec->strategy != PARTITION_STRATEGY_RANGE)
 			ereport(ERROR,
@@ -3373,59 +3371,65 @@ transformPartitionBound(ParseState *psta
 					 errmsg("invalid bound specification for a range partition"),
 					 parser_errposition(pstate, exprLocation((Node *) spec))));
 
-		if (list_length(spec->lowerdatums) != partnatts)
+		if (list_length(spec->lowerdatums) > partnatts)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("FROM must specify exactly one value per partitioning column")));
-		if (list_length(spec->upperdatums) != partnatts)
+					 errmsg("too many values in FROM list")));
+
+		if (list_length(spec->upperdatums) > partnatts)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("TO must specify exactly one value per partitioning column")));
+					 errmsg("too many values in TO list")));
 
-		/*
-		 * Check that no finite value follows an UNBOUNDED item in either of
-		 * lower and upper bound lists.
-		 */
-		seen_unbounded = false;
-		foreach(cell1, spec->lowerdatums)
+		/* Transform the lower bound values */
+		i = j = 0;
+		result_spec->lowerdatums = NIL;
+		foreach(cell, spec->lowerdatums)
 		{
-			PartitionRangeDatum *ldatum = castNode(PartitionRangeDatum,
-												   lfirst(cell1));
+			A_Const    *con = castNode(A_Const, lfirst(cell));
+			char	   *colname;
+			Oid			coltype;
+			int32		coltypmod;
+			Const	   *value;
 
-			if (ldatum->infinite)
-				seen_unbounded = true;
-			else if (seen_unbounded)
-				ereport(ERROR,
-						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("cannot specify finite value after UNBOUNDED"),
-						 parser_errposition(pstate, exprLocation((Node *) ldatum))));
-		}
-		seen_unbounded = false;
-		foreach(cell1, spec->upperdatums)
-		{
-			PartitionRangeDatum *rdatum = castNode(PartitionRangeDatum,
-												   lfirst(cell1));
+			/* Get the column's name in case we need to output an error */
+			if (key->partattrs[i] != 0)
+				colname = get_relid_attribute_name(RelationGetRelid(parent),
+												   key->partattrs[i]);
+			else
+			{
+				colname = deparse_expression((Node *) list_nth(partexprs, j),
+											 deparse_context_for(RelationGetRelationName(parent),
+																 RelationGetRelid(parent)),
+											 false, false);
+				++j;
+			}
+			/* Need its type data too */
+			coltype = get_partition_col_typid(key, i);
+			coltypmod = get_partition_col_typmod(key, i);
 
-			if (rdatum->infinite)
-				seen_unbounded = true;
-			else if (seen_unbounded)
+			value = transformPartitionBoundValue(pstate, con,
+												 colname,
+												 coltype, coltypmod);
+			if (value->constisnull)
 				ereport(ERROR,
-						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("cannot specify finite value after UNBOUNDED"),
-						 parser_errposition(pstate, exprLocation((Node *) rdatum))));
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("cannot specify NULL in range bound")));
+
+			result_spec->lowerdatums = lappend(result_spec->lowerdatums,
+											   value);
+			++i;
 		}
 
-		/* Transform all the constants */
+		/* Transform the upper bound values */
 		i = j = 0;
-		result_spec->lowerdatums = result_spec->upperdatums = NIL;
-		forboth(cell1, spec->lowerdatums, cell2, spec->upperdatums)
+		result_spec->upperdatums = NIL;
+		foreach(cell, spec->upperdatums)
 		{
-			PartitionRangeDatum *ldatum = (PartitionRangeDatum *) lfirst(cell1);
-			PartitionRangeDatum *rdatum = (PartitionRangeDatum *) lfirst(cell2);
+			A_Const    *con = castNode(A_Const, lfirst(cell));
 			char	   *colname;
 			Oid			coltype;
 			int32		coltypmod;
-			A_Const    *con;
 			Const	   *value;
 
 			/* Get the column's name in case we need to output an error */
@@ -3444,39 +3448,16 @@ transformPartitionBound(ParseState *psta
 			coltype = get_partition_col_typid(key, i);
 			coltypmod = get_partition_col_typmod(key, i);
 
-			if (ldatum->value)
-			{
-				con = castNode(A_Const, ldatum->value);
-				value = transformPartitionBoundValue(pstate, con,
-													 colname,
-													 coltype, coltypmod);
-				if (value->constisnull)
-					ereport(ERROR,
-							(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-							 errmsg("cannot specify NULL in range bound")));
-				ldatum = copyObject(ldatum);	/* don't scribble on input */
-				ldatum->value = (Node *) value;
-			}
-
-			if (rdatum->value)
-			{
-				con = castNode(A_Const, rdatum->value);
-				value = transformPartitionBoundValue(pstate, con,
-													 colname,
-													 coltype, coltypmod);
-				if (value->constisnull)
-					ereport(ERROR,
-							(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-							 errmsg("cannot specify NULL in range bound")));
-				rdatum = copyObject(rdatum);	/* don't scribble on input */
-				rdatum->value = (Node *) value;
-			}
+			value = transformPartitionBoundValue(pstate, con,
+												 colname,
+												 coltype, coltypmod);
+			if (value->constisnull)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("cannot specify NULL in range bound")));
 
-			result_spec->lowerdatums = lappend(result_spec->lowerdatums,
-											   ldatum);
 			result_spec->upperdatums = lappend(result_spec->upperdatums,
-											   rdatum);
-
+											   value);
 			++i;
 		}
 	}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index 18d9e27..4df1230
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -8667,48 +8667,43 @@ get_rule_expr(Node *node, deparse_contex
 						break;
 
 					case PARTITION_STRATEGY_RANGE:
-						Assert(spec->lowerdatums != NIL &&
-							   spec->upperdatums != NIL &&
-							   list_length(spec->lowerdatums) ==
-							   list_length(spec->upperdatums));
-
-						appendStringInfoString(buf, "FOR VALUES FROM (");
-						sep = "";
-						foreach(cell, spec->lowerdatums)
+						/* Bounds may be empty, which means UNBOUNDED */
+						appendStringInfoString(buf, "FOR VALUES FROM ");
+						if (spec->lowerdatums)
 						{
-							PartitionRangeDatum *datum =
-							castNode(PartitionRangeDatum, lfirst(cell));
-
-							appendStringInfoString(buf, sep);
-							if (datum->infinite)
-								appendStringInfoString(buf, "UNBOUNDED");
-							else
+							appendStringInfoString(buf, "(");
+							sep = "";
+							foreach(cell, spec->lowerdatums)
 							{
-								Const	   *val = castNode(Const, datum->value);
+								Const	   *val = castNode(Const, lfirst(cell));
 
+								appendStringInfoString(buf, sep);
 								get_const_expr(val, context, -1);
+								sep = ", ";
 							}
-							sep = ", ";
+							appendStringInfoString(buf, ")");
 						}
-						appendStringInfoString(buf, ") TO (");
-						sep = "";
-						foreach(cell, spec->upperdatums)
-						{
-							PartitionRangeDatum *datum =
-							castNode(PartitionRangeDatum, lfirst(cell));
+						else
+							appendStringInfoString(buf, "UNBOUNDED");
 
-							appendStringInfoString(buf, sep);
-							if (datum->infinite)
-								appendStringInfoString(buf, "UNBOUNDED");
-							else
+						appendStringInfoString(buf, " TO ");
+						if (spec->upperdatums)
+						{
+							appendStringInfoString(buf, "(");
+							sep = "";
+							foreach(cell, spec->upperdatums)
 							{
-								Const	   *val = castNode(Const, datum->value);
+								Const	   *val = castNode(Const, lfirst(cell));
 
+								appendStringInfoString(buf, sep);
 								get_const_expr(val, context, -1);
+								sep = ", ";
 							}
-							sep = ", ";
+							appendStringInfoString(buf, ")");
 						}
-						appendStringInfoString(buf, ")");
+						else
+							appendStringInfoString(buf, "UNBOUNDED");
+
 						break;
 
 					default:
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
new file mode 100644
index 0152739..b800aab
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -466,7 +466,6 @@ typedef enum NodeTag
 	T_PartitionElem,
 	T_PartitionSpec,
 	T_PartitionBoundSpec,
-	T_PartitionRangeDatum,
 	T_PartitionCmd,
 
 	/*
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index 1d96169..30a8ef8
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -802,28 +802,13 @@ typedef struct PartitionBoundSpec
 	List	   *listdatums;		/* List of Consts (or A_Consts in raw tree) */
 
 	/* Partitioning info for RANGE strategy: */
-	List	   *lowerdatums;	/* List of PartitionRangeDatums */
-	List	   *upperdatums;	/* List of PartitionRangeDatums */
+	List	   *lowerdatums;	/* List of Consts (or A_Consts in raw tree) */
+	List	   *upperdatums;	/* List of Consts (or A_Consts in raw tree) */
 
 	int			location;		/* token location, or -1 if unknown */
 } PartitionBoundSpec;
 
 /*
- * PartitionRangeDatum - can be either a value or UNBOUNDED
- *
- * "value" is an A_Const in raw grammar output, a Const after analysis
- */
-typedef struct PartitionRangeDatum
-{
-	NodeTag		type;
-
-	bool		infinite;		/* true if UNBOUNDED */
-	Node	   *value;			/* null if UNBOUNDED */
-
-	int			location;		/* token location, or -1 if unknown */
-} PartitionRangeDatum;
-
-/*
  * PartitionCmd - info for ALTER TABLE ATTACH/DETACH PARTITION commands
  */
 typedef struct PartitionCmd
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
new file mode 100644
index fb8745b..f24fa58
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -505,22 +505,19 @@ CREATE TABLE fail_part PARTITION OF rang
 ERROR:  invalid bound specification for a range partition
 LINE 1: ...BLE fail_part PARTITION OF range_parted FOR VALUES IN ('a');
                                                               ^
--- each of start and end bounds must have same number of values as the
--- length of the partition key
+-- start and end bounds cannot have more values than the length of the partition key
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a', 1) TO ('z');
-ERROR:  FROM must specify exactly one value per partitioning column
+ERROR:  too many values in FROM list
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a') TO ('z', 1);
-ERROR:  TO must specify exactly one value per partitioning column
+ERROR:  too many values in TO list
 -- cannot specify null values in range bounds
-CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO ('z');
 ERROR:  cannot specify NULL in range bound
--- 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...
+-- cannot specify UNBOUNDED values in range bounds
+CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (UNBOUNDED) TO ('z');
+ERROR:  syntax error at or near "UNBOUNDED"
+LINE 1: ...l_part PARTITION OF range_parted FOR VALUES FROM (UNBOUNDED)...
                                                              ^
-DROP TABLE range_parted_multicol;
 -- check if compatible with the specified parent
 -- cannot create as partition of a non-partitioned table
 CREATE TABLE unparted (
@@ -578,35 +575,35 @@ ERROR:  cannot create range partition wi
 -- note that the range '[1, 1)' has no elements
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (1);
 ERROR:  cannot create range partition with empty range
-CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (1);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (2);
+CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM unbounded TO (1);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM unbounded TO (2);
 ERROR:  partition "fail_part" would overlap partition "part0"
 CREATE TABLE part1 PARTITION OF range_parted2 FOR VALUES FROM (1) TO (10);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO unbounded;
 ERROR:  partition "fail_part" would overlap partition "part1"
 CREATE TABLE part2 PARTITION OF range_parted2 FOR VALUES FROM (20) TO (30);
 CREATE TABLE part3 PARTITION OF range_parted2 FOR VALUES FROM (30) TO (40);
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (30);
 ERROR:  partition "fail_part" would overlap partition "part2"
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (50);
-ERROR:  partition "fail_part" would overlap partition "part3"
+ERROR:  partition "fail_part" would overlap partition "part2"
 -- now check for multi-column range partition key
 CREATE TABLE range_parted3 (
 	a int,
 	b int
 ) PARTITION BY RANGE (a, (b+1));
-CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, unbounded);
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, 1);
+CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0) TO (1);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0) TO (0, 1);
 ERROR:  partition "fail_part" would overlap partition "part00"
-CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, 1);
+CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1) TO (1, 1);
 CREATE TABLE part11 PARTITION OF range_parted3 FOR VALUES FROM (1, 1) TO (1, 10);
-CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, unbounded);
+CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (2);
 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
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1) TO (2);
 ERROR:  partition "fail_part" would overlap partition "part10"
 -- check schema propagation from parent
 CREATE TABLE parted (
@@ -708,7 +705,7 @@ Number of partitions: 3 (Use \d+ to list
 
 -- 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 TO UNBOUNDED;
 \d+ unbounded_range_part
                            Table "public.unbounded_range_part"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -716,11 +713,11 @@ CREATE TABLE unbounded_range_part PARTIT
  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 TO UNBOUNDED
 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 TO (2);
 \d+ range_parted4_1
                               Table "public.range_parted4_1"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -728,10 +725,10 @@ CREATE TABLE range_parted4_1 PARTITION O
  a      | integer |           |          |         | plain   |              | 
  b      | integer |           |          |         | plain   |              | 
  c      | integer |           |          |         | plain   |              | 
-Partition of: range_parted4 FOR VALUES FROM (UNBOUNDED, UNBOUNDED, UNBOUNDED) TO (1, UNBOUNDED, UNBOUNDED)
-Partition constraint: ((abs(a) IS NOT NULL) AND (abs(b) IS NOT NULL) AND (c IS NOT NULL) AND (abs(a) <= 1))
+Partition of: range_parted4 FOR VALUES FROM UNBOUNDED TO (2)
+Partition constraint: ((abs(a) IS NOT NULL) AND (abs(b) IS NOT NULL) AND (c IS NOT NULL) AND (abs(a) < 2))
 
-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, 8);
 \d+ range_parted4_2
                               Table "public.range_parted4_2"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -739,10 +736,10 @@ CREATE TABLE range_parted4_2 PARTITION O
  a      | integer |           |          |         | plain   |              | 
  b      | integer |           |          |         | plain   |              | 
  c      | integer |           |          |         | plain   |              | 
-Partition of: range_parted4 FOR VALUES FROM (3, 4, 5) TO (6, 7, UNBOUNDED)
-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))))
+Partition of: range_parted4 FOR VALUES FROM (3, 4, 5) TO (6, 8)
+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) < 8))))
 
-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) TO (10);
 \d+ range_parted4_3
                               Table "public.range_parted4_3"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -750,8 +747,8 @@ CREATE TABLE range_parted4_3 PARTITION O
  a      | integer |           |          |         | plain   |              | 
  b      | integer |           |          |         | plain   |              | 
  c      | integer |           |          |         | plain   |              | 
-Partition of: range_parted4 FOR VALUES FROM (6, 8, UNBOUNDED) TO (9, UNBOUNDED, UNBOUNDED)
-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))
+Partition of: range_parted4 FOR VALUES FROM (6, 8) TO (10)
+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) < 10))
 
 DROP TABLE range_parted4;
 -- cleanup
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
new file mode 100644
index 35d182d..b563ab0
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1718,7 +1718,7 @@ create table part_10_20_cd partition of
 create table part_21_30 partition of range_list_parted for values from (21) to (30) partition by list (b);
 create table part_21_30_ab partition of part_21_30 for values in ('ab');
 create table part_21_30_cd partition of part_21_30 for values in ('cd');
-create table part_40_inf partition of range_list_parted for values from (40) to (unbounded) partition by list (b);
+create table part_40_inf partition of range_list_parted for values from (40) to unbounded partition by list (b);
 create table part_40_inf_ab partition of part_40_inf for values in ('ab');
 create table part_40_inf_cd partition of part_40_inf for values in ('cd');
 create table part_40_inf_null partition of part_40_inf for values in (null);
@@ -1831,12 +1831,12 @@ drop table range_list_parted;
 -- check that constraint exclusion is able to cope with the partition
 -- constraint emitted for multi-column range partitioned tables
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, 1, 1);
+create table mcrparted0 partition of mcrparted for values from unbounded to (1, 1, 1);
 create table mcrparted1 partition of mcrparted for values from (1, 1, 1) to (10, 5, 10);
 create table mcrparted2 partition of mcrparted for values from (10, 5, 10) to (10, 10, 10);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
 create table mcrparted4 partition of mcrparted for values from (20, 10, 10) to (20, 20, 20);
-create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to unbounded;
 explain (costs off) select * from mcrparted where a = 0;	-- scans mcrparted0
           QUERY PLAN          
 ------------------------------
diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out
new file mode 100644
index d1153f4..2718213
--- a/src/test/regress/expected/insert.out
+++ b/src/test/regress/expected/insert.out
@@ -288,7 +288,7 @@ select tableoid::regclass, * from list_p
 
 -- some more tests to exercise tuple-routing with multi-level partitioning
 create table part_gg partition of list_parted for values in ('gg') partition by range (b);
-create table part_gg1 partition of part_gg for values from (unbounded) to (1);
+create table part_gg1 partition of part_gg for values from unbounded to (1);
 create table part_gg2 partition of part_gg for values from (1) to (10) partition by range (b);
 create table part_gg2_1 partition of part_gg2 for values from (1) to (5);
 create table part_gg2_2 partition of part_gg2 for values from (5) to (10);
@@ -439,12 +439,12 @@ drop table key_desc, key_desc_1;
 -- check multi-column range partitioning expression enforces the same
 -- constraint as what tuple-routing would determine it to be
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, unbounded, unbounded);
-create table mcrparted1 partition of mcrparted for values from (2, 1, unbounded) to (10, 5, 10);
-create table mcrparted2 partition of mcrparted for values from (10, 6, unbounded) to (10, unbounded, unbounded);
+create table mcrparted0 partition of mcrparted for values from unbounded to (2);
+create table mcrparted1 partition of mcrparted for values from (2, 1) to (10, 5, 10);
+create table mcrparted2 partition of mcrparted for values from (10, 6) to (11);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
-create table mcrparted4 partition of mcrparted for values from (21, unbounded, unbounded) to (30, 20, unbounded);
-create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted4 partition of mcrparted for values from (21) to (30, 21);
+create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to unbounded;
 -- routed to mcrparted0
 insert into mcrparted values (0, 1, 1);
 insert into mcrparted0 values (0, 1, 1);
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
new file mode 100644
index cb7aa5b..89ce126
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -477,18 +477,15 @@ CREATE TABLE range_parted (
 
 -- trying to specify list for range partitioned table
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES IN ('a');
--- each of start and end bounds must have same number of values as the
--- length of the partition key
+-- start and end bounds cannot have more values than the length of the partition key
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a', 1) TO ('z');
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a') TO ('z', 1);
 
 -- cannot specify null values in range bounds
-CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO ('z');
 
--- 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);
-DROP TABLE range_parted_multicol;
+-- cannot specify UNBOUNDED values in range bounds
+CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (UNBOUNDED) TO ('z');
 
 -- check if compatible with the specified parent
 
@@ -542,10 +539,10 @@ CREATE TABLE fail_part PARTITION OF rang
 -- note that the range '[1, 1)' has no elements
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (1);
 
-CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (1);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (2);
+CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM unbounded TO (1);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM unbounded TO (2);
 CREATE TABLE part1 PARTITION OF range_parted2 FOR VALUES FROM (1) TO (10);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO unbounded;
 CREATE TABLE part2 PARTITION OF range_parted2 FOR VALUES FROM (20) TO (30);
 CREATE TABLE part3 PARTITION OF range_parted2 FOR VALUES FROM (30) TO (40);
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (30);
@@ -557,18 +554,18 @@ CREATE TABLE range_parted3 (
 	b int
 ) PARTITION BY RANGE (a, (b+1));
 
-CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, unbounded);
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, 1);
+CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0) TO (1);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0) TO (0, 1);
 
-CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, 1);
+CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1) TO (1, 1);
 CREATE TABLE part11 PARTITION OF range_parted3 FOR VALUES FROM (1, 1) TO (1, 10);
-CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, unbounded);
+CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (2);
 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
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1) TO (2);
 
 -- check schema propagation from parent
 
@@ -626,14 +623,14 @@ CREATE TABLE part_c_1_10 PARTITION OF pa
 
 -- 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 TO 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 TO (2);
 \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, 8);
 \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) TO (10);
 \d+ range_parted4_3
 DROP TABLE range_parted4;
 
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
new file mode 100644
index 70fe971..9149bda
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -623,7 +623,7 @@ create table part_10_20_cd partition of
 create table part_21_30 partition of range_list_parted for values from (21) to (30) partition by list (b);
 create table part_21_30_ab partition of part_21_30 for values in ('ab');
 create table part_21_30_cd partition of part_21_30 for values in ('cd');
-create table part_40_inf partition of range_list_parted for values from (40) to (unbounded) partition by list (b);
+create table part_40_inf partition of range_list_parted for values from (40) to unbounded partition by list (b);
 create table part_40_inf_ab partition of part_40_inf for values in ('ab');
 create table part_40_inf_cd partition of part_40_inf for values in ('cd');
 create table part_40_inf_null partition of part_40_inf for values in (null);
@@ -647,12 +647,12 @@ drop table range_list_parted;
 -- check that constraint exclusion is able to cope with the partition
 -- constraint emitted for multi-column range partitioned tables
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, 1, 1);
+create table mcrparted0 partition of mcrparted for values from unbounded to (1, 1, 1);
 create table mcrparted1 partition of mcrparted for values from (1, 1, 1) to (10, 5, 10);
 create table mcrparted2 partition of mcrparted for values from (10, 5, 10) to (10, 10, 10);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
 create table mcrparted4 partition of mcrparted for values from (20, 10, 10) to (20, 20, 20);
-create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to unbounded;
 explain (costs off) select * from mcrparted where a = 0;	-- scans mcrparted0
 explain (costs off) select * from mcrparted where a = 10 and abs(b) < 5;	-- scans mcrparted1
 explain (costs off) select * from mcrparted where a = 10 and abs(b) = 5;	-- scans mcrparted1, mcrparted2
diff --git a/src/test/regress/sql/insert.sql b/src/test/regress/sql/insert.sql
new file mode 100644
index 83c3ad8..2b8637d
--- a/src/test/regress/sql/insert.sql
+++ b/src/test/regress/sql/insert.sql
@@ -169,7 +169,7 @@ select tableoid::regclass, * from list_p
 
 -- some more tests to exercise tuple-routing with multi-level partitioning
 create table part_gg partition of list_parted for values in ('gg') partition by range (b);
-create table part_gg1 partition of part_gg for values from (unbounded) to (1);
+create table part_gg1 partition of part_gg for values from unbounded to (1);
 create table part_gg2 partition of part_gg for values from (1) to (10) partition by range (b);
 create table part_gg2_1 partition of part_gg2 for values from (1) to (5);
 create table part_gg2_2 partition of part_gg2 for values from (5) to (10);
@@ -293,12 +293,12 @@ drop table key_desc, key_desc_1;
 -- check multi-column range partitioning expression enforces the same
 -- constraint as what tuple-routing would determine it to be
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, unbounded, unbounded);
-create table mcrparted1 partition of mcrparted for values from (2, 1, unbounded) to (10, 5, 10);
-create table mcrparted2 partition of mcrparted for values from (10, 6, unbounded) to (10, unbounded, unbounded);
+create table mcrparted0 partition of mcrparted for values from unbounded to (2);
+create table mcrparted1 partition of mcrparted for values from (2, 1) to (10, 5, 10);
+create table mcrparted2 partition of mcrparted for values from (10, 6) to (11);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
-create table mcrparted4 partition of mcrparted for values from (21, unbounded, unbounded) to (30, 20, unbounded);
-create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted4 partition of mcrparted for values from (21) to (30, 21);
+create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to unbounded;
 
 -- routed to mcrparted0
 insert into mcrparted values (0, 1, 1);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
new file mode 100644
index 23a4bbd..1bb6a8a
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1561,7 +1561,6 @@ PartitionElem
 PartitionKey
 PartitionListValue
 PartitionRangeBound
-PartitionRangeDatum
 PartitionSpec
 PartitionedChildRelInfo
 PasswordType
#20Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Amit Langote (#18)
Re: Multi column range partition table

On 5 July 2017 at 10:43, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

In retrospect, that sounds like something that was implemented in the
earlier versions of the patch, whereby there was no ability to specify
UNBOUNDED on a per-column basis. So the syntax was:

FROM { (x [, ...]) | UNBOUNDED } TO { (y [, ...]) | UNBOUNDED }

But, it was pointed out to me [1] that that doesn't address the use case,
for example, where part1 goes up to (10, 10) and part2 goes from (10, 10)
up to (10, unbounded).

[Reading that other thread]

It's a reasonable point that our syntax is quite different from
Oracle's, and doing this takes it even further away, and removes
support for things that they do support.

For the record, Oracle allows things like the following:

DROP TABLE t1;
CREATE TABLE t1 (a NUMBER, b NUMBER, c NUMBER)
PARTITION BY RANGE (a,b,c)
(PARTITION t1p1 VALUES LESS THAN (1,2,3),
PARTITION t1p2 VALUES LESS THAN (2,3,4),
PARTITION t1p3 VALUES LESS THAN (3,MAXVALUE,5),
PARTITION t1p4 VALUES LESS THAN (4,MAXVALUE,6)
);

INSERT INTO t1 VALUES(1,2,3);
INSERT INTO t1 VALUES(2,3,4);
INSERT INTO t1 VALUES(3,4,5);
INSERT INTO t1 VALUES(3.01,4,5);
INSERT INTO t1 VALUES(4,5,10);

COLUMN subobject_name FORMAT a20;
SELECT a, b, c, subobject_name
FROM t1, user_objects o
WHERE o.data_object_id = dbms_rowid.rowid_object(t1.ROWID)
ORDER BY a,b,c;

A B C SUBOBJECT_NAME
---------- ---------- ---------- --------------------
1 2 3 T1P2
2 3 4 T1P3
3 4 5 T1P3
3.01 4 5 T1P4
4 5 10 T1P4

So they use MAXVALUE instead of UNBOUNDED for an upper bound, which is
more explicit. They don't have an equivalent MINVALUE, but it's
arguably not necessary, since the first partition's lower bound is
implicitly unbounded.

With this syntax they don't need to worry about gaps or overlaps
between partitions, which is nice, but arguably less flexible.

They're also more lax about allowing finite values after MAXVALUE, and
they document the fact that any value after a MAXVALUE is ignored.

I don't think their scheme provides any way to define a partition of
the above table that would hold all rows for which a < some value.

So if we were to go for maximum flexibility and compatibility with
Oracle, then perhaps what we would do is more like the original idea
of UNBOUNDED ABOVE/BELOW, except call them MINVALUE and MAXVALUE,
which conveniently are already unreserved keywords, as well as being
much shorter. Plus, we would also relax the constraint about having
finite values after MINVALUE/MAXVALUE.

I think I'll go play around with that idea to see what it looks like
in practice. Your previous patch already does much of that, and is far
less invasive.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#21Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Dean Rasheed (#19)
Re: Multi column range partition table

Hi Dean,

On 2017/07/05 23:18, Dean Rasheed wrote:

On 5 July 2017 at 10:43, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

In retrospect, that sounds like something that was implemented in the
earlier versions of the patch, whereby there was no ability to specify
UNBOUNDED on a per-column basis. So the syntax was:

FROM { (x [, ...]) | UNBOUNDED } TO { (y [, ...]) | UNBOUNDED }

Yes, that's where I ended up too.

I see.

But, it was pointed out to me [1] that that doesn't address the use case,
for example, where part1 goes up to (10, 10) and part2 goes from (10, 10)
up to (10, unbounded).

The new design will limit the usage of unbounded range partitions at the
tail ends.

True, but I don't think that's really a problem. When the first column
is a discrete type, an upper bound of (10, unbounded) can be rewritten
as (11) in the new design. When it's a continuous type, e.g. floating
point, it can no longer be represented, because (10.0, unbounded)
really means (col1 <= 10.0). But we've already decided not to support
anything other than inclusive lower bounds and exclusive upper bounds,
so allowing this upper bound goes against that design choice.

Yes.

Of course, it's pretty late in the day to be proposing this kind of
redesign, but I fear that if we don't tackle it now, it will just be
harder to deal with in the future.

Actually, a quick, simple hacky implementation might be to just fill
in any omitted values in a partition bound with negative infinity
internally, and when printing a bound, omit any values after an
infinite value. But really, I think we'd want to tidy up the
implementation, and I think a number of things would actually get much
simpler. For example, get_qual_for_range() could simply stop when it
reached the end of the list of values for the bound, and it wouldn't
need to worry about an unbounded value following a bounded one.

Thoughts?

I cooked up a patch for the "hacky" implementation for now, just as you
described in the above paragraph. Will you be willing to give it a look?
I will also think about the non-hacky way of implementing this.

OK, I'll take a look.

Thanks a lot for your time.

Meanwhile, I already had a go at the "non-hacky" implementation (WIP
patch attached). The more I worked on it, the simpler things got,
which I think is a good sign.

It definitely looks good to me. I was thinking of more or less the same
approach, but couldn't have done as clean a job as you've done with your
patch.

Part-way through, I realised that the PartitionRangeDatum Node type is
no longer needed, because each bound value is now necessarily finite,
so the lowerdatums and upperdatums lists in a PartitionBoundSpec can
now be made into lists of Const nodes, making them match the
listdatums field used for LIST partitioning, and then a whole lot of
related code gets simplified.

Yeah, seems that way.

It needed a little bit more code in partition.c to track individual
bound sizes, but there were a number of other places that could be
simplified, so overall this represents a reduction in the code size
and complexity.

Sounds reasonable.

It's not complete (e.g., no doc updates yet), but it passes all the
tests, and so far seems to work as I would expect.

Thanks a lot for working on it.

Regards,
Amit

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#22Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Amit Langote (#18)
Re: Multi column range partition table

On 5 July 2017 at 10:43, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

0001 is your patch to tidy up check_new_partition_bound() (must be
applied before 0002)

I pushed this first patch, simplifying check_new_partition_bound() for
range partitions, since it seemed like a good simplification, but note
that I don't think that was actually the cause of the latent bug you
saw upthread.

I think the real issue was in partition_rbound_cmp() -- normally, if
the upper bound of one partition coincides with the lower bound of
another, that function would report the upper bound as the smaller
one, but that logic breaks if any of the bound values are infinite,
since then it will exit early, returning 0, without ever comparing the
"lower" flags on the bounds.

I'm tempted to push a fix for that independently, since it's a bug
waiting to happen, even though it's not possible to hit it currently.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#23Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Dean Rasheed (#20)
1 attachment(s)
Re: Multi column range partition table

On 5 July 2017 at 18:07, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

So if we were to go for maximum flexibility and compatibility with
Oracle, then perhaps what we would do is more like the original idea
of UNBOUNDED ABOVE/BELOW, except call them MINVALUE and MAXVALUE,
which conveniently are already unreserved keywords, as well as being
much shorter. Plus, we would also relax the constraint about having
finite values after MINVALUE/MAXVALUE.

So I know that I have flip-flopped a few times on this now, but I'm
now starting to think that this approach, replacing UNBOUNDED with
MINVALUE and MAXVALUE is the best way to go, along with permitting
finite values after MINVALUE/MAXVALUE.

This gives the greatest flexibility, it's not too verbose, and it
makes it easy to define contiguous sets of partitions just by making
the lower bound of one match the upper bound of another.

With this approach, any partition bounds that Oracle allows are also
valid in PostgreSQL, not that I would normally give too much weight to
that, but it is I think quite a nice syntax. Of course, we also
support things that Oracle doesn't allow, such as MINVALUE and gaps
between partitions.

Parts of the patch are similar to your UNBOUNDED ABOVE/BELOW patch,
but there are a number of differences -- most notably, I replaced the
"infinite" boolean flag on PartitionRangeDatum with a 3-value enum and
did away with all the DefElem nodes and the associated special string
constants being copied and compared.

However, this is also an incompatible syntax change, and any attempt
to support both the old and new syntaxes is likely to be messy, so we
really need to get consensus on whether this is the right thing to do,
and whether it *can* be done now for PG10.

Regards,
Dean

Attachments:

Replace_UNBOUNDED_with_MINVALUE_and_MAXVALUE.patchapplication/octet-stream; name=Replace_UNBOUNDED_with_MINVALUE_and_MAXVALUE.patchDownload
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
new file mode 100644
index b15c19d..2e3fac7
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -87,8 +87,8 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY
 <phrase>and <replaceable class="PARAMETER">partition_bound_spec</replaceable> is:</phrase>
 
 IN ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | NULL } [, ...] ) |
-FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | UNBOUNDED } [, ...] )
-  TO ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | UNBOUNDED } [, ...] )
+FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | MINVALUE | MAXVALUE } [, ...] )
+  TO ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | MINVALUE | MAXVALUE } [, ...] )
 
 <phrase><replaceable class="PARAMETER">index_parameters</replaceable> in <literal>UNIQUE</literal>, <literal>PRIMARY KEY</literal>, and <literal>EXCLUDE</literal> constraints are:</phrase>
 
@@ -300,13 +300,47 @@ FROM ( { <replaceable class="PARAMETER">
      </para>
 
      <para>
-      Writing <literal>UNBOUNDED</literal> in <literal>FROM</literal>
-      signifies <literal>-infinity</literal> as the lower bound of the
-      corresponding column, whereas when written in <literal>TO</literal>,
-      it signifies <literal>+infinity</literal> as the upper bound.
-      All items following an <literal>UNBOUNDED</literal> item within
-      a <literal>FROM</literal> or <literal>TO</literal> list must also
-      be <literal>UNBOUNDED</literal>.
+      The special values <literal>MINVALUE</> and <literal>MAXVALUE</>
+      may be use when creating a range partition to indicate that there
+      is no lower or upper bound on the column's value. For example, a
+      partition defined using <literal>FROM (MINVALUE) TO (10)</> allows
+      any values less than 10, and a partition defined using
+      <literal>FROM (10) TO (MAXVALUE)</> allows any values greater than
+      or equal to 10.
+     </para>
+
+     <para>
+      When creating a range partition involving more than one column, it
+      can also make sense to use <literal>MAXVALUE</> as part of the lower
+      bound, and <literal>MINVALUE</> as part of the upper bound. For
+      example, a partition defined using
+      <literal>FROM (0, MAXVALUE) TO (10, MAXVALUE)</> allows any rows
+      where the first partitioned column is greater than 0 and less than
+      or equal to 10. Similarly, a partition defined using
+      <literal>FROM ('a', MINVALUE) TO ('b', MINVALUE)</>
+      allows only rows where the first partitioned column starts with "a".
+     </para>
+
+     <para>
+      Note that any values after <literal>MINVALUE</> or
+      <literal>MAXVALUE</> in a partition bound are ignored; so the bound
+      <literal>(10, MINVALUE, 0)</> is equivalent to
+      <literal>(10, MINVALUE, 10)</> and <literal>(10, MINVALUE, MINVALUE)</>
+      and <literal>(10, MINVALUE, MAXVALUE)</>.
+     </para>
+
+     <para>
+      Also note that some element types, such as <literal>timestamp</>,
+      have a notion of "infinity", which is just another value that can
+      be stored. This is different from <literal>MINVALUE</> and
+      <literal>MAXVALUE</>, which are not real values that can be stored,
+      but rather they are ways of saying the value is unbounded.
+      <literal>MAXVALUE</> can be thought of as being greater than any
+      other value, including "infinity" and <literal>MINVALUE</> as being
+      less than any other value, including "minus infinity". Thus the range
+      <literal>FROM ('infinity') TO (MAXVALUE)</> is not an empty range; it
+      allows precisely one value to be stored &mdash; the timestamp
+      "infinity".
      </para>
 
      <para>
diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c
new file mode 100644
index 43b8924..92b5e0c
--- a/src/backend/catalog/partition.c
+++ b/src/backend/catalog/partition.c
@@ -67,23 +67,14 @@
  * is an upper bound.
  */
 
-/* Ternary value to represent what's contained in a range bound datum */
-typedef enum RangeDatumContent
-{
-	RANGE_DATUM_FINITE = 0,		/* actual datum stored elsewhere */
-	RANGE_DATUM_NEG_INF,		/* negative infinity */
-	RANGE_DATUM_POS_INF			/* positive infinity */
-} RangeDatumContent;
-
 typedef struct PartitionBoundInfoData
 {
 	char		strategy;		/* list or range bounds? */
 	int			ndatums;		/* Length of the datums following array */
 	Datum	  **datums;			/* Array of datum-tuples with key->partnatts
 								 * datums each */
-	RangeDatumContent **content;	/* what's contained in each range bound
-									 * datum? (see the above enum); NULL for
-									 * list partitioned tables */
+	PartitionRangeDatumKind **kind;	/* The kind of each range bound datum;
+									 * NULL for list partitioned tables */
 	int		   *indexes;		/* Partition indexes; one entry per member of
 								 * the datums array (plus one if range
 								 * partitioned table) */
@@ -110,7 +101,7 @@ typedef struct PartitionRangeBound
 {
 	int			index;
 	Datum	   *datums;			/* range bound datums */
-	RangeDatumContent *content; /* what's contained in each datum? */
+	PartitionRangeDatumKind *kind; /* the kind of each datum */
 	bool		lower;			/* this is the lower (vs upper) bound */
 } PartitionRangeBound;
 
@@ -136,10 +127,10 @@ static List *generate_partition_qual(Rel
 static PartitionRangeBound *make_one_range_bound(PartitionKey key, int index,
 					 List *datums, bool lower);
 static int32 partition_rbound_cmp(PartitionKey key,
-					 Datum *datums1, RangeDatumContent *content1, bool lower1,
-					 PartitionRangeBound *b2);
+					 Datum *datums1, PartitionRangeDatumKind *kind1,
+					 bool lower1, PartitionRangeBound *b2);
 static int32 partition_rbound_datum_cmp(PartitionKey key,
-						   Datum *rb_datums, RangeDatumContent *rb_content,
+						   Datum *rb_datums, PartitionRangeDatumKind *rb_kind,
 						   Datum *tuple_datums);
 
 static int32 partition_bound_cmp(PartitionKey key,
@@ -371,24 +362,20 @@ RelationBuildPartitionDesc(Relation rel)
 				{
 					Datum		cmpval;
 
-					if (prev == NULL)
+					if (prev == NULL || cur->kind[j] != prev->kind[j])
 					{
 						is_distinct = true;
 						break;
 					}
 
 					/*
-					 * 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 the bounds are both MINVALUE or MAXVALUE, stop now
+					 * and treat them as equal, since any values after this
+					 * point must be ignored.
 					 */
-					if (cur->content[j] != RANGE_DATUM_FINITE ||
-						prev->content[j] != RANGE_DATUM_FINITE)
-					{
-						is_distinct = true;
+					if (cur->kind[j] != PARTITION_RANGE_DATUM_VALUE)
 						break;
-					}
+
 					cmpval = FunctionCall2Coll(&key->partsupfunc[j],
 											   key->partcollation[j],
 											   cur->datums[j],
@@ -513,8 +500,9 @@ RelationBuildPartitionDesc(Relation rel)
 
 			case PARTITION_STRATEGY_RANGE:
 				{
-					boundinfo->content = (RangeDatumContent **) palloc(ndatums *
-																	   sizeof(RangeDatumContent *));
+					boundinfo->kind = (PartitionRangeDatumKind **)
+							palloc(ndatums *
+								   sizeof(PartitionRangeDatumKind *));
 					boundinfo->indexes = (int *) palloc((ndatums + 1) *
 														sizeof(int));
 
@@ -524,18 +512,17 @@ RelationBuildPartitionDesc(Relation rel)
 
 						boundinfo->datums[i] = (Datum *) palloc(key->partnatts *
 																sizeof(Datum));
-						boundinfo->content[i] = (RangeDatumContent *)
+						boundinfo->kind[i] = (PartitionRangeDatumKind *)
 							palloc(key->partnatts *
-								   sizeof(RangeDatumContent));
+								   sizeof(PartitionRangeDatumKind));
 						for (j = 0; j < key->partnatts; j++)
 						{
-							if (rbounds[i]->content[j] == RANGE_DATUM_FINITE)
+							if (rbounds[i]->kind[j] == PARTITION_RANGE_DATUM_VALUE)
 								boundinfo->datums[i][j] =
 									datumCopy(rbounds[i]->datums[j],
 											  key->parttypbyval[j],
 											  key->parttyplen[j]);
-							/* Remember, we are storing the tri-state value. */
-							boundinfo->content[i][j] = rbounds[i]->content[j];
+							boundinfo->kind[i][j] = rbounds[i]->kind[j];
 						}
 
 						/*
@@ -617,17 +604,14 @@ partition_bounds_equal(PartitionKey key,
 		for (j = 0; j < key->partnatts; j++)
 		{
 			/* For range partitions, the bounds might not be finite. */
-			if (b1->content != NULL)
+			if (b1->kind != NULL)
 			{
-				/*
-				 * A finite bound always differs from an infinite bound, and
-				 * different kinds of infinities differ from each other.
-				 */
-				if (b1->content[i][j] != b2->content[i][j])
+				/* The different kinds of bound all differ from each other */
+				if (b1->kind[i][j] != b2->kind[i][j])
 					return false;
 
 				/* Non-finite bounds are equal without further examination. */
-				if (b1->content[i][j] != RANGE_DATUM_FINITE)
+				if (b1->kind[i][j] != PARTITION_RANGE_DATUM_VALUE)
 					continue;
 			}
 
@@ -736,7 +720,7 @@ check_new_partition_bound(char *relname,
 				 * First check if the resulting range would be empty with
 				 * specified lower and upper bounds
 				 */
-				if (partition_rbound_cmp(key, lower->datums, lower->content, true,
+				if (partition_rbound_cmp(key, lower->datums, lower->kind, true,
 										 upper) >= 0)
 					ereport(ERROR,
 							(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -1432,12 +1416,12 @@ get_range_key_properties(PartitionKey ke
 	}
 
 	/* Get appropriate Const nodes for the bounds */
-	if (!ldatum->infinite)
+	if (ldatum->kind == PARTITION_RANGE_DATUM_VALUE)
 		*lower_val = castNode(Const, copyObject(ldatum->value));
 	else
 		*lower_val = NULL;
 
-	if (!udatum->infinite)
+	if (udatum->kind == PARTITION_RANGE_DATUM_VALUE)
 		*upper_val = castNode(Const, copyObject(udatum->value));
 	else
 		*upper_val = NULL;
@@ -1471,8 +1455,10 @@ get_range_key_properties(PartitionKey ke
  *		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 a bound datum is either MINVALUE or MAXVALUE, these expressions are
+ * simplified using the fact that any value is greater than MINVALUE and less
+ * than MAXVALUE. So, for example, if cu = MAXVALUE, c < cu is automatically
+ * true, and we need not emit any expression for it, and the last line becomes
  *
  *	(b < bu) OR (b = bu), which is simplified to (b <= bu)
  *
@@ -1668,12 +1654,15 @@ get_qual_for_range(PartitionKey key, Par
 
 				/*
 				 * 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 column of this arm, use GT, unless this is the
+				 * last column of the whole bound check, or the next bound
+				 * datum is MINVALUE, in which case use GE.
 				 */
 				if (j - i < current_or_arm)
 					strategy = BTEqualStrategyNumber;
-				else if ((ldatum_next && ldatum_next->infinite) ||
-						 j == key->partnatts - 1)
+				else if (j == key->partnatts - 1 ||
+						 (ldatum_next &&
+						  ldatum_next->kind == PARTITION_RANGE_DATUM_MINVALUE))
 					strategy = BTGreaterEqualStrategyNumber;
 				else
 					strategy = BTGreaterStrategyNumber;
@@ -1691,11 +1680,13 @@ get_qual_for_range(PartitionKey key, Par
 
 				/*
 				 * For the non-last columns of this arm, use the EQ operator.
-				 * For the last finite-valued column, use LE.
+				 * For the last column of this arm, use LT, unless the next
+				 * bound datum is MAXVALUE, in which case use LE.
 				 */
 				if (j - i < current_or_arm)
 					strategy = BTEqualStrategyNumber;
-				else if (udatum_next && udatum_next->infinite)
+				else if (udatum_next &&
+						 udatum_next->kind == PARTITION_RANGE_DATUM_MAXVALUE)
 					strategy = BTLessEqualStrategyNumber;
 				else
 					strategy = BTLessStrategyNumber;
@@ -1716,11 +1707,15 @@ get_qual_for_range(PartitionKey key, Par
 			if (j - i > current_or_arm)
 			{
 				/*
-				 * We need not emit the next arm if the new column that will
-				 * be considered is unbounded.
+				 * We must not emit any more arms if the new column that will
+				 * be considered is unbounded, or this one was.
 				 */
-				need_next_lower_arm = ldatum_next && !ldatum_next->infinite;
-				need_next_upper_arm = udatum_next && !udatum_next->infinite;
+				if (!lower_val || !ldatum_next ||
+					ldatum_next->kind != PARTITION_RANGE_DATUM_VALUE)
+					need_next_lower_arm = false;
+				if (!upper_val || !udatum_next ||
+					udatum_next->kind != PARTITION_RANGE_DATUM_VALUE)
+					need_next_upper_arm = false;
 				break;
 			}
 		}
@@ -2092,8 +2087,8 @@ make_one_range_bound(PartitionKey key, i
 	bound = (PartitionRangeBound *) palloc0(sizeof(PartitionRangeBound));
 	bound->index = index;
 	bound->datums = (Datum *) palloc0(key->partnatts * sizeof(Datum));
-	bound->content = (RangeDatumContent *) palloc0(key->partnatts *
-												   sizeof(RangeDatumContent));
+	bound->kind = (PartitionRangeDatumKind *) palloc0(key->partnatts *
+													  sizeof(PartitionRangeDatumKind));
 	bound->lower = lower;
 
 	i = 0;
@@ -2102,12 +2097,9 @@ make_one_range_bound(PartitionKey key, i
 		PartitionRangeDatum *datum = castNode(PartitionRangeDatum, lfirst(lc));
 
 		/* What's contained in this range datum? */
-		bound->content[i] = !datum->infinite
-			? RANGE_DATUM_FINITE
-			: (lower ? RANGE_DATUM_NEG_INF
-			   : RANGE_DATUM_POS_INF);
+		bound->kind[i] = datum->kind;
 
-		if (bound->content[i] == RANGE_DATUM_FINITE)
+		if (datum->kind == PARTITION_RANGE_DATUM_VALUE)
 		{
 			Const	   *val = castNode(Const, datum->value);
 
@@ -2130,7 +2122,7 @@ qsort_partition_rbound_cmp(const void *a
 	PartitionRangeBound *b2 = (*(PartitionRangeBound *const *) b);
 	PartitionKey key = (PartitionKey) arg;
 
-	return partition_rbound_cmp(key, b1->datums, b1->content, b1->lower, b2);
+	return partition_rbound_cmp(key, b1->datums, b1->kind, b1->lower, b2);
 }
 
 /*
@@ -2148,13 +2140,13 @@ qsort_partition_rbound_cmp(const void *a
  */
 static int32
 partition_rbound_cmp(PartitionKey key,
-					 Datum *datums1, RangeDatumContent *content1, bool lower1,
-					 PartitionRangeBound *b2)
+					 Datum *datums1, PartitionRangeDatumKind *kind1,
+					 bool lower1, PartitionRangeBound *b2)
 {
 	int32		cmpval = 0;		/* placate compiler */
 	int			i;
 	Datum	   *datums2 = b2->datums;
-	RangeDatumContent *content2 = b2->content;
+	PartitionRangeDatumKind *kind2 = b2->kind;
 	bool		lower2 = b2->lower;
 
 	for (i = 0; i < key->partnatts; i++)
@@ -2162,28 +2154,16 @@ partition_rbound_cmp(PartitionKey key,
 		/*
 		 * First, handle cases where the column is unbounded, which should not
 		 * invoke the comparison procedure, and should not consider any later
-		 * columns.
+		 * columns. Note that the PartitionRangeDatumKind enum elements
+		 * compare the same way as the values they represent.
 		 */
-		if (content1[i] != RANGE_DATUM_FINITE ||
-			content2[i] != RANGE_DATUM_FINITE)
-		{
-			/*
-			 * If the bound values are equal, fall through and compare whether
-			 * they are upper or lower bounds.
-			 */
-			if (content1[i] == content2[i])
-				break;
-
-			/* Otherwise, one bound is definitely larger than the other */
-			if (content1[i] == RANGE_DATUM_NEG_INF)
-				return -1;
-			else if (content1[i] == RANGE_DATUM_POS_INF)
-				return 1;
-			else if (content2[i] == RANGE_DATUM_NEG_INF)
-				return 1;
-			else if (content2[i] == RANGE_DATUM_POS_INF)
-				return -1;
-		}
+		if (kind1[i] < kind2[i])
+			return -1;
+		else if (kind1[i] > kind2[i])
+			return 1;
+		else if (kind1[i] != PARTITION_RANGE_DATUM_VALUE)
+			/* No values after a MINVALUE or MAXVALUE should be considered */
+			break;
 
 		cmpval = DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[i],
 												 key->partcollation[i],
@@ -2208,12 +2188,12 @@ partition_rbound_cmp(PartitionKey key,
 /*
  * partition_rbound_datum_cmp
  *
- * Return whether range bound (specified in rb_datums, rb_content, and
- * rb_lower) <=, =, >= partition key of tuple (tuple_datums)
+ * Return whether range bound (specified in rb_datums, rb_kind, and rb_lower)
+ * is <, =, or > partition key of tuple (tuple_datums)
  */
 static int32
 partition_rbound_datum_cmp(PartitionKey key,
-						   Datum *rb_datums, RangeDatumContent *rb_content,
+						   Datum *rb_datums, PartitionRangeDatumKind *rb_kind,
 						   Datum *tuple_datums)
 {
 	int			i;
@@ -2221,8 +2201,10 @@ partition_rbound_datum_cmp(PartitionKey
 
 	for (i = 0; i < key->partnatts; i++)
 	{
-		if (rb_content[i] != RANGE_DATUM_FINITE)
-			return rb_content[i] == RANGE_DATUM_NEG_INF ? -1 : 1;
+		if (rb_kind[i] == PARTITION_RANGE_DATUM_MINVALUE)
+			return -1;
+		else if (rb_kind[i] == PARTITION_RANGE_DATUM_MAXVALUE)
+			return 1;
 
 		cmpval = DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[i],
 												 key->partcollation[i],
@@ -2238,7 +2220,7 @@ partition_rbound_datum_cmp(PartitionKey
 /*
  * partition_bound_cmp
  *
- * Return whether the bound at offset in boundinfo is <=, =, >= the argument
+ * Return whether the bound at offset in boundinfo is <, =, or > the argument
  * specified in *probe.
  */
 static int32
@@ -2259,7 +2241,7 @@ partition_bound_cmp(PartitionKey key, Pa
 
 		case PARTITION_STRATEGY_RANGE:
 			{
-				RangeDatumContent *content = boundinfo->content[offset];
+				PartitionRangeDatumKind *kind = boundinfo->kind[offset];
 
 				if (probe_is_bound)
 				{
@@ -2271,12 +2253,12 @@ partition_bound_cmp(PartitionKey key, Pa
 					bool		lower = boundinfo->indexes[offset] < 0;
 
 					cmpval = partition_rbound_cmp(key,
-												  bound_datums, content, lower,
+												  bound_datums, kind, lower,
 												  (PartitionRangeBound *) probe);
 				}
 				else
 					cmpval = partition_rbound_datum_cmp(key,
-														bound_datums, content,
+														bound_datums, kind,
 														(Datum *) probe);
 				break;
 			}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index 67ac814..45a04b0
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4458,7 +4458,7 @@ _copyPartitionRangeDatum(const Partition
 {
 	PartitionRangeDatum *newnode = makeNode(PartitionRangeDatum);
 
-	COPY_SCALAR_FIELD(infinite);
+	COPY_SCALAR_FIELD(kind);
 	COPY_NODE_FIELD(value);
 	COPY_LOCATION_FIELD(location);
 
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index 91d64b7..8d92c03
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2849,7 +2849,7 @@ _equalPartitionBoundSpec(const Partition
 static bool
 _equalPartitionRangeDatum(const PartitionRangeDatum *a, const PartitionRangeDatum *b)
 {
-	COMPARE_SCALAR_FIELD(infinite);
+	COMPARE_SCALAR_FIELD(kind);
 	COMPARE_NODE_FIELD(value);
 	COMPARE_LOCATION_FIELD(location);
 
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index b0abe9e..124be75
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -3573,7 +3573,7 @@ _outPartitionRangeDatum(StringInfo str,
 {
 	WRITE_NODE_TYPE("PARTITIONRANGEDATUM");
 
-	WRITE_BOOL_FIELD(infinite);
+	WRITE_ENUM_FIELD(kind, PartitionRangeDatumKind);
 	WRITE_NODE_FIELD(value);
 	WRITE_LOCATION_FIELD(location);
 }
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index 1380703..76fee41
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -2390,7 +2390,7 @@ _readPartitionRangeDatum(void)
 {
 	READ_LOCALS(PartitionRangeDatum);
 
-	READ_BOOL_FIELD(infinite);
+	READ_ENUM_FIELD(kind, PartitionRangeDatumKind);
 	READ_NODE_FIELD(value);
 	READ_LOCATION_FIELD(location);
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
new file mode 100644
index 0f3998f..4b1ce09
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2696,11 +2696,21 @@ range_datum_list:
 		;
 
 PartitionRangeDatum:
-			UNBOUNDED
+			MINVALUE
 				{
 					PartitionRangeDatum *n = makeNode(PartitionRangeDatum);
 
-					n->infinite = true;
+					n->kind = PARTITION_RANGE_DATUM_MINVALUE;
+					n->value = NULL;
+					n->location = @1;
+
+					$$ = (Node *) n;
+				}
+			| MAXVALUE
+				{
+					PartitionRangeDatum *n = makeNode(PartitionRangeDatum);
+
+					n->kind = PARTITION_RANGE_DATUM_MAXVALUE;
 					n->value = NULL;
 					n->location = @1;
 
@@ -2710,7 +2720,7 @@ PartitionRangeDatum:
 				{
 					PartitionRangeDatum *n = makeNode(PartitionRangeDatum);
 
-					n->infinite = false;
+					n->kind = PARTITION_RANGE_DATUM_VALUE;
 					n->value = $1;
 					n->location = @1;
 
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
new file mode 100644
index ee5f3a3..9f37f1b
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3365,7 +3365,6 @@ transformPartitionBound(ParseState *psta
 				   *cell2;
 		int			i,
 					j;
-		bool		seen_unbounded;
 
 		if (spec->strategy != PARTITION_STRATEGY_RANGE)
 			ereport(ERROR,
@@ -3382,39 +3381,6 @@ transformPartitionBound(ParseState *psta
 					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 					 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.
-		 */
-		seen_unbounded = false;
-		foreach(cell1, spec->lowerdatums)
-		{
-			PartitionRangeDatum *ldatum = castNode(PartitionRangeDatum,
-												   lfirst(cell1));
-
-			if (ldatum->infinite)
-				seen_unbounded = true;
-			else if (seen_unbounded)
-				ereport(ERROR,
-						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("cannot specify finite value after UNBOUNDED"),
-						 parser_errposition(pstate, exprLocation((Node *) ldatum))));
-		}
-		seen_unbounded = false;
-		foreach(cell1, spec->upperdatums)
-		{
-			PartitionRangeDatum *rdatum = castNode(PartitionRangeDatum,
-												   lfirst(cell1));
-
-			if (rdatum->infinite)
-				seen_unbounded = true;
-			else if (seen_unbounded)
-				ereport(ERROR,
-						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("cannot specify finite value after UNBOUNDED"),
-						 parser_errposition(pstate, exprLocation((Node *) rdatum))));
-		}
-
 		/* Transform all the constants */
 		i = j = 0;
 		result_spec->lowerdatums = result_spec->upperdatums = NIL;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index 18d9e27..ba728b9
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -8680,8 +8680,10 @@ get_rule_expr(Node *node, deparse_contex
 							castNode(PartitionRangeDatum, lfirst(cell));
 
 							appendStringInfoString(buf, sep);
-							if (datum->infinite)
-								appendStringInfoString(buf, "UNBOUNDED");
+							if (datum->kind == PARTITION_RANGE_DATUM_MINVALUE)
+								appendStringInfoString(buf, "MINVALUE");
+							else if (datum->kind == PARTITION_RANGE_DATUM_MAXVALUE)
+								appendStringInfoString(buf, "MAXVALUE");
 							else
 							{
 								Const	   *val = castNode(Const, datum->value);
@@ -8698,8 +8700,10 @@ get_rule_expr(Node *node, deparse_contex
 							castNode(PartitionRangeDatum, lfirst(cell));
 
 							appendStringInfoString(buf, sep);
-							if (datum->infinite)
-								appendStringInfoString(buf, "UNBOUNDED");
+							if (datum->kind == PARTITION_RANGE_DATUM_MINVALUE)
+								appendStringInfoString(buf, "MINVALUE");
+							else if (datum->kind == PARTITION_RANGE_DATUM_MAXVALUE)
+								appendStringInfoString(buf, "MAXVALUE");
 							else
 							{
 								Const	   *val = castNode(Const, datum->value);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index 1d96169..f731163
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -809,16 +809,24 @@ typedef struct PartitionBoundSpec
 } PartitionBoundSpec;
 
 /*
- * PartitionRangeDatum - can be either a value or UNBOUNDED
+ * PartitionRangeDatum - one of the values in a range partition bound
  *
- * "value" is an A_Const in raw grammar output, a Const after analysis
+ * This can be MINVALUE, MAXVALUE or a specific bounded value.
  */
+typedef enum PartitionRangeDatumKind
+{
+	PARTITION_RANGE_DATUM_MINVALUE = -1,	/* less than any other value */
+	PARTITION_RANGE_DATUM_VALUE = 0,	/* specific (bounded) value */
+	PARTITION_RANGE_DATUM_MAXVALUE = 1	/* greater than any other value */
+} PartitionRangeDatumKind;
+
 typedef struct PartitionRangeDatum
 {
 	NodeTag		type;
 
-	bool		infinite;		/* true if UNBOUNDED */
-	Node	   *value;			/* null if UNBOUNDED */
+	PartitionRangeDatumKind kind;
+	Node	   *value;			/* Const (or A_Const in raw tree), if kind is
+								 * PARTITION_RANGE_DATUM_VALUE, else NULL */
 
 	int			location;		/* token location, or -1 if unknown */
 } PartitionRangeDatum;
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
new file mode 100644
index b6f794e..b301f22
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -512,15 +512,8 @@ ERROR:  FROM must specify exactly one va
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a') TO ('z', 1);
 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);
+CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (maxvalue);
 ERROR:  cannot specify NULL in range bound
--- 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...
-                                                             ^
-DROP TABLE range_parted_multicol;
 -- check if compatible with the specified parent
 -- cannot create as partition of a non-partitioned table
 CREATE TABLE unparted (
@@ -578,11 +571,11 @@ ERROR:  cannot create range partition wi
 -- note that the range '[1, 1)' has no elements
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (1);
 ERROR:  cannot create range partition with empty range
-CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (1);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (2);
+CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (minvalue) TO (1);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (minvalue) TO (2);
 ERROR:  partition "fail_part" would overlap partition "part0"
 CREATE TABLE part1 PARTITION OF range_parted2 FOR VALUES FROM (1) TO (10);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (maxvalue);
 ERROR:  partition "fail_part" would overlap partition "part1"
 CREATE TABLE part2 PARTITION OF range_parted2 FOR VALUES FROM (20) TO (30);
 CREATE TABLE part3 PARTITION OF range_parted2 FOR VALUES FROM (30) TO (40);
@@ -595,18 +588,18 @@ CREATE TABLE range_parted3 (
 	a int,
 	b int
 ) PARTITION BY RANGE (a, (b+1));
-CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, unbounded);
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, 1);
+CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, minvalue) TO (0, maxvalue);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, minvalue) TO (0, 1);
 ERROR:  partition "fail_part" would overlap partition "part00"
-CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, 1);
+CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, minvalue) TO (1, 1);
 CREATE TABLE part11 PARTITION OF range_parted3 FOR VALUES FROM (1, 1) TO (1, 10);
-CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, unbounded);
+CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, maxvalue);
 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
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, minvalue) TO (1, maxvalue);
 ERROR:  partition "fail_part" would overlap partition "part10"
 -- check schema propagation from parent
 CREATE TABLE parted (
@@ -708,7 +701,7 @@ Number of partitions: 3 (Use \d+ to list
 
 -- 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 (MINVALUE, 0, 0) TO (MAXVALUE, 0, 0);
 \d+ unbounded_range_part
                            Table "public.unbounded_range_part"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -716,11 +709,11 @@ CREATE TABLE unbounded_range_part PARTIT
  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 (MINVALUE, 0, 0) TO (MAXVALUE, 0, 0)
 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 (MINVALUE, 0, 0) TO (1, MAXVALUE, 0);
 \d+ range_parted4_1
                               Table "public.range_parted4_1"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -728,10 +721,10 @@ CREATE TABLE range_parted4_1 PARTITION O
  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 (MINVALUE, 0, 0) TO (1, MAXVALUE, 0)
 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, MAXVALUE);
 \d+ range_parted4_2
                               Table "public.range_parted4_2"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -739,10 +732,10 @@ CREATE TABLE range_parted4_2 PARTITION O
  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, MAXVALUE)
 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, MINVALUE) TO (9, MAXVALUE, 0);
 \d+ range_parted4_3
                               Table "public.range_parted4_3"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -750,7 +743,7 @@ CREATE TABLE range_parted4_3 PARTITION O
  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, MINVALUE) TO (9, MAXVALUE, 0)
 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;
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
new file mode 100644
index 35d182d..1fa9650
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1718,7 +1718,7 @@ create table part_10_20_cd partition of
 create table part_21_30 partition of range_list_parted for values from (21) to (30) partition by list (b);
 create table part_21_30_ab partition of part_21_30 for values in ('ab');
 create table part_21_30_cd partition of part_21_30 for values in ('cd');
-create table part_40_inf partition of range_list_parted for values from (40) to (unbounded) partition by list (b);
+create table part_40_inf partition of range_list_parted for values from (40) to (maxvalue) partition by list (b);
 create table part_40_inf_ab partition of part_40_inf for values in ('ab');
 create table part_40_inf_cd partition of part_40_inf for values in ('cd');
 create table part_40_inf_null partition of part_40_inf for values in (null);
@@ -1831,12 +1831,12 @@ drop table range_list_parted;
 -- check that constraint exclusion is able to cope with the partition
 -- constraint emitted for multi-column range partitioned tables
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, 1, 1);
+create table mcrparted0 partition of mcrparted for values from (minvalue, 0, 0) to (1, 1, 1);
 create table mcrparted1 partition of mcrparted for values from (1, 1, 1) to (10, 5, 10);
 create table mcrparted2 partition of mcrparted for values from (10, 5, 10) to (10, 10, 10);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
 create table mcrparted4 partition of mcrparted for values from (20, 10, 10) to (20, 20, 20);
-create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (maxvalue, 0, 0);
 explain (costs off) select * from mcrparted where a = 0;	-- scans mcrparted0
           QUERY PLAN          
 ------------------------------
diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out
new file mode 100644
index d1153f4..a2a471c
--- a/src/test/regress/expected/insert.out
+++ b/src/test/regress/expected/insert.out
@@ -288,7 +288,7 @@ select tableoid::regclass, * from list_p
 
 -- some more tests to exercise tuple-routing with multi-level partitioning
 create table part_gg partition of list_parted for values in ('gg') partition by range (b);
-create table part_gg1 partition of part_gg for values from (unbounded) to (1);
+create table part_gg1 partition of part_gg for values from (minvalue) to (1);
 create table part_gg2 partition of part_gg for values from (1) to (10) partition by range (b);
 create table part_gg2_1 partition of part_gg2 for values from (1) to (5);
 create table part_gg2_2 partition of part_gg2 for values from (5) to (10);
@@ -439,12 +439,12 @@ drop table key_desc, key_desc_1;
 -- check multi-column range partitioning expression enforces the same
 -- constraint as what tuple-routing would determine it to be
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, unbounded, unbounded);
-create table mcrparted1 partition of mcrparted for values from (2, 1, unbounded) to (10, 5, 10);
-create table mcrparted2 partition of mcrparted for values from (10, 6, unbounded) to (10, unbounded, unbounded);
+create table mcrparted0 partition of mcrparted for values from (minvalue, 0, 0) to (1, maxvalue, 0);
+create table mcrparted1 partition of mcrparted for values from (2, 1, minvalue) to (10, 5, 10);
+create table mcrparted2 partition of mcrparted for values from (10, 6, minvalue) to (10, maxvalue, 0);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
-create table mcrparted4 partition of mcrparted for values from (21, unbounded, unbounded) to (30, 20, unbounded);
-create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted4 partition of mcrparted for values from (21, minvalue, 0) to (30, 20, maxvalue);
+create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (maxvalue, 0, 0);
 -- routed to mcrparted0
 insert into mcrparted values (0, 1, 1);
 insert into mcrparted0 values (0, 1, 1);
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
new file mode 100644
index cb7aa5b..1c0ce92
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -483,12 +483,7 @@ CREATE TABLE fail_part PARTITION OF rang
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a') TO ('z', 1);
 
 -- 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
-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);
-DROP TABLE range_parted_multicol;
+CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (maxvalue);
 
 -- check if compatible with the specified parent
 
@@ -542,10 +537,10 @@ CREATE TABLE fail_part PARTITION OF rang
 -- note that the range '[1, 1)' has no elements
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (1);
 
-CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (1);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (2);
+CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (minvalue) TO (1);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (minvalue) TO (2);
 CREATE TABLE part1 PARTITION OF range_parted2 FOR VALUES FROM (1) TO (10);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (maxvalue);
 CREATE TABLE part2 PARTITION OF range_parted2 FOR VALUES FROM (20) TO (30);
 CREATE TABLE part3 PARTITION OF range_parted2 FOR VALUES FROM (30) TO (40);
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (30);
@@ -557,18 +552,18 @@ CREATE TABLE range_parted3 (
 	b int
 ) PARTITION BY RANGE (a, (b+1));
 
-CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, unbounded);
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, 1);
+CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, minvalue) TO (0, maxvalue);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, minvalue) TO (0, 1);
 
-CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, 1);
+CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, minvalue) TO (1, 1);
 CREATE TABLE part11 PARTITION OF range_parted3 FOR VALUES FROM (1, 1) TO (1, 10);
-CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, unbounded);
+CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, maxvalue);
 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
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, minvalue) TO (1, maxvalue);
 
 -- check schema propagation from parent
 
@@ -626,14 +621,14 @@ CREATE TABLE part_c_1_10 PARTITION OF pa
 
 -- 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 (MINVALUE, 0, 0) TO (MAXVALUE, 0, 0);
 \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 (MINVALUE, 0, 0) TO (1, MAXVALUE, 0);
 \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, MAXVALUE);
 \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, MINVALUE) TO (9, MAXVALUE, 0);
 \d+ range_parted4_3
 DROP TABLE range_parted4;
 
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
new file mode 100644
index 70fe971..c96580c
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -623,7 +623,7 @@ create table part_10_20_cd partition of
 create table part_21_30 partition of range_list_parted for values from (21) to (30) partition by list (b);
 create table part_21_30_ab partition of part_21_30 for values in ('ab');
 create table part_21_30_cd partition of part_21_30 for values in ('cd');
-create table part_40_inf partition of range_list_parted for values from (40) to (unbounded) partition by list (b);
+create table part_40_inf partition of range_list_parted for values from (40) to (maxvalue) partition by list (b);
 create table part_40_inf_ab partition of part_40_inf for values in ('ab');
 create table part_40_inf_cd partition of part_40_inf for values in ('cd');
 create table part_40_inf_null partition of part_40_inf for values in (null);
@@ -647,12 +647,12 @@ drop table range_list_parted;
 -- check that constraint exclusion is able to cope with the partition
 -- constraint emitted for multi-column range partitioned tables
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, 1, 1);
+create table mcrparted0 partition of mcrparted for values from (minvalue, 0, 0) to (1, 1, 1);
 create table mcrparted1 partition of mcrparted for values from (1, 1, 1) to (10, 5, 10);
 create table mcrparted2 partition of mcrparted for values from (10, 5, 10) to (10, 10, 10);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
 create table mcrparted4 partition of mcrparted for values from (20, 10, 10) to (20, 20, 20);
-create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (maxvalue, 0, 0);
 explain (costs off) select * from mcrparted where a = 0;	-- scans mcrparted0
 explain (costs off) select * from mcrparted where a = 10 and abs(b) < 5;	-- scans mcrparted1
 explain (costs off) select * from mcrparted where a = 10 and abs(b) = 5;	-- scans mcrparted1, mcrparted2
diff --git a/src/test/regress/sql/insert.sql b/src/test/regress/sql/insert.sql
new file mode 100644
index 83c3ad8..28f0c24
--- a/src/test/regress/sql/insert.sql
+++ b/src/test/regress/sql/insert.sql
@@ -169,7 +169,7 @@ select tableoid::regclass, * from list_p
 
 -- some more tests to exercise tuple-routing with multi-level partitioning
 create table part_gg partition of list_parted for values in ('gg') partition by range (b);
-create table part_gg1 partition of part_gg for values from (unbounded) to (1);
+create table part_gg1 partition of part_gg for values from (minvalue) to (1);
 create table part_gg2 partition of part_gg for values from (1) to (10) partition by range (b);
 create table part_gg2_1 partition of part_gg2 for values from (1) to (5);
 create table part_gg2_2 partition of part_gg2 for values from (5) to (10);
@@ -293,12 +293,12 @@ drop table key_desc, key_desc_1;
 -- check multi-column range partitioning expression enforces the same
 -- constraint as what tuple-routing would determine it to be
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, unbounded, unbounded);
-create table mcrparted1 partition of mcrparted for values from (2, 1, unbounded) to (10, 5, 10);
-create table mcrparted2 partition of mcrparted for values from (10, 6, unbounded) to (10, unbounded, unbounded);
+create table mcrparted0 partition of mcrparted for values from (minvalue, 0, 0) to (1, maxvalue, 0);
+create table mcrparted1 partition of mcrparted for values from (2, 1, minvalue) to (10, 5, 10);
+create table mcrparted2 partition of mcrparted for values from (10, 6, minvalue) to (10, maxvalue, 0);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
-create table mcrparted4 partition of mcrparted for values from (21, unbounded, unbounded) to (30, 20, unbounded);
-create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted4 partition of mcrparted for values from (21, minvalue, 0) to (30, 20, maxvalue);
+create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (maxvalue, 0, 0);
 
 -- routed to mcrparted0
 insert into mcrparted values (0, 1, 1);
#24Tom Lane
tgl@sss.pgh.pa.us
In reply to: Dean Rasheed (#23)
Re: Multi column range partition table

Dean Rasheed <dean.a.rasheed@gmail.com> writes:

However, this is also an incompatible syntax change, and any attempt
to support both the old and new syntaxes is likely to be messy, so we
really need to get consensus on whether this is the right thing to do,
and whether it *can* be done now for PG10.

FWIW, I'd much rather see us get it right the first time than release
PG10 with a syntax that we'll regret later. I do not think that beta2,
or even beta3, is too late for such a change.

I'm not taking a position on whether this proposal is actually better
than what we have. But if there's a consensus that it is, we should
go ahead and do it, not worry that it's too late.

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#25Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Tom Lane (#24)
Re: Multi column range partition table

On 6 July 2017 at 21:04, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Dean Rasheed <dean.a.rasheed@gmail.com> writes:

However, this is also an incompatible syntax change, and any attempt
to support both the old and new syntaxes is likely to be messy, so we
really need to get consensus on whether this is the right thing to do,
and whether it *can* be done now for PG10.

FWIW, I'd much rather see us get it right the first time than release
PG10 with a syntax that we'll regret later. I do not think that beta2,
or even beta3, is too late for such a change.

I'm not taking a position on whether this proposal is actually better
than what we have. But if there's a consensus that it is, we should
go ahead and do it, not worry that it's too late.

OK, thanks. That's good to know.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#26Joe Conway
mail@joeconway.com
In reply to: Dean Rasheed (#25)
Re: Multi column range partition table

On 07/06/2017 01:24 PM, Dean Rasheed wrote:

On 6 July 2017 at 21:04, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Dean Rasheed <dean.a.rasheed@gmail.com> writes:

However, this is also an incompatible syntax change, and any attempt
to support both the old and new syntaxes is likely to be messy, so we
really need to get consensus on whether this is the right thing to do,
and whether it *can* be done now for PG10.

FWIW, I'd much rather see us get it right the first time than release
PG10 with a syntax that we'll regret later. I do not think that beta2,
or even beta3, is too late for such a change.

I'm not taking a position on whether this proposal is actually better
than what we have. But if there's a consensus that it is, we should
go ahead and do it, not worry that it's too late.

OK, thanks. That's good to know.

I agree we should get this right the first time and I also agree with
Dean's proposal, so I guess I'm a +2

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

#27Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Dean Rasheed (#22)
Re: Multi column range partition table

On 2017/07/06 18:30, Dean Rasheed wrote:

On 5 July 2017 at 10:43, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

0001 is your patch to tidy up check_new_partition_bound() (must be
applied before 0002)

I pushed this first patch, simplifying check_new_partition_bound() for
range partitions, since it seemed like a good simplification, but note
that I don't think that was actually the cause of the latent bug you
saw upthread.

I like how simple check_new_partition_bound() has now become.

I think the real issue was in partition_rbound_cmp() -- normally, if
the upper bound of one partition coincides with the lower bound of
another, that function would report the upper bound as the smaller
one, but that logic breaks if any of the bound values are infinite,
since then it will exit early, returning 0, without ever comparing the
"lower" flags on the bounds.

I'm tempted to push a fix for that independently, since it's a bug
waiting to happen, even though it's not possible to hit it currently.

Oops, you're right. Thanks for the fix.

Regards,
Amit

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#28Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Dean Rasheed (#23)
1 attachment(s)
Re: Multi column range partition table

On 2017/07/07 4:55, Dean Rasheed wrote:

On 5 July 2017 at 18:07, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

So if we were to go for maximum flexibility and compatibility with
Oracle, then perhaps what we would do is more like the original idea
of UNBOUNDED ABOVE/BELOW, except call them MINVALUE and MAXVALUE,
which conveniently are already unreserved keywords, as well as being
much shorter. Plus, we would also relax the constraint about having
finite values after MINVALUE/MAXVALUE.

So I know that I have flip-flopped a few times on this now, but I'm
now starting to think that this approach, replacing UNBOUNDED with
MINVALUE and MAXVALUE is the best way to go, along with permitting
finite values after MINVALUE/MAXVALUE.

Sure.

This gives the greatest flexibility, it's not too verbose, and it
makes it easy to define contiguous sets of partitions just by making
the lower bound of one match the upper bound of another.

With this approach, any partition bounds that Oracle allows are also
valid in PostgreSQL, not that I would normally give too much weight to
that, but it is I think quite a nice syntax. Of course, we also
support things that Oracle doesn't allow, such as MINVALUE and gaps
between partitions.

Agreed. MINVALUE/MAXVALUE seems like a good way forward.

Parts of the patch are similar to your UNBOUNDED ABOVE/BELOW patch,
but there are a number of differences -- most notably, I replaced the
"infinite" boolean flag on PartitionRangeDatum with a 3-value enum and
did away with all the DefElem nodes and the associated special string
constants being copied and compared.

That's better.

However, this is also an incompatible syntax change, and any attempt
to support both the old and new syntaxes is likely to be messy, so we
really need to get consensus on whether this is the right thing to do,
and whether it *can* be done now for PG10.

+1 to releasing this syntax in PG 10.

The patch looks generally good, although I found and fixed some minor
issues (typos and such). Please find attached the updated patch.

Thanks,
Amit

Attachments:

Replace_UNBOUNDED_with_MINVALUE_and_MAXVALUE-v2.patchtext/plain; charset=UTF-8; name=Replace_UNBOUNDED_with_MINVALUE_and_MAXVALUE-v2.patchDownload
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index b15c19d3d0..06bea2a18a 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
 <phrase>and <replaceable class="PARAMETER">partition_bound_spec</replaceable> is:</phrase>
 
 IN ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | NULL } [, ...] ) |
-FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | UNBOUNDED } [, ...] )
-  TO ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | UNBOUNDED } [, ...] )
+FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | MINVALUE | MAXVALUE } [, ...] )
+  TO ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replaceable class="PARAMETER">string_literal</replaceable> | MINVALUE | MAXVALUE } [, ...] )
 
 <phrase><replaceable class="PARAMETER">index_parameters</replaceable> in <literal>UNIQUE</literal>, <literal>PRIMARY KEY</literal>, and <literal>EXCLUDE</literal> constraints are:</phrase>
 
@@ -269,10 +269,10 @@ FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replace
      <para>
       Each of the values specified in
       the <replaceable class="PARAMETER">partition_bound_spec</> is
-      a literal, <literal>NULL</literal>, or <literal>UNBOUNDED</literal>.
-      Each literal value must be either a numeric constant that is coercible
-      to the corresponding partition key column's type, or a string literal
-      that is valid input for that type.
+      a literal, <literal>NULL</literal>, <literal>MINVALUE</literal>, or
+      <literal>MAXVALUE</literal>.  Each literal value must be either a
+      numeric constant that is coercible to the corresponding partition key
+      column's type, or a string literal that is valid input for that type.
      </para>
 
      <para>
@@ -300,13 +300,47 @@ FROM ( { <replaceable class="PARAMETER">numeric_literal</replaceable> | <replace
      </para>
 
      <para>
-      Writing <literal>UNBOUNDED</literal> in <literal>FROM</literal>
-      signifies <literal>-infinity</literal> as the lower bound of the
-      corresponding column, whereas when written in <literal>TO</literal>,
-      it signifies <literal>+infinity</literal> as the upper bound.
-      All items following an <literal>UNBOUNDED</literal> item within
-      a <literal>FROM</literal> or <literal>TO</literal> list must also
-      be <literal>UNBOUNDED</literal>.
+      The special values <literal>MINVALUE</> and <literal>MAXVALUE</>
+      may be use when creating a range partition to indicate that there
+      is no lower or upper bound on the column's value. For example, a
+      partition defined using <literal>FROM (MINVALUE) TO (10)</> allows
+      any values less than 10, and a partition defined using
+      <literal>FROM (10) TO (MAXVALUE)</> allows any values greater than
+      or equal to 10.
+     </para>
+
+     <para>
+      When creating a range partition involving more than one column, it
+      can also make sense to use <literal>MAXVALUE</> as part of the lower
+      bound, and <literal>MINVALUE</> as part of the upper bound. For
+      example, a partition defined using
+      <literal>FROM (0, MAXVALUE) TO (10, MAXVALUE)</> allows any rows
+      where the first partitioned column is greater than 0 and less than
+      or equal to 10. Similarly, a partition defined using
+      <literal>FROM ('a', MINVALUE) TO ('b', MINVALUE)</>
+      allows only rows where the first partitioned column starts with "a".
+     </para>
+
+     <para>
+      Note that any values after <literal>MINVALUE</> or
+      <literal>MAXVALUE</> in a partition bound are ignored; so the bound
+      <literal>(10, MINVALUE, 0)</> is equivalent to
+      <literal>(10, MINVALUE, 10)</> and <literal>(10, MINVALUE, MINVALUE)</>
+      and <literal>(10, MINVALUE, MAXVALUE)</>.
+     </para>
+
+     <para>
+      Also note that some element types, such as <literal>timestamp</>,
+      have a notion of "infinity", which is just another value that can
+      be stored. This is different from <literal>MINVALUE</> and
+      <literal>MAXVALUE</>, which are not real values that can be stored,
+      but rather they are ways of saying the value is unbounded.
+      <literal>MAXVALUE</> can be thought of as being greater than any
+      other value, including "infinity" and <literal>MINVALUE</> as being
+      less than any other value, including "minus infinity". Thus the range
+      <literal>FROM ('infinity') TO (MAXVALUE)</> is not an empty range; it
+      allows precisely one value to be stored &mdash; the timestamp
+      "infinity".
      </para>
 
      <para>
diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c
index 43b8924261..e6388032c5 100644
--- a/src/backend/catalog/partition.c
+++ b/src/backend/catalog/partition.c
@@ -67,23 +67,14 @@
  * is an upper bound.
  */
 
-/* Ternary value to represent what's contained in a range bound datum */
-typedef enum RangeDatumContent
-{
-	RANGE_DATUM_FINITE = 0,		/* actual datum stored elsewhere */
-	RANGE_DATUM_NEG_INF,		/* negative infinity */
-	RANGE_DATUM_POS_INF			/* positive infinity */
-} RangeDatumContent;
-
 typedef struct PartitionBoundInfoData
 {
 	char		strategy;		/* list or range bounds? */
 	int			ndatums;		/* Length of the datums following array */
 	Datum	  **datums;			/* Array of datum-tuples with key->partnatts
 								 * datums each */
-	RangeDatumContent **content;	/* what's contained in each range bound
-									 * datum? (see the above enum); NULL for
-									 * list partitioned tables */
+	PartitionRangeDatumKind **kind;	/* The kind of each range bound datum;
+									 * NULL for list partitioned tables */
 	int		   *indexes;		/* Partition indexes; one entry per member of
 								 * the datums array (plus one if range
 								 * partitioned table) */
@@ -110,7 +101,7 @@ typedef struct PartitionRangeBound
 {
 	int			index;
 	Datum	   *datums;			/* range bound datums */
-	RangeDatumContent *content; /* what's contained in each datum? */
+	PartitionRangeDatumKind *kind; /* the kind of each datum */
 	bool		lower;			/* this is the lower (vs upper) bound */
 } PartitionRangeBound;
 
@@ -136,10 +127,10 @@ static List *generate_partition_qual(Relation rel);
 static PartitionRangeBound *make_one_range_bound(PartitionKey key, int index,
 					 List *datums, bool lower);
 static int32 partition_rbound_cmp(PartitionKey key,
-					 Datum *datums1, RangeDatumContent *content1, bool lower1,
-					 PartitionRangeBound *b2);
+					 Datum *datums1, PartitionRangeDatumKind *kind1,
+					 bool lower1, PartitionRangeBound *b2);
 static int32 partition_rbound_datum_cmp(PartitionKey key,
-						   Datum *rb_datums, RangeDatumContent *rb_content,
+						   Datum *rb_datums, PartitionRangeDatumKind *rb_kind,
 						   Datum *tuple_datums);
 
 static int32 partition_bound_cmp(PartitionKey key,
@@ -366,29 +357,25 @@ RelationBuildPartitionDesc(Relation rel)
 				bool		is_distinct = false;
 				int			j;
 
-				/* Is current bound is distinct from the previous? */
+				/* Is current bound distinct from the previous? */
 				for (j = 0; j < key->partnatts; j++)
 				{
 					Datum		cmpval;
 
-					if (prev == NULL)
+					if (prev == NULL || cur->kind[j] != prev->kind[j])
 					{
 						is_distinct = true;
 						break;
 					}
 
 					/*
-					 * 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 the bounds are both MINVALUE or MAXVALUE, stop now
+					 * and treat them as equal, since any values after this
+					 * point must be ignored.
 					 */
-					if (cur->content[j] != RANGE_DATUM_FINITE ||
-						prev->content[j] != RANGE_DATUM_FINITE)
-					{
-						is_distinct = true;
+					if (cur->kind[j] != PARTITION_RANGE_DATUM_VALUE)
 						break;
-					}
+
 					cmpval = FunctionCall2Coll(&key->partsupfunc[j],
 											   key->partcollation[j],
 											   cur->datums[j],
@@ -513,8 +500,9 @@ RelationBuildPartitionDesc(Relation rel)
 
 			case PARTITION_STRATEGY_RANGE:
 				{
-					boundinfo->content = (RangeDatumContent **) palloc(ndatums *
-																	   sizeof(RangeDatumContent *));
+					boundinfo->kind = (PartitionRangeDatumKind **)
+							palloc(ndatums *
+								   sizeof(PartitionRangeDatumKind *));
 					boundinfo->indexes = (int *) palloc((ndatums + 1) *
 														sizeof(int));
 
@@ -524,18 +512,17 @@ RelationBuildPartitionDesc(Relation rel)
 
 						boundinfo->datums[i] = (Datum *) palloc(key->partnatts *
 																sizeof(Datum));
-						boundinfo->content[i] = (RangeDatumContent *)
+						boundinfo->kind[i] = (PartitionRangeDatumKind *)
 							palloc(key->partnatts *
-								   sizeof(RangeDatumContent));
+								   sizeof(PartitionRangeDatumKind));
 						for (j = 0; j < key->partnatts; j++)
 						{
-							if (rbounds[i]->content[j] == RANGE_DATUM_FINITE)
+							if (rbounds[i]->kind[j] == PARTITION_RANGE_DATUM_VALUE)
 								boundinfo->datums[i][j] =
 									datumCopy(rbounds[i]->datums[j],
 											  key->parttypbyval[j],
 											  key->parttyplen[j]);
-							/* Remember, we are storing the tri-state value. */
-							boundinfo->content[i][j] = rbounds[i]->content[j];
+							boundinfo->kind[i][j] = rbounds[i]->kind[j];
 						}
 
 						/*
@@ -617,17 +604,14 @@ partition_bounds_equal(PartitionKey key,
 		for (j = 0; j < key->partnatts; j++)
 		{
 			/* For range partitions, the bounds might not be finite. */
-			if (b1->content != NULL)
+			if (b1->kind != NULL)
 			{
-				/*
-				 * A finite bound always differs from an infinite bound, and
-				 * different kinds of infinities differ from each other.
-				 */
-				if (b1->content[i][j] != b2->content[i][j])
+				/* The different kinds of bound all differ from each other */
+				if (b1->kind[i][j] != b2->kind[i][j])
 					return false;
 
 				/* Non-finite bounds are equal without further examination. */
-				if (b1->content[i][j] != RANGE_DATUM_FINITE)
+				if (b1->kind[i][j] != PARTITION_RANGE_DATUM_VALUE)
 					continue;
 			}
 
@@ -736,7 +720,7 @@ check_new_partition_bound(char *relname, Relation parent,
 				 * First check if the resulting range would be empty with
 				 * specified lower and upper bounds
 				 */
-				if (partition_rbound_cmp(key, lower->datums, lower->content, true,
+				if (partition_rbound_cmp(key, lower->datums, lower->kind, true,
 										 upper) >= 0)
 					ereport(ERROR,
 							(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -1399,8 +1383,8 @@ 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.
- * All of these structures are freshly palloc'd.
+ * *upper_val).  For MINVALUE/MAXVALUE 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
  * the key->partexprs list, or NULL.  It may be advanced upon return.
@@ -1432,12 +1416,12 @@ get_range_key_properties(PartitionKey key, int keynum,
 	}
 
 	/* Get appropriate Const nodes for the bounds */
-	if (!ldatum->infinite)
+	if (ldatum->kind == PARTITION_RANGE_DATUM_VALUE)
 		*lower_val = castNode(Const, copyObject(ldatum->value));
 	else
 		*lower_val = NULL;
 
-	if (!udatum->infinite)
+	if (udatum->kind == PARTITION_RANGE_DATUM_VALUE)
 		*upper_val = castNode(Const, copyObject(udatum->value));
 	else
 		*upper_val = NULL;
@@ -1471,18 +1455,16 @@ 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 a bound datum is either MINVALUE or MAXVALUE, these expressions are
+ * simplified using the fact that any value is greater than MINVALUE and less
+ * than MAXVALUE. So, for example, if cu = MAXVALUE, c < cu is automatically
+ * true, and we need not emit any expression for it, and the last line becomes
  *
  *	(b < bu) OR (b = bu), which is simplified to (b <= bu)
  *
  * 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
  *
- * 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.
- *
  * 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.
  */
@@ -1668,12 +1650,15 @@ get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec)
 
 				/*
 				 * 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 column of this arm, use GT, unless this is the
+				 * last column of the whole bound check, or the next bound
+				 * datum is MINVALUE, in which case use GE.
 				 */
 				if (j - i < current_or_arm)
 					strategy = BTEqualStrategyNumber;
-				else if ((ldatum_next && ldatum_next->infinite) ||
-						 j == key->partnatts - 1)
+				else if (j == key->partnatts - 1 ||
+						 (ldatum_next &&
+						  ldatum_next->kind == PARTITION_RANGE_DATUM_MINVALUE))
 					strategy = BTGreaterEqualStrategyNumber;
 				else
 					strategy = BTGreaterStrategyNumber;
@@ -1691,11 +1676,13 @@ get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec)
 
 				/*
 				 * For the non-last columns of this arm, use the EQ operator.
-				 * For the last finite-valued column, use LE.
+				 * For the last column of this arm, use LT, unless the next
+				 * bound datum is MAXVALUE, in which case use LE.
 				 */
 				if (j - i < current_or_arm)
 					strategy = BTEqualStrategyNumber;
-				else if (udatum_next && udatum_next->infinite)
+				else if (udatum_next &&
+						 udatum_next->kind == PARTITION_RANGE_DATUM_MAXVALUE)
 					strategy = BTLessEqualStrategyNumber;
 				else
 					strategy = BTLessStrategyNumber;
@@ -1716,11 +1703,15 @@ get_qual_for_range(PartitionKey key, PartitionBoundSpec *spec)
 			if (j - i > current_or_arm)
 			{
 				/*
-				 * We need not emit the next arm if the new column that will
-				 * be considered is unbounded.
+				 * We must not emit any more arms if the new column that will
+				 * be considered is unbounded, or this one was.
 				 */
-				need_next_lower_arm = ldatum_next && !ldatum_next->infinite;
-				need_next_upper_arm = udatum_next && !udatum_next->infinite;
+				if (!lower_val || !ldatum_next ||
+					ldatum_next->kind != PARTITION_RANGE_DATUM_VALUE)
+					need_next_lower_arm = false;
+				if (!upper_val || !udatum_next ||
+					udatum_next->kind != PARTITION_RANGE_DATUM_VALUE)
+					need_next_upper_arm = false;
 				break;
 			}
 		}
@@ -2092,8 +2083,8 @@ make_one_range_bound(PartitionKey key, int index, List *datums, bool lower)
 	bound = (PartitionRangeBound *) palloc0(sizeof(PartitionRangeBound));
 	bound->index = index;
 	bound->datums = (Datum *) palloc0(key->partnatts * sizeof(Datum));
-	bound->content = (RangeDatumContent *) palloc0(key->partnatts *
-												   sizeof(RangeDatumContent));
+	bound->kind = (PartitionRangeDatumKind *) palloc0(key->partnatts *
+													  sizeof(PartitionRangeDatumKind));
 	bound->lower = lower;
 
 	i = 0;
@@ -2102,12 +2093,9 @@ make_one_range_bound(PartitionKey key, int index, List *datums, bool lower)
 		PartitionRangeDatum *datum = castNode(PartitionRangeDatum, lfirst(lc));
 
 		/* What's contained in this range datum? */
-		bound->content[i] = !datum->infinite
-			? RANGE_DATUM_FINITE
-			: (lower ? RANGE_DATUM_NEG_INF
-			   : RANGE_DATUM_POS_INF);
+		bound->kind[i] = datum->kind;
 
-		if (bound->content[i] == RANGE_DATUM_FINITE)
+		if (datum->kind == PARTITION_RANGE_DATUM_VALUE)
 		{
 			Const	   *val = castNode(Const, datum->value);
 
@@ -2130,7 +2118,7 @@ qsort_partition_rbound_cmp(const void *a, const void *b, void *arg)
 	PartitionRangeBound *b2 = (*(PartitionRangeBound *const *) b);
 	PartitionKey key = (PartitionKey) arg;
 
-	return partition_rbound_cmp(key, b1->datums, b1->content, b1->lower, b2);
+	return partition_rbound_cmp(key, b1->datums, b1->kind, b1->lower, b2);
 }
 
 /*
@@ -2148,13 +2136,13 @@ qsort_partition_rbound_cmp(const void *a, const void *b, void *arg)
  */
 static int32
 partition_rbound_cmp(PartitionKey key,
-					 Datum *datums1, RangeDatumContent *content1, bool lower1,
-					 PartitionRangeBound *b2)
+					 Datum *datums1, PartitionRangeDatumKind *kind1,
+					 bool lower1, PartitionRangeBound *b2)
 {
 	int32		cmpval = 0;		/* placate compiler */
 	int			i;
 	Datum	   *datums2 = b2->datums;
-	RangeDatumContent *content2 = b2->content;
+	PartitionRangeDatumKind *kind2 = b2->kind;
 	bool		lower2 = b2->lower;
 
 	for (i = 0; i < key->partnatts; i++)
@@ -2162,28 +2150,16 @@ partition_rbound_cmp(PartitionKey key,
 		/*
 		 * First, handle cases where the column is unbounded, which should not
 		 * invoke the comparison procedure, and should not consider any later
-		 * columns.
+		 * columns. Note that the PartitionRangeDatumKind enum elements
+		 * compare the same way as the values they represent.
 		 */
-		if (content1[i] != RANGE_DATUM_FINITE ||
-			content2[i] != RANGE_DATUM_FINITE)
-		{
-			/*
-			 * If the bound values are equal, fall through and compare whether
-			 * they are upper or lower bounds.
-			 */
-			if (content1[i] == content2[i])
-				break;
-
-			/* Otherwise, one bound is definitely larger than the other */
-			if (content1[i] == RANGE_DATUM_NEG_INF)
-				return -1;
-			else if (content1[i] == RANGE_DATUM_POS_INF)
-				return 1;
-			else if (content2[i] == RANGE_DATUM_NEG_INF)
-				return 1;
-			else if (content2[i] == RANGE_DATUM_POS_INF)
-				return -1;
-		}
+		if (kind1[i] < kind2[i])
+			return -1;
+		else if (kind1[i] > kind2[i])
+			return 1;
+		else if (kind1[i] != PARTITION_RANGE_DATUM_VALUE)
+			/* No values after a MINVALUE or MAXVALUE should be considered */
+			break;
 
 		cmpval = DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[i],
 												 key->partcollation[i],
@@ -2208,12 +2184,12 @@ partition_rbound_cmp(PartitionKey key,
 /*
  * partition_rbound_datum_cmp
  *
- * Return whether range bound (specified in rb_datums, rb_content, and
- * rb_lower) <=, =, >= partition key of tuple (tuple_datums)
+ * Return whether range bound (specified in rb_datums, rb_kind, and rb_lower)
+ * is <, =, or > partition key of tuple (tuple_datums)
  */
 static int32
 partition_rbound_datum_cmp(PartitionKey key,
-						   Datum *rb_datums, RangeDatumContent *rb_content,
+						   Datum *rb_datums, PartitionRangeDatumKind *rb_kind,
 						   Datum *tuple_datums)
 {
 	int			i;
@@ -2221,8 +2197,10 @@ partition_rbound_datum_cmp(PartitionKey key,
 
 	for (i = 0; i < key->partnatts; i++)
 	{
-		if (rb_content[i] != RANGE_DATUM_FINITE)
-			return rb_content[i] == RANGE_DATUM_NEG_INF ? -1 : 1;
+		if (rb_kind[i] == PARTITION_RANGE_DATUM_MINVALUE)
+			return -1;
+		else if (rb_kind[i] == PARTITION_RANGE_DATUM_MAXVALUE)
+			return 1;
 
 		cmpval = DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[i],
 												 key->partcollation[i],
@@ -2238,7 +2216,7 @@ partition_rbound_datum_cmp(PartitionKey key,
 /*
  * partition_bound_cmp
  *
- * Return whether the bound at offset in boundinfo is <=, =, >= the argument
+ * Return whether the bound at offset in boundinfo is <, =, or > the argument
  * specified in *probe.
  */
 static int32
@@ -2259,7 +2237,7 @@ partition_bound_cmp(PartitionKey key, PartitionBoundInfo boundinfo,
 
 		case PARTITION_STRATEGY_RANGE:
 			{
-				RangeDatumContent *content = boundinfo->content[offset];
+				PartitionRangeDatumKind *kind = boundinfo->kind[offset];
 
 				if (probe_is_bound)
 				{
@@ -2271,12 +2249,12 @@ partition_bound_cmp(PartitionKey key, PartitionBoundInfo boundinfo,
 					bool		lower = boundinfo->indexes[offset] < 0;
 
 					cmpval = partition_rbound_cmp(key,
-												  bound_datums, content, lower,
+												  bound_datums, kind, lower,
 												  (PartitionRangeBound *) probe);
 				}
 				else
 					cmpval = partition_rbound_datum_cmp(key,
-														bound_datums, content,
+														bound_datums, kind,
 														(Datum *) probe);
 				break;
 			}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 67ac8145a0..45a04b0b27 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4458,7 +4458,7 @@ _copyPartitionRangeDatum(const PartitionRangeDatum *from)
 {
 	PartitionRangeDatum *newnode = makeNode(PartitionRangeDatum);
 
-	COPY_SCALAR_FIELD(infinite);
+	COPY_SCALAR_FIELD(kind);
 	COPY_NODE_FIELD(value);
 	COPY_LOCATION_FIELD(location);
 
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 91d64b7331..8d92c03633 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2849,7 +2849,7 @@ _equalPartitionBoundSpec(const PartitionBoundSpec *a, const PartitionBoundSpec *
 static bool
 _equalPartitionRangeDatum(const PartitionRangeDatum *a, const PartitionRangeDatum *b)
 {
-	COMPARE_SCALAR_FIELD(infinite);
+	COMPARE_SCALAR_FIELD(kind);
 	COMPARE_NODE_FIELD(value);
 	COMPARE_LOCATION_FIELD(location);
 
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index b0abe9ec10..124be75acf 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -3573,7 +3573,7 @@ _outPartitionRangeDatum(StringInfo str, const PartitionRangeDatum *node)
 {
 	WRITE_NODE_TYPE("PARTITIONRANGEDATUM");
 
-	WRITE_BOOL_FIELD(infinite);
+	WRITE_ENUM_FIELD(kind, PartitionRangeDatumKind);
 	WRITE_NODE_FIELD(value);
 	WRITE_LOCATION_FIELD(location);
 }
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 1380703cbc..76fee4161c 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -2390,7 +2390,7 @@ _readPartitionRangeDatum(void)
 {
 	READ_LOCALS(PartitionRangeDatum);
 
-	READ_BOOL_FIELD(infinite);
+	READ_ENUM_FIELD(kind, PartitionRangeDatumKind);
 	READ_NODE_FIELD(value);
 	READ_LOCATION_FIELD(location);
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0f3998ff89..4b1ce09c44 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2696,11 +2696,21 @@ range_datum_list:
 		;
 
 PartitionRangeDatum:
-			UNBOUNDED
+			MINVALUE
 				{
 					PartitionRangeDatum *n = makeNode(PartitionRangeDatum);
 
-					n->infinite = true;
+					n->kind = PARTITION_RANGE_DATUM_MINVALUE;
+					n->value = NULL;
+					n->location = @1;
+
+					$$ = (Node *) n;
+				}
+			| MAXVALUE
+				{
+					PartitionRangeDatum *n = makeNode(PartitionRangeDatum);
+
+					n->kind = PARTITION_RANGE_DATUM_MAXVALUE;
 					n->value = NULL;
 					n->location = @1;
 
@@ -2710,7 +2720,7 @@ PartitionRangeDatum:
 				{
 					PartitionRangeDatum *n = makeNode(PartitionRangeDatum);
 
-					n->infinite = false;
+					n->kind = PARTITION_RANGE_DATUM_VALUE;
 					n->value = $1;
 					n->location = @1;
 
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index ee5f3a3a52..9f37f1b920 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3365,7 +3365,6 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 				   *cell2;
 		int			i,
 					j;
-		bool		seen_unbounded;
 
 		if (spec->strategy != PARTITION_STRATEGY_RANGE)
 			ereport(ERROR,
@@ -3382,39 +3381,6 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 					 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.
-		 */
-		seen_unbounded = false;
-		foreach(cell1, spec->lowerdatums)
-		{
-			PartitionRangeDatum *ldatum = castNode(PartitionRangeDatum,
-												   lfirst(cell1));
-
-			if (ldatum->infinite)
-				seen_unbounded = true;
-			else if (seen_unbounded)
-				ereport(ERROR,
-						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("cannot specify finite value after UNBOUNDED"),
-						 parser_errposition(pstate, exprLocation((Node *) ldatum))));
-		}
-		seen_unbounded = false;
-		foreach(cell1, spec->upperdatums)
-		{
-			PartitionRangeDatum *rdatum = castNode(PartitionRangeDatum,
-												   lfirst(cell1));
-
-			if (rdatum->infinite)
-				seen_unbounded = true;
-			else if (seen_unbounded)
-				ereport(ERROR,
-						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("cannot specify finite value after UNBOUNDED"),
-						 parser_errposition(pstate, exprLocation((Node *) rdatum))));
-		}
-
 		/* Transform all the constants */
 		i = j = 0;
 		result_spec->lowerdatums = result_spec->upperdatums = NIL;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 18d9e27d1e..ba728b98b0 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -8680,8 +8680,10 @@ get_rule_expr(Node *node, deparse_context *context,
 							castNode(PartitionRangeDatum, lfirst(cell));
 
 							appendStringInfoString(buf, sep);
-							if (datum->infinite)
-								appendStringInfoString(buf, "UNBOUNDED");
+							if (datum->kind == PARTITION_RANGE_DATUM_MINVALUE)
+								appendStringInfoString(buf, "MINVALUE");
+							else if (datum->kind == PARTITION_RANGE_DATUM_MAXVALUE)
+								appendStringInfoString(buf, "MAXVALUE");
 							else
 							{
 								Const	   *val = castNode(Const, datum->value);
@@ -8698,8 +8700,10 @@ get_rule_expr(Node *node, deparse_context *context,
 							castNode(PartitionRangeDatum, lfirst(cell));
 
 							appendStringInfoString(buf, sep);
-							if (datum->infinite)
-								appendStringInfoString(buf, "UNBOUNDED");
+							if (datum->kind == PARTITION_RANGE_DATUM_MINVALUE)
+								appendStringInfoString(buf, "MINVALUE");
+							else if (datum->kind == PARTITION_RANGE_DATUM_MAXVALUE)
+								appendStringInfoString(buf, "MAXVALUE");
 							else
 							{
 								Const	   *val = castNode(Const, datum->value);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 1d96169d34..f731163d0a 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -809,16 +809,24 @@ typedef struct PartitionBoundSpec
 } PartitionBoundSpec;
 
 /*
- * PartitionRangeDatum - can be either a value or UNBOUNDED
+ * PartitionRangeDatum - one of the values in a range partition bound
  *
- * "value" is an A_Const in raw grammar output, a Const after analysis
+ * This can be MINVALUE, MAXVALUE or a specific bounded value.
  */
+typedef enum PartitionRangeDatumKind
+{
+	PARTITION_RANGE_DATUM_MINVALUE = -1,	/* less than any other value */
+	PARTITION_RANGE_DATUM_VALUE = 0,	/* specific (bounded) value */
+	PARTITION_RANGE_DATUM_MAXVALUE = 1	/* greater than any other value */
+} PartitionRangeDatumKind;
+
 typedef struct PartitionRangeDatum
 {
 	NodeTag		type;
 
-	bool		infinite;		/* true if UNBOUNDED */
-	Node	   *value;			/* null if UNBOUNDED */
+	PartitionRangeDatumKind kind;
+	Node	   *value;			/* Const (or A_Const in raw tree), if kind is
+								 * PARTITION_RANGE_DATUM_VALUE, else NULL */
 
 	int			location;		/* token location, or -1 if unknown */
 } PartitionRangeDatum;
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index b6f794e1c2..b301f22ca5 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -512,15 +512,8 @@ ERROR:  FROM must specify exactly one value per partitioning column
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a') TO ('z', 1);
 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);
+CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (maxvalue);
 ERROR:  cannot specify NULL in range bound
--- 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...
-                                                             ^
-DROP TABLE range_parted_multicol;
 -- check if compatible with the specified parent
 -- cannot create as partition of a non-partitioned table
 CREATE TABLE unparted (
@@ -578,11 +571,11 @@ ERROR:  cannot create range partition with empty range
 -- note that the range '[1, 1)' has no elements
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (1);
 ERROR:  cannot create range partition with empty range
-CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (1);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (2);
+CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (minvalue) TO (1);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (minvalue) TO (2);
 ERROR:  partition "fail_part" would overlap partition "part0"
 CREATE TABLE part1 PARTITION OF range_parted2 FOR VALUES FROM (1) TO (10);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (maxvalue);
 ERROR:  partition "fail_part" would overlap partition "part1"
 CREATE TABLE part2 PARTITION OF range_parted2 FOR VALUES FROM (20) TO (30);
 CREATE TABLE part3 PARTITION OF range_parted2 FOR VALUES FROM (30) TO (40);
@@ -595,18 +588,18 @@ CREATE TABLE range_parted3 (
 	a int,
 	b int
 ) PARTITION BY RANGE (a, (b+1));
-CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, unbounded);
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, 1);
+CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, minvalue) TO (0, maxvalue);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, minvalue) TO (0, 1);
 ERROR:  partition "fail_part" would overlap partition "part00"
-CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, 1);
+CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, minvalue) TO (1, 1);
 CREATE TABLE part11 PARTITION OF range_parted3 FOR VALUES FROM (1, 1) TO (1, 10);
-CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, unbounded);
+CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, maxvalue);
 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
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, minvalue) TO (1, maxvalue);
 ERROR:  partition "fail_part" would overlap partition "part10"
 -- check schema propagation from parent
 CREATE TABLE parted (
@@ -708,7 +701,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 (MINVALUE, 0, 0) TO (MAXVALUE, 0, 0);
 \d+ unbounded_range_part
                            Table "public.unbounded_range_part"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -716,11 +709,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 (MINVALUE, 0, 0) TO (MAXVALUE, 0, 0)
 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 (MINVALUE, 0, 0) TO (1, MAXVALUE, 0);
 \d+ range_parted4_1
                               Table "public.range_parted4_1"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -728,10 +721,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 (MINVALUE, 0, 0) TO (1, MAXVALUE, 0)
 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, MAXVALUE);
 \d+ range_parted4_2
                               Table "public.range_parted4_2"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -739,10 +732,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, MAXVALUE)
 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, MINVALUE) TO (9, MAXVALUE, 0);
 \d+ range_parted4_3
                               Table "public.range_parted4_3"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -750,7 +743,7 @@ 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, MINVALUE) TO (9, MAXVALUE, 0)
 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;
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 35d182d599..1fa9650ec9 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1718,7 +1718,7 @@ create table part_10_20_cd partition of part_10_20 for values in ('cd');
 create table part_21_30 partition of range_list_parted for values from (21) to (30) partition by list (b);
 create table part_21_30_ab partition of part_21_30 for values in ('ab');
 create table part_21_30_cd partition of part_21_30 for values in ('cd');
-create table part_40_inf partition of range_list_parted for values from (40) to (unbounded) partition by list (b);
+create table part_40_inf partition of range_list_parted for values from (40) to (maxvalue) partition by list (b);
 create table part_40_inf_ab partition of part_40_inf for values in ('ab');
 create table part_40_inf_cd partition of part_40_inf for values in ('cd');
 create table part_40_inf_null partition of part_40_inf for values in (null);
@@ -1831,12 +1831,12 @@ drop table range_list_parted;
 -- check that constraint exclusion is able to cope with the partition
 -- constraint emitted for multi-column range partitioned tables
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, 1, 1);
+create table mcrparted0 partition of mcrparted for values from (minvalue, 0, 0) to (1, 1, 1);
 create table mcrparted1 partition of mcrparted for values from (1, 1, 1) to (10, 5, 10);
 create table mcrparted2 partition of mcrparted for values from (10, 5, 10) to (10, 10, 10);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
 create table mcrparted4 partition of mcrparted for values from (20, 10, 10) to (20, 20, 20);
-create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (maxvalue, 0, 0);
 explain (costs off) select * from mcrparted where a = 0;	-- scans mcrparted0
           QUERY PLAN          
 ------------------------------
diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out
index d1153f410b..a2a471c1f6 100644
--- a/src/test/regress/expected/insert.out
+++ b/src/test/regress/expected/insert.out
@@ -288,7 +288,7 @@ select tableoid::regclass, * from list_parted;
 
 -- some more tests to exercise tuple-routing with multi-level partitioning
 create table part_gg partition of list_parted for values in ('gg') partition by range (b);
-create table part_gg1 partition of part_gg for values from (unbounded) to (1);
+create table part_gg1 partition of part_gg for values from (minvalue) to (1);
 create table part_gg2 partition of part_gg for values from (1) to (10) partition by range (b);
 create table part_gg2_1 partition of part_gg2 for values from (1) to (5);
 create table part_gg2_2 partition of part_gg2 for values from (5) to (10);
@@ -439,12 +439,12 @@ drop table key_desc, key_desc_1;
 -- check multi-column range partitioning expression enforces the same
 -- constraint as what tuple-routing would determine it to be
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, unbounded, unbounded);
-create table mcrparted1 partition of mcrparted for values from (2, 1, unbounded) to (10, 5, 10);
-create table mcrparted2 partition of mcrparted for values from (10, 6, unbounded) to (10, unbounded, unbounded);
+create table mcrparted0 partition of mcrparted for values from (minvalue, 0, 0) to (1, maxvalue, 0);
+create table mcrparted1 partition of mcrparted for values from (2, 1, minvalue) to (10, 5, 10);
+create table mcrparted2 partition of mcrparted for values from (10, 6, minvalue) to (10, maxvalue, 0);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
-create table mcrparted4 partition of mcrparted for values from (21, unbounded, unbounded) to (30, 20, unbounded);
-create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted4 partition of mcrparted for values from (21, minvalue, 0) to (30, 20, maxvalue);
+create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (maxvalue, 0, 0);
 -- routed to mcrparted0
 insert into mcrparted values (0, 1, 1);
 insert into mcrparted0 values (0, 1, 1);
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index cb7aa5bbc6..1c0ce92763 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -483,12 +483,7 @@ CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a', 1) TO ('z
 CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM ('a') TO ('z', 1);
 
 -- 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
-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);
-DROP TABLE range_parted_multicol;
+CREATE TABLE fail_part PARTITION OF range_parted FOR VALUES FROM (null) TO (maxvalue);
 
 -- check if compatible with the specified parent
 
@@ -542,10 +537,10 @@ CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (0);
 -- note that the range '[1, 1)' has no elements
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (1) TO (1);
 
-CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (1);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (unbounded) TO (2);
+CREATE TABLE part0 PARTITION OF range_parted2 FOR VALUES FROM (minvalue) TO (1);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (minvalue) TO (2);
 CREATE TABLE part1 PARTITION OF range_parted2 FOR VALUES FROM (1) TO (10);
-CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (9) TO (maxvalue);
 CREATE TABLE part2 PARTITION OF range_parted2 FOR VALUES FROM (20) TO (30);
 CREATE TABLE part3 PARTITION OF range_parted2 FOR VALUES FROM (30) TO (40);
 CREATE TABLE fail_part PARTITION OF range_parted2 FOR VALUES FROM (10) TO (30);
@@ -557,18 +552,18 @@ CREATE TABLE range_parted3 (
 	b int
 ) PARTITION BY RANGE (a, (b+1));
 
-CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, unbounded);
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, unbounded) TO (0, 1);
+CREATE TABLE part00 PARTITION OF range_parted3 FOR VALUES FROM (0, minvalue) TO (0, maxvalue);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (0, minvalue) TO (0, 1);
 
-CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, 1);
+CREATE TABLE part10 PARTITION OF range_parted3 FOR VALUES FROM (1, minvalue) TO (1, 1);
 CREATE TABLE part11 PARTITION OF range_parted3 FOR VALUES FROM (1, 1) TO (1, 10);
-CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, unbounded);
+CREATE TABLE part12 PARTITION OF range_parted3 FOR VALUES FROM (1, 10) TO (1, maxvalue);
 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
-CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, unbounded) TO (1, unbounded);
+CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, minvalue) TO (1, maxvalue);
 
 -- check schema propagation from parent
 
@@ -626,14 +621,14 @@ 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 (MINVALUE, 0, 0) TO (MAXVALUE, 0, 0);
 \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 (MINVALUE, 0, 0) TO (1, MAXVALUE, 0);
 \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, MAXVALUE);
 \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, MINVALUE) TO (9, MAXVALUE, 0);
 \d+ range_parted4_3
 DROP TABLE range_parted4;
 
diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql
index 70fe971d51..c96580cd81 100644
--- a/src/test/regress/sql/inherit.sql
+++ b/src/test/regress/sql/inherit.sql
@@ -623,7 +623,7 @@ create table part_10_20_cd partition of part_10_20 for values in ('cd');
 create table part_21_30 partition of range_list_parted for values from (21) to (30) partition by list (b);
 create table part_21_30_ab partition of part_21_30 for values in ('ab');
 create table part_21_30_cd partition of part_21_30 for values in ('cd');
-create table part_40_inf partition of range_list_parted for values from (40) to (unbounded) partition by list (b);
+create table part_40_inf partition of range_list_parted for values from (40) to (maxvalue) partition by list (b);
 create table part_40_inf_ab partition of part_40_inf for values in ('ab');
 create table part_40_inf_cd partition of part_40_inf for values in ('cd');
 create table part_40_inf_null partition of part_40_inf for values in (null);
@@ -647,12 +647,12 @@ drop table range_list_parted;
 -- check that constraint exclusion is able to cope with the partition
 -- constraint emitted for multi-column range partitioned tables
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, 1, 1);
+create table mcrparted0 partition of mcrparted for values from (minvalue, 0, 0) to (1, 1, 1);
 create table mcrparted1 partition of mcrparted for values from (1, 1, 1) to (10, 5, 10);
 create table mcrparted2 partition of mcrparted for values from (10, 5, 10) to (10, 10, 10);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
 create table mcrparted4 partition of mcrparted for values from (20, 10, 10) to (20, 20, 20);
-create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted5 partition of mcrparted for values from (20, 20, 20) to (maxvalue, 0, 0);
 explain (costs off) select * from mcrparted where a = 0;	-- scans mcrparted0
 explain (costs off) select * from mcrparted where a = 10 and abs(b) < 5;	-- scans mcrparted1
 explain (costs off) select * from mcrparted where a = 10 and abs(b) = 5;	-- scans mcrparted1, mcrparted2
diff --git a/src/test/regress/sql/insert.sql b/src/test/regress/sql/insert.sql
index 83c3ad8f53..28f0c24ed6 100644
--- a/src/test/regress/sql/insert.sql
+++ b/src/test/regress/sql/insert.sql
@@ -169,7 +169,7 @@ select tableoid::regclass, * from list_parted;
 
 -- some more tests to exercise tuple-routing with multi-level partitioning
 create table part_gg partition of list_parted for values in ('gg') partition by range (b);
-create table part_gg1 partition of part_gg for values from (unbounded) to (1);
+create table part_gg1 partition of part_gg for values from (minvalue) to (1);
 create table part_gg2 partition of part_gg for values from (1) to (10) partition by range (b);
 create table part_gg2_1 partition of part_gg2 for values from (1) to (5);
 create table part_gg2_2 partition of part_gg2 for values from (5) to (10);
@@ -293,12 +293,12 @@ drop table key_desc, key_desc_1;
 -- check multi-column range partitioning expression enforces the same
 -- constraint as what tuple-routing would determine it to be
 create table mcrparted (a int, b int, c int) partition by range (a, abs(b), c);
-create table mcrparted0 partition of mcrparted for values from (unbounded, unbounded, unbounded) to (1, unbounded, unbounded);
-create table mcrparted1 partition of mcrparted for values from (2, 1, unbounded) to (10, 5, 10);
-create table mcrparted2 partition of mcrparted for values from (10, 6, unbounded) to (10, unbounded, unbounded);
+create table mcrparted0 partition of mcrparted for values from (minvalue, 0, 0) to (1, maxvalue, 0);
+create table mcrparted1 partition of mcrparted for values from (2, 1, minvalue) to (10, 5, 10);
+create table mcrparted2 partition of mcrparted for values from (10, 6, minvalue) to (10, maxvalue, 0);
 create table mcrparted3 partition of mcrparted for values from (11, 1, 1) to (20, 10, 10);
-create table mcrparted4 partition of mcrparted for values from (21, unbounded, unbounded) to (30, 20, unbounded);
-create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (unbounded, unbounded, unbounded);
+create table mcrparted4 partition of mcrparted for values from (21, minvalue, 0) to (30, 20, maxvalue);
+create table mcrparted5 partition of mcrparted for values from (30, 21, 20) to (maxvalue, 0, 0);
 
 -- routed to mcrparted0
 insert into mcrparted values (0, 1, 1);
#29Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Amit Langote (#28)
Re: Multi column range partition table

On 7 July 2017 at 03:21, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

The patch looks generally good, although I found and fixed some minor
issues (typos and such). Please find attached the updated patch.

Thanks for the review. Those changes all look good. I also see that I
missed an example in the docs at the bottom of the CREATE TABLE page,
so I'll go update that.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#30Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Joe Conway (#26)
Re: Multi column range partition table

On 6 July 2017 at 22:43, Joe Conway <mail@joeconway.com> wrote:

I agree we should get this right the first time and I also agree with
Dean's proposal, so I guess I'm a +2

On 7 July 2017 at 03:21, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

+1 to releasing this syntax in PG 10.

So, that's 3 votes in favour of replacing UNBOUNDED with
MINVALUE/MAXVALUE for range partition bounds in PG 10. Not a huge
consensus, but no objections either. Any one else have an opinion?

Robert, have you been following this thread?

I was thinking of pushing this later today, in time for beta2.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#31Noah Misch
noah@leadboat.com
In reply to: Dean Rasheed (#30)
Re: Multi column range partition table

On Sun, Jul 09, 2017 at 08:42:32AM +0100, Dean Rasheed wrote:

On 6 July 2017 at 22:43, Joe Conway <mail@joeconway.com> wrote:

I agree we should get this right the first time and I also agree with
Dean's proposal, so I guess I'm a +2

On 7 July 2017 at 03:21, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

+1 to releasing this syntax in PG 10.

So, that's 3 votes in favour of replacing UNBOUNDED with
MINVALUE/MAXVALUE for range partition bounds in PG 10. Not a huge
consensus, but no objections either. Any one else have an opinion?

Robert, have you been following this thread?

I was thinking of pushing this later today, in time for beta2.

[Action required within three days. This is a generic notification.]

The above-described topic is currently a PostgreSQL 10 open item. Robert,
since you committed the patch believed to have created it, you own this open
item. If some other commit is more relevant or if this does not belong as a
v10 open item, please let us know. Otherwise, please observe the policy on
open item ownership[1]/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com and send a status update within three calendar days of
this message. Include a date for your subsequent status update. Testers may
discover new open items at any time, and I want to plan to get them all fixed
well in advance of shipping v10. Consequently, I will appreciate your efforts
toward speedy resolution. Thanks.

[1]: /messages/by-id/20170404140717.GA2675809@tornado.leadboat.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#32Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Dean Rasheed (#30)
Re: Multi column range partition table

On Sun, Jul 9, 2017 at 1:12 PM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

On 6 July 2017 at 22:43, Joe Conway <mail@joeconway.com> wrote:

I agree we should get this right the first time and I also agree with
Dean's proposal, so I guess I'm a +2

On 7 July 2017 at 03:21, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

+1 to releasing this syntax in PG 10.

So, that's 3 votes in favour of replacing UNBOUNDED with
MINVALUE/MAXVALUE for range partition bounds in PG 10. Not a huge
consensus, but no objections either. Any one else have an opinion?

+     <para>
+      Also note that some element types, such as <literal>timestamp</>,
+      have a notion of "infinity", which is just another value that can
+      be stored. This is different from <literal>MINVALUE</> and
+      <literal>MAXVALUE</>, which are not real values that can be stored,
+      but rather they are ways of saying the value is unbounded.
+      <literal>MAXVALUE</> can be thought of as being greater than any
+      other value, including "infinity" and <literal>MINVALUE</> as being
+      less than any other value, including "minus infinity". Thus the range
+      <literal>FROM ('infinity') TO (MAXVALUE)</> is not an empty range; it
+      allows precisely one value to be stored &mdash; the timestamp
+      "infinity".
      </para>

The description in this paragraph seems to be attaching intuitive
meaning of word "unbounded" to MAXVALUE and MINVALUE, which have
different intuitive meanings of themselves. Not sure if that's how we
should describe MAXVALUE/MINVALUE.

Most of the patch seems to be replacing "content" with "kind",
RangeDatumContent with PartitionRangeDatumKind and RANGE_DATUM_FINITE
with PARTITION_RANGE_DATUM_VALUE. But those changes in name don't seem
to be adding much value to the patch. Replacing RANGE_DATUM_NEG_INF
and RANGE_DATUM_POS_INF with PARTITION_RANGE_DATUM_MINVALUE and
PARTITION_RANGE_DATUM_MAXVALUE looks like a good change in line with
MINVALUE/MAXVALUE change. May be we should reuse the previous
variables, enum type name and except those two, so that the total
change introduced by the patch is minimal.

--
Best Wishes,
Ashutosh Bapat
EnterpriseDB Corporation
The Postgres Database Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#33Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Ashutosh Bapat (#32)
Re: Multi column range partition table

On 11 July 2017 at 13:29, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

+     <para>
+      Also note that some element types, such as <literal>timestamp</>,
+      have a notion of "infinity", which is just another value that can
+      be stored. This is different from <literal>MINVALUE</> and
+      <literal>MAXVALUE</>, which are not real values that can be stored,
+      but rather they are ways of saying the value is unbounded.
+      <literal>MAXVALUE</> can be thought of as being greater than any
+      other value, including "infinity" and <literal>MINVALUE</> as being
+      less than any other value, including "minus infinity". Thus the range
+      <literal>FROM ('infinity') TO (MAXVALUE)</> is not an empty range; it
+      allows precisely one value to be stored &mdash; the timestamp
+      "infinity".
</para>

The description in this paragraph seems to be attaching intuitive
meaning of word "unbounded" to MAXVALUE and MINVALUE, which have
different intuitive meanings of themselves. Not sure if that's how we
should describe MAXVALUE/MINVALUE.

I'm not sure I understand your point. MINVALUE and MAXVALUE do mean
unbounded below and above respectively. This paragraph is just making
the point that that isn't the same as -/+ infinity.

Most of the patch seems to be replacing "content" with "kind",
RangeDatumContent with PartitionRangeDatumKind and RANGE_DATUM_FINITE
with PARTITION_RANGE_DATUM_VALUE. But those changes in name don't seem
to be adding much value to the patch. Replacing RANGE_DATUM_NEG_INF
and RANGE_DATUM_POS_INF with PARTITION_RANGE_DATUM_MINVALUE and
PARTITION_RANGE_DATUM_MAXVALUE looks like a good change in line with
MINVALUE/MAXVALUE change. May be we should reuse the previous
variables, enum type name and except those two, so that the total
change introduced by the patch is minimal.

No, this isn't just renaming that other enum. It's about replacing the
boolean "infinite" flag on PartitionRangeDatum with something that can
properly enumerate the 3 kinds of PartitionRangeDatum that are allowed
(and, as noted above "finite"/"infinite isn't the right terminology
either). Putting that new enum in parsenodes.h makes it globally
available, wherever the PartitionRangeDatum structure is used. A
side-effect of that change is that the old RangeDatumContent enum that
was local to partition.c is no longer needed.

RangeDatumContent wouldn't be a good name for a globally visible enum
of this kind because that name fails to link it to partitioning in any
way, and could easily be confused as having something to do with RTEs
or range types. Also, the term "content" is more traditionally used in
the Postgres sources for a field *holding* content, rather than a
field specifying the *kind* of content. On the other hand, you'll note
that the term "kind" is by far the most commonly used term for naming
this kind of enum, and any matching fields.

IMO, code consistency and readability takes precedence over keeping
patch sizes down.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#34Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Dean Rasheed (#33)
Re: Multi column range partition table

On 2017/07/12 4:24, Dean Rasheed wrote:

On 11 July 2017 at 13:29, Ashutosh Bapat <ashutosh.bapat@enterprisedb.com> wrote:

Most of the patch seems to be replacing "content" with "kind",
RangeDatumContent with PartitionRangeDatumKind and RANGE_DATUM_FINITE
with PARTITION_RANGE_DATUM_VALUE. But those changes in name don't seem
to be adding much value to the patch. Replacing RANGE_DATUM_NEG_INF
and RANGE_DATUM_POS_INF with PARTITION_RANGE_DATUM_MINVALUE and
PARTITION_RANGE_DATUM_MAXVALUE looks like a good change in line with
MINVALUE/MAXVALUE change. May be we should reuse the previous
variables, enum type name and except those two, so that the total
change introduced by the patch is minimal.

No, this isn't just renaming that other enum. It's about replacing the
boolean "infinite" flag on PartitionRangeDatum with something that can
properly enumerate the 3 kinds of PartitionRangeDatum that are allowed
(and, as noted above "finite"/"infinite isn't the right terminology
either). Putting that new enum in parsenodes.h makes it globally
available, wherever the PartitionRangeDatum structure is used. A
side-effect of that change is that the old RangeDatumContent enum that
was local to partition.c is no longer needed.

RangeDatumContent wouldn't be a good name for a globally visible enum
of this kind because that name fails to link it to partitioning in any
way, and could easily be confused as having something to do with RTEs
or range types. Also, the term "content" is more traditionally used in
the Postgres sources for a field *holding* content, rather than a
field specifying the *kind* of content. On the other hand, you'll note
that the term "kind" is by far the most commonly used term for naming
this kind of enum, and any matching fields.

IMO, code consistency and readability takes precedence over keeping
patch sizes down.

I agree with Dean here that the new global PartitionRangeDatumKind enum is
an improvement over the previous infinite flag in the parse node plus the
partition.c local RangeDatumContent enum for all the reasons he mentioned.

Thanks,
Amit

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#35Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Dean Rasheed (#33)
Re: Multi column range partition table

On Wed, Jul 12, 2017 at 12:54 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

On 11 July 2017 at 13:29, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

+     <para>
+      Also note that some element types, such as <literal>timestamp</>,
+      have a notion of "infinity", which is just another value that can
+      be stored. This is different from <literal>MINVALUE</> and
+      <literal>MAXVALUE</>, which are not real values that can be stored,
+      but rather they are ways of saying the value is unbounded.
+      <literal>MAXVALUE</> can be thought of as being greater than any
+      other value, including "infinity" and <literal>MINVALUE</> as being
+      less than any other value, including "minus infinity". Thus the range
+      <literal>FROM ('infinity') TO (MAXVALUE)</> is not an empty range; it
+      allows precisely one value to be stored &mdash; the timestamp
+      "infinity".
</para>

The description in this paragraph seems to be attaching intuitive
meaning of word "unbounded" to MAXVALUE and MINVALUE, which have
different intuitive meanings of themselves. Not sure if that's how we
should describe MAXVALUE/MINVALUE.

I'm not sure I understand your point. MINVALUE and MAXVALUE do mean
unbounded below and above respectively. This paragraph is just making
the point that that isn't the same as -/+ infinity.

What confuses me and probably users is something named min/max"value"
is not a value but something lesser or greater than any other "value".
The paragraph above explains that <literal>FROM ('infinity') TO
(MAXVALUE)</> implies a partition with only infinity value in there.
What would be the meaning of <literal>FROM (MINVALUE) TO ('minus
infinity')</>, would that be allowed? What would it contain esp. when
the upper bounds are always exclusive?

Most of the patch seems to be replacing "content" with "kind",
RangeDatumContent with PartitionRangeDatumKind and RANGE_DATUM_FINITE
with PARTITION_RANGE_DATUM_VALUE. But those changes in name don't seem
to be adding much value to the patch. Replacing RANGE_DATUM_NEG_INF
and RANGE_DATUM_POS_INF with PARTITION_RANGE_DATUM_MINVALUE and
PARTITION_RANGE_DATUM_MAXVALUE looks like a good change in line with
MINVALUE/MAXVALUE change. May be we should reuse the previous
variables, enum type name and except those two, so that the total
change introduced by the patch is minimal.

No, this isn't just renaming that other enum. It's about replacing the
boolean "infinite" flag on PartitionRangeDatum with something that can
properly enumerate the 3 kinds of PartitionRangeDatum that are allowed
(and, as noted above "finite"/"infinite isn't the right terminology
either).

Right. I think we need that change.

Putting that new enum in parsenodes.h makes it globally
available, wherever the PartitionRangeDatum structure is used. A
side-effect of that change is that the old RangeDatumContent enum that
was local to partition.c is no longer needed.

Hmm, I failed to notice the changes in _out, _equal, _read functions.
The downside is that enum can not be used for anything other than
partitioning. But I can not imagine where will we use it though.

RangeDatumContent wouldn't be a good name for a globally visible enum
of this kind because that name fails to link it to partitioning in any
way, and could easily be confused as having something to do with RTEs
or range types. Also, the term "content" is more traditionally used in
the Postgres sources for a field *holding* content, rather than a
field specifying the *kind* of content. On the other hand, you'll note
that the term "kind" is by far the most commonly used term for naming
this kind of enum, and any matching fields.

Ok.

IMO, code consistency and readability takes precedence over keeping
patch sizes down.

No doubt about that.

--
Best Wishes,
Ashutosh Bapat
EnterpriseDB Corporation
The Postgres Database Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#36Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Ashutosh Bapat (#35)
Re: Multi column range partition table

On 12 July 2017 at 10:46, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

On Wed, Jul 12, 2017 at 12:54 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

On 11 July 2017 at 13:29, Ashutosh Bapat

The description in this paragraph seems to be attaching intuitive
meaning of word "unbounded" to MAXVALUE and MINVALUE, which have
different intuitive meanings of themselves. Not sure if that's how we
should describe MAXVALUE/MINVALUE.

I'm not sure I understand your point. MINVALUE and MAXVALUE do mean
unbounded below and above respectively. This paragraph is just making
the point that that isn't the same as -/+ infinity.

What confuses me and probably users is something named min/max"value"
is not a value but something lesser or greater than any other "value".

Ah OK, I see what you're saying.

It's worth noting though that, after a little looking around, I found
that Oracle, MySQL and DB2 all use MINVALUE/MAXVALUE for unbounded
range partitions (although in the case of Oracle and MySQL, they
currently only support specifying upper bounds, and only use MAXVALUE
at the moment).

So MINVALUE/MAXVALUE are likely to be familiar to at least some people
coming from other databases. Of course, for those other databases, the
surrounding syntax for creating partitioned tables is completely
different, but at least this makes the bounds themselves portable (our
supported set of bounds will be a superset of those supported by
Oracle and MySQL, and I think the same as those supported by DB2).

I also personally quite like those terms, because they're nice and
concise, and it's pretty obvious which is which.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#37Robert Haas
robertmhaas@gmail.com
In reply to: Dean Rasheed (#30)
Re: Multi column range partition table

On Sun, Jul 9, 2017 at 2:42 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

On 6 July 2017 at 22:43, Joe Conway <mail@joeconway.com> wrote:

I agree we should get this right the first time and I also agree with
Dean's proposal, so I guess I'm a +2

On 7 July 2017 at 03:21, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote:

+1 to releasing this syntax in PG 10.

So, that's 3 votes in favour of replacing UNBOUNDED with
MINVALUE/MAXVALUE for range partition bounds in PG 10. Not a huge
consensus, but no objections either. Any one else have an opinion?

Robert, have you been following this thread?

Uh, no. Sorry. I agree that it's a big problem that (10, UNBOUNDED)
interpreted as a maximum value means first_column <= 10 and when
interpreted as a minimum value means first_column >= 10, because those
things aren't opposites of each other. I guess the proposal here
would make (10, MAXVALUE) as a maximum value mean first_column <= 10
and as a minimum would mean first_column > 10, and contrariwise for
MINVALUE. That seems to restore the intended design principle of the
system, which is good, but...

...originally, Amit proposed to attach a postfix INCLUSIVE or
EXCLUSIVE to each bound specification, and this does feel like a bit
of a back door to the same place, kinda. A partition defined to run
from (10, MAXVALUE) TO (11, MAXVALUE) is a lot like a partition
defined to run from (10) EXCLUSIVE to (11) EXCLUSIVE. And if we
eventually decide to allow that, then what will be the difference
between a partition which starts at (10, MAXVALUE) EXCLUSIVE and one
which starts from (10, MAXVALUE) INCLUSIVE?

I haven't thought through this well enough to be sure that there's any
problem with what is being proposed, and I definitely don't have a
better solution off the top of my head, but I feel slightly nervous.

Apologies again for the slow response - will update again by Monday.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#38Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Robert Haas (#37)
Re: Multi column range partition table

On 14 July 2017 at 06:12, Robert Haas <robertmhaas@gmail.com> wrote:

I agree that it's a big problem that (10, UNBOUNDED)
interpreted as a maximum value means first_column <= 10 and when
interpreted as a minimum value means first_column >= 10, because those
things aren't opposites of each other. I guess the proposal here
would make (10, MAXVALUE) as a maximum value mean first_column <= 10
and as a minimum would mean first_column > 10, and contrariwise for
MINVALUE. That seems to restore the intended design principle of the
system, which is good

Right. So in general, when using MINVALUE/MAXVALUE for the 2nd column
of a 2-column partitioning scheme, the partition constraints simplify
as follows:

FROM (x, MINVALUE) => col1 >= x
FROM (x, MAXVALUE) => col1 > x

TO (x, MINVALUE) => col1 < x
TO (x, MAXVALUE) => col1 <= x

which restores the property that one partition can be made contiguous
with another by having the upper bounds of one partition equal to the
lower bounds of the other.

Note that the choice of MINVALUE or MAXVALUE only affects whether the
constraint on the previous column is inclusive or exclusive. That's
quite different from what an INCLUSIVE/EXCLUSIVE flag would do.

, but...

...originally, Amit proposed to attach a postfix INCLUSIVE or
EXCLUSIVE to each bound specification, and this does feel like a bit
of a back door to the same place, kinda. A partition defined to run
from (10, MAXVALUE) TO (11, MAXVALUE) is a lot like a partition
defined to run from (10) EXCLUSIVE to (11) EXCLUSIVE. And if we
eventually decide to allow that, then what will be the difference
between a partition which starts at (10, MAXVALUE) EXCLUSIVE and one
which starts from (10, MAXVALUE) INCLUSIVE?

The INCLUSIVE/EXCLUSIVE flag would apply to the constraint as a whole:

FROM (x, y) INCLUSIVE => (col1, col2) >= (x, y)
FROM (x, y) EXCLUSIVE => (col1, col2) > (x, y)

TO (x, y) INCLUSIVE => (col1, col2) <= (x, y)
TO (x, y) EXCLUSIVE => (col1, col2) < (x, y)

which, when expanded out, actually only affects the constraint on the
final column, and then only in the case where all the other columns
are equal to the partition bound value:

FROM (x, y) INCLUSIVE => col1 > x OR (col1 = x AND col2 >= y)
FROM (x, y) EXCLUSIVE => col1 > x OR (col1 = x AND col2 > y)

TO (x, y) INCLUSIVE => col1 < x OR (col2 = x AND col2 <= y)
TO (x, y) EXCLUSIVE => col1 < x OR (col2 = x AND col2 < y)

So while MINVALUE/MAXVALUE makes a particular column unbounded
below/above, and as a side-effect can influence the inclusivity of the
preceding column, INCLUSIVE/EXCLUSIVE affects the inclusivity of the
final column (something that MINVALUE/MAXVALUE cannot do).

MINVALUE/MAXVALUE takes precedence, in the sense that if the bound on
any column is MINVALUE/MAXVALUE, that column and any later columns are
unbounded and no longer appear in the partition constraint expression,
and so any INCLUSIVE/EXCLUSIVE flag would have no effect. That seems
pretty intuitive to me -- "unbounded inclusive" is no different from
"unbounded exclusive".

Technically, anything that can be done using INCLUSIVE/EXCLUSIVE can
also be done using using MINVALUE/MAXVALUE, by artificially adding
another partitioning column and making it unbounded above/below, but
that would really just be a hack, and it (artificially adding an extra
column) would be unnecessary if we added INCLUSIVE/EXCLUSIVE support
in a later release. Thus, I think the 2 features would complement each
other quite nicely.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#39Robert Haas
robertmhaas@gmail.com
In reply to: Dean Rasheed (#38)
Re: Multi column range partition table

On Sun, Jul 16, 2017 at 6:40 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

Technically, anything that can be done using INCLUSIVE/EXCLUSIVE can
also be done using using MINVALUE/MAXVALUE, by artificially adding
another partitioning column and making it unbounded above/below, but
that would really just be a hack, and it (artificially adding an extra
column) would be unnecessary if we added INCLUSIVE/EXCLUSIVE support
in a later release. Thus, I think the 2 features would complement each
other quite nicely.

OK, works for me. I'm not really keen about the MINVALUE/MAXVALUE
syntax -- it's really +/- infinity, not a value at all -- but I
haven't got a better proposal and yours at least has the virtue of
perhaps being familiar to those who know about Oracle.

Do you want to own this open item, then?

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#40Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Robert Haas (#39)
Re: Multi column range partition table

On 17 July 2017 at 16:34, Robert Haas <robertmhaas@gmail.com> wrote:

On Sun, Jul 16, 2017 at 6:40 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

Technically, anything that can be done using INCLUSIVE/EXCLUSIVE can
also be done using using MINVALUE/MAXVALUE, by artificially adding
another partitioning column and making it unbounded above/below, but
that would really just be a hack, and it (artificially adding an extra
column) would be unnecessary if we added INCLUSIVE/EXCLUSIVE support
in a later release. Thus, I think the 2 features would complement each
other quite nicely.

OK, works for me. I'm not really keen about the MINVALUE/MAXVALUE
syntax -- it's really +/- infinity, not a value at all -- but I
haven't got a better proposal and yours at least has the virtue of
perhaps being familiar to those who know about Oracle.

Cool. Sounds like we've reached a consensus, albeit with some
reservations around the fact that MINVALUE/MAXVALUE aren't actually
values, despite their names.

+/- infinity *are* values for some datatypes such as timestamps, so it
had to be something different from that, and MINVALUE/MAXVALUE are
quite short and simple, and match the syntax used by 3 other
databases.

Do you want to own this open item, then?

OK.

I need to give the patch another read-through, and then I'll aim to
push it sometime in the next few days.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#41Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Dean Rasheed (#40)
Re: Multi column range partition table

On 17 July 2017 at 17:37, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

On 17 July 2017 at 16:34, Robert Haas <robertmhaas@gmail.com> wrote:

Do you want to own this open item, then?

OK.

I need to give the patch another read-through, and then I'll aim to
push it sometime in the next few days.

Committed.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers