active_record_seek 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []