has_dynamic_columns 0.2.1 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +8 -3
- data/README.md +25 -1
- data/has_dynamic_columns.gemspec +3 -1
- data/lib/generators/has_dynamic_columns/active_record_generator.rb +1 -1
- data/lib/generators/has_dynamic_columns/templates/migration.rb +4 -4
- data/lib/generators/has_dynamic_columns/templates/migration_0.3.0.rb +89 -0
- data/lib/generators/has_dynamic_columns/upgrade_0_3_0_active_record_generator.rb +22 -0
- data/lib/has_dynamic_columns/active_record/query_methods.rb +1 -167
- data/lib/has_dynamic_columns/active_record/relation.rb +1 -21
- data/lib/has_dynamic_columns/active_record/v3/query_methods.rb +281 -0
- data/lib/has_dynamic_columns/active_record/v3/relation.rb +34 -0
- data/lib/has_dynamic_columns/active_record/v4/query_methods.rb +257 -0
- data/lib/has_dynamic_columns/active_record/v4/relation.rb +34 -0
- data/lib/has_dynamic_columns/dynamic_column_boolean_datum.rb +5 -0
- data/lib/has_dynamic_columns/dynamic_column_date_datum.rb +5 -0
- data/lib/has_dynamic_columns/dynamic_column_datetime_datum.rb +5 -0
- data/lib/has_dynamic_columns/dynamic_column_datum.rb +8 -54
- data/lib/has_dynamic_columns/dynamic_column_enum_datum.rb +5 -0
- data/lib/has_dynamic_columns/dynamic_column_float_datum.rb +5 -0
- data/lib/has_dynamic_columns/dynamic_column_integer_datum.rb +5 -0
- data/lib/has_dynamic_columns/dynamic_column_string_datum.rb +5 -0
- data/lib/has_dynamic_columns/dynamic_column_text_datum.rb +5 -0
- data/lib/has_dynamic_columns/dynamic_column_time_datum.rb +5 -0
- data/lib/has_dynamic_columns/dynamic_column_timestamp_datum.rb +5 -0
- data/lib/has_dynamic_columns/model/class_methods.rb +22 -2
- data/lib/has_dynamic_columns/version.rb +1 -1
- data/lib/has_dynamic_columns.rb +11 -0
- data/rspec_rvm +39 -0
- data/spec/factories/account.rb +24 -0
- data/spec/factories/customer.rb +35 -0
- data/spec/has_dynamic_columns/active_record/query_methods_spec.rb +252 -0
- data/spec/has_dynamic_columns/dynamic_columns_integer_datum_spec.rb +124 -0
- data/spec/has_dynamic_columns/dynamic_columns_string_datum_spec.rb +7 -0
- data/spec/has_dynamic_columns_spec.rb +93 -63
- data/spec/spec_helper.rb +13 -8
- metadata +67 -6
@@ -0,0 +1,281 @@
|
|
1
|
+
module HasDynamicColumns
|
2
|
+
module ActiveRecord
|
3
|
+
module QueryMethods
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
alias_method_chain :order, :dynamic_columns
|
7
|
+
alias_method_chain :where, :dynamic_columns
|
8
|
+
alias_method_chain :build_arel, :dynamic_columns
|
9
|
+
|
10
|
+
def preprocess_order_args(order_args)
|
11
|
+
order_args.flatten!
|
12
|
+
#validate_order_args(order_args)
|
13
|
+
|
14
|
+
references = order_args.grep(String)
|
15
|
+
references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
|
16
|
+
references!(references) if references.any?
|
17
|
+
|
18
|
+
# if a symbol is given we prepend the quoted table name
|
19
|
+
order_args.map! do |arg|
|
20
|
+
case arg
|
21
|
+
when Symbol
|
22
|
+
#arg = klass.attribute_alias(arg) if klass.attribute_alias?(arg)
|
23
|
+
table[arg].asc
|
24
|
+
when Hash
|
25
|
+
arg.map { |field, dir|
|
26
|
+
#field = klass.attribute_alias(field) if klass.attribute_alias?(field)
|
27
|
+
table[field].send(dir.downcase)
|
28
|
+
}
|
29
|
+
else
|
30
|
+
arg
|
31
|
+
end
|
32
|
+
end.flatten!
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def dynamic_column_process_arel_nodes(rel, scope, index, joins)
|
37
|
+
case rel
|
38
|
+
when Arel::Nodes::Grouping
|
39
|
+
dynamic_column_process_arel_nodes(rel.expr, scope, index+1, joins)
|
40
|
+
when Arel::Nodes::Or
|
41
|
+
dynamic_column_process_arel_nodes(rel.left, scope, index+1, joins)
|
42
|
+
dynamic_column_process_arel_nodes(rel.right, scope, index+10000, joins) # Hack - queries with over 10,000 dynamic where conditions may break
|
43
|
+
when Arel::Nodes::And
|
44
|
+
dynamic_column_process_arel_nodes(rel.left, scope, index+1, joins)
|
45
|
+
dynamic_column_process_arel_nodes(rel.right, scope, index+10000, joins) # Hack - queries with over 10,000 dynamic where conditions may break
|
46
|
+
# We can work with this
|
47
|
+
when Arel::Nodes::Descending
|
48
|
+
col_name = rel.expr.name
|
49
|
+
dynamic_type = rel.expr.relation.engine
|
50
|
+
|
51
|
+
rel.expr.relation = dynamic_column_build_arel_joins(col_name, dynamic_type, scope, index+1, joins)[:table]
|
52
|
+
rel.expr.name = :value
|
53
|
+
# We can work with this
|
54
|
+
when Arel::Nodes::Ascending
|
55
|
+
col_name = rel.expr.name
|
56
|
+
dynamic_type = rel.expr.relation.engine
|
57
|
+
|
58
|
+
rel.expr.relation = dynamic_column_build_arel_joins(col_name, dynamic_type, scope, index+1, joins)[:table]
|
59
|
+
rel.expr.name = :value
|
60
|
+
# We can work with this
|
61
|
+
else
|
62
|
+
col_name = rel.left.name
|
63
|
+
dynamic_type = rel.left.relation.engine.to_s
|
64
|
+
|
65
|
+
rel.left.relation = dynamic_column_build_arel_joins(col_name, dynamic_type, scope, index+1, joins)[:table] # modify the where to use the aliased table
|
66
|
+
rel.left.name = :value # value is the data storage column searchable on dynamic_column_data table
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Builds the joins required for this dynamic column
|
71
|
+
# Modifies the where to use the dynamic_column_data table alias
|
72
|
+
#
|
73
|
+
# rel - an arel node
|
74
|
+
# scope - scope to run the conditions in
|
75
|
+
# index - unique table identifier
|
76
|
+
# joins - list of joins processed (to prevent duplicates)
|
77
|
+
def dynamic_column_build_arel_joins(col_name, dynamic_type, scope, index, joins)
|
78
|
+
field_scope = scope
|
79
|
+
field_scope_id = (!field_scope.nil?) ? field_scope.id : nil
|
80
|
+
field_scope_type = (!field_scope.nil?) ? field_scope.class.name.constantize.to_s : nil
|
81
|
+
|
82
|
+
joins_scope_key = "#{field_scope_type}_#{field_scope_id}"
|
83
|
+
joins[joins_scope_key] ||= {}
|
84
|
+
|
85
|
+
column_datum_store_table_type = "HasDynamicColumns::DynamicColumnStringDatum"
|
86
|
+
if !field_scope.nil? && a = field_scope.activerecord_dynamic_columns.where(key: col_name).first
|
87
|
+
column_datum_store_table_type = "HasDynamicColumns::DynamicColumn#{a.data_type.to_s.capitalize}Datum"
|
88
|
+
end
|
89
|
+
|
90
|
+
column_table = HasDynamicColumns::DynamicColumn.arel_table.alias("dynamic_where_#{index}_#{col_name}")
|
91
|
+
column_datum_table = HasDynamicColumns::DynamicColumnDatum.arel_table.alias("dynamic_where_data_#{index}_#{col_name}")
|
92
|
+
column_datum_store_table = column_datum_store_table_type.constantize.arel_table.alias("dynamic_where_data_store_#{index}_#{col_name}")
|
93
|
+
|
94
|
+
# Join for this scope/col already added - continue
|
95
|
+
if !joins[joins_scope_key][col_name].nil?
|
96
|
+
return joins[joins_scope_key][col_name]
|
97
|
+
end
|
98
|
+
|
99
|
+
# Join on the column with the key
|
100
|
+
on_query = column_table[:key].eq(col_name)
|
101
|
+
on_query = on_query.and(
|
102
|
+
column_table[:field_scope_type].eq(field_scope_type)
|
103
|
+
) unless field_scope_type.nil?
|
104
|
+
|
105
|
+
on_query = on_query.and(
|
106
|
+
column_table[:field_scope_id].eq(field_scope_id)
|
107
|
+
) unless field_scope_id.nil?
|
108
|
+
|
109
|
+
column_table_join_on = column_table
|
110
|
+
.create_on(
|
111
|
+
on_query
|
112
|
+
)
|
113
|
+
|
114
|
+
column_table_join = table.create_join(column_table, column_table_join_on)
|
115
|
+
self.joins_values += [column_table_join]
|
116
|
+
|
117
|
+
# Join on all the data with the provided key
|
118
|
+
column_table_datum_join_on = column_datum_table
|
119
|
+
.create_on(
|
120
|
+
column_datum_table[:owner_id].eq(table[:id]).and(
|
121
|
+
column_datum_table[:owner_type].eq(dynamic_type.to_s)
|
122
|
+
).and(
|
123
|
+
column_datum_table[:dynamic_column_id].eq(column_table[:id])
|
124
|
+
)
|
125
|
+
)
|
126
|
+
|
127
|
+
column_table_datum_join = table.create_join(column_datum_table, column_table_datum_join_on, Arel::Nodes::OuterJoin)
|
128
|
+
self.joins_values += [column_table_datum_join]
|
129
|
+
|
130
|
+
# Join on the actual data
|
131
|
+
column_table_datum_store_join_on = column_datum_store_table
|
132
|
+
.create_on(
|
133
|
+
column_datum_table[:datum_id].eq(column_datum_store_table[:id]).and(
|
134
|
+
column_datum_table[:datum_type].eq(column_datum_store_table_type)
|
135
|
+
)
|
136
|
+
)
|
137
|
+
|
138
|
+
column_table_datum_store_join = table.create_join(column_datum_store_table, column_table_datum_store_join_on, Arel::Nodes::OuterJoin)
|
139
|
+
self.joins_values += [column_table_datum_store_join]
|
140
|
+
|
141
|
+
joins[joins_scope_key][col_name] = {
|
142
|
+
:join => column_table_datum_store_join,
|
143
|
+
:table => column_datum_store_table
|
144
|
+
}
|
145
|
+
|
146
|
+
joins[joins_scope_key][col_name]
|
147
|
+
end
|
148
|
+
|
149
|
+
# Builds all the joins required for the dynamic columns in the where/order clauses
|
150
|
+
def build_dynamic_column_joins
|
151
|
+
joins = {}
|
152
|
+
|
153
|
+
self.where_dynamic_columns_values.each_with_index { |dynamic_scope, index_outer|
|
154
|
+
dynamic_scope[:where].each_with_index { |rel, index_inner|
|
155
|
+
# Process each where
|
156
|
+
dynamic_column_process_arel_nodes(rel, dynamic_scope[:scope], (index_outer*1000)+(index_inner*10000), joins)
|
157
|
+
|
158
|
+
# Warning
|
159
|
+
# Must cast rel to a string - I've encountered ***strange*** situations where this will change the 'col_name' to value in the where clause
|
160
|
+
# specifically, the test 'case should restrict if scope specified' will fail
|
161
|
+
self.where_values += [rel.to_sql]
|
162
|
+
}
|
163
|
+
}
|
164
|
+
self.order_dynamic_columns_values.each_with_index { |dynamic_scope, index_outer|
|
165
|
+
dynamic_scope[:order].each_with_index { |rel, index_inner|
|
166
|
+
# Process each order
|
167
|
+
dynamic_column_process_arel_nodes(rel, dynamic_scope[:scope], (index_outer*1000)+(index_inner*10000), joins)
|
168
|
+
}
|
169
|
+
}
|
170
|
+
|
171
|
+
true
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# When arel starts building - filter
|
177
|
+
def build_arel_with_dynamic_columns
|
178
|
+
self.build_dynamic_column_joins
|
179
|
+
|
180
|
+
if self.where_dynamic_columns_values.length > 0 || self.order_dynamic_columns_values.length > 0
|
181
|
+
self.group_values += [Arel::Nodes::Group.new(table[:id])]
|
182
|
+
end
|
183
|
+
|
184
|
+
build_arel_without_dynamic_columns
|
185
|
+
end
|
186
|
+
|
187
|
+
# lifted from
|
188
|
+
# http://erniemiller.org/2013/10/07/activerecord-where-not-sane-true/
|
189
|
+
class WhereChain
|
190
|
+
def initialize(scope)
|
191
|
+
@scope = scope
|
192
|
+
end
|
193
|
+
|
194
|
+
# Extends where to chain a has_dynamic_columns method
|
195
|
+
# This builds all the joins needed to search the has_dynamic_columns_data tables
|
196
|
+
def has_dynamic_columns(opts = :chain, *rest)
|
197
|
+
# Map
|
198
|
+
dynamic_columns_value = {
|
199
|
+
:scope => nil,
|
200
|
+
:where => @scope.send(:build_where, opts, rest)
|
201
|
+
}
|
202
|
+
@scope.where_dynamic_columns_values = dynamic_columns_value
|
203
|
+
|
204
|
+
chain = ::ActiveRecord::QueryMethods::WhereChain.new(@scope)
|
205
|
+
chain.instance_eval do
|
206
|
+
# Make outer scope variable accessible
|
207
|
+
@dynamic_columns_value = dynamic_columns_value
|
208
|
+
|
209
|
+
# Extends where to chain with a has_scope method
|
210
|
+
# This scopes the where from above
|
211
|
+
def with_scope(opt)
|
212
|
+
@dynamic_columns_value[:scope] = opt
|
213
|
+
|
214
|
+
@scope
|
215
|
+
end
|
216
|
+
def without_scope
|
217
|
+
@scope
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
chain
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def where_with_dynamic_columns(opts = :chain, *rest)
|
226
|
+
if opts == :chain
|
227
|
+
::ActiveRecord::QueryMethods::WhereChain.new(clone)
|
228
|
+
else
|
229
|
+
where_without_dynamic_columns(opts, rest)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# OrderChain objects act as placeholder for queries in which #order does not have any parameter.
|
234
|
+
# In this case, #order must be chained with #by_dynamic_columns to return a new relation.
|
235
|
+
class OrderChain
|
236
|
+
def initialize(scope)
|
237
|
+
@scope = scope
|
238
|
+
end
|
239
|
+
|
240
|
+
def by_dynamic_columns(*args)
|
241
|
+
@scope.send(:preprocess_order_args, args)
|
242
|
+
|
243
|
+
# Add now - want to keep the order with the regular column orders
|
244
|
+
@scope.order_values += args
|
245
|
+
|
246
|
+
# Map
|
247
|
+
dynamic_columns_value = {
|
248
|
+
:scope => nil,
|
249
|
+
:order => args,
|
250
|
+
}
|
251
|
+
@scope.order_dynamic_columns_values = dynamic_columns_value
|
252
|
+
|
253
|
+
chain = ::ActiveRecord::QueryMethods::OrderChain.new(@scope)
|
254
|
+
chain.instance_eval do
|
255
|
+
# Make outer scope variable accessible
|
256
|
+
@dynamic_columns_value = dynamic_columns_value
|
257
|
+
|
258
|
+
# Extends where to chain with a has_scope method
|
259
|
+
# This scopes the where from above
|
260
|
+
def with_scope(opt)
|
261
|
+
@dynamic_columns_value[:scope] = opt
|
262
|
+
|
263
|
+
@scope
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
chain
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def order_with_dynamic_columns(opts = :chain)
|
272
|
+
# Chain - by_dynamic_columns
|
273
|
+
if opts == :chain
|
274
|
+
::ActiveRecord::QueryMethods::OrderChain.new(clone)
|
275
|
+
else
|
276
|
+
order_without_dynamic_columns(opts)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module HasDynamicColumns
|
2
|
+
module ActiveRecord
|
3
|
+
module Relation
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
attr_accessor :where_dynamic_columns_values, :order_dynamic_columns_values
|
7
|
+
|
8
|
+
# Collect all where clauses
|
9
|
+
def where_dynamic_columns_values
|
10
|
+
@where_dynamic_columns_values || []
|
11
|
+
end
|
12
|
+
def where_dynamic_columns_values=values
|
13
|
+
raise ImmutableRelation if @loaded
|
14
|
+
@where_dynamic_columns_values ||= []
|
15
|
+
@where_dynamic_columns_values << values
|
16
|
+
end
|
17
|
+
|
18
|
+
# Collect all order clauses
|
19
|
+
def order_dynamic_columns_values
|
20
|
+
@order_dynamic_columns_values || []
|
21
|
+
end
|
22
|
+
def order_dynamic_columns_values=values
|
23
|
+
raise ImmutableRelation if @loaded
|
24
|
+
@order_dynamic_columns_values ||= []
|
25
|
+
@order_dynamic_columns_values << values
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
base.instance_eval do
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,257 @@
|
|
1
|
+
module HasDynamicColumns
|
2
|
+
module ActiveRecord
|
3
|
+
module QueryMethods
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
alias_method_chain :order, :dynamic_columns
|
7
|
+
alias_method_chain :where, :dynamic_columns
|
8
|
+
alias_method_chain :build_arel, :dynamic_columns
|
9
|
+
|
10
|
+
def dynamic_column_process_arel_nodes(rel, scope, index, joins)
|
11
|
+
case rel
|
12
|
+
when Arel::Nodes::Grouping
|
13
|
+
dynamic_column_process_arel_nodes(rel.expr, scope, index+1, joins)
|
14
|
+
when Arel::Nodes::Or
|
15
|
+
dynamic_column_process_arel_nodes(rel.left, scope, index+1, joins)
|
16
|
+
dynamic_column_process_arel_nodes(rel.right, scope, index+10000, joins) # Hack - queries with over 10,000 dynamic where conditions may break
|
17
|
+
when Arel::Nodes::And
|
18
|
+
dynamic_column_process_arel_nodes(rel.left, scope, index+1, joins)
|
19
|
+
dynamic_column_process_arel_nodes(rel.right, scope, index+10000, joins) # Hack - queries with over 10,000 dynamic where conditions may break
|
20
|
+
# We can work with this
|
21
|
+
when Arel::Nodes::Descending
|
22
|
+
col_name = rel.expr.name
|
23
|
+
dynamic_type = rel.expr.relation.engine
|
24
|
+
|
25
|
+
rel.expr.relation = dynamic_column_build_arel_joins(col_name, dynamic_type, scope, index+1, joins)[:table]
|
26
|
+
rel.expr.name = :value
|
27
|
+
# We can work with this
|
28
|
+
when Arel::Nodes::Ascending
|
29
|
+
col_name = rel.expr.name
|
30
|
+
dynamic_type = rel.expr.relation.engine
|
31
|
+
|
32
|
+
rel.expr.relation = dynamic_column_build_arel_joins(col_name, dynamic_type, scope, index+1, joins)[:table]
|
33
|
+
rel.expr.name = :value
|
34
|
+
# We can work with this
|
35
|
+
else
|
36
|
+
col_name = rel.left.name
|
37
|
+
dynamic_type = rel.left.relation.engine.to_s
|
38
|
+
|
39
|
+
rel.left.relation = dynamic_column_build_arel_joins(col_name, dynamic_type, scope, index+1, joins)[:table] # modify the where to use the aliased table
|
40
|
+
rel.left.name = :value # value is the data storage column searchable on dynamic_column_data table
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Builds the joins required for this dynamic column
|
45
|
+
# Modifies the where to use the dynamic_column_data table alias
|
46
|
+
#
|
47
|
+
# rel - an arel node
|
48
|
+
# scope - scope to run the conditions in
|
49
|
+
# index - unique table identifier
|
50
|
+
# joins - list of joins processed (to prevent duplicates)
|
51
|
+
def dynamic_column_build_arel_joins(col_name, dynamic_type, scope, index, joins)
|
52
|
+
field_scope = scope
|
53
|
+
field_scope_id = (!field_scope.nil?) ? field_scope.id : nil
|
54
|
+
field_scope_type = (!field_scope.nil?) ? field_scope.class.name.constantize.to_s : nil
|
55
|
+
|
56
|
+
joins_scope_key = "#{field_scope_type}_#{field_scope_id}"
|
57
|
+
joins[joins_scope_key] ||= {}
|
58
|
+
|
59
|
+
column_datum_store_table_type = "HasDynamicColumns::DynamicColumnStringDatum"
|
60
|
+
if !field_scope.nil? && a = field_scope.activerecord_dynamic_columns.where(key: col_name).first
|
61
|
+
column_datum_store_table_type = "HasDynamicColumns::DynamicColumn#{a.data_type.to_s.capitalize}Datum"
|
62
|
+
end
|
63
|
+
|
64
|
+
column_table = HasDynamicColumns::DynamicColumn.arel_table.alias("dynamic_where_#{index}_#{col_name}")
|
65
|
+
column_datum_table = HasDynamicColumns::DynamicColumnDatum.arel_table.alias("dynamic_where_data_#{index}_#{col_name}")
|
66
|
+
column_datum_store_table = column_datum_store_table_type.constantize.arel_table.alias("dynamic_where_data_store_#{index}_#{col_name}")
|
67
|
+
|
68
|
+
# Join for this scope/col already added - continue
|
69
|
+
if !joins[joins_scope_key][col_name].nil?
|
70
|
+
return joins[joins_scope_key][col_name]
|
71
|
+
end
|
72
|
+
|
73
|
+
# Join on the column with the key
|
74
|
+
on_query = column_table[:key].eq(col_name)
|
75
|
+
on_query = on_query.and(
|
76
|
+
column_table[:field_scope_type].eq(field_scope_type)
|
77
|
+
) unless field_scope_type.nil?
|
78
|
+
|
79
|
+
on_query = on_query.and(
|
80
|
+
column_table[:field_scope_id].eq(field_scope_id)
|
81
|
+
) unless field_scope_id.nil?
|
82
|
+
|
83
|
+
column_table_join_on = column_table
|
84
|
+
.create_on(
|
85
|
+
on_query
|
86
|
+
)
|
87
|
+
|
88
|
+
column_table_join = table.create_join(column_table, column_table_join_on)
|
89
|
+
self.joins_values += [column_table_join]
|
90
|
+
|
91
|
+
# Join on all the data with the provided key
|
92
|
+
column_table_datum_join_on = column_datum_table
|
93
|
+
.create_on(
|
94
|
+
column_datum_table[:owner_id].eq(table[:id]).and(
|
95
|
+
column_datum_table[:owner_type].eq(dynamic_type)
|
96
|
+
).and(
|
97
|
+
column_datum_table[:dynamic_column_id].eq(column_table[:id])
|
98
|
+
)
|
99
|
+
)
|
100
|
+
|
101
|
+
column_table_datum_join = table.create_join(column_datum_table, column_table_datum_join_on, Arel::Nodes::OuterJoin)
|
102
|
+
self.joins_values += [column_table_datum_join]
|
103
|
+
|
104
|
+
# Join on the actual data
|
105
|
+
column_table_datum_store_join_on = column_datum_store_table
|
106
|
+
.create_on(
|
107
|
+
column_datum_table[:datum_id].eq(column_datum_store_table[:id]).and(
|
108
|
+
column_datum_table[:datum_type].eq(column_datum_store_table_type)
|
109
|
+
)
|
110
|
+
)
|
111
|
+
|
112
|
+
column_table_datum_store_join = table.create_join(column_datum_store_table, column_table_datum_store_join_on, Arel::Nodes::OuterJoin)
|
113
|
+
self.joins_values += [column_table_datum_store_join]
|
114
|
+
|
115
|
+
joins[joins_scope_key][col_name] = {
|
116
|
+
:join => column_table_datum_store_join,
|
117
|
+
:table => column_datum_store_table
|
118
|
+
}
|
119
|
+
|
120
|
+
joins[joins_scope_key][col_name]
|
121
|
+
end
|
122
|
+
|
123
|
+
# Builds all the joins required for the dynamic columns in the where/order clauses
|
124
|
+
def build_dynamic_column_joins
|
125
|
+
joins = {}
|
126
|
+
|
127
|
+
self.where_dynamic_columns_values.each_with_index { |dynamic_scope, index_outer|
|
128
|
+
dynamic_scope[:where].each_with_index { |rel, index_inner|
|
129
|
+
# Process each where
|
130
|
+
dynamic_column_process_arel_nodes(rel, dynamic_scope[:scope], (index_outer*1000)+(index_inner*10000), joins)
|
131
|
+
|
132
|
+
# Warning
|
133
|
+
# Must cast rel to a string - I've encountered ***strange*** situations where this will change the 'col_name' to value in the where clause
|
134
|
+
# specifically, the test 'case should restrict if scope specified' will fail
|
135
|
+
self.where_values += [rel.to_sql]
|
136
|
+
}
|
137
|
+
}
|
138
|
+
self.order_dynamic_columns_values.each_with_index { |dynamic_scope, index_outer|
|
139
|
+
dynamic_scope[:order].each_with_index { |rel, index_inner|
|
140
|
+
# Process each order
|
141
|
+
dynamic_column_process_arel_nodes(rel, dynamic_scope[:scope], (index_outer*1000)+(index_inner*10000), joins)
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
145
|
+
true
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# When arel starts building - filter
|
151
|
+
def build_arel_with_dynamic_columns
|
152
|
+
self.build_dynamic_column_joins
|
153
|
+
|
154
|
+
if self.where_dynamic_columns_values.length > 0 || self.order_dynamic_columns_values.length > 0
|
155
|
+
self.group_values += [Arel::Nodes::Group.new(table[:id])]
|
156
|
+
end
|
157
|
+
|
158
|
+
build_arel_without_dynamic_columns
|
159
|
+
end
|
160
|
+
|
161
|
+
# lifted from
|
162
|
+
# http://erniemiller.org/2013/10/07/activerecord-where-not-sane-true/
|
163
|
+
module WhereChainCompatibility
|
164
|
+
#include ::ActiveRecord::QueryMethods
|
165
|
+
#define_method :build_where,
|
166
|
+
# ::ActiveRecord::QueryMethods.instance_method(:build_where)
|
167
|
+
|
168
|
+
# Extends where to chain a has_dynamic_columns method
|
169
|
+
# This builds all the joins needed to search the has_dynamic_columns_data tables
|
170
|
+
def has_dynamic_columns(opts = :chain, *rest)
|
171
|
+
# Map
|
172
|
+
dynamic_columns_value = {
|
173
|
+
:scope => nil,
|
174
|
+
:where => @scope.send(:build_where, opts, rest)
|
175
|
+
}
|
176
|
+
@scope.where_dynamic_columns_values = dynamic_columns_value
|
177
|
+
|
178
|
+
chain = ::ActiveRecord::QueryMethods::WhereChain.new(@scope)
|
179
|
+
chain.instance_eval do
|
180
|
+
# Make outer scope variable accessible
|
181
|
+
@dynamic_columns_value = dynamic_columns_value
|
182
|
+
|
183
|
+
# Extends where to chain with a has_scope method
|
184
|
+
# This scopes the where from above
|
185
|
+
def with_scope(opt)
|
186
|
+
@dynamic_columns_value[:scope] = opt
|
187
|
+
|
188
|
+
@scope
|
189
|
+
end
|
190
|
+
def without_scope
|
191
|
+
@scope
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
chain
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def where_with_dynamic_columns(opts = :chain, *rest)
|
200
|
+
if opts == :chain
|
201
|
+
scope = spawn
|
202
|
+
chain = ::ActiveRecord::QueryMethods::WhereChain.new(scope)
|
203
|
+
chain.send(:extend, WhereChainCompatibility)
|
204
|
+
else
|
205
|
+
where_without_dynamic_columns(opts, rest)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# OrderChain objects act as placeholder for queries in which #order does not have any parameter.
|
210
|
+
# In this case, #order must be chained with #by_dynamic_columns to return a new relation.
|
211
|
+
class OrderChain
|
212
|
+
def initialize(scope)
|
213
|
+
@scope = scope
|
214
|
+
end
|
215
|
+
|
216
|
+
def by_dynamic_columns(*args)
|
217
|
+
@scope.send(:preprocess_order_args, args)
|
218
|
+
|
219
|
+
# Add now - want to keep the order with the regular column orders
|
220
|
+
@scope.order_values += args
|
221
|
+
|
222
|
+
# Map
|
223
|
+
dynamic_columns_value = {
|
224
|
+
:scope => nil,
|
225
|
+
:order => args,
|
226
|
+
}
|
227
|
+
@scope.order_dynamic_columns_values = dynamic_columns_value
|
228
|
+
|
229
|
+
chain = ::ActiveRecord::QueryMethods::OrderChain.new(@scope)
|
230
|
+
chain.instance_eval do
|
231
|
+
# Make outer scope variable accessible
|
232
|
+
@dynamic_columns_value = dynamic_columns_value
|
233
|
+
|
234
|
+
# Extends where to chain with a has_scope method
|
235
|
+
# This scopes the where from above
|
236
|
+
def with_scope(opt)
|
237
|
+
@dynamic_columns_value[:scope] = opt
|
238
|
+
|
239
|
+
@scope
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
chain
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def order_with_dynamic_columns(opts = :chain)
|
248
|
+
# Chain - by_dynamic_columns
|
249
|
+
if opts == :chain
|
250
|
+
::ActiveRecord::QueryMethods::OrderChain.new(spawn)
|
251
|
+
else
|
252
|
+
order_without_dynamic_columns(opts)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
ActiveRecord::VERSION::MAJOR
|
2
|
+
|
3
|
+
module HasDynamicColumns
|
4
|
+
module ActiveRecord
|
5
|
+
module Relation
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
# Collect all where clauses
|
9
|
+
def where_dynamic_columns_values
|
10
|
+
@values[:where_dynamic_columns_values] || []
|
11
|
+
end
|
12
|
+
def where_dynamic_columns_values=values
|
13
|
+
raise ImmutableRelation if @loaded
|
14
|
+
@values[:where_dynamic_columns_values] ||= []
|
15
|
+
@values[:where_dynamic_columns_values] << values
|
16
|
+
end
|
17
|
+
|
18
|
+
# Collect all order clauses
|
19
|
+
def order_dynamic_columns_values
|
20
|
+
@values[:order_dynamic_columns_values] || []
|
21
|
+
end
|
22
|
+
def order_dynamic_columns_values=values
|
23
|
+
raise ImmutableRelation if @loaded
|
24
|
+
@values[:order_dynamic_columns_values] ||= []
|
25
|
+
@values[:order_dynamic_columns_values] << values
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
base.instance_eval do
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -4,62 +4,16 @@ module HasDynamicColumns
|
|
4
4
|
belongs_to :dynamic_column_option, :class_name => "HasDynamicColumns::DynamicColumnOption"
|
5
5
|
belongs_to :owner, :polymorphic => true
|
6
6
|
|
7
|
-
|
8
|
-
def value
|
9
|
-
if self.dynamic_column
|
10
|
-
case self.dynamic_column.data_type
|
11
|
-
when "list"
|
12
|
-
if self.dynamic_column_option
|
13
|
-
self.dynamic_column_option.key
|
14
|
-
end
|
15
|
-
when "datetime"
|
16
|
-
self[:value]
|
17
|
-
when "boolean"
|
18
|
-
|
19
|
-
if self[:value].is_a?(TrueClass) || self[:value].is_a?(FalseClass)
|
20
|
-
self[:value]
|
21
|
-
else
|
22
|
-
self[:value].to_i === 1
|
23
|
-
end
|
24
|
-
when "integer"
|
25
|
-
self[:value]
|
26
|
-
when "date"
|
27
|
-
self[:value]
|
28
|
-
when "string"
|
29
|
-
self[:value]
|
30
|
-
end
|
31
|
-
else
|
32
|
-
self[:value]
|
33
|
-
end
|
34
|
-
end
|
7
|
+
belongs_to :datum, :polymorphic => true
|
35
8
|
|
36
|
-
# Set value base don dynamic_column data_type
|
37
9
|
def value=v
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
# Hacky, -1 indicates to the validator that an invalid option was set
|
46
|
-
self.dynamic_column_option = nil
|
47
|
-
self.dynamic_column_option_id = (v.to_s.length > 0)? -1 : nil
|
48
|
-
end
|
49
|
-
when "datetime"
|
50
|
-
self[:value] = v
|
51
|
-
when "boolean"
|
52
|
-
self[:value] = (v)? 1 : 0
|
53
|
-
when "integer"
|
54
|
-
self[:value] = v
|
55
|
-
when "date"
|
56
|
-
self[:value] = v
|
57
|
-
when "string"
|
58
|
-
self[:value] = v
|
59
|
-
end
|
60
|
-
else
|
61
|
-
self[:value] = v
|
62
|
-
end
|
10
|
+
data_type = "string"
|
11
|
+
data_type = self.dynamic_column.data_type if self.dynamic_column
|
12
|
+
|
13
|
+
self.datum = "::HasDynamicColumns::DynamicColumn#{data_type.capitalize}Datum".constantize.new(value: v)
|
14
|
+
end
|
15
|
+
def value
|
16
|
+
self.datum.value if self.datum
|
63
17
|
end
|
64
18
|
end
|
65
19
|
end
|