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 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