includes_many 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +24 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +14 -0
  5. data/Appraisals +18 -0
  6. data/Gemfile +14 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +66 -0
  9. data/Rakefile +18 -0
  10. data/certs/razum2um.pem +21 -0
  11. data/gemfiles/.gitignore +1 -0
  12. data/gemfiles/rails_3_2.gemfile +14 -0
  13. data/gemfiles/rails_4_0.gemfile +14 -0
  14. data/gemfiles/rails_4_1.gemfile +14 -0
  15. data/gemfiles/rails_4_2.gemfile +17 -0
  16. data/includes_many.gemspec +30 -0
  17. data/lib/includes_many.rb +19 -0
  18. data/lib/includes_many/active_record32.rb +4 -0
  19. data/lib/includes_many/active_record32/association_scope.rb +93 -0
  20. data/lib/includes_many/active_record32/has_many.rb +43 -0
  21. data/lib/includes_many/active_record32/join_association.rb +37 -0
  22. data/lib/includes_many/active_record40.rb +4 -0
  23. data/lib/includes_many/active_record40/association_scope.rb +97 -0
  24. data/lib/includes_many/active_record40/has_many.rb +32 -0
  25. data/lib/includes_many/active_record40/join_association.rb +35 -0
  26. data/lib/includes_many/active_record41.rb +4 -0
  27. data/lib/includes_many/active_record41/association_scope.rb +88 -0
  28. data/lib/includes_many/active_record41/has_many.rb +34 -0
  29. data/lib/includes_many/active_record41/join_association.rb +30 -0
  30. data/lib/includes_many/active_record42.rb +4 -0
  31. data/lib/includes_many/active_record42/association_scope.rb +81 -0
  32. data/lib/includes_many/active_record42/has_many.rb +34 -0
  33. data/lib/includes_many/active_record42/join_association.rb +30 -0
  34. data/lib/includes_many/version.rb +3 -0
  35. data/spec/includes_many_spec.rb +83 -0
  36. data/spec/internal/app/models/comment.rb +12 -0
  37. data/spec/internal/config/database.yml +9 -0
  38. data/spec/internal/db/schema.rb +6 -0
  39. data/spec/internal/log/.gitkeep +0 -0
  40. data/spec/spec_helper.rb +26 -0
  41. metadata +207 -0
