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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc34df60947ca8d92f1fa00ab74271fdec70439b07eccc2d5f3a2fa19a67efbe
4
- data.tar.gz: 7a77182c754af5c0cfd6689a86745af8edc260fb721076a7c317fab3cebb6c1c
3
+ metadata.gz: 5f2590ba264934763d86310d4cd9b92459cb053589afa23fb4ea4c4b423f2eb7
4
+ data.tar.gz: abb8aad716399e6b87d351e33a8f9ff629256964aed2f131795255cec3353b2a
5
5
  SHA512:
6
- metadata.gz: f9aac2db52de95e034e05c1fa2f078ea744cbecf715c92add927b2e227ec0ca20734575bec25f51da28bcab511febc2fd02a7efdb7def6a2a12165aebfa5eb8e
7
- data.tar.gz: 89bff53f5b3e975e03074b4bfb7088f25441ecf170acbe3f676376eae8fcaa8b943274d5731d65d12fe2757d1327b27b8730adc5e101025cd8c72a563ef03d22
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 allows you to specify modules to be included in your results.
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
- lookup(trace.parent) << trace.name
11
+ name = trace.through ? "through(#{trace.name})" : trace.name
12
+ lookup(trace.parent) << name
12
13
  end
13
14
  end
14
15
  end
@@ -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! Found at #{loads}"
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! Found at #{loads}"
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
@@ -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::Base] the eager loaded that loaded this class of records
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
- return super if args.any? or !block.nil? or model.nil?
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
- if name_str =~ IDS_SUFFIX and can_define_ids_reader? assoc
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
- super
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) &&
@@ -3,5 +3,5 @@
3
3
  #
4
4
  module OccamsRecord
5
5
  # @private
6
- VERSION = "1.6.1".freeze
6
+ VERSION = "1.7.0".freeze
7
7
  end
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.6.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-02-17 00:00:00.000000000 Z
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