# Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
module AWS
class DynamoDB
# Represents a DynamoDB table.
#
# == Working with Tables
#
# Dynamo DB allows you to organize data into tables. Tables have a
# unique name and a key schema. A key schema is comprised of a
# hash key and an optional range key.
#
# Dynamo DB automatically partitions the data contained in a table
# across multiple nodes so that the data throughput is not constrained
# by the scale of a single box. You can reserve the required throughput
# by specifying a number of reads and writes per second to support.
#
# == Creating a Table
#
# To get started you can create a table by supplying a name
# and the read/write capacity. A default schema with a hash_key
# of :id => :string will be provided.
#
# dynamo_db = AWS::DynamoDB.new
# dynamo_db.tables.create('mytable', 10, 5)
#
# You can provide your own hash key and optional range key.
#
# dynamo_db.tables.create('comments', 10, 5,
# :hash_key => { :blog_post_id => :number },
# :range_key => { :comment_id => :number }
# )
#
# == Provisioning Throughput
#
# You must specify the desired read and write capacity when
# creating a table. After a table is created you can see what has
# been provisioned.
#
# table.read_capacity_units #=> 10
# table.write_capacity_units #=> 5
#
# To change these values, call {#provision_throughput}:
#
# table.provision_throughput :read_capacity_units => 100, :write_capacity_units => 100
#
# Please note that provisioned throughput can be decreased only once
# within a 24 hour period.
#
# == Table Status
#
# When you create or update a table the changes can take some time to
# apply. You can query the status of your table at any time:
#
# # creating a table can be a *very* slow operation
# table = dynamo_db.tables.create('mytable')
# sleep 1 while table.status == :creating
# table.status #=> :active
#
# @attr_reader [Time] created_at When the table was first creatd.
#
# @attr_reader [Symbol] status
#
# @attr [Integer] read_capacity_units
#
# @attr [Integer] write_capacity_units
#
# @attr [Time] throughput_last_increased_at
#
# @attr [Time] throughput_last_decreased_at
#
# @attr [PrimaryKeyElement] hash_key Returns the hash key element
# for this table.
#
# @attr [PrimaryKeyElement,nil] range_key Returns the range key
# element for this table, or nil if the table does not have a range
# key.
#
class Table < Resource
# @private
def initialize name, options = {}
@name = name
super
end
# @return [String] The name of this table.
attr_reader :name
attribute :creation_date_time, :timestamp => true, :static => true
alias_method :created_at, :creation_date_time
attribute :status, :as => 'TableStatus', :to_sym => true
attribute :throughput_last_increased_at,
:as => 'LastIncreaseDateTime',
:timestamp => true
attribute :throughput_last_decreased_at,
:as => 'LastDecreaseDateTime',
:timestamp => true
attribute :read_capacity_units
attribute :write_capacity_units
attribute :hash_key, :as => "HashKeyElement", :static => true do
translates_output {|v| PrimaryKeyElement.new(v) }
end
attribute :range_key, :as => "RangeKeyElement", :static => true do
translates_output {|v| PrimaryKeyElement.new(v) }
end
alias_method :range_key_without_schema_override, :range_key
populates_from :describe_table do |resp|
desc = resp.data['Table']
if desc['TableName'] == name
desc.
merge(desc['ProvisionedThroughput']).
merge(desc['KeySchema'] || {})
end
end
populates_from :create_table, :delete_table do |resp|
desc = resp.data['TableDescription']
if desc['TableName'] == name
desc.
merge(desc['ProvisionedThroughput']).
merge(desc['KeySchema'] || {})
end
end
# @return [PrimaryKeyElement]
def range_key
if schema_loaded?
static_attributes[:range_key]
else
range_key_without_schema_override
end
end
# @param [Hash] options
#
# @option options [Integer] :read_capacity_units
#
# @option options [Integer] :write_capacity_units
#
# @return [Hash] Returns a hash with the current throughput
# provisioning (+:read_capacity_units+ and +:write_capacity_units+).
#
def provision_throughput options = {}
options[:read_capacity_units] ||= read_capacity_units
options[:write_capacity_units] ||= write_capacity_units
client_opts = {}
client_opts[:table_name] = name
client_opts[:provisioned_throughput] = options
client.update_table(client_opts)
options
end
# @param [Integer] read_capacity_units
def read_capacity_units= read_capacity_units
provision_throughput(:read_capacity_units => read_capacity_units)
end
# @param [Integer] write_capacity_units
def write_capacity_units= write_capacity_units
provision_throughput(:write_capacity_units => write_capacity_units)
end
# @return [Boolean] Returns true if the table has a hash key and no
# range key.
def simple_key?
range_key.nil?
end
# @return [Boolean] Returns true if the table has both a hash key and
# a range key.
def composite_key?
!simple_key?
end
alias_method :has_range_key?, :composite_key?
# @return [Boolean] True if the table's schema information is
# loaded into memory.
#
# @note You must load the the table schema using {#load_schema},
# {#hash_key} or {#range_key} or configure it using
# {#hash_key=} and optionally {#range_key=} in order to work
# with DynamoDB items.
#
def schema_loaded?
static_attributes.include?(:hash_key)
end
# Raises an exception unless the table schema is loaded.
#
# @return [nil]
#
def assert_schema!
raise "table schema not loaded" unless schema_loaded?
end
# Loads the table's schema information into memory. This method
# should not be used in a high-volume code path, and is intended
# only as a convenience for exploring the API. In general you
# should configure a schema with {#hash_key=} and {#range_key=}
# before using the table.
#
# @note You must load the the table schema using {#load_schema},
# {#hash_key} or {#range_key} or configure it using
# {#hash_key=} and optionally {#range_key=} in order to work
# with DynamoDB items.
#
# @return self
def load_schema
hash_key
self
end
# Configures the hash key element of the table's key schema.
# This is the preferred way to load the table schema so that it
# can be used to work with DynamoDB items.
#
# # these are equivalent:
# table.hash_key = [:id, :string]
# table.hash_key = { :id => :string }
#
# @note For tables with composite primary keys, you must call
# this method first followed by {#range_key=} to configure the
# table schema.
#
# @param description A description of the hash key element. If
# this is a hash, it may contain a single mapping; the key is
# the name of the hash key attribute and the value is the type
# (+:string+ or +:number+). If it is an array, the first
# element is the name and the second element is the type.
#
def hash_key= description
static_attributes[:hash_key] =
PrimaryKeyElement.from_description(description)
end
# Configures the range key element of the table's key schema.
# This is the preferred way to load the table schema so that it
# can be used to work with DynamoDB items. This method is only
# valid if the table has a composite key schema, and it may only
# be called after {#hash_key=} has been used to configure the
# hash key element.
#
# # these are equivalent:
# table.range_key = [:id, :string]
# table.range_key = { :id => :string }
#
# @param description A description of the range key element. If
# this is a hash, it may contain a single mapping; the key is
# the name of the hash key attribute and the value is the type
# (+:string+ or +:number+). If it is an array, the first
# element is the name and the second element is the type.
#
def range_key= description
raise "attempted to set a range key without configuring a hash key first" unless
schema_loaded?
static_attributes[:range_key] =
PrimaryKeyElement.from_description(description)
end
# Deletes a table and all of its items. The table must be in an
# +:active+ state (see {#status}).
#
# @return [nil]
#
def delete
client.delete_table(:table_name => name)
nil
end
# @return [ItemCollection] Returns an object representing all the
# items in the table.
def items
ItemCollection.new(self)
end
# @return [Boolean] Returns true if the table exists. Note that a table
# exists even when it is in a +:deleting+ state; this method
# only returns false when DynamoDB no longer returns any
# information about the table.
def exists?
get_resource
true
rescue Errors::ResourceNotFoundException
false
end
# Requets a list of attributes for a list of items in the same table.
#
# If you want to request a list of attributes for items that span
# multiple tables, see {DynamoDB#batch_get}.
#
# You can call this method in two forms:
#
# # block form
# table.batch_get(:all, items) do |attributes|
# # yeilds one hash of attribute names/values for each item
# puts attributes.to_yaml
# end
#
# # enumerable return value
# attribute_hashes = table.batch_get(:all, items)
# attribute_hashes.each do |attributes|
# # ...
# end
#
# @note This method does not require the table schema to be loaded.
#
# == Attributes
#
# You can specify the list of attributes to request in 3 ways:
#
# * The symbol +:all+ (to recieve all attributes)
# * A single attribute name (e.g. 'size')
# * An array of attribute names (e.g. ['size', 'color'])
#
# A few exmaples:
#
# # get all attributes
# table.batch_get(:all, items)
#
# # only get the 'color' attribute
# table.batch_get('color', items)
#
# # get 'color' and 'size' attributes
# table.batch_get(['color', size'], items)
#
# == Items
#
# You must specify an array of items to fetch attributes for.
# The +items+ param should always be an array with:
#
# * String hash key values
# * Arrays of string hash key and range key values
# * Item objects
#
# Here are a few examples:
#
# # items as a list of hash key values
# items = %w(hashkey1 hashkey2 hashkey3)
# table.batch_get(:all, items)
#
# # items as a list of hash and range key values
# items = [['hashkey1', 'rangekey2'], ['hashkey1', 'rangekey2']]
# table.batch_get(:all, items)
#
# # items as a list of Item objects
# items = []
# items << Item.new(table, 'hashkey1')
# items << Item.new(table, 'hashkey2')
# table.batch_get(:all, items)
#
# Please note that you must provide both hash and range keys for tables
# that include a range key in the schema.
#
# @param [:all, String, Array<String>] attributes The list of
# attributes you want to fetch for each item. +attributes+ may be:
#
# * the symbol +:all+
# * a single attribute name string
# * an array of attribute name strings
#
# @param [Mixed] items A list of 2 or more items to fetch attributes
# for. You may provide +items+ as:
#
# * an array of hash key value strings
# * an array of hash and range key value pairs (nested arrays)
# * an array of {Item} objects
#
# @yield [Hash] Yields a hash of attributes for each item.
#
# @return [Enumerable] Returns an enumerable object that yields
# hashes of attributes.
#
def batch_get attributes, items, &block
batch = BatchGet.new(:config => config)
batch.table(name, attributes, items)
enum = batch.to_enum(:each_attributes)
block_given? ? enum.each(&block) : enum
end
# Batch puts up to 25 items to this table.
#
# table.batch_put([
# { :id => 'id1', :color => 'red' },
# { :id => 'id2', :color => 'blue' },
# { :id => 'id3', :color => 'green' },
# ])
#
# @param [Array<Hash>] items A list of item attributes to put.
# The hash must contain the table hash key element and range key
# element (if one is defined).
#
# @return (see BatchWrite#process!)
#
def batch_put items
batch = BatchWrite.new(:config => config)
batch.put(self, items)
batch.process!
end
# Batch writes up to 25 items to this table. A batch may contain
# a mix of items to put and items to delete.
#
# table.batch_write(
# :put => [
# { :id => 'id1', :color => 'red' },
# { :id => 'id2', :color => 'blue' },
# { :id => 'id3', :color => 'green' },
# ],
# :delete => ['id4', 'id5']
# )
#
# @param [Hash] options
#
# @option options (BatchWrite#write)
#
# @return (see BatchWrite#process!)
#
def batch_write options = {}
batch = BatchWrite.new(:config => config)
batch.write(self, options)
batch.process!
end
# Delete up to 25 items in a single batch.
#
# table.batch_delete(%w(id1 id2 id3 id4 id5))
#
# @param [Array<String>,Array<Array>] items A list of item keys to
# delete. For tables without a range key, items should be an array
# of hash key strings.
#
# batch.delete('table-name', ['hk1', 'hk2', 'hk3'])
#
# For tables with a range key, items should be an array of
# hash key and range key pairs.
#
# batch.delete('table-name', [['hk1', 'rk1'], ['hk1', 'rk2']])
#
# @return (see BatchWrite#process!)
#
def batch_delete items
batch = BatchWrite.new(:config => config)
batch.delete(self, items)
batch.process!
end
protected
def get_resource attribute_name = nil
client.describe_table(resource_options)
end
protected
def resource_identifiers
[[:table_name, name]]
end
end
end
end