aws-record 2.10.1 → 2.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +83 -19
- data/VERSION +1 -1
- data/lib/aws-record/record/attribute.rb +8 -8
- data/lib/aws-record/record/attributes.rb +36 -49
- data/lib/aws-record/record/batch.rb +13 -12
- data/lib/aws-record/record/batch_read.rb +10 -12
- data/lib/aws-record/record/batch_write.rb +2 -1
- data/lib/aws-record/record/buildable_search.rb +37 -39
- data/lib/aws-record/record/client_configuration.rb +14 -14
- data/lib/aws-record/record/dirty_tracking.rb +29 -40
- data/lib/aws-record/record/errors.rb +11 -2
- data/lib/aws-record/record/item_collection.rb +7 -7
- data/lib/aws-record/record/item_data.rb +13 -17
- data/lib/aws-record/record/item_operations.rb +150 -138
- data/lib/aws-record/record/key_attributes.rb +0 -2
- data/lib/aws-record/record/marshalers/boolean_marshaler.rb +2 -5
- data/lib/aws-record/record/marshalers/date_marshaler.rb +1 -6
- data/lib/aws-record/record/marshalers/date_time_marshaler.rb +2 -5
- data/lib/aws-record/record/marshalers/epoch_time_marshaler.rb +2 -8
- data/lib/aws-record/record/marshalers/float_marshaler.rb +3 -8
- data/lib/aws-record/record/marshalers/integer_marshaler.rb +3 -8
- data/lib/aws-record/record/marshalers/list_marshaler.rb +4 -7
- data/lib/aws-record/record/marshalers/map_marshaler.rb +4 -7
- data/lib/aws-record/record/marshalers/numeric_set_marshaler.rb +7 -9
- data/lib/aws-record/record/marshalers/string_marshaler.rb +1 -2
- data/lib/aws-record/record/marshalers/string_set_marshaler.rb +5 -7
- data/lib/aws-record/record/marshalers/time_marshaler.rb +1 -5
- data/lib/aws-record/record/model_attributes.rb +17 -29
- data/lib/aws-record/record/query.rb +8 -11
- data/lib/aws-record/record/secondary_indexes.rb +40 -51
- data/lib/aws-record/record/table_config.rb +93 -115
- data/lib/aws-record/record/table_migration.rb +56 -72
- data/lib/aws-record/record/transactions.rb +40 -43
- data/lib/aws-record/record/version.rb +1 -1
- data/lib/aws-record/record.rb +36 -44
- metadata +13 -8
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
module Aws
|
4
4
|
module Record
|
5
|
-
|
6
5
|
# +Aws::Record::TableConfig+ provides a DSL for describing and modifying
|
7
6
|
# the remote configuration of your DynamoDB tables. A table configuration
|
8
7
|
# object can perform intelligent comparisons and incremental migrations
|
@@ -90,11 +89,9 @@ module Aws
|
|
90
89
|
# end
|
91
90
|
#
|
92
91
|
class TableConfig
|
93
|
-
|
94
92
|
attr_accessor :client
|
95
93
|
|
96
94
|
class << self
|
97
|
-
|
98
95
|
# Creates a new table configuration, using a DSL in the provided block.
|
99
96
|
# The DSL has the following methods:
|
100
97
|
# * +#model_class+ A class name reference to the +Aws::Record+ model
|
@@ -158,7 +155,7 @@ module Aws
|
|
158
155
|
def initialize
|
159
156
|
@client_options = {}
|
160
157
|
@global_secondary_indexes = {}
|
161
|
-
@billing_mode =
|
158
|
+
@billing_mode = 'PROVISIONED' # default
|
162
159
|
end
|
163
160
|
|
164
161
|
# @api private
|
@@ -191,16 +188,15 @@ module Aws
|
|
191
188
|
# @api private
|
192
189
|
def configure_client
|
193
190
|
@client = Aws::DynamoDB::Client.new(@client_options)
|
191
|
+
@client.config.user_agent_frameworks << 'aws-record'
|
194
192
|
end
|
195
193
|
|
196
194
|
# @api private
|
197
195
|
def ttl_attribute(attribute_symbol)
|
198
196
|
attribute = @model_class.attributes.attribute_for(attribute_symbol)
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
raise ArgumentError, "Invalid attribute #{attribute_symbol} for #{@model_class}"
|
203
|
-
end
|
197
|
+
raise ArgumentError, "Invalid attribute #{attribute_symbol} for #{@model_class}" unless attribute
|
198
|
+
|
199
|
+
@ttl_attribute = attribute.database_name
|
204
200
|
end
|
205
201
|
|
206
202
|
# @api private
|
@@ -231,9 +227,9 @@ module Aws
|
|
231
227
|
unless _gsi_superset(resp)
|
232
228
|
@client.update_table(_update_index_opts(resp))
|
233
229
|
@client.wait_until(
|
234
|
-
|
235
|
-
|
236
|
-
|
230
|
+
:table_exists,
|
231
|
+
table_name: @model_class.table_name
|
232
|
+
)
|
237
233
|
end
|
238
234
|
end
|
239
235
|
rescue DynamoDB::Errors::ResourceNotFoundException
|
@@ -247,17 +243,18 @@ module Aws
|
|
247
243
|
# First up is TTL attribute. Since this migration is not exact match,
|
248
244
|
# we will only alter TTL status if we have a TTL attribute defined. We
|
249
245
|
# may someday support explicit TTL deletion, but we do not yet do this.
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
246
|
+
return unless @ttl_attribute
|
247
|
+
return if _ttl_compatibility_check
|
248
|
+
|
249
|
+
client.update_time_to_live(
|
250
|
+
table_name: @model_class.table_name,
|
251
|
+
time_to_live_specification: {
|
252
|
+
enabled: true,
|
253
|
+
attribute_name: @ttl_attribute
|
254
|
+
}
|
255
|
+
)
|
256
|
+
# Else TTL is compatible and we are done.
|
257
|
+
# Else our work is done.
|
261
258
|
end
|
262
259
|
|
263
260
|
# Checks the remote table for compatibility. Similar to +#exact_match?+,
|
@@ -270,12 +267,10 @@ module Aws
|
|
270
267
|
#
|
271
268
|
# @return [Boolean] true if remote is compatible, false otherwise.
|
272
269
|
def compatible?
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
false
|
278
|
-
end
|
270
|
+
resp = @client.describe_table(table_name: @model_class.table_name)
|
271
|
+
_compatible_check(resp) && _ttl_compatibility_check
|
272
|
+
rescue DynamoDB::Errors::ResourceNotFoundException
|
273
|
+
false
|
279
274
|
end
|
280
275
|
|
281
276
|
# Checks against the remote table's configuration. If the remote table
|
@@ -285,26 +280,25 @@ module Aws
|
|
285
280
|
#
|
286
281
|
# @return [Boolean] true if remote is an exact match, false otherwise.
|
287
282
|
def exact_match?
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
false
|
297
|
-
end
|
283
|
+
resp = @client.describe_table(table_name: @model_class.table_name)
|
284
|
+
_throughput_equal(resp) &&
|
285
|
+
_keys_equal(resp) &&
|
286
|
+
_ad_equal(resp) &&
|
287
|
+
_gsi_equal(resp) &&
|
288
|
+
_ttl_match_check
|
289
|
+
rescue DynamoDB::Errors::ResourceNotFoundException
|
290
|
+
false
|
298
291
|
end
|
299
292
|
|
300
293
|
private
|
294
|
+
|
301
295
|
def _ttl_compatibility_check
|
302
296
|
if @ttl_attribute
|
303
297
|
ttl_status = @client.describe_time_to_live(
|
304
298
|
table_name: @model_class.table_name
|
305
299
|
)
|
306
300
|
desc = ttl_status.time_to_live_description
|
307
|
-
[
|
301
|
+
%w[ENABLED ENABLING].include?(desc.time_to_live_status) &&
|
308
302
|
desc.attribute_name == @ttl_attribute
|
309
303
|
else
|
310
304
|
true
|
@@ -317,11 +311,11 @@ module Aws
|
|
317
311
|
)
|
318
312
|
desc = ttl_status.time_to_live_description
|
319
313
|
if @ttl_attribute
|
320
|
-
[
|
314
|
+
%w[ENABLED ENABLING].include?(desc.time_to_live_status) &&
|
321
315
|
desc.attribute_name == @ttl_attribute
|
322
316
|
else
|
323
|
-
|
324
|
-
desc.attribute_name
|
317
|
+
!%w[ENABLED ENABLING].include?(desc.time_to_live_status) ||
|
318
|
+
desc.attribute_name.nil?
|
325
319
|
end
|
326
320
|
end
|
327
321
|
|
@@ -336,30 +330,25 @@ module Aws
|
|
336
330
|
opts = {
|
337
331
|
table_name: @model_class.table_name
|
338
332
|
}
|
339
|
-
if @billing_mode ==
|
333
|
+
if @billing_mode == 'PROVISIONED'
|
340
334
|
opts[:provisioned_throughput] = {
|
341
335
|
read_capacity_units: @read_capacity_units,
|
342
336
|
write_capacity_units: @write_capacity_units
|
343
337
|
}
|
344
|
-
elsif @billing_mode ==
|
338
|
+
elsif @billing_mode == 'PAY_PER_REQUEST'
|
345
339
|
opts[:billing_mode] = @billing_mode
|
346
340
|
else
|
347
341
|
raise ArgumentError, "Unsupported billing mode #{@billing_mode}"
|
348
342
|
end
|
349
|
-
|
350
343
|
opts[:key_schema] = _key_schema
|
351
344
|
opts[:attribute_definitions] = _attribute_definitions
|
352
345
|
gsi = _global_secondary_indexes
|
353
|
-
unless gsi.empty?
|
354
|
-
opts[:global_secondary_indexes] = gsi
|
355
|
-
end
|
346
|
+
opts[:global_secondary_indexes] = gsi unless gsi.empty?
|
356
347
|
opts
|
357
348
|
end
|
358
349
|
|
359
350
|
def _add_global_secondary_index_throughput(opts, resp_gsis)
|
360
|
-
gsis = resp_gsis.map
|
361
|
-
g.index_name
|
362
|
-
end
|
351
|
+
gsis = resp_gsis.map(&:index_name)
|
363
352
|
gsi_updates = []
|
364
353
|
gsis.each do |index_name|
|
365
354
|
lgsi = @global_secondary_indexes[index_name.to_sym]
|
@@ -375,7 +364,7 @@ module Aws
|
|
375
364
|
end
|
376
365
|
|
377
366
|
def _update_throughput_opts(resp)
|
378
|
-
if @billing_mode ==
|
367
|
+
if @billing_mode == 'PROVISIONED'
|
379
368
|
opts = {
|
380
369
|
table_name: @model_class.table_name,
|
381
370
|
provisioned_throughput: {
|
@@ -386,18 +375,18 @@ module Aws
|
|
386
375
|
# special case: we have global secondary indexes existing, and they
|
387
376
|
# need provisioned capacity to be set within this call
|
388
377
|
if !resp.table.billing_mode_summary.nil? &&
|
389
|
-
|
378
|
+
resp.table.billing_mode_summary.billing_mode == 'PAY_PER_REQUEST'
|
390
379
|
opts[:billing_mode] = @billing_mode
|
391
380
|
if resp.table.global_secondary_indexes
|
392
381
|
resp_gsis = resp.table.global_secondary_indexes
|
393
382
|
_add_global_secondary_index_throughput(opts, resp_gsis)
|
394
383
|
end
|
395
|
-
end
|
384
|
+
end
|
396
385
|
opts
|
397
|
-
elsif @billing_mode ==
|
386
|
+
elsif @billing_mode == 'PAY_PER_REQUEST'
|
398
387
|
{
|
399
388
|
table_name: @model_class.table_name,
|
400
|
-
billing_mode:
|
389
|
+
billing_mode: 'PAY_PER_REQUEST'
|
401
390
|
}
|
402
391
|
else
|
403
392
|
raise ArgumentError, "Unsupported billing mode #{@billing_mode}"
|
@@ -410,9 +399,7 @@ module Aws
|
|
410
399
|
table_name: @model_class.table_name,
|
411
400
|
global_secondary_index_updates: gsi_updates
|
412
401
|
}
|
413
|
-
unless attribute_definitions.empty?
|
414
|
-
opts[:attribute_definitions] = attribute_definitions
|
415
|
-
end
|
402
|
+
opts[:attribute_definitions] = attribute_definitions unless attribute_definitions.empty?
|
416
403
|
opts
|
417
404
|
end
|
418
405
|
|
@@ -431,7 +418,7 @@ module Aws
|
|
431
418
|
gsi[:key_schema].each do |k|
|
432
419
|
attributes_referenced.add(k[:attribute_name])
|
433
420
|
end
|
434
|
-
if @billing_mode ==
|
421
|
+
if @billing_mode == 'PROVISIONED'
|
435
422
|
lgsi = @global_secondary_indexes[index_name.to_sym]
|
436
423
|
gsi[:provisioned_throughput] = lgsi.provisioned_throughput
|
437
424
|
end
|
@@ -440,7 +427,7 @@ module Aws
|
|
440
427
|
}
|
441
428
|
end
|
442
429
|
# we don't currently update anything other than throughput
|
443
|
-
if @billing_mode ==
|
430
|
+
if @billing_mode == 'PROVISIONED'
|
444
431
|
update_candidates.each do |index_name|
|
445
432
|
lgsi = @global_secondary_indexes[index_name.to_sym]
|
446
433
|
gsi_updates << {
|
@@ -464,19 +451,19 @@ module Aws
|
|
464
451
|
_keys.map do |type, attr|
|
465
452
|
{
|
466
453
|
attribute_name: attr.database_name,
|
467
|
-
key_type: type == :hash ?
|
454
|
+
key_type: type == :hash ? 'HASH' : 'RANGE'
|
468
455
|
}
|
469
456
|
end
|
470
457
|
end
|
471
458
|
|
472
459
|
def _attribute_definitions
|
473
|
-
attribute_definitions = _keys.map do |
|
460
|
+
attribute_definitions = _keys.map do |_type, attr|
|
474
461
|
{
|
475
462
|
attribute_name: attr.database_name,
|
476
463
|
attribute_type: attr.dynamodb_type
|
477
464
|
}
|
478
465
|
end
|
479
|
-
@model_class.global_secondary_indexes.
|
466
|
+
@model_class.global_secondary_indexes.each_value do |attributes|
|
480
467
|
gsi_keys = [attributes[:hash_key]]
|
481
468
|
gsi_keys << attributes[:range_key] if attributes[:range_key]
|
482
469
|
gsi_keys.each do |name|
|
@@ -484,52 +471,52 @@ module Aws
|
|
484
471
|
exists = attribute_definitions.any? do |ad|
|
485
472
|
ad[:attribute_name] == attribute.database_name
|
486
473
|
end
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
474
|
+
next if exists
|
475
|
+
|
476
|
+
attribute_definitions << {
|
477
|
+
attribute_name: attribute.database_name,
|
478
|
+
attribute_type: attribute.dynamodb_type
|
479
|
+
}
|
493
480
|
end
|
494
481
|
end
|
495
482
|
attribute_definitions
|
496
483
|
end
|
497
484
|
|
498
485
|
def _keys
|
499
|
-
@model_class.keys.
|
486
|
+
@model_class.keys.each_with_object({}) do |(type, name), acc|
|
500
487
|
acc[type] = @model_class.attributes.attribute_for(name)
|
501
488
|
acc
|
502
489
|
end
|
503
490
|
end
|
504
491
|
|
505
492
|
def _throughput_equal(resp)
|
506
|
-
if @billing_mode ==
|
493
|
+
if @billing_mode == 'PAY_PER_REQUEST'
|
507
494
|
!resp.table.billing_mode_summary.nil? &&
|
508
|
-
resp.table.billing_mode_summary.billing_mode ==
|
495
|
+
resp.table.billing_mode_summary.billing_mode == 'PAY_PER_REQUEST'
|
509
496
|
else
|
510
497
|
expected = resp.table.provisioned_throughput.to_h
|
511
498
|
actual = {
|
512
499
|
read_capacity_units: @read_capacity_units,
|
513
500
|
write_capacity_units: @write_capacity_units
|
514
501
|
}
|
515
|
-
actual.all? do |k,v|
|
502
|
+
actual.all? do |k, v|
|
516
503
|
expected[k] == v
|
517
504
|
end
|
518
505
|
end
|
519
506
|
end
|
520
507
|
|
521
508
|
def _keys_equal(resp)
|
522
|
-
remote_key_schema = resp.table.key_schema.map
|
509
|
+
remote_key_schema = resp.table.key_schema.map(&:to_h)
|
523
510
|
_array_unsorted_eql(remote_key_schema, _key_schema)
|
524
511
|
end
|
525
512
|
|
526
513
|
def _ad_equal(resp)
|
527
|
-
remote_ad = resp.table.attribute_definitions.map
|
514
|
+
remote_ad = resp.table.attribute_definitions.map(&:to_h)
|
528
515
|
_array_unsorted_eql(remote_ad, _attribute_definitions)
|
529
516
|
end
|
530
517
|
|
531
518
|
def _ad_superset(resp)
|
532
|
-
remote_ad = resp.table.attribute_definitions.map
|
519
|
+
remote_ad = resp.table.attribute_definitions.map(&:to_h)
|
533
520
|
_attribute_definitions.all? do |attribute_definition|
|
534
521
|
remote_ad.include?(attribute_definition)
|
535
522
|
end
|
@@ -540,7 +527,7 @@ module Aws
|
|
540
527
|
local_gsis = _global_secondary_indexes
|
541
528
|
remote_idx, local_idx = _gsi_index_names(remote_gsis, local_gsis)
|
542
529
|
if local_idx.subset?(remote_idx)
|
543
|
-
|
530
|
+
_gsi_set_compare(remote_gsis, local_gsis)
|
544
531
|
else
|
545
532
|
# If we have any local indexes not on the remote table,
|
546
533
|
# guaranteed false.
|
@@ -565,26 +552,26 @@ module Aws
|
|
565
552
|
r.index_name == lgsi[:index_name].to_s
|
566
553
|
end
|
567
554
|
|
568
|
-
remote_key_schema = rgsi.key_schema.map
|
555
|
+
remote_key_schema = rgsi.key_schema.map(&:to_h)
|
569
556
|
ks_match = _array_unsorted_eql(remote_key_schema, lgsi[:key_schema])
|
570
557
|
|
571
558
|
# Throughput Check: Dependent on Billing Mode
|
572
559
|
rpt = rgsi.provisioned_throughput.to_h
|
573
560
|
lpt = lgsi[:provisioned_throughput]
|
574
|
-
if @billing_mode ==
|
575
|
-
pt_match = lpt.all? do |k,v|
|
561
|
+
if @billing_mode == 'PROVISIONED'
|
562
|
+
pt_match = lpt.all? do |k, v|
|
576
563
|
rpt[k] == v
|
577
564
|
end
|
578
|
-
elsif @billing_mode ==
|
579
|
-
pt_match = lpt.nil?
|
565
|
+
elsif @billing_mode == 'PAY_PER_REQUEST'
|
566
|
+
pt_match = lpt.nil?
|
580
567
|
else
|
581
568
|
raise ArgumentError, "Unsupported billing mode #{@billing_mode}"
|
582
569
|
end
|
583
570
|
|
584
571
|
rp = rgsi.projection.to_h
|
585
572
|
lp = lgsi[:projection]
|
586
|
-
rp[:non_key_attributes]
|
587
|
-
lp[:non_key_attributes]
|
573
|
+
rp[:non_key_attributes]&.sort!
|
574
|
+
lp[:non_key_attributes]&.sort!
|
588
575
|
p_match = rp == lp
|
589
576
|
|
590
577
|
ks_match && pt_match && p_match
|
@@ -594,15 +581,11 @@ module Aws
|
|
594
581
|
def _gsi_index_names(remote, local)
|
595
582
|
remote_index_names = Set.new
|
596
583
|
local_index_names = Set.new
|
597
|
-
|
598
|
-
|
599
|
-
remote_index_names.add(gsi.index_name)
|
600
|
-
end
|
584
|
+
remote&.each do |gsi|
|
585
|
+
remote_index_names.add(gsi.index_name)
|
601
586
|
end
|
602
|
-
|
603
|
-
|
604
|
-
local_index_names.add(gsi[:index_name].to_s)
|
605
|
-
end
|
587
|
+
local&.each do |gsi|
|
588
|
+
local_index_names.add(gsi[:index_name].to_s)
|
606
589
|
end
|
607
590
|
[remote_index_names, local_index_names]
|
608
591
|
end
|
@@ -611,17 +594,15 @@ module Aws
|
|
611
594
|
gsis = []
|
612
595
|
model_gsis = @model_class.global_secondary_indexes_for_migration
|
613
596
|
gsi_config = @global_secondary_indexes
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
end
|
624
|
-
end
|
597
|
+
model_gsis&.each do |mgsi|
|
598
|
+
config = gsi_config[mgsi[:index_name]]
|
599
|
+
gsis << if @billing_mode == 'PROVISIONED'
|
600
|
+
mgsi.merge(
|
601
|
+
provisioned_throughput: config.provisioned_throughput
|
602
|
+
)
|
603
|
+
else
|
604
|
+
mgsi
|
605
|
+
end
|
625
606
|
end
|
626
607
|
gsis
|
627
608
|
end
|
@@ -633,18 +614,16 @@ module Aws
|
|
633
614
|
def _validate_required_configuration
|
634
615
|
missing_config = []
|
635
616
|
missing_config << 'model_class' unless @model_class
|
636
|
-
if @billing_mode ==
|
617
|
+
if @billing_mode == 'PROVISIONED'
|
637
618
|
missing_config << 'read_capacity_units' unless @read_capacity_units
|
638
619
|
missing_config << 'write_capacity_units' unless @write_capacity_units
|
639
|
-
|
640
|
-
|
641
|
-
raise ArgumentError.new("Cannot have billing mode #{@billing_mode} with provisioned capacity.")
|
642
|
-
end
|
643
|
-
end
|
644
|
-
unless missing_config.empty?
|
645
|
-
msg = missing_config.join(', ')
|
646
|
-
raise Errors::MissingRequiredConfiguration, 'Missing: ' + msg
|
620
|
+
elsif @read_capacity_units || @write_capacity_units
|
621
|
+
raise ArgumentError, "Cannot have billing mode #{@billing_mode} with provisioned capacity."
|
647
622
|
end
|
623
|
+
return if missing_config.empty?
|
624
|
+
|
625
|
+
msg = missing_config.join(', ')
|
626
|
+
raise Errors::MissingRequiredConfiguration, "Missing: #{msg}"
|
648
627
|
end
|
649
628
|
|
650
629
|
# @api private
|
@@ -663,7 +642,6 @@ module Aws
|
|
663
642
|
@provisioned_throughput[:write_capacity_units] = units
|
664
643
|
end
|
665
644
|
end
|
666
|
-
|
667
645
|
end
|
668
646
|
end
|
669
647
|
end
|