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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +53 -0
  3. data/.rubocop.yml +158 -0
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +5 -5
  6. data/Gemfile +2 -0
  7. data/README.md +15 -5
  8. data/Rakefile +7 -2
  9. data/config.ru +2 -0
  10. data/gemfiles/Gemfile.6.0.mysql.lock +84 -84
  11. data/gemfiles/Gemfile.6.0.pg.lock +103 -98
  12. data/gemfiles/Gemfile.6.1.mysql.lock +180 -0
  13. data/gemfiles/Gemfile.6.1.pg.lock +180 -0
  14. data/gemfiles/{Gemfile.5.2.mysql → Gemfile.7.0.mysql} +2 -2
  15. data/gemfiles/Gemfile.7.0.mysql.lock +173 -0
  16. data/gemfiles/{Gemfile.5.2.pg → Gemfile.7.0.pg} +1 -1
  17. data/gemfiles/Gemfile.7.0.pg.lock +173 -0
  18. data/lib/temporal_tables/arel_table.rb +10 -9
  19. data/lib/temporal_tables/association_extensions.rb +2 -0
  20. data/lib/temporal_tables/connection_adapters/mysql_adapter.rb +5 -3
  21. data/lib/temporal_tables/connection_adapters/postgresql_adapter.rb +5 -3
  22. data/lib/temporal_tables/history_hook.rb +8 -5
  23. data/lib/temporal_tables/preloader_extensions.rb +2 -0
  24. data/lib/temporal_tables/reflection_extensions.rb +11 -14
  25. data/lib/temporal_tables/relation_extensions.rb +11 -24
  26. data/lib/temporal_tables/temporal_adapter.rb +77 -90
  27. data/lib/temporal_tables/temporal_class.rb +29 -28
  28. data/lib/temporal_tables/version.rb +3 -1
  29. data/lib/temporal_tables/whodunnit.rb +5 -3
  30. data/lib/temporal_tables.rb +42 -32
  31. data/spec/basic_history_spec.rb +52 -43
  32. data/spec/internal/app/models/broom.rb +2 -0
  33. data/spec/internal/app/models/cat.rb +3 -1
  34. data/spec/internal/app/models/cat_life.rb +2 -0
  35. data/spec/internal/app/models/coven.rb +2 -0
  36. data/spec/internal/app/models/flying_machine.rb +2 -0
  37. data/spec/internal/app/models/person.rb +2 -0
  38. data/spec/internal/app/models/rocket_broom.rb +2 -0
  39. data/spec/internal/app/models/wart.rb +3 -1
  40. data/spec/internal/config/database.ci.yml +12 -0
  41. data/spec/internal/db/schema.rb +8 -4
  42. data/spec/spec_helper.rb +39 -5
  43. data/spec/support/database.rb +10 -6
  44. data/temporal_tables.gemspec +31 -18
  45. metadata +103 -35
  46. data/.github/workflow/test.yml +0 -44
  47. data/gemfiles/Gemfile.5.1.mysql +0 -16
  48. data/gemfiles/Gemfile.5.1.mysql.lock +0 -147
  49. data/gemfiles/Gemfile.5.1.pg +0 -16
  50. data/gemfiles/Gemfile.5.1.pg.lock +0 -147
  51. data/gemfiles/Gemfile.5.2.mysql.lock +0 -155
  52. data/gemfiles/Gemfile.5.2.pg.lock +0 -155
  53. data/lib/temporal_tables/join_extensions.rb +0 -20
  54. data/spec/extensions/combustion.rb +0 -9
  55. 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 table_name, options, &block
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 table_name, options do |t|
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 == "updated_by" }
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 :updated_by, TemporalTables.updated_by_type
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 temporal_name(table_name), options.merge(id: false, primary_key: "history_id", temporal_bypass: true) do |t|
30
- if options[:id] != false
31
- t.column :id, options.fetch(:id, :integer)
32
- end
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: "9999-12-31"
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
- if table_exists?(temporal_name(table_name))
55
- drop_temporal_triggers table_name
56
- drop_table_without_temporal temporal_name(table_name)
57
- end
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 table_name, options
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
- if table_exists?(temporal_name(name))
76
- super temporal_name(name), temporal_name(new_name)
77
- create_temporal_triggers new_name
78
- end
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 table_name, column_name, type, options
82
+ super(table_name, column_name, type, **options)
83
83
 
84
- if table_exists?(temporal_name(table_name))
85
- super temporal_name(table_name), column_name, type, options
86
- create_temporal_triggers table_name
87
- end
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 table_name, *column_names
91
+ super(table_name, *column_names)
92
92
 
93
- if table_exists?(temporal_name(table_name))
94
- super temporal_name(table_name), *column_names
95
- create_temporal_triggers table_name
96
- end
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 table_name, column_name, type, options
100
+ super(table_name, column_name, type, options)
101
101
 
102
- if table_exists?(temporal_name(table_name))
103
- super temporal_name(table_name), column_name, type, options
104
- # Don't need to update triggers here...
105
- end
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 table_name, column_name, new_column_name
109
+ super(table_name, column_name, new_column_name)
110
110
 
111
- if table_exists?(temporal_name(table_name))
112
- super temporal_name(table_name), column_name, new_column_name
113
- create_temporal_triggers table_name
114
- end
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 table_name, column_name, options
118
+ super(table_name, column_name, **options)
119
119
 
