miscellany 0.1.19 → 0.1.20

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: e919c60e03780d5fab1a21ae6d528482db21d5a4fcfbc176b4808cc1d9acdc77
4
- data.tar.gz: c9ecbbbb69e28e8fbef7aa9efe93db1cab9576634ede5ad2dc698180c1743dda
3
+ metadata.gz: 31028be35ab95451d190d0a669321bd265bc82230f8c07ddab8c52a4e526fcf0
4
+ data.tar.gz: ee7c51e33f2147ab49e05f3c0783dd5a8f938a931a33468d1289214233b34a4e
5
5
  SHA512:
6
- metadata.gz: 7468e0d150c79e5ebfb5760648e97699c7b88340c68b7eaa530c58e512ee982eb1d2b44c2599b374a7348a5b9cefe7d7037603a98aa5a8a895f18bac6d4efe84
7
- data.tar.gz: 0a7dfb9250e3f686d88f7c2932cbd1a8b07f0a38ae4d3e491e98ee4c14a22126bcf3defd58227130fe3a43f8cd7d0b5dbaf002f6ded24fd187706b445fcce28e
6
+ metadata.gz: 9f94c830dc69bf00eb3b0c9e423741ce5d70caac4f7cad67e85e4df9c9b929fc60ad2034516ed9924c2f37ce2dab6cbb56f92d02a45b7cd6179b753479f278d8
7
+ data.tar.gz: 7cfdc3d21e2c648c9031ce061a59529db42236734b50a00bfc058fafde17039ef7660def4f8e8a8775d4a0b65cf30317fc4bbd87a01900b61d6866823904ba19
@@ -47,146 +47,186 @@ module Miscellany
47
47
  qs = qs.merge(source_refl.scope_for(model.unscoped)) if source_refl.scope
48
48
  qs
49
49
  }
50
+ pass_opts = source_refl.options.merge(
51
+ class_name: source_refl.class_name,
52
+ inverse_of: nil,
53
+ arbitrary_source_reflection: source_refl,
54
+ )
55
+ if source_refl.is_a?(ActiveRecord::Reflection::ThroughReflection)
56
+ pass_opts[:source] = source_refl.source_reflection_name
57
+ end
50
58
  ActiveRecord::Reflection.create(
51
59
  options[:type],
52
60
  @target_attribute,
53
61
  scope,
54
- source_refl.options.merge(
55
- class_name: source_refl.class_name,
56
- inverse_of: nil
57
- ),
62
+ pass_opts,
58
63
  model
59
64
  )
60
65
  end
61
66
  end
62
67
  end
63
68
 
64
- module ActiveRecordBasePatch
65
- extend ActiveSupport::Concern
69
+ module ActiveRecordPatches
70
+ module BasePatch
71
+ extend ActiveSupport::Concern
66
72
 
67
- included do
68
- class << self
69
- delegate :prefetch, to: :all
73
+ included do
74
+ class << self
75
+ delegate :prefetch, to: :all
76
+ end
70
77
  end
71
78
  end
72
- end
73
79
 
74
- module ActiveRecordRelationPatch
75
- def exec_queries
76
- return super if loaded?
80
+ module RelationPatch
81
+ def exec_queries
82
+ return super if loaded?
83
+
84
+ records = super
85
+ (@values[:prefetches] || {}).each do |_key, opts|
86
+ pfc = PrefetcherContext.new(model, opts)
87
+ pfc.link_models(records)
88
+
89
+ unless defined?(Goldiloader) && Goldiloader.enabled?
90
+ if PRE_RAILS_6_2
91
+ ::ActiveRecord::Associations::Preloader.new.preload(records, [opts[:attribute]])
92
+ else
93
+ ::ActiveRecord::Associations::Preloader.new(records: records, associations: [opts[:attribute]]).call
94
+ end
95
+ end
96
+ end
97
+ records
98
+ end
77
99
 
78
- records = super
79
- (@values[:prefetches] || {}).each do |_key, opts|
80
- pfc = PrefetcherContext.new(model, opts)
81
- pfc.link_models(records)
100
+ def prefetch(**kwargs)
101
+ spawn.add_prefetches!(kwargs)
102
+ end
82
103
 
