occams-record 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a367407e2af3e4110327d26438fcb2a590b7fbd34e6b60dd4bd9396665c495fe
4
- data.tar.gz: 8cf6fb913ac787126a80fe38baff3a56940cea8afc80e1b05e3c864ae8368e11
3
+ metadata.gz: 6a233847b8c1a9de8c77e8c5e1d12f70ea7c8b9efa498208491a17ff09a6f217
4
+ data.tar.gz: f87fbaf43703cd29b60b94a480ad903d00cad2b9b3bd9bfb4a77b7748f68aa81
5
5
  SHA512:
6
- metadata.gz: 2bc45d11bc974e5a5ec6b2b9ffab8e3f069048ee8335122ad81a8b49cc852fa4bdb27116865000e4b70e4f6d617b3f870861288e00a43b273e3ec14d0dfdb6b6
7
- data.tar.gz: af1dac7260e93ed3778ff022c3d6113819528e37d77c275b9cada6cd5e731593bbe9e4f24ae283ed572a4876672126df4cb002ab8f9c863e3de4943f95acdbc2
6
+ metadata.gz: 21115e90f5f455eaf33d5c0596d6276957905fb14932583fcdfacdb1a44f7afeaee029458de903fa58c2801e7504643c6c6e4a530822409ff93da6954648a06e
7
+ data.tar.gz: a38eba237379a4a0b55d027746a8796f9aa9904e60bb1ba6acd6fd46348fc84e0aef17fc6de482d5b32d6677c09b2ea53602b000bf6da9a60d5bd2dec0e72b3f
data/README.md CHANGED
@@ -146,7 +146,7 @@ OccamsRecord
146
146
  order.line_items.each { |line_item|
147
147
  puts line_item.product.name
148
148
  puts line_item.product.category.name
149
- OccamsRecord::MissingEagerLoadError: Association 'category' is unavailable on Product because it was not eager loaded!
149
+ OccamsRecord::MissingEagerLoadError: Association 'category' is unavailable on Product because it was not eager loaded! Found at root.line_items.product
150
150
  }
151
151
  }
152
152
  ```
@@ -303,7 +303,11 @@ The SQL string and binds should be familiar. `%{ids}` will be provided for you -
303
303
 
304
304
  ## Injecting instance methods
305
305
 
306
- Occams Records results are just plain rows; there are no methods from your Rails models. (Separating your persistence layer from your domain is good thing!) But sometimes you need a few methods. Occams Record allows you to specify modules to be included in your results.
306
+ Occams Records results are just plain rows; there are no methods from your Rails models. (Separating your persistence layer from your domain is good thing!) But sometimes you need a few methods. Occams Record provides two ways of accomplishing this.
307
+
308
+ ### Include custom modules
309
+
310
+ You may also specify one or more modules to be included in your results:
307
311
 
308
312
  ```ruby
309
313
  module MyOrderMethods
@@ -326,6 +330,23 @@ orders = OccamsRecord
326
330
  .run
