occams-record 0.7.0 → 0.8.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 +30 -0
- data/lib/occams-record.rb +2 -1
- data/lib/occams-record/batches.rb +2 -1
- data/lib/occams-record/eager_loaders.rb +48 -13
- data/lib/occams-record/eager_loaders/base.rb +3 -3
- data/lib/occams-record/eager_loaders/belongs_to.rb +3 -3
- data/lib/occams-record/eager_loaders/habtm.rb +4 -4
- data/lib/occams-record/eager_loaders/has_many.rb +2 -2
- data/lib/occams-record/eager_loaders/has_one.rb +3 -3
- data/lib/occams-record/eager_loaders/polymorphic_belongs_to.rb +1 -1
- data/lib/occams-record/merge.rb +3 -3
- data/lib/occams-record/query.rb +5 -34
- data/lib/occams-record/raw_query.rb +128 -0
- data/lib/occams-record/results.rb +106 -0
- data/lib/occams-record/version.rb +1 -1
- metadata +5 -4
- data/lib/occams-record/result_row.rb +0 -95
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 704aeee89a29c6faaf5044e3eba09efdffa64bbd
|
4
|
+
data.tar.gz: 70415bb9a9a32a11dca5d1a25d4eff04ffc19571
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 487a7e553edb7f30e84ec4071d01596d36cf1232aa1016265bdf4b589f5a2118cfbcffaff0b2508927811638e6b6f4af5a51bd52726ab46f1b8297eb0db8dcda
|
7
|
+
data.tar.gz: 8f3f6d2850eb49dabefc1dd95221f19ec56ec70c00c067b5ddf79d0dd96b87bcab854ed99fc041502c598f6df267dd3e657e908f685f06d5a660aed44657869b
|
data/README.md
CHANGED
@@ -138,6 +138,36 @@ widgets[0].orders[0].description
|
|
138
138
|
=> "O839SJZ98B 1/8/2017"
|
139
139
|
```
|
140
140
|
|
141
|
+
## Raw SQL queries
|
142
|
+
|
143
|
+
If you have a complicated query to run, you may drop down to hand-written SQL while still taking advantage of eager loading and variable escaping. (Note the slightly different syntax for binding variables.)
|
144
|
+
|
145
|
+
NOTE this feature is quite new and might have some bugs. Issues and Pull Requests welcome.
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
widgets = OccamsRecord.sql(%(
|
149
|
+
SELECT * FROM widgets
|
150
|
+
WHERE category_id = %{cat_id}
|
151
|
+
), {
|
152
|
+
cat_id: 5
|
153
|
+
}).run
|
154
|
+
```
|
155
|
+
|
156
|
+
To perform eager loading, you must specify the base model. NOTE some database adapters, notably SQLite, require you to always specify the model.
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
widgets = OccamsRecord.
|
160
|
+
sql(%(
|
161
|
+
SELECT * FROM widgets
|
162
|
+
WHERE category_id IN (%{cat_ids})
|
163
|
+
), {
|
164
|
+
cat_ids: [5, 10]
|
165
|
+
}).
|
166
|
+
model(Widget).
|
167
|
+
eager_load(:category).
|
168
|
+
run
|
169
|
+
```
|
170
|
+
|
141
171
|
## Unsupported features
|
142
172
|
|
143
173
|
The following `ActiveRecord` are not supported, and I have no plans to do so. However, I'd be glad to accept pull requests.
|
data/lib/occams-record.rb
CHANGED
@@ -50,7 +50,8 @@ module OccamsRecord
|
|
50
50
|
#
|
51
51
|
# Returns an Enumerator that yields batches of records, of size "of".
|
52
52
|
# NOTE ActiveRecord 5+ provides the 'in_batches' method to do something
|
53
|
-
# similiar, although 4.2 does not. Also it does not respect ORDER BY
|
53
|
+
# similiar, although 4.2 does not. Also it does not respect ORDER BY,
|
54
|
+
# whereas this does.
|
54
55
|
#
|
55
56
|
# @param of [Integer] batch size
|
56
57
|
# @return [Enumerator] yields batches
|
@@ -10,19 +10,54 @@ module OccamsRecord
|
|
10
10
|
autoload :HasMany, 'occams-record/eager_loaders/has_many'
|
11
11
|
autoload :Habtm, 'occams-record/eager_loaders/habtm'
|
12
12
|
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
13
|
+
# Methods for adding eager loading to a query.
|
14
|
+
module Builder
|
15
|
+
#
|
16
|
+
# Specify an association to be eager-loaded. You may optionally pass a block that accepts a scope
|
17
|
+
# which you may modify to customize the query. For maximum memory savings, always `select` only
|
18
|
+
# the colums you actually need.
|
19
|
+
#
|
20
|
+
# @param assoc [Symbol] name of association
|
21
|
+
# @param scope [Proc] a scope to apply to the query (optional)
|
22
|
+
# @param select [String] a custom SELECT statement, minus the SELECT (optional)
|
23
|
+
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
24
|
+
# @param eval_block [Proc] a block where you may perform eager loading on *this* association (optional)
|
25
|
+
# @return [OccamsRecord::Query] returns self
|
26
|
+
#
|
27
|
+
def eager_load(assoc, scope = nil, select: nil, use: nil, &eval_block)
|
28
|
+
ref = @model ? @model.reflections[assoc.to_s] : nil
|
29
|
+
raise "OccamsRecord: No assocation `:#{assoc}` on `#{@model&.name || '<model missing>'}`" if ref.nil?
|
30
|
+
scope ||= -> { self.select select } if select
|
31
|
+
@eager_loaders << eager_loader_for_association(ref).new(ref, scope, use, &eval_block)
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Run all defined eager loaders into the given result rows
|
38
|
+
def eager_load!(rows)
|
39
|
+
@eager_loaders.each { |loader|
|
40
|
+
loader.query(rows) { |scope|
|
41
|
+
assoc_rows = Query.new(scope, use: loader.use, query_logger: @query_logger, &loader.eval_block).run
|
42
|
+
loader.merge! assoc_rows, rows
|
43
|
+
}
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Fetch the appropriate eager loader for the given association type.
|
48
|
+
def eager_loader_for_association(ref)
|
49
|
+
case ref.macro
|
50
|
+
when :belongs_to
|
51
|
+
ref.options[:polymorphic] ? PolymorphicBelongsTo : BelongsTo
|
52
|
+
when :has_one
|
53
|
+
HasOne
|
54
|
+
when :has_many
|
55
|
+
HasMany
|
56
|
+
when :has_and_belongs_to_many
|
57
|
+
EagerLoaders::Habtm
|
58
|
+
else
|
59
|
+
raise "Unsupported association type `#{macro}`"
|
60
|
+
end
|
26
61
|
end
|
27
62
|
end
|
28
63
|
end
|
@@ -25,7 +25,7 @@ module OccamsRecord
|
|
25
25
|
#
|
26
26
|
# Yield one or more ActiveRecord::Relation objects to a given block.
|
27
27
|
#
|
28
|
-
# @param rows [Array<OccamsRecord::
|
28
|
+
# @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
|
29
29
|
#
|
30
30
|
def query(rows)
|
31
31
|
raise 'Not Implemented'
|
@@ -34,8 +34,8 @@ module OccamsRecord
|
|
34
34
|
#
|
35
35
|
# Merges the associated rows into the parent rows.
|
36
36
|
#
|
37
|
-
# @param assoc_rows [Array<OccamsRecord::
|
38
|
-
# @param rows [Array<OccamsRecord::
|
37
|
+
# @param assoc_rows [Array<OccamsRecord::Results::Row>]
|
38
|
+
# @param rows [Array<OccamsRecord::Results::Row>]
|
39
39
|
#
|
40
40
|
def merge!(assoc_rows, rows)
|
41
41
|
raise 'Not Implemented'
|
@@ -5,7 +5,7 @@ module OccamsRecord
|
|
5
5
|
#
|
6
6
|
# Yield one or more ActiveRecord::Relation objects to a given block.
|
7
7
|
#
|
8
|
-
# @param rows [Array<OccamsRecord::
|
8
|
+
# @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
|
9
9
|
#
|
10
10
|
def query(rows)
|
11
11
|
ids = rows.map { |r| r.send @ref.foreign_key }.compact.uniq
|
@@ -15,8 +15,8 @@ module OccamsRecord
|
|
15
15
|
#
|
16
16
|
# Merge the association rows into the given rows.
|
17
17
|
#
|
18
|
-
# @param assoc_rows [Array<OccamsRecord::
|
19
|
-
# @param rows [Array<OccamsRecord::
|
18
|
+
# @param assoc_rows [Array<OccamsRecord::Results::Row>] rows loaded from the association
|
19
|
+
# @param rows [Array<OccamsRecord::Results::Row>] rows loaded from the main model
|
20
20
|
#
|
21
21
|
def merge!(assoc_rows, rows)
|
22
22
|
Merge.new(rows, name).
|
@@ -5,7 +5,7 @@ module OccamsRecord
|
|
5
5
|
#
|
6
6
|
# Yield one or more ActiveRecord::Relation objects to a given block.
|
7
7
|
#
|
8
|
-
# @param rows [Array<OccamsRecord::
|
8
|
+
# @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
|
9
9
|
#
|
10
10
|
def query(rows)
|
11
11
|
assoc_ids = join_rows(rows).map { |row| row[1] }.compact.uniq
|
@@ -15,8 +15,8 @@ module OccamsRecord
|
|
15
15
|
#
|
16
16
|
# Merge the association rows into the given rows.
|
17
17
|
#
|
18
|
-
# @param assoc_rows [Array<OccamsRecord::
|
19
|
-
# @param rows [Array<OccamsRecord::
|
18
|
+
# @param assoc_rows [Array<OccamsRecord::Results::Row>] rows loaded from the association
|
19
|
+
# @param rows [Array<OccamsRecord::Results::Row>] rows loaded from the main model
|
20
20
|
#
|
21
21
|
def merge!(assoc_rows, rows)
|
22
22
|
joins_by_id = join_rows(rows).reduce({}) { |a, join|
|
@@ -46,7 +46,7 @@ module OccamsRecord
|
|
46
46
|
#
|
47
47
|
# Fetches (and caches) an array of rows from the join table. The rows are [fkey, assoc_fkey].
|
48
48
|
#
|
49
|
-
# @param rows [Array<OccamsRecord::
|
49
|
+
# @param rows [Array<OccamsRecord::Results::Row>]
|
50
50
|
# @return [Array<Array<String>>]
|
51
51
|
#
|
52
52
|
def join_rows(rows)
|
@@ -5,8 +5,8 @@ module OccamsRecord
|
|
5
5
|
#
|
6
6
|
# Merge the association rows into the given rows.
|
7
7
|
#
|
8
|
-
# @param assoc_rows [Array<OccamsRecord::
|
9
|
-
# @param rows [Array<OccamsRecord::
|
8
|
+
# @param assoc_rows [Array<OccamsRecord::Results::Row>] rows loaded from the association
|
9
|
+
# @param rows [Array<OccamsRecord::Results::Row>] rows loaded from the main model
|
10
10
|
#
|
11
11
|
def merge!(assoc_rows, rows)
|
12
12
|
Merge.new(rows, name).
|
@@ -5,7 +5,7 @@ module OccamsRecord
|
|
5
5
|
#
|
6
6
|
# Yield one or more ActiveRecord::Relation objects to a given block.
|
7
7
|
#
|
8
|
-
# @param rows [Array<OccamsRecord::
|
8
|
+
# @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
|
9
9
|
#
|
10
10
|
def query(rows)
|
11
11
|
return if rows.empty?
|
@@ -18,8 +18,8 @@ module OccamsRecord
|
|
18
18
|
#
|
19
19
|
# Merge the association rows into the given rows.
|
20
20
|
#
|
21
|
-
# @param assoc_rows [Array<OccamsRecord::
|
22
|
-
# @param rows [Array<OccamsRecord::
|
21
|
+
# @param assoc_rows [Array<OccamsRecord::Results::Row>] rows loaded from the association
|
22
|
+
# @param rows [Array<OccamsRecord::Results::Row>] rows loaded from the main model
|
23
23
|
#
|
24
24
|
def merge!(assoc_rows, rows)
|
25
25
|
Merge.new(rows, name).
|
@@ -25,7 +25,7 @@ module OccamsRecord
|
|
25
25
|
#
|
26
26
|
# Yield ActiveRecord::Relations to the given block, one for every "type" represented in the given rows.
|
27
27
|
#
|
28
|
-
# @param rows [Array<OccamsRecord::
|
28
|
+
# @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
|
29
29
|
#
|
30
30
|
def query(rows)
|
31
31
|
rows_by_type = rows.group_by(&@foreign_type)
|
data/lib/occams-record/merge.rb
CHANGED
@@ -6,13 +6,13 @@ module OccamsRecord
|
|
6
6
|
# After initializing, perform a specific type of merge by calling the appropriate *! method.
|
7
7
|
#
|
8
8
|
class Merge
|
9
|
-
# @return [Array<OccamsRecord::
|
9
|
+
# @return [Array<OccamsRecord::Results::Row>] the rows into which associated rows will be merged
|
10
10
|
attr_reader :target_rows
|
11
11
|
|
12
12
|
#
|
13
13
|
# Initialize a new Merge operation.
|
14
14
|
#
|
15
|
-
# @param target_rows [Array<OccamsRecord::
|
15
|
+
# @param target_rows [Array<OccamsRecord::Results::Row] the rows into which associated rows should be merged
|
16
16
|
# @param assoc_name [String|Symbol] name of the attribute where associated rows will be put
|
17
17
|
#
|
18
18
|
def initialize(target_rows, assoc_name)
|
@@ -24,7 +24,7 @@ module OccamsRecord
|
|
24
24
|
# Merge a single assoc_row into each target_rows (or nil if one can't be found).
|
25
25
|
# target_attr and assoc_attr are the matching keys on target_rows and assoc_rows, respectively.
|
26
26
|
#
|
27
|
-
# @param assoc_rows [Array<OccamsRecord::
|
27
|
+
# @param assoc_rows [Array<OccamsRecord::Results::Row>] rows to merge into target_rows
|
28
28
|
# @param target_attr [String|Symbol] name of the matching key on the target records
|
29
29
|
# @param assoc_attr [String] name of the matching key on the associated records
|
30
30
|
#
|
data/lib/occams-record/query.rb
CHANGED
@@ -35,10 +35,9 @@ module OccamsRecord
|
|
35
35
|
attr_reader :model
|
36
36
|
# @return [ActiveRecord::Relation] scope for building the main SQL query
|
37
37
|
attr_reader :scope
|
38
|
-
# @return [ActiveRecord::Connection]
|
39
|
-
attr_reader :conn
|
40
38
|
|
41
39
|
include Batches
|
40
|
+
include EagerLoaders::Builder
|
42
41
|
|
43
42
|
#
|
44
43
|
# Initialize a new query.
|
@@ -53,51 +52,23 @@ module OccamsRecord
|
|
53
52
|
@model = scope.klass
|
54
53
|
@scope = scope
|
55
54
|
@eager_loaders = eager_loaders
|
56
|
-
@conn = model.connection
|
57
55
|
@use = use
|
58
56
|
@query_logger = query_logger
|
59
57
|
instance_eval(&eval_block) if eval_block
|
60
58
|
end
|
61
59
|
|
62
|
-
#
|
63
|
-
# Specify an association to be eager-loaded. You may optionally pass a block that accepts a scope
|
64
|
-
# which you may modify to customize the query. For maximum memory savings, always `select` only
|
65
|
-
# the colums you actually need.
|
66
|
-
#
|
67
|
-
# @param assoc [Symbol] name of association
|
68
|
-
# @param scope [Proc] a scope to apply to the query (optional)
|
69
|
-
# @param select [String] a custom SELECT statement, minus the SELECT (optional)
|
70
|
-
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
71
|
-
# @param eval_block [Proc] a block where you may perform eager loading on *this* association (optional)
|
72
|
-
# @return [OccamsRecord::Query] returns self
|
73
|
-
#
|
74
|
-
def eager_load(assoc, scope = nil, select: nil, use: nil, &eval_block)
|
75
|
-
ref = model.reflections[assoc.to_s]
|
76
|
-
raise "OccamsRecord: No assocation `:#{assoc}` on `#{model.name}`" if ref.nil?
|
77
|
-
scope ||= -> { self.select select } if select
|
78
|
-
@eager_loaders << EagerLoaders.fetch!(ref).new(ref, scope, use, &eval_block)
|
79
|
-
self
|
80
|
-
end
|
81
|
-
|
82
60
|
#
|
83
61
|
# Run the query and return the results.
|
84
62
|
#
|
85
|
-
# @return [Array<OccamsRecord::
|
63
|
+
# @return [Array<OccamsRecord::Results::Row>]
|
86
64
|
#
|
87
65
|
def run
|
88
66
|
sql = scope.to_sql
|
89
67
|
@query_logger << sql if @query_logger
|
90
|
-
result =
|
91
|
-
row_class = OccamsRecord.
|
68
|
+
result = model.connection.exec_query sql
|
69
|
+
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.map(&:name), model: model, modules: @use)
|
92
70
|
rows = result.rows.map { |row| row_class.new row }
|
93
|
-
|
94
|
-
@eager_loaders.each { |loader|
|
95
|
-
loader.query(rows) { |scope|
|
96
|
-
assoc_rows = Query.new(scope, use: loader.use, query_logger: @query_logger, &loader.eval_block).run
|
97
|
-
loader.merge! assoc_rows, rows
|
98
|
-
}
|
99
|
-
}
|
100
|
-
|
71
|
+
eager_load! rows
|
101
72
|
rows
|
102
73
|
end
|
103
74
|
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module OccamsRecord
|
2
|
+
#
|
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
|
5
|
+
# write safe, complicated SQL by hand while also supporting eager loading.
|
6
|
+
#
|
7
|
+
# results = OccamsRecord.sql(%(
|
8
|
+
# SELECT * FROM widgets
|
9
|
+
# WHERE category_id = %{cat_id}
|
10
|
+
# ), {
|
11
|
+
# cat_id: 5
|
12
|
+
# }).run
|
13
|
+
#
|
14
|
+
# If you want to do eager loading, you must first the define a model to pull the associations from.
|
15
|
+
# NOTE If you're using SQLite, you must *always* specify the model.
|
16
|
+
#
|
17
|
+
# results = OccamsRecord.
|
18
|
+
# sql(%(
|
19
|
+
# SELECT * FROM widgets
|
20
|
+
# WHERE category_id IN (%{cat_ids})
|
21
|
+
# ), {
|
22
|
+
# cat_ids: [5, 10]
|
23
|
+
# }).
|
24
|
+
# model(Widget).
|
25
|
+
# eager_load(:category).
|
26
|
+
# run
|
27
|
+
#
|
28
|
+
# @param sql [String] The SELECT statement to run. Binds should use the built-in Ruby "%{bind_name}" syntax.
|
29
|
+
# @param binds [Hash] Bind values (Symbol keys)
|
30
|
+
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
31
|
+
# @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
|
32
|
+
# @return [OccamsRecord::RawQuery]
|
33
|
+
#
|
34
|
+
def self.sql(sql, binds, use: nil, query_logger: nil)
|
35
|
+
RawQuery.new(sql, binds, use: use, query_logger: nil)
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Represents a raw SQL query to be run and eager associations to be loaded. Use OccamsRecord.sql to create your queries
|
40
|
+
# instead of instantiating objects directly.
|
41
|
+
#
|
42
|
+
class RawQuery
|
43
|
+
# @return [String]
|
44
|
+
attr_reader :sql
|
45
|
+
# @return [Hash]
|
46
|
+
attr_reader :binds
|
47
|
+
|
48
|
+
include EagerLoaders::Builder
|
49
|
+
|
50
|
+
#
|
51
|
+
# Initialize a new query.
|
52
|
+
#
|
53
|
+
# @param sql [String] The SELECT statement to run. Binds should use the built-in Ruby "%{bind_name}" syntax.
|
54
|
+
# @param binds [Hash] Bind values (Symbol keys)
|
55
|
+
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
56
|
+
# @param eager_loaders [OccamsRecord::EagerLoaders::Base]
|
57
|
+
# @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
|
58
|
+
# @param eval_block [Proc] block that will be eval'd on this instance. Can be used for eager loading. (optional)
|
59
|
+
#
|
60
|
+
def initialize(sql, binds, use: nil, eager_loaders: [], query_logger: nil)
|
61
|
+
@sql = sql
|
62
|
+
@binds = binds
|
63
|
+
@use = use
|
64
|
+
@eager_loaders = eager_loaders
|
65
|
+
@query_logger = query_logger
|
66
|
+
@model = nil
|
67
|
+
@conn = @model&.connection || ActiveRecord::Base.connection
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Specify the model to be used to load eager associations. Normally this would be the main table you're
|
72
|
+
# SELECTing from.
|
73
|
+
#
|
74
|
+
# NOTE Some database adapters, notably SQLite's, require that the model *always* be specified, even if you
|
75
|
+
# aren't doing eager loading.
|
76
|
+
#
|
77
|
+
# @param klass [ActiveRecord::Base]
|
78
|
+
# @return [OccamsRecord::RawQuery] self
|
79
|
+
#
|
80
|
+
def model(klass)
|
81
|
+
@model = klass
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# Run the query and return the results.
|
87
|
+
#
|
88
|
+
# @return [Array<OccamsRecord::Results::Row>]
|
89
|
+
#
|
90
|
+
def run
|
91
|
+
result = @conn.exec_query escaped_sql
|
92
|
+
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.map(&:name), model: @model, modules: @use)
|
93
|
+
rows = result.rows.map { |row| row_class.new row }
|
94
|
+
eager_load! rows
|
95
|
+
rows
|
96
|
+
end
|
97
|
+
|
98
|
+
alias_method :to_a, :run
|
99
|
+
|
100
|
+
#
|
101
|
+
# If you pass a block, each result row will be yielded to it. If you don't,
|
102
|
+
# an Enumerable will be returned.
|
103
|
+
#
|
104
|
+
# @return Enumerable
|
105
|
+
#
|
106
|
+
def each
|
107
|
+
if block_given?
|
108
|
+
to_a.each { |row| yield row }
|
109
|
+
else
|
110
|
+
to_a.each
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
# Returns the SQL as a String with all variables escaped
|
117
|
+
def escaped_sql
|
118
|
+
sql % binds.reduce({}) { |a, (col, val)|
|
119
|
+
a[col.to_sym] = if val.is_a? Array
|
120
|
+
val.map { |x| @conn.quote x }.join(', ')
|
121
|
+
else
|
122
|
+
@conn.quote val
|
123
|
+
end
|
124
|
+
a
|
125
|
+
}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module OccamsRecord
|
2
|
+
module Results
|
3
|
+
# ActiveRecord's internal type casting API changes from version to version.
|
4
|
+
CASTER = case ActiveRecord::VERSION::MAJOR
|
5
|
+
when 4 then :type_cast_from_database
|
6
|
+
when 5 then :deserialize
|
7
|
+
end
|
8
|
+
|
9
|
+
#
|
10
|
+
# Dynamically build a class for a specific set of result rows. It inherits from OccamsRecord::Results::Row, and optionall includes
|
11
|
+
# a user-defined module.
|
12
|
+
#
|
13
|
+
# @param column_names [Array<String>] the column names in the result set. The order MUST match the order returned by the query.
|
14
|
+
# @param column_types [Hash] Column name => type from an ActiveRecord::Result
|
15
|
+
# @param association_names [Array<String>] names of associations that will be eager loaded into the results.
|
16
|
+
# @param model [ActiveRecord::Base] the AR model representing the table (it holds column & type info).
|
17
|
+
# @param modules [Array<Module>] (optional)
|
18
|
+
# @return [OccamsRecord::Results::Row] a class customized for this result set
|
19
|
+
#
|
20
|
+
def self.klass(column_names, column_types, association_names = [], model: nil, modules: nil)
|
21
|
+
Class.new(Results::Row) do
|
22
|
+
Array(modules).each { |mod| include mod } if modules
|
23
|
+
|
24
|
+
self.columns = column_names.map(&:to_s)
|
25
|
+
self.associations = association_names.map(&:to_s)
|
26
|
+
self.model_name = model ? model.name : nil
|
27
|
+
|
28
|
+
# Build getters & setters for associations. (We need setters b/c they're set AFTER the row is initialized
|
29
|
+
attr_accessor(*association_names)
|
30
|
+
|
31
|
+
# Build a getter for each attribute returned by the query. The values will be type converted on demand.
|
32
|
+
model_column_types = model ? model.attributes_builder.types : {}
|
33
|
+
self.columns.each_with_index do |col, idx|
|
34
|
+
type =
|
35
|
+
column_types[col] ||
|
36
|
+
model_column_types[col] ||
|
37
|
+
raise("OccamsRecord: Column `#{col}` does not exist on model `#{self.model_name}`")
|
38
|
+
|
39
|
+
case type.type
|
40
|
+
when :datetime
|
41
|
+
define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx]).in_time_zone }
|
42
|
+
else
|
43
|
+
define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx]) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Abstract class for result rows.
|
51
|
+
#
|
52
|
+
class Row
|
53
|
+
class << self
|
54
|
+
# Array of column names
|
55
|
+
attr_accessor :columns
|
56
|
+
# Array of associations names
|
57
|
+
attr_accessor :associations
|
58
|
+
# Name of Rails model
|
59
|
+
attr_accessor :model_name
|
60
|
+
end
|
61
|
+
self.columns = []
|
62
|
+
self.associations = []
|
63
|
+
|
64
|
+
#
|
65
|
+
# Initialize a new result row.
|
66
|
+
#
|
67
|
+
# @param raw_values [Array] array of raw values from db
|
68
|
+
#
|
69
|
+
def initialize(raw_values)
|
70
|
+
@raw_values = raw_values
|
71
|
+
@cast_values = {}
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Return row as a Hash (recursive).
|
76
|
+
#
|
77
|
+
# @param symbolize_names [Boolean] if true, make Hash keys Symbols instead of Strings
|
78
|
+
# @return [Hash] a Hash with String or Symbol keys
|
79
|
+
#
|
80
|
+
def to_h(symbolize_names: false)
|
81
|
+
hash = self.class.columns.reduce({}) { |a, col_name|
|
82
|
+
key = symbolize_names ? col_name.to_sym : col_name
|
83
|
+
a[key] = send col_name
|
84
|
+
a
|
85
|
+
}
|
86
|
+
|
87
|
+
self.class.associations.reduce(hash) { |a, assoc_name|
|
88
|
+
key = symbolize_names ? assoc_name.to_sym : assoc_name
|
89
|
+
assoc = send assoc_name
|
90
|
+
a[key] = if assoc.is_a? Array
|
91
|
+
assoc.map { |x| x.to_h(symbolize_names: symbolize_names) }
|
92
|
+
elsif assoc
|
93
|
+
assoc.to_h(symbolize_names: symbolize_names)
|
94
|
+
end
|
95
|
+
a
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
alias_method :to_hash, :to_h
|
100
|
+
|
101
|
+
def inspect
|
102
|
+
"#<OccamsRecord::Results::Row @model_name=#{self.class.model_name} @raw_values=#{@raw_values}>"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
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: 0.
|
4
|
+
version: 0.8.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: 2017-
|
11
|
+
date: 2017-12-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -49,7 +49,8 @@ files:
|
|
49
49
|
- lib/occams-record/eager_loaders/polymorphic_belongs_to.rb
|
50
50
|
- lib/occams-record/merge.rb
|
51
51
|
- lib/occams-record/query.rb
|
52
|
-
- lib/occams-record/
|
52
|
+
- lib/occams-record/raw_query.rb
|
53
|
+
- lib/occams-record/results.rb
|
53
54
|
- lib/occams-record/version.rb
|
54
55
|
homepage: https://github.com/jhollinger/occams-record
|
55
56
|
licenses:
|
@@ -63,7 +64,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
63
64
|
requirements:
|
64
65
|
- - ">="
|
65
66
|
- !ruby/object:Gem::Version
|
66
|
-
version: 2.
|
67
|
+
version: 2.3.0
|
67
68
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
69
|
requirements:
|
69
70
|
- - ">="
|
@@ -1,95 +0,0 @@
|
|
1
|
-
module OccamsRecord
|
2
|
-
# ActiveRecord's internal type casting API changes from version to version.
|
3
|
-
TYPE_CAST_METHOD = case ActiveRecord::VERSION::MAJOR
|
4
|
-
when 4 then :type_cast_from_database
|
5
|
-
when 5 then :deserialize
|
6
|
-
end
|
7
|
-
|
8
|
-
#
|
9
|
-
# Dynamically build a class for a specific set of result rows. It inherits from OccamsRecord::ResultRow, and optionall includes
|
10
|
-
# a user-defined module.
|
11
|
-
#
|
12
|
-
# @param model [ActiveRecord::Base] the AR model representing the table (it holds column & type info).
|
13
|
-
# @param column_names [Array<String>] the column names in the result set. The order MUST match the order returned by the query.
|
14
|
-
# @param column_types [Hash] Column name => type from an ActiveRecord::Result
|
15
|
-
# @param association_names [Array<String>] names of associations that will be eager loaded into the results.
|
16
|
-
# @param modules [Array<Module>] (optional)
|
17
|
-
# @return [OccamsRecord::ResultRow] a class customized for this result set
|
18
|
-
#
|
19
|
-
def self.build_result_row_class(model, column_names, column_types, association_names, modules: nil)
|
20
|
-
Class.new(ResultRow) do
|
21
|
-
Array(modules).each { |mod| include mod } if modules
|
22
|
-
|
23
|
-
self.columns = column_names.map(&:to_s)
|
24
|
-
self.associations = association_names.map(&:to_s)
|
25
|
-
self.model_name = model.name
|
26
|
-
|
27
|
-
# Build getters & setters for associations. (We need setters b/c they're set AFTER the row is initialized
|
28
|
-
attr_accessor(*association_names)
|
29
|
-
|
30
|
-
# Build a getter for each attribute returned by the query. The values will be type converted on demand.
|
31
|
-
self.columns.each_with_index do |col, idx|
|
32
|
-
type =
|
33
|
-
column_types[col] ||
|
34
|
-
model.attributes_builder.types[col] ||
|
35
|
-
raise("OccamsRecord: Column `#{col}` does not exist on model `#{self.model_name}`")
|
36
|
-
define_method col do
|
37
|
-
@cast_values_cache[idx] ||= type.send(TYPE_CAST_METHOD, @raw_values[idx])
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
#
|
44
|
-
# Abstract class for result rows.
|
45
|
-
#
|
46
|
-
class ResultRow
|
47
|
-
class << self
|
48
|
-
# Array of column names
|
49
|
-
attr_accessor :columns
|
50
|
-
# Array of associations names
|
51
|
-
attr_accessor :associations
|
52
|
-
# Name of Rails model
|
53
|
-
attr_accessor :model_name
|
54
|
-
end
|
55
|
-
self.columns = []
|
56
|
-
self.associations = []
|
57
|
-
|
58
|
-
#
|
59
|
-
# Initialize a new result row.
|
60
|
-
#
|
61
|
-
# @param raw_values [Array] array of raw values from db
|
62
|
-
#
|
63
|
-
def initialize(raw_values)
|
64
|
-
@raw_values = raw_values
|
65
|
-
@cast_values_cache = {}
|
66
|
-
end
|
67
|
-
|
68
|
-
#
|
69
|
-
# Return row as a Hash (recursive).
|
70
|
-
#
|
71
|
-
# @param symbolize_names [Boolean] if true, make Hash keys Symbols instead of Strings
|
72
|
-
# @return [Hash] a Hash with String or Symbol keys
|
73
|
-
#
|
74
|
-
def to_h(symbolize_names: false)
|
75
|
-
hash = self.class.columns.reduce({}) { |a, col_name|
|
76
|
-
key = symbolize_names ? col_name.to_sym : col_name
|
77
|
-
a[key] = send col_name
|
78
|
-
a
|
79
|
-
}
|
80
|
-
|
81
|
-
self.class.associations.reduce(hash) { |a, assoc_name|
|
82
|
-
key = symbolize_names ? assoc_name.to_sym : assoc_name
|
83
|
-
assoc = send assoc_name
|
84
|
-
a[key] = if assoc.is_a? Array
|
85
|
-
assoc.map { |x| x.to_h(symbolize_names: symbolize_names) }
|
86
|
-
elsif assoc
|
87
|
-
assoc.to_h(symbolize_names: symbolize_names)
|
88
|
-
end
|
89
|
-
a
|
90
|
-
}
|
91
|
-
end
|
92
|
-
|
93
|
-
alias_method :to_hash, :to_h
|
94
|
-
end
|
95
|
-
end
|