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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1dc718c0f99f5ad039974f8db3bd1c5304e41a3b3275d26dd1d86235c7790fe
4
- data.tar.gz: 8af374148ca1f1c8342b7eb8fdb30031d6c31a1d923a6e7cab55b96712ed2602
3
+ metadata.gz: e26a6e33886dfdb5cb2d3000e32c9de7e04bb7b26cc4434c5df7f1deb094ac58
4
+ data.tar.gz: 9cc37f3ab8beeef6e55d5adae2e1f76268acad3381b789f70c896a515abe8cfa
5
5
  SHA512:
6
- metadata.gz: 54b539bc70b1fc92c087fbc91f7edbaa1eb4dabacfda80ca0afa4ccd4ac53e832d431f3c95e35785cfa7bd7075879c655ae54019c3f341255b4411929b5ae615
7
- data.tar.gz: c1c9aed1007bcfeb0e577d08af6f86fa340e375fede08af03bd18fbc2538cfcbd4838fc5bd07c672fca6e02ca3c0ec9e8d442b3e3275deff58215dfba873df7a
6
+ metadata.gz: a8236d746e52a2890dfb83e838db153150c249ff0b5fb53a1bc949d386258b5047dc93e63a40d747e0f584ffbcaf3d069122962c23a2fc3691a4e162044927af
7
+ data.tar.gz: 1883cb2f7d7817561837b911828b56b540b5dff7845a943340c489786b73be9b3c24270ec2d32a5e7b9ecdb324a21f346948f183e13439e9339424ee9b593ad9
data/README.md CHANGED
@@ -1,7 +1,9 @@
1
- # Occams Record [![Build Status](https://travis-ci.org/jhollinger/occams-record.svg?branch=master)](https://travis-ci.org/jhollinger/occams-record)
1
+ # Occams Record
2
2
 
3
3
  > Do not multiply entities beyond necessity. -- Occam's Razor
4
4
 
