occams-record 1.6.0 → 1.6.2

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: 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