terrestrial 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/.ruby-version +1 -1
  3. data/Gemfile.lock +44 -53
  4. data/README.md +3 -6
  5. data/bin/test +1 -1
  6. data/features/env.rb +12 -2
  7. data/features/example.feature +23 -26
  8. data/lib/terrestrial.rb +31 -0
  9. data/lib/terrestrial/adapters/abstract_adapter.rb +6 -0
  10. data/lib/terrestrial/adapters/memory_adapter.rb +82 -6
  11. data/lib/terrestrial/adapters/sequel_postgres_adapter.rb +191 -0
  12. data/lib/terrestrial/configurations/conventional_association_configuration.rb +65 -35
  13. data/lib/terrestrial/configurations/conventional_configuration.rb +280 -124
  14. data/lib/terrestrial/configurations/mapping_config_options_proxy.rb +97 -0
  15. data/lib/terrestrial/deleted_record.rb +12 -8
  16. data/lib/terrestrial/dirty_map.rb +17 -9
  17. data/lib/terrestrial/functional_pipeline.rb +64 -0
  18. data/lib/terrestrial/inspection_string.rb +6 -1
  19. data/lib/terrestrial/lazy_object_proxy.rb +1 -0
  20. data/lib/terrestrial/many_to_many_association.rb +34 -20
  21. data/lib/terrestrial/many_to_one_association.rb +11 -3
  22. data/lib/terrestrial/one_to_many_association.rb +9 -0
  23. data/lib/terrestrial/public_conveniencies.rb +65 -82
  24. data/lib/terrestrial/record.rb +106 -0
  25. data/lib/terrestrial/relation_mapping.rb +43 -12
  26. data/lib/terrestrial/relational_store.rb +33 -11
  27. data/lib/terrestrial/upsert_record.rb +54 -0
  28. data/lib/terrestrial/version.rb +1 -1
  29. data/spec/automatic_timestamps_spec.rb +339 -0
  30. data/spec/changes_api_spec.rb +81 -0
  31. data/spec/config_override_spec.rb +28 -19
  32. data/spec/custom_serializers_spec.rb +3 -2
  33. data/spec/database_default_fields_spec.rb +213 -0
  34. data/spec/database_generated_id_spec.rb +291 -0
  35. data/spec/database_owned_fields_and_timestamps_spec.rb +200 -0
  36. data/spec/deletion_spec.rb +1 -1
  37. data/spec/error_handling/factory_error_handling_spec.rb +1 -4
  38. data/spec/error_handling/serialization_error_spec.rb +1 -4
  39. data/spec/error_handling/upsert_error_spec.rb +7 -11
  40. data/spec/graph_persistence_spec.rb +52 -18
  41. data/spec/ordered_association_spec.rb +10 -12
  42. data/spec/predefined_queries_spec.rb +14 -12
  43. data/spec/readme_examples_spec.rb +1 -1
  44. data/spec/sequel_query_efficiency_spec.rb +19 -16
  45. data/spec/spec_helper.rb +6 -1
  46. data/spec/support/blog_schema.rb +7 -3
  47. data/spec/support/object_graph_setup.rb +30 -39
  48. data/spec/support/object_store_setup.rb +16 -196
  49. data/spec/support/seed_data_setup.rb +15 -149
  50. data/spec/support/seed_records.rb +141 -0
  51. data/spec/support/sequel_test_support.rb +46 -13
  52. data/spec/terrestrial/abstract_record_spec.rb +138 -106
  53. data/spec/terrestrial/adapters/sequel_postgres_adapter_spec.rb +138 -0
  54. data/spec/terrestrial/deleted_record_spec.rb +0 -27
  55. data/spec/terrestrial/dirty_map_spec.rb +52 -77
  56. data/spec/terrestrial/functional_pipeline_spec.rb +153 -0
  57. data/spec/terrestrial/inspection_string_spec.rb +61 -0
  58. data/spec/terrestrial/upsert_record_spec.rb +29 -0
  59. data/terrestrial.gemspec +7 -8
  60. metadata +43 -40
  61. data/MissingFeatures.md +0 -64
  62. data/lib/terrestrial/abstract_record.rb +0 -99
  63. data/lib/terrestrial/association_loaders.rb +0 -52
  64. data/lib/terrestrial/upserted_record.rb +0 -15
  65. data/spec/terrestrial/public_conveniencies_spec.rb +0 -63
  66. data/spec/terrestrial/upserted_record_spec.rb +0 -59
@@ -0,0 +1,106 @@
1
+ require "forwardable"
2
+ require "set"
3
+
4
+ module Terrestrial
5
+ class Record
6
+ extend Forwardable
7
+
8
+ def initialize(mapping, attributes)
9
+ @mapping = mapping
10
+ @attributes = attributes
11
+ end
12
+
13
+ attr_reader :mapping, :attributes
14
+ def_delegators :to_h, :fetch
15
+
16
+ def namespace
17
+ mapping.namespace
18
+ end
19
+
20
+ def if_upsert(&block)
21
+ self
22
+ end
23
+
24
+ def if_delete(&block)
25
+ self
26
+ end
27
+
28
+ def updatable?
29
+ updatable_attributes.any?
30
+ end
31
+
32
+ def updatable_attributes
33
+ attributes.reject { |k, _v| non_updatable_fields.include?(k) }
34
+ end
35
+
36
+ def keys
37
+ attributes.keys
38
+ end
39
+
40
+ def identity_values
41
+ identity.values
42
+ end
43
+
44
+ def identity
45
+ attributes.select { |k,_v| identity_fields.include?(k) }
46
+ end
47
+
48
+ def identity_fields
49
+ mapping.primary_key
50
+ end
51
+
52
+ def merge(more_attributes)
53
+ new_with_attributes(attributes.merge(more_attributes))
54
+ end
55
+
56
+ def merge!(more_attributes)
57
+ attributes.merge!(more_attributes)
58
+ end
59
+
60
+ def reject(&block)
61
+ new_with_attributes(updatable_attributes.reject(&block).merge(identity))
62
+ end
63
+
64
+ def to_h
65
+ attributes.to_h
66
+ end
67
+
68
+ def empty?
69
+ updatable_attributes.empty?
70
+ end
71
+
72
+ def subset?(other_record)
73
+ mapping == other_record.mapping &&
74
+ to_set.subset?(other_record.to_set)
75
+ end
76
+
77
+ def deep_clone
78
+ new_with_attributes(Marshal.load(Marshal.dump(attributes)))
79
+ end
80
+
81
+ def ==(other)
82
+ other.is_a?(self.class) &&
83
+ [other.mapping, other.attributes] == [mapping, attributes]
84
+ end
85
+
86
+ protected
87
+
88
+ def to_set
89
+ Set.new(attributes.to_a)
90
+ end
91
+
92
+ private
93
+
94
+ def non_updatable_fields
95
+ identity_fields + mapping.database_owned_fields + nil_fields_expecting_default_value
96
+ end
97
+
98
+ def nil_fields_expecting_default_value
99
+ mapping.database_default_fields.select { |k| attributes[k].nil? }
100
+ end
101
+
102
+ def new_with_attributes(new_attributes)
103
+ self.class.new(mapping, new_attributes)
104
+ end
105
+ end
106
+ end
@@ -1,29 +1,38 @@
1
1
  require "terrestrial/error"
2
- require "terrestrial/upserted_record"
2
+ require "terrestrial/upsert_record"
3
3
  require "terrestrial/deleted_record"
4
4
 
5
5
  module Terrestrial
6
6
  class RelationMapping
7
- def initialize(name:, namespace:, fields:, primary_key:, factory:, serializer:, associations:, subsets:)
7
+ def initialize(name:, namespace:, fields:, database_owned_fields:, database_default_fields:, primary_key:, factory:, serializer:, associations:, subsets:, observers:)
8
8
  @name = name
9
9
  @namespace = namespace
10
10
  @fields = fields
11
+ @database_owned_fields = database_owned_fields
12
+ @database_default_fields = database_default_fields
11
13
  @primary_key = primary_key
12
14
  @factory = factory
13
15
  @serializer = serializer
14
16
  @associations = associations
15
17
  @subsets = subsets
18
+ @observers = observers
19
+
20
+ @incoming_foreign_keys = []
16
21
  end
17
22
 
18
- attr_reader :name, :namespace, :fields, :primary_key, :factory, :serializer, :associations, :subsets
19
- private :factory, :serializer
23
+ attr_reader :name, :namespace, :fields, :database_owned_fields, :database_default_fields, :primary_key, :factory, :serializer, :associations, :subsets, :created_at_field, :updated_at_field, :observers
24
+ private :factory, :serializer, :observers
20
25
 
21
26
  def add_association(name, new_association)
22
27
  @associations = associations.merge(name => new_association)
23
28
  end
24
29
 
30
+ def register_foreign_key(fk)
31
+ @incoming_foreign_keys += fk
32
+ end
33
+
25
34
  def load(record)
26
- factory.call(record)
35
+ factory.call(reject_non_factory_fields(record))
27
36
  rescue => e
28
37
  raise LoadError.new(namespace, factory, record, e)
29
38
  end
@@ -31,8 +40,11 @@ module Terrestrial
31
40
  def serialize(object, depth, foreign_keys = {})
32
41
  object_attributes = serializer.call(object)
33
42
 
43
+ record = upsertable_record(object, object_attributes, depth, foreign_keys)
44
+ observers.each { |o| o.post_serialize(self, object, record) }
45
+
34
46
  [
35
- record(object_attributes, depth, foreign_keys),
47
+ record,
36
48
  extract_associations(object_attributes)
37
49
  ]
38
50
  rescue => e
@@ -45,12 +57,20 @@ module Terrestrial
45
57
  [deleted_record(object_attributes, depth)]
46
58
  end
47
59
 
60
+ def post_save(object, record, new_attributes)
61
+ new_record = upsertable_record(object, new_attributes, 0, {})
62
+
63
+ observers.each { |o| o.post_save(self, object, record, new_record) }
64
+
65
+ record.merge!(new_attributes)
66
+ end
67
+
48
68
  private
49
69
 
50
- def record(attributes, depth, foreign_keys)
51
- UpsertedRecord.new(
52
- namespace,
53
- primary_key,
70
+ def upsertable_record(object, attributes, depth, foreign_keys)
71
+ UpsertRecord.new(
72
+ self,
73
+ object,
54
74
  select_mapped_fields(attributes).merge(foreign_keys),
55
75
  depth,
56
76
  )
@@ -58,8 +78,7 @@ module Terrestrial
58
78
 
59
79
  def deleted_record(attributes, depth)
60
80
  DeletedRecord.new(
61
- namespace,
62
- primary_key,
81
+ self,
63
82
  attributes,
64
83
  depth,
65
84
  )
@@ -76,5 +95,17 @@ module Terrestrial
76
95
  def select_mapped_fields(attributes)
77
96
  attributes.select { |name, _value| fields.include?(name) }
78
97
  end
98
+
99
+ def reject_non_factory_fields(attributes)
100
+ attributes.reject { |name, _value| (@incoming_foreign_keys + local_foreign_keys).include?(name) }
101
+ end
102
+
103
+ def factory_fields
104
+ @factory_fields ||= fields - (local_foreign_keys + @incoming_foreign_keys)
105
+ end
106
+
107
+ def local_foreign_keys
108
+ @local_foreign_keys ||= associations.values.flat_map(&:local_foreign_keys)
109
+ end
79
110
  end
80
111
  end
@@ -7,7 +7,7 @@ module Terrestrial
7
7
  include Enumerable
8
8
  include InspectionString
9
9
 
10
- def initialize(mappings:, mapping_name:, datastore:, dataset:, load_pipeline:, dump_pipeline:)
10
+ def initialize(mappings:, mapping_name:, datastore:, load_pipeline:, dump_pipeline:, dataset: nil)
11
11
  @mappings = mappings
12
12
  @mapping_name = mapping_name
13
13
  @datastore = datastore
@@ -17,17 +17,33 @@ module Terrestrial
17
17
  @eager_data = {}
18
18
  end
19
19
 
20
- attr_reader :mappings, :mapping_name, :datastore, :dataset, :load_pipeline, :dump_pipeline
21
- private :mappings, :mapping_name, :datastore, :dataset, :load_pipeline, :dump_pipeline
20
+ attr_reader :mappings, :mapping_name, :datastore, :load_pipeline, :dump_pipeline
21
+ private :mappings, :mapping_name, :datastore, :load_pipeline, :dump_pipeline
22
22
 
23
23
  def save(graph)
24
- record_dump = graph_serializer.call(mapping_name, graph)
24
+ record_dump = serialize_graph(graph)
25
25
 
26
26
  dump_pipeline.call(record_dump)
27
27
 
28
28
  self
29
29
  end
30
30
 
31
+ def changes_sql(graph)
32
+ changes(graph).map { |record|
33
+ datastore.changes_sql(record)
34
+ }
35
+ end
36
+
37
+ def changes(graph)
38
+ changes, _ = dump_pipeline
39
+ .take_until(:remove_unchanged_fields)
40
+ .call(
41
+ serialize_graph(graph)
42
+ )
43
+
44
+ changes
45
+ end
46
+
31
47
  def all
32
48
  self
33
49
  end
@@ -58,22 +74,20 @@ module Terrestrial
58
74
  self
59
75
  end
60
76
 
61
- def delete(object, cascade: false)
77
+ def delete(object)
62
78
  dump_pipeline.call(
63
- graph_serializer.call(mapping_name, object)
79
+ serialize_graph(object)
64
80
  .select { |record| record.depth == 0 }
65
81
  .reverse
66
82
  .take(1)
67
- .map { |record|
68
- DeletedRecord.new(record.namespace, record.identity)
69
- }
83
+ .map { |record| DeletedRecord.new(mapping, record.attributes, 0) }
70
84
  )
71
85
  end
72
86
 
73
87
  private
74
88
 
75
- def mapping
76
- mappings.fetch(mapping_name)
89
+ def serialize_graph(graph)
90
+ graph_serializer.call(mapping_name, graph)
77
91
  end
78
92
 
79
93
  def eager_load_associations(mapping, parent_dataset, association_name_map)
@@ -136,6 +150,14 @@ module Terrestrial
136
150
  )
137
151
  end
138
152
 
153
+ def dataset
154
+ @dataset ||= datastore[mapping.namespace]
155
+ end
156
+
157
+ def mapping
158
+ mappings.fetch(mapping_name)
159
+ end
160
+
139
161
  def inspectable_properties
