occams-record 1.8.1 → 1.9.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|