chrono_model 0.3.1 → 0.4.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.
- data/Gemfile.lock +1 -1
- data/README.md +4 -0
- data/lib/chrono_model/adapter.rb +59 -8
- data/lib/chrono_model/railtie.rb +12 -8
- data/lib/chrono_model/time_machine.rb +4 -0
- data/lib/chrono_model/version.rb +1 -1
- data/spec/time_machine_spec.rb +50 -0
- metadata +6 -6
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -181,6 +181,10 @@ them in your output, use `rake VERBOSE=true`.
|
|
181
181
|
|
182
182
|
* The schema dumper is WAY TOO hacky.
|
183
183
|
|
184
|
+
* Savepoints are disabled, because there is
|
185
|
+
[currently](http://archives.postgresql.org/pgsql-hackers/2012-08/msg01094.php)
|
186
|
+
no way to identify a subtransaction belonging to the current transaction.
|
187
|
+
|
184
188
|
|
185
189
|
## Contributing
|
186
190
|
|
data/lib/chrono_model/adapter.rb
CHANGED
@@ -309,6 +309,17 @@ module ChronoModel
|
|
309
309
|
end
|
310
310
|
end
|
311
311
|
|
312
|
+
# Disable savepoints support, as they break history keeping.
|
313
|
+
# http://archives.postgresql.org/pgsql-hackers/2012-08/msg01094.php
|
314
|
+
#
|
315
|
+
def supports_savepoints?
|
316
|
+
false
|
317
|
+
end
|
318
|
+
|
319
|
+
def create_savepoint; end
|
320
|
+
def rollback_to_savepoint; end
|
321
|
+
def release_savepoint; end
|
322
|
+
|
312
323
|
private
|
313
324
|
# Create the history table in the history schema
|
314
325
|
def chrono_create_history_for(table)
|
@@ -364,7 +375,8 @@ module ChronoModel
|
|
364
375
|
|
365
376
|
# SELECT - return only current data
|
366
377
|
#
|
367
|
-
execute "
|
378
|
+
execute "DROP VIEW #{table}" if table_exists? table
|
379
|
+
execute "CREATE VIEW #{table} AS SELECT *, xmin AS __xid FROM ONLY #{current}"
|
368
380
|
|
369
381
|
columns = columns(table).map(&:name)
|
370
382
|
sequence = serial_sequence(current, pk) # For INSERT
|
@@ -378,13 +390,13 @@ module ChronoModel
|
|
378
390
|
#
|
379
391
|
if sequence.present?
|
380
392
|
execute <<-SQL
|
381
|
-
CREATE
|
393
|
+
CREATE RULE #{table}_ins AS ON INSERT TO #{table} DO INSTEAD (
|
382
394
|
|
383
395
|
INSERT INTO #{current} ( #{fields} ) VALUES ( #{values} );
|
384
396
|
|
385
397
|
INSERT INTO #{history} ( #{pk}, #{fields}, valid_from )
|
386
398
|
VALUES ( currval('#{sequence}'), #{values}, timezone('UTC', now()) )
|
387
|
-
RETURNING #{pk}, #{fields}
|
399
|
+
RETURNING #{pk}, #{fields}, xmin
|
388
400
|
)
|
389
401
|
SQL
|
390
402
|
else
|
@@ -392,7 +404,7 @@ module ChronoModel
|
|
392
404
|
values_with_pk = "new.#{pk}, " << values
|
393
405
|
|
394
406
|
execute <<-SQL
|
395
|
-
CREATE
|
407
|
+
CREATE RULE #{table}_ins AS ON INSERT TO #{table} DO INSTEAD (
|
396
408
|
|
397
409
|
INSERT INTO #{current} ( #{fields_with_pk} ) VALUES ( #{values_with_pk} );
|
398
410
|
|
@@ -405,9 +417,27 @@ module ChronoModel
|
|
405
417
|
|
406
418
|
# UPDATE - set the last history entry validity to now, save the current data
|
407
419
|
# in a new history entry and update the temporal table with the new data.
|
420
|
+
|
421
|
+
# If this is the first statement of a transaction, inferred by the last
|
422
|
+
# transaction ID that updated the row, create a new row in the history.
|
423
|
+
#
|
424
|
+
# The current transaction ID is returned by txid_current() as a 64-bit
|
425
|
+
# signed integer, while the last transaction ID that changed a row is
|
426
|
+
# stored into a 32-bit unsigned integer in the __xid column. As XIDs
|
427
|
+
# wrap over time, txid_current() adds an "epoch" counter in the most
|
428
|
+
# significant bits (http://bit.ly/W2Srt7) of the int - thus here we
|
429
|
+
# remove it by and'ing with 2^32-1.
|
430
|
+
#
|
431
|
+
# XID are 32-bit unsigned integers, and by design cannot be casted nor
|
432
|
+
# compared to anything else, adding a CAST or an operator requires
|
433
|
+
# super-user privileges, so here we do a double-cast from varchar to
|
434
|
+
# int8, to finally compare it with the current XID. We're using 64bit
|
435
|
+
# integers as in PG there is no 32-bit unsigned data type.
|
408
436
|
#
|
409
437
|
execute <<-SQL
|
410
|
-
CREATE
|
438
|
+
CREATE RULE #{table}_upd_first AS ON UPDATE TO #{table}
|
439
|
+
WHERE old.__xid::char(10)::int8 <> (txid_current() & (2^32-1)::int8)
|
440
|
+
DO INSTEAD (
|
411
441
|
|
412
442
|
UPDATE #{history} SET valid_to = timezone('UTC', now())
|
413
443
|
WHERE #{pk} = old.#{pk} AND valid_to = '9999-12-31';
|
@@ -420,11 +450,32 @@ module ChronoModel
|
|
420
450
|
)
|
421
451
|
SQL
|
422
452
|
|
423
|
-
#
|
424
|
-
#
|
453
|
+
# Else, update the already present history row with new data. This logic
|
454
|
+
# makes possible to "squash" together changes made in a transaction in a
|
455
|
+
# single history row, assuring timestamps consistency.
|
425
456
|
#
|
426
457
|
execute <<-SQL
|
427
|
-
CREATE
|
458
|
+
CREATE RULE #{table}_upd_next AS ON UPDATE TO #{table} DO INSTEAD (
|
459
|
+
UPDATE #{history} SET #{updates}
|
460
|
+
WHERE #{pk} = old.#{pk} AND valid_from = timezone('UTC', now());
|
461
|
+
|
462
|
+
UPDATE ONLY #{current} SET #{updates}
|
463
|
+
WHERE #{pk} = old.#{pk}
|
464
|
+
)
|
465
|
+
SQL
|
466
|
+
|
467
|
+
# DELETE - save the current data in the history and eventually delete the
|
468
|
+
# data from the temporal table.
|
469
|
+
# The first DELETE is required to remove history for records INSERTed and
|
470
|
+
# DELETEd in the same transaction.
|
471
|
+
#
|
472
|
+
execute <<-SQL
|
473
|
+
CREATE RULE #{table}_del AS ON DELETE TO #{table} DO INSTEAD (
|
474
|
+
|
475
|
+
DELETE FROM #{history}
|
476
|
+
WHERE #{pk} = old.#{pk}
|
477
|
+
AND valid_from = timezone('UTC', now())
|
478
|
+
AND valid_to = '9999-12-31';
|
428
479
|
|
429
480
|
UPDATE #{history} SET valid_to = timezone('UTC', now())
|
430
481
|
WHERE #{pk} = old.#{pk} AND valid_to = '9999-12-31';
|
data/lib/chrono_model/railtie.rb
CHANGED
@@ -1,17 +1,21 @@
|
|
1
1
|
module ChronoModel
|
2
2
|
class Railtie < ::Rails::Railtie
|
3
|
+
initializer :chrono_create_schemas do
|
4
|
+
ActiveRecord::Base.connection.chrono_create_schemas!
|
5
|
+
end
|
6
|
+
|
3
7
|
rake_tasks do
|
4
8
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
namespace :db do
|
10
|
+
namespace :chrono do
|
11
|
+
task :create_schemas do
|
12
|
+
puts 'create schemas'
|
13
|
+
ActiveRecord::Base.connection.chrono_create_schemas!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
12
17
|
|
13
18
|
task 'db:schema:load' => 'db:chrono:create_schemas'
|
14
|
-
task 'db:migrate' => 'db:chrono:create_schemas'
|
15
19
|
end
|
16
20
|
|
17
21
|
class SchemaDumper < ::ActiveRecord::SchemaDumper
|
@@ -137,6 +137,10 @@ module ChronoModel
|
|
137
137
|
self.kind_of? self.class.history
|
138
138
|
end
|
139
139
|
|
140
|
+
def attributes(*)
|
141
|
+
super.tap {|x| x.delete('__xid')}
|
142
|
+
end
|
143
|
+
|
140
144
|
module ClassMethods
|
141
145
|
# Returns an ActiveRecord::Relation on the history of this model as
|
142
146
|
# it was +time+ ago.
|
data/lib/chrono_model/version.rb
CHANGED
data/spec/time_machine_spec.rb
CHANGED
@@ -306,4 +306,54 @@ describe ChronoModel::TimeMachine do
|
|
306
306
|
end
|
307
307
|
end
|
308
308
|
|
309
|
+
# Transactions
|
310
|
+
context 'Within transactions' do
|
311
|
+
context 'multiple updates to an existing record' do
|
312
|
+
let!(:r1) do
|
313
|
+
Foo.create!(:name => 'xact test').tap do |record|
|
314
|
+
Foo.transaction do
|
315
|
+
record.update_attribute 'name', 'lost into oblivion'
|
316
|
+
record.update_attribute 'name', 'does work'
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
it "generate only a single history record" do
|
322
|
+
r1.history.should have(2).entries
|
323
|
+
|
324
|
+
r1.history.first.name.should == 'xact test'
|
325
|
+
r1.history.last.name.should == 'does work'
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
context 'insertion and subsequent update' do
|
330
|
+
let!(:r2) do
|
331
|
+
Foo.transaction do
|
332
|
+
Foo.create!(:name => 'lost into oblivion').tap do |record|
|
333
|
+
record.update_attribute 'name', 'I am Bar'
|
334
|
+
record.update_attribute 'name', 'I am Foo'
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
it 'generates a single history record' do
|
340
|
+
r2.history.should have(1).entry
|
341
|
+
|
342
|
+
r2.history.first.name.should == 'I am Foo'
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
context 'insertion and subsequent deletion' do
|
347
|
+
let!(:r3) do
|
348
|
+
Foo.transaction do
|
349
|
+
Foo.create!(:name => 'it never happened').destroy
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
it 'does not generate any history' do
|
354
|
+
Foo.history.where(:id => r3.id).should be_empty
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
309
359
|
end
|
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.
|
4
|
+
version: 0.4.0
|
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-10-
|
12
|
+
date: 2012-10-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
16
|
-
requirement: &
|
16
|
+
requirement: &79419830 !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: *79419830
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: pg
|
27
|
-
requirement: &
|
27
|
+
requirement: &79493370 !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: *79493370
|
36
36
|
description: Give your models as-of date temporal extensions. Built entirely for PostgreSQL
|
37
37
|
>= 9.0
|
38
38
|
email:
|