chrono_model 0.13.0 → 0.13.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +17 -1
- data/lib/chrono_model.rb +10 -0
- data/lib/chrono_model/adapter.rb +53 -15
- data/lib/chrono_model/patches.rb +3 -2
- data/lib/chrono_model/version.rb +1 -1
- data/spec/spec_helper.rb +4 -0
- data/spec/time_machine_spec.rb +29 -8
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 940172f2bad591657a9db2e6124c5b36ccab57cae41f8a0a85c2d473345b8f2c
|
4
|
+
data.tar.gz: 0060e673c40a25cd8d44f925483c85cb08dc794b15291705fa69e64c2b3d9eca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b850d300fe412425c472f04e022ff89ca9ab9d299f65c52ac65a4ccfd401828301d524868b57f5490e84d32d7baf4dd2c446ba9cf9ea6ab780910ee9ced80ad5
|
7
|
+
data.tar.gz: 26f7751a0ff8dc234f32d391a6cb84a853edd2213547613db3d413cceaba42475f12537a7f5e647d6c307f9016b445c16f956a5f912542c2c83a19b3de4f31e0
|
data/README.md
CHANGED
@@ -257,7 +257,23 @@ more information.
|
|
257
257
|
|
258
258
|
## History manipulation
|
259
259
|
|
260
|
-
History objects can be changed and `.save`d just like any other record.
|
260
|
+
History objects can be changed and `.save`d just like any other record. They
|
261
|
+
cannot be deleted.
|
262
|
+
|
263
|
+
## Upgrading
|
264
|
+
|
265
|
+
ChronoModel currently performs upgrades by dropping and re-creating the views
|
266
|
+
that give access to current data. If you have built other database objects on
|
267
|
+
these views, the upgrade cannot be performed automatically as the dependant
|
268
|
+
objects must be dropped first.
|
269
|
+
|
270
|
+
When booting, ChronoModel will issue a warning in your logs about the need of
|
271
|
+
a structure upgrade. Structure usually changes across versions. In this case,
|
272
|
+
you need to set up a rake task that drops your dependant objects, runs
|
273
|
+
ChronoModel.upgrade! and then re-creates them.
|
274
|
+
|
275
|
+
A migration system should be introduced, but it is seen as overkill for now,
|
276
|
+
given that usually database objects have creation and dropping scripts.
|
261
277
|
|
262
278
|
|
263
279
|
## Running tests
|
data/lib/chrono_model.rb
CHANGED
@@ -8,6 +8,16 @@ require 'chrono_model/utils'
|
|
8
8
|
module ChronoModel
|
9
9
|
class Error < ActiveRecord::ActiveRecordError #:nodoc:
|
10
10
|
end
|
11
|
+
|
12
|
+
def self.upgrade!
|
13
|
+
connection = ActiveRecord::Base.connection
|
14
|
+
|
15
|
+
unless connection.is_a?(ChronoModel::Adapter)
|
16
|
+
raise ChronoModel::Error, "This database connection is not a ChronoModel::Adapter"
|
17
|
+
end
|
18
|
+
|
19
|
+
connection.chrono_upgrade!
|
20
|
+
end
|
11
21
|
end
|
12
22
|
|
13
23
|
if defined?(Rails)
|
data/lib/chrono_model/adapter.rb
CHANGED
@@ -462,7 +462,13 @@ module ChronoModel
|
|
462
462
|
end
|
463
463
|
|
464
464
|
def chrono_setup!
|
465
|
-
|
465
|
+
chrono_ensure_schemas
|
466
|
+
|
467
|
+
chrono_upgrade_warning
|
468
|
+
end
|
469
|
+
|
470
|
+
def chrono_upgrade!
|
471
|
+
chrono_ensure_schemas
|
466
472
|
|
467
473
|
chrono_upgrade_structure!
|
468
474
|
end
|
@@ -541,37 +547,69 @@ module ChronoModel
|
|
541
547
|
private
|
542
548
|
# Create the temporal and history schemas, unless they already exist
|
543
549
|
#
|
544
|
-
def
|
550
|
+
def chrono_ensure_schemas
|
545
551
|
[TEMPORAL_SCHEMA, HISTORY_SCHEMA].each do |schema|
|
546
552
|
execute "CREATE SCHEMA #{schema}" unless schema_exists?(schema)
|
547
553
|
end
|
548
554
|
end
|
549
555
|
|
556
|
+
# Locate tables needing a structure upgrade
|
557
|
+
#
|
558
|
+
def chrono_tables_needing_upgrade
|
559
|
+
tables = { }
|
560
|
+
|
561
|
+
_on_temporal_schema { self.tables }.each do |table_name|
|
562
|
+
next unless is_chrono?(table_name)
|
563
|
+
metadata = chrono_metadata_for(table_name)
|
564
|
+
version = metadata['chronomodel']
|
565
|
+
|
566
|
+
if version.blank?
|
567
|
+
tables[table_name] = { version: nil, priority: 'CRITICAL' }
|
568
|
+
elsif version != VERSION
|
569
|
+
tables[table_name] = { version: version, priority: 'LOW' }
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
return tables
|
574
|
+
end
|
575
|
+
|
576
|
+
# Emit a warning about tables needing an upgrade
|
577
|
+
#
|
578
|
+
def chrono_upgrade_warning
|
579
|
+
upgrade = chrono_tables_needing_upgrade.map do |table, desc|
|
580
|
+
"#{table} - priority: #{desc[:priority]}"
|
581
|
+
end.join('; ')
|
582
|
+
|
583
|
+
return if upgrade.empty?
|
584
|
+
|
585
|
+
logger.warn "ChronoModel: There are tables needing a structure upgrade, and ChronoModel structures need to be recreated."
|
586
|
+
logger.warn "ChronoModel: Please run ChronoModel.upgrade! to attempt the upgrade. If you have dependant database objects"
|
587
|
+
logger.warn "ChronoModel: the upgrade will fail and you have to drop the dependent objects, run .upgrade! and create them"
|
588
|
+
logger.warn "ChronoModel: again. Sorry. Some features or the whole library may not work correctly until upgrade is complete."
|
589
|
+
logger.warn "ChronoModel: Tables pending upgrade: #{upgrade}"
|
590
|
+
end
|
591
|
+
|
550
592
|
# Upgrades existing structure for each table, if required.
|
551
|
-
# TODO: allow upgrades from pre-0.6 structure with box() and stuff.
|
552
593
|
#
|
553
594
|
def chrono_upgrade_structure!
|
554
595
|
transaction do
|
555
|
-
current = VERSION
|
556
596
|
|
557
|
-
|
558
|
-
next unless is_chrono?(table_name)
|
559
|
-
metadata = chrono_metadata_for(table_name)
|
560
|
-
version = metadata['chronomodel']
|
597
|
+
chrono_tables_needing_upgrade.each do |table_name, desc|
|
561
598
|
|
562
|
-
if version.blank?
|
599
|
+
if desc[:version].blank?
|
600
|
+
logger.info "ChronoModel: Upgrading legacy table #{table_name} to #{VERSION}"
|
563
601
|
upgrade_from_legacy(table_name)
|
602
|
+
logger.info "ChronoModel: legacy #{table_name} upgrade complete"
|
603
|
+
else
|
604
|
+
logger.info "ChronoModel: upgrading #{table_name} from #{version} to #{VERSION}"
|
605
|
+
chrono_create_view_for(table_name)
|
606
|
+
logger.info "ChronoModel: #{table_name} upgrade complete"
|
564
607
|
end
|
565
608
|
|
566
|
-
next if version == current
|
567
|
-
|
568
|
-
logger.info "ChronoModel: upgrading #{table_name} from #{version} to #{current}"
|
569
|
-
chrono_create_view_for(table_name)
|
570
|
-
logger.info "ChronoModel: upgrade complete"
|
571
609
|
end
|
572
610
|
end
|
573
611
|
rescue => e
|
574
|
-
message = "ChronoModel structure upgrade failed: #{e.message}. Please drop dependent objects and then run
|
612
|
+
message = "ChronoModel structure upgrade failed: #{e.message}. Please drop dependent objects first and then run ChronoModel.upgrade! again."
|
575
613
|
|
576
614
|
# Quite important, output it also to stderr.
|
577
615
|
#
|
data/lib/chrono_model/patches.rb
CHANGED
@@ -88,7 +88,8 @@ module ChronoModel
|
|
88
88
|
# Build a preloader at the +as_of_time+ of this relation
|
89
89
|
#
|
90
90
|
def build_preloader
|
91
|
-
|
91
|
+
options = as_of_time ? {as_of_time: as_of_time} : {}
|
92
|
+
ActiveRecord::Associations::Preloader.new(options)
|
92
93
|
end
|
93
94
|
end
|
94
95
|
|
@@ -157,7 +158,7 @@ module ChronoModel
|
|
157
158
|
def build_scope
|
158
159
|
scope = super
|
159
160
|
|
160
|
-
if preload_scope.respond_to?(:as_of_time)
|
161
|
+
if preload_scope.respond_to?(:as_of_time) && preload_scope.as_of_time
|
161
162
|
scope = scope.as_of(preload_scope.as_of_time)
|
162
163
|
end
|
163
164
|
|
data/lib/chrono_model/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
require 'simplecov'
|
4
4
|
SimpleCov.start
|
5
5
|
|
6
|
+
require 'byebug'
|
7
|
+
|
6
8
|
require 'chrono_model'
|
7
9
|
|
8
10
|
require 'support/connection'
|
@@ -13,6 +15,8 @@ require 'support/matchers/index'
|
|
13
15
|
require 'support/matchers/function'
|
14
16
|
require 'support/aruba'
|
15
17
|
|
18
|
+
puts "Testing against Active Record #{ActiveRecord::VERSION::STRING} with Arel #{Arel::VERSION}"
|
19
|
+
|
16
20
|
# Rails 5 returns a True/FalseClass
|
17
21
|
AR_TRUE, AR_FALSE = ActiveRecord::VERSION::MAJOR == 4 ? ['t', 'f'] : [true, false]
|
18
22
|
|
data/spec/time_machine_spec.rb
CHANGED
@@ -28,6 +28,10 @@ describe ChronoModel::TimeMachine do
|
|
28
28
|
ts_eval(subbar) { update_attributes! :name => 'sub-bar sub-bar' }
|
29
29
|
ts_eval(subbar) { update_attributes! :name => 'new sub-bar' }
|
30
30
|
|
31
|
+
#
|
32
|
+
foos = Array.new(2) {|i| ts_eval { Foo.create! :name => "foo #{i}" } }
|
33
|
+
bars = Array.new(2) {|i| ts_eval { Bar.create! :name => "bar #{i}", :foo => foos[i] } }
|
34
|
+
|
31
35
|
#
|
32
36
|
baz = Baz.create :name => 'baz', :bar => bar
|
33
37
|
|
@@ -90,6 +94,28 @@ describe ChronoModel::TimeMachine do
|
|
90
94
|
) }
|
91
95
|
end
|
92
96
|
|
97
|
+
describe 'does not interfere with AR standard behaviour' do
|
98
|
+
all_foos = [ foo ] + foos
|
99
|
+
all_bars = [ bar ] + bars
|
100
|
+
|
101
|
+
it { expect(Foo.count).to eq all_foos.size }
|
102
|
+
it { expect(Bar.count).to eq all_bars.size }
|
103
|
+
|
104
|
+
it { expect(Foo.includes(bars: :sub_bars)).to eq all_foos }
|
105
|
+
it { expect(Foo.includes(:bars).preload(bars: :sub_bars)).to eq all_foos }
|
106
|
+
|
107
|
+
it { expect(Foo.includes(:bars).first.name).to eq 'new foo' }
|
108
|
+
it { expect(Foo.includes(:bars).as_of(foo.ts[0]).first.name).to eq 'foo' }
|
109
|
+
|
110
|
+
it { expect(Foo.joins(:bars).map(&:bars).flatten).to eq all_bars }
|
111
|
+
it { expect(Foo.joins(:bars).first.bars.joins(:sub_bars).first.name).to eq 'new bar' }
|
112
|
+
|
113
|
+
it { expect(Foo.joins(bars: :sub_bars).first.bars.joins(:sub_bars).first.sub_bars.first.name).to eq 'new sub-bar' }
|
114
|
+
|
115
|
+
it { expect(Foo.first.bars.includes(:sub_bars)).to eq [ bar ] }
|
116
|
+
|
117
|
+
end
|
118
|
+
|
93
119
|
describe '#as_of' do
|
94
120
|
describe 'accepts a Time instance' do
|
95
121
|
it { expect(foo.as_of(Time.now).name).to eq 'new foo' }
|
@@ -264,7 +290,7 @@ describe ChronoModel::TimeMachine do
|
|
264
290
|
end
|
265
291
|
|
266
292
|
describe 'does not add as_of_time when there are aggregates' do
|
267
|
-
it { expect(foo.history.select('max(id)').to_sql).to_not match
|
293
|
+
it { expect(foo.history.select('max(id)').to_sql).to_not match(/as_of_time/) }
|
268
294
|
|
269
295
|
it { expect(foo.history.except(:order).select('max(id) as foo, min(id) as bar').group('id').first.attributes.keys).to eq %w( id foo bar ) }
|
270
296
|
end
|
@@ -278,7 +304,7 @@ describe ChronoModel::TimeMachine do
|
|
278
304
|
|
279
305
|
context '.sorted' do
|
280
306
|
describe 'orders by recorded_at, hid' do
|
281
|
-
it { expect(foo.history.sorted.to_sql).to match
|
307
|
+
it { expect(foo.history.sorted.to_sql).to match(/order by .+"recorded_at", .+"hid"/i) }
|
282
308
|
end
|
283
309
|
end
|
284
310
|
end
|
@@ -416,7 +442,7 @@ describe ChronoModel::TimeMachine do
|
|
416
442
|
split = lambda {|ts| ts.map!{|t| [t.to_i, t.usec]} }
|
417
443
|
|
418
444
|
timestamps_from = lambda {|*records|
|
419
|
-
|
445
|
+
records.map(&:history).flatten!.inject([]) {|ret, rec|
|
420
446
|
ret.push [rec.valid_from.to_i, rec.valid_from.usec] if rec.try(:valid_from)
|
421
447
|
ret.push [rec.valid_to .to_i, rec.valid_to .usec] if rec.try(:valid_to)
|
422
448
|
ret
|
@@ -570,11 +596,6 @@ describe ChronoModel::TimeMachine do
|
|
570
596
|
|
571
597
|
# Class methods
|
572
598
|
context do
|
573
|
-
foos = Array.new(2) {|i| ts_eval { Foo.create! :name => "foo #{i}" } }
|
574
|
-
bars = Array.new(2) {|i| ts_eval { Bar.create! :name => "bar #{i}", :foo => foos[i] } }
|
575
|
-
|
576
|
-
after(:all) { foos.each(&:destroy); bars.each(&:destroy) }
|
577
|
-
|
578
599
|
describe '.as_of' do
|
579
600
|
it { expect(Foo.as_of(1.month.ago)).to eq [] }
|
580
601
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chrono_model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.13.
|
4
|
+
version: 0.13.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marcello Barnaba
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2019-04-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -278,7 +278,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
278
278
|
version: '0'
|
279
279
|
requirements: []
|
280
280
|
rubyforge_project:
|
281
|
-
rubygems_version: 2.
|
281
|
+
rubygems_version: 2.7.6.2
|
282
282
|
signing_key:
|
283
283
|
specification_version: 4
|
284
284
|
summary: Temporal extensions (SCD Type II) for Active Record
|