occams-record 0.36.0 → 1.0.0.rc1
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 +10 -6
- data/lib/occams-record/eager_loaders/ad_hoc_base.rb +26 -25
- data/lib/occams-record/eager_loaders/ad_hoc_many.rb +1 -1
- data/lib/occams-record/eager_loaders/ad_hoc_one.rb +1 -1
- data/lib/occams-record/eager_loaders/belongs_to.rb +1 -1
- data/lib/occams-record/eager_loaders/builder.rb +14 -13
- data/lib/occams-record/eager_loaders/has_many.rb +1 -1
- data/lib/occams-record/eager_loaders/has_one.rb +1 -1
- data/lib/occams-record/eager_loaders/polymorphic_belongs_to.rb +1 -1
- data/lib/occams-record/merge.rb +24 -13
- data/lib/occams-record/raw_query.rb +4 -4
- data/lib/occams-record/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 447d501a296a56063a1abfc6e07767dabfef9161c62a59a42e91dc1bc1824f19
|
|
4
|
+
data.tar.gz: c109e3e6a589b86dec40b87b7ad4d9fcc6cc1758485a8f82f8b6ac1d3ed878bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ec3f54c425f77b285e734387aa3c9ea67b1fc7a7669da79ec43028f10732aa492947a8451dcc1b0ecea183ae1fd01ef618788c19441cd7a0714d7805ab47f7b
|
|
7
|
+
data.tar.gz: d123d9795fd06173756dad4f6cb5fb602e05a30b7327e31be81610722c3fdacdeae7ed0af56c607870d4167081f39d8e40baaab6dbd1bbca244a345ef23a7dc6
|
data/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> Do not multiply entities beyond necessity. -- Occam's Razor
|
|
4
4
|
|
|
5
|
+
**Breaking change in 1.0.0 RC1** See HISTORY.md.
|
|
6
|
+
|
|
5
7
|
Occam's Record is a high-efficiency, advanced query library for ActiveRecord apps. It is **not** an ORM or an ActiveRecord replacement. Use it to solve pain points in your existing ActiveRecord app. Occams Record gives you two things:
|
|
6
8
|
|
|
7
9
|
**Performance**
|
|
@@ -110,13 +112,13 @@ To use `find_each`/`find_in_batches` you must provide the limit and offset state
|
|
|
110
112
|
|
|
111
113
|
```ruby
|
|
112
114
|
OccamsRecord.
|
|
113
|
-
sql(
|
|
115
|
+
sql("
|
|
114
116
|
SELECT * FROM orders
|
|
115
117
|
WHERE order_date > %{date}
|
|
116
118
|
ORDER BY order_date DESC, id
|
|
117
119
|
LIMIT %{batch_limit}
|
|
118
120
|
OFFSET %{batch_offset}
|
|
119
|
-
|
|
121
|
+
", {
|
|
120
122
|
date: 30.days.ago
|
|
121
123
|
}).
|
|
122
124
|
find_each(batch_size: 1000) do |order|
|
|
@@ -130,11 +132,11 @@ To use `eager_load` with a raw SQL query you must tell Occams what the base mode
|
|
|
130
132
|
|
|
131
133
|
```ruby
|
|
132
134
|
orders = OccamsRecord.
|
|
133
|
-
sql(
|
|
135
|
+
sql("
|
|
134
136
|
SELECT * FROM orders
|
|
135
137
|
WHERE order_date > %{date}
|
|
136
138
|
ORDER BY order_date DESC, id
|
|
137
|
-
|
|
139
|
+
", {
|
|
138
140
|
date: 30.days.ago
|
|
139
141
|
}).
|
|
140
142
|
model(Order).
|
|
@@ -165,7 +167,7 @@ But that's very wasteful. Occams gives us better options: `eager_load_many` and
|
|
|
165
167
|
```ruby
|
|
166
168
|
products = OccamsRecord.
|
|
167
169
|
query(Product.all).
|
|
168
|
-
eager_load_many(:customers, {:
|
|
170
|
+
eager_load_many(:customers, {:id => :product_id}, %w(
|
|
169
171
|
SELECT DISTINCT product_id, customers.*
|
|
170
172
|
FROM line_items
|
|
171
173
|
INNER JOIN orders ON line_items.order_id = orders.id
|
|
@@ -177,7 +179,9 @@ products = OccamsRecord.
|
|
|
177
179
|
run
|
|
178
180
|
```
|
|
179
181
|
|
|
180
|
-
`eager_load_many` allows us to declare an ad hoc *has_many* association called *customers*. The `{:
|
|
182
|
+
`eager_load_many` allows us to declare an ad hoc *has_many* association called *customers*. The `{:id => :product_id}` Hash defines the mapping: *id* in the parent record maps to *product_id* in the child records.
|
|
183
|
+
|
|
184
|
+
The SQL string and binds should be familiar by now. `%{ids}` will be provided for you - just stick it in the right place. Note that it won't always be called *ids*; the name will be the plural version of the key in your mapping.
|
|
181
185
|
|
|
182
186
|
`eager_load_one` defines an ad hoc `has_one`/`belongs_to` association.
|
|
183
187
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
1
3
|
module OccamsRecord
|
|
2
4
|
module EagerLoaders
|
|
3
5
|
#
|
|
@@ -13,7 +15,7 @@ module OccamsRecord
|
|
|
13
15
|
# Initialize a new add hoc association.
|
|
14
16
|
#
|
|
15
17
|
# @param name [Symbol] name of attribute to load records into
|
|
16
|
-
# @param mapping [Hash] a
|
|
18
|
+
# @param mapping [Hash] a Hash with the key being the parent id and the value being fkey in the child
|
|
17
19
|
# @param sql [String] the SQL to query the associated records. Include a bind params called '%{ids}' for the foreign/parent ids.
|
|
18
20
|
# @param binds [Hash] any additional binds for your query.
|
|
19
21
|
# @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
|
|
@@ -21,11 +23,8 @@ module OccamsRecord
|
|
|
21
23
|
# @yield eager load associations nested under this one
|
|
22
24
|
#
|
|
23
25
|
def initialize(name, mapping, sql, binds: {}, model: nil, use: nil, &builder)
|
|
24
|
-
@name = name.to_s
|
|
26
|
+
@name, @mapping = name.to_s, mapping
|
|
25
27
|
@sql, @binds, @use, @model = sql, binds, use, model
|
|
26
|
-
raise ArgumentError, "Add-hoc eager loading mapping must contain exactly one key-value pair" unless mapping.size == 1
|
|
27
|
-
@local_key = mapping.keys.first
|
|
28
|
-
@foreign_key = mapping.fetch(@local_key)
|
|
29
28
|
@eager_loaders = EagerLoaders::Context.new(@model)
|
|
30
29
|
instance_eval(&builder) if builder
|
|
31
30
|
end
|
|
@@ -37,34 +36,36 @@ module OccamsRecord
|
|
|
37
36
|
# @param query_logger [Array<String>]
|
|
38
37
|
#
|
|
39
38
|
def run(rows, query_logger: nil)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
39
|
+
fkey_binds = calc_fkey_binds rows
|
|
40
|
+
assoc = if fkey_binds.any?(&:any?)
|
|
41
|
+
binds = @binds.merge(fkey_binds)
|
|
42
|
+
RawQuery.new(@sql, binds, use: @use, eager_loaders: @eager_loaders, query_logger: query_logger).run
|
|
43
|
+
else
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
merge! assoc, rows
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
private
|
|
52
50
|
|
|
53
51
|
#
|
|
54
|
-
#
|
|
52
|
+
# Returns bind values from the parent rows.
|
|
55
53
|
#
|
|
56
54
|
# @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
|
|
57
|
-
# @yield
|
|
58
55
|
#
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
def calc_fkey_binds(rows)
|
|
57
|
+
@mapping.keys.reduce({}) { |a, fkey|
|
|
58
|
+
a[fkey.to_s.pluralize.to_sym] = rows.reduce(Set.new) { |aa, row|
|
|
59
|
+
begin
|
|
60
|
+
val = row.send fkey
|
|
61
|
+
aa << val if val
|
|
62
|
+
rescue NoMethodError => e
|
|
63
|
+
raise MissingColumnError.new(row, e.name)
|
|
64
|
+
end
|
|
65
|
+
aa
|
|
66
|
+
}.to_a
|
|
67
|
+
a
|
|
68
|
+
}
|
|
68
69
|
end
|
|
69
70
|
|
|
70
71
|
def merge!(assoc_rows, rows)
|
|
@@ -37,7 +37,7 @@ module OccamsRecord
|
|
|
37
37
|
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
|
38
38
|
# @param as [Symbol] Load the association usign a different attribute name
|
|
39
39
|
# @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
|
|
40
|
-
# @return [OccamsRecord::EagerLoaders::Base]
|
|
40
|
+
# @return [OccamsRecord::EagerLoaders::Base]
|
|
41
41
|
#
|
|
42
42
|
#
|
|
43
43
|
def nest(assoc, scope = nil, select: nil, use: nil, as: nil, optimizer: :select)
|
|
@@ -51,21 +51,21 @@ module OccamsRecord
|
|
|
51
51
|
# hold either one record or none.
|
|
52
52
|
#
|
|
53
53
|
# In the example below, :category is NOT an association on Widget. Though if it where it would be a belongs_to. The
|
|
54
|
-
# mapping argument says "The
|
|
55
|
-
#
|
|
56
|
-
# query.
|
|
54
|
+
# mapping argument says "The category_id in the parent (Widget) maps to the id column in the child records (Category).
|
|
55
|
+
#
|
|
56
|
+
# The %{category_ids} bind param will be provided for you, and in this case will be all the category_id values from the Widget query.
|
|
57
57
|
#
|
|
58
58
|
# res = OccamsRecord.
|
|
59
59
|
# query(Widget.order("name")).
|
|
60
|
-
# eager_load_one(:category, {:
|
|
61
|
-
# SELECT * FROM categories WHERE id IN (%{
|
|
62
|
-
#
|
|
60
|
+
# eager_load_one(:category, {:category_id => :id}, "
|
|
61
|
+
# SELECT * FROM categories WHERE id IN (%{category_ids}) AND name != %{bad_name}
|
|
62
|
+
# ", binds: {
|
|
63
63
|
# bad_name: "Bad Category"
|
|
64
64
|
# }).
|
|
65
65
|
# run
|
|
66
66
|
#
|
|
67
67
|
# @param name [Symbol] name of attribute to load records into
|
|
68
|
-
# @param mapping [Hash] a
|
|
68
|
+
# @param mapping [Hash] a Hash that defines the key mapping of the parent (widgets.category_id) to the child (categories.id).
|
|
69
69
|
# @param sql [String] the SQL to query the associated records. Include a bind params called '%{ids}' for the foreign/parent ids.
|
|
70
70
|
# @param binds [Hash] any additional binds for your query.
|
|
71
71
|
# @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
|
|
@@ -82,21 +82,22 @@ module OccamsRecord
|
|
|
82
82
|
# hold an array of 0 or more associated records.
|
|
83
83
|
#
|
|
84
84
|
# In the example below, :parts is NOT an association on Widget. Though if it where it would be a has_many. The
|
|
85
|
-
# mapping argument says "The
|
|
86
|
-
#
|
|
85
|
+
# mapping argument says "The id column in the parent (Widget) maps to the widget_id column in the children.
|
|
86
|
+
#
|
|
87
|
+
# The %{ids} bind param will be provided for you, and in this case will be all the id values from the Widget
|
|
87
88
|
# query.
|
|
88
89
|
#
|
|
89
90
|
# res = OccamsRecord.
|
|
90
91
|
# query(Widget.order("name")).
|
|
91
|
-
# eager_load_many(:parts, {:
|
|
92
|
+
# eager_load_many(:parts, {:id => :widget_id}, "
|
|
92
93
|
# SELECT * FROM parts WHERE widget_id IN (%{ids}) AND sku NOT IN (%{bad_skus})
|
|
93
|
-
#
|
|
94
|
+
# ", binds: {
|
|
94
95
|
# bad_skus: ["G90023ASDf0"]
|
|
95
96
|
# }).
|
|
96
97
|
# run
|
|
97
98
|
#
|
|
98
99
|
# @param name [Symbol] name of attribute to load records into
|
|
99
|
-
# @param mapping [Hash] a
|
|
100
|
+
# @param mapping [Hash] a Hash that defines the key mapping of the parent (widgets.id) to the children (parts.widget_id).
|
|
100
101
|
# @param sql [String] the SQL to query the associated records. Include a bind params called '%{ids}' for the foreign/parent ids.
|
|
101
102
|
# @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
|
|
102
103
|
# @param binds [Hash] any additional binds for your query.
|
|
@@ -32,7 +32,7 @@ module OccamsRecord
|
|
|
32
32
|
#
|
|
33
33
|
def merge!(assoc_rows, rows)
|
|
34
34
|
Merge.new(rows, name).
|
|
35
|
-
single!(assoc_rows, @ref.active_record_primary_key.to_s
|
|
35
|
+
single!(assoc_rows, {@ref.active_record_primary_key.to_s => @ref.foreign_key.to_s})
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
end
|
|
@@ -74,7 +74,7 @@ module OccamsRecord
|
|
|
74
74
|
}
|
|
75
75
|
model = type.constantize
|
|
76
76
|
Merge.new(rows_of_type, name).
|
|
77
|
-
single!(assoc_rows_of_type, @ref.foreign_key.to_s
|
|
77
|
+
single!(assoc_rows_of_type, {@ref.foreign_key.to_s => model.primary_key.to_s})
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
private
|
data/lib/occams-record/merge.rb
CHANGED
|
@@ -25,49 +25,60 @@ module OccamsRecord
|
|
|
25
25
|
# target_attr and assoc_attr are the matching keys on target_rows and assoc_rows, respectively.
|
|
26
26
|
#
|
|
27
27
|
# @param assoc_rows [Array<OccamsRecord::Results::Row>] rows to merge into target_rows
|
|
28
|
-
# @param
|
|
29
|
-
#
|
|
28
|
+
# @param mapping [Hash] The fields that should match up. The keys are for the target rows and the values
|
|
29
|
+
# for the associated rows.
|
|
30
30
|
#
|
|
31
|
-
def single!(assoc_rows,
|
|
32
|
-
|
|
31
|
+
def single!(assoc_rows, mapping)
|
|
32
|
+
target_attrs = mapping.keys.map
|
|
33
|
+
assoc_attrs = mapping.values
|
|
34
|
+
|
|
35
|
+
assoc_rows_by_ids = assoc_rows.reduce({}) { |a, assoc_row|
|
|
33
36
|
begin
|
|
34
|
-
|
|
37
|
+
ids = assoc_attrs.map { |attr| assoc_row.send attr }
|
|
35
38
|
rescue NoMethodError => e
|
|
36
39
|
raise MissingColumnError.new(assoc_row, e.name)
|
|
37
40
|
end
|
|
38
|
-
a[
|
|
41
|
+
a[ids] ||= assoc_row
|
|
39
42
|
a
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
target_rows.each do |row|
|
|
43
46
|
begin
|
|
44
|
-
|
|
47
|
+
attrs = target_attrs.map { |attr| row.send attr }
|
|
45
48
|
rescue NoMethodError => e
|
|
46
49
|
raise MissingColumnError.new(row, e.name)
|
|
47
50
|
end
|
|
48
|
-
row.send @assign,
|
|
51
|
+
row.send @assign, attrs.any? ? assoc_rows_by_ids[attrs] : nil
|
|
49
52
|
end
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
#
|
|
53
56
|
# Merge an array of assoc_rows into the target_rows. Some target_rows may end up with 0 matching
|
|
54
57
|
# associations, and they'll be assigned empty arrays.
|
|
55
|
-
# target_attr and assoc_attr are the matching keys on target_rows and assoc_rows, respectively.
|
|
56
58
|
#
|
|
57
|
-
|
|
59
|
+
# @param assoc_rows [Array<OccamsRecord::Results::Row>] rows to merge into target_rows
|
|
60
|
+
# @param mapping [Hash] The fields that should match up. The keys are for the target rows and the values
|
|
61
|
+
# for the associated rows.
|
|
62
|
+
#
|
|
63
|
+
def many!(assoc_rows, mapping)
|
|
64
|
+
target_attrs = mapping.keys
|
|
65
|
+
assoc_attrs = mapping.values
|
|
66
|
+
|
|
58
67
|
begin
|
|
59
|
-
|
|
68
|
+
assoc_rows_by_attrs = assoc_rows.group_by { |r|
|
|
69
|
+
assoc_attrs.map { |attr| r.send attr }
|
|
70
|
+
}
|
|
60
71
|
rescue NoMethodError => e
|
|
61
72
|
raise MissingColumnError.new(assoc_rows[0], e.name)
|
|
62
73
|
end
|
|
63
74
|
|
|
64
75
|
target_rows.each do |row|
|
|
65
76
|
begin
|
|
66
|
-
|
|
77
|
+
pkeys = target_attrs.map { |attr| row.send attr }
|
|
67
78
|
rescue NoMethodError => e
|
|
68
79
|
raise MissingColumnError.new(row, e.name)
|
|
69
80
|
end
|
|
70
|
-
row.send @assign,
|
|
81
|
+
row.send @assign, assoc_rows_by_attrs[pkeys] || []
|
|
71
82
|
end
|
|
72
83
|
end
|
|
73
84
|
end
|
|
@@ -4,10 +4,10 @@ module OccamsRecord
|
|
|
4
4
|
# a Hash 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
|
-
# results = OccamsRecord.sql(
|
|
7
|
+
# results = OccamsRecord.sql(
|
|
8
8
|
# SELECT * FROM widgets
|
|
9
9
|
# WHERE category_id = %{cat_id}
|
|
10
|
-
#
|
|
10
|
+
# ", {
|
|
11
11
|
# cat_id: 5
|
|
12
12
|
# }).run
|
|
13
13
|
#
|
|
@@ -16,10 +16,10 @@ module OccamsRecord
|
|
|
16
16
|
# NOTE If you're using SQLite, you must *always* specify the model.
|
|
17
17
|
#
|
|
18
18
|
# results = OccamsRecord.
|
|
19
|
-
# sql(
|
|
19
|
+
# sql("
|
|
20
20
|
# SELECT * FROM widgets
|
|
21
21
|
# WHERE category_id IN (%{cat_ids})
|
|
22
|
-
#
|
|
22
|
+
# ", {
|
|
23
23
|
# cat_ids: [5, 10]
|
|
24
24
|
# }).
|
|
25
25
|
# model(Widget).
|
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: 1.0.0.rc1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jordan Hollinger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2018-
|
|
11
|
+
date: 2018-12-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -74,9 +74,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
74
74
|
version: 2.3.0
|
|
75
75
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
76
|
requirements:
|
|
77
|
-
- - "
|
|
77
|
+
- - ">"
|
|
78
78
|
- !ruby/object:Gem::Version
|
|
79
|
-
version:
|
|
79
|
+
version: 1.3.1
|
|
80
80
|
requirements: []
|
|
81
81
|
rubyforge_project:
|
|
82
82
|
rubygems_version: 2.7.6
|