Skip to content

Commit c2e09bf

Browse files
authored
Merge pull request #913 from Dynamoid/ak/fix-sending-partition-key-and-always-dump-it
Fix partition keys with Dynamoid-specific type and always dump them into a proper DynamoDB type
2 parents 3f23dbf + 342c575 commit c2e09bf

File tree

9 files changed

+259
-45
lines changed

9 files changed

+259
-45
lines changed

lib/dynamoid/finders.rb

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,16 @@ def find_by_id(id, options = {})
109109
def _find_all(ids, options = {})
110110
raise Errors::MissingRangeKey if range_key && ids.any? { |_pk, sk| sk.nil? }
111111

112-
if range_key
113-
ids = ids.map do |pk, sk|
114-
sk_casted = TypeCasting.cast_field(sk, attributes[range_key])
115-
sk_dumped = Dumping.dump_field(sk_casted, attributes[range_key])
116-
117-
[pk, sk_dumped]
112+
ids = ids.map do |id|
113+
if range_key
114+
# expect [hash key, range key] pair
115+
pk, sk = id
116+
pk_dumped = cast_and_dump(hash_key, pk)
117+
sk_dumped = cast_and_dump(range_key, sk)
118+
119+
[pk_dumped, sk_dumped]
120+
else
121+
cast_and_dump(hash_key, id)
118122
end
119123
end
120124

@@ -155,15 +159,13 @@ def _find_all(ids, options = {})
155159
def _find_by_id(id, options = {})
156160
raise Errors::MissingRangeKey if range_key && options[:range_key].nil?
157161

158-
if range_key
159-
key = options[:range_key]
160-
key_casted = TypeCasting.cast_field(key, attributes[range_key])
161-
key_dumped = Dumping.dump_field(key_casted, attributes[range_key])
162+
partition_key_dumped = cast_and_dump(hash_key, id)
162163

163-
options[:range_key] = key_dumped
164+
if range_key
165+
options[:range_key] = cast_and_dump(range_key, options[:range_key])
164166
end
165167

166-
if item = Dynamoid.adapter.read(table_name, id, options.slice(:range_key, :consistent_read))
168+
if item = Dynamoid.adapter.read(table_name, partition_key_dumped, options.slice(:range_key, :consistent_read))
167169
model = from_database(item)
168170
model.run_callbacks :find
169171
model
@@ -308,6 +310,14 @@ def method_missing(method, *args)
308310
super
309311
end
310312
end
313+
314+
private
315+
316+
def cast_and_dump(name, value)
317+
attribute_options = attributes[name]
318+
casted_value = TypeCasting.cast_field(value, attribute_options)
319+
Dumping.dump_field(casted_value, attribute_options)
320+
end
311321
end
312322
end
313323
end

lib/dynamoid/persistence.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -685,9 +685,10 @@ def update!(conditions = {})
685685

686686
begin
687687
table_name = self.class.table_name
688+
partition_key_dumped = Dumping.dump_field(hash_key, self.class.attributes[self.class.hash_key])
688689
update_item_options = options.merge(conditions: conditions)
689690

690-
new_attrs = Dynamoid.adapter.update_item(table_name, hash_key, update_item_options) do |t|
691+
new_attrs = Dynamoid.adapter.update_item(table_name, partition_key_dumped, update_item_options) do |t|
691692
item_updater = ItemUpdaterWithDumping.new(self.class, t)
692693

693694
item_updater.add(lock_version: 1) if self.class.attributes[:lock_version]
@@ -926,6 +927,7 @@ def destroy!
926927
# @since 0.2.0
927928
def delete
928929
options = range_key ? { range_key: Dumping.dump_field(read_attribute(range_key), self.class.attributes[range_key]) } : {}
930+
partition_key_dumped = Dumping.dump_field(hash_key, self.class.attributes[self.class.hash_key])
929931

930932
# Add an optimistic locking check if the lock_version column exists
931933
if self.class.attributes[:lock_version]
@@ -941,7 +943,7 @@ def delete
941943

