activerecord-has_some_of_many 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a519b095203661135d0889d1679af1641796fd1ad08f22ef7c98018a907e700b
4
- data.tar.gz: 6619a47edd047b8fe5bbc4c8c1ecc7c1c8b0b0d4d97f5230019104e1f359f290
3
+ metadata.gz: f35b023998c92a01d9614640c84f296ac53c15ba1459daec41da6fc042b7ff8c
4
+ data.tar.gz: d004c4ce3f54eea253f86a0347d5690693b220e9f364e9c36037df91502c021e
5
5
  SHA512:
6
- metadata.gz: 166385a50777f3b578a0d14d880ccd878510545942cab07208cc8ca5f7ab88a86e448df460d591d90d2e2afd68f8de8b776b079b965052d12b51526753ae9aa3
7
- data.tar.gz: 3d76b0b5882c2baa1c597fc69fdb6a302b56c011433d8e6df370f220d1280d1f42bc18186c8fec388d3f0819d60eeec108793e2118831d6dc6738013fb0e43bf
6
+ metadata.gz: 52201a2fb0479d35037a324178434820f87eb3b527733e9d7ce797153b50bc8160fe01a38c8fbe698f86bb9728054b8a1a73030f500436586236d10c6d3a3841
7
+ data.tar.gz: b827a9092bd4d422c367b61afca9cd0d3edbe70738c9b7a49db1de6848e3c0c9a55d4c187b817389ea03d332a91f9536b1d1de78b7ba96ec327c86fdd6a891b7
data/README.md CHANGED
@@ -1,13 +1,23 @@
1
1
  # `activerecord-has_some_of_many`
2
2
 
