chrono_model 0.5.3 → 0.8.0

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