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.
@@ -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, but it is here
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].all? {|ts| ts.respond_to?(:zone) && ts.zone == 'UTC'}
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 "valid_from" = #{connection.quote(from)},
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
 
@@ -1,3 +1,3 @@
1
1
  module ChronoModel
2
- VERSION = "0.5.3"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -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 => 90000) }
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 => 80400) }
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
@@ -1,5 +1,6 @@
1
1
  # CREATE ROLE chronomodel LOGIN ENCRYPTED PASSWORD 'chronomodel' CREATEDB;
2
2
  #
3
+ adapter: chronomodel
3
4
  hostname: localhost
4
5
  username: chronomodel
5
6
  password: chronomodel
@@ -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
@@ -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 => 'postgres')
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
- ensure
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 => 'postgresql')
44
+ conf.update(:adapter => 'chronomodel')
50
45
 
51
46
  def conf.to_s
52
47
  'pgsql://%s:%s@%s/%s' % [
@@ -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
- ['valid_from', 'timestamp without time zone'],
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)