chrono_model 1.2.0 → 1.2.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/Rakefile +2 -1
- data/lib/chrono_model/time_machine.rb +1 -1
- data/lib/chrono_model/time_machine/history_model.rb +4 -0
- data/lib/chrono_model/version.rb +1 -1
- data/spec/chrono_model/adapter/base_spec.rb +157 -0
- data/spec/chrono_model/adapter/ddl_spec.rb +243 -0
- data/spec/chrono_model/adapter/indexes_spec.rb +72 -0
- data/spec/chrono_model/adapter/migrations_spec.rb +312 -0
- data/spec/chrono_model/history_models_spec.rb +32 -0
- data/spec/chrono_model/time_machine/as_of_spec.rb +188 -0
- data/spec/chrono_model/time_machine/changes_spec.rb +50 -0
- data/spec/chrono_model/{adapter → time_machine}/counter_cache_race_spec.rb +2 -2
- data/spec/chrono_model/time_machine/default_scope_spec.rb +37 -0
- data/spec/chrono_model/time_machine/history_spec.rb +104 -0
- data/spec/chrono_model/time_machine/keep_cool_spec.rb +27 -0
- data/spec/chrono_model/time_machine/manipulations_spec.rb +84 -0
- data/spec/chrono_model/time_machine/model_identification_spec.rb +46 -0
- data/spec/chrono_model/time_machine/sequence_spec.rb +74 -0
- data/spec/chrono_model/time_machine/sti_spec.rb +100 -0
- data/spec/chrono_model/{time_query_spec.rb → time_machine/time_query_spec.rb} +22 -5
- data/spec/chrono_model/time_machine/timeline_spec.rb +63 -0
- data/spec/chrono_model/time_machine/timestamps_spec.rb +43 -0
- data/spec/chrono_model/time_machine/transactions_spec.rb +69 -0
- data/spec/support/adapter/helpers.rb +53 -0
- data/spec/support/adapter/structure.rb +44 -0
- data/spec/support/time_machine/helpers.rb +47 -0
- data/spec/support/time_machine/structure.rb +111 -0
- metadata +48 -14
- data/spec/chrono_model/adapter/sti_bug_spec.rb +0 -49
- data/spec/chrono_model/adapter_spec.rb +0 -788
- data/spec/chrono_model/time_machine_spec.rb +0 -749
- data/spec/support/helpers.rb +0 -198
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 21c2181fc18f6dd3a5bdd89940eae284b340427bc81a4f024b6ad0686c146598
|
4
|
+
data.tar.gz: e7afa88021f63cd42e160a800f66e84bff1ba90f706c209df436059f70ae04e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a99b96b4166a7355016840fd7e7c6cf01febc86e89b1a6e07421db89e7d234972e214a9881b9a3bea2c8210bac2f83735a0d5d7d52c877c5b60160165fc09b16
|
7
|
+
data.tar.gz: 84300076a2b5211d3f1f3b317c087f893c754fcec43a2b64614b1a80b53304d95d6e686b5e25144758dd5731d7cd12de662dd45b981d9e259810346e186fe75d
|
data/Rakefile
CHANGED
@@ -3,7 +3,8 @@ require "bundler/gem_tasks"
|
|
3
3
|
|
4
4
|
# RSpec
|
5
5
|
require 'rspec/core/rake_task'
|
6
|
-
RSpec::Core::RakeTask.new
|
6
|
+
spec_task = RSpec::Core::RakeTask.new
|
7
|
+
spec_task.rspec_opts = '-f doc'
|
7
8
|
task :default => ['testapp:create', :spec]
|
8
9
|
|
9
10
|
# Create a test Rails app in tmp/railsapp for testing the rake
|
@@ -132,7 +132,7 @@ module ChronoModel
|
|
132
132
|
# Returns a boolean indicating whether this record is an history entry.
|
133
133
|
#
|
134
134
|
def historical?
|
135
|
-
self.as_of_time.present?
|
135
|
+
self.as_of_time.present?
|
136
136
|
end
|
137
137
|
|
138
138
|
# Inhibit destroy of historical records
|
data/lib/chrono_model/version.rb
CHANGED
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/adapter/structure'
|
3
|
+
|
4
|
+
describe ChronoModel::Adapter do
|
5
|
+
include ChronoTest::Adapter::Helpers
|
6
|
+
include ChronoTest::Adapter::Structure
|
7
|
+
|
8
|
+
subject { adapter }
|
9
|
+
it { is_expected.to be_a_kind_of(ChronoModel::Adapter) }
|
10
|
+
|
11
|
+
context do
|
12
|
+
subject { adapter.adapter_name }
|
13
|
+
it { is_expected.to eq 'PostgreSQL' }
|
14
|
+
end
|
15
|
+
|
16
|
+
context do
|
17
|
+
before { expect(adapter).to receive(:postgresql_version).and_return(90300) }
|
18
|
+
it { is_expected.to be_chrono_supported }
|
19
|
+
end
|
20
|
+
|
21
|
+
context do
|
22
|
+
before { expect(adapter).to receive(:postgresql_version).and_return(90000) }
|
23
|
+
it { is_expected.to_not be_chrono_supported }
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '.primary_key' do
|
27
|
+
subject { adapter.primary_key(table) }
|
28
|
+
|
29
|
+
assert = proc do
|
30
|
+
it { is_expected.to eq 'id' }
|
31
|
+
end
|
32
|
+
|
33
|
+
with_temporal_table(&assert)
|
34
|
+
with_plain_table( &assert)
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '.indexes' do
|
38
|
+
subject { adapter.indexes(table) }
|
39
|
+
|
40
|
+
assert = proc do
|
41
|
+
before(:all) do
|
42
|
+
adapter.add_index table, :foo, :name => 'foo_index'
|
43
|
+
adapter.add_index table, [:bar, :baz], :name => 'bar_index'
|
44
|
+
end
|
45
|
+
|
46
|
+
it { expect(subject.map(&:name)).to match_array %w( foo_index bar_index ) }
|
47
|
+
it { expect(subject.map(&:columns)).to match_array [['foo'], ['bar', 'baz']] }
|
48
|
+
end
|
49
|
+
|
50
|
+
with_temporal_table(&assert)
|
51
|
+
with_plain_table( &assert)
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '.column_definitions' do
|
55
|
+
subject { adapter.column_definitions(table).map {|d| d.take(2)} }
|
56
|
+
|
57
|
+
assert = proc do
|
58
|
+
it { expect(subject & columns).to eq columns }
|
59
|
+
it { is_expected.to include(['id', pk_type]) }
|
60
|
+
end
|
61
|
+
|
62
|
+
with_temporal_table(&assert)
|
63
|
+
with_plain_table( &assert)
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '.on_schema' do
|
67
|
+
before(:all) do
|
68
|
+
adapter.execute 'BEGIN'
|
69
|
+
5.times {|i| adapter.execute "CREATE SCHEMA test_#{i}"}
|
70
|
+
end
|
71
|
+
|
72
|
+
after(:all) do
|
73
|
+
adapter.execute 'ROLLBACK'
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'by default' do
|
77
|
+
it 'saves the schema at each recursion' do
|
78
|
+
is_expected.to be_in_schema(:default)
|
79
|
+
|
80
|
+
adapter.on_schema('test_1') { is_expected.to be_in_schema('test_1')
|
81
|
+
adapter.on_schema('test_2') { is_expected.to be_in_schema('test_2')
|
82
|
+
adapter.on_schema('test_3') { is_expected.to be_in_schema('test_3')
|
83
|
+
}
|
84
|
+
is_expected.to be_in_schema('test_2')
|
85
|
+
}
|
86
|
+
is_expected.to be_in_schema('test_1')
|
87
|
+
}
|
88
|
+
|
89
|
+
is_expected.to be_in_schema(:default)
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'when errors occur' do
|
93
|
+
subject do
|
94
|
+
adapter.on_schema('test_1') do
|
95
|
+
|
96
|
+
adapter.on_schema('test_2') do
|
97
|
+
adapter.execute 'BEGIN'
|
98
|
+
adapter.execute 'ERRORING ON PURPOSE'
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
it {
|
105
|
+
expect { subject }.
|
106
|
+
to raise_error(/current transaction is aborted/).
|
107
|
+
and change { adapter.instance_variable_get(:@schema_search_path) }
|
108
|
+
}
|
109
|
+
|
110
|
+
after do
|
111
|
+
adapter.execute 'ROLLBACK'
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'with recurse: :ignore' do
|
117
|
+
it 'ignores recursive calls' do
|
118
|
+
is_expected.to be_in_schema(:default)
|
119
|
+
|
120
|
+
adapter.on_schema('test_1', recurse: :ignore) { is_expected.to be_in_schema('test_1')
|
121
|
+
adapter.on_schema('test_2', recurse: :ignore) { is_expected.to be_in_schema('test_1')
|
122
|
+
adapter.on_schema('test_3', recurse: :ignore) { is_expected.to be_in_schema('test_1')
|
123
|
+
} } }
|
124
|
+
|
125
|
+
is_expected.to be_in_schema(:default)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe '.is_chrono?' do
|
131
|
+
with_temporal_table do
|
132
|
+
it { expect(adapter.is_chrono?(table)).to be(true) }
|
133
|
+
end
|
134
|
+
|
135
|
+
with_plain_table do
|
136
|
+
it { expect(adapter.is_chrono?(table)).to be(false) }
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'when schemas are not there yet' do
|
140
|
+
before(:all) do
|
141
|
+
adapter.execute 'BEGIN'
|
142
|
+
adapter.execute 'DROP SCHEMA temporal CASCADE'
|
143
|
+
adapter.execute 'DROP SCHEMA history CASCADE'
|
144
|
+
adapter.execute 'CREATE TABLE test_table (id integer)'
|
145
|
+
end
|
146
|
+
|
147
|
+
after(:all) do
|
148
|
+
adapter.execute 'ROLLBACK'
|
149
|
+
end
|
150
|
+
|
151
|
+
it { expect { adapter.is_chrono?(table) }.to_not raise_error }
|
152
|
+
|
153
|
+
it { expect(adapter.is_chrono?(table)).to be(false) }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/adapter/structure'
|
3
|
+
|
4
|
+
describe ChronoModel::Adapter do
|
5
|
+
include ChronoTest::Adapter::Helpers
|
6
|
+
include ChronoTest::Adapter::Structure
|
7
|
+
|
8
|
+
let(:current) { [ChronoModel::Adapter::TEMPORAL_SCHEMA, table].join('.') }
|
9
|
+
let(:history) { [ChronoModel::Adapter::HISTORY_SCHEMA, table].join('.') }
|
10
|
+
|
11
|
+
def count(table)
|
12
|
+
adapter.select_value("SELECT COUNT(*) FROM ONLY #{table}").to_i
|
13
|
+
end
|
14
|
+
|
15
|
+
def ids(table)
|
16
|
+
adapter.select_values("SELECT id FROM ONLY #{table} ORDER BY id")
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'INSERT multiple values' do
|
20
|
+
before :all do
|
21
|
+
adapter.create_table table, :temporal => true, &columns
|
22
|
+
end
|
23
|
+
|
24
|
+
after :all do
|
25
|
+
adapter.drop_table table
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when succeeding' do
|
29
|
+
def insert
|
30
|
+
adapter.execute <<-SQL
|
31
|
+
INSERT INTO #{table} (test, foo) VALUES
|
32
|
+
('test1', 1),
|
33
|
+
('test2', 2);
|
34
|
+
SQL
|
35
|
+
end
|
36
|
+
|
37
|
+
it { expect { insert }.to_not raise_error }
|
38
|
+
it { expect(count(current)).to eq 2 }
|
39
|
+
it { expect(count(history)).to eq 2 }
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'when failing' do
|
43
|
+
def insert
|
44
|
+
adapter.execute <<-SQL
|
45
|
+
INSERT INTO #{table} (test, foo) VALUES
|
46
|
+
('test3', 3),
|
47
|
+
(NULL, 0);
|
48
|
+
SQL
|
49
|
+
end
|
50
|
+
|
51
|
+
it { expect { insert }.to raise_error(ActiveRecord::StatementInvalid) }
|
52
|
+
it { expect(count(current)).to eq 2 } # Because the previous
|
53
|
+
it { expect(count(history)).to eq 2 } # records are preserved
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'after a failure' do
|
57
|
+
def insert
|
58
|
+
adapter.execute <<-SQL
|
59
|
+
INSERT INTO #{table} (test, foo) VALUES
|
60
|
+
('test4', 3),
|
61
|
+
('test5', 4);
|
62
|
+
SQL
|
63
|
+
end
|
64
|
+
|
65
|
+
it { expect { insert }.to_not raise_error }
|
66
|
+
|
67
|
+
it { expect(count(current)).to eq 4 }
|
68
|
+
it { expect(count(history)).to eq 4 }
|
69
|
+
|
70
|
+
it { expect(ids(current)).to eq ids(history) }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'INSERT on NOT NULL columns but with a DEFAULT value' do
|
75
|
+
before :all do
|
76
|
+
adapter.create_table table, :temporal => true, &columns
|
77
|
+
end
|
78
|
+
|
79
|
+
after :all do
|
80
|
+
adapter.drop_table table
|
81
|
+
end
|
82
|
+
|
83
|
+
def insert
|
84
|
+
adapter.execute <<-SQL
|
85
|
+
INSERT INTO #{table} DEFAULT VALUES
|
86
|
+
SQL
|
87
|
+
end
|
88
|
+
|
89
|
+
def select
|
90
|
+
adapter.select_values <<-SQL
|
91
|
+
SELECT test FROM #{table}
|
92
|
+
SQL
|
93
|
+
end
|
94
|
+
|
95
|
+
it { expect { insert }.to_not raise_error }
|
96
|
+
it { insert; expect(select.uniq).to eq ['default-value'] }
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'redundant UPDATEs' do
|
100
|
+
|
101
|
+
before :all do
|
102
|
+
adapter.create_table table, :temporal => true, &columns
|
103
|
+
|
104
|
+
adapter.execute <<-SQL
|
105
|
+
INSERT INTO #{table} (test, foo) VALUES ('test1', 1);
|
106
|
+
SQL
|
107
|
+
|
108
|
+
adapter.execute <<-SQL
|
109
|
+
UPDATE #{table} SET test = 'test2';
|
110
|
+
SQL
|
111
|
+
|
112
|
+
adapter.execute <<-SQL
|
113
|
+
UPDATE #{table} SET test = 'test2';
|
114
|
+
SQL
|
115
|
+
end
|
116
|
+
|
117
|
+
after :all do
|
118
|
+
adapter.drop_table table
|
119
|
+
end
|
120
|
+
|
121
|
+
it { expect(count(current)).to eq 1 }
|
122
|
+
it { expect(count(history)).to eq 2 }
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
context 'updates on non-journaled fields' do
|
127
|
+
before :all do
|
128
|
+
adapter.create_table table, :temporal => true do |t|
|
129
|
+
t.string 'test'
|
130
|
+
t.timestamps null: false
|
131
|
+
end
|
132
|
+
|
133
|
+
adapter.execute <<-SQL
|
134
|
+
INSERT INTO #{table} (test, created_at, updated_at) VALUES ('test', now(), now());
|
135
|
+
SQL
|
136
|
+
|
137
|
+
adapter.execute <<-SQL
|
138
|
+
UPDATE #{table} SET test = 'test2', updated_at = now();
|
139
|
+
SQL
|
140
|
+
|
141
|
+
2.times do
|
142
|
+
adapter.execute <<-SQL # Redundant update with only updated_at change
|
143
|
+
UPDATE #{table} SET test = 'test2', updated_at = now();
|
144
|
+
SQL
|
145
|
+
|
146
|
+
adapter.execute <<-SQL
|
147
|
+
UPDATE #{table} SET updated_at = now();
|
148
|
+
SQL
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
after :all do
|
153
|
+
adapter.drop_table table
|
154
|
+
end
|
155
|
+
|
156
|
+
it { expect(count(current)).to eq 1 }
|
157
|
+
it { expect(count(history)).to eq 2 }
|
158
|
+
end
|
159
|
+
|
160
|
+
context 'selective journaled fields' do
|
161
|
+
describe 'basic behaviour' do
|
162
|
+
specify do
|
163
|
+
adapter.create_table table, :temporal => true, :journal => %w( foo ) do |t|
|
164
|
+
t.string 'foo'
|
165
|
+
t.string 'bar'
|
166
|
+
end
|
167
|
+
|
168
|
+
adapter.execute <<-SQL
|
169
|
+
INSERT INTO #{table} (foo, bar) VALUES ('test foo', 'test bar');
|
170
|
+
SQL
|
171
|
+
|
172
|
+
adapter.execute <<-SQL
|
173
|
+
UPDATE #{table} SET foo = 'test foo', bar = 'no history';
|
174
|
+
SQL
|
175
|
+
|
176
|
+
2.times do
|
177
|
+
adapter.execute <<-SQL
|
178
|
+
UPDATE #{table} SET bar = 'really no history';
|
179
|
+
SQL
|
180
|
+
end
|
181
|
+
|
182
|
+
expect(count(current)).to eq 1
|
183
|
+
expect(count(history)).to eq 1
|
184
|
+
|
185
|
+
adapter.drop_table table
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
describe 'schema changes' do
|
190
|
+
table 'journaled_things'
|
191
|
+
|
192
|
+
before do
|
193
|
+
adapter.create_table table, :temporal => true, :journal => %w( foo ) do |t|
|
194
|
+
t.string 'foo'
|
195
|
+
t.string 'bar'
|
196
|
+
t.string 'baz'
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
after do
|
201
|
+
adapter.drop_table table
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'preserves options upon column change' do
|
205
|
+
adapter.change_table table, temporal: true, journal: %w(foo bar)
|
206
|
+
|
207
|
+
adapter.execute <<-SQL
|
208
|
+
INSERT INTO #{table} (foo, bar) VALUES ('test foo', 'test bar');
|
209
|
+
SQL
|
210
|
+
|
211
|
+
expect(count(current)).to eq 1
|
212
|
+
expect(count(history)).to eq 1
|
213
|
+
|
214
|
+
adapter.execute <<-SQL
|
215
|
+
UPDATE #{table} SET foo = 'test foo', bar = 'chronomodel';
|
216
|
+
SQL
|
217
|
+
|
218
|
+
expect(count(current)).to eq 1
|
219
|
+
expect(count(history)).to eq 2
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'changes option upon table change' do
|
223
|
+
adapter.change_table table, temporal: true, journal: %w(bar)
|
224
|
+
|
225
|
+
adapter.execute <<-SQL
|
226
|
+
INSERT INTO #{table} (foo, bar) VALUES ('test foo', 'test bar');
|
227
|
+
UPDATE #{table} SET foo = 'test foo', bar = 'no history';
|
228
|
+
SQL
|
229
|
+
|
230
|
+
expect(count(current)).to eq 1
|
231
|
+
expect(count(history)).to eq 1
|
232
|
+
|
233
|
+
adapter.execute <<-SQL
|
234
|
+
UPDATE #{table} SET foo = 'test foo again', bar = 'no history';
|
235
|
+
SQL
|
236
|
+
|
237
|
+
expect(count(current)).to eq 1
|
238
|
+
expect(count(history)).to eq 1
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|