occams-record 1.4.0 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,6 +10,12 @@ module OccamsRecord
10
10
  # Specify an association to be eager-loaded. For maximum memory savings, only SELECT the
11
11
  # colums you actually need.
12
12
  #
13
+ # If you pass a block to nest more eager loads, you may call it with one of two forms: with an argument and without:
14
+ #
15
+ # If you ommit the block argument, the "self" inside the block will be the eager loader. You can call "eager_load" and "scope" directly.
16
+ #
17
+ # If you include the block argument, the "self" inside the block is the same as the self outside the block. The argument will be the eager loader, which you can use to make additional "eager_load" or "scope" calls.
18
+ #
13
19
  # @param assoc [Symbol] name of association
14
20
  # @param scope [Proc] a scope to apply to the query (optional). It will be passed an
15
21
  # ActiveRecord::Relation on which you may call all the normal query hethods (select, where, etc) as well as any scopes you've defined on the model.
@@ -18,11 +24,12 @@ module OccamsRecord
18
24
  # @param as [Symbol] Load the association usign a different attribute name
19
25
  # @param from [Symbol] Opposite of `as`. `assoc` is the custom name and `from` is the name of association on the ActiveRecord model.
20
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)
21
28
  # @yield a block where you may perform eager loading on *this* association (optional)
22
29
  # @return self
23
30
  #
24
- def eager_load(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, &builder)
25
- @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)
26
33
  self
27
34
  end
28
35
 
@@ -39,11 +46,12 @@ module OccamsRecord
39
46
  # @param as [Symbol] Load the association usign a different attribute name
40
47
  # @param from [Symbol] Opposite of `as`. `assoc` is the custom name and `from` is the name of association on the ActiveRecord model.
41
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)
42
50
  # @return [OccamsRecord::EagerLoaders::Base]
43
51
  #
44
- 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)
45
53
  raise ArgumentError, "OccamsRecord::EagerLoaders::Builder#nest does not accept a block!" if block_given?
46
- @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) ||
47
55
  raise("OccamsRecord::EagerLoaders::Builder#nest may not be called under a polymorphic association")
48
56
  end
49
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) ||
118
- raise("OccamsRecord: No assocation `:#{assoc}` on `#{@model.name}` or subclasses")
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) ||
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'
@@ -24,23 +24,21 @@ module OccamsRecord
24
24
  # @param join_rows [Array<Array<String>>] raw join'd ids from the db
25
25
  #
26
26
  def merge!(assoc_rows, rows, join_rows)
27
- joins_by_id = join_rows.reduce({}) { |a, join|
27
+ joins_by_id = join_rows.each_with_object({}) { |join, acc|
28
28
  id = join[0].to_s
29
- a[id] ||= []
30
- a[id] << join[1].to_s
31
- a
29
+ acc[id] ||= []
30
+ acc[id] << join[1].to_s
32
31
  }
33
32
 
34
33
  assoc_order_cache = {} # maintains the original order of assoc_rows
35
- assoc_rows_by_id = assoc_rows.each_with_index.reduce({}) { |a, (row, idx)|
34
+ assoc_rows_by_id = assoc_rows.each_with_index.each_with_object({}) { |(row, idx), acc|
36
35
  begin
37
36
  id = row.send(@ref.association_primary_key).to_s
38
37
  rescue NoMethodError => e
39
38
  raise MissingColumnError.new(row, e.name)
40
39
  end
41
40
  assoc_order_cache[id] = idx
42
- a[id] = row
43
- a
41
+ acc[id] = row
44
42
  }
45
43
 
46
44
  assign = "#{name}="
@@ -20,7 +20,6 @@ module OccamsRecord
20
20
  raise MissingColumnError.new(row, e.name)
21
21
  end
22
22
  }.compact.uniq
23
- ids.sort! if $occams_record_test
24
23
 
25
24
  q = base_scope.where(@ref.foreign_key => ids)
26
25
  q.where!(@ref.type => rows[0].class&.model_name) if @ref.options[:as]
@@ -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,15 +20,39 @@ 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)
20
- @ref, @scope, @use = ref, scope, use
27
+ def initialize(ref, scope = nil, use: nil, as: nil, optimizer: nil, parent: nil, active_record_fallback: nil, &builder)
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
- @eager_loaders = EagerLoaders::Context.new(nil, polymorphic: true)
25
- instance_exec(&builder) if builder
32
+ @tracer = Tracer.new(name, parent)
33
+ @eager_loaders = EagerLoaders::Context.new(nil, tracer: @tracer, polymorphic: true)
34
+ @active_record_fallback = active_record_fallback
35
+ if builder
36
+ if builder.arity > 0
37
+ builder.call(self)
38
+ else
39
+ instance_exec(&builder)
40
+ end
41
+ end
42
+ end
43
+
44
+ #
45
+ # An alternative to passing a "scope" lambda to the constructor. Your block is passed the query
46
+ # so you can call select, where, order, etc on it.
47
+ #
48
+ # If you call scope multiple times, the results will be additive.
49
+ #
50
+ # @yield [ActiveRecord::Relation] a relation to modify with select, where, order, etc
51
+ # @return self
52
+ #
53
+ def scope(&scope)
54
+ @scopes << scope if scope
55
+ self
26
56
  end
