chrono_model 0.3.0 → 0.3.1
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.
- data/.gitignore +0 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +60 -0
- data/README.md +19 -1
- data/lib/chrono_model/adapter.rb +49 -10
- data/lib/chrono_model/railtie.rb +1 -0
- data/lib/chrono_model/time_machine.rb +148 -65
- data/lib/chrono_model/version.rb +1 -1
- data/spec/support/helpers.rb +11 -0
- data/spec/time_machine_spec.rb +35 -25
- metadata +7 -6
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
chrono_model (0.3.0)
|
5
|
+
activerecord (~> 3.2)
|
6
|
+
pg
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activemodel (3.2.8)
|
12
|
+
activesupport (= 3.2.8)
|
13
|
+
builder (~> 3.0.0)
|
14
|
+
activerecord (3.2.8)
|
15
|
+
activemodel (= 3.2.8)
|
16
|
+
activesupport (= 3.2.8)
|
17
|
+
arel (~> 3.0.2)
|
18
|
+
tzinfo (~> 0.3.29)
|
19
|
+
activesupport (3.2.8)
|
20
|
+
i18n (~> 0.6)
|
21
|
+
multi_json (~> 1.0)
|
22
|
+
arel (3.0.2)
|
23
|
+
builder (3.0.4)
|
24
|
+
coderay (1.0.8)
|
25
|
+
columnize (0.3.6)
|
26
|
+
debugger (1.2.1)
|
27
|
+
columnize (>= 0.3.1)
|
28
|
+
debugger-linecache (~> 1.1.1)
|
29
|
+
debugger-ruby_core_source (~> 1.1.4)
|
30
|
+
debugger-linecache (1.1.2)
|
31
|
+
debugger-ruby_core_source (>= 1.1.1)
|
32
|
+
debugger-ruby_core_source (1.1.4)
|
33
|
+
diff-lcs (1.1.3)
|
34
|
+
i18n (0.6.1)
|
35
|
+
method_source (0.8.1)
|
36
|
+
multi_json (1.3.6)
|
37
|
+
pg (0.14.1)
|
38
|
+
pry (0.9.10)
|
39
|
+
coderay (~> 1.0.5)
|
40
|
+
method_source (~> 0.8)
|
41
|
+
slop (~> 3.3.1)
|
42
|
+
rspec (2.11.0)
|
43
|
+
rspec-core (~> 2.11.0)
|
44
|
+
rspec-expectations (~> 2.11.0)
|
45
|
+
rspec-mocks (~> 2.11.0)
|
46
|
+
rspec-core (2.11.1)
|
47
|
+
rspec-expectations (2.11.3)
|
48
|
+
diff-lcs (~> 1.1.3)
|
49
|
+
rspec-mocks (2.11.3)
|
50
|
+
slop (3.3.3)
|
51
|
+
tzinfo (0.3.33)
|
52
|
+
|
53
|
+
PLATFORMS
|
54
|
+
ruby
|
55
|
+
|
56
|
+
DEPENDENCIES
|
57
|
+
chrono_model!
|
58
|
+
debugger
|
59
|
+
pry
|
60
|
+
rspec
|
data/README.md
CHANGED
@@ -85,6 +85,23 @@ by the other schema statements. E.g.:
|
|
85
85
|
* `remove_index` - removes the index from the history table as well
|
86
86
|
|
87
87
|
|
88
|
+
## Adding Temporal extensions to an existing table
|
89
|
+
|
90
|
+
Use `change_table`:
|
91
|
+
|
92
|
+
change_table :your_table, :temporal => true
|
93
|
+
|
94
|
+
If you want to also set up the history from your current data:
|
95
|
+
|
96
|
+
change_table :your_table, :temporal => true, :copy_data => true
|
97
|
+
|
98
|
+
This will create an history record for each record in your table, setting its
|
99
|
+
validity from midnight, January 1st, 1 CE. You can set a specific validity
|
100
|
+
with the `:validity` option:
|
101
|
+
|
102
|
+
change_table :your_table, :temporal => true, :copy_data => true, :validity => '1977-01-01'
|
103
|
+
|
104
|
+
|
88
105
|
## Data querying
|
89
106
|
|
90
107
|
A model backed by a temporal view will behave like any other model backed by a
|
@@ -97,7 +114,8 @@ plain table. If you want to do as-of-date queries, you need to include the
|
|
97
114
|
has_many :compositions
|
98
115
|
end
|
99
116
|
|
100
|
-
This will
|
117
|
+
This will create a `Country::History` model inherited from `Country`, and it
|
118
|
+
will make an `as_of` class method available to your model. E.g.:
|
101
119
|
|
102
120
|
Country.as_of(1.year.ago)
|
103
121
|
|
data/lib/chrono_model/adapter.rb
CHANGED
@@ -8,9 +8,14 @@ module ChronoModel
|
|
8
8
|
# adapter for a clean override of its methods using super.
|
9
9
|
#
|
10
10
|
class Adapter < ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
11
|
-
|
12
|
-
|
11
|
+
# The schema holding current data
|
12
|
+
TEMPORAL_SCHEMA = 'temporal'
|
13
13
|
|
14
|
+
# The schema holding historical data
|
15
|
+
HISTORY_SCHEMA = 'history'
|
16
|
+
|
17
|
+
# Chronomodel is supported starting with PostgreSQL >= 9.0
|
18
|
+
#
|
14
19
|
def chrono_supported?
|
15
20
|
postgresql_version >= 90000
|
16
21
|
end
|
@@ -87,6 +92,24 @@ module ChronoModel
|
|
87
92
|
chrono_create_view_for(table_name)
|
88
93
|
|
89
94
|
TableCache.add! table_name
|
95
|
+
|
96
|
+
# Optionally copy the plain table data, setting up history
|
97
|
+
# retroactively.
|
98
|
+
#
|
99
|
+
if options[:copy_data]
|
100
|
+
seq = _on_history_schema { serial_sequence(table_name, primary_key(table_name)) }
|
101
|
+
from = options[:validity] || '0001-01-01 00:00:00'
|
102
|
+
|
103
|
+
execute %[
|
104
|
+
INSERT INTO #{HISTORY_SCHEMA}.#{table_name}
|
105
|
+
SELECT *,
|
106
|
+
nextval('#{seq}') AS hid,
|
107
|
+
timestamp '#{from}' AS valid_from,
|
108
|
+
timestamp '9999-12-31 00:00:00' AS valid_to,
|
109
|
+
timezone('UTC', now()) AS recorded_at
|
110
|
+
FROM #{TEMPORAL_SCHEMA}.#{table_name}
|
111
|
+
]
|
112
|
+
end
|
90
113
|
end
|
91
114
|
|
92
115
|
chrono_alter(table_name) { super table_name, options, &block }
|
@@ -353,16 +376,32 @@ module ChronoModel
|
|
353
376
|
|
354
377
|
# INSERT - inert data both in the temporal table and in the history one.
|
355
378
|
#
|
356
|
-
|
357
|
-
|
379
|
+
if sequence.present?
|
380
|
+
execute <<-SQL
|
381
|
+
CREATE OR REPLACE RULE #{table}_ins AS ON INSERT TO #{table} DO INSTEAD (
|
358
382
|
|
359
|
-
|
383
|
+
INSERT INTO #{current} ( #{fields} ) VALUES ( #{values} );
|
360
384
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
385
|
+
INSERT INTO #{history} ( #{pk}, #{fields}, valid_from )
|
386
|
+
VALUES ( currval('#{sequence}'), #{values}, timezone('UTC', now()) )
|
387
|
+
RETURNING #{pk}, #{fields}
|
388
|
+
)
|
389
|
+
SQL
|
390
|
+
else
|
391
|
+
fields_with_pk = "#{pk}, " << fields
|
392
|
+
values_with_pk = "new.#{pk}, " << values
|
393
|
+
|
394
|
+
execute <<-SQL
|
395
|
+
CREATE OR REPLACE RULE #{table}_ins AS ON INSERT TO #{table} DO INSTEAD (
|
396
|
+
|
397
|
+
INSERT INTO #{current} ( #{fields_with_pk} ) VALUES ( #{values_with_pk} );
|
398
|
+
|
399
|
+
INSERT INTO #{history} ( #{fields_with_pk}, valid_from )
|
400
|
+
VALUES ( #{values_with_pk}, timezone('UTC', now()) )
|
401
|
+
RETURNING #{fields_with_pk}
|
402
|
+
)
|
403
|
+
SQL
|
404
|
+
end
|
366
405
|
|
367
406
|
# UPDATE - set the last history entry validity to now, save the current data
|
368
407
|
# in a new history entry and update the temporal table with the new data.
|
data/lib/chrono_model/railtie.rb
CHANGED
@@ -11,122 +11,205 @@ module ChronoModel
|
|
11
11
|
"Currently, only PostgreSQL >= 9.0 is supported."
|
12
12
|
end
|
13
13
|
|
14
|
-
|
14
|
+
if table_exists? && !chrono?
|
15
15
|
raise Error, "#{table_name} is not a temporal table. " \
|
16
16
|
"Please use change_table :#{table_name}, :temporal => true"
|
17
17
|
end
|
18
18
|
|
19
|
-
|
19
|
+
history = TimeMachine.define_history_model_for(self)
|
20
|
+
TimeMachine.chrono_models[table_name] = history
|
20
21
|
end
|
21
22
|
|
22
|
-
# Returns an Hash keyed by table name of
|
23
|
-
# ChronoModel::TimeMachine
|
23
|
+
# Returns an Hash keyed by table name of ChronoModels
|
24
24
|
#
|
25
25
|
def self.chrono_models
|
26
26
|
(@chrono_models ||= {})
|
27
27
|
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
self.class.as_of(time).find(self)
|
33
|
-
end
|
29
|
+
def self.define_history_model_for(model)
|
30
|
+
history = Class.new(model) do
|
31
|
+
self.table_name = [Adapter::HISTORY_SCHEMA, model.table_name].join('.')
|
34
32
|
|
35
|
-
|
36
|
-
#
|
37
|
-
def history
|
38
|
-
self.class.history_of(self)
|
39
|
-
end
|
33
|
+
extend TimeMachine::HistoryMethods
|
40
34
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
35
|
+
# The history id is `hid`, but this cannot set as primary key
|
36
|
+
# or temporal assocations will break. Solutions are welcome.
|
37
|
+
def id
|
38
|
+
hid
|
39
|
+
end
|
40
|
+
|
41
|
+
# Referenced record ID
|
42
|
+
#
|
43
|
+
def rid
|
44
|
+
attributes[self.class.primary_key]
|
45
|
+
end
|
46
|
+
|
47
|
+
# HACK to make ActiveAdmin work properly. This will be surely
|
48
|
+
# better written in the future.
|
49
|
+
#
|
50
|
+
def self.find(*args)
|
51
|
+
old = self.primary_key
|
52
|
+
self.primary_key = :hid
|
53
|
+
super
|
54
|
+
ensure
|
55
|
+
self.primary_key = old
|
56
|
+
end
|
57
|
+
|
58
|
+
# SCD Type 2 validity from timestamp
|
59
|
+
#
|
60
|
+
def valid_from
|
61
|
+
utc_timestamp_from('valid_from')
|
62
|
+
end
|
63
|
+
|
64
|
+
# SCD Type 2 validity to timestamp
|
65
|
+
#
|
66
|
+
def valid_to
|
67
|
+
utc_timestamp_from('valid_to')
|
68
|
+
end
|
69
|
+
|
70
|
+
# History recording timestamp
|
71
|
+
#
|
72
|
+
def recorded_at
|
73
|
+
utc_timestamp_from('recorded_at')
|
74
|
+
end
|
75
|
+
|
76
|
+
# Virtual attribute used to pass around the
|
77
|
+
# current timestamp in association queries
|
78
|
+
#
|
79
|
+
def as_of_time
|
80
|
+
Conversions.string_to_utc_time attributes['as_of_time']
|
81
|
+
end
|
82
|
+
|
83
|
+
# Inhibit destroy of historical records
|
84
|
+
#
|
85
|
+
def destroy
|
86
|
+
raise ActiveRecord::ReadOnlyRecord, 'Cannot delete historical records'
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
# Hack around AR timezone support. These timestamps are recorded
|
91
|
+
# by the chrono rewrite rules in UTC, but AR reads them as they
|
92
|
+
# were stored in the local timezone - thus here we reset its
|
93
|
+
# assumption. TODO: OPTIMIZE.
|
94
|
+
#
|
95
|
+
if ActiveRecord::Base.default_timezone != :utc
|
96
|
+
def utc_timestamp_from(attr)
|
97
|
+
attributes[attr].utc + Time.now.utc_offset
|
98
|
+
end
|
99
|
+
else
|
100
|
+
def utc_timestamp_from(attr)
|
101
|
+
attributes[attr]
|
102
|
+
end
|
103
|
+
end
|
48
104
|
end
|
49
|
-
end
|
50
105
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
attributes.key?('hid')
|
55
|
-
end
|
106
|
+
model.singleton_class.instance_eval do
|
107
|
+
define_method(:history) { history }
|
108
|
+
end
|
56
109
|
|
57
|
-
|
58
|
-
|
110
|
+
model.const_set :History, history
|
111
|
+
|
112
|
+
return history
|
59
113
|
end
|
60
114
|
|
61
|
-
#
|
115
|
+
# Returns a read-only representation of this record as it was +time+ ago.
|
62
116
|
#
|
63
|
-
def
|
64
|
-
|
117
|
+
def as_of(time)
|
118
|
+
self.class.as_of(time).where(:id => self.id).first!
|
119
|
+
end
|
65
120
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
@new_record = true
|
71
|
-
end
|
121
|
+
# Return the complete read-only history of this instance.
|
122
|
+
#
|
123
|
+
def history
|
124
|
+
self.class.history.of(self)
|
72
125
|
end
|
73
126
|
|
74
127
|
# Returns an Array of timestamps for which this instance has an history
|
75
128
|
# record. Takes temporal associations into account.
|
76
129
|
#
|
77
130
|
def history_timestamps
|
78
|
-
self.class.
|
131
|
+
self.class.history.timestamps do |query|
|
79
132
|
query.where(:id => self)
|
80
133
|
end
|
81
134
|
end
|
82
135
|
|
136
|
+
def historical?
|
137
|
+
self.kind_of? self.class.history
|
138
|
+
end
|
139
|
+
|
83
140
|
module ClassMethods
|
84
|
-
#
|
85
|
-
#
|
141
|
+
# Returns an ActiveRecord::Relation on the history of this model as
|
142
|
+
# it was +time+ ago.
|
86
143
|
def as_of(time)
|
87
|
-
|
144
|
+
history.as_of(time, current_scope)
|
145
|
+
end
|
146
|
+
end
|
88
147
|
|
89
|
-
|
90
|
-
|
148
|
+
# Methods that make up the history interface of the companion History
|
149
|
+
# model build on each Model that includes TimeMachine
|
150
|
+
module HistoryMethods
|
151
|
+
# Fetches as of +time+ records.
|
152
|
+
#
|
153
|
+
def as_of(time, scope = nil)
|
154
|
+
time = Conversions.time_to_utc_string(time.utc) if time.kind_of? Time
|
155
|
+
|
156
|
+
as_of = superclass.unscoped.readonly.
|
157
|
+
with(superclass.table_name, at(time))
|
158
|
+
|
159
|
+
# Add default scopes back if we're passed nil or a
|
160
|
+
# specific scope, because we're .unscopeing above.
|
161
|
+
#
|
162
|
+
scopes = scope.present? ? [scope] : (
|
163
|
+
superclass.default_scopes.map do |s|
|
164
|
+
s.respond_to?(:call) ? s.call : s
|
165
|
+
end)
|
166
|
+
|
167
|
+
scopes.each do |scope|
|
168
|
+
scope.order_values.each {|clause| as_of = as_of.order(clause.to_sql)}
|
169
|
+
scope.where_values.each {|clause| as_of = as_of.where(clause.to_sql)}
|
91
170
|
end
|
171
|
+
|
172
|
+
as_of.instance_variable_set(:@temporal, time)
|
173
|
+
|
174
|
+
return as_of
|
92
175
|
end
|
93
176
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
177
|
+
# Fetches history record at the given time
|
178
|
+
#
|
179
|
+
def at(time)
|
180
|
+
from, to = quoted_history_fields
|
181
|
+
unscoped.
|
182
|
+
select("#{quoted_table_name}.*, '#{time}' AS as_of_time").
|
183
|
+
where("'#{time}' >= #{from} AND '#{time}' < #{to}")
|
98
184
|
end
|
99
185
|
|
100
186
|
# Returns the whole history as read only.
|
101
187
|
#
|
102
|
-
def
|
103
|
-
readonly.
|
188
|
+
def all
|
189
|
+
readonly.
|
190
|
+
order("#{table_name}.recorded_at, hid").all
|
104
191
|
end
|
105
192
|
|
106
193
|
# Fetches the given +object+ history, sorted by history record time.
|
107
194
|
#
|
108
|
-
def
|
109
|
-
|
110
|
-
|
111
|
-
select(
|
195
|
+
def of(object)
|
196
|
+
now = 'LEAST(valid_to, now()::timestamp)'
|
197
|
+
readonly.
|
198
|
+
select("#{table_name}.*, #{now} AS as_of_time").
|
199
|
+
order("#{table_name}.recorded_at, hid").
|
112
200
|
where(:id => object)
|
113
201
|
end
|
114
202
|
|
115
|
-
# Returns this table name in the +Adapter::HISTORY_SCHEMA+
|
116
|
-
#
|
117
|
-
def history_table_name
|
118
|
-
[Adapter::HISTORY_SCHEMA, table_name].join('.')
|
119
|
-
end
|
120
203
|
|
121
204
|
# Returns an Array of unique UTC timestamps for which at least an
|
122
205
|
# history record exists. Takes temporal associations into account.
|
123
206
|
#
|
124
|
-
def
|
207
|
+
def timestamps
|
125
208
|
assocs = reflect_on_all_associations.select {|a|
|
126
|
-
[:has_one, :has_many].include?(a.macro) && a.klass.chrono?
|
209
|
+
[:belongs_to, :has_one, :has_many].include?(a.macro) && a.klass.chrono?
|
127
210
|
}
|
128
211
|
|
129
|
-
models = [self].concat(assocs.map
|
212
|
+
models = [self].concat(assocs.map {|a| a.klass.history})
|
130
213
|
fields = models.inject([]) {|a,m| a.concat m.quoted_history_fields}
|
131
214
|
|
132
215
|
relation = self.
|
@@ -140,14 +223,14 @@ module ChronoModel
|
|
140
223
|
sql.gsub! 'INNER JOIN', 'LEFT OUTER JOIN'
|
141
224
|
|
142
225
|
connection.on_schema(Adapter::HISTORY_SCHEMA) do
|
143
|
-
connection.select_values(sql, "#{self.name}
|
226
|
+
connection.select_values(sql, "#{self.name} periods").map! do |ts|
|
144
227
|
Conversions.string_to_utc_time ts
|
145
228
|
end
|
146
229
|
end
|
147
230
|
end
|
148
231
|
|
149
232
|
def quoted_history_fields
|
150
|
-
[:valid_from, :valid_to].map do |field|
|
233
|
+
@quoted_history_fields ||= [:valid_from, :valid_to].map do |field|
|
151
234
|
[connection.quote_table_name(table_name),
|
152
235
|
connection.quote_column_name(field)
|
153
236
|
].join('.')
|
@@ -162,7 +245,7 @@ module ChronoModel
|
|
162
245
|
# Extract joined tables and add temporal WITH if appropriate
|
163
246
|
arel.join_sources.map {|j| j.to_sql =~ /JOIN "(\w+)" ON/ && $1}.compact.each do |table|
|
164
247
|
next unless (model = TimeMachine.chrono_models[table])
|
165
|
-
with(table, model.
|
248
|
+
with(table, model.history.at(@temporal))
|
166
249
|
end if @temporal
|
167
250
|
|
168
251
|
end
|
data/lib/chrono_model/version.rb
CHANGED
data/spec/support/helpers.rb
CHANGED
@@ -64,6 +64,11 @@ module ChronoTest::Helpers
|
|
64
64
|
t.string :name
|
65
65
|
t.references :bar
|
66
66
|
end
|
67
|
+
|
68
|
+
adapter.create_table 'defoos', :temporal => true do |t|
|
69
|
+
t.string :name
|
70
|
+
t.boolean :active
|
71
|
+
end
|
67
72
|
end
|
68
73
|
|
69
74
|
after(:all) do
|
@@ -90,6 +95,12 @@ module ChronoTest::Helpers
|
|
90
95
|
class ::Baz < ActiveRecord::Base
|
91
96
|
belongs_to :baz
|
92
97
|
end
|
98
|
+
|
99
|
+
class ::Defoo < ActiveRecord::Base
|
100
|
+
include ChronoModel::TimeMachine
|
101
|
+
|
102
|
+
default_scope where(:active => true)
|
103
|
+
end
|
93
104
|
}
|
94
105
|
|
95
106
|
def define_models!
|
data/spec/time_machine_spec.rb
CHANGED
@@ -10,7 +10,7 @@ describe ChronoModel::TimeMachine do
|
|
10
10
|
describe '.chrono_models' do
|
11
11
|
subject { ChronoModel::TimeMachine.chrono_models }
|
12
12
|
|
13
|
-
it { should == {'foos' => Foo, 'bars' => Bar} }
|
13
|
+
it { should == {'foos' => Foo::History, 'defoos' => Defoo::History, 'bars' => Bar::History} }
|
14
14
|
end
|
15
15
|
|
16
16
|
|
@@ -88,6 +88,28 @@ describe ChronoModel::TimeMachine do
|
|
88
88
|
it 'raises RecordNotFound when no history records are found' do
|
89
89
|
expect { foo.as_of(1.minute.ago) }.to raise_error
|
90
90
|
end
|
91
|
+
|
92
|
+
describe 'it honors default_scopes' do
|
93
|
+
let!(:active) {
|
94
|
+
active = ts_eval { Defoo.create! :name => 'active 1', :active => true }
|
95
|
+
ts_eval(active) { update_attributes! :name => 'active 2' }
|
96
|
+
}
|
97
|
+
|
98
|
+
let!(:hidden) {
|
99
|
+
hidden = ts_eval { Defoo.create! :name => 'hidden 1', :active => false }
|
100
|
+
ts_eval(hidden) { update_attributes! :name => 'hidden 2' }
|
101
|
+
}
|
102
|
+
|
103
|
+
it { Defoo.as_of(active.ts[0]).map(&:name).should == ['active 1'] }
|
104
|
+
it { Defoo.as_of(active.ts[1]).map(&:name).should == ['active 2'] }
|
105
|
+
it { Defoo.as_of(hidden.ts[0]).map(&:name).should == ['active 2'] }
|
106
|
+
it { Defoo.as_of(hidden.ts[1]).map(&:name).should == ['active 2'] }
|
107
|
+
|
108
|
+
it { Defoo.unscoped.as_of(active.ts[0]).map(&:name).should == ['active 1'] }
|
109
|
+
it { Defoo.unscoped.as_of(active.ts[1]).map(&:name).should == ['active 2'] }
|
110
|
+
it { Defoo.unscoped.as_of(hidden.ts[0]).map(&:name).should == ['active 2', 'hidden 1'] }
|
111
|
+
it { Defoo.unscoped.as_of(hidden.ts[1]).map(&:name).should == ['active 2', 'hidden 2'] }
|
112
|
+
end
|
91
113
|
end
|
92
114
|
|
93
115
|
describe '#history' do
|
@@ -194,21 +216,19 @@ describe ChronoModel::TimeMachine do
|
|
194
216
|
describe 'on records having a :belongs_to relationship' do
|
195
217
|
subject { bar.history_timestamps }
|
196
218
|
|
197
|
-
describe 'returns timestamps of the record
|
198
|
-
its(:size) { should == bar.ts.size }
|
199
|
-
it { should == timestamps_from.call(bar) }
|
219
|
+
describe 'returns timestamps of the record and its associations' do
|
220
|
+
its(:size) { should == foo.ts.size + bar.ts.size }
|
221
|
+
it { should == timestamps_from.call(foo, bar) }
|
200
222
|
end
|
201
223
|
end
|
202
224
|
end
|
203
225
|
|
204
226
|
context do
|
205
|
-
history_attrs = ChronoModel::TimeMachine::HISTORY_ATTRIBUTES
|
206
|
-
|
207
227
|
let!(:history) { foo.history.first }
|
208
228
|
let!(:current) { foo }
|
209
229
|
|
210
|
-
|
211
|
-
|
230
|
+
spec = lambda {|attr|
|
231
|
+
return lambda {|*|
|
212
232
|
describe 'on history records' do
|
213
233
|
subject { history.public_send(attr) }
|
214
234
|
|
@@ -219,23 +239,13 @@ describe ChronoModel::TimeMachine do
|
|
219
239
|
|
220
240
|
describe 'on current records' do
|
221
241
|
subject { current.public_send(attr) }
|
222
|
-
it {
|
223
|
-
end
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
describe '#initialize_dup' do
|
228
|
-
describe 'on history records' do
|
229
|
-
subject { history.dup }
|
230
|
-
|
231
|
-
history_attrs.each do |attr|
|
232
|
-
its(attr) { should be_nil }
|
242
|
+
it { expect { subject }.to raise_error(NoMethodError) }
|
233
243
|
end
|
244
|
+
}
|
245
|
+
}
|
234
246
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
end
|
247
|
+
%w( valid_from valid_to recorded_at as_of_time ).each do |attr|
|
248
|
+
describe ['#', attr].join, &spec.call(attr)
|
239
249
|
end
|
240
250
|
end
|
241
251
|
|
@@ -291,8 +301,8 @@ describe ChronoModel::TimeMachine do
|
|
291
301
|
['bar', 'foo bar', 'bar bar', 'new bar', 'bar 0', 'bar 1']
|
292
302
|
}
|
293
303
|
|
294
|
-
it { Foo.history.map(&:name).should == foo_history }
|
295
|
-
it { Bar.history.map(&:name).should == bar_history }
|
304
|
+
it { Foo.history.all.map(&:name).should == foo_history }
|
305
|
+
it { Bar.history.all.map(&:name).should == bar_history }
|
296
306
|
end
|
297
307
|
end
|
298
308
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chrono_model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-10-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
16
|
-
requirement: &
|
16
|
+
requirement: &70658630 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '3.2'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70658630
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: pg
|
27
|
-
requirement: &
|
27
|
+
requirement: &70658260 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,7 +32,7 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70658260
|
36
36
|
description: Give your models as-of date temporal extensions. Built entirely for PostgreSQL
|
37
37
|
>= 9.0
|
38
38
|
email:
|
@@ -44,6 +44,7 @@ files:
|
|
44
44
|
- .gitignore
|
45
45
|
- .rspec
|
46
46
|
- Gemfile
|
47
|
+
- Gemfile.lock
|
47
48
|
- LICENSE
|
48
49
|
- README.md
|
49
50
|
- README.sql
|