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 +4 -4
- data/lib/miscellany/active_record/arbitrary_prefetch.rb +136 -96
- data/lib/miscellany/active_record/complex_query.rb +2 -0
- data/lib/miscellany/controller/sliced_response.rb +3 -2
- data/lib/miscellany/version.rb +1 -1
- data/spec/miscellany/arbitrary_prefetch_spec.rb +109 -54
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31028be35ab95451d190d0a669321bd265bc82230f8c07ddab8c52a4e526fcf0
|
4
|
+
data.tar.gz: ee7c51e33f2147ab49e05f3c0783dd5a8f938a931a33468d1289214233b34a4e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
65
|
-
|
69
|
+
module ActiveRecordPatches
|
70
|
+
module BasePatch
|
71
|
+
extend ActiveSupport::Concern
|
66
72
|
|
67
|
-
|
68
|
-
|
69
|
-
|
73
|
+
included do
|
74
|
+
class << self
|
75
|
+
delegate :prefetch, to: :all
|
76
|
+
end
|
70
77
|
end
|
71
78
|
end
|
72
|
-
end
|
73
79
|
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
pfc.link_models(records)
|
100
|
+
def prefetch(**kwargs)
|
101
|
+
spawn.add_prefetches!(kwargs)
|
102
|
+
end
|
82
103
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
99
|
-
|
126
|
+
norm[:attribute] = attr
|
127
|
+
norm[:type] ||= (attr.to_s.pluralize == attr.to_s) ? :has_many : :has_one
|
100
128
|
|
101
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
121
|
-
norm[:type] ||= (attr.to_s.pluralize == attr.to_s) ? :has_many : :has_one
|
141
|
+
private
|
122
142
|
|
123
|
-
|
143
|
+
def merge_prefetches
|
144
|
+
relation.add_prefetches!(other.values[:prefetches])
|
145
|
+
end
|
146
|
+
end
|
124
147
|
end
|
125
|
-
end
|
126
148
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
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
|
-
|
137
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
152
|
-
|
153
|
-
end
|
213
|
+
final_mod_name = [install_base, *mod_rel_bits].select(&:present?).join("::")
|
214
|
+
install_mod = final_mod_name.constantize
|
154
215
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
171
|
-
|
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
|
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?
|
data/lib/miscellany/version.rb
CHANGED
@@ -2,84 +2,139 @@
|
|
2
2
|
require 'spec_helper'
|
3
3
|
|
4
4
|
describe Miscellany::ArbitraryPrefetch do
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
12
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
25
|
-
|
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
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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.
|
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-
|
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:
|