83
- unless defined?(Goldiloader) && Goldiloader.enabled?
84
- if PRE_RAILS_6_2
85
- ::ActiveRecord::Associations::Preloader.new.preload(records, [opts[:attribute]])
86
- else
87
- ::ActiveRecord::Associations::Preloader.new(records: records, associations: [opts[:attribute]]).call
88
- end
104
+ def add_prefetches!(kwargs)
105
+ return unless kwargs.present?
106
+
107
+ assert_mutability!
108
+ @values[:prefetches] ||= {}
109
+ kwargs.each do |attr, opts|
110
+ @values[:prefetches][attr] = normalize_prefetch_options(attr, opts)
89
111
  end
112
+ self
90
113
  end
91
- records
92
- end
93
114
 
94
- def prefetch(**kwargs)
95
- spawn.add_prefetches!(kwargs)
96
- end
115
+ def normalize_prefetch_options(attr, opts)
116
+ norm = if opts.is_a?(Array)
117
+ { relation: opts[0], queryset: opts[1] }
118
+ elsif opts.is_a?(ActiveRecord::Relation)
119
+ rel_name = opts.model.name.underscore
120
+ rel = (model.reflections[rel_name] || model.reflections[rel_name.pluralize])&.name
121
+ { relation: rel, queryset: opts }
122
+ else
123
+ opts
124
+ end
97
125
 
98
- def add_prefetches!(kwargs)
99
- return unless kwargs.present?
126
+ norm[:attribute] = attr
127
+ norm[:type] ||= (attr.to_s.pluralize == attr.to_s) ? :has_many : :has_one
100
128
 
101
- assert_mutability!
102
- @values[:prefetches] ||= {}
103
- kwargs.each do |attr, opts|
104
- @values[:prefetches][attr] = normalize_prefetch_options(attr, opts)
129
+ norm
105
130
  end
106
- self
107
131
  end
108
132
 
109
- def normalize_prefetch_options(attr, opts)
110
- norm = if opts.is_a?(Array)
111
- { relation: opts[0], queryset: opts[1] }
112
- elsif opts.is_a?(ActiveRecord::Relation)
113
- rel_name = opts.model.name.underscore
114
- rel = (model.reflections[rel_name] || model.reflections[rel_name.pluralize])&.name
115
- { relation: rel, queryset: opts }
116
- else
117
- opts
118
- end
133
+ module Relation
134
+ module MergerPatch
135
+ def merge
136
+ super.tap do
137
+ merge_prefetches
138
+ end
139
+ end
119
140
 
120
- norm[:attribute] = attr
121
- norm[:type] ||= (attr.to_s.pluralize == attr.to_s) ? :has_many : :has_one
141
+ private
122
142
 
123
- norm
143
+ def merge_prefetches
144
+ relation.add_prefetches!(other.values[:prefetches])
145
+ end
146
+ end
124
147
  end
125
- end
126
148
 
127
- module ActiveRecordMergerPatch
128
- def merge
129
- super.tap do
130
- merge_prefetches
149
+ module Associations
150
+ if ACTIVE_RECORD_VERSION >= ::Gem::Version.new('7.0.0')
151
+ module Preloader
152
+ module BranchPatch
153
+ def grouped_records
154
+ h = {}
155
+ polymorphic_parent = !root? && parent.polymorphic?
156
+ source_records.each do |record|
157
+ next unless record
158
+ reflection = record.class._reflect_on_association(association)
159
+ reflection ||= record.association(association)&.reflection rescue nil
160
+ next if polymorphic_parent && !reflection || !record.association(association).klass
161
+ (h[reflection] ||= []) << record
162
+ end
163
+ h
164
+ end
165
+ end
166
+ end
167
+ elsif ACTIVE_RECORD_VERSION >= ::Gem::Version.new('6.0.0')
168
+ module PreloaderPatch
169
+ def grouped_records(association, records, polymorphic_parent)
170
+ h = {}
171
+ records.each do |record|
172
+ next unless record
173
+ reflection = record.class._reflect_on_association(association)
174
+ reflection ||= record.association(association)&.reflection rescue nil
175
+ next if polymorphic_parent && !reflection || !record.association(association).klass
176
+ (h[reflection] ||= []) << record
177
+ end
178
+ h
179
+ end
180
+ end
131
181
  end
132
182
  end
133
183
 
134
- private
184
+ module Reflection
185
+ module AssociationReflectionPatch
186
+ def check_preloadable!
187
+ return if scope && scope.arity < 0
188
+ super
189
+ end
190
+ end
135
191
 
