activerecord_nested_scope 1.0.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: a69c4a48717739fd16dacdb44003583d15f8989c
4
- data.tar.gz: 7362eef6445f2ca2969cd9df5d04618c30aedba5
2
+ SHA256:
3
+ metadata.gz: 6e06209d1f222b99ba36ec6619390cec6465e41000e4dc9100a0de23e47e962b
4
+ data.tar.gz: 7ed179db041bff9e317e0eb4e51b7e6e8877c0a2ef782cbd1e464c5fe640dec3
5
5
  SHA512:
6
- metadata.gz: 43628d21ea4aae5f2dd93d78e913eb7a659b0fab1232a2a0a183f0cbf46f0a39ea6485176edb1dac944bfeb41c136f77fbce9a3defef4da93d9cc5f8b8f7677e
7
- data.tar.gz: b832f7295a60cd32e4af55507cf7a05a504179c0658c6d025ce77def50582e7c6aac24fbf800204801521482bed18c8b5fbbf7ee32fe230328e3f129785bb195
6
+ metadata.gz: 2a2b55f78f24d9e1e1cf50f3d2260dff3e62dc70ce2ba6594835192e0189eee3b222d17a2b55e6f0ff5351a2927befcd35f0f3e660c45ec6679d5a186a3fb18a
7
+ data.tar.gz: 73a819a21306bb1ed00645063af41eea0851687c944942a8aa7a9f8b2f209980ca9bb99b007e778f197c1c40126d0585f199f9a9d5742a57937cd3fb6fd1220b
data/.gitignore CHANGED
@@ -4,12 +4,12 @@
4
4
  Gemfile.lock
5
5
  _yardoc/
6
6
  coverage/
