miscellany 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|