has_dynamic_columns 0.2.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8e226dfbdf7a08adfe5c9fe89fd93704b94ce06c
4
- data.tar.gz: f0a3a6de371ded59b17db4690d54093d2ab98d16
3
+ metadata.gz: 448332c912f1846a9b67d402a0be6560fe2562c5
4
+ data.tar.gz: 256541a493f2e38a31683c7076239b42a6da5f6b
5
5
  SHA512:
6
- metadata.gz: 8dd05a3bd91dc8419c67506b463cb0c6741e1756901d9b4979202e4da4ea4c7a01b809de42d33a215f888ea490b458f5321ba2cad17291306d5e092db4bf0399
7
- data.tar.gz: a543799ebbf31103a117652fe127a002e8fa6bbc00870e6c65c5eb54035f7c73af1eb2c8d81eed5eae3bddd3139cddb92c41da28fb749f39ee983511028da179
6
+ metadata.gz: 95a6df76f9a6000ebffeac04915cc8c8f8a422ded713a25f4fda3a02ea52d0c9273fbb37398945d16a0ff26754ffd716e6cf4dd4216bb5571b3f540392b74373
7
+ data.tar.gz: f27225cb04d612c509225626c44625b25f0cc1b1822a4d364e609a6a25e616e538459e3bc368420f3078fa1c78eef43b5d9971adac6ddddcb0d4369d2fe22441
data/README.md CHANGED
@@ -139,30 +139,49 @@ puts customer.as_json
139
139
  account = Account.find(1)
140
140
 
141
141
  # 1 result
142
- Customer.where.has_dynamic_scope({ :first_name => "Butch" }).with_scope(account)
142
+ Customer
143
+ .where
144
+ .has_dynamic_columns({ :first_name => "Butch" })
145
+ .with_scope(account)
143
146
 
144
147
  # 1 result
145
- Customer.where.has_dynamic_scope({ :first_name => "Butch", :company => "Aperture Science" }).with_scope(account)
148
+ Customer
149
+ .where
150
+ .has_dynamic_columns({ :first_name => "Butch", :company => "Aperture Science" })
151
+ .with_scope(account)
146
152
 
147
153
  # 0 results
148
- Customer.where.has_dynamic_scope({ :first_name => "Butch", :company => "Blaaaaa" }).with_scope(account)
154
+ Customer
155
+ .where
156
+ .has_dynamic_columns({ :first_name => "Butch", :company => "Blaaaaa" })
157
+ .with_scope(account)
149
158
 
150
159
  # 2 results
151
- Customer.where.has_dynamic_scope({ :company => "Aperture Science" }).with_scope(account)
160
+ Customer
161
+ .where.has_dynamic_columns({ :company => "Aperture Science" })
162
+ .with_scope(account)
152
163
 
153
164
  # ------------------------------------------------
154
165
  # Find customers under the second account
155
166
  # ------------------------------------------------
156
167
  account = Account.find(2)
157
168
  # 1 result
158
- Customer.where.has_dynamic_scope({ :first_name => "Butch" }).with_scope(account)
169
+ Customer
170
+ .where
171
+ .has_dynamic_columns({ :first_name => "Butch" })
172
+ .with_scope(account)
159
173
 
160
174
  # 1 result
161
- Customer.where.has_dynamic_scope({ :first_name => "Butch", :country => "Canada" }).with_scope(account)
175
+ Customer
176
+ .where
177
+ .has_dynamic_columns({ :first_name => "Butch", :country => "Canada" })
178
+ .with_scope(account)
162
179
 
163
180
  # 0 results
164
- Customer.where.has_dynamic_scope({ :first_name => "Butch", :country => "Japan" }).with_scope(account)
165
- ```
181
+ Customer
182
+ .where
183
+ .has_dynamic_columns({ :first_name => "Butch", :country => "Japan" })
184
+ .with_scope(account)
166
185
 
167
186
  # ------------------------------------------------
168
187
  # without_scope
@@ -170,18 +189,42 @@ Customer.where.has_dynamic_scope({ :first_name => "Butch", :country => "Japan" }
170
189
 
171
190
  # 6 results
172
191
  # finds everyone named butch, no matter what account they're apart of
173
- Customer.where.has_dynamic_scope({ :first_name => "Butch" }).without_scope
192
+ Customer
193
+ .where
194
+ .has_dynamic_columns({ :first_name => "Butch" })
195
+ .without_scope
174
196
 
175
197
  # ------------------------------------------------
176
198
  # with Arel
177
- # WARNING: compound conditionals such as Customer.arel_table[:first_Name].matches("B%").and(Customer.arel_table[:first_Name].eq("Canada")) are NOT currently supported
178
199
  # ------------------------------------------------
179
200
 
180
201
  # 6 matches
181
- Customer.where.has_dynamic_scope(Customer.arel_table[:first_Name].matches("B%")).without_scope
202
+ Customer
203
+ .where.has_dynamic_columns(Customer.arel_table[:first_Name].matches("B%"))
204
+ .without_scope
182
205
 
183
206
  # 1 match
184
- Customer.where.has_dynamic_scope(Customer.arel_table[:first_Name].eq("Canada")).with_scope(Account.find(1))
207
+ Customer
208
+ .where
209
+ .has_dynamic_columns(Customer.arel_table[:country].eq("Canada"))
210
+ .with_scope(Account.find(1))
211
+
212
+ # ------------------------------------------------
213
+ # with nested or/and Arel
214
+ # ------------------------------------------------
215
+
216
+ # Anyone with country: Canada or first_name: John
217
+ # 2 match
218
+ Customer
219
+ .where
220
+ .has_dynamic_columns(
221
+ Customer.arel_table[:country].eq("Canada").or(
222
+ Customer.arel_table[:first_name].eq("John")
223
+ )
224
+ )
225
+ .with_scope(Account.find(1))
226
+
227
+ ```
185
228
 
186
229
  ## **has_many** relationship
187
230
 
@@ -2,46 +2,59 @@ module HasDynamicColumns
2
2
  module ActiveRecord
3
3
  module QueryMethods
4
4
  def self.included(base)
5
-
6
5
  base.class_eval do
7
6
  alias_method_chain :where, :dynamic_columns
8
7
  alias_method_chain :build_arel, :dynamic_columns
9
- end
10
8
 
11
- base.instance_eval do
12
- def has_dynamic_column_values=a
13
- end
14
- def has_dynamic_column_values
9
+ # Recurses through arel nodes until it finds one it can work with
10
+ def dynamic_column_process_nodes(rel, scope, index)
11
+ case rel
12
+ when Arel::Nodes::Grouping
13
+ dynamic_column_process_nodes(rel.expr, scope, index+1)
14
+ when Arel::Nodes::Or
15
+ dynamic_column_process_nodes(rel.left, scope, index+1)
16
+ dynamic_column_process_nodes(rel.right, scope, index+10000) # Hack - queries with over 10,000 dynamic where conditions may break
17
+ when Arel::Nodes::And
18
+ dynamic_column_process_nodes(rel.left, scope, index+1)
19
+ dynamic_column_process_nodes(rel.right, scope, index+10000) # Hack - queries with over 10,000 dynamic where conditions may break
20
+ # We can work with this
21
+ else
22
+ dynamic_column_build_arel_joins_and_modify_wheres(rel, scope, index+1)
23
+ end
15
24
  end
16
- end
17
- end
18
25
 
19
- # When arel starts building - filter
20
- def build_arel_with_dynamic_columns
21
- #arel = Arel::SelectManager.new(table.engine, table)
26
+ # Builds the joins required for this dynamic column
27
+ # Modifies the where to use the dynamic_column_data table alias
28
+ #
29
+ # rel - an arel node
30
+ # scope - scope to run the conditions in
31
+ # index - unique table identifier
32
+ def dynamic_column_build_arel_joins_and_modify_wheres(rel, scope, index)
33
+ col_name = rel.left.name
34
+ value = rel.right
22
35
 