3
- This gem adds new optimized Active Record association methods (`has_one_of_many`, `has_some_of_many`) for "top N" queries to ActiveRecord using `JOIN LATERAL` that are eager-loadable (`includes(:association)`, `preloads(:association)`) to avoid N+1 queries, and is compatible with typical queries and batch methods (`find_each`, `in_batches`, find_in_batches`). For example, you might have these types of queries in your application:
3
+ This gem adds new optimized Active Record association methods (`has_one_of_many`, `has_some_of_many`) for "top N" queries to ActiveRecord using `JOIN LATERAL` that are eager-loadable (`includes(:association)`, `preloads(:association)`) to avoid N+1 queries, and is compatible with typical queries and batch methods (`find_each`, `in_batches`, `find_in_batches`). For example, you might have these types of queries in your application:
4
4
 
5
- - Users have many posts, and you want to query the most recent post for each user
5
+ - Users have many posts, and you want to query the most recent post for each user
6
6
  - Posts have many comments, and you want to query the 5 most recent visible comments for each post
7
7
  - Posts have many comments, and you want to query the one comment with the largest `votes_count` for each post
8
8
 
9
9
  You can read more about these types of queries on [Benito Serna's "Fetching the top n per group with a lateral join with rails"](https://bhserna.com/fetching-the-top-n-per-group-with-a-lateral-join-with-rails.html).
10
10
 
11
+ ## Compatibility
12
+
13
+ This gem is only compatible with databases that offer `LATERAL` joins within Active Record. As far as I'm aware, that is **only Postgres**.
14
+
15
+ This gem is not necessary on SQLite, as SQLite will perform lateral-like behavior on join queries by default. MySQL has support for lateral queries, but they are not yet implemented in Active Record.
16
+
17
+ **Really complex queries may not work; please open an issue!** This library works by rewriting Active Record queries; it's possible to create some very complex queries with Active Record. Some things that are specifically known to work:
18
+ - Model associations to the same model. It works.
19
+ - _Please help expand this list by opening an issue if you find something that doesn't work!_
20
+
11
21
  ## Usage
12
22
 
13
23
  Add to your gemfile, and run `bundle install`:
@@ -25,7 +35,7 @@ class User < ActiveRecord::Base
25
35
  # You can also use `has_some_of_many` to get the top N records. Be sure to add a limit to the scope.
26
36
  has_some_of_many :last_five_posts, -> { order("created_at DESC").limit(5) }, class_name: "Post"
27
37
 
28
- # More complex scopees are possible, for example:
38
+ # More complex scopes are possible, for example:
29
39
  has_one_of_many :top_comment, -> { where(published: true).order("votes_count DESC") }, class_name: "Comment"
30
40
  has_some_of_many :top_ten_comments, -> { where(published: true).order("votes_count DESC").limit(10) }, class_name: "Comment"
31
41
  end
@@ -37,17 +47,17 @@ User.where(active: true).includes(:last_post, :last_five_posts, :top_comment).ea
37
47
  user.top_comment
38
48
  end
39
49
 
40
- # Aad compound indexes to your database to make these queries fast!
50
+ # Add compound indexes to your database to make these queries fast!
41
51
  add_index :comments, [:post_id, :created_at]
42
52
  add_index :comments, [:post_id, :votes_count]
43
53
  ```
44
54
 
45
55
  ## Why?
46
56
 
47
- Finding the "Top N" is a common problem, that can be easily solved with a `JOIN LATERAL` when writing raw SQL queries. Lateral Joins were introduced in Postgres 9.3:
57
+ Finding the "Top N" is a common problem, that can be easily solved with a `JOIN LATERAL` when writing raw SQL queries. Lateral Joins were introduced in Postgres 9.3:
48
58
 
49
59
  > a LATERAL join is like a SQL foreach loop, in which Postgres will iterate over each row in a result set and evaluate a subquery using that row as a parameter. ([source](https://www.heap.io/blog/postgresqls-powerful-new-join-type-lateral))
50
-
60
+
51
61
  For example, to find only the one most recent comments for a collection of posts, we might write:
52
62
 
53
63
  ```sql
@@ -33,7 +33,8 @@ module ActiveRecord
33
33
  .select(klass.arel_table[primary_key].as(foreign_key_alias), lateral_table[Arel.star])
34
34
  .arel.join(
35
35
  relation
36
- .where(relation.arel_table[foreign_key].eq(klass.arel_table[primary_key]))
36
+ .then { |query| relation.table.name == klass.arel_table.name ? ActiveRecord::HasSomeOfMany::RelationRewriter.new(query).alias_table("#{klass.table_name}__alias") : query }
37
+ .then { |query| query.where(query.table[foreign_key].eq(klass.arel_table[primary_key])) }
37
38
  .then { |query| limit ? query.limit(limit) : query }
38
39
  .arel.lateral(lateral_table.name)
39
40
  ).on("TRUE")
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module HasSomeOfMany
5
+ class RelationRewriter
6
+ def initialize(relation)
7
+ @relation = relation
8
+ end
9
+
10
+ def alias_table(alias_name)
11
+ relation_class = @relation.klass
12
+ original_table = relation_class.arel_table
13
+ alias_table = original_table.alias(alias_name)
14
+
15
+ where_clause = recursively_alias_table(@relation.where_clause.ast, original_table, alias_table)
16
+ order_clause = @relation.order_values.map do |order|
17
+ if order.respond_to?(:expr)
18
+ order.expr = alias_table[order.expr.name] if order.expr.relation == original_table
19
+ end
20
+ order
21
+ end
22
+
23
+ new_relation = @relation.dup.unscope(:select, :where, :order)
24
+ new_relation.instance_variable_set(:@table, alias_table) # Is there a better way to modify the original relation?
25
+ new_relation.where(where_clause).order(order_clause)
26
+ end
27
+
28
+ def recursively_alias_table(node, original_table, alias_table)
29
+ case node
30
+ when Arel::Nodes::And, Arel::Nodes::Or
31
+ return nil if node.children.empty?
32
+
33
+ # Recurse for left and right nodes
34
+ node.children.each { |child| recursively_alias_table(child, original_table, alias_table) }
35
+ node
36
+ when Arel::Nodes::Grouping
37
+ # Recurse for the expression inside the grouping
38
+ node.expr = recursively_alias_table(node.expr, original_table, alias_table)
39
+ node
40
+ when Arel::Nodes::Equality, Arel::Nodes::GreaterThan, Arel::Nodes::LessThan, Arel::Nodes::HomogeneousIn
41
+ # For conditions, modify left-hand side (the column)
42
+ if node.left.relation == original_table
43
+ node.left.relation = alias_table
44
+ end
45
+ node
46
+ else
47
+ node
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module HasSomeOfMany
5
- VERSION = "1.0.0"
5
+ VERSION = "1.1.0"
6
6
  end
7
7
  end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/lazy_load_hooks'
4
- require_relative "version"
4
+ require_relative "activerecord/has_some_of_many/version"
5
5
 
6
6
  ActiveSupport.on_load(:active_record) do
7
7
  require_relative "activerecord/has_some_of_many/associations"
8
+ require_relative "activerecord/has_some_of_many/relation_rewriter"
9
+
8
10
  include ActiveRecord::HasSomeOfMany::Associations
9
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-has_some_of_many
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Sheldon [he/him]
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-31 00:00:00.000000000 Z
11
+ date: 2024-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -36,6 +36,7 @@ files:
36
36
  - README.md
37
37
  - lib/activerecord-has_some_of_many.rb
38
38
  - lib/activerecord/has_some_of_many/associations.rb
39
+ - lib/activerecord/has_some_of_many/relation_rewriter.rb
39
40
  - lib/activerecord/has_some_of_many/version.rb
40
41
  homepage: https://github.com/bensheldon/activerecord-has_some_of_many
41
42
  licenses: