attr_pouch 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,482 +1,551 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe AttrPouch do
4
- def make_pouchy(field_name, opts={})
5
- Class.new(Sequel::Model(:items)) do
6
- include AttrPouch
7
-
8
- pouch(:attrs) do
9
- field field_name, opts
4
+ %i[hstore json jsonb].each do |storage_kind|
5
+
6
+ context "with a #{storage_kind} backing column" do
7
+
8
+ let(:column_name) { "attrs_#{storage_kind}".to_sym }
9
+ let(:storage_wrapper) do
10
+ case storage_kind
11
+ when :hstore
12
+ ->(hash) { Sequel.hstore(hash) }
13
+ when :json, :jsonb
14
+ ->(hash) { Sequel.pg_json(hash) }
15
+ else
16
+ raise ArgumentError, "Unknown kind, #{kind}"
17
+ end
10
18
  end
11
- end.create
12
- end
13
19
 
14
- context "with a simple attribute" do
15
- let(:pouchy) { make_pouchy(:foo, type: String) }
20
+ def make_pouchy(field_name, opts={})
21
+ col_name = column_name
22
+ Class.new(Sequel::Model(:items)) do
23
+ include AttrPouch
16
24
 
17
- it "generates getter and setter" do
18
- pouchy.foo = 'bar'
19
- expect(pouchy.foo).to eq('bar')
20
- end
25
+ pouch(col_name) do
26
+ field field_name, opts
27
+ end
28
+ end.create
29
+ end
21
30
 
22
- it "clears on reload" do
23
- pouchy.update(foo: 'bar')
24
- expect(pouchy.foo).to eq('bar')
25
- pouchy.foo = 'baz'
26
- pouchy.reload
27
- expect(pouchy.foo).to eq('bar')
28
- end
31
+ def wrap_hash(hash)
32
+ hash = Hash[hash.map { |k,v| [ k.to_s, v ] }]
33
+ storage_wrapper.call(hash)
34
+ end
29
35
 
30
- it "marks the field as modified" do
31
- pouchy.foo = 'bar'
32
- result = pouchy.save_changes
33
- expect(result).to_not be_nil
34
- pouchy.reload
35
- expect(pouchy.foo).to eq('bar')
36
- end
36
+ context "with a simple attribute" do
37
+ let(:pouchy) { make_pouchy(:foo, type: String) }
37
38
 
38
- it "avoids marking the field as modified if it is not changing" do
39
- pouchy.foo = 'bar'
40
- expect(pouchy.save_changes).to_not be_nil
41
- pouchy.foo = 'bar'
42
- expect(pouchy.save_changes).to be_nil
43
- end
39
+ it "generates getter and setter" do
40
+ pouchy.foo = 'bar'
41
+ expect(pouchy.foo).to eq('bar')
42
+ end
44
43
 
45
- it "requires the attribute to be present if read" do
46
- expect { pouchy.foo }.to raise_error(AttrPouch::MissingRequiredFieldError)
47
- end
44
+ it "clears on reload" do
45
+ pouchy.update(foo: 'bar')
46
+ expect(pouchy.foo).to eq('bar')
47
+ pouchy.foo = 'baz'
48
+ pouchy.reload
49
+ expect(pouchy.foo).to eq('bar')
50
+ end
48
51
 
49
- context "with nil values" do
50
- let(:pouchy) { make_pouchy(:f1, type: :nil_hater) }
52
+ it "marks the field as modified" do
53
+ pouchy.foo = 'bar'
54
+ result = pouchy.save_changes
55
+ expect(result).to_not be_nil
56
+ pouchy.reload
57
+ expect(pouchy.foo).to eq('bar')
58
+ end
51
59
 
52
- before do
53
- AttrPouch.configure do |config|
54
- config.encode(:nil_hater) { |f,v| v.nil? ? (raise ArgumentError) : v }
55
- config.decode(:nil_hater) { |f,v| v.nil? ? (raise ArgumentError) : v }
60
+ it "avoids marking the field as modified if it is not changing" do
61
+ pouchy.foo = 'bar'
62
+ expect(pouchy.save_changes).to_not be_nil
63
+ pouchy.foo = 'bar'
64
+ expect(pouchy.save_changes).to be_nil
56
65
  end
57
- end
58
66
 
59
- it "bypasses encoding" do
60
- pouchy.update(f1: 'foo')
61
- expect { pouchy.update(f1: nil) }.not_to raise_error
62
- expect(pouchy.f1).to be_nil
63
- end
67
+ it "requires the attribute to be present if read" do
68
+ expect { pouchy.foo }.to raise_error(AttrPouch::MissingRequiredFieldError)
69
+ end
64
70
 
