-
Notifications
You must be signed in to change notification settings - Fork 898
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
Allow models to include methods for MiqExpression sql evaluation #17562
Allow models to include methods for MiqExpression sql evaluation #17562
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this solution, just minor suggestions.
Do want a second opinion
app/models/hardware.rb
Outdated
@@ -31,7 +31,7 @@ class Hardware < ApplicationRecord | |||
virtual_aggregate :allocated_disk_storage, :disks, :sum, :size | |||
|
|||
def ipaddresses | |||
@ipaddresses ||= networks.collect(&:ipaddress).compact.uniq + networks.collect(&:ipv6address).compact.uniq | |||
@ipaddresses ||= networks.pluck(:ipaddress, :ipv6address).flatten.tap(&:compact!).uniq! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for some reason I'd like to see a networks.loaded?
around this.
Wonder if the solution for our problem is just to modify the yml file and preload the networks.
(download everything solutions :( )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nah, I think that isn't a bad idea. It also seems like I have broken something with this in the test suite, so I might not include this change in the final PR. I mostly included it because we did try it when testing some things out on Friday, but probably doesn't help with the long term fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, so the thought is to remove this
app/models/vm_or_template.rb
Outdated
@@ -1618,6 +1618,21 @@ def self.vms_by_ipaddress(ipaddress) | |||
end | |||
end | |||
|
|||
def self.miq_expression_includes_any_ipaddresses_arel(ipaddress) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add sample SQL at the top of this?
Arel gets hard to read/scan.
@@ -1343,6 +1352,11 @@ def to_arel(exp, tz) | |||
escape = nil | |||
case_sensitive = true | |||
arel_attribute.matches("%#{parsed_value}%", escape, case_sensitive) | |||
when "includes all", "includes any", "includes only" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this will be called if the sql query is partially sql friendly.
Can we add a respond_to?
check here as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is necessary:
The only call to to_arel
is in MiqExpression#to_sql
:
https://github.com/search?q=org%3AManageIQ+to_arel&type=Code
And in the line previous, we call #preprocess_for_sql
, which will make the check in for the change I added in #sql_supports_atom?
, so I think we don't need an additional check here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, #preprocess_for_sql
will remove any non-SQL-friendly portions of the expression, so accidentally calling this when it doesn't support it will not happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there value in "including" includes all
and includes only
if we're not going to get here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there value in "including"
includes all
andincludes only
if we're not going to get here?
I think this is answered in https://github.com/ManageIQ/manageiq/pull/17562/files#r194541491
(and yes... I noticed the pun...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(and yes... I noticed the pun...)
Yay!
140ec59
to
8fd09bb
Compare
# Support this only from the main model (for now) | ||
if exp[operator].keys.include?("field") && exp[operator]["field"].split(".").length == 1 | ||
model, field = exp[operator]["field"].split("-") | ||
method = "miq_expression_#{operator.downcase.tr(' ', '_')}_#{field}_arel" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aside: I feel like we'll never know we should implement this method for other models if they're having the same problem. We probably don't want to log that we're doing the horrible ruby stuff instead of blazing fast sql every time but it feels like we should somehow inform other models they might want to implement it. Maybe? I don't know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I took a bit of time to think about this one.
The best idea I could come up with was to add some kind of rake task to check against every field that MiqExpression
can be built against, and see if it can be supported in SQL for "includes all", "includes any", and "includes only". That way, we can get a remaining list, and maybe farm these out at a "Hacktoberfest", or as we find time here and there.
Otherwise, I don't have a way to automate this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I don't have a better suggestion. It just seems really hard to ask someone to add this support for other operators/models/etc.
networks.collect(&:ipaddress).compact.uniq + networks.collect(&:ipv6address).compact.uniq | ||
else | ||
networks.pluck(:ipaddress, :ipv6address).flatten.tap(&:compact!).tap(&:uniq!) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thnx nick
@@ -344,6 +344,15 @@ def sql_supports_atom?(exp) | |||
# TODO: Support includes operator for sub-sub-tables | |||
return false | |||
end | |||
when "includes any", "includes all", "includes only" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we planning on implementing includes all and includes only in the future?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So my thought with this is that they would all fall in the same vain eventually, so just put them here for now since this is probably where those three would go in the future anyway. Yes, it is extra lines of code now, but in the future it would be how we would probably be where we through this anyway, so figured I would just save the effort of remembering to do that later.
That said, I could look into includes all
and includes only
implementations for Vm.ipaddresses
if you think that has a use right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose it doesn't matter as long as we have tests demonstrating that each goes through arel
or ruby based upon the operator.
hardwares = Hardware.arel_table | ||
|
||
match_grouping = networks[:ipaddress].matches("%#{ipaddress}%") | ||
.or(networks[:ipv6address].matches("%#{ipaddress}%")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very specific. I wonder if it can be made more generic in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note, this is ok. I'm just unsure what I would do if we were to try to expand this to other operators/models/fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, not sure there is a good way to abstract this. To me, it seems like this has to be done on a case by case bases, maybe with some abstraction for certain fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the pattern of 1 = (select 1 from relation where CRITERIA)
is pretty generic. Very common for oracle queries. Wonder if we could do that with some pattern
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, the 1 =
bit is generic, but not the rest of the WHERE
clause, which is the bulk of things being defined in this method.
Personally, I think I would rather determine a pattern after we see on become more defined after adding more of these methods, instead of trying to figure out one now. YAGNI and all of that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it was more something that looked odd... being so specific for this problem. We can always make it more generic later.
query = Vm.miq_expression_includes_any_ipaddresses_arel("10.10.10") | ||
result = Vm.where(query) | ||
expect(result.to_a).to eq([subject]) | ||
end.to match_query_limit_of(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the miq_expression spec, do we need?
- a test to ensure we go through
to_ruby
for theincludes all
andincludes only
operators? (and vice versa for thesql_supports_atom?
method) - a test showing that
includes any
is true forsql_supports_atom?
(and vice versa forto_ruby
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could do that, sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, so I don't think what you suggested makes sense, since .to_ruby
doesn't strip out the "SQL-able bits". I basically tested INCLUDES ALL
, INCLUDES ANY
, and INCLUDES ONLY
against .to_sql
, and confirmed that only INCLUDES ANY
with Vm.ipaddresses
gets turned to SQL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it makes sense... you might find that some of them are redundant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looking nice
app/models/hardware.rb
Outdated
@@ -31,7 +31,7 @@ class Hardware < ApplicationRecord | |||
virtual_aggregate :allocated_disk_storage, :disks, :sum, :size | |||
|
|||
def ipaddresses | |||
@ipaddresses ||= networks.collect(&:ipaddress).compact.uniq + networks.collect(&:ipv6address).compact.uniq | |||
@ipaddresses ||= networks.pluck(:ipaddress, :ipv6address).flatten.tap(&:compact!).uniq! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, so the thought is to remove this
hardwares = Hardware.arel_table | ||
|
||
match_grouping = networks[:ipaddress].matches("%#{ipaddress}%") | ||
.or(networks[:ipv6address].matches("%#{ipaddress}%")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the pattern of 1 = (select 1 from relation where CRITERIA)
is pretty generic. Very common for oracle queries. Wonder if we could do that with some pattern
bf93327
to
f2785d0
Compare
@miq-bot add_label blocker |
@NickLaMuro if this can be backported, please add the gaprindashvili/yes label. |
it "does not generate SQL for an INCLUDES ONLY without an expression method" do | ||
sql, _, attrs = MiqExpression.new("INCLUDES ONLY" => {"field" => "Vm-ipaddresses", "value" => "foo"}).to_sql | ||
expect(sql).to be nil | ||
expect(attrs).to eq(:supported_by_sql => false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🥇
@NickLaMuro Looks good to me. Can you fix any relevant cop complaints and add the BZ to the commit messages? Finally, do you have numbers for number of vms, hardwares, networks, etc. and time before/after this change? |
@NickLaMuro do you have an understanding of when networks is loaded (and how frequently) and can't use this optimization? Is it an actual problem? |
@jrafanie sorry, that commit message wasn't updated from when I only had the |
@jrafanie For this cop:
I am tempted leave it as is. Part of what I was trying to do was make the 1 = (SELECT 1
FROM hardwares
JOIN networks ON networks.hardware_id = hardwares.id
WHERE hardwares.vm_or_template_id = vms.id
AND (networks.ipaddress LIKE "%IPADDRESS%"
OR networks.ipv6address LIKE "%IPADDRESS%")
LIMIT 1
) Should match with what I was trying to do in the code. With the
This is just one issue, duplicated to two cops, and while I honestly think it looks better with the extra spacing, it isn't worth arguing over, so I will make the change with my addition of the BZ link in the commit message and other rebasing tasks... you slavedriver... |
Yeah, I will add some benchmarking numbers after I do the rebase. |
# AND (networks.ipaddress LIKE "%IPADDRESS%" | ||
# OR networks.ipv6address LIKE "%IPADDRESS%") | ||
# LIMIT 1 | ||
# ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
select 'ya' where 1 = (select 1 where 1=2); -- works (negative)
select 'ya' where 1 = (select 1); -- works (positive)
select 'ya' where 1 = (select 1 union all select 1); -- fails
ERROR: more than one row returned by a subquery used as an expression
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aaah - LIMIT 1
. good job. lookin' good
This should avoid a query when networks are already loaded, but if not, will make the minimal amount of object allocations and queries necessary to fetch the data and massage it into the expected format.
This sets up support MiqExpression to introspect the current model of the expression fragment to see if there are any methods defined there that can help allow SQL to be executed in MiqExpression for that given field. Vm.ipaddresses has been the first attempt at this. Fixes https://bugzilla.redhat.com/show_bug.cgi?id=1588082
f2785d0
to
1cad1ab
Compare
Checked commits NickLaMuro/manageiq@938efff~...1cad1ab with ruby 2.3.3, rubocop 0.52.1, haml-lint 0.20.0, and yamllint 1.10.0 app/models/vm_or_template.rb
|
@jrafanie benchmarks added to the PR description. |
@miq-bot add_label gaprindashvili/yes |
.join(networks).on(networks[:hardware_id].eq(hardwares[:id])) | ||
.where(hardwares[:vm_or_template_id].eq(vms[:id]).and(match_grouping)) | ||
.take(1) | ||
Arel::Nodes::SqlLiteral.new("1").eq(query) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
taken from another PR of yours:
Arel::Nodes::SqlLiteral.new("1").eq(
VmOrTemplate.select(1).joins(:hardware => :networks).where(match_group).limit(1)
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kbrock Doesn't work. You are mixing and matching pure arel
with ActiveRecord::Relations
. You can inject arel
into a Relation
, but not vice versa, which you are doing here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I approve of 30s -> 0.3s (22k queries -> 9)
…es_exp_sql_support Allow models to include methods for MiqExpression sql evaluation (cherry picked from commit 52176a7) Fixes https://bugzilla.redhat.com/show_bug.cgi?id=1591422
Gaprindashvili backport details:
|
This is currently a first pass at making it so models can define methods for SQL fragments for specific expression types. Specifically, this allows the "INCLUDE ANY" to work in SQL for following expression:
Also of note: The ruby implementation of
INCLUDES ANY
doesn't seem work as expected, so not sure if this is a bug withMiqExpression
or I have implemented this expression in SQL incorrectly.Benchmarks
So there is some additional slowness issues that I noticed on master that don't seem to be present on the
gaprindashvili
branch, but they seem to be mitigated by this branch. That said, seems like they areLEFT JOIN
based issues, and could be an issue when requesting more that 20 Vms per-page, so it might be worth looking into at a later date.For now, as show in the benchmarks, the times are WAY faster (and this isn't including another 200k ms request that also was slow on master, but now isn't an issue), so I think this fix is fine on it's own.
BEFORE
AFTER
Links
Steps for Testing/QA
Go to
Compute
>Infrastructure
>VirtualMachines
Select "All VMs" from the tree on the left
Click the down arrow to the right of the search box
Create the following query (replace "XXX.XXX.XXX" with something that makes sense for your environment)
On master, this should be quite slow on a environment with a reasonable number of VMs (10k+), but very quick with this patch.