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 +4 -4
- data/.travis.yml +6 -2
- data/CHANGELOG.md +4 -2
- data/README.md +86 -9
- data/lib/pluck_map/association_scope.rb +102 -0
- data/lib/pluck_map/attribute.rb +26 -13
- data/lib/pluck_map/attribute_builder.rb +16 -1
- data/lib/pluck_map/attributes.rb +14 -2
- data/lib/pluck_map/errors.rb +4 -0
- data/lib/pluck_map/nodes.rb +40 -0
- data/lib/pluck_map/presenter.rb +10 -26
- data/lib/pluck_map/presenters/to_csv.rb +6 -0
- data/lib/pluck_map/presenters/to_json.rb +67 -1
- data/lib/pluck_map/relationships.rb +85 -0
- data/lib/pluck_map/relationships/base.rb +39 -0
- data/lib/pluck_map/relationships/many.rb +34 -0
- data/lib/pluck_map/relationships/one.rb +24 -0
- data/lib/pluck_map/relationships/polymorphic_one.rb +58 -0
- data/lib/pluck_map/version.rb +1 -1
- data/lib/pluck_map/visitors.rb +4 -0
- data/lib/pluck_map/visitors/mysql.rb +36 -0
- data/lib/pluck_map/visitors/postgresql.rb +30 -0
- data/lib/pluck_map/visitors/sqlite.rb +30 -0
- metadata +16 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e5a889fad3cf9ac653fb2140beb4b9e11c3a74686564dbdac7cee79d0c0cb0f6
|
4
|
+
data.tar.gz: 7d544995a42e94eae52ad59ecb6357abedcac9396a26fb801ed4998abdc48445
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 83c0063cd6bbac89d8fa4b9d4efab07c43f20dc6a5d0a7216e04b4c127d9e93175e737a0ea37b929f318641255935992ac7160e650fa7675d4a9d9780a150003
|
7
|
+
data.tar.gz: 6a9a3780212e568dd7b5ab56dadb79914e3bebc956fb6d591a054040f9c1819997d6c2a706d1ea7936d62a6a48eefb5b8d85b9335d20f15deeab1940b56784cc
|
data/.travis.yml
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm: 2.5.5
|
3
3
|
|
4
|
-
#
|
4
|
+
# we need MySQL 5.7+ to support JSON aggregation
|
5
|
+
dist: xenial
|
6
|
+
|
5
7
|
addons:
|
6
|
-
|
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
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
-
##
|
1
|
+
## v1.0.0.rc1 (2019 May 12)
|
2
2
|
|
3
|
-
*
|
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?](
|
11
|
+
- [Why PluckMap?](#why-pluckmap)
|
12
12
|
- Usage
|
13
|
-
- [Defining attributes to present](
|
14
|
-
- [
|
15
|
-
- [
|
16
|
-
- [
|
17
|
-
- [
|
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
|
data/lib/pluck_map/attribute.rb
CHANGED
@@ -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 &&
|
19
|
+
"more than one expression from the database" if @selects.length > 1 && !@block
|
20
20
|
|
21
|
-
@selects
|
21
|
+
@selects.each do |select|
|
22
22
|
if select.is_a?(String) && !select.is_a?(Arel::Nodes::SqlLiteral)
|
23
|
-
|
24
|
-
|
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
|
48
|
-
if
|
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
|
data/lib/pluck_map/attributes.rb
CHANGED
@@ -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,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
|
data/lib/pluck_map/presenter.rb
CHANGED
@@ -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
|
12
|
-
|
13
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/pluck_map/version.rb
CHANGED
@@ -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.
|
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-
|
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:
|
300
|
+
version: 1.3.1
|
289
301
|
requirements: []
|
290
302
|
rubyforge_project:
|
291
303
|
rubygems_version: 2.7.6
|