active_record_seek 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +5 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +45 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +47 -0
  10. data/Rakefile +21 -0
  11. data/active_record_seek.gemspec +31 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/gemfiles/mysql.gemfile +5 -0
  15. data/gemfiles/mysql.gemfile.lock +47 -0
  16. data/gemfiles/postgresql.gemfile +5 -0
  17. data/gemfiles/postgresql.gemfile.lock +47 -0
  18. data/gemfiles/sqlite.gemfile +7 -0
  19. data/gemfiles/sqlite.gemfile.lock +47 -0
  20. data/lib/active_record_seek.rb +29 -0
  21. data/lib/active_record_seek/component.rb +57 -0
  22. data/lib/active_record_seek/concerns/active_record_concern.rb +20 -0
  23. data/lib/active_record_seek/concerns/instance_variable_concern.rb +40 -0
  24. data/lib/active_record_seek/middleware.rb +24 -0
  25. data/lib/active_record_seek/operators/base_operator.rb +65 -0
  26. data/lib/active_record_seek/operators/ci_base_operator.rb +66 -0
  27. data/lib/active_record_seek/operators/ci_matches_base_operator.rb +38 -0
  28. data/lib/active_record_seek/operators/ci_regexp_base_operator.rb +46 -0
  29. data/lib/active_record_seek/operators/matches_base_operator.rb +32 -0
  30. data/lib/active_record_seek/operators/regexp_base_operator.rb +46 -0
  31. data/lib/active_record_seek/query.rb +68 -0
  32. data/lib/active_record_seek/scopes/association_scope.rb +98 -0
  33. data/lib/active_record_seek/scopes/base_scope.rb +10 -0
  34. data/lib/active_record_seek/scopes/seek_or_scope.rb +53 -0
  35. data/lib/active_record_seek/scopes/seek_scope.rb +54 -0
  36. data/lib/active_record_seek/version.rb +3 -0
  37. metadata +149 -0