136
- def merge_prefetches
137
- relation.add_prefetches!(other.values[:prefetches])
192
+ module ThroughReflectionPatch
193
+ def check_validity!
194
+ return if options[:arbitrary_source_reflection] # Rails already checked the base relation, we're good
195
+ super
196
+ end
197
+ end
138
198
  end
139
199
  end
140
200
 
141
- module ActiveRecordPreloaderPatch
142
- def grouped_records(association, records, polymorphic_parent)
143
- h = {}
144
- records.each do |record|
145
- next unless record
146
- reflection = record.class._reflect_on_association(association)
147
- reflection ||= record.association(association)&.reflection rescue nil
148
- next if polymorphic_parent && !reflection || !record.association(association).klass
149
- (h[reflection] ||= []) << record
201
+ def self.apply_patches(mod, install_base, base_module: nil)
202
+ return unless mod.is_a?(Module)
203
+
204
+ base_module ||= mod
205
+
206
+ if mod.name.ends_with? "Patch"
207
+ base_full_name = base_module.to_s
208
+ mod_full_name = mod.to_s
209
+ mod_rel_name = mod_full_name.sub(base_full_name, '')
210
+ mod_rel_bits = mod_rel_name.split('::').select(&:present?).map do |bit|
211
+ bit.ends_with?('Patch') ? bit[0..-6] : bit
150
212
  end
151
- h
152
- end
153
- end
213
+ final_mod_name = [install_base, *mod_rel_bits].select(&:present?).join("::")
214
+ install_mod = final_mod_name.constantize
154
215
 
155
- module ActiveRecordPreloaderBranchPatch
156
- def grouped_records
157
- h = {}
158
- polymorphic_parent = !root? && parent.polymorphic?
159
- source_records.each do |record|
160
- next unless record
161
- reflection = record.class._reflect_on_association(association)
162
- reflection ||= record.association(association)&.reflection rescue nil
163
- next if polymorphic_parent && !reflection || !record.association(association).klass
164
- (h[reflection] ||= []) << record
216
+ if mod.is_a?(ActiveSupport::Concern)
217
+ install_mod.include(mod)
218
+ else
219
+ install_mod.prepend(mod)
165
220
  end
166
- h
167
221
  end
168
- end
169
222
 
170
- module ActiveRecordReflectionPatch
171
- def check_preloadable!
172
- return if scope && scope.arity < 0
173
- super
223
+ mod.constants.map {|const| mod.const_get(const) }.each do |const|
224
+ apply_patches(const, install_base, base_module: base_module || mod)
174
225
  end
175
226
  end
176
227
 
177
228
  def self.install
178
- ::ActiveRecord::Base.include(ActiveRecordBasePatch)
179
-
180
- ::ActiveRecord::Relation.prepend(ActiveRecordRelationPatch)
181
- ::ActiveRecord::Relation::Merger.prepend(ActiveRecordMergerPatch)
182
-
183
- if ACTIVE_RECORD_VERSION >= ::Gem::Version.new('7.0.0')
184
- ::ActiveRecord::Associations::Preloader::Branch.prepend(ActiveRecordPreloaderBranchPatch)
185
- elsif ACTIVE_RECORD_VERSION >= ::Gem::Version.new('6.0.0')
186
- ::ActiveRecord::Associations::Preloader.prepend(ActiveRecordPreloaderPatch)
187
- end
188
-
189
- ::ActiveRecord::Reflection::AssociationReflection.prepend(ActiveRecordReflectionPatch)
229
+ apply_patches(ActiveRecordPatches, ::ActiveRecord)
190
230
 
191
231
  return unless defined? ::Goldiloader
192
232
 
@@ -47,6 +47,7 @@ module Miscellany
47
47
  end
48
48
  psql += " LIMIT #{length} OFFSET #{start}"
49
49
  records = ActiveRecord::Base.connection.exec_query(psql).to_a
50
+ records.map!(&:with_indifferent_access)
50
51
  augment_batch(records)
51
52
  records
52
53
  end
@@ -62,6 +63,7 @@ module Miscellany
62
63
  batch = ActiveRecord::Base.connection.exec_query(
63
64
  "SELECT * FROM #{tbl} LIMIT #{of} OFFSET #{offset}",
64
65
  )
66
+ batch.map!(&:with_indifferent_access)
65
67
  augment_batch(batch)
66
68
  yield batch
67
69
  offset += of
@@ -187,6 +187,7 @@ module Miscellany
187
187
  def sliced_items
188
188
  @sliced_items ||= begin
