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,138 @@
1
+ require "spec_helper"
2
+
3
+ require "terrestrial/adapters/sequel_postgres_adapter"
4
+ require "terrestrial/upsert_record"
5
+
6
+ RSpec.describe Terrestrial::Adapters::SequelPostgresAdapter, backend: "sequel" do
7
+
8
+ let(:adapter) { Terrestrial::Adapters::SequelPostgresAdapter.new(datastore) }
9
+
10
+ describe "#tables" do
11
+ it "returns all table names as symbols" do
12
+ expect(adapter.tables).to match_array(
13
+ [:users, :posts, :categories, :comments, :categories_to_posts]
14
+ )
15
+ end
16
+ end
17
+
18
+ describe "#primary_key" do
19
+ context "when the table has a regular primary key" do
20
+ let(:table_name) { :users }
21
+ it "returns the primary key field(s) for a table as an array of symbols" do
22
+ expect(adapter.primary_key(table_name)).to eq([:id])
23
+ end
24
+ end
25
+
26
+ context "when the table has no primary key" do
27
+ let(:table_name) { :categories_to_posts }
28
+
29
+ it "returns an empty array" do
30
+ expect(adapter.primary_key(table_name)).to eq([])
31
+ end
32
+ end
33
+ end
34
+
35
+ describe "#unique_indexes" do
36
+ before(:all) do
37
+ adapter_support.create_tables(schema_with_unique_index.fetch(:tables))
38
+ adapter_support.add_unique_indexes(schema_with_unique_index.fetch(:unique_indexes))
39
+ end
40
+
41
+ after(:all) do
42
+ adapter_support.drop_tables(schema_with_unique_index.fetch(:tables).keys)
43
+ end
44
+
45
+ before(:each) { adapter_support.clean_table(:unique_index_table) }
46
+
47
+ context "when the table has no primary key" do
48
+ let(:table_name) { :unique_index_table }
49
+
50
+ it "returns an array of the indexed fields" do
51
+ expect(adapter.unique_indexes(table_name)).to eq([
52
+ [:field_one, :field_two]
53
+ ])
54
+ end
55
+
56
+ context "when there is no conflicting row" do
57
+ let(:record) {
58
+ create_record(field_one: "1", field_two: "2", text: "initial value")
59
+ }
60
+
61
+ it "upserts resulting in a new row" do
62
+ expect { adapter.upsert(record) }
63
+ .to change { datastore[:unique_index_table].count }
64
+ .by(1)
65
+ end
66
+ end
67
+
68
+ context "when a conflicting row" do
69
+ let(:updated_record) {
70
+ create_record(field_one: "1", field_two: "2", text: "new value")
71
+ }
72
+
73
+ before do
74
+ record = create_record(field_one: "1", field_two: "2", text: "initial value")
75
+ adapter.upsert(record)
76
+ end
77
+
78
+ it "upserts, updating the existing row" do
79
+ expect { adapter.upsert(updated_record) }
80
+ .to change { datastore[:unique_index_table].count }
81
+ .by(0)
82
+
83
+ expect(adapter[:unique_index_table].first.fetch(:text)).to eq("new value")
84
+ end
85
+ end
86
+ end
87
+
88
+ def create_record(values)
89
+ Terrestrial::UpsertRecord.new(
90
+ mapping,
91
+ object,
92
+ values,
93
+ 0,
94
+ )
95
+ end
96
+
97
+ let(:object) { double(:object)
98
+ }
99
+
100
+ let(:mapping) {
101
+ double(
102
+ :mapping,
103
+ namespace: :unique_index_table,
104
+ primary_key: [],
105
+ database_owned_fields: [],
106
+ database_default_fields: [],
107
+ post_save: nil,
108
+ )
109
+ }
110
+
111
+ context "when the has a primary key and no other indexes" do
112
+ let(:table_name) { :users }
113
+
114
+ it "returns an empty array" do
115
+ expect(adapter.unique_indexes(table_name)).to eq([])
116
+ end
117
+ end
118
+
119
+ def adapter_support
120
+ Terrestrial::SequelTestSupport
121
+ end
122
+
123
+ def schema_with_unique_index
124
+ {
125
+ tables: {
126
+ unique_index_table: [
127
+ { name: :field_one, type: String, options: { null: false } },
128
+ { name: :field_two, type: String, options: { null: false } },
129
+ { name: :text, type: String, options: { null: false } },
130
+ ],
131
+ },
132
+ unique_indexes: [
133
+ [:unique_index_table, :field_one, :field_two]
134
+ ],
135
+ }
136
+ end
137
+ end
138
+ end
@@ -29,31 +29,4 @@ RSpec.describe Terrestrial::DeletedRecord do
29
29
  }.to yield_with_args(record)
