has_dynamic_columns 0.2.1 → 0.3.2
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 +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
|