chrono_model 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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