327
331
  ```
328
332
 
333
+ ### ActiveRecord method fallback
334
+
335
+ This is an ugly hack of last resort if you can't easily extract a method from your model into a shared module. Plugins, like `carrierwave`, are a good example. When you call a method that doesn't exist on an Occams Record result, it will initialize an ActiveRecord object and forward the method call to it.
336
+
337
+ The `active_record_fallback` option must be passed either `:lazy` or `:strict` (recommended). `:strict` enables ActiveRecord's strict loading option, helping you avoid N+1 queries. :lazy allows them. Note that `:strict` is only available for ActiveRecord 6.1 and later.
338
+
339
+ The following will forward any nonexistent methods for `Order` and `Product` records:
340
+
341
+ ```ruby
342
+ orders = OccamsRecord
343
+ .query(Order.all, active_record_fallback: :strict)
344
+ .eager_load(:line_items) {
345
+ eager_load(:product, active_record_fallback: :strict)
346
+ }
347
+ .run
348
+ ```
349
+
329
350
  ---
330
351
 
331
352
  # Unsupported features
@@ -11,6 +11,12 @@ module OccamsRecord
11
11
  # @return [String] association name
12
12
  attr_reader :name
13
13
 
14
+ # @return [OccamsRecord::EagerLoaders::Tracer | nil] a reference to this eager loader and its parent (if any)
15
+ attr_reader :tracer
16
+
17
+ # @return [OccamsRecord::EagerLoaders::Context]
18
+ attr_reader :eager_loaders
19
+
14
20
  #
15
21
  # Initialize a new add hoc association.
16
22
  #
@@ -20,12 +26,14 @@ module OccamsRecord
20
26
  # @param binds [Hash] any additional binds for your query.
21
27
  # @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
22
28
  # @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
29
+ # @param parent [OccamsRecord::EagerLoaders::Tracer] the eager loader this one is nested under (if any)
23
30
  # @yield eager load associations nested under this one
24
31
  #
25
- def initialize(name, mapping, sql, binds: {}, model: nil, use: nil, &builder)
32
+ def initialize(name, mapping, sql, binds: {}, model: nil, use: nil, parent: nil, &builder)
26
33
  @name, @mapping = name.to_s, mapping
27
34
  @sql, @binds, @use, @model = sql, binds, use, model
28
- @eager_loaders = EagerLoaders::Context.new(@model)
35
+ @tracer = Tracer.new(name, parent)
36
+ @eager_loaders = EagerLoaders::Context.new(@model, tracer: @tracer)
29
37
  if builder
30
38
  if builder.arity > 0
31
39
  builder.call(self)
@@ -9,6 +9,12 @@ module OccamsRecord
9
9
  # @return [String] association name
10
10
  attr_reader :name
11
11
 
12
+ # @return [OccamsRecord::EagerLoaders::Tracer | nil] a reference to this eager loader and its parent (if any)
13
+ attr_reader :tracer
14
+
15
+ # @return [OccamsRecord::EagerLoaders::Context]
16
+ attr_reader :eager_loaders
17
+
12
18
  #
13
19
  # @param ref [ActiveRecord::Association] the ActiveRecord association
14
20
  # @param scope [Proc] a scope to apply to the query (optional). It will be passed an
@@ -16,13 +22,17 @@ module OccamsRecord
16
22
  # @param use [Array(Module)] optional Module to include in the result class (single or array)
17
23
  # @param as [Symbol] Load the association usign a different attribute name
18
24
  # @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
25
+ # @param parent [OccamsRecord::EagerLoaders::Tracer] the eager loader this one is nested under (if any)
26
+ # @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
19
27
  # @yield perform eager loading on *this* association (optional)
20
28
  #
21
- def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, &builder)
29
+ def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, parent: nil, active_record_fallback: nil, &builder)
22
30
  @ref, @scopes, @use, @as = ref, Array(scope), use, as
23
31
  @model = ref.klass
24
32
  @name = (as || ref.name).to_s
25
- @eager_loaders = EagerLoaders::Context.new(@model)
33
+ @tracer = Tracer.new(name, parent)
34
+ @eager_loaders = EagerLoaders::Context.new(@model, tracer: @tracer)
35
+ @active_record_fallback = active_record_fallback
26
36
  @optimizer = optimizer
27
37
  if builder
28
38
  if builder.arity > 0
@@ -55,7 +65,7 @@ module OccamsRecord
55
65
  #
56
66
  def run(rows, query_logger: nil, measurements: nil)
57
67
  query(rows) { |*args|
58
- assoc_rows = args[0] ? Query.new(args[0], use: @use, eager_loaders: @eager_loaders, query_logger: query_logger, measurements: measurements).run : []
68
+ assoc_rows = args[0] ? Query.new(args[0], use: @use, eager_loaders: @eager_loaders, query_logger: query_logger, measurements: measurements, active_record_fallback: @active_record_fallback).run : []
59
69
  merge! assoc_rows, rows, *args[1..-1]
60
70
  }
61
71
  nil
@@ -24,11 +24,12 @@ module OccamsRecord
24
24
  # @param as [Symbol] Load the association usign a different attribute name
25
25
  # @param from [Symbol] Opposite of `as`. `assoc` is the custom name and `from` is the name of association on the ActiveRecord model.
26
26
  # @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
27
+ # @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
27
28
  # @yield a block where you may perform eager loading on *this* association (optional)
28
29
  # @return self
29
30
  #
30
- def eager_load(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, &builder)
31
- @eager_loaders.add(assoc, scope, select: select, use: use, as: as, from: from, optimizer: optimizer, &builder)
31
+ def eager_load(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, active_record_fallback: nil, &builder)
32
+ @eager_loaders.add(assoc, scope, select: select, use: use, as: as, from: from, optimizer: optimizer, active_record_fallback: active_record_fallback, &builder)
32
33
  self
33
34
  end
34
35
 
@@ -45,11 +46,12 @@ module OccamsRecord
45
46
  # @param as [Symbol] Load the association usign a different attribute name
46
47
  # @param from [Symbol] Opposite of `as`. `assoc` is the custom name and `from` is the name of association on the ActiveRecord model.
47
48
  # @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
49
+ # @param active_record_fallback [Symbol] If passed, the record(s) will be converted to read-only ActiveRecord objects. Options are :lazy (allow lazy loading) or :strict (enables strict loading)
48
50
  # @return [OccamsRecord::EagerLoaders::Base]
49
51
  #
50
- def nest(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select)
52
+ def nest(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, active_record_fallback: nil)
51
53
  raise ArgumentError, "OccamsRecord::EagerLoaders::Builder#nest does not accept a block!" if block_given?
52
- @eager_loaders.add(assoc, scope, select: select, use: use, as: as, from: from, optimizer: optimizer) ||
54
+ @eager_loaders.add(assoc, scope, select: select, use: use, as: as, from: from, optimizer: optimizer, active_record_fallback: active_record_fallback) ||
53
55
  raise("OccamsRecord::EagerLoaders::Builder#nest may not be called under a polymorphic association")
54
56
  end
55
57
 
@@ -14,14 +14,19 @@ module OccamsRecord
14
14
  # @return [ActiveRecord::Base]
15
15
  attr_reader :model
16
16
 
17
+ # @return [OccamsRecord::EagerLoaders::Tracer]
18
+ attr_reader :tracer
19
+
17
20
  #
18
21
  # Initialize a new eager loading context.
19
22
  #
20
23
  # @param mode [ActiveRecord::Base] the model that contains the associations that will be referenced.
24
+ # @param tracer [OccamsRecord::EagerLoaders::Tracer] the eager loader that owns this context (if any)
21
25
  # @param polymorphic [Boolean] When true, model is allowed to change, and it's assumed that not every loader is applicable to every model.
22
26
  #
23
- def initialize(model = nil, polymorphic: false)
27
+ def initialize(model = nil, tracer: nil, polymorphic: false)
24
28
  @model, @polymorphic = model, polymorphic
29
+ @tracer = tracer || OccamsRecord::EagerLoaders::Tracer.new("root")
25
30
  @loaders = []
26
31
  @dynamic_loaders = []
27
32
  end
@@ -72,10 +77,11 @@ module OccamsRecord
72
77
  # @param as [Symbol] Load the association usign a different attribute name
73
78
  # @param from [Symbol] Opposite of `as`. `assoc` is the custom name and `from` is the name of association on the ActiveRecord model.
74
79
  # @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
80
+ # @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
75
81
  # @yield a block where you may perform eager loading on *this* association (optional)
76
82
  # @return [OccamsRecord::EagerLoaders::Base] the new loader. if @model is nil, nil will be returned.
77
83
  #
78
- def add(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, &builder)
84
+ def add(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, active_record_fallback: nil, &builder)
79
85
  if from
80
86
  real_assoc = from
81
87
  custom_name = assoc
@@ -88,11 +94,11 @@ module OccamsRecord
88
94
  end
89
95
 
90
96
  if @model
91
- loader = build_loader!(real_assoc, custom_name, scope, select, use, optimizer, builder)
97
+ loader = build_loader!(real_assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback)
92
98
  @loaders << loader
93
99
  loader
94
100
  else
95
- @dynamic_loaders << [real_assoc, custom_name, scope, select, use, optimizer, builder]
101
+ @dynamic_loaders << [real_assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback]
96
102
  nil
97
103
  end
98
104
  end
@@ -111,21 +117,27 @@ module OccamsRecord
111
117
  nil
112
118
  end
113
119
 
120
+ # Iterate over each loader
121
+ def each
122
+ return @loaders.each unless block_given?
123
+ @loaders.each { |l| yield l }
124
+ end
125
+
114
126
  private
115
127
 
116
- def build_loader!(assoc, custom_name, scope, select, use, optimizer, builder)
117
- build_loader(assoc, custom_name, scope, select, use, optimizer, builder) ||
128
+ def build_loader!(assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback)
129
+ build_loader(assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback) ||
118
130
  raise("OccamsRecord: No association `:#{assoc}` on `#{@model.name}` or subclasses")
