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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -19
  3. data/VERSION +1 -1
  4. data/lib/aws-record/record/attribute.rb +8 -8
  5. data/lib/aws-record/record/attributes.rb +36 -49
  6. data/lib/aws-record/record/batch.rb +13 -12
  7. data/lib/aws-record/record/batch_read.rb +10 -12
  8. data/lib/aws-record/record/batch_write.rb +2 -1
  9. data/lib/aws-record/record/buildable_search.rb +37 -39
  10. data/lib/aws-record/record/client_configuration.rb +14 -14
  11. data/lib/aws-record/record/dirty_tracking.rb +29 -40
  12. data/lib/aws-record/record/errors.rb +11 -2
  13. data/lib/aws-record/record/item_collection.rb +7 -7
  14. data/lib/aws-record/record/item_data.rb +13 -17
  15. data/lib/aws-record/record/item_operations.rb +150 -138
  16. data/lib/aws-record/record/key_attributes.rb +0 -2
  17. data/lib/aws-record/record/marshalers/boolean_marshaler.rb +2 -5
  18. data/lib/aws-record/record/marshalers/date_marshaler.rb +1 -6
  19. data/lib/aws-record/record/marshalers/date_time_marshaler.rb +2 -5
  20. data/lib/aws-record/record/marshalers/epoch_time_marshaler.rb +2 -8
  21. data/lib/aws-record/record/marshalers/float_marshaler.rb +3 -8
  22. data/lib/aws-record/record/marshalers/integer_marshaler.rb +3 -8
  23. data/lib/aws-record/record/marshalers/list_marshaler.rb +4 -7
  24. data/lib/aws-record/record/marshalers/map_marshaler.rb +4 -7
  25. data/lib/aws-record/record/marshalers/numeric_set_marshaler.rb +7 -9
  26. data/lib/aws-record/record/marshalers/string_marshaler.rb +1 -2
  27. data/lib/aws-record/record/marshalers/string_set_marshaler.rb +5 -7
  28. data/lib/aws-record/record/marshalers/time_marshaler.rb +1 -5
  29. data/lib/aws-record/record/model_attributes.rb +17 -29
  30. data/lib/aws-record/record/query.rb +8 -11
  31. data/lib/aws-record/record/secondary_indexes.rb +40 -51
  32. data/lib/aws-record/record/table_config.rb +93 -115
  33. data/lib/aws-record/record/table_migration.rb +56 -72
  34. data/lib/aws-record/record/transactions.rb +40 -43
  35. data/lib/aws-record/record/version.rb +1 -1
  36. data/lib/aws-record/record.rb +36 -44
  37. 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 = "PROVISIONED" # default
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
- if attribute
200
- @ttl_attribute = attribute.database_name
201
- else
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
- :table_exists,
235
- table_name: @model_class.table_name
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
- if @ttl_attribute
251
- if !_ttl_compatibility_check
252
- client.update_time_to_live(
253
- table_name: @model_class.table_name,
254
- time_to_live_specification: {
255
- enabled: true,
256
- attribute_name: @ttl_attribute
257
- }
258
- )
259
- end # Else TTL is compatible and we are done.
260
- end # Else our work is done.
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
- begin
274
- resp = @client.describe_table(table_name: @model_class.table_name)
275
- _compatible_check(resp) && _ttl_compatibility_check
276
- rescue DynamoDB::Errors::ResourceNotFoundException
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
- begin
289
- resp = @client.describe_table(table_name: @model_class.table_name)
290
- _throughput_equal(resp) &&
291
- _keys_equal(resp) &&
292
- _ad_equal(resp) &&
293
- _gsi_equal(resp) &&
294
- _ttl_match_check
295
- rescue DynamoDB::Errors::ResourceNotFoundException
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
- ["ENABLED", "ENABLING"].include?(desc.time_to_live_status) &&
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
- ["ENABLED", "ENABLING"].include?(desc.time_to_live_status) &&
314
+ %w[ENABLED ENABLING].include?(desc.time_to_live_status) &&
321
315
  desc.attribute_name == @ttl_attribute
322
316
  else
323
- !["ENABLED", "ENABLING"].include?(desc.time_to_live_status) ||
324
- desc.attribute_name == nil
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 == "PROVISIONED"
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 == "PAY_PER_REQUEST"
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 do |g|
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 == "PROVISIONED"
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
- resp.table.billing_mode_summary.billing_mode == "PAY_PER_REQUEST"
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 # else don't include billing mode
384
+ end
396
385
  opts
397
- elsif @billing_mode == "PAY_PER_REQUEST"
386
+ elsif @billing_mode == 'PAY_PER_REQUEST'
398
387
  {
399
388
  table_name: @model_class.table_name,
400
- billing_mode: "PAY_PER_REQUEST"
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 == "PROVISIONED"
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 == "PROVISIONED"
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 ? "HASH" : "RANGE"
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 |type, attr|
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.each do |_, attributes|
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
- unless exists
488
- attribute_definitions << {
489
- attribute_name: attribute.database_name,
490
- attribute_type: attribute.dynamodb_type
491
- }
492
- end
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.inject({}) do |acc, (type, name)|
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 == "PAY_PER_REQUEST"
493
+ if @billing_mode == 'PAY_PER_REQUEST'
507
494
  !resp.table.billing_mode_summary.nil? &&
508
- resp.table.billing_mode_summary.billing_mode == "PAY_PER_REQUEST"
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 { |i| i.to_h }
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 { |i| i.to_h }
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 { |i| i.to_h }
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
- _gsi_set_compare(remote_gsis, local_gsis)
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 { |i| i.to_h }
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 == "PROVISIONED"
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 == "PAY_PER_REQUEST"
579
- pt_match = lpt.nil? ? true : false
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].sort! if rp[:non_key_attributes]
587
- lp[:non_key_attributes].sort! if 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
- if remote
598
- remote.each do |gsi|
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
- if local
603
- local.each do |gsi|
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
- if model_gsis
615
- model_gsis.each do |mgsi|
616
- config = gsi_config[mgsi[:index_name]]
617
- if @billing_mode == "PROVISIONED"
618
- gsis << mgsi.merge(
619
- provisioned_throughput: config.provisioned_throughput
620
- )
621
- else
622
- gsis << mgsi
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 == "PROVISIONED"
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
- else
640
- if @read_capacity_units || @write_capacity_units
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