temporal_tables 0.6.10 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +3 -1
  4. data/README.md +1 -1
  5. data/gemfiles/Gemfile.5.0.mysql.lock +3 -3
  6. data/gemfiles/Gemfile.5.0.pg.lock +3 -3
  7. data/gemfiles/Gemfile.5.1.mysql.lock +3 -3
  8. data/gemfiles/Gemfile.5.1.pg.lock +3 -3
  9. data/gemfiles/Gemfile.5.2.mysql.lock +3 -3
  10. data/gemfiles/Gemfile.5.2.pg.lock +3 -3
  11. data/gemfiles/Gemfile.6.0.mysql +16 -0
  12. data/gemfiles/Gemfile.6.0.mysql.lock +171 -0
  13. data/gemfiles/Gemfile.6.0.pg +16 -0
  14. data/gemfiles/Gemfile.6.0.pg.lock +171 -0
  15. data/lib/temporal_tables/arel_table.rb +19 -0
  16. data/lib/temporal_tables/association_extensions.rb +16 -16
  17. data/lib/temporal_tables/connection_adapters/mysql_adapter.rb +54 -54
  18. data/lib/temporal_tables/connection_adapters/postgresql_adapter.rb +64 -64
  19. data/lib/temporal_tables/history_hook.rb +43 -43
  20. data/lib/temporal_tables/join_extensions.rb +14 -14
  21. data/lib/temporal_tables/preloader_extensions.rb +14 -14
  22. data/lib/temporal_tables/reflection_extensions.rb +14 -14
  23. data/lib/temporal_tables/relation_extensions.rb +79 -79
  24. data/lib/temporal_tables/temporal_adapter.rb +181 -174
  25. data/lib/temporal_tables/temporal_class.rb +101 -101
  26. data/lib/temporal_tables/version.rb +1 -1
  27. data/lib/temporal_tables/whodunnit.rb +16 -16
  28. data/lib/temporal_tables.rb +46 -45
  29. data/spec/basic_history_spec.rb +138 -138
  30. data/spec/internal/app/models/flying_machine.rb +1 -1
  31. data/spec/internal/app/models/person.rb +3 -3
  32. data/spec/internal/db/schema.rb +17 -17
  33. data/spec/spec_helper.rb +4 -4
  34. data/spec/support/database.rb +7 -7
  35. data/temporal_tables.gemspec +15 -15
  36. metadata +16 -6
@@ -1,176 +1,183 @@
1
1
  module TemporalTables