@@ -0,0 +1,34 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class Preloader
4
+ class HasMany
5
+ safe_monkeypatch :owners_by_key, md5: '70b0628dd3c79928ee0bcff75052894a'
6
+
7
+ def owners_by_key
8
+ @owners_by_key ||= begin
9
+ res = Hash.new { |h,k| h[k] = Set.new }
10
+ owners.each do |owner|
11
+ key = if owner_key_name.respond_to?(:call)
12
+ owner_key_name.call(owner)
13
+ else
14
+ owner[owner_key_name]
15
+ end
16
+
17
+ if key.respond_to?(:each)
18
+ key.each do |k|
19
+ k = k.to_s if key_conversion_required?
20
+ res[k] << owner
21
+ end
22
+ else
23
+ key = key.to_s if key_conversion_required?
24
+ res[key] << owner
25
+ end
26
+ end
27
+ res
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,30 @@
1
+ module ActiveRecord
2
+ class NonScalarPrimaryKeyError < ActiveRecordError
3
+ def initialize(reflection)
4
+ super("Can not join association #{reflection.name.inspect}, because :primary_key is a callable. Use Relation#includes or specify a join column name")
5
+ end
6
+ end
7
+
8
+ module Associations
9
+ class JoinDependency
10
+ class JoinAssociation
11
+ safe_monkeypatch :initialize, md5: '2332c88140aa9e9efa96975fd48e6958'
12
+
13
+ def initialize(reflection, children)
14
+
15
+ # PATCH here
16
+ if reflection.options[:primary_key].respond_to?(:call)
17
+ raise NonScalarPrimaryKeyError.new(reflection)
18
+ end
19
+ # end PATCH
20
+
21
+ super(reflection.klass, children)
22
+
23
+ @reflection = reflection
24
+ @tables = nil
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,3 @@
1
+ module IncludesMany
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,83 @@
1
+ require 'spec_helper'
2
+
3
+ class SQLCounter
4
+ attr_accessor :log, :ignore
5
+
6
+ def initialize
7
+ @log = []
8
+ @ignore = [/^PRAGMA (?!(table_info))/,
9
+ /^SELECT currval/, /^SELECT CAST/,
10
+ /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/,
11
+ /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/,
12
+ /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/,
13
+ /^BEGIN/, /^COMMIT/]
14
+ end
15
+
16
+ def call(name, start, finish, message_id, values)
17
+ sql = values[:sql]
18
+ return if 'CACHE' == values[:name] || ignore.any? { |x| x =~ sql }
19
+ @log << sql
20
+ end
21
+ end
22
+
23
+ RSpec.describe IncludesMany do
24
+ describe 'self_siblings_and_children' do
25
+ let(:parent) { Comment.create! :body => 'some comment' }
26
+ let(:child1) { Comment.create! :body => 'child1', :parent => parent }
27
+ let(:child2) { Comment.create! :body => 'child2', :parent => parent }
28
+ let(:child3) { Comment.create! :body => 'child3', :parent => parent }
29
+ let(:subchild11) { Comment.create! :body => 'subchild11', :parent => child1 }
30
+ let(:subchild12) { Comment.create! :body => 'subchild12', :parent => child1 }
31
+ let(:subchild21) { Comment.create! :body => 'subchild21', :parent => child2 }
32
+ let(:subchild31) { Comment.create! :body => 'subchild31', :parent => child3 }
33
+
34
+ let(:subscriber) { SQLCounter.new }
35
+
36
+ before do
37
+ Comment.find_each(&:destroy)
38
+ subchild11
39
+ subchild12
40
+ subchild21
41
+ subchild31
42
+ ActiveSupport::Notifications.subscribe('sql.active_record', subscriber)
43
+ end
44
+
45
+ it 'works', :focus do
46
+ x = child1.self_siblings_and_children.to_a
47
+ expect(x.map(&:body).sort).to eq ["child1", "child2", "child3", "subchild11", "subchild12"]
48
+ # expect(child2.self_siblings_and_children.map(&:body).sort).to eq ["child1", "child2", "child3", "subchild21"]
49
+ # expect(child3.self_siblings_and_children.map(&:body).sort).to eq ["child1", "child2", "child3", "subchild31"]
50
+ end
51
+
52
+ it 'executes N+1 if not used includes' do
53
+ expect {
54
+ childs = Comment.where("body like 'child%'").order('body ASC')
55
+ expect(childs.map(&:self_siblings_and_children).map(&:size).flatten).to eq [5, 4, 4]
56
+ }.to change { subscriber.log.size } .from(0).to(4)
57
+ end
58
+
59
+ it 'includes relation' do
60
+ expect {
61
+ childs = Comment.where("body like 'child%'").order('body ASC').includes(:self_siblings_and_children)
62
+ expect(childs.map(&:self_siblings_and_children).map(&:size).flatten).to eq [5, 4, 4]
63
+ }.to change { subscriber.log.size } .from(0).to(2)
64
+
65
+ expect(subscriber.log.first).to match Regexp.new( #Regexp.escape(
66
+ %q{SELECT "comments".* FROM "comments"\s+WHERE \(body like 'child%'\)\s+ORDER BY body ASC}
67
+ )
68
+
69
+ expect(subscriber.log.last).to match Regexp.new( #Regexp.escape(
70
+ %q{SELECT "comments".* FROM "comments"\s+WHERE "comments"."parent_id" IN}
71
+ )
72
+ end
73
+
74
+ it 'denies join on includes_many' do
75
+ expect {
76
+ Comment.where("body like 'child%'").order('body ASC').joins(:self_siblings_and_children).to_a
77
+ }.to raise_error(
78
+ ActiveRecord::NonScalarPrimaryKeyError,
79
+ /Can not join association :self_siblings_and_children, because :primary_key is a callable/
80
+ )
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,12 @@
1
+ class Comment < ::ActiveRecord::Base
2
+ belongs_to :parent, :class_name => 'Comment', :foreign_key => :parent_id
3
+ has_many :children, :class_name => 'Comment', :foreign_key => :parent_id
4
+ has_many :self_siblings, :class_name => 'Comment',
5
+ :foreign_key => :parent_id, :primary_key => :parent_id
6
+
7
+ includes_many :self_or_children, :class_name => 'Comment',
8
+ :foreign_key => proc { parent_id && :parent_id or :id }, :primary_key => :id
9
+
10
+ includes_many :self_siblings_and_children, :class_name => 'Comment',
11
+ :foreign_key => :parent_id, :primary_key => proc { |c| [c.parent_id, c.id] }
12
+ end
@@ -0,0 +1,9 @@
1
+ development: &development
2
+ adapter: sqlite3
3
+ database: db/includes_many.sqlite3
4
+
5
+ production:
6
+ <<: *development
7
+
8
+ test:
9
+ <<: *development
@@ -0,0 +1,6 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table(:comments, :force => true) do |t|
3
+ t.string :body
4
+ t.integer :parent_id
5
+ end
6
+ end
File without changes
@@ -0,0 +1,26 @@
1
+ require 'bundler/setup'
2
+ require 'combustion'
3
+ require 'pry-debugger'
4
+
5
+ Combustion.initialize! :active_record
6
+
7
+ RSpec.configure do |config|
8
+ config.filter_run :focus
9
+ config.run_all_when_everything_filtered = true
10
+
11
+ if config.files_to_run.one?
12
+ config.default_formatter = 'doc'
13
+ end
14
+
15
+ config.order = :random
16
+ Kernel.srand config.seed
17
+
18
+ config.expect_with :rspec do |expectations|
19
+ expectations.syntax = :expect
20
+ end
21
+
22
+ config.mock_with :rspec do |mocks|
23
+ mocks.syntax = :expect
24
+ mocks.verify_partial_doubles = true
25
+ end
26
+ end
metadata ADDED
@@ -0,0 +1,207 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: includes_many
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Vlad Bokov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '4.2'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '4.2'
33
+ - !ruby/object:Gem::Dependency
34
+ name: safe_monkeypatch
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.1'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.1'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.6'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.6'
61
+ - !ruby/object:Gem::Dependency
62
+ name: sqlite3
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rspec
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rake
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: combustion
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: appraisal
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ description: ActiveRecord has_many + includes(:relation) on steroids
132
+ email:
133
+ - razum2um@mail.ru
134
+ executables: []
135
+ extensions: []
136
+ extra_rdoc_files: []
137
+ files:
138
+ - ".gitignore"
139
+ - ".rspec"
140
+ - ".travis.yml"
141
+ - Appraisals
142
+ - Gemfile
143
+ - LICENSE.txt
144
+ - README.md
145
+ - Rakefile
146
+ - certs/razum2um.pem
147
+ - gemfiles/.gitignore
148
+ - gemfiles/rails_3_2.gemfile
149
+ - gemfiles/rails_4_0.gemfile
150
+ - gemfiles/rails_4_1.gemfile
151
+ - gemfiles/rails_4_2.gemfile
152
+ - includes_many.gemspec
153
+ - lib/includes_many.rb
154
+ - lib/includes_many/active_record32.rb
155
+ - lib/includes_many/active_record32/association_scope.rb
156
+ - lib/includes_many/active_record32/has_many.rb
157
+ - lib/includes_many/active_record32/join_association.rb
158
+ - lib/includes_many/active_record40.rb
159
+ - lib/includes_many/active_record40/association_scope.rb
160
+ - lib/includes_many/active_record40/has_many.rb
161
+ - lib/includes_many/active_record40/join_association.rb
162
+ - lib/includes_many/active_record41.rb
163
+ - lib/includes_many/active_record41/association_scope.rb
164
+ - lib/includes_many/active_record41/has_many.rb
165
+ - lib/includes_many/active_record41/join_association.rb
166
+ - lib/includes_many/active_record42.rb
167
+ - lib/includes_many/active_record42/association_scope.rb
168
+ - lib/includes_many/active_record42/has_many.rb
169
+ - lib/includes_many/active_record42/join_association.rb
170
+ - lib/includes_many/version.rb
171
+ - spec/includes_many_spec.rb
172
+ - spec/internal/app/models/comment.rb
173
+ - spec/internal/config/database.yml
174
+ - spec/internal/db/schema.rb
175
+ - spec/internal/log/.gitkeep
176
+ - spec/spec_helper.rb
177
+ homepage: https://github.com/razum2um/include_many
178
+ licenses:
179
+ - MIT
180
+ metadata: {}
181
+ post_install_message:
182
+ rdoc_options: []
183
+ require_paths:
184
+ - lib
185
+ required_ruby_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ required_rubygems_version: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ requirements: []
196
+ rubyforge_project:
197
+ rubygems_version: 2.2.2
198
+ signing_key:
199
+ specification_version: 4
200
+ summary: ActiveRecord has_many + includes(:relation) on steroids
201
+ test_files:
202
+ - spec/includes_many_spec.rb
203
+ - spec/internal/app/models/comment.rb
204
+ - spec/internal/config/database.yml
205
+ - spec/internal/db/schema.rb
206
+ - spec/internal/log/.gitkeep
207
+ - spec/spec_helper.rb