chrono_model 0.5.3 → 0.8.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 +7 -0
- data/.rspec +1 -1
- data/.travis.yml +7 -0
- data/Gemfile +10 -1
- data/LICENSE +3 -1
- data/README.md +239 -136
- data/README.sql +108 -94
- data/chrono_model.gemspec +5 -4
- data/lib/active_record/connection_adapters/chronomodel_adapter.rb +42 -0
- data/lib/chrono_model.rb +0 -8
- data/lib/chrono_model/adapter.rb +346 -212
- data/lib/chrono_model/patches.rb +21 -8
- data/lib/chrono_model/railtie.rb +1 -13
- data/lib/chrono_model/time_gate.rb +2 -2
- data/lib/chrono_model/time_machine.rb +153 -87
- data/lib/chrono_model/utils.rb +35 -8
- data/lib/chrono_model/version.rb +1 -1
- data/spec/adapter_spec.rb +154 -14
- data/spec/config.yml.example +1 -0
- data/spec/json_ops_spec.rb +48 -0
- data/spec/support/connection.rb +4 -9
- data/spec/support/helpers.rb +27 -2
- data/spec/support/matchers/column.rb +5 -2
- data/spec/support/matchers/index.rb +4 -0
- data/spec/support/matchers/schema.rb +4 -0
- data/spec/support/matchers/table.rb +94 -21
- data/spec/time_machine_spec.rb +62 -28
- data/spec/time_query_spec.rb +227 -0
- data/sql/json_ops.sql +56 -0
- data/sql/uninstall-json_ops.sql +24 -0
- metadata +44 -18
- data/lib/chrono_model/compatibility.rb +0 -31
data/lib/chrono_model/utils.rb
CHANGED
@@ -17,28 +17,55 @@ module ChronoModel
|
|
17
17
|
end
|
18
18
|
end
|
19
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
|
+
|
20
43
|
module Utilities
|
21
44
|
# Amends the given history item setting a different period.
|
22
|
-
# Useful when migrating from legacy systems
|
23
|
-
# as this is not a proper API.
|
24
|
-
#
|
25
|
-
# Extend your model with the Utilities model if you want to
|
26
|
-
# use it.
|
45
|
+
# Useful when migrating from legacy systems.
|
27
46
|
#
|
28
47
|
def amend_period!(hid, from, to)
|
29
|
-
unless [from, to].
|
48
|
+
unless [from, to].any? {|ts| ts.respond_to?(:zone) && ts.zone == 'UTC'}
|
30
49
|
raise 'Can amend history only with UTC timestamps'
|
31
50
|
end
|
32
51
|
|
33
52
|
connection.execute %[
|
34
53
|
UPDATE #{quoted_table_name}
|
35
|
-
SET "
|
36
|
-
"valid_to" = #{connection.quote(to )}
|
54
|
+
SET "validity" = tsrange(#{connection.quote(from)}, #{connection.quote(to)})
|
37
55
|
WHERE "hid" = #{hid.to_i}
|
38
56
|
]
|
39
57
|
end
|
58
|
+
|
59
|
+
# Returns true if this model is backed by a temporal table,
|
60
|
+
# false otherwise.
|
61
|
+
#
|
62
|
+
def chrono?
|
63
|
+
connection.is_chrono?(table_name)
|
64
|
+
end
|
40
65
|
end
|
41
66
|
|
67
|
+
ActiveRecord::Base.extend Utilities
|
68
|
+
|
42
69
|
module Migrate
|
43
70
|
extend self
|
44
71
|
|
data/lib/chrono_model/version.rb
CHANGED
data/spec/adapter_spec.rb
CHANGED
@@ -34,14 +34,15 @@ describe ChronoModel::Adapter do
|
|
34
34
|
context do
|
35
35
|
subject { adapter }
|
36
36
|
it { should be_a_kind_of(ChronoModel::Adapter) }
|
37
|
+
its(:adapter_name) { should == 'PostgreSQL' }
|
37
38
|
|
38
39
|
context do
|
39
|
-
before { adapter.stub(:postgresql_version =>
|
40
|
+
before { adapter.stub(:postgresql_version => 90300) }
|
40
41
|
it { should be_chrono_supported }
|
41
42
|
end
|
42
43
|
|
43
44
|
context do
|
44
|
-
before { adapter.stub(:postgresql_version =>
|
45
|
+
before { adapter.stub(:postgresql_version => 90000) }
|
45
46
|
it { should_not be_chrono_supported }
|
46
47
|
end
|
47
48
|
end
|
@@ -59,10 +60,12 @@ describe ChronoModel::Adapter do
|
|
59
60
|
|
60
61
|
def native.to_proc
|
61
62
|
proc {|t|
|
62
|
-
t.string :test, :null => false
|
63
|
+
t.string :test, :null => false, :default => 'default-value'
|
63
64
|
t.integer :foo
|
64
65
|
t.float :bar
|
65
66
|
t.text :baz
|
67
|
+
t.integer :ary, :array => true, :null => false, :default => []
|
68
|
+
t.boolean :bool, :null => false, :default => false
|
66
69
|
}
|
67
70
|
end
|
68
71
|
|
@@ -117,10 +120,27 @@ describe ChronoModel::Adapter do
|
|
117
120
|
|
118
121
|
with_plain_table do
|
119
122
|
before :all do
|
123
|
+
adapter.add_index table, :foo
|
124
|
+
adapter.add_index table, :bar, :unique => true
|
125
|
+
|
120
126
|
adapter.change_table table, :temporal => true
|
121
127
|
end
|
122
128
|
|
123
129
|
it_should_behave_like 'temporal table'
|
130
|
+
|
131
|
+
let(:history_indexes) do
|
132
|
+
adapter.on_schema(ChronoModel::Adapter::HISTORY_SCHEMA) do
|
133
|
+
adapter.indexes(table)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
it "copies plain index to history" do
|
138
|
+
history_indexes.find {|i| i.columns == ['foo']}.should be_present
|
139
|
+
end
|
140
|
+
|
141
|
+
it "copies unique index to history without uniqueness constraint" do
|
142
|
+
history_indexes.find {|i| i.columns == ['bar'] && i.unique == false}.should be_present
|
143
|
+
end
|
124
144
|
end
|
125
145
|
end
|
126
146
|
|
@@ -395,6 +415,17 @@ describe ChronoModel::Adapter do
|
|
395
415
|
end
|
396
416
|
|
397
417
|
|
418
|
+
let(:current) { [ChronoModel::Adapter::TEMPORAL_SCHEMA, table].join('.') }
|
419
|
+
let(:history) { [ChronoModel::Adapter::HISTORY_SCHEMA, table].join('.') }
|
420
|
+
|
421
|
+
def count(table)
|
422
|
+
adapter.select_value("SELECT COUNT(*) FROM ONLY #{table}").to_i
|
423
|
+
end
|
424
|
+
|
425
|
+
def ids(table)
|
426
|
+
adapter.select_values("SELECT id FROM ONLY #{table} ORDER BY id")
|
427
|
+
end
|
428
|
+
|
398
429
|
context 'INSERT multiple values' do
|
399
430
|
before :all do
|
400
431
|
adapter.create_table table, :temporal => true, &columns
|
@@ -404,17 +435,6 @@ describe ChronoModel::Adapter do
|
|
404
435
|
adapter.drop_table table
|
405
436
|
end
|
406
437
|
|
407
|
-
let(:current) { [ChronoModel::Adapter::TEMPORAL_SCHEMA, table].join('.') }
|
408
|
-
let(:history) { [ChronoModel::Adapter::HISTORY_SCHEMA, table].join('.') }
|
409
|
-
|
410
|
-
def count(table)
|
411
|
-
adapter.select_value("SELECT COUNT(*) FROM ONLY #{table}").to_i
|
412
|
-
end
|
413
|
-
|
414
|
-
def ids(table)
|
415
|
-
adapter.select_values("SELECT id FROM ONLY #{table} ORDER BY id")
|
416
|
-
end
|
417
|
-
|
418
438
|
context 'when succeeding' do
|
419
439
|
def insert
|
420
440
|
adapter.execute <<-SQL
|
@@ -461,4 +481,124 @@ describe ChronoModel::Adapter do
|
|
461
481
|
end
|
462
482
|
end
|
463
483
|
|
484
|
+
context 'INSERT on NOT NULL columns but with a DEFAULT value' do
|
485
|
+
before :all do
|
486
|
+
adapter.create_table table, :temporal => true, &columns
|
487
|
+
end
|
488
|
+
|
489
|
+
after :all do
|
490
|
+
adapter.drop_table table
|
491
|
+
end
|
492
|
+
|
493
|
+
def insert
|
494
|
+
adapter.execute <<-SQL
|
495
|
+
INSERT INTO #{table} DEFAULT VALUES
|
496
|
+
SQL
|
497
|
+
end
|
498
|
+
|
499
|
+
def select
|
500
|
+
adapter.select_values <<-SQL
|
501
|
+
SELECT test FROM #{table}
|
502
|
+
SQL
|
503
|
+
end
|
504
|
+
|
505
|
+
it { expect { insert }.to_not raise_error }
|
506
|
+
it { insert; select.uniq.should == ['default-value'] }
|
507
|
+
end
|
508
|
+
|
509
|
+
context 'redundant UPDATEs' do
|
510
|
+
|
511
|
+
before :all do
|
512
|
+
adapter.create_table table, :temporal => true, &columns
|
513
|
+
|
514
|
+
adapter.execute <<-SQL
|
515
|
+
INSERT INTO #{table} (test, foo) VALUES ('test1', 1);
|
516
|
+
SQL
|
517
|
+
|
518
|
+
adapter.execute <<-SQL
|
519
|
+
UPDATE #{table} SET test = 'test2';
|
520
|
+
SQL
|
521
|
+
|
522
|
+
adapter.execute <<-SQL
|
523
|
+
UPDATE #{table} SET test = 'test2';
|
524
|
+
SQL
|
525
|
+
end
|
526
|
+
|
527
|
+
after :all do
|
528
|
+
adapter.drop_table table
|
529
|
+
end
|
530
|
+
|
531
|
+
it { count(current).should == 1 }
|
532
|
+
it { count(history).should == 2 }
|
533
|
+
|
534
|
+
end
|
535
|
+
|
536
|
+
context 'updates on non-journaled fields' do
|
537
|
+
before :all do
|
538
|
+
adapter.create_table table, :temporal => true do |t|
|
539
|
+
t.string 'test'
|
540
|
+
t.timestamps
|
541
|
+
end
|
542
|
+
|
543
|
+
adapter.execute <<-SQL
|
544
|
+
INSERT INTO #{table} (test, created_at, updated_at) VALUES ('test', now(), now());
|
545
|
+
SQL
|
546
|
+
|
547
|
+
adapter.execute <<-SQL
|
548
|
+
UPDATE #{table} SET test = 'test2', updated_at = now();
|
549
|
+
SQL
|
550
|
+
|
551
|
+
2.times do
|
552
|
+
adapter.execute <<-SQL # Redundant update with only updated_at change
|
553
|
+
UPDATE #{table} SET test = 'test2', updated_at = now();
|
554
|
+
SQL
|
555
|
+
|
556
|
+
adapter.execute <<-SQL
|
557
|
+
UPDATE #{table} SET updated_at = now();
|
558
|
+
SQL
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
after :all do
|
563
|
+
adapter.drop_table table
|
564
|
+
end
|
565
|
+
|
566
|
+
it { count(current).should == 1 }
|
567
|
+
it { count(history).should == 2 }
|
568
|
+
end
|
569
|
+
|
570
|
+
context 'selective journaled fields' do
|
571
|
+
before :all do
|
572
|
+
adapter.create_table table, :temporal => true, :journal => %w( foo ) do |t|
|
573
|
+
t.string 'foo'
|
574
|
+
t.string 'bar'
|
575
|
+
end
|
576
|
+
|
577
|
+
adapter.execute <<-SQL
|
578
|
+
INSERT INTO #{table} (foo, bar) VALUES ('test foo', 'test bar');
|
579
|
+
SQL
|
580
|
+
|
581
|
+
adapter.execute <<-SQL
|
582
|
+
UPDATE #{table} SET foo = 'test foo', bar = 'no history';
|
583
|
+
SQL
|
584
|
+
|
585
|
+
2.times do
|
586
|
+
adapter.execute <<-SQL
|
587
|
+
UPDATE #{table} SET bar = 'really no history';
|
588
|
+
SQL
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
after :all do
|
593
|
+
adapter.drop_table table
|
594
|
+
end
|
595
|
+
|
596
|
+
it { count(current).should == 1 }
|
597
|
+
it { count(history).should == 1 }
|
598
|
+
|
599
|
+
it 'preserves options upon column change'
|
600
|
+
it 'changes option upon table change'
|
601
|
+
|
602
|
+
end
|
603
|
+
|
464
604
|
end
|
data/spec/config.yml.example
CHANGED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/helpers'
|
3
|
+
|
4
|
+
describe 'JSON equality operator' do
|
5
|
+
include ChronoTest::Helpers::Adapter
|
6
|
+
|
7
|
+
table 'json_test'
|
8
|
+
|
9
|
+
before :all do
|
10
|
+
ChronoModel::Json.create
|
11
|
+
end
|
12
|
+
|
13
|
+
after :all do
|
14
|
+
ChronoModel::Json.drop
|
15
|
+
end
|
16
|
+
|
17
|
+
it { adapter.select_value(%[ SELECT '{"a":1}'::json = '{"a":1}'::json ]).should == 't' }
|
18
|
+
it { adapter.select_value(%[ SELECT '{"a":1}'::json = '{"a" : 1}'::json ]).should == 't' }
|
19
|
+
it { adapter.select_value(%[ SELECT '{"a":1}'::json = '{"a":2}'::json ]).should == 'f' }
|
20
|
+
it { adapter.select_value(%[ SELECT '{"a":1,"b":2}'::json = '{"b":2,"a":1}'::json ]).should == 't' }
|
21
|
+
it { adapter.select_value(%[ SELECT '{"a":1,"b":2,"x":{"c":4,"d":5}}'::json = '{"b":2, "x": { "d": 5, "c": 4}, "a":1}'::json ]).should == 't' }
|
22
|
+
|
23
|
+
context 'on a temporal table' do
|
24
|
+
before :all do
|
25
|
+
adapter.create_table table, :temporal => true do |t|
|
26
|
+
t.json 'data'
|
27
|
+
end
|
28
|
+
|
29
|
+
adapter.execute %[
|
30
|
+
INSERT INTO #{table} ( data ) VALUES ( '{"a":1,"b":2}' )
|
31
|
+
]
|
32
|
+
end
|
33
|
+
|
34
|
+
after :all do
|
35
|
+
adapter.drop_table table
|
36
|
+
end
|
37
|
+
|
38
|
+
it { expect {
|
39
|
+
adapter.execute "UPDATE #{table} SET data = NULL"
|
40
|
+
}.to_not raise_error }
|
41
|
+
|
42
|
+
it { expect {
|
43
|
+
adapter.execute %[UPDATE #{table} SET data = '{"x":1,"y":2}']
|
44
|
+
|
45
|
+
}.to_not raise_error }
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/spec/support/connection.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'pathname'
|
2
2
|
require 'active_record'
|
3
|
-
require 'active_support/core_ext/logger'
|
4
3
|
|
5
4
|
module ChronoTest
|
6
5
|
extend self
|
@@ -29,24 +28,20 @@ module ChronoTest
|
|
29
28
|
|
30
29
|
def recreate_database!
|
31
30
|
database = config.fetch(:database)
|
32
|
-
connect! config.merge(:database =>
|
33
|
-
|
34
|
-
unless AR.supports_chrono?
|
35
|
-
raise 'Your postgresql version is not supported. >= 9.0 is required.'
|
36
|
-
end
|
31
|
+
connect! config.merge(:database => :postgres)
|
37
32
|
|
38
33
|
connection.drop_database database
|
39
34
|
connection.create_database database
|
35
|
+
connect! config
|
40
36
|
|
41
|
-
|
42
|
-
connect!
|
37
|
+
connection.execute 'CREATE EXTENSION btree_gist'
|
43
38
|
logger.info "Connected to #{config}"
|
44
39
|
end
|
45
40
|
|
46
41
|
def config
|
47
42
|
@config ||= YAML.load(config_file.read).tap do |conf|
|
48
43
|
conf.symbolize_keys!
|
49
|
-
conf.update(:adapter => '
|
44
|
+
conf.update(:adapter => 'chronomodel')
|
50
45
|
|
51
46
|
def conf.to_s
|
52
47
|
'pgsql://%s:%s@%s/%s' % [
|
data/spec/support/helpers.rb
CHANGED
@@ -48,7 +48,12 @@ module ChronoTest::Helpers
|
|
48
48
|
end
|
49
49
|
|
50
50
|
module DSL
|
51
|
+
@@schema_setup = false
|
52
|
+
|
51
53
|
def setup_schema!
|
54
|
+
return if @@schema_setup
|
55
|
+
@@schema_setup = true
|
56
|
+
|
52
57
|
# Set up database structure
|
53
58
|
#
|
54
59
|
adapter.create_table 'foos', :temporal => true do |t|
|
@@ -75,6 +80,15 @@ module ChronoTest::Helpers
|
|
75
80
|
t.string :title
|
76
81
|
t.string :type
|
77
82
|
end
|
83
|
+
|
84
|
+
adapter.create_table 'plains' do |t|
|
85
|
+
t.string :foo
|
86
|
+
end
|
87
|
+
|
88
|
+
adapter.create_table 'events' do |t|
|
89
|
+
t.string :name
|
90
|
+
t.daterange :interval
|
91
|
+
end
|
78
92
|
end
|
79
93
|
|
80
94
|
Models = lambda {
|
@@ -104,7 +118,7 @@ module ChronoTest::Helpers
|
|
104
118
|
class ::Defoo < ActiveRecord::Base
|
105
119
|
include ChronoModel::TimeMachine
|
106
120
|
|
107
|
-
default_scope where(:active => true)
|
121
|
+
default_scope proc { where(:active => true) }
|
108
122
|
end
|
109
123
|
|
110
124
|
# STI case (https://github.com/ifad/chronomodel/issues/5)
|
@@ -114,9 +128,20 @@ module ChronoTest::Helpers
|
|
114
128
|
|
115
129
|
class ::Publication < Element
|
116
130
|
end
|
131
|
+
|
132
|
+
class ::Plain < ActiveRecord::Base
|
133
|
+
end
|
134
|
+
|
135
|
+
class ::Event < ActiveRecord::Base
|
136
|
+
extend ChronoModel::TimeMachine::TimeQuery
|
137
|
+
end
|
117
138
|
}
|
118
139
|
|
140
|
+
@@models_defined = false
|
119
141
|
def define_models!
|
142
|
+
return if @@models_defined
|
143
|
+
@@models_defined = true
|
144
|
+
|
120
145
|
Models.call
|
121
146
|
end
|
122
147
|
|
@@ -141,7 +166,7 @@ module ChronoTest::Helpers
|
|
141
166
|
define_method(:ts) { @_ts ||= [] }
|
142
167
|
end unless obj.methods.include?(:ts)
|
143
168
|
|
144
|
-
now = ChronoTest.connection.select_value('select now()::timestamp')
|
169
|
+
now = ChronoTest.connection.select_value('select now()::timestamp') + 'Z'
|
145
170
|
obj.ts.push(Time.parse(now))
|
146
171
|
end
|
147
172
|
end
|
@@ -7,6 +7,10 @@ module ChronoTest::Matchers
|
|
7
7
|
@schema = schema
|
8
8
|
end
|
9
9
|
|
10
|
+
def description
|
11
|
+
'have columns'
|
12
|
+
end
|
13
|
+
|
10
14
|
def matches?(table)
|
11
15
|
super(table)
|
12
16
|
|
@@ -68,8 +72,7 @@ module ChronoTest::Matchers
|
|
68
72
|
class HaveHistoryExtraColumns < HaveColumns
|
69
73
|
def initialize
|
70
74
|
super([
|
71
|
-
['
|
72
|
-
['valid_to', 'timestamp without time zone'],
|
75
|
+
['validity', 'tsrange'],
|
73
76
|
['recorded_at', 'timestamp without time zone'],
|
74
77
|
['hid', 'integer']
|
75
78
|
], history_schema)
|