temporal_tables 1.0.3 → 1.1.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/lib/temporal_tables/connection_adapters/mysql_adapter.rb +3 -3
- data/lib/temporal_tables/connection_adapters/postgresql_adapter.rb +3 -3
- data/lib/temporal_tables/history_hook.rb +1 -1
- data/lib/temporal_tables/temporal_adapter.rb +41 -12
- data/lib/temporal_tables/temporal_adapter_six_oh.rb +32 -13
- data/lib/temporal_tables/temporal_class.rb +1 -1
- data/lib/temporal_tables/version.rb +1 -1
- data/spec/basic_history_spec.rb +39 -0
- data/spec/internal/app/models/dog.rb +5 -0
- data/spec/internal/db/schema.rb +17 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60c0235cfec08bde495d37a7304eeca6a0bd46aa310224b8a240bbc2ff98bd23
|
4
|
+
data.tar.gz: b68fb0409c0fd389e6a544f0d3ba10f560da3e6536e6dff5a3655e8fe21c3494
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b9e2a3855dcce4b6f50a40e887451097efe51377cd3edbfe4ee7af6023dfb62f3f8deb9d3664827cc7db6426d29aab235349ab33b00b691b7dcaca639c0874b
|
7
|
+
data.tar.gz: f35f963e1540f869f0d0375d63aacd02eb76805a51f11324163712e48a7e38e54153c6ab496088edfc647dc3dfa1b689d03d17908033c9addd10fad6138ff5ac
|
@@ -9,7 +9,7 @@ module TemporalTables
|
|
9
9
|
execute "drop trigger #{table_name}_ad"
|
10
10
|
end
|
11
11
|
|
12
|
-
def create_temporal_triggers(table_name) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
12
|
+
def create_temporal_triggers(table_name, primary_key) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
13
13
|
column_names = columns(table_name).map(&:name)
|
14
14
|
|
15
15
|
execute "drop trigger if exists #{table_name}_ai"
|
@@ -33,7 +33,7 @@ module TemporalTables
|
|
33
33
|
set @current_time = utc_timestamp(6);
|
34
34
|
|
35
35
|
update #{temporal_name(table_name)} set eff_to = @current_time
|
36
|
-
where
|
36
|
+
where #{primary_key} = new.#{primary_key}
|
37
37
|
and eff_to = '9999-12-31';
|
38
38
|
|
39
39
|
insert into #{temporal_name(table_name)} (#{column_names.join(', ')}, eff_from)
|
@@ -50,7 +50,7 @@ module TemporalTables
|
|
50
50
|
set @current_time = utc_timestamp(6);
|
51
51
|
|
52
52
|
update #{temporal_name(table_name)} set eff_to = @current_time
|
53
|
-
where
|
53
|
+
where #{primary_key} = old.#{primary_key}
|
54
54
|
and eff_to = '9999-12-31';
|
55
55
|
|
56
56
|
end
|
@@ -9,7 +9,7 @@ module TemporalTables
|
|
9
9
|
execute "drop trigger #{table_name}_ad on #{table_name}"
|
10
10
|
end
|
11
11
|
|
12
|
-
def create_temporal_triggers(table_name) # rubocop:disable Metrics/MethodLength
|
12
|
+
def create_temporal_triggers(table_name, primary_key) # rubocop:disable Metrics/MethodLength
|
13
13
|
column_names = columns(table_name).map(&:name)
|
14
14
|
|
15
15
|
execute %{
|
@@ -39,7 +39,7 @@ module TemporalTables
|
|
39
39
|
cur_time := localtimestamp;
|
40
40
|
|
41
41
|
update #{temporal_name(table_name)} set eff_to = cur_time
|
42
|
-
where
|
42
|
+
where #{primary_key} = new.#{primary_key}
|
43
43
|
and eff_to = '9999-12-31';
|
44
44
|
|
45
45
|
insert into #{temporal_name(table_name)} (#{column_list(column_names)}, eff_from)
|
@@ -62,7 +62,7 @@ module TemporalTables
|
|
62
62
|
cur_time := localtimestamp;
|
63
63
|
|
64
64
|
update #{temporal_name(table_name)} set eff_to = cur_time
|
65
|
-
where
|
65
|
+
where #{primary_key} = old.#{primary_key}
|
66
66
|
and eff_to = '9999-12-31';
|
67
67
|
|
68
68
|
return null;
|
@@ -45,7 +45,7 @@ module TemporalTables
|
|
45
45
|
def history
|
46
46
|
clazz = is_a?(TemporalTables::TemporalClass) ? self.class : self.class.history
|
47
47
|
oid = is_a?(TemporalTables::TemporalClass) ? orig_class.primary_key : self.class.primary_key
|
48
|
-
clazz.unscoped.where(
|
48
|
+
clazz.unscoped.where(oid => attributes[oid]).order(:eff_from)
|
49
49
|
end
|
50
50
|
end
|
51
51
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'digest'
|
4
|
+
|
3
5
|
module TemporalTables
|
4
6
|
module TemporalAdapter # rubocop:disable Metrics/ModuleLength
|
5
7
|
def create_table(table_name, **options, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
@@ -32,14 +34,22 @@ module TemporalTables
|
|
32
34
|
temporal_name(table_name),
|
33
35
|
**options.merge(id: false, primary_key: 'history_id', temporal_bypass: true)
|
34
36
|
) do |t|
|
35
|
-
t.column :id, options.fetch(:id, :integer) if options[:id] != false
|
36
37
|
t.datetime :eff_from, null: false, limit: 6
|
37
38
|
t.datetime :eff_to, null: false, limit: 6, default: '9999-12-31'
|
38
39
|
|
39
40
|
columns(table_name).each do |c|
|
40
|
-
|
41
|
+
column_options = { limit: c.limit }
|
42
|
+
column_type = c.type
|
43
|
+
if column_type == :enum
|
44
|
+
enum_type = c.sql_type_metadata.sql_type
|
45
|
+
if t.respond_to?(:enum)
|
46
|
+
column_options[:enum_type] = enum_type
|
47
|
+
else
|
48
|
+
column_type = enum_type
|
49
|
+
end
|
50
|
+
end
|
41
51
|
|
42
|
-
t.
|
52
|
+
t.column c.name, column_type, **column_options
|
43
53
|
end
|
44
54
|
end
|
45
55
|
|
@@ -49,8 +59,13 @@ module TemporalTables
|
|
49
59
|
end
|
50
60
|
end
|
51
61
|
|
52
|
-
|
53
|
-
|
62
|
+
original_primary_key = original_primary_key(table_name)
|
63
|
+
temporal_table_index_name = index_name(temporal_name(table_name), [original_primary_key, :eff_to])
|
64
|
+
if temporal_table_index_name.length > index_name_length
|
65
|
+
temporal_table_index_name = truncated_index_name(temporal_table_index_name)
|
66
|
+
end
|
67
|
+
add_index temporal_name(table_name), [original_primary_key, :eff_to], name: temporal_table_index_name
|
68
|
+
create_temporal_triggers table_name, original_primary_key
|
54
69
|
create_temporal_indexes table_name
|
55
70
|
end
|
56
71
|
|
@@ -75,7 +90,7 @@ module TemporalTables
|
|
75
90
|
return unless table_exists?(temporal_name(name))
|
76
91
|
|
77
92
|
super(temporal_name(name), temporal_name(new_name))
|
78
|
-
create_temporal_triggers new_name
|
93
|
+
create_temporal_triggers new_name, original_primary_key(table_name)
|
79
94
|
end
|
80
95
|
|
81
96
|
def add_column(table_name, column_name, type, **options)
|
@@ -84,7 +99,7 @@ module TemporalTables
|
|
84
99
|
return unless table_exists?(temporal_name(table_name))
|
85
100
|
|
86
101
|
super temporal_name(table_name), column_name, type, **options
|
87
|
-
create_temporal_triggers table_name
|
102
|
+
create_temporal_triggers table_name, original_primary_key(table_name)
|
88
103
|
end
|
89
104
|
|
90
105
|
def remove_columns(table_name, *column_names, **options)
|
@@ -93,7 +108,7 @@ module TemporalTables
|
|
93
108
|
return unless table_exists?(temporal_name(table_name))
|
94
109
|
|
95
110
|
super temporal_name(table_name), *column_names, **options
|
96
|
-
create_temporal_triggers table_name
|
111
|
+
create_temporal_triggers table_name, original_primary_key(table_name)
|
97
112
|
end
|
98
113
|
|
99
114
|
def remove_column(table_name, column_name, type = nil, **options)
|
@@ -102,7 +117,7 @@ module TemporalTables
|
|
102
117
|
return unless table_exists?(temporal_name(table_name))
|
103
118
|
|
104
119
|
super temporal_name(table_name), column_name, type, **options
|
105
|
-
create_temporal_triggers table_name
|
120
|
+
create_temporal_triggers table_name, original_primary_key(table_name)
|
106
121
|
end
|
107
122
|
|
108
123
|
def change_column(table_name, column_name, type, **options)
|
@@ -120,7 +135,7 @@ module TemporalTables
|
|
120
135
|
return unless table_exists?(temporal_name(table_name))
|
121
136
|
|
122
137
|
super temporal_name(table_name), column_name, new_column_name
|
123
|
-
create_temporal_triggers table_name
|
138
|
+
create_temporal_triggers table_name, original_primary_key(table_name)
|
124
139
|
end
|
125
140
|
|
126
141
|
def add_index(table_name, column_name, **options)
|
@@ -173,13 +188,27 @@ module TemporalTables
|
|
173
188
|
raise NotImplementedError, 'drop_temporal_triggers is not implemented'
|
174
189
|
end
|
175
190
|
|
176
|
-
#
|
191
|
+
# Index names max out at 63 characters. If appending _h to the index name would surpass that limit,
|
192
|
+
# we can trim the index name and append a deterministically generated 5 character hash as well as _h.
|
177
193
|
def temporal_index_name(index_name)
|
178
|
-
index_name.
|
194
|
+
"#{index_name.length < 62 ? index_name : truncated_index_name(index_name, 2)}_h"
|
195
|
+
end
|
196
|
+
|
197
|
+
def truncated_index_name(index_name, required_padding = 0)
|
198
|
+
max_length = index_name_length - required_padding
|
199
|
+
index_name_hash = Digest::SHA1.base64digest(index_name.to_s)[0, 5]
|
200
|
+
"#{index_name[0, max_length - 6]}_#{index_name_hash}"
|
179
201
|
end
|
180
202
|
|
181
203
|
def temporal_index_exists?(table_name, index_name)
|
182
204
|
index_name_exists?(temporal_name(table_name), index_name)
|
183
205
|
end
|
206
|
+
|
207
|
+
def original_primary_key(table_name)
|
208
|
+
original_primary_key = primary_key(table_name)
|
209
|
+
raise 'temporal_adapter requires that the table has a single primary key' unless original_primary_key.is_a? String
|
210
|
+
|
211
|
+
original_primary_key
|
212
|
+
end
|
184
213
|
end
|
185
214
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'digest'
|
4
|
+
|
3
5
|
module TemporalTables
|
4
6
|
# The main difference here is the add_index method, which still uses
|
5
7
|
# the old options={} syntax
|
@@ -34,14 +36,12 @@ module TemporalTables
|
|
34
36
|
temporal_name(table_name),
|
35
37
|
**options.merge(id: false, primary_key: 'history_id', temporal_bypass: true)
|
36
38
|
) do |t|
|
37
|
-
t.column :id, options.fetch(:id, :integer) if options[:id] != false
|
38
39
|
t.datetime :eff_from, null: false, limit: 6
|
39
40
|
t.datetime :eff_to, null: false, limit: 6, default: '9999-12-31'
|
40
41
|
|
41
42
|
columns(table_name).each do |c|
|
42
|
-
|
43
|
-
|
44
|
-
t.send c.type, c.name, limit: c.limit
|
43
|
+
column_type = c.type == :enum ? c.sql_type_metadata.sql_type : c.type
|
44
|
+
t.column c.name, column_type, limit: c.limit
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
@@ -51,8 +51,13 @@ module TemporalTables
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
-
|
55
|
-
|
54
|
+
original_primary_key = original_primary_key(table_name)
|
55
|
+
temporal_table_index_name = index_name(temporal_name(table_name), [original_primary_key, :eff_to])
|
56
|
+
if temporal_table_index_name.length > index_name_length
|
57
|
+
temporal_table_index_name = truncated_index_name(temporal_table_index_name)
|
58
|
+
end
|
59
|
+
add_index temporal_name(table_name), [original_primary_key, :eff_to], { name: temporal_table_index_name }
|
60
|
+
create_temporal_triggers table_name, original_primary_key
|
56
61
|
create_temporal_indexes table_name
|
57
62
|
end
|
58
63
|
|
@@ -77,7 +82,7 @@ module TemporalTables
|
|
77
82
|
return unless table_exists?(temporal_name(name))
|
78
83
|
|
79
84
|
super(temporal_name(name), temporal_name(new_name))
|
80
|
-
create_temporal_triggers new_name
|
85
|
+
create_temporal_triggers new_name, original_primary_key(table_name)
|
81
86
|
end
|
82
87
|
|
83
88
|
def add_column(table_name, column_name, type, **options)
|
@@ -86,7 +91,7 @@ module TemporalTables
|
|
86
91
|
return unless table_exists?(temporal_name(table_name))
|
87
92
|
|
88
93
|
super temporal_name(table_name), column_name, type, **options
|
89
|
-
create_temporal_triggers table_name
|
94
|
+
create_temporal_triggers table_name, original_primary_key(table_name)
|
90
95
|
end
|
91
96
|
|
92
97
|
def remove_columns(table_name, *column_names, **options)
|
@@ -95,7 +100,7 @@ module TemporalTables
|
|
95
100
|
return unless table_exists?(temporal_name(table_name))
|
96
101
|
|
97
102
|
super temporal_name(table_name), *column_names, **options
|
98
|
-
create_temporal_triggers table_name
|
103
|
+
create_temporal_triggers table_name, original_primary_key(table_name)
|
99
104
|
end
|
100
105
|
|
101
106
|
def remove_column(table_name, column_name, type = nil, **options)
|
@@ -104,7 +109,7 @@ module TemporalTables
|
|
104
109
|
return unless table_exists?(temporal_name(table_name))
|
105
110
|
|
106
111
|
super temporal_name(table_name), column_name, type, **options
|
107
|
-
create_temporal_triggers table_name
|
112
|
+
create_temporal_triggers table_name, original_primary_key(table_name)
|
108
113
|
end
|
109
114
|
|
110
115
|
def change_column(table_name, column_name, type, **options)
|
@@ -122,7 +127,7 @@ module TemporalTables
|
|
122
127
|
return unless table_exists?(temporal_name(table_name))
|
123
128
|
|
124
129
|
super temporal_name(table_name), column_name, new_column_name
|
125
|
-
create_temporal_triggers table_name
|
130
|
+
create_temporal_triggers table_name, original_primary_key(table_name)
|
126
131
|
end
|
127
132
|
|
128
133
|
def add_index(table_name, column_name, options = {})
|
@@ -176,13 +181,27 @@ module TemporalTables
|
|
176
181
|
raise NotImplementedError, 'drop_temporal_triggers is not implemented'
|
177
182
|
end
|
178
183
|
|
179
|
-
#
|
184
|
+
# Index names max out at 63 characters. If appending _h to the index name would surpass that limit,
|
185
|
+
# we can trim the index name and append a deterministically generated 5 character hash as well as _h.
|
180
186
|
def temporal_index_name(index_name)
|
181
|
-
index_name.
|
187
|
+
"#{index_name.length < 62 ? index_name : truncated_index_name(index_name, 2)}_h"
|
188
|
+
end
|
189
|
+
|
190
|
+
def truncated_index_name(index_name, required_padding = 0)
|
191
|
+
max_length = index_name_length - required_padding
|
192
|
+
index_name_hash = Digest::SHA1.base64digest(index_name.to_s)[0, 5]
|
193
|
+
"#{index_name[0, max_length - 6]}_#{index_name_hash}"
|
182
194
|
end
|
183
195
|
|
184
196
|
def temporal_index_exists?(table_name, index_name)
|
185
197
|
index_name_exists?(temporal_name(table_name), index_name)
|
186
198
|
end
|
199
|
+
|
200
|
+
def original_primary_key(table_name)
|
201
|
+
original_primary_key = primary_key(table_name)
|
202
|
+
raise 'temporal_adapter requires that the table has a single primary key' unless original_primary_key.is_a? String
|
203
|
+
|
204
|
+
original_primary_key
|
205
|
+
end
|
187
206
|
end
|
188
207
|
end
|
data/spec/basic_history_spec.rb
CHANGED
@@ -161,6 +161,12 @@ describe Person do
|
|
161
161
|
cat.lives.create started_at: Time.zone.now
|
162
162
|
end
|
163
163
|
|
164
|
+
# The following tests enum type columns for postgres
|
165
|
+
it 'breed is set correctly' do
|
166
|
+
expect(cat.breed).to eq('ragdoll')
|
167
|
+
expect(cat.history.last.breed).to eq('ragdoll')
|
168
|
+
end
|
169
|
+
|
164
170
|
it 'shows one life at the beginning' do
|
165
171
|
expect(cat.history.at(@init_time).last.lives.size).to eq(1)
|
166
172
|
end
|
@@ -169,4 +175,37 @@ describe Person do
|
|
169
175
|
expect(cat.history.last.lives.size).to eq(2)
|
170
176
|
end
|
171
177
|
end
|
178
|
+
|
179
|
+
# The following tests PKs with names other than "id"
|
180
|
+
describe 'when spawning and renaming a creature with PK not named id' do
|
181
|
+
let!(:dog) { Dog.create name: 'Fido' }
|
182
|
+
|
183
|
+
context 'Fido is renamed to Max' do
|
184
|
+
before do
|
185
|
+
dog.name = 'Max'
|
186
|
+
dog.save!
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'name is set correctly and we remember Max\'s original name' do
|
190
|
+
expect(dog.name).to eq('Max')
|
191
|
+
expect(dog.history.last.name).to eq('Max')
|
192
|
+
|
193
|
+
fido = dog.history.first
|
194
|
+
expect(fido.name).to eq('Fido')
|
195
|
+
expect(fido.orig_obj.name).to eq('Max')
|
196
|
+
end
|
197
|
+
|
198
|
+
context 'Max is rehomed' do
|
199
|
+
before do
|
200
|
+
dog.destroy!
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'Max is no longer home but Max/Fido lives on in our memories' do
|
204
|
+
expect(Dog.count).to eq(0)
|
205
|
+
expect(dog.history.last.name).to eq('Max')
|
206
|
+
expect(dog.history.first.name).to eq('Fido')
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
172
211
|
end
|
data/spec/internal/db/schema.rb
CHANGED
@@ -7,7 +7,12 @@ rescue NameError
|
|
7
7
|
end
|
8
8
|
|
9
9
|
ActiveRecord::Schema.define do
|
10
|
-
|
10
|
+
if postgres
|
11
|
+
enable_extension 'pgcrypto'
|
12
|
+
execute <<-SQL
|
13
|
+
CREATE TYPE cat_breed AS ENUM ('ragdoll', 'persian', 'sphynx');
|
14
|
+
SQL
|
15
|
+
end
|
11
16
|
|
12
17
|
create_table :covens, force: true do |t|
|
13
18
|
t.string :name
|
@@ -36,6 +41,7 @@ ActiveRecord::Schema.define do
|
|
36
41
|
create_table :cats, id: (postgres ? :uuid : :integer), temporal: true, force: true do |t|
|
37
42
|
t.string :name
|
38
43
|
t.string :color
|
44
|
+
t.column :breed, (postgres ? :cat_breed : :string), null: false, default: 'ragdoll'
|
39
45
|
end
|
40
46
|
|
41
47
|
create_table :cat_lives, id: (postgres ? :uuid : :integer), temporal: true do |t|
|
@@ -44,4 +50,14 @@ ActiveRecord::Schema.define do
|
|
44
50
|
t.timestamp :ended_at
|
45
51
|
t.string :death_reason
|
46
52
|
end
|
53
|
+
|
54
|
+
create_table :dogs, primary_key: 'dog_id', temporal: true do |t|
|
55
|
+
t.string :name
|
56
|
+
end
|
57
|
+
add_index :dogs, :name, name: 'name_index_with_a_name_that_happens_to_be_exactly_63_chars_long'
|
58
|
+
remove_index :dogs, name: :name_index_with_a_name_that_happens_to_be_exactly_63_chars_long
|
59
|
+
|
60
|
+
create_table :a_very_very_very_very_very_long_table_name, temporal: true do |t|
|
61
|
+
t.string :name
|
62
|
+
end
|
47
63
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: temporal_tables
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brent Kroeker
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-11-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -193,6 +193,7 @@ files:
|
|
193
193
|
- spec/internal/app/models/cat.rb
|
194
194
|
- spec/internal/app/models/cat_life.rb
|
195
195
|
- spec/internal/app/models/coven.rb
|
196
|
+
- spec/internal/app/models/dog.rb
|
196
197
|
- spec/internal/app/models/flying_machine.rb
|
197
198
|
- spec/internal/app/models/person.rb
|
198
199
|
- spec/internal/app/models/rocket_broom.rb
|
@@ -234,6 +235,7 @@ test_files:
|
|
234
235
|
- spec/internal/app/models/cat.rb
|
235
236
|
- spec/internal/app/models/cat_life.rb
|
236
237
|
- spec/internal/app/models/coven.rb
|
238
|
+
- spec/internal/app/models/dog.rb
|
237
239
|
- spec/internal/app/models/flying_machine.rb
|
238
240
|
- spec/internal/app/models/person.rb
|
239
241
|
- spec/internal/app/models/rocket_broom.rb
|