119
131
  end
120
132
 
121
- def build_loader(assoc, custom_name, scope, select, use, optimizer, builder)
133
+ def build_loader(assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback)
122
134
  ref = @model.reflections[assoc.to_s] ||
123
135
  @model.subclasses.map(&:reflections).detect { |x| x.has_key? assoc.to_s }&.[](assoc.to_s)
124
136
  return nil if ref.nil?
125
137
 
126
138
  scope ||= ->(q) { q.select select } if select
127
139
  loader_class = !!ref.through_reflection ? EagerLoaders::Through : EagerLoaders.fetch!(ref)
128
- loader_class.new(ref, scope, use: use, as: custom_name, optimizer: optimizer, &builder)
140
+ loader_class.new(ref, scope, use: use, as: custom_name, optimizer: optimizer, parent: @tracer, active_record_fallback: active_record_fallback, &builder)
129
141
  end
130
142
  end
131
143
  end
@@ -5,6 +5,7 @@ module OccamsRecord
5
5
  module EagerLoaders
6
6
  autoload :Builder, 'occams-record/eager_loaders/builder'
7
7
  autoload :Context, 'occams-record/eager_loaders/context'
8
+ autoload :Tracer, 'occams-record/eager_loaders/tracer'
8
9
 
9
10
  autoload :Base, 'occams-record/eager_loaders/base'
