occams-record 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +51 -21
- 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 +19 -37
- data/lib/occams-record/query.rb +20 -18
- data/lib/occams-record/raw_query.rb +51 -37
- data/lib/occams-record/results/results.rb +16 -53
- data/lib/occams-record/results/row.rb +9 -10
- data/lib/occams-record/type_caster.rb +62 -0
- data/lib/occams-record/version.rb +1 -1
- data/lib/occams-record.rb +6 -0
- metadata +7 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e26a6e33886dfdb5cb2d3000e32c9de7e04bb7b26cc4434c5df7f1deb094ac58
|
|
4
|
+
data.tar.gz: 9cc37f3ab8beeef6e55d5adae2e1f76268acad3381b789f70c896a515abe8cfa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8236d746e52a2890dfb83e838db153150c249ff0b5fb53a1bc949d386258b5047dc93e63a40d747e0f584ffbcaf3d069122962c23a2fc3691a4e162044927af
|
|
7
|
+
data.tar.gz: 1883cb2f7d7817561837b911828b56b540b5dff7845a943340c489786b73be9b3c24270ec2d32a5e7b9ecdb324a21f346948f183e13439e9339424ee9b593ad9
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Occams Record
|
|
1
|
+
# Occams Record
|
|
2
2
|
|
|
3
3
|
> Do not multiply entities beyond necessity. -- Occam's Razor
|
|
4
4
|
|
|
@@ -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.
|
|
@@ -298,32 +310,50 @@ On the other hand, Active Record makes it *very* easy to forget to eager load as
|
|
|
298
310
|
|
|
299
311
|
# Testing
|
|
300
312
|
|
|
301
|
-
|
|
302
|
-
bundle install
|
|
303
|
-
|
|
304
|
-
# test against SQLite
|
|
305
|
-
bundle exec rake test
|
|
313
|
+
Tests are run with `appraisal` in Docker Compose using the `bin/test` or `bin/testall` scripts.
|
|
306
314
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
315
|
+
```bash
|
|
316
|
+
# Run tests against all supported ActiveRecord versions, Ruby versions, and databases
|
|
317
|
+
bin/testall
|
|
318
|
+
|
|
319
|
+
# Run tests for Ruby vX only
|
|
320
|
+
bin/testall ruby-3.1
|
|
321
|
+
|
|
322
|
+
# Run tests for ActiveRecord vX only
|
|
323
|
+
bin/testall ar-6.1
|
|
324
|
+
|
|
325
|
+
# Run tests against a specific database
|
|
326
|
+
bin/testall sqlite|postgres-14|mysql-8
|
|
327
|
+
|
|
328
|
+
# Run exactly one set of tests
|
|
329
|
+
bin/test ruby-3.1 ar-7.0 postgres-14
|
|
330
|
+
|
|
331
|
+
# If all tests complete successfully, you'll be rewarded by an ASCII Nyancat!
|
|
332
|
+
|
|
333
|
+
+ o + o
|
|
334
|
+
+ o + +
|
|
335
|
+
o +
|
|
336
|
+
o + + +
|
|
337
|
+
+ o o + o
|
|
338
|
+
-_-_-_-_-_-_-_,------, o
|
|
339
|
+
_-_-_-_-_-_-_-| /\_/\
|
|
340
|
+
-_-_-_-_-_-_-~|__( ^ .^) + +
|
|
341
|
+
_-_-_-_-_-_-_-"" ""
|
|
342
|
+
+ o o + o
|
|
343
|
+
+ +
|
|
344
|
+
o o o o +
|
|
345
|
+
o +
|
|
346
|
+
+ + o o +
|
|
312
347
|
```
|
|
313
348
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
```bash
|
|
317
|
-
bundle exec appraisal install
|
|
349
|
+
## Testing without Docker
|
|
318
350
|
|
|
319
|
-
|
|
320
|
-
bundle exec appraisal rake test
|
|
351
|
+
It's possible to run tests without Docker Compose, but you'll be limited by the Ruby version(s) and database(s) you have on your system.
|
|
321
352
|
|
|
322
|
-
|
|
353
|
+
```bash
|
|
354
|
+
bundle install
|
|
355
|
+
bundle exec appraisal ar-7.0 bundle install
|
|
323
356
|
bundle exec appraisal ar-7.0 rake test
|
|
324
|
-
|
|
325
|
-
# test against Postgres
|
|
326
|
-
TEST_DATABASE_URL=postgresql://postgres@localhost:5432/occams_record bundle exec appraisal rake test
|
|
327
357
|
```
|
|
328
358
|
|
|
329
359
|
# License
|
|
@@ -9,8 +9,12 @@ module OccamsRecord
|
|
|
9
9
|
@conn, @sql, @binds = conn, sql, binds
|
|
10
10
|
@use, @query_logger, @eager_loaders = use, query_logger, eager_loaders
|
|
11
11
|
|
|
12
|
+
unless binds.is_a? Hash
|
|
13
|
+
raise ArgumentError, "When using find_each/find_in_batches with raw SQL, binds MUST be a Hash. SQL statement: #{@sql}"
|
|
14
|
+
end
|
|
15
|
+
|
|
12
16
|
unless @sql =~ /LIMIT\s+%\{batch_limit\}/i and @sql =~ /OFFSET\s+%\{batch_offset\}/i
|
|
13
|
-
raise ArgumentError, "When using find_each/find_in_batches you must specify 'LIMIT %{batch_limit} OFFSET %{batch_offset}'. SQL statement: #{@sql}"
|
|
17
|
+
raise ArgumentError, "When using find_each/find_in_batches with raw SQL, you must specify 'LIMIT %{batch_limit} OFFSET %{batch_offset}'. SQL statement: #{@sql}"
|
|
14
18
|
end
|
|
15
19
|
end
|
|
16
20
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module OccamsRecord
|
|
2
|
+
module BindsConverter
|
|
3
|
+
#
|
|
4
|
+
# A base class for converting a SQL string with Rails-style query params (?, :foo) to native Ruby format (%s, %{foo}).
|
|
5
|
+
#
|
|
6
|
+
# It works kind of like a tokenizer. Subclasses must 1) implement get_bind to return the converted bind
|
|
7
|
+
# from the current position and 2) pass the bind sigil (e.g. ?, :) to the parent constructor.
|
|
8
|
+
#
|
|
9
|
+
class Abstract
|
|
10
|
+
# @private
|
|
11
|
+
ESCAPE = "\\".freeze
|
|
12
|
+
|
|
13
|
+
def initialize(sql, bind_sigil)
|
|
14
|
+
@sql = sql
|
|
15
|
+
@end = sql.size - 1
|
|
16
|
+
@start_i, @i = 0, 0
|
|
17
|
+
@bind_sigil = bind_sigil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [String] The converted SQL string
|
|
21
|
+
def to_s
|
|
22
|
+
sql = ""
|
|
23
|
+
each { |frag| sql << frag }
|
|
24
|
+
sql
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
# Yields each SQL fragment and converted bind to the given block
|
|
30
|
+
def each
|
|
31
|
+
escape = false
|
|
32
|
+
until @i > @end
|
|
33
|
+
char = @sql[@i]
|
|
34
|
+
unescape = escape
|
|
35
|
+
case char
|
|
36
|
+
when @bind_sigil
|
|
37
|
+
if escape
|
|
38
|
+
@i += 1
|
|
39
|
+
elsif @i > @start_i
|
|
40
|
+
yield flush_sql
|
|
41
|
+
else
|
|
42
|
+
yield get_bind
|
|
43
|
+
end
|
|
44
|
+
when ESCAPE
|
|
45
|
+
if escape
|
|
46
|
+
@i += 1
|
|
47
|
+
elsif @i > @start_i
|
|
48
|
+
yield flush_sql
|
|
49
|
+
escape = true
|
|
50
|
+
@i += 1
|
|
51
|
+
@start_i = @i
|
|
52
|
+
else
|
|
53
|
+
escape = true
|
|
54
|
+
@i += 1
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
@i += 1
|
|
58
|
+
end
|
|
59
|
+
escape = false if unescape
|
|
60
|
+
end
|
|
61
|
+
yield flush_sql if @i > @start_i
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def flush_sql
|
|
65
|
+
t = @sql[@start_i..@i - 1]
|
|
66
|
+
@start_i = @i
|
|
67
|
+
t
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module OccamsRecord
|
|
2
|
+
module BindsConverter
|
|
3
|
+
# @private
|
|
4
|
+
WORD = /\w/
|
|
5
|
+
|
|
6
|
+
#
|
|
7
|
+
# Converts Rails-style named binds (:foo) into native Ruby format (%{foo}).
|
|
8
|
+
#
|
|
9
|
+
class Named < Abstract
|
|
10
|
+
def initialize(sql)
|
|
11
|
+
super(sql, ":".freeze)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
protected
|
|
15
|
+
|
|
16
|
+
def get_bind
|
|
17
|
+
old_i = @i
|
|
18
|
+
@i += 1
|
|
19
|
+
@start_i = @i
|
|
20
|
+
|
|
21
|
+
until @i > @end or @sql[@i] !~ WORD
|
|
22
|
+
@i += 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if @i > @start_i
|
|
26
|
+
name = @sql[@start_i..@i - 1]
|
|
27
|
+
@start_i = @i
|
|
28
|
+
"%{#{name}}"
|
|
29
|
+
else
|
|
30
|
+
@sql[old_i]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module OccamsRecord
|
|
2
|
+
module BindsConverter
|
|
3
|
+
#
|
|
4
|
+
# Converts Rails-style positional binds (?) into native Ruby format (%s).
|
|
5
|
+
#
|
|
6
|
+
class Positional < Abstract
|
|
7
|
+
def initialize(sql)
|
|
8
|
+
super(sql, "?".freeze)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
def get_bind
|
|
14
|
+
@i += 1
|
|
15
|
+
@start_i = @i
|
|
16
|
+
"%s".freeze
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module OccamsRecord
|
|
2
|
+
#
|
|
3
|
+
# Classes and methods for converting from Rails-style binds (?, :foo) to native Ruby format (%s, %{foo}).
|
|
4
|
+
#
|
|
5
|
+
module BindsConverter
|
|
6
|
+
#
|
|
7
|
+
# Convert any Rails-style binds (?, :foo) to native Ruby format (%s, %{foo}).
|
|
8
|
+
#
|
|
9
|
+
# @param sql [String]
|
|
10
|
+
# @param binds [Hash|Array]
|
|
11
|
+
# @return [String] the converted SQL string
|
|
12
|
+
#
|
|
13
|
+
def self.convert(sql, binds)
|
|
14
|
+
converter =
|
|
15
|
+
case binds
|
|
16
|
+
when Hash then Named.new(sql)
|
|
17
|
+
when Array then Positional.new(sql)
|
|
18
|
+
else raise ArgumentError, "OccamsRecord: Unsupported SQL bind params '#{binds.inspect}'. Only Hash and Array are supported"
|
|
19
|
+
end
|
|
20
|
+
converter.to_s
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/occams-record/cursor.rb
CHANGED
|
@@ -203,9 +203,8 @@ module OccamsRecord
|
|
|
203
203
|
# end
|
|
204
204
|
#
|
|
205
205
|
def execute(sql, binds = {})
|
|
206
|
-
conn.execute(sql % binds.
|
|
206
|
+
conn.execute(sql % binds.each_with_object({}) { |(key, val), acc|
|
|
207
207
|
acc[key] = conn.quote(val)
|
|
208
|
-
acc
|
|
209
208
|
})
|
|
210
209
|
end
|
|
211
210
|
|
|
@@ -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,55 +2,37 @@ 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
|
-
|
|
16
|
-
|
|
17
|
-
if
|
|
18
|
-
results.map { |
|
|
19
|
-
val =
|
|
20
|
-
|
|
15
|
+
def pluck_results_single(results, casters)
|
|
16
|
+
col = results.columns[0]
|
|
17
|
+
caster = casters[col]
|
|
18
|
+
if caster
|
|
19
|
+
results.map { |row|
|
|
20
|
+
val = row[col]
|
|
21
|
+
caster.(val)
|
|
21
22
|
}
|
|
22
23
|
else
|
|
23
|
-
|
|
24
|
-
results.map { |r| r[col] }
|
|
24
|
+
results.map { |row| row[col] }
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# returns an array of arrays
|
|
29
|
-
def pluck_results_multi(results,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
[col, enum, enum&.invert]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if any_enums
|
|
38
|
-
results.map { |row|
|
|
39
|
-
cols_with_enums.map { |(col, enum, inv_enum)|
|
|
40
|
-
if enum
|
|
41
|
-
val = row[col]
|
|
42
|
-
enum.has_key?(val) ? val : inv_enum[val]
|
|
43
|
-
else
|
|
44
|
-
row[col]
|
|
45
|
-
end
|
|
46
|
-
}
|
|
29
|
+
def pluck_results_multi(results, casters)
|
|
30
|
+
results.map { |row|
|
|
31
|
+
row.map { |col, val|
|
|
32
|
+
caster = casters[col]
|
|
33
|
+
caster ? caster.(val) : val
|
|
47
34
|
}
|
|
48
|
-
|
|
49
|
-
# micro-optimization for when there are no enums
|
|
50
|
-
results.map { |row|
|
|
51
|
-
cols.map { |col| row[col] }
|
|
52
|
-
}
|
|
53
|
-
end
|
|
35
|
+
}
|
|
54
36
|
end
|
|
55
37
|
end
|
|
56
38
|
end
|
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
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
module OccamsRecord
|
|
2
2
|
# Classes and methods for handing query results.
|
|
3
3
|
module Results
|
|
4
|
-
# ActiveRecord's internal type casting API changes from version to version.
|
|
5
|
-
CASTER = case ActiveRecord::VERSION::MAJOR
|
|
6
|
-
when 4 then :type_cast_from_database
|
|
7
|
-
when 5, 6, 7 then :deserialize
|
|
8
|
-
else raise "OccamsRecord::Results::CASTER does yet support this version of ActiveRecord"
|
|
9
|
-
end
|
|
10
|
-
|
|
11
4
|
#
|
|
12
5
|
# Dynamically build a class for a specific set of result rows. It inherits from OccamsRecord::Results::Row, and optionall prepends
|
|
13
6
|
# user-defined modules.
|
|
@@ -35,61 +28,31 @@ module OccamsRecord
|
|
|
35
28
|
self.table_name = model ? model.table_name : nil
|
|
36
29
|
self.eager_loader_trace = tracer
|
|
37
30
|
self.active_record_fallback = active_record_fallback
|
|
38
|
-
self.primary_key =
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
self.primary_key =
|
|
32
|
+
if model&.primary_key and (pkey = model.primary_key.to_s) and columns.include?(pkey)
|
|
33
|
+
pkey
|
|
34
|
+
end
|
|
41
35
|
|
|
42
36
|
# Build getters & setters for associations. (We need setters b/c they're set AFTER the row is initialized
|
|
43
37
|
attr_accessor(*association_names)
|
|
44
38
|
|
|
45
39
|
# Build a getter for each attribute returned by the query. The values will be type converted on demand.
|
|
46
|
-
|
|
40
|
+
casters = TypeCaster.generate(column_names, column_types, model: model)
|
|
47
41
|
self.columns.each_with_index do |col, idx|
|
|
48
|
-
|
|
49
|
-
# NOTE there's lots of variation between DB adapters and AR versions here. Some notes:
|
|
50
|
-
# * Postgres AR < 6.1 `column_types` will contain entries for every column.
|
|
51
|
-
# * Postgres AR >= 6.1 `column_types` only contains entries for "exotic" types. Columns with "common" types have already been converted by the PG adapter.
|
|
52
|
-
# * SQLite `column_types` will always be empty. Some types will have already been convered by the SQLite adapter, but others will depend on
|
|
53
|
-
# `model_column_types` for converstion. See test/raw_query_test.rb#test_common_types for examples.
|
|
54
|
-
# * MySQL ?
|
|
55
|
-
#
|
|
56
|
-
type = column_types[col] || model_column_types&.[](col)
|
|
42
|
+
caster = casters[col]
|
|
57
43
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
enum = model&.defined_enums&.[](col)
|
|
64
|
-
inv_enum = enum&.invert
|
|
65
|
-
|
|
66
|
-
case type&.type
|
|
67
|
-
when nil
|
|
68
|
-
if enum
|
|
69
|
-
define_method(col) {
|
|
70
|
-
val = @raw_values[idx]
|
|
71
|
-
enum.has_key?(val) ? val : inv_enum[val]
|
|
72
|
-
}
|
|
73
|
-
else
|
|
74
|
-
define_method(col) { @raw_values[idx] }
|
|
75
|
-
end
|
|
76
|
-
when :datetime
|
|
77
|
-
define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx])&.in_time_zone }
|
|
78
|
-
when :boolean
|
|
79
|
-
define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx]) }
|
|
80
|
-
define_method("#{col}?") { !!send(col) }
|
|
44
|
+
if caster
|
|
45
|
+
define_method(col) {
|
|
46
|
+
@cast_values[idx] = caster.(@raw_values[idx]) unless @cast_values.has_key?(idx)
|
|
47
|
+
@cast_values[idx]
|
|
48
|
+
}
|
|
81
49
|
else
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
val = type.send(CASTER, @raw_values[idx])
|
|
86
|
-
enum.has_key?(val) ? val : inv_enum[val]
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
else
|
|
90
|
-
define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx]) }
|
|
91
|
-
end
|
|
50
|
+
define_method(col) {
|
|
51
|
+
@raw_values[idx]
|
|
52
|
+
}
|
|
92
53
|
end
|
|
54
|
+
|
|
55
|
+
define_method("#{col}?") { send(col).present? }
|
|
93
56
|
end
|
|
94
57
|
end
|
|
95
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
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module OccamsRecord
|
|
2
|
+
module TypeCaster
|
|
3
|
+
# @private
|
|
4
|
+
CASTER =
|
|
5
|
+
case ActiveRecord::VERSION::MAJOR
|
|
6
|
+
when 4 then :type_cast_from_database
|
|
7
|
+
when 5, 6, 7 then :deserialize
|
|
8
|
+
else raise "OccamsRecord::TypeCaster::CASTER does yet support this version of ActiveRecord"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
#
|
|
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).
|
|
19
|
+
# @return [Hash<Proc>] a Hash of casting Proc's keyed by column
|
|
20
|
+
#
|
|
21
|
+
def self.generate(column_names, column_types, model: nil)
|
|
22
|
+
column_names.each_with_object({}) { |col, memo|
|
|
23
|
+
#
|
|
24
|
+
# NOTE there's lots of variation between DB adapters and AR versions here. Some notes:
|
|
25
|
+
# * Postgres AR < 6.1 `column_types` will contain entries for every column.
|
|
26
|
+
# * Postgres AR >= 6.1 `column_types` only contains entries for "exotic" types. Columns with "common" types have already been converted by the PG adapter.
|
|
27
|
+
# * SQLite `column_types` will always be empty. Some types will have already been convered by the SQLite adapter, but others will depend on
|
|
28
|
+
# `model_column_types` for converstion. See test/raw_query_test.rb#test_common_types for examples.
|
|
29
|
+
# * MySQL ?
|
|
30
|
+
#
|
|
31
|
+
type = column_types[col] || model&.attributes_builder&.types&.[](col)
|
|
32
|
+
|
|
33
|
+
#
|
|
34
|
+
# NOTE is also some variation in when enum values are mapped in different AR versions.
|
|
35
|
+
# In >=5.0, <=7.0, ActiveRecord::Result objects *usually* contain the human-readable values. In 4.2 and
|
|
36
|
+
# pre-release versions of 7.1, they instead have the RAW values (e.g. integers) which we must map ourselves.
|
|
37
|
+
#
|
|
38
|
+
enum = model&.defined_enums&.[](col)
|
|
39
|
+
inv_enum = enum&.invert
|
|
40
|
+
|
|
41
|
+
memo[col] =
|
|
42
|
+
case type&.type
|
|
43
|
+
when nil
|
|
44
|
+
if enum
|
|
45
|
+
->(val) { enum.has_key?(val) ? val : inv_enum[val] }
|
|
46
|
+
end
|
|
47
|
+
when :datetime
|
|
48
|
+
->(val) { type.send(CASTER, val)&.in_time_zone }
|
|
49
|
+
else
|
|
50
|
+
if enum
|
|
51
|
+
->(val) {
|
|
52
|
+
val = type.send(CASTER, val)
|
|
53
|
+
enum.has_key?(val) ? val : inv_enum[val]
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
->(val) { type.send(CASTER, val) }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/occams-record.rb
CHANGED
|
@@ -3,6 +3,7 @@ require 'occams-record/version'
|
|
|
3
3
|
require 'occams-record/merge'
|
|
4
4
|
require 'occams-record/measureable'
|
|
5
5
|
require 'occams-record/eager_loaders/eager_loaders'
|
|
6
|
+
require 'occams-record/type_caster'
|
|
6
7
|
require 'occams-record/results/results'
|
|
7
8
|
require 'occams-record/results/row'
|
|
8
9
|
require 'occams-record/pluck'
|
|
@@ -13,6 +14,11 @@ require 'occams-record/batches/offset_limit/scoped'
|
|
|
13
14
|
require 'occams-record/batches/offset_limit/raw_query'
|
|
14
15
|
require 'occams-record/batches/cursor_helpers'
|
|
15
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
|
+
|
|
16
22
|
require 'occams-record/query'
|
|
17
23
|
require 'occams-record/raw_query'
|
|
18
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
|
|
@@ -30,20 +30,6 @@ dependencies:
|
|
|
30
30
|
- - "<"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '7.1'
|
|
33
|
-
- !ruby/object:Gem::Dependency
|
|
34
|
-
name: appraisal
|
|
35
|
-
requirement: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
40
|
-
type: :development
|
|
41
|
-
prerelease: false
|
|
42
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - ">="
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0'
|
|
47
33
|
description: A faster, lower-memory, fuller-featured querying API for ActiveRecord
|
|
48
34
|
that returns results as unadorned, read-only objects.
|
|
49
35
|
email: jordan.hollinger@gmail.com
|
|
@@ -56,6 +42,10 @@ files:
|
|
|
56
42
|
- lib/occams-record/batches/cursor_helpers.rb
|
|
57
43
|
- lib/occams-record/batches/offset_limit/raw_query.rb
|
|
58
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
|
|
59
49
|
- lib/occams-record/cursor.rb
|
|
60
50
|
- lib/occams-record/eager_loaders/ad_hoc_base.rb
|
|
61
51
|
- lib/occams-record/eager_loaders/ad_hoc_many.rb
|
|
@@ -79,6 +69,7 @@ files:
|
|
|
79
69
|
- lib/occams-record/raw_query.rb
|
|
80
70
|
- lib/occams-record/results/results.rb
|
|
81
71
|
- lib/occams-record/results/row.rb
|
|
72
|
+
- lib/occams-record/type_caster.rb
|
|
82
73
|
- lib/occams-record/ugly.rb
|
|
83
74
|
- lib/occams-record/version.rb
|
|
84
75
|
homepage: https://jhollinger.github.io/occams-record/
|