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,66 +1,68 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe Person do
4
- let(:emily) { Person.create name: "Emily" }
6
+ let(:emily) { Person.create name: 'Emily' }
5
7
  let(:historical_emily) { emily.history.last }
6
8
 
7
9
  before do
8
10
  emily
9
- @init_time = Time.now
11
+ @init_time = Time.zone.now
10
12
  sleep 0.1
11
13
  end
12
14
 
13
- describe "upon making significant life changes" do
14
- let!(:coven) { Coven.create name: "Double Double Toil & Trouble" }
15
+ describe 'upon making significant life changes' do
16
+ let!(:coven) { Coven.create name: 'Double Double Toil & Trouble' }
15
17
  let!(:wart) { Wart.create person: emily, hairiness: 3 }
16
18
 
17
19
  before do
18
- emily.update name: "Grunthilda", coven: coven
20
+ emily.update name: 'Grunthilda', coven: coven
19
21
  sleep 0.1
20
22
  end
21
23
 
22
- describe "when affirming changes" do
23
- it "should have new name" do
24
- expect(emily.name).to eq("Grunthilda")
25
- expect(historical_emily.name).to eq("Grunthilda")
24
+ describe 'when affirming changes' do
25
+ it 'should have new name' do
26
+ expect(emily.name).to eq('Grunthilda')
27
+ expect(historical_emily.name).to eq('Grunthilda')
26
28
  end
27
29
 
28
- it "should belong to coven" do
30
+ it 'should belong to coven' do
29
31
  expect(emily.coven.name).to eq(coven.name)
30
32
  expect(historical_emily.coven.name).to eq(coven.name)
31
33
  end
32
34
 
33
- it "should have a wart" do
35
+ it 'should have a wart' do
34
36
  expect(emily.warts).to eq([wart])
35
- expect(emily.history.at(Time.now).last.warts).to eq([wart.history.last])
37
+ expect(emily.history.at(Time.zone.now).last.warts).to eq([wart.history.last])
36
38
  end
37
39
 
38
- it "should allow scopes on associations" do
40
+ it 'should allow scopes on associations' do
39
41
  expect(emily.warts.very_hairy).to eq([wart])
40
42
  expect(historical_emily.warts.very_hairy).to eq([wart.history.last])
41
43
  end
42
44
 
43
- it "should allow at value on class too" do
44
- expect(Wart.history.at(Time.now).where(person: emily).count).to eq(1)
45
+ it 'should allow at value on class too' do
46
+ expect(Wart.history.at(Time.zone.now).where(person: emily).count).to eq(1)
45
47
  expect(Wart.history.at(1.minute.ago).where(person: emily).count).to eq(0)
46
48
  end
47
49
  end
48
50
 
49
- describe "when reflecting on the past" do
51
+ describe 'when reflecting on the past' do
50
52
  let(:orig_emily) { emily.history.at(@init_time).last }
51
53
 
52
- it "should have historical name" do
53
- expect(orig_emily.name).to eq("Emily")
54
+ it 'should have historical name' do
55
+ expect(orig_emily.name).to eq('Emily')
54
56
  expect(orig_emily.at_value).to eq(@init_time)
55
57
  end
56
58
 
57
- it "should not belong to a coven or have warts" do
59
+ it 'should not belong to a coven or have warts' do
58
60
  expect(orig_emily.coven).to eq(nil)
59
61
  expect(orig_emily.warts.count).to eq(0)
60
62
  end
61
63
  end
62
64
 
63
- describe "when preloading associations" do
65
+ describe 'when preloading associations' do
64
66
  let(:orig_emily) { emily.history.at(@init_time).preload(:warts).first }
65
67
 
66
68
  it 'should preload the correct time' do
@@ -68,7 +70,7 @@ describe Person do
68
70
  end
69
71
  end
70
72
 
71
- describe "when eager_loading associations" do
73
+ describe 'when eager_loading associations' do
72
74
  let(:orig_emily) { emily.history.at(@init_time).eager_load(:warts).first }
73
75
 
