activerecord-has_some_of_many 1.0.1 → 1.2.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: 7ab0ac9fc08cfe5684f0bc33fb071360ffd130fcb61e6ce582d0372d97875cee
4
- data.tar.gz: 747c855f75f1cac9aa5b0419107c8c4b7650db107587c11f1af7633b319cd747
3
+ metadata.gz: '0290c685fa491a5956467a8fc6109104b1622f27e524efeca9f5d147433214cd'
4
+ data.tar.gz: 8b39ae1096090a9747198730bd2a98fb87e162dd03f0692dad73b6ea374d37cf
5
5
  SHA512:
6
- metadata.gz: ee776f879139db208e90494496ca4f311398220aed654184822b586b327465a295f212c7b965a176da8231b5c2506dc8df1d2be5be7b0a0f83be0bec29ebfe02
7
- data.tar.gz: 66deb994b6d7524e0c77fd508bfc84ed8d8c55ed7af15655d88c4cccc869bc9a08b366034620fde1f8c3c3406184d1a80d955ebaf91e24be1ef3b9dde2c15f17
6
+ metadata.gz: 19388466d6c9e3ca6166cef76bc4e376cf6804be4bbafb6232505077e65b293d42d7fbb8e448c95befdc163197f8eac6b812b8bac4c95b1c3557457b0561af8c
7
+ data.tar.gz: d4752090dd3d5c239550c1ed334583d990512328c2f7cbcf8fc220b845a944779c94e6afb780f5a6c524c976710fa637a6f5a89d75abdfbaa79e5b03afe43007
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.1"
5
+ VERSION = "1.2.0"
6
6
  end
7
7
  end
@@ -5,5 +5,7 @@ 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,17 +1,31 @@
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.1
4
+ version: 1.2.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-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 7.0.0.alpha
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.0.alpha
27
+ - !ruby/object:Gem::Dependency
28
+ name: railties
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - ">="
@@ -36,6 +50,7 @@ files:
36
50
  - README.md
37
51
  - lib/activerecord-has_some_of_many.rb
38
52
  - lib/activerecord/has_some_of_many/associations.rb
53
+ - lib/activerecord/has_some_of_many/relation_rewriter.rb
39
54
  - lib/activerecord/has_some_of_many/version.rb
40
55
  homepage: https://github.com/bensheldon/activerecord-has_some_of_many
41
56
  licenses: