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

api sort on sql friendly virtual attributes #13409

Merged
merged 4 commits into from
Jan 16, 2017
Merged

api sort on sql friendly virtual attributes #13409

merged 4 commits into from
Jan 16, 2017

Conversation

jntullo
Copy link

@jntullo jntullo commented Jan 9, 2017

This updates the sort directive to allow for sorting on sql friendly virtual attributes. Adds an additional error message to indicate the collection cannot be sorted by the non-sql friendly virtual attribute.

@miq-bot assign @abellotti
@miq-bot add_label api, enhancement

cc: @jeff-phillips-18

@@ -161,6 +161,8 @@ def sort_params(klass)
options = String(params['sort_options']).split(",")
params['sort_by'].split(",").zip(orders).collect do |attr, order|
if klass.attribute_method?(attr) || klass.method_defined?(attr) || attr == klass.primary_key
raise BadRequestError,
"#{klass.name} cannot be sorted by #{attr}" unless klass.attribute_supported_by_sql?(attr)
Copy link
Contributor

Choose a reason for hiding this comment

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

Just wondering if attribute_supported_by_sql? implies the condition on 163? Could this be simplified?

Copy link
Author

Choose a reason for hiding this comment

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

@imtayadeway no it's not implied on 163. I will try to simplify it, but I also want to send back that specific error message, rather than the one below that indicates it is not an attribute (since it is, just not a sortable one)

sort_item << " ASC" if order && order.downcase.start_with?("asc")
sort_item << " DESC" if order && order.downcase.start_with?("desc")
sort_item
if options.map(&:downcase).include?("ignore_case")
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this a change of behavior or a refactoring?

Copy link
Author

Choose a reason for hiding this comment

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

@imtayadeway change of behavior. Format needs to be { :attribute => order } rather than the <attribute> <ORDER>

Copy link
Contributor

Choose a reason for hiding this comment

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

Does this preserve the behavior of doing ignore case/descending?

Copy link
Author

Choose a reason for hiding this comment

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

@imtayadeway good catch I'll update it

Copy link
Contributor

Choose a reason for hiding this comment

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

@jntullo if this was green we must have insufficient test coverage here....can you add some to verify existing behavior? Thanks!

Copy link
Author

Choose a reason for hiding this comment

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

@imtayadeway so, afaik (spent a bit of time diving into this) it's not possible to add the LOWER to the hash that's needed to sort on the virtual_attributes. Because it was getting a little icky, I decided to account for that by adding in another method for sorting of virtual attributes, but not sure how I feel about that. Thoughts, suggestions?

