temporal_tables 0.6.10 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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