BaseRangeField and equal lower/upper values

Currently, when you give input in a range form field with identical lower and upper values, the cleaned data is giving something like: 'field_name': Range(datetime.date(2024, 8, 1), datetime.date(2024, 8, 1).

When you save that value in the database, you will get an empty range in the model field: Range(empty=True). Is that expected?

If this is not expected (which is my current feeling), we could raise an error in the range field validation when lower value == upper value.
Otherwise, if anyone can provide a use case with the current behavior, this should at least be documented as a note in PostgreSQL specific form fields and widgets | Django documentation | Django

What do you think?

Given what I read in the docs for both DateRangeField and IntegerRangeField:

Regardless of the bounds specified when saving the data, PostgreSQL always returns a range in a canonical form that includes the lower bound and excludes the upper bound, that is [) .

Along with the docs for Range Input/Output that includes the line:

Notice that the final pattern is empty , which represents an empty range (a range that contains no points).

And that running these queries directly in psql yields this:

test=# select int4range(1,1);
 int4range 
-----------
 empty
(1 row)

test=# select int4range(1,2);
 int4range 
-----------
 [1,2)
(1 row)

test=# select daterange('2024-08-01', '2024-08-01');
 daterange 
-----------
 empty
(1 row)

test=# select daterange('2024-08-01', '2024-08-02');
        daterange        
-------------------------
 [2024-08-01,2024-08-02)
(1 row)

This all looks reasonable to me. This doesn’t seem to be an issue with either Django or Psycopg, it’s how PostgreSQL works with these ranges.

Hi Ken,
Yes, it’s surely as PostgreSQL works and I don’t argue about any technical bug. I’m arguing about a behavioral issue:

Some user is entering '2024-08-01', '2024-08-01' in a duration widget of a model form, send the form (hence saving the model), and when the form is re-displayed from the saved instance, the widget only shows empty fields. I pretend that this behavior is counter-intuitive for end users, and maybe Django could improve that.

I agree that it may be counter-intuitive, but it’s Postgres that is doing it.

It appears to me from what I can tell, Postgres itself is disposing of the originally supplied values. I’m not finding any way to retrieve what was originally entered, and the operations I’ve tested (containment, “left of”, “right of”, union, intersection, overlap) yield results that are logically consistent with considering this to effectively be a null value.

I’m not sure what Django can do for that. At the database level, you can’t cast the range to any other data type, nor can you specify a different notation for the result of the query.

You’re also going to encounter this regardless of how the field data is entered - the results are the same if you’re submitting the range in a model form or programatically generating it in code.

Since this is valid, I would think at most, you’d want to identify this as a special-case situation for form validation.

(We use ranges a lot, but usually with timestamp fields and inclusive on both ends, which avoids this issue.)

I think that at a minimum, this should be documented. Another suggestion as I wrote in the first post would be to raise a ValidationError for equal values in the same way an error is raised when upper < lower. The current behavior could be considered as some form of data loss.