Maybe @kbrock can provide some insight into case-insensitive sort on virtual attributes? reorder(LOWER(attr) does not work, and it requires the format .reorder({:attr => 'asc'}). Is there a way to add case insensitive sort to this?

Copy link
Member

@kbrock kbrock left a comment

Choose a reason for hiding this comment

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

This is looking great.

Just one main comment.
Possibly, it will allow you to keep all in sort_directive and not have to implement 2 different methods for this to work


def sort_virtual_attr_directive(klass, attr, order, options)
raise BadRequestError,
"#{klass.name} cannot be sorted by #{attr}" unless klass.attribute_supported_by_sql?(attr)
Copy link
Member

Choose a reason for hiding this comment

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

This is great.

In reports/views, we do support sorting by non sql friendly attributes.
It is a major memory hit and very slow.

@@ -175,6 +177,18 @@ def sort_directive(attr, order, options)
sort_item << " DESC" if order && order.downcase.start_with?("desc")
sort_item
end

def sort_virtual_attr_directive(klass, attr, order, options)
Copy link
Member

Choose a reason for hiding this comment

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

darn, was hoping that virtual and regular ones can be implemented together.

raise BadRequestError,
"#{klass.name} cannot be sorted with ignored case" if options.map(&:downcase).include?("ignore_case")
if order
{ attr.to_sym => order.downcase }
Copy link
Member

Choose a reason for hiding this comment

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

maybe something like:

klass.arel_attribute(attr).downcase.send(order.downcase)

I wonder if there is a way for us to reuse miq_report/search.rb#get_order_info

Also, a while ago, arel_attribute().lower was not supported for either attributes or virtual attributes - can't quite remember. So instead of the Arel::Nodes::Lower you may want to just try .lower and see if it will work

Copy link
Author

Choose a reason for hiding this comment

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

@kbrock that worked! Thank you!

@kbrock
Copy link
Member

kbrock commented Jan 11, 2017

kicking

@kbrock kbrock closed this Jan 11, 2017
@kbrock kbrock reopened this Jan 11, 2017
@@ -160,20 +160,24 @@ def sort_params(klass)
orders = String(params['sort_order']).split(",")
options = String(params['sort_options']).split(",")
params['sort_by'].split(",").zip(orders).collect do |attr, order|
if klass.attribute_method?(attr) || klass.method_defined?(attr) || attr == klass.primary_key
sort_directive(attr, order, options)
if klass.virtual_attribute?(attr) && !klass.attribute_supported_by_sql?(attr)
Copy link
Member

Choose a reason for hiding this comment

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

you can probably drop klass.virtual_attribute?(attr)

Copy link
Member

@kbrock kbrock Jan 11, 2017

Choose a reason for hiding this comment

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

heck, can you just do:

if klass.attribute_supported_by_sql?(attr)
  sort_directive(klass, attr, order, options)
else
  raise BadRequestError, "#{klass.name} cannot be sorted by #{attr}"
end

(I probably missed something obvious here - e.g.: may need attr == klass.primary_key)

Copy link
Author

Choose a reason for hiding this comment

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

@kbrock would definitely be more simplistic, but I thought it might be a good idea to have both the is not a valid attribute and cannot be sorted by messages separate for clarification

Copy link
Member

@kbrock kbrock Jan 12, 2017

Choose a reason for hiding this comment

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

ok

I don't think this part is necessary:

klass.attribute_method?(attr) || klass.method_defined?(attr) || attr == klass.primary_key

@abellotti you ok just using klass.attribute_supported_by_sql?(attr) instead and dropping the ever neat previous one?

Did we need to support any ruby method or only fields?

Copy link
Member

Choose a reason for hiding this comment

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

While the simpler path is more elegant, I like what @jntullo did to have the 2 different error messages. I'm ok with this approach here as this is low count iteration.

sort_item << " ASC" if order && order.downcase.start_with?("asc")
sort_item << " DESC" if order && order.downcase.start_with?("desc")
sort_item
def sort_directive(klass, attr, order, options)
Copy link
Member

Choose a reason for hiding this comment

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

whoa - now THAT is what I'm talking about

sort_item
def sort_directive(klass, attr, order, options)
if options.map(&:downcase).include?("ignore_case") && order
klass.arel_attribute(attr).lower.send(order.downcase)
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems unlikely, but could send be exploited? At the very least I think public_send might be better.

Copy link
Member

@abellotti abellotti left a comment

Choose a reason for hiding this comment

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

👍 very nice enhancement @jntullo. Thanks !!

run_get vms_url, :sort_by => 'host_name', :sort_order => 'desc', :expand => 'resources'

expect_query_result(:vms, 2, 2)
expect_result_resources_to_match_hash([{'name' => 'vm_bar'}, {'name' => 'vm_foo'}])
Copy link
Member

Choose a reason for hiding this comment

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

if we're sorting in descending order by host_name, shouldn't the list coming back be vm_foo then vm_bar ?

Copy link
Member

Choose a reason for hiding this comment

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

Huh. Is there a bug in the core for sorting?

Curious what the sql is for this query?

Copy link
Author

Choose a reason for hiding this comment

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

@kbrock oh...yikes...I totally missed that

Copy link
Author

Choose a reason for hiding this comment

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

@abellotti turned out to be a mistake in the test. a facepalm moment. good to go now 👍

@@ -160,20 +160,24 @@ def sort_params(klass)
orders = String(params['sort_order']).split(",")
options = String(params['sort_options']).split(",")
params['sort_by'].split(",").zip(orders).collect do |attr, order|
if klass.attribute_method?(attr) || klass.method_defined?(attr) || attr == klass.primary_key
sort_directive(attr, order, options)
if klass.virtual_attribute?(attr) && !klass.attribute_supported_by_sql?(attr)
Copy link
Member

Choose a reason for hiding this comment

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

While the simpler path is more elegant, I like what @jntullo did to have the 2 different error messages. I'm ok with this approach here as this is low count iteration.

changing from send to public send

fix spec

simplifying sort params to use supported by sql
sort_item << " DESC" if order && order.downcase.start_with?("desc")
sort_item
def sort_directive(klass, attr, order, options)
if options.map(&:downcase).include?("ignore_case") && order
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit, but I noticed all three branches had the same code, and two of them contained the same condition. I don't know if something like this is any better:

arel = klass.arel_attribute(attr)
if order
  if options.map(&:downcase).include?("ignore_case")
    arel = arel.lower
  end
  arel = arel.asc if order.downcase == "asc"
  arel = arel.desc if order.downcase == "desc" # more verbose, but lower cognitive load than dynamic method sending
end
arel

Copy link
Member

@kbrock kbrock Jan 16, 2017

Choose a reason for hiding this comment

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

Think arel.asc is not necessary.

So you can skip the entire line: arel = arel.asc if order.downcase == "asc"

if order
if options.map(&:downcase).include?("ignore_case") && order
arel = arel.lower
end
Copy link
Member

Choose a reason for hiding this comment

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

no need for && order above, you can replace lines 176-178 with:

  arel = arel.lower if options.map(&:downcase).include?("ignore_case")

@abellotti
Copy link
Member

Thanks @jntullo for the update, LGTM!! Waiting for 🍏

@miq-bot
Copy link
Member

miq-bot commented Jan 16, 2017

Checked commits jntullo/manageiq@f1f932d~...3f7034b with ruby 2.2.5, rubocop 0.37.2, and haml-lint 0.16.1
2 files checked, 0 offenses detected
Everything looks good. ⭐

@abellotti abellotti merged commit bd3834b into ManageIQ:master Jan 16, 2017
@abellotti abellotti added this to the Sprint 52 Ending Jan 16, 2017 milestone Jan 16, 2017
run_get vms_url, :sort_by => 'host_name', :sort_order => 'desc', :expand => 'resources'

expect_query_result(:vms, 2, 2)
expect_result_resources_to_match_hash([{'name' => 'vm_foo'}, {'name' => 'vm_bar'}])
Copy link
Contributor

Choose a reason for hiding this comment

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

From the outside, it looks like this query returned your objects in the order you created them. I think to prove that they were ordered successfully it may be necessary to create three objects out of order, so that you can see that the result put them into some order in a way that couldn't be an accident.

Copy link
Member

Choose a reason for hiding this comment

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

@jntullo can you create another PR to enhance the tests as per @imtayadeway ? Thanks.

Copy link
Author

Choose a reason for hiding this comment

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

@abellotti will do!

it 'supports sql friendly virtual attributes' do
host_foo = FactoryGirl.create(:host, :name => 'foo')
host_bar = FactoryGirl.create(:host, :name => 'bar')
vm_foo = FactoryGirl.create(:vm, :name => 'vm_foo')
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you try :host => host_foo here?

@kbrock
Copy link
Member

kbrock commented Jan 16, 2017

🎉

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.

5 participants