aws-record 2.10.1 → 2.12.0
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.
- 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
|