occams-record 1.8.1 → 1.9.1
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 +35 -6
- 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 +10 -11
- data/lib/occams-record/eager_loaders/belongs_to.rb +0 -1
- 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 +0 -1
- data/lib/occams-record/eager_loaders/through.rb +25 -15
- data/lib/occams-record/merge.rb +4 -6
- data/lib/occams-record/pluck.rb +15 -12
- data/lib/occams-record/query.rb +20 -18
- data/lib/occams-record/raw_query.rb +51 -37
- data/lib/occams-record/results/results.rb +6 -5
- data/lib/occams-record/results/row.rb +10 -10
- data/lib/occams-record/type_caster.rb +8 -6
- data/lib/occams-record/version.rb +1 -1
- data/lib/occams-record.rb +5 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7d307ad8676050496976606227a5a671dbccb156be8ba2799b2e845f7b79be8
|
|
4
|
+
data.tar.gz: 0c268a2bf9dc72f045067608b7981b12c18577623ebb497bd3551a82a43cbeea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6073e65f4e3f7cac8b12f7da49f66dd914249f5b32eb3c42a8a8b0635eafe5bbdff6f63af1cbb8d227cdff82843f839a44b74b9d0d1c193351f96ef7089bd1d9
|
|
7
|
+
data.tar.gz: f3f42c3c8d3e6f42515a864bd69407093176f1794c92241b9c0c17045a61d5b4698ae1ea699d781d824b5c4779138b006b50079e9b07b1ce089c58be59c3ed9b
|
data/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Occams Record
|
|
2
2
|
|
|
3
3
|
> Do not multiply entities beyond necessity. -- Occam's Razor
|
|
4
|
+
>
|
|
5
|
+
>
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
Learn OccamsRecord by reading [The Book at occams.jordanhollinger.com](https://occams.jordanhollinger.com/).
|
|
8
|
+
|
|
9
|
+
API documentation is available at [rubydoc.info/gems/occams-record](http://www.rubydoc.info/gems/occams-record).
|
|
6
10
|
|
|
7
11
|
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:
|
|
8
12
|
|
|
@@ -130,6 +134,18 @@ orders = OccamsRecord
|
|
|
130
134
|
|
|
131
135
|
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.
|
|
132
136
|
|
|
137
|
+
**Query params**
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
# Supported in all versions of OccamsRecord
|
|
141
|
+
OccamsRecord.sql("SELECT * FROM orders WHERE user_id = %{user_id}", {user_id: user.id}).run
|
|
142
|
+
|
|
143
|
+
# Supported in OccamsRecord 1.9+
|
|
144
|
+
OccamsRecord.sql("SELECT * FROM orders WHERE user_id = :user_id", {user_id: user.id}).run
|
|
145
|
+
OccamsRecord.sql("SELECT * FROM orders WHERE user_id = ?", [user.id]).run
|
|
146
|
+
OccamsRecord.sql("SELECT * FROM orders WHERE user_id = %s", [user.id]).run
|
|
147
|
+
```
|
|
148
|
+
|
|
133
149
|
**Batched loading with cursors**
|
|
134
150
|
|
|
135
151
|
`find_each_with_cursor`, `find_in_batches_with_cursor`, and `cursor.open` are all available.
|
|
@@ -298,24 +314,27 @@ On the other hand, Active Record makes it *very* easy to forget to eager load as
|
|
|
298
314
|
|
|
299
315
|
# Testing
|
|
300
316
|
|
|
301
|
-
Tests are run with `appraisal` in Docker Compose using the `bin/test` or `bin/testall` scripts.
|
|
317
|
+
Tests are run with `appraisal` in Docker Compose using the `bin/test` or `bin/testall` scripts. See [test/matrix](./test/matrix) for the full list of Ruby, ActiveRecord, and database versions that are tested against.
|
|
302
318
|
|
|
303
319
|
```bash
|
|
304
320
|
# Run tests against all supported ActiveRecord versions, Ruby versions, and databases
|
|
305
321
|
bin/testall
|
|
306
322
|
|
|
307
|
-
# Run tests for Ruby
|
|
323
|
+
# Run tests only for Ruby 3.1
|
|
308
324
|
bin/testall ruby-3.1
|
|
309
325
|
|
|
310
|
-
# Run tests for
|
|
311
|
-
bin/testall ar-6.1
|
|
326
|
+
# Run tests only for Ruby 3.1 and ActiveRecored 6.1
|
|
327
|
+
bin/testall ruby-3.1 ar-6.1
|
|
312
328
|
|
|
313
329
|
# Run tests against a specific database
|
|
314
|
-
bin/testall
|
|
330
|
+
bin/testall sqlite3|postgres-14|mysql-8
|
|
315
331
|
|
|
316
332
|
# Run exactly one set of tests
|
|
317
333
|
bin/test ruby-3.1 ar-7.0 postgres-14
|
|
318
334
|
|
|
335
|
+
# Use Podman Compose
|
|
336
|
+
OCCAMS_PODMAN=1 bin/testall
|
|
337
|
+
|
|
319
338
|
# If all tests complete successfully, you'll be rewarded by an ASCII Nyancat!
|
|
320
339
|
|
|
321
340
|
+ o + o
|
|
@@ -334,6 +353,16 @@ o o o o +
|
|
|
334
353
|
+ + o o +
|
|
335
354
|
```
|
|
336
355
|
|
|
356
|
+
## Testing without Docker
|
|
357
|
+
|
|
358
|
+
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.
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
bundle install
|
|
362
|
+
bundle exec appraisal ar-7.0 bundle install
|
|
363
|
+
bundle exec appraisal ar-7.0 rake test
|
|
364
|
+
```
|
|
365
|
+
|
|
337
366
|
# License
|
|
338
367
|
|
|
339
368
|
MIT License. See LICENSE for details.
|
|
@@ -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
|
+
clear_escape = 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 clear_escape
|
|
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
|
|
|
@@ -51,12 +51,13 @@ module OccamsRecord
|
|
|
51
51
|
#
|
|
52
52
|
def run(rows, query_logger: nil, measurements: nil)
|
|
53
53
|
fkey_binds = calc_fkey_binds rows
|
|
54
|
-
assoc =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
60
61
|
merge! assoc, rows
|
|
61
62
|
nil
|
|
62
63
|
end
|
|
@@ -69,17 +70,15 @@ module OccamsRecord
|
|
|
69
70
|
# @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
|
|
70
71
|
#
|
|
71
72
|
def calc_fkey_binds(rows)
|
|
72
|
-
@mapping.keys.
|
|
73
|
-
|
|
73
|
+
@mapping.keys.each_with_object({}) { |fkey, acc|
|
|
74
|
+
acc[fkey.to_s.pluralize.to_sym] = rows.each_with_object(Set.new) { |row, acc2|
|
|
74
75
|
begin
|
|
75
76
|
val = row.send fkey
|
|
76
|
-
|
|
77
|
+
acc2 << val if val
|
|
77
78
|
rescue NoMethodError => e
|
|
78
79
|
raise MissingColumnError.new(row, e.name)
|
|
79
80
|
end
|
|
80
|
-
aa
|
|
81
81
|
}.to_a
|
|
82
|
-
a
|
|
83
82
|
}
|
|
84
83
|
end
|
|
85
84
|
|
|
@@ -24,23 +24,21 @@ module OccamsRecord
|
|
|
24
24
|
# @param join_rows [Array<Array<String>>] raw join'd ids from the db
|
|
25
25
|
#
|
|
26
26
|
def merge!(assoc_rows, rows, join_rows)
|
|
27
|
-
joins_by_id = join_rows.
|
|
27
|
+
joins_by_id = join_rows.each_with_object({}) { |join, acc|
|
|
28
28
|
id = join[0].to_s
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
a
|
|
29
|
+
acc[id] ||= []
|
|
30
|
+
acc[id] << join[1].to_s
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
assoc_order_cache = {} # maintains the original order of assoc_rows
|
|
35
|
-
assoc_rows_by_id = assoc_rows.each_with_index.
|
|
34
|
+
assoc_rows_by_id = assoc_rows.each_with_index.each_with_object({}) { |(row, idx), acc|
|
|
36
35
|
begin
|
|
37
36
|
id = row.send(@ref.association_primary_key).to_s
|
|
38
37
|
rescue NoMethodError => e
|
|
39
38
|
raise MissingColumnError.new(row, e.name)
|
|
40
39
|
end
|
|
41
40
|
assoc_order_cache[id] = idx
|
|
42
|
-
|
|
43
|
-
a
|
|
41
|
+
acc[id] = row
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
assign = "#{name}="
|
|
@@ -85,7 +85,6 @@ module OccamsRecord
|
|
|
85
85
|
next if type.nil? or type == ""
|
|
86
86
|
model = type.constantize
|
|
87
87
|
ids = rows_of_type.map(&@foreign_key).uniq
|
|
88
|
-
ids.sort! if $occams_record_test
|
|
89
88
|
q = base_scope(model).where(@ref.active_record_primary_key => ids)
|
|
90
89
|
yield q if ids.any?
|
|
91
90
|
end
|
|
@@ -20,7 +20,7 @@ module OccamsRecord
|
|
|
20
20
|
raise ArgumentError, "#{@ref.active_record.name}##{@ref.name} cannot be eager loaded because these `through` associations are polymorphic: #{names.join ', '}"
|
|
21
21
|
end
|
|
22
22
|
unless @optimizer == :none or @optimizer == :select
|
|
23
|
-
raise ArgumentError, "Unrecognized optimizer '#{@optimizer}'"
|
|
23
|
+
raise ArgumentError, "Unrecognized optimizer '#{@optimizer}' (valid options are :none, :select)"
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
chain = @ref.chain.reverse
|
|
@@ -31,7 +31,6 @@ module OccamsRecord
|
|
|
31
31
|
@loader = build_loader
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
# TODO make not hacky
|
|
35
34
|
def through_name
|
|
36
35
|
@loader.name
|
|
37
36
|
end
|
|
@@ -47,6 +46,7 @@ module OccamsRecord
|
|
|
47
46
|
|
|
48
47
|
private
|
|
49
48
|
|
|
49
|
+
# starting at the top of the chain, recurse and return the leaf node(s)
|
|
50
50
|
def reduce(node, depth = 0)
|
|
51
51
|
link = @chain[depth]
|
|
52
52
|
case link&.macro
|
|
@@ -69,38 +69,48 @@ module OccamsRecord
|
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
# build all the nested eager loaders
|
|
72
73
|
def build_loader
|
|
73
74
|
head = @chain[0]
|
|
74
75
|
links = @chain[1..-2]
|
|
75
76
|
tail = @chain[-1]
|
|
76
77
|
|
|
77
|
-
outer_loader = EagerLoaders.fetch!(head.ref).new(head.ref,
|
|
78
|
+
outer_loader = EagerLoaders.fetch!(head.ref).new(head.ref, optimized_scope(head), parent: tracer.parent)
|
|
78
79
|
outer_loader.tracer.through = true
|
|
79
80
|
|
|
80
81
|
inner_loader = links.
|
|
81
82
|
reduce(outer_loader) { |loader, link|
|
|
82
|
-
nested_loader = loader.nest(link.ref.source_reflection.name,
|
|
83
|
+
nested_loader = loader.nest(link.ref.source_reflection.name, optimized_scope(link))
|
|
83
84
|
nested_loader.tracer.through = true
|
|
84
85
|
nested_loader
|
|
85
86
|
}.
|
|
86
|
-
nest(tail.ref.source_reflection.name, @
|
|
87
|
+
nest(tail.ref.source_reflection.name, @scopes, use: @use, as: @as, active_record_fallback: @active_record_fallback)
|
|
87
88
|
|
|
88
89
|
@eager_loaders.each { |loader| inner_loader.eager_loaders << loader }
|
|
89
90
|
inner_loader.tracer.name = tracer.name
|
|
90
91
|
outer_loader
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
def
|
|
94
|
-
|
|
94
|
+
def optimized_scope(link)
|
|
95
|
+
case @optimizer
|
|
96
|
+
when :select
|
|
97
|
+
optimized_select(link)
|
|
98
|
+
when :none
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
end
|
|
95
102
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
# only select the ids/foreign keys required to link the parent/child records
|
|
104
|
+
def optimized_select(link)
|
|
105
|
+
cols =
|
|
106
|
+
case link.macro
|
|
107
|
+
when :belongs_to
|
|
108
|
+
[link.ref.association_primary_key]
|
|
109
|
+
when :has_one, :has_many
|
|
110
|
+
[link.ref.association_primary_key, link.ref.foreign_key]
|
|
111
|
+
else
|
|
112
|
+
raise "Unsupported through chain link type '#{link.macro}'"
|
|
113
|
+
end
|
|
104
114
|
|
|
105
115
|
case link.next_ref.source_reflection.macro
|
|
106
116
|
when :belongs_to
|
data/lib/occams-record/merge.rb
CHANGED
|
@@ -35,14 +35,13 @@ module OccamsRecord
|
|
|
35
35
|
# Optimized for merges where there's a single mapping key pair (which is the vast majority)
|
|
36
36
|
if mapping.size == 1
|
|
37
37
|
target_attr, assoc_attr = target_attrs[0], assoc_attrs[0]
|
|
38
|
-
assoc_rows_by_ids = assoc_rows.
|
|
38
|
+
assoc_rows_by_ids = assoc_rows.each_with_object({}) { |assoc_row, acc|
|
|
39
39
|
begin
|
|
40
40
|
id = assoc_row.send assoc_attr
|
|
41
41
|
rescue NoMethodError => e
|
|
42
42
|
raise MissingColumnError.new(assoc_row, e.name)
|
|
43
43
|
end
|
|
44
|
-
|
|
45
|
-
a
|
|
44
|
+
acc[id] ||= assoc_row
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
target_rows.each do |row|
|
|
@@ -56,14 +55,13 @@ module OccamsRecord
|
|
|
56
55
|
|
|
57
56
|
# Slower but works with any number of mapping key pairs
|
|
58
57
|
else
|
|
59
|
-
assoc_rows_by_ids = assoc_rows.
|
|
58
|
+
assoc_rows_by_ids = assoc_rows.each_with_object({}) { |assoc_row, acc|
|
|
60
59
|
begin
|
|
61
60
|
ids = assoc_attrs.map { |attr| assoc_row.send attr }
|
|
62
61
|
rescue NoMethodError => e
|
|
63
62
|
raise MissingColumnError.new(assoc_row, e.name)
|
|
64
63
|
end
|
|
65
|
-
|
|
66
|
-
a
|
|
64
|
+
acc[ids] ||= assoc_row
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
target_rows.each do |row|
|
data/lib/occams-record/pluck.rb
CHANGED
|
@@ -2,28 +2,31 @@ module OccamsRecord
|
|
|
2
2
|
module Pluck
|
|
3
3
|
private
|
|
4
4
|
|
|
5
|
-
def pluck_results(results,
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
def pluck_results(results, model: nil)
|
|
6
|
+
casters = TypeCaster.generate(results.columns, results.column_types, model: model)
|
|
7
|
+
if results[0]&.size == 1
|
|
8
|
+
pluck_results_single(results, casters)
|
|
8
9
|
else
|
|
9
|
-
pluck_results_multi(results,
|
|
10
|
+
pluck_results_multi(results, casters)
|
|
10
11
|
end
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
# returns an array of values
|
|
14
|
-
def pluck_results_single(results,
|
|
15
|
-
casters = TypeCaster.generate(results.columns, results.column_types, model: model)
|
|
15
|
+
def pluck_results_single(results, casters)
|
|
16
16
|
col = results.columns[0]
|
|
17
17
|
caster = casters[col]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
if caster
|
|
19
|
+
results.map { |row|
|
|
20
|
+
val = row[col]
|
|
21
|
+
caster.(val)
|
|
22
|
+
}
|
|
23
|
+
else
|
|
24
|
+
results.map { |row| row[col] }
|
|
25
|
+
end
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
# returns an array of arrays
|
|
25
|
-
def pluck_results_multi(results,
|
|
26
|
-
casters = TypeCaster.generate(results.columns, results.column_types, model: model)
|
|
29
|
+
def pluck_results_multi(results, casters)
|
|
27
30
|
results.map { |row|
|
|
28
31
|
row.map { |col, val|
|
|
29
32
|
caster = casters[col]
|
data/lib/occams-record/query.rb
CHANGED
|
@@ -99,14 +99,15 @@ module OccamsRecord
|
|
|
99
99
|
return [] if sql.blank? # return early in case ActiveRecord::QueryMethods#none was used
|
|
100
100
|
|
|
101
101
|
@query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
|
|
102
|
-
result =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
result =
|
|
103
|
+
if measure?
|
|
104
|
+
record_start_time!
|
|
105
|
+
measure!(model.table_name, sql) {
|
|
106
|
+
model.connection.exec_query sql
|
|
107
|
+
}
|
|
108
|
+
else
|
|
109
|
+
model.connection.exec_query sql
|
|
110
|
+
end
|
|
110
111
|
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: model, modules: @use, tracer: @eager_loaders.tracer, active_record_fallback: @active_record_fallback)
|
|
111
112
|
rows = result.rows.map { |row| row_class.new row }
|
|
112
113
|
@eager_loaders.run!(rows, query_logger: @query_logger, measurements: @measurements)
|
|
@@ -238,19 +239,20 @@ module OccamsRecord
|
|
|
238
239
|
# @return [Array]
|
|
239
240
|
#
|
|
240
241
|
def pluck(*cols)
|
|
241
|
-
sql = (block_given? ? yield(scope).to_sql : scope).select(*cols).to_sql
|
|
242
|
+
sql = (block_given? ? yield(scope).to_sql : scope).unscope(:select).select(*cols).to_sql
|
|
242
243
|
return [] if sql.blank? # return early in case ActiveRecord::QueryMethods#none was used
|
|
243
244
|
|
|
244
245
|
@query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
|
|
245
|
-
result =
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
246
|
+
result =
|
|
247
|
+
if measure?
|
|
248
|
+
record_start_time!
|
|
249
|
+
measure!(model.table_name, sql) {
|
|
250
|
+
model.connection.exec_query sql
|
|
251
|
+
}
|
|
252
|
+
else
|
|
253
|
+
model.connection.exec_query sql
|
|
254
|
+
end
|
|
255
|
+
pluck_results(result, model: @model)
|
|
254
256
|
end
|
|
255
257
|
end
|
|
256
258
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module OccamsRecord
|
|
2
2
|
#
|
|
3
3
|
# Starts building a OccamsRecord::RawQuery. Pass it a raw SQL statement, optionally followed by
|
|
4
|
-
# a Hash of binds. While this doesn't offer an additional performance boost, it's a nice way to
|
|
4
|
+
# a Hash or Array of binds. While this doesn't offer an additional performance boost, it's a nice way to
|
|
5
5
|
# write safe, complicated SQL by hand while also supporting eager loading.
|
|
6
6
|
#
|
|
7
7
|
# results = OccamsRecord.sql("
|
|
@@ -39,10 +39,10 @@ module OccamsRecord
|
|
|
39
39
|
# It is possible to coerce the SQLite adapter into returning native types for everything IF they're columns of a table
|
|
40
40
|
# that you have an AR model for. e.g. if you're selecting from the widgets, table: `OccamsRecord.sql("...").model(Widget)...`.
|
|
41
41
|
#
|
|
42
|
-
# MySQL
|
|
42
|
+
# MySQL Mostly native Ruby types, but more testing is needed.
|
|
43
43
|
#
|
|
44
|
-
# @param sql [String] The SELECT statement to run. Binds
|
|
45
|
-
# @param binds [Hash] Bind values (Symbol keys)
|
|
44
|
+
# @param sql [String] The SELECT statement to run. Binds may be Rails-style (?, :foo) or Ruby-style (%s, %{foo}).
|
|
45
|
+
# @param binds [Hash] Bind values as Hash (with Symbol keys) or an Array
|
|
46
46
|
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
|
47
47
|
# @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
|
|
48
48
|
# @return [OccamsRecord::RawQuery]
|
|
@@ -58,7 +58,7 @@ module OccamsRecord
|
|
|
58
58
|
class RawQuery
|
|
59
59
|
# @return [String]
|
|
60
60
|
attr_reader :sql
|
|
61
|
-
# @return [Hash]
|
|
61
|
+
# @return [Hash|Array]
|
|
62
62
|
attr_reader :binds
|
|
63
63
|
|
|
64
64
|
include OccamsRecord::Batches::CursorHelpers
|
|
@@ -70,8 +70,8 @@ module OccamsRecord
|
|
|
70
70
|
#
|
|
71
71
|
# Initialize a new query.
|
|
72
72
|
#
|
|
73
|
-
# @param sql [String] The SELECT statement to run. Binds
|
|
74
|
-
# @param binds [Hash] Bind values (Symbol keys)
|
|
73
|
+
# @param sql [String] The SELECT statement to run. Binds may be Rails-style (?, :foo) or Ruby-style (%s, %{foo}).
|
|
74
|
+
# @param binds [Hash] Bind values as Hash (with Symbol keys) or an Array
|
|
75
75
|
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
|
76
76
|
# @param eager_loaders [OccamsRecord::EagerLoaders::Context]
|
|
77
77
|
# @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
|
|
@@ -79,7 +79,7 @@ module OccamsRecord
|
|
|
79
79
|
# @param connection
|
|
80
80
|
#
|
|
81
81
|
def initialize(sql, binds, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil, connection: nil)
|
|
82
|
-
@sql = sql
|
|
82
|
+
@sql = BindsConverter.convert(sql, binds)
|
|
83
83
|
@binds = binds
|
|
84
84
|
@use = use
|
|
85
85
|
@eager_loaders = eager_loaders || EagerLoaders::Context.new
|
|
@@ -110,14 +110,15 @@ module OccamsRecord
|
|
|
110
110
|
def run
|
|
111
111
|
_escaped_sql = escaped_sql
|
|
112
112
|
@query_logger << _escaped_sql if @query_logger
|
|
113
|
-
result =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
result =
|
|
114
|
+
if measure?
|
|
115
|
+
record_start_time!
|
|
116
|
+
measure!(table_name, _escaped_sql) {
|
|
117
|
+
conn.exec_query _escaped_sql
|
|
118
|
+
}
|
|
119
|
+
else
|
|
120
|
+
conn.exec_query _escaped_sql
|
|
121
|
+
end
|
|
121
122
|
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: @eager_loaders.model, modules: @use, tracer: @eager_loaders.tracer)
|
|
122
123
|
rows = result.rows.map { |row| row_class.new row }
|
|
123
124
|
@eager_loaders.run!(rows, query_logger: @query_logger, measurements: @measurements)
|
|
@@ -211,25 +212,29 @@ module OccamsRecord
|
|
|
211
212
|
end
|
|
212
213
|
|
|
213
214
|
#
|
|
214
|
-
# Returns the
|
|
215
|
+
# Returns the column(s) you've SELECT as an array of values.
|
|
215
216
|
#
|
|
216
|
-
# If
|
|
217
|
+
# If you're selecting multiple columns, you'll get back an array of arrays.
|
|
218
|
+
# Otherwise you'll get an array of the single column's values.
|
|
217
219
|
#
|
|
218
|
-
# @param
|
|
220
|
+
# @param *args DEPRECATED
|
|
219
221
|
# @return [Array]
|
|
220
222
|
#
|
|
221
|
-
def pluck(*
|
|
223
|
+
def pluck(*args)
|
|
224
|
+
$stderr.puts "OccamsRecord: passing arguments to OccamsRecord.sql(\"...\").pluck is deprecated and will be removed in a future version. Called from #{caller[0]}" if args.any?
|
|
225
|
+
|
|
222
226
|
_escaped_sql = escaped_sql
|
|
223
227
|
@query_logger << _escaped_sql if @query_logger
|
|
224
|
-
result =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
228
|
+
result =
|
|
229
|
+
if measure?
|
|
230
|
+
record_start_time!
|
|
231
|
+
measure!(table_name, _escaped_sql) {
|
|
232
|
+
conn.exec_query _escaped_sql
|
|
233
|
+
}
|
|
234
|
+
else
|
|
235
|
+
conn.exec_query _escaped_sql
|
|
236
|
+
end
|
|
237
|
+
pluck_results(result, model: @eager_loaders.model)
|
|
233
238
|
end
|
|
234
239
|
|
|
235
240
|
private
|
|
@@ -237,14 +242,23 @@ module OccamsRecord
|
|
|
237
242
|
# Returns the SQL as a String with all variables escaped
|
|
238
243
|
def escaped_sql
|
|
239
244
|
return sql if binds.empty?
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
245
|
+
escaped_binds =
|
|
246
|
+
if binds.is_a? Array
|
|
247
|
+
binds.map { |val| quote val }
|
|
248
|
+
else
|
|
249
|
+
binds.each_with_object({}) { |(col, val), acc|
|
|
250
|
+
acc[col.to_sym] = quote val
|
|
251
|
+
}
|
|
252
|
+
end
|
|
253
|
+
sql % escaped_binds
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def quote(val)
|
|
257
|
+
if val.is_a? Array
|
|
258
|
+
val.map { |x| conn.quote x }.join(', ')
|
|
259
|
+
else
|
|
260
|
+
conn.quote val
|
|
261
|
+
end
|
|
248
262
|
end
|
|
249
263
|
|
|
250
264
|
def table_name
|
|
@@ -28,9 +28,10 @@ module OccamsRecord
|
|
|
28
28
|
self.table_name = model ? model.table_name : nil
|
|
29
29
|
self.eager_loader_trace = tracer
|
|
30
30
|
self.active_record_fallback = active_record_fallback
|
|
31
|
-
self.primary_key =
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
self.primary_key =
|
|
32
|
+
if model&.primary_key and (pkey = model.primary_key.to_s) and columns.include?(pkey)
|
|
33
|
+
pkey
|
|
34
|
+
end
|
|
34
35
|
|
|
35
36
|
# Build getters & setters for associations. (We need setters b/c they're set AFTER the row is initialized
|
|
36
37
|
attr_accessor(*association_names)
|
|
@@ -42,7 +43,7 @@ module OccamsRecord
|
|
|
42
43
|
|
|
43
44
|
if caster
|
|
44
45
|
define_method(col) {
|
|
45
|
-
@cast_values[idx] = caster.(@raw_values[idx])
|
|
46
|
+
@cast_values[idx] = caster.(@raw_values[idx]) unless @cast_values.has_key?(idx)
|
|
46
47
|
@cast_values[idx]
|
|
47
48
|
}
|
|
48
49
|
else
|
|
@@ -51,7 +52,7 @@ module OccamsRecord
|
|
|
51
52
|
}
|
|
52
53
|
end
|
|
53
54
|
|
|
54
|
-
define_method("#{col}?") {
|
|
55
|
+
define_method("#{col}?") { send(col).present? }
|
|
55
56
|
end
|
|
56
57
|
end
|
|
57
58
|
end
|
|
@@ -69,25 +69,25 @@ module OccamsRecord
|
|
|
69
69
|
# @return [Hash] a Hash with String or Symbol keys
|
|
70
70
|
#
|
|
71
71
|
def to_h(symbolize_names: false, recursive: false)
|
|
72
|
-
hash = self.class.columns.
|
|
72
|
+
hash = self.class.columns.each_with_object({}) { |col_name, acc|
|
|
73
73
|
key = symbolize_names ? col_name.to_sym : col_name
|
|
74
|
-
|
|
75
|
-
a
|
|
74
|
+
acc[key] = send col_name
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
recursive ? self.class.associations.
|
|
77
|
+
recursive ? self.class.associations.each_with_object(hash) { |assoc_name, acc|
|
|
79
78
|
key = symbolize_names ? assoc_name.to_sym : assoc_name
|
|
80
79
|
assoc = send assoc_name
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
acc[key] =
|
|
81
|
+
if assoc.is_a? Array
|
|
82
|
+
assoc.map { |x| x.to_h(symbolize_names: symbolize_names, recursive: true) }
|
|
83
|
+
elsif assoc
|
|
84
|
+
assoc.to_h(symbolize_names: symbolize_names, recursive: true)
|
|
85
|
+
end
|
|
87
86
|
} : hash
|
|
88
87
|
end
|
|
89
88
|
|
|
90
89
|
alias_method :to_hash, :to_h
|
|
90
|
+
alias_method :attributes, :to_h
|
|
91
91
|
|
|
92
92
|
#
|
|
93
93
|
# Returns the name of the model and the attributes.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module OccamsRecord
|
|
2
|
-
# @private
|
|
3
2
|
module TypeCaster
|
|
3
|
+
# @private
|
|
4
4
|
CASTER =
|
|
5
5
|
case ActiveRecord::VERSION::MAJOR
|
|
6
6
|
when 4 then :type_cast_from_database
|
|
@@ -9,9 +9,13 @@ module OccamsRecord
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# Returns a Hash containing type converters (a Proc) for each column. The Proc's accept a value and return a converted value, mapping enum values from the model if necessary.
|
|
13
|
+
#
|
|
14
|
+
# NOTE Some columns may have no Proc (particularly if you're using SQLite and running a raw SQL query).
|
|
15
|
+
#
|
|
16
|
+
# @param column_names [Array<String>] the column names in the result set (ActiveRecord::Result#columns). The order MUST match the order returned by the query.
|
|
17
|
+
# @param column_types [Hash] Column name => type (ActiveRecord::Result#column_types)
|
|
18
|
+
# @param model [ActiveRecord::Base] the AR model representing the table (it holds column & type info as well as enums).
|
|
15
19
|
# @return [Hash<Proc>] a Hash of casting Proc's keyed by column
|
|
16
20
|
#
|
|
17
21
|
def self.generate(column_names, column_types, model: nil)
|
|
@@ -42,8 +46,6 @@ module OccamsRecord
|
|
|
42
46
|
end
|
|
43
47
|
when :datetime
|
|
44
48
|
->(val) { type.send(CASTER, val)&.in_time_zone }
|
|
45
|
-
when :boolean
|
|
46
|
-
->(val) { type.send(CASTER, val) }
|
|
47
49
|
else
|
|
48
50
|
if enum
|
|
49
51
|
->(val) {
|
data/lib/occams-record.rb
CHANGED
|
@@ -14,6 +14,11 @@ require 'occams-record/batches/offset_limit/scoped'
|
|
|
14
14
|
require 'occams-record/batches/offset_limit/raw_query'
|
|
15
15
|
require 'occams-record/batches/cursor_helpers'
|
|
16
16
|
|
|
17
|
+
require 'occams-record/binds_converter'
|
|
18
|
+
require 'occams-record/binds_converter/abstract'
|
|
19
|
+
require 'occams-record/binds_converter/named'
|
|
20
|
+
require 'occams-record/binds_converter/positional'
|
|
21
|
+
|
|
17
22
|
require 'occams-record/query'
|
|
18
23
|
require 'occams-record/raw_query'
|
|
19
24
|
|
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.
|
|
4
|
+
version: 1.9.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-
|
|
11
|
+
date: 2023-09-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -42,6 +42,10 @@ files:
|
|
|
42
42
|
- lib/occams-record/batches/cursor_helpers.rb
|
|
43
43
|
- lib/occams-record/batches/offset_limit/raw_query.rb
|
|
44
44
|
- lib/occams-record/batches/offset_limit/scoped.rb
|
|
45
|
+
- lib/occams-record/binds_converter.rb
|
|
46
|
+
- lib/occams-record/binds_converter/abstract.rb
|
|
47
|
+
- lib/occams-record/binds_converter/named.rb
|
|
48
|
+
- lib/occams-record/binds_converter/positional.rb
|
|
45
49
|
- lib/occams-record/cursor.rb
|
|
46
50
|
- lib/occams-record/eager_loaders/ad_hoc_base.rb
|
|
47
51
|
- lib/occams-record/eager_loaders/ad_hoc_many.rb
|