pluck_map 0.6.2 → 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: 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