27
57
 
28
58
  #
@@ -35,7 +65,7 @@ module OccamsRecord
35
65
  query(rows) { |scope|
36
66
  eager_loaders = @eager_loaders.dup
37
67
  eager_loaders.model = scope.klass
38
- 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
39
69
  merge! assoc_rows, rows
40
70
  }
41
71
  nil
@@ -55,7 +85,6 @@ module OccamsRecord
55
85
  next if type.nil? or type == ""
56
86
  model = type.constantize
57
87
  ids = rows_of_type.map(&@foreign_key).uniq
58
- ids.sort! if $occams_record_test
59
88
  q = base_scope(model).where(@ref.active_record_primary_key => ids)
60
89
  yield q if ids.any?
61
90
  end
@@ -83,7 +112,7 @@ module OccamsRecord
83
112
  def base_scope(model)
84
113
  q = model.all
85
114
  q = q.instance_exec(&@ref.scope) if @ref.scope
86
- q = @scope.(q) if @scope
115
+ q = @scopes.reduce(q) { |acc, scope| scope.(acc) }
87
116
  q
88
117
  end
89
118
  end
@@ -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
@@ -20,7 +20,7 @@ module OccamsRecord
20
20
  raise ArgumentError, "#{@ref.active_record.name}##{@ref.name} cannot be eager loaded because these `through` associations are polymorphic: #{names.join ', '}"
21
21
  end
22
22
  unless @optimizer == :none or @optimizer == :select
23
- raise ArgumentError, "Unrecognized optimizer '#{@optimizer}'"
23
+ raise ArgumentError, "Unrecognized optimizer '#{@optimizer}' (valid options are :none, :select)"
24
24
  end
25
25
 
26
26
  chain = @ref.chain.reverse
@@ -31,7 +31,6 @@ module OccamsRecord
31
31
  @loader = build_loader
32
32
  end
33
33
 
34
- # TODO make not hacky
35
34
  def through_name
36
35
  @loader.name
37
36
  end
@@ -47,6 +46,7 @@ module OccamsRecord
47
46
 
48
47
  private
49
48
 
49
+ # starting at the top of the chain, recurse and return the leaf node(s)
50
50
  def reduce(node, depth = 0)
51
51
  link = @chain[depth]
52
52
  case link&.macro
@@ -69,33 +69,48 @@ module OccamsRecord
69
69
  end
70
70
  end
71
71
 
72
+ # build all the nested eager loaders
72
73
  def build_loader
73
74
  head = @chain[0]
74
75
  links = @chain[1..-2]
75
76
  tail = @chain[-1]
76
77
 
77
- outer_loader = EagerLoaders.fetch!(head.ref).new(head.ref, optimized_select(head))
78
+ outer_loader = EagerLoaders.fetch!(head.ref).new(head.ref, optimized_scope(head), parent: tracer.parent)
79
+ outer_loader.tracer.through = true
78
80
 
79
- links.
81
+ inner_loader = links.
80
82
  reduce(outer_loader) { |loader, link|
81
- loader.nest(link.ref.source_reflection.name, optimized_select(link))
83
+ nested_loader = loader.nest(link.ref.source_reflection.name, optimized_scope(link))
84
+ nested_loader.tracer.through = true
85
+ nested_loader
82
86
  }.
83
- nest(tail.ref.source_reflection.name, @scope, use: @use, as: @as)
87
+ nest(tail.ref.source_reflection.name, @scopes, use: @use, as: @as, active_record_fallback: @active_record_fallback)
84
88
 
89
+ @eager_loaders.each { |loader| inner_loader.eager_loaders << loader }
90
+ inner_loader.tracer.name = tracer.name
85
91
  outer_loader
86
92
  end
87
93
 
88
- def optimized_select(link)
89
- return nil unless @optimizer == :select
94
+ def optimized_scope(link)
95
+ case @optimizer
96
+ when :select
97
+ optimized_select(link)
98
+ when :none
99
+ nil
100
+ end
101
+ end
90
102
 
91
- cols = case link.macro
92
- when :belongs_to
93
- [link.ref.association_primary_key]
94
- when :has_one, :has_many
95
- [link.ref.association_primary_key, link.ref.foreign_key]
96
- else
97
- raise "Unsupported through chain link type '#{link.macro}'"
98
- end
103
+ # only select the ids/foreign keys required to link the parent/child records
104
+ def optimized_select(link)
105
+ cols =
106
+ case link.macro
107
+ when :belongs_to
108
+ [link.ref.association_primary_key]
109
+ when :has_one, :has_many
110
+ [link.ref.association_primary_key, link.ref.foreign_key]
111
+ else
112
+ raise "Unsupported through chain link type '#{link.macro}'"
113
+ end
99
114
 
100
115
  case link.next_ref.source_reflection.macro
101
116
  when :belongs_to
