has_dynamic_columns 0.3.3 → 0.3.5
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/.travis.yml +6 -0
- data/README.md +11 -0
- data/has_dynamic_columns.gemspec +7 -3
- data/lib/generators/has_dynamic_columns/templates/migration_0.3.4.rb +8 -0
- data/lib/generators/has_dynamic_columns/templates/migration_0.3.5.rb +15 -0
- data/lib/generators/has_dynamic_columns/upgrade_0_3_4_active_record_generator.rb +22 -0
- data/lib/generators/has_dynamic_columns/upgrade_0_3_5_active_record_generator.rb +22 -0
- data/lib/has_dynamic_columns/active_record/query_methods.rb +232 -1
- data/lib/has_dynamic_columns/active_record/v3/query_methods.rb +28 -216
- data/lib/has_dynamic_columns/active_record/v4/query_methods.rb +3 -193
- data/lib/has_dynamic_columns/dynamic_column_model_datum.rb +6 -0
- data/lib/has_dynamic_columns/model/class_methods.rb +54 -19
- data/lib/has_dynamic_columns/version.rb +1 -1
- data/lib/has_dynamic_columns.rb +1 -0
- data/spec/factories/account.rb +3 -0
- data/spec/factories/customer.rb +4 -0
- data/spec/has_dynamic_columns/active_record/query_methods_spec.rb +53 -4
- data/spec/has_dynamic_columns/dynamic_columns_integer_datum_spec.rb +1 -1
- data/spec/has_dynamic_columns/dynamic_columns_model_datum_spec.rb +118 -0
- data/spec/spec_helper.rb +10 -2
- metadata +12 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 654ea8aee466792c91ae8278022ad10ff034c072
|
4
|
+
data.tar.gz: 7f5f8b41a767bd2fc12ab09b4d5c3e8df57e5e96
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 76a5cf1aa4b44b2ba1e6a02b40150ef23222015d3c812e3cb3061b44ceda271f5ebe9296cb2d5f19d44c1ee7ed064c0c90beacefa56903d256b2217969f19c69
|
7
|
+
data.tar.gz: f2adf795d3b55a202237b5edbfb8f780ceb79d2011d19bb9916c16dc8a0edb389c2fc01d31d35e7c924523267900038e4a3c3aee2160b8626c69c6cdfb1fc9f3
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
[](http://badge.fury.io/rb/has_dynamic_columns)
|
2
|
+
[](https://travis-ci.org/butchmarshall/has_dynamic_columns)
|
3
|
+
|
1
4
|
has_dynamic_columns
|
2
5
|
============
|
3
6
|
|
@@ -6,6 +9,13 @@ This plugin gives ActiveRecord models the ability to dynamically define collecta
|
|
6
9
|
Release Notes
|
7
10
|
============
|
8
11
|
|
12
|
+
**0.3.5**
|
13
|
+
- Added model storage type (for storing association with other activerecord objects)
|
14
|
+
|
15
|
+
**0.3.4**
|
16
|
+
- Can now store array of data
|
17
|
+
- JRuby support
|
18
|
+
|
9
19
|
**0.3.0**
|
10
20
|
- Moved to storing data types in separate tables (where/order now correct!)
|
11
21
|
- Added order.by_dynamic_columns
|
@@ -24,6 +34,7 @@ running the following command:
|
|
24
34
|
|
25
35
|
rails generate has_dynamic_columns:active_record
|
26
36
|
rails generate has_dynamic_columns:upgrade_0_3_0_active_record
|
37
|
+
rails generate has_dynamic_columns:upgrade_0_3_4_active_record
|
27
38
|
rake db:migrate
|
28
39
|
|
29
40
|
Usage
|
data/has_dynamic_columns.gemspec
CHANGED
@@ -19,9 +19,13 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
21
|
spec.add_dependency "activerecord", [">= 3.0", "< 5.0"]
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
if RUBY_PLATFORM == 'java'
|
23
|
+
spec.add_development_dependency "jdbc-sqlite3", "> 0"
|
24
|
+
spec.add_development_dependency "activerecord-jdbcsqlite3-adapter", "> 0"
|
25
|
+
else
|
26
|
+
spec.add_development_dependency "sqlite3", "> 0"
|
27
|
+
end
|
28
|
+
spec.add_development_dependency "factory_girl", "> 4.0"
|
25
29
|
spec.add_development_dependency "bundler", "~> 1.7"
|
26
30
|
spec.add_development_dependency "rake", "~> 10.0"
|
27
31
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Upgrade035HasDynamicColumns < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :dynamic_column_model_data do |t|
|
4
|
+
t.integer :value_id
|
5
|
+
t.string :value_type
|
6
|
+
t.timestamps null: false
|
7
|
+
end
|
8
|
+
add_index(:dynamic_column_model_data, [:value_id,:value_type], name: "index_by_value")
|
9
|
+
add_column :dynamic_columns, :class_name, :string
|
10
|
+
add_column :dynamic_columns, :column_name, :string
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.down
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "generators/has_dynamic_columns/has_dynamic_columns_generator"
|
2
|
+
require "generators/has_dynamic_columns/next_migration_version"
|
3
|
+
require "rails/generators/migration"
|
4
|
+
require "rails/generators/active_record"
|
5
|
+
|
6
|
+
# Extend the HasDynamicColumnsGenerator so that it creates an AR migration
|
7
|
+
module HasDynamicColumns
|
8
|
+
class Upgrade_0_3_4_ActiveRecordGenerator < ::HasDynamicColumnsGenerator
|
9
|
+
include Rails::Generators::Migration
|
10
|
+
extend NextMigrationVersion
|
11
|
+
|
12
|
+
source_paths << File.join(File.dirname(__FILE__), "templates")
|
13
|
+
|
14
|
+
def create_migration_file
|
15
|
+
migration_template "migration_0.3.4.rb", "db/migrate/upgrade_0_3_4_has_dynamic_columns.rb"
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.next_migration_number(dirname)
|
19
|
+
::ActiveRecord::Generators::Base.next_migration_number dirname
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "generators/has_dynamic_columns/has_dynamic_columns_generator"
|
2
|
+
require "generators/has_dynamic_columns/next_migration_version"
|
3
|
+
require "rails/generators/migration"
|
4
|
+
require "rails/generators/active_record"
|
5
|
+
|
6
|
+
# Extend the HasDynamicColumnsGenerator so that it creates an AR migration
|
7
|
+
module HasDynamicColumns
|
8
|
+
class Upgrade_0_3_5_ActiveRecordGenerator < ::HasDynamicColumnsGenerator
|
9
|
+
include Rails::Generators::Migration
|
10
|
+
extend NextMigrationVersion
|
11
|
+
|
12
|
+
source_paths << File.join(File.dirname(__FILE__), "templates")
|
13
|
+
|
14
|
+
def create_migration_file
|
15
|
+
migration_template "migration_0.3.5.rb", "db/migrate/upgrade_0_3_5_has_dynamic_columns.rb"
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.next_migration_number(dirname)
|
19
|
+
::ActiveRecord::Generators::Base.next_migration_number dirname
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -1 +1,232 @@
|
|
1
|
-
|
1
|
+
# Common modifications to ActiveRecord::QueryMethods
|
2
|
+
require_relative "v#{ActiveRecord::VERSION::MAJOR}/query_methods"
|
3
|
+
|
4
|
+
module HasDynamicColumns
|
5
|
+
module ActiveRecord
|
6
|
+
module QueryMethods
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
include ClassMethods
|
10
|
+
|
11
|
+
alias_method_chain :order, :dynamic_columns
|
12
|
+
alias_method_chain :where, :dynamic_columns
|
13
|
+
alias_method_chain :build_arel, :dynamic_columns
|
14
|
+
|
15
|
+
def dynamic_column_process_arel_nodes(rel, scope, index, joins)
|
16
|
+
case rel
|
17
|
+
when Arel::Nodes::Grouping
|
18
|
+
dynamic_column_process_arel_nodes(rel.expr, scope, index+1, joins)
|
19
|
+
when Arel::Nodes::Or
|
20
|
+
dynamic_column_process_arel_nodes(rel.left, scope, index+1, joins)
|
21
|
+
dynamic_column_process_arel_nodes(rel.right, scope, index+10000, joins) # Hack - queries with over 10,000 dynamic where conditions may break
|
22
|
+
when Arel::Nodes::And
|
23
|
+
dynamic_column_process_arel_nodes(rel.left, scope, index+1, joins)
|
24
|
+
dynamic_column_process_arel_nodes(rel.right, scope, index+10000, joins) # Hack - queries with over 10,000 dynamic where conditions may break
|
25
|
+
# We can work with this
|
26
|
+
when Arel::Nodes::Descending
|
27
|
+
col_name = rel.expr.name
|
28
|
+
dynamic_type = rel.expr.relation.engine
|
29
|
+
|
30
|
+
rel.expr.relation = dynamic_column_build_arel_joins(col_name, dynamic_type, scope, index+1, joins)[:table]
|
31
|
+
rel.expr.name = :value
|
32
|
+
# We can work with this
|
33
|
+
when Arel::Nodes::Ascending
|
34
|
+
col_name = rel.expr.name
|
35
|
+
dynamic_type = rel.expr.relation.engine
|
36
|
+
|
37
|
+
rel.expr.relation = dynamic_column_build_arel_joins(col_name, dynamic_type, scope, index+1, joins)[:table]
|
38
|
+
rel.expr.name = :value
|
39
|
+
# We can work with this
|
40
|
+
else
|
41
|
+
col_name = rel.left.name
|
42
|
+
dynamic_type = rel.left.relation.engine.to_s
|
43
|
+
|
44
|
+
res = dynamic_column_build_arel_joins(col_name, dynamic_type, scope, index+1, joins) # modify the where to use the aliased table
|
45
|
+
|
46
|
+
rel.left.relation = res[:table]
|
47
|
+
rel.left.name = res[:column] || :value # value is the data storage column searchable on dynamic_column_data table
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Builds the joins required for this dynamic column
|
52
|
+
# Modifies the where to use the dynamic_column_data table alias
|
53
|
+
#
|
54
|
+
# rel - an arel node
|
55
|
+
# scope - scope to run the conditions in
|
56
|
+
# index - unique table identifier
|
57
|
+
# joins - list of joins processed (to prevent duplicates)
|
58
|
+
def dynamic_column_build_arel_joins(col_name, dynamic_type, scope, index, joins)
|
59
|
+
field_scope = scope
|
60
|
+
field_scope_id = (!field_scope.nil?) ? field_scope.id : nil
|
61
|
+
field_scope_type = (!field_scope.nil?) ? field_scope.class.name.constantize.to_s : nil
|
62
|
+
|
63
|
+
joins_scope_key = "#{field_scope_type}_#{field_scope_id}"
|
64
|
+
joins[joins_scope_key] ||= {}
|
65
|
+
|
66
|
+
column_datum_store_table_type = "HasDynamicColumns::DynamicColumnStringDatum"
|
67
|
+
if !field_scope.nil? && a = field_scope.activerecord_dynamic_columns.where(key: col_name).first
|
68
|
+
column_datum_store_table_type = "HasDynamicColumns::DynamicColumn#{a.data_type.to_s.capitalize}Datum"
|
69
|
+
end
|
70
|
+
|
71
|
+
column_table = HasDynamicColumns::DynamicColumn.arel_table.alias("dynamic_where_#{index}_#{col_name}")
|
72
|
+
column_datum_table = HasDynamicColumns::DynamicColumnDatum.arel_table.alias("dynamic_where_data_#{index}_#{col_name}")
|
73
|
+
column_datum_store_table = column_datum_store_table_type.constantize.arel_table.alias("dynamic_where_data_store_#{index}_#{col_name}")
|
74
|
+
|
75
|
+
# Join for this scope/col already added - continue
|
76
|
+
if !joins[joins_scope_key][col_name].nil?
|
77
|
+
return joins[joins_scope_key][col_name]
|
78
|
+
end
|
79
|
+
|
80
|
+
# Join on the column with the key
|
81
|
+
on_query = column_table[:key].eq(col_name)
|
82
|
+
on_query = on_query.and(
|
83
|
+
column_table[:field_scope_type].eq(field_scope_type)
|
84
|
+
) unless field_scope_type.nil?
|
85
|
+
|
86
|
+
on_query = on_query.and(
|
87
|
+
column_table[:field_scope_id].eq(field_scope_id)
|
88
|
+
) unless field_scope_id.nil?
|
89
|
+
|
90
|
+
column_table_join_on = column_table
|
91
|
+
.create_on(
|
92
|
+
on_query
|
93
|
+
)
|
94
|
+
|
95
|
+
join_scope_type = (field_scope_id.nil?)? Arel::Nodes::OuterJoin : Arel::Nodes::InnerJoin
|
96
|
+
|
97
|
+
column_table_join = table.create_join(column_table, column_table_join_on)
|
98
|
+
self.joins_values += [column_table_join]
|
99
|
+
|
100
|
+
# Join on all the data with the provided key
|
101
|
+
column_table_datum_join_on = column_datum_table
|
102
|
+
.create_on(
|
103
|
+
column_datum_table[:owner_id].eq(table[:id]).and(
|
104
|
+
column_datum_table[:owner_type].eq(dynamic_type.to_s)
|
105
|
+
).and(
|
106
|
+
column_datum_table[:dynamic_column_id].eq(column_table[:id])
|
107
|
+
)
|
108
|
+
)
|
109
|
+
|
110
|
+
column_table_datum_join = table.create_join(column_datum_table, column_table_datum_join_on, join_scope_type)
|
111
|
+
self.joins_values += [column_table_datum_join]
|
112
|
+
|
113
|
+
# Join on the actual data
|
114
|
+
column_table_datum_store_join_on = column_datum_store_table
|
115
|
+
.create_on(
|
116
|
+
column_datum_table[:datum_id].eq(column_datum_store_table[:id]).and(
|
117
|
+
column_datum_table[:datum_type].eq(column_datum_store_table_type)
|
118
|
+
)
|
119
|
+
)
|
120
|
+
|
121
|
+
column_table_datum_store_join = table.create_join(column_datum_store_table, column_table_datum_store_join_on, join_scope_type)
|
122
|
+
self.joins_values += [column_table_datum_store_join]
|
123
|
+
|
124
|
+
column_name = :value
|
125
|
+
|
126
|
+
# If this dynamic column points to another model we need to join on that table
|
127
|
+
if !field_scope.nil? && assoc = field_scope.activerecord_dynamic_columns.where(data_type: "model", key: col_name).first
|
128
|
+
assoc_table = assoc.class_name.constantize.arel_table.alias("dynamic_where_associated_data_#{index}_#{col_name}")
|
129
|
+
|
130
|
+
join_on = assoc_table.create_on(
|
131
|
+
column_datum_store_table[:value_id].eq(assoc_table[:id]).and(
|
132
|
+
column_datum_store_table[:value_type].eq(assoc.class_name)
|
133
|
+
)
|
134
|
+
)
|
135
|
+
join = table.create_join(assoc_table, join_on)
|
136
|
+
self.joins_values += [join]
|
137
|
+
|
138
|
+
column_table_datum_store_join = join
|
139
|
+
column_datum_store_table = assoc_table
|
140
|
+
column_name = assoc.column_name.to_sym
|
141
|
+
end
|
142
|
+
|
143
|
+
# Track the joins
|
144
|
+
# - So they can be referenced later
|
145
|
+
# - So we don't make more joins than we have to
|
146
|
+
joins[joins_scope_key][col_name] = {
|
147
|
+
:join => column_table_datum_store_join,
|
148
|
+
:table => column_datum_store_table,
|
149
|
+
:column => column_name
|
150
|
+
}
|
151
|
+
|
152
|
+
joins[joins_scope_key][col_name]
|
153
|
+
end
|
154
|
+
|
155
|
+
# Builds all the joins required for the dynamic columns in the where/order clauses
|
156
|
+
def build_dynamic_column_joins
|
157
|
+
joins = {}
|
158
|
+
|
159
|
+
self.where_dynamic_columns_values.each_with_index { |dynamic_scope, index_outer|
|
160
|
+
dynamic_scope[:where].each_with_index { |rel, index_inner|
|
161
|
+
# Process each where
|
162
|
+
dynamic_column_process_arel_nodes(rel, dynamic_scope[:scope], (index_outer*1000)+(index_inner*10000), joins)
|
163
|
+
|
164
|
+
# Warning
|
165
|
+
# Must cast rel to a string - I've encountered ***strange*** situations where this will change the 'col_name' to value in the where clause
|
166
|
+
# specifically, the test 'case should restrict if scope specified' will fail
|
167
|
+
self.where_values += [rel.to_sql]
|
168
|
+
}
|
169
|
+
}
|
170
|
+
self.order_dynamic_columns_values.each_with_index { |dynamic_scope, index_outer|
|
171
|
+
dynamic_scope[:order].each_with_index { |rel, index_inner|
|
172
|
+
# Process each order
|
173
|
+
dynamic_column_process_arel_nodes(rel, dynamic_scope[:scope], (index_outer*1000)+(index_inner*10000), joins)
|
174
|
+
}
|
175
|
+
}
|
176
|
+
|
177
|
+
true
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# When arel starts building - filter
|
183
|
+
def build_arel_with_dynamic_columns
|
184
|
+
self.build_dynamic_column_joins
|
185
|
+
|
186
|
+
if self.where_dynamic_columns_values.length > 0 || self.order_dynamic_columns_values.length > 0
|
187
|
+
self.group_values += [Arel::Nodes::Group.new(table[:id])]
|
188
|
+
end
|
189
|
+
|
190
|
+
build_arel_without_dynamic_columns
|
191
|
+
end
|
192
|
+
|
193
|
+
# OrderChain objects act as placeholder for queries in which #order does not have any parameter.
|
194
|
+
# In this case, #order must be chained with #by_dynamic_columns to return a new relation.
|
195
|
+
class OrderChain
|
196
|
+
def initialize(scope)
|
197
|
+
@scope = scope
|
198
|
+
end
|
199
|
+
|
200
|
+
def by_dynamic_columns(*args)
|
201
|
+
@scope.send(:preprocess_order_args, args)
|
202
|
+
|
203
|
+
# Add now - want to keep the order with the regular column orders
|
204
|
+
@scope.order_values += args
|
205
|
+
|
206
|
+
# Map
|
207
|
+
dynamic_columns_value = {
|
208
|
+
:scope => nil,
|
209
|
+
:order => args,
|
210
|
+
}
|
211
|
+
@scope.order_dynamic_columns_values = dynamic_columns_value
|
212
|
+
|
213
|
+
chain = ::ActiveRecord::QueryMethods::OrderChain.new(@scope)
|
214
|
+
chain.instance_eval do
|
215
|
+
# Make outer scope variable accessible
|
216
|
+
@dynamic_columns_value = dynamic_columns_value
|
217
|
+
|
218
|
+
# Extends where to chain with a has_scope method
|
219
|
+
# This scopes the where from above
|
220
|
+
def with_scope(opt)
|
221
|
+
@dynamic_columns_value[:scope] = opt
|
222
|
+
|
223
|
+
@scope
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
chain
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -1,189 +1,39 @@
|
|
1
|
+
# ActiveRecord v3 specific changes
|
2
|
+
|
1
3
|
module HasDynamicColumns
|
2
4
|
module ActiveRecord
|
3
5
|
module QueryMethods
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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"
|
6
|
+
module ClassMethods
|
7
|
+
def self.included base
|
8
|
+
base.class_eval do
|
9
|
+
def preprocess_order_args(order_args)
|
10
|
+
order_args.flatten!
|
11
|
+
#validate_order_args(order_args)
|
12
|
+
|
13
|
+
references = order_args.grep(String)
|
14
|
+
references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
|
15
|
+
references!(references) if references.any?
|
16
|
+
|
17
|
+
# if a symbol is given we prepend the quoted table name
|
18
|
+
order_args.map! do |arg|
|
19
|
+
case arg
|
20
|
+
when Symbol
|
21
|
+
#arg = klass.attribute_alias(arg) if klass.attribute_alias?(arg)
|
22
|
+
table[arg].asc
|
23
|
+
when Hash
|
24
|
+
arg.map { |field, dir|
|
25
|
+
#field = klass.attribute_alias(field) if klass.attribute_alias?(field)
|
26
|
+
table[field].send(dir.downcase)
|
27
|
+
}
|
28
|
+
else
|
29
|
+
arg
|
30
|
+
end
|
31
|
+
end.flatten!
|
88
32
|
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
33
|
end
|
173
34
|
end
|
174
35
|
end
|
175
36
|
|
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
37
|
# lifted from
|
188
38
|
# http://erniemiller.org/2013/10/07/activerecord-where-not-sane-true/
|
189
39
|
class WhereChain
|
@@ -230,44 +80,6 @@ module HasDynamicColumns
|
|
230
80
|
end
|
231
81
|
end
|
232
82
|
|
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
83
|
def order_with_dynamic_columns(opts = :chain)
|
272
84
|
# Chain - by_dynamic_columns
|
273
85
|
if opts == :chain
|
@@ -1,161 +1,9 @@
|
|
1
|
+
# ActiveRecord v4 specific changes
|
2
|
+
|
1
3
|
module HasDynamicColumns
|
2
4
|
module ActiveRecord
|
3
5
|
module QueryMethods
|
4
|
-
|
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
|
6
|
+
module ClassMethods
|
159
7
|
end
|
160
8
|
|
161
9
|
# lifted from
|
@@ -206,44 +54,6 @@ module HasDynamicColumns
|
|
206
54
|
end
|
207
55
|
end
|
208
56
|
|
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
57
|
def order_with_dynamic_columns(opts = :chain)
|
248
58
|
# Chain - by_dynamic_columns
|
249
59
|
if opts == :chain
|
@@ -229,9 +229,9 @@ module HasDynamicColumns
|
|
229
229
|
|
230
230
|
@@has_dynamic_columns_configurations.each { |config|
|
231
231
|
if !options[:root].nil?
|
232
|
-
json[options[:root]][config[:as].to_s] = self.send(config[:as].to_s)
|
232
|
+
json[options[:root]][config[:as].to_s] = self.send(config[:as].to_s, true)
|
233
233
|
else
|
234
|
-
json[config[:as].to_s] = self.send(config[:as].to_s)
|
234
|
+
json[config[:as].to_s] = self.send(config[:as].to_s, true)
|
235
235
|
end
|
236
236
|
}
|
237
237
|
|
@@ -242,28 +242,55 @@ module HasDynamicColumns
|
|
242
242
|
def #{configuration[:as]}=data
|
243
243
|
data.each_pair { |key, value|
|
244
244
|
# We dont play well with this key
|
245
|
-
if !self.storable_#{configuration[:as].to_s.singularize}_key?(key)
|
246
|
-
|
247
|
-
end
|
245
|
+
raise NoMethodError.new "This key isn't storable" if !self.storable_#{configuration[:as].to_s.singularize}_key?(key)
|
246
|
+
|
248
247
|
dynamic_column = self.#{configuration[:as].to_s.singularize}_key_to_dynamic_column(key)
|
249
248
|
|
250
|
-
#
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
249
|
+
# Expecting array data type
|
250
|
+
raise ArgumentError.new "Multiple columns must be passed arrays" if dynamic_column.multiple && !value.is_a?(Array)
|
251
|
+
|
252
|
+
# Treat everything as an array - makes building easier
|
253
|
+
value = [value] if !value.is_a?(Array)
|
254
|
+
|
255
|
+
# Loop each value - sets existing data or builds a new data node
|
256
|
+
existing = self.activerecord_dynamic_column_data.select { |i| i.dynamic_column == dynamic_column }
|
257
|
+
value.each_with_index { |datum, datum_index|
|
258
|
+
if existing[datum_index]
|
259
|
+
# Undelete this node if its now needed
|
260
|
+
existing[datum_index].reload if existing[datum_index].marked_for_destruction?
|
261
|
+
existing[datum_index].value = datum
|
262
|
+
# No existing placeholder - build a new one
|
263
|
+
else
|
264
|
+
self.activerecord_dynamic_column_data.build(:dynamic_column => dynamic_column, :value => datum)
|
265
|
+
end
|
266
|
+
}
|
267
|
+
|
268
|
+
# Any record no longer needed should be marked for destruction
|
269
|
+
existing.each_with_index { |i,index|
|
270
|
+
if index > value.length
|
271
|
+
i.mark_for_destruction
|
272
|
+
end
|
273
|
+
}
|
256
274
|
}
|
257
275
|
end
|
258
276
|
|
259
|
-
def #{configuration[:as]}
|
277
|
+
def #{configuration[:as]}(as_json = false)
|
260
278
|
h = {}
|
261
279
|
self.field_scope_#{configuration[:as]}.each { |i|
|
262
|
-
h[i.key] = nil
|
280
|
+
h[i.key] = (i.multiple)? [] : nil
|
263
281
|
}
|
264
282
|
|
265
283
|
self.activerecord_dynamic_column_data.each { |i|
|
266
|
-
|
284
|
+
if i.dynamic_column && h.has_key?(i.dynamic_column.key)
|
285
|
+
v = i.value
|
286
|
+
v = v.as_json(:root => nil) if as_json && v.respond_to?(:as_json)
|
287
|
+
|
288
|
+
if i.dynamic_column.multiple
|
289
|
+
h[i.dynamic_column.key] << v
|
290
|
+
else
|
291
|
+
h[i.dynamic_column.key] = v
|
292
|
+
end
|
293
|
+
end
|
267
294
|
}
|
268
295
|
|
269
296
|
h
|
@@ -274,25 +301,33 @@ module HasDynamicColumns
|
|
274
301
|
end
|
275
302
|
|
276
303
|
def field_scope_#{configuration[:as]}
|
304
|
+
obj = self.get_#{configuration[:as]}_field_scope
|
305
|
+
|
277
306
|
# has_many relationship
|
278
|
-
if
|
279
|
-
|
280
|
-
i.send("activerecord_dynamic_columns")
|
307
|
+
if obj.respond_to?(:select) && obj.respond_to?(:collect)
|
308
|
+
obj.collect { |i|
|
309
|
+
i.send("activerecord_dynamic_columns") if i.respond_to?(:activerecord_dynamic_columns)
|
281
310
|
}.flatten.select { |i|
|
282
311
|
i.dynamic_type.to_s.empty? || i.dynamic_type.to_s == self.class.to_s
|
283
312
|
}
|
284
313
|
# belongs_to relationship
|
285
|
-
|
286
|
-
|
314
|
+
elsif obj.respond_to?(:activerecord_dynamic_columns)
|
315
|
+
obj.send("activerecord_dynamic_columns").select { |i|
|
287
316
|
# Only get things with no dynamic type defined or dynamic types defined as this class
|
288
317
|
i.dynamic_type.to_s.empty? || i.dynamic_type.to_s == self.class.to_s
|
289
318
|
}
|
319
|
+
else
|
320
|
+
[]
|
290
321
|
end
|
291
322
|
end
|
292
323
|
|
293
324
|
protected
|
294
325
|
def get_#{configuration[:as]}_field_scope
|
326
|
+
# Sometimes association doesnt exist
|
327
|
+
begin
|
295
328
|
#{configuration[:field_scope]}
|
329
|
+
rescue
|
330
|
+
end
|
296
331
|
end
|
297
332
|
|
298
333
|
# Whether this is storable
|
data/lib/has_dynamic_columns.rb
CHANGED
@@ -22,6 +22,7 @@ require "has_dynamic_columns/dynamic_column_string_datum"
|
|
22
22
|
require "has_dynamic_columns/dynamic_column_text_datum"
|
23
23
|
require "has_dynamic_columns/dynamic_column_time_datum"
|
24
24
|
require "has_dynamic_columns/dynamic_column_timestamp_datum"
|
25
|
+
require "has_dynamic_columns/dynamic_column_model_datum"
|
25
26
|
|
26
27
|
module HasDynamicColumns
|
27
28
|
end
|
data/spec/factories/account.rb
CHANGED
@@ -12,6 +12,9 @@ FactoryGirl.define do
|
|
12
12
|
account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "trusted", :data_type => "boolean")
|
13
13
|
account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "last_contacted", :data_type => "datetime")
|
14
14
|
account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "total_purchases", :data_type => "integer")
|
15
|
+
account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "tags", :data_type => "string", :multiple => true)
|
16
|
+
account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "address", :data_type => "model", :class_name => "CustomerAddress", :column_name => "name")
|
17
|
+
account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "products", :data_type => "model", :multiple => true)
|
15
18
|
end
|
16
19
|
end
|
17
20
|
|
data/spec/factories/customer.rb
CHANGED
@@ -22,9 +22,13 @@ FactoryGirl.define do
|
|
22
22
|
DateTime.now.change(hour: evaluator.index)
|
23
23
|
when 'boolean'
|
24
24
|
true
|
25
|
+
when 'model'
|
26
|
+
nil
|
25
27
|
else
|
26
28
|
"#{evaluator.index } - #{i.data_type}"
|
27
29
|
end
|
30
|
+
|
31
|
+
hash[i.key.to_s] = [hash[i.key.to_s]]if i.multiple
|
28
32
|
}
|
29
33
|
customer.fields = hash
|
30
34
|
|
@@ -44,14 +44,63 @@ describe ::HasDynamicColumns::ActiveRecord::QueryMethods do
|
|
44
44
|
end
|
45
45
|
|
46
46
|
context 'Customer' do
|
47
|
-
it 'should
|
47
|
+
it 'should store, retrieve, and search array data' do
|
48
48
|
table = Customer.arel_table
|
49
49
|
|
50
|
+
customer = Customer.new(:account => account, :name => "1")
|
51
|
+
customer.fields = {
|
52
|
+
"first_name" => "Butch",
|
53
|
+
"last_name" => "Marshall",
|
54
|
+
"email" => "butch.marshall@unittest.com",
|
55
|
+
"trusted" => false,
|
56
|
+
"last_contacted" => DateTime.parse("2012-01-01 01:01:01"),
|
57
|
+
"total_purchases" => 10,
|
58
|
+
"tags" => [
|
59
|
+
"a", "b", "c"
|
60
|
+
]
|
61
|
+
}
|
62
|
+
customer.save
|
63
|
+
|
64
|
+
customer = Customer.new(:account => account, :name => "1")
|
65
|
+
customer.fields = {
|
66
|
+
"first_name" => "Merridyth",
|
67
|
+
"last_name" => "Marshall",
|
68
|
+
"email" => "merridyth.marshall@unittest.com",
|
69
|
+
"trusted" => false,
|
70
|
+
"last_contacted" => DateTime.parse("2014-05-01 01:01:01"),
|
71
|
+
"total_purchases" => 10,
|
72
|
+
"tags" => [
|
73
|
+
"c", "d", "e"
|
74
|
+
]
|
75
|
+
}
|
76
|
+
customer.save
|
77
|
+
|
78
|
+
result = Customer.where
|
79
|
+
.has_dynamic_columns(table[:tags].eq("e"))
|
80
|
+
.with_scope(account)
|
81
|
+
expect(result.length).to eq(1)
|
82
|
+
|
83
|
+
result = Customer.where
|
84
|
+
.has_dynamic_columns(table[:tags].eq("c"))
|
85
|
+
.with_scope(account)
|
86
|
+
expect(result.length).to eq(2)
|
87
|
+
|
88
|
+
result = Customer.where
|
89
|
+
.has_dynamic_columns(
|
90
|
+
table[:tags].eq("c").and(
|
91
|
+
table[:first_name].eq("Butch")
|
92
|
+
)
|
93
|
+
)
|
94
|
+
.with_scope(account)
|
95
|
+
expect(result.length).to eq(1)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should order by first_name' do
|
50
99
|
result = Customer
|
51
100
|
.order
|
52
101
|
.by_dynamic_columns(first_name: :desc)
|
53
102
|
.with_scope(account)
|
54
|
-
|
103
|
+
.collect { |i|
|
55
104
|
json = i.as_json(:root => nil)
|
56
105
|
[json["name"], json["fields"]["first_name"], json["fields"]["last_name"]]
|
57
106
|
}
|
@@ -96,7 +145,7 @@ describe ::HasDynamicColumns::ActiveRecord::QueryMethods do
|
|
96
145
|
expect(result).to eq([["2", "Butch", "Marshall"], ["1", "Butch", "Casidy"], ["1", "George", "Marshall"]])
|
97
146
|
end
|
98
147
|
|
99
|
-
it 'should order by dynamic and regular columns'
|
148
|
+
it 'should order by dynamic and regular columns' do
|
100
149
|
table = Customer.arel_table
|
101
150
|
|
102
151
|
result = Customer
|
@@ -253,7 +302,7 @@ describe ::HasDynamicColumns::ActiveRecord::QueryMethods do
|
|
253
302
|
|
254
303
|
context 'ActiveRecord' do
|
255
304
|
it 'should not clobber where IN queries' do
|
256
|
-
sql = Account.where("id IN (?)", [1,2,3]).to_sql
|
305
|
+
sql = Account.where("id IN (?)", [1,2,3]).to_sql.gsub(/\s\s/,' ')
|
257
306
|
expect(sql).to eq('SELECT "accounts".* FROM "accounts" WHERE (id IN (1,2,3))')
|
258
307
|
end
|
259
308
|
end
|
@@ -16,7 +16,7 @@ describe HasDynamicColumns::DynamicColumnIntegerDatum do
|
|
16
16
|
}
|
17
17
|
customer.save
|
18
18
|
json = customer.as_json(:root => nil)
|
19
|
-
expect(json["fields"]).to eq({"first_name"=>"Butch", "last_name"=>"Marshall", "email"=>nil, "trusted"=>true, "last_contacted"=>nil, "total_purchases"=>123654})
|
19
|
+
expect(json["fields"]).to eq({"first_name"=>"Butch", "last_name"=>"Marshall", "email"=>nil, "trusted"=>true, "last_contacted"=>nil, "total_purchases"=>123654, "tags" => [], "address"=>nil, "products"=>[]})
|
20
20
|
end
|
21
21
|
|
22
22
|
context 'where.has_dynamic_columns' do
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HasDynamicColumns::DynamicColumnModelDatum do
|
4
|
+
let (:account) do
|
5
|
+
FactoryGirl.create(:account_with_customer_dynamic_columns)
|
6
|
+
end
|
7
|
+
|
8
|
+
context 'Customer' do
|
9
|
+
it 'should output json as polymoprhic' do
|
10
|
+
customer = Customer.new(:account => account)
|
11
|
+
customer.fields = {
|
12
|
+
"first_name" => "Butch",
|
13
|
+
"last_name" => "Marshall",
|
14
|
+
"total_purchases" => 123654,
|
15
|
+
"trusted" => true,
|
16
|
+
"address" => CustomerAddress.new(:name => "1796 18th St, San Francisco, CA 94107, United States"),
|
17
|
+
"products" => [
|
18
|
+
Product.new(:name => "P1"),
|
19
|
+
Product.new(:name => "P2"),
|
20
|
+
Product.new(:name => "P3"),
|
21
|
+
Product.new(:name => "P4"),
|
22
|
+
Product.new(:name => "P5"),
|
23
|
+
]
|
24
|
+
}
|
25
|
+
customer.save
|
26
|
+
json = customer.as_json(:root => nil)
|
27
|
+
|
28
|
+
expect(json["fields"]["address"]["name"]).to eq("1796 18th St, San Francisco, CA 94107, United States")
|
29
|
+
expect(json["fields"]["products"].length).to eq(5)
|
30
|
+
# Each of the products should be json
|
31
|
+
json["fields"]["products"].each_with_index { |i, index|
|
32
|
+
expect(i["name"]).to eq("P#{index+1}")
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should be searchable', :focus => true do
|
37
|
+
table = Customer.arel_table
|
38
|
+
|
39
|
+
customer = Customer.new(:account => account)
|
40
|
+
customer.fields = {
|
41
|
+
"first_name" => "Linus",
|
42
|
+
"last_name" => "Rorvalds",
|
43
|
+
"total_purchases" => 123654,
|
44
|
+
"trusted" => true,
|
45
|
+
"address" => CustomerAddress.new(:name => "1796 18th St, San Francisco, CA 94107, United States")
|
46
|
+
}
|
47
|
+
customer.save
|
48
|
+
|
49
|
+
customer = Customer.new(:account => account)
|
50
|
+
customer.fields = {
|
51
|
+
"first_name" => "Bill",
|
52
|
+
"last_name" => "Gates",
|
53
|
+
"total_purchases" => 123654,
|
54
|
+
"trusted" => true,
|
55
|
+
"address" => CustomerAddress.new(:name => "15010 NE 36th Street, Redmond, WA 98052, United States")
|
56
|
+
}
|
57
|
+
customer.save
|
58
|
+
|
59
|
+
customer = Customer.new(:account => account)
|
60
|
+
customer.fields = {
|
61
|
+
"first_name" => "Mark",
|
62
|
+
"last_name" => "Zuceberg",
|
63
|
+
"total_purchases" => 123654,
|
64
|
+
"trusted" => true,
|
65
|
+
"address" => CustomerAddress.new(:name => "1 Hacker Way, Menlo Park, CA 94025, United States")
|
66
|
+
}
|
67
|
+
customer.save
|
68
|
+
|
69
|
+
customer = Customer.new(:account => account)
|
70
|
+
customer.fields = {
|
71
|
+
"first_name" => "Larry",
|
72
|
+
"last_name" => "Page",
|
73
|
+
"total_purchases" => 123654,
|
74
|
+
"trusted" => true,
|
75
|
+
"address" => CustomerAddress.new(:name => "1600 Amphitheatre Pkwy, Mountain View, CA 94043, United States")
|
76
|
+
}
|
77
|
+
customer.save
|
78
|
+
|
79
|
+
result = Customer
|
80
|
+
.where
|
81
|
+
.has_dynamic_columns(table[:address].matches("%United States%"))
|
82
|
+
.with_scope(account)
|
83
|
+
expect(result.length).to eq(4)
|
84
|
+
|
85
|
+
result = Customer
|
86
|
+
.where
|
87
|
+
.has_dynamic_columns(table[:address].matches("% CA %"))
|
88
|
+
.with_scope(account)
|
89
|
+
expect(result.length).to eq(3)
|
90
|
+
|
91
|
+
result = Customer
|
92
|
+
.where
|
93
|
+
.has_dynamic_columns(
|
94
|
+
table[:address].matches("% Hacker %").or(
|
95
|
+
table[:address].matches("1600 %")
|
96
|
+
)
|
97
|
+
)
|
98
|
+
.with_scope(account)
|
99
|
+
expect(result.length).to eq(2)
|
100
|
+
|
101
|
+
result = Customer
|
102
|
+
.where
|
103
|
+
.has_dynamic_columns(
|
104
|
+
table[:address].matches("% Hacker %").or(
|
105
|
+
table[:first_name].eq("Larry")
|
106
|
+
)
|
107
|
+
)
|
108
|
+
.with_scope(account)
|
109
|
+
.order
|
110
|
+
.by_dynamic_columns(
|
111
|
+
last_name: :asc
|
112
|
+
)
|
113
|
+
.with_scope(account)
|
114
|
+
expect(result.length).to eq(2)
|
115
|
+
expect(result.first.as_json["fields"]["address"]["name"]).to eq("1600 Amphitheatre Pkwy, Mountain View, CA 94043, United States")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -23,15 +23,23 @@ end
|
|
23
23
|
# Add this directory so the ActiveSupport autoloading works
|
24
24
|
ActiveSupport::Dependencies.autoload_paths << File.dirname(__FILE__)
|
25
25
|
|
26
|
-
|
27
|
-
ActiveRecord::Base.establish_connection :adapter => '
|
26
|
+
if RUBY_PLATFORM == 'java'
|
27
|
+
ActiveRecord::Base.establish_connection :adapter => 'jdbcsqlite3', :database => ':memory:'
|
28
|
+
else
|
29
|
+
ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
|
30
|
+
end
|
31
|
+
|
28
32
|
ActiveRecord::Migration.verbose = false
|
29
33
|
|
30
34
|
require "generators/has_dynamic_columns/templates/migration"
|
31
35
|
require "generators/has_dynamic_columns/templates/migration_0.3.0"
|
36
|
+
require "generators/has_dynamic_columns/templates/migration_0.3.4"
|
37
|
+
require "generators/has_dynamic_columns/templates/migration_0.3.5"
|
32
38
|
ActiveRecord::Schema.define do
|
33
39
|
AddHasDynamicColumns.up
|
34
40
|
Upgrade030HasDynamicColumns.up
|
41
|
+
Upgrade034HasDynamicColumns.up
|
42
|
+
Upgrade035HasDynamicColumns.up
|
35
43
|
|
36
44
|
create_table :accounts, force: true do |t|
|
37
45
|
t.string :name
|
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.3.
|
4
|
+
version: 0.3.5
|
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-09-
|
11
|
+
date: 2015-09-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -50,14 +50,14 @@ dependencies:
|
|
50
50
|
requirements:
|
51
51
|
- - ">"
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: '0'
|
53
|
+
version: '4.0'
|
54
54
|
type: :development
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
58
|
- - ">"
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version: '0'
|
60
|
+
version: '4.0'
|
61
61
|
- !ruby/object:Gem::Dependency
|
62
62
|
name: bundler
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -97,6 +97,7 @@ extra_rdoc_files: []
|
|
97
97
|
files:
|
98
98
|
- ".gitignore"
|
99
99
|
- ".rspec"
|
100
|
+
- ".travis.yml"
|
100
101
|
- Gemfile
|
101
102
|
- LICENSE.txt
|
102
103
|
- README.md
|
@@ -109,7 +110,11 @@ files:
|
|
109
110
|
- lib/generators/has_dynamic_columns/next_migration_version.rb
|
110
111
|
- lib/generators/has_dynamic_columns/templates/migration.rb
|
111
112
|
- lib/generators/has_dynamic_columns/templates/migration_0.3.0.rb
|
113
|
+
- lib/generators/has_dynamic_columns/templates/migration_0.3.4.rb
|
114
|
+
- lib/generators/has_dynamic_columns/templates/migration_0.3.5.rb
|
112
115
|
- lib/generators/has_dynamic_columns/upgrade_0_3_0_active_record_generator.rb
|
116
|
+
- lib/generators/has_dynamic_columns/upgrade_0_3_4_active_record_generator.rb
|
117
|
+
- lib/generators/has_dynamic_columns/upgrade_0_3_5_active_record_generator.rb
|
113
118
|
- lib/has_dynamic_columns.rb
|
114
119
|
- lib/has_dynamic_columns/active_record.rb
|
115
120
|
- lib/has_dynamic_columns/active_record/query_methods.rb
|
@@ -128,6 +133,7 @@ files:
|
|
128
133
|
- lib/has_dynamic_columns/dynamic_column_enum_datum.rb
|
129
134
|
- lib/has_dynamic_columns/dynamic_column_float_datum.rb
|
130
135
|
- lib/has_dynamic_columns/dynamic_column_integer_datum.rb
|
136
|
+
- lib/has_dynamic_columns/dynamic_column_model_datum.rb
|
131
137
|
- lib/has_dynamic_columns/dynamic_column_option.rb
|
132
138
|
- lib/has_dynamic_columns/dynamic_column_string_datum.rb
|
133
139
|
- lib/has_dynamic_columns/dynamic_column_text_datum.rb
|
@@ -143,6 +149,7 @@ files:
|
|
143
149
|
- spec/factories/customer.rb
|
144
150
|
- spec/has_dynamic_columns/active_record/query_methods_spec.rb
|
145
151
|
- spec/has_dynamic_columns/dynamic_columns_integer_datum_spec.rb
|
152
|
+
- spec/has_dynamic_columns/dynamic_columns_model_datum_spec.rb
|
146
153
|
- spec/has_dynamic_columns/dynamic_columns_string_datum_spec.rb
|
147
154
|
- spec/has_dynamic_columns_spec.rb
|
148
155
|
- spec/spec_helper.rb
|
@@ -175,6 +182,7 @@ test_files:
|
|
175
182
|
- spec/factories/customer.rb
|
176
183
|
- spec/has_dynamic_columns/active_record/query_methods_spec.rb
|
177
184
|
- spec/has_dynamic_columns/dynamic_columns_integer_datum_spec.rb
|
185
|
+
- spec/has_dynamic_columns/dynamic_columns_model_datum_spec.rb
|
178
186
|
- spec/has_dynamic_columns/dynamic_columns_string_datum_spec.rb
|
179
187
|
- spec/has_dynamic_columns_spec.rb
|
180
188
|
- spec/spec_helper.rb
|