140
162
  [
141
163
  :mapping_name,
@@ -0,0 +1,54 @@
1
+ require "terrestrial/record"
2
+
3
+ module Terrestrial
4
+ class UpsertRecord < Record
5
+ def initialize(mapping, object, attributes, depth)
6
+ @mapping = mapping
7
+ @object = object
8
+ @attributes = attributes
9
+ @depth = depth
10
+ end
11
+
12
+ attr_reader :mapping, :object, :attributes, :depth
13
+
14
+ def id?
15
+ identity_values.reject(&:nil?).any?
16
+ end
17
+
18
+ def set_id(id)
19
+ raise "Cannot use #set_id with composite key" if identity_fields.length > 1
20
+ merge!(identity_fields[0] => id)
21
+ end
22
+
23
+ def get(name)
24
+ fetch(name)
25
+ end
26
+
27
+ def set(name, value)
28
+ merge!(name => value)
29
+ end
30
+
31
+ def if_upsert(&block)
32
+ block.call(self)
33
+ self
34
+ end
35
+
36
+ def on_upsert(new_attributes)
37
+ mapping.post_save(object, self, new_attributes)
38
+ end
39
+
40
+ def insertable
41
+ to_h.reject { |k, v| v.nil? && identity_fields.include?(k) }
42
+ end
43
+
44
+ def include?(field_name)
45
+ keys.include?(field_name)
46
+ end
47
+
48
+ private
49
+
50
+ def new_with_attributes(new_attributes)
51
+ self.class.new(mapping, object, new_attributes, depth)
52
+ end
53
+ end
54
+ end
@@ -1,3 +1,3 @@
1
1
  module Terrestrial
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -0,0 +1,339 @@
1
+ require "spec_helper"
2
+
3
+ require "support/have_persisted_matcher"
4
+ require "support/object_store_setup"
5
+ require "support/seed_data_setup"
6
+
7
+ RSpec.describe "Automatic timestamps", backend: "sequel" do
8
+ include_context "object store setup"
9
+
10
+ before(:all) do
11
+ create_db_timestamp_tables
12
+ end
13
+
14
+ after(:all) do
15
+ drop_db_timestamp_tables
16
+ end
17
+
18
+ before(:each) do
19
+ clean_db_timestamp_tables
20
+ end
21
+
22
+ let(:user_store) {
23
+ object_store[:users]
24
+ }
25
+
26
+ let(:object_store) {
27
+ Terrestrial.object_store(config: with_auto_timestamps_config)
28
+ }
29
+
30
+ let(:user_with_post) {
31
+ User.new(
32
+ id: "users/1",
33
+ first_name: "Hansel",
34
+ last_name: "Trickett",
35
+ email: "hansel@tricketts.org",
36
+ posts: [post],
37
+ )
38
+ }
39
+
40
+ let(:post) {
41
+ Post.new(
42
+ id: "posts/1",
43
+ author: nil,
44
+ subject: "Biscuits",
45
+ body: "I like them",
46
+ comments: [],
47
+ categories: [],
48
+ updated_at: nil,
49
+ created_at: nil,
50
+ )
51
+ }
52
+
53
+ let(:clock) {
54
+ StaticClock.new(Time.parse("2020-04-20T17:00:00 UTC"))
55
+ }
56
+
57
+ let(:with_auto_timestamps_config) {
58
+ Terrestrial.config(datastore, clock: clock)
59
+ .setup_mapping(:users) { |users|
60
+ users.has_many(:posts, foreign_key: :author_id)
61
+ }
62
+ .setup_mapping(:posts) { |posts|
63
+ posts.relation_name(:timestamped_posts)
64
+ posts.created_at_timestamp
65
+ posts.updated_at_timestamp
66
+ }
67
+ }
68
+
69
+ context "new objects" do
70
+ it "adds the current time to the timestamp fields" do
71
+ expected_timestamp = clock.now.utc
72
+
73
+ user_store.save(user_with_post)
74
+
75
+ expect(datastore).to have_persisted(
76
+ :timestamped_posts,
77
+ hash_including(
78
+ created_at: expected_timestamp,
79
+ updated_at: expected_timestamp,
80
+ )
81
+ )
82
+ end
83
+
84
+ it "updates the objects with the new timestamp values" do
85
+ expect(post).to receive(:created_at=).with(clock.now)
86
+ expect(post).to receive(:updated_at=).with(clock.now)
87
+
88
+ user_store.save(user_with_post)
89
+ end
90
+ end
91
+
92
+ context "after an initial successful save of the object graph" do
93
+ before do
94
+ user_store.save(user_with_post)
95
+ end
96
+
97
+ context "if the clock has not yet advanced" do
98
+ context "when saving again without modifications" do
99
+ it "does not perform any more database writes" do
100
+ expect {
101
+ user_store.save(user_with_post)
102
+ }.not_to change { query_counter.write_count }
103
+ end
104
+
105
+ it "does not produce any change records" do
106
+ expect(user_store.changes(user_with_post)).to be_empty
107
+ end
108
+ end
109
+ end
110
+
111
+ context "when saving modifications and the clock has advanced" do
112
+ before do
113
+ @created_at_time = clock.now
114
+ clock.tick
115
+ end
116
+ let(:created_at_time) { @created_at_time }
117
+
118
+ it "persists the updated_at field at the current time" do
119
+ current_time = clock.now
120
+ post.body = post.body + " edited"
121
+
122
+ user_store.save(user_with_post)
123
+
124
+ expect(datastore).to have_persisted(
125
+ :timestamped_posts,
126
+ hash_including(
127
+ id: post.id,
128
+ body: post.body,
129
+ updated_at: current_time,
130
+ )
131
+ )
132
+ end
133
+
134
+ it "updates the object's updated_at field to the current time" do
135
+ current_time = clock.now
136
+ post.body = post.body + " edited"
137
+
138
+ expect(post).to receive(:updated_at=).with(current_time)
139
+
140
+ user_store.save(user_with_post)
141
+ end
142
+
143
+ it "does not change the created_at time" do
144
+ post.body = post.body + " edited"
145
+
146
+ user_store.save(user_with_post)
147
+
148
+ expect(datastore).to have_persisted(
149
+ :timestamped_posts,
150
+ hash_including(
151
+ id: post.id,
152
+ created_at: created_at_time,
153
+ )
154
+ )
155
+ end
156
+ end
157
+ end
158
+
159
+ context "user modifies a the created_at field" do
160
+ it "persists the user's value" do
161
+ party_time = Time.parse("1999-01-01t00:00:00 utc")
162
+ post.created_at = party_time
163
+ user_store.save(user_with_post)
164
+
165
+ expect(datastore).to have_persisted(
166
+ :timestamped_posts,
167
+ hash_including(
168
+ id: post.id,
169
+ created_at: party_time,
170
+ )
171
+ )
172
+ end
173
+ end
174
+
175
+ context "with user-defined timestamp callbacks" do
176
+ before do
177
+ post = user_with_post.posts.first
178
+ change_objects_timestamp_setter_methods(post)
179
+ end
180
+
181
+ let(:with_auto_timestamps_config) {
182
+ Terrestrial.config(datastore, clock: clock)
183
+ .setup_mapping(:users) { |users|
184
+ users.has_many(:posts, foreign_key: :author_id)
185
+ }
186
+ .setup_mapping(:posts) { |posts|
187
+ posts.relation_name(:timestamped_posts)
188
+ posts.created_at_timestamp { |object, timestamp|
189
+ object.unconventional_created_at = timestamp
190
+ }
191
+ posts.updated_at_timestamp { |object, timestamp|
192
+ object.unconventional_updated_at = timestamp
193
+ }
194
+ }
195
+ }
196
+
197
+ it "sets the timestamps via the callbacks" do
198
+ post = user_with_post.posts.first
199
+
200
+ user_store.save(user_with_post)
201
+
202
+ expect(post.created_at).to eq(clock.now)
203
+ expect(post.updated_at).to eq(clock.now)
204
+ end
205
+
206
+ xcontext "if there's an error in the callback" do
207
+ before do
208
+ post = user_with_post.posts.first
209
+
210
+ def post.unconventional_updated_at=(time)
211
+ raise "Original error message"
212
+ end
213
+ end
214
+
215
+ it "is caught, wrapped and re-raised" do
216
+ expect {
217
+ user_store.save(user_with_post)
218
+ }.to raise_error(
219
+ "Error running user-defined setter function defined in Terrestrial mapping lib/spec/automatic_timestamps_spec.rb:183.\n" +
220
+ "Got Error: Original error message"
221
+ )
222
+ end
223
+
224
+ it "raises an error which has a backtrace pointing to where the callback is invoked" do
225
+ begin
226
+ user_store.save(user_with_post)
227
+ rescue => e
228
+ ensure
229
+ unless e
230
+ raise "Failed to intentionally raise error in code under test"
231
+ end
232
+
233
+ puts filtered_backtrace = filter_library_code_from_backtrace(e.backtrace)
234
+
235
+ actual_setter_location = /#{__FILE__}:[0-9]+:in .unconventional_created_at=/
236
+
237
+ expected_files_and_methods = [
238
+ actual_setter_location,
239
+ /time_stamp_observer\.rb:[0-9]+:in .post_save/,
240
+ /relation_mapping\.rb:[0-9]+:in .post_save/,
241
+ /upsert_record\.rb:[0-9]+:in .on_upsert/,
242
+ /upsert_record\.rb:[0-9]+:in .if_upsert/,
243
+ ]
244
+
245
+ aggregate_failures do
246
+ expected_files_and_methods.each do |pattern|
247
+ # TODO: Seems like this should be possible with an RSpec machter for a better failure message
248
+ expect(filtered_backtrace.any? { |l| pattern.match(l) }).to be true
249
+ end
250
+ end
251
+ end
252
+ end
253
+
254
+ def filter_library_code_from_backtrace(backtrace)
255
+ backtrace
256
+ .reject { |l| l.include?("lib/rspec") }
257
+ .reject { |l| l.include?("lib/bundler") }
258
+ .reject { |l| l.include?("lib/sequel") }
259
+ end
260
+ end
261
+
262
+ def change_objects_timestamp_setter_methods(post)
263
+ def post.created_at=(_)
264
+ raise "Should not be called"
265
+ end
266
+ def post.updated_at=(_)
267
+ raise "Should not be called"
268
+ end
269
+ def post.unconventional_created_at=(time)
270
+ @created_at = time
271
+ end
272
+ def post.unconventional_updated_at=(time)
273
+ @updated_at = time
274
+ end
275
+ end
276
+ end
277
+
278
+ def schema
279
+ {
280
+ :tables => {
281
+ :timestamped_posts => [
282
+ {
283
+ :name => :id,
284
+ :type => String,
285
+ :options => {
286
+ :primary_key => true,
287
+ }
288
+ },
289
+ {
290
+ :name => :subject,
291
+ :type => String,
292
+ },
293
+ {
294
+ :name => :body,
295
+ :type => String,
296
+ },
297
+ {
298
+ :name => :author_id,
299
+ :type => String,
300
+ },
301
+ {
302
+ :name => :created_at,
303
+ :type => DateTime,
304
+ },
305
+ {
306
+ :name => :updated_at,
307
+ :type => DateTime,
308
+ },
309
+ ],
310
+ },
311
+ }
312
+ end
313
+
314
+ def create_db_timestamp_tables
315
+ Terrestrial::SequelTestSupport.create_tables(schema.fetch(:tables))
316
+ end
317
+
318
+ def drop_db_timestamp_tables
319
+ Terrestrial::SequelTestSupport.drop_tables(schema.fetch(:tables).keys)
320
+ end
321
+
322
+ def clean_db_timestamp_tables
323
+ Terrestrial::SequelTestSupport.clean_tables(schema.fetch(:tables).keys)
324
+ end
325
+
326
+ class StaticClock
327
+ def initialize(time)
328
+ @time = time
329
+ end
330
+
331
+ def now
332
+ @time
333
+ end
334
+
335
+ def tick
336
+ @time += 1
337
+ end
338
+ end
339
+ end