terrestrial 0.1.0 → 0.1.1

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -9
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +28 -0
  6. data/Gemfile.lock +73 -0
  7. data/LICENSE.txt +22 -0
  8. data/MissingFeatures.md +64 -0
  9. data/README.md +161 -16
  10. data/Rakefile +30 -0
  11. data/TODO.md +41 -0
  12. data/features/env.rb +60 -0
  13. data/features/example.feature +120 -0
  14. data/features/step_definitions/example_steps.rb +46 -0
  15. data/lib/terrestrial/abstract_record.rb +99 -0
  16. data/lib/terrestrial/association_loaders.rb +52 -0
  17. data/lib/terrestrial/collection_mutability_proxy.rb +81 -0
  18. data/lib/terrestrial/configurations/conventional_association_configuration.rb +186 -0
  19. data/lib/terrestrial/configurations/conventional_configuration.rb +302 -0
  20. data/lib/terrestrial/dataset.rb +49 -0
  21. data/lib/terrestrial/deleted_record.rb +20 -0
  22. data/lib/terrestrial/dirty_map.rb +42 -0
  23. data/lib/terrestrial/graph_loader.rb +63 -0
  24. data/lib/terrestrial/graph_serializer.rb +91 -0
  25. data/lib/terrestrial/identity_map.rb +22 -0
  26. data/lib/terrestrial/lazy_collection.rb +74 -0
  27. data/lib/terrestrial/lazy_object_proxy.rb +55 -0
  28. data/lib/terrestrial/many_to_many_association.rb +138 -0
  29. data/lib/terrestrial/many_to_one_association.rb +66 -0
  30. data/lib/terrestrial/mapper_facade.rb +137 -0
  31. data/lib/terrestrial/one_to_many_association.rb +66 -0
  32. data/lib/terrestrial/public_conveniencies.rb +139 -0
  33. data/lib/terrestrial/query_order.rb +32 -0
  34. data/lib/terrestrial/relation_mapping.rb +50 -0
  35. data/lib/terrestrial/serializer.rb +18 -0
  36. data/lib/terrestrial/short_inspection_string.rb +18 -0
  37. data/lib/terrestrial/struct_factory.rb +17 -0
  38. data/lib/terrestrial/subset_queries_proxy.rb +11 -0
  39. data/lib/terrestrial/upserted_record.rb +15 -0
  40. data/lib/terrestrial/version.rb +1 -1
  41. data/lib/terrestrial.rb +5 -2
  42. data/sequel_mapper.gemspec +31 -0
  43. data/spec/config_override_spec.rb +193 -0
  44. data/spec/custom_serializers_spec.rb +49 -0
  45. data/spec/deletion_spec.rb +101 -0
  46. data/spec/graph_persistence_spec.rb +313 -0
  47. data/spec/graph_traversal_spec.rb +121 -0
  48. data/spec/new_graph_persistence_spec.rb +71 -0
  49. data/spec/object_identity_spec.rb +70 -0
  50. data/spec/ordered_association_spec.rb +51 -0
  51. data/spec/persistence_efficiency_spec.rb +224 -0
  52. data/spec/predefined_queries_spec.rb +62 -0
  53. data/spec/proxying_spec.rb +88 -0
  54. data/spec/querying_spec.rb +48 -0
  55. data/spec/readme_examples_spec.rb +35 -0
  56. data/spec/sequel_mapper/abstract_record_spec.rb +244 -0
  57. data/spec/sequel_mapper/collection_mutability_proxy_spec.rb +135 -0
  58. data/spec/sequel_mapper/deleted_record_spec.rb +59 -0
  59. data/spec/sequel_mapper/dirty_map_spec.rb +214 -0
  60. data/spec/sequel_mapper/lazy_collection_spec.rb +119 -0
  61. data/spec/sequel_mapper/lazy_object_proxy_spec.rb +140 -0
  62. data/spec/sequel_mapper/public_conveniencies_spec.rb +58 -0
  63. data/spec/sequel_mapper/upserted_record_spec.rb +59 -0
  64. data/spec/spec_helper.rb +36 -0
  65. data/spec/support/blog_schema.rb +38 -0
  66. data/spec/support/have_persisted_matcher.rb +19 -0
  67. data/spec/support/mapper_setup.rb +221 -0
  68. data/spec/support/mock_sequel.rb +193 -0
  69. data/spec/support/object_graph_setup.rb +139 -0
  70. data/spec/support/seed_data_setup.rb +165 -0
  71. data/spec/support/sequel_persistence_setup.rb +19 -0
  72. data/spec/support/sequel_test_support.rb +166 -0
  73. metadata +207 -13
  74. data/.travis.yml +0 -4
  75. data/bin/console +0 -14
  76. data/bin/setup +0 -7
  77. data/terrestrial.gemspec +0 -23
@@ -0,0 +1,224 @@
1
+ require "spec_helper"
2
+
3
+ require "support/mapper_setup"
4
+ require "support/sequel_persistence_setup"
5
+ require "support/seed_data_setup"
6
+ require "terrestrial"
7
+
8
+ RSpec.describe "Graph persistence efficiency" do
9
+ include_context "mapper setup"
10
+ include_context "sequel persistence setup"
11
+ include_context "seed data setup"
12
+
13
+ let(:mapper) { user_mapper }
14
+ let(:user_query) { mapper.where(id: "users/1") }
15
+ let(:user) { user_query.first }
16
+
17
+ context "when modifying the root node" do
18
+ let(:modified_email) { "modified@example.com" }
19
+
20
+ context "and only the root node" do
21
+ before do
22
+ user.email = modified_email
23
+ end
24
+
25
+ it "performs 1 update" do
26
+ expect {
27
+ mapper.save(user)
28
+ }.to change { query_counter.update_count }.by(1)
29
+ end
30
+
31
+ it "sends only the updated fields to the datastore" do
32
+ mapper.save(user)
33
+ update_sql = query_counter.updates.last
34
+
35
+ expect(update_sql).to eq(
36
+ %{UPDATE "users" SET "email" = '#{modified_email}' WHERE ("id" = '#{user.id}')}
37
+ )
38
+ end
39
+ end
40
+ end
41
+
42
+ context "when modifying a directly associated (has many) object" do
43
+ let(:modified_post_subject) { "modified post subject" }
44
+
45
+ before do
46
+ user.posts.first.subject = modified_post_subject
47
+ end
48
+
49
+ it "performs 1 update" do
50
+ expect {
51
+ mapper.save(user)
52
+ }.to change { query_counter.update_count }.by(1)
53
+ end
54
+
55
+ it "performs 0 deletes" do
56
+ expect {
57
+ mapper.save(user)
58
+ }.to change { query_counter.delete_count }.by(0)
59
+ end
60
+
61
+ it "performs 0 additional reads" do
62
+ expect {
63
+ mapper.save(user)
64
+ }.to change { query_counter.read_count }.by(0)
65
+ end
66
+ end
67
+
68
+ context "when loading many nodes of the graph" do
69
+ let(:post) {
70
+ user.posts.first
71
+ }
72
+
73
+ context "and modifying an intermediate node" do
74
+ before do
75
+ post.subject = "MODIFIED"
76
+ end
77
+
78
+ it "performs 1 write" do
79
+ expect {
80
+ mapper.save(user)
81
+ }.to change { query_counter.update_count }.by(1)
82
+ end
83
+ end
84
+
85
+ context "and modifying a leaf node" do
86
+ let(:comment) { post.comments.first }
87
+
88
+ before do
89
+ comment.body = "UPDATED!"
90
+ end
91
+
92
+ it "performs 1 update" do
93
+ expect {
94
+ mapper.save(user)
95
+ }.to change { query_counter.update_count }.by(1)
96
+ end
97
+ end
98
+
99
+ context "and modifying both a leaf and intermediate node" do
100
+ let(:comment) { post.comments.first }
101
+
102
+ before do
103
+ comment.body = "UPDATED!"
104
+ post.subject = "MODIFIED"
105
+ end
106
+
107
+ it "performs 2 updates" do
108
+ expect {
109
+ mapper.save(user)
110
+ }.to change { query_counter.update_count }.by(2)
111
+ end
112
+ end
113
+ end
114
+
115
+ context "when modifying a many to many association" do
116
+ let(:post) { user.posts.first }
117
+ let(:category) { post.categories.first }
118
+
119
+ before do
120
+ category.name = "UPDATED"
121
+ end
122
+
123
+ it "performs 1 write" do
124
+ expect {
125
+ mapper.save(user)
126
+ }.to change { query_counter.update_count }.by(1)
127
+ end
128
+ end
129
+
130
+ context "eager loading" do
131
+ context "on root node" do
132
+ it "performs 1 read per table rather than n + 1" do
133
+ expect {
134
+ mapper.eager_load(:posts => []).all.map { |user|
135
+ [user.id, user.posts.map(&:id)]
136
+ }
137
+ }.to change { query_counter.read_count }.by(2)
138
+ end
139
+ end
140
+
141
+ context "with nested has many" do
142
+ it "performs 1 read per table rather than n + 1" do
143
+ expect {
144
+ user_query
145
+ .eager_load(:posts => { :comments => [] })
146
+ .first
147
+ .posts
148
+ .map { |post| post.comments.map(&:id) }
149
+ }.to change { query_counter.read_count }.by(3)
150
+ end
151
+ end
152
+
153
+ context "with has many and belongs to" do
154
+ it "performs 1 read per table rather than n + 1" do
155
+ expect {
156
+ user_query
157
+ .eager_load(:posts => { :comments => { :commenter => [] }})
158
+ .flat_map(&:posts)
159
+ .flat_map(&:comments)
160
+ .flat_map(&:commenter)
161
+ }.to change { query_counter.read_count }.by(4)
162
+ end
163
+ end
164
+
165
+ context "for has many to has many through" do
166
+ it "performs 1 read per table (including join table) rather than n + 1" do
167
+ expect {
168
+ user_query
169
+ .eager_load(:posts => { :categories => [] })
170
+ .flat_map(&:posts)
171
+ .flat_map(&:categories)
172
+ .flat_map(&:id)
173
+ }.to change { query_counter.read_count }.by(4)
174
+ end
175
+ end
176
+
177
+ context "for has many through to has many" do
178
+ it "performs 1 read per table (includiing join table) rather than n + 1" do
179
+ expect {
180
+ user_query
181
+ .eager_load(:posts => { :categories => { :posts => [] }})
182
+ .flat_map(&:posts)
183
+ .flat_map(&:categories)
184
+ .flat_map(&:posts)
185
+ }.to change { query_counter.read_count }.by(6)
186
+ end
187
+ end
188
+
189
+ context "eager load multiple associations at same level" do
190
+ it "performs 1 read per table (includiing join table) rather than n + 1" do
191
+ expect {
192
+ posts = user_query
193
+ .eager_load(:posts => { :comments => {}, :categories => {} })
194
+ .flat_map(&:posts)
195
+
196
+ categories = posts.flat_map(&:categories)
197
+ comments = posts.flat_map(&:comments)
198
+ }.to change { query_counter.read_count }.by(5)
199
+ end
200
+ end
201
+
202
+ context "mixed eager and lazy loading" do
203
+ it "lazy data can still be loaded while eager data remains efficient" do
204
+ eager_queries = 6
205
+ lazy_comment_queries = 3
206
+
207
+ expect {
208
+ user_query
209
+ .eager_load(:posts => { :categories => { :posts => [] }})
210
+ .flat_map(&:posts)
211
+ .flat_map(&:categories)
212
+ .flat_map(&:posts)
213
+ .flat_map(&:comments)
214
+ }.to change {
215
+ query_counter.read_count
216
+ }.by(eager_queries + lazy_comment_queries)
217
+ end
218
+ end
219
+ end
220
+
221
+ after do |example|
222
+ query_counter.show_queries if example.exception
223
+ end
224
+ end
@@ -0,0 +1,62 @@
1
+ require "spec_helper"
2
+
3
+ require "support/mapper_setup"
4
+ require "support/sequel_persistence_setup"
5
+ require "support/seed_data_setup"
6
+ require "terrestrial"
7
+ require "terrestrial/configurations/conventional_configuration"
8
+
9
+ RSpec.describe "Predefined subset queries" do
10
+ include_context "mapper setup"
11
+ include_context "sequel persistence setup"
12
+ include_context "seed data setup"
13
+
14
+ subject(:users) { user_mapper }
15
+
16
+ context "on the top level mapper" do
17
+ context "subset is defined with a block" do
18
+ before do
19
+ configs.fetch(:users).merge!(
20
+ subsets: {
21
+ tricketts: ->(dataset) {
22
+ dataset
23
+ .where(last_name: "Trickett")
24
+ .order(:first_name)
25
+ },
26
+ },
27
+ )
28
+ end
29
+
30
+ it "maps the result of the subset" do
31
+ expect(users.subset(:tricketts).map(&:first_name)).to eq([
32
+ "Hansel",
33
+ "Jasper",
34
+ ])
35
+ end
36
+ end
37
+ end
38
+
39
+ context "on a has many association" do
40
+ before do
41
+ configs.fetch(:posts).merge!(
42
+ subsets: {
43
+ body_contains: ->(dataset, search_string) {
44
+ dataset.where("body like '%#{search_string}%'")
45
+ },
46
+ },
47
+ )
48
+ end
49
+
50
+ let(:user) { users.first }
51
+
52
+ it "maps the datastore subset" do
53
+ expect(user.posts.subset(:body_contains, "purrr").map(&:id))
54
+ .to eq(["posts/2"])
55
+ end
56
+
57
+ it "returns an immutable collection" do
58
+ expect(user.posts.subset(:body_contains, "purrr").public_methods)
59
+ .not_to include(:push, :<<, :delete)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,88 @@
1
+ require "spec_helper"
2
+
3
+ require "support/mapper_setup"
4
+ require "support/sequel_persistence_setup"
5
+ require "support/seed_data_setup"
6
+ require "terrestrial"
7
+
8
+ RSpec.describe "Proxying associations" do
9
+ include_context "mapper setup"
10
+ include_context "sequel persistence setup"
11
+ include_context "seed data setup"
12
+
13
+ context "of type `has_many`" do
14
+ subject(:mapper) { user_mapper }
15
+
16
+ let(:user) {
17
+ mapper.where(id: "users/1").first
18
+ }
19
+
20
+ let(:posts) { user.posts }
21
+
22
+ describe "limiting datastore reads" do
23
+ context "when loading the root node" do
24
+ it "only performs one read" do
25
+ expect {
26
+ user
27
+ }.to change { query_counter.read_count }.by(1)
28
+ end
29
+ end
30
+
31
+ context "when getting a reference to an association proxy" do
32
+ before { user }
33
+
34
+ it "does no additional reads" do
35
+ expect{
36
+ user.posts
37
+ }.to change { query_counter.read_count }.by(0)
38
+ end
39
+ end
40
+
41
+ context "when iteratiing over a has many association" do
42
+ before { posts }
43
+
44
+ it "does a single additional read for the association collection" do
45
+ expect {
46
+ user.posts.each { |x| x }
47
+ }.to change { query_counter.read_count }.by(1)
48
+ end
49
+
50
+ context "when doing this more than once" do
51
+ before do
52
+ posts.each { |x| x }
53
+ end
54
+
55
+ it "performs no additional reads" do
56
+ expect {
57
+ user.posts.each { |x| x }
58
+ }.not_to change { query_counter.read_count }
59
+ end
60
+ end
61
+ end
62
+
63
+ context "when getting a reference to a many to many association" do
64
+ before { post }
65
+
66
+ let(:post) { user.posts.first }
67
+
68
+ it "does no additional reads" do
69
+ expect {
70
+ post.categories
71
+ }.to change { query_counter.read_count }.by(0)
72
+ end
73
+ end
74
+
75
+ context "when iterating over a many to many association" do
76
+ let(:category_count) { 3 }
77
+
78
+ it "does 1 read" do
79
+ post = user.posts.first
80
+
81
+ expect {
82
+ post.categories.each { |x| x }
83
+ }.to change { query_counter.read_count }.by(1)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,48 @@
1
+ require "spec_helper"
2
+
3
+ require "support/mapper_setup"
4
+ require "support/sequel_persistence_setup"
5
+ require "support/seed_data_setup"
6
+ require "terrestrial"
7
+
8
+ RSpec.describe "Querying" do
9
+ include_context "mapper setup"
10
+ include_context "sequel persistence setup"
11
+ include_context "seed data setup"
12
+
13
+ subject(:mapper) { user_mapper }
14
+
15
+ let(:user) {
16
+ mapper.where(id: "users/1").first
17
+ }
18
+
19
+ let(:query_criteria) {
20
+ {
21
+ body: "I do it three times purrr day",
22
+ }
23
+ }
24
+
25
+ let(:filtered_posts) {
26
+ user.posts.where(query_criteria)
27
+ }
28
+
29
+ describe "arbitrary where query" do
30
+ it "returns a filtered version of the association" do
31
+ expect(filtered_posts.map(&:id)).to eq(["posts/2"])
32
+ end
33
+
34
+ it "delegates the query to the datastore, performs two additiona reads" do
35
+ expect {
36
+ filtered_posts.map(&:id)
37
+ }.to change { query_counter.read_count }.by(2)
38
+ end
39
+
40
+ it "returns another collection" do
41
+ expect(filtered_posts).not_to be(user.posts)
42
+ end
43
+
44
+ it "returns an immutable collection" do
45
+ expect(filtered_posts.public_methods).not_to include(:push, :<<, :delete)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,35 @@
1
+ require "spec_helper"
2
+
3
+ require "support/mapper_setup"
4
+ require "support/sequel_persistence_setup"
5
+ require "support/seed_data_setup"
6
+ require "terrestrial"
7
+
8
+ require "spec_helper"
9
+
10
+ require "support/mapper_setup"
11
+ require "support/sequel_persistence_setup"
12
+ require "support/seed_data_setup"
13
+ require "terrestrial"
14
+
15
+ RSpec.describe "README examples" do
16
+ include_context "sequel persistence setup"
17
+
18
+ readme_contents = File.read("README.md")
19
+
20
+ code_samples = readme_contents
21
+ .split("```ruby")
22
+ .drop(1)
23
+ .map { |s| s.split("```").first }
24
+
25
+ code_samples.each_with_index do |code_sample, i|
26
+ it "executes without error" do
27
+ begin
28
+ Module.new.module_eval(code_sample)
29
+ rescue => e
30
+ File.open("./example#{i}.rb", "w") { |f| f.puts(code_sample) }
31
+ binding.pry if ENV["DEBUG"]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,244 @@
1
+ require "spec_helper"
2
+
3
+ require "terrestrial/abstract_record"
4
+
5
+ RSpec.describe Terrestrial::AbstractRecord do
6
+ subject(:record) {
7
+ Terrestrial::AbstractRecord.new(
8
+ namespace,
9
+ primary_key_fields,
10
+ raw_data,
11
+ depth,
12
+ )
13
+ }
14
+
15
+ let(:namespace) { double(:namespace) }
16
+ let(:primary_key_fields) { [ :id1, :id2 ] }
17
+ let(:depth) { 0 }
18
+
19
+ let(:raw_data) {
20
+ {
21
+ id1: id1,
22
+ id2: id2,
23
+ name: name,
24
+ }
25
+ }
26
+
27
+ let(:id1) { double(:id1) }
28
+ let(:id2) { double(:id2) }
29
+ let(:name) { double(:name) }
30
+
31
+ describe "#namespace" do
32
+ it "returns the namespace" do
33
+ expect(record.namespace).to eq(namespace)
34
+ end
35
+ end
36
+
37
+ describe "#identity" do
38
+ it "returns the primary key fields" do
39
+ expect(record.identity).to eq(
40
+ id1: id1,
41
+ id2: id2,
42
+ )
43
+ end
44
+ end
45
+
46
+ describe "#fetch" do
47
+ it "delegates to the underlying Hash representation" do
48
+ expect(record.fetch(:id1)).to eq(id1)
49
+ expect(record.fetch(:name)).to eq(name)
50
+ expect(record.fetch(:not_there, "nope")).to eq("nope")
51
+ expect(record.fetch(:not_there) { "lord no" }).to eq("lord no")
52
+ end
53
+ end
54
+
55
+ describe "#to_h" do
56
+ it "returns a raw_data merged with identity" do
57
+ expect(record.to_h).to eq(
58
+ id1: id1,
59
+ id2: id2,
60
+ name: name,
61
+ )
62
+ end
63
+ end
64
+
65
+ describe "#if_upsert" do
66
+ it "returns self" do
67
+ expect(
68
+ record.if_upsert { |_| }
69
+ ).to be(record)
70
+ end
71
+
72
+ it "does not call the block" do
73
+ expect {
74
+ record.if_upsert { |_| raise "Does not happen" }
75
+ }.not_to raise_error
76
+ end
77
+ end
78
+
79
+ describe "#if_delete" do
80
+ it "returns self" do
81
+ expect(
82
+ record.if_delete { |_| }
83
+ ).to be(record)
84
+ end
85
+
86
+ it "does not call the block" do
87
+ expect {
88
+ record.if_delete { |_| raise "Does not happen" }
89
+ }.not_to raise_error
90
+ end
91
+ end
92
+
93
+ describe "#merge" do
94
+ let(:extra_data) {
95
+ {
96
+ location: location,
97
+ }
98
+ }
99
+
100
+ let(:location) { double(:location) }
101
+
102
+ it "returns a new record with same identity" do
103
+ expect(record.merge(extra_data).identity).to eq(
104
+ id1: id1,
105
+ id2: id2,
106
+ )
107
+ end
108
+
109
+ it "returns a new record with same namespace" do
110
+ expect(
111
+ record.merge(extra_data).namespace
112
+ ).to eq(namespace)
113
+ end
114
+
115
+ it "returns a new record with merged data" do
116
+ merged_record = record.merge(extra_data)
117
+
118
+ expect(merged_record.to_h).to eq(
119
+ id1: id1,
120
+ id2: id2,
121
+ name: name,
122
+ location: location,
123
+ )
124
+ end
125
+
126
+ it "does not mutate the original record" do
127
+ expect {
128
+ record.merge(extra_data)
129
+ }.not_to change { record.to_h }
130
+ end
131
+
132
+ context "when attempting to overwrite the existing identity" do
133
+ let(:extra_data) {
134
+ {
135
+ id1: double(:new_id),
136
+ location: location,
137
+ }
138
+ }
139
+
140
+ it "does not change the identity" do
141
+ expect {
142
+ record.merge(extra_data)
143
+ }.not_to change { record.identity }
144
+ end
145
+ end
146
+ end
147
+
148
+ describe "#<=>" do
149
+ let(:deep_record) {
150
+ Terrestrial::AbstractRecord.new(
151
+ namespace,
152
+ primary_key_fields,
153
+ raw_data,
154
+ _depth = 5,
155
+ )
156
+ }
157
+
158
+ let(:shallow_record) {
159
+ Terrestrial::AbstractRecord.new(
160
+ namespace,
161
+ primary_key_fields,
162
+ raw_data,
163
+ _depth = 1,
164
+ )
165
+ }
166
+
167
+ context "when other record has deeper depth" do
168
+ it "is sortable by depth" do
169
+ expect([shallow_record, record, deep_record].sort).to eq(
170
+ [record, deep_record, shallow_record]
171
+ )
172
+ end
173
+ end
174
+ end
175
+
176
+ describe "#==" do
177
+ context "super class contract" do
178
+ let(:comparitor) { record.merge({}) }
179
+
180
+ it "compares" do
181
+ record == comparitor
182
+ end
183
+
184
+ context "when subclassed" do
185
+ subject(:record) {
186
+ record_subclass.new(namespace, primary_key_fields, raw_data)
187
+ }
188
+
189
+ let(:record_subclass) {
190
+ Class.new(Terrestrial::AbstractRecord) {
191
+ protected
192
+
193
+ def operation
194
+ :do_a_thing
195
+ end
196
+ }
197
+ }
198
+
199
+ context "when comparitor is of the wrong type" do
200
+ it "is not equal" do
201
+ expect(record.==(Object.new)).to be(false)
202
+ end
203
+ end
204
+
205
+ context "when the operation type is equal" do
206
+ context "when the combined `raw_data` and `identity` are equal" do
207
+ let(:comparitor) { record.merge({}) }
208
+
209
+ it "is equal" do
210
+ expect(record.==(comparitor)).to be(true)
211
+ end
212
+ end
213
+
214
+ context "when the combined `raw_data` and `identity` are not equal" do
215
+ let(:comparitor) { record.merge(something_else: "i'm different") }
216
+
217
+ it "is not equal" do
218
+ expect(record.==(comparitor)).to be(false)
219
+ end
220
+ end
221
+ end
222
+
223
+ context "when the operation name differs" do
224
+ let(:comparitor) {
225
+ record_class_with_different_operation.new(namespace, primary_key_fields, raw_data)
226
+ }
227
+
228
+ let(:record_class_with_different_operation) {
229
+ Class.new(Terrestrial::AbstractRecord) {
230
+ protected
231
+ def operation
232
+ :do_a_different_thing
233
+ end
234
+ }
235
+ }
236
+
237
+ it "is not equal" do
238
+ expect(record.==(comparitor)).to be(false)
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end