pluck_map 0.6.2 → 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: d6713a4fb63be352fb7c8d54bbbdb7a559fc4b523093f3cff0a72b9507803a8e
4
- data.tar.gz: e7f9e9caea6745daeb1beab73c711fd87656309fd3221422ad027072d6fcd8d7
3
+ metadata.gz: e5a889fad3cf9ac653fb2140beb4b9e11c3a74686564dbdac7cee79d0c0cb0f6
4
+ data.tar.gz: 7d544995a42e94eae52ad59ecb6357abedcac9396a26fb801ed4998abdc48445
5
5
  SHA512:
6
- metadata.gz: ca03d1e1f7e82934ee018c93ac980b7f35c8d9805782de196cb1941b36992880ef19f193d8d9055ac03a3999cf58168503e65ccd74fd10a59d806178c2d8950f
7
- data.tar.gz: 48822bd995380e8a97e5166c94f890b010823b55227884c66f06e6d4d36b6baf22c9b577aa3acf9670d2cca186c4dac3ccb6ee165b8c27e7861f89d31c4ba5ce
6
+ metadata.gz: 83c0063cd6bbac89d8fa4b9d4efab07c43f20dc6a5d0a7216e04b4c127d9e93175e737a0ea37b929f318641255935992ac7160e650fa7675d4a9d9780a150003
7
+ data.tar.gz: 6a9a3780212e568dd7b5ab56dadb79914e3bebc956fb6d591a054040f9c1819997d6c2a706d1ea7936d62a6a48eefb5b8d85b9335d20f15deeab1940b56784cc
@@ -1,9 +1,13 @@
1
1
  language: ruby
2
2
  rvm: 2.5.5
3
3
 
4
- # Rails 6 requires Postgres 9.3+
4
+ # we need MySQL 5.7+ to support JSON aggregation
5
+ dist: xenial
6
+
5
7
  addons:
6
- postgresql: "9.3"
8
+ # Rails 6 requires Postgres 9.3+
9
+ # We need Postgres 9.4+ to support JSON aggregation
10
+ postgresql: "9.4"
7
11
 
8
12
  services:
9
13
  - postgresql
@@ -1,6 +1,8 @@
1
- ## v0.6.2 (2019 Jun 13)
1
+ ## v1.0.0.rc1 (2019 May 12)
2
2
 
3
- * FIX: Allow presenting a subclass of the presenter's model (@kobsy)
3
+ * BREAKING: Remove deprecated features/methods (@boblail)
4
+ * FEATURE: Optimize `to_json` when a presenter doesn't need to process values in Ruby (@boblail)
5
+ * FEATURE: Add `has_many` and `has_one` DSL for presenting nested resources (@boblail)
4
6
 
5
7
  ## v0.6.1 (2019 May 12)
6
8
 
data/README.md CHANGED
@@ -8,13 +8,14 @@ This library provides a DSL for presenting ActiveRecord::Relations without insta
8
8
 
9
9
  ### Table of Contents
10
10
 
11
- - [Why PluckMap?](https://github.com/boblail/pluck_map#why-pluckmap)
11
+ - [Why PluckMap?](#why-pluckmap)
12
12
  - Usage
13
- - [Defining attributes to present](https://github.com/boblail/pluck_map#defining-attributes-to-present)
14
- - [Presenting Records](https://github.com/boblail/pluck_map#presenting-records)
15
- - [Installation](https://github.com/boblail/pluck_map#installation)
16
- - [Requirements](https://github.com/boblail/pluck_map#requirements)
17
- - [Development & Contributing](https://github.com/boblail/pluck_map#development)
13
+ - [Defining attributes to present](#defining-attributes-to-present)
14
+ - [Relationships](#relationships)
15
+ - [Presenting Records](#presenting-records)
16
+ - [Installation](#installation)
17
+ - [Requirements](#requirements)
18
+ - [Development & Contributing](#development)
18
19
 
19
20
 
20
21
  ## Why PluckMap?
@@ -207,6 +208,80 @@ presenter = PluckMap[Person].define do
207
208
  end
208
209
  ```
209
210
 
211
+ ### Relationships
212
+
213
+ PluckMap can also describe nested data. There are two special methods in the `define` block that introduce child resources:
214
+
215
+ 1. `has_one` will treat the resource as a nested object or null
216
+ 2. `has_many` will treat the resource as an array of nested objects (which may be empty)
217
+
218
+ The first argument to either of these methods is the name of an association on the presented model.
219
+
220
+ You can use either of these methods with any kind of ActiveRecord relation (`belongs_to`, `has_one`, `has_many`, `has_and_belongs_to_many`), although it generally makes more sense to use `has_one` with Rails' singular associations and `has_many` with Rails' plural associations.
221
+
222
+ #### `has_one`
223
+
224
+ In the example below, assume
225
+
226
+ ```ruby
227
+ class Book < ActiveRecord::Base
228
+ belongs_to :author
229
+ end
230
+ ```
231
+
232
+ This presenter :point_down: selects the title of every book as well as its author's name:
233
+
234
+ ```ruby
235
+ presenter = PluckMap[Book].define do
236
+ title
237
+ has_one :author do
238
+ name
239
+ end
240
+ end
241
+ ```
242
+
243
+ (We can also write it using block variables, if that's easier to read.)
244
+
245
+ ```ruby
246
+ presenter = PluckMap[Book].define do |book|
247
+ book.title
248
+ book.has_one :author do |author|
249
+ author.name
250
+ end
251
+ end
252
+ ```
253
+
254
+ Attributes defined for a relationship support all the same features as [attributes defined at the root level](#defining-attributes-to-present).
255
+
256
+
257
+ #### `has_many`
258
+
259
+ We can present the reverse of the above example with `has_many`. This example will select a list of authors and, for each, a list of the books they wrote:
260
+
261
+ ```ruby
262
+ presenter = PluckMap[Author].define do
263
+ name
264
+ has_many :books do
265
+ title
266
+ end
267
+ end
268
+ ```
269
+
270
+ #### scopes
271
+
272
+ An optional second argument to both `has_one` and `has_many` is a scope block that you can use to modify the query that would select the associated records. You can use any of ActiveRecord's standard [querying methods](https://guides.rubyonrails.org/active_record_querying.html) inside the scope block.
273
+
274
+ In this example, we've altered our last presenter to ensure that books are listed alphabetically:
275
+
276
+ ```ruby
277
+ presenter = PluckMap[Author].define do
278
+ name
279
+ has_many :books, -> { order(title: :asc) } do
280
+ title
281
+ end
282
+ end
283
+ ```
284
+
210
285
 
211
286
  ### Presenting Records
212
287
 
@@ -299,9 +374,11 @@ The gem's only runtime requirement is:
299
374
 
300
375
  It supports these databases out of the box:
301
376
 
302
- - PostgreSQL
303
- - MySQL
304
- - SQLite
377
+ - PostgreSQL 9.4+
378
+ - MySQL 5.7.22+
379
+ - SQLite 3.10.0+
380
+
381
+ (Note: the versions given above are when certain JSON aggregate functions were introduced in each supported database. `PluckMap`'s core behavior will work with earlier versions of the database above but certain features like optimizations to `to_json` and relationships require the specified versions.)
305
382
 
306
383
 
307
384
 
@@ -0,0 +1,102 @@
1
+ require "active_record"
2
+
3
+ # ActiveRecord::Associations::AssociationScope assumes that values
4
+ # for Owner's fields will be concrete values that need to be type-cast.
5
+ #
6
+ # But our AbstractOwner returns field references (Arel::Attributes::Attribute)
7
+ # and we need them to bypass type-casting.
8
+ #
9
+ module PluckMap
10
+ module AssociationScope
11
+ def self.[](version)
12
+ const_get "Rails#{version.to_s.delete(".")}"
13
+ end
14
+
15
+ def self.create
16
+ case ActiveRecord.version.segments.take(2)
17
+ when [4,2] then self[4.2].create
18
+ when [5,0] then self[5.0].create
19
+ else self::Current.create
20
+ end
21
+ end
22
+
23
+
24
+ # Rails 5.1+
25
+ class Current < ActiveRecord::Associations::AssociationScope
26
+ def apply_scope(scope, table, key, value)
27
+ if value.is_a?(Arel::Attributes::Attribute)
28
+ scope.where!(table[key].eq(value))
29
+ else
30
+ super
31
+ end
32
+ end
33
+
34
+ def scope(association)
35
+ if ActiveRecord.version.version < "5.2"
36
+ super(association, association.reflection.active_record.connection)
37
+ else
38
+ super
39
+ end
40
+ end
41
+ end
42
+
43
+
44
+ # In Rails 5.0, `apply_scope` isn't extracted from `last_chain_scope`
45
+ # and `next_chain_scope` so we have to override the entire methods to
46
+ # extract `apply_scope` and bypass type-casting.
47
+ #
48
+ # Refer to https://github.com/rails/rails/blob/v5.0.7.2/activerecord/lib/active_record/associations/association_scope.rb#L61-L94
49
+ #
50
+ class Rails50 < Current
51
+ def last_chain_scope(scope, table, reflection, owner, association_klass)
52
+ join_keys = reflection.join_keys(association_klass)
53
+ key = join_keys.key
54
+ foreign_key = join_keys.foreign_key
55
+
56
+ value = transform_value(owner[foreign_key])
57
+ scope = apply_scope(scope, table, key, value)
58
+
59
+ if reflection.type
60
+ polymorphic_type = transform_value(owner.class.base_class.name)
61
+ scope = scope.where(table.name => { reflection.type => polymorphic_type })
62
+ end
63
+
64
+ scope
65
+ end
66
+
67
+ def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection)
68
+ join_keys = reflection.join_keys(association_klass)
69
+ key = join_keys.key
70
+ foreign_key = join_keys.foreign_key
71
+
72
+ constraint = table[key].eq(foreign_table[foreign_key])
73
+
74
+ if reflection.type
75
+ value = transform_value(next_reflection.klass.base_class.name)
76
+ scope = apply_scope(scope, table, reflection.type, value)
77
+ end
78
+
79
+ scope = scope.joins(join(foreign_table, constraint))
80
+ end
81
+
82
+ def apply_scope(scope, table, key, value)
83
+ if value.is_a?(Arel::Attributes::Attribute)
84
+ scope.where(table[key].eq(value))
85
+ else
86
+ scope.where(table.name => { key => value })
87
+ end
88
+ end
89
+ end
90
+
91
+
92
+ class Rails42 < Current
93
+ def bind(_, _, _, value, _)
94
+ if value.is_a?(Arel::Attributes::Attribute)
95
+ value
96
+ else
97
+ super
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -14,16 +14,14 @@ module PluckMap
14
14
  @value = options[:value]
15
15
  @selects = []
16
16
  else
17
- raise ArgumentError, "You must select at least one column" if selects.empty?
17
+ raise ArgumentError, "You must select at least one column" if @selects.empty?
18
18
  raise ArgumentError, "You must define a block if you are going to select " <<
19
- "more than one expression from the database" if selects.length > 1 && !block
19
+ "more than one expression from the database" if @selects.length > 1 && !@block
20
20
 
21
- @selects = @selects.map do |select|
21
+ @selects.each do |select|
22
22
  if select.is_a?(String) && !select.is_a?(Arel::Nodes::SqlLiteral)
23
- puts "DEPRECATION WARNING: Passing raw SQL as a String to :select is deprecated. Known-safe values can be passed by wrapping them in Arel.sql()."
24
- Arel.sql(select)
25
- else
26
- select
23
+ raise ArgumentError, "#{select.inspect} is not a valid value for :select. " <<
24
+ "If a string of raw SQL is safe, wrap it in Arel.sql()."
27
25
  end
28
26
  end
29
27
  end
@@ -33,10 +31,21 @@ module PluckMap
33
31
  block.call(*object)
34
32
  end
35
33
 
34
+ def value?
35
+ defined?(@value)
36
+ end
37
+
36
38
  def will_map?
37
39
  !block.nil?
38
40
  end
39
41
 
42
+ def nested?
43
+ false
44
+ end
45
+
46
+ def preload!(results)
47
+ end
48
+
40
49
  # When the PluckMapPresenter performs the query, it will
41
50
  # receive an array of rows. Each row will itself be an
42
51
  # array of values.
@@ -44,18 +53,22 @@ module PluckMap
44
53
  # This method constructs a Ruby expression that will
45
54
  # extract the appropriate values from each row that
46
55
  # correspond to this Attribute.
47
- def to_ruby(selects = nil)
48
- if selects
49
- puts "DEPRECATION WARNING: PluckMap::Attribute#to_ruby no longer requires an argument. Replace `attribute.to_ruby(keys)` with `attribute.to_ruby`."
50
- end
51
-
52
- return @value.inspect if defined?(@value)
56
+ def to_ruby
57
+ return @value.inspect if value?
53
58
  return "values[#{indexes[0]}]" if indexes.length == 1 && !block
54
59
  ruby = "values.values_at(#{indexes.join(", ")})"
55
60
  ruby = "invoke(:\"#{id}\", #{ruby})" if block
56
61
  ruby
57
62
  end
58
63
 
64
+ def exec(values)
65
+ return @value if value?
66
+ return values[indexes[0]] if indexes.length == 1 && !block
67
+ _values = values.values_at(*indexes)
68
+ _values = apply(_values) if block
69
+ _values
70
+ end
71
+
59
72
 
60
73
 
61
74
  def values
@@ -1,5 +1,6 @@
1
1
  require "pluck_map/attribute"
2
2
  require "pluck_map/attributes"
3
+ require "pluck_map/relationships"
3
4
 
4
5
  module PluckMap
5
6
  class AttributeBuilder < BasicObject
@@ -12,7 +13,7 @@ module PluckMap
12
13
  else
13
14
  builder.instance_eval(&block)
14
15
  end
15
- Attributes.new(attributes)
16
+ Attributes.new(attributes, model)
16
17
  end
17
18
 
18
19
  def initialize(attributes, model)
@@ -27,5 +28,19 @@ module PluckMap
27
28
  :attribute_added
28
29
  end
29
30
 
31
+ def has_many(name, *args, &block)
32
+ options = args.extract_options!
33
+ options[:scope_block] = args.first unless args.empty?
34
+ @attributes.push Relationships.many(@model, name, block, options)
35
+ :relationship_added
36
+ end
37
+
38
+ def has_one(name, *args, &block)
39
+ options = args.extract_options!
40
+ options[:scope_block] = args.first unless args.empty?
41
+ @attributes.push Relationships.one(@model, name, block, options)
42
+ :relationship_added
43
+ end
44
+
30
45
  end
31
46
  end
@@ -2,9 +2,10 @@ module PluckMap
2
2
  class Attributes
3
3
  include Enumerable
4
4
 
5
- attr_reader :selects
5
+ attr_reader :selects, :model
6
6
 
7
- def initialize(attributes)
7
+ def initialize(attributes, model)
8
+ @model = model
8
9
  @_attributes = attributes.freeze
9
10
  @_attributes_by_id = {}
10
11
  @selects = []
@@ -40,12 +41,23 @@ module PluckMap
40
41
  _attributes_by_id
41
42
  end
42
43
 
44
+ def to_json_array
45
+ PluckMap::BuildJsonArray.new(*selects.map do |select|
46
+ select = model.arel_table[select] if select.is_a?(Symbol)
47
+ select
48
+ end)
49
+ end
50
+
43
51
 
44
52
 
45
53
  def will_map?
46
54
  _attributes.any?(&:will_map?)
47
55
  end
48
56
 
57
+ def nested?
58
+ _attributes.any?(&:nested?)
59
+ end
60
+
49
61
 
50
62
 
51
63
  def ==(other)
@@ -0,0 +1,4 @@
1
+ module PluckMap
2
+ class UnsupportedAttributeError < ArgumentError
3
+ end
4
+ end
@@ -0,0 +1,40 @@
1
+ require "arel"
2
+
3
+ module PluckMap
4
+ class BuildJsonObject < Arel::Nodes::Node
5
+ include Arel::AliasPredication
6
+
7
+ attr_reader :args
8
+
9
+ def initialize(*args)
10
+ @args = args
11
+ end
12
+ end
13
+
14
+ class BuildJsonArray < Arel::Nodes::Node
15
+ include Arel::AliasPredication
16
+
17
+ attr_reader :args
18
+
19
+ def initialize(*args)
20
+ @args = args
21
+ end
22
+ end
23
+
24
+ class JsonArrayAggregate < Arel::Nodes::Node
25
+ attr_reader :arg
26
+
27
+ def initialize(arg)
28
+ @arg = arg
29
+ end
30
+ end
31
+
32
+ class JsonSubqueryAggregate < Arel::Nodes::Node
33
+ attr_reader :scope, :select
34
+
35
+ def initialize(scope, select)
36
+ @scope = scope
37
+ @select = select
38
+ end
39
+ end
40
+ end
@@ -1,5 +1,8 @@
1
1
  require "pluck_map/attribute_builder"
2
+ require "pluck_map/errors"
3
+ require "pluck_map/nodes"
2
4
  require "pluck_map/presenters"
5
+ require "pluck_map/visitors"
3
6
  require "active_record"
4
7
 
5
8
  module PluckMap
@@ -8,38 +11,24 @@ module PluckMap
8
11
 
9
12
  attr_reader :model, :attributes
10
13
 
11
- def initialize(model = nil, attributes = nil, &block)
12
- if block_given?
13
- puts "DEPRECATION WARNING: `PluckMap::Presenter.new` will be deprecated. Use `PluckMap[Model].define` instead."
14
- @attributes = PluckMap::AttributeBuilder.build(model: nil, &block)
15
- else
16
- @model = model
17
- @attributes = attributes
18
- end
19
-
20
- if respond_to?(:define_presenters!, true)
21
- puts "DEPRECATION WARNING: `define_presenters!` is deprecated; instead mix in a module that implements your presenter method (e.g. `to_h`). Optionally have the method redefine itself the first time it is called."
22
- # because overridden `define_presenters!` will probably call `super`
23
- PluckMap::Presenter.class_eval 'protected def define_presenters!; end'
24
- define_presenters!
25
- end
26
- end
27
-
28
- def no_map?
29
- puts "DEPRECATION WARNING: `PluckMap::Presenter#no_map?` is deprecated. You can replace it with `!attributes.will_map?`"
30
- !attributes.will_map?
14
+ def initialize(model, attributes)
15
+ @model = model
16
+ @attributes = attributes
31
17
  end
32
18
 
33
19
  protected
34
20
 
35
21
  def pluck(query)
36
- unless model.nil? || query.model <= model
22
+ if query.model != model
37
23
  raise ArgumentError, "Query for #{query.model} but #{model} expected"
38
24
  end
39
25
 
40
26
  # puts "\e[95m#{query.select(*selects).to_sql}\e[0m"
41
27
  results = benchmark("pluck(#{query.table_name})") { query.pluck(*selects) }
42
28
  return results unless block_given?
29
+ attributes.each do |attribute|
30
+ attribute.preload!(results)
31
+ end
43
32
  benchmark("map(#{query.table_name})") { yield results }
44
33
  end
45
34
 
@@ -89,10 +78,5 @@ module PluckMap
89
78
  attributes.by_id
90
79
  end
91
80
 
92
- def keys
93
- puts "DEPRECATION WARNING: PluckMap::Presenter#keys is deprecated; use #selects instead"
94
- selects
95
- end
96
-
97
81
  end
98
82
  end
@@ -1,7 +1,13 @@
1
+ require "pluck_map/errors"
2
+
1
3
  module PluckMap
2
4
  module CsvPresenter
3
5
 
4
6
  def to_csv(query)
7
+ if attributes.nested?
8
+ raise PluckMap::UnsupportedAttributeError, "to_csv can not be used to present nested attributes"
9
+ end
10
+
5
11
  define_to_csv!
6
12
  to_csv(query)
7
13
  end
@@ -4,11 +4,77 @@ module PluckMap
4
4
  module JsonPresenter
5
5
 
6
6
  def to_json(query, json: default_json, **)
7
- json.dump(to_h(query))
7
+ if attributes.will_map?
8
+ to_json__default(query, json: json)
9
+ else
10
+ to_json__optimized(query)
11
+ end
8
12
  end
9
13
 
10
14
  private
11
15
 
16
+ def to_json__default(query, json: default_json, **)
17
+ json.dump(to_h(query))
18
+ end
19
+
20
+ def to_json__optimized(query, **)
21
+ define_to_json__optimized!
22
+ to_json__optimized(query)
23
+ end
24
+
25
+ def define_to_json__optimized!
26
+ sql = compile(to_json_object(attributes).as("object"))
27
+
28
+ ruby = <<-RUBY
29
+ private def to_json__optimized(query, **)
30
+ sql = wrap_aggregate(query.select(Arel.sql(#{sql.inspect})))
31
+ query.connection.select_value(sql)
32
+ end
33
+ RUBY
34
+ # puts "\e[34m#{ruby}\e[0m" # <-- helps debugging PluckMapPresenter
35
+ class_eval ruby, __FILE__, __LINE__ - ruby.length
36
+ end
37
+
38
+ def to_json_object(attributes)
39
+ args = []
40
+ attributes.each do |attribute|
41
+ args << Arel::Nodes::Quoted.new(attribute.name)
42
+ args << send(:"prepare_#{attribute.class.name.gsub("::", "_")}", attribute)
43
+ end
44
+ PluckMap::BuildJsonObject.new(*args)
45
+ end
46
+
47
+ def prepare_PluckMap_Attribute(attribute)
48
+ return Arel::Nodes::Quoted.new(attribute.value) if attribute.value?
49
+ arg = attribute.selects[0]
50
+ arg = attribute.model.arel_table[arg] if arg.is_a?(Symbol)
51
+ arg
52
+ end
53
+
54
+ def prepare_PluckMap_Relationships_Many(attribute)
55
+ PluckMap::JsonSubqueryAggregate.new(attribute.scope, to_json_object(attribute.attributes))
56
+ end
57
+
58
+ def prepare_PluckMap_Relationships_One(attribute)
59
+ Arel.sql("(#{attribute.scope.select(to_json_object(attribute.attributes)).to_sql})")
60
+ end
61
+
62
+ def wrap_aggregate(subquery)
63
+ "SELECT #{compile(aggregate(Arel.sql("d.object")))} FROM (#{subquery.to_sql}) AS d"
64
+ end
65
+
66
+ def aggregate(object)
67
+ PluckMap::JsonArrayAggregate.new(object)
68
+ end
69
+
70
+ def compile(node)
71
+ visitor.compile(node)
72
+ end
73
+
74
+ def visitor
75
+ model.connection.visitor
76
+ end
77
+
12
78
  def default_json
13
79
  if defined?(MultiJson)
14
80
  MultiJson
@@ -0,0 +1,85 @@
1
+ require "pluck_map/association_scope"
2
+ require "pluck_map/relationships/base"
3
+ require "pluck_map/relationships/many"
4
+ require "pluck_map/relationships/one"
5
+ require "pluck_map/relationships/polymorphic_one"
6
+
7
+ module PluckMap
8
+ module Relationships
9
+ class << self
10
+
11
+ def one(model, name, block, options)
12
+ reflection = reflection_for(model, name)
13
+ if reflection.polymorphic?
14
+ Relationships::PolymorphicOne.new(name, reflection, block, options)
15
+ else
16
+ Relationships::One.new(name, scope_for_reflection(reflection), block, options)
17
+ end
18
+ end
19
+
20
+ def many(model, name, block, options)
21
+ Relationships::Many.new(name, scope(model, name), block, options)
22
+ end
23
+
24
+ private
25
+
26
+ def scope(model, name)
27
+ scope_for_reflection(reflection_for(model, name))
28
+ end
29
+
30
+ def scope_for_reflection(reflection)
31
+ scope_for(association_for(reflection))
32
+ end
33
+
34
+ def reflection_for(model, name)
35
+ # Use `_reflections.fetch(name)` instead of `reflect_on_association(name)`
36
+ # because they have different behavior when it comes to HasAndBelongsToMany
37
+ # associations.
38
+ #
39
+ # `reflect_on_association` will return a HasAndBelongsToManyReflection
40
+ # while `_reflections.fetch(name)` will return a ThroughReflection that
41
+ # wraps a HasAndBelongsToManyReflection.
42
+ #
43
+ # ActiveRecord::Associations::AssociationScope expects the latter.
44
+ #
45
+ model._reflections.fetch(name.to_s) do
46
+ raise ArgumentError, "#{name} is not an association on #{model}"
47
+ end
48
+ end
49
+
50
+ def association_for(reflection)
51
+ owner = AbstractOwner.new(reflection)
52
+ reflection.association_class.new(owner, reflection)
53
+ end
54
+
55
+ def scope_for(association)
56
+ AssociationScope.create.scope(association)
57
+ end
58
+
59
+ # ActiveRecord constructs an Association from a Reflection and an
60
+ # Owner. It expects Owner to be an instance of an ActiveRecord object
61
+ # and uses `[]` to access specific values for fields on the record.
62
+ #
63
+ # e.g. WHERE books.author_id = 7
64
+ #
65
+ # We want to create a subquery that will reference those fields
66
+ # but not their specific values.
67
+ #
68
+ # e.g. WHERE books.author_id = authors.id
69
+ #
70
+ # So we create an object that serves the purpose of Owner but returns
71
+ # appropriate selectors.
72
+ #
73
+ AbstractOwner = Struct.new(:reflection) do
74
+ def class
75
+ reflection.active_record
76
+ end
77
+
78
+ def [](value)
79
+ self.class.arel_table[value]
80
+ end
81
+ end
82
+
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,39 @@
1
+ require "pluck_map/attribute"
2
+
3
+ module PluckMap
4
+ module Relationships
5
+ class Base < Attribute
6
+ attr_reader :attributes, :scope
7
+
8
+ def initialize(attribute_name, scope, block, options)
9
+ @scope = scope
10
+ @attributes = AttributeBuilder.build(model: scope.klass, &block)
11
+ @scope = @scope.instance_exec(&options[:scope_block]) if options[:scope_block]
12
+ options = options.slice(:as).merge(
13
+ select: build_select,
14
+ map: build_map)
15
+
16
+ super(attribute_name, scope.klass, options)
17
+ end
18
+
19
+ def will_map?
20
+ attributes.any?(&:will_map?)
21
+ end
22
+
23
+ def nested?
24
+ true
25
+ end
26
+
27
+ protected
28
+
29
+ def build_select
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def build_map
34
+ raise NotImplementedError
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ require "active_record/version"
2
+ require "pluck_map/attribute"
3
+
4
+ module PluckMap
5
+ module Relationships
6
+ class Many < Base
7
+ protected
8
+
9
+ def build_select
10
+ node = PluckMap::JsonSubqueryAggregate.new(scope, attributes.to_json_array)
11
+
12
+ # On Rails 4.2, `pluck` can't accept Arel nodes
13
+ if ActiveRecord.version.segments.take(2) == [4,2]
14
+ Arel.sql(scope.connection.visitor.compile(node))
15
+ else
16
+ node
17
+ end
18
+ end
19
+
20
+ def build_map
21
+ lambda do |results|
22
+ return [] if results.nil?
23
+ results = JSON.parse(results) if results.is_a?(String)
24
+ results.map do |values|
25
+ attributes.each_with_object({}) do |attribute, hash|
26
+ hash[attribute.name] = attribute.exec(values)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ require "pluck_map/attribute"
2
+
3
+ module PluckMap
4
+ module Relationships
5
+ class One < Base
6
+ protected
7
+
8
+ def build_select
9
+ Arel.sql("(#{scope.select(attributes.to_json_array).to_sql})")
10
+ end
11
+
12
+ def build_map
13
+ lambda do |values|
14
+ return nil if values.nil?
15
+ values = JSON.parse(values) if values.is_a?(String)
16
+ attributes.each_with_object({}) do |attribute, hash|
17
+ hash[attribute.name] = attribute.exec(values)
18
+ end
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,58 @@
1
+ require "pluck_map/attribute"
2
+
3
+ module PluckMap
4
+ module Relationships
5
+ class PolymorphicOne < Attribute
6
+
7
+ def initialize(attribute_name, reflection, block, options)
8
+ @reflection = reflection
9
+ @attributes_block = block
10
+ @scope_block = options[:scope_block]
11
+
12
+ options = options.slice(:as).merge(
13
+ select: [ reflection.foreign_type.to_sym, reflection.foreign_key.to_sym ],
14
+ map: ->(*args) { @preloads[args] })
15
+
16
+ super(attribute_name, reflection.active_record, options)
17
+ end
18
+
19
+ def nested?
20
+ true
21
+ end
22
+
23
+ def preload!(results)
24
+ ids_by_type = Hash.new { |hash, key| hash[key] = [] }
25
+
26
+ results.each do |values|
27
+ type, id = values.values_at(*indexes)
28
+ ids_by_type[type].push(id)
29
+ end
30
+
31
+ @preloads = Hash.new
32
+ ids_by_type.each do |type, ids|
33
+ klass = type.constantize
34
+ scope = klass.where(id: ids)
35
+ scope = scope.instance_exec(&@scope_block) if @scope_block
36
+
37
+ presenter = PluckMap[klass].define do |q|
38
+ q.__id select: klass.primary_key.to_sym
39
+
40
+ if @attributes_block.arity == 1
41
+ @attributes_block.call(q)
42
+ else
43
+ q.instance_eval(&@attributes_block)
44
+ end
45
+ end
46
+
47
+ presenter.to_h(scope).each do |h|
48
+ id = h.delete(:__id)
49
+ @preloads[[type, id]] = h
50
+ end
51
+ end
52
+
53
+ nil
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -1,3 +1,3 @@
1
1
  module PluckMapPresenter
2
- VERSION = "0.6.2"
2
+ VERSION = "1.0.0.rc1"
3
3
  end
@@ -0,0 +1,4 @@
1
+ require "arel"
2
+ require "pluck_map/visitors/sqlite"
3
+ require "pluck_map/visitors/postgresql"
4
+ require "pluck_map/visitors/mysql"
@@ -0,0 +1,36 @@
1
+ require "arel/visitors/mysql"
2
+
3
+ module Arel
4
+ module Visitors
5
+ class MySQL
6
+ def visit_PluckMap_BuildJsonObject(o, collector)
7
+ collector << "json_object("
8
+ visit o.args, collector
9
+ collector << ")"
10
+ end
11
+
12
+ def visit_PluckMap_BuildJsonArray(o, collector)
13
+ collector << "json_array("
14
+ visit o.args, collector
15
+ collector << ")"
16
+ end
17
+
18
+ def visit_PluckMap_JsonArrayAggregate(o, collector)
19
+ collector << "json_arrayagg("
20
+ visit o.arg, collector
21
+ collector << ")"
22
+ end
23
+
24
+ def visit_PluckMap_JsonSubqueryAggregate(o, collector)
25
+ interior = compile(o.select)
26
+ if o.scope.order_values.present?
27
+ interior = "#{interior} ORDER BY #{compile(o.scope.order_values)}"
28
+ end
29
+ interior = "CAST(CONCAT('[',GROUP_CONCAT(#{interior}),']') AS JSON)"
30
+ sql = o.scope.reorder(nil).select(Arel.sql(interior)).to_sql
31
+
32
+ collector << "COALESCE((#{sql}), json_array())"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ require "arel/visitors/postgresql"
2
+
3
+ module Arel
4
+ module Visitors
5
+ class PostgreSQL
6
+ def visit_PluckMap_BuildJsonObject(o, collector)
7
+ collector << "json_build_object("
8
+ visit o.args, collector
9
+ collector << ")"
10
+ end
11
+
12
+ def visit_PluckMap_BuildJsonArray(o, collector)
13
+ collector << "json_build_array("
14
+ visit o.args, collector
15
+ collector << ")"
16
+ end
17
+
18
+ def visit_PluckMap_JsonArrayAggregate(o, collector)
19
+ collector << "json_agg("
20
+ visit o.arg, collector
21
+ collector << ")"
22
+ end
23
+
24
+ def visit_PluckMap_JsonSubqueryAggregate(o, collector)
25
+ sql = o.scope.select(o.select.as("object")).to_sql
26
+ collector << "COALESCE((SELECT json_agg(d.object) FROM (#{sql}) AS d), json_build_array())"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ require "arel/visitors/sqlite"
2
+
3
+ module Arel
4
+ module Visitors
5
+ class SQLite
6
+ def visit_PluckMap_BuildJsonObject(o, collector)
7
+ collector << "json_object("
8
+ visit o.args, collector
9
+ collector << ")"
10
+ end
11
+
12
+ def visit_PluckMap_BuildJsonArray(o, collector)
13
+ collector << "json_array("
14
+ visit o.args, collector
15
+ collector << ")"
16
+ end
17
+
18
+ def visit_PluckMap_JsonArrayAggregate(o, collector)
19
+ collector << "json_group_array(json("
20
+ visit o.arg, collector
21
+ collector << "))"
22
+ end
23
+
24
+ def visit_PluckMap_JsonSubqueryAggregate(o, collector)
25
+ sql = o.scope.select(o.select.as("object")).to_sql
26
+ collector << "COALESCE((SELECT json_group_array(json(d.object)) FROM (#{sql}) AS d), json_array())"
27
+ end
28
+ end
29
+ end
30
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pluck_map
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 1.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Lail
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-06-14 00:00:00.000000000 Z
11
+ date: 2019-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -257,17 +257,29 @@ files:
257
257
  - gemfiles/rails_5.2.gemfile
258
258
  - gemfiles/rails_edge.gemfile
259
259
  - lib/pluck_map.rb
260
+ - lib/pluck_map/association_scope.rb
260
261
  - lib/pluck_map/attribute.rb
261
262
  - lib/pluck_map/attribute_builder.rb
262
263
  - lib/pluck_map/attributes.rb
264
+ - lib/pluck_map/errors.rb
263
265
  - lib/pluck_map/model_context.rb
266
+ - lib/pluck_map/nodes.rb
264
267
  - lib/pluck_map/null_logger.rb
265
268
  - lib/pluck_map/presenter.rb
266
269
  - lib/pluck_map/presenters.rb
267
270
  - lib/pluck_map/presenters/to_csv.rb
268
271
  - lib/pluck_map/presenters/to_h.rb
269
272
  - lib/pluck_map/presenters/to_json.rb
273
+ - lib/pluck_map/relationships.rb
274
+ - lib/pluck_map/relationships/base.rb
275
+ - lib/pluck_map/relationships/many.rb
276
+ - lib/pluck_map/relationships/one.rb
277
+ - lib/pluck_map/relationships/polymorphic_one.rb
270
278
  - lib/pluck_map/version.rb
279
+ - lib/pluck_map/visitors.rb
280
+ - lib/pluck_map/visitors/mysql.rb
281
+ - lib/pluck_map/visitors/postgresql.rb
282
+ - lib/pluck_map/visitors/sqlite.rb
271
283
  - pluck_map.gemspec
272
284
  homepage: https://github.com/boblail/pluck_map
273
285
  licenses: []
@@ -283,9 +295,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
283
295
  version: '0'
284
296
  required_rubygems_version: !ruby/object:Gem::Requirement
285
297
  requirements:
286
- - - ">="
298
+ - - ">"
287
299
  - !ruby/object:Gem::Version
288
- version: '0'
300
+ version: 1.3.1
289
301
  requirements: []
290
302
  rubyforge_project:
291
303
  rubygems_version: 2.7.6