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