occams-record 1.6.0 → 1.6.2
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/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
|