65
- it "bypasses decoding" do
66
- pouchy.update(attrs: Sequel.hstore(f1: nil))
67
- expect { pouchy.f1 }.not_to raise_error
68
- expect(pouchy.f1).to be_nil
71
+ context "with nil values" do
72
+ let(:pouchy) { make_pouchy(:f1, type: :nil_hater) }
73
+
74
+ before do
75
+ AttrPouch.configure do |config|
76
+ config.encode(:nil_hater) { |f,v| v.nil? ? (raise ArgumentError) : v }
77
+ config.decode(:nil_hater) { |f,v| v.nil? ? (raise ArgumentError) : v }
78
+ end
79
+ end
80
+
81
+ it "bypasses encoding" do
82
+ pouchy.update(f1: 'foo')
83
+ expect { pouchy.update(f1: nil) }.not_to raise_error
84
+ expect(pouchy.f1).to be_nil
85
+ end
86
+
87
+ it "bypasses decoding" do
88
+ pouchy.update(column_name => wrap_hash(f1: nil))
89
+ expect { pouchy.f1 }.not_to raise_error
90
+ expect(pouchy.f1).to be_nil
91
+ end
92
+
93
+ it "still records the value as nil if not present when writing" do
94
+ pouchy.update(f1: nil)
95
+ expect(pouchy[column_name]).to have_key('f1')
96
+ expect(pouchy[column_name].fetch('f1')).to be_nil
97
+ end
98
+ end
69
99
  end
70
100
 
71
- it "still records the value as nil if not present when writing" do
72
- pouchy.update(f1: nil)
73
- expect(pouchy.attrs).to have_key(:f1)
74
- expect(pouchy.attrs[:f1]).to be_nil
75
- end
76
- end
77
- end
78
-
79
- context "with an integer attribute" do
80
- let(:pouchy) { make_pouchy(:foo, type: Integer) }
81
-
82
- it "preserves the type" do
83
- pouchy.update(foo: 42)
84
- pouchy.reload
85
- expect(pouchy.foo).to eq(42)
86
- end
87
- end
88
-
89
- context "with a float attribute" do
90
- let(:pouchy) { make_pouchy(:foo, type: Float) }
91
-
92
- it "preserves the type" do
93
- pouchy.update(foo: 2.78)
94
- pouchy.reload
95
- expect(pouchy.foo).to eq(2.78)
96
- end
97
- end
101
+ context "with an integer attribute" do
102
+ let(:pouchy) { make_pouchy(:foo, type: Integer) }
98
103
 
99
- context "with a boolean attribute" do
100
- let(:pouchy) { make_pouchy(:foo, type: :bool) }
104
+ it "preserves the type" do
105
+ pouchy.update(foo: 42)
106
+ pouchy.reload
107
+ expect(pouchy.foo).to eq(42)
108
+ end
109
+ end
101
110
 
102
- it "preserves the type" do
103
- pouchy.update(foo: true)
104
- pouchy.reload
105
- expect(pouchy.foo).to be true
106
- end
107
- end
111
+ context "with a float attribute" do
112
+ let(:pouchy) { make_pouchy(:foo, type: Float) }
108
113
 
109
- context "with a Time attribute" do
110
- let(:pouchy) { make_pouchy(:foo, type: Time) }
114
+ it "preserves the type" do
115
+ pouchy.update(foo: 2.78)
116
+ pouchy.reload
117
+ expect(pouchy.foo).to eq(2.78)
118
+ end
119
+ end
111
120
 
112
- it "preserves the type" do
113
- now = Time.now
114
- pouchy.update(foo: now)
115
- pouchy.reload
116
- expect(pouchy.foo).to eq(now)
117
- end
118
- end
121
+ context "with a boolean attribute" do
122
+ let(:pouchy) { make_pouchy(:foo, type: :bool) }
119
123
 
120
- context "with a Sequel::Model attribute" do
121
- let(:model_class) { Class.new(Sequel::Model(:items)) }
122
- let(:pouchy) { make_pouchy(:foo, type: model_class) }
124
+ it "preserves the type" do
125
+ pouchy.update(foo: true)
126
+ pouchy.reload
127
+ expect(pouchy.foo).to be true
128
+ end
129
+ end
123
130
 
124
- it "preserves the type" do
125
- new_model = model_class.create
126
- pouchy.update(foo: new_model)
127
- pouchy.reload
128
- expect(pouchy.foo).to be_a(model_class)
129
- expect(pouchy.foo.id).to eq(new_model.id)
130
- end
131
- end
131
+ context "with a Time attribute" do
132
+ let(:pouchy) { make_pouchy(:foo, type: Time) }
132
133
 
133
- context "with a Sequel::Model attribute provided as a String" do
134
- let(:model_class) { module A; class B < Sequel::Model(:items); end; end; A::B }
135
- let(:pouchy) { make_pouchy(:foo, type: model_class.name) }
134
+ it "preserves the type" do
135
+ now = Time.now
136
+ pouchy.update(foo: now)
137
+ pouchy.reload
138
+ expect(pouchy.foo).to eq(now)
139
+ end
140
+ end
136
141
 
