has_dynamic_columns 0.2.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -3
  3. data/README.md +25 -1
  4. data/has_dynamic_columns.gemspec +3 -1
  5. data/lib/generators/has_dynamic_columns/active_record_generator.rb +1 -1
  6. data/lib/generators/has_dynamic_columns/templates/migration.rb +4 -4
  7. data/lib/generators/has_dynamic_columns/templates/migration_0.3.0.rb +89 -0
  8. data/lib/generators/has_dynamic_columns/upgrade_0_3_0_active_record_generator.rb +22 -0
  9. data/lib/has_dynamic_columns/active_record/query_methods.rb +1 -167
  10. data/lib/has_dynamic_columns/active_record/relation.rb +1 -21
  11. data/lib/has_dynamic_columns/active_record/v3/query_methods.rb +281 -0
  12. data/lib/has_dynamic_columns/active_record/v3/relation.rb +34 -0
  13. data/lib/has_dynamic_columns/active_record/v4/query_methods.rb +257 -0
  14. data/lib/has_dynamic_columns/active_record/v4/relation.rb +34 -0
  15. data/lib/has_dynamic_columns/dynamic_column_boolean_datum.rb +5 -0
  16. data/lib/has_dynamic_columns/dynamic_column_date_datum.rb +5 -0
  17. data/lib/has_dynamic_columns/dynamic_column_datetime_datum.rb +5 -0
  18. data/lib/has_dynamic_columns/dynamic_column_datum.rb +8 -54
  19. data/lib/has_dynamic_columns/dynamic_column_enum_datum.rb +5 -0
  20. data/lib/has_dynamic_columns/dynamic_column_float_datum.rb +5 -0
  21. data/lib/has_dynamic_columns/dynamic_column_integer_datum.rb +5 -0
  22. data/lib/has_dynamic_columns/dynamic_column_string_datum.rb +5 -0
  23. data/lib/has_dynamic_columns/dynamic_column_text_datum.rb +5 -0
  24. data/lib/has_dynamic_columns/dynamic_column_time_datum.rb +5 -0
  25. data/lib/has_dynamic_columns/dynamic_column_timestamp_datum.rb +5 -0
  26. data/lib/has_dynamic_columns/model/class_methods.rb +22 -2
  27. data/lib/has_dynamic_columns/version.rb +1 -1
  28. data/lib/has_dynamic_columns.rb +11 -0
  29. data/rspec_rvm +39 -0
  30. data/spec/factories/account.rb +24 -0
  31. data/spec/factories/customer.rb +35 -0
  32. data/spec/has_dynamic_columns/active_record/query_methods_spec.rb +252 -0
  33. data/spec/has_dynamic_columns/dynamic_columns_integer_datum_spec.rb +124 -0
  34. data/spec/has_dynamic_columns/dynamic_columns_string_datum_spec.rb +7 -0
  35. data/spec/has_dynamic_columns_spec.rb +93 -63
  36. data/spec/spec_helper.rb +13 -8
  37. metadata +67 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 448332c912f1846a9b67d402a0be6560fe2562c5
4
- data.tar.gz: 256541a493f2e38a31683c7076239b42a6da5f6b
3
+ metadata.gz: f872c8cb5a681f06b58bd3e57477bc275a153c3c
4
+ data.tar.gz: c4d47b6fb08df2fd3bdd4d531988adc00c5312b0
5
5
  SHA512:
6
- metadata.gz: 95a6df76f9a6000ebffeac04915cc8c8f8a422ded713a25f4fda3a02ea52d0c9273fbb37398945d16a0ff26754ffd716e6cf4dd4216bb5571b3f540392b74373
7
- data.tar.gz: f27225cb04d612c509225626c44625b25f0cc1b1822a4d364e609a6a25e616e538459e3bc368420f3078fa1c78eef43b5d9971adac6ddddcb0d4369d2fe22441
6
+ metadata.gz: dd399b22a0be68a064d8e9f8e67bbc2558d290035c59fce659be6597d0406d928766d75f514b4c7b7cf67d0aa363abaa6af4c0ec79f85251195ef4bbbc8ca367
7
+ data.tar.gz: 7de3e24535b04a0b3a5abcbd40d573c6688164e954088f5e3f5878c46a2a2e5364a95b5792c19b78db9cb35564953dbabf3595f51d9c59605383fe93b99af191
data/Gemfile CHANGED
@@ -2,13 +2,18 @@ source 'https://rubygems.org'
2
2
 
