occams-record 1.6.1 → 1.7.0
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 +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
|