137
- it "preserves the type" do
138
- new_model = model_class.create
139
- pouchy.update(foo: new_model)
140
- pouchy.reload
141
- expect(pouchy.foo).to be_a(model_class)
142
- expect(pouchy.foo.id).to eq(new_model.id)
143
- end
144
- end
142
+ context "with a Sequel::Model attribute" do
143
+ let(:model_class) { Class.new(Sequel::Model(:items)) }
144
+ let(:pouchy) { make_pouchy(:foo, type: model_class) }
145
145
 
146
- context "with an attribute that is not a simple method name" do
147
- it "raises an error when defining the class" do
148
- expect do
149
- make_pouchy(:"nope, not valid", type: String)
150
- end.to raise_error(AttrPouch::InvalidFieldError)
151
- end
152
- end
146
+ it "preserves the type" do
147
+ new_model = model_class.create
148
+ pouchy.update(foo: new_model)
149
+ pouchy.reload
150
+ expect(pouchy.foo).to be_a(model_class)
151
+ expect(pouchy.foo.id).to eq(new_model.id)
152
+ end
153
+ end
153
154
 
154
- context "with an attribute name that ends in a question mark" do
155
- let(:pouchy) { make_pouchy(:foo?, type: :bool) }
155
+ context "with a Sequel::Model attribute provided as a String" do
156
+ let(:model_class) do
157
+ module A; class B < Sequel::Model(:items); end; end; A::B
158
+ end
159
+ let(:pouchy) { make_pouchy(:foo, type: model_class.name) }
160
+
161
+ it "preserves the type" do
162
+ new_model = model_class.create
163
+ pouchy.update(foo: new_model)
164
+ pouchy.reload
165
+ expect(pouchy.foo).to be_a(model_class)
166
+ expect(pouchy.foo.id).to eq(new_model.id)
167
+ end
168
+ end
156
169
 
157
- it "generates normal getter" do
158
- pouchy.attrs = Sequel.hstore(foo?: true)
159
- expect(pouchy.foo?).to be true
160
- end
170
+ context "with an attribute that is not a simple method name" do
171
+ it "raises an error when defining the class" do
172
+ expect do
173
+ make_pouchy(:"nope, not valid", type: String)
174
+ end.to raise_error(AttrPouch::InvalidFieldError)
175
+ end
176
+ end
161
177
 
162
- it "generates setter by stripping trailing question mark" do
163
- pouchy.foo = true
164
- expect(pouchy.foo?).to be true
165
- end
166
- end
178
+ context "with an attribute name that ends in a question mark" do
179
+ let(:pouchy) { make_pouchy(:foo?, type: :bool) }
167
180
 
168
- context "with multiple attributes" do
169
- let(:bepouched) do
170
- Class.new(Sequel::Model(:items)) do
171
- include AttrPouch
181
+ it "generates normal getter" do
182
+ pouchy[column_name] = wrap_hash(foo?: true)
183
+ expect(pouchy.foo?).to be true
184
+ end
172
185
 
173
- pouch(:attrs) do
174
- field :f1, type: String
175
- field :f2, type: :bool
176
- field :f3, type: Integer
186
+ it "generates setter by stripping trailing question mark" do
187
+ pouchy.foo = true
188
+ expect(pouchy.foo?).to be true
177
189
  end
178
190
  end
179
- end
180
- let(:pouchy) { bepouched.create }
181
191
 
182
- it "allows updating multiple attributes simultaneously" do
183
- pouchy.update(f1: 'hello', f2: true, f3: 42)
184
- expect(pouchy.f1).to eq('hello')
185
- expect(pouchy.f2).to eq(true)
186
- expect(pouchy.f3).to eq(42)
187
- end
192
+ context "with multiple attributes" do
193
+ let(:bepouched) do
194
+ col_name = column_name
188
195
 
189
- it "allows updating multiple attributes sequentially" do
190
- pouchy.f1 = 'hello'
191
- pouchy.f2 = true
192
- pouchy.f3 = 42
193
- pouchy.save_changes
194
- pouchy.reload
195
- expect(pouchy.f1).to eq('hello')
196
- expect(pouchy.f2).to eq(true)
197
- expect(pouchy.f3).to eq(42)
198
- end
199
- end
196
+ Class.new(Sequel::Model(:items)) do
197
+ include AttrPouch
200
198
 
201
- context "with the default option" do
202
- let(:pouchy) { make_pouchy(:foo, type: String, default: 'hello') }
203
-
204
- it "returns the default if the key is absent" do
205
- expect(pouchy.foo).to eq('hello')
206
- end
207
-
208
- it "returns the value if the key is present" do
209
- pouchy.update(foo: 'goodbye')
210
- expect(pouchy.foo).to eq('goodbye')
211
- end
199
+ pouch(col_name) do
200
+ field :f1, type: String
201
+ field :f2, type: :bool
202
+ field :f3, type: Integer
203
+ end
204
+ end
205
+ end
206
+ let(:pouchy) { bepouched.create }
212
207
 
213
- context "with the deletable option" do
214
- let(:pouchy) { make_pouchy(:foo, type: String,
215
- default: 'hello',
216
- deletable: true) }
208
+ it "allows updating multiple attributes simultaneously" do
209
+ pouchy.update(f1: 'hello', f2: true, f3: 42)
210
+ expect(pouchy.f1).to eq('hello')
211
+ expect(pouchy.f2).to eq(true)
212
+ expect(pouchy.f3).to eq(42)
213
+ end
217
214
 
218
- it "it returns the default if the key is absent" do
219
- expect(pouchy.foo).to eq('hello')
215
+ it "allows updating multiple attributes sequentially" do
216
+ pouchy.f1 = 'hello'
217
+ pouchy.f2 = true
218
+ pouchy.f3 = 42
219
+ pouchy.save_changes
220
+ pouchy.reload
221
+ expect(pouchy.f1).to eq('hello')
222
+ expect(pouchy.f2).to eq(true)
223
+ expect(pouchy.f3).to eq(42)
224
+ end
220
225
  end
221
226
 
222
- it "it returns the default after the field has been deleted" do
223
- pouchy.update(foo: 'goodbye')
224
- expect(pouchy.foo).to eq('goodbye')
225
- pouchy.delete_foo
226
- expect(pouchy.foo).to eq('hello')
227
- end
228
- end
229
- end
227
+ context "with the default option" do
228
+ let(:pouchy) { make_pouchy(:foo, type: String, default: 'hello') }
230
229
 
231
- context "with the deletable option" do
232
- let(:pouchy) { make_pouchy(:foo, type: Integer, deletable: true) }
230
+ it "returns the default if the key is absent" do
231
+ expect(pouchy.foo).to eq('hello')
232
+ end
233
233
 
234
- it "is nil if the field is absent" do
235
- expect(pouchy.foo).to be_nil
236
- end
234
+ it "returns the value if the key is present" do
235
+ pouchy.update(foo: 'goodbye')
236
+ expect(pouchy.foo).to eq('goodbye')
237
+ end
237
238
 
238
- it "supports deleting existing fields" do
239
- pouchy.update(foo: 42)
240
- expect(pouchy.foo).to eq(42)
241
- pouchy.delete_foo
242
- expect(pouchy.attrs).not_to have_key(:foo)
243
- pouchy.reload
244
- expect(pouchy.foo).to eq(42)
245
- end
239
+ context "with the deletable option" do
240
+ let(:pouchy) { make_pouchy(:foo, type: String,
241
+ default: 'hello',
242
+ deletable: true) }
243
+
244
+ it "it returns the default if the key is absent" do
245
+ expect(pouchy.foo).to eq('hello')
246
+ end
247
+
248
+ it "it returns the default after the field has been deleted" do
249
+ pouchy.update(foo: 'goodbye')
250
+ expect(pouchy.foo).to eq('goodbye')
251
+ pouchy.delete_foo
252
+ expect(pouchy.foo).to eq('hello')
253
+ end
254
+ end
255
+ end
246
256
 
247
- it "supports deleting existing fields and immediately persisting changes" do
248
- pouchy.update(foo: 42)
249
- expect(pouchy.foo).to eq(42)
250
- pouchy.delete_foo!
251
- expect(pouchy.attrs).not_to have_key(:foo)
252
- pouchy.reload
253
- expect(pouchy.attrs).not_to have_key(:foo)
254
- end
257
+ context "with the deletable option" do
258
+ let(:pouchy) { make_pouchy(:foo, type: Integer, deletable: true) }
255
259
 
256
- it "ignores deleting absent fields" do
257
- expect(pouchy.attrs).not_to have_key(:foo)
258
- pouchy.delete_foo
259
- expect(pouchy.attrs).not_to have_key(:foo)
260
- end
260
+ it "is nil if the field is absent" do
261
+ expect(pouchy.foo).to be_nil
262
+ end
261
263
 
262
- it "also deletes aliases from the was option" do
263
- pouchy = make_pouchy(:foo, type: Integer, deletable: true, was: :bar)
264
+ it "supports deleting existing fields" do
265
+ pouchy.update(foo: 42)
266
+ expect(pouchy.foo).to eq(42)
267
+ pouchy.delete_foo
268
+ expect(pouchy[column_name]).not_to have_key(:foo)
269
+ pouchy.reload
270
+ expect(pouchy.foo).to eq(42)
271
+ end
264
272
 
265
- pouchy.update(attrs: Sequel.hstore(bar: 42))
266
- expect(pouchy.foo).to eq(42)
267
- pouchy.delete_foo
268
- expect(pouchy.attrs).not_to have_key(:bar)
269
- end
270
- end
273
+ it "supports deleting existing fields and immediately persisting changes" do
274
+ pouchy.update(foo: 42)
275
+ expect(pouchy.foo).to eq(42)
276
+ pouchy.delete_foo!
277
+ expect(pouchy[column_name]).not_to have_key(:foo)
278
+ pouchy.reload
279
+ expect(pouchy[column_name]).not_to have_key(:foo)
280
+ end
271
281
 
272
- context "with the mutable option" do
273
- let(:pouchy) { make_pouchy(:foo, type: Integer, mutable: false) }
282
+ it "ignores deleting absent fields" do
283
+ expect(pouchy[column_name]).not_to have_key(:foo)
284
+ pouchy.delete_foo
285
+ expect(pouchy[column_name]).not_to have_key(:foo)
286
+ end
274
287
 
275
- it "it allows setting the field value for the first time" do
276
- pouchy.update(foo: 42)
277
- end
288
+ it "also deletes aliases from the was option" do
289
+ pouchy = make_pouchy(:foo, type: Integer, deletable: true, was: :bar)
278
290
 
279
- it "forbids subsequent modifications to the field" do
280
- pouchy.update(foo: 42)
281
- expect do
282
- pouchy.update(foo: 43)
283
- end.to raise_error(AttrPouch::ImmutableFieldUpdateError)
284
- end
285
- end
291
+ pouchy.update(column_name => wrap_hash(bar: 42))
292
+ expect(pouchy.foo).to eq(42)
293
+ pouchy.delete_foo
294
+ expect(pouchy[column_name]).not_to have_key(:bar)
295
+ end
296
+ end
286
297
 
287
- context "with the was option" do
288
- let(:pouchy) { make_pouchy(:foo, type: String, was: %w(bar baz)) }
298
+ context "with the mutable option" do
299
+ let(:pouchy) { make_pouchy(:foo, type: Integer, mutable: false) }
289
300
 
290
- it "supports aliases for renaming fields" do
291
- pouchy.update(attrs: Sequel.hstore(bar: 'hello'))
292
- expect(pouchy.foo).to eq('hello')
293
- end
301
+ it "it allows setting the field value for the first time" do
302
+ pouchy.update(foo: 42)
303
+ end
294
304
 
295
- it "supports multiple aliases" do
296
- pouchy.update(attrs: Sequel.hstore(baz: 'hello'))
297
- expect(pouchy.foo).to eq('hello')
298
- end
305
+ it "forbids subsequent modifications to the field" do
306
+ pouchy.update(foo: 42)
307
+ expect do
308
+ pouchy.update(foo: 43)
309
+ end.to raise_error(AttrPouch::ImmutableFieldUpdateError)
310
+ end
311
+ end
299
312
 
300
- it "deletes old names when writing the current one" do
301
- pouchy.update(attrs: Sequel.hstore(bar: 'hello'))
302
- pouchy.update(foo: 'goodbye')
303
- expect(pouchy.attrs).not_to have_key(:bar)
304
- end
313
+ context "with the was option" do
314
+ let(:pouchy) { make_pouchy(:foo, type: String, was: %w(bar baz)) }
305
315
 
306
- it "supports a shorthand for the single-alias case" do
307
- pouchy = make_pouchy(:foo, type: String, was: :bar)
308
- pouchy.update(attrs: Sequel.hstore(bar: 'hello'))
309
- expect(pouchy.foo).to eq('hello')
310
- end
311
- end
316
+ it "supports aliases for renaming fields" do
317
+ pouchy.update(column_name => wrap_hash(bar: 'hello'))
318
+ expect(pouchy.foo).to eq('hello')
319
+ end
312
320
 
313
- context "with the raw_field option" do
314
- let(:pouchy) { make_pouchy(:foo, type: Float, raw_field: :raw_foo) }
321
+ it "supports multiple aliases" do
322
+ pouchy.update(column_name => wrap_hash(baz: 'hello'))
323
+ expect(pouchy.foo).to eq('hello')
324
+ end
315
325
 
316
- it "supports direct access to the encoded value" do
317
- pouchy.update(foo: 2.78)
318
- expect(pouchy.raw_foo).to eq('2.78')
319
- end
326
+ it "deletes old names when writing the current one" do
327
+ pouchy.update(column_name => wrap_hash(bar: 'hello'))
328
+ pouchy.update(foo: 'goodbye')
329
+ expect(pouchy[column_name]).not_to have_key(:bar)
330
+ end
320
331
 
321
- it "is required when read" do
322
- expect do
323
- pouchy.raw_foo
324
- end.to raise_error(AttrPouch::MissingRequiredFieldError)
325
- end
332
+ it "supports a shorthand for the single-alias case" do
333
+ pouchy = make_pouchy(:foo, type: String, was: :bar)
334
+ pouchy.update(column_name => wrap_hash(bar: 'hello'))
335
+ expect(pouchy.foo).to eq('hello')
336
+ end
337
+ end
326
338
 
327
- it "avoids marking the field as modified if it is not changing" do
328
- pouchy.raw_foo = 'bar'
329
- expect(pouchy.save_changes).to_not be_nil
330
- pouchy.raw_foo = 'bar'
331
- expect(pouchy.save_changes).to be_nil
332
- end
339
+ context "with the raw_field option" do
340
+ let(:pouchy) { make_pouchy(:foo, type: Float, raw_field: :raw_foo) }
333
341
 
