lib/aws-record/record/table_migration.rb (140 lines of code) (raw):
# frozen_string_literal: true
module Aws
module Record
class TableMigration
# @!attribute [rw] client
# @return [Aws::DynamoDB::Client] the
# {http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html Aws::DynamoDB::Client}
# class used by this table migration instance.
attr_accessor :client
# @param [Aws::Record] model a model class that includes {Aws::Record}.
# @param [Hash] opts
# @option opts [Aws::DynamoDB::Client] :client Allows you to inject your
# own
# {http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html Aws::DynamoDB::Client}
# class. If this option is not included, a client will be constructed for
# you with default parameters.
def initialize(model, opts = {})
_assert_model_valid(model)
@model = model
@client = opts[:client] || model.dynamodb_client || Aws::DynamoDB::Client.new
@client.config.user_agent_frameworks << 'aws-record'
end
# This method calls
# {http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#create_table-instance_method
# Aws::DynamoDB::Client#create_table}, populating the attribute definitions and
# key schema based on your model class, as well as passing through other
# parameters as provided by you.
#
# @example Creating a table with a global secondary index named +:gsi+
# migration.create!(
# provisioned_throughput: {
# read_capacity_units: 5,
# write_capacity_units: 2
# },
# global_secondary_index_throughput: {
# gsi: {
# read_capacity_units: 3,
# write_capacity_units: 1
# }
# }
# )
#
# @param [Hash] opts options to pass on to the client call to
# +#create_table+. See the documentation above in the AWS SDK for Ruby
# V2.
# @option opts [Hash] :billing_mode Accepts values 'PAY_PER_REQUEST' or
# 'PROVISIONED'. If :provisioned_throughput option is specified, this
# option is not required, as 'PROVISIONED' is assumed. If
# :provisioned_throughput is not specified, this option is required
# and must be set to 'PAY_PER_REQUEST'.
# @option opts [Hash] :provisioned_throughput Unless :billing_mode is
# set to 'PAY_PER_REQUEST', this is a required argument, in which
# you must specify the +:read_capacity_units+ and
# +:write_capacity_units+ of your new table.
# @option opts [Hash] :global_secondary_index_throughput This argument is
# required if you define any global secondary indexes, unless
# :billing_mode is set to 'PAY_PER_REQUEST'. It should map your
# global secondary index names to their provisioned throughput, similar
# to how you define the provisioned throughput for the table in general.
def create!(opts)
gsit = opts.delete(:global_secondary_index_throughput)
_validate_billing(opts)
create_opts = opts.merge(
table_name: @model.table_name,
attribute_definitions: _attribute_definitions,
key_schema: _key_schema
)
if (lsis = @model.local_secondary_indexes_for_migration)
create_opts[:local_secondary_indexes] = lsis
_append_to_attribute_definitions(lsis, create_opts)
end
if (gsis = @model.global_secondary_indexes_for_migration)
unless gsit || opts[:billing_mode] == 'PAY_PER_REQUEST'
raise ArgumentError, 'If you define global secondary indexes, you must also define ' \
':global_secondary_index_throughput on table creation, ' \
"unless :billing_mode is set to 'PAY_PER_REQUEST'."
end
gsis_opts = if opts[:billing_mode] == 'PAY_PER_REQUEST'
gsis
else
_add_throughput_to_gsis(gsis, gsit)
end
create_opts[:global_secondary_indexes] = gsis_opts
_append_to_attribute_definitions(gsis, create_opts)
end
@client.create_table(create_opts)
end
# This method calls
# {http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#update_table-instance_method
# Aws::DynamoDB::Client#update_table} using the parameters that you provide.
#
# @param [Hash] opts options to pass on to the client call to
# +#update_table+. See the documentation above in the AWS SDK for Ruby
# V2.
# @raise [Aws::Record::Errors::TableDoesNotExist] if the table does not
# currently exist in Amazon DynamoDB.
def update!(opts)
update_opts = opts.merge(
table_name: @model.table_name
)
@client.update_table(update_opts)
rescue DynamoDB::Errors::ResourceNotFoundException
raise Errors::TableDoesNotExist
end
# This method calls
# {http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#delete_table-instance_method
# Aws::DynamoDB::Client#delete_table} using the table name of your model.
#
# @raise [Aws::Record::Errors::TableDoesNotExist] if the table did not
# exist in Amazon DynamoDB at the time of calling.
def delete!
@client.delete_table(table_name: @model.table_name)
rescue DynamoDB::Errors::ResourceNotFoundException
raise Errors::TableDoesNotExist
end
# This method waits on the table specified in the model to exist and be
# marked as ACTIVE in Amazon DynamoDB. Note that this method can run for
# several minutes if the table does not exist, and is not created within
# the wait period.
def wait_until_available
@client.wait_until(:table_exists, table_name: @model.table_name)
end
private
def _assert_model_valid(model)
_assert_required_include(model)
model.model_valid?
end
def _assert_required_include(model)
return if model.include?(::Aws::Record)
raise Errors::InvalidModel, 'Table models must include Aws::Record'
end
def _validate_billing(opts)
valid_modes = %w[PAY_PER_REQUEST PROVISIONED]
if opts.key?(:billing_mode) && !valid_modes.include?(opts[:billing_mode])
raise ArgumentError, ":billing_mode option must be one of #{valid_modes.join(', ')} " \
"current value is: #{opts[:billing_mode]}"
end
if opts.key?(:provisioned_throughput)
if opts[:billing_mode] == 'PAY_PER_REQUEST'
raise ArgumentError, 'when :provisioned_throughput option is specified, :billing_mode ' \
"must either be unspecified or have a value of 'PROVISIONED'"
end
elsif opts[:billing_mode] != 'PAY_PER_REQUEST'
raise ArgumentError, 'when :provisioned_throughput option is not specified, ' \
":billing_mode must be set to 'PAY_PER_REQUEST'"
end
end
def _attribute_definitions
_keys.map do |_type, attr|
{
attribute_name: attr.database_name,
attribute_type: attr.dynamodb_type
}
end
end
def _append_to_attribute_definitions(secondary_indexes, create_opts)
attributes = @model.attributes
attr_def = create_opts[:attribute_definitions]
secondary_indexes.each do |si|
si[:key_schema].each do |key_schema|
exists = attr_def.find do |a|
a[:attribute_name] == key_schema[:attribute_name]
end
next if exists
attr = attributes.attribute_for(
attributes.db_to_attribute_name(key_schema[:attribute_name])
)
attr_def << {
attribute_name: attr.database_name,
attribute_type: attr.dynamodb_type
}
end
end
create_opts[:attribute_definitions] = attr_def
end
def _add_throughput_to_gsis(global_secondary_indexes, gsi_throughput)
missing_throughput = []
ret = global_secondary_indexes.map do |params|
name = params[:index_name]
throughput = gsi_throughput[name]
missing_throughput << name unless throughput
params.merge(provisioned_throughput: throughput)
end
unless missing_throughput.empty?
raise ArgumentError, 'Missing provisioned throughput for the following global secondary ' \
"indexes: #{missing_throughput.join(', ')}. GSIs: " \
"#{global_secondary_indexes} and defined throughput: " \
"#{gsi_throughput}"
end
ret
end
def _key_schema
_keys.map do |type, attr|
{
attribute_name: attr.database_name,
key_type: type == :hash ? 'HASH' : 'RANGE'
}
end
end
def _keys
@model.keys.each_with_object({}) do |(type, name), acc|
acc[type] = @model.attributes.attribute_for(name)
acc
end
end
end
end
end