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