preload_pluck 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2a3f1d75e72b1ac020ff2482bcd6ca4f6e6567ef
4
- data.tar.gz: 6b6aa4f06b7d7cb0acd732829053e182751d35ae
3
+ metadata.gz: 8dcb60367b14df2613159448fec6c4b5cbfbf655
4
+ data.tar.gz: a716b978a51994de1ab2c587946a8edfd7efc860
5
5
  SHA512:
6
- metadata.gz: a0716aafcb1a5bc1bd95dad6e07b7ec14004c1d38fd56126e6c6601da3b0609964525f0cb52be4a2714274b45ec9474d81ab718f5e74c7dedd3e3c2b99e49df2
7
- data.tar.gz: 366240a3423b5a52bd4b6c60dd587a218a54e49f7b1999d5891a328268747026dc00abbfb8e629e14b469445bff834d82b07ef136f38618af7334dacbefe5102
6
+ metadata.gz: 95384d11406759a070fd841474c0f32e42a7f34987aa0990e1ee372ed57c8c6d397372e43afa76d9610361948455d7a260883dc15147fafbaa0db72edb08c670
7
+ data.tar.gz: 8a3ac28dc9621347ce23e217cd36131c3c0b34e96ac99bb98261cd801f89d5aad23b20f7e6d18b4ac030a3521fb577d1c77568ce3ce04280f9370a816c90a90e
data/README.md CHANGED
@@ -1,49 +1,88 @@
1
- # Preload Pluck
2
-
3
- [![Build Status](https://travis-ci.org/assetricity/preload_pluck.png)](https://travis-ci.org/assetricity/preload_pluck)
4
- [![Dependency Status](https://gemnasium.com/assetricity/preload_pluck.png)](https://gemnasium.com/assetricity/preload_pluck)
5
-
6
- Adds a `preload_pluck` method to ActiveRecord that allows querying using Rails 4 eager loading-style for joined tables (`preload`), and returns a 2-dimensional array without ActiveRecord model creation overhead (`pluck`).
7
-
8
- Note: Preload Pluck may not always increase query performance - always benchmark with your own queries and production data.
9
-
10
- ## Install
11
-
12
- Add to the preload_pluck gem to your Gemfile:
13
-
14
- ```ruby
15
- gem 'preload_pluck'
16
- ```
17
-
18
- ## Usage
19
-
20
- Call `preload_pluck` after any SQL conditions (e.g. where clauses, scopes, orders, limits) have been applied and pass immediate attributes or traverse nested `belongs_to` associations.
21
-
22
- ```ruby
23
- Comment.order(:created_at).preload_pluck(:text, 'user.name')
24
- ```
25
-
26
- See `spec/preload_pluck_spec.rb` for more examples.
27
-
28
- ## Running Tests
29
-
30
- SQLite must be installed before running tests.
31
-
32
- To run tests:
33
-
34
- ```
35
- bundle install
36
- rspec spec
37
- ```
38
-
39
- By default, performance tests are disabled as it takes several minutes to insert data. To run performance tests:
40
-
41
- ```
42
- rspec spec --tag performance
43
- ```
44
-
45
- ## License
46
-
47
- Copyright [Assetricity, LLC](http://assetricity.com)
48
-
49
- Preload Pluck is released under the MIT License. See [LICENSE](https://github.com/assetricity/preload_pluck/blob/master/LICENSE) for details.
1
+ # Preload Pluck
2
+
3
+ [![Build Status](https://travis-ci.org/assetricity/preload_pluck.png)](https://travis-ci.org/assetricity/preload_pluck)
4
+ [![Coverage Status](https://coveralls.io/repos/assetricity/preload_pluck/badge.png?branch=master)](https://coveralls.io/r/assetricity/preload_pluck?branch=master)
5
+ [![Dependency Status](https://gemnasium.com/assetricity/preload_pluck.png)](https://gemnasium.com/assetricity/preload_pluck)
6
+
7
+ Adds a `preload_pluck` method to ActiveRecord that allows querying using Rails 4 eager loading-style for joined tables (`preload`), and returns a 2-dimensional array without ActiveRecord model creation overhead (`pluck`).
8
+
9
+ The typical use case is for querying and displaying tabular data, such as on an index page, without any further manipulation needed by the involved ActiveRecord models. Data may originate from immediate attributes on the current model or from attributes from other models associated via `belongs_to`.
10
+
11
+ Note: Preload Pluck may not always increase query performance - always benchmark with your own queries and production data.
12
+
13
+ ## Install
14
+
15
+ Add to the preload_pluck gem to your Gemfile:
16
+
17
+ ```ruby
18
+ gem 'preload_pluck'
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Call `preload_pluck` on an ActiveRecord model:
24
+
25
+ ```ruby
26
+ Comment.order(:created_at).preload_pluck(:text, 'user.name')
27
+ ```
28
+
29
+ This will return a 2-dimensional array where columns correspond to the passed arguments:
30
+
31
+ ```ruby
32
+ [
33
+ ['That was an interesting post', 'Alice']
34
+ ['I thought so too', 'Bob']
35
+ ]
36
+ ```
37
+
38
+ Attributes on the current model can be supplied by name:
39
+
40
+ ```ruby
41
+ Comment.preload_pluck(:title, :text)
42
+ ```
43
+
44
+ Nested attributes should be separated by a period:
45
+
46
+ ```ruby
47
+ Comment.preload_pluck('post.title', 'post.text')
48
+ ```
49
+
50
+ Both immediate and nested attributes can be mixed:
51
+
52
+ ```ruby
53
+ Comment.preload_pluck(:title, :text, 'post.title', 'post.text')
54
+ ```
55
+
56
+ Any SQL conditions (e.g. where clauses, scopes, orders, limits) should be set before `preload_pluck` is called:
57
+
58
+ ```ruby
59
+ Comment.order(:created_at)
60
+ .joins(:user)
61
+ .where(user: {name: 'Alice'))
62
+ .preload_pluck(:title, :text, 'post.title', 'post.text')
63
+ ```
64
+
65
+ See `spec/preload_pluck_spec.rb` for more examples.
66
+
67
+ ## Running Tests
68
+
69
+ SQLite must be installed before running tests.
70
+
71
+ To run tests:
72
+
73
+ ```
74
+ bundle
75
+ rspec
76
+ ```
77
+
78
+ By default, performance tests are disabled as it takes several minutes to insert data. To run performance tests:
79
+
80
+ ```
81
+ rspec --tag performance
82
+ ```
83
+
84
+ ## License
85
+
86
+ Copyright [Assetricity, LLC](http://assetricity.com)
87
+
88
+ Preload Pluck is released under the MIT License. See [LICENSE](https://github.com/assetricity/preload_pluck/blob/master/LICENSE) for details.
@@ -1,3 +1,3 @@
1
1
  module PreloadPluck
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/preload_pluck.rb CHANGED
@@ -1,118 +1,147 @@
1
- require 'preload_pluck/version'
2
- require 'active_record'
3
-
4
- module PreloadPluck
5
- class Field < Struct.new(:base_class, :path)
6
- def nested?(level)
7
- level + 2 <= path.length
8
- end
9
-
10
- def level?(level)
11
- !!path[level]
12
- end
13
-
14
- def path_upto(level)
15
- path[0..level].join('.')
16
- end
17
-
18
- def assoc(level)
19
- return nil if level >= path.length
20
- current_class = base_class
21
- assoc = nil
22
- (level + 1).times do |l|
23
- assoc = current_class.reflect_on_association(path[l])
24
- raise 'preload_pluck only supports belongs_to associations' unless assoc.macro == :belongs_to
25
- current_class = assoc.class_name.constantize
26
- end
27
- assoc
28
- end
29
- end
30
-
31
- def preload_pluck(*args)
32
- fields = args.map {|arg| Field.new(self, arg.to_s.split('.'))}
33
-
34
- plucked_cols = fields.map do |field|
35
- if field.nested?(0)
36
- field.assoc(0).foreign_key
37
- else
38
- field.path.last
39
- end
40
- end.uniq
41
- data = pluck(*plucked_cols)
42
- if fields.length == 1
43
- # Pluck returns a flat array if only one value, so use a consistent structure if there is one or multiple fields
44
- data.map! {|val| [val]}
45
- end
46
- data = __preload_pluck_to_attrs(data, plucked_cols)
47
-
48
- # A cache of records that we populate then join to later based on foreign keys
49
- joined_data = {}
50
-
51
- # Incrementally process nested fields by level
52
- max_level = fields.map {|f| f.path.length - 1}.max
53
- max_level.times do |level|
54
- fields.select {|f| f.nested?(level)}
55
- .group_by {|f| f.path_upto(level)}
56
- .each do |current_path, group|
57
- # Just use the first item - could use any item in the group
58
- assoc = group.first.assoc(level)
59
- klass = assoc.class_name.constantize
60
-
61
- # List of ids that are related to the previous objects (the IN clause in SQL preload statement)
62
- if level == 0 # Level 0 is different as data is stored in a different structure
63
- collection = data
64
- else
65
- prev_path = group.first.path_upto(level - 1)
66
- collection = joined_data[prev_path].values
67
- end
68
- ids = collection.map {|a| a[assoc.foreign_key]}.uniq
69
-
70
- # Select id and other fields at the next level
71
- cols = group.map do |f|
72
- if f.nested?(level + 1)
73
- f.assoc(level + 1).foreign_key
74
- else
75
- f.path[level + 1]
76
- end
77
- end.uniq
78
- joined_plucked_cols = [klass.primary_key, *cols]
79
- joined = klass.where(klass.primary_key => ids).pluck(*joined_plucked_cols)
80
- attrs = __preload_pluck_to_attrs(joined, joined_plucked_cols)
81
-
82
- # Index to quickly search on id
83
- joined_data[current_path] = attrs.index_by {|a| a[klass.primary_key]}
84
- end
85
- end
86
-
87
- data.map do |attr|
88
- fields.map do |field|
89
- if field.nested?(0)
90
- assoc = field.assoc(0)
91
- val = attr[assoc.foreign_key]
92
- (field.path.length - 1).times do |level|
93
- current_path = field.path_upto(level)
94
- if field.nested?(level + 1)
95
- col = field.assoc(level + 1).foreign_key
96
- else
97
- col = field.path.last
98
- end
99
- val = joined_data[current_path][val]
100
- val = val[col] if val
101
- end
102
- val
103
- else
104
- attr[field.path.last]
105
- end
106
- end
107
- end
108
- end
109
-
110
- def __preload_pluck_to_attrs(array, column_names)
111
- array.map do |item|
112
- pairs = item.map.with_index {|element, index| [column_names[index], element]}.flatten
113
- Hash[*pairs]
114
- end
115
- end
116
- end
117
-
118
- ActiveRecord::Base.extend(PreloadPluck)
1
+ require 'preload_pluck/version'
2
+ require 'active_record'
3
+
4
+ module PreloadPluck
5
+ class Field < Struct.new(:base_class, :path)
6
+ def nested?(level)
7
+ level + 2 <= path.length
8
+ end
9
+
10
+ def level?(level)
11
+ !!path[level]
12
+ end
13
+
14
+ def path_upto(level)
15
+ path[0..level].join('.')
16
+ end
17
+
18
+ def assoc(level)
19
+ return nil if level >= path.length
20
+ current_class = base_class
21
+ assoc = nil
22
+ (level + 1).times do |l|
23
+ assoc = current_class.reflect_on_association(path[l])
24
+ raise 'preload_pluck only supports belongs_to associations' unless assoc.macro == :belongs_to
25
+ current_class = assoc.class_name.constantize
26
+ end
27
+ assoc
28
+ end
29
+ end
30
+
31
+ # Return a 2-dimensional array of values where columns correspond to supplied arguments. Data from associations is
32
+ # eager loaded.
33
+ #
34
+ # Attributes on the current model can be supplied by name:
35
+ #
36
+ # Comment.preload_pluck(:title, :text)
37
+ #
38
+ # Nested attributes should be separated by a period:
39
+ #
40
+ # Comment.preload_pluck('post.title', 'post.text')
41
+ #
42
+ # Both immediate and nested attributes can be mixed:
43
+ #
44
+ # Comment.preload_pluck(:title, :text, 'post.title', 'post.text')
45
+ #
46
+ # Any SQL conditions should be set before `preload_pluck` is called:
47
+ #
48
+ # Comment.order(:created_at)
49
+ # .joins(:user)
50
+ # .where(user: {name: 'Alice'))
51
+ # .preload_pluck(:title, :text, 'post.title', 'post.text')
52
+ #
53
+ # @param args [Array<Symbol or String>] list of immediate and/or nested model attributes.
54
+ # @return [Array<Array>] 2-dimensional array where columns correspond to supplied arguments.
55
+ def preload_pluck(*args)
56
+ fields = args.map {|arg| Field.new(self, arg.to_s.split('.'))}
57
+
58
+ plucked_cols = fields.map do |field|
59
+ if field.nested?(0)
60
+ field.assoc(0).foreign_key
61
+ else
62
+ field.path.last
63
+ end
64
+ end.uniq
65
+ data = pluck(*plucked_cols)
66
+ if fields.length == 1
67
+ # Pluck returns a flat array if only one value, so use a consistent structure if there is one or multiple fields
68
+ data.map! {|val| [val]}
69
+ end
70
+ data_headers = __preload_pluck_to_header_lookup(plucked_cols)
71
+
72
+ # A cache of records that we populate then join to later based on foreign keys
73
+ nested_data = {}
74
+
75
+ # Incrementally process nested fields by level
76
+ max_level = fields.map {|f| f.path.length - 1}.max
77
+ max_level.times do |level|
78
+ fields.select {|f| f.nested?(level)}
79
+ .group_by {|f| f.path_upto(level)}
80
+ .each do |current_path, group|
81
+ # Just use the first item - could use any item in the group
82
+ assoc = group.first.assoc(level)
83
+ klass = assoc.class_name.constantize
84
+
85
+ # List of ids that are related to the previous objects (the IN clause in SQL preload statement)
86
+ if level == 0 # Level 0 is different as data is stored in a different structure
87
+ index = data_headers[assoc.foreign_key]
88
+ collection = data
89
+ else
90
+ prev_path = group.first.path_upto(level - 1)
91
+ index = nested_data[prev_path][:header][assoc.foreign_key]
92
+ collection = nested_data[prev_path][:data].values
93
+ end
94
+ ids = collection.map {|d| d[index]}.uniq
95
+
96
+ # Select id and other fields at the next level
97
+ cols = group.map do |f|
98
+ if f.nested?(level + 1)
99
+ f.assoc(level + 1).foreign_key
100
+ else
101
+ f.path[level + 1]
102
+ end
103
+ end.uniq
104
+ plucked_cols = [klass.primary_key, *cols]
105
+ indexed_data = klass.where(klass.primary_key => ids)
106
+ .pluck(*plucked_cols)
107
+ .index_by {|d| d[0]} # Index to quickly search on id
108
+ nested_data[current_path] = {
109
+ header: __preload_pluck_to_header_lookup(plucked_cols),
110
+ data: indexed_data
111
+ }
112
+ end
113
+ end
114
+
115
+ data.map do |attr|
116
+ fields.map do |field|
117
+ if field.nested?(0)
118
+ assoc = field.assoc(0)
119
+ val = attr[data_headers[assoc.foreign_key]]
120
+ (field.path.length - 1).times do |level|
121
+ current_path = field.path_upto(level)
122
+ if field.nested?(level + 1)
123
+ col = field.assoc(level + 1).foreign_key
124
+ else
125
+ col = field.path.last
126
+ end
127
+ current_data = nested_data[current_path]
128
+ current_row = current_data[:data][val]
129
+ if current_row
130
+ index = current_data[:header][col]
131
+ val = current_row[index]
132
+ end
133
+ end
134
+ val
135
+ else
136
+ attr[data_headers[field.path.last]]
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ def __preload_pluck_to_header_lookup(array)
143
+ Hash[array.map.with_index {|x, i| [x, i]}]
144
+ end
145
+ end
146
+
147
+ ActiveRecord::Base.extend(PreloadPluck)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: preload_pluck
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Assetricity
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ~>
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.7.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: database_cleaner
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 1.4.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 1.4.0
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: factory_girl
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -94,20 +108,6 @@ dependencies:
94
108
  - - ~>
95
109
  - !ruby/object:Gem::Version
96
110
  version: 1.3.10
97
- - !ruby/object:Gem::Dependency
98
- name: database_cleaner
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ~>
102
- - !ruby/object:Gem::Version
103
- version: 1.4.0
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ~>
109
- - !ruby/object:Gem::Version
110
- version: 1.4.0
111
111
  description: Adds a preload_pluck method to ActiveRecord that allows querying using
112
112
  Rails 4 preload-style eager loading, and return a 2-dimensional array without ActiveRecord
113
113
  model creation overhead.