3
3
  gem 'rake'
4
4
 
5
- group :test do
6
- if ENV['RAILS_VERSION'] == 'edge'
5
+ group :development, :test do
6
+ activerecord_version = ENV['HAS_DYNAMIC_COLUMNS_ACTIVERECORD_VERSION']
7
+
8
+ if ENV['RAILS_VERSION'] == 'edge' || activerecord_version == "edge"
9
+ gem 'arel', :github => 'rails/arel'
7
10
  gem 'activerecord', :github => 'rails/rails'
11
+ elsif activerecord_version && activerecord_version.strip != ""
12
+ gem "activerecord", activerecord_version
8
13
  else
9
14
  gem 'activerecord', (ENV['RAILS_VERSION'] || ['>= 3.0', '< 5.0'])
10
15
  end
11
-
16
+
12
17
  gem 'coveralls', :require => false
13
18
  gem 'rspec', '>= 3'
14
19
  gem 'rubocop', '>= 0.25'
data/README.md CHANGED
@@ -3,6 +3,15 @@ has_dynamic_columns
3
3
 
4
4
  This plugin gives ActiveRecord models the ability to dynamically define collectable data based on ***has_many*** and ***belongs_to*** relationships.
5
5
 
6
+ Release Notes
7
+ ============
8
+
9
+ **0.3.0**
10
+ - Moved to storing data types in separate tables (where/order now correct!)
11
+ - Added order.by_dynamic_columns
12
+ - Improved how joins were built to avoid duplicates
13
+ - Added ActiveRecord 3 and 4 compatibility
14
+
6
15
  Installation
7
16
  ============
8
17
 
@@ -14,6 +23,7 @@ The Active Record migration is required to create the has_dynamic_columns table.
14
23
  running the following command:
15
24
 
16
25
  rails generate has_dynamic_columns:active_record
26
+ rails generate has_dynamic_columns:upgrade_0_3_0_active_record
17
27
  rake db:migrate
18
28
 
19
29
  Usage
@@ -228,4 +238,18 @@ Customer
228
238
 
229
239
  ## **has_many** relationship
230
240
 
231
- TODO example.
241
+ TODO example.
242
+
243
+ **Ordering**
244
+ ```ruby
245
+
246
+ # ------------------------------------------------
247
+ # by dynamic column
248
+ # ------------------------------------------------
249
+
250
+ Customer
251
+ .order
252
+ .by_dynamic_columns(country: :asc)
253
+ .with_scope(Account.find(1))
254
+
255
+ ```
@@ -18,8 +18,10 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "activerecord", "~> 4.2"
21
+ spec.add_dependency "activerecord", [">= 3.0", "< 5.0"]
22
22
 
23
+ spec.add_development_dependency "sqlite3", "> 0"
24
+ spec.add_development_dependency "factory_girl", "> 0"
23
25
  spec.add_development_dependency "bundler", "~> 1.7"
24
26
  spec.add_development_dependency "rake", "~> 10.0"
25
27
  end
@@ -16,7 +16,7 @@ module HasDynamicColumns
16
16
  end
17
17
 
18
18
  def self.next_migration_number(dirname)
19
- ActiveRecord::Generators::Base.next_migration_number dirname
19
+ ::ActiveRecord::Generators::Base.next_migration_number dirname
20
20
  end
21
21
  end
22
22
  end
@@ -8,7 +8,7 @@ class AddHasDynamicColumns < ActiveRecord::Migration
8
8
  t.string :key
9
9
  t.string :data_type
10
10
 
11
- t.timestamps
11
+ t.timestamps null: false
12
12
  end
13
13
  add_index(:dynamic_columns, [:field_scope_id, :field_scope_type, :dynamic_type], name: 'index1')