334
- it "obeys the 'mutable' option" do
335
- pouchy = make_pouchy(:foo, type: Float, raw_field: :raw_foo, mutable: false)
336
- pouchy.update(foo: 42)
337
- expect do
338
- pouchy.update(foo: 43)
339
- end.to raise_error(AttrPouch::ImmutableFieldUpdateError)
340
- end
342
+ it "supports direct access to the encoded value" do
343
+ pouchy.update(foo: 2.78)
344
+ expect(pouchy.raw_foo).to eq('2.78')
345
+ end
341
346
 
342
- it "is nil when the 'default' option is present" do
343
- pouchy = make_pouchy(:foo, type: Float, raw_field: :raw_foo, default: 7.2)
344
- expect(pouchy.raw_foo).to be_nil
345
- end
347
+ it "is required when read" do
348
+ expect do
349
+ pouchy.raw_foo
350
+ end.to raise_error(AttrPouch::MissingRequiredFieldError)
351
+ end
346
352
 
347
- it "obeys the 'was' option when reading" do
348
- pouchy = make_pouchy(:foo, type: String, raw_field: :raw_foo, was: :bar)
349
- pouchy.attrs = Sequel.hstore(bar: 'hello')
350
- expect(pouchy.raw_foo).to eq('hello')
351
- end
353
+ it "avoids marking the field as modified if it is not changing" do
354
+ pouchy.raw_foo = 'bar'
355
+ expect(pouchy.save_changes).to_not be_nil
356
+ pouchy.raw_foo = 'bar'
357
+ expect(pouchy.save_changes).to be_nil
358
+ end
352
359
 
353
- it "obeys the 'was' option when writing" do
354
- pouchy = make_pouchy(:foo, type: String, raw_field: :raw_foo, was: :bar)
355
- pouchy.attrs = Sequel.hstore(bar: 'hello')
356
- pouchy.update(raw_foo: 'goodbye')
357
- expect(pouchy.attrs).not_to have_key(:bar)
358
- end
359
- end
360
+ it "obeys the 'mutable' option" do
361
+ pouchy = make_pouchy(:foo, type: Float,
362
+ raw_field: :raw_foo,
363
+ mutable: false)
364
+ pouchy.update(foo: 42)
365
+ expect do
366
+ pouchy.update(foo: 43)
367
+ end.to raise_error(AttrPouch::ImmutableFieldUpdateError)
368
+ end
360
369
 
361
- context "inferring field types" do
362
- it "infers field named num_foo to be of type Integer" do
363
- pouchy = make_pouchy(:num_foo)
364
- pouchy.update(num_foo: 42)
365
- expect(pouchy.num_foo).to eq(42)
366
- end
370
+ it "is nil when the 'default' option is present" do
371
+ pouchy = make_pouchy(:foo, type: Float, raw_field: :raw_foo, default: 7.2)
372
+ expect(pouchy.raw_foo).to be_nil
373
+ end
367
374
 
368
- it "infers field named foo_count to be of type Integer" do
369
- pouchy = make_pouchy(:foo_count)
370
- pouchy.update(foo_count: 42)
371
- expect(pouchy.foo_count).to eq(42)
372
- end
375
+ it "obeys the 'was' option when reading" do
376
+ pouchy = make_pouchy(:foo, type: String, raw_field: :raw_foo, was: :bar)
377
+ pouchy[column_name] = wrap_hash(bar: 'hello')
378
+ expect(pouchy.raw_foo).to eq('hello')
379
+ end
373
380
 
374
- it "infers field named foo_size to be of type Integer" do
375
- pouchy = make_pouchy(:foo_size)
376
- pouchy.update(foo_size: 42)
377
- expect(pouchy.foo_size).to eq(42)
378
- end
381
+ it "obeys the 'was' option when writing" do
382
+ pouchy = make_pouchy(:foo, type: String, raw_field: :raw_foo, was: :bar)
383
+ pouchy[column_name] = wrap_hash(bar: 'hello')
384
+ pouchy.update(raw_foo: 'goodbye')
385
+ expect(pouchy[column_name]).not_to have_key(:bar)
386
+ end
387
+ end
379
388
 
380
- it "infers field named foo? to be of type :bool" do
381
- pouchy = make_pouchy(:foo?)
382
- pouchy.update(foo: true)
383
- expect(pouchy.foo?).to be true
384
- end
389
+ context "inferring field types" do
390
+ it "infers field named num_foo to be of type Integer" do
391
+ pouchy = make_pouchy(:num_foo)
392
+ pouchy.update(num_foo: 42)
393
+ expect(pouchy.num_foo).to eq(42)
394
+ end
385
395
 
386
- it "infers field named foo_at to be of type Time" do
387
- now = Time.now
388
- pouchy = make_pouchy(:foo_at)
389
- pouchy.update(foo_at: now)
390
- expect(pouchy.foo_at).to eq(now)
391
- end
396
+ it "infers field named foo_count to be of type Integer" do
397
+ pouchy = make_pouchy(:foo_count)
398
+ pouchy.update(foo_count: 42)
399
+ expect(pouchy.foo_count).to eq(42)
400
+ end
392
401
 