7
+ gemfiles/*.lock
7
8
  doc/
8
9
  pkg/
9
10
  spec/reports/
10
11
  spec/dummy/config/database.yml
11
- spec/dummy/db/schema.rb
12
+ spec/dummy/db/schema*.rb
12
13
  spec/dummy/db/*.sqlite3
13
14
  spec/dummy/log/*.log
14
15
  spec/dummy/tmp/*
15
- tmp/
@@ -3,6 +3,8 @@ rvm:
3
3
  - 2.3
4
4
  - 2.4
5
5
  - 2.5
6
+ - 2.6
7
+ - 2.7
6
8
  services:
7
9
  - mysql
8
10
  - postgresql
@@ -16,10 +18,16 @@ gemfile:
16
18
  - gemfiles/rails50.gemfile
17
19
  - gemfiles/rails51.gemfile
18
20
  - gemfiles/rails52.gemfile
21
+ - gemfiles/rails60.gemfile
22
+ matrix:
23
+ exclude:
24
+ - rvm: 2.3
25
+ gemfile: gemfiles/rails60.gemfile
26
+ - rvm: 2.4
27
+ gemfile: gemfiles/rails60.gemfile
19
28
  before_script:
20
29
  - cd spec/dummy
21
- - bundle exec rake db:create RAILS_ENV=test
22
- - bundle exec rake db:migrate RAILS_ENV=test
23
- - bundle exec rake db:seed RAILS_ENV=test
30
+ - bundle exec rake db:create db:migrate db:seed RAILS_ENV=test
24
31
  - cd ../..
25
- script: bundle exec rspec
32
+ script:
33
+ - DEBUG=1 bundle exec rspec
@@ -0,0 +1,35 @@
1
+ # CHANGELOG
2
+
3
+ ## 1.2.0
4
+
5
+ * Add source_map option to resolve polymorphic type.
6
+
7
+ ## 1.1.1
8
+
9
+ * Fix query for associations with primary key option.
10
+
11
+ ## 1.1.0
12
+
13
+ * Add short query feature for simple belongs_to association.
14
+ * Rename `_nested_scope_options` to `nested_scope_options`.
15
+ * Remove support for active_record_union.
16
+
17
+ ## 1.0.4
18
+
19
+ * Fix query for string value.
20
+
21
+ ## 1.0.3
22
+
23
+ * Remove nil from polymorphic types.
24
+
25
+ ## 1.0.2
26
+
27
+ * Fix query error when there are no records in polymorphic tables.
28
+
29
+ ## 1.0.1
30
+
31
+ * Fix query for array value.
32
+
33
+ ## 1.0.0
34
+
35
+ * First release.
data/README.md CHANGED
@@ -5,7 +5,8 @@ An ActiveRecord extension to build nested scopes through pre-defined association
5
5
  ## Dependencies
6
6
 
7
7
  * ruby 2.3+
8
- * rails 5.0+ (activerecord and activesupport)
8
+ * activerecord 5.0+
9
+ * activesupport 5.0+
9
10
 
10
11
  ## Installation
11
12
 
@@ -57,35 +58,36 @@ UserConfig.in_group(id: 1)
57
58
 
58
59
  ### Polymorphic association
59
60
 
60
- If you define a polymorphic association like that,
61
+ If you define a polymorphic association,
62
+ `in_group` generates union of subqueries for each polymorpchic type as follows:
61
63
 
62
64
  ```ruby
63
- class Name < ActiveRecord::Base
64
- belongs_to :data, polymorphic: true
65
- nested_scope :in_group, through: :data
65
+ class Polymorphism < ActiveRecord::Base
66
+ belongs_to :record, polymorphic: true
67
+ nested_scope :in_group, through: :record
66
68
  end
67
- ```
68
-
69
- `in_group` scope generates union of subqueries for each polymorpchic type:
70
69
 
71
- ```ruby
72
- Name.in_group(id: 1)
73
- #=> SELECT "names"."data_type" FROM "names" GROUP BY "names"."data_type"
74
- # SELECT "names".* FROM (
75
- # SELECT "names".* FROM "names" WHERE "names"."data_type" = 'Group' AND "names"."data_id" IN (SELECT "groups"."id" FROM "groups" WHERE "groups"."id" = 1) UNION
76
- # SELECT "names".* FROM "names" WHERE "names"."data_type" = 'Manager' AND "names"."data_id" IN (SELECT "managers"."id" FROM "managers" WHERE "managers"."id" IN (SELECT "groups"."manager_id" FROM "groups" WHERE "groups"."id" = 1)) UNION
77
- # SELECT "names".* FROM "names" WHERE "names"."data_type" = 'Supervisor' AND "names"."data_id" IN (SELECT "supervisors"."id" FROM "supervisors" WHERE "supervisors"."id" IN (SELECT "managers"."supervisor_id" FROM "managers" WHERE "managers"."id" IN (SELECT "groups"."manager_id" FROM "groups" WHERE "groups"."id" = 1) ORDER BY "managers"."id" ASC)) UNION
78
- # SELECT "names".* FROM "names" WHERE "names"."data_type" = 'User' AND "names"."data_id" IN (SELECT "users"."id" FROM "users" WHERE "users"."group_id" IN (SELECT "groups"."id" FROM "groups" WHERE "groups"."id" = 1))) AS names
70
+ Polymorphism.in_group(id: 1)
71
+ #=> SELECT "polymorphisms"."record_type" FROM "polymorphisms" GROUP BY "polymorphisms"."record_type"
72
+ #=> SELECT "polymorphisms".* FROM (
73
+ # SELECT "polymorphisms".* FROM "polymorphisms" WHERE "polymorphisms"."record_type" = 'Group' AND "polymorphisms"."record_id" IN (SELECT "groups"."id" FROM "groups" WHERE "groups"."id" = 1) UNION
74
+ # SELECT "polymorphisms".* FROM "polymorphisms" WHERE "polymorphisms"."record_type" = 'User' AND "polymorphisms"."record_id" IN (SELECT "users"."id" FROM "users" WHERE "users"."group_id" IN (SELECT "groups"."id" FROM "groups" WHERE "groups"."id" = 1)) UNION
75
+ # SELECT "polymorphisms".* FROM "polymorphisms" WHERE "polymorphisms"."record_type" = 'UserConfig' AND "polymorphisms"."record_id" IN (SELECT "user_configs"."id" FROM "user_configs" WHERE "user_configs"."user_id" IN (SELECT "users"."id" FROM "users" WHERE "users"."group_id" IN (SELECT "groups"."id" FROM "groups" WHERE "groups"."id" = 1)))
76
+ # ) AS polymorphisms
79
77
  ```
80
78
 
81
- Note that the first SQL is executed to load `data_type`, and the second SQL is built using loaded `data_type` variations.
79
+ Note that the first SQL is executed to load `record_type`,
80
+ and the second SQL is built using loaded `record_type` variations.
82
81
 
83
- ### Scope examples
82
+ ### Scope arguments
84
83
 
85
84
  ```ruby
86
85
  # pass a integer
87
86
  User.in_group(1)
88
87
 
88
+ # pass an array
89
+ User.in_group([1, 2, 3])
90
+
89
91
  # pass a hash
90
92
  User.in_group(id: 1)
91
93
 
@@ -21,12 +21,9 @@ Gem::Specification.new do |spec|
21
21
  spec.add_dependency "activesupport", ">= 5.0"
22
22
 
23
23
  spec.add_development_dependency "rails", ">= 5.0"
24
- spec.add_development_dependency "active_record_union"
25
24
  spec.add_development_dependency "mysql2"
26
25
  spec.add_development_dependency "pg"
27
26
  spec.add_development_dependency "sqlite3"
28
27
  spec.add_development_dependency "rspec-rails"
29
28
  spec.add_development_dependency "simplecov"
30
- spec.add_development_dependency "pry-rails"
31
- spec.add_development_dependency "pry-byebug"
32
29
  end
@@ -1,5 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem "rails", "~> 5.2.0.rc1"
3
+ gem "rails", "~> 5.2.0"
4
+ gem "sqlite3", "~> 1.3.6"
4
5
 
5
6
  gemspec path: "../"
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "rails", "~> 6.0.0"
4
+
5
+ gemspec path: "../"
@@ -1,97 +1,102 @@
1
1
  module ActiveRecordNestedScope
2
2
  class Builder
3
3
  def initialize(klass, name, args)
4
- @klass = klass
5
- @name = name
4
+ @node = Node.new(klass, name)
6
5
  @args = args
6
+ @args_type = args_type(args)
7
7
  end
8
8
 
9
- def build
10
- build_for(@klass)
9
+ def build(node = @node)
10
+ if node.leaf?
11
+ leaf_relation(node)
12
+ else
13
+ build_relation(node)
14
+ end
11
15
  end
12
16
 
13
17
  private
14
18
 
15
- def build_for(klass)
16
- if klass._nested_scope_options
17
- if (through = klass._nested_scope_options[@name][:through])
18
- if (ref = klass.reflect_on_association(through))
19
- build_relation(klass, ref)
20
- else
21
- raise ArgumentError.new("can't find reflection #{through} in #{klass}")
22
- end
23
- else
24
- root_relation(klass)
25
- end
19
+ def build_relation(node)
20
+ if node.has_many?
21
+ has_many_relation(node)
22
+ elsif node.polymorphic_belongs_to?
23
+ polymorphic_belongs_to_relation(node)
24
+ elsif node.belongs_to?
25
+ belongs_to_relation(node)
26
26
  else
27
- klass.none
27
+ raise ArgumentError.new("unsupported reflection: #{node.reflection} in #{node.klass}")
28
28
  end
29
29
  end
30
30
 
31
- def build_relation(klass, ref)
32
- case ref.class.to_s
33
- when 'ActiveRecord::Reflection::HasManyReflection', 'ActiveRecord::Reflection::HasOneReflection'
34
- has_many_relation(klass, ref)
35
- when 'ActiveRecord::Reflection::BelongsToReflection'
36
- if ref.polymorphic?
37
- belongs_to_polymorphic_relation(klass, ref)
38
- else
39
- belongs_to_relation(klass, ref)
40
- end
41
- else
42
- raise ArgumentError.new("unsupported reflection: #{ref} in #{klass}")
43
- end
31
+ def has_many_relation(node)
32
+ child = node.children.first
33
+ return node.klass.none unless child
34
+
35
+ relation = child_relation(child, select: node.reflection.foreign_key)
36
+ node.klass.where(node.reflection.active_record_primary_key => relation)
44
37
  end
45
38
 
46
- def has_many_relation(klass, ref)
47
- klass.where(klass.primary_key => build_for(ref.klass).merge(scoped(ref.klass, ref.scope)).select(ref.foreign_key))
39
+ def belongs_to_relation(node)
40
+ child = node.children.first
41
+ return node.klass.none unless child
42
+
43
+ relation = child_relation(child, select: node.reflection.active_record_primary_key)
44
+ node.klass.where(node.reflection.foreign_key => relation)
48
45
  end
49
46
 
50
- def belongs_to_relation(klass, ref)
51
- klass.where(ref.foreign_key => build_for(ref.klass).merge(scoped(ref.klass, ref.scope)).select(klass.primary_key))
47
+ def polymorphic_belongs_to_relation(node)
48
+ rels = node.children.map do |child|
49
+ relation = child_relation(child, select: child.klass.primary_key)
50
+ node.klass.where(
51
+ node.reflection.foreign_type => child.source_type,
52
+ node.reflection.foreign_key => relation
53
+ )
54
+ end
55
+
56
+ union(node.klass, rels)
52
57
  end
53
58
 
54
- def belongs_to_polymorphic_relation(klass, ref)
55
- types = klass.unscoped.group(ref.foreign_type).pluck(ref.foreign_type)
56
- if types.present?
57
- rels = types.map { |type|
58
- if (parent = type.safe_constantize)
59
- klass.where(ref.foreign_type => type, ref.foreign_key => build_for(parent).merge(scoped(parent, ref.scope)))
60
- else
61
- klass.none
62
- end
63
- }
64
- union(klass, rels)
59
+ def child_relation(child, select:)
60
+ if simple_leaf_relation?(child)
61
+ @args
65
62
  else
66
- klass.none
63
+ relation = build(child).select(select)
64
+ relation = relation.merge(child.scope) if child.has_scope?
65
+ relation
67
66
  end
68
67
  end
69
68
 
70
- def root_relation(klass)
71
- if @args.is_a?(Integer) || @args.is_a?(Array) || @args.is_a?(ActiveRecord::Base)
72
- klass.where(klass.primary_key => @args)
73
- elsif @args.is_a?(ActiveRecord::Relation)
74
- klass.all.merge(@args)
75
- elsif @args
76
- klass.where(@args)
69
+ def simple_leaf_relation?(child)
70
+ @args_type == :simple && child.leaf? && child.parent.belongs_to? && !child.has_scope?
71
+ end
72
+
73
+ def leaf_relation(node)
74
+ case @args_type
75
+ when :relation
76
+ node.klass.all.merge(@args)
77
+ when :hash
78
+ node.klass.where(@args)
79
+ when :simple
80
+ node.klass.where(node.klass.primary_key => @args)
77
81
  else
78
- klass.all
82
+ raise ArgumentError.new("unexpected argument type: #{@args_type}")
79
83
  end
80
84
  end
81
85
 
82
- def scoped(klass, scope)
83
- rel = klass.all
84
- rel = rel.instance_eval(&scope) if scope
85
- rel
86
+ def args_type(args)
87
+ if args.is_a?(ActiveRecord::Relation)
88
+ :relation
89
+ elsif args.is_a?(Hash)
90
+ :hash
91
+ else
92
+ :simple
93
+ end
86
94
  end
87
95
 
88
96
  def union(klass, rels)
89
- if defined? ActiveRecordUnion
90
- rels.reduce(:union)
91
- else
92
- union = rels.map { |rel| "#{rel.to_sql}" }.reject(&:empty?).join(' UNION ')
93
- klass.from(Arel.sql("(#{union}) AS #{klass.table_name}"))
94
- end
97
+ return klass.none if rels.blank?
98
+ union = rels.map { |rel| "#{rel.to_sql}" }.reject(&:empty?).join(' UNION ')
99
+ klass.from(Arel.sql("(#{union}) AS #{klass.table_name}"))
95
100
  end
96
101
  end
97
102
  end
@@ -1,4 +1,5 @@
1
1
  require 'active_support'
2
+ require_relative 'node'
2
3
  require_relative 'builder'
3
4
 
4
5
  module ActiveRecordNestedScope
@@ -6,13 +7,13 @@ module ActiveRecordNestedScope
6
7
  extend ActiveSupport::Concern
7
8
 
8
9
  included do
9
- class_attribute :_nested_scope_options
10
+ class_attribute :nested_scope_options
10
11
  end
11
12
 
12
13
  class_methods do
13
14
  def nested_scope(name, options = {})
14
- self._nested_scope_options ||= {}
15
- self._nested_scope_options[name] = options
15
+ self.nested_scope_options ||= {}
16
+ self.nested_scope_options[name] = options
16
17
 
17
18
  scope name, ->(args) {
18
19
  ActiveRecordNestedScope::Builder.new(self, name, args).build
@@ -0,0 +1,111 @@
1
+ module ActiveRecordNestedScope
2
+ class Node
3
+ attr_accessor :klass, :name, :parent, :source_type
4
+
5
+ def initialize(klass, name, parent = nil, source_type = nil)
6
+ @klass = klass
7
+ @name = name
8
+ @parent = parent
9
+ @source_type = source_type
10
+ end
11
+
12
+ def has_options?
13
+ options = @klass.nested_scope_options
14
+ options && options[@name]
15
+ end
16
+
17
+ def options(key)
18
+ options = @klass.nested_scope_options.to_h
19
+ options.dig(@name, key)
20
+ end
21
+
22
+ def reflection
23
+ @klass.reflect_on_association(options(:through))
24
+ end
25
+
26
+ def leaf?
27
+ options(:through).blank?
28
+ end
29
+
30
+ def has_many?
31
+ reflection.class.name.in?(['ActiveRecord::Reflection::HasManyReflection', 'ActiveRecord::Reflection::HasOneReflection'])
32
+ end
33
+
34
+ def belongs_to?
35
+ reflection.class.name == 'ActiveRecord::Reflection::BelongsToReflection' && !reflection.polymorphic?
36
+ end
37
+
38
+ def polymorphic_belongs_to?
39
+ reflection.class.name == 'ActiveRecord::Reflection::BelongsToReflection' && reflection.polymorphic?
40
+ end
41
+
42
+ def children
43
+ @children ||= search_children.select(&:valid?)
44
+ end
45
+
46
+ def search_children
47
+ if leaf?
48
+ []
49
+ elsif polymorphic_belongs_to?
50
+ types = PolymorphicType.new(self, reflection).resolve
51
+ types.map { |klass, source_type| Node.new(klass, @name, self, source_type) }
52
+ else
53
+ [Node.new(reflection.klass, @name, self)]
54
+ end
55
+ end
56
+
57
+ def valid?
58
+ return false unless has_options?
59
+
60
+ if options(:through) && !reflection
61
+ STDERR.puts "can't find reflection for #{options(:through)} in #{klass}"
62
+ return false
63
+ end
64
+
65
+ return true
66
+ end
67
+
68
+ def has_scope?
69
+ @parent && @parent.reflection.scope.present?
70
+ end
71
+
72
+ def scope
73
+ @klass.all.instance_eval(&@parent.reflection.scope) if has_scope?
74
+ end
75
+
76
+ class PolymorphicType
77
+ def initialize(node, reflection)
78
+ @node = node
79
+ @reflection = reflection
80
+ end
81
+
82
+ def resolve
83
+ types = @node.klass.unscoped.group(@reflection.foreign_type).pluck(@reflection.foreign_type).compact
84
+ types.map do |type|
85
+ klass = resolve_source_type(type)&.safe_constantize
86
+ [klass, type] if klass
87
+ end.compact
88
+ end
89
+
90
+ private
91
+
92
+ def resolve_source_type(type)
93
+ if (map = @node.options(:source_map))
94
+ resolve_from_map(type, map)
95
+ else
96
+ type
97
+ end
98
+ end
99
+
100
+ def resolve_from_map(type, map)
101
+ if map.is_a?(Proc)
102
+ map.call(type)
103
+ elsif map.is_a?(Hash)
104
+ map[type]
105
+ else
106
+ raise ArgumentError.new("unsupported argument type for source_map option: #{map.class}.")
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordNestedScope
2
- VERSION = '1.0.2'
2
+ VERSION = '1.2.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord_nested_scope
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoshikazu Kaneta
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-03-16 00:00:00.000000000 Z
11
+ date: 2020-11-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.0'
55
- - !ruby/object:Gem::Dependency
56
- name: active_record_union
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: mysql2
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -136,34 +122,6 @@ dependencies:
136
122
  - - ">="
137
123
  - !ruby/object:Gem::Version
138
124
  version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: pry-rails
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: '0'
153
- - !ruby/object:Gem::Dependency
154
- name: pry-byebug
155
- requirement: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - ">="
158
- - !ruby/object:Gem::Version
159
- version: '0'
160
- type: :development
161
- prerelease: false
162
- version_requirements: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - ">="
165
- - !ruby/object:Gem::Version
166
- version: '0'
167
125
  description: An ActiveRecord extension to build nested scopes through pre-defined
168
126
  associations
169
127
  email:
@@ -175,6 +133,7 @@ files:
175
133
  - ".gitignore"
176
134
  - ".rspec"
177
135
  - ".travis.yml"
136
+ - CHANGELOG.md
178
137
  - Gemfile
179
138
  - LICENSE
180
139
  - README.md
@@ -183,9 +142,11 @@ files:
183
142
  - gemfiles/rails50.gemfile
184
143
  - gemfiles/rails51.gemfile
185
144
  - gemfiles/rails52.gemfile
145
+ - gemfiles/rails60.gemfile
186
146
  - lib/activerecord_nested_scope.rb
187
147
  - lib/activerecord_nested_scope/builder.rb
188
148
  - lib/activerecord_nested_scope/extension.rb
149
+ - lib/activerecord_nested_scope/node.rb
189
150
  - lib/activerecord_nested_scope/railtie.rb
190
151
  - lib/activerecord_nested_scope/version.rb
191
152
  homepage: https://github.com/kanety/activerecord_nested_scope
@@ -206,8 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
206
167
  - !ruby/object:Gem::Version
207
168
  version: '0'
208
169
  requirements: []
209
- rubyforge_project:
210
- rubygems_version: 2.5.2.2
170
+ rubygems_version: 3.0.3
211
171
  signing_key:
212
172
  specification_version: 4
213
173
  summary: An ActiveRecord extension to build nested scopes