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.
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