2
- module TemporalAdapter
3
- def create_table(table_name, options = {}, &block)
4
- if options[:temporal_bypass]
5
- super table_name, options, &block
6
- else
7
- skip_table = TemporalTables.skipped_temporal_tables.include?(table_name.to_sym) || table_name.to_s =~ /_h$/
8
-
9
- super table_name, options do |t|
10
- block.call t
11
-
12
- if TemporalTables.add_updated_by_field && !skip_table
13
- t.column :updated_by, TemporalTables.updated_by_type
14
- end
15
- end
16
-
17
- if options[:temporal] || (TemporalTables.create_by_default && !skip_table)
18
- add_temporal_table table_name, options
19
- end
20
- end
21
- end
22
-
23
- def add_temporal_table(table_name, options = {})
24
- create_table temporal_name(table_name), options.merge(id: false, primary_key: "history_id", temporal_bypass: true) do |t|
25
- t.integer :id
26
- t.datetime :eff_from, :null => false, limit: 6
27
- t.datetime :eff_to, :null => false, limit: 6, :default => "9999-12-31"
28
-
29
- for c in columns(table_name)
30
- t.send c.type, c.name, :limit => c.limit
31
- end
32
- end
33
-
34
- if TemporalTables.add_updated_by_field && !column_exists?(table_name, :updated_by)
35
- change_table table_name do |t|
36
- t.column :updated_by, TemporalTables.updated_by_type
37
- end
38
- end
39
-
40
- add_index temporal_name(table_name), [:id, :eff_to]
41
- create_temporal_triggers table_name
42
- create_temporal_indexes table_name
43
- end
44
-
45
- def remove_temporal_table(table_name)
46
- if table_exists?(temporal_name(table_name))
47
- drop_temporal_triggers table_name
48
- drop_table_without_temporal temporal_name(table_name)
49
- end
50
- end
51
-
52
- def drop_table(table_name, options = {})
53
- super table_name, options
54
-
55
- if table_exists?(temporal_name(table_name))
56
- super temporal_name(table_name), options
57
- end
58
- end
59
-
60
- def rename_table(name, new_name)
61
- if table_exists?(temporal_name(name))
62
- drop_temporal_triggers name
63
- end
64
-
65
- super name, new_name
66
-
67
- if table_exists?(temporal_name(name))
68
- super temporal_name(name), temporal_name(new_name)
69
- create_temporal_triggers new_name
70
- end
71
- end
72
-
73
- def add_column(table_name, column_name, type, options = {})
74
- super table_name, column_name, type, options
75
-
76
- if table_exists?(temporal_name(table_name))
77
- super temporal_name(table_name), column_name, type, options
78
- create_temporal_triggers table_name
79
- end
80
- end
81
-
82
- def remove_column(table_name, *column_names)
83
- super table_name, *column_names
84
-
85
- if table_exists?(temporal_name(table_name))
86
- super temporal_name(table_name), *column_names
87
- create_temporal_triggers table_name
88
- end
89
- end
90
-
91
- def change_column(table_name, column_name, type, options = {})
92
- super table_name, column_name, type, options
93
-
94
- if table_exists?(temporal_name(table_name))
95
- super temporal_name(table_name), column_name, type, options
96
- # Don't need to update triggers here...
97
- end
98
- end
99
-
100
- def rename_column(table_name, column_name, new_column_name)
101
- super table_name, column_name, new_column_name
102
-
103
- if table_exists?(temporal_name(table_name))
104
- super temporal_name(table_name), column_name, new_column_name
105
- create_temporal_triggers table_name
106
- end
107
- end
108
-
109
- def add_index(table_name, column_name, options = {})
110
- super table_name, column_name, options
111
-
112
- if table_exists?(temporal_name(table_name))
113
- column_names = Array.wrap(column_name)
114
- idx_name = temporal_index_name(options[:name] || index_name(table_name, :column => column_names))
115
-
116
- super temporal_name(table_name), column_name, options.except(:unique).merge(name: idx_name)
117
- end
118
- end
119
-
120
- def remove_index(table_name, options = {})
121
- super table_name, options
122
-
123
- if table_exists?(temporal_name(table_name))
124
- idx_name = temporal_index_name(index_name(table_name, options))
125
-
126
- super temporal_name(table_name), :name => idx_name
127
- end
128
- end
129
-
130
- def create_temporal_indexes(table_name)
131
- indexes = ActiveRecord::Base.connection.indexes(table_name)
132
-
133
- indexes.each do |index|
134
- index_name = temporal_index_name(index.name)
135
-
136
- unless temporal_index_exists?(table_name, index_name)
137
- add_index(
138
- temporal_name(table_name),
139
- index.columns, {
140
- # exclude unique constraints for temporal tables
141
- :name => index_name,
142
- :length => index.lengths,
143
- :order => index.orders
144
- })
145
- end
146
- end
147
- end
148
-
149
- def temporal_name(table_name)
150
- "#{table_name}_h"
151
- end
152
-
153
- def create_temporal_triggers(table_name)
154
- raise NotImplementedError, "create_temporal_triggers is not implemented"
155
- end
156
-
157
- def drop_temporal_triggers(table_name)
158
- raise NotImplementedError, "drop_temporal_triggers is not implemented"
159
- end
160
-
161
- # It's important not to increase the length of the returned string.
162
- def temporal_index_name(index_name)
163
- index_name.to_s.sub(/^index/, "ind_h").sub(/_ix(\d+)$/, '_hi\1')
164
- end
165
-
166
- def temporal_index_exists?(table_name, index_name)
167
- raise "Rails version not supported" unless Rails::VERSION::MAJOR == 5
168
- case Rails::VERSION::MINOR
169
- when 0
170
- index_name_exists?(temporal_name(table_name), index_name, false)
171
- else
172
- index_name_exists?(temporal_name(table_name), index_name)
173
- end
174
- end
175
- end
2
+ module TemporalAdapter
3
+ def create_table(table_name, options = {}, &block)
4
+ if options[:temporal_bypass]
5
+ super table_name, options, &block
6
+ else
7
+ skip_table = TemporalTables.skipped_temporal_tables.include?(table_name.to_sym) || table_name.to_s =~ /_h$/
8
+
9
+ super table_name, options do |t|
10
+ block.call t
11
+
12
+ if TemporalTables.add_updated_by_field && !skip_table
13
+ t.column :updated_by, TemporalTables.updated_by_type
14
+ end
15
+ end
16
+
17
+ if options[:temporal] || (TemporalTables.create_by_default && !skip_table)
18
+ add_temporal_table table_name, options
19
+ end
20
+ end
21
+ end
22
+
23
+ def add_temporal_table(table_name, options = {})
24
+ create_table temporal_name(table_name), options.merge(id: false, primary_key: "history_id", temporal_bypass: true) do |t|
25
+ t.integer :id
26
+ t.datetime :eff_from, :null => false, limit: 6
27
+ t.datetime :eff_to, :null => false, limit: 6, :default => "9999-12-31"
28
+
29
+ for c in columns(table_name)
30
+ next if c.name == "id"
31
+ t.send c.type, c.name, :limit => c.limit
32
+ end
33
+ end
34
+
35
+ if TemporalTables.add_updated_by_field && !column_exists?(table_name, :updated_by)
36
+ change_table table_name do |t|
37
+ t.column :updated_by, TemporalTables.updated_by_type
38
+ end
39
+ end
40
+
41
+ add_index temporal_name(table_name), [:id, :eff_to]
42
+ create_temporal_triggers table_name
43
+ create_temporal_indexes table_name
44
+ end
45
+
46
+ def remove_temporal_table(table_name)
47
+ if table_exists?(temporal_name(table_name))
48
+ drop_temporal_triggers table_name
49
+ drop_table_without_temporal temporal_name(table_name)
50
+ end
51
+ end
52
+
53
+ def drop_table(table_name, options = {})
54
+ super table_name, options
55
+
56
+ if table_exists?(temporal_name(table_name))
57
+ super temporal_name(table_name), options
58
+ end
59
+ end
60
+
61
+ def rename_table(name, new_name)
62
+ if table_exists?(temporal_name(name))
63
+ drop_temporal_triggers name
64
+ end
65
+
66
+ super name, new_name
67
+
68
+ if table_exists?(temporal_name(name))
69
+ super temporal_name(name), temporal_name(new_name)
70
+ create_temporal_triggers new_name
71
+ end
72
+ end
73
+
74
+ def add_column(table_name, column_name, type, options = {})
75
+ super table_name, column_name, type, options
76
+
77
+ if table_exists?(temporal_name(table_name))
78
+ super temporal_name(table_name), column_name, type, options
79
+ create_temporal_triggers table_name
80
+ end
81
+ end
82
+
83
+ def remove_column(table_name, *column_names)
84
+ super table_name, *column_names
85
+
86
+ if table_exists?(temporal_name(table_name))
87
+ super temporal_name(table_name), *column_names
88
+ create_temporal_triggers table_name
89
+ end
90
+ end
91
+
92
+ def change_column(table_name, column_name, type, options = {})
93
+ super table_name, column_name, type, options
94
+
95
+ if table_exists?(temporal_name(table_name))
96
+ super temporal_name(table_name), column_name, type, options
97
+ # Don't need to update triggers here...
98
+ end
99
+ end
100
+
101
+ def rename_column(table_name, column_name, new_column_name)
102
+ super table_name, column_name, new_column_name
103
+
104
+ if table_exists?(temporal_name(table_name))
105
+ super temporal_name(table_name), column_name, new_column_name
106
+ create_temporal_triggers table_name
107
+ end
108
+ end
109
+
110
+ def add_index(table_name, column_name, options = {})
111
+ super table_name, column_name, options
112
+
113
+ if table_exists?(temporal_name(table_name))
114
+ column_names = Array.wrap(column_name)
115
+ idx_name = temporal_index_name(options[:name] || index_name(table_name, :column => column_names))
116
+
117
+ super temporal_name(table_name), column_name, options.except(:unique).merge(name: idx_name)
118
+ end
119
+ end
120
+
121
+ def remove_index(table_name, options = {})
122
+ super table_name, options
123
+
124
+ if table_exists?(temporal_name(table_name))
125
+ idx_name = temporal_index_name(index_name(table_name, options))
126
+
127
+ super temporal_name(table_name), :name => idx_name
128
+ end
129
+ end
130
+
131
+ def create_temporal_indexes(table_name)
132
+ indexes = ActiveRecord::Base.connection.indexes(table_name)
133
+
134
+ indexes.each do |index|
135
+ index_name = temporal_index_name(index.name)
136
+
137
+ unless temporal_index_exists?(table_name, index_name)
138
+ add_index(
139
+ temporal_name(table_name),
140
+ index.columns, {
141
+ # exclude unique constraints for temporal tables
142
+ :name => index_name,
143
+ :length => index.lengths,
144
+ :order => index.orders
145
+ })
146
+ end
147
+ end
148
+ end
149
+
150
+ def temporal_name(table_name)
151
+ "#{table_name}_h"
152
+ end
153
+
154
+ def create_temporal_triggers(table_name)
155
+ raise NotImplementedError, "create_temporal_triggers is not implemented"
156
+ end
157
+
158
+ def drop_temporal_triggers(table_name)
159
+ raise NotImplementedError, "drop_temporal_triggers is not implemented"
160
+ end
161
+
162
+ # It's important not to increase the length of the returned string.
163
+ def temporal_index_name(index_name)
164
+ index_name.to_s.sub(/^index/, "ind_h").sub(/_ix(\d+)$/, '_hi\1')
165
+ end
166
+
167
+ def temporal_index_exists?(table_name, index_name)
168
+ case Rails::VERSION::MAJOR
169
+ when 5
170
+ case Rails::VERSION::MINOR
171
+ when 0
172
+ index_name_exists?(temporal_name(table_name), index_name, false)
173
+ else
174
+ index_name_exists?(temporal_name(table_name), index_name)
175
+ end
176
+ when 6
177
+ index_name_exists?(temporal_name(table_name), index_name)
178
+ else
179
+ raise "Rails version not supported"
180
+ end
181
+ end
182
+ end
176
183
  end