10
11
  autoload :BelongsTo, 'occams-record/eager_loaders/belongs_to'
@@ -7,6 +7,12 @@ module OccamsRecord
7
7
  # @return [String] association name
8
8
  attr_reader :name
9
9
 
10
+ # @return [OccamsRecord::EagerLoaders::Tracer | nil] a reference to this eager loader and its parent (if any)
11
+ attr_reader :tracer
12
+
13
+ # @return [OccamsRecord::EagerLoaders::Context]
14
+ attr_reader :eager_loaders
15
+
10
16
  #
11
17
  # @param ref [ActiveRecord::Association] the ActiveRecord association
12
18
  # @param scope [Proc] a scope to apply to the query (optional). It will be passed an
@@ -14,14 +20,18 @@ module OccamsRecord
14
20
  # @param use [Array<Module>] optional Module to include in the result class (single or array)
15
21
  # @param as [Symbol] Load the association usign a different attribute name
16
22
  # @param optimizer [Symbol] Only used for `through` associations. A no op here.
23
+ # @param parent [OccamsRecord::EagerLoaders::Tracer] the eager loader this one is nested under (if any)
24
+ # @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
17
25
  # @yield perform eager loading on *this* association (optional)
18
26
  #
19
- def initialize(ref, scope = nil, use: nil, as: nil, optimizer: nil, &builder)
27
+ def initialize(ref, scope = nil, use: nil, as: nil, optimizer: nil, parent: nil, active_record_fallback: nil, &builder)
20
28
  @ref, @scopes, @use = ref, Array(scope), use
