occams-record 1.6.1 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +35 -1
- data/lib/occams-record/eager_loaders/ad_hoc_base.rb +3 -0
- data/lib/occams-record/eager_loaders/base.rb +7 -2
- data/lib/occams-record/eager_loaders/builder.rb +6 -4
- data/lib/occams-record/eager_loaders/context.rb +14 -7
- data/lib/occams-record/eager_loaders/polymorphic_belongs_to.rb +7 -2
- data/lib/occams-record/eager_loaders/through.rb +9 -4
- data/lib/occams-record/eager_loaders/tracer.rb +3 -2
- data/lib/occams-record/errors.rb +2 -2
- data/lib/occams-record/pluck.rb +15 -0
- data/lib/occams-record/query.rb +30 -4
- data/lib/occams-record/raw_query.rb +23 -0
- data/lib/occams-record/results/results.rb +7 -2
- data/lib/occams-record/results/row.rb +21 -5
- data/lib/occams-record/version.rb +1 -1
- data/lib/occams-record.rb +1 -0
- 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: 5f2590ba264934763d86310d4cd9b92459cb053589afa23fb4ea4c4b423f2eb7
|
4
|
+
data.tar.gz: abb8aad716399e6b87d351e33a8f9ff629256964aed2f131795255cec3353b2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2aa46cce2b26cd5a6e0219651dcdace5391a7a1b67ae5d4e6c3a9a849c6b12a63a2c913208af0ae58d5e7c7ce6880fdc2c1081a3a4b7582b2a8300f76548c0e3
|
7
|
+
data.tar.gz: c3cdc38d42b7c97894bbe34215e9a8143209732c78fd8b5d8351932d5e7290783ec8239c8f5c21e6fa4f2f36325b97a76088953a9b0a28a7fd71c697aaa91993
|
data/README.md
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
> Do not multiply entities beyond necessity. -- Occam's Razor
|
4
4
|
|
5
|
+
Full documentation is available at [rubydoc.info/gems/occams-record](http://www.rubydoc.info/gems/occams-record).
|
6
|
+
|
5
7
|
OccamsRecord is a high-efficiency, advanced query library for use alongside ActiveRecord. It is **not** an ORM or an ActiveRecord replacement. OccamsRecord can breathe fresh life into your ActiveRecord app by giving it two things:
|
6
8
|
|
7
9
|
### 1) Huge performance gains
|
@@ -73,6 +75,17 @@ OccamsRecord
|
|
73
75
|
.eager_load(:orders)
|
74
76
|
```
|
75
77
|
|
78
|
+
**Use `pluck` with raw SQL**
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
OccamsRecord
|
82
|
+
.sql("
|
83
|
+
SELECT some_col FROM users
|
84
|
+
INNER JOIN ...
|
85
|
+
")
|
86
|
+
.pluck(:some_col)
|
87
|
+
```
|
88
|
+
|
76
89
|
**Eager load "ad hoc associations" using raw SQL**
|
77
90
|
|
78
91
|
Relationships are complicated, and sometimes they can't be expressed in ActiveRecord models. Define your relationship on the fly!
|
@@ -303,7 +316,11 @@ The SQL string and binds should be familiar. `%{ids}` will be provided for you -
|
|
303
316
|
|
304
317
|
## Injecting instance methods
|
305
318
|
|
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
|
319
|
+
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.
|
320
|
+
|
321
|
+
### Include custom modules
|
322
|
+
|
323
|
+
You may also specify one or more modules to be included in your results:
|
307
324
|
|
308
325
|
```ruby
|
309
326
|
module MyOrderMethods
|
@@ -326,6 +343,23 @@ orders = OccamsRecord
|
|
326
343
|
.run
|
327
344
|
```
|
328
345
|
|
346
|
+
### ActiveRecord method fallback
|
347
|
+
|
348
|
+
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.
|
349
|
+
|
350
|
+
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.
|
351
|
+
|
352
|
+
The following will forward any nonexistent methods for `Order` and `Product` records:
|
353
|
+
|
354
|
+
```ruby
|
355
|
+
orders = OccamsRecord
|
356
|
+
.query(Order.all, active_record_fallback: :strict)
|
357
|
+
.eager_load(:line_items) {
|
358
|
+
eager_load(:product, active_record_fallback: :strict)
|
359
|
+
}
|
360
|
+
.run
|
361
|
+
```
|
362
|
+
|
329
363
|
---
|
330
364
|
|
331
365
|
# Unsupported features
|
@@ -14,6 +14,9 @@ module OccamsRecord
|
|
14
14
|
# @return [OccamsRecord::EagerLoaders::Tracer | nil] a reference to this eager loader and its parent (if any)
|
15
15
|
attr_reader :tracer
|
16
16
|
|
17
|
+
# @return [OccamsRecord::EagerLoaders::Context]
|
18
|
+
attr_reader :eager_loaders
|
19
|
+
|
17
20
|
#
|
18
21
|
# Initialize a new add hoc association.
|
19
22
|
#
|
@@ -12,6 +12,9 @@ module OccamsRecord
|
|
12
12
|
# @return [OccamsRecord::EagerLoaders::Tracer | nil] a reference to this eager loader and its parent (if any)
|
13
13
|
attr_reader :tracer
|
14
14
|
|
15
|
+
# @return [OccamsRecord::EagerLoaders::Context]
|
16
|
+
attr_reader :eager_loaders
|
17
|
+
|
15
18
|
#
|
16
19
|
# @param ref [ActiveRecord::Association] the ActiveRecord association
|
17
20
|
# @param scope [Proc] a scope to apply to the query (optional). It will be passed an
|
@@ -20,14 +23,16 @@ module OccamsRecord
|
|
20
23
|
# @param as [Symbol] Load the association usign a different attribute name
|
21
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)
|
22
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)
|
23
27
|
# @yield perform eager loading on *this* association (optional)
|
24
28
|
#
|
25
|
-
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, parent: nil, &builder)
|
29
|
+
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, parent: nil, active_record_fallback: nil, &builder)
|
26
30
|
@ref, @scopes, @use, @as = ref, Array(scope), use, as
|
27
31
|
@model = ref.klass
|
28
32
|
@name = (as || ref.name).to_s
|
29
33
|
@tracer = Tracer.new(name, parent)
|
30
34
|
@eager_loaders = EagerLoaders::Context.new(@model, tracer: @tracer)
|
35
|
+
@active_record_fallback = active_record_fallback
|
31
36
|
@optimizer = optimizer
|
32
37
|
if builder
|
33
38
|
if builder.arity > 0
|
@@ -60,7 +65,7 @@ module OccamsRecord
|
|
60
65
|
#
|
61
66
|
def run(rows, query_logger: nil, measurements: nil)
|
62
67
|
query(rows) { |*args|
|
63
|
-
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 : []
|
64
69
|
merge! assoc_rows, rows, *args[1..-1]
|
65
70
|
}
|
66
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
|
|
@@ -77,10 +77,11 @@ module OccamsRecord
|
|
77
77
|
# @param as [Symbol] Load the association usign a different attribute name
|
78
78
|
# @param from [Symbol] Opposite of `as`. `assoc` is the custom name and `from` is the name of association on the ActiveRecord model.
|
79
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)
|
80
81
|
# @yield a block where you may perform eager loading on *this* association (optional)
|
81
82
|
# @return [OccamsRecord::EagerLoaders::Base] the new loader. if @model is nil, nil will be returned.
|
82
83
|
#
|
83
|
-
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)
|
84
85
|
if from
|
85
86
|
real_assoc = from
|
86
87
|
custom_name = assoc
|
@@ -93,11 +94,11 @@ module OccamsRecord
|
|
93
94
|
end
|
94
95
|
|
95
96
|
if @model
|
96
|
-
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)
|
97
98
|
@loaders << loader
|
98
99
|
loader
|
99
100
|
else
|
100
|
-
@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]
|
101
102
|
nil
|
102
103
|
end
|
103
104
|
end
|
@@ -116,21 +117,27 @@ module OccamsRecord
|
|
116
117
|
nil
|
117
118
|
end
|
118
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
|
+
|
119
126
|
private
|
120
127
|
|
121
|
-
def build_loader!(assoc, custom_name, scope, select, use, optimizer, builder)
|
122
|
-
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) ||
|
123
130
|
raise("OccamsRecord: No association `:#{assoc}` on `#{@model.name}` or subclasses")
|
124
131
|
end
|
125
132
|
|
126
|
-
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)
|
127
134
|
ref = @model.reflections[assoc.to_s] ||
|
128
135
|
@model.subclasses.map(&:reflections).detect { |x| x.has_key? assoc.to_s }&.[](assoc.to_s)
|
129
136
|
return nil if ref.nil?
|
130
137
|
|
131
138
|
scope ||= ->(q) { q.select select } if select
|
132
139
|
loader_class = !!ref.through_reflection ? EagerLoaders::Through : EagerLoaders.fetch!(ref)
|
133
|
-
loader_class.new(ref, scope, use: use, as: custom_name, optimizer: optimizer, parent: @tracer, &builder)
|
140
|
+
loader_class.new(ref, scope, use: use, as: custom_name, optimizer: optimizer, parent: @tracer, active_record_fallback: active_record_fallback, &builder)
|
134
141
|
end
|
135
142
|
end
|
136
143
|
end
|
@@ -10,6 +10,9 @@ module OccamsRecord
|
|
10
10
|
# @return [OccamsRecord::EagerLoaders::Tracer | nil] a reference to this eager loader and its parent (if any)
|
11
11
|
attr_reader :tracer
|
12
12
|
|
13
|
+
# @return [OccamsRecord::EagerLoaders::Context]
|
14
|
+
attr_reader :eager_loaders
|
15
|
+
|
13
16
|
#
|
14
17
|
# @param ref [ActiveRecord::Association] the ActiveRecord association
|
15
18
|
# @param scope [Proc] a scope to apply to the query (optional). It will be passed an
|
@@ -18,15 +21,17 @@ module OccamsRecord
|
|
18
21
|
# @param as [Symbol] Load the association usign a different attribute name
|
19
22
|
# @param optimizer [Symbol] Only used for `through` associations. A no op here.
|
20
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)
|
21
25
|
# @yield perform eager loading on *this* association (optional)
|
22
26
|
#
|
23
|
-
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: nil, parent: nil, &builder)
|
27
|
+
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: nil, parent: nil, active_record_fallback: nil, &builder)
|
24
28
|
@ref, @scopes, @use = ref, Array(scope), use
|
25
29
|
@name = (as || ref.name).to_s
|
26
30
|
@foreign_type = @ref.foreign_type.to_sym
|
27
31
|
@foreign_key = @ref.foreign_key.to_sym
|
28
32
|
@tracer = Tracer.new(name, parent)
|
29
33
|
@eager_loaders = EagerLoaders::Context.new(nil, tracer: @tracer, polymorphic: true)
|
34
|
+
@active_record_fallback = active_record_fallback
|
30
35
|
if builder
|
31
36
|
if builder.arity > 0
|
32
37
|
builder.call(self)
|
@@ -60,7 +65,7 @@ module OccamsRecord
|
|
60
65
|
query(rows) { |scope|
|
61
66
|
eager_loaders = @eager_loaders.dup
|
62
67
|
eager_loaders.model = scope.klass
|
63
|
-
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
|
64
69
|
merge! assoc_rows, rows
|
65
70
|
}
|
66
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, parent: nil, &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
|
@@ -75,13 +75,18 @@ module OccamsRecord
|
|
75
75
|
tail = @chain[-1]
|
76
76
|
|
77
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
|
|
@@ -1,14 +1,15 @@
|
|
1
1
|
module OccamsRecord
|
2
2
|
module EagerLoaders
|
3
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) do
|
4
|
+
Tracer = Struct.new(:name, :parent, :through) do
|
5
5
|
def to_s
|
6
6
|
lookup.join(".")
|
7
7
|
end
|
8
8
|
|
9
9
|
def lookup(trace = self)
|
10
10
|
return [] if trace.nil?
|
11
|
-
|
11
|
+
name = trace.through ? "through(#{trace.name})" : trace.name
|
12
|
+
lookup(trace.parent) << name
|
12
13
|
end
|
13
14
|
end
|
14
15
|
end
|
data/lib/occams-record/errors.rb
CHANGED
@@ -27,7 +27,7 @@ module OccamsRecord
|
|
27
27
|
# @return [String]
|
28
28
|
def message
|
29
29
|
loads = @load_trace.to_s
|
30
|
-
"Column '#{name}' is unavailable on #{model_name} because it was not included in the SELECT statement!
|
30
|
+
"Column '#{name}' is unavailable on #{model_name} because it was not included in the SELECT statement! Occams Record trace: #{loads}"
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
@@ -36,7 +36,7 @@ module OccamsRecord
|
|
36
36
|
# @return [String]
|
37
37
|
def message
|
38
38
|
loads = @load_trace.to_s
|
39
|
-
"Association '#{name}' is unavailable on #{model_name} because it was not eager loaded!
|
39
|
+
"Association '#{name}' is unavailable on #{model_name} because it was not eager loaded! Occams Record trace: #{loads}"
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module OccamsRecord
|
2
|
+
module Pluck
|
3
|
+
private
|
4
|
+
|
5
|
+
def pluck_results(results, cols)
|
6
|
+
if cols.size == 1
|
7
|
+
col = cols[0].to_s
|
8
|
+
results.map { |r| r[col] }
|
9
|
+
else
|
10
|
+
cols = cols.map(&:to_s)
|
11
|
+
results.map { |r| r.values_at(*cols) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
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
|
#
|
@@ -35,6 +36,7 @@ module OccamsRecord
|
|
35
36
|
attr_reader :scope
|
36
37
|
|
37
38
|
include OccamsRecord::Batches::CursorHelpers
|
39
|
+
include OccamsRecord::Pluck
|
38
40
|
include EagerLoaders::Builder
|
39
41
|
include Enumerable
|
40
42
|
include Measureable
|
@@ -47,13 +49,15 @@ module OccamsRecord
|
|
47
49
|
# @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
|
48
50
|
# @param eager_loaders [OccamsRecord::EagerLoaders::Context]
|
49
51
|
# @param measurements [Array]
|
52
|
+
# @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
53
|
#
|
51
|
-
def initialize(scope, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil)
|
54
|
+
def initialize(scope, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil, active_record_fallback: nil)
|
52
55
|
@model = scope.klass
|
53
56
|
@scope = scope
|
54
57
|
@eager_loaders = eager_loaders || EagerLoaders::Context.new(@model)
|
55
58
|
@use = use
|
56
59
|
@query_logger, @measurements = query_logger, measurements
|
60
|
+
@active_record_fallback = active_record_fallback
|
57
61
|
end
|
58
62
|
|
59
63
|
#
|
@@ -101,7 +105,7 @@ module OccamsRecord
|
|
101
105
|
else
|
102
106
|
model.connection.exec_query sql
|
103
107
|
end
|
104
|
-
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: model, modules: @use, tracer: @eager_loaders.tracer)
|
108
|
+
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
109
|
rows = result.rows.map { |row| row_class.new row }
|
106
110
|
@eager_loaders.run!(rows, query_logger: @query_logger, measurements: @measurements)
|
107
111
|
yield_measurements!
|
@@ -222,5 +226,27 @@ module OccamsRecord
|
|
222
226
|
use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders,
|
223
227
|
)
|
224
228
|
end
|
229
|
+
|
230
|
+
#
|
231
|
+
# Returns the specified column(s) as an array of values.
|
232
|
+
#
|
233
|
+
# If more than one column is given, the result will be an array of arrays.
|
234
|
+
#
|
235
|
+
# @param cols [Array] one or more column names as Symbols or Strings. Also accepts SQL functions, e.g. "LENGTH(name)".
|
236
|
+
# @return [Array]
|
237
|
+
#
|
238
|
+
def pluck(*cols)
|
239
|
+
sql = (block_given? ? yield(scope).to_sql : scope).select(*cols).to_sql
|
240
|
+
@query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
|
241
|
+
result = if measure?
|
242
|
+
record_start_time!
|
243
|
+
measure!(model.table_name, sql) {
|
244
|
+
model.connection.exec_query sql
|
245
|
+
}
|
246
|
+
else
|
247
|
+
model.connection.exec_query sql
|
248
|
+
end
|
249
|
+
pluck_results result, cols
|
250
|
+
end
|
225
251
|
end
|
226
252
|
end
|
@@ -62,6 +62,7 @@ module OccamsRecord
|
|
62
62
|
attr_reader :binds
|
63
63
|
|
64
64
|
include OccamsRecord::Batches::CursorHelpers
|
65
|
+
include OccamsRecord::Pluck
|
65
66
|
include EagerLoaders::Builder
|
66
67
|
include Enumerable
|
67
68
|
include Measureable
|
@@ -209,6 +210,28 @@ module OccamsRecord
|
|
209
210
|
)
|
210
211
|
end
|
211
212
|
|
213
|
+
#
|
214
|
+
# Returns the specified column(s) as an array of values.
|
215
|
+
#
|
216
|
+
# If more than one column is given, the result will be an array of arrays.
|
217
|
+
#
|
218
|
+
# @param cols [Array] one or more column names as Symbols or Strings. Also accepts SQL functions, e.g. "LENGTH(name)".
|
219
|
+
# @return [Array]
|
220
|
+
#
|
221
|
+
def pluck(*cols)
|
222
|
+
_escaped_sql = escaped_sql
|
223
|
+
@query_logger << _escaped_sql if @query_logger
|
224
|
+
result = if measure?
|
225
|
+
record_start_time!
|
226
|
+
measure!(table_name, _escaped_sql) {
|
227
|
+
conn.exec_query _escaped_sql
|
228
|
+
}
|
229
|
+
else
|
230
|
+
conn.exec_query _escaped_sql
|
231
|
+
end
|
232
|
+
pluck_results result, cols
|
233
|
+
end
|
234
|
+
|
212
235
|
private
|
213
236
|
|
214
237
|
# Returns the SQL as a String with all variables escaped
|
@@ -17,10 +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 [OccamsRecord::EagerLoaders::
|
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)
|
21
22
|
# @return [OccamsRecord::Results::Row] a class customized for this result set
|
22
23
|
#
|
23
|
-
def self.klass(column_names, column_types, association_names = [], model: nil, modules: nil, tracer: 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
|
+
|
24
28
|
Class.new(Results::Row) do
|
25
29
|
Array(modules).each { |mod| prepend mod } if modules
|
26
30
|
|
@@ -30,6 +34,7 @@ module OccamsRecord
|
|
30
34
|
self.model_name = model ? model.name : nil
|
31
35
|
self.table_name = model ? model.table_name : nil
|
32
36
|
self.eager_loader_trace = tracer
|
37
|
+
self.active_record_fallback = active_record_fallback
|
33
38
|
self.primary_key = if model&.primary_key and (pkey = model.primary_key.to_s) and columns.include?(pkey)
|
34
39
|
pkey
|
35
40
|
end
|
@@ -21,6 +21,8 @@ module OccamsRecord
|
|
21
21
|
attr_accessor :primary_key
|
22
22
|
# A trace of how this record was loaded (for debugging)
|
23
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
|
24
26
|
end
|
25
27
|
self.columns = []
|
26
28
|
self.associations = []
|
@@ -112,19 +114,24 @@ module OccamsRecord
|
|
112
114
|
IDS_SUFFIX = /_ids$/
|
113
115
|
def method_missing(name, *args, &block)
|
114
116
|
model = self.class._model
|
115
|
-
|
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?
|
116
119
|
|
117
120
|
name_str = name.to_s
|
118
121
|
assoc = name_str.sub(IDS_SUFFIX, "").pluralize
|
119
|
-
|
122
|
+
no_args = args.empty? && block.nil?
|
123
|
+
|
124
|
+
if no_args and name_str =~ IDS_SUFFIX and can_define_ids_reader? assoc
|
120
125
|
define_ids_reader! assoc
|
121
126
|
send name
|
122
|
-
elsif model.reflections.has_key? name_str
|
127
|
+
elsif no_args and model.reflections.has_key? name_str
|
123
128
|
raise MissingEagerLoadError.new(self, name)
|
124
|
-
elsif model.columns_hash.has_key? name_str
|
129
|
+
elsif no_args and model.columns_hash.has_key? name_str
|
125
130
|
raise MissingColumnError.new(self, name)
|
131
|
+
elsif self.class.active_record_fallback
|
132
|
+
active_record_fallback(name, *args, &block)
|
126
133
|
else
|
127
|
-
|
134
|
+
raise ex
|
128
135
|
end
|
129
136
|
end
|
130
137
|
|
@@ -143,6 +150,15 @@ module OccamsRecord
|
|
143
150
|
|
144
151
|
private
|
145
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
|
+
|
146
162
|
def can_define_ids_reader?(assoc)
|
147
163
|
model = self.class._model
|
148
164
|
self.class.associations.include?(assoc) &&
|
data/lib/occams-record.rb
CHANGED
@@ -5,6 +5,7 @@ require 'occams-record/measureable'
|
|
5
5
|
require 'occams-record/eager_loaders/eager_loaders'
|
6
6
|
require 'occams-record/results/results'
|
7
7
|
require 'occams-record/results/row'
|
8
|
+
require 'occams-record/pluck'
|
8
9
|
require 'occams-record/cursor'
|
9
10
|
require 'occams-record/errors'
|
10
11
|
|
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.
|
4
|
+
version: 1.7.0
|
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-
|
11
|
+
date: 2023-03-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -74,6 +74,7 @@ files:
|
|
74
74
|
- lib/occams-record/errors.rb
|
75
75
|
- lib/occams-record/measureable.rb
|
76
76
|
- lib/occams-record/merge.rb
|
77
|
+
- lib/occams-record/pluck.rb
|
77
78
|
- lib/occams-record/query.rb
|
78
79
|
- lib/occams-record/raw_query.rb
|
79
80
|
- lib/occams-record/results/results.rb
|