942944
@destroyed = true
943945

944-
Dynamoid.adapter.delete(self.class.table_name, hash_key, options)
946+
Dynamoid.adapter.delete(self.class.table_name, partition_key_dumped, options)
945947

946948
self.class.associations.each_key do |name|
947949
send(name).disassociate_source

lib/dynamoid/persistence/inc.rb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ def initialize(model_class, hash_key, range_key = nil, counters)
2121

2222
def call
2323
touch = @counters.delete(:touch)
24+
hash_key_dumped = cast_and_dump(@model_class.hash_key, @hash_key)
2425

25-
Dynamoid.adapter.update_item(@model_class.table_name, @hash_key, update_item_options) do |t|
26+
Dynamoid.adapter.update_item(@model_class.table_name, hash_key_dumped, update_item_options) do |t|
2627
item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
2728

2829
@counters.each do |name, value|
@@ -43,10 +44,8 @@ def call
4344

4445
def update_item_options
4546
if @model_class.range_key
46-
range_key_options = @model_class.attributes[@model_class.range_key]
47-
value_casted = TypeCasting.cast_field(@range_key, range_key_options)
48-
value_dumped = Dumping.dump_field(value_casted, range_key_options)
49-
{ range_key: value_dumped }
47+
range_key_dumped = cast_and_dump(@model_class.range_key, @range_key)
48+
{ range_key: range_key_dumped }
5049
else
5150
{}
5251
end
@@ -60,6 +59,12 @@ def timestamp_attributes_to_touch(touch)
6059
names += Array.wrap(touch) if touch != true
6160
names
6261
end
62+
63+
def cast_and_dump(name, value)
64+
options = @model_class.attributes[name]
65+
value_casted = TypeCasting.cast_field(value, options)
66+
Dumping.dump_field(value_casted, options)
67+
end
6368
end
6469
end
6570
end

lib/dynamoid/persistence/save.rb

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ def call
3636
Dynamoid.adapter.write(@model.class.table_name, attributes_dumped, conditions_for_write)
3737
else
3838
attributes_to_persist = @model.attributes.slice(*@model.changed.map(&:to_sym))
39+
partition_key_dumped = dump(@model.class.hash_key, @model.hash_key)
40+
options = options_to_update_item(partition_key_dumped)
3941

40-
Dynamoid.adapter.update_item(@model.class.table_name, @model.hash_key, options_to_update_item) do |t|
42+
Dynamoid.adapter.update_item(@model.class.table_name, partition_key_dumped, options) do |t|
4143
item_updater = ItemUpdaterWithDumping.new(@model.class, t)
4244

4345
attributes_to_persist.each do |name, value|
@@ -63,11 +65,9 @@ def conditions_for_write
6365
conditions = {}
6466

6567
# Add an 'exists' check to prevent overwriting existing records with new ones
66-
if @model.new_record?
67-
conditions[:unless_exists] = [@model.class.hash_key]
68-
if @model.range_key
69-
conditions[:unless_exists] << @model.range_key
70-
end
68+
conditions[:unless_exists] = [@model.class.hash_key]
69+
if @model.range_key
70+
conditions[:unless_exists] << @model.range_key
7171
end
7272

7373
# Add an optimistic locking check if the lock_version column exists
@@ -81,17 +81,17 @@ def conditions_for_write
8181
conditions
8282
end
8383

84-
def options_to_update_item
84+
def options_to_update_item(partition_key_dumped)
8585
options = {}
8686

8787
if @model.class.range_key
88-
value_dumped = Dumping.dump_field(@model.range_value, @model.class.attributes[@model.class.range_key])
88+
value_dumped = dump(@model.class.range_key, @model.range_value)
8989
options[:range_key] = value_dumped
9090
end
9191

9292
conditions = {}
9393
conditions[:if] ||= {}
94-
conditions[:if][@model.class.hash_key] = @model.hash_key
94+
conditions[:if][@model.class.hash_key] = partition_key_dumped
9595

