temporal_tables 1.0.3 → 2.0.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: 457aecc9f2b96e0a7ef98f6e7e2d4c30fc0aa091ddeb3abc9a3c684cae67768c
4
+ data.tar.gz: 1bf1f9bd6e98b063b73c3bdafb9f5d31bba6e75a2e605838c466915c85231317
5
5
  SHA512:
6
- metadata.gz: 7d3599da77f429b650b262128841864d774ec3be87dc5393702146f06d2fb5acc2ff8fd0c146e02cc328eb563b07f1d3a9638551dcc3a63d25ccbedc29d48e53
7
- data.tar.gz: 6ce73a4fac38c371fe1d663e1970ee09e824b1e476872e5e7cf49c3b488ea9811efae0754798f8b39b60e340d14b9944fdeb8ed27fc4f20fb19386b4170f9ffb
6
+ metadata.gz: 9b9baeefc81aadfe5f23db7fc5790321cf2550812a6818a9b3d2d3f96af4f12efb8672bea34e9b7981f6e577c9c43574378d72894684a866c762aaf94d17d5f2
7
+ data.tar.gz: 9ae4da3db11583308195eca7c1965c0a91242ab61711794bb8b0f3264e25df39b2453a6dcafd3ae7dad119f7f5d31d3a5d681b18bd711422c12d0965e8b184fb
@@ -9,7 +9,7 @@ module TemporalTables
9
9
  if at_value
10
10
  join =
11
11
  join
12
- .and(to[:eff_to].gteq(at_value))
12
+ .and(to[:eff_to].gt(at_value).or(to[:eff_to].eq(TemporalTables::END_OF_TIME)))
13
13
  .and(to[:eff_from].lteq(at_value))
14
14
  end
15
15
  join
@@ -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,8 +33,8 @@ 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
37
- and eff_to = '9999-12-31';
36
+ where #{primary_key} = new.#{primary_key}
37
+ and eff_to = '#{TemporalTables::END_OF_TIME}';
38
38
 
39
39
  insert into #{temporal_name(table_name)} (#{column_names.join(', ')}, eff_from)
40
40
  values (#{column_names.collect { |c| "new.#{c}" }.join(', ')}, @current_time);
@@ -50,8 +50,8 @@ 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
54
- and eff_to = '9999-12-31';
53
+ where #{primary_key} = old.#{primary_key}
54
+ and eff_to = '#{TemporalTables::END_OF_TIME}';
55
55
 
56
56
  end
57
57
  }
@@ -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,8 +39,8 @@ 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
43
- and eff_to = '9999-12-31';
42
+ where #{primary_key} = new.#{primary_key}
43
+ and eff_to = '#{TemporalTables::END_OF_TIME}';
44
44
 
45
45
  insert into #{temporal_name(table_name)} (#{column_list(column_names)}, eff_from)
