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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf7a65a415b9c610e9ea05fd20ddbe8466dd37279f65bf62d4d499cab1d5527f
4
- data.tar.gz: 803d774d51c82624c33b4fd732d2f2b9c6f16fe288725aca6ced174f7e35071e
3
+ metadata.gz: 60c0235cfec08bde495d37a7304eeca6a0bd46aa310224b8a240bbc2ff98bd23
4
+ data.tar.gz: b68fb0409c0fd389e6a544f0d3ba10f560da3e6536e6dff5a3655e8fe21c3494
5
5
  SHA512:
6
- metadata.gz: 7d3599da77f429b650b262128841864d774ec3be87dc5393702146f06d2fb5acc2ff8fd0c146e02cc328eb563b07f1d3a9638551dcc3a63d25ccbedc29d48e53
7
- data.tar.gz: 6ce73a4fac38c371fe1d663e1970ee09e824b1e476872e5e7cf49c3b488ea9811efae0754798f8b39b60e340d14b9944fdeb8ed27fc4f20fb19386b4170f9ffb
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 id = new.id
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 id = old.id
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 id = new.id
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 id = old.id
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(id: attributes[oid]).order(:eff_from)
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
- next if c.name == 'id'
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.send c.type, c.name, limit: c.limit
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
- add_index temporal_name(table_name), [:id, :eff_to]
53
- create_temporal_triggers table_name
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
- # It's important not to increase the length of the returned string.
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.to_s.sub(/^index/, 'ind_h').sub(/_ix(\d+)$/, '_hi\1')
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
- next if c.name == 'id'
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
- add_index temporal_name(table_name), [:id, :eff_to]
55
- create_temporal_triggers table_name
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
- # It's important not to increase the length of the returned string.
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.to_s.sub(/^index/, 'ind_h').sub(/_ix(\d+)$/, '_hi\1')
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
@@ -90,7 +90,7 @@ module TemporalTables
90
90
  end
91
91
 
92
92
  def orig_obj
93
- @orig_obj ||= orig_class.find_by id: orig_id
93
+ @orig_obj ||= orig_class.find_by(orig_class.primary_key => orig_id)
94
94
  end
95
95
 
96
96
  def prev
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TemporalTables
4
- VERSION = '1.0.3'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Dog < ActiveRecord::Base
4
+ self.primary_key = :dog_id
5
+ end
@@ -7,7 +7,12 @@ rescue NameError
7
7
  end
8
8
 
9
9
  ActiveRecord::Schema.define do
10
- enable_extension 'pgcrypto' if postgres
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.3
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-02-01 00:00:00.000000000 Z
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