9696
# Add an optimistic locking check if the lock_version column exists
9797
# Uses the original lock_version value from Dirty API
@@ -105,6 +105,11 @@ def options_to_update_item
105105

106106
options
107107
end
108+
109+
def dump(name, value)
110+
options = @model.class.attributes[name]
111+
Dumping.dump_field(value, options)
112+
end
108113
end
109114
end
110115
end

lib/dynamoid/persistence/update_fields.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ def initialize(model_class, partition_key:, sort_key:, attributes:, conditions:)
1616
@sort_key = sort_key
1717
@attributes = attributes.symbolize_keys
1818
@conditions = conditions
19+
20+
@partition_key_dumped = cast_and_dump(@model_class.hash_key, @partition_key)
1921
end
2022

2123
def call
@@ -33,7 +35,7 @@ def call
3335
private
3436

3537
def update_item
36-
Dynamoid.adapter.update_item(@model_class.table_name, @partition_key, options_to_update_item) do |t|
38+
Dynamoid.adapter.update_item(@model_class.table_name, @partition_key_dumped, options_to_update_item) do |t|
3739
item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
3840

3941
@attributes.each do |k, v|
@@ -50,18 +52,23 @@ def options_to_update_item
5052
options = {}
5153

5254
if @model_class.range_key
53-
value_casted = TypeCasting.cast_field(@sort_key, @model_class.attributes[@model_class.range_key])
54-
value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[@model_class.range_key])
55-
options[:range_key] = value_dumped
55+
range_key_dumped = cast_and_dump(@model_class.range_key, @sort_key)
56+
options[:range_key] = range_key_dumped
5657
end
5758

5859
conditions = @conditions.deep_dup
5960
conditions[:if] ||= {}
60-
conditions[:if][@model_class.hash_key] = @partition_key
61+
conditions[:if][@model_class.hash_key] = @partition_key_dumped
6162
options[:conditions] = conditions
6263

6364
options
6465
end
66+
67+
def cast_and_dump(name, value)
68+
options = @model_class.attributes[name]
69+
value_casted = TypeCasting.cast_field(value, options)
70+
Dumping.dump_field(value_casted, options)
71+
end
6572
end
6673
end
6774
end

lib/dynamoid/persistence/upsert.rb

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ def call
3333
private
3434

3535
def update_item
36-
Dynamoid.adapter.update_item(@model_class.table_name, @partition_key, options_to_update_item) do |t|
36+
partition_key_dumped = cast_and_dump(@model_class.hash_key, @partition_key)
37+
38+
Dynamoid.adapter.update_item(@model_class.table_name, partition_key_dumped, options_to_update_item) do |t|
3739
item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
3840

3941
@attributes.each do |k, v|
@@ -46,9 +48,8 @@ def options_to_update_item
4648
options = {}
4749

4850
if @model_class.range_key
49-
value_casted = TypeCasting.cast_field(@sort_key, @model_class.attributes[@model_class.range_key])
50-
value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[@model_class.range_key])
51-
options[:range_key] = value_dumped
51+
range_key_dumped = cast_and_dump(@model_class.range_key, @sort_key)
52+
options[:range_key] = range_key_dumped
5253
end
5354

5455
options[:conditions] = @conditions
@@ -58,6 +59,12 @@ def options_to_update_item
5859
def undump_attributes(raw_attributes)
5960
Undumping.undump_attributes(raw_attributes, @model_class.attributes)
6061
end
62+
63+
def cast_and_dump(name, value)
64+
options = @model_class.attributes[name]
65+
value_casted = TypeCasting.cast_field(value, options)
66+
Dumping.dump_field(value_casted, options)
67+
end
6168
end
6269
end
6370
end

