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

Performance Improvements for has_many through queries #205

Merged
merged 5 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### 2.13.0
* Feature Release - Adds ability to tell refine to handle join tables more efficiently
* Adds ability for user to force an index for a through relationship
### 2.12.2
* Advanced Condition Select: Fixes card styling to truncate automatically
### 2.12.1
* Category Picker - smooths out category picker and makes search mutually exclusive
### 2.12.0
Expand Down
6 changes: 3 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
refine-rails (2.12.2)
refine-rails (2.13.0)
rails (>= 6.0)

GEM
Expand Down Expand Up @@ -102,7 +102,7 @@ GEM
minitest-ci (3.4.0)
minitest (>= 5.0.6)
mysql2 (0.5.6)
net-imap (0.5.1)
net-imap (0.5.2)
date
net-protocol
net-pop (0.1.2)
Expand Down Expand Up @@ -179,7 +179,7 @@ GEM
rubocop (= 1.35.1)
rubocop-performance (= 1.14.3)
thor (1.3.2)
timeout (0.4.2)
timeout (0.4.3)
tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
unicode-display_width (2.3.0)
Expand Down
5 changes: 4 additions & 1 deletion app/models/refine/conditions/condition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class Condition
include HasMeta
include UsesAttributes
include HasRefinements
include HasThroughIdRelationship
include WithForcedIndex
include HasIcon

attr_reader :ensurances, :before_validations, :clause, :filter
Expand Down Expand Up @@ -122,7 +124,7 @@ def run_before_validate_validations(input)
# @param [Arel::Table] table The arel_table the query is built on
# @param [ActiveRecord::Relation] initial_query The base query the query is built on
# @return [Arel::Node]
def apply(input, table, initial_query, inverse_clause=false)
def apply(input, table, initial_query, inverse_clause=false, through_attribute=nil)
table ||= filter.table
# Ensurance validations are checking the developer configured correctly
run_ensurance_validations
Expand Down Expand Up @@ -150,6 +152,7 @@ def apply(input, table, initial_query, inverse_clause=false)
return
end
# No longer a relationship attribute, apply condition normally
self.attribute = through_attribute if through_attribute
nodes = apply_condition(input, table, inverse_clause)
if !is_refinement && has_any_refinements?
refined_node = apply_refinements(input)
Expand Down
10 changes: 10 additions & 0 deletions app/models/refine/conditions/has_through_id_relationship.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Refine::Conditions::HasThroughIdRelationship
def through_id_relationship
@through_id_relationship ||= false
end

def with_through_id_relationship
@through_id_relationship = true
self
end
end
25 changes: 23 additions & 2 deletions app/models/refine/conditions/uses_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,30 @@ def create_pending_has_many_through_subquery(input:, relation:, instance:, query
relation_table_being_queried = instance.klass.arel_table

relation_class = instance.klass

node_to_apply = apply(input, relation_table_being_queried, relation_class, inverse_clause)

if self.through_id_relationship
if instance.is_a? ActiveRecord::Reflection::ThroughReflection
through_reflection = instance.through_reflection
parent_foreign_key = through_reflection.foreign_key
child_foreign_key = instance.source_reflection.foreign_key
relation_table_being_queried = through_reflection.klass.arel_table
relation_class = through_reflection.klass
subquery_path = through_reflection.klass.select(parent_foreign_key).arel

node_to_apply = apply(input, relation_table_being_queried, relation_class, inverse_clause, child_foreign_key)
else
raise "with_through_id must be used with a has_many :through relationship"
end
else
node_to_apply = apply(input, relation_table_being_queried, relation_class, inverse_clause)
end

if self.forced_index
mysql_from_segment = "`#{through_reflection.table_name}` FORCE INDEX(`#{self.forced_index}`)"
# Ensuring input is sanitized
subquery_path = subquery_path.from(Arel.sql(mysql_from_segment))
end

complete_subquery = subquery_path.where(node_to_apply)
subquery = filter.get_pending_relationship_subquery || complete_subquery
filter.add_pending_relationship_subquery(subquery: subquery, primary_key: key_1(instance), secondary_key: nil, inverse_clause: inverse_clause)
Expand Down
10 changes: 10 additions & 0 deletions app/models/refine/conditions/with_forced_index.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Refine::Conditions::WithForcedIndex
def forced_index
@forced_index ||= nil
end

def with_forced_index(index_string)
@forced_index = index_string
self
end
end
2 changes: 1 addition & 1 deletion lib/refine/rails/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Refine
module Rails
VERSION = "2.12.2"
VERSION = "2.13.0"
end
end
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clickfunnels/refine-stimulus",
"version": "2.12.2",
"version": "2.13.0",
"description": "Refine is a flexible query builder for your apps. It lets your users filter down to exactly what they're looking for. Completely configured on the backend.",
"browserslist": [
"defaults",
Expand Down
47 changes: 41 additions & 6 deletions test/refine/models/conditions/negative_option_condition_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,34 @@ module Refine::Conditions
assert_equal convert(expected_sql), query.get_query.to_sql
end

it "properly handles negative option conditions with and condition" do
it "properly handles negative option conditions with through id set" do
# TODO
query = create_filter(does_not_contain_option_condition)
query = create_filter_with_through_id(does_not_contain_option_condition)
expected_sql = <<~SQL.squish
SELECT
`contacts`.*
FROM
`contacts`
WHERE (`contacts`.`id` NOT IN (SELECT
`contacts`.`id` FROM `contacts`
INNER JOIN `contacts_applied_tags` ON `contacts_applied_tags`.`contact_id` = `contacts`.`id`
INNER JOIN `contacts_tags` ON `contacts_tags`.`id` = `contacts_applied_tags`.`tag_id`
WHERE (`contacts_tags`.`id` IN (1))))
`contacts_applied_tags`.`contact_id` FROM `contacts_applied_tags`
WHERE (`contacts_applied_tags`.`tag_id` IN (1))))

SQL
assert_equal convert(expected_sql), query.get_query.to_sql
end

it "properly handles negative option conditions with through id set and forced index" do
# TODO
query = create_filter_with_through_id_forced_index(does_not_contain_option_condition, "index_contacts_applied_tags_on_contact_id")
expected_sql = <<~SQL.squish
SELECT
`contacts`.*
FROM
`contacts`
WHERE (`contacts`.`id` NOT IN (SELECT
`contacts_applied_tags`.`contact_id` FROM `contacts_applied_tags`
FORCE INDEX(`index_contacts_applied_tags_on_contact_id`)
WHERE (`contacts_applied_tags`.`tag_id` IN (1))))

SQL
assert_equal convert(expected_sql), query.get_query.to_sql
Expand All @@ -64,6 +79,26 @@ def create_filter(blueprint)
],
Contact.arel_table)
end

def create_filter_with_through_id(blueprint)
# Contacts Filter
BlankTestFilter.new(blueprint,
Contact.all,
[
OptionCondition.new("tags.id").with_options([{id: "1", display: "Option 1"}]).with_through_id_relationship
],
Contact.arel_table)
end

def create_filter_with_through_id_forced_index(blueprint, index)
# Contacts Filter
BlankTestFilter.new(blueprint,
Contact.all,
[
OptionCondition.new("tags.id").with_options([{id: "1", display: "Option 1"}]).with_through_id_relationship.with_forced_index(index)
],
Contact.arel_table)
end
end

class Contact < ActiveRecord::Base
Expand Down
Loading