temporal_tables 0.8.1 → 1.0.1

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