189
189
  if items.is_a?(Array)
190
+ # TODO Can we apply sorting?
190
191
  start, finish = slice_bounds
191
192
  if start && finish
192
193
  items[start...finish]
@@ -194,6 +195,7 @@ module Miscellany
194
195
  items
195
196
  end
196
197
  elsif items.is_a?(Proc)
198
+ # TODO Can we apply sorting?
197
199
  items.call(self)
198
200
  elsif items.is_a?(ActiveRecord::Relation)
199
201
  offset, limit = slice_bounds
@@ -218,8 +220,7 @@ module Miscellany
218
220
  end
219
221
 
220
222
  def sort_sql
221
- sorts = [ *Array(self.sort) ]
222
- sorts.push(*options[:sort_parser]&.default_sorts)
223
+ sorts = [ *Array(self.sort), *options[:sort_parser]&.default_sorts]
223
224
  sorts.compact!
224
225
 
225
226
  return nil unless sorts.present?
@@ -1,3 +1,3 @@
1
1
  module Miscellany
2
- VERSION = "0.1.19".freeze
2
+ VERSION = "0.1.20".freeze
3
3
  end
@@ -2,84 +2,139 @@
2
2
  require 'spec_helper'
3
3
 
4
4
  describe Miscellany::ArbitraryPrefetch do
5
- with_model :Post do
6
- table do |t|
7
- t.string :title
8
- t.timestamps null: false
5
+ shared_examples "general specs" do
6
+ it 'generally works' do
7
+ posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
8
+ expect(posts.count).to eq 10
9
+ expect(posts[0].favorite_comment).to be
9
10
  end
10
11
 
11
- model do
12
- has_many :comments
12
+ it 'works with Goldiloader active' do
13
+ Goldiloader.enabled do
14
+ posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
15
+ expect(posts.count).to eq 10
16
+ expect(posts[0].favorite_comment).to be
17
+ end
13
18
  end
14
- end
15
19
 
16
- with_model :Comment do
17
- table do |t|
18
- t.belongs_to :post
19
- t.string :title
20
- t.boolean :favorite
21
- t.timestamps null: false
20
+ it 'works with Goldiloader disabled' do
21
+ Goldiloader.disabled do
22
+ posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
23
+ expect(posts.count).to eq 10
24
+ expect(posts[0].favorite_comment).to be
25
+ end
22
26
  end
23
27
 
24
- model do
25
- belongs_to :post
28
+ context 'prefetch is singluar' do
29
+ it 'returns a single object' do
30
+ posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
31
+ expect(posts[0].favorite_comment).to be_a Comment
32
+ end
33
+
34
+ it 'with multiple items returns a single object' do
35
+ posts = Post.prefetch(favorite_comment: Comment.where(favorite: nil))
36
+ expect(posts[0].favorite_comment).to be_a Comment
37
+ end
26
38
  end
27
- end
28
39
 
29
- let!(:posts) { 10.times.map{|i| Post.create!(title: "Post #{i}") } }
40
+ context 'prefetch is plural' do
41
+ it 'returns an Array' do
42
+ posts = Post.prefetch(non_favorite_comments: Comment.where(favorite: nil))
43
+ expect(posts[0].non_favorite_comments).to respond_to :[]
44
+ expect(posts[0].non_favorite_comments.length).to eq 4
45
+ end
30
46
 
31
- before :each do
32
- posts.each do |p|
33
- 5.times{|i| p.comments.create!(title: "#{p.title} Comment #{i}") }
34
- p.comments.last.update!(favorite: true)
47
+ it 'with 1 item returns an Array' do
48
+ posts = Post.prefetch(non_favorite_comments: Comment.where(favorite: true))
49
+ expect(posts[0].non_favorite_comments).to respond_to :[]
50
+ expect(posts[0].non_favorite_comments.length).to eq 1
51
+ end
35
52
  end
36
53
  end
37
54
 
38
- it 'generally works' do
39
- posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
40
- expect(posts.count).to eq 10
41
- expect(posts[0].favorite_comment).to be
42
- end
55
+ context "normal association" do
56
+ with_model :Post do
57
+ table do |t|
58
+ t.string :title
59
+ t.timestamps null: false
60
+ end
43
61
 
44
- it 'works with Goldiloader active' do
45
- Goldiloader.enabled do
46
- posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
47
- expect(posts.count).to eq 10
48
- expect(posts[0].favorite_comment).to be
62
+ model do
63
+ has_many :comments
64
+ end
49
65
  end
