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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +53 -0
- data/.rubocop.yml +158 -0
- data/.ruby-version +1 -1
- data/.travis.yml +5 -5
- data/Gemfile +2 -0
- data/README.md +15 -5
- data/Rakefile +7 -2
- data/config.ru +2 -0
- data/gemfiles/Gemfile.6.0.mysql.lock +84 -84
- data/gemfiles/Gemfile.6.0.pg.lock +103 -98
- data/gemfiles/Gemfile.6.1.mysql.lock +180 -0
- data/gemfiles/Gemfile.6.1.pg.lock +180 -0
- data/gemfiles/{Gemfile.5.2.mysql → Gemfile.7.0.mysql} +2 -2
- data/gemfiles/Gemfile.7.0.mysql.lock +173 -0
- data/gemfiles/{Gemfile.5.2.pg → Gemfile.7.0.pg} +1 -1
- data/gemfiles/Gemfile.7.0.pg.lock +173 -0
- data/lib/temporal_tables/arel_table.rb +10 -9
- data/lib/temporal_tables/association_extensions.rb +2 -0
- data/lib/temporal_tables/connection_adapters/mysql_adapter.rb +5 -3
- data/lib/temporal_tables/connection_adapters/postgresql_adapter.rb +5 -3
- data/lib/temporal_tables/history_hook.rb +8 -5
- data/lib/temporal_tables/preloader_extensions.rb +2 -0
- data/lib/temporal_tables/reflection_extensions.rb +11 -14
- data/lib/temporal_tables/relation_extensions.rb +11 -24
- data/lib/temporal_tables/temporal_adapter.rb +77 -90
- data/lib/temporal_tables/temporal_class.rb +29 -28
- data/lib/temporal_tables/version.rb +3 -1
- data/lib/temporal_tables/whodunnit.rb +5 -3
- data/lib/temporal_tables.rb +42 -32
- data/spec/basic_history_spec.rb +52 -43
- data/spec/internal/app/models/broom.rb +2 -0
- data/spec/internal/app/models/cat.rb +3 -1
- data/spec/internal/app/models/cat_life.rb +2 -0
- data/spec/internal/app/models/coven.rb +2 -0
- data/spec/internal/app/models/flying_machine.rb +2 -0
- data/spec/internal/app/models/person.rb +2 -0
- data/spec/internal/app/models/rocket_broom.rb +2 -0
- data/spec/internal/app/models/wart.rb +3 -1
- data/spec/internal/config/database.ci.yml +12 -0
- data/spec/internal/db/schema.rb +8 -4
- data/spec/spec_helper.rb +39 -5
- data/spec/support/database.rb +10 -6
- data/temporal_tables.gemspec +31 -18
- metadata +103 -35
- data/.github/workflow/test.yml +0 -44
- data/gemfiles/Gemfile.5.1.mysql +0 -16
- data/gemfiles/Gemfile.5.1.mysql.lock +0 -147
- data/gemfiles/Gemfile.5.1.pg +0 -16
- data/gemfiles/Gemfile.5.1.pg.lock +0 -147
- data/gemfiles/Gemfile.5.2.mysql.lock +0 -155
- data/gemfiles/Gemfile.5.2.pg.lock +0 -155
- data/lib/temporal_tables/join_extensions.rb +0 -20
- data/spec/extensions/combustion.rb +0 -9
- data/spec/internal/config/routes.rb +0 -3
data/spec/basic_history_spec.rb
CHANGED
@@ -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:
|
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
|
14
|
-
let!(:coven) { Coven.create name:
|
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:
|
20
|
+
emily.update name: 'Grunthilda', coven: coven
|
19
21
|
sleep 0.1
|
20
22
|
end
|
21
23
|
|
22
|
-
describe
|
23
|
-
it
|
24
|
-
expect(emily.name).to eq(
|
25
|
-
expect(historical_emily.name).to eq(
|
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
|
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
|
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
|
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
|
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
|
51
|
+
describe 'when reflecting on the past' do
|
50
52
|
let(:orig_emily) { emily.history.at(@init_time).last }
|
51
53
|
|
52
|
-
it
|
53
|
-
expect(orig_emily.name).to eq(
|
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
|
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
|
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
|
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 =
|
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
|
94
|
-
it
|
95
|
-
expect(emily.class.name).to eq(
|
96
|
-
expect(historical_emily.class.name).to eq(
|
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
|
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
|
111
|
-
it
|
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
|
127
|
-
let!(:broom) { Broom.create person: emily, model:
|
135
|
+
describe 'when working with STI one level deep' do
|
136
|
+
let!(:broom) { Broom.create person: emily, model: 'Cackler 2000' }
|
128
137
|
|
129
|
-
it
|
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
|
135
|
-
let!(:rocket_broom) { RocketBroom.create person: emily, model:
|
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
|
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
|
145
|
-
let!(:cat) { Cat.create name:
|
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:
|
151
|
-
cat.lives.first.update ended_at: Time.now, death_reason:
|
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
|
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
|
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
|
data/spec/internal/db/schema.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
|
-
|
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
|
-
|
8
|
-
|
12
|
+
def adapter_name
|
13
|
+
if Gemika::Env.gem?('mysql2')
|
14
|
+
'mysql'
|
15
|
+
else
|
16
|
+
'postgresql'
|
17
|
+
end
|
9
18
|
end
|
10
19
|
|
11
|
-
|
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
|
data/spec/support/database.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
data/temporal_tables.gemspec
CHANGED
@@ -1,24 +1,37 @@
|
|
1
|
-
#
|
2
|
-
|
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
|
8
|
-
gem.version
|
9
|
-
gem.authors
|
10
|
-
gem.email
|
11
|
-
gem.description
|
12
|
-
|
13
|
-
|
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
|
16
|
-
gem.executables
|
17
|
-
gem.test_files
|
18
|
-
gem.require_paths = [
|
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
|
21
|
-
gem.add_development_dependency
|
22
|
-
gem.add_development_dependency
|
23
|
-
gem.add_development_dependency
|
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
|