sequel_bitemporal 0.4.15 → 0.4.16

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 CHANGED
@@ -1,2 +1,4 @@
1
1
  source "http://rubygems.org"
2
2
  gemspec
3
+
4
+ gem "pry"
@@ -18,7 +18,7 @@ module Sequel
18
18
  end
19
19
 
20
20
  THREAD_NOW_KEY = :sequel_plugins_bitemporal_now
21
- def self.at(time)
21
+ def self.at(time)
22
22
  previous = Thread.current[THREAD_NOW_KEY]
23
23
  raise ArgumentError, "requires a block" unless block_given?
24
24
  Thread.current[THREAD_NOW_KEY] = time.to_datetime
@@ -31,11 +31,14 @@ module Sequel
31
31
  Thread.current[THREAD_NOW_KEY] || DateTime.now
32
32
  end
33
33
 
34
+ def self.bitemporal_version_columns
35
+ @bitemporal_version_columns ||= [:master_id, :valid_from, :valid_to, :created_at, :expired_at]
36
+ end
37
+
34
38
  def self.configure(master, opts = {})
35
39
  version = opts[:version_class]
36
40
  raise Error, "please specify version class to use for bitemporal plugin" unless version
37
- required = [:master_id, :valid_from, :valid_to, :created_at, :expired_at]
38
- missing = required - version.columns
41
+ missing = bitemporal_version_columns - version.columns
39
42
  raise Error, "bitemporal plugin requires the following missing column#{"s" if missing.size>1} on version class: #{missing.join(", ")}" unless missing.empty?
40
43
  master.instance_eval do
41
44
  @version_class = version
@@ -44,6 +47,7 @@ module Sequel
44
47
  @current_version_alias = "#{base_alias}_current_version".to_sym
45
48
  @audit_class = opts[:audit_class]
46
49
  @audit_updated_by_method = opts[:audit_updated_by_method] || :updated_by_id
50
+ @propagate_per_column = opts.fetch(:propagate_per_column, false)
47
51
  end
48
52
  master.one_to_many :versions, class: version, key: :master_id, graph_alias_base: master.versions_alias