spec/dynamoid/criteria/chain_spec.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,36 @@ def request_params
256256
model.where(name: 'Bob', 'age.foo': 10).to_a
257257
end.to raise_error(Dynamoid::Errors::Error, 'Unsupported operator foo in age.foo')
258258
end
259+
260+
context 'primary key dumping' do
261+
it 'uses dumped value of partition key to query item' do
262+
klass = new_class(partition_key: { name: :published_on, type: :date })
263+
264+
obj1 = klass.create(published_on: Date.today + 1)
265+
obj2 = klass.create(published_on: Date.today + 2)
266+
267+
chain = described_class.new(klass)
268+
expect(chain).to receive(:raw_pages_via_query).and_call_original
269+
270+
objects_found = chain.where(published_on: obj1.published_on).all
271+
expect(objects_found).to contain_exactly(obj1)
272+
end
273+
274+
it 'uses dumped value of sort key to query item' do
275+
klass = new_class do
276+
range :published_on, :date
277+
end
278+
279+
obj1 = klass.create(published_on: Date.today + 1)
280+
obj2 = klass.create(published_on: Date.today + 2)
281+
282+
chain = described_class.new(klass)
283+
expect(chain).to receive(:raw_pages_via_query).and_call_original
284+
285+
objects_found = chain.where(id: obj1.id, published_on: obj1.published_on).all
286+
expect(objects_found).to contain_exactly(obj1)
287+
end
288+
end
259289
end
260290

261291
# http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.QueryFilter.html

spec/dynamoid/finders_spec.rb

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
require 'spec_helper'
44

55
describe Dynamoid::Finders do
6-
let!(:address) { Address.create(city: 'Chicago') }
7-
86
describe '.find' do
97
let(:klass) do
108
new_class(class_name: 'Document')
@@ -111,6 +109,22 @@
111109
end
112110
end
113111
end
112+
113+
it 'uses dumped value of partition key to update item' do
114+
klass = new_class(partition_key: { name: :published_on, type: :date })
115+
116+
obj = klass.create!(published_on: '2018-10-07'.to_date)
117+
expect(klass.find(obj.published_on)).to eql(obj)
118+
end
119+
120+
it 'uses dumped value of sort key to update item' do
121+
klass = new_class do
122+
range :published_on, :date
123+
end
124+
125+
obj = klass.create!(published_on: '2018-10-07'.to_date)
126+
expect(klass.find(obj.id, range_key: obj.published_on)).to eql(obj)
127+
end
114128
end
115129

116130
context 'multiple primary keys provided' do
@@ -270,6 +284,26 @@
270284
end
271285
end
272286

287+
it 'uses dumped value of partition key to update item' do
288+
klass = new_class(partition_key: { name: :published_on, type: :date })
289+
objects = (1..2).map { |i| klass.create(published_on: '2018-10-07'.to_date + i) }
290+
obj1, obj2 = objects
291+
292+
expect(klass.find([obj1.published_on, obj2.published_on])).to match_array(objects)
293+
end
294+
295+
it 'uses dumped value of sort key to update item' do
296+
klass = new_class do
297+
range :published_on, :date
298+
end
299+
300+
objects = (1..2).map { |i| klass.create(published_on: '2018-10-07'.to_date + i) }
301+
obj1, obj2 = objects
302+
found_objects = klass.find([[obj1.id, obj1.published_on], [obj2.id, obj2.published_on]])
303+
304+
expect(found_objects).to match_array(objects)
305+
end
306+
273307
context 'field is not declared in document' do
274308
let(:class_with_not_declared_field) do
275309
new_class do
@@ -386,6 +420,8 @@
386420
end
387421

388422
it 'sends consistent option to the adapter' do
423+
address = Address.create(city: 'Chicago')
424+
389425
expect(Dynamoid.adapter).to receive(:get_item)
390426
.with(anything, anything, hash_including(consistent_read: true))
391427
.and_call_original
@@ -394,6 +430,7 @@
394430

395431
context 'with users' do
396432
it 'finds using method_missing for attributes' do
433+
address = Address.create(city: 'Chicago')
397434
array = Address.find_by_city('Chicago')
398435

399436
expect(array).to eq address

0 commit comments

Comments
 (0)