Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

learn ActiveReccord::Querying#order work with hash arguments #7765

Merged
merged 1 commit into from

4 participants

Tima Maslyuchenko Rafael Mendonça França Jon Leighton Allen Madsen
Tima Maslyuchenko

Added posibility work with hashes and symbols as agruments for order
When symbol or hash passed we convert it to Arel::Node, and it make requests more flexible

For example

string_order = User.where(:name => 1).order('id ASC')
string_order.to_sql # SELECT "users".* FROM "users" WHERE "users"."name" = 1 ORDER BY 'id ASC'

symbol_order = User.where(:name => 1).order(:id) # or order(id: :asc) 
symbol_order.to_sql # SELECT "users".* FROM "users" WHERE "users"."name" = 1 ORDER BY "users"."id" ASC

User.arel_table.table_alias = 'u'

string_order.to_sql # SELECT "u".* FROM "users" "u" WHERE "u"."name" = 1 ORDER BY 'id ASC'

symbol_order.to_sql # SELECT "u".* FROM "users" "u" WHERE "u"."name" = 1 ORDER BY "u"."id" ASC
Tima Maslyuchenko

In the previous verion of order implementation, when you pass string and symbol to order it create Arel::Nodes::SqlLiteral object(see here). This is like a raw sql, withour any metadata like table, etc

I have added hash support and rewrite symbol parsing, so right now it create Arel::Nodes::Ordering object, which contanins information about current arel table

Tima Maslyuchenko

@rafaelfranca @steveklabnik
Guys, can you please provide some feedback for this feature?

activerecord/lib/active_record/relation/query_methods.rb
((6 lines not shown))
o.to_s.split(',').collect do |s|
s.strip!
s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
end
+ when Symbol
+ { o => :desc }
+ when Hash
+ o.inject({}) do |memo, (field, dir)|
+ memo[field] = (dir == :asc ? :desc : :asc )
Allen Madsen
blatyo added a note

Should be either:

memo[field] = (dir == :desc ? :desc : :asc )

or

memo[field] = (dir == :asc ? :asc : :desc )
Tima Maslyuchenko
timsly added a note

Purpose of reverse_sql_order is to reverse current order directions
So if we have :asc we need to change to :desc, etc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
activerecord/lib/active_record/relation/query_methods.rb
((6 lines not shown))
o.to_s.split(',').collect do |s|
s.strip!
s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
end
+ when Symbol
+ { o => :desc }
+ when Hash
+ o.inject({}) do |memo, (field, dir)|
Rafael Mendonça França Owner

I think is better to use each_with_object here

Tima Maslyuchenko
timsly added a note

You mean like this

o.each_with_object({}) do |(field, dir), memo| 
  memo[field] = (dir == :asc ? :desc : :asc )
end

This will be a little bit shorter, but i thought using native functions will be faster

Rafael Mendonça França Owner

I remember that we are changing some inject by alternative solutions because of performance issues. I don't know if it is still valid.

Tima Maslyuchenko
timsly added a note

So i will use each_with_object then

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
activerecord/lib/active_record/relation/query_methods.rb
@@ -800,6 +805,30 @@ def reverse_sql_order(order_query)
def array_of_strings?(o)
o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
end
+
+ def build_order(arel)
+ orders = order_values
+ orders = reverse_sql_order(orders) if reverse_order_value
+
+ orders = orders.uniq.reject{|o| o.blank?}.map do |order|
+ case order
+ when Symbol
+ create_order_node(order)
+ when Hash
+ order.map { |field, dir| create_order_node(field, dir) }
+ else
+ order
+ end
+ end.flatten
Rafael Mendonça França Owner

Do we need to call flatten?

Tima Maslyuchenko
timsly added a note

We need flatten for cases such order(:id, [:date, :name])
But i agree, overall we do not need it

Tima Maslyuchenko
timsly added a note

@rafaelfranca, i remembered why i use flatten, because i found same method inside reverse_sql_order method.
So i thought this two methods should be symmetrical

Rafael Mendonça França Owner

Seems fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Rafael Mendonça França
Owner

Sorry for the delay. I like the idea. :+1:

@tenderlove @jonleighton what you guys think?

Tima Maslyuchenko

@rafaelfranca, i have removed inject and replace it with each_with_object also added few docs line for order method

Jon Leighton jonleighton commented on the diff
activerecord/lib/active_record/relation/query_methods.rb
((6 lines not shown))
o.to_s.split(',').collect do |s|
s.strip!
s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
end
+ when Symbol
+ { o => :desc }
+ when Hash
+ o.each_with_object({}) do |(field, dir), memo|
Jon Leighton Collaborator

I don't really care that much, but I hate stuff like this just for the sake of saving one object allocation. To me:

Hash[o.map { |field, dir| [field, dir.downcase == :asc ? :desc : :asc] }]

