occams-record 1.6.0 → 1.6.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +23 -2
- data/lib/occams-record/eager_loaders/ad_hoc_base.rb +10 -2
- data/lib/occams-record/eager_loaders/base.rb +13 -3
- data/lib/occams-record/eager_loaders/builder.rb +6 -4
- data/lib/occams-record/eager_loaders/context.rb +20 -8
- data/lib/occams-record/eager_loaders/eager_loaders.rb +1 -0
- data/lib/occams-record/eager_loaders/polymorphic_belongs_to.rb +13 -3
- data/lib/occams-record/eager_loaders/through.rb +10 -5
- data/lib/occams-record/eager_loaders/tracer.rb +16 -0
- data/lib/occams-record/errors.rb +6 -2
- data/lib/occams-record/query.rb +8 -5
- data/lib/occams-record/raw_query.rb +1 -1
- data/lib/occams-record/results/results.rb +8 -1
- data/lib/occams-record/results/row.rb +23 -5
- data/lib/occams-record/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a233847b8c1a9de8c77e8c5e1d12f70ea7c8b9efa498208491a17ff09a6f217
|
4
|
+
data.tar.gz: f87fbaf43703cd29b60b94a480ad903d00cad2b9b3bd9bfb4a77b7748f68aa81
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 21115e90f5f455eaf33d5c0596d6276957905fb14932583fcdfacdb1a44f7afeaee029458de903fa58c2801e7504643c6c6e4a530822409ff93da6954648a06e
|
7
|
+
data.tar.gz: a38eba237379a4a0b55d027746a8796f9aa9904e60bb1ba6acd6fd46348fc84e0aef17fc6de482d5b32d6677c09b2ea53602b000bf6da9a60d5bd2dec0e72b3f
|
data/README.md
CHANGED
@@ -146,7 +146,7 @@ OccamsRecord
|
|
146
146
|
order.line_items.each { |line_item|
|
147
147
|
puts line_item.product.name
|
148
148
|
puts line_item.product.category.name
|
149
|
-
OccamsRecord::MissingEagerLoadError: Association 'category' is unavailable on Product because it was not eager loaded!
|
149
|
+
OccamsRecord::MissingEagerLoadError: Association 'category' is unavailable on Product because it was not eager loaded! Found at root.line_items.product
|
150
150
|
}
|
151
151
|
}
|
152
152
|
```
|
@@ -303,7 +303,11 @@ The SQL string and binds should be familiar. `%{ids}` will be provided for you -
|
|
303
303
|
|
304
304
|
## Injecting instance methods
|
305
305
|
|
306
|
-
Occams Records results are just plain rows; there are no methods from your Rails models. (Separating your persistence layer from your domain is good thing!) But sometimes you need a few methods. Occams Record
|
306
|
+
Occams Records results are just plain rows; there are no methods from your Rails models. (Separating your persistence layer from your domain is good thing!) But sometimes you need a few methods. Occams Record provides two ways of accomplishing this.
|
307
|
+
|
308
|
+
### Include custom modules
|
309
|
+
|
310
|
+
You may also specify one or more modules to be included in your results:
|
307
311
|
|
308
312
|
```ruby
|
309
313
|
module MyOrderMethods
|
@@ -326,6 +330,23 @@ orders = OccamsRecord
|
|
326
330
|
.run
|
327
331
|
```
|
328
332
|
|
333
|
+
### ActiveRecord method fallback
|
334
|
+
|
335
|
+
This is an ugly hack of last resort if you can't easily extract a method from your model into a shared module. Plugins, like `carrierwave`, are a good example. When you call a method that doesn't exist on an Occams Record result, it will initialize an ActiveRecord object and forward the method call to it.
|
336
|
+
|
337
|
+
The `active_record_fallback` option must be passed either `:lazy` or `:strict` (recommended). `:strict` enables ActiveRecord's strict loading option, helping you avoid N+1 queries. :lazy allows them. Note that `:strict` is only available for ActiveRecord 6.1 and later.
|
338
|
+
|
339
|
+
The following will forward any nonexistent methods for `Order` and `Product` records:
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
orders = OccamsRecord
|
343
|
+
.query(Order.all, active_record_fallback: :strict)
|
344
|
+
.eager_load(:line_items) {
|
345
|
+
eager_load(:product, active_record_fallback: :strict)
|
346
|
+
}
|
347
|
+
.run
|
348
|
+
```
|
349
|
+
|
329
350
|
---
|
330
351
|
|
331
352
|
# Unsupported features
|
@@ -11,6 +11,12 @@ module OccamsRecord
|
|
11
11
|
# @return [String] association name
|
12
12
|
attr_reader :name
|
13
13
|
|
14
|
+
# @return [OccamsRecord::EagerLoaders::Tracer | nil] a reference to this eager loader and its parent (if any)
|
15
|
+
attr_reader :tracer
|
16
|
+
|
17
|
+
# @return [OccamsRecord::EagerLoaders::Context]
|
18
|
+
attr_reader :eager_loaders
|
19
|
+
|
14
20
|
#
|
15
21
|
# Initialize a new add hoc association.
|
16
22
|
#
|
@@ -20,12 +26,14 @@ module OccamsRecord
|
|
20
26
|
# @param binds [Hash] any additional binds for your query.
|
21
27
|
# @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
|
22
28
|
# @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
|
29
|
+
# @param parent [OccamsRecord::EagerLoaders::Tracer] the eager loader this one is nested under (if any)
|
23
30
|
# @yield eager load associations nested under this one
|
24
31
|
#
|
25
|
-
def initialize(name, mapping, sql, binds: {}, model: nil, use: nil, &builder)
|
32
|
+
def initialize(name, mapping, sql, binds: {}, model: nil, use: nil, parent: nil, &builder)
|
26
33
|
@name, @mapping = name.to_s, mapping
|
27
34
|
@sql, @binds, @use, @model = sql, binds, use, model
|
28
|
-
@
|
35
|
+
@tracer = Tracer.new(name, parent)
|
36
|
+
@eager_loaders = EagerLoaders::Context.new(@model, tracer: @tracer)
|
29
37
|
if builder
|
30
38
|
if builder.arity > 0
|
31
39
|
builder.call(self)
|
@@ -9,6 +9,12 @@ module OccamsRecord
|
|
9
9
|
# @return [String] association name
|
10
10
|
attr_reader :name
|
11
11
|
|
12
|
+
# @return [OccamsRecord::EagerLoaders::Tracer | nil] a reference to this eager loader and its parent (if any)
|
13
|
+
attr_reader :tracer
|
14
|
+
|
15
|
+
# @return [OccamsRecord::EagerLoaders::Context]
|
16
|
+
attr_reader :eager_loaders
|
17
|
+
|
12
18
|
#
|
13
19
|
# @param ref [ActiveRecord::Association] the ActiveRecord association
|
14
20
|
# @param scope [Proc] a scope to apply to the query (optional). It will be passed an
|
@@ -16,13 +22,17 @@ module OccamsRecord
|
|
16
22
|
# @param use [Array(Module)] optional Module to include in the result class (single or array)
|
17
23
|
# @param as [Symbol] Load the association usign a different attribute name
|
18
24
|
# @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
|
25
|
+
# @param parent [OccamsRecord::EagerLoaders::Tracer] the eager loader this one is nested under (if any)
|
26
|
+
# @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
|
19
27
|
# @yield perform eager loading on *this* association (optional)
|
20
28
|
#
|
21
|
-
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, &builder)
|
29
|
+
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, parent: nil, active_record_fallback: nil, &builder)
|
22
30
|
@ref, @scopes, @use, @as = ref, Array(scope), use, as
|
23
31
|
@model = ref.klass
|
24
32
|
@name = (as || ref.name).to_s
|
25
|
-
@
|
33
|
+
@tracer = Tracer.new(name, parent)
|
34
|
+
@eager_loaders = EagerLoaders::Context.new(@model, tracer: @tracer)
|
35
|
+
@active_record_fallback = active_record_fallback
|
26
36
|
@optimizer = optimizer
|
27
37
|
if builder
|
28
38
|
if builder.arity > 0
|
@@ -55,7 +65,7 @@ module OccamsRecord
|
|
55
65
|
#
|
56
66
|
def run(rows, query_logger: nil, measurements: nil)
|
57
67
|
query(rows) { |*args|
|
58
|
-
assoc_rows = args[0] ? Query.new(args[0], use: @use, eager_loaders: @eager_loaders, query_logger: query_logger, measurements: measurements).run : []
|
68
|
+
assoc_rows = args[0] ? Query.new(args[0], use: @use, eager_loaders: @eager_loaders, query_logger: query_logger, measurements: measurements, active_record_fallback: @active_record_fallback).run : []
|
59
69
|
merge! assoc_rows, rows, *args[1..-1]
|
60
70
|
}
|
61
71
|
nil
|
@@ -24,11 +24,12 @@ module OccamsRecord
|
|
24
24
|
# @param as [Symbol] Load the association usign a different attribute name
|
25
25
|
# @param from [Symbol] Opposite of `as`. `assoc` is the custom name and `from` is the name of association on the ActiveRecord model.
|
26
26
|
# @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
|
27
|
+
# @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
|
27
28
|
# @yield a block where you may perform eager loading on *this* association (optional)
|
28
29
|
# @return self
|
29
30
|
#
|
30
|
-
def eager_load(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, &builder)
|
31
|
-
@eager_loaders.add(assoc, scope, select: select, use: use, as: as, from: from, optimizer: optimizer, &builder)
|
31
|
+
def eager_load(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, active_record_fallback: nil, &builder)
|
32
|
+
@eager_loaders.add(assoc, scope, select: select, use: use, as: as, from: from, optimizer: optimizer, active_record_fallback: active_record_fallback, &builder)
|
32
33
|
self
|
33
34
|
end
|
34
35
|
|
@@ -45,11 +46,12 @@ module OccamsRecord
|
|
45
46
|
# @param as [Symbol] Load the association usign a different attribute name
|
46
47
|
# @param from [Symbol] Opposite of `as`. `assoc` is the custom name and `from` is the name of association on the ActiveRecord model.
|
47
48
|
# @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
|
49
|
+
# @param active_record_fallback [Symbol] If passed, the record(s) will be converted to read-only ActiveRecord objects. Options are :lazy (allow lazy loading) or :strict (enables strict loading)
|
48
50
|
# @return [OccamsRecord::EagerLoaders::Base]
|
49
51
|
#
|
50
|
-
def nest(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select)
|
52
|
+
def nest(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, active_record_fallback: nil)
|
51
53
|
raise ArgumentError, "OccamsRecord::EagerLoaders::Builder#nest does not accept a block!" if block_given?
|
52
|
-
@eager_loaders.add(assoc, scope, select: select, use: use, as: as, from: from, optimizer: optimizer) ||
|
54
|
+
@eager_loaders.add(assoc, scope, select: select, use: use, as: as, from: from, optimizer: optimizer, active_record_fallback: active_record_fallback) ||
|
53
55
|
raise("OccamsRecord::EagerLoaders::Builder#nest may not be called under a polymorphic association")
|
54
56
|
end
|
55
57
|
|
@@ -14,14 +14,19 @@ module OccamsRecord
|
|
14
14
|
# @return [ActiveRecord::Base]
|
15
15
|
attr_reader :model
|
16
16
|
|
17
|
+
# @return [OccamsRecord::EagerLoaders::Tracer]
|
18
|
+
attr_reader :tracer
|
19
|
+
|
17
20
|
#
|
18
21
|
# Initialize a new eager loading context.
|
19
22
|
#
|
20
23
|
# @param mode [ActiveRecord::Base] the model that contains the associations that will be referenced.
|
24
|
+
# @param tracer [OccamsRecord::EagerLoaders::Tracer] the eager loader that owns this context (if any)
|
21
25
|
# @param polymorphic [Boolean] When true, model is allowed to change, and it's assumed that not every loader is applicable to every model.
|
22
26
|
#
|
23
|
-
def initialize(model = nil, polymorphic: false)
|
27
|
+
def initialize(model = nil, tracer: nil, polymorphic: false)
|
24
28
|
@model, @polymorphic = model, polymorphic
|
29
|
+
@tracer = tracer || OccamsRecord::EagerLoaders::Tracer.new("root")
|
25
30
|
@loaders = []
|
26
31
|
@dynamic_loaders = []
|
27
32
|
end
|
@@ -72,10 +77,11 @@ module OccamsRecord
|
|
72
77
|
# @param as [Symbol] Load the association usign a different attribute name
|
73
78
|
# @param from [Symbol] Opposite of `as`. `assoc` is the custom name and `from` is the name of association on the ActiveRecord model.
|
74
79
|
# @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
|
80
|
+
# @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
|
75
81
|
# @yield a block where you may perform eager loading on *this* association (optional)
|
76
82
|
# @return [OccamsRecord::EagerLoaders::Base] the new loader. if @model is nil, nil will be returned.
|
77
83
|
#
|
78
|
-
def add(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, &builder)
|
84
|
+
def add(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, active_record_fallback: nil, &builder)
|
79
85
|
if from
|
80
86
|
real_assoc = from
|
81
87
|
custom_name = assoc
|
@@ -88,11 +94,11 @@ module OccamsRecord
|
|
88
94
|
end
|
89
95
|
|
90
96
|
if @model
|
91
|
-
loader = build_loader!(real_assoc, custom_name, scope, select, use, optimizer, builder)
|
97
|
+
loader = build_loader!(real_assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback)
|
92
98
|
@loaders << loader
|
93
99
|
loader
|
94
100
|
else
|
95
|
-
@dynamic_loaders << [real_assoc, custom_name, scope, select, use, optimizer, builder]
|
101
|
+
@dynamic_loaders << [real_assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback]
|
96
102
|
nil
|
97
103
|
end
|
98
104
|
end
|
@@ -111,21 +117,27 @@ module OccamsRecord
|
|
111
117
|
nil
|
112
118
|
end
|
113
119
|
|
120
|
+
# Iterate over each loader
|
121
|
+
def each
|
122
|
+
return @loaders.each unless block_given?
|
123
|
+
@loaders.each { |l| yield l }
|
124
|
+
end
|
125
|
+
|
114
126
|
private
|
115
127
|
|
116
|
-
def build_loader!(assoc, custom_name, scope, select, use, optimizer, builder)
|
117
|
-
build_loader(assoc, custom_name, scope, select, use, optimizer, builder) ||
|
128
|
+
def build_loader!(assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback)
|
129
|
+
build_loader(assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback) ||
|
118
130
|
raise("OccamsRecord: No association `:#{assoc}` on `#{@model.name}` or subclasses")
|
119
131
|
end
|
120
132
|
|
121
|
-
def build_loader(assoc, custom_name, scope, select, use, optimizer, builder)
|
133
|
+
def build_loader(assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback)
|
122
134
|
ref = @model.reflections[assoc.to_s] ||
|
123
135
|
@model.subclasses.map(&:reflections).detect { |x| x.has_key? assoc.to_s }&.[](assoc.to_s)
|
124
136
|
return nil if ref.nil?
|
125
137
|
|
126
138
|
scope ||= ->(q) { q.select select } if select
|
127
139
|
loader_class = !!ref.through_reflection ? EagerLoaders::Through : EagerLoaders.fetch!(ref)
|
128
|
-
loader_class.new(ref, scope, use: use, as: custom_name, optimizer: optimizer, &builder)
|
140
|
+
loader_class.new(ref, scope, use: use, as: custom_name, optimizer: optimizer, parent: @tracer, active_record_fallback: active_record_fallback, &builder)
|
129
141
|
end
|
130
142
|
end
|
131
143
|
end
|
@@ -5,6 +5,7 @@ module OccamsRecord
|
|
5
5
|
module EagerLoaders
|
6
6
|
autoload :Builder, 'occams-record/eager_loaders/builder'
|
7
7
|
autoload :Context, 'occams-record/eager_loaders/context'
|
8
|
+
autoload :Tracer, 'occams-record/eager_loaders/tracer'
|
8
9
|
|
9
10
|
autoload :Base, 'occams-record/eager_loaders/base'
|
10
11
|
autoload :BelongsTo, 'occams-record/eager_loaders/belongs_to'
|
@@ -7,6 +7,12 @@ module OccamsRecord
|
|
7
7
|
# @return [String] association name
|
8
8
|
attr_reader :name
|
9
9
|
|
10
|
+
# @return [OccamsRecord::EagerLoaders::Tracer | nil] a reference to this eager loader and its parent (if any)
|
11
|
+
attr_reader :tracer
|
12
|
+
|
13
|
+
# @return [OccamsRecord::EagerLoaders::Context]
|
14
|
+
attr_reader :eager_loaders
|
15
|
+
|
10
16
|
#
|
11
17
|
# @param ref [ActiveRecord::Association] the ActiveRecord association
|
12
18
|
# @param scope [Proc] a scope to apply to the query (optional). It will be passed an
|
@@ -14,14 +20,18 @@ module OccamsRecord
|
|
14
20
|
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
15
21
|
# @param as [Symbol] Load the association usign a different attribute name
|
16
22
|
# @param optimizer [Symbol] Only used for `through` associations. A no op here.
|
23
|
+
# @param parent [OccamsRecord::EagerLoaders::Tracer] the eager loader this one is nested under (if any)
|
24
|
+
# @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
|
17
25
|
# @yield perform eager loading on *this* association (optional)
|
18
26
|
#
|
19
|
-
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: nil, &builder)
|
27
|
+
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: nil, parent: nil, active_record_fallback: nil, &builder)
|
20
28
|
@ref, @scopes, @use = ref, Array(scope), use
|
21
29
|
@name = (as || ref.name).to_s
|
22
30
|
@foreign_type = @ref.foreign_type.to_sym
|
23
31
|
@foreign_key = @ref.foreign_key.to_sym
|
24
|
-
@
|
32
|
+
@tracer = Tracer.new(name, parent)
|
33
|
+
@eager_loaders = EagerLoaders::Context.new(nil, tracer: @tracer, polymorphic: true)
|
34
|
+
@active_record_fallback = active_record_fallback
|
25
35
|
if builder
|
26
36
|
if builder.arity > 0
|
27
37
|
builder.call(self)
|
@@ -55,7 +65,7 @@ module OccamsRecord
|
|
55
65
|
query(rows) { |scope|
|
56
66
|
eager_loaders = @eager_loaders.dup
|
57
67
|
eager_loaders.model = scope.klass
|
58
|
-
assoc_rows = Query.new(scope, use: @use, eager_loaders: eager_loaders, query_logger: query_logger, measurements: measurements).run
|
68
|
+
assoc_rows = Query.new(scope, use: @use, eager_loaders: eager_loaders, query_logger: query_logger, measurements: measurements, active_record_fallback: @active_record_fallback).run
|
59
69
|
merge! assoc_rows, rows
|
60
70
|
}
|
61
71
|
nil
|
@@ -9,7 +9,7 @@ module OccamsRecord
|
|
9
9
|
#
|
10
10
|
# See documentation for OccamsRecord::EagerLoaders::Base.
|
11
11
|
#
|
12
|
-
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, &builder)
|
12
|
+
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, parent: nil, active_record_fallback: nil, &builder)
|
13
13
|
super
|
14
14
|
|
15
15
|
unless @ref.macro == :has_one or @ref.macro == :has_many
|
@@ -74,14 +74,19 @@ module OccamsRecord
|
|
74
74
|
links = @chain[1..-2]
|
75
75
|
tail = @chain[-1]
|
76
76
|
|
77
|
-
outer_loader = EagerLoaders.fetch!(head.ref).new(head.ref, optimized_select(head))
|
77
|
+
outer_loader = EagerLoaders.fetch!(head.ref).new(head.ref, optimized_select(head), parent: tracer.parent)
|
78
|
+
outer_loader.tracer.through = true
|
78
79
|
|
79
|
-
links.
|
80
|
+
inner_loader = links.
|
80
81
|
reduce(outer_loader) { |loader, link|
|
81
|
-
loader.nest(link.ref.source_reflection.name, optimized_select(link))
|
82
|
+
nested_loader = loader.nest(link.ref.source_reflection.name, optimized_select(link))
|
83
|
+
nested_loader.tracer.through = true
|
84
|
+
nested_loader
|
82
85
|
}.
|
83
|
-
nest(tail.ref.source_reflection.name, @scope, use: @use, as: @as)
|
86
|
+
nest(tail.ref.source_reflection.name, @scope, use: @use, as: @as, active_record_fallback: @active_record_fallback)
|
84
87
|
|
88
|
+
@eager_loaders.each { |loader| inner_loader.eager_loaders << loader }
|
89
|
+
inner_loader.tracer.name = tracer.name
|
85
90
|
outer_loader
|
86
91
|
end
|
87
92
|
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module OccamsRecord
|
2
|
+
module EagerLoaders
|
3
|
+
# A low-memory way to trace the path of eager loads from any point back to the root query
|
4
|
+
Tracer = Struct.new(:name, :parent, :through) do
|
5
|
+
def to_s
|
6
|
+
lookup.join(".")
|
7
|
+
end
|
8
|
+
|
9
|
+
def lookup(trace = self)
|
10
|
+
return [] if trace.nil?
|
11
|
+
name = trace.through ? "through(#{trace.name})" : trace.name
|
12
|
+
lookup(trace.parent) << name
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/occams-record/errors.rb
CHANGED
@@ -13,6 +13,7 @@ module OccamsRecord
|
|
13
13
|
def initialize(record, name)
|
14
14
|
@record, @name = record, name
|
15
15
|
@model_name = record.class.model_name
|
16
|
+
@load_trace = record.class.eager_loader_trace
|
16
17
|
end
|
17
18
|
|
18
19
|
# @return [String]
|
@@ -25,7 +26,8 @@ module OccamsRecord
|
|
25
26
|
class MissingColumnError < MissingDataError
|
26
27
|
# @return [String]
|
27
28
|
def message
|
28
|
-
|
29
|
+
loads = @load_trace.to_s
|
30
|
+
"Column '#{name}' is unavailable on #{model_name} because it was not included in the SELECT statement! Occams Record trace: #{loads}"
|
29
31
|
end
|
30
32
|
end
|
31
33
|
|
@@ -33,7 +35,8 @@ module OccamsRecord
|
|
33
35
|
class MissingEagerLoadError < MissingDataError
|
34
36
|
# @return [String]
|
35
37
|
def message
|
36
|
-
|
38
|
+
loads = @load_trace.to_s
|
39
|
+
"Association '#{name}' is unavailable on #{model_name} because it was not eager loaded! Occams Record trace: #{loads}"
|
37
40
|
end
|
38
41
|
end
|
39
42
|
|
@@ -45,6 +48,7 @@ module OccamsRecord
|
|
45
48
|
attr_reader :attrs
|
46
49
|
|
47
50
|
# @param model_name [String]
|
51
|
+
#
|
48
52
|
# @param attrs [Hash]
|
49
53
|
def initialize(model_name, attrs)
|
50
54
|
@model_name, @attrs = model_name, attrs
|
data/lib/occams-record/query.rb
CHANGED
@@ -18,10 +18,11 @@ module OccamsRecord
|
|
18
18
|
# @param scope [ActiveRecord::Relation]
|
19
19
|
# @param use [Module] optional Module to include in the result class
|
20
20
|
# @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
|
21
|
+
# @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
|
21
22
|
# @return [OccamsRecord::Query]
|
22
23
|
#
|
23
|
-
def self.query(scope, use: nil, query_logger: nil)
|
24
|
-
Query.new(scope, use: use, query_logger: query_logger)
|
24
|
+
def self.query(scope, use: nil, active_record_fallback: nil, query_logger: nil)
|
25
|
+
Query.new(scope, use: use, query_logger: query_logger, active_record_fallback: active_record_fallback)
|
25
26
|
end
|
26
27
|
|
27
28
|
#
|
@@ -47,13 +48,15 @@ module OccamsRecord
|
|
47
48
|
# @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
|
48
49
|
# @param eager_loaders [OccamsRecord::EagerLoaders::Context]
|
49
50
|
# @param measurements [Array]
|
51
|
+
# @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
|
50
52
|
#
|
51
|
-
def initialize(scope, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil)
|
53
|
+
def initialize(scope, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil, active_record_fallback: nil)
|
52
54
|
@model = scope.klass
|
53
55
|
@scope = scope
|
54
56
|
@eager_loaders = eager_loaders || EagerLoaders::Context.new(@model)
|
55
57
|
@use = use
|
56
58
|
@query_logger, @measurements = query_logger, measurements
|
59
|
+
@active_record_fallback = active_record_fallback
|
57
60
|
end
|
58
61
|
|
59
62
|
#
|
@@ -92,7 +95,7 @@ module OccamsRecord
|
|
92
95
|
#
|
93
96
|
def run
|
94
97
|
sql = block_given? ? yield(scope).to_sql : scope.to_sql
|
95
|
-
@query_logger << sql if @query_logger
|
98
|
+
@query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
|
96
99
|
result = if measure?
|
97
100
|
record_start_time!
|
98
101
|
measure!(model.table_name, sql) {
|
@@ -101,7 +104,7 @@ module OccamsRecord
|
|
101
104
|
else
|
102
105
|
model.connection.exec_query sql
|
103
106
|
end
|
104
|
-
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: model, modules: @use)
|
107
|
+
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: model, modules: @use, tracer: @eager_loaders.tracer, active_record_fallback: @active_record_fallback)
|
105
108
|
rows = result.rows.map { |row| row_class.new row }
|
106
109
|
@eager_loaders.run!(rows, query_logger: @query_logger, measurements: @measurements)
|
107
110
|
yield_measurements!
|
@@ -117,7 +117,7 @@ module OccamsRecord
|
|
117
117
|
else
|
118
118
|
conn.exec_query _escaped_sql
|
119
119
|
end
|
120
|
-
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: @eager_loaders.model, modules: @use)
|
120
|
+
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: @eager_loaders.model, modules: @use, tracer: @eager_loaders.tracer)
|
121
121
|
rows = result.rows.map { |row| row_class.new row }
|
122
122
|
@eager_loaders.run!(rows, query_logger: @query_logger, measurements: @measurements)
|
123
123
|
yield_measurements!
|
@@ -17,9 +17,14 @@ module OccamsRecord
|
|
17
17
|
# @param association_names [Array<String>] names of associations that will be eager loaded into the results.
|
18
18
|
# @param model [ActiveRecord::Base] the AR model representing the table (it holds column & type info).
|
19
19
|
# @param modules [Array<Module>] (optional)
|
20
|
+
# @param tracer [OccamsRecord::EagerLoaders::Tracer] the eager loaded that loaded this class of records
|
21
|
+
# @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
|
20
22
|
# @return [OccamsRecord::Results::Row] a class customized for this result set
|
21
23
|
#
|
22
|
-
def self.klass(column_names, column_types, association_names = [], model: nil, modules: nil)
|
24
|
+
def self.klass(column_names, column_types, association_names = [], model: nil, modules: nil, tracer: nil, active_record_fallback: nil)
|
25
|
+
raise ArgumentError, "Invalid active_record_fallback option :#{active_record_fallback}. Valid options are :lazy, :strict" if active_record_fallback and !%i(lazy strict).include?(active_record_fallback)
|
26
|
+
raise ArgumentError, "Option active_record_fallback is not allowed when no model is present" if active_record_fallback and model.nil?
|
27
|
+
|
23
28
|
Class.new(Results::Row) do
|
24
29
|
Array(modules).each { |mod| prepend mod } if modules
|
25
30
|
|
@@ -28,6 +33,8 @@ module OccamsRecord
|
|
28
33
|
self._model = model
|
29
34
|
self.model_name = model ? model.name : nil
|
30
35
|
self.table_name = model ? model.table_name : nil
|
36
|
+
self.eager_loader_trace = tracer
|
37
|
+
self.active_record_fallback = active_record_fallback
|
31
38
|
self.primary_key = if model&.primary_key and (pkey = model.primary_key.to_s) and columns.include?(pkey)
|
32
39
|
pkey
|
33
40
|
end
|
@@ -19,6 +19,10 @@ module OccamsRecord
|
|
19
19
|
attr_accessor :table_name
|
20
20
|
# Name of primary key column (nil if column wasn't in the SELECT)
|
21
21
|
attr_accessor :primary_key
|
22
|
+
# A trace of how this record was loaded (for debugging)
|
23
|
+
attr_accessor :eager_loader_trace
|
24
|
+
# If present, missing methods will be forwarded to the ActiveRecord model. :lazy allows lazy loading in AR, :strict doesn't
|
25
|
+
attr_accessor :active_record_fallback
|
22
26
|
end
|
23
27
|
self.columns = []
|
24
28
|
self.associations = []
|
@@ -110,19 +114,24 @@ module OccamsRecord
|
|
110
114
|
IDS_SUFFIX = /_ids$/
|
111
115
|
def method_missing(name, *args, &block)
|
112
116
|
model = self.class._model
|
113
|
-
|
117
|
+
ex = NoMethodError.new("Undefined method `#{name}' for #{self.inspect}. Occams Record trace: #{self.class.eager_loader_trace}", name, args)
|
118
|
+
raise ex if model.nil?
|
114
119
|
|
115
120
|
name_str = name.to_s
|
116
121
|
assoc = name_str.sub(IDS_SUFFIX, "").pluralize
|
117
|
-
|
122
|
+
no_args = args.empty? && block.nil?
|
123
|
+
|
124
|
+
if no_args and name_str =~ IDS_SUFFIX and can_define_ids_reader? assoc
|
118
125
|
define_ids_reader! assoc
|
119
126
|
send name
|
120
|
-
elsif model.reflections.has_key? name_str
|
127
|
+
elsif no_args and model.reflections.has_key? name_str
|
121
128
|
raise MissingEagerLoadError.new(self, name)
|
122
|
-
elsif model.columns_hash.has_key? name_str
|
129
|
+
elsif no_args and model.columns_hash.has_key? name_str
|
123
130
|
raise MissingColumnError.new(self, name)
|
131
|
+
elsif self.class.active_record_fallback
|
132
|
+
active_record_fallback(name, *args, &block)
|
124
133
|
else
|
125
|
-
|
134
|
+
raise ex
|
126
135
|
end
|
127
136
|
end
|
128
137
|
|
@@ -141,6 +150,15 @@ module OccamsRecord
|
|
141
150
|
|
142
151
|
private
|
143
152
|
|
153
|
+
def active_record_fallback(name, *args, &block)
|
154
|
+
@active_record_fallback ||= Ugly::active_record(self.class._model, self).tap { |record|
|
155
|
+
record.strict_loading! if self.class.active_record_fallback == :strict
|
156
|
+
}
|
157
|
+
@active_record_fallback.send(name, *args, &block)
|
158
|
+
rescue NoMethodError => e
|
159
|
+
raise NoMethodError.new("#{e.message}. Occams Record trace: #{self.class.eager_loader_trace}.active_record_fallback(#{self.class._model.name})", name, args)
|
160
|
+
end
|
161
|
+
|
144
162
|
def can_define_ids_reader?(assoc)
|
145
163
|
model = self.class._model
|
146
164
|
self.class.associations.include?(assoc) &&
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: occams-record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.6.
|
4
|
+
version: 1.6.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jordan Hollinger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-02-
|
11
|
+
date: 2023-02-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -70,6 +70,7 @@ files:
|
|
70
70
|
- lib/occams-record/eager_loaders/has_one.rb
|
71
71
|
- lib/occams-record/eager_loaders/polymorphic_belongs_to.rb
|
72
72
|
- lib/occams-record/eager_loaders/through.rb
|
73
|
+
- lib/occams-record/eager_loaders/tracer.rb
|
73
74
|
- lib/occams-record/errors.rb
|
74
75
|
- lib/occams-record/measureable.rb
|
75
76
|
- lib/occams-record/merge.rb
|