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 PGRange class. #15098

Closed
wants to merge 3 commits into from
Closed

Add PGRange class. #15098

wants to merge 3 commits into from

Conversation

jefflai2
Copy link
Contributor

Addresses #14010.

Adding a new class PGRange that PostgreSQL Ranges will map to instead of Ruby Ranges. The new PGRange class addresses the shortcomings when attempting to directly translate the Postgres Range to Ruby Range. Namely, Ruby ranges do not support unbound beginnings and ends, nor do they support exclusive beginnings.

@jefflai2
Copy link
Contributor Author

@matthewd @senny

@senny
Copy link
Member

senny commented May 14, 2014

@jefflai2 did you prototype the integration into AR? Would be nice to see the required changes to get it working full-stack.

@jefflai2
Copy link
Contributor Author

Do we want to make the use of PGRange transparent, i.e. do we want this to be completely backward compatible or do we want to make the use of PGRange explicit.
Which do we expect to return a Ruby ::Range?

OID::Range.new('Integer').type_cast('[1,5]')

or

OID::Range.new('Integer').type_cast('[1,5]').to_range

I've sophisticated the eql? and == methods a bit, but it is not bidirectional. e.g.

r = OID::Range.new('Integer').type_cast('[1,11)')
r == 1...11 # => true
1...11 == r # => false

@senny
Copy link
Member

senny commented May 15, 2014

@jefflai2 We either break backwards compatibility directly or introduce a switch combined with a deprecation warning. The idea is that your models hold a PGRange. You (the user) will work with PGRange objects. They should suffice for most common use-cases. If you need a Ruby range, it's up to the user to ask for it. Because the conversion is not lossless. We want to delegate that responsibility to the user. We map the stored data to a correct representation. The rest is up to the user.

@jefflai2
Copy link
Contributor Author