49
53
  master.one_to_one :current_version, class: version, key: :master_id, graph_alias_base: master.current_version_alias, :graph_block=>(proc do |j, lj, js|
@@ -91,7 +95,7 @@ module Sequel
91
95
  end
92
96
  end
93
97
  unless opts[:delegate]==false
94
- (version.columns-required-[:id]).each do |column|
98
+ (version.columns-bitemporal_version_columns-[:id]).each do |column|
95
99
  master.class_eval <<-EOS
96
100
  def #{column}
97
101
  pending_or_current_version.#{column} if pending_or_current_version
@@ -102,6 +106,7 @@ module Sequel
102
106
  end
103
107
  module ClassMethods
104
108
  attr_reader :version_class, :versions_alias, :current_version_alias
109
+ attr_reader :propagate_per_column
105
110
  attr_reader :audit_class, :audit_updated_by_method
106
111
  end
107
112
  module DatasetMethods
@@ -140,8 +145,8 @@ module Sequel
140
145
  end
141
146
 
142
147
  def attributes=(attributes)
148
+ @current_version_values = current_version ? current_version.values : {}
143
149
  if attributes.delete(:partial_update) && !@pending_version && !new? && current_version
144
- @current_version_values = current_version.values if audited?
145
150
  current_attributes = current_version.keys.inject({}) do |hash, key|
146
151
  hash[key] = current_version.send key
147
152
  hash
@@ -149,8 +154,6 @@ module Sequel
149
154
  current_attributes.delete :valid_from
150
155
  current_attributes.delete :valid_to
151
156
  attributes = current_attributes.merge attributes
152
- elsif audited? && !new? && current_version
153
- @current_version_values = current_version.values
154
157
  end
155
158
  attributes.delete :id
156
159
  @pending_version ||= model.version_class.new
@@ -242,8 +245,51 @@ module Sequel
242
245
  versions_dataset.where(id: expired.collect(&:id)).update expired_at: pending_version.created_at
243
246
  end
244
247
 
248
+ def propagate_changes_to_future_versions
249
+ return true unless self.class.propagate_per_column
250
+ lock!
251
+ futures = versions_dataset.where expired_at: nil
252
+ futures = futures.exclude "valid_from=valid_to"
253
+ futures = futures.exclude "valid_to<=?", pending_version.valid_from
254
+ futures = futures.where "valid_from>?", pending_version.valid_from
255
+ futures = futures.order(:valid_from).all
256
+
257
+ excluded_columns = Sequel::Plugins::Bitemporal.bitemporal_version_columns + [:id]
258
+ to_check_columns = self.class.version_class.columns - excluded_columns
259
+ updated_by = send(self.class.audit_updated_by_method) if audited?
260
+ previous_values = @current_version_values
261
+ current_version_values = pending_version.values
262
+
263
+ futures.each do |future_version|
264
+ attrs = {}
265
+ to_check_columns.each do |col|
266
+ if previous_values[col]==future_version[col] &&
267
+ previous_values[col]!=current_version_values[col]
268
+ attrs[col] = current_version_values[col]
269
+ end
270
+ end
271
+ if attrs.any?
272
+ propagated = save_propagated future_version, attrs
273
+ if !propagated.new? && audited?
274
+ self.class.audit_class.audit(
275
+ self,
276
+ future_version.values,
277
+ propagated.values,
278
+ propagated.valid_from,
279
+ send(self.class.audit_updated_by_method)
280
+ )
281
+ end
282
+ previous_values = future_version.values.dup
283
+ current_version_values = propagated.values
284
+ future_version.this.update :expired_at => Sequel::Plugins::Bitemporal.point_in_time
285
+ else
286
+ break
287
+ end
288
+ end
289
+ end
290
+
245
291
  def save_pending_version
246
- current_values_for_audit = @current_version_values || {} if audited?
292
+ current_values_for_audit = @current_version_values || {}
247
293
  pending_version.valid_to ||= Time.utc 9999
248
294
  success = add_version pending_version
249
295
  if success
@@ -251,10 +297,10 @@ module Sequel
251
297
  self,
252
298
  current_values_for_audit,
253
299
  pending_version.values,
254
- pending_version.valid_from,
300
+ pending_version.valid_from,
255
301
  send(self.class.audit_updated_by_method)
256
302
  ) if audited?
257
- @current_version_values = nil if audited?
303
+ propagate_changes_to_future_versions
258
304
  @pending_version = nil
259
305
  end
260
306
  success
@@ -267,6 +313,16 @@ module Sequel
267
313
  fossil.send :set_values, expired_attributes.merge(attributes)
268
314
  fossil.save validate: false
269
315
  end
316
+
317
+ def save_propagated(version, attributes={})
318
+ propagated = model.version_class.new
319
+ version_attributes = version.values.dup
320
+ version_attributes.delete :id
321
+ version_attributes[:created_at] = Sequel::Plugins::Bitemporal.point_in_time
322
+ propagated.send :set_values, version_attributes.merge(attributes)
323
+ propagated.save validate: false
324
+ propagated
325
+ end
270
326
  end
271
327
  end
272
328
  end
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "sequel_bitemporal"
6
- s.version = "0.4.15"
6
+ s.version = "0.4.16"
7
7
  s.authors = ["Joseph HALTER", "Jonathan TRON"]
8
8
  s.email = ["joseph.halter@thetalentbox.com", "jonathan.tron@thetalentbox.com"]
9
9
  s.homepage = "https://github.com/TalentBox/sequel_bitemporal"
@@ -312,6 +312,37 @@ describe "Sequel::Plugins::Bitemporal" do
312
312
  | Single Standard | 94 | 2009-11-29 | | 2009-11-29 | MAX DATE | true |
313
313
  }
314
314
  end