@@ -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
@@ -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
- "Column '#{name}' is unavailable on #{model_name} because it was not included in the SELECT statement!"
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
- "Association '#{name}' is unavailable on #{model_name} because it was not eager loaded!"
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
@@ -35,14 +35,13 @@ module OccamsRecord
35
35
  # Optimized for merges where there's a single mapping key pair (which is the vast majority)
36
36
  if mapping.size == 1
37
37
  target_attr, assoc_attr = target_attrs[0], assoc_attrs[0]
38
- assoc_rows_by_ids = assoc_rows.reduce({}) { |a, assoc_row|
38
+ assoc_rows_by_ids = assoc_rows.each_with_object({}) { |assoc_row, acc|
39
39
  begin
40
40
  id = assoc_row.send assoc_attr
41
41
  rescue NoMethodError => e
42
42
  raise MissingColumnError.new(assoc_row, e.name)
43
43
  end
44
- a[id] ||= assoc_row
45
- a
44
+ acc[id] ||= assoc_row
46
45
  }
47
46
 
48
47
  target_rows.each do |row|
@@ -56,14 +55,13 @@ module OccamsRecord
56
55
 
57
56
  # Slower but works with any number of mapping key pairs
58
57
  else
59
- assoc_rows_by_ids = assoc_rows.reduce({}) { |a, assoc_row|
58
+ assoc_rows_by_ids = assoc_rows.each_with_object({}) { |assoc_row, acc|
60
59
  begin
61
60
  ids = assoc_attrs.map { |attr| assoc_row.send attr }
62
61
  rescue NoMethodError => e
63
62
  raise MissingColumnError.new(assoc_row, e.name)
64
63
  end
65
- a[ids] ||= assoc_row
66
- a
64
+ acc[ids] ||= assoc_row
67
65
  }
68
66
 
69
67
  target_rows.each do |row|
@@ -0,0 +1,38 @@
1
+ module OccamsRecord
2
+ module Pluck
3
+ private
4
+
5
+ def pluck_results(results, model: nil)
6
+ casters = TypeCaster.generate(results.columns, results.column_types, model: model)
7
+ if results[0]&.size == 1
8
+ pluck_results_single(results, casters)
9
+ else
10
+ pluck_results_multi(results, casters)
11
+ end
12
+ end
13
+
14
+ # returns an array of values
15
+ def pluck_results_single(results, casters)
16
+ col = results.columns[0]
17
+ caster = casters[col]
18
+ if caster
19
+ results.map { |row|
20
+ val = row[col]
21
+ caster.(val)
22
+ }
23
+ else
24
+ results.map { |row| row[col] }
25
+ end
26
+ end
27
+
28
+ # returns an array of arrays
29
+ def pluck_results_multi(results, casters)
30
+ results.map { |row|
31
+ row.map { |col, val|
32
+ caster = casters[col]
33
+ caster ? caster.(val) : val
34
+ }
35
+ }
36
+ end
37
+ end
38
+ 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
  #
@@ -92,16 +96,19 @@ module OccamsRecord
92
96
  #
93
97
  def run
94
98
  sql = block_given? ? yield(scope).to_sql : scope.to_sql
95
- @query_logger << sql if @query_logger
96
- result = if measure?
97
- record_start_time!
98
- measure!(model.table_name, sql) {
99
- model.connection.exec_query sql
100
- }
101
- else
102
- model.connection.exec_query sql
103
- end
104
- row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: model, modules: @use)
99
+ return [] if sql.blank? # return early in case ActiveRecord::QueryMethods#none was used
100
+
101
+ @query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
102
+ result =
103
+ if measure?
104
+ record_start_time!
105
+ measure!(model.table_name, sql) {
106
+ model.connection.exec_query sql
107
+ }
108
+ else
109
+ model.connection.exec_query sql
110
+ end
111
+ 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
112
  rows = result.rows.map { |row| row_class.new row }
106
113
  @eager_loaders.run!(rows, query_logger: @query_logger, measurements: @measurements)
107
114
  yield_measurements!
@@ -222,5 +229,30 @@ module OccamsRecord
222
229
  use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders,
223
230
  )
224
231
  end
232
+
233
+ #
234
+ # Returns the specified column(s) as an array of values.
235
+ #
236
+ # If more than one column is given, the result will be an array of arrays.
237
+ #
238
+ # @param cols [Array] one or more column names as Symbols or Strings. Also accepts SQL functions, e.g. "LENGTH(name)".
239
+ # @return [Array]
240
+ #
241
+ def pluck(*cols)
242
+ sql = (block_given? ? yield(scope).to_sql : scope).unscope(:select).select(*cols).to_sql
243
+ return [] if sql.blank? # return early in case ActiveRecord::QueryMethods#none was used
244
+
245
+ @query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
246
+ result =
247
+ if measure?
248
+ record_start_time!
249
+ measure!(model.table_name, sql) {
250
+ model.connection.exec_query sql
251
+ }
252
+ else
253
+ model.connection.exec_query sql
254
+ end
255
+ pluck_results(result, model: @model)
256
+ end
225
257
  end
226
258
  end