occams-record 1.4.0 → 1.9.0
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 +4 -4
- data/README.md +101 -120
- data/lib/occams-record/batches/offset_limit/raw_query.rb +5 -1
- data/lib/occams-record/binds_converter/abstract.rb +71 -0
- data/lib/occams-record/binds_converter/named.rb +35 -0
- data/lib/occams-record/binds_converter/positional.rb +20 -0
- data/lib/occams-record/binds_converter.rb +23 -0
- data/lib/occams-record/cursor.rb +1 -2
- data/lib/occams-record/eager_loaders/ad_hoc_base.rb +27 -14
- data/lib/occams-record/eager_loaders/base.rb +36 -6
- data/lib/occams-record/eager_loaders/belongs_to.rb +0 -1
- data/lib/occams-record/eager_loaders/builder.rb +12 -4
- data/lib/occams-record/eager_loaders/context.rb +21 -9
- data/lib/occams-record/eager_loaders/eager_loaders.rb +1 -0
- data/lib/occams-record/eager_loaders/habtm.rb +5 -7
- data/lib/occams-record/eager_loaders/has_one.rb +0 -1
- data/lib/occams-record/eager_loaders/polymorphic_belongs_to.rb +36 -7
- data/lib/occams-record/eager_loaders/through.rb +32 -17
- data/lib/occams-record/eager_loaders/tracer.rb +16 -0
- data/lib/occams-record/errors.rb +6 -2
- data/lib/occams-record/merge.rb +4 -6
- data/lib/occams-record/pluck.rb +38 -0
- data/lib/occams-record/query.rb +45 -13
- data/lib/occams-record/raw_query.rb +62 -25
- data/lib/occams-record/results/results.rb +25 -30
- data/lib/occams-record/results/row.rb +33 -16
- data/lib/occams-record/type_caster.rb +62 -0
- data/lib/occams-record/version.rb +1 -1
- data/lib/occams-record.rb +7 -0
- metadata +13 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e26a6e33886dfdb5cb2d3000e32c9de7e04bb7b26cc4434c5df7f1deb094ac58
|
4
|
+
data.tar.gz: 9cc37f3ab8beeef6e55d5adae2e1f76268acad3381b789f70c896a515abe8cfa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a8236d746e52a2890dfb83e838db153150c249ff0b5fb53a1bc949d386258b5047dc93e63a40d747e0f584ffbcaf3d069122962c23a2fc3691a4e162044927af
|
7
|
+
data.tar.gz: 1883cb2f7d7817561837b911828b56b540b5dff7845a943340c489786b73be9b3c24270ec2d32a5e7b9ecdb324a21f346948f183e13439e9339424ee9b593ad9
|
data/README.md
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
-
# 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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
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
|
-
|
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
|
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
|
-
#
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
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
|
-
|
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
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
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
|
-
|
349
|
+
## Testing without Docker
|
364
350
|
|
365
|
-
|
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
|
-
|
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
|
data/lib/occams-record/cursor.rb
CHANGED
@@ -203,9 +203,8 @@ module OccamsRecord
|
|
203
203
|
# end
|
204
204
|
#
|
205
205
|
def execute(sql, binds = {})
|
206
|
-
conn.execute(sql % binds.
|
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
|
-
@
|
29
|
-
|
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 =
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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.
|
59
|
-
|
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
|
-
|
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, @
|
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
|
-
@
|
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
|
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 = @
|
104
|
+
q = @scopes.reduce(q) { |acc, scope| scope.(acc) }
|
75
105
|
q
|
76
106
|
end
|
77
107
|
end
|