23
- # Calculate any dynamic scope that was passed
24
- self.has_dynamic_columns_values.each { |dynamic_scope|
25
- field_scope = dynamic_scope[:scope]
26
- field_scope_id = (!field_scope.nil?) ? field_scope.id : nil
27
- field_scope_type = (!field_scope.nil?) ? field_scope.class.name.constantize.to_s : nil
36
+ field_scope = scope
37
+ field_scope_id = (!field_scope.nil?) ? field_scope.id : nil
38
+ field_scope_type = (!field_scope.nil?) ? field_scope.class.name.constantize.to_s : nil
28
39
 
29
- # TODO - make this work on compound arel queries like: table[:last_name].eq("Paterson").or(table[:first_name].eq("John"))
30
- #collapsed = collapse_wheres(arel, dynamic_scope[:where])
40
+ column_table = HasDynamicColumns::DynamicColumn.arel_table.alias("dynamic_where_#{index}_#{col_name}")
41
+ column_datum_table = HasDynamicColumns::DynamicColumnDatum.arel_table.alias("dynamic_where_data_#{index}_#{col_name}")
31
42
 
32
- dynamic_scope[:where].each_with_index { |rel, index|
33
- case rel
34
- when String
35
- next
43
+ dynamic_type = rel.left.relation.engine.to_s
44
+ rel.left.relation = column_datum_table # modify the where to use the aliased table
45
+ rel.left.name = :value # value is the data storage column searchable on dynamic_column_data table
46
+
47
+ rel.right = case rel.right
48
+ # Map true -> 1
49
+ when ::TrueClass
50
+ 1
51
+ # Map false -> 0
52
+ when ::FalseClass
53
+ 0
36
54
  else
37
- dynamic_type = rel.left.relation.engine.to_s
38
- col_name = rel.left.name
39
- value = rel.right
55
+ rel.right
40
56
  end
41
57
 
42
- column_table = HasDynamicColumns::DynamicColumn.arel_table.alias("dynamic_where_#{index}_#{col_name}")
43
- column_datum_table = HasDynamicColumns::DynamicColumnDatum.arel_table.alias("dynamic_where_data_#{index}_#{col_name}")
44
-
45
58
  # Join on the column with the key
46
59
  on_query = column_table[:key].eq(col_name)
47
60
  on_query = on_query.and(
@@ -60,13 +73,6 @@ module HasDynamicColumns
60
73
  column_table_join = table.create_join(column_table, column_table_join_on)
61
74
  self.joins_values += [column_table_join]
62
75
 
63
- arel_node = case rel
64
- when Arel::Nodes::Equality
65
- column_datum_table[:value].eq(value)
66
- else
67
- column_datum_table[:value].matches(value)
68
- end
69
-
70
76
  # Join on all the data with the provided key
71
77
  column_table_datum_join_on = column_datum_table
72
78
  .create_on(
@@ -74,15 +80,37 @@ module HasDynamicColumns
74
80
  column_datum_table[:owner_type].eq(dynamic_type)
75
81
  ).and(
76
82
  column_datum_table[:dynamic_column_id].eq(column_table[:id])
77
- ).and(
78
- arel_node
79
83
  )
80
84
  )
81
85
 
82
- column_table_datum_join = table.create_join(column_datum_table, column_table_datum_join_on)
86
+ column_table_datum_join = table.create_join(column_datum_table, column_table_datum_join_on, Arel::Nodes::OuterJoin)
83
87
  self.joins_values += [column_table_datum_join]
88
+
89
+ end
90
+ end
91
+ end
92
+
93
+ # When arel starts building - filter
94
+ def build_arel_with_dynamic_columns
95
+ # Calculate any dynamic scope that was passed
96
+ self.has_dynamic_columns_values.each_with_index { |dynamic_scope, index_outer|
97
+ dynamic_scope[:where].each_with_index { |rel, index_inner|
98
+ # Process each where
99
+ dynamic_column_process_nodes(rel, dynamic_scope[:scope], (index_outer*1000)+(index_inner*10000))
100
+
101
+ # It's now safe to use the original where query
102
+ # All the conditions in rel have been modified to be a where of the aliases dynamic_where_data table
103
+
104
+ # Warning
105
+ # Must cast rel to a string - I've encountered ***strange*** situations where this will change the 'col_name' to value in the where clause
106
+ # specifically, the test 'case should restrict if scope specified' will fail
107
+ self.where_values += [rel.to_sql]
84
108
  }
85
109
  }
110
+ # At least one dynamic where run - we need to group or we're going to get duplicates
111
+ if self.has_dynamic_columns_values.length > 0
112
+ self.group_values += [Arel::Nodes::Group.new(table[:id])]
113
+ end
86
114
 
87
115
  build_arel_without_dynamic_columns
88
116
  end
@@ -134,98 +162,6 @@ module HasDynamicColumns
134
162
  where_without_dynamic_columns(opts, rest)
135
163
  end
136
164
  end
137
- =begin
138
- def where_with_dynamic_columns(opts = :chain, *rest)
139
- puts "--------------------------------------"
140
- puts "Calling where_with_dynamic_columns"
141
- puts opts.inspect
142
- puts rest.inspect
143
-
144
- if opts == :chain
145
- scope = spawn
146
- scope.extend(WhereChainCompatibility)
147
- chain = ::ActiveRecord::QueryMethods::WhereChain.new(scope)
148
- chain.instance_eval do
149
- # Provide search on dynamic columns
150
- def dynamic(opts, *rest)
151
- options = rest.extract_options!
152
- field_scope = opts.respond_to?(:class) && opts.class.respond_to?(:ancestors) && (opts.class.ancestors.include?(::ActiveRecord::Base)) ? opts : nil
153
-
154
- # Field scope passed to the where clause
155
- if !field_scope.nil?
156
- opts = options
157
- rest = []
158
- end
159
-
160
- field_scope_id = (!field_scope.nil?) ? field_scope.id : nil
161
- field_scope_type = (!field_scope.nil?) ? field_scope.class.name.constantize.to_s : nil
162
-
163
- # Join dynamic_column table
164
- # This filter by scope if field_scope is passed
165
- @scope.joins_values += @scope.send(:build_where, opts, rest).map do |rel|
166
- dynamic_type = rel.left.relation.engine.to_s
167
-
168
- col_name = rel.left.name
169
- value = rel.right
170
-
171
- table = dynamic_type.constantize.arel_table
172
- column_table = ::HasDynamicColumns::DynamicColumn.arel_table.alias("dynamic_where_#{col_name}")
173
-
174
- on_query = column_table[:key].eq(col_name)
175
- if !field_scope_type.nil?
176
- on_query = on_query.and(
177
- column_table[:field_scope_type].eq(field_scope_type)
178
- )
179
- end
180
- if !field_scope_id.nil?
181
- on_query = on_query.and(
182
- column_table[:field_scope_id].eq(field_scope_id)
183
- )
184
- end
185
-
186
- column_table_join_on = column_table.create_on(on_query)
187
- table.create_join(column_table, column_table_join_on)
188
- end unless field_scope.nil?
189
-
190
- # Join dynamic_column_data table
191
- # This filters on data irrigardless of scope
192
- join_value = @scope.send(:build_where, opts, rest).map do |rel|
193
- dynamic_type = rel.left.relation.engine.to_s
194
-
195
- col_name = rel.left.name
196
- value = rel.right
197
-
198
- table = dynamic_type.constantize.arel_table
199
- column_table = ::HasDynamicColumns::DynamicColumn.arel_table.alias("dynamic_where_#{col_name}")
200
- column_datum_table = ::HasDynamicColumns::DynamicColumnDatum.arel_table.alias("dynamic_where_data_#{col_name}")
201
-
202
- # Join on all the data with the provided key
203
- column_table_datum_join_on = column_datum_table
204
- .create_on(
205
- column_datum_table[:owner_id].eq(table[:id]).and(
206
- column_datum_table[:owner_type].eq(dynamic_type)
207
- ).and(
208
- column_datum_table[:dynamic_column_id].eq(column_table[:id])
209
- ).and(
210
- column_datum_table[:value].matches("%"+value+"%")
211
- )
212
- )
213
-
214
- table.create_join(column_datum_table, column_table_datum_join_on)
215
- end
216
- @scope.joins_values += join_value
217
-
218
- @scope
219
- end
220
- end
221
-
222
- chain
223
- else
224
- # Default where
225
- where_without_dynamic_columns(opts, rest)
226
- end
227
- end
228
- =end
229
165
  end
230
166
  end
231
167
  end
@@ -140,6 +140,7 @@ module HasDynamicColumns
140
140
  query
141
141
  end
142
142
 
143
+ # Depricated
143
144
  # Find by dynamic columns
144
145
  def self.dynamic_where(*args)
145
146
  field_scope = args[0].is_a?(Hash) ? nil : args[0]
@@ -1,3 +1,3 @@
1
1
  module HasDynamicColumns
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
@@ -38,8 +38,22 @@ describe HasDynamicColumns do
38
38
  account
39
39
  end
40
40
 
41
+ let (:account2) do
42
+ account = Account.new(:name => "Account #2")
41
43
 
42
- describe HasDynamicColumns::ActiveRecord, :focus => true do
44
+ # Setup dynamic fields for Customer under this account
45
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "first_name", :data_type => "string")
46
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "last_name", :data_type => "string")
47
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "country", :data_type => "string")
48
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "company", :data_type => "string")
49
+
50
+ # Product fields
51
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Product", :key => "rarity", :data_type => "string")
52
+
53
+ account
54
+ end
55
+
56
+ describe HasDynamicColumns::ActiveRecord, :focus => true do
43
57
  it 'should find everyone in the current account scope' do