21
29
  @name = (as || ref.name).to_s
22
30
  @foreign_type = @ref.foreign_type.to_sym
23
31
  @foreign_key = @ref.foreign_key.to_sym
24
- @eager_loaders = EagerLoaders::Context.new(nil, polymorphic: true)
32
+ @tracer = Tracer.new(name, parent)
33
+ @eager_loaders = EagerLoaders::Context.new(nil, tracer: @tracer, polymorphic: true)
34
+ @active_record_fallback = active_record_fallback
25
35
  if builder
26
36
  if builder.arity > 0
27
37
  builder.call(self)
@@ -55,7 +65,7 @@ module OccamsRecord
55
65
  query(rows) { |scope|
56
66
  eager_loaders = @eager_loaders.dup
57
67
  eager_loaders.model = scope.klass
58
- assoc_rows = Query.new(scope, use: @use, eager_loaders: eager_loaders, query_logger: query_logger, measurements: measurements).run
68
+ assoc_rows = Query.new(scope, use: @use, eager_loaders: eager_loaders, query_logger: query_logger, measurements: measurements, active_record_fallback: @active_record_fallback).run
59
69
  merge! assoc_rows, rows
60
70
  }
61
71
  nil
@@ -9,7 +9,7 @@ module OccamsRecord
9
9
  #
10
10
  # See documentation for OccamsRecord::EagerLoaders::Base.
11
11
  #
12
- def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, &builder)
12
+ def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, parent: nil, active_record_fallback: nil, &builder)
13
13
  super
14
14
 
15
15
  unless @ref.macro == :has_one or @ref.macro == :has_many
@@ -74,14 +74,19 @@ module OccamsRecord
74
74
  links = @chain[1..-2]
75
75
  tail = @chain[-1]
76
76
 
77
- outer_loader = EagerLoaders.fetch!(head.ref).new(head.ref, optimized_select(head))
77
+ outer_loader = EagerLoaders.fetch!(head.ref).new(head.ref, optimized_select(head), parent: tracer.parent)
78
+ outer_loader.tracer.through = true
78
79
 
79
- links.
80
+ inner_loader = links.
80
81
  reduce(outer_loader) { |loader, link|
81
- loader.nest(link.ref.source_reflection.name, optimized_select(link))
82
+ nested_loader = loader.nest(link.ref.source_reflection.name, optimized_select(link))
83
+ nested_loader.tracer.through = true
84
+ nested_loader
82
85
  }.
83
- nest(tail.ref.source_reflection.name, @scope, use: @use, as: @as)
86
+ nest(tail.ref.source_reflection.name, @scope, use: @use, as: @as, active_record_fallback: @active_record_fallback)
84
87
 
88
+ @eager_loaders.each { |loader| inner_loader.eager_loaders << loader }
89
+ inner_loader.tracer.name = tracer.name
85
90
  outer_loader
86
91
  end
87
92
 
@@ -0,0 +1,16 @@
1
+ module OccamsRecord
2
+ module EagerLoaders
3
+ # A low-memory way to trace the path of eager loads from any point back to the root query
4
+ Tracer = Struct.new(:name, :parent, :through) do
5
+ def to_s
6
+ lookup.join(".")
7
+ end
8
+
9
+ def lookup(trace = self)
10
+ return [] if trace.nil?
11
+ name = trace.through ? "through(#{trace.name})" : trace.name
12
+ lookup(trace.parent) << name
13
+ end
14
+ end
15
+ end
16
+ end
@@ -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
@@ -18,10 +18,11 @@ module OccamsRecord
18
18
  # @param scope [ActiveRecord::Relation]
19
19
  # @param use [Module] optional Module to include in the result class
20
20
  # @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
21
+ # @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
21
22
  # @return [OccamsRecord::Query]
22
23
  #
23
- def self.query(scope, use: nil, query_logger: nil)
24
- Query.new(scope, use: use, query_logger: query_logger)
24
+ def self.query(scope, use: nil, active_record_fallback: nil, query_logger: nil)
25
+ Query.new(scope, use: use, query_logger: query_logger, active_record_fallback: active_record_fallback)
25
26
  end
26
27
 
27
28
  #
@@ -47,13 +48,15 @@ module OccamsRecord
47
48
  # @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
48
49
  # @param eager_loaders [OccamsRecord::EagerLoaders::Context]
49
50
  # @param measurements [Array]
51
+ # @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
50
52
  #
51
- def initialize(scope, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil)
53
+ def initialize(scope, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil, active_record_fallback: nil)
52
54
  @model = scope.klass
53
55
  @scope = scope
54
56
  @eager_loaders = eager_loaders || EagerLoaders::Context.new(@model)
55
57
  @use = use
56
58
  @query_logger, @measurements = query_logger, measurements
59
+ @active_record_fallback = active_record_fallback
57
60
  end
58
61
 
59
62
  #
@@ -92,7 +95,7 @@ module OccamsRecord
92
95
  #
93
96
  def run
94
97
  sql = block_given? ? yield(scope).to_sql : scope.to_sql
95
- @query_logger << sql if @query_logger
98
+ @query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
96
99
  result = if measure?
97
100
  record_start_time!
98
101
  measure!(model.table_name, sql) {
@@ -101,7 +104,7 @@ module OccamsRecord
101
104
  else
102
105
  model.connection.exec_query sql
103
106
  end
104
- row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: model, modules: @use)
107
+ row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: model, modules: @use, tracer: @eager_loaders.tracer, active_record_fallback: @active_record_fallback)
105
108
  rows = result.rows.map { |row| row_class.new row }
106
109
  @eager_loaders.run!(rows, query_logger: @query_logger, measurements: @measurements)
107
110
  yield_measurements!
@@ -117,7 +117,7 @@ module OccamsRecord
117
117
  else
118
118
  conn.exec_query _escaped_sql
119
119
  end
120
- row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: @eager_loaders.model, modules: @use)
120
+ row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: @eager_loaders.model, modules: @use, tracer: @eager_loaders.tracer)
121
121
  rows = result.rows.map { |row| row_class.new row }
122
122
  @eager_loaders.run!(rows, query_logger: @query_logger, measurements: @measurements)
123
123
  yield_measurements!
@@ -17,9 +17,14 @@ module OccamsRecord
17
17
  # @param association_names [Array<String>] names of associations that will be eager loaded into the results.
18
18
  # @param model [ActiveRecord::Base] the AR model representing the table (it holds column & type info).
19
19
  # @param modules [Array<Module>] (optional)
20
+ # @param tracer [OccamsRecord::EagerLoaders::Tracer] the eager loaded that loaded this class of records
21
+ # @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
20
22
  # @return [OccamsRecord::Results::Row] a class customized for this result set
21
23
  #
22
- def self.klass(column_names, column_types, association_names = [], model: nil, modules: nil)
24
+ def self.klass(column_names, column_types, association_names = [], model: nil, modules: nil, tracer: nil, active_record_fallback: nil)
25
+ raise ArgumentError, "Invalid active_record_fallback option :#{active_record_fallback}. Valid options are :lazy, :strict" if active_record_fallback and !%i(lazy strict).include?(active_record_fallback)
26
+ raise ArgumentError, "Option active_record_fallback is not allowed when no model is present" if active_record_fallback and model.nil?
27
+
23
28
  Class.new(Results::Row) do