30
30
  end
31
31
  end
32
-
33
- describe "#==" do
34
- context "with another record that deletes" do
35
- let(:comparitor) {
36
- record.merge({})
37
- }
38
-
39
- it "is equal" do
40
- expect(record.==(comparitor)).to be(true)
41
- end
42
- end
43
-
44
- context "with another record that does not delete" do
45
- let(:comparitor) {
46
- Class.new(Terrestrial::AbstractRecord) do
47
- protected
48
- def operation
49
- :something_else
50
- end
51
- end
52
- }
53
-
54
- it "is not equal" do
55
- expect(record.==(comparitor)).to be(false)
56
- end
57
- end
58
- end
59
32
  end
@@ -1,7 +1,7 @@
1
1
  require "spec_helper"
2
2
 
3
3
  require "terrestrial/dirty_map"
4
- require "terrestrial/upserted_record"
4
+ require "terrestrial/upsert_record"
5
5
  require "terrestrial/deleted_record"
6
6
 
7
7
  RSpec.describe Terrestrial::DirtyMap do
@@ -11,13 +11,24 @@ RSpec.describe Terrestrial::DirtyMap do
11
11
 
12
12
  let(:storage) { {} }
13
13
 
14
- let(:record) {
15
- create_record( namespace, identity_fields, attributes, depth)
14
+ let(:record) { create_record(mapping, attributes) }
15
+ let(:dirty_record) {
16
+ create_record( mapping, attributes.merge(name: "record/dirty_name"))
17
+ }
18
+
19
+ let(:mapping) {
20
+ double(
21
+ :mapping,
22
+ namespace: namespace,
23
+ primary_key: identity_fields,
24
+ database_owned_fields: [],
25
+ database_default_fields: [],
26
+ )
16
27
  }
17
28
 
18
29
  let(:namespace) { :table_name }
19
30
  let(:identity_fields) { [:id] }
20
- let(:depth) { 0 }
31
+ let(:identity) { { id: "record/id" } }
21
32
 
22
33
  let(:attributes) {
23
34
  {
@@ -72,22 +83,9 @@ RSpec.describe Terrestrial::DirtyMap do
72
83
  end
73
84
 
74
85
  describe "#dirty" do
75
- let(:clean_record) {
76
- create_record(namespace, identity_fields, attributes, depth)
77
- }
78
-
79
- let(:dirty_record) {
80
- create_record(
81
- namespace,
82
- identity_fields,
83
- attributes.merge(name: "record/dirty_name"),
84
- depth,
85
- )
86
- }
87
-
88
- context "when the record has not been loaded (new record)" do
86
+ context "when the record has not been loaded (a new record)" do
89
87
  it "return true" do
90
- expect(dirty_map.dirty?(clean_record)).to be(true)
88
+ expect(dirty_map.dirty?(record)).to be(true)
91
89
  end
92
90
  end
93
91
 
@@ -98,7 +96,7 @@ RSpec.describe Terrestrial::DirtyMap do
98
96
 
99
97
  context "when the record is unchanged" do
100
98
  it "returns false" do
101
- expect(dirty_map.dirty?(clean_record)).to be(false)
99
+ expect(dirty_map.dirty?(record)).to be(false)
102
100
  end
103
101
  end
104
102
 
@@ -110,12 +108,7 @@ RSpec.describe Terrestrial::DirtyMap do
110
108
 
111
109
  context "when the record is deleted" do
112
110
  let(:deleted_record) {
113
- Terrestrial::DeletedRecord.new(
114
- namespace,
115
- identity_fields,
116
- attributes,
117
- depth,
118
- )
111
+ Terrestrial::DeletedRecord.new(mapping, attributes, _depth = 0)
119
112
  }
120
113
 
121
114
  it "is always dirty" do
@@ -125,32 +118,27 @@ RSpec.describe Terrestrial::DirtyMap do
125
118
 
126
119
  context "when the record's attributes hash is mutated" do
127
120
  before do
128
- attributes.merge!(name: "new_value")
121
+ record.attributes.merge!(name: "new_value")
129
122
  end
130
123
 
131
124
  it "returns true" do
132
- expect(dirty_map.dirty?(clean_record)).to be(true)
125
+ expect(dirty_map.dirty?(record)).to be(true)
133
126
  end
134
127
  end
135
128
 
136
129
  context "when a record's string value is mutated" do
137
130
  before do
138
- attributes.fetch(:name) << "MUTANT"
131
+ record.attributes.fetch(:name) << "MUTANT"
139
132
  end
140
133
 
141
134
  it "returns true" do
142
- expect(dirty_map.dirty?(clean_record)).to be(true)
135
+ expect(dirty_map.dirty?(record)).to be(true)
143
136
  end
144
137
  end
145
138
 
146
139
  context "when record contains an unchanged subset of the fields loaded" do
147
140
  let(:partial_record) {
148
- create_record(
149
- namespace,
150
- identity_fields,
151
- partial_clean_attrbiutes,
152
- depth,
153
- )
141
+ create_record(mapping, partial_clean_attrbiutes)
154
142
  }
155
143
 
156
144
  let(:partial_clean_attrbiutes) {
@@ -164,12 +152,7 @@ RSpec.describe Terrestrial::DirtyMap do
164
152
 
165
153
  context "when record contains a changed subset of the fields loaded" do
166
154
  let(:partial_record) {
167
- create_record(
168
- namespace,
169
- identity_fields,
170
- partial_dirty_attrbiutes,
171
- depth,
172
- )
155
+ create_record(mapping, partial_dirty_attrbiutes)
173
156
  }
174
157
 
175
158
  let(:partial_dirty_attrbiutes) {
@@ -185,12 +168,7 @@ RSpec.describe Terrestrial::DirtyMap do
185
168
 
186
169
  context "when record contains an unchanged superset of the fields loaded" do
187
170
  let(:super_record) {
188
- create_record(
189
- namespace,
190
- identity_fields,
191
- super_clean_attributes,
192
- depth,
193
- )
171
+ create_record(mapping, super_clean_attributes)
194
172
  }
195
173
 
196
174
  let(:super_clean_attributes) {
@@ -202,45 +180,42 @@ RSpec.describe Terrestrial::DirtyMap do
202
180
  end
203
181
  end
204
182
  end
183
+ end
205
184
 
206
- context "#reject_unchanged_fields" do
207
- context "when the record has not been loaded (new record)" do
208
- it "returns an eqiuivalent record" do
209
- expect(dirty_map.reject_unchanged_fields(dirty_record))
210
- .to eq(dirty_record)
211
- end
185
+ describe "#reject_unchanged_fields" do
186
+ context "when the record has not been loaded (new record)" do
187
+ it "returns an eqiuivalent record" do
188
+ expect(dirty_map.reject_unchanged_fields(dirty_record))
189
+ .to eq(dirty_record)
212
190
  end
191
+ end
213
192
 
214
- context "when a record with same identity has been loaded (existing record)" do
215
- before do
216
- dirty_map.load(record)
217
- end
193
+ context "when a record with same identity has been loaded (existing record)" do
194
+ before do
195
+ dirty_map.load(record)
196
+ end
197
+
198
+ let(:dup_record) { create_record(mapping, attributes) }
218
199
 
219
- context "with a equivalent record" do
220
- it "returns an empty record" do
221
- expect(dirty_map.reject_unchanged_fields(clean_record)).to be_empty
222
- end
200
+ context "with a equivalent record" do
201
+ it "returns an empty record" do
202
+ expect(dirty_map.reject_unchanged_fields(dup_record)).to be_empty
223
203
  end
204
+ end
224
205
 
225
- context "a record with a changed field" do
226
- it "returns a record containing just that field" do
227
- expect(
228
- dirty_map
229
- .reject_unchanged_fields(dirty_record)
230
- .non_identity_attributes
231
- ).to eq( name: "record/dirty_name" )
232
- end
206
+ context "a record with a changed field" do
207
+ it "returns a record containing just that field" do
208
+ expect(
209
+ dirty_map
210
+ .reject_unchanged_fields(dirty_record)
211
+ .updatable_attributes
212
+ ).to eq( name: "record/dirty_name" )
233
213
  end
234
214
  end
235
215
  end
236
216
  end
237
217
 
238
- def create_record(namespace, identity_fields, attributes, depth)
239
- Terrestrial::UpsertedRecord.new(
240
- namespace,
241
- identity_fields,
242
- attributes,
243
- depth,
244
- )
218
+ def create_record(mapping, attributes)
219
+ Terrestrial::Record.new(mapping, attributes)
245
220
  end
246
221
  end
@@ -0,0 +1,153 @@
1
+ require "terrestrial/functional_pipeline"
2
+ RSpec.describe Terrestrial::FunctionalPipeline do
3
+ subject(:pipeline) { Terrestrial::FunctionalPipeline.new }
4
+
5
+ let(:input) { double(:input) }
6
+ let(:step1) { double(:step1, call: "step1_result") }
7
+ let(:step2) { double(:step2, call: "step2_result") }
8
+ let(:step3) { double(:step3, call: "step3_result") }
9
+
10
+ describe "#append" do
11
+ it "returns a new pipeline" do
12
+ expect(pipeline.append(:step1, step1)).not_to be(pipeline)
13
+ end
14
+
15
+ it "appends a step to the existing steps" do
16
+ p1 = pipeline.append(:step1, step1)
17
+ result, _ = p1.call(input)
18
+
19
+ expect(result).to eq("step1_result")
20
+ end
21
+ end
22
+
23
+ context "three step pipeline" do
24
+ let(:pipeline) do
25
+ Terrestrial::FunctionalPipeline.from_array(
26
+ [
27
+ [:step1, step1],
28
+ [:step2, step2],
29
+ [:step3, step3],
30
+ ]
31
+ )
32
+ end
33
+
34
+ describe "#call" do
35
+ it "calls the first step with the input" do
36
+ pipeline.call(input)
37
+
38
+ expect(step1).to have_received(:call).with(input)
39
+ end
40
+
41
+ it "calls the second step with the result of the first" do
42
+ pipeline.call(input)
43
+
44
+ expect(step2).to have_received(:call).with("step1_result")
45
+ end
46
+
47
+ it "calls the thrid step with the result of the second" do
48
+ pipeline.call(input)
49
+
50
+ expect(step3).to have_received(:call).with("step2_result")
51
+ end
52
+
53
+ it "returns the result of the last step" do
54
+ result, _ = pipeline.call(input)
55
+
56
+ expect(result).to eq("step3_result")
57
+ end
58
+
59
+ it "returns the intermediate results of all steps" do
60
+ _, intermediates = pipeline.call(input)
61
+
62
+ expect(intermediates).to eq([
63
+ [:input, input],
64
+ [:step1, "step1_result"],
65
+ [:step2, "step2_result"],
66
+ [:step3, "step3_result"],
67
+ ])
68
+ end
69
+
70
+ context "when called with a block" do
71
+ it "yields the result of each step to the block" do
72
+ yielded = []
73
+
74
+ pipeline.call(input) do |name, result|
75
+ yielded << [name, result]
76
+ end
77
+
78
+ expect(yielded).to eq([
79
+ [:step1, "step1_result"],
80
+ [:step2, "step2_result"],
81
+ [:step3, "step3_result"],
82
+ ])
83
+ end
84
+ end
85
+ end
86
+
87
+ describe "#describe" do
88
+ it "returns the list of named steps" do
89
+ expect(pipeline.describe).to eq([:step1, :step2, :step3])
90
+ end
91
+ end
92
+
93
+ describe "#take_until" do
94
+ it "returns a new a pipeline" do
95
+ expect(pipeline.take_until(:step2)).to be_a(Terrestrial::FunctionalPipeline)
96
+ end
97
+
98
+ describe "the new pipeline" do
99
+ subject(:new_pipeline) { pipeline.take_until(:step2) }
100
+
101
+ it "contains steps from the beginning up to specified step" do
102
+ expect(new_pipeline.describe).to eq([:step1, :step2])
103
+ end
104
+
105
+ context "when executed" do
106
+ it "returns the result of the steps" do
107
+ result, _ = new_pipeline.call(input)
108
+
109
+ expect(result).to eq("step2_result")
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ describe "#drop_until" do
116
+ it "returns a new a pipeline" do
117
+ expect(pipeline.drop_until(:step2)).to be_a(Terrestrial::FunctionalPipeline)
118
+ end
119
+
120
+ describe "the new pipeline" do
121
+ subject(:new_pipeline) { pipeline.drop_until(:step2) }
122
+
123
+ it "contains steps that appear before the specified step" do
124
+ expect(new_pipeline.describe).to eq([:step3])
125
+ end
126
+
127
+ context "when executed" do
128
+ it "starts execution with the step after the specified one" do
129
+ result, _ = new_pipeline.call(input)
130
+
131
+ expect(step3).to have_received(:call).with(input)
132
+ end
133
+
134
+ it "does not execute steps dropped from the original pipeline" do
135
+ result, _ = new_pipeline.call(input)
136
+
137
+ expect(step1).not_to have_received(:call)
138
+ expect(step2).not_to have_received(:call)
139
+ end
140
+
141
+ it "returns the result of the steps" do
142
+ result, _ = new_pipeline.call(input)
143
+
144
+ expect(result).to eq("step3_result")
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ describe "#each" do
151
+ end
152
+ end
153
+ end