393
- it "infers field named foo_by to be of type Time" do
394
- now = Time.now
395
- pouchy = make_pouchy(:foo_by)
396
- pouchy.update(foo_by: now)
397
- expect(pouchy.foo_by).to eq(now)
398
- end
402
+ it "infers field named foo_size to be of type Integer" do
403
+ pouchy = make_pouchy(:foo_size)
404
+ pouchy.update(foo_size: 42)
405
+ expect(pouchy.foo_size).to eq(42)
406
+ end
399
407
 
400
- it "infers field named foo to be of type String" do
401
- pouchy = make_pouchy(:foo)
402
- pouchy.update(foo: 'hello')
403
- expect(pouchy.foo).to eq('hello')
404
- end
405
- end
408
+ it "infers field named foo? to be of type :bool" do
409
+ pouchy = make_pouchy(:foo?)
410
+ pouchy.update(foo: true)
411
+ expect(pouchy.foo?).to be true
412
+ end
406
413
 
407
- context "with dataset methods" do
408
- let(:bepouched) do
409
- Class.new(Sequel::Model(:items)) do
410
- include AttrPouch
411
-
412
- pouch(:attrs) do
413
- field :f1, type: String
414
- field :f2, type: String
415
- field :f3, type: String
416
- field :f4, type: :rot13
417
- end
418
- end
419
- end
414
+ it "infers field named foo_at to be of type Time" do
415
+ now = Time.now
416
+ pouchy = make_pouchy(:foo_at)
417
+ pouchy.update(foo_at: now)
418
+ expect(pouchy.foo_at).to eq(now)
419
+ end
420
420
 
421
- def rot13(str)
422
- str.each_byte.map do |c|
423
- case c
424
- when 'a'.ord..('z'.ord - 13)
425
- c + 13
426
- when ('z'.ord - 13)..'z'.ord
427
- c - 13
421
+ it "infers field named foo_by to be of type Time" do
422
+ now = Time.now
423
+ pouchy = make_pouchy(:foo_by)
424
+ pouchy.update(foo_by: now)
425
+ expect(pouchy.foo_by).to eq(now)
428
426
  end
429
- end.map(&:chr).join
430
- end
431
427
 
432
- before do
433
- AttrPouch.configure do |config|
434
- config.encode(:rot13) { |f,v| rot13(v.to_s) }
435
- config.decode(:rot13) { |f,v| rot13(v) }
428
+ it "infers field named foo to be of type String" do
429
+ pouchy = make_pouchy(:foo)
430
+ pouchy.update(foo: 'hello')
431
+ expect(pouchy.foo).to eq('hello')
432
+ end
436
433
  end
437
- end
438
434
 
439
- it "finds the right item with a scalar field value" do
440
- pouchy = bepouched.create(f1: 'foo', f2: 'bar', f3: 'baz')
441
- bepouched.create(f1: 'bar', f2: 'foo', f3: 'baz') # *not* matching
442
- matching = bepouched.where_pouch(:attrs, f1: 'foo').all
443
- expect(matching.count).to eq(1)
444
- match = matching.first
445
- expect(match.id).to eq(pouchy.id)
446
- end
435
+ context "with dataset methods" do
436
+ let(:bepouched) do
437
+ col_name = column_name
447
438
 
448
- it "finds the right item with an array field value" do
449
- p1 = bepouched.create(f1: 'foo', f2: 'bar', f3: 'baz')
450
- p2 = bepouched.create(f1: 'bar', f2: 'foo', f3: 'baz')
451
- bepouched.create(f1: 'baz', f2: 'foo', f3: 'bar') # *not* matching
452
- matching = bepouched.where_pouch(:attrs, f1: %w(foo bar)).all
453
- expect(matching.count).to eq(2)
454
- expect(matching.map(&:id)).to include(p1.id, p2.id)
455
- end
439
+ Class.new(Sequel::Model(:items)) do
440
+ include AttrPouch
456
441
 
457
- it "finds the right item with a missing field value" do
458
- p1 = bepouched.create(f2: 'bar', f3: 'baz')
459
- bepouched.create(f1: '', f2: 'foo', f3: 'baz') # *not* matching
460
- bepouched.create(f1: 'baz', f2: 'foo', f3: 'bar') # *not* matching
461
- matching = bepouched.where_pouch(:attrs, f1: nil).all
462
- expect(matching.count).to eq(1)
463
- expect(matching.first.id).to eq(p1.id)
464
- end
442
+ pouch(col_name) do
443
+ field :f1, type: String
444
+ field :f2, type: String
445
+ field :f3, type: String
446
+ field :f4, type: :rot13
447
+ end
448
+ end
449
+ end
465
450
 
466
- it "finds the right item with a nil field value" do
467
- p1 = bepouched.create(attrs: Sequel.hstore(f1: nil))
468
- matching = bepouched.where_pouch(:attrs, f1: nil).all
469
- expect(matching.count).to eq(1)
470
- expect(matching.first.id).to eq(p1.id)
471
- end
451
+ def rot13(str)
452
+ str.each_byte.map do |c|
453
+ case c
454
+ when 'a'.ord..('z'.ord - 13)
455
+ c + 13
456
+ when ('z'.ord - 13)..'z'.ord
457
+ c - 13
458
+ end
459
+ end.map(&:chr).join
460
+ end
472
461
 
