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