occams-record 0.36.0 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62c577ebb2e46ac7b6de067e466d7c01a7ff4c94cd0938d05dc156e3556fd964
4
- data.tar.gz: 535fffef48b996406b15a2f2c6f96b45fb5380882396c155cd227489d27f4a4d
3
+ metadata.gz: 447d501a296a56063a1abfc6e07767dabfef9161c62a59a42e91dc1bc1824f19
4
+ data.tar.gz: c109e3e6a589b86dec40b87b7ad4d9fcc6cc1758485a8f82f8b6ac1d3ed878bb
5
5
  SHA512:
6
- metadata.gz: 66216cd4f5013b6b0b5266f264bd3bd202e8384e1a4c036d67e8abb1c7b19842b3771bd45cfb731945d04a1a314e7319d9e21357d007a4835871b9171b2f0bcd
7
- data.tar.gz: c4a42814b3e1985b9b49e0d29cc3c8f8162f77e8e1a073bb7e04c10deebdd2c7149ca943250b97b74c791dd6e276b6a236b1d2558eb455c95d4481dd18c6c78e
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, {:product_id => :id}, %w(
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 `{:product_id => :id}` Hash defines the mapping: *product_id* in these results maps to *id* in the parent Product. The SQL string and binds should be familiar by now. The `%{ids}` value will be provided for you - just stick it in the right place.
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 one element Hash with the key being the local/child id and the value being the foreign/parent id
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
- calc_ids(rows) { |ids|
41
- assoc = if ids.any?
42
- binds = @binds.merge({:ids => ids})
43
- RawQuery.new(@sql, binds, use: @use, eager_loaders: @eager_loaders, query_logger: query_logger).run
44
- else
45
- []
46
- end
47
- merge! assoc, rows
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
- # Yield ids from the parent association to a block.
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 calc_ids(rows)
60
- ids = rows.map { |row|
61
- begin
62
- row.send @foreign_key
63
- rescue NoMethodError => e
64
- raise MissingColumnError.new(row, e.name)
65
- end
66
- }.compact.uniq
67
- yield ids
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)
@@ -14,7 +14,7 @@ module OccamsRecord
14
14
  #
15
15
  def merge!(assoc_rows, rows)
16
16
  Merge.new(rows, name).
17
- many!(assoc_rows, @foreign_key.to_s, @local_key.to_s)
17
+ many!(assoc_rows, @mapping)
18
18
  end
19
19
  end
20
20
  end
@@ -14,7 +14,7 @@ module OccamsRecord
14
14
  #
15
15
  def merge!(assoc_rows, rows)
16
16
  Merge.new(rows, name).
17
- single!(assoc_rows, @foreign_key.to_s, @local_key.to_s)
17
+ single!(assoc_rows, @mapping)
18
18
  end
19
19
  end
20
20
  end
@@ -29,7 +29,7 @@ module OccamsRecord
29
29
  #
30
30
  def merge!(assoc_rows, rows)
31
31
  Merge.new(rows, name).
32
- single!(assoc_rows, @ref.foreign_key.to_s, @model.primary_key.to_s)
32
+ single!(assoc_rows, {@ref.foreign_key.to_s => @model.primary_key.to_s})
33
33
  end
34
34
  end
35
35
  end
@@ -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] returns self
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 id column in this table (categories) maps to the category_id column in the other table (widgets)".
55
- # The %{ids} bind param will be provided for you, and in this case will be all the category_id values from the main
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, {:id => :category_id}, %(
61
- # SELECT * FROM categories WHERE id IN (%{ids}) AND name != %{bad_name}
62
- # ), binds: {
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 one element Hash with the key being the local/child id and the value being the foreign/parent id
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 widget_id column in this table (parts) maps to the id column in the other table (widgets)".
86
- # The %{ids} bind param will be provided for you, and in this case will be all the id values from the main
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, {:widget_id => :id}, %(
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
- # ), binds: {
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 one element Hash with the key being the local/child id and the value being the foreign/parent id
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.
@@ -12,7 +12,7 @@ module OccamsRecord
12
12
  #
13
13
  def merge!(assoc_rows, rows)
14
14
  Merge.new(rows, name).
15
- many!(assoc_rows, @ref.active_record_primary_key, @ref.foreign_key)
15
+ many!(assoc_rows, {@ref.active_record_primary_key => @ref.foreign_key})
16
16
  end
17
17
  end
18
18
  end
@@ -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, @ref.foreign_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, model.primary_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
@@ -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 target_attr [String|Symbol] name of the matching key on the target records
29
- # @param assoc_attr [String] name of the matching key on the associated records
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, target_attr, assoc_attr)
32
- assoc_rows_by_id = assoc_rows.reduce({}) { |a, assoc_row|
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
- id = assoc_row.send assoc_attr
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[id] ||= assoc_row
41
+ a[ids] ||= assoc_row
39
42
  a
40
43
  }
41
44
 
42
45
  target_rows.each do |row|
43
46
  begin
44
- attr = row.send target_attr
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, attr ? assoc_rows_by_id[attr] : nil
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
- def many!(assoc_rows, target_attr, assoc_attr)
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
- assoc_rows_by_attr = assoc_rows.group_by(&assoc_attr.to_sym)
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
- pkey = row.send target_attr
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, assoc_rows_by_attr[pkey] || []
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).
@@ -3,5 +3,5 @@
3
3
  #
4
4
  module OccamsRecord
5
5
  # Library version
6
- VERSION = '0.36.0'.freeze
6
+ VERSION = '1.0.0.rc1'.freeze
7
7
  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.36.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-14 00:00:00.000000000 Z
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: '0'
79
+ version: 1.3.1
80
80
  requirements: []
81
81
  rubyforge_project:
82
82
  rubygems_version: 2.7.6