473
- it "uses the associated encoder for lookups" do
474
- encoded = rot13('hello')
475
- p1 = bepouched.create(f4: 'hello')
476
- expect(p1.attrs[:f4]).to eq(encoded) # nothing behind the curtain
477
- matching = bepouched.where_pouch(:attrs, f4: 'hello')
478
- expect(matching.count).to eq(1)
479
- expect(matching.first.id).to eq(p1.id)
462
+ before do
463
+ AttrPouch.configure do |config|
464
+ config.encode(:rot13) { |f,v| rot13(v.to_s) }
465
+ config.decode(:rot13) { |f,v| rot13(v) }
466
+ end
467
+ end
468
+
469
+ if storage_kind == :json
470
+ it "does not support dataset methods" do
471
+ pouchy = bepouched.create(f1: 'foo', f2: 'bar', f3: 'baz')
472
+ bepouched.create(f1: 'bar', f2: 'foo', f3: 'baz') # *not* matching
473
+ expect do
474
+ bepouched.where_pouch(column_name, f1: 'foo').all
475
+ end.to raise_error(::AttrPouch::UnsupportedError)
476
+ end
477
+ else
478
+ it "finds the right item with a scalar field value" do
479
+ pouchy = bepouched.create(f1: 'foo', f2: 'bar', f3: 'baz')
480
+ bepouched.create(f1: 'bar', f2: 'foo', f3: 'baz') # *not* matching
481
+ matching = bepouched.where_pouch(column_name, f1: 'foo').all
482
+ expect(matching.count).to eq(1)
483
+ match = matching.first
484
+ expect(match.id).to eq(pouchy.id)
485
+ end
486
+
487
+ it "finds the right item with an array field value" do
488
+ p1 = bepouched.create(f1: 'foo', f2: 'bar', f3: 'baz')
489
+ p2 = bepouched.create(f1: 'bar', f2: 'foo', f3: 'baz')
490
+ bepouched.create(f1: 'baz', f2: 'foo', f3: 'bar') # *not* matching
491
+ matching = bepouched.where_pouch(column_name, f1: %w(foo bar)).all
492
+ expect(matching.count).to eq(2)
493
+ expect(matching.map(&:id)).to include(p1.id, p2.id)
494
+ end
495
+
496
+ it "finds the right item with a missing field value" do
497
+ p1 = bepouched.create(f2: 'bar', f3: 'baz')
498
+ bepouched.create(f1: '', f2: 'foo', f3: 'baz') # *not* matching
499
+ bepouched.create(f1: 'baz', f2: 'foo', f3: 'bar') # *not* matching
500
+ matching = bepouched.where_pouch(column_name, f1: nil).all
501
+ expect(matching.count).to eq(1)
502
+ expect(matching.first.id).to eq(p1.id)
503
+ end
504
+
505
+ it "finds the right item with a nil field value" do
506
+ p1 = bepouched.create(column_name => wrap_hash(f1: nil))
507
+ matching = bepouched.where_pouch(column_name, f1: nil).all
508
+ expect(matching.count).to eq(1)
509
+ expect(matching.first.id).to eq(p1.id)
510
+ end
511
+
512
+ it "uses the associated encoder for lookups" do
513
+ encoded = rot13('hello')
514
+ p1 = bepouched.create(f4: 'hello')
515
+ expect(p1[column_name]['f4']).to eq(encoded) # nothing behind the curtain
516
+ matching = bepouched.where_pouch(column_name, f4: 'hello')
517
+ expect(matching.count).to eq(1)
518
+ expect(matching.first.id).to eq(p1.id)
519
+ end
520
+
521
+ context "using indexes" do
522
+ before do
523
+ bepouched.create(column_name => wrap_hash(f1: nil))
524
+ end
525
+
526
+ def plan_when_looking_for(values)
527
+ stmt = bepouched.where_pouch(column_name, f1: values).sql
528
+ db = bepouched.db
529
+ db.transaction do
530
+ db.run("SET LOCAL enable_seqscan = false")
531
+ db.fetch("EXPLAIN #{stmt}").all
532
+ end.map { |line| line.fetch(:"QUERY PLAN") }.join("\n")
533
+ end
534
+
535
+ it "uses index when looking for a single value" do
536
+ expect(plan_when_looking_for('hello')).to match(/index/i)
537
+ end
538
+
539
+ it "uses index when looking for multiple values" do
540
+ expect(plan_when_looking_for(%(hello world))).to match(/index/i)
541
+ end
542
+
543
+ xit "uses index when looking for a nil value" do
544
+ expect(plan_when_looking_for(nil)).to match(/index/i)
545
+ end
546
+ end
547
+ end
548
+ end
480
549
  end
481
550
  end
482
551
  end