temporal_tables 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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