terrestrial 0.1.0 → 0.1.1

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