pluck_map 0.6.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +8 -3
- data/Appraisals +5 -0
- data/CHANGELOG.md +19 -0
- data/README.md +99 -9
- data/gemfiles/rails_6.0.gemfile +8 -0
- data/lib/pluck_map/association_scope.rb +102 -0
- data/lib/pluck_map/attribute.rb +26 -13
- data/lib/pluck_map/attribute_builder.rb +20 -3
- data/lib/pluck_map/attributes.rb +18 -2
- data/lib/pluck_map/errors.rb +4 -0
- data/lib/pluck_map/model_context.rb +22 -1
- data/lib/pluck_map/nodes.rb +40 -0
- data/lib/pluck_map/presenter.rb +15 -29
- data/lib/pluck_map/presenters/to_csv.rb +16 -4
- data/lib/pluck_map/presenters/to_h.rb +10 -4
- data/lib/pluck_map/presenters/to_json.rb +78 -2
- data/lib/pluck_map/relationships.rb +89 -0
- data/lib/pluck_map/relationships/base.rb +26 -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/struct.rb +13 -0
- data/lib/pluck_map/structured_attribute.rb +37 -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
- data/pluck_map.gemspec +4 -1
- metadata +23 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 597612beb6033e9f03ae69a121aa0a91eab830e400917e3dc8e1f4b42052545b
|
4
|
+
data.tar.gz: 0bac2743a30f9795a6bfa572ef2faf7ee230aea78b75b9d0b0ac41399fd37077
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5dab317224c87b728585ebfd45267be0780854fe0a383b31cabcf88781c58900845af6d3dcf760f2beb35199b58fc7636f3a55dad125ead09579414cdd867fa2
|
7
|
+
data.tar.gz: 2cc8bfb68996ca3a350a0cd3324e9d56309485975622b72c0a13584c523f47a7bf1f90ca59b16fee0afc145c4283da12cf3504f04e2b8cfb71c43b42eb9ba26a
|
data/.travis.yml
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
language: ruby
|
2
|
-
rvm: 2.
|
2
|
+
rvm: 2.6.3
|
3
|
+
|
4
|
+
# we need MySQL 5.7+ to support JSON aggregation
|
5
|
+
dist: xenial
|
3
6
|
|
4
|
-
# Rails 6 requires Postgres 9.3+
|
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
|
@@ -17,6 +21,7 @@ matrix:
|
|
17
21
|
- gemfile: gemfiles/rails_5.0.gemfile
|
18
22
|
- gemfile: gemfiles/rails_5.1.gemfile
|
19
23
|
- gemfile: gemfiles/rails_5.2.gemfile
|
24
|
+
- gemfile: gemfiles/rails_6.0.gemfile
|
20
25
|
- gemfile: gemfiles/rails_edge.gemfile
|
21
26
|
allow_failures:
|
22
27
|
- gemfile: gemfiles/rails_edge.gemfile
|
data/Appraisals
CHANGED
@@ -19,6 +19,11 @@ appraise "rails-5.2" do
|
|
19
19
|
gem "sqlite3", "~> 1.3.6"
|
20
20
|
end
|
21
21
|
|
22
|
+
appraise "rails-6.0" do
|
23
|
+
gem "activerecord", "~> 6.0.0"
|
24
|
+
gem "sqlite3", "~> 1.4.0"
|
25
|
+
end
|
26
|
+
|
22
27
|
appraise "rails-edge" do
|
23
28
|
gem "rails", git: "https://github.com/rails/rails.git", branch: "master", require: "activerecord"
|
24
29
|
gem "sqlite3", "~> 1.4.0"
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,22 @@
|
|
1
|
+
## Unreleased
|
2
|
+
|
3
|
+
* BREAKING: `define` returns a subclass of `PluckMap::Presenter` instead of an instance (@boblail)
|
4
|
+
* FEATURE: `define` also creates structs to correspond to each presenter (@boblail)
|
5
|
+
|
6
|
+
## v1.0.0 (2019 Jul 17)
|
7
|
+
|
8
|
+
* FIX: Respect default_scopes for relationships (@boblail)
|
9
|
+
|
10
|
+
## v1.0.0.rc2 (2019 Jun 17)
|
11
|
+
|
12
|
+
* FEATURE: Add structured attributes to allow nesting attributes as a hash (@kobsy)
|
13
|
+
|
14
|
+
## v1.0.0.rc1 (2019 May 12)
|
15
|
+
|
16
|
+
* BREAKING: Remove deprecated features/methods (@boblail)
|
17
|
+
* FEATURE: Optimize `to_json` when a presenter doesn't need to process values in Ruby (@boblail)
|
18
|
+
* FEATURE: Add `has_many` and `has_one` DSL for presenting nested resources (@boblail)
|
19
|
+
|
1
20
|
## v0.6.2 (2019 Jun 13)
|
2
21
|
|
3
22
|
* FIX: Allow presenting a subclass of the presenter's model (@kobsy)
|
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,93 @@ presenter = PluckMap[Person].define do
|
|
207
208
|
end
|
208
209
|
```
|
209
210
|
|
211
|
+
### Structured attributes
|
212
|
+
|
213
|
+
You can also nest attributes by passing a block to the attribute method:
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
presenter = PluckMap[Person].define do
|
217
|
+
parent do
|
218
|
+
id select: :parent_id
|
219
|
+
type "Parent"
|
220
|
+
end
|
221
|
+
end
|
222
|
+
```
|
223
|
+
|
224
|
+
### Relationships
|
225
|
+
|
226
|
+
PluckMap can also describe nested data. There are two special methods in the `define` block that introduce child resources:
|
227
|
+
|
228
|
+
1. `has_one` will treat the resource as a nested object or null
|
229
|
+
2. `has_many` will treat the resource as an array of nested objects (which may be empty)
|
230
|
+
|
231
|
+
The first argument to either of these methods is the name of an association on the presented model.
|
232
|
+
|
233
|
+
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.
|
234
|
+
|
235
|
+
#### `has_one`
|
236
|
+
|
237
|
+
In the example below, assume
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
class Book < ActiveRecord::Base
|
241
|
+
belongs_to :author
|
242
|
+
end
|
243
|
+
```
|
244
|
+
|
245
|
+
This presenter :point_down: selects the title of every book as well as its author's name:
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
presenter = PluckMap[Book].define do
|
249
|
+
title
|
250
|
+
has_one :author do
|
251
|
+
name
|
252
|
+
end
|
253
|
+
end
|
254
|
+
```
|
255
|
+
|
256
|
+
(We can also write it using block variables, if that's easier to read.)
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
presenter = PluckMap[Book].define do |book|
|
260
|
+
book.title
|
261
|
+
book.has_one :author do |author|
|
262
|
+
author.name
|
263
|
+
end
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
Attributes defined for a relationship support all the same features as [attributes defined at the root level](#defining-attributes-to-present).
|
268
|
+
|
269
|
+
|
270
|
+
#### `has_many`
|
271
|
+
|
272
|
+
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:
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
presenter = PluckMap[Author].define do
|
276
|
+
name
|
277
|
+
has_many :books do
|
278
|
+
title
|
279
|
+
end
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
#### scopes
|
284
|
+
|
285
|
+
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.
|
286
|
+
|
287
|
+
In this example, we've altered our last presenter to ensure that books are listed alphabetically:
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
presenter = PluckMap[Author].define do
|
291
|
+
name
|
292
|
+
has_many :books, -> { order(title: :asc) } do
|
293
|
+
title
|
294
|
+
end
|
295
|
+
end
|
296
|
+
```
|
297
|
+
|
210
298
|
|
211
299
|
### Presenting Records
|
212
300
|
|
@@ -299,9 +387,11 @@ The gem's only runtime requirement is:
|
|
299
387
|
|
300
388
|
It supports these databases out of the box:
|
301
389
|
|
302
|
-
- PostgreSQL
|
303
|
-
- MySQL
|
304
|
-
- SQLite
|
390
|
+
- PostgreSQL 9.4+
|
391
|
+
- MySQL 5.7.22+
|
392
|
+
- SQLite 3.10.0+
|
393
|
+
|
394
|
+
(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
395
|
|
306
396
|
|
307
397
|
|
@@ -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,7 @@
|
|
1
1
|
require "pluck_map/attribute"
|
2
|
+
require "pluck_map/structured_attribute"
|
2
3
|
require "pluck_map/attributes"
|
4
|
+
require "pluck_map/relationships"
|
3
5
|
|
4
6
|
module PluckMap
|
5
7
|
class AttributeBuilder < BasicObject
|
@@ -12,7 +14,7 @@ module PluckMap
|
|
12
14
|
else
|
13
15
|
builder.instance_eval(&block)
|
14
16
|
end
|
15
|
-
Attributes.new(attributes)
|
17
|
+
Attributes.new(attributes, model)
|
16
18
|
end
|
17
19
|
|
18
20
|
def initialize(attributes, model)
|
@@ -20,12 +22,27 @@ module PluckMap
|
|
20
22
|
@model = model
|
21
23
|
end
|
22
24
|
|
23
|
-
def method_missing(attribute_name, *args)
|
25
|
+
def method_missing(attribute_name, *args, &block)
|
24
26
|
options = args.extract_options!
|
25
27
|
options[:value] = args.first unless args.empty?
|
26
|
-
@attributes.push Attribute.new(attribute_name, @model, options)
|
28
|
+
@attributes.push block.nil? ? Attribute.new(attribute_name, @model, options) :
|
29
|
+
StructuredAttribute.new(attribute_name, @model, block, options)
|
27
30
|
:attribute_added
|
28
31
|
end
|
29
32
|
|
33
|
+
def has_many(name, *args, &block)
|
34
|
+
options = args.extract_options!
|
35
|
+
options[:scope_block] = args.first unless args.empty?
|
36
|
+
@attributes.push Relationships.many(@model, name, block, options)
|
37
|
+
:relationship_added
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_one(name, *args, &block)
|
41
|
+
options = args.extract_options!
|
42
|
+
options[:scope_block] = args.first unless args.empty?
|
43
|
+
@attributes.push Relationships.one(@model, name, block, options)
|
44
|
+
:relationship_added
|
45
|
+
end
|
46
|
+
|
30
47
|
end
|
31
48
|
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 = []
|
@@ -36,16 +37,31 @@ module PluckMap
|
|
36
37
|
|
37
38
|
|
38
39
|
|
40
|
+
def ids
|
41
|
+
_attributes_by_id.keys
|
42
|
+
end
|
43
|
+
|
39
44
|
def by_id
|
40
45
|
_attributes_by_id
|
41
46
|
end
|
42
47
|
|
48
|
+
def to_json_array
|
49
|
+
PluckMap::BuildJsonArray.new(*selects.map do |select|
|
50
|
+
select = model.arel_table[select] if select.is_a?(Symbol)
|
51
|
+
select
|
52
|
+
end)
|
53
|
+
end
|
54
|
+
|
43
55
|
|
44
56
|
|
45
57
|
def will_map?
|
46
58
|
_attributes.any?(&:will_map?)
|
47
59
|
end
|
48
60
|
|
61
|
+
def nested?
|
62
|
+
_attributes.any?(&:nested?)
|
63
|
+
end
|
64
|
+
|
49
65
|
|
50
66
|
|
51
67
|
def ==(other)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "pluck_map/presenter"
|
2
|
+
require "pluck_map/struct"
|
2
3
|
|
3
4
|
module PluckMap
|
4
5
|
class ModelContext
|
@@ -8,7 +9,27 @@ module PluckMap
|
|
8
9
|
|
9
10
|
def define(&block)
|
10
11
|
attributes = PluckMap::AttributeBuilder.build(model: @model, &block)
|
11
|
-
|
12
|
+
define_class!(@model, attributes)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def define_class!(model, attributes)
|
18
|
+
# Create a new subclass of PluckMap::Presenter
|
19
|
+
klass = Class.new(PluckMap::Presenter)
|
20
|
+
|
21
|
+
# Partially apply initialize with the parameters passed to this method
|
22
|
+
klass.define_method(:initialize) do |query|
|
23
|
+
super(model, attributes, query)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Generate a Struct constant in the namespace of the new subclass
|
27
|
+
struct = ::Struct.new(*attributes.ids, keyword_init: true)
|
28
|
+
struct.extend PluckMap::Struct::ClassMethods
|
29
|
+
struct.instance_variable_set :@presenter, klass
|
30
|
+
klass.const_set :Struct, struct
|
31
|
+
|
32
|
+
klass
|
12
33
|
end
|
13
34
|
end
|
14
35
|
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
|
data/lib/pluck_map/presenter.rb
CHANGED
@@ -1,45 +1,35 @@
|
|
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
|
6
9
|
class Presenter
|
7
10
|
include CsvPresenter, HashPresenter, JsonPresenter
|
8
11
|
|
9
|
-
attr_reader :model, :attributes
|
12
|
+
attr_reader :model, :attributes, :query
|
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!
|
14
|
+
def initialize(model, attributes, query)
|
15
|
+
unless query.model <= model
|
16
|
+
raise ArgumentError, "Query for #{query.model} but #{model} expected"
|
25
17
|
end
|
26
|
-
end
|
27
18
|
|
28
|
-
|
29
|
-
|
30
|
-
|
19
|
+
@model = model
|
20
|
+
@attributes = attributes
|
21
|
+
@query = query
|
31
22
|
end
|
32
23
|
|
33
24
|
protected
|
34
25
|
|
35
|
-
def pluck
|
36
|
-
unless model.nil? || query.model <= model
|
37
|
-
raise ArgumentError, "Query for #{query.model} but #{model} expected"
|
38
|
-
end
|
39
|
-
|
26
|
+
def pluck
|
40
27
|
# puts "\e[95m#{query.select(*selects).to_sql}\e[0m"
|
41
28
|
results = benchmark("pluck(#{query.table_name})") { query.pluck(*selects) }
|
42
29
|
return results unless block_given?
|
30
|
+
attributes.each do |attribute|
|
31
|
+
attribute.preload!(results)
|
32
|
+
end
|
43
33
|
benchmark("map(#{query.table_name})") { yield results }
|
44
34
|
end
|
45
35
|
|
@@ -81,6 +71,7 @@ module PluckMap
|
|
81
71
|
|
82
72
|
# On Rails 4.2, `pluck` can't accept Arel nodes
|
83
73
|
select = Arel.sql(select.to_sql) if ActiveRecord.version.segments.take(2) == [4,2] && select.respond_to?(:to_sql)
|
74
|
+
|
84
75
|
select
|
85
76
|
}
|
86
77
|
end
|
@@ -89,10 +80,5 @@ module PluckMap
|
|
89
80
|
attributes.by_id
|
90
81
|
end
|
91
82
|
|
92
|
-
def keys
|
93
|
-
puts "DEPRECATION WARNING: PluckMap::Presenter#keys is deprecated; use #selects instead"
|
94
|
-
selects
|
95
|
-
end
|
96
|
-
|
97
83
|
end
|
98
84
|
end
|
@@ -1,9 +1,21 @@
|
|
1
|
+
require "pluck_map/errors"
|
2
|
+
|
1
3
|
module PluckMap
|
2
4
|
module CsvPresenter
|
3
5
|
|
4
|
-
def
|
6
|
+
def self.included(base)
|
7
|
+
def base.to_csv(query, **kargs)
|
8
|
+
new(query).to_csv(**kargs)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_csv
|
13
|
+
if attributes.nested?
|
14
|
+
raise PluckMap::UnsupportedAttributeError, "to_csv can not be used to present nested attributes"
|
15
|
+
end
|
16
|
+
|
5
17
|
define_to_csv!
|
6
|
-
to_csv
|
18
|
+
to_csv
|
7
19
|
end
|
8
20
|
|
9
21
|
private def define_to_csv!
|
@@ -11,8 +23,8 @@ module PluckMap
|
|
11
23
|
|
12
24
|
headers = CSV.generate_line(attributes.map(&:name))
|
13
25
|
ruby = <<-RUBY
|
14
|
-
def to_csv
|
15
|
-
pluck
|
26
|
+
def to_csv
|
27
|
+
pluck do |results|
|
16
28
|
rows = [#{headers.inspect}]
|
17
29
|
results.each_with_object(rows) do |values, rows|
|
18
30
|
values = Array(values)
|
@@ -1,15 +1,21 @@
|
|
1
1
|
module PluckMap
|
2
2
|
module HashPresenter
|
3
3
|
|
4
|
-
def
|
4
|
+
def self.included(base)
|
5
|
+
def base.to_h(query, **kargs)
|
6
|
+
new(query).to_h(**kargs)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_h
|
5
11
|
define_to_h!
|
6
|
-
to_h
|
12
|
+
to_h
|
7
13
|
end
|
8
14
|
|
9
15
|
private def define_to_h!
|
10
16
|
ruby = <<-RUBY
|
11
|
-
def to_h
|
12
|
-
pluck
|
17
|
+
def to_h
|
18
|
+
pluck do |results|
|
13
19
|
results.map { |values| values = Array(values); { #{attributes.map { |attribute| "#{attribute.name.inspect} => #{attribute.to_ruby}" }.join(", ")} } }
|
14
20
|
end
|
15
21
|
end
|
@@ -3,12 +3,88 @@ require "json"
|
|
3
3
|
module PluckMap
|
4
4
|
module JsonPresenter
|
5
5
|
|
6
|
-
def
|
7
|
-
|
6
|
+
def self.included(base)
|
7
|
+
def base.to_json(query, **kargs)
|
8
|
+
new(query).to_json(**kargs)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_json(json: default_json, **)
|
13
|
+
if attributes.will_map?
|
14
|
+
to_json__default(json: json)
|
15
|
+
else
|
16
|
+
to_json__optimized
|
17
|
+
end
|
8
18
|
end
|
9
19
|
|
10
20
|
private
|
11
21
|
|
22
|
+
def to_json__default(json: default_json, **)
|
23
|
+
json.dump(to_h)
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_json__optimized(**)
|
27
|
+
define_to_json__optimized!
|
28
|
+
to_json__optimized
|
29
|
+
end
|
30
|
+
|
31
|
+
def define_to_json__optimized!
|
32
|
+
sql = compile(to_json_object(attributes).as("object"))
|
33
|
+
|
34
|
+
ruby = <<-RUBY
|
35
|
+
private def to_json__optimized(**)
|
36
|
+
sql = wrap_aggregate(query.select(Arel.sql(#{sql.inspect})))
|
37
|
+
query.connection.select_value(sql)
|
38
|
+
end
|
39
|
+
RUBY
|
40
|
+
# puts "\e[34m#{ruby}\e[0m" # <-- helps debugging PluckMapPresenter
|
41
|
+
class_eval ruby, __FILE__, __LINE__ - ruby.length
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_json_object(attributes)
|
45
|
+
args = []
|
46
|
+
attributes.each do |attribute|
|
47
|
+
args << Arel::Nodes::Quoted.new(attribute.name)
|
48
|
+
args << send(:"prepare_#{attribute.class.name.gsub("::", "_")}", attribute)
|
49
|
+
end
|
50
|
+
PluckMap::BuildJsonObject.new(*args)
|
51
|
+
end
|
52
|
+
|
53
|
+
def prepare_PluckMap_Attribute(attribute)
|
54
|
+
return Arel::Nodes::Quoted.new(attribute.value) if attribute.value?
|
55
|
+
arg = attribute.selects[0]
|
56
|
+
arg = attribute.model.arel_table[arg] if arg.is_a?(Symbol)
|
57
|
+
arg
|
58
|
+
end
|
59
|
+
|
60
|
+
def prepare_PluckMap_StructuredAttribute(attribute)
|
61
|
+
to_json_object(attribute.attributes)
|
62
|
+
end
|
63
|
+
|
64
|
+
def prepare_PluckMap_Relationships_Many(attribute)
|
65
|
+
PluckMap::JsonSubqueryAggregate.new(attribute.scope, to_json_object(attribute.attributes))
|
66
|
+
end
|
67
|
+
|
68
|
+
def prepare_PluckMap_Relationships_One(attribute)
|
69
|
+
Arel.sql("(#{attribute.scope.select(to_json_object(attribute.attributes)).to_sql})")
|
70
|
+
end
|
71
|
+
|
72
|
+
def wrap_aggregate(subquery)
|
73
|
+
"SELECT #{compile(aggregate(Arel.sql("d.object")))} FROM (#{subquery.to_sql}) AS d"
|
74
|
+
end
|
75
|
+
|
76
|
+
def aggregate(object)
|
77
|
+
PluckMap::JsonArrayAggregate.new(object)
|
78
|
+
end
|
79
|
+
|
80
|
+
def compile(node)
|
81
|
+
visitor.compile(node)
|
82
|
+
end
|
83
|
+
|
84
|
+
def visitor
|
85
|
+
model.connection.visitor
|
86
|
+
end
|
87
|
+
|
12
88
|
def default_json
|
13
89
|
if defined?(MultiJson)
|
14
90
|
MultiJson
|
@@ -0,0 +1,89 @@
|
|
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
|
+
default_scope_for(association).merge(AssociationScope.create.scope(association))
|
57
|
+
end
|
58
|
+
|
59
|
+
def default_scope_for(association)
|
60
|
+
association.klass.all
|
61
|
+
end
|
62
|
+
|
63
|
+
# ActiveRecord constructs an Association from a Reflection and an
|
64
|
+
# Owner. It expects Owner to be an instance of an ActiveRecord object
|
65
|
+
# and uses `[]` to access specific values for fields on the record.
|
66
|
+
#
|
67
|
+
# e.g. WHERE books.author_id = 7
|
68
|
+
#
|
69
|
+
# We want to create a subquery that will reference those fields
|
70
|
+
# but not their specific values.
|
71
|
+
#
|
72
|
+
# e.g. WHERE books.author_id = authors.id
|
73
|
+
#
|
74
|
+
# So we create an object that serves the purpose of Owner but returns
|
75
|
+
# appropriate selectors.
|
76
|
+
#
|
77
|
+
AbstractOwner = Struct.new(:reflection) do
|
78
|
+
def class
|
79
|
+
reflection.active_record
|
80
|
+
end
|
81
|
+
|
82
|
+
def [](value)
|
83
|
+
self.class.arel_table[value]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "pluck_map/structured_attribute"
|
2
|
+
|
3
|
+
module PluckMap
|
4
|
+
module Relationships
|
5
|
+
class Base < StructuredAttribute
|
6
|
+
attr_reader :scope
|
7
|
+
|
8
|
+
def initialize(attribute_name, scope, block, options)
|
9
|
+
@scope = scope
|
10
|
+
@scope = @scope.instance_exec(&options[:scope_block]) if options[:scope_block]
|
11
|
+
super(attribute_name, scope.klass, block, options)
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def build_select
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
def build_map
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
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
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "pluck_map/attribute"
|
2
|
+
|
3
|
+
module PluckMap
|
4
|
+
class StructuredAttribute < Attribute
|
5
|
+
attr_reader :attributes
|
6
|
+
|
7
|
+
def initialize(attribute_name, model, block, options={})
|
8
|
+
@attributes = AttributeBuilder.build(model: model, &block)
|
9
|
+
options = options.slice(:as).merge(select: build_select, map: build_map)
|
10
|
+
super(attribute_name, model, options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def will_map?
|
14
|
+
attributes.any?(&:will_map?)
|
15
|
+
end
|
16
|
+
|
17
|
+
def nested?
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def build_select
|
24
|
+
attributes.selects
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_map
|
28
|
+
lambda do |*values|
|
29
|
+
return nil if values.none?
|
30
|
+
attributes.each_with_object({}) do |attribute, hash|
|
31
|
+
hash[attribute.name] = attribute.exec(values)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
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
|
data/pluck_map.gemspec
CHANGED
@@ -17,11 +17,14 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
|
+
# uses `keyword_init` argument of `Struct.new`
|
21
|
+
spec.required_ruby_version = '>= 2.5.0'
|
22
|
+
|
20
23
|
spec.add_dependency "activerecord", ">= 4.2"
|
21
24
|
|
22
25
|
spec.add_development_dependency "appraisal"
|
23
26
|
spec.add_development_dependency "bundler"
|
24
|
-
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rake"
|
25
28
|
spec.add_development_dependency "minitest-reporters"
|
26
29
|
spec.add_development_dependency "minitest-reporters-turn_reporter"
|
27
30
|
spec.add_development_dependency "sqlite3"
|
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: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bob Lail
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-10-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -56,16 +56,16 @@ dependencies:
|
|
56
56
|
name: rake
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: minitest-reporters
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -255,19 +255,34 @@ files:
|
|
255
255
|
- gemfiles/rails_5.0.gemfile
|
256
256
|
- gemfiles/rails_5.1.gemfile
|
257
257
|
- gemfiles/rails_5.2.gemfile
|
258
|
+
- gemfiles/rails_6.0.gemfile
|
258
259
|
- gemfiles/rails_edge.gemfile
|
259
260
|
- lib/pluck_map.rb
|
261
|
+
- lib/pluck_map/association_scope.rb
|
260
262
|
- lib/pluck_map/attribute.rb
|
261
263
|
- lib/pluck_map/attribute_builder.rb
|
262
264
|
- lib/pluck_map/attributes.rb
|
265
|
+
- lib/pluck_map/errors.rb
|
263
266
|
- lib/pluck_map/model_context.rb
|
267
|
+
- lib/pluck_map/nodes.rb
|
264
268
|
- lib/pluck_map/null_logger.rb
|
265
269
|
- lib/pluck_map/presenter.rb
|
266
270
|
- lib/pluck_map/presenters.rb
|
267
271
|
- lib/pluck_map/presenters/to_csv.rb
|
268
272
|
- lib/pluck_map/presenters/to_h.rb
|
269
273
|
- lib/pluck_map/presenters/to_json.rb
|
274
|
+
- lib/pluck_map/relationships.rb
|
275
|
+
- lib/pluck_map/relationships/base.rb
|
276
|
+
- lib/pluck_map/relationships/many.rb
|
277
|
+
- lib/pluck_map/relationships/one.rb
|
278
|
+
- lib/pluck_map/relationships/polymorphic_one.rb
|
279
|
+
- lib/pluck_map/struct.rb
|
280
|
+
- lib/pluck_map/structured_attribute.rb
|
270
281
|
- lib/pluck_map/version.rb
|
282
|
+
- lib/pluck_map/visitors.rb
|
283
|
+
- lib/pluck_map/visitors/mysql.rb
|
284
|
+
- lib/pluck_map/visitors/postgresql.rb
|
285
|
+
- lib/pluck_map/visitors/sqlite.rb
|
271
286
|
- pluck_map.gemspec
|
272
287
|
homepage: https://github.com/boblail/pluck_map
|
273
288
|
licenses: []
|
@@ -280,15 +295,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
280
295
|
requirements:
|
281
296
|
- - ">="
|
282
297
|
- !ruby/object:Gem::Version
|
283
|
-
version:
|
298
|
+
version: 2.5.0
|
284
299
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
285
300
|
requirements:
|
286
301
|
- - ">="
|
287
302
|
- !ruby/object:Gem::Version
|
288
303
|
version: '0'
|
289
304
|
requirements: []
|
290
|
-
|
291
|
-
rubygems_version: 2.7.6
|
305
|
+
rubygems_version: 3.1.4
|
292
306
|
signing_key:
|
293
307
|
specification_version: 4
|
294
308
|
summary: A DSL for presenting ActiveRecord::Relations without instantiating ActiveRecord
|