14
14
  create_table :dynamic_column_validations do |t|
@@ -17,13 +17,13 @@ class AddHasDynamicColumns < ActiveRecord::Migration
17
17
  t.string :error
18
18
  t.string :regexp
19
19
 
20
- t.timestamps
20
+ t.timestamps null: false
21
21
  end
22
22
  create_table :dynamic_column_options do |t|
23
23
  t.integer :dynamic_column_id
24
24
  t.string :key
25
25
 
26
- t.timestamps
26
+ t.timestamps null: false
27
27
  end
28
28
  create_table :dynamic_column_data do |t|
29
29
  t.string :owner_type
@@ -32,7 +32,7 @@ class AddHasDynamicColumns < ActiveRecord::Migration
32
32
  t.integer :dynamic_column_option_id
33
33
  t.string :value
34
34
 
35
- t.timestamps
35
+ t.timestamps null: false
36
36
  end
37
37
  add_index(:dynamic_column_data, [:owner_id, :owner_type, :dynamic_column_id], name: 'index2')
38
38
  end
@@ -0,0 +1,89 @@
1
+ class Upgrade030HasDynamicColumns < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :dynamic_column_data, :datum_type, :string
4
+ add_column :dynamic_column_data, :datum_id, :integer
5
+
6
+ create_table :dynamic_column_boolean_data do |t|
7
+ t.boolean :value
8
+ t.timestamps null: false
9
+ end
10
+ add_index(:dynamic_column_boolean_data, [:value])
11
+
12
+ create_table :dynamic_column_date_data do |t|
13
+ t.date :value
14
+ t.timestamps null: false
15
+ end
16
+ add_index(:dynamic_column_date_data, [:value])
17
+
18
+ create_table :dynamic_column_datetime_data do |t|
19
+ t.datetime :value
20
+ t.timestamps null: false
21
+ end
22
+ add_index(:dynamic_column_datetime_data, [:value])
23
+
24
+ create_table :dynamic_column_enum_data do |t|
25
+ t.string :value
26
+ t.timestamps null: false
27
+ end
28
+ add_index(:dynamic_column_enum_data, [:value])
29
+
30
+ create_table :dynamic_column_float_data do |t|
31
+ t.float :value
32
+ t.timestamps null: false
33
+ end
34
+ add_index(:dynamic_column_float_data, [:value])
35
+
36
+ create_table :dynamic_column_integer_data do |t|
37
+ t.integer :value
38
+ t.timestamps null: false
39
+ end
40
+ add_index(:dynamic_column_integer_data, [:value])
41
+
42
+ create_table :dynamic_column_string_data do |t|
43
+ t.string :value
44
+ t.timestamps null: false
45
+ end
46
+ add_index(:dynamic_column_string_data, [:value])
47
+
48
+ create_table :dynamic_column_text_data do |t|
49
+ t.text :value
50
+ t.timestamps null: false
51
+ end
52
+ add_index(:dynamic_column_text_data, [:value], :length => 255)
53
+
54
+ create_table :dynamic_column_time_data do |t|
55
+ t.time :value
56
+ t.timestamps null: false
57
+ end
58
+ add_index(:dynamic_column_time_data, [:value])
59
+
60
+ create_table :dynamic_column_timestamp_data do |t|
61
+ t.timestamp :value
62
+ t.timestamps null: false
63
+ end
64
+ add_index(:dynamic_column_timestamp_data, [:value])
65
+
66
+ # Migrate data
67
+ ["string","integer","boolean","text"].each { |data_type|
68
+ ActiveRecord::Base.connection.select_all("
69
+ SELECT
70
+ `dynamic_column_data`.*
71
+ FROM `dynamic_columns`
72
+ INNER JOIN `dynamic_column_data`
73
+ ON `dynamic_column_data`.`dynamic_column_id` = `dynamic_columns`.`id`
74
+ WHERE `dynamic_columns`.`data_type` = '#{data_type}'
75
+ ").each { |i|
76
+ obj = "::HasDynamicColumns::DynamicColumn#{data_type.camelize}Datum".constantize.create(:value => i["value"])
77
+ if datum = ::HasDynamicColumns::DynamicColumnDatum.where(:id => i["id"]).first
78
+ datum.datum = obj
79
+ datum.save
80
+ end
81
+ }
82
+ }
83
+
84
+ remove_column :dynamic_column_data, :value
85
+ end
86
+
87
+ def self.down
88
+ end
89
+ 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_0_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.0.rb", "db/migrate/upgrade_0_3_0_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,167 +1 @@
1
- module HasDynamicColumns
2
- module ActiveRecord
3
- module QueryMethods
4
- def self.included(base)
5
- base.class_eval do
6
- alias_method_chain :where, :dynamic_columns
7
- alias_method_chain :build_arel, :dynamic_columns
8
-
9
- # Recurses through arel nodes until it finds one it can work with
10
- def dynamic_column_process_nodes(rel, scope, index)
11
- case rel
12
- when Arel::Nodes::Grouping
13
- dynamic_column_process_nodes(rel.expr, scope, index+1)
14
- when Arel::Nodes::Or
15
- dynamic_column_process_nodes(rel.left, scope, index+1)
16
- dynamic_column_process_nodes(rel.right, scope, index+10000) # Hack - queries with over 10,000 dynamic where conditions may break
17
- when Arel::Nodes::And
18
- dynamic_column_process_nodes(rel.left, scope, index+1)
19
- dynamic_column_process_nodes(rel.right, scope, index+10000) # Hack - queries with over 10,000 dynamic where conditions may break
20
- # We can work with this
21
- else
22
- dynamic_column_build_arel_joins_and_modify_wheres(rel, scope, index+1)
23
- end
24
- end
25
-
26
- # Builds the joins required for this dynamic column
27
- # Modifies the where to use the dynamic_column_data table alias
28
- #
29
- # rel - an arel node
30
- # scope - scope to run the conditions in
31
- # index - unique table identifier
32
- def dynamic_column_build_arel_joins_and_modify_wheres(rel, scope, index)
33
- col_name = rel.left.name
34
- value = rel.right
35
-
36
- field_scope = scope
37
- field_scope_id = (!field_scope.nil?) ? field_scope.id : nil
38
- field_scope_type = (!field_scope.nil?) ? field_scope.class.name.constantize.to_s : nil
39
-
40
- column_table = HasDynamicColumns::DynamicColumn.arel_table.alias("dynamic_where_#{index}_#{col_name}")
41
- column_datum_table = HasDynamicColumns::DynamicColumnDatum.arel_table.alias("dynamic_where_data_#{index}_#{col_name}")
42
-
43
- dynamic_type = rel.left.relation.engine.to_s
44
- rel.left.relation = column_datum_table # modify the where to use the aliased table
45
- rel.left.name = :value # value is the data storage column searchable on dynamic_column_data table
46
-
47
- rel.right = case rel.right
48
- # Map true -> 1
49
- when ::TrueClass
50
- 1
51
- # Map false -> 0
52
- when ::FalseClass
53
- 0
54
- else
55
- rel.right
56
- end
57
-
58
- # Join on the column with the key
59
- on_query = column_table[:key].eq(col_name)
60
- on_query = on_query.and(
61
- column_table[:field_scope_type].eq(field_scope_type)
62
- ) unless field_scope_type.nil?
63
-
64
- on_query = on_query.and(
65
- column_table[:field_scope_id].eq(field_scope_id)
66
- ) unless field_scope_id.nil?
67
-
68
- column_table_join_on = column_table
69
- .create_on(
70
- on_query
71
- )
72
-
73
- column_table_join = table.create_join(column_table, column_table_join_on)
74
- self.joins_values += [column_table_join]
75
-
76
- # Join on all the data with the provided key
77
- column_table_datum_join_on = column_datum_table
78
- .create_on(
79
- column_datum_table[:owner_id].eq(table[:id]).and(
80
- column_datum_table[:owner_type].eq(dynamic_type)
81
- ).and(
82
- column_datum_table[:dynamic_column_id].eq(column_table[:id])
83
- )
84
- )
85
-
86
- column_table_datum_join = table.create_join(column_datum_table, column_table_datum_join_on, Arel::Nodes::OuterJoin)
87
- self.joins_values += [column_table_datum_join]
88
-
89
- end
90
- end
91
- end
92
-
93
- # When arel starts building - filter
94
- def build_arel_with_dynamic_columns
95
- # Calculate any dynamic scope that was passed
96
- self.has_dynamic_columns_values.each_with_index { |dynamic_scope, index_outer|
97
- dynamic_scope[:where].each_with_index { |rel, index_inner|
98
- # Process each where
99
- dynamic_column_process_nodes(rel, dynamic_scope[:scope], (index_outer*1000)+(index_inner*10000))
100
-
101
- # It's now safe to use the original where query
102
- # All the conditions in rel have been modified to be a where of the aliases dynamic_where_data table
103
-
104
- # Warning
105
- # Must cast rel to a string - I've encountered ***strange*** situations where this will change the 'col_name' to value in the where clause
106
- # specifically, the test 'case should restrict if scope specified' will fail
107
- self.where_values += [rel.to_sql]
108
- }
109
- }
110
- # At least one dynamic where run - we need to group or we're going to get duplicates
111
- if self.has_dynamic_columns_values.length > 0
112
- self.group_values += [Arel::Nodes::Group.new(table[:id])]
113
- end
114
-
115
- build_arel_without_dynamic_columns
116
- end
117
-
118
- # lifted from
119
- # http://erniemiller.org/2013/10/07/activerecord-where-not-sane-true/
120
- module WhereChainCompatibility
121
- #include ::ActiveRecord::QueryMethods
122
- #define_method :build_where,
123
- # ::ActiveRecord::QueryMethods.instance_method(:build_where)
124
-
125
- # Extends where to chain a has_dynamic_columns method
126
- # This builds all the joins needed to search the has_dynamic_columns_data tables
127
- def has_dynamic_columns(opts = :chain, *rest)
128
- # Map
129
- dynamic_columns_value = {
130
- :scope => nil,
131
- :where => @scope.send(:build_where, opts, rest)
132
- }
133
- @scope.has_dynamic_columns_values = dynamic_columns_value
134
-
135
- chain = ::ActiveRecord::QueryMethods::WhereChain.new(@scope)
136
- chain.instance_eval do
137
- # Make outer scope variable accessible
138
- @dynamic_columns_value = dynamic_columns_value
139
-
140
- # Extends where to chain with a has_scope method
141
- # This scopes the where from above
142
- def with_scope(opt)
143
- @dynamic_columns_value[:scope] = opt
144
-
145
- @scope
146
- end
147
- def without_scope
148
- @scope
149
- end
150
- end
151
-
152
- chain
153
- end
154
- end
155
-
156
- def where_with_dynamic_columns(opts = :chain, *rest)
157
- if opts == :chain
158
- scope = spawn
159
- chain = ::ActiveRecord::QueryMethods::WhereChain.new(scope)
160
- chain.send(:extend, WhereChainCompatibility)
161
- else
162
- where_without_dynamic_columns(opts, rest)
163
- end
164
- end
165
- end
166
- end
167
- end
1
+ require_relative "v#{ActiveRecord::VERSION::MAJOR}/query_methods"
@@ -1,21 +1 @@
1
- module HasDynamicColumns
2
- module ActiveRecord
3
- module Relation
4
- def self.included(base)
5
- base.class_eval do
6
- def has_dynamic_columns_values
7
- @values[:has_dynamic_columns_values] || []
8
- end
9
- def has_dynamic_columns_values=values
10
- raise ImmutableRelation if @loaded
11
- @values[:has_dynamic_columns_values] ||= []
12
- @values[:has_dynamic_columns_values] << values
13
- end
14
- end
15
-
16
- base.instance_eval do
17
- end
18
- end
19
- end
20
- end
21
- end
1
+ require_relative "v#{ActiveRecord::VERSION::MAJOR}/relation"