Updated, modifying the range_test.rb. Passes rake test_postgresql. Feedback? (BTW, is there a way to avoid fully qualifying PGRange when I use it elsewhere?

assert_equal 1...11, @first_range.int4_range
assert_equal 1...10, @second_range.int4_range
assert_equal 1...Float::INFINITY, @third_range.int4_range
assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition, can we keep the old assertions and verify them agains a converted PGRange -> Range? This should help us to see where we changed behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would apply for the following tests as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean this would be preferrable? assert_equal 1...11, @first_range.int4_range.to_range

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jefflai2 at least for the cases where it's different.

@senny
Copy link
Member

senny commented May 22, 2014

@jefflai2 keep in mind to perform a rebase. The Type code has changed significantly on master.

/cc @sgrif

@jefflai2
Copy link
Contributor Author

Updated, and put PGRange into .../oid/range.rb.

def infinity?(value)
value.respond_to?(:infinite?) && value.infinite?
end

def type_cast_single(value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method isn't used outside of this class. Do you think we can just remove it instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

@sgrif
Copy link
Contributor

sgrif commented May 24, 2014

@jefflai2 Can you push the updated code so I can re-review?

end
end

delegate :each, to: :to_range
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move these to the top of the class below the attr readers, and merge them? delegate :bsearch, :each, to: :to_range

@jefflai2
Copy link
Contributor Author

Just pushed, sorry I was just commenting to make sure I hit everything. I'm thinking about the change to #cast_value. I'm not sure exactly where/how that method is used, so you're probably right. I just wanted to make sure there wasn't any inconsistency in the uses.

end

# Different from first; we can't depend on #succ to hack around an unbounded start or end.
delegate :last, to: :to_range
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge above.

@sgrif
Copy link
Contributor

sgrif commented May 24, 2014

A few more minor comments, but this looks good to me. Would like to see it merged so I can do a few refactorings based off of it.

# def test_update_int8range
# assert_equal_round_trip(@first_range, :int8_range, pgrange(60000, 10000000, :integer, true))
# assert_nil_round_trip(@first_range, :int8_range, pgrange(39999, 39999, :integer, true))
# end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is everything commented out by intention?

Unlike Ruby Ranges, the new `PGRange` can support exclusive beginnings
and unbounded beginnings/ends. `PGRange` supports the most common
operations and provides a method to attempt to convert to a Ruby Range.
There are issues with some methods because it does not make
sense to enumerate unbounded ranges.
@jefflai2
Copy link
Contributor Author

Updated, but I still have a question/concern:

It appears that inclusive ranges, e.g. [1,10] or 1..10, when saved and then loaded, get turned into ranges with exclusive ends, e.g. [1,11) or 1...11. I'm print debugging around my code and all of my code seems to make sense. The first time any of my code sees the [1,11) is after the range is being inspected/read (e.g. puts @first_range.int4_range), when the [1,11) is passed as an argument to #cast_value. It appears that this issue only appears with inclusive ranges.

It also appears that this was expected/accepted behavior before:
https://github.com/rails/rails/blob/master/activerecord/test/cases/adapters/postgresql/range_test.rb#L111

I don't know if/why this should happen and I can't find out where this change is happening. @senny do you know?

@matthewd
Copy link
Member

In an integer range, (0,11), (0,10], [1,11), and [1,10] are identical, so it doesn't seem shocking that PostgreSQL is choosing to normalize them -- note it behaves differently, even given the same numbers, for a numrange.

I haven't read through this closely enough to count as a review, but I will mention: please avoid 'rng' -- that has a well-defined meaning, which is not "range".

@matthewd
Copy link
Member

To be clear, the canonicalization is happening inside the database, and isn't something we could have any control over even if we wanted to:

[local]/postgres=# select '(0,10]'::int4range, '(0,11)'::int4range;
 int4range | int4range 
-----------+-----------
 [1,11)    | [1,11)
(1 row)

(see http://www.postgresql.org/docs/9.2/static/rangetypes.html#RANGETYPES-DISCRETE)

@sgrif
Copy link
Contributor

sgrif commented May 26, 2014

It is important that we maintain model.attr == model.reload.attr, however, so PGRange should probably handle this inconsistency in its equality method.

@sgrif
Copy link
Contributor

sgrif commented Jun 1, 2014

Does anyone have additional feedback on this? Would like to get it merged so I can work on some of the refactorings that this enables.

@senny
Copy link
Member

senny commented Jun 2, 2014

what about the comparisons of the transformations that pg performs?—
Sent from Mailbox for iPhone

On Mon, Jun 2, 2014 at 1:36 AM, Sean Griffin notifications@github.com
wrote:

Does anyone have additional feedback on this? Would like to get it merged so I can work on some of the refactorings that this enables.

Reply to this email directly or view it on GitHub:
#15098 (comment)

@jefflai2
Copy link
Contributor Author

jefflai2 commented Jun 2, 2014

Sorry, had work and was working on resolving issue on another PR. :(

Made changes to allow comparisons on transformed ranges. But there are a couple major caveats:

  1. I only handled comparisons between two pgranges, since I would only be able to affect comparisons between a PGRange and a ::Range when the PGRange is on the left side.

  2. The tests are written to compare between ::Range objects, by calling #to_range. Thus, the existing tests would not be affected by this latest change, because there is no way for a PGRange to predict which "equivalent" ::Range it should return in #to_range.

  3. In the event that we are okay with the above two caveats, there is a shortcoming in my implementation. I delegate resolving the "second to last" element to #to_range, so there are situations when my implementation returns false when it could/should return true (e.g. (-infinity, 10] == (-infinity, 11)). In order to fix this, I would have to have some way to retrieve the value that precedes the last value, which would require (IMO) an unreasonable amount of changes.

In light of all this, I think it's best to not try to be robust to these transformations, but rather, just document this issue and perhaps provide a warning to the user. As it is now, ::Ranges are neither robust to the transformation, nor is it documented, so this may still be an improvement.

@jefflai2
Copy link
Contributor Author

Any comments on this?

@sgrif
Copy link
Contributor

sgrif commented Nov 26, 2014

@jefflai2 The code this affects has changed drastically. Can you rebase onto master, so we can review the code in the context of the more recent structure?

@sgrif
Copy link
Contributor

sgrif commented Nov 26, 2014

I may have overstated how drastic it might be, but could you rebase regardless?

@jefflai2
Copy link
Contributor Author

Hmm, after revisiting this, it looks like there have been several changes to the range class that address several issues originally mentioned in since the last update to this PR. If the approach originally proposed by @senny in #14010 is still something we want to move forward with (i.e. introduce a PGRange class), I'd like to close this and reopen a new PR based off the new master.

@rafaelfranca rafaelfranca removed this from the 5.0.0 [temp] milestone Apr 5, 2016
@yawboakye
Copy link
Contributor

Since there's support for PostgreSQL ranges (since 2014) this PR can be closed @rafaelfranca

@axelson
Copy link

axelson commented Sep 14, 2017

@yawboakye From what I can tell it looks like that support doesn't include a tsrange that has a value like ["2017-09-14 00:00:00",), i.e. a range from today to infinity. When I attempt to load a model with that value in a field and access the field I get a bad value for range error. This is on Rails 4.2.8 and I'm getting a similar error on Rails 5.1.4.

@yawboakye
Copy link
Contributor

@axelson I'll take a look today

@rails-bot
Copy link

rails-bot bot commented Dec 18, 2019

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
Thank you for your contributions.

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

Successfully merging this pull request may close these issues.

None yet

9 participants