temporal_tables 0.6.10 → 0.7.0
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/.ruby-version +1 -1
- data/.travis.yml +3 -1
- data/README.md +1 -1
- data/gemfiles/Gemfile.5.0.mysql.lock +3 -3
- data/gemfiles/Gemfile.5.0.pg.lock +3 -3
- data/gemfiles/Gemfile.5.1.mysql.lock +3 -3
- data/gemfiles/Gemfile.5.1.pg.lock +3 -3
- data/gemfiles/Gemfile.5.2.mysql.lock +3 -3
- data/gemfiles/Gemfile.5.2.pg.lock +3 -3
- data/gemfiles/Gemfile.6.0.mysql +16 -0
- data/gemfiles/Gemfile.6.0.mysql.lock +171 -0
- data/gemfiles/Gemfile.6.0.pg +16 -0
- data/gemfiles/Gemfile.6.0.pg.lock +171 -0
- data/lib/temporal_tables/arel_table.rb +19 -0
- data/lib/temporal_tables/association_extensions.rb +16 -16
- data/lib/temporal_tables/connection_adapters/mysql_adapter.rb +54 -54
- data/lib/temporal_tables/connection_adapters/postgresql_adapter.rb +64 -64
- data/lib/temporal_tables/history_hook.rb +43 -43
- data/lib/temporal_tables/join_extensions.rb +14 -14
- data/lib/temporal_tables/preloader_extensions.rb +14 -14
- data/lib/temporal_tables/reflection_extensions.rb +14 -14
- data/lib/temporal_tables/relation_extensions.rb +79 -79
- data/lib/temporal_tables/temporal_adapter.rb +181 -174
- data/lib/temporal_tables/temporal_class.rb +101 -101
- data/lib/temporal_tables/version.rb +1 -1
- data/lib/temporal_tables/whodunnit.rb +16 -16
- data/lib/temporal_tables.rb +46 -45
- data/spec/basic_history_spec.rb +138 -138
- data/spec/internal/app/models/flying_machine.rb +1 -1
- data/spec/internal/app/models/person.rb +3 -3
- data/spec/internal/db/schema.rb +17 -17
- data/spec/spec_helper.rb +4 -4
- data/spec/support/database.rb +7 -7
- data/temporal_tables.gemspec +15 -15
- metadata +16 -6
data/lib/temporal_tables.rb
CHANGED
@@ -9,58 +9,59 @@ require "temporal_tables/association_extensions"
|
|
9
9
|
require "temporal_tables/join_extensions"
|
10
10
|
require "temporal_tables/preloader_extensions"
|
11
11
|
require "temporal_tables/reflection_extensions"
|
12
|
+
require "temporal_tables/arel_table"
|
12
13
|
require "temporal_tables/version"
|
13
14
|
|
14
15
|
module TemporalTables
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
class Railtie < ::Rails::Railtie
|
17
|
+
initializer "temporal_tables.load" do
|
18
|
+
# Iterating the subclasses will find any adapter implementations
|
19
|
+
# which are in use by the rails app, and mixin the temporal functionality.
|
20
|
+
# It's necessary to do this on the implementations in order for the
|
21
|
+
# alias method chain hooks to work.
|
22
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.subclasses.each do |subclass|
|
23
|
+
subclass.send :prepend, TemporalTables::TemporalAdapter
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
25
|
+
module_name = subclass.name.split("::").last
|
26
|
+
subclass.send :prepend, TemporalTables::ConnectionAdapters.const_get(module_name) if TemporalTables::ConnectionAdapters.const_defined?(module_name)
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
ActiveRecord::Base.send :include, TemporalTables::Whodunnit
|
30
|
+
end
|
31
|
+
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
33
|
+
@@create_by_default = false
|
34
|
+
def self.create_by_default
|
35
|
+
@@create_by_default
|
36
|
+
end
|
37
|
+
def self.create_by_default=(default)
|
38
|
+
@@create_by_default = default
|
39
|
+
end
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
41
|
+
@@skipped_temporal_tables = [:schema_migrations, :sessions, :ar_internal_metadata]
|
42
|
+
def self.skip_temporal_table_for(*tables)
|
43
|
+
@@skipped_temporal_tables += tables
|
44
|
+
end
|
45
|
+
def self.skipped_temporal_tables
|
46
|
+
@@skipped_temporal_tables.dup
|
47
|
+
end
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
49
|
+
@@add_updated_by_field = false
|
50
|
+
@@updated_by_type = :string
|
51
|
+
@@updated_by_proc = nil
|
52
|
+
def self.updated_by_type
|
53
|
+
@@updated_by_type
|
54
|
+
end
|
55
|
+
def self.updated_by_proc
|
56
|
+
@@updated_by_proc
|
57
|
+
end
|
58
|
+
def self.add_updated_by_field(type = :string, &block)
|
59
|
+
if block_given?
|
60
|
+
@@add_updated_by_field = true
|
61
|
+
@@updated_by_type = type
|
62
|
+
@@updated_by_proc = block
|
63
|
+
end
|
63
64
|
|
64
|
-
|
65
|
-
|
65
|
+
@@add_updated_by_field
|
66
|
+
end
|
66
67
|
end
|
data/spec/basic_history_spec.rb
CHANGED
@@ -1,142 +1,142 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Person do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
4
|
+
let(:emily) { Person.create name: "Emily" }
|
5
|
+
let(:historical_emily) { emily.history.last }
|
6
|
+
|
7
|
+
before do
|
8
|
+
emily
|
9
|
+
@init_time = Time.now
|
10
|
+
sleep 0.1
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "upon making significant life changes" do
|
14
|
+
let!(:coven) { Coven.create name: "Double Double Toil & Trouble" }
|
15
|
+
let!(:wart) { Wart.create person: emily, hairiness: 3 }
|
16
|
+
|
17
|
+
before do
|
18
|
+
emily.update name: "Grunthilda", coven: coven
|
19
|
+
sleep 0.1
|
20
|
+
end
|
21
|
+
|
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")
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should belong to coven" do
|
29
|
+
expect(emily.coven.name).to eq(coven.name)
|
30
|
+
expect(historical_emily.coven.name).to eq(coven.name)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should have a wart" do
|
34
|
+
expect(emily.warts).to eq([wart])
|
35
|
+
expect(emily.history.at(Time.now).last.warts).to eq([wart.history.last])
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should allow scopes on associations" do
|
39
|
+
expect(emily.warts.very_hairy).to eq([wart])
|
40
|
+
expect(historical_emily.warts.very_hairy).to eq([wart.history.last])
|
41
|
+
end
|
42
|
+
|
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
|
+
expect(Wart.history.at(1.minute.ago).where(person: emily).count).to eq(0)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "when reflecting on the past" do
|
50
|
+
let(:orig_emily) { emily.history.at(@init_time).last }
|
51
|
+
|
52
|
+
it "should have historical name" do
|
53
|
+
expect(orig_emily.name).to eq("Emily")
|
54
|
+
expect(orig_emily.at_value).to eq(@init_time)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should not belong to a coven or have warts" do
|
58
|
+
expect(orig_emily.coven).to eq(nil)
|
59
|
+
expect(orig_emily.warts.count).to eq(0)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "when preloading associations" do
|
64
|
+
let(:orig_emily) { emily.history.at(@init_time).preload(:warts).first }
|
65
|
+
|
66
|
+
it 'should preload the correct time' do
|
67
|
+
expect(orig_emily.warts).to be_empty
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "when eager_loading associations" do
|
72
|
+
let(:orig_emily) { emily.history.at(@init_time).eager_load(:warts).first }
|
73
|
+
|
74
|
+
it 'should include the correct time' do
|
75
|
+
expect(orig_emily.warts).to be_empty
|
76
|
+
end
|
77
|
+
|
78
|
+
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)/)
|
80
|
+
from = sql[2]
|
81
|
+
where = sql[4]
|
82
|
+
|
83
|
+
expect(from.scan(/.warts_h.\..eff_from./i).count).to eq(1)
|
84
|
+
expect(from.scan(/.warts_h.\..eff_to./i).count).to eq(1)
|
85
|
+
|
86
|
+
expect(where.scan(/.people_h.\..eff_from./i).count).to eq(1)
|
87
|
+
expect(where.scan(/.people_h.\..eff_to./i).count).to eq(1)
|
88
|
+
expect(where.scan(/.warts_h.\..eff_from./i).count).to eq(0)
|
89
|
+
expect(where.scan(/.warts_h.\..eff_to./i).count).to eq(0)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
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")
|
97
|
+
|
98
|
+
expect(Person.history).to eq(PersonHistory)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should have correct class hierarchies" do
|
102
|
+
expect(emily.is_a?(Person)).to eq(true)
|
103
|
+
expect(emily.is_a?(PersonHistory)).to eq(false)
|
104
|
+
|
105
|
+
expect(historical_emily.is_a?(Person)).to eq(true)
|
106
|
+
expect(historical_emily.is_a?(PersonHistory)).to eq(true)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "when checking current state" do
|
111
|
+
it "should have correct information" do
|
112
|
+
# ie. we shouldn't break regular ActiveRecord behaviour
|
113
|
+
expect(Person.count).to eq(1)
|
114
|
+
expect(Wart.count).to eq(1)
|
115
|
+
|
116
|
+
emily = Person.first
|
117
|
+
expect(emily.warts.count).to eq(1)
|
118
|
+
expect(emily.warts.first.hairiness).to eq(3)
|
119
|
+
|
120
|
+
emily = Person.where(id: emily.id).eager_load(:warts).first
|
121
|
+
expect(emily.warts.count).to eq(1)
|
122
|
+
expect(emily.warts.first.hairiness).to eq(3)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "when working with STI one level deep" do
|
127
|
+
let!(:broom) { Broom.create person: emily, model: "Cackler 2000" }
|
128
|
+
|
129
|
+
it "should initialize model correctly" do
|
130
|
+
expect(emily.history.last.flying_machines).to eq([broom.history.last])
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe "when working with STI two levels deep" do
|
135
|
+
let!(:rocket_broom) { RocketBroom.create person: emily, model: "Pyrocackler 3000X" }
|
136
|
+
|
137
|
+
it "should initialize model correctly" do
|
138
|
+
expect(emily.history.last.flying_machines).to eq([rocket_broom.history.last])
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
142
|
end
|
data/spec/internal/db/schema.rb
CHANGED
@@ -1,22 +1,22 @@
|
|
1
1
|
ActiveRecord::Schema.define do
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
2
|
+
create_table :people, temporal: true, force: true do |t|
|
3
|
+
t.belongs_to :coven
|
4
|
+
t.string :name
|
5
|
+
end
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
create_table :covens, force: true do |t|
|
8
|
+
t.string :name
|
9
|
+
end
|
10
|
+
add_temporal_table :covens
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
create_table :warts, temporal: true, force: true do |t|
|
13
|
+
t.belongs_to :person
|
14
|
+
t.integer :hairiness
|
15
|
+
end
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
create_table :flying_machines, temporal: true, force: true do |t|
|
18
|
+
t.belongs_to :person
|
19
|
+
t.string :type
|
20
|
+
t.string :model
|
21
|
+
end
|
22
22
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -5,13 +5,13 @@ Dir["#{File.dirname(__FILE__)}/extensions/*.rb"].sort.each {|f| require f}
|
|
5
5
|
Dir["#{File.dirname(__FILE__)}/support/*.rb"].sort.each {|f| require f}
|
6
6
|
|
7
7
|
Combustion.initialize! :active_record do
|
8
|
-
|
8
|
+
Rails.env = TemporalTables::DatabaseAdapter.adapter_name
|
9
9
|
end
|
10
10
|
|
11
11
|
DatabaseCleaner.strategy = :deletion
|
12
12
|
|
13
13
|
RSpec.configure do |config|
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
config.before(:each) do
|
15
|
+
DatabaseCleaner.clean
|
16
|
+
end
|
17
17
|
end
|
data/spec/support/database.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module TemporalTables::DatabaseAdapter
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
2
|
+
def self.adapter_name
|
3
|
+
if Gemika::Env.gem?('pg')
|
4
|
+
"postgresql"
|
5
|
+
elsif Gemika::Env.gem?('mysql2')
|
6
|
+
"mysql"
|
7
|
+
end
|
8
|
+
end
|
9
9
|
end
|
data/temporal_tables.gemspec
CHANGED
@@ -4,21 +4,21 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'temporal_tables/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |gem|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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 = ""
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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"]
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
gem.add_dependency "rails", ">= 5.0", "<= 6.0"
|
21
|
+
gem.add_development_dependency "rspec", "~> 3.4"
|
22
|
+
gem.add_development_dependency "combustion", "~> 0.9.1"
|
23
|
+
gem.add_development_dependency "gemika"
|
24
24
|
end
|
metadata
CHANGED
@@ -1,29 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: temporal_tables
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brent Kroeker
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-08-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '5.0'
|
20
|
+
- - "<="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '6.0'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - "
|
27
|
+
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
29
|
version: '5.0'
|
30
|
+
- - "<="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '6.0'
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: rspec
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -96,7 +102,12 @@ files:
|
|
96
102
|
- gemfiles/Gemfile.5.2.mysql.lock
|
97
103
|
- gemfiles/Gemfile.5.2.pg
|
98
104
|
- gemfiles/Gemfile.5.2.pg.lock
|
105
|
+
- gemfiles/Gemfile.6.0.mysql
|
106
|
+
- gemfiles/Gemfile.6.0.mysql.lock
|
107
|
+
- gemfiles/Gemfile.6.0.pg
|
108
|
+
- gemfiles/Gemfile.6.0.pg.lock
|
99
109
|
- lib/temporal_tables.rb
|
110
|
+
- lib/temporal_tables/arel_table.rb
|
100
111
|
- lib/temporal_tables/association_extensions.rb
|
101
112
|
- lib/temporal_tables/connection_adapters/mysql_adapter.rb
|
102
113
|
- lib/temporal_tables/connection_adapters/postgresql_adapter.rb
|
@@ -143,8 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
143
154
|
- !ruby/object:Gem::Version
|
144
155
|
version: '0'
|
145
156
|
requirements: []
|
146
|
-
|
147
|
-
rubygems_version: 2.7.6
|
157
|
+
rubygems_version: 3.0.3
|
148
158
|
signing_key:
|
149
159
|
specification_version: 4
|
150
160
|
summary: Tracks all history of changes to a table automatically in a history table.
|