24
29
  Array(modules).each { |mod| prepend mod } if modules
25
30
 
@@ -28,6 +33,8 @@ module OccamsRecord
28
33
  self._model = model
29
34
  self.model_name = model ? model.name : nil
30
35
  self.table_name = model ? model.table_name : nil
36
+ self.eager_loader_trace = tracer
37
+ self.active_record_fallback = active_record_fallback
31
38
  self.primary_key = if model&.primary_key and (pkey = model.primary_key.to_s) and columns.include?(pkey)
32
39
  pkey
33
40
  end
@@ -19,6 +19,10 @@ module OccamsRecord
19
19
  attr_accessor :table_name
20
20
  # Name of primary key column (nil if column wasn't in the SELECT)
21
21
  attr_accessor :primary_key
22
+ # A trace of how this record was loaded (for debugging)
23
+ attr_accessor :eager_loader_trace
24
+ # If present, missing methods will be forwarded to the ActiveRecord model. :lazy allows lazy loading in AR, :strict doesn't
25
+ attr_accessor :active_record_fallback
22
26
  end
23
27
  self.columns = []
24
28
  self.associations = []
@@ -110,19 +114,24 @@ module OccamsRecord
110
114
  IDS_SUFFIX = /_ids$/
111
115
  def method_missing(name, *args, &block)
112
116
  model = self.class._model
113
- 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?
114
119
 
115
120
  name_str = name.to_s
116
121
  assoc = name_str.sub(IDS_SUFFIX, "").pluralize
117
- 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
118
125
  define_ids_reader! assoc
119
126
  send name
120
- elsif model.reflections.has_key? name_str
127
+ elsif no_args and model.reflections.has_key? name_str
121
128
  raise MissingEagerLoadError.new(self, name)
122
- elsif model.columns_hash.has_key? name_str
129
+ elsif no_args and model.columns_hash.has_key? name_str
123
130
  raise MissingColumnError.new(self, name)
131
+ elsif self.class.active_record_fallback
132
+ active_record_fallback(name, *args, &block)
124
133
  else
125
- super
134
+ raise ex
126
135
  end
127
136
  end
128
137
 
@@ -141,6 +150,15 @@ module OccamsRecord
141
150
 
142
151
  private
143
152
 
153
+ def active_record_fallback(name, *args, &block)
154
+ @active_record_fallback ||= Ugly::active_record(self.class._model, self).tap { |record|
155
+ record.strict_loading! if self.class.active_record_fallback == :strict
156
+ }
157
+ @active_record_fallback.send(name, *args, &block)
158
+ rescue NoMethodError => e
159
+ raise NoMethodError.new("#{e.message}. Occams Record trace: #{self.class.eager_loader_trace}.active_record_fallback(#{self.class._model.name})", name, args)
160
+ end
161
+
144
162
  def can_define_ids_reader?(assoc)
145
163
  model = self.class._model
146
164
  self.class.associations.include?(assoc) &&
@@ -3,5 +3,5 @@
3
3
  #
4
4
  module OccamsRecord
5
5
  # @private
6
- VERSION = "1.6.0".freeze
6
+ VERSION = "1.6.2".freeze
7
7
  end
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.0
4
+ version: 1.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Hollinger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-14 00:00:00.000000000 Z
11
+ date: 2023-02-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -70,6 +70,7 @@ files:
70
70
  - lib/occams-record/eager_loaders/has_one.rb
71
71
  - lib/occams-record/eager_loaders/polymorphic_belongs_to.rb
72
72
  - lib/occams-record/eager_loaders/through.rb
73
+ - lib/occams-record/eager_loaders/tracer.rb
73
74
  - lib/occams-record/errors.rb
74
75
  - lib/occams-record/measureable.rb
75
76
  - lib/occams-record/merge.rb