chrono_model 1.0.1 → 1.1.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.
Files changed (38) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +19 -14
  3. data/README.md +49 -25
  4. data/lib/chrono_model.rb +37 -3
  5. data/lib/chrono_model/adapter.rb +91 -874
  6. data/lib/chrono_model/adapter/ddl.rb +225 -0
  7. data/lib/chrono_model/adapter/indexes.rb +194 -0
  8. data/lib/chrono_model/adapter/migrations.rb +282 -0
  9. data/lib/chrono_model/adapter/tsrange.rb +57 -0
  10. data/lib/chrono_model/adapter/upgrade.rb +120 -0
  11. data/lib/chrono_model/conversions.rb +20 -0
  12. data/lib/chrono_model/json.rb +28 -0
  13. data/lib/chrono_model/patches.rb +8 -232
  14. data/lib/chrono_model/patches/as_of_time_holder.rb +23 -0
  15. data/lib/chrono_model/patches/as_of_time_relation.rb +19 -0
  16. data/lib/chrono_model/patches/association.rb +52 -0
  17. data/lib/chrono_model/patches/db_console.rb +11 -0
  18. data/lib/chrono_model/patches/join_node.rb +32 -0
  19. data/lib/chrono_model/patches/preloader.rb +68 -0
  20. data/lib/chrono_model/patches/relation.rb +58 -0
  21. data/lib/chrono_model/time_gate.rb +5 -5
  22. data/lib/chrono_model/time_machine.rb +47 -427
  23. data/lib/chrono_model/time_machine/history_model.rb +196 -0
  24. data/lib/chrono_model/time_machine/time_query.rb +86 -0
  25. data/lib/chrono_model/time_machine/timeline.rb +94 -0
  26. data/lib/chrono_model/utilities.rb +27 -0
  27. data/lib/chrono_model/version.rb +1 -1
  28. data/spec/aruba/dbconsole_spec.rb +25 -0
  29. data/spec/chrono_model/adapter/counter_cache_race_spec.rb +46 -0
  30. data/spec/{adapter_spec.rb → chrono_model/adapter_spec.rb} +124 -5
  31. data/spec/{utils_spec.rb → chrono_model/conversions_spec.rb} +0 -0
  32. data/spec/{json_ops_spec.rb → chrono_model/json_ops_spec.rb} +11 -0
  33. data/spec/{time_machine_spec.rb → chrono_model/time_machine_spec.rb} +15 -5
  34. data/spec/{time_query_spec.rb → chrono_model/time_query_spec.rb} +0 -0
  35. data/spec/config.travis.yml +1 -0
  36. data/spec/config.yml.example +1 -0
  37. metadata +35 -14
  38. data/lib/chrono_model/utils.rb +0 -117
@@ -78,6 +78,33 @@ describe ChronoModel::Adapter do
78
78
  native
79
79
  end
80
80
 
81
+ describe '.is_chrono?' do
82
+ with_temporal_table do
83
+ it { expect(adapter.is_chrono?(table)).to be(true) }
84
+ end
85
+
86
+ with_plain_table do
87
+ it { expect(adapter.is_chrono?(table)).to be(false) }
88
+ end
89
+
90
+ context 'when schemas are not there yet' do
91
+ before(:all) do
92
+ adapter.execute 'BEGIN'
93
+ adapter.execute 'DROP SCHEMA temporal CASCADE'
94
+ adapter.execute 'DROP SCHEMA history CASCADE'
95
+ adapter.execute 'CREATE TABLE test_table (id integer)'
96
+ end
97
+
98
+ after(:all) do
99
+ adapter.execute 'ROLLBACK'
100
+ end
101
+
102
+ it { expect { adapter.is_chrono?(table) }.to_not raise_error }
103
+
104
+ it { expect(adapter.is_chrono?(table)).to be(false) }
105
+ end
106
+ end
107
+
81
108
  describe '.create_table' do
82
109
  with_temporal_table do
83
110
  it_should_behave_like 'temporal table'
@@ -395,11 +422,15 @@ describe ChronoModel::Adapter do
395
422
 
396
423
  describe '.on_schema' do
397
424
  before(:all) do
425
+ adapter.execute 'BEGIN'
398
426
  5.times {|i| adapter.execute "CREATE SCHEMA test_#{i}"}
399
427
  end
400
428
 
401
- context 'with nesting' do
429
+ after(:all) do
430
+ adapter.execute 'ROLLBACK'
431
+ end
402
432
 
