temporal_tables 0.7.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) 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 -7
  6. data/Gemfile +2 -0
  7. data/README.md +21 -5
  8. data/Rakefile +7 -2
  9. data/config.ru +2 -0
  10. data/gemfiles/Gemfile.6.0.mysql +1 -1
  11. data/gemfiles/Gemfile.6.0.mysql.lock +104 -104
  12. data/gemfiles/Gemfile.6.0.pg +1 -1
  13. data/gemfiles/Gemfile.6.0.pg.lock +110 -105
  14. data/gemfiles/{Gemfile.5.1.mysql → Gemfile.6.1.mysql} +2 -2
  15. data/gemfiles/Gemfile.6.1.mysql.lock +180 -0
  16. data/gemfiles/{Gemfile.5.2.pg → Gemfile.6.1.pg} +1 -1
  17. data/gemfiles/Gemfile.6.1.pg.lock +180 -0
  18. data/gemfiles/{Gemfile.5.0.mysql → Gemfile.7.0.mysql} +2 -2
  19. data/gemfiles/Gemfile.7.0.mysql.lock +173 -0
  20. data/gemfiles/{Gemfile.5.1.pg → Gemfile.7.0.pg} +1 -1
  21. data/gemfiles/Gemfile.7.0.pg.lock +173 -0
  22. data/lib/temporal_tables/arel_table.rb +11 -10
  23. data/lib/temporal_tables/association_extensions.rb +2 -0
  24. data/lib/temporal_tables/connection_adapters/mysql_adapter.rb +5 -3
  25. data/lib/temporal_tables/connection_adapters/postgresql_adapter.rb +5 -3
  26. data/lib/temporal_tables/history_hook.rb +8 -5
  27. data/lib/temporal_tables/preloader_extensions.rb +2 -0
  28. data/lib/temporal_tables/reflection_extensions.rb +11 -14
  29. data/lib/temporal_tables/relation_extensions.rb +11 -24
  30. data/lib/temporal_tables/temporal_adapter.rb +82 -88
  31. data/lib/temporal_tables/temporal_class.rb +29 -28
  32. data/lib/temporal_tables/version.rb +3 -1
  33. data/lib/temporal_tables/whodunnit.rb +5 -3
  34. data/lib/temporal_tables.rb +42 -32
  35. data/spec/basic_history_spec.rb +65 -35
  36. data/spec/internal/app/models/broom.rb +2 -0
  37. data/spec/internal/app/models/cat.rb +5 -0
  38. data/spec/internal/app/models/cat_life.rb +5 -0
  39. data/spec/internal/app/models/coven.rb +2 -0
  40. data/spec/internal/app/models/flying_machine.rb +2 -0
  41. data/spec/internal/app/models/person.rb +2 -0
  42. data/spec/internal/app/models/rocket_broom.rb +2 -0
  43. data/spec/internal/app/models/wart.rb +3 -1
  44. data/spec/internal/config/database.ci.yml +12 -0
  45. data/spec/internal/db/schema.rb +22 -0
  46. data/spec/spec_helper.rb +39 -5
  47. data/spec/support/database.rb +10 -6
  48. data/temporal_tables.gemspec +31 -18
  49. metadata +111 -41
  50. data/CHANGELOG.md +0 -73
  51. data/gemfiles/Gemfile.5.0.mysql.lock +0 -147
  52. data/gemfiles/Gemfile.5.0.pg +0 -16
  53. data/gemfiles/Gemfile.5.0.pg.lock +0 -147
  54. data/gemfiles/Gemfile.5.1.mysql.lock +0 -147
  55. data/gemfiles/Gemfile.5.1.pg.lock +0 -147
  56. data/gemfiles/Gemfile.5.2.mysql +0 -16
  57. data/gemfiles/Gemfile.5.2.mysql.lock +0 -155
  58. data/gemfiles/Gemfile.5.2.pg.lock +0 -155
  59. data/lib/temporal_tables/join_extensions.rb +0 -20
  60. data/spec/extensions/combustion.rb +0 -9
  61. data/spec/internal/config/routes.rb +0 -3
@@ -1,16 +1,23 @@
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
- t.column :updated_by, TemporalTables.updated_by_type
15
+ updated_by_already_exists = t.columns.any? { |c| c.name == 'updated_by' }
16
+ if updated_by_already_exists
17
+ puts "consider adding #{table_name} to TemporalTables skip_table" # rubocop:disable Rails/Output
18
+ else
19
+ t.column(:updated_by, TemporalTables.updated_by_type)
20
+ end
14
21
  end
15
22
  end
16
23
 
@@ -20,15 +27,19 @@ module TemporalTables
20
27
  end
21
28
  end
22
29
 
23
- def add_temporal_table(table_name, options = {})
24
- create_table temporal_name(table_name), options.merge(id: false, primary_key: "history_id", temporal_bypass: true) do |t|
25
- t.integer :id
26
- t.datetime :eff_from, :null => false, limit: 6
27
- t.datetime :eff_to, :null => false, limit: 6, :default => "9999-12-31"
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
36
+ t.datetime :eff_from, null: false, limit: 6
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'
28
41
 
29
- for c in columns(table_name)
30
- next if c.name == "id"
31
- t.send c.type, c.name, :limit => c.limit
42
+ t.send c.type, c.name, limit: c.limit
32
43
  end
33
44
  end
34
45
 
@@ -44,106 +55,101 @@ module TemporalTables
44
55
  end
45
56
 
46
57
  def remove_temporal_table(table_name)
47
- if table_exists?(temporal_name(table_name))
48
- drop_temporal_triggers table_name
49
- drop_table_without_temporal temporal_name(table_name)
50
- 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)
51
62
  end
52
63
 
53
64
  def drop_table(table_name, options = {})
54
- super table_name, options
65
+ super(table_name, **options)
55
66
 
56
- if table_exists?(temporal_name(table_name))
57
- super temporal_name(table_name), options
58
- end
67
+ super(temporal_name(table_name), **options) if table_exists?(temporal_name(table_name))
59
68
  end
60
69
 
61
70
  def rename_table(name, new_name)
62
- if table_exists?(temporal_name(name))
63
- drop_temporal_triggers name
64
- end
71
+ drop_temporal_triggers name if table_exists?(temporal_name(name))
65
72
 
66
73
  super name, new_name
67
74
 
68
- if table_exists?(temporal_name(name))
69
- super temporal_name(name), temporal_name(new_name)
70
- create_temporal_triggers new_name
71
- 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
72
79
  end
73
80
 
74
81
  def add_column(table_name, column_name, type, options = {})
75
- super table_name, column_name, type, options
82
+ super(table_name, column_name, type, **options)
76
83
 
77
- if table_exists?(temporal_name(table_name))
78
- super temporal_name(table_name), column_name, type, options
79
- create_temporal_triggers table_name
80
- 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
81
88
  end
82
89
 
83
90
  def remove_column(table_name, *column_names)
84
- super table_name, *column_names
91
+ super(table_name, *column_names)
85
92
 
86
- if table_exists?(temporal_name(table_name))
87
- super temporal_name(table_name), *column_names
88
- create_temporal_triggers table_name
89
- 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
90
97
  end
91
98
 
92
99
  def change_column(table_name, column_name, type, options = {})
93
- super table_name, column_name, type, options
100
+ super(table_name, column_name, type, options)
94
101
 
95
- if table_exists?(temporal_name(table_name))
96
- super temporal_name(table_name), column_name, type, options
97
- # Don't need to update triggers here...
98
- 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...
99
106
  end
100
107
 
101
108
  def rename_column(table_name, column_name, new_column_name)
102
- super table_name, column_name, new_column_name
109
+ super(table_name, column_name, new_column_name)
103
110
 
104
- if table_exists?(temporal_name(table_name))
105
- super temporal_name(table_name), column_name, new_column_name
106
- create_temporal_triggers table_name
107
- 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
108
115
  end
109
116
 
110
117
  def add_index(table_name, column_name, options = {})
111
- super table_name, column_name, options
118
+ super(table_name, column_name, **options)
112
119
 
113
- if table_exists?(temporal_name(table_name))
114
- column_names = Array.wrap(column_name)
115
- idx_name = temporal_index_name(options[:name] || index_name(table_name, :column => column_names))
120
+ return unless table_exists?(temporal_name(table_name))
116
121
 
117
- super temporal_name(table_name), column_name, options.except(:unique).merge(name: idx_name)
118
- 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)
119
125
  end
120
126
 
121
127
  def remove_index(table_name, options = {})
122
- super table_name, options
128
+ super(table_name, options)
123
129
 
124
- if table_exists?(temporal_name(table_name))
125
- idx_name = temporal_index_name(index_name(table_name, options))
130
+ return unless table_exists?(temporal_name(table_name))
126
131
 
127
- super temporal_name(table_name), :name => idx_name
128
- end
132
+ idx_name = temporal_index_name(index_name(table_name, options))
133
+ super temporal_name(table_name), name: idx_name
129
134
  end
130
135
 
131
- def create_temporal_indexes(table_name)
136
+ def create_temporal_indexes(table_name) # rubocop:disable Metrics/MethodLength
132
137
  indexes = ActiveRecord::Base.connection.indexes(table_name)
133
138
 
134
139
  indexes.each do |index|
135
140
  index_name = temporal_index_name(index.name)
136
141
 
137
- unless temporal_index_exists?(table_name, index_name)
138
- add_index(
139
- temporal_name(table_name),
140
- index.columns, {
141
- # exclude unique constraints for temporal tables
142
- :name => index_name,
143
- :length => index.lengths,
144
- :order => index.orders
145
- })
146
- 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
+ )
147
153
  end
148
154
  end
149
155
 
@@ -151,33 +157,21 @@ module TemporalTables
151
157
  "#{table_name}_h"
152
158
  end
153
159
 
154
- def create_temporal_triggers(table_name)
155
- raise NotImplementedError, "create_temporal_triggers is not implemented"
160
+ def create_temporal_triggers(_table_name)
161
+ raise NotImplementedError, 'create_temporal_triggers is not implemented'
156
162
  end
157
163
 
158
- def drop_temporal_triggers(table_name)
159
- raise NotImplementedError, "drop_temporal_triggers is not implemented"
164
+ def drop_temporal_triggers(_table_name)
165
+ raise NotImplementedError, 'drop_temporal_triggers is not implemented'
160
166
  end
161
167
 
162
168
  # It's important not to increase the length of the returned string.
163
169
  def temporal_index_name(index_name)
164
- 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')
165
171
  end
166
172
 
167
173
  def temporal_index_exists?(table_name, index_name)
168
- case Rails::VERSION::MAJOR
169
- when 5
170
- case Rails::VERSION::MINOR
171
- when 0
172
- index_name_exists?(temporal_name(table_name), index_name, false)
173
- else
174
- index_name_exists?(temporal_name(table_name), index_name)
175
- end
176
- when 6
177
- index_name_exists?(temporal_name(table_name), index_name)
178
- else
179
- raise "Rails version not supported"
180
- end
174
+ index_name_exists?(temporal_name(table_name), index_name)
181
175
  end
182
176
  end
183
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.7.0"
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