315
+ it "can propagate changes to future versions per column" do
316
+ propagate_per_column = @master_class.propagate_per_column
317
+ begin
318
+ @master_class.instance_variable_set :@propagate_per_column, true
319
+ master = @master_class.new
320
+ master.update_attributes name: "Single Standard", price: 12, length: nil, width: 1
321
+ initial_today = Date.today
322
+ Timecop.freeze initial_today+1 do
323
+ master.update_attributes valid_from: initial_today+4, name: "King Size", price: 15, length: 2, width: 2, partial_update: true
324
+ end
325
+ Timecop.freeze initial_today+2 do
326
+ master.update_attributes valid_from: initial_today+3, length: 1, width: 1, partial_update: true
327
+ end
328
+ Timecop.freeze initial_today+3 do
329
+ master.update_attributes valid_from: initial_today+2, length: 3, width: 4, partial_update: true
330
+ end
331
+ master.should have_versions %Q{
332
+ | name | price | length | width | created_at | expired_at | valid_from | valid_to | current |
333
+ | Single Standard | 12 | | 1 | 2009-11-28 | 2009-11-29 | 2009-11-28 | MAX DATE | true |
334
+ | Single Standard | 12 | | 1 | 2009-11-29 | 2009-11-30 | 2009-11-28 | 2009-12-02 | |
335
+ | King Size | 15 | 2 | 2 | 2009-11-29 | | 2009-12-02 | MAX DATE | |
336
+ | Single Standard | 12 | | 1 | 2009-11-30 | 2009-12-01 | 2009-11-28 | 2009-12-01 | |
337
+ | Single Standard | 12 | 1 | 1 | 2009-11-30 | 2009-12-01 | 2009-12-01 | 2009-12-02 | |
338
+ | Single Standard | 12 | | 1 | 2009-12-01 | | 2009-11-28 | 2009-11-30 | |
339
+ | Single Standard | 12 | 3 | 4 | 2009-12-01 | | 2009-11-30 | 2009-12-01 | |
340
+ | Single Standard | 12 | 1 | 4 | 2009-12-01 | | 2009-12-01 | 2009-12-02 | |
341
+ }
342
+ ensure
343
+ @master_class.instance_variable_set :@propagate_per_column, propagate_per_column
344
+ end
345
+ end
315
346
  it "allows eager loading with conditions on current version" do
316
347
  master = @master_class.new
317
348
  master.update_attributes name: "Single Standard", price: 98
@@ -532,7 +563,9 @@ end
532
563
  describe "Sequel::Plugins::Bitemporal", "with audit" do
533
564
  include DbHelpers
534
565
  before :all do
535
- @audit_class = Class.new
566
+ @audit_class = Class.new do
567
+ def self.audit(*args); end
568
+ end
536
569
  db_setup audit_class: @audit_class
537
570
  end
538
571
  before do
@@ -556,7 +589,6 @@ describe "Sequel::Plugins::Bitemporal", "with audit" do
556
589
  it "generates a new audit on full update" do
557
590
  master = @master_class.new
558
591
  master.should_receive(:updated_by_id).twice.and_return(updated_by_id = stub)
559
- @audit_class.stub(:audit)
560
592
  master.update_attributes name: "Single Standard", price: 98