433
+ context 'by default' do
403
434
  it 'saves the schema at each recursion' do
404
435
  is_expected.to be_in_schema(:default)
405
436
 
@@ -415,15 +446,37 @@ describe ChronoModel::Adapter do
415
446
  is_expected.to be_in_schema(:default)
416
447
  end
417
448
 
449
+ context 'when errors occur' do
450
+ subject do
451
+ adapter.on_schema('test_1') do
452
+
453
+ adapter.on_schema('test_2') do
454
+ adapter.execute 'BEGIN'
455
+ adapter.execute 'ERRORING ON PURPOSE'
456
+ end
457
+
458
+ end
459
+ end
460
+
461
+ it {
462
+ expect { subject }.
463
+ to raise_error(/current transaction is aborted/).
464
+ and change { adapter.instance_variable_get(:@schema_search_path) }
465
+ }
466
+
467
+ after do
468
+ adapter.execute 'ROLLBACK'
469
+ end
470
+ end
418
471
  end
419
472
 
420
- context 'without nesting' do
473
+ context 'with recurse: :ignore' do
421
474
  it 'ignores recursive calls' do
422
475
  is_expected.to be_in_schema(:default)
423
476
 
424
- adapter.on_schema('test_1', false) { is_expected.to be_in_schema('test_1')
425
- adapter.on_schema('test_2', false) { is_expected.to be_in_schema('test_1')
426
- adapter.on_schema('test_3', false) { is_expected.to be_in_schema('test_1')
477
+ adapter.on_schema('test_1', recurse: :ignore) { is_expected.to be_in_schema('test_1')
478
+ adapter.on_schema('test_2', recurse: :ignore) { is_expected.to be_in_schema('test_1')
479
+ adapter.on_schema('test_3', recurse: :ignore) { is_expected.to be_in_schema('test_1')
427
480
  } } }
428
481
 
429
482
  is_expected.to be_in_schema(:default)
@@ -431,6 +484,72 @@ describe ChronoModel::Adapter do
431
484
  end
432
485
  end
433
486
 
487
+ context 'migration extensions' do
488
+ before :all do
489
+ adapter.create_table :meetings do |t|
490
+ t.string :name
491
+ t.tsrange :interval
492
+ end
493
+ end
494
+
495
+ after :all do
496
+ adapter.drop_table :meetings
497
+ end
498
+
499
+ describe '.add_temporal_indexes' do
500
+ before do
501
+ adapter.add_temporal_indexes :meetings, :interval
502
+ end
503
+
504
+ it { expect(adapter.indexes(:meetings).map(&:name)).to eq [
505
+ 'index_meetings_temporal_on_interval',
506
+ 'index_meetings_temporal_on_lower_interval',
507
+ 'index_meetings_temporal_on_upper_interval'
508
+ ] }
509
+
510
+ after do
511
+ adapter.remove_temporal_indexes :meetings, :interval
512
+ end
513
+ end
514
+
515
+ describe '.remove_temporal_indexes' do
516
+ before :all do
517
+ adapter.add_temporal_indexes :meetings, :interval
518
+ end
519
+
520
+ before do
521
+ adapter.remove_temporal_indexes :meetings, :interval
522
+ end
523
+
524
+ it { expect(adapter.indexes(:meetings)).to be_empty }
525
+ end
526
+
527
+ describe '.add_timeline_consistency_constraint' do
528
+ before do
529
+ adapter.add_timeline_consistency_constraint(:meetings, :interval)
530
+ end
531
+
532
+ it { expect(adapter.indexes(:meetings).map(&:name)).to eq [
533
+ 'meetings_timeline_consistency'
534
+ ] }
535
+
536
+ after do
537
+ adapter.remove_timeline_consistency_constraint(:meetings)
538
+ end
539
+ end
540
+
541
+ describe '.remove_timeline_consistency_constraint' do
542
+ before :all do
543
+ adapter.add_timeline_consistency_constraint :meetings, :interval
544
+ end
545
+
546
+ before do
547
+ adapter.remove_timeline_consistency_constraint(:meetings)
548
+ end
549
+
550
+ it { expect(adapter.indexes(:meetings)).to be_empty }
551
+ end
552
+ end
434
553
 
435
554
  let(:current) { [ChronoModel::Adapter::TEMPORAL_SCHEMA, table].join('.') }
436
555
  let(:history) { [ChronoModel::Adapter::HISTORY_SCHEMA, table].join('.') }
@@ -1,6 +1,14 @@
1
+ ##########################################################
2
+ ### DEPRECATED: JSON operators are an hack and there is no
3
+ ### reason not to use jsonb other than migrating your data
4
+ ##########################################################
5
+ if ENV['HAVE_PLPYTHON'] == '1'
6
+
1
7
  require 'spec_helper'
2
8
  require 'support/helpers'
3
9
 
10
+ require 'chrono_model/json'
11
+
4
12
  describe 'JSON equality operator' do
5
13
  include ChronoTest::Helpers::Adapter
6
14
 
@@ -46,3 +54,6 @@ describe 'JSON equality operator' do
46
54
  end
47
55
 
48
56
  end
57
+
58
+
59
+ end
@@ -82,14 +82,16 @@ describe ChronoModel::TimeMachine do
82
82
  it { is_expected.to include(Publication) }
83
83
  end
84
84
 
85
- describe '.chrono_models' do
86
- subject { ChronoModel::TimeMachine.chrono_models }
85
+ describe '.history_models' do
86
+ subject { ChronoModel.history_models }
87
87
 
88
88
  it { is_expected.to eq(
89
+ 'articles' => Article::History,
89
90
  'foos' => Foo::History,
90
91
  'defoos' => Defoo::History,
91
92
  'bars' => Bar::History,
92
93
  'elements' => Element::History,
94
+ 'sections' => Section::History,
93
95
  'sub_bars' => SubBar::History,
94
96
  ) }
95
97
  end
@@ -210,9 +212,17 @@ describe ChronoModel::TimeMachine do
210
212
  it { expect(Bar.as_of(bar.ts[2]).includes(foo: :sub_bars).first.foo.name).to eq 'new foo' }
211
213
  it { expect(Bar.as_of(bar.ts[3]).includes(foo: :sub_bars).first.foo.name).to eq 'new foo' }
212
214
 
213
- it { expect(Foo.as_of(foo.ts[0]).includes(bars: :sub_bars).first.sub_bars.count).to eq 0 }
214
- it { expect(Foo.as_of(foo.ts[1]).includes(bars: :sub_bars).first.sub_bars.count).to eq 0 }
215
- it { expect(Foo.as_of(foo.ts[2]).includes(bars: :sub_bars).first.sub_bars.count).to eq 1 }
215
+ it { expect(Foo.as_of(foo.ts[0]).includes(:bars, :sub_bars).first.sub_bars.count).to eq 0 }
216
+ it { expect(Foo.as_of(foo.ts[1]).includes(:bars, :sub_bars).first.sub_bars.count).to eq 0 }
217
+ it { expect(Foo.as_of(foo.ts[2]).includes(:bars, :sub_bars).first.sub_bars.count).to eq 1 }
218
+
219
+ it { expect(Foo.as_of(foo.ts[0]).includes(:bars, :sub_bars).first.sub_bars.first).to be nil }
220
+ it { expect(Foo.as_of(foo.ts[1]).includes(:bars, :sub_bars).first.sub_bars.first).to be nil }
221
+
222
+ it { expect(Foo.as_of(subbar.ts[0]).includes(:bars, :sub_bars).first.sub_bars.first.name).to eq 'sub-bar' }
223
+ it { expect(Foo.as_of(subbar.ts[1]).includes(:bars, :sub_bars).first.sub_bars.first.name).to eq 'bar sub-bar' }
224
+ it { expect(Foo.as_of(subbar.ts[2]).includes(:bars, :sub_bars).first.sub_bars.first.name).to eq 'sub-bar sub-bar' }
225
+ it { expect(Foo.as_of(subbar.ts[3]).includes(:bars, :sub_bars).first.sub_bars.first.name).to eq 'new sub-bar' }
216
226
  end
217
227
 
218
228
  it 'doesn\'t raise RecordNotFound when no history records are found' do
@@ -2,3 +2,4 @@ hostname: localhost
2
2
  username: postgres
3
3
  password: ""
4
4
  database: chronomodel
5
+ pool: 11
@@ -5,4 +5,5 @@ hostname: localhost
5
5
  username: chronomodel
6
6
  password: chronomodel
7
7
  database: chronomodel
8
+ pool: 11
8
9
  #
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: 1.0.1
4
+ version: 1.1.0
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: 2019-04-06 00:00:00.000000000 Z
12
+ date: 2019-04-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -218,13 +218,30 @@ files:
218
218
  - lib/active_record/tasks/chronomodel_database_tasks.rb
219
219
  - lib/chrono_model.rb
220
220
  - lib/chrono_model/adapter.rb
221
+ - lib/chrono_model/adapter/ddl.rb
222
+ - lib/chrono_model/adapter/indexes.rb
223
+ - lib/chrono_model/adapter/migrations.rb
224
+ - lib/chrono_model/adapter/tsrange.rb
225
+ - lib/chrono_model/adapter/upgrade.rb
226
+ - lib/chrono_model/conversions.rb
227
+ - lib/chrono_model/json.rb
221
228
  - lib/chrono_model/patches.rb
229
+ - lib/chrono_model/patches/as_of_time_holder.rb
230
+ - lib/chrono_model/patches/as_of_time_relation.rb
231
+ - lib/chrono_model/patches/association.rb
232
+ - lib/chrono_model/patches/db_console.rb
233
+ - lib/chrono_model/patches/join_node.rb
234
+ - lib/chrono_model/patches/preloader.rb
235
+ - lib/chrono_model/patches/relation.rb
222
236
  - lib/chrono_model/railtie.rb
223
237
  - lib/chrono_model/time_gate.rb
224
238
  - lib/chrono_model/time_machine.rb
225
- - lib/chrono_model/utils.rb
239
+ - lib/chrono_model/time_machine/history_model.rb
240
+ - lib/chrono_model/time_machine/time_query.rb
241
+ - lib/chrono_model/time_machine/timeline.rb
242
+ - lib/chrono_model/utilities.rb
226
243
  - lib/chrono_model/version.rb
227
- - spec/adapter_spec.rb
244
+ - spec/aruba/dbconsole_spec.rb
228
245
  - spec/aruba/fixtures/database_with_default_username_and_password.yml
229
246
  - spec/aruba/fixtures/database_without_username_and_password.yml
230
247
  - spec/aruba/fixtures/empty_structure.sql
@@ -235,9 +252,14 @@ files:
235
252
  - spec/aruba/fixtures/railsapp/config/environments/development.rb
236
253
  - spec/aruba/migrations_spec.rb
237
254
  - spec/aruba/rake_task_spec.rb
255
+ - spec/chrono_model/adapter/counter_cache_race_spec.rb
256
+ - spec/chrono_model/adapter_spec.rb
257
+ - spec/chrono_model/conversions_spec.rb
258
+ - spec/chrono_model/json_ops_spec.rb
259
+ - spec/chrono_model/time_machine_spec.rb
260
+ - spec/chrono_model/time_query_spec.rb
238
261
  - spec/config.travis.yml
239
262
  - spec/config.yml.example
240
- - spec/json_ops_spec.rb
241
263
  - spec/spec_helper.rb
242
264
  - spec/support/aruba.rb
243
265
  - spec/support/connection.rb
@@ -248,9 +270,6 @@ files:
248
270
  - spec/support/matchers/index.rb
249
271
  - spec/support/matchers/schema.rb
250
272
  - spec/support/matchers/table.rb
251
- - spec/time_machine_spec.rb
252
- - spec/time_query_spec.rb
253
- - spec/utils_spec.rb
254
273
  - sql/json_ops.sql
255
274
  - sql/uninstall-json_ops.sql
256
275
  homepage: https://github.com/ifad/chronomodel
@@ -272,12 +291,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
272
291
  version: '0'
273
292
  requirements: []
274
293
  rubyforge_project:
275
- rubygems_version: 2.5.1
294
+ rubygems_version: 2.7.6.2
276
295
  signing_key:
277
296
  specification_version: 4
278
297
  summary: Temporal extensions (SCD Type II) for Active Record
279
298
  test_files:
280
- - spec/adapter_spec.rb
299
+ - spec/aruba/dbconsole_spec.rb
281
300
  - spec/aruba/fixtures/database_with_default_username_and_password.yml
282
301
  - spec/aruba/fixtures/database_without_username_and_password.yml
283
302
  - spec/aruba/fixtures/empty_structure.sql
@@ -288,9 +307,14 @@ test_files:
288
307
  - spec/aruba/fixtures/railsapp/config/environments/development.rb
289
308
  - spec/aruba/migrations_spec.rb
290
309
  - spec/aruba/rake_task_spec.rb
310
+ - spec/chrono_model/adapter/counter_cache_race_spec.rb
311
+ - spec/chrono_model/adapter_spec.rb
312
+ - spec/chrono_model/conversions_spec.rb
313
+ - spec/chrono_model/json_ops_spec.rb
314
+ - spec/chrono_model/time_machine_spec.rb
315
+ - spec/chrono_model/time_query_spec.rb
291
316
  - spec/config.travis.yml
292
317
  - spec/config.yml.example
293
- - spec/json_ops_spec.rb
294
318
  - spec/spec_helper.rb
295
319
  - spec/support/aruba.rb
296
320
  - spec/support/connection.rb
@@ -301,6 +325,3 @@ test_files:
301
325
  - spec/support/matchers/index.rb
302
326
  - spec/support/matchers/schema.rb
303
327
  - spec/support/matchers/table.rb
304
- - spec/time_machine_spec.rb
305
- - spec/time_query_spec.rb
306
- - spec/utils_spec.rb
@@ -1,117 +0,0 @@
1
- module ChronoModel
2
-
3
- module Conversions
4
- extend self
5
-
6
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(?:\.(\d+))?\z/
7
-
8
- def string_to_utc_time(string)
9
- if string =~ ISO_DATETIME
10
- usec = $7.nil? ? '000000' : $7.ljust(6, '0') # .1 is .100000, not .000001
11
- Time.utc $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec.to_i
12
- end
13
- end
14
-
15
- def time_to_utc_string(time)
16
- [time.to_s(:db), sprintf('%06d', time.usec)].join '.'
17
- end
18
- end
19
-
20
- module Json
21
- extend self
22
-
23
- def create
24
- adapter.execute 'CREATE OR REPLACE LANGUAGE plpythonu'
25
- adapter.execute File.read(sql 'json_ops.sql')
26
- end
27
-
28
- def drop
29
- adapter.execute File.read(sql 'uninstall-json_ops.sql')
30
- adapter.execute 'DROP LANGUAGE IF EXISTS plpythonu'
31
- end
32
-
33
- private
34
- def sql(file)
35
- File.dirname(__FILE__) + '/../../sql/' + file
36
- end
37
-
38
- def adapter
39
- ActiveRecord::Base.connection
40
- end
41
- end
42
-
43
- module Utilities
44
- # Amends the given history item setting a different period.
45
- # Useful when migrating from legacy systems.
46
- #
47
- def amend_period!(hid, from, to)
48
- unless [from, to].any? {|ts| ts.respond_to?(:zone) && ts.zone == 'UTC'}
49
- raise 'Can amend history only with UTC timestamps'
50
- end
51
-
52
- connection.execute %[
53
- UPDATE #{quoted_table_name}
54
- SET "validity" = tsrange(#{connection.quote(from)}, #{connection.quote(to)}),
55
- "recorded_at" = #{connection.quote(from)}
56
- WHERE "hid" = #{hid.to_i}
57
- ]
58
- end
59
-
60
- # Returns true if this model is backed by a temporal table,
61
- # false otherwise.
62
- #
63
- def chrono?
64
- connection.is_chrono?(table_name)
65
- end
66
- end
67
-
68
- ActiveRecord::Base.extend Utilities
69
-
70
- module Migrate
71
- extend self
72
-
73
- def upgrade_indexes!(base = ActiveRecord::Base)
74
- use base
75
-
76
- db.on_schema(Adapter::HISTORY_SCHEMA) do
77
- db.tables.each do |table|
78
- if db.is_chrono?(table)
79
- upgrade_indexes_for(table)
80
- end
81
- end
82
- end
83
- end
84
-
85
- private
86
- attr_reader :db
87
-
88
- def use(ar)
89
- @db = ar.connection
90
- end
91
-
92
- def upgrade_indexes_for(table_name)
93
- upgradeable =
94
- %r{_snapshot$|_valid_(?:from|to)$|_recorded_at$|_instance_(?:update|history)$}
95
-
96
- indexes_sql = %[
97
- SELECT DISTINCT i.relname
98
- FROM pg_class t
99
- INNER JOIN pg_index d ON t.oid = d.indrelid
100
- INNER JOIN pg_class i ON d.indexrelid = i.oid
101
- WHERE i.relkind = 'i'
102
- AND d.indisprimary = 'f'
103
- AND t.relname = '#{table_name}'
104
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY(current_schemas(false)) )
105
- ]
106
-
107
- db.select_values(indexes_sql).each do |idx|
108
- if idx =~ upgradeable
109
- db.execute "DROP INDEX #{idx}"
110
- end
111
- end
112
-
113
- db.send(:chrono_create_history_indexes_for, table_name)
114
- end
115
- end
116
-
117
- end