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

Ensure #second (and others) acts like #first AR finder #13757

Merged
merged 1 commit into from Jan 20, 2014
Merged

Ensure #second (and others) acts like #first AR finder #13757

merged 1 commit into from Jan 20, 2014

Conversation

terracatta
Copy link
Contributor

This PR addresses issue #13743 where the ordinal ActiveSupport Array extentions were not assuming a default order like first was.

As suggested by @dhh this commit brings the famous ordinal Array instance methods defined
in ActiveSupport into ActiveRecord as fully-fledged finders.

These finders ensure a default ascending order of the table's primary key, and utilize the OFFSET SQL verb to locate the user's desired record. If an offset is defined in the query, calling #second adds
to the offset to get the actual desired record.

Example:

    User.all.second

    # Before
    # => 'SELECT  "users".* FROM "users"'

    # After
    # => SELECT  "users".* FROM "users"   ORDER BY "users"."id" ASC LIMIT 1 OFFSET 1'

    User.offset(3).second

    # Before
    # => 'SELECT "users".* FROM "users"  LIMIT -1 OFFSET 3' # sqlite3 gem
    # => 'SELECT "users".* FROM "users"  OFFSET 3' # pg gem
    # => 'SELECT `users`.* FROM `users`  LIMIT 18446744073709551615 OFFSET 3' # mysql2 gem"'

    # After
    # => SELECT  "users".* FROM "users"   ORDER BY "users"."id" ASC LIMIT 1 OFFSET 4'

@dhh
Copy link
Member

dhh commented Jan 19, 2014

Looking good to me! 👍

def find_first_with_limit(limit)
def find_nth_with_limit(ordinal, limit)
offset = case ordinal
when :first
Copy link
Contributor

Choose a reason for hiding this comment

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

This function should not have knowledge of :first, :second and friends. We should probably just pass the offset_value as argument.

@terracatta
Copy link
Contributor Author

@josevalim Ok, fixed that up for you! Any other concerns?

The famous ordinal Array instance methods defined in ActiveSupport
(`first`, `second`, `third`, `fourth`, and `fifth`) are now available as
full-fledged finders in ActiveRecord. The biggest benefit of this is ordering
of the records returned now defaults to the table's primary key in ascending order.
Copy link
Member

Choose a reason for hiding this comment

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

Can you add:

Fixes #13743.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added it after the examples! Thanks for the heads up!

@senny
Copy link
Member

senny commented Jan 20, 2014

@terracatta this looks good. Can you squash your commits?

if loaded?
@records.first
else
@first ||= find_first_with_limit(1).first
@nth ||= find_nth_with_limit(offset, 1).first

Choose a reason for hiding this comment

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

I have a question: how problematic can this cached variable be now that we are using it for different "finds"?

Copy link
Member

Choose a reason for hiding this comment

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

Very problematic. It will cache only the first call.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok so I removed the caching here, but obviously that isn't the right way to go as that will cause multiple queries to fire unnecessarily when calling .first a bunch of times. I think the only option here is cache based on offset being passed into the private method.

Any other suggestions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok I just went with that

@terracatta
Copy link
Contributor Author

@rafaelfranca @carlosantoniodasilva @senny, commits are squashed, updated the changelog, fixed the caching issue by dynamically setting and manually memoizing the instance variable based on the offset passed in. Tested it again on several personal rails projects and the test suite looks good!

Any other feedback?

if loaded?
@records.first
else
@first ||= find_first_with_limit(1).first
unless instance_variable_defined?("@offset_#{offset||0}")
Copy link
Member

Choose a reason for hiding this comment

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

It is better to initialize a empty hash in the initialize of Relation and use the offset as the key to memoize it.

Since we are adding an internal variable to cache the value we have to update reset method

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great suggestion, I'll implement it ASAP

@terracatta
Copy link
Contributor Author

@rafaelfranca, I made the requested changes, but couldn't reproduce your issue or create a failing test case (still trying), can you try again with the latest code and see if you still get a negative limit?

This commit bring the famous ordinal Array instance methods defined
in ActiveSupport into ActiveRecord as fully-fledged finders.

These finders ensure a default ascending order of the table's primary
key, and utilize the OFFSET SQL verb to locate the user's desired
record. If an offset is defined in the query, calling #second adds
to the offset to get the actual desired record.

Fixes #13743.
@terracatta
Copy link
Contributor Author

@rafaelfranca Ok great, added better before examples for pg, sqlite3, and mysql2 gems. I also added some extra test cases for the offset functionality just in case!

Anything else? Sorry for all the back and forth!

@rafaelfranca
Copy link
Member

Looks good. Thank you so much for working on this.

rafaelfranca added a commit that referenced this pull request Jan 20, 2014
Ensure #second (and others) acts like #first AR finder
@rafaelfranca
Copy link
Member

Merged ❤️

@rafaelfranca rafaelfranca merged commit cafe31a into rails:master Jan 20, 2014
@@ -364,19 +444,19 @@ def find_take
end
end

def find_first
def find_nth(offset)
if loaded?
@records.first

Choose a reason for hiding this comment

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

This is apparently not going to work with already loaded associations. Just testing around I got two different issues:

# consider the models `Author` has many `Post`s

## 1st scenario - raises exception
>> carlos.posts.reload.first
  Post Load (0.1ms)  SELECT "posts".* FROM "posts"  WHERE "posts"."author_id" = ?  [["author_id", 1]]
=> #<Post id: 1, author_id: 1, title: "ZOMG", body: nil, created_at: "2014-01-21 11:44:22", updated_at: "2014-01-21 11:44:22">
>> carlos.posts.reload.second
  Post Load (0.1ms)  SELECT "posts".* FROM "posts"  WHERE "posts"."author_id" = ?  [["author_id", 1]]
NoMethodError: undefined method 'first' for nil:NilClass
  from....rails/rails/activerecord/lib/active_record/relation/finder_methods.rb:449:in 'find_nth'


## 2nd scenario - returns the same record
>> carlos.posts.first
=> #<Post id: 1, author_id: 1, title: "ZOMG", body: nil, created_at: "2014-01-21 11:44:22", updated_at: "2014-01-21 11:44:22">
>> carlos.posts.second
=> #<Post id: 1, author_id: 1, title: "ZOMG", body: nil, created_at: "2014-01-21 11:44:22", updated_at: "2014-01-21 11:44:22">
>> carlos.posts.third
=> #<Post id: 1, author_id: 1, title: "ZOMG", body: nil, created_at: "2014-01-21 11:44:22", updated_at: "2014-01-21 11:44:22">

@terracatta can you take a look please? It'd be good to provide some tests for already loaded associations with some of these methods. Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Absolutely! Thanks for testing!

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.

None yet

6 participants