44
58
  customer = Customer.create(:account => account)
45
59
  customer.fields = {
@@ -71,7 +85,7 @@ describe HasDynamicColumns do
71
85
  customer = Customer.create(:account => account)
72
86
  customer.fields = {
73
87
  "first_name" => "Carl",
74
- "last_name" => "Marx",
88
+ "last_name" => "Paterson",
75
89
  "email" => "carl@communist.com",
76
90
  "trusted" => false,
77
91
  }
@@ -81,17 +95,17 @@ describe HasDynamicColumns do
81
95
 
82
96
  # 1 communist
83
97
  result = Customer.where.has_dynamic_columns(table[:email].matches("%gmail.com")).with_scope(account)
84
- expect(result.length).to eq(3)
98
+ expect(result.all.length).to eq(3)
85
99
 
86
100
  # 2 patersons
87
101
  result = Customer.where.has_dynamic_columns(table[:last_name].eq("Paterson")).with_scope(account)
88
- expect(result.length).to eq(2)
89
-
102
+ expect(result.all.length).to eq(3)
103
+
90
104
  # 1 john paterson
91
105
  result = Customer
92
106
  .where.has_dynamic_columns(table[:first_name].eq("John")).with_scope(account)
93
107
  .where.has_dynamic_columns(table[:last_name].eq("Paterson")).with_scope(account)
94
- expect(result.length).to eq(1)
108
+ expect(result.all.length).to eq(1)
95
109
  end
96
110
 
97
111
  it 'should find the single person in this scope' do
@@ -105,12 +119,88 @@ describe HasDynamicColumns do
105
119
  customer.save
106
120
 
107
121
  result = Customer.where.has_dynamic_columns(Customer.arel_table[:email].matches("%gmail.com")).with_scope(account)
108
- expect(result.length).to eq(1)
122
+ expect(result.all.length).to eq(1)
109
123
  end
110
124
 
111
125
  it 'should find all 4 gmail users when no scope passed' do
112
126
  result = Customer.where.has_dynamic_columns(Customer.arel_table[:email].matches("%gmail.com")).without_scope
113
- expect(result.length).to eq(4)
127
+ expect(result.all.length).to eq(4)
128
+ end
129
+
130
+ it 'should find anyone with first names Steve or John in account 1\'s scope' do
131
+ customer = Customer.create(:account => account)
132
+ customer.fields = {
133
+ "first_name" => "Steve",
134
+ "last_name" => "Jobs",
135
+ "email" => "steve.jobs@apple.com",
136
+ "trusted" => false,
137
+ }
138
+ customer.save
139
+
140
+ result = Customer.where.has_dynamic_columns(Customer.arel_table[:first_name].eq("Steve").or(Customer.arel_table[:first_name].eq("John"))).with_scope(Account.find(1))
141
+ expect(result.all.length).to eq(2)
142
+ end
143
+
144
+ it 'should find anyone with first names Steve or John in any scope' do
145
+ result = Customer.where.has_dynamic_columns(Customer.arel_table[:first_name].eq("Steve").or(Customer.arel_table[:first_name].eq("John"))).without_scope
146
+ expect(result.all.length).to eq(3)
147
+ end
148
+
149
+ it 'should find anyone with first names Steve or John and is trusted in any scope' do
150
+ result = Customer
151
+ .where.has_dynamic_columns(Customer.arel_table[:trusted].eq(true)).without_scope
152
+ .where.has_dynamic_columns(Customer.arel_table[:first_name].eq("Steve").or(Customer.arel_table[:first_name].eq("John"))).without_scope
153
+ expect(result.all.length).to eq(2)
154
+ end
155
+
156
+ it 'should find anyone with first names Steve and is not trusted in any scope' do
157
+ result = Customer
158
+ .where.has_dynamic_columns(Customer.arel_table[:trusted].eq(false)).without_scope
159
+ .where.has_dynamic_columns(Customer.arel_table[:first_name].eq("Steve")).without_scope
160
+ expect(result.all.length).to eq(1)
161
+ end
162
+
163
+ it 'should find all the Steves who are trusted in account 3\'s scope' do
164
+ result = Customer
165
+ .where.has_dynamic_columns(Customer.arel_table[:trusted].eq(true)).with_scope(Account.find(3))
166
+ .where.has_dynamic_columns(Customer.arel_table[:first_name].eq("Steve")).without_scope
167
+ expect(result.all.length).to eq(0)
168
+ end
169
+
170
+ it 'should find all the Steves who are trusted in account 1\'s scope' do
171
+ result = Customer
172
+ .where.has_dynamic_columns(Customer.arel_table[:trusted].eq(true)).with_scope(Account.find(1))
173
+ .where.has_dynamic_columns(Customer.arel_table[:first_name].eq("Steve")).without_scope
174
+ expect(result.all.length).to eq(1)
175
+ end
176
+
177
+ it 'should find across column types if no scope specified' do
178
+ customer = Customer.create(:account => account2)
179
+ customer.fields = {
180
+ "first_name" => "Steve",
181
+ "last_name" => "Jobs",
182
+ "company" => "Apple Computers",
183
+ "country" => "USA",
184
+ }
185
+ customer.save
186
+
187
+ result = Customer
188
+ .where.has_dynamic_columns(
189
+ Customer.arel_table[:first_name].eq("John").or(
190
+ Customer.arel_table[:company].eq("Apple Computers")
191
+ )
192
+ ).without_scope
193
+ expect(result.all.length).to eq(2)
194
+ end
195
+
196
+ it 'should restrict if scope specified' do
197
+ result = Customer
198
+ .where.has_dynamic_columns(
199
+ Customer.arel_table[:first_name].eq("John").or(
200
+ Customer.arel_table[:company].eq("Apple Computers")
201
+ )
202
+ ).with_scope(Account.find(4))
203
+ expect(result.all.length).to eq(1)
114
204
  end
115
205
  end
116
206
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: has_dynamic_columns
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Butch Marshall
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-11 00:00:00.000000000 Z
11
+ date: 2015-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord