chrono_model 0.8.2 → 0.9.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 +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'
|