dm-is-temporal 0.0.1 → 0.2.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/README.md CHANGED
@@ -6,12 +6,13 @@ DataMapper plugin implementing temporal patterns on DataMapper models.
6
6
  These patterns are based on research by Martin Fowler, Richard Snodgrass and others. For more information follow these links:
7
7
 
8
8
  + [Temporal Patterns](http://martinfowler.com/eaaDev/timeNarrative.html)
9
-
10
9
  + [Developing Time-Oriented Database Applications in SQL](http://www.cs.arizona.edu/people/rts/publications.html)
11
10
 
12
11
  Examples
13
12
  ---------
14
13
 
14
+ So lets assume you have a simple class. The plugin will automatically create some auxillary tables when you auto_migrate.
15
+
15
16
  require 'rubygems'
16
17
  require 'dm-core'
17
18
  require 'dm-migrations'
@@ -32,38 +33,114 @@ Examples
32
33
  end
33
34
 
34
35
  DataMapper.auto_migrate!
35
-
36
- m = MyModel.create(:name => 'start', :foo => 1)
37
-
38
- m.foo
39
- #= 1
40
- m.name
41
- #=> 'start'
42
-
43
- m.foo = 2
44
-
45
- m.name = 'hello'
46
- m.name
47
- #=> 'hello'
48
-
49
- m.foo
50
- #= 2
51
-
52
- m.foo = 42
53
-
36
+
37
+ You can create, modify and access it as normal:
38
+
39
+ m = MyModel.create(:name => 'start', :foo => 42)
40
+
41
+ m.bar = 'hello'
42
+ m.foo #= 42
43
+ m.name #=> 'start'
44
+
45
+ Or you can access it at different times (future or past):
46
+
54
47
  old = DateTime.parse('-4712-01-01T00:00:00+00:00')
55
- m.at(old).foo
56
- #=> nil
57
- m.foo
58
- #=> 42
59
-
60
- m.update(:foo => 100, :name => 'goodbye')
61
-
62
- m.foo
63
- #=> 100
64
- m.name
65
- #=> 'goodbye'
66
-
48
+ now = DateTime.now
49
+
50
+ m.at(old).foo #=> nil
51
+ m.at(now).foo #=> 42
52
+
53
+ But it really gets interesting when you modify it `at` different `DateTime`s
54
+
55
+ oldish = DateTime.parse('-4712-01-01T00:00:00+00:00')
56
+ nowish = DateTime.parse('2011-03-01T00:00:00+00:00')
57
+ future = DateTime.parse('4712-01-01T00:00:00+00:00')
58
+
59
+ m.at(oldish).foo = 1
60
+
61
+ m.at(oldish).foo #=> 1
62
+ m.at(nowish).foo #=> 42
63
+ m.at(future).foo #=> 42
64
+
65
+ m.at(nowish).foo = 1024
66
+
67
+ m.at(oldish).foo #=> 1
68
+ m.at(nowish).foo #=> 1024
69
+ m.at(future).foo #=> 1024
70
+
71
+ m.at(future).foo = 3
72
+
73
+ m.at(oldish).foo #=> 1
74
+ m.at(nowish).foo #=> 1024
75
+ m.at(future).foo #=> 3
76
+
77
+ Remember that properties outside of the `is_temporal` block are not versioned. But you can read and write them though the `at(time)` method if you want:
78
+
79
+ m.at(now).name = "finished"
80
+
81
+ m.at(old).name #=> 'finished'
82
+ m.at(now).name #=> 'finished'
83
+
84
+ If you try to set a value at the same time as one you already set, it will overwrite the previous value (like non-temporal models). In future versions of dm-is-temporal you will be able to configure if this works or causes an error.
85
+
86
+ m.at(nowish).foo = 11
87
+ m.at(nowish).foo #=> 11
88
+
89
+ m.at(nowish).foo = 22
90
+ m.at(nowish).foo #=> 22
91
+
92
+ You can also update several properties with the same time in a block (I use `v` for "version" here)
93
+
94
+
95
+ m.at(nowish) do |v|
96
+ v.foo = 42
97
+ v.bar = "cat"
98
+ end
99
+
100
+ m.at(nowish).foo #=> 42
101
+ m.at(nowish).bar #=> 'cat'
102
+
103
+
104
+ How it works
105
+ -------------
106
+ Temporal patterns differ from versioning patterns (such as [dm-is-versioned](https://github.com/datamapper/dm-is-versioned))
107
+ in that every version of the temporal properties is a peer (even the most recent). Accessing temporal properties without the `at(time)` method
108
+ is just a convinience for `at(DateTime.now)`.
109
+
110
+ In addition, you have the ability inject versions at previous time-states (modifying history).
111
+
112
+ When you use the `is_temporal` form, the plugin will dynamically create a temporal version table. In the example above,
113
+ these two tables would be created:
114
+
115
+ # db.my_models table
116
+ ---------------------------------------------
117
+ | id | name |
118
+ ---------------------------------------------
119
+ | 1 | 'start' |
120
+
121
+
122
+ # db.my_model_temporal_versions table
123
+ -----------------------------------------------------------------------
124
+ | id | foo | bar | updated_at | my_model_id |
125
+ -----------------------------------------------------------------------
126
+ | 1 | '42' | null | DateTime | 1 |
127
+ | 2 | '1024' | null | DateTime | 1 |
128
+ | 3 | '1024' | 'hello' | DateTime | 1 |
129
+
130
+ Thanks
131
+ ------
132
+ Thanks to the [dm-is-versioned](https://github.com/datamapper/dm-is-versioned) folks! I based a lot of my infrastructure
133
+ on that project.
134
+
135
+ TODO
136
+ ------
137
+
138
+ + MyClass.update (update all records for a model) doesn't work
139
+ + Temporal Associations
140
+ + Temporal Property pattern (i.e. multiple independent temporal properties per class)
141
+ + Bi-temporality
142
+ + Add a config flag that enables an error to be raised when attempting to rewrite existing versions
143
+
67
144
 
68
145
  Copyright
69
146
  ----------
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ begin
4
4
 
5
5
  Jeweler::Tasks.new do |gem|
6
6
  gem.name = 'dm-is-temporal'
7
- gem.version = '0.0.1'
7
+ gem.version = '0.2.0'
8
8
  gem.summary = 'DataMapper plugin implementing temporal patterns'
9
9
  gem.description = gem.summary
10
10
  gem.email = 'jpkutner [a] gmail [d] com'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.2.0
@@ -41,7 +41,7 @@ module DataMapper
41
41
 
42
42
  version_model.property(:id, DataMapper::Property::Serial)
43
43
  version_model.property(:updated_at, DataMapper::Property::DateTime)
44
- version_model.before(:save) { self.updated_at = DateTime.now }
44
+ version_model.before(:save) { self.updated_at ||= DateTime.now }
45
45
  version_model.instance_eval(&block)
46
46
 
47
47
  const_set(:TemporalVersion, version_model)
@@ -70,11 +70,16 @@ module DataMapper
70
70
  class_eval <<-RUBY
71
71
 
72
72
  def self.update(options={})
73
- raise 'TODO'
73
+ raise "Updating all doesn't work yet"
74
+ # t_opts = __select_temporal_options__(options)
75
+ # raise "Can't update at temporal properties from class level yet." if !t_opts.empty?
76
+ # super.update(options)
74
77
  end
75
78
 
76
- def self.all(*args)
77
- raise 'TODO'
79
+ def self.all(options={})
80
+ t_opts = __select_temporal_options__(options)
81
+ raise "Can't select all by temporal properties from class level yet." if !t_opts.empty?
82
+ super.all(options)
78
83
  end
79
84
 
80
85
  def self.create(options={})
@@ -87,8 +92,13 @@ module DataMapper
87
92
  base
88
93
  end
89
94
 
90
- def at(context)
91
- @__at__ = context
95
+ def at(context=DateTime.now, &block)
96
+ if block_given?
97
+ yield TemporalProxy.new(self, context)
98
+ else
99
+ # this is hokie. need to do better
100
+ @__at__ = context
101
+ end
92
102
  self
93
103
  end
94
104
 
@@ -141,10 +151,15 @@ module DataMapper
141
151
  def create_temporal_writer(name)
142
152
  class_eval <<-RUBY
143
153
  def #{name}=(x)
154
+ at = @__at__
144
155
  t = __version_for_context__
145
- attrs = t.nil? ? {} : t.attributes
146
- t = TemporalVersion.create(attrs.merge(:id => nil, :updated_at => nil))
147
- temporal_versions << t
156
+ if t.nil?
157
+ t = TemporalVersion.create(:updated_at => at)
158
+ temporal_versions << t
159
+ elsif t.updated_at != at
160
+ t = TemporalVersion.create(t.attributes.merge(:id => nil, :updated_at => at))
161
+ temporal_versions << t
162
+ end
148
163
  t.#{name} = x
149
164
  self.save
150
165
  #{name}
@@ -152,6 +167,20 @@ module DataMapper
152
167
  RUBY
153
168
  end
154
169
 
170
+ class TemporalProxy
171
+ # make this a blank slate
172
+ instance_methods.each { |m| undef_method m unless m =~ /^__/ }
173
+
174
+ def initialize(proxied_object, context)
175
+ @proxied_object = proxied_object
176
+ @context = context
177
+ end
178
+
179
+ def method_missing(sym, *args, &block)
180
+ @proxied_object.at(@context).__send__(sym, *args, &block)
181
+ end
182
+ end
183
+
155
184
  module Migration
156
185
 
157
186
  def auto_migrate!(repository_name = self.repository_name)
@@ -4,10 +4,6 @@ require 'spec_helper'
4
4
  class MyModel
5
5
  include DataMapper::Resource
6
6
 
7
- # def self.default_repository_name
8
- # :test
9
- # end
10
-
11
7
  property :id, Serial
12
8
  property :name, String
13
9
 
@@ -47,6 +43,98 @@ describe DataMapper::Is::Temporal do
47
43
  MyModel.create
48
44
  end
49
45
 
46
+ it "version has the right parent" do
47
+ subject.foo = 42
48
+ subject.instance_eval { puts self.temporal_versions[0].my_model_id.should == self.id}
49
+ end
50
+
51
+ it "update all still works for non-temporal properties" do
52
+ pending
53
+ MyModel.update(:name => 'all the same')
54
+
55
+ subject.name.should == 'all the same'
56
+ end
57
+
58
+ it "select all still works for non-temporal properties" do
59
+ subject.name = 'looking for me!'
60
+ subject.save
61
+ all = MyModel.all(:name => 'looking for me!')
62
+ all.size.should == 1
63
+ all[0].name.should == 'looking for me!'
64
+ end
65
+
66
+ context "non-temporal properties" do
67
+ it "should work as normal" do
68
+ subject.name = 'foo'
69
+ subject.name.should == 'foo'
70
+
71
+ subject.name = 'bar'
72
+ subject.name.should == 'bar'
73
+ end
74
+
75
+ it "should work when accessed via at(time)" do
76
+ oldish = DateTime.parse('-4712-01-01T00:00:00+00:00')
77
+ nowish = DateTime.parse('2011-03-01T00:00:00+00:00')
78
+ future = DateTime.parse('4712-01-01T00:00:00+00:00')
79
+
80
+ subject.at(oldish).name = 'foo'
81
+
82
+ subject.at(oldish).name.should == 'foo'
83
+ subject.at(nowish).name.should == 'foo'
84
+ subject.at(future).name.should == 'foo'
85
+
86
+ subject.at(nowish).name = 'bar'
87
+
88
+ subject.at(oldish).name.should == 'bar'
89
+ subject.at(nowish).name.should == 'bar'
90
+ subject.at(future).name.should == 'bar'
91
+
92
+ subject.name = 'rat'
93
+
94
+ subject.at(oldish).name.should == 'rat'
95
+ subject.at(nowish).name.should == 'rat'
96
+ subject.at(future).name.should == 'rat'
97
+ end
98
+ end
99
+
100
+ context "setting temporal properties" do
101
+ it "works" do
102
+ oldish = DateTime.parse('-4712-01-01T00:00:00+00:00')
103
+ nowish = DateTime.parse('2011-03-01T00:00:00+00:00')
104
+ future = DateTime.parse('4712-01-01T00:00:00+00:00')
105
+
106
+ subject.at(oldish).foo = 42
107
+
108
+ subject.at(oldish).foo.should == 42
109
+ subject.at(nowish).foo.should == 42
110
+ subject.at(future).foo.should == 42
111
+
112
+ subject.at(nowish).foo = 1024
113
+
114
+ subject.at(oldish).foo.should == 42
115
+ subject.at(nowish).foo.should == 1024
116
+ subject.at(future).foo.should == 1024
117
+
118
+ subject.at(future).foo = 3
119
+
120
+ subject.at(oldish).foo.should == 42
121
+ subject.at(nowish).foo.should == 1024
122
+ subject.at(future).foo.should == 3
123
+
124
+ subject.instance_eval { puts self.temporal_versions.size.should == 3}
125
+ end
126
+
127
+ it "and rewriting works" do
128
+ now = DateTime.parse('2011-03-01T00:00:00+00:00')
129
+
130
+ subject.at(now).foo = 42
131
+ subject.at(now).foo.should == 42
132
+
133
+ subject.at(now).foo = 1
134
+ subject.at(now).foo.should == 1
135
+ end
136
+ end
137
+
50
138
  context "when at context" do
51
139
  it "returns old values" do
52
140
  subject.foo.should == nil
@@ -58,6 +146,12 @@ describe DataMapper::Is::Temporal do
58
146
 
59
147
  subject.at(old).foo.should == nil
60
148
  subject.at(now).foo.should == 42
149
+
150
+ subject.foo = 1024
151
+ subject.foo.should == 1024
152
+
153
+ subject.at(old).foo.should == nil
154
+ subject.at(now).foo.should == 42
61
155
  end
62
156
  end
63
157
 
@@ -67,6 +161,15 @@ describe DataMapper::Is::Temporal do
67
161
  subject.foo = 42
68
162
  subject.foo.should == 42
69
163
  end
164
+
165
+ it "returns 'same' for bar" do
166
+ subject.bar = 'same'
167
+ subject.bar.should == 'same'
168
+ subject.foo.should == nil
169
+ subject.foo = 42
170
+ subject.foo.should == 42
171
+ subject.bar.should == 'same'
172
+ end
70
173
  end
71
174
 
72
175
  context "when bar is 'hello'" do
@@ -166,5 +269,142 @@ describe DataMapper::Is::Temporal do
166
269
  end
167
270
  end
168
271
  end
272
+
273
+ context "when multi setting" do
274
+ it "works with different times" do
275
+ oldish = DateTime.parse('-4712-01-01T00:00:00+00:00')
276
+ nowish = DateTime.parse('2011-03-01T00:00:00+00:00')
277
+ future = DateTime.parse('4712-01-01T00:00:00+00:00')
278
+
279
+ subject.at(oldish) do |s|
280
+ s.foo = 42
281
+ s.bar = "cat"
282
+ end
283
+
284
+ subject.at(oldish).foo.should == 42
285
+ subject.at(nowish).foo.should == 42
286
+ subject.at(future).foo.should == 42
287
+
288
+ subject.at(oldish).bar.should == "cat"
289
+ subject.at(nowish).bar.should == "cat"
290
+ subject.at(future).bar.should == "cat"
291
+
292
+ subject.at(nowish) do |s|
293
+ s.foo = 1024
294
+ s.bar = "dog"
295
+ end
296
+
297
+ subject.at(oldish).foo.should == 42
298
+ subject.at(nowish).foo.should == 1024
299
+ subject.at(future).foo.should == 1024
300
+
301
+ subject.at(oldish).bar.should == "cat"
302
+ subject.at(nowish).bar.should == "dog"
303
+ subject.at(future).bar.should == "dog"
304
+
305
+ subject.at(future) do |s|
306
+ s.foo = 3
307
+ s.bar = "rat"
308
+ end
309
+
310
+ subject.at(oldish).foo.should == 42
311
+ subject.at(nowish).foo.should == 1024
312
+ subject.at(future).foo.should == 3
313
+
314
+ subject.at(oldish).bar.should == "cat"
315
+ subject.at(nowish).bar.should == "dog"
316
+ subject.at(future).bar.should == "rat"
317
+
318
+ subject.instance_eval { puts self.temporal_versions.size.should == 3}
319
+ end
320
+
321
+ it "works with different times and non-temporal properties" do
322
+ oldish = DateTime.parse('-4712-01-01T00:00:00+00:00')
323
+ nowish = DateTime.parse('2011-03-01T00:00:00+00:00')
324
+ future = DateTime.parse('4712-01-01T00:00:00+00:00')
325
+
326
+ subject.at(oldish) do |s|
327
+ s.name = "same"
328
+ s.foo = 42
329
+ s.bar = "cat"
330
+ end
331
+
332
+ subject.at(oldish).foo.should == 42
333
+ subject.at(nowish).foo.should == 42
334
+ subject.at(future).foo.should == 42
335
+
336
+ subject.at(oldish).bar.should == "cat"
337
+ subject.at(nowish).bar.should == "cat"
338
+ subject.at(future).bar.should == "cat"
339
+
340
+ subject.at(oldish).name.should == "same"
341
+ subject.at(nowish).name.should == "same"
342
+ subject.at(future).name.should == "same"
343
+
344
+ subject.at(nowish) do |s|
345
+ s.name = "every"
346
+ s.foo = 1024
347
+ s.bar = "dog"
348
+ end
349
+
350
+ subject.at(oldish).foo.should == 42
351
+ subject.at(nowish).foo.should == 1024
352
+ subject.at(future).foo.should == 1024
353
+
354
+ subject.at(oldish).bar.should == "cat"
355
+ subject.at(nowish).bar.should == "dog"
356
+ subject.at(future).bar.should == "dog"
357
+
358
+ subject.at(oldish).name.should == "every"
359
+ subject.at(nowish).name.should == "every"
360
+ subject.at(future).name.should == "every"
361
+
362
+ subject.at(future) do |s|
363
+ s.name = "time"
364
+ s.foo = 3
365
+ s.bar = "rat"
366
+ end
367
+
368
+ subject.at(oldish).foo.should == 42
369
+ subject.at(nowish).foo.should == 1024
370
+ subject.at(future).foo.should == 3
371
+
372
+ subject.at(oldish).bar.should == "cat"
373
+ subject.at(nowish).bar.should == "dog"
374
+ subject.at(future).bar.should == "rat"
375
+
376
+ subject.at(oldish).name.should == "time"
377
+ subject.at(nowish).name.should == "time"
378
+ subject.at(future).name.should == "time"
379
+
380
+ subject.instance_eval { puts self.temporal_versions.size.should == 3}
381
+ end
382
+
383
+ it "works with no time (i.e. now)" do
384
+ subject.at do |s|
385
+ s.foo = 42
386
+ s.bar = "cat"
387
+ end
388
+
389
+ subject.foo.should == 42
390
+ subject.bar.should == "cat"
391
+
392
+ subject.instance_eval { puts self.temporal_versions.size.should == 1}
393
+ end
394
+
395
+ it "works with no time (i.e. now)" do
396
+ subject.at do |s|
397
+ s.name = "foobar"
398
+ s.foo = 42
399
+ s.bar = "cat"
400
+ end
401
+
402
+ subject.foo.should == 42
403
+ subject.bar.should == "cat"
404
+ subject.name.should == "foobar"
405
+
406
+ subject.instance_eval { puts self.temporal_versions.size.should == 1}
407
+ end
408
+ end
169
409
  end
170
410
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
+ - 2
7
8
  - 0
8
- - 1
9
- version: 0.0.1
9
+ version: 0.2.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Joe Kutner
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-03-08 00:00:00 -06:00
17
+ date: 2011-03-22 00:00:00 -05:00
18
18
  default_executable:
19
19
  dependencies: []
20
20
 
@@ -28,7 +28,6 @@ extra_rdoc_files:
28
28
  - LICENSE.txt
29
29
  - README.md
30
30
  files:
31
- - Gemfile
32
31
  - LICENSE.txt
33
32
  - README.md
34
33
  - Rakefile
data/Gemfile DELETED
File without changes