46
46
  values (#{column_names.collect { |c| "new.#{c}" }.join(', ')}, cur_time);
@@ -62,8 +62,8 @@ 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
66
- and eff_to = '9999-12-31';
65
+ where #{primary_key} = old.#{primary_key}
66
+ and eff_to = '#{TemporalTables::END_OF_TIME}';
67
67
 
68
68
  return null;
69
69
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TemporalTables
4
+ END_OF_TIME = '9999-12-31'
5
+ end
@@ -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
- t.datetime :eff_to, null: false, limit: 6, default: '9999-12-31'
38
+ t.datetime :eff_to, null: false, limit: 6, default: TemporalTables::END_OF_TIME
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
- t.datetime :eff_to, null: false, limit: 6, default: '9999-12-31'
40
+ t.datetime :eff_to, null: false, limit: 6, default: TemporalTables::END_OF_TIME
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
@@ -74,8 +74,10 @@ module TemporalTables
74
74
 
75
75
  delegate :descends_from_active_record?, to: :superclass
76
76
 
77
+ # An object at a given time should fall within the range, excluding the effective end date.
78
+ # However, when using '9999-12-31', this is effectively infinity and should not be excluded.
77
79
  def build_temporal_constraint(at_value)
78
- arel_table[:eff_to].gteq(at_value).and(
80
+ (arel_table[:eff_to].gt(at_value).or(arel_table[:eff_to].eq(TemporalTables::END_OF_TIME))).and(
79
81
  arel_table[:eff_from].lteq(at_value)
80
82
  )
81
83
  end
@@ -90,7 +92,7 @@ module TemporalTables
90
92
  end
91
93
 
92
94
  def orig_obj
93
- @orig_obj ||= orig_class.find_by id: orig_id
95
+ @orig_obj ||= orig_class.find_by(orig_class.primary_key => orig_id)
94
96
  end
95
97
 
96
98
  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 = '2.0.0'
5
5
  end
@@ -12,6 +12,7 @@ require 'temporal_tables/association_extensions'
12
12
  require 'temporal_tables/preloader_extensions'
13
13
  require 'temporal_tables/reflection_extensions'
14
14
  require 'temporal_tables/arel_table'
15
+ require 'temporal_tables/constants'
15
16
  require 'temporal_tables/version'
16
17
 
17
18
  module TemporalTables
@@ -90,10 +90,10 @@ describe Person do
90
90
  where = sql[4]
91
91
 
92
92
  expect(from.scan(/.warts_h.\..eff_from./i).count).to eq(1)
93
- expect(from.scan(/.warts_h.\..eff_to./i).count).to eq(1)
93
+ expect(from.scan(/.warts_h.\..eff_to./i).count).to eq(2)
94
94
 
95
95
  expect(where.scan(/.people_h.\..eff_from./i).count).to eq(1)
96
- expect(where.scan(/.people_h.\..eff_to./i).count).to eq(1)
96
+ expect(where.scan(/.people_h.\..eff_to./i).count).to eq(2)
97
97
  expect(where.scan(/.warts_h.\..eff_from./i).count).to eq(0)
98
98
  expect(where.scan(/.warts_h.\..eff_to./i).count).to eq(0)
99
99
  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,43 @@ 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
+ it 'at the exact time of the name change, the dog should not be both Max and Fido' do
199
+ dog_at_moment_of_name_change = dog.history.at(dog.history.last.eff_from)
200
+ expect(dog_at_moment_of_name_change.count).to eq(1)
201
+ expect(dog_at_moment_of_name_change.first.name).to eq('Max')
202
+ end
203
+
204
+ context 'Max is rehomed' do
205
+ before do
206
+ dog.destroy!
207
+ end
208
+
209
+ it 'Max is no longer home but Max/Fido lives on in our memories' do
210
+ expect(Dog.count).to eq(0)
211
+ expect(dog.history.last.name).to eq('Max')
212
+ expect(dog.history.first.name).to eq('Fido')
213
+ end
214
+ end
215
+ end
216
+ end
172
217
  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: 2.0.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-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -179,6 +179,7 @@ files:
179
179
  - lib/temporal_tables/association_extensions.rb
180
180
  - lib/temporal_tables/connection_adapters/mysql_adapter.rb
181
181
  - lib/temporal_tables/connection_adapters/postgresql_adapter.rb
182
+ - lib/temporal_tables/constants.rb
182
183
  - lib/temporal_tables/history_hook.rb
183
184
  - lib/temporal_tables/preloader_extensions.rb
184
185
  - lib/temporal_tables/reflection_extensions.rb
@@ -193,6 +194,7 @@ files:
193
194
  - spec/internal/app/models/cat.rb
194
195
  - spec/internal/app/models/cat_life.rb
195
196
  - spec/internal/app/models/coven.rb
197
+ - spec/internal/app/models/dog.rb
196
198
  - spec/internal/app/models/flying_machine.rb
197
199
  - spec/internal/app/models/person.rb
198
200
  - spec/internal/app/models/rocket_broom.rb
@@ -234,6 +236,7 @@ test_files:
234
236
  - spec/internal/app/models/cat.rb
235
237
  - spec/internal/app/models/cat_life.rb
236
238
  - spec/internal/app/models/coven.rb
239
+ - spec/internal/app/models/dog.rb
237
240
  - spec/internal/app/models/flying_machine.rb
238
241
  - spec/internal/app/models/person.rb
239
242
  - spec/internal/app/models/rocket_broom.rb