50
- end
51
66
 
52
- it 'works with Goldiloader disabled' do
53
- Goldiloader.disabled do
54
- posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
55
- expect(posts.count).to eq 10
56
- expect(posts[0].favorite_comment).to be
67
+ with_model :Comment do
68
+ table do |t|
69
+ t.belongs_to :post
70
+ t.string :title
71
+ t.boolean :favorite
72
+ t.timestamps null: false
73
+ end
74
+
75
+ model do
76
+ belongs_to :post
77
+ end
57
78
  end
79
+
80
+ let!(:posts) { 10.times.map{|i| Post.create!(title: "Post #{i}") } }
81
+
82
+ before :each do
83
+ posts.each do |p|
84
+ 5.times{|i| p.comments.create!(title: "#{p.title} Comment #{i}") }
85
+ p.comments.last.update!(favorite: true)
86
+ end
87
+ end
88
+
89
+ include_examples "general specs"
58
90
  end
59
91
 
60
- context 'prefetch is singluar' do
61
- it 'returns a single object' do
62
- posts = Post.prefetch(favorite_comment: Comment.where(favorite: true))
63
- expect(posts[0].favorite_comment).to be_a Comment
92
+ context "through: association" do
93
+ with_model :Post do
94
+ table do |t|
95
+ t.string :title
96
+ t.timestamps null: false
97
+ end
98
+
99
+ model do
100
+ has_many :interims
101
+ has_many :comments, through: :interims
102
+ end
64
103
  end
65
104
 
66
- it 'with multiple items returns a single object' do
67
- posts = Post.prefetch(favorite_comment: Comment.where(favorite: nil))
68
- expect(posts[0].favorite_comment).to be_a Comment
105
+ with_model :Interim do
106
+ table do |t|
107
+ t.belongs_to :post
108
+ t.belongs_to :comment
109
+ t.timestamps null: false
110
+ end
111
+
112
+ model do
113
+ belongs_to :post
114
+ belongs_to :comment
115
+ end
69
116
  end
70
- end
71
117
 
72
- context 'prefetch is plural' do
73
- it 'returns an Array' do
74
- posts = Post.prefetch(non_favorite_comments: Comment.where(favorite: nil))
75
- expect(posts[0].non_favorite_comments).to respond_to :[]
76
- expect(posts[0].non_favorite_comments.length).to eq 4
118
+ with_model :Comment do
119
+ table do |t|
120
+ t.string :title
121
+ t.boolean :favorite
122
+ t.timestamps null: false
123
+ end
124
+
125
+ model do
126
+ end
77
127
  end
78
128
 
79
- it 'with 1 item returns an Array' do
80
- posts = Post.prefetch(non_favorite_comments: Comment.where(favorite: true))
81
- expect(posts[0].non_favorite_comments).to respond_to :[]
82
- expect(posts[0].non_favorite_comments.length).to eq 1
129
+ let!(:posts) { 10.times.map{|i| Post.create!(title: "Post #{i}") } }
130
+
131
+ before :each do
132
+ posts.each do |p|
133
+ 5.times{|i| p.comments.create!(title: "#{p.title} Comment #{i}") }
134
+ p.comments.last.update!(favorite: true)
135
+ end
83
136
  end
137
+
138
+ include_examples "general specs"
84
139
  end
85
140
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: miscellany
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.19
4
+ version: 0.1.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ethan Knapp
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-02 00:00:00.000000000 Z
11
+ date: 2023-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -114,7 +114,7 @@ dependencies:
114
114
  - - ">="
115
115
  - !ruby/object:Gem::Version
116
116
  version: '0'
117
- description:
117
+ description:
118
118
  email:
119
119
  - eknapp@instructure.com
120
120
  executables: []
@@ -154,7 +154,7 @@ files:
154
154
  homepage: https://instructure.com
155
155
  licenses: []
156
156
  metadata: {}
157
- post_install_message:
157
+ post_install_message:
158
158
  rdoc_options: []
159
159
  require_paths:
160
160
  - lib
@@ -170,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
170
  version: '0'
171
171
  requirements: []
172
172
  rubygems_version: 3.1.6
173
- signing_key:
173
+ signing_key:
174
174
  specification_version: 4
175
175
  summary: Gem for a bunch of random, re-usable Rails Concerns & Helpers
176
176
  test_files: