terrestrial 0.3.0 → 0.5.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.
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