is way cleaner and easier to read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
activerecord/lib/active_record/relation/query_methods.rb
@@ -800,6 +813,30 @@ def reverse_sql_order(order_query)
def array_of_strings?(o)
o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
end
+
+ def build_order(arel)
+ orders = order_values
+ orders = reverse_sql_order(orders) if reverse_order_value
+
+ orders = orders.uniq.reject{|o| o.blank?}.map do |order|
Jon Leighton Collaborator

reject(&:blank?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
activerecord/test/cases/relations_test.rb
@@ -158,6 +158,18 @@ def test_finding_with_arel_order
assert_equal 4, topics.to_a.size
assert_equal topics(:first).title, topics.first.title
end
+
+ def test_finding_with_assoc_order
+ topics = Topic.order(:id => :DESC)
Jon Leighton Collaborator

I don't really think we should support :DESC and :desc and :DeSc. :desc is fine, we don't need to mimic sql by allowing all-caps.

Jon Leighton Collaborator

You could make id: :DESC and id: :foo etc raise an ArgumentError though.

Tima Maslyuchenko
timsly added a note

Right now, i am checking if dir in [:desc, :asc], and if not - set to :asc
Should i raise ArgumentError instead?

Jon Leighton Collaborator

Yeah, but you should raise it when #order is called, not in #build_order. So Post.order(id: :DESC) should raise an error.

Tima Maslyuchenko
timsly added a note

Hm, but this mean that i need to add extra logic into order function and parse order function arguments just for indicate if hash was passed correctly(all values are in [:asc, :desc]). Also i need to modify reorder function.

So maybe better use :asc as default direction and set it even if value not in [:asc, :desc]

Tima Maslyuchenko
timsly added a note

I just reviewed all methods like order, where, join, and all of them just update relation object. So all magic happens inside build_arel(agruments parsing, etc).

So the easiest way is to raise ArgumentError inside build_order or even create_order_node method(where i build Arel::Nodes::Ordering object)

Jon Leighton Collaborator

It definitely doesn't make sense to raise ArgumentError in build_order. Otherwise you'll have a situation like this:

posts = Post.order(id: :DESC) # ArgumentError should be raised now, as the argument to `order` is invalid
# ... some other code ...
posts.to_a # but instead it is raised now

We could just default to :asc but this seems more surprising from a user's perspective if they make a mistake.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jon Leighton
Collaborator

I am :+1: on this in general. I added a few code comments.

Tima Maslyuchenko

@jonleighton i have made few updates according to your suggestions.
I left support only for downcase directions and raised exception on invalid order directions

activerecord/lib/active_record/relation/query_methods.rb
((13 lines not shown))
+ when Hash
+ order.map { |field, dir| create_order_node(field, dir) }
+ else
+ order
+ end
+ end.flatten
+
+ arel.order(*orders) unless orders.empty?
+ end
+
+ def create_order_node(field, dir = :asc)
+ dir = :asc unless [:asc, :desc].include?(dir)
+ table[field].send(dir)
+ end
+
+ def detect_invalid_params!(args)
Jon Leighton Collaborator

The method name is too generic. I'd rename it to something like validate_order_args. I don't think the bang is necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
activerecord/lib/active_record/relation/query_methods.rb
@@ -800,6 +817,37 @@ def reverse_sql_order(order_query)
def array_of_strings?(o)
o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
end
+
+ def build_order(arel)
+ orders = order_values
+ orders = reverse_sql_order(orders) if reverse_order_value
+
+ orders = orders.uniq.reject(&:blank?).map do |order|
+ case order
+ when Symbol
+ create_order_node(order)
+ when Hash
+ order.map { |field, dir| create_order_node(field, dir) }
Jon Leighton Collaborator

I would get rid of create_order_node and change this to:

        when Symbol
          table[order].asc
        when Hash
          order.map { |field, dir| table[field].send(dir) }

We don't need to validate the dir here as we're doing that earlier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Tima Maslyuchenko

@jonleighton thanks for feadback. I have updated code

Jon Leighton
Collaborator

ok, looks good. please squash the commit and I will merge.

Tima Maslyuchenko

@jonleighton Shell i add release notes?

Jon Leighton
Collaborator

yep, good point, please do

Tima Maslyuchenko

I have added changelog and squash commits

Jon Leighton jonleighton commented on the diff
activerecord/CHANGELOG.md
@@ -1,5 +1,12 @@
## Rails 4.0.0 (unreleased) ##
+* Learn ActiveRecord::QueryMethods#order work with hash arguments
+
+ When symbol or hash passed we convert it to Arel::Nodes::Ordering.
+ If we pass invalid direction(like name: :DeSc) ActiveRecord::QueryMethods#order will raise an exception
Jon Leighton Collaborator

Please can you add a code example? It's a bit unclear what the functionality does at the moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jon Leighton
Collaborator

Please rebase against master as well, as this doesn't merge cleanly at the moment.

Jon Leighton jonleighton merged commit 1cb7cb0 into from
Rafael Mendonça França

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
10 activerecord/CHANGELOG.md
View
@@ -1,5 +1,15 @@
## Rails 4.0.0 (unreleased) ##
+* Learn ActiveRecord::QueryMethods#order work with hash arguments
+
+ When symbol or hash passed we convert it to Arel::Nodes::Ordering.
+ If we pass invalid direction(like name: :DeSc) ActiveRecord::QueryMethods#order will raise an exception
Jon Leighton Collaborator

Please can you add a code example? It's a bit unclear what the functionality does at the moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+ User.order(:name, email: :desc)
+ # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
+
+ *Tima Maslyuchenko*
+
* Rename `ActiveRecord::Fixtures` class to `ActiveRecord::FixtureSet`.
Instances of this class normally hold a collection of fixtures (records)
loaded either from a single YAML file, or from a file and a folder
51 activerecord/lib/active_record/relation/query_methods.rb
View
@@ -202,6 +202,15 @@ def group!(*args)
#
# User.order('name DESC, email')
# => SELECT "users".* FROM "users" ORDER BY name DESC, email
+ #
+ # User.order(:name)
+ # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
+ #
+ # User.order(email: :desc)
+ # => SELECT "users".* FROM "users" ORDER BY "users"."email" DESC
+ #
+ # User.order(:name, email: :desc)
+ # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
def order(*args)
args.blank? ? self : spawn.order!(*args)
end
@@ -209,6 +218,8 @@ def order(*args)
# Like #order, but modifies relation in place.
def order!(*args)
args.flatten!
+
+ validate_order_args args
references = args.reject { |arg| Arel::Node === arg }
references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
@@ -234,6 +245,8 @@ def reorder(*args)
# Like #reorder, but modifies relation in place.
def reorder!(*args)
args.flatten!
+
+ validate_order_args args
self.reordering_value = true
self.order_values = args
@@ -658,9 +671,7 @@ def build_arel
arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty?
- order = order_values
- order = reverse_sql_order(order) if reverse_order_value
- arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty?
+ build_order(arel)
build_select(arel, select_values.uniq)
@@ -786,11 +797,17 @@ def reverse_sql_order(order_query)
case o
when Arel::Nodes::Ordering
o.reverse
- when String, Symbol
+ when String
o.to_s.split(',').collect do |s|
s.strip!
s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
end
+ when Symbol
+ { o => :desc }
+ when Hash
+ o.each_with_object({}) do |(field, dir), memo|
Jon Leighton Collaborator

I don't really care that much, but I hate stuff like this just for the sake of saving one object allocation. To me:

Hash[o.map { |field, dir| [field, dir.downcase == :asc ? :desc : :asc] }]

is way cleaner and easier to read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ memo[field] = (dir == :asc ? :desc : :asc )
+ end
else
o
end
@@ -800,6 +817,32 @@ def reverse_sql_order(order_query)
def array_of_strings?(o)
o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
end
+
+ def build_order(arel)
+ orders = order_values
+ orders = reverse_sql_order(orders) if reverse_order_value
+
+ orders = orders.uniq.reject(&:blank?).map do |order|
+ case order
+ when Symbol
+ table[order].asc
+ when Hash
+ order.map { |field, dir| table[field].send(dir) }
+ else
+ order
+ end
+ end.flatten
+
+ arel.order(*orders) unless orders.empty?
+ end
+
+ def validate_order_args(args)
+ args.select { |a| Hash === a }.each do |h|
+ unless (h.values - [:asc, :desc]).empty?
+ raise ArgumentError, 'Direction should be :asc or :desc'
+ end
+ end
+ end
end
end
16 activerecord/test/cases/relations_test.rb
View
@@ -157,6 +157,22 @@ def test_finding_with_arel_order
assert_equal 4, topics.to_a.size
assert_equal topics(:first).title, topics.first.title
end
+
+ def test_finding_with_assoc_order
+ topics = Topic.order(:id => :desc)
+ assert_equal 4, topics.to_a.size
+ assert_equal topics(:fourth).title, topics.first.title
+ end
+
+ def test_finding_with_reverted_assoc_order
+ topics = Topic.order(:id => :asc).reverse_order
+ assert_equal 4, topics.to_a.size
+ assert_equal topics(:fourth).title, topics.first.title
+ end
+
+ def test_raising_exception_on_invalid_hash_params
+ assert_raise(ArgumentError) { Topic.order(:name, "id DESC", :id => :DeSc) }
+ end
def test_finding_last_with_arel_order
topics = Topic.order(Topic.arel_table[:id].asc)
Something went wrong with that request. Please try again.