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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.travis.yml +3 -1
- data/README.md +1 -1
- data/gemfiles/Gemfile.5.0.mysql.lock +3 -3
- data/gemfiles/Gemfile.5.0.pg.lock +3 -3
- data/gemfiles/Gemfile.5.1.mysql.lock +3 -3
- data/gemfiles/Gemfile.5.1.pg.lock +3 -3
- data/gemfiles/Gemfile.5.2.mysql.lock +3 -3
- data/gemfiles/Gemfile.5.2.pg.lock +3 -3
- data/gemfiles/Gemfile.6.0.mysql +16 -0
- data/gemfiles/Gemfile.6.0.mysql.lock +171 -0
- data/gemfiles/Gemfile.6.0.pg +16 -0
- data/gemfiles/Gemfile.6.0.pg.lock +171 -0
- data/lib/temporal_tables/arel_table.rb +19 -0
- data/lib/temporal_tables/association_extensions.rb +16 -16
- data/lib/temporal_tables/connection_adapters/mysql_adapter.rb +54 -54
- data/lib/temporal_tables/connection_adapters/postgresql_adapter.rb +64 -64
- data/lib/temporal_tables/history_hook.rb +43 -43
- data/lib/temporal_tables/join_extensions.rb +14 -14
- data/lib/temporal_tables/preloader_extensions.rb +14 -14
- data/lib/temporal_tables/reflection_extensions.rb +14 -14
- data/lib/temporal_tables/relation_extensions.rb +79 -79
- data/lib/temporal_tables/temporal_adapter.rb +181 -174
- data/lib/temporal_tables/temporal_class.rb +101 -101
- data/lib/temporal_tables/version.rb +1 -1
- data/lib/temporal_tables/whodunnit.rb +16 -16
- data/lib/temporal_tables.rb +46 -45
- data/spec/basic_history_spec.rb +138 -138
- data/spec/internal/app/models/flying_machine.rb +1 -1
- data/spec/internal/app/models/person.rb +3 -3
- data/spec/internal/db/schema.rb +17 -17
- data/spec/spec_helper.rb +4 -4
- data/spec/support/database.rb +7 -7
- data/temporal_tables.gemspec +15 -15
- metadata +16 -6
@@ -1,176 +1,183 @@
|
|
1
1
|
module TemporalTables
|
2
|
-
|
3
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
3
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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,19 +1,19 @@
|
|
1
1
|
module TemporalTables
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
2
|
+
module Whodunnit
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
include InstanceMethods
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|