occams-record 1.8.1 → 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 +22 -0
- 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 +9 -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: 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
|
@@ -130,6 +130,18 @@ orders = OccamsRecord
|
|
|
130
130
|
|
|
131
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.
|
|
132
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
|
+
|
|
133
145
|
**Batched loading with cursors**
|
|
134
146
|
|
|
135
147
|
`find_each_with_cursor`, `find_in_batches_with_cursor`, and `cursor.open` are all available.
|
|
@@ -334,6 +346,16 @@ o o o o +
|
|
|
334
346
|
+ + o o +
|
|
335
347
|
```
|
|
336
348
|
|
|
349
|
+
## Testing without Docker
|
|
350
|
+
|
|
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.
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
bundle install
|
|
355
|
+
bundle exec appraisal ar-7.0 bundle install
|
|
356
|
+
bundle exec appraisal ar-7.0 rake test
|
|
357
|
+
```
|
|
358
|
+
|
|
337
359
|
# License
|
|
338
360
|
|
|
339
361
|
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
|
+
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
|
|
|
@@ -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,21 +69,20 @@ 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
|
|
|
@@ -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.0
|
|
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-07-
|
|
11
|
+
date: 2023-07-11 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
|