occams-record 1.5.0 → 1.6.1

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: d09d84ccc4713f32959380098b74112e45fa6a84a47698f1cec2d6fc4c4886a9
4
- data.tar.gz: b94244c91ed19b02cfb36ff02c72b6510ab38da73f7d76e13e8104713fa2ed03
3
+ metadata.gz: bc34df60947ca8d92f1fa00ab74271fdec70439b07eccc2d5f3a2fa19a67efbe
4
+ data.tar.gz: 7a77182c754af5c0cfd6689a86745af8edc260fb721076a7c317fab3cebb6c1c
5
5
  SHA512:
6
- metadata.gz: 950a0baa705302727a9ab9d20bb286e2fdc44cdd8b6aee3edb1b02c0a640786beca3492e799bcf4efe87c3a01289c6f4edb2daa07dc24650a5c29d6276733737
7
- data.tar.gz: dc07af2830bf268dac941834d36bc1cca3ffe64f5e1e3eadfe901b11fe1fcf545ba9804da4fd683697ee26201faf0936b06aaa203ac661aa14eed69b036e2109
6
+ metadata.gz: f9aac2db52de95e034e05c1fa2f078ea744cbecf715c92add927b2e227ec0ca20734575bec25f51da28bcab511febc2fd02a7efdb7def6a2a12165aebfa5eb8e
7
+ data.tar.gz: 89bff53f5b3e975e03074b4bfb7088f25441ecf170acbe3f676376eae8fcaa8b943274d5731d65d12fe2757d1327b27b8730adc5e101025cd8c72a563ef03d22
data/README.md CHANGED
@@ -20,8 +20,8 @@ Continue using ActiveRecord's query builder, but let Occams take over running th
20
20
  ```ruby
21
21
  OccamsRecord
22
22
  .query(User.active)
23
- .eager_load(:orders) {
24
- scope { |q| q.where("created_at >= ?", date).order("created_at DESC") }
23
+ .eager_load(:orders) { |l|
24
+ l.scope { |q| q.where("created_at >= ?", date).order("created_at DESC") }
25
25
  }
26
26
  ```
27
27
 
@@ -137,16 +137,16 @@ Eager loading is similiar to ActiveRecord's `preload`: each association is loade
137
137
  OccamsRecord
138
138
  .query(q)
139
139
  .eager_load(:customer)
140
- .eager_load(:line_items) {
141
- eager_load(:product)
142
- eager_load(:something_else)
140
+ .eager_load(:line_items) { |l|
141
+ l.eager_load(:product)
142
+ l.eager_load(:something_else)
143
143
  }
144
144
  .find_each { |order|
145
145
  puts order.customer.name
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
  ```
@@ -163,11 +163,11 @@ orders = OccamsRecord
163
163
 
164
164
  # Or use 'scope' to access the full power of ActiveRecord's query builder.
165
165
  # Here, only 'active' line items will be returned, and in a specific order.
166
- .eager_load(:line_items) {
167
- scope { |q| q.active.order("created_at") }
166
+ .eager_load(:line_items) { |l|
167
+ l.scope { |q| q.active.order("created_at") }
168
168
 
169
- eager_load(:product)
170
- eager_load(:something_else)
169
+ l.eager_load(:product)
170
+ l.eager_load(:something_else)
171
171
  }
172
172
  .run
173
173
  ```
@@ -267,9 +267,9 @@ Let's say we want to load each product with an array of all customers who've ord
267
267
  ```ruby
268
268
  products_with_orders = OccamsRecord
269
269
  .query(Product.all)
270
- .eager_load(:line_items) {
271
- eager_load(:order) {
272
- eager_load(:customer)
270
+ .eager_load(:line_items) { |l|
271
+ l.eager_load(:order) { |l|
272
+ l.eager_load(:customer)
273
273
  }
274
274
  }
275
275
  .map { |product|
@@ -11,6 +11,9 @@ 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
+
14
17
  #
15
18
  # Initialize a new add hoc association.
16
19
  #
@@ -20,13 +23,21 @@ module OccamsRecord
20
23
  # @param binds [Hash] any additional binds for your query.
21
24
  # @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
22
25
  # @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
26
+ # @param parent [OccamsRecord::EagerLoaders::Tracer] the eager loader this one is nested under (if any)
23
27
  # @yield eager load associations nested under this one
24
28
  #
25
- def initialize(name, mapping, sql, binds: {}, model: nil, use: nil, &builder)
29
+ def initialize(name, mapping, sql, binds: {}, model: nil, use: nil, parent: nil, &builder)
26
30
  @name, @mapping = name.to_s, mapping
27
31
  @sql, @binds, @use, @model = sql, binds, use, model
28
- @eager_loaders = EagerLoaders::Context.new(@model)
29
- instance_exec(&builder) if builder
32
+ @tracer = Tracer.new(name, parent)
33
+ @eager_loaders = EagerLoaders::Context.new(@model, tracer: @tracer)
34
+ if builder
35
+ if builder.arity > 0
36
+ builder.call(self)
37
+ else
38
+ instance_exec(&builder)
39
+ end
40
+ end
30
41
  end
31
42
 
32
43
  #
@@ -9,6 +9,9 @@ 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
+
12
15
  #
13
16
  # @param ref [ActiveRecord::Association] the ActiveRecord association
14
17
  # @param scope [Proc] a scope to apply to the query (optional). It will be passed an
@@ -16,15 +19,23 @@ module OccamsRecord
16
19
  # @param use [Array(Module)] optional Module to include in the result class (single or array)
17
20
  # @param as [Symbol] Load the association usign a different attribute name
18
21
  # @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
+ # @param parent [OccamsRecord::EagerLoaders::Tracer] the eager loader this one is nested under (if any)
19
23
  # @yield perform eager loading on *this* association (optional)
20
24
  #
21
- def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, &builder)
25
+ def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, parent: nil, &builder)
22
26
  @ref, @scopes, @use, @as = ref, Array(scope), use, as
23
27
  @model = ref.klass
24
28
  @name = (as || ref.name).to_s
25
- @eager_loaders = EagerLoaders::Context.new(@model)
29
+ @tracer = Tracer.new(name, parent)
30
+ @eager_loaders = EagerLoaders::Context.new(@model, tracer: @tracer)
26
31
  @optimizer = optimizer
27
- instance_exec(&builder) if builder
32
+ if builder
33
+ if builder.arity > 0
34
+ builder.call(self)
35
+ else
36
+ instance_exec(&builder)
37
+ end
38
+ end
28
39
  end
29
40
 
30
41
  #
@@ -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.
@@ -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
@@ -125,7 +130,7 @@ module OccamsRecord
125
130
 
126
131
  scope ||= ->(q) { q.select select } if select
127
132
  loader_class = !!ref.through_reflection ? EagerLoaders::Through : EagerLoaders.fetch!(ref)
128
- loader_class.new(ref, scope, use: use, as: custom_name, optimizer: optimizer, &builder)
133
+ loader_class.new(ref, scope, use: use, as: custom_name, optimizer: optimizer, parent: @tracer, &builder)
129
134
  end
130
135
  end
131
136
  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,9 @@ 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
+
10
13
  #
11
14
  # @param ref [ActiveRecord::Association] the ActiveRecord association
12
15
  # @param scope [Proc] a scope to apply to the query (optional). It will be passed an
@@ -14,15 +17,23 @@ module OccamsRecord
14
17
  # @param use [Array<Module>] optional Module to include in the result class (single or array)
15
18
  # @param as [Symbol] Load the association usign a different attribute name
16
19
  # @param optimizer [Symbol] Only used for `through` associations. A no op here.
20
+ # @param parent [OccamsRecord::EagerLoaders::Tracer] the eager loader this one is nested under (if any)
17
21
  # @yield perform eager loading on *this* association (optional)
18
22
  #
19
- def initialize(ref, scope = nil, use: nil, as: nil, optimizer: nil, &builder)
23
+ def initialize(ref, scope = nil, use: nil, as: nil, optimizer: nil, parent: nil, &builder)
20
24
  @ref, @scopes, @use = ref, Array(scope), use
21
25
  @name = (as || ref.name).to_s
22
26
  @foreign_type = @ref.foreign_type.to_sym
23
27
  @foreign_key = @ref.foreign_key.to_sym
24
- @eager_loaders = EagerLoaders::Context.new(nil, polymorphic: true)
25
- instance_exec(&builder) if builder
28
+ @tracer = Tracer.new(name, parent)
29
+ @eager_loaders = EagerLoaders::Context.new(nil, tracer: @tracer, polymorphic: true)
30
+ if builder
31
+ if builder.arity > 0
32
+ builder.call(self)
33
+ else
34
+ instance_exec(&builder)
35
+ end
36
+ end
26
37
  end
27
38
 
28
39
  #
@@ -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, &builder)
13
13
  super
14
14
 
15
15
  unless @ref.macro == :has_one or @ref.macro == :has_many
@@ -74,7 +74,7 @@ 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
78
 
79
79
  links.
80
80
  reduce(outer_loader) { |loader, link|
@@ -0,0 +1,15 @@
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) do
5
+ def to_s
6
+ lookup.join(".")
7
+ end
8
+
9
+ def lookup(trace = self)
10
+ return [] if trace.nil?
11
+ lookup(trace.parent) << trace.name
12
+ end
13
+ end
14
+ end
15
+ 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! Found at #{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! Found at #{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
@@ -92,7 +92,7 @@ module OccamsRecord
92
92
  #
93
93
  def run
94
94
  sql = block_given? ? yield(scope).to_sql : scope.to_sql
95
- @query_logger << sql if @query_logger
95
+ @query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
96
96
  result = if measure?
97
97
  record_start_time!
98
98
  measure!(model.table_name, sql) {
@@ -101,7 +101,7 @@ module OccamsRecord
101
101
  else
102
102
  model.connection.exec_query sql
103
103
  end
104
- row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: model, modules: @use)
104
+ row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: model, modules: @use, tracer: @eager_loaders.tracer)
105
105
  rows = result.rows.map { |row| row_class.new row }
106
106
  @eager_loaders.run!(rows, query_logger: @query_logger, measurements: @measurements)
107
107
  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,10 @@ 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
21
  # @return [OccamsRecord::Results::Row] a class customized for this result set
21
22
  #
22
- def self.klass(column_names, column_types, association_names = [], model: nil, modules: nil)
23
+ def self.klass(column_names, column_types, association_names = [], model: nil, modules: nil, tracer: nil)
23
24
  Class.new(Results::Row) do
24
25
  Array(modules).each { |mod| prepend mod } if modules
25
26
 
@@ -28,6 +29,7 @@ module OccamsRecord
28
29
  self._model = model
29
30
  self.model_name = model ? model.name : nil
30
31
  self.table_name = model ? model.table_name : nil
32
+ self.eager_loader_trace = tracer
31
33
  self.primary_key = if model&.primary_key and (pkey = model.primary_key.to_s) and columns.include?(pkey)
32
34
  pkey
33
35
  end
@@ -19,6 +19,8 @@ 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
22
24
  end
23
25
  self.columns = []
24
26
  self.associations = []
@@ -3,5 +3,5 @@
3
3
  #
4
4
  module OccamsRecord
5
5
  # @private
6
- VERSION = "1.5.0".freeze
6
+ VERSION = "1.6.1".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.5.0
4
+ version: 1.6.1
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-10 00:00:00.000000000 Z
11
+ date: 2023-02-17 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