5
+ Full documentation is available at [rubydoc.info/gems/occams-record](http://www.rubydoc.info/gems/occams-record).
6
+
5
7
  OccamsRecord is a high-efficiency, advanced query library for use alongside ActiveRecord. It is **not** an ORM or an ActiveRecord replacement. OccamsRecord can breathe fresh life into your ActiveRecord app by giving it two things:
6
8
 
7
9
  ### 1) Huge performance gains
@@ -9,107 +11,33 @@ OccamsRecord is a high-efficiency, advanced query library for use alongside Acti
9
11
  * 3x-5x faster than ActiveRecord queries, *minimum*.
10
12
  * Uses 1/3 the memory of ActiveRecord query results.
11
13
  * Eliminates the N+1 query problem. (This often exceeds the baseline 3x-5x gain.)
12
- * Support for cursors (Postgres only, new in v1.4.0-beta1)
13
14
 
14
15
  ### 2) Supercharged querying & eager loading
15
16
 
16
- Continue using ActiveRecord's query builder, but let Occams take over running them, eager loading, and raw SQL calls. None of the examples below are possible with ActiveRecord, but OccamsRecord makes them trivial. (More complete examples are shown later, but these should whet your appetite.)
17
-
18
- **Customize the SQL used to eager load associations**
19
-
20
- ```ruby
21
- OccamsRecord
22
- .query(User.active)
23
- .eager_load(:orders, ->(q) { q.where("created_at >= ?", date).order("created_at DESC") })
24
- ```
25
-
26
- **Use `ORDER BY` with `find_each`/`find_in_batches`**
27
-
28
- ```ruby
29
- OccamsRecord
30
- .query(Order.order("created_at DESC"))
31
- .find_each { |order|
32
- ...
33
- }
34
- ```
35
-
36
- **Use cursors**
37
-
38
- ```ruby
39
- OccamsRecord
40
- .query(Order.order("created_at DESC"))
41
- .find_each_with_cursor { |order|
42
- ...
43
- }
44
- ```
45
-
46
- **Use `find_each`/`find_in_batches` with raw SQL**
47
-
48
- ```ruby
49
- OccamsRecord
50
- .sql("
51
- SELECT * FROM orders
52
- WHERE created_at >= %{date}
53
- LIMIT %{batch_limit}
54
- OFFSET %{batch_offset}",
55
- {date: 10.years.ago}
56
- )
57
- .find_each { |order|
58
- ...
59
- }
60
- ```
61
-
62
- **Eager load associations when you're writing raw SQL**
63
-
64
- ```ruby
65
- OccamsRecord
66
- .sql("
67
- SELECT * FROM users
68
- LEFT OUTER JOIN ...
69
- ")
70
- .model(User)
71
- .eager_load(:orders)
72
- ```
73
-
74
- **Eager load "ad hoc associations" using raw SQL**
75
-
76
- Relationships are complicated, and sometimes they can't be expressed in ActiveRecord models. Define your relationship on the fly!
77
- (Don't worry, there's a full explanation later on.)
78
-
79
- ```ruby
80
- OccamsRecord
81
- .query(User.all)
82
- .eager_load_many(:orders, {:id => :user_id}, "
83
- SELECT user_id, orders.*
84
- FROM orders INNER JOIN ...
85
- WHERE user_id IN (%{ids})
86
- ")
87
- ```
17
+ * Customize the SQL used to eager load associations (order them, apply filters, etc)
18
+ * Use cursors (Postgres only)
19
+ * Use `ORDER BY` with `find_each`/`find_in_batches`
20
+ * Use `find_each`/`find_in_batches` with raw SQL
21
+ * Eager load associations off of raw SQL queries
22
+ * Use `pluck` with raw SQL queries
88
23
 
24
+ ### How does OccamsRecord do all this?
89
25
  [Look over the speed and memory measurements yourself!](https://github.com/jhollinger/occams-record/wiki/Measurements) OccamsRecord achieves all of this by making some **very specific trade-offs:**
90
26
 
91
27
  * OccamsRecord results are *read-only*.
92
28
  * OccamsRecord results are *purely database rows* - they don't have any instance methods from your Rails models.
93
- * You *must eager load* each assocation you intend to use. If you forget one, an exception will be raised.
29
+ * You *must eager load* each assocation you intend to use. If you try to use one you didn't eager load, an exception will be raised.
94
30
 
95
- ---
31
+ # Overview
96
32
 
97
- # Installation
33
+ Full documentation is available at [rubydoc.info/gems/occams-record](http://www.rubydoc.info/gems/occams-record). Code lives at at [github.com/jhollinger/occams-record](https://github.com/jhollinger/occams-record). Contributions welcome!
98
34
 
99
- Simply add it to your `Gemfile`:
35
+ Simply add `occams-record` to your `Gemfile`:
100
36
 
101
37
  ```ruby
102
38
  gem 'occams-record'
103
39
  ```
104
40
 
105
- ---
106
-
107
- # Overview
108
-
109
- Full documentation is available at [rubydoc.info/gems/occams-record](http://www.rubydoc.info/gems/occams-record).
110
-
111
- Code lives at at [github.com/jhollinger/occams-record](https://github.com/jhollinger/occams-record). Contributions welcome!
112
-
113
41
  Build your queries like normal, using ActiveRecord's excellent query builder. Then pass them off to Occams Record.
114
42
 
115
43
  ```ruby
@@ -135,16 +63,16 @@ Eager loading is similiar to ActiveRecord's `preload`: each association is loade
135
63
  OccamsRecord
136
64
  .query(q)
137
65
  .eager_load(:customer)
138
- .eager_load(:line_items) {
139
- eager_load(:product)
140
- eager_load(:something_else)
66
+ .eager_load(:line_items) { |l|
67
+ l.eager_load(:product)
68
+ l.eager_load(:something_else)
141
69
  }
142
70
  .find_each { |order|
143
71
  puts order.customer.name
144
72
  order.line_items.each { |line_item|
145
73
  puts line_item.product.name
146
74
  puts line_item.product.category.name
147
- OccamsRecord::MissingEagerLoadError: Association 'category' is unavailable on Product because it was not eager loaded!
75
+ OccamsRecord::MissingEagerLoadError: Association 'category' is unavailable on Product because it was not eager loaded! Found at root.line_items.product
148
76
  }
149
77
  }
150
78
  ```
@@ -158,11 +86,14 @@ orders = OccamsRecord
158
86
  .query(q)
159
87
  # Only SELECT the columns you need. Your DBA will thank you.
160
88
  .eager_load(:customer, select: "id, name")
161
-
162
- # A Proc can use ActiveRecord's query builder
163
- .eager_load(:line_items, ->(q) { q.active.order("created_at") }) {
164
- eager_load(:product)
165
- eager_load(:something_else)
89
+
90
+ # Or use 'scope' to access the full power of ActiveRecord's query builder.
91
+ # Here, only 'active' line items will be returned, and in a specific order.
92
+ .eager_load(:line_items) { |l|
93
+ l.scope { |q| q.active.order("created_at") }
94
+
95
+ l.eager_load(:product)
96
+ l.eager_load(:something_else)
166
97
  }
167
98
  .run
168
99
  ```
@@ -199,6 +130,18 @@ orders = OccamsRecord
199
130
 
200
131
  ActiveRecord has raw SQL escape hatches like `find_by_sql` and `exec_query`, but they give up critical features like eager loading and `find_each`/`find_in_batches`. Occams Record's escape hatches don't make you give up anything.
201
132
 
133
+ **Query params**
134
+
135
+ ```ruby
136
+ # Supported in all versions of OccamsRecord
137
+ OccamsRecord.sql("SELECT * FROM orders WHERE user_id = %{user_id}", {user_id: user.id}).run
138
+
139
+ # Supported in OccamsRecord 1.9+
140
+ OccamsRecord.sql("SELECT * FROM orders WHERE user_id = :user_id", {user_id: user.id}).run
141
+ OccamsRecord.sql("SELECT * FROM orders WHERE user_id = ?", [user.id]).run
142
+ OccamsRecord.sql("SELECT * FROM orders WHERE user_id = %s", [user.id]).run
143
+ ```
144
+
202
145
  **Batched loading with cursors**
203
146
 
204
147
  `find_each_with_cursor`, `find_in_batches_with_cursor`, and `cursor.open` are all available.
@@ -262,9 +205,9 @@ Let's say we want to load each product with an array of all customers who've ord
262
205
  ```ruby
263
206
  products_with_orders = OccamsRecord
264
207
  .query(Product.all)
265
- .eager_load(:line_items) {
266
- eager_load(:order) {
267
- eager_load(:customer)
208
+ .eager_load(:line_items) { |l|
209
+ l.eager_load(:order) { |l|
210
+ l.eager_load(:customer)
268
211
  }
269
212
  }
270
213
  .map { |product|
@@ -298,7 +241,11 @@ The SQL string and binds should be familiar. `%{ids}` will be provided for you -
298
241
 
299
242
  ## Injecting instance methods
300
243
 
301
- 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.
244
+ 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.
245
+
246
+ ### Include custom modules
247
+
248
+ You may also specify one or more modules to be included in your results:
302
249
 
303
250
  ```ruby
304
251
  module MyOrderMethods
@@ -321,6 +268,23 @@ orders = OccamsRecord
321
268
  .run
322
269
  ```
323
270
 
271
+ ### ActiveRecord method fallback
272
+
273
+ 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.
274
+
275
+ 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.
276
+
277
+ The following will forward any nonexistent methods for `Order` and `Product` records:
278
+
279
+ ```ruby
280
+ orders = OccamsRecord
281
+ .query(Order.all, active_record_fallback: :strict)
282
+ .eager_load(:line_items) {
283
+ eager_load(:product, active_record_fallback: :strict)
284
+ }
285
+ .run
286
+ ```
287
+
324
288
  ---
325
289
 
326
290
  # Unsupported features
@@ -332,7 +296,6 @@ The following ActiveRecord features are under consideration, but not high priori
332
296
  The following ActiveRecord features are not supported, and likely never will be. Pull requests are still welcome, though.
333
297
 
334
298
  * Eager loading `through` associations that involve a polymorphic association.
335
- * ActiveRecord enum types
336
299
  * ActiveRecord serialized types
337
300
 
338
301
  ---
@@ -347,32 +310,50 @@ On the other hand, Active Record makes it *very* easy to forget to eager load as
347
310
 
348
311
  # Testing
349
312
 
350
- ```bash
351
- bundle install
352
-
353
- # test against SQLite
354
- bundle exec rake test
313
+ Tests are run with `appraisal` in Docker Compose using the `bin/test` or `bin/testall` scripts.
355
314
 
356
- # test against Postgres
357
- TEST_DATABASE_URL=postgres://postgres@localhost:5432/occams_record bundle exec rake test
358
-
359
- # test against MySQL
360
- TEST_DATABASE_URL=mysql2://root:@127.0.0.1:3306/occams_record bundle exec rake test
315
+ ```bash
316
+ # Run tests against all supported ActiveRecord versions, Ruby versions, and databases
317
+ bin/testall
318
+
319
+ # Run tests for Ruby vX only
320
+ bin/testall ruby-3.1
321
+
322
+ # Run tests for ActiveRecord vX only
323
+ bin/testall ar-6.1
324
+
325
+ # Run tests against a specific database
326
+ bin/testall sqlite|postgres-14|mysql-8
327
+
328
+ # Run exactly one set of tests
329
+ bin/test ruby-3.1 ar-7.0 postgres-14
330
+
331
+ # If all tests complete successfully, you'll be rewarded by an ASCII Nyancat!
332
+
333
+ + o + o
334
+ + o + +
335
+ o +
336
+ o + + +
337
+ + o o + o
338
+ -_-_-_-_-_-_-_,------, o
339
+ _-_-_-_-_-_-_-| /\_/\
340
+ -_-_-_-_-_-_-~|__( ^ .^) + +
341
+ _-_-_-_-_-_-_-"" ""
342
+ + o o + o
343
+ + +
344
+ o o o o +
345
+ o +
346
+ + + o o +
361
347
  ```
362
348
 
363
- **Test against all supported ActiveRecord versions**
349
+ ## Testing without Docker
364
350
 
365
- ```bash
366
- bundle exec appraisal install
367
-
368
- # test against all supported AR versions (defaults to SQLite)
369
- bundle exec appraisal rake test
351
+ It's possible to run tests without Docker Compose, but you'll be limited by the Ruby version(s) and database(s) you have on your system.
370
352
 
371
- # test against a specific AR version
353
+ ```bash
354
+ bundle install
355
+ bundle exec appraisal ar-7.0 bundle install
372
356
  bundle exec appraisal ar-7.0 rake test
373
-
374
- # test against Postgres
375
- TEST_DATABASE_URL=postgresql://postgres@localhost:5432/occams_record bundle exec appraisal rake test
376
357
  ```
377
358
 
378
359
  # License
@@ -9,8 +9,12 @@ module OccamsRecord
9
9
  @conn, @sql, @binds = conn, sql, binds
10
10
  @use, @query_logger, @eager_loaders = use, query_logger, eager_loaders
11
11
 
12
+ unless binds.is_a? Hash
13
+ raise ArgumentError, "When using find_each/find_in_batches with raw SQL, binds MUST be a Hash. SQL statement: #{@sql}"
14
+ end
15
+
12
16
  unless @sql =~ /LIMIT\s+%\{batch_limit\}/i and @sql =~ /OFFSET\s+%\{batch_offset\}/i
13
- raise ArgumentError, "When using find_each/find_in_batches you must specify 'LIMIT %{batch_limit} OFFSET %{batch_offset}'. SQL statement: #{@sql}"
17
+ raise ArgumentError, "When using find_each/find_in_batches with raw SQL, you must specify 'LIMIT %{batch_limit} OFFSET %{batch_offset}'. SQL statement: #{@sql}"
14
18
  end
15
19
  end
16
20
 
@@ -0,0 +1,71 @@
1
+ module OccamsRecord
2
+ module BindsConverter
3
+ #
4
+ # A base class for converting a SQL string with Rails-style query params (?, :foo) to native Ruby format (%s, %{foo}).
5
+ #
6
+ # It works kind of like a tokenizer. Subclasses must 1) implement get_bind to return the converted bind
7
+ # from the current position and 2) pass the bind sigil (e.g. ?, :) to the parent constructor.
8
+ #
9
+ class Abstract
10
+ # @private
11
+ ESCAPE = "\\".freeze
12
+
13
+ def initialize(sql, bind_sigil)
14
+ @sql = sql
15
+ @end = sql.size - 1
16
+ @start_i, @i = 0, 0
17
+ @bind_sigil = bind_sigil
18
+ end
19
+
20
+ # @return [String] The converted SQL string
21
+ def to_s
22
+ sql = ""
23
+ each { |frag| sql << frag }
24
+ sql
25
+ end
26
+
27
+ protected
28
+
29
+ # Yields each SQL fragment and converted bind to the given block
30
+ def each
31
+ escape = false
32
+ until @i > @end
33
+ char = @sql[@i]
34
+ unescape = escape
35
+ case char
36
+ when @bind_sigil
37
+ if escape
38
+ @i += 1
39
+ elsif @i > @start_i
40
+ yield flush_sql
41
+ else
42
+ yield get_bind
43
+ end
44
+ when ESCAPE
45
+ if escape
46
+ @i += 1
47
+ elsif @i > @start_i
48
+ yield flush_sql
49
+ escape = true
50
+ @i += 1
51
+ @start_i = @i
52
+ else
53
+ escape = true
54
+ @i += 1
55
+ end
56
+ else
57
+ @i += 1
58
+ end
59
+ escape = false if unescape
60
+ end
61
+ yield flush_sql if @i > @start_i
62
+ end
63
+
64
+ def flush_sql
65
+ t = @sql[@start_i..@i - 1]
66
+ @start_i = @i
67
+ t
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,35 @@
1
+ module OccamsRecord
2
+ module BindsConverter
3
+ # @private
4
+ WORD = /\w/
5
+
6
+ #
7
+ # Converts Rails-style named binds (:foo) into native Ruby format (%{foo}).
8
+ #
9
+ class Named < Abstract
10
+ def initialize(sql)
11
+ super(sql, ":".freeze)
12
+ end
13
+
14
+ protected
15
+
16
+ def get_bind
17
+ old_i = @i
18
+ @i += 1
19
+ @start_i = @i
20
+
21
+ until @i > @end or @sql[@i] !~ WORD
22
+ @i += 1
23
+ end
24
+
25
+ if @i > @start_i
26
+ name = @sql[@start_i..@i - 1]
27
+ @start_i = @i
28
+ "%{#{name}}"
29
+ else
30
+ @sql[old_i]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ module OccamsRecord
2
+ module BindsConverter
3
+ #
4
+ # Converts Rails-style positional binds (?) into native Ruby format (%s).
5
+ #
6
+ class Positional < Abstract
7
+ def initialize(sql)
8
+ super(sql, "?".freeze)
9
+ end
10
+
11
+ protected
12
+
13
+ def get_bind
14
+ @i += 1
15
+ @start_i = @i
16
+ "%s".freeze
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ module OccamsRecord
2
+ #
3
+ # Classes and methods for converting from Rails-style binds (?, :foo) to native Ruby format (%s, %{foo}).
4
+ #
5
+ module BindsConverter
6
+ #
7
+ # Convert any Rails-style binds (?, :foo) to native Ruby format (%s, %{foo}).
8
+ #
9
+ # @param sql [String]
10
+ # @param binds [Hash|Array]
11
+ # @return [String] the converted SQL string
12
+ #
13
+ def self.convert(sql, binds)
14
+ converter =
15
+ case binds
16
+ when Hash then Named.new(sql)
17
+ when Array then Positional.new(sql)
18
+ else raise ArgumentError, "OccamsRecord: Unsupported SQL bind params '#{binds.inspect}'. Only Hash and Array are supported"
19
+ end
20
+ converter.to_s
21
+ end
22
+ end
23
+ end
@@ -203,9 +203,8 @@ module OccamsRecord
203
203
  # end
204
204
  #
205
205
  def execute(sql, binds = {})
206
- conn.execute(sql % binds.reduce({}) { |acc, (key, val)|
206
+ conn.execute(sql % binds.each_with_object({}) { |(key, val), acc|
207
207
  acc[key] = conn.quote(val)
208
- acc
209
208
  })
210
209
  end
211
210
 
@@ -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,13 +26,21 @@ 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)
29
- instance_exec(&builder) if builder
35
+ @tracer = Tracer.new(name, parent)
36
+ @eager_loaders = EagerLoaders::Context.new(@model, tracer: @tracer)
37
+ if builder
38
+ if builder.arity > 0
39
+ builder.call(self)
40
+ else
41
+ instance_exec(&builder)
42
+ end
43
+ end
30
44
  end
31
45
 
32
46
  #
@@ -37,12 +51,13 @@ module OccamsRecord
37
51
  #
38
52
  def run(rows, query_logger: nil, measurements: nil)
39
53
  fkey_binds = calc_fkey_binds rows
40
- assoc = if fkey_binds.all? { |_, vals| vals.any? }
41
- binds = @binds.merge(fkey_binds)
42
- RawQuery.new(@sql, binds, use: @use, eager_loaders: @eager_loaders, query_logger: query_logger, measurements: measurements).run
43
- else
44
- []
45
- end
54
+ assoc =
55
+ if fkey_binds.all? { |_, vals| vals.any? }
56
+ binds = @binds.merge(fkey_binds)
57
+ RawQuery.new(@sql, binds, use: @use, eager_loaders: @eager_loaders, query_logger: query_logger, measurements: measurements).run
58
+ else
59
+ []
60
+ end
46
61
  merge! assoc, rows
47
62
  nil
48
63
  end
@@ -55,17 +70,15 @@ module OccamsRecord
55
70
  # @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
56
71
  #
57
72
  def calc_fkey_binds(rows)
58
- @mapping.keys.reduce({}) { |a, fkey|
59
- a[fkey.to_s.pluralize.to_sym] = rows.reduce(Set.new) { |aa, row|
73
+ @mapping.keys.each_with_object({}) { |fkey, acc|
74
+ acc[fkey.to_s.pluralize.to_sym] = rows.each_with_object(Set.new) { |row, acc2|
60
75
  begin
61
76
  val = row.send fkey
62
- aa << val if val
77
+ acc2 << val if val
63
78
  rescue NoMethodError => e
64
79
  raise MissingColumnError.new(row, e.name)
65
80
  end
66
- aa
67
81
  }.to_a
68
- a
69
82
  }
70
83
  end
71
84
 
@@ -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,15 +22,39 @@ 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)
22
- @ref, @scope, @use, @as = ref, scope, use, as
29
+ def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, parent: nil, active_record_fallback: nil, &builder)
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
- instance_exec(&builder) if builder
37
+ if builder
38
+ if builder.arity > 0
39
+ builder.call(self)
40
+ else
41
+ instance_exec(&builder)
42
+ end
43
+ end
44
+ end
45
+
46
+ #
47
+ # An alternative to passing a "scope" lambda to the constructor. Your block is passed the query
48
+ # so you can call select, where, order, etc on it.
49
+ #
50
+ # If you call scope multiple times, the results will be additive.
51
+ #
52
+ # @yield [ActiveRecord::Relation] a relation to modify with select, where, order, etc
53
+ # @return self
54
+ #
55
+ def scope(&scope)
56
+ @scopes << scope if scope
57
+ self
28
58
  end
29
59
 
30
60
  #
@@ -35,7 +65,7 @@ module OccamsRecord
35
65
  #
36
66
  def run(rows, query_logger: nil, measurements: nil)
37
67
  query(rows) { |*args|
38
- 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 : []
39
69
  merge! assoc_rows, rows, *args[1..-1]
40
70
  }
41
71
  nil
@@ -71,7 +101,7 @@ module OccamsRecord
71
101
  def base_scope
72
102
  q = @ref.klass.all
73
103
  q = q.instance_exec(&@ref.scope) if @ref.scope
74
- q = @scope.(q) if @scope
104
+ q = @scopes.reduce(q) { |acc, scope| scope.(acc) }
75
105
  q
76
106
  end
77
107
  end
@@ -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.association_primary_key => ids)
26
25
  yield q if ids.any?