Skip to content
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

Merged
merged 9 commits into from
Mar 5, 2024

Conversation

jarshwah
Copy link
Contributor

@jarshwah jarshwah commented Feb 24, 2024

The range fields provided by django.contrib.postgres.fields.ranges depend on types within psycopg2.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:

  • FiniteDateRangeField
  • FiniteDateTimeRangeField
  • HalfFiniteDateTimeRangeField

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.

@jarshwah jarshwah force-pushed the range-fields branch 2 times, most recently from 175597c to 0bcb101 Compare February 24, 2024 23:52
"""
A DateRangeField with Inclusive-Inclusive [] bounds.

The underlying postgres type will always store the range as [). This field
Copy link
Contributor

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

Copy link
Contributor Author

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?

Copy link
Contributor

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?

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.
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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?

Copy link
Contributor Author

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

Comment on lines 61 to 62
start = pg_utils.AttributeSetter(base_field.attname, value.start)
end = pg_utils.AttributeSetter(base_field.attname, value.end)
Copy link
Contributor

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

Copy link
Contributor Author

@jarshwah jarshwah Feb 28, 2024

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",
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

@delfick delfick Mar 1, 2024

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

Copy link
Contributor

@delfick delfick left a 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 :)

Copy link
Contributor Author

@jarshwah jarshwah left a 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",
Copy link
Contributor Author

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 ]
Copy link
Contributor Author

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.

@jarshwah jarshwah force-pushed the range-fields branch 3 times, most recently from 239d366 to 0491227 Compare March 1, 2024 06:23
@jarshwah jarshwah marked this pull request as ready for review March 1, 2024 06:26
pyproject.toml Outdated
@@ -65,6 +66,8 @@ dev = [
docs = [
"Sphinx==4.5.0",
"myst-parser==0.18.1",
"sphinxcontrib-serializinghtml==1.1.5",
Copy link
Contributor Author

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

jarshwah added 3 commits March 1, 2024 17:48
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.
@jarshwah jarshwah mentioned this pull request Mar 1, 2024
@jarshwah jarshwah requested review from a team March 1, 2024 07:03
jarshwah added 5 commits March 1, 2024 18:09
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(
Copy link
Contributor

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?

Copy link
Contributor Author

@jarshwah jarshwah Mar 4, 2024

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.

Edit: https://code.djangoproject.com/ticket/35267

== half_finite_datetime_range_london
== half_finite_datetime_range_melb
)
assert obj.half_finite_datetime_range.start.tzinfo != TZ_MELB
Copy link
Contributor

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...

Copy link
Contributor

@delfick delfick left a 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

@jarshwah jarshwah requested a review from a-musing-moose March 3, 2024 23:41
@jarshwah
Copy link
Contributor Author

jarshwah commented Mar 3, 2024

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
Copy link

@a-musing-moose a-musing-moose left a 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

xocto/fields/postgres/ranges.py Show resolved Hide resolved
docs/xocto/model_fields.md Show resolved Hide resolved
datetime.date(2024, 1, 11),
datetime.date(2024, 1, 15),
)
)

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.

Copy link
Contributor Author

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.

@jarshwah jarshwah merged commit 8fa4059 into main Mar 5, 2024
1 check passed
@jarshwah jarshwah deleted the range-fields branch March 5, 2024 05:38
@jmurphyau
Copy link

🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants