has_dynamic_columns 0.3.3 → 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/has_dynamic_columns.svg)](http://badge.fury.io/rb/has_dynamic_columns)
|
2
|
+
[![Build Status](https://travis-ci.org/butchmarshall/has_dynamic_columns.svg?branch=master)](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
|