adaptive_alias 0.0.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,27 +1,24 @@
1
- require 'active_model'
2
-
3
- module ActiveModel::AttributeMethods
4
- module ClassMethods
5
- def remove_proxy_call(mod, name)
6
- defn = if NAME_COMPILABLE_REGEXP.match?(name)
7
- "undef #{name}"
8
- else
9
- "remove_method(:'#{name}')"
10
- end
11
-
12
- mod.module_eval defn, __FILE__, __LINE__ + 1
13
- end
14
-
15
- def remove_alias_attribute(new_name)
16
- # association_scope -> add_constraints -> last_chain_scope -> where!(key => model[foreign_key])
17
- # self[attr_name] -> read_attribute(attr_name) -> attribute_aliases
18
- # where! -> where_clause_factory.build -> attributes = predicate_builder.resolve_column_aliases(opts) -> attribute_aliases
19
- self.attribute_aliases = attribute_aliases.except(new_name.to_s)
20
-
21
- attribute_method_matchers.each do |matcher|
22
- matcher_new = matcher.method_name(new_name).to_s
23
- remove_proxy_call self, matcher_new
24
- end
25
- end
26
- end
27
- end
1
+ require 'active_model'
2
+
3
+ module AdaptiveAlias
4
+ module ActiveModelPatches
5
+ module RemoveAliasAttribute
6
+ def remove_proxy_call(mod, name)
7
+ mod.module_eval "remove_method(:'#{name}')", __FILE__, __LINE__ + 1
8
+ end
9
+
10
+ def remove_alias_attribute(new_name)
11
+ self.attribute_aliases = attribute_aliases.except(new_name.to_s)
12
+
13
+ attribute_method_matchers.each do |matcher|
14
+ matcher_new = matcher.method_name(new_name).to_s
15
+ remove_proxy_call self, matcher_new
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ module ActiveModel::AttributeMethods::ClassMethods
23
+ include AdaptiveAlias::ActiveModelPatches::RemoveAliasAttribute
24
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_record'
2
+
3
+ module AdaptiveAlias
4
+ module ActiveModelPatches
5
+ module WriteAttribute
6
+ def write_attribute(attr_name, value)
7
+ name = attr_name.to_s
8
+ name = self.class.attribute_aliases[name] || name
9
+ super(name, value)
10
+ end
11
+
12
+ # This method exists to avoid the expensive primary_key check internally, without
13
+ # breaking compatibility with the write_attribute API
14
+ def _write_attribute(attr_name, value) # :nodoc:
15
+ name = attr_name.to_s
16
+ name = self.class.attribute_aliases[name] || name
17
+ super(name, value)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ module ActiveRecord::AttributeMethods::Write
24
+ prepend AdaptiveAlias::ActiveModelPatches::WriteAttribute
25
+ end
@@ -1,13 +1,17 @@
1
- module AdaptiveAlias
2
- module Hooks
3
- module Association
4
- def find_target(*)
5
- AdaptiveAlias.rescue_statement_invalid(nil){ super }
6
- end
7
- end
8
- end
9
- end
10
-
11
- class ActiveRecord::Associations::Association
12
- prepend AdaptiveAlias::Hooks::Association
13
- end
1
+ module AdaptiveAlias
2
+ module Hooks
3
+ module Association
4
+ def find_target(*)
5
+ AdaptiveAlias.rescue_statement_invalid(nil, reflection){ super }
6
+ end
7
+
8
+ def create!(attributes = {}, &block)
9
+ AdaptiveAlias.rescue_statement_invalid(association_scope, reflection){ super }
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ class ActiveRecord::Associations::Association
16
+ prepend AdaptiveAlias::Hooks::Association
17
+ end
@@ -1,13 +1,13 @@
1
- module AdaptiveAlias
2
- module Hooks
3
- module AssociationScope
4
- def last_chain_scope(*)
5
- AdaptiveAlias.rescue_missing_attribute{ super }
6
- end
7
- end
8
- end
9
- end
10
-
11
- class ActiveRecord::Associations::AssociationScope
12
- prepend AdaptiveAlias::Hooks::AssociationScope
13
- end
1
+ module AdaptiveAlias
2
+ module Hooks
3
+ module AssociationScope
4
+ def last_chain_scope(_scope, reflection, owner)
5
+ AdaptiveAlias.rescue_missing_attribute(owner.class){ super }
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ class ActiveRecord::Associations::AssociationScope
12
+ prepend AdaptiveAlias::Hooks::AssociationScope
13
+ end
@@ -1,21 +1,21 @@
1
- module AdaptiveAlias
2
- module Hooks
3
- module Relation
4
- def pluck(*)
5
- AdaptiveAlias.rescue_statement_invalid(self){ super }
6
- end
7
-
8
- def select_all(*)
9
- AdaptiveAlias.rescue_statement_invalid(self){ super }
10
- end
11
-
12
- def exec_queries(*)
13
- AdaptiveAlias.rescue_statement_invalid(self){ super }
14
- end
15
- end
16
- end
17
- end
18
-
19
- class ActiveRecord::Relation
20
- prepend AdaptiveAlias::Hooks::Relation
21
- end
1
+ module AdaptiveAlias
2
+ module Hooks
3
+ module Relation
4
+ def pluck(*)
5
+ AdaptiveAlias.rescue_statement_invalid(self, nil){ super }
6
+ end
7
+
8
+ def select_all(*)
9
+ AdaptiveAlias.rescue_statement_invalid(self, nil){ super }
10
+ end
11
+
12
+ def exec_queries(*)
13
+ AdaptiveAlias.rescue_statement_invalid(self, nil){ super }
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ class ActiveRecord::Relation
20
+ prepend AdaptiveAlias::Hooks::Relation
21
+ end
@@ -1,13 +1,13 @@
1
- module AdaptiveAlias
2
- module Hooks
3
- module SingularAssociation
4
- def reader(*)
5
- AdaptiveAlias.rescue_missing_attribute{ super }
6
- end
7
- end
8
- end
9
- end
10
-
11
- class ActiveRecord::Associations::SingularAssociation
12
- prepend AdaptiveAlias::Hooks::SingularAssociation
13
- end
1
+ module AdaptiveAlias
2
+ module Hooks
3
+ module SingularAssociation
4
+ def reader(*)
5
+ AdaptiveAlias.rescue_missing_attribute(owner.class){ super }
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ class ActiveRecord::Associations::SingularAssociation
12
+ prepend AdaptiveAlias::Hooks::SingularAssociation
13
+ end
@@ -1,21 +1,24 @@
1
- # frozen_string_literal: true
2
-
3
- require 'adaptive_alias/patches/base'
4
-
5
- module AdaptiveAlias
6
- module Patches
7
- class BackwardPatch < Base
8
- def apply!
9
- AdaptiveAlias.current_patches[[@klass, @old_column, @new_column]] = self
10
- @klass.alias_attribute(@old_column, @new_column)
11
- add_hooks!(current_column: @new_column, alias_column: @old_column, log_warning: true)
12
- end
13
-
14
- def remove!
15
- super
16
- @klass.remove_alias_attribute(@old_column)
17
- ForwardPatch.new(@klass, @old_column, @new_column).apply!
18
- end
19
- end
20
- end
21
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'adaptive_alias/patches/base'
4
+
5
+ module AdaptiveAlias
6
+ module Patches
7
+ class BackwardPatch < Base
8
+ def apply!
9
+ AdaptiveAlias.current_patches[[@klass, @old_column, @new_column]] = self
10
+ @klass.alias_attribute(@old_column, @new_column)
11
+ add_hooks!(current_column: @new_column, alias_column: @old_column, log_warning: true)
12
+ end
13
+
14
+ def remove!
15
+ super
16
+ @klass.remove_alias_attribute(@old_column)
17
+ @klass.define_attribute_method(@old_column)
18
+ new_patch = ForwardPatch.new(@klass, @old_column, @new_column)
19
+ new_patch.apply!
20
+ return new_patch
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,75 +1,110 @@
1
- # frozen_string_literal: true
2
-
3
- module AdaptiveAlias
4
- module Patches
5
- class Base
6
- attr_reader :fix_association
7
- attr_reader :fix_missing_attribute
8
- attr_reader :removed
9
-
10
- def initialize(klass, old_column, new_column)
11
- @klass = klass
12
- @old_column = old_column
13
- @new_column = new_column
14
- end
15
-
16
- def add_hooks!(current_column:, alias_column:, log_warning: false)
17
- patch = self
18
- old_column = @old_column
19
- new_column = @new_column
20
-
21
- AdaptiveAlias.get_or_create_model_module(@klass).instance_exec do
22
- remove_method(new_column) if method_defined?(new_column)
23
- define_method(new_column) do
24
- AdaptiveAlias.rescue_missing_attribute{ self[new_column] }
25
- end
26
-
27
- remove_method(old_column) if method_defined?(old_column)
28
- define_method(old_column) do
29
- patch.log_warning if log_warning
30
- AdaptiveAlias.rescue_missing_attribute{ self[old_column] }
31
- end
32
- end
33
-
34
- expected_error_message = "Mysql2::Error: Unknown column '#{@klass.table_name}.#{current_column}' in 'where clause'".freeze
35
-
36
- @fix_missing_attribute = proc do
37
- next false if patch.removed
38
-
39
- patch.remove!
40
- next true
41
- end
42
-
43
- @fix_association = proc do |target, error|
44
- next false if patch.removed || error.message != expected_error_message
45
-
46
- patch.remove!
47
-
48
- if target
49
- hash = target.where_values_hash
50
- hash[alias_column] = hash.delete(current_column) if hash.key?(current_column)
51
- target.instance_variable_set(:@arel, nil)
52
- target.unscope!(:where).where!(hash)
53
- end
54
-
55
- next true
56
- end
57
- end
58
-
59
- def log_warning
60
- if @prev_warning_time == nil || @prev_warning_time < AdaptiveAlias.log_interval.ago
61
- @prev_warning_time = Time.now
62
- AdaptiveAlias.unexpected_old_column_proc&.call
63
- end
64
- end
65
-
66
- def remove!
67
- @removed = true
68
- @klass.send(:reload_schema_from_cache)
69
- @klass.initialize_find_by_cache
70
- @fix_association = nil
71
- @fix_missing_attribute = nil
72
- end
73
- end
74
- end
75
- end
1
+ # frozen_string_literal: true
2
+
3
+ module AdaptiveAlias
4
+ module Patches
5
+ class Base
6
+ attr_reader :fix_association
7
+ attr_reader :fix_missing_attribute
8
+ attr_reader :removed
9
+ attr_reader :removable
10
+
11
+ def initialize(klass, old_column, new_column)
12
+ @klass = klass
13
+ @old_column = old_column
14
+ @new_column = new_column
15
+ end
16
+
17
+ def add_hooks!(current_column:, alias_column:, log_warning: false)
18
+ patch = self
19
+ klass = @klass
20
+ old_column = @old_column
21
+ new_column = @new_column
22
+
23
+ AdaptiveAlias.get_or_create_model_module(klass).instance_exec do
24
+ remove_method(new_column) if method_defined?(new_column)
25
+ define_method(new_column) do
26
+ AdaptiveAlias.rescue_missing_attribute(klass){ self[new_column] }
27
+ end
28
+
29
+ remove_method("#{new_column}=") if method_defined?("#{new_column}=")
30
+ define_method("#{new_column}=") do |*args|
31
+ AdaptiveAlias.rescue_missing_attribute(klass){ super(*args) }
32
+ end
33
+
34
+ remove_method(old_column) if method_defined?(old_column)
35
+ define_method(old_column) do
36
+ patch.log_warning if log_warning
37
+ AdaptiveAlias.rescue_missing_attribute(klass){ self[old_column] }
38
+ end
39
+
40
+ remove_method("#{old_column}=") if method_defined?("#{old_column}=")
41
+ define_method("#{old_column}=") do |*args|
42
+ patch.log_warning if log_warning
43
+ AdaptiveAlias.rescue_missing_attribute(klass){ super(*args) }
44
+ end
45
+ end
46
+
47
+ expected_association_err_msgs = [
48
+ "Mysql2::Error: Unknown column '#{klass.table_name}.#{current_column}' in 'where clause'".freeze,
49
+ "Mysql2::Error: Unknown column '#{current_column}' in 'field list'".freeze,
50
+ ].freeze
51
+
52
+ expected_attribute_err_msgs = [
53
+ "can't write unknown attribute `#{current_column}`".freeze,
54
+ "missing attribute: #{current_column}".freeze,
55
+ ].freeze
56
+
57
+ @fix_missing_attribute = proc do |error_klass, error|
58
+ next false if not patch.removable
59
+ next false if patch.removed
60
+ next false if klass != error_klass
61
+ next false if not expected_attribute_err_msgs.include?(error.message)
62
+
63
+ patch.remove!
64
+ next true
65
+ end
66
+
67
+ @fix_association = proc do |relation, reflection, error|
68
+ next false if not patch.removable
69
+ next false if patch.removed
70
+ next false if not expected_association_err_msgs.include?(error.message)
71
+
72
+ patch.remove!
73
+
74
+ if relation
75
+ relation.where_clause.send(:predicates).each do |node|
76
+ next if node.left.name != current_column.to_s
77
+ next if klass.table_name != node.left.relation.name
78
+
79
+ node.left = node.left.clone
80
+ node.left.name = alias_column.to_s
81
+ end
82
+ end
83
+
84
+ reflection.clear_association_scope_cache if reflection
85
+
86
+ next true
87
+ end
88
+ end
89
+
90
+ def log_warning
91
+ if @prev_warning_time == nil || @prev_warning_time < Time.now - AdaptiveAlias.log_interval
92
+ @prev_warning_time = Time.now
93
+ AdaptiveAlias.unexpected_old_column_proc&.call
94
+ end
95
+ end
96
+
97
+ def remove!
98
+ @removed = true
99
+ @klass.reset_column_information
100
+ @klass.columns_hash
101
+ @fix_association = nil
102
+ @fix_missing_attribute = nil
103
+ end
104
+
105
+ def mark_removable
106
+ @removable = true
107
+ end
108
+ end
109
+ end
110
+ end
@@ -1,21 +1,24 @@
1
- # frozen_string_literal: true
2
-
3
- require 'adaptive_alias/patches/base'
4
-
5
- module AdaptiveAlias
6
- module Patches
7
- class ForwardPatch < Base
8
- def apply!
9
- AdaptiveAlias.current_patches[[@klass, @old_column, @new_column]] = self
10
- @klass.alias_attribute(@new_column, @old_column)
11
- add_hooks!(current_column: @old_column, alias_column: @new_column)
12
- end
13
-
14
- def remove!
15
- super
16
- @klass.remove_alias_attribute(@new_column)
17
- BackwardPatch.new(@klass, @old_column, @new_column).apply!
18
- end
19
- end
20
- end
21
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'adaptive_alias/patches/base'
4
+
5
+ module AdaptiveAlias
6
+ module Patches
7
+ class ForwardPatch < Base
8
+ def apply!
9
+ AdaptiveAlias.current_patches[[@klass, @old_column, @new_column]] = self
10
+ @klass.alias_attribute(@new_column, @old_column)
11
+ add_hooks!(current_column: @old_column, alias_column: @new_column)
12
+ end
13
+
14
+ def remove!
15
+ super
16
+ @klass.remove_alias_attribute(@new_column)
17
+ @klass.define_attribute_method(@new_column)
18
+ new_patch = BackwardPatch.new(@klass, @old_column, @new_column)
19
+ new_patch.apply!
20
+ return new_patch
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,3 +1,3 @@
1
- module AdaptiveAlias
2
- VERSION = '0.0.2'
3
- end
1
+ module AdaptiveAlias
2
+ VERSION = '0.2.0'
3
+ end
@@ -1,65 +1,79 @@
1
- # frozen_string_literal: true
2
-
3
- require 'adaptive_alias/version'
4
- require 'adaptive_alias/active_model_patches/read_attribute'
5
- require 'adaptive_alias/active_model_patches/remove_alias_attribute'
6
- require 'adaptive_alias/patches/backward_patch'
7
- require 'adaptive_alias/patches/forward_patch'
8
-
9
- require 'adaptive_alias/hooks/association'
10
- require 'adaptive_alias/hooks/association_scope'
11
- require 'adaptive_alias/hooks/singular_association'
12
- require 'adaptive_alias/hooks/relation'
13
-
14
- module AdaptiveAlias
15
- @log_interval = 10 * 60
16
- @current_patches = {}
17
- @model_modules ||= {}
18
-
19
- class << self
20
- attr_accessor :unexpected_old_column_proc
21
- attr_accessor :log_interval
22
- attr_accessor :current_patches
23
- end
24
-
25
- class << self
26
- def [](old_column, new_column)
27
- old_column = old_column.to_sym
28
- new_column = new_column.to_sym
29
-
30
- Module.new do
31
- extend ActiveSupport::Concern
32
-
33
- included do
34
- if column_names.include?(new_column)
35
- Patches::BackwardPatch.new(self, old_column, new_column).apply!
36
- else
37
- Patches::ForwardPatch.new(self, old_column, new_column).apply!
38
- end
39
- end
40
- end
41
- end
42
-
43
- def rescue_statement_invalid(relation)
44
- yield
45
- rescue ActiveRecord::StatementInvalid => error
46
- raise error if AdaptiveAlias.current_patches.all?{|_key, patch| !patch.fix_association.call(relation, error) }
47
- retry
48
- end
49
-
50
- def rescue_missing_attribute
51
- yield
52
- rescue ActiveModel::MissingAttributeError => error
53
- raise error if AdaptiveAlias.current_patches.all?{|_key, patch| !patch.fix_missing_attribute.call }
54
- retry
55
- end
56
-
57
- def get_or_create_model_module(klass)
58
- return @model_modules[klass] if @model_modules[klass]
59
-
60
- @model_modules[klass] = Module.new
61
- klass.prepend(@model_modules[klass])
62
- return @model_modules[klass]
63
- end
64
- end
65
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'adaptive_alias/version'
4
+ require 'adaptive_alias/active_model_patches/read_attribute'
5
+ require 'adaptive_alias/active_model_patches/write_attribute'
6
+ require 'adaptive_alias/active_model_patches/remove_alias_attribute'
7
+ require 'adaptive_alias/active_model_patches/apply_scope'
8
+ require 'adaptive_alias/patches/backward_patch'
9
+ require 'adaptive_alias/patches/forward_patch'
10
+
11
+ require 'adaptive_alias/hooks/association'
12
+ require 'adaptive_alias/hooks/association_scope'
13
+ require 'adaptive_alias/hooks/singular_association'
14
+ require 'adaptive_alias/hooks/relation'
15
+
16
+ module AdaptiveAlias
17
+ @log_interval = 10 * 60
18
+ @current_patches = {}
19
+ @model_modules ||= {}
20
+
21
+ class << self
22
+ attr_accessor :unexpected_old_column_proc
23
+ attr_accessor :log_interval
24
+ attr_accessor :current_patches
25
+ end
26
+
27
+ class << self
28
+ def [](old_column, new_column)
29
+ old_column = old_column.to_sym
30
+ new_column = new_column.to_sym
31
+
32
+ Module.new do
33
+ extend ActiveSupport::Concern
34
+
35
+ included do
36
+ patch = (column_names.include?(new_column) ? Patches::BackwardPatch : Patches::ForwardPatch).new(self, old_column, new_column)
37
+ patch.apply!
38
+ patch.mark_removable
39
+ end
40
+ end
41
+ end
42
+
43
+ def rescue_statement_invalid(relation, reflection, &block)
44
+ yield
45
+ rescue ActiveRecord::StatementInvalid => error
46
+ raise error if AdaptiveAlias.current_patches.all?{|_key, patch| !patch.fix_association.call(relation, reflection, error) }
47
+
48
+ result = rescue_statement_invalid(relation, reflection, &block)
49
+ AdaptiveAlias.current_patches.each_value(&:mark_removable)
50
+ return result
51
+ end
52
+
53
+ def rescue_missing_attribute(klass, &block)
54
+ yield
55
+ rescue ActiveModel::MissingAttributeError => error
56
+ raise error if AdaptiveAlias.current_patches.all?{|_key, patch| !patch.fix_missing_attribute.call(klass, error) }
57
+
58
+ result = rescue_missing_attribute(klass, &block)
59
+ AdaptiveAlias.current_patches.each_value(&:mark_removable)
60
+ return result
61
+ end
62
+
63
+ def get_or_create_model_module(klass)
64
+ return @model_modules[klass] if @model_modules[klass]
65
+
66
+ @model_modules[klass] = Module.new
67
+ klass.prepend(@model_modules[klass])
68
+ return @model_modules[klass]
69
+ end
70
+
71
+ def missing_value?(attributes, klass, name)
72
+ return false if attributes.key?(name)
73
+
74
+ old_name = klass.attribute_aliases.key(name)
75
+ return false if old_name == nil
76
+ return !!AdaptiveAlias.current_patches[[klass, old_name.to_sym, name.to_sym]]
77
+ end
78
+ end
79
+ end