561
593
  @audit_class.should_receive(:audit).with(
562
594
  master,
@@ -570,7 +602,6 @@ describe "Sequel::Plugins::Bitemporal", "with audit" do
570
602
  it "generates a new audit on partial update" do
571
603
  master = @master_class.new
572
604
  master.should_receive(:updated_by_id).twice.and_return(updated_by_id = stub)
573
- @audit_class.stub(:audit)
574
605
  master.update_attributes name: "Single Standard", price: 98
575
606
  @audit_class.should_receive(:audit).with(
576
607
  master,
@@ -581,11 +612,55 @@ describe "Sequel::Plugins::Bitemporal", "with audit" do
581
612
  )
582
613
  master.update_attributes partial_update: true, name: "King size", price: 98
583
614
  end
615
+ it "generate a new audit for each future version update when propagating changes" do
616
+ propagate_per_column = @master_class.propagate_per_column
617
+ begin
618
+ @master_class.instance_variable_set :@propagate_per_column, true
619
+ master = @master_class.new
620
+ master.should_receive(:updated_by_id).exactly(9).times.and_return(updated_by_id = stub)
621
+
622
+ master.update_attributes name: "Single Standard", price: 12, length: nil, width: 1
623
+ initial_today = Date.today
624
+ Timecop.freeze initial_today+1 do
625
+ Sequel::Plugins::Bitemporal.at initial_today+4 do
626
+ master.update_attributes valid_from: initial_today+4, name: "King Size", price: 15, length: 2, width: 2, partial_update: true
627
+ end
628
+ end
629
+ Timecop.freeze initial_today+2 do
630
+ Sequel::Plugins::Bitemporal.at initial_today+3 do
631
+ master.update_attributes valid_from: initial_today+3, length: 1, width: 1, partial_update: true
632
+ end
633
+ end
634
+ @audit_class.should_receive(:audit).with(
635
+ master,
636
+ hash_including({name: "Single Standard", price: 12, length: nil, width: 1}),
637
+ hash_including({name: "Single Standard", price: 12, length: 3, width: 4}),
638
+ initial_today+2,
639
+ updated_by_id
640
+ )
641
+ @audit_class.should_receive(:audit).with(
642
+ master,
643
+ hash_including({name: "Single Standard", price: 12, length: 1, width: 1}),
644
+ hash_including({name: "Single Standard", price: 12, length: 1, width: 4}),
645
+ initial_today+3,
646
+ updated_by_id
647
+ )
648
+ Timecop.freeze initial_today+3 do
649
+ Sequel::Plugins::Bitemporal.at initial_today+2 do
650
+ master.update_attributes valid_from: initial_today+2, length: 3, width: 4, partial_update: true
651
+ end
652
+ end
653
+ ensure
654
+ @master_class.instance_variable_set :@propagate_per_column, propagate_per_column
655
+ end
656
+ end
584
657
  end
585
658
  describe "Sequel::Plugins::Bitemporal", "with audit, specifying how to get the updated id" do
586
659
  include DbHelpers
587
660
  before :all do
588
- @audit_class = Class.new
661
+ @audit_class = Class.new do
662
+ def self.audit(*args); end
663
+ end
589
664
  db_setup audit_class: @audit_class, audit_updated_by_method: :author_id
590
665
  end
591
666
  before do
@@ -609,7 +684,6 @@ describe "Sequel::Plugins::Bitemporal", "with audit, specifying how to get the u
609
684
  it "generates a new audit on full update" do
610
685
  master = @master_class.new
611
686
  master.should_receive(:author_id).twice.and_return(updated_by_id = stub)
612
- @audit_class.stub(:audit)
613
687
  master.update_attributes name: "Single Standard", price: 98
614
688
  @audit_class.should_receive(:audit).with(
615
689
  master,
@@ -623,7 +697,6 @@ describe "Sequel::Plugins::Bitemporal", "with audit, specifying how to get the u
623
697
  it "generates a new audit on partial update" do
624
698
  master = @master_class.new
625
699
  master.should_receive(:author_id).twice.and_return(updated_by_id = stub)
626
- @audit_class.stub(:audit)
627
700
  master.update_attributes name: "Single Standard", price: 98
628
701
  @audit_class.should_receive(:audit).with(
629
702
  master,
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "sequel"
2
2
  require "timecop"
3
+ require "pry"
3
4
  DB = Sequel.sqlite
4
- Dir[File.expand_path("../support/*.rb", __FILE__)].each{|f| require f}
5
+ Dir[File.expand_path("../support/*.rb", __FILE__)].each{|f| require f}
data/spec/support/db.rb CHANGED
@@ -19,6 +19,8 @@ module DbHelpers
19
19
  foreign_key :master_id, :rooms
20
20
  String :name
21
21
  Fixnum :price
22
+ Fixnum :length
23
+ Fixnum :width
22
24
  send(use_time ? :Time : :Date, :created_at)
23
25
  send(use_time ? :Time : :Date, :expired_at)
24
26
  send(use_time ? :Time : :Date, :valid_from)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel_bitemporal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.15
4
+ version: 0.4.16
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-07-20 00:00:00.000000000 Z
13
+ date: 2012-07-23 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: sequel
@@ -127,7 +127,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
127
127
  version: '0'
128
128
  segments:
129
129
  - 0
130
- hash: -2816087216667685426
130
+ hash: -3018429499339951723
131
131
  required_rubygems_version: !ruby/object:Gem::Requirement
132
132
  none: false
133
133
  requirements:
@@ -136,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
136
  version: '0'
137
137
  segments:
138
138
  - 0
139
- hash: -2816087216667685426
139
+ hash: -3018429499339951723
140
140
  requirements: []
141
141
  rubyforge_project:
142
142
  rubygems_version: 1.8.24