74
76
  it 'should include the correct time' do
@@ -76,7 +78,14 @@ describe Person do
76
78
  end
77
79
 
78
80
  it 'should generate sensible sql' do
79
- sql = emily.history.at(@init_time).eager_load(:warts).where(Wart.history.arel_table[:hairiness].gteq(2)).to_sql.split(/(FROM)|(WHERE)|(ORDER)/)
81
+ sql =
82
+ emily
83
+ .history
84
+ .at(@init_time)
85
+ .eager_load(:warts)
86
+ .where(Wart.history.arel_table[:hairiness].gteq(2))
87
+ .to_sql
88
+ .split(/(FROM)|(WHERE)|(ORDER)/)
80
89
  from = sql[2]
81
90
  where = sql[4]
82
91
 
@@ -90,15 +99,15 @@ describe Person do
90
99
  end
91
100
  end
92
101
 
93
- describe "when checking simple code values" do
94
- it "should have correct class names" do
95
- expect(emily.class.name).to eq("Person")
96
- expect(historical_emily.class.name).to eq("PersonHistory")
102
+ describe 'when checking simple code values' do
103
+ it 'should have correct class names' do
104
+ expect(emily.class.name).to eq('Person')
105
+ expect(historical_emily.class.name).to eq('PersonHistory')
97
106
 
98
107
  expect(Person.history).to eq(PersonHistory)
99
108
  end
100
109
 
101
- it "should have correct class hierarchies" do
110
+ it 'should have correct class hierarchies' do
102
111
  expect(emily.is_a?(Person)).to eq(true)
103
112
  expect(emily.is_a?(PersonHistory)).to eq(false)
104
113
 
@@ -107,8 +116,8 @@ describe Person do
107
116
  end
108
117
  end
109
118
 
110
- describe "when checking current state" do
111
- it "should have correct information" do
119
+ describe 'when checking current state' do
120
+ it 'should have correct information' do
112
121
  # ie. we shouldn't break regular ActiveRecord behaviour
113
122
  expect(Person.count).to eq(1)
114
123
  expect(Wart.count).to eq(1)
@@ -123,40 +132,40 @@ describe Person do
123
132
  end
124
133
  end
125
134
 
126
- describe "when working with STI one level deep" do
127
- let!(:broom) { Broom.create person: emily, model: "Cackler 2000" }
135
+ describe 'when working with STI one level deep' do
136
+ let!(:broom) { Broom.create person: emily, model: 'Cackler 2000' }
128
137
 
129
- it "should initialize model correctly" do
138
+ it 'should initialize model correctly' do
130
139
  expect(emily.history.last.flying_machines).to eq([broom.history.last])
131
140
  end
132
141
  end
133
142
 
134
- describe "when working with STI two levels deep" do
135
- let!(:rocket_broom) { RocketBroom.create person: emily, model: "Pyrocackler 3000X" }
143
+ describe 'when working with STI two levels deep' do
144
+ let!(:rocket_broom) { RocketBroom.create person: emily, model: 'Pyrocackler 3000X' }
136
145
 
137
- it "should initialize model correctly" do
146
+ it 'should initialize model correctly' do
138
147
  expect(emily.history.last.flying_machines).to eq([rocket_broom.history.last])
139
148
  end
140
149
  end
141
150
  end
142
151
 
143
152
  # The following only tests non-integer ids for postgres (see schema.rb)
144
- describe "when spawning and aging a creature with a non-integer id" do
145
- let!(:cat) { Cat.create name: "Mr. Mittens", color: "black" }
153
+ describe 'when spawning and aging a creature with a non-integer id' do
154
+ let!(:cat) { Cat.create name: 'Mr. Mittens', color: 'black' }
146
155
 
147
156
  before do
148
157
  cat.lives.create started_at: 3.years.ago
149
- @init_time = Time.now
150
- cat.update name: "Old Mr. Mittens"
151
- cat.lives.first.update ended_at: Time.now, death_reason: "fell into cauldron"
152
- cat.lives.create started_at: Time.now
158
+ @init_time = Time.zone.now
159
+ cat.update name: 'Old Mr. Mittens'
160
+ cat.lives.first.update ended_at: Time.zone.now, death_reason: 'fell into cauldron'
161
+ cat.lives.create started_at: Time.zone.now
153
162
  end
154
163
 
155
- it "shows one life at the beginning" do
164
+ it 'shows one life at the beginning' do
156
165
  expect(cat.history.at(@init_time).last.lives.size).to eq(1)
157
166
  end
158
167
 
159
- it "shows two lives at the end" do
168
+ it 'shows two lives at the end' do
160
169
  expect(cat.history.last.lives.size).to eq(2)
161
170
  end
162
171
  end
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Broom < FlyingMachine
2
4
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Cat < ActiveRecord::Base
2
- has_many :lives, class_name: "CatLife"
4
+ has_many :lives, class_name: 'CatLife'
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class CatLife < ActiveRecord::Base
2
4
  belongs_to :cat
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Coven < ActiveRecord::Base
2
4
  has_many :people
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class FlyingMachine < ActiveRecord::Base
2
4
  belongs_to :person
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Person < ActiveRecord::Base
2
4
  belongs_to :coven
3
5
  has_many :warts
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class RocketBroom < Broom
2
4
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Wart < ActiveRecord::Base
2
4
  belongs_to :person
3
5
 
4
- scope :very_hairy, -> {
6
+ scope :very_hairy, lambda {
5
7
  where(arel_table[:hairiness].gteq(3))
6
8
  }
7
9
  end
@@ -0,0 +1,12 @@
1
+ mysql:
2
+ adapter: mysql2
3
+ database: temporal_tables_test
4
+ host: 127.0.0.1
5
+ username: root
6
+ password: password
7
+
8
+ postgresql:
9
+ adapter: postgresql
10
+ database: temporal_tables_test
11
+ user: postgres
12
+ password:
@@ -1,9 +1,13 @@
1
- postgres = ActiveRecord::Base.connection.class.name == "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ postgres = ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
5
+ rescue NameError
6
+ postgres = false
7
+ end
2
8
 
3
9
  ActiveRecord::Schema.define do
4
- if postgres
5
- enable_extension "pgcrypto"
6
- end
10
+ enable_extension 'pgcrypto' if postgres
7
11
 
8
12
  create_table :people, temporal: true, force: true do |t|
9
13
  t.belongs_to :coven
data/spec/spec_helper.rb CHANGED
@@ -1,14 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'gemika'
2
4
  require 'combustion'
5
+ require 'yaml'
3
6
 
4
- Dir["#{File.dirname(__FILE__)}/extensions/*.rb"].sort.each {|f| require f}
5
- Dir["#{File.dirname(__FILE__)}/support/*.rb"].sort.each {|f| require f}
7
+ Dir["#{File.dirname(__FILE__)}/extensions/*.rb"].sort.each { |f| require f }
8
+ Dir["#{File.dirname(__FILE__)}/support/*.rb"].sort.each { |f| require f }
9
+ READ_DATABASE_CONFIG_LOCATION = 'spec/internal/config/database.ci.yml'
10
+ WRITE_DATABASE_CONFIG_LOCATION = 'spec/internal/config/database.yml'
6
11
 
7
- Combustion.initialize! :active_record do
8
- Rails.env = TemporalTables::DatabaseAdapter.adapter_name
12
+ def adapter_name
13
+ if Gemika::Env.gem?('mysql2')
14
+ 'mysql'
15
+ else
16
+ 'postgresql'
17
+ end
9
18
  end
10
19
 
11
- DatabaseCleaner.strategy = :deletion
20
+ def database_config_from_gems(file_location)
21
+ config = YAML.load_file(file_location)
22
+ data = config.slice(adapter_name)
23
+ { Rails.env.to_s => data }
24
+ end
25
+
26
+ original_env = Rails.env
27
+
28
+ puts database_config_from_gems(READ_DATABASE_CONFIG_LOCATION)
29
+ File.write(
30
+ WRITE_DATABASE_CONFIG_LOCATION,
31
+ database_config_from_gems(READ_DATABASE_CONFIG_LOCATION).to_yaml
32
+ )
33
+
34
+ Rails.env = adapter_name
35
+ database = Gemika::Database.new
36
+ database.connect
37
+
38
+ Gemika::RSpec.configure_clean_database_before_example
39
+ Rails.env = original_env
40
+
41
+ begin
42
+ Combustion.initialize! :active_record
43
+ rescue ActiveRecord::RecordNotUnique
44
+ # noop
45
+ end
12
46
 
13
47
  RSpec.configure do |config|
14
48
  config.before(:each) do
@@ -1,9 +1,13 @@
1
- module TemporalTables::DatabaseAdapter
2
- def self.adapter_name
3
- if Gemika::Env.gem?('pg')
4
- "postgresql"
5
- elsif Gemika::Env.gem?('mysql2')
6
- "mysql"
1
+ # frozen_string_literal: true
2
+
3
+ module TemporalTables
4
+ module DatabaseAdapter
5
+ def self.adapter_name
6
+ if Gemika::Env.gem?('pg')
7
+ 'postgresql'
8
+ elsif Gemika::Env.gem?('mysql2')
9
+ 'mysql'
10
+ end
7
11
  end
8
12
  end
9
13
  end
@@ -1,24 +1,37 @@
1
- # -*- encoding: utf-8 -*-
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ lib = File.expand_path('lib', __dir__)
3
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
6
  require 'temporal_tables/version'
5
7
 
6
- Gem::Specification.new do |gem|
7
- gem.name = "temporal_tables"
8
- gem.version = TemporalTables::VERSION
9
- gem.authors = ["Brent Kroeker"]
10
- gem.email = ["brent@bkroeker.com"]
11
- gem.description = %q{Easily recall what your data looked like at any point in the past! TemporalTables sets up and maintains history tables to track all temporal changes to to your data.}
12
- gem.summary = %q{Tracks all history of changes to a table automatically in a history table.}
13
- gem.homepage = ""
8
+ Gem::Specification.new do |gem| # rubocop:disable Metrics/BlockLength
9
+ gem.name = 'temporal_tables'
10
+ gem.version = TemporalTables::VERSION
11
+ gem.authors = ['Brent Kroeker']
12
+ gem.email = ['brent@bkroeker.com']
13
+ gem.description = <<-DESC
14
+ Easily recall what your data looked like at any point in the past!
15
+ TemporalTables sets up and maintains history tables to track all temporal changes to to your data.
16
+ DESC
17
+ gem.summary = 'Tracks all history of changes to a table automatically in a history table.'
18
+ gem.homepage = ''
14
19
 
15
- gem.files = `git ls-files`.split($/)
16
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
- gem.require_paths = ["lib"]
20
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
21
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
22
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
23
+ gem.require_paths = ['lib']
24
+ gem.required_ruby_version = '>= 2.5.0'
25
+ gem.metadata = { 'rubygems_mfa_required' => 'true' }
19
26
 
20
- gem.add_dependency "rails", ">= 5.0", "< 6.1"
21
- gem.add_development_dependency "rspec", "~> 3.4"
22
- gem.add_development_dependency "combustion", "~> 0.9.1"
23
- gem.add_development_dependency "gemika"
27
+ gem.add_dependency 'rails', '>= 6.0', '< 7.1'
28
+ gem.add_development_dependency 'combustion', '~> 1'
29
+ gem.add_development_dependency 'database_cleaner'
30
+ gem.add_development_dependency 'gemika', '~> 0.6'
31
+ gem.add_development_dependency 'mysql2'
32
+ gem.add_development_dependency 'pg'
33
+ gem.add_development_dependency 'pry'
34
+ gem.add_development_dependency 'rspec', '~> 3.4'
35
+ gem.add_development_dependency 'rubocop'
36
+ gem.metadata['rubygems_mfa_required'] = 'true'
24
37
  end