-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add new date and datetime based range fields that work with xocto.range types #136
Conversation
175597c
to
0bcb101
Compare
xocto/fields/ranges.py
Outdated
""" | ||
A DateRangeField with Inclusive-Inclusive [] bounds. | ||
|
||
The underlying postgres type will always store the range as [). This field |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this comment should expand on the notation you've used here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not keen to be repeating Word-Word [symbol-symbol) everywhere. I'm thinking of defaulting to just symbols as that's common-enough symbolism. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps a full explanation somewhere and a reference to that where you use them?
xocto/fields/ranges.py
Outdated
if value is None: | ||
return None | ||
# DateRange is always stored as [) in the database, so we need to remove | ||
# a day from the upper bound to make it inclusive. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't fully understand whether that would have undesirable consequences or not, is there more that can be added to this comment to explain the choice of a day?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can expand on the comment, yes. But, for your understanding prior to that (and using numbers for terseness):
If we have [1, 10]
, postgres will store that as [1, 11)
AND return it as [1, 11)
. We deduct 1 to get back to [1, 10]`. This is how all discrete ranges work in postgres https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-DISCRETE
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and the range is always in full days?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DateRange is, yes 👍
DateTimeRanges are continuous and used with [)
bounds, so there's no need for canonicalisation
xocto/fields/ranges.py
Outdated
start = pg_utils.AttributeSetter(base_field.attname, value.start) | ||
end = pg_utils.AttributeSetter(base_field.attname, value.end) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unsure what these two lines are doing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are partially cargo-culted from the underlying DateRange type. Initially I left them out, but base_field
(DateTimeField) calls value_from_object
which essentially does getattr(self, attname)
(yay descriptors). This sets up the correct attributes so that the getattr
works since the base_field isn't really attached to the model.
@@ -49,6 +49,7 @@ dev = [ | |||
"mypy==0.991", | |||
"numpy==1.22.2", | |||
"pre-commit>=3.2.0", | |||
"psycopg2>=2.8.4", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
given it's an open source library, should we make the postgres stuff optional? and then also make the CI run against both postgres and sqlite?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is dependency is optional. I don't think there's any real value in running tests against both PG and sqlite. We don't have that much in xocto that depends on the database, but these fields absolutely depend on postgres. It'll be noted in the docs (once written).
Is there value in namespacing the fields
folder into a fields/postgres
structure so folks don't unintentionally import something they can't use?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah it is too, sorry, didn't see which list it was in.
The namespacing sounds like a reasonable idea
don't think there's any real value in running tests against both PG and sqlite.
mmmkay
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would be a great addition :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🍷 🧀 👨🍳
pyproject.toml
Outdated
@@ -66,6 +66,7 @@ dev = [ | |||
docs = [ | |||
"Sphinx==4.5.0", | |||
"myst-parser==0.18.1", | |||
"sphinx-rtd-theme==2.0.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is required to build the docs locally.
@@ -34,7 +34,6 @@ repos: | |||
args: [ --fix ] | |||
# Run the formatter. | |||
- id: ruff-format | |||
args: [ --fix ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was incorrectly added in a previous change but didn't complain for some reason.
239d366
to
0491227
Compare
pyproject.toml
Outdated
@@ -65,6 +66,8 @@ dev = [ | |||
docs = [ | |||
"Sphinx==4.5.0", | |||
"myst-parser==0.18.1", | |||
"sphinxcontrib-serializinghtml==1.1.5", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extracting these changes into #139
Prior to this change, we used sqlite as the test database. This change switches the database to postgres in anticipation of new postgres-only features.
Adapting between pg_extras.DateRange and xocto.range types requires some care, particularly considering how both treat bounds on either end of the range. Rather than constantly translating between the two in applications, we provide an adapting field type so that the built-in pg DateRange type doesn't need to be exposed to the application at all.
The Django implementation of range fields allows querying them with tuples of (start, end), presumably because the underlying pg_extras.Range types aren't really supposed to be exposed to the user. Considering we're attempting to enforce strict usages of types, we explicitly prevent non-range types from being used in queries.
obj = models.HalfFiniteDateTimeRangeModel.objects.create( | ||
half_finite_datetime_range=half_finite_datetime_range_melb, | ||
) | ||
half_finite_datetime_range_london = ranges.HalfFiniteDatetimeRange( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should the test confirm that timezone.localtime doesn't use melbourne itself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good shout - the returned timezone is utc which I didn't expect, I thought (and docs suggest..) it should be converted to the local time. Will dig in.
== half_finite_datetime_range_london | ||
== half_finite_datetime_range_melb | ||
) | ||
assert obj.half_finite_datetime_range.start.tzinfo != TZ_MELB |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
related to the other comment on this test, this doesn't confirm that it's the local timezone that it converted to, only that it's not melbourne...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would recommend more than just one approve, but I like this
Thanks @delfick - I've tagged a few folks that have implemented range fields recently to have a look |
Prior to this change, values returned from the database would be in UTC, despite the Django docs suggesting that data returned on Postgres will be translated to the local timezone. This change always returns the values in the local timezone. https://code.djangoproject.com/ticket/35267
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of questions/suggestions but nothing blocking. This is going to be handy. Thanks
datetime.date(2024, 1, 11), | ||
datetime.date(2024, 1, 15), | ||
) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick (non-blocking): This is 3 tests hidden in a trench-coat
There are 3 separate tests here - one for each of the filter lookups. Whilst they are all effectively the same check under the hood, tests should ideally only test 1 thing to ensure that they are specific and can more easily pinpoint an issue to the specific area effected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair, but we don't expect these to ever fail, as they all go via the same codepath. It's more a demonstration of that.
🤔 |
The range fields provided by
django.contrib.postgres.fields.ranges
depend on types withinpsycopg2.extras
that aren't all that intuitive and shouldn't be exposed too heavily within a regular application.The range types within
xocto.ranges
are more ergonomic and are designed to be used throughout an application.This change adapts the postgres contrib range fields to work nicely with xocto.ranges.
Fields implemented in this change:
Notably missing is:
HalfFiniteDateRangeField
because we don't have a corresponding type in xocto.ranges. We already have usage of a half-open daterange field within our main application, so it would be a useful addition later.