@@ -0,0 +1,68 @@
1
+ module ActiveRecordSeek
2
+ class Query
3
+
4
+ include Concerns::InstanceVariableConcern
5
+
6
+ delegate(*%w[ table_name arel_table reflect_on_association ], to: :model)
7
+ attr_reader(*%w[ active_record_query ])
8
+
9
+ def active_record_query=(new_active_record_query)
10
+ instance_variable_reset(:@where_sql)
11
+ @active_record_query = new_active_record_query
12
+ end
13
+ alias_method(:to_active_record_query, :active_record_query)
14
+
15
+ def to_seek_query
16
+ self
17
+ end
18
+
19
+ def model
20
+ to_active_record_query.klass
21
+ end
22
+
23
+ def adapter_name
24
+ model.connection.adapter_name
25
+ end
26
+
27
+ def arel_column(column_name)
28
+ arel_table[column_name]
29
+ end
30
+
31
+ def merge_where_sql(other_query)
32
+ to_active_record_query.where(other_query.to_seek_query.to_where_sql(enclose_with_parentheses: false))
33
+ end
34
+
35
+ def to_where_sql(enclose_with_parentheses: true)
36
+ if !instance_variable_defined?(:@where_sql)
37
+ @where_sql = active_record_query.reorder(nil).to_sql.split(" WHERE ", 2)[1].to_s.strip
38
+ end
39
+ return "" if @where_sql.blank?
40
+ enclose_with_parentheses ? "(#{@where_sql})" : @where_sql
41
+ end
42
+
43
+ def has_where_sql?
44
+ to_where_sql.present?
45
+ end
46
+
47
+ def apply(components)
48
+ to_active_record_query.seek_or(self) do |this|
49
+ components.group_by(&:namespace).each do |namespace, namespace_components|
50
+ case namespace
51
+ when "unscoped"
52
+ namespace_components.each do |component|
53
+ add_query { component.apply(self) }
54
+ end
55
+ else
56
+ add_query do |namespace_query|
57
+ namespace_components.each do |component|
58
+ namespace_query = component.apply(namespace_query)
59
+ end
60
+ namespace_query
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,98 @@
1
+ module ActiveRecordSeek
2
+ module Scopes
3
+ class AssociationScope < BaseScope
4
+
5
+ attr_accessor(*%w[ components base_query association ])
6
+
7
+ class JumpPlan
8
+ include Concerns::InstanceVariableConcern
9
+
10
+ attr_accessor(*%w[ reflection ])
11
+ delegate(:each, to: :jumps)
12
+
13
+ def after_initialize
14
+ add_reflection_jump(reflection)
15
+ end
16
+
17
+ def jumps
18
+ @jumps ||= []
19
+ end
20
+
21
+ def add_jump(model, association)
22
+ jumps.push(Jump.new(model: model, association: association))
23
+ end
24
+
25
+ def add_reflection_jump(jump_reflection)
26
+ # used to protect against infinite loop
27
+ association = jump_reflection.source_reflection.name
28
+ through_ref = jump_reflection.through_reflection
29
+ if through_ref
30
+ throughception = through_ref.klass.reflect_on_association(association)
31
+ # check for throughception (jump through another association)
32
+ # normal example:
33
+ # GroupProperty.has_many(:member_groups, through: :group)
34
+ # GroupProperty.has_many(:members, through: :member_groups)
35
+ # throughception example:
36
+ # GroupProperty.has_many(:members, through: :group)
37
+ if throughception && throughception.through_reflection
38
+ add_reflection_jump(throughception) # add all throughception jumps
39
+ add_reflection_jump(through_ref) # continue
40
+ else
41
+ add_jump(through_ref.klass, association)
42
+ add_reflection_jump(through_ref)
43
+ end
44
+ else
45
+ add_jump(jump_reflection.active_record, jump_reflection.name)
46
+ end
47
+ end
48
+ end
49
+
50
+ class Jump
51
+ include Concerns::InstanceVariableConcern
52
+
53
+ attr_accessor(*%w[ model association query ])
54
+
55
+ def reflection
56
+ model.reflect_on_association(association)
57
+ end
58
+
59
+ def primary_key
60
+ reflection.macro == :belongs_to ? reflection.foreign_key : reflection.active_record_primary_key
61
+ end
62
+
63
+ def foreign_key
64
+ reflection.macro == :belongs_to ? reflection.active_record_primary_key : reflection.foreign_key
65
+ end
66
+
67
+ def to_s
68
+ "#{model}.where(#{primary_key} => #{query.model}.#{foreign_key})"
69
+ end
70
+
71
+ def apply
72
+ model.unscoped.where(primary_key => query.to_active_record_query.select(foreign_key)).to_seek_query
73
+ end
74
+ end
75
+
76
+ # Group => Member
77
+ # jump through MemberGroup.group
78
+ # jump base Member.member_groups
79
+ # MemberGroup.where_group_id(in: Group.select_id)
80
+ def apply
81
+ association_reflection = base_query.model.reflect_on_association(association)
82
+ if !association_reflection
83
+ raise(
84
+ ArgumentError,
85
+ "#{base_query.model} does not have an association with the name: #{association.inspect}"
86
+ )
87
+ end
88
+ association_query = association_reflection.klass.unscoped.to_seek_query
89
+ association_query = association_query.apply(components).to_seek_query
90
+ JumpPlan.new(reflection: association_reflection).each do |jump|
91
+ association_query = jump.set(query: association_query).apply
92
+ end
93
+ association_query
94
+ end
95
+
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,10 @@
1
+ # WHERE (clause) has many conditions (predicates)
2
+ module ActiveRecordSeek
3
+ module Scopes
4
+ class BaseScope
5
+
6
+ include Concerns::InstanceVariableConcern
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,53 @@
1
+ module ActiveRecordSeek
2
+ module Scopes
3
+ class SeekOrScope < BaseScope
4
+
5
+ attr_accessor(*%w[ context_block ])
6
+
7
+ def apply(query, *context_arguments, &context_block)
8
+ query = query.to_seek_query
9
+ context = Context.new(query.model)
10
+ context.instance_exec(*context_arguments, &context_block)
11
+ context.apply(query)
12
+ end
13
+
14
+ class Context
15
+ attr_accessor(*%w[ model queries ])
16
+
17
+ def initialize(model)
18
+ self.model = model
19
+ self.queries = []
20
+ end
21
+
22
+ def add_query(&block)
23
+ unscoped_query = model.unscoped
24
+ query = unscoped_query.instance_exec(unscoped_query, &block).to_seek_query
25
+ queries.push(query) if query.has_where_sql?
26
+ self
27
+ end
28
+
29
+ # combine queries into single OR clause
30
+ def apply(query)
31
+ queries_sql = queries.map do |context_query|
32
+ context_query.to_where_sql(enclose_with_parentheses: queries.size > 1)
33
+ end.join(" OR ")
34
+ query.to_active_record_query.where(queries_sql).to_seek_query
35
+ end
36
+ end
37
+
38
+ module ActiveRecordScopeConcern
39
+
40
+ extend ActiveSupport::Concern
41
+
42
+ class_methods do
43
+ def seek_or(*params, &block)
44
+ SeekOrScope.new.apply(all, *params, &block).to_active_record_query
45
+ end
46
+ end
47
+
48
+ end
49
+
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,54 @@
1
+ module ActiveRecordSeek
2
+ module Scopes
3
+ class SeekScope < BaseScope
4
+
5
+ attr_reader(*%w[ seek_query active_record_query components components_hash ])
6
+
7
+ def query=(new_query)
8
+ @seek_query = new_query.to_seek_query
9
+ @active_record_query = @seek_query.to_active_record_query
10
+ @seek_query
11
+ end
12
+
13
+ def components_hash=(new_components_hash)
14
+ @components = new_components_hash.stringify_keys.map do |key, value|
15
+ Component.new(base_query: self, key: key, value: value)
16
+ end
17
+ end
18
+
19
+ def apply
20
+ components_for_base_query = components.select(&:is_base_query_component?)
21
+ components_by_association = components.reject(&:is_base_query_component?).group_by(&:association)
22
+ self.query = seek_query.apply(components_for_base_query)
23
+ self.query = active_record_query.seek_or(self) do |this|
24
+ components_by_association.each do |association, association_components|
25
+ add_query do
26
+ AssociationScope.new(
27
+ base_query: to_seek_query,
28
+ association: association,
29
+ components: association_components,
30
+ ).apply
31
+ end
32
+ end
33
+ end
34
+ seek_query
35
+ end
36
+
37
+ module ActiveRecordScopeConcern
38
+
39
+ extend ActiveSupport::Concern
40
+
41
+ class_methods do
42
+ def seek(components_hash = {}, &block)
43
+ raise(ArgumentError, "#{self.class}#seek does not accept a block") if block
44
+ SeekScope.new(
45
+ query: all,
46
+ components_hash: components_hash,
47
+ ).apply.to_active_record_query
48
+ end
49
+ end
50
+
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveRecordSeek
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_record_seek
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Griffith Chaffee
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-03-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '12'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: factory_bot
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '11'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '11'
83
+ description: Build complex ActiveRecord queries using hash syntax. Includes support
84
+ for Arel operators, OR queries, and assocition subqueries.
85
+ email:
86
+ - griffithchaffee@gmail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".gitignore"
92
+ - ".ruby-version"
93
+ - ".travis.yml"
94
+ - CODE_OF_CONDUCT.md
95
+ - Gemfile
96
+ - Gemfile.lock
97
+ - LICENSE.txt
98
+ - README.md
99
+ - Rakefile
100
+ - active_record_seek.gemspec
101
+ - bin/console
102
+ - bin/setup
103
+ - gemfiles/mysql.gemfile
104
+ - gemfiles/mysql.gemfile.lock
105
+ - gemfiles/postgresql.gemfile
106
+ - gemfiles/postgresql.gemfile.lock
107
+ - gemfiles/sqlite.gemfile
108
+ - gemfiles/sqlite.gemfile.lock
109
+ - lib/active_record_seek.rb
110
+ - lib/active_record_seek/component.rb
111
+ - lib/active_record_seek/concerns/active_record_concern.rb
112
+ - lib/active_record_seek/concerns/instance_variable_concern.rb
113
+ - lib/active_record_seek/middleware.rb
114
+ - lib/active_record_seek/operators/base_operator.rb
115
+ - lib/active_record_seek/operators/ci_base_operator.rb
116
+ - lib/active_record_seek/operators/ci_matches_base_operator.rb
117
+ - lib/active_record_seek/operators/ci_regexp_base_operator.rb
118
+ - lib/active_record_seek/operators/matches_base_operator.rb
119
+ - lib/active_record_seek/operators/regexp_base_operator.rb
120
+ - lib/active_record_seek/query.rb
121
+ - lib/active_record_seek/scopes/association_scope.rb
122
+ - lib/active_record_seek/scopes/base_scope.rb
123
+ - lib/active_record_seek/scopes/seek_or_scope.rb
124
+ - lib/active_record_seek/scopes/seek_scope.rb
125
+ - lib/active_record_seek/version.rb
126
+ homepage: https://github.com/griffithchaffee/active_record_seek
127
+ licenses:
128
+ - MIT
129
+ metadata: {}
130
+ post_install_message:
131
+ rdoc_options: []
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubygems_version: 3.0.3
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: Build complex ActiveRecord queries using hash syntax.
149
+ test_files: []