Skip to content

Commit 5ff2abd

Browse files
committed
docs(format): make range spec independent of the subtype
Allow any orderable type as the range subtype and compare bounds with the order of that type; the spec defines only the storage layout. State that only null means unbounded and that all empty ranges denote the same set. Add string subtype cases to the C++ and Python tests.
1 parent 09d1657 commit 5ff2abd

3 files changed

Lines changed: 39 additions & 20 deletions

File tree

‎cpp/src/arrow/extension/range_test.cc‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,8 @@ TEST(FixedClosednessRangeType, MetadataRoundTrip) {
287287
extension::fixed_closedness_range(int32(), C::Both),
288288
extension::fixed_closedness_range(int32(), C::Neither),
289289
extension::fixed_closedness_range(int64(), C::Right),
290-
extension::fixed_closedness_range(date32(), C::Both)}) {
290+
extension::fixed_closedness_range(date32(), C::Both),
291+
extension::fixed_closedness_range(utf8(), C::Left)}) {
291292
auto rt = checked_pointer_cast<extension::FixedClosednessRangeType>(type);
292293
std::string serialized = rt->Serialize();
293294
ASSERT_OK_AND_ASSIGN(auto deserialized,
@@ -521,6 +522,7 @@ TEST(VariableClosednessRangeType, MetadataRoundTrip) {
521522
for (const auto& type : {extension::variable_closedness_range(int32()),
522523
extension::variable_closedness_range(int64()),
523524
extension::variable_closedness_range(date32()),
525+
extension::variable_closedness_range(utf8()),
524526
extension::variable_closedness_range(int32(), false)}) {
525527
auto rt = checked_pointer_cast<extension::VariableClosednessRangeType>(type);
526528
std::string serialized = rt->Serialize();

‎docs/source/format/CanonicalExtensions.rst‎

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -611,17 +611,20 @@ instead.
611611
When the field is nullable, a null value means the range is unbounded above
612612
(positive infinity).
613613

614-
**T** (the *subtype* or *value type*) may be any orderable Arrow type:
615-
integer, floating-point, decimal, date, time, or timestamp types. Both
616-
fields share the same type T. The subtype is read directly from the
617-
storage struct and is **not** duplicated in the extension metadata.
614+
**T** (the *subtype* or *value type*) may be any orderable Arrow type, for
615+
example an integer, floating-point, decimal, date, time, timestamp,
616+
duration, string or binary type. Bounds are compared with the order of T.
617+
This specification defines only the storage layout, not the order of any
618+
type. Both fields share the same type T. The subtype is read directly from
619+
the storage struct and is **not** duplicated in the extension metadata.
618620

619621
Each of ``lower`` and ``upper`` **may** be nullable, independently of the
620622
other. Nullability is **only** needed to represent an unbounded side: a
621623
nullable bound may hold null to mean an infinite endpoint, while a
622-
non-nullable bound is always finite. A null bound is **always treated as
623-
exclusive**, regardless of the value of the ``closed`` parameter; positive and
624-
negative infinity can never be included in a closed bound. A null ``lower``
624+
non-nullable bound always holds a value. A null bound is **always treated as
625+
exclusive**, regardless of the value of the ``closed`` parameter, so an
626+
unbounded side is never included. Only null means unbounded: every non-null
627+
value is an ordinary bound, and ``closed`` applies to it. A null ``lower``
625628
means the range extends to negative infinity, a null ``upper`` means it
626629
extends to positive infinity, and a range whose ``lower`` and ``upper`` are
627630
both null (and both nullable) is the universal range ``(-inf, +inf)``. The
@@ -630,7 +633,7 @@ instead.
630633

631634
* Extension type parameters:
632635

633-
* **closed** = which finite bound(s) are inclusive. Allowed values
636+
* **closed** = which non-null bound(s) are inclusive. Allowed values
634637
(following pandas interval vocabulary):
635638

636639
* ``"left"``: ``[lower, upper)``, the lower bound is inclusive and the
@@ -640,11 +643,13 @@ instead.
640643
* ``"both"``: ``[lower, upper]``, both bounds are inclusive.
641644
* ``"neither"``: ``(lower, upper)``, both bounds are exclusive.
642645

643-
A range thus contains every value x permitted by its finite bounds and
646+
A range thus contains every value x permitted by its non-null bounds and
644647
``closed`` setting: with ``closed="both"`` every x such that
645648
``lower <= x <= upper``, with ``closed="neither"`` every x such that
646649
``lower < x < upper``. A range is *empty* when ``lower > upper``, or when
647-
``lower == upper`` and at least one bound is exclusive.
650+
``lower == upper`` and at least one bound is exclusive. All empty values
651+
denote the same empty set, and no canonical encoding is required: a
652+
PostgreSQL ``empty`` range, for example, may be written as any empty value.
648653

649654
For example, with ``closed="left"`` and T = ``Int32`` (both bounds
650655
nullable):
@@ -719,8 +724,10 @@ type for ranges that cannot be canonicalized to a uniform closedness.
719724
* ``upper_inc``: a **non-nullable** ``boolean`` that is ``true`` when the
720725
upper bound is inclusive for that value and ``false`` when it is exclusive.
721726

722-
**T** (the *subtype* or *value type*) may be any orderable Arrow type:
723-
integer, floating-point, decimal, date, time, or timestamp types. The
727+
**T** (the *subtype* or *value type*) follows the same rules as in
728+
:ref:`arrow.fixed_closedness_range <fixed_closedness_range_extension>`: it
729+
may be any orderable Arrow type, and bounds are compared with the order of
730+
T. The
724731
``lower`` and ``upper`` fields share the same type T, read directly from the
725732
storage struct; the subtype is **not** duplicated in the extension metadata.
726733

@@ -729,11 +736,13 @@ type for ranges that cannot be canonicalized to a uniform closedness.
729736
:ref:`arrow.fixed_closedness_range <fixed_closedness_range_extension>`:
730737
nullability is only needed to represent an unbounded side. A null bound is
731738
**always treated as exclusive**, regardless of its ``lower_inc`` /
732-
``upper_inc`` flag; positive and negative infinity can never be included.
733-
Producers should set the flag of a null bound to ``false``, as PostgreSQL
734-
does. The ``lower_inc`` and ``upper_inc`` fields are **always
735-
non-nullable**. The outer struct's validity bit marks a null/absent range
736-
(a missing range, distinct from an empty range).
739+
``upper_inc`` flag, so an unbounded side is never included. Only null means
740+
unbounded: every non-null value is an ordinary bound, and its flag applies
741+
to it. Producers should set the flag of a
742+
null bound to ``false``, as PostgreSQL does. The ``lower_inc`` and
743+
``upper_inc`` fields are **always non-nullable**. The outer struct's
744+
validity bit marks a null/absent range (a missing range, distinct from an
745+
empty range).
737746

738747
* Extension type parameters:
739748

@@ -742,12 +751,15 @@ type for ranges that cannot be canonicalized to a uniform closedness.
742751
inclusivity is not fixed by the type; it is carried per value in the
743752
``lower_inc`` and ``upper_inc`` fields.
744753

745-
For a given value, the range contains every x permitted by its finite bounds
754+
For a given value, the range contains every x permitted by its non-null bounds
746755
and per-value flags: with both flags ``true`` every x such that
747756
``lower <= x <= upper``, with both flags ``false`` every x such that
748757
``lower < x < upper``. A value is *empty* when ``lower > upper``, or when
749758
``lower == upper`` and at least one of ``lower_inc`` / ``upper_inc`` is
750-
``false``.
759+
``false``. As in
760+
:ref:`arrow.fixed_closedness_range <fixed_closedness_range_extension>`, all
761+
empty values denote the same empty set, and no canonical encoding is
762+
required.
751763

752764
Each ``closed`` value of
753765
:ref:`arrow.fixed_closedness_range <fixed_closedness_range_extension>`

‎python/pyarrow/tests/test_extension_type.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2085,6 +2085,7 @@ def test_opaque_type(pickle_module, storage_type, storage):
20852085
(pa.int32(), [{"lower": 1, "upper": 5}, {"lower": None, "upper": 10}]),
20862086
(pa.int64(), [{"lower": None, "upper": None}, {"lower": 2, "upper": 8}]),
20872087
(pa.float64(), [{"lower": 0.0, "upper": 1.5}, None]),
2088+
(pa.string(), [{"lower": "a", "upper": "m"}, {"lower": "m", "upper": None}]),
20882089
])
20892090
def test_fixed_closedness_range_type(pickle_module, closed, value_type, bounds):
20902091
range_type = pa.fixed_closedness_range(value_type, closed)
@@ -2177,6 +2178,10 @@ def test_fixed_closedness_range_type_allow_unbounded():
21772178
{"lower": 0.0, "upper": 1.5, "lower_inc": True, "upper_inc": True},
21782179
None,
21792180
]),
2181+
(pa.string(), [
2182+
{"lower": "a", "upper": "m", "lower_inc": True, "upper_inc": False},
2183+
{"lower": "m", "upper": None, "lower_inc": True, "upper_inc": False},
2184+
]),
21802185
])
21812186
def test_variable_closedness_range_type(pickle_module, value_type, rows):
21822187
range_type = pa.variable_closedness_range(value_type)

0 commit comments

Comments
 (0)