dm-is-temporal 0.0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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