chrono_model 0.8.2 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +11 -10
- data/Gemfile +0 -19
- data/README.md +55 -26
- data/chrono_model.gemspec +11 -3
- data/lib/chrono_model.rb +10 -0
- data/lib/chrono_model/adapter.rb +46 -26
- data/lib/chrono_model/patches.rb +69 -6
- data/lib/chrono_model/schema_format.rake +23 -7
- data/lib/chrono_model/schema_format.rb +2 -1
- data/lib/chrono_model/time_gate.rb +3 -11
- data/lib/chrono_model/time_machine.rb +53 -102
- data/lib/chrono_model/version.rb +1 -1
- data/spec/adapter_spec.rb +115 -111
- data/spec/json_ops_spec.rb +5 -5
- data/spec/spec_helper.rb +3 -2
- data/spec/support/matchers/base.rb +5 -8
- data/spec/support/matchers/column.rb +22 -9
- data/spec/support/matchers/index.rb +8 -4
- data/spec/support/matchers/schema.rb +5 -1
- data/spec/support/matchers/table.rb +55 -24
- data/spec/time_machine_spec.rb +195 -176
- data/spec/time_query_spec.rb +39 -39
- metadata +116 -18
@@ -6,20 +6,36 @@ namespace :db do
|
|
6
6
|
task :dump => :environment do
|
7
7
|
config = PG.config!
|
8
8
|
target = ENV['DB_STRUCTURE'] || Rails.root.join('db', 'structure.sql')
|
9
|
-
|
9
|
+
schema_search_path = config[:schema_search_path]
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
unless schema_search_path.blank?
|
12
|
+
# add in chronomodel schemas
|
13
|
+
schema_search_path << ",#{ChronoModel::Adapter::TEMPORAL_SCHEMA}"
|
14
|
+
schema_search_path << ",#{ChronoModel::Adapter::HISTORY_SCHEMA}"
|
15
|
+
|
16
|
+
# convert to command line arguments
|
17
|
+
schema_search_path = schema_search_path.split(",").map{|part| "--schema=#{part.strip}" }.join(" ")
|
18
|
+
end
|
19
|
+
|
20
|
+
PG.make_dump target,
|
21
|
+
*config.values_at(:username, :database),
|
22
|
+
'-i', '-x', '-s', '-O', schema_search_path
|
15
23
|
|
16
24
|
# Add migration information, after resetting the schema to the default one
|
17
25
|
File.open(target, 'a') do |f|
|
18
|
-
f.puts "SET search_path
|
26
|
+
f.puts "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n"
|
19
27
|
f.puts ActiveRecord::Base.connection.dump_schema_information
|
20
28
|
end
|
21
|
-
end
|
22
29
|
|
30
|
+
# the structure.sql file will contain CREATE SCHEMA statements
|
31
|
+
# but chronomodel creates the temporal and history schemas
|
32
|
+
# when the connection is established, so a db:structure:load fails
|
33
|
+
# fix up create schema statements to include the IF NOT EXISTS directive
|
34
|
+
|
35
|
+
sql = File.read(target)
|
36
|
+
sql.gsub!(/CREATE SCHEMA /, 'CREATE SCHEMA IF NOT EXISTS ')
|
37
|
+
File.open(target, "w") { |file| file << sql }
|
38
|
+
end
|
23
39
|
|
24
40
|
desc "Load structure.sql file into the current environment's database"
|
25
41
|
task :load => :environment do
|
@@ -24,11 +24,12 @@ module PG
|
|
24
24
|
ENV['PGHOST'] = config[:host].to_s if config.key?(:host)
|
25
25
|
ENV['PGPORT'] = config[:port].to_s if config.key?(:port)
|
26
26
|
ENV['PGPASSWORD'] = config[:password].to_s if config.key?(:password)
|
27
|
+
ENV['PGUSER'] = config[:username].to_s if config.key?(:username)
|
27
28
|
end
|
28
29
|
end
|
29
30
|
|
30
31
|
def make_dump(target, username, database, *options)
|
31
|
-
exec 'pg_dump', '-f', target, '-U', username, database, *options
|
32
|
+
exec 'pg_dump', '-f', target, '-U', username, '-d', database, *options
|
32
33
|
end
|
33
34
|
|
34
35
|
def load_dump(source, username, database, *options)
|
@@ -8,22 +8,14 @@ module ChronoModel
|
|
8
8
|
|
9
9
|
module ClassMethods
|
10
10
|
def as_of(time)
|
11
|
-
|
12
|
-
|
13
|
-
virtual_table = select(%[
|
14
|
-
#{quoted_table_name}.*, #{connection.quote(time)}::timestamp AS "as_of_time"]
|
15
|
-
).to_sql
|
16
|
-
|
17
|
-
as_of = all.from("(#{virtual_table}) #{quoted_table_name}")
|
18
|
-
|
19
|
-
as_of.instance_variable_set(:@temporal, time)
|
20
|
-
|
21
|
-
return as_of
|
11
|
+
all.as_of_time!(time)
|
22
12
|
end
|
23
13
|
|
24
14
|
include TimeMachine::HistoryMethods::Timeline
|
25
15
|
end
|
26
16
|
|
17
|
+
include Patches::AsOfTimeHolder
|
18
|
+
|
27
19
|
def as_of(time)
|
28
20
|
self.class.as_of(time).where(:id => self.id).first!
|
29
21
|
end
|
@@ -5,6 +5,8 @@ module ChronoModel
|
|
5
5
|
module TimeMachine
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
|
+
include Patches::AsOfTimeHolder
|
9
|
+
|
8
10
|
included do
|
9
11
|
if table_exists? && !chrono?
|
10
12
|
puts "WARNING: #{table_name} is not a temporal table. " \
|
@@ -14,16 +16,28 @@ module ChronoModel
|
|
14
16
|
history = TimeMachine.define_history_model_for(self)
|
15
17
|
TimeMachine.chrono_models[table_name] = history
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
class << self
|
20
|
+
alias_method :direct_descendants_with_history, :direct_descendants
|
21
|
+
def direct_descendants
|
22
|
+
direct_descendants_with_history.reject(&:history?)
|
23
|
+
end
|
24
|
+
|
25
|
+
alias_method :descendants_with_history, :descendants
|
26
|
+
def descendants
|
27
|
+
descendants_with_history.reject(&:history?)
|
28
|
+
end
|
21
29
|
|
22
|
-
#
|
23
|
-
#
|
24
|
-
|
25
|
-
|
26
|
-
|
30
|
+
# STI support. TODO: more thorough testing
|
31
|
+
#
|
32
|
+
def inherited(subclass)
|
33
|
+
super
|
34
|
+
|
35
|
+
# Do not smash stack: as the below method is defining a
|
36
|
+
# new anonymous class, without this check this leads to
|
37
|
+
# infinite recursion.
|
38
|
+
unless subclass.name.nil?
|
39
|
+
TimeMachine.define_inherited_history_model_for(subclass)
|
40
|
+
end
|
27
41
|
end
|
28
42
|
end
|
29
43
|
end
|
@@ -40,6 +54,8 @@ module ChronoModel
|
|
40
54
|
|
41
55
|
extend TimeMachine::HistoryMethods
|
42
56
|
|
57
|
+
scope :chronological, -> { order(:recorded_at, :hid) }
|
58
|
+
|
43
59
|
# The history id is `hid`, but this cannot set as primary key
|
44
60
|
# or temporal assocations will break. Solutions are welcome.
|
45
61
|
def id
|
@@ -148,6 +164,7 @@ module ChronoModel
|
|
148
164
|
def valid_to
|
149
165
|
validity.last
|
150
166
|
end
|
167
|
+
alias as_of_time valid_to
|
151
168
|
|
152
169
|
def recorded_at
|
153
170
|
Conversions.string_to_utc_time attributes_before_type_cast['recorded_at']
|
@@ -212,7 +229,7 @@ module ChronoModel
|
|
212
229
|
# Return the complete read-only history of this instance.
|
213
230
|
#
|
214
231
|
def history
|
215
|
-
self.class.history.of(self)
|
232
|
+
self.class.history.chronological.of(self)
|
216
233
|
end
|
217
234
|
|
218
235
|
# Returns an Array of timestamps for which this instance has an history
|
@@ -225,13 +242,7 @@ module ChronoModel
|
|
225
242
|
# Returns a boolean indicating whether this record is an history entry.
|
226
243
|
#
|
227
244
|
def historical?
|
228
|
-
self.
|
229
|
-
end
|
230
|
-
|
231
|
-
# Read the virtual 'as_of_time' attribute and return it as an UTC timestamp.
|
232
|
-
#
|
233
|
-
def as_of_time
|
234
|
-
Conversions.string_to_utc_time attributes_before_type_cast['as_of_time']
|
245
|
+
self.as_of_time.present? || self.kind_of?(self.class.history)
|
235
246
|
end
|
236
247
|
|
237
248
|
# Inhibit destroy of historical records
|
@@ -317,15 +328,21 @@ module ChronoModel
|
|
317
328
|
end
|
318
329
|
|
319
330
|
module ClassMethods
|
331
|
+
# Identify this class as the parent, non-history, class.
|
332
|
+
#
|
333
|
+
def history?
|
334
|
+
false
|
335
|
+
end
|
336
|
+
|
320
337
|
# Returns an ActiveRecord::Relation on the history of this model as
|
321
338
|
# it was +time+ ago.
|
322
339
|
def as_of(time)
|
323
|
-
history.as_of(time
|
340
|
+
history.as_of(time)
|
324
341
|
end
|
325
342
|
|
326
343
|
def attribute_names_for_history_changes
|
327
344
|
@attribute_names_for_history_changes ||= attribute_names -
|
328
|
-
%w( id hid validity recorded_at
|
345
|
+
%w( id hid validity recorded_at )
|
329
346
|
end
|
330
347
|
|
331
348
|
def has_timeline(options)
|
@@ -401,6 +418,10 @@ module ChronoModel
|
|
401
418
|
def build_time_query_at(time, range)
|
402
419
|
time = if time.kind_of?(Array)
|
403
420
|
time.map! {|t| time_for_time_query(t, range)}
|
421
|
+
|
422
|
+
# If both edges of the range are the same the query fails using the '&&' operator.
|
423
|
+
# The correct solution is to use the <@ operator.
|
424
|
+
time.first == time.last ? time.first : time
|
404
425
|
else
|
405
426
|
time_for_time_query(time, range)
|
406
427
|
end
|
@@ -446,34 +467,21 @@ module ChronoModel
|
|
446
467
|
# superclass of Fruit::History, which is Fruit. So, we use
|
447
468
|
# non_history_superclass instead. -npj
|
448
469
|
def non_history_superclass(klass = self)
|
449
|
-
if klass.superclass.
|
470
|
+
if klass.superclass.history?
|
450
471
|
non_history_superclass(klass.superclass)
|
451
472
|
else
|
452
473
|
klass.superclass
|
453
474
|
end
|
454
475
|
end
|
455
476
|
|
477
|
+
def relation
|
478
|
+
super.as_of_time!(Time.now)
|
479
|
+
end
|
480
|
+
|
456
481
|
# Fetches as of +time+ records.
|
457
482
|
#
|
458
|
-
def as_of(time
|
459
|
-
|
460
|
-
|
461
|
-
# Add default scopes back if we're passed nil or a
|
462
|
-
# specific scope, because we're .unscopeing above.
|
463
|
-
#
|
464
|
-
scopes = !scope.nil? ? [scope] : (
|
465
|
-
superclass.default_scopes.map do |s|
|
466
|
-
s.respond_to?(:call) ? s.call : s
|
467
|
-
end)
|
468
|
-
|
469
|
-
scopes.each do |s|
|
470
|
-
s.order_values.each {|clause| as_of = as_of.order(clause)}
|
471
|
-
s.where_values.each {|clause| as_of = as_of.where(clause)}
|
472
|
-
end
|
473
|
-
|
474
|
-
as_of.instance_variable_set(:@temporal, time)
|
475
|
-
|
476
|
-
return as_of
|
483
|
+
def as_of(time)
|
484
|
+
non_history_superclass.from(virtual_table_at(time)).as_of_time!(time)
|
477
485
|
end
|
478
486
|
|
479
487
|
def virtual_table_at(time, name = nil)
|
@@ -486,11 +494,7 @@ module ChronoModel
|
|
486
494
|
# Fetches history record at the given time
|
487
495
|
#
|
488
496
|
def at(time)
|
489
|
-
|
490
|
-
|
491
|
-
unscoped.
|
492
|
-
select("#{quoted_table_name}.*, #{connection.quote(time)}::timestamp AS as_of_time").
|
493
|
-
time_query(:at, time)
|
497
|
+
time_query(:at, time).from(quoted_table_name).as_of_time!(time)
|
494
498
|
end
|
495
499
|
|
496
500
|
# Returns the history sorted by recorded_at
|
@@ -505,49 +509,15 @@ module ChronoModel
|
|
505
509
|
# is maximum.
|
506
510
|
#
|
507
511
|
def of(object)
|
508
|
-
where(:id => object)
|
512
|
+
where(:id => object)
|
509
513
|
end
|
510
514
|
|
511
|
-
#
|
512
|
-
# timestamps and sorting if there is an aggregate function in the
|
513
|
-
# select list - as it is likely what you'll want. However, if you
|
514
|
-
# have a query that performs an aggregate in a subquery, the code
|
515
|
-
# below will do the wrong thing - and you'll have to forcibly add
|
516
|
-
# back the history fields yourself.
|
515
|
+
# FIXME Remove, this was a workaround to a former design flaw.
|
517
516
|
#
|
518
|
-
#
|
519
|
-
# the added history fields, and remove all this crap from here...
|
520
|
-
# but it is not easily feasible. So we're going with a workaround
|
521
|
-
# for now.
|
522
|
-
#
|
523
|
-
# - vjt Wed Apr 2 19:56:35 CEST 2014
|
517
|
+
# - vjt Wed Oct 28 17:13:57 CET 2015
|
524
518
|
#
|
525
519
|
def force_history_fields
|
526
|
-
|
527
|
-
end
|
528
|
-
|
529
|
-
module HistorySelect #:nodoc:
|
530
|
-
Aggregates = %r{(?:(?:bit|bool)_(?:and|or)|(?:array_|string_|xml)agg|count|every|m(?:in|ax)|sum|stddev|var(?:_pop|_samp|iance)|corr|covar_|regr_)\w*\s*\(}i
|
531
|
-
|
532
|
-
SELECT_VALUES = "upper(validity) AS as_of_time"
|
533
|
-
ORDER_VALUES = lambda {|tbl| %[#{tbl}."recorded_at", #{tbl}."hid"]}
|
534
|
-
|
535
|
-
def build_arel
|
536
|
-
has_aggregate = select_values.any? do |v|
|
537
|
-
v.kind_of?(Arel::Nodes::Function) || # FIXME this is a bit ugly.
|
538
|
-
v.to_s =~ Aggregates
|
539
|
-
end
|
540
|
-
|
541
|
-
return super if has_aggregate
|
542
|
-
|
543
|
-
if order_values.blank?
|
544
|
-
self.order_values += [ ORDER_VALUES[quoted_table_name] ]
|
545
|
-
end
|
546
|
-
|
547
|
-
super.tap do |rel|
|
548
|
-
rel.project(SELECT_VALUES)
|
549
|
-
end
|
550
|
-
end
|
520
|
+
self
|
551
521
|
end
|
552
522
|
|
553
523
|
include(Timeline = Module.new do
|
@@ -568,7 +538,7 @@ module ChronoModel
|
|
568
538
|
|
569
539
|
fields = models.inject([]) {|a,m| a.concat m.quoted_history_fields}
|
570
540
|
|
571
|
-
relation = self.
|
541
|
+
relation = self.except(:order).
|
572
542
|
select("DISTINCT UNNEST(ARRAY[#{fields.join(',')}]) AS ts")
|
573
543
|
|
574
544
|
if assocs.present?
|
@@ -639,25 +609,6 @@ module ChronoModel
|
|
639
609
|
end
|
640
610
|
end
|
641
611
|
end
|
642
|
-
|
643
|
-
module QueryMethods
|
644
|
-
def build_arel
|
645
|
-
super.tap do |arel|
|
646
|
-
|
647
|
-
arel.join_sources.each do |join|
|
648
|
-
model = TimeMachine.chrono_models[join.left.table_name]
|
649
|
-
next unless model
|
650
|
-
|
651
|
-
join.left = Arel::Nodes::SqlLiteral.new(
|
652
|
-
model.history.virtual_table_at(@temporal, join.left.table_alias || join.left.table_name)
|
653
|
-
)
|
654
|
-
end if @temporal
|
655
|
-
|
656
|
-
end
|
657
|
-
end
|
658
|
-
end
|
659
|
-
ActiveRecord::Relation.instance_eval { include QueryMethods }
|
660
|
-
|
661
612
|
end
|
662
613
|
|
663
614
|
end
|
data/lib/chrono_model/version.rb
CHANGED
data/spec/adapter_spec.rb
CHANGED
@@ -2,30 +2,30 @@ require 'spec_helper'
|
|
2
2
|
require 'support/helpers'
|
3
3
|
|
4
4
|
shared_examples_for 'temporal table' do
|
5
|
-
it { adapter.is_chrono?(subject).
|
5
|
+
it { expect(adapter.is_chrono?(subject)).to be(true) }
|
6
6
|
|
7
|
-
it {
|
7
|
+
it { is_expected.to_not have_public_backing }
|
8
8
|
|
9
|
-
it {
|
10
|
-
it {
|
11
|
-
it {
|
12
|
-
it {
|
9
|
+
it { is_expected.to have_temporal_backing }
|
10
|
+
it { is_expected.to have_history_backing }
|
11
|
+
it { is_expected.to have_history_extra_columns }
|
12
|
+
it { is_expected.to have_public_interface }
|
13
13
|
|
14
|
-
it {
|
15
|
-
it {
|
16
|
-
it {
|
14
|
+
it { is_expected.to have_columns(columns) }
|
15
|
+
it { is_expected.to have_temporal_columns(columns) }
|
16
|
+
it { is_expected.to have_history_columns(columns) }
|
17
17
|
end
|
18
18
|
|
19
19
|
shared_examples_for 'plain table' do
|
20
|
-
it { adapter.is_chrono?(subject).
|
20
|
+
it { expect(adapter.is_chrono?(subject)).to be(false) }
|
21
21
|
|
22
|
-
it {
|
22
|
+
it { is_expected.to have_public_backing }
|
23
23
|
|
24
|
-
it {
|
25
|
-
it {
|
26
|
-
it {
|
24
|
+
it { is_expected.to_not have_temporal_backing }
|
25
|
+
it { is_expected.to_not have_history_backing }
|
26
|
+
it { is_expected.to_not have_public_interface }
|
27
27
|
|
28
|
-
it {
|
28
|
+
it { is_expected.to have_columns(columns) }
|
29
29
|
end
|
30
30
|
|
31
31
|
describe ChronoModel::Adapter do
|
@@ -33,17 +33,21 @@ describe ChronoModel::Adapter do
|
|
33
33
|
|
34
34
|
context do
|
35
35
|
subject { adapter }
|
36
|
-
it {
|
37
|
-
its(:adapter_name) { should == 'PostgreSQL' }
|
36
|
+
it { is_expected.to be_a_kind_of(ChronoModel::Adapter) }
|
38
37
|
|
39
38
|
context do
|
40
|
-
|
41
|
-
it {
|
39
|
+
subject { adapter.adapter_name }
|
40
|
+
it { is_expected.to eq 'PostgreSQL' }
|
42
41
|
end
|
43
42
|
|
44
43
|
context do
|
45
|
-
before { adapter.
|
46
|
-
it {
|
44
|
+
before { expect(adapter).to receive(:postgresql_version).and_return(90300) }
|
45
|
+
it { is_expected.to be_chrono_supported }
|
46
|
+
end
|
47
|
+
|
48
|
+
context do
|
49
|
+
before { expect(adapter).to receive(:postgresql_version).and_return(90000) }
|
50
|
+
it { is_expected.to_not be_chrono_supported }
|
47
51
|
end
|
48
52
|
end
|
49
53
|
|
@@ -52,7 +56,7 @@ describe ChronoModel::Adapter do
|
|
52
56
|
|
53
57
|
columns do
|
54
58
|
native = [
|
55
|
-
['test', 'character varying
|
59
|
+
['test', 'character varying'],
|
56
60
|
['foo', 'integer'],
|
57
61
|
['bar', 'double precision'],
|
58
62
|
['baz', 'text']
|
@@ -135,11 +139,11 @@ describe ChronoModel::Adapter do
|
|
135
139
|
end
|
136
140
|
|
137
141
|
it "copies plain index to history" do
|
138
|
-
history_indexes.find {|i| i.columns == ['foo']}.
|
142
|
+
expect(history_indexes.find {|i| i.columns == ['foo']}).to be_present
|
139
143
|
end
|
140
144
|
|
141
145
|
it "copies unique index to history without uniqueness constraint" do
|
142
|
-
history_indexes.find {|i| i.columns == ['bar'] && i.unique == false}.
|
146
|
+
expect(history_indexes.find {|i| i.columns == ['bar'] && i.unique == false}).to be_present
|
143
147
|
end
|
144
148
|
end
|
145
149
|
end
|
@@ -151,10 +155,10 @@ describe ChronoModel::Adapter do
|
|
151
155
|
adapter.drop_table table
|
152
156
|
end
|
153
157
|
|
154
|
-
it {
|
155
|
-
it {
|
156
|
-
it {
|
157
|
-
it {
|
158
|
+
it { is_expected.to_not have_public_backing }
|
159
|
+
it { is_expected.to_not have_temporal_backing }
|
160
|
+
it { is_expected.to_not have_history_backing }
|
161
|
+
it { is_expected.to_not have_public_interface }
|
158
162
|
end
|
159
163
|
|
160
164
|
describe '.add_index' do
|
@@ -164,13 +168,13 @@ describe ChronoModel::Adapter do
|
|
164
168
|
adapter.add_index table, [:test], :name => 'test_index'
|
165
169
|
end
|
166
170
|
|
167
|
-
it {
|
168
|
-
it {
|
169
|
-
it {
|
170
|
-
it {
|
171
|
+
it { is_expected.to have_temporal_index 'foobar_index', %w( foo bar ) }
|
172
|
+
it { is_expected.to have_history_index 'foobar_index', %w( foo bar ) }
|
173
|
+
it { is_expected.to have_temporal_index 'test_index', %w( test ) }
|
174
|
+
it { is_expected.to have_history_index 'test_index', %w( test ) }
|
171
175
|
|
172
|
-
it {
|
173
|
-
it {
|
176
|
+
it { is_expected.to_not have_index 'foobar_index', %w( foo bar ) }
|
177
|
+
it { is_expected.to_not have_index 'test_index', %w( test ) }
|
174
178
|
end
|
175
179
|
|
176
180
|
with_plain_table do
|
@@ -179,13 +183,13 @@ describe ChronoModel::Adapter do
|
|
179
183
|
adapter.add_index table, [:test], :name => 'test_index'
|
180
184
|
end
|
181
185
|
|
182
|
-
it {
|
183
|
-
it {
|
184
|
-
it {
|
185
|
-
it {
|
186
|
+
it { is_expected.to_not have_temporal_index 'foobar_index', %w( foo bar ) }
|
187
|
+
it { is_expected.to_not have_history_index 'foobar_index', %w( foo bar ) }
|
188
|
+
it { is_expected.to_not have_temporal_index 'test_index', %w( test ) }
|
189
|
+
it { is_expected.to_not have_history_index 'test_index', %w( test ) }
|
186
190
|
|
187
|
-
it {
|
188
|
-
it {
|
191
|
+
it { is_expected.to have_index 'foobar_index', %w( foo bar ) }
|
192
|
+
it { is_expected.to have_index 'test_index', %w( test ) }
|
189
193
|
end
|
190
194
|
end
|
191
195
|
|
@@ -198,9 +202,9 @@ describe ChronoModel::Adapter do
|
|
198
202
|
adapter.remove_index table, :name => 'test_index'
|
199
203
|
end
|
200
204
|
|
201
|
-
it {
|
202
|
-
it {
|
203
|
-
it {
|
205
|
+
it { is_expected.to_not have_temporal_index 'test_index', %w( test ) }
|
206
|
+
it { is_expected.to_not have_history_index 'test_index', %w( test ) }
|
207
|
+
it { is_expected.to_not have_index 'test_index', %w( test ) }
|
204
208
|
end
|
205
209
|
|
206
210
|
with_plain_table do
|
@@ -211,9 +215,9 @@ describe ChronoModel::Adapter do
|
|
211
215
|
adapter.remove_index table, :name => 'test_index'
|
212
216
|
end
|
213
217
|
|
214
|
-
it {
|
215
|
-
it {
|
216
|
-
it {
|
218
|
+
it { is_expected.to_not have_temporal_index 'test_index', %w( test ) }
|
219
|
+
it { is_expected.to_not have_history_index 'test_index', %w( test ) }
|
220
|
+
it { is_expected.to_not have_index 'test_index', %w( test ) }
|
217
221
|
end
|
218
222
|
end
|
219
223
|
|
@@ -225,9 +229,9 @@ describe ChronoModel::Adapter do
|
|
225
229
|
adapter.add_column table, :foobarbaz, :integer
|
226
230
|
end
|
227
231
|
|
228
|
-
it {
|
229
|
-
it {
|
230
|
-
it {
|
232
|
+
it { is_expected.to have_columns(extra_columns) }
|
233
|
+
it { is_expected.to have_temporal_columns(extra_columns) }
|
234
|
+
it { is_expected.to have_history_columns(extra_columns) }
|
231
235
|
end
|
232
236
|
|
233
237
|
with_plain_table do
|
@@ -235,7 +239,7 @@ describe ChronoModel::Adapter do
|
|
235
239
|
adapter.add_column table, :foobarbaz, :integer
|
236
240
|
end
|
237
241
|
|
238
|
-
it {
|
242
|
+
it { is_expected.to have_columns(extra_columns) }
|
239
243
|
end
|
240
244
|
end
|
241
245
|
|
@@ -247,13 +251,13 @@ describe ChronoModel::Adapter do
|
|
247
251
|
adapter.remove_column table, :foo
|
248
252
|
end
|
249
253
|
|
250
|
-
it {
|
251
|
-
it {
|
252
|
-
it {
|
254
|
+
it { is_expected.to have_columns(resulting_columns) }
|
255
|
+
it { is_expected.to have_temporal_columns(resulting_columns) }
|
256
|
+
it { is_expected.to have_history_columns(resulting_columns) }
|
253
257
|
|
254
|
-
it {
|
255
|
-
it {
|
256
|
-
it {
|
258
|
+
it { is_expected.to_not have_columns([['foo', 'integer']]) }
|
259
|
+
it { is_expected.to_not have_temporal_columns([['foo', 'integer']]) }
|
260
|
+
it { is_expected.to_not have_history_columns([['foo', 'integer']]) }
|
257
261
|
end
|
258
262
|
|
259
263
|
with_plain_table do
|
@@ -261,8 +265,8 @@ describe ChronoModel::Adapter do
|
|
261
265
|
adapter.remove_column table, :foo
|
262
266
|
end
|
263
267
|
|
264
|
-
it {
|
265
|
-
it {
|
268
|
+
it { is_expected.to have_columns(resulting_columns) }
|
269
|
+
it { is_expected.to_not have_columns([['foo', 'integer']]) }
|
266
270
|
end
|
267
271
|
end
|
268
272
|
|
@@ -272,13 +276,13 @@ describe ChronoModel::Adapter do
|
|
272
276
|
adapter.rename_column table, :foo, :taratapiatapioca
|
273
277
|
end
|
274
278
|
|
275
|
-
it {
|
276
|
-
it {
|
277
|
-
it {
|
279
|
+
it { is_expected.to_not have_columns([['foo', 'integer']]) }
|
280
|
+
it { is_expected.to_not have_temporal_columns([['foo', 'integer']]) }
|
281
|
+
it { is_expected.to_not have_history_columns([['foo', 'integer']]) }
|
278
282
|
|
279
|
-
it {
|
280
|
-
it {
|
281
|
-
it {
|
283
|
+
it { is_expected.to have_columns([['taratapiatapioca', 'integer']]) }
|
284
|
+
it { is_expected.to have_temporal_columns([['taratapiatapioca', 'integer']]) }
|
285
|
+
it { is_expected.to have_history_columns([['taratapiatapioca', 'integer']]) }
|
282
286
|
end
|
283
287
|
|
284
288
|
with_plain_table do
|
@@ -286,8 +290,8 @@ describe ChronoModel::Adapter do
|
|
286
290
|
adapter.rename_column table, :foo, :taratapiatapioca
|
287
291
|
end
|
288
292
|
|
289
|
-
it {
|
290
|
-
it {
|
293
|
+
it { is_expected.to_not have_columns([['foo', 'integer']]) }
|
294
|
+
it { is_expected.to have_columns([['taratapiatapioca', 'integer']]) }
|
291
295
|
end
|
292
296
|
end
|
293
297
|
|
@@ -297,13 +301,13 @@ describe ChronoModel::Adapter do
|
|
297
301
|
adapter.change_column table, :foo, :float
|
298
302
|
end
|
299
303
|
|
300
|
-
it {
|
301
|
-
it {
|
302
|
-
it {
|
304
|
+
it { is_expected.to_not have_columns([['foo', 'integer']]) }
|
305
|
+
it { is_expected.to_not have_temporal_columns([['foo', 'integer']]) }
|
306
|
+
it { is_expected.to_not have_history_columns([['foo', 'integer']]) }
|
303
307
|
|
304
|
-
it {
|
305
|
-
it {
|
306
|
-
it {
|
308
|
+
it { is_expected.to have_columns([['foo', 'double precision']]) }
|
309
|
+
it { is_expected.to have_temporal_columns([['foo', 'double precision']]) }
|
310
|
+
it { is_expected.to have_history_columns([['foo', 'double precision']]) }
|
307
311
|
end
|
308
312
|
|
309
313
|
with_plain_table do
|
@@ -311,8 +315,8 @@ describe ChronoModel::Adapter do
|
|
311
315
|
adapter.change_column table, :foo, :float
|
312
316
|
end
|
313
317
|
|
314
|
-
it {
|
315
|
-
it {
|
318
|
+
it { is_expected.to_not have_columns([['foo', 'integer']]) }
|
319
|
+
it { is_expected.to have_columns([['foo', 'double precision']]) }
|
316
320
|
end
|
317
321
|
end
|
318
322
|
|
@@ -322,9 +326,9 @@ describe ChronoModel::Adapter do
|
|
322
326
|
adapter.remove_column table, :foo
|
323
327
|
end
|
324
328
|
|
325
|
-
it {
|
326
|
-
it {
|
327
|
-
it {
|
329
|
+
it { is_expected.to_not have_columns([['foo', 'integer']]) }
|
330
|
+
it { is_expected.to_not have_temporal_columns([['foo', 'integer']]) }
|
331
|
+
it { is_expected.to_not have_history_columns([['foo', 'integer']]) }
|
328
332
|
end
|
329
333
|
|
330
334
|
with_plain_table do
|
@@ -332,7 +336,7 @@ describe ChronoModel::Adapter do
|
|
332
336
|
adapter.remove_column table, :foo
|
333
337
|
end
|
334
338
|
|
335
|
-
it {
|
339
|
+
it { is_expected.to_not have_columns([['foo', 'integer']]) }
|
336
340
|
end
|
337
341
|
end
|
338
342
|
|
@@ -340,8 +344,8 @@ describe ChronoModel::Adapter do
|
|
340
344
|
subject { adapter.column_definitions(table).map {|d| d.take(2)} }
|
341
345
|
|
342
346
|
assert = proc do
|
343
|
-
it { (subject & columns).
|
344
|
-
it {
|
347
|
+
it { expect(subject & columns).to eq columns }
|
348
|
+
it { is_expected.to include(['id', 'integer']) }
|
345
349
|
end
|
346
350
|
|
347
351
|
with_temporal_table(&assert)
|
@@ -352,7 +356,7 @@ describe ChronoModel::Adapter do
|
|
352
356
|
subject { adapter.primary_key(table) }
|
353
357
|
|
354
358
|
assert = proc do
|
355
|
-
it {
|
359
|
+
it { is_expected.to eq 'id' }
|
356
360
|
end
|
357
361
|
|
358
362
|
with_temporal_table(&assert)
|
@@ -368,8 +372,8 @@ describe ChronoModel::Adapter do
|
|
368
372
|
adapter.add_index table, [:bar, :baz], :name => 'bar_index'
|
369
373
|
end
|
370
374
|
|
371
|
-
it { subject.map(&:name).
|
372
|
-
it { subject.map(&:columns).
|
375
|
+
it { expect(subject.map(&:name)).to match_array %w( foo_index bar_index ) }
|
376
|
+
it { expect(subject.map(&:columns)).to match_array [['foo'], ['bar', 'baz']] }
|
373
377
|
end
|
374
378
|
|
375
379
|
with_temporal_table(&assert)
|
@@ -384,32 +388,32 @@ describe ChronoModel::Adapter do
|
|
384
388
|
context 'with nesting' do
|
385
389
|
|
386
390
|
it 'saves the schema at each recursion' do
|
387
|
-
|
391
|
+
is_expected.to be_in_schema(:default)
|
388
392
|
|
389
|
-
adapter.on_schema('test_1') {
|
390
|
-
adapter.on_schema('test_2') {
|
391
|
-
adapter.on_schema('test_3') {
|
393
|
+
adapter.on_schema('test_1') { is_expected.to be_in_schema('test_1')
|
394
|
+
adapter.on_schema('test_2') { is_expected.to be_in_schema('test_2')
|
395
|
+
adapter.on_schema('test_3') { is_expected.to be_in_schema('test_3')
|
392
396
|
}
|
393
|
-
|
397
|
+
is_expected.to be_in_schema('test_2')
|
394
398
|
}
|
395
|
-
|
399
|
+
is_expected.to be_in_schema('test_1')
|
396
400
|
}
|
397
401
|
|
398
|
-
|
402
|
+
is_expected.to be_in_schema(:default)
|
399
403
|
end
|
400
404
|
|
401
405
|
end
|
402
406
|
|
403
407
|
context 'without nesting' do
|
404
408
|
it 'ignores recursive calls' do
|
405
|
-
|
409
|
+
is_expected.to be_in_schema(:default)
|
406
410
|
|
407
|
-
adapter.on_schema('test_1', false) {
|
408
|
-
adapter.on_schema('test_2', false) {
|
409
|
-
adapter.on_schema('test_3', false) {
|
411
|
+
adapter.on_schema('test_1', false) { is_expected.to be_in_schema('test_1')
|
412
|
+
adapter.on_schema('test_2', false) { is_expected.to be_in_schema('test_1')
|
413
|
+
adapter.on_schema('test_3', false) { is_expected.to be_in_schema('test_1')
|
410
414
|
} } }
|
411
415
|
|
412
|
-
|
416
|
+
is_expected.to be_in_schema(:default)
|
413
417
|
end
|
414
418
|
end
|
415
419
|
end
|
@@ -445,8 +449,8 @@ describe ChronoModel::Adapter do
|
|
445
449
|
end
|
446
450
|
|
447
451
|
it { expect { insert }.to_not raise_error }
|
448
|
-
it { count(current).
|
449
|
-
it { count(history).
|
452
|
+
it { expect(count(current)).to eq 2 }
|
453
|
+
it { expect(count(history)).to eq 2 }
|
450
454
|
end
|
451
455
|
|
452
456
|
context 'when failing' do
|
@@ -458,9 +462,9 @@ describe ChronoModel::Adapter do
|
|
458
462
|
SQL
|
459
463
|
end
|
460
464
|
|
461
|
-
it { expect { insert }.to raise_error }
|
462
|
-
it { count(current).
|
463
|
-
it { count(history).
|
465
|
+
it { expect { insert }.to raise_error(ActiveRecord::StatementInvalid) }
|
466
|
+
it { expect(count(current)).to eq 2 } # Because the previous
|
467
|
+
it { expect(count(history)).to eq 2 } # records are preserved
|
464
468
|
end
|
465
469
|
|
466
470
|
context 'after a failure' do
|
@@ -474,10 +478,10 @@ describe ChronoModel::Adapter do
|
|
474
478
|
|
475
479
|
it { expect { insert }.to_not raise_error }
|
476
480
|
|
477
|
-
it { count(current).
|
478
|
-
it { count(history).
|
481
|
+
it { expect(count(current)).to eq 4 }
|
482
|
+
it { expect(count(history)).to eq 4 }
|
479
483
|
|
480
|
-
it { ids(current).
|
484
|
+
it { expect(ids(current)).to eq ids(history) }
|
481
485
|
end
|
482
486
|
end
|
483
487
|
|
@@ -503,7 +507,7 @@ describe ChronoModel::Adapter do
|
|
503
507
|
end
|
504
508
|
|
505
509
|
it { expect { insert }.to_not raise_error }
|
506
|
-
it { insert; select.uniq.
|
510
|
+
it { insert; expect(select.uniq).to eq ['default-value'] }
|
507
511
|
end
|
508
512
|
|
509
513
|
context 'redundant UPDATEs' do
|
@@ -528,8 +532,8 @@ describe ChronoModel::Adapter do
|
|
528
532
|
adapter.drop_table table
|
529
533
|
end
|
530
534
|
|
531
|
-
it { count(current).
|
532
|
-
it { count(history).
|
535
|
+
it { expect(count(current)).to eq 1 }
|
536
|
+
it { expect(count(history)).to eq 2 }
|
533
537
|
|
534
538
|
end
|
535
539
|
|
@@ -537,7 +541,7 @@ describe ChronoModel::Adapter do
|
|
537
541
|
before :all do
|
538
542
|
adapter.create_table table, :temporal => true do |t|
|
539
543
|
t.string 'test'
|
540
|
-
t.timestamps
|
544
|
+
t.timestamps null: false
|
541
545
|
end
|
542
546
|
|
543
547
|
adapter.execute <<-SQL
|
@@ -563,8 +567,8 @@ describe ChronoModel::Adapter do
|
|
563
567
|
adapter.drop_table table
|
564
568
|
end
|
565
569
|
|
566
|
-
it { count(current).
|
567
|
-
it { count(history).
|
570
|
+
it { expect(count(current)).to eq 1 }
|
571
|
+
it { expect(count(history)).to eq 2 }
|
568
572
|
end
|
569
573
|
|
570
574
|
context 'selective journaled fields' do
|
@@ -593,8 +597,8 @@ describe ChronoModel::Adapter do
|
|
593
597
|
adapter.drop_table table
|
594
598
|
end
|
595
599
|
|
596
|
-
it { count(current).
|
597
|
-
it { count(history).
|
600
|
+
it { expect(count(current)).to eq 1 }
|
601
|
+
it { expect(count(history)).to eq 1 }
|
598
602
|
|
599
603
|
it 'preserves options upon column change'
|
600
604
|
it 'changes option upon table change'
|