@@ -1,103 +1,103 @@
1
1
  module TemporalTables
2
- # This is mixed into all History classes.
3
- module TemporalClass
4
- def self.included(base)
5
- base.class_eval do
6
- base.extend ClassMethods
7
-
8
- self.table_name += "_h"
9
-
10
- cattr_accessor :visited_associations
11
- @@visited_associations = []
12
-
13
- # The at_value field stores the time from the query that yielded
14
- # this record.
15
- attr_accessor :at_value
16
-
17
- class << self
18
- prepend STIWithHistory
19
-
20
- delegate :at, to: :all
21
- end
22
-
23
- # Iterates all associations, makes sure their history classes are
24
- # created and initialized, and modifies the associations to point
25
- # to the target classes' history classes.
26
- def self.temporalize_associations!
27
- reflect_on_all_associations.dup.each do |association|
28
- unless @@visited_associations.include?(association.name) || association.options[:polymorphic]
29
- @@visited_associations << association.name
30
-
31
- # Calling .history here will ensure that the history class
32
- # for this association is created and initialized
33
- clazz = association.class_name.constantize.history
34
-
35
- # Recreate the association, updating it to point at the
36
- # history class. The foreign key is explicitly set since it's
37
- # inferred from the class_name, but shouldn't be in this case.
38
- send(association.macro, association.name,
39
- association.options.merge(
40
- class_name: clazz.name,
41
- foreign_key: association.foreign_key,
42
- primary_key: clazz.orig_class.primary_key
43
- )
44
- )
45
- end
46
- end
47
- end
48
- end
49
- end
50
-
51
- module STIWithHistory
52
- def sti_name
53
- super.sub /History$/, ""
54
- end
55
-
56
- def find_sti_class(type_name)
57
- type_name += "History" unless type_name =~ /History\Z/
58
-
59
- begin
60
- super
61
- rescue ActiveRecord::SubclassNotFound
62
- superclass.send(:find_sti_class, type_name)
63
- end
64
- end
65
- end
66
-
67
- module ClassMethods
68
- def orig_class
69
- name.sub(/History$/, "").constantize
70
- end
71
-
72
- def descends_from_active_record?
73
- superclass.descends_from_active_record?
74
- end
75
-
76
- def build_temporal_constraint(at_value)
77
- arel_table[:eff_to].gteq(at_value).and(
78
- arel_table[:eff_from].lteq(at_value)
79
- )
80
- end
81
- end
82
-
83
- def orig_class
84
- self.class.orig_class
85
- end
86
-
87
- def orig_id
88
- attributes[orig_class.primary_key]
89
- end
90
-
91
- def orig_obj
92
- @orig_obj ||= orig_class.find_by_id orig_id
93
- end
94
-
95
- def prev
96
- @prev ||= history.where(self.class.arel_table[:eff_from].lt(eff_from)).last
97
- end
98
-
99
- def next
100
- @next ||= history.where(self.class.arel_table[:eff_from].gt(eff_from)).first
101
- end
102
- end
2
+ # This is mixed into all History classes.
3
+ module TemporalClass
4
+ def self.included(base)
5
+ base.class_eval do
6
+ base.extend ClassMethods
7
+
8
+ self.table_name += "_h"
9
+
10
+ cattr_accessor :visited_associations
11
+ @@visited_associations = []
12
+
13
+ # The at_value field stores the time from the query that yielded
14
+ # this record.
15
+ attr_accessor :at_value
16
+
17
+ class << self
18
+ prepend STIWithHistory
19
+
20
+ delegate :at, to: :all
21
+ end
22
+
23
+ # Iterates all associations, makes sure their history classes are
24
+ # created and initialized, and modifies the associations to point
25
+ # to the target classes' history classes.
26
+ def self.temporalize_associations!
27
+ reflect_on_all_associations.dup.each do |association|
28
+ unless @@visited_associations.include?(association.name) || association.options[:polymorphic]
29
+ @@visited_associations << association.name
30
+
31
+ # Calling .history here will ensure that the history class
32
+ # for this association is created and initialized
33
+ clazz = association.class_name.constantize.history
34
+
35
+ # Recreate the association, updating it to point at the
36
+ # history class. The foreign key is explicitly set since it's
37
+ # inferred from the class_name, but shouldn't be in this case.
38
+ send(association.macro, association.name,
39
+ association.options.merge(
40
+ class_name: clazz.name,
41
+ foreign_key: association.foreign_key,
42
+ primary_key: clazz.orig_class.primary_key
43
+ )
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ module STIWithHistory
52
+ def sti_name
53
+ super.sub /History$/, ""
54
+ end
55
+
56
+ def find_sti_class(type_name)
57
+ type_name += "History" unless type_name =~ /History\Z/
58
+
59
+ begin
60
+ super
61
+ rescue ActiveRecord::SubclassNotFound
62
+ superclass.send(:find_sti_class, type_name)
63
+ end
64
+ end
65
+ end
66
+
67
+ module ClassMethods
68
+ def orig_class
69
+ name.sub(/History$/, "").constantize
70
+ end
71
+
72
+ def descends_from_active_record?
73
+ superclass.descends_from_active_record?
74
+ end
75
+
76
+ def build_temporal_constraint(at_value)
77
+ arel_table[:eff_to].gteq(at_value).and(
78
+ arel_table[:eff_from].lteq(at_value)
79
+ )
80
+ end
81
+ end
82
+
83
+ def orig_class
84
+ self.class.orig_class
85
+ end
86
+
87
+ def orig_id
88
+ attributes[orig_class.primary_key]
89
+ end
90
+
91
+ def orig_obj
92
+ @orig_obj ||= orig_class.find_by_id orig_id
93
+ end
94
+
95
+ def prev
96
+ @prev ||= history.where(self.class.arel_table[:eff_from].lt(eff_from)).last
97
+ end
98
+
99
+ def next
100
+ @next ||= history.where(self.class.arel_table[:eff_from].gt(eff_from)).first
101
+ end
102
+ end
103
103
  end
@@ -1,3 +1,3 @@
1
1
  module TemporalTables
2
- VERSION = "0.6.10"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -1,19 +1,19 @@
1
1
  module TemporalTables
2
- module Whodunnit
3
- def self.included(base)
4
- base.class_eval do
5
- include InstanceMethods
2
+ module Whodunnit
3
+ def self.included(base)
4
+ base.class_eval do
5
+ include InstanceMethods
6
6
 
7
- before_validation :set_updated_by
8
- end
9
- end
10
-
11
- module InstanceMethods
12
- def set_updated_by
13
- if TemporalTables.updated_by_proc && respond_to?(:updated_by)
14
- self.updated_by = TemporalTables.updated_by_proc.call(self)
15
- end
16
- end
17
- end
18
- end
7
+ before_validation :set_updated_by
8
+ end
9
+ end
10
+
11
+ module InstanceMethods
12
+ def set_updated_by
13
+ if TemporalTables.updated_by_proc && respond_to?(:updated_by)
14
+ self.updated_by = TemporalTables.updated_by_proc.call(self)
15
+ end
16
+ end
17
+ end
18
+ end
19
19
  end