120
- if table_exists?(temporal_name(table_name))
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
- super temporal_name(table_name), column_name, options.except(:unique).merge(name: idx_name)
125
- end
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 table_name, options
128
+ super(table_name, options)
130
129
 
131
- if table_exists?(temporal_name(table_name))
132
- idx_name = temporal_index_name(index_name(table_name, options))
130
+ return unless table_exists?(temporal_name(table_name))
133
131
 
134
- super temporal_name(table_name), :name => idx_name
135
- end
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
- unless temporal_index_exists?(table_name, index_name)
145
- add_index(
146
- temporal_name(table_name),
147
- index.columns, {
148
- # exclude unique constraints for temporal tables
149
- :name => index_name,
150
- :length => index.lengths,
151
- :order => index.orders
152
- })
153
- end
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(table_name)
162
- raise NotImplementedError, "create_temporal_triggers is not implemented"
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(table_name)
166
- raise NotImplementedError, "drop_temporal_triggers is not implemented"
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/, "ind_h").sub(/_ix(\d+)$/, '_hi\1')
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
- case Rails::VERSION::MAJOR
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 += "_h"
10
+ self.table_name += '_h'
9
11
 
10
12
  cattr_accessor :visited_associations
11
- @@visited_associations = []
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
- unless @@visited_associations.include?(association.name) || association.options[:polymorphic]
29
- @@visited_associations << association.name
30
-
31
- # Calling .history here will ensure that the history class
32
- # for this association is created and initialized
33
- clazz = association.class_name.constantize.history
34
-
35
- # Recreate the association, updating it to point at the
36
- # history class. The foreign key is explicitly set since it's
37
- # inferred from the class_name, but shouldn't be in this case.
38
- send(association.macro, association.name,
39
- association.options.merge(
40
- class_name: clazz.name,
41
- foreign_key: association.foreign_key,
42
- primary_key: clazz.orig_class.primary_key
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
- end
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 /History$/, ""
56
+ super.sub(/History$/, '')
54
57
  end
55
58
 
56
59
  def find_sti_class(type_name)
57
- type_name += "History" unless type_name =~ /History\Z/
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$/, "").constantize
72
+ name.sub(/History$/, '').constantize
70
73
  end
71
74
 
72
- def descends_from_active_record?
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.find_by_id orig_id
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
- VERSION = "0.8.1"
4
+ VERSION = '1.0.1'
3
5
  end
@@ -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
- if TemporalTables.updated_by_proc && respond_to?(:updated_by)
14
- self.updated_by = TemporalTables.updated_by_proc.call(self)
15
- end
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
@@ -1,20 +1,21 @@
1
- require "temporal_tables/temporal_adapter"
2
- require "temporal_tables/connection_adapters/mysql_adapter"
3
- require "temporal_tables/connection_adapters/postgresql_adapter"
4
- require "temporal_tables/whodunnit"
5
- require "temporal_tables/temporal_class"
6
- require "temporal_tables/history_hook"
7
- require "temporal_tables/relation_extensions"
8
- require "temporal_tables/association_extensions"
9
- require "temporal_tables/join_extensions"
10
- require "temporal_tables/preloader_extensions"
11
- require "temporal_tables/reflection_extensions"
12
- require "temporal_tables/arel_table"
13
- require "temporal_tables/version"
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 "temporal_tables.load" do
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("::").last
26
- subclass.send :prepend, TemporalTables::ConnectionAdapters.const_get(module_name) if TemporalTables::ConnectionAdapters.const_defined?(module_name)
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.send :include, TemporalTables::Whodunnit
35
+ ActiveRecord::Base.include TemporalTables::Whodunnit
30
36
  end
31
37
  end
32
38
 
33
- @@create_by_default = false
39
+ @create_by_default = false
34
40
  def self.create_by_default
35
- @@create_by_default
41
+ @create_by_default
36
42
  end
43
+
37
44
  def self.create_by_default=(default)
38
- @@create_by_default = default
45
+ @create_by_default = default
39
46
  end
40
47
 
41
- @@skipped_temporal_tables = [:schema_migrations, :sessions, :ar_internal_metadata]
48
+ @skipped_temporal_tables = [:schema_migrations, :sessions, :ar_internal_metadata]
42
49
  def self.skip_temporal_table_for(*tables)
43
- @@skipped_temporal_tables += tables
50
+ @skipped_temporal_tables += tables
44
51
  end
52
+
45
53
  def self.skipped_temporal_tables
46
- @@skipped_temporal_tables.dup
54
+ @skipped_temporal_tables.dup
47
55
  end
48
56
 
49
- @@add_updated_by_field = false
50
- @@updated_by_type = :string
51
- @@updated_by_proc = nil
57
+ @add_updated_by_field = false
58
+ @updated_by_type = :string
59
+ @updated_by_proc = nil
52
60
  def self.updated_by_type
53
- @@updated_by_type
61
+ @updated_by_type
54
62
  end
63
+
55
64
  def self.updated_by_proc
56
- @@updated_by_proc
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
- @@add_updated_by_field = true
61
- @@updated_by_type = type
62
- @@updated_by_proc = block
70
+ @add_updated_by_field = true
71
+ @updated_by_type = type
72
+ @updated_by_proc = block
63
73
  end
64
74
 
65
- @@add_updated_by_field
75
+ @add_updated_by_field
66
76
  end
67
77
  end