DynamoDB improvements (#4849)

This commit is contained in:
Bert Blommers 2022-02-10 19:09:45 -01:00 committed by GitHub
parent b28d763c08
commit e5c8cf058c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 652 additions and 329 deletions

View File

@ -205,6 +205,13 @@ class MultipleTransactionsException(MockValidationException):
super().__init__(self.msg) super().__init__(self.msg)
class TooManyTransactionsException(MockValidationException):
msg = "Validation error at transactItems: Member must have length less than or equal to 25."
def __init__(self):
super().__init__(self.msg)
class EmptyKeyAttributeException(MockValidationException): class EmptyKeyAttributeException(MockValidationException):
empty_str_msg = "One or more parameter values were invalid: An AttributeValue may not contain an empty string" empty_str_msg = "One or more parameter values were invalid: An AttributeValue may not contain an empty string"
# AWS has a different message for empty index keys # AWS has a different message for empty index keys

View File

@ -24,6 +24,7 @@ from moto.dynamodb2.exceptions import (
EmptyKeyAttributeException, EmptyKeyAttributeException,
InvalidAttributeTypeError, InvalidAttributeTypeError,
MultipleTransactionsException, MultipleTransactionsException,
TooManyTransactionsException,
) )
from moto.dynamodb2.models.utilities import bytesize from moto.dynamodb2.models.utilities import bytesize
from moto.dynamodb2.models.dynamo_type import DynamoType from moto.dynamodb2.models.dynamo_type import DynamoType
@ -388,12 +389,16 @@ class Table(CloudFormationModel):
def __init__( def __init__(
self, self,
table_name, table_name,
region,
schema=None, schema=None,
attr=None, attr=None,
throughput=None, throughput=None,
billing_mode=None,
indexes=None, indexes=None,
global_indexes=None, global_indexes=None,
streams=None, streams=None,
sse_specification=None,
tags=None,
): ):
self.name = table_name self.name = table_name
self.attr = attr self.attr = attr
@ -417,8 +422,9 @@ class Table(CloudFormationModel):
self.table_key_attrs = [ self.table_key_attrs = [
key for key in (self.hash_key_attr, self.range_key_attr) if key key for key in (self.hash_key_attr, self.range_key_attr) if key
] ]
self.billing_mode = billing_mode
if throughput is None: if throughput is None:
self.throughput = {"WriteCapacityUnits": 10, "ReadCapacityUnits": 10} self.throughput = {"WriteCapacityUnits": 0, "ReadCapacityUnits": 0}
else: else:
self.throughput = throughput self.throughput = throughput
self.throughput["NumberOfDecreasesToday"] = 0 self.throughput["NumberOfDecreasesToday"] = 0
@ -433,11 +439,14 @@ class Table(CloudFormationModel):
self.created_at = datetime.datetime.utcnow() self.created_at = datetime.datetime.utcnow()
self.items = defaultdict(dict) self.items = defaultdict(dict)
self.table_arn = self._generate_arn(table_name) self.table_arn = self._generate_arn(table_name)
self.tags = [] self.tags = tags or []
self.ttl = { self.ttl = {
"TimeToLiveStatus": "DISABLED" # One of 'ENABLING'|'DISABLING'|'ENABLED'|'DISABLED', "TimeToLiveStatus": "DISABLED" # One of 'ENABLING'|'DISABLING'|'ENABLED'|'DISABLED',
# 'AttributeName': 'string' # Can contain this # 'AttributeName': 'string' # Can contain this
} }
self.stream_specification = {"StreamEnabled": False}
self.latest_stream_label = None
self.stream_shard = None
self.set_stream_specification(streams) self.set_stream_specification(streams)
self.lambda_event_source_mappings = {} self.lambda_event_source_mappings = {}
self.continuous_backups = { self.continuous_backups = {
@ -446,6 +455,32 @@ class Table(CloudFormationModel):
"PointInTimeRecoveryStatus": "DISABLED" # One of 'ENABLED'|'DISABLED' "PointInTimeRecoveryStatus": "DISABLED" # One of 'ENABLED'|'DISABLED'
}, },
} }
self.sse_specification = sse_specification
if sse_specification and "KMSMasterKeyId" not in self.sse_specification:
self.sse_specification["KMSMasterKeyId"] = self._get_default_encryption_key(
region
)
def _get_default_encryption_key(self, region):
from moto.kms import kms_backends
# https://aws.amazon.com/kms/features/#AWS_Service_Integration
# An AWS managed CMK is created automatically when you first create
# an encrypted resource using an AWS service integrated with KMS.
kms = kms_backends[region]
ddb_alias = "alias/aws/dynamodb"
if not kms.alias_exists(ddb_alias):
key = kms.create_key(
policy="",
key_usage="ENCRYPT_DECRYPT",
customer_master_key_spec="SYMMETRIC_DEFAULT",
description="Default master key that protects my DynamoDB table storage",
tags=None,
region=region,
)
kms.add_alias(key.id, ddb_alias)
ebs_key = kms.describe_key(ddb_alias)
return ebs_key.arn
@classmethod @classmethod
def has_cfn_attr(cls, attribute): def has_cfn_attr(cls, attribute):
@ -466,7 +501,7 @@ class Table(CloudFormationModel):
return self.name return self.name
@property @property
def key_attributes(self): def attribute_keys(self):
# A set of all the hash or range attributes for all indexes # A set of all the hash or range attributes for all indexes
def keys_from_index(idx): def keys_from_index(idx):
schema = idx.schema schema = idx.schema
@ -519,7 +554,7 @@ class Table(CloudFormationModel):
return table return table
def _generate_arn(self, name): def _generate_arn(self, name):
return "arn:aws:dynamodb:us-east-1:123456789011:table/" + name return f"arn:aws:dynamodb:us-east-1:{ACCOUNT_ID}:table/{name}"
def set_stream_specification(self, streams): def set_stream_specification(self, streams):
self.stream_specification = streams self.stream_specification = streams
@ -529,14 +564,13 @@ class Table(CloudFormationModel):
self.stream_shard = StreamShard(self) self.stream_shard = StreamShard(self)
else: else:
self.stream_specification = {"StreamEnabled": False} self.stream_specification = {"StreamEnabled": False}
self.latest_stream_label = None
self.stream_shard = None
def describe(self, base_key="TableDescription"): def describe(self, base_key="TableDescription"):
results = { results = {
base_key: { base_key: {
"AttributeDefinitions": self.attr, "AttributeDefinitions": self.attr,
"ProvisionedThroughput": self.throughput, "ProvisionedThroughput": self.throughput,
"BillingModeSummary": {"BillingMode": self.billing_mode},
"TableSizeBytes": 0, "TableSizeBytes": 0,
"TableName": self.name, "TableName": self.name,
"TableStatus": "ACTIVE", "TableStatus": "ACTIVE",
@ -550,13 +584,19 @@ class Table(CloudFormationModel):
"LocalSecondaryIndexes": [index.describe() for index in self.indexes], "LocalSecondaryIndexes": [index.describe() for index in self.indexes],
} }
} }
if self.stream_specification and self.stream_specification["StreamEnabled"]:
results[base_key]["StreamSpecification"] = self.stream_specification
if self.latest_stream_label: if self.latest_stream_label:
results[base_key]["LatestStreamLabel"] = self.latest_stream_label results[base_key]["LatestStreamLabel"] = self.latest_stream_label
results[base_key]["LatestStreamArn"] = ( results[base_key][
self.table_arn + "/stream/" + self.latest_stream_label "LatestStreamArn"
) ] = f"{self.table_arn}/stream/{self.latest_stream_label}"
if self.stream_specification and self.stream_specification["StreamEnabled"]:
results[base_key]["StreamSpecification"] = self.stream_specification
if self.sse_specification and self.sse_specification.get("Enabled") is True:
results[base_key]["SSEDescription"] = {
"Status": "ENABLED",
"SSEType": "KMS",
"KMSMasterKeyArn": self.sse_specification.get("KMSMasterKeyId"),
}
return results return results
def __len__(self): def __len__(self):
@ -988,9 +1028,9 @@ class Table(CloudFormationModel):
class RestoredTable(Table): class RestoredTable(Table):
def __init__(self, name, backup): def __init__(self, name, region, backup):
params = self._parse_params_from_backup(backup) params = self._parse_params_from_backup(backup)
super().__init__(name, **params) super().__init__(name, region=region, **params)
self.indexes = copy.deepcopy(backup.table.indexes) self.indexes = copy.deepcopy(backup.table.indexes)
self.global_indexes = copy.deepcopy(backup.table.global_indexes) self.global_indexes = copy.deepcopy(backup.table.global_indexes)
self.items = copy.deepcopy(backup.table.items) self.items = copy.deepcopy(backup.table.items)
@ -1020,9 +1060,9 @@ class RestoredTable(Table):
class RestoredPITTable(Table): class RestoredPITTable(Table):
def __init__(self, name, source): def __init__(self, name, region, source):
params = self._parse_params_from_table(source) params = self._parse_params_from_table(source)
super().__init__(name, **params) super().__init__(name, region=region, **params)
self.indexes = copy.deepcopy(source.indexes) self.indexes = copy.deepcopy(source.indexes)
self.global_indexes = copy.deepcopy(source.global_indexes) self.global_indexes = copy.deepcopy(source.global_indexes)
self.items = copy.deepcopy(source.items) self.items = copy.deepcopy(source.items)
@ -1145,7 +1185,7 @@ class DynamoDBBackend(BaseBackend):
def create_table(self, name, **params): def create_table(self, name, **params):
if name in self.tables: if name in self.tables:
return None return None
table = Table(name, **params) table = Table(name, region=self.region_name, **params)
self.tables[name] = table self.tables[name] = table
return table return table
@ -1205,12 +1245,24 @@ class DynamoDBBackend(BaseBackend):
table = self.tables[name] table = self.tables[name]
return table.describe(base_key="Table") return table.describe(base_key="Table")
def update_table(self, name, global_index, throughput, stream_spec): def update_table(
self,
name,
attr_definitions,
global_index,
throughput,
billing_mode,
stream_spec,
):
table = self.get_table(name) table = self.get_table(name)
if attr_definitions:
table.attr = attr_definitions
if global_index: if global_index:
table = self.update_table_global_indexes(name, global_index) table = self.update_table_global_indexes(name, global_index)
if throughput: if throughput:
table = self.update_table_throughput(name, throughput) table = self.update_table_throughput(name, throughput)
if billing_mode:
table = self.update_table_billing_mode(name, billing_mode)
if stream_spec: if stream_spec:
table = self.update_table_streams(name, stream_spec) table = self.update_table_streams(name, stream_spec)
return table return table
@ -1220,6 +1272,11 @@ class DynamoDBBackend(BaseBackend):
table.throughput = throughput table.throughput = throughput
return table return table
def update_table_billing_mode(self, name, billing_mode):
table = self.tables[name]
table.billing_mode = billing_mode
return table
def update_table_streams(self, name, stream_specification): def update_table_streams(self, name, stream_specification):
table = self.tables[name] table = self.tables[name]
if ( if (
@ -1495,7 +1552,7 @@ class DynamoDBBackend(BaseBackend):
item = table.get_item(hash_value, range_value) item = table.get_item(hash_value, range_value)
if attribute_updates: if attribute_updates:
item.validate_no_empty_key_values(attribute_updates, table.key_attributes) item.validate_no_empty_key_values(attribute_updates, table.attribute_keys)
if update_expression: if update_expression:
validator = UpdateExpressionValidator( validator = UpdateExpressionValidator(
@ -1568,6 +1625,8 @@ class DynamoDBBackend(BaseBackend):
return table.ttl return table.ttl
def transact_write_items(self, transact_items): def transact_write_items(self, transact_items):
if len(transact_items) > 25:
raise TooManyTransactionsException()
# Create a backup in case any of the transactions fail # Create a backup in case any of the transactions fail
original_table_state = copy.deepcopy(self.tables) original_table_state = copy.deepcopy(self.tables)
target_items = set() target_items = set()
@ -1739,7 +1798,9 @@ class DynamoDBBackend(BaseBackend):
existing_table = self.get_table(target_table_name) existing_table = self.get_table(target_table_name)
if existing_table is not None: if existing_table is not None:
raise ValueError() raise ValueError()
new_table = RestoredTable(target_table_name, backup) new_table = RestoredTable(
target_table_name, region=self.region_name, backup=backup
)
self.tables[target_table_name] = new_table self.tables[target_table_name] = new_table
return new_table return new_table
@ -1755,7 +1816,9 @@ class DynamoDBBackend(BaseBackend):
existing_table = self.get_table(target_table_name) existing_table = self.get_table(target_table_name)
if existing_table is not None: if existing_table is not None:
raise ValueError() raise ValueError()
new_table = RestoredPITTable(target_table_name, source) new_table = RestoredPITTable(
target_table_name, region=self.region_name, source=source
)
self.tables[target_table_name] = new_table self.tables[target_table_name] = new_table
return new_table return new_table

View File

@ -410,6 +410,6 @@ class UpdateExpressionValidator(Validator):
UpdateExpressionFunctionEvaluator(), UpdateExpressionFunctionEvaluator(),
NoneExistingPathChecker(), NoneExistingPathChecker(),
ExecuteOperations(), ExecuteOperations(),
EmptyStringKeyValueValidator(self.table.key_attributes), EmptyStringKeyValueValidator(self.table.attribute_keys),
] ]
return processors return processors

View File

@ -69,7 +69,7 @@ def include_consumed_capacity(val=1.0):
def put_has_empty_keys(field_updates, table): def put_has_empty_keys(field_updates, table):
if table: if table:
key_names = table.key_attributes key_names = table.attribute_keys
# string/binary fields with empty string as value # string/binary fields with empty string as value
empty_str_fields = [ empty_str_fields = [
@ -168,12 +168,20 @@ class DynamoHandler(BaseResponse):
er = "com.amazonaws.dynamodb.v20111205#ValidationException" er = "com.amazonaws.dynamodb.v20111205#ValidationException"
return self.error( return self.error(
er, er,
"ProvisionedThroughput cannot be specified \ "ProvisionedThroughput cannot be specified when BillingMode is PAY_PER_REQUEST",
when BillingMode is PAY_PER_REQUEST",
) )
throughput = None throughput = None
billing_mode = "PAY_PER_REQUEST"
else: # Provisioned (default billing mode) else: # Provisioned (default billing mode)
throughput = body.get("ProvisionedThroughput") throughput = body.get("ProvisionedThroughput")
if throughput is None:
return self.error(
"ValidationException",
"One or more parameter values were invalid: ReadCapacityUnits and WriteCapacityUnits must both be specified when BillingMode is PROVISIONED",
)
billing_mode = "PROVISIONED"
# getting ServerSideEncryption details
sse_spec = body.get("SSESpecification")
# getting the schema # getting the schema
key_schema = body["KeySchema"] key_schema = body["KeySchema"]
# getting attribute definition # getting attribute definition
@ -218,6 +226,8 @@ class DynamoHandler(BaseResponse):
) )
# get the stream specification # get the stream specification
streams = body.get("StreamSpecification") streams = body.get("StreamSpecification")
# Get any tags
tags = body.get("Tags", [])
table = self.dynamodb_backend.create_table( table = self.dynamodb_backend.create_table(
table_name, table_name,
@ -227,6 +237,9 @@ class DynamoHandler(BaseResponse):
global_indexes=global_indexes, global_indexes=global_indexes,
indexes=local_secondary_indexes, indexes=local_secondary_indexes,
streams=streams, streams=streams,
billing_mode=billing_mode,
sse_specification=sse_spec,
tags=tags,
) )
if table is not None: if table is not None:
return dynamo_json_dump(table.describe()) return dynamo_json_dump(table.describe())
@ -339,14 +352,18 @@ class DynamoHandler(BaseResponse):
def update_table(self): def update_table(self):
name = self.body["TableName"] name = self.body["TableName"]
attr_definitions = self.body.get("AttributeDefinitions", None)
global_index = self.body.get("GlobalSecondaryIndexUpdates", None) global_index = self.body.get("GlobalSecondaryIndexUpdates", None)
throughput = self.body.get("ProvisionedThroughput", None) throughput = self.body.get("ProvisionedThroughput", None)
billing_mode = self.body.get("BillingMode", None)
stream_spec = self.body.get("StreamSpecification", None) stream_spec = self.body.get("StreamSpecification", None)
try: try:
table = self.dynamodb_backend.update_table( table = self.dynamodb_backend.update_table(
name=name, name=name,
attr_definitions=attr_definitions,
global_index=global_index, global_index=global_index,
throughput=throughput, throughput=throughput,
billing_mode=billing_mode,
stream_spec=stream_spec, stream_spec=stream_spec,
) )
return dynamo_json_dump(table.describe()) return dynamo_json_dump(table.describe())

View File

@ -115,6 +115,7 @@ def test_invoke_function_from_dynamodb_put():
"StreamEnabled": True, "StreamEnabled": True,
"StreamViewType": "NEW_AND_OLD_IMAGES", "StreamViewType": "NEW_AND_OLD_IMAGES",
}, },
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5},
) )
conn = boto3.client("lambda", region_name="us-east-1") conn = boto3.client("lambda", region_name="us-east-1")
@ -165,6 +166,7 @@ def test_invoke_function_from_dynamodb_update():
"StreamEnabled": True, "StreamEnabled": True,
"StreamViewType": "NEW_AND_OLD_IMAGES", "StreamViewType": "NEW_AND_OLD_IMAGES",
}, },
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5},
) )
conn = boto3.client("lambda", region_name="us-east-1") conn = boto3.client("lambda", region_name="us-east-1")

View File

@ -6,6 +6,7 @@ from moto.dynamodb2.models import Table
def table(): def table():
return Table( return Table(
"Forums", "Forums",
region="us-east-1",
schema=[ schema=[
{"KeyType": "HASH", "AttributeName": "forum_name"}, {"KeyType": "HASH", "AttributeName": "forum_name"},
{"KeyType": "RANGE", "AttributeName": "subject"}, {"KeyType": "RANGE", "AttributeName": "subject"},

View File

@ -502,3 +502,35 @@ def test_multiple_transactions_on_same_item():
err["Message"].should.equal( err["Message"].should.equal(
"Transaction request cannot include multiple operations on one item" "Transaction request cannot include multiple operations on one item"
) )
@mock_dynamodb2
def test_transact_write_items__too_many_transactions():
table_schema = {
"KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
"AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"},],
}
dynamodb = boto3.client("dynamodb", region_name="us-east-1")
dynamodb.create_table(
TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
)
def update_email_transact(email):
return {
"Put": {
"TableName": "test-table",
"Item": {"pk": {"S": ":v"}},
"ExpressionAttributeValues": {":v": {"S": email}},
}
}
update_email_transact(f"test1@moto.com")
with pytest.raises(ClientError) as exc:
dynamodb.transact_write_items(
TransactItems=[
update_email_transact(f"test{idx}@moto.com") for idx in range(26)
]
)
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.match("Member must have length less than or equal to 25")

View File

@ -1605,45 +1605,6 @@ def test_bad_scan_filter():
raise RuntimeError("Should have raised ResourceInUseException") raise RuntimeError("Should have raised ResourceInUseException")
@mock_dynamodb2
def test_create_table_pay_per_request():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="test1",
AttributeDefinitions=[
{"AttributeName": "client", "AttributeType": "S"},
{"AttributeName": "app", "AttributeType": "S"},
],
KeySchema=[
{"AttributeName": "client", "KeyType": "HASH"},
{"AttributeName": "app", "KeyType": "RANGE"},
],
BillingMode="PAY_PER_REQUEST",
)
@mock_dynamodb2
def test_create_table_error_pay_per_request_with_provisioned_param():
client = boto3.client("dynamodb", region_name="us-east-1")
try:
client.create_table(
TableName="test1",
AttributeDefinitions=[
{"AttributeName": "client", "AttributeType": "S"},
{"AttributeName": "app", "AttributeType": "S"},
],
KeySchema=[
{"AttributeName": "client", "KeyType": "HASH"},
{"AttributeName": "app", "KeyType": "RANGE"},
],
ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
BillingMode="PAY_PER_REQUEST",
)
except ClientError as err:
err.response["Error"]["Code"].should.equal("ValidationException")
@mock_dynamodb2 @mock_dynamodb2
def test_duplicate_create(): def test_duplicate_create():
client = boto3.client("dynamodb", region_name="us-east-1") client = boto3.client("dynamodb", region_name="us-east-1")
@ -2356,61 +2317,6 @@ def test_query_global_secondary_index_when_created_via_update_table_resource():
} }
@mock_dynamodb2
def test_dynamodb_streams_1():
conn = boto3.client("dynamodb", region_name="us-east-1")
resp = conn.create_table(
TableName="test-streams",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
StreamSpecification={
"StreamEnabled": True,
"StreamViewType": "NEW_AND_OLD_IMAGES",
},
)
assert "StreamSpecification" in resp["TableDescription"]
assert resp["TableDescription"]["StreamSpecification"] == {
"StreamEnabled": True,
"StreamViewType": "NEW_AND_OLD_IMAGES",
}
assert "LatestStreamLabel" in resp["TableDescription"]
assert "LatestStreamArn" in resp["TableDescription"]
resp = conn.delete_table(TableName="test-streams")
assert "StreamSpecification" in resp["TableDescription"]
@mock_dynamodb2
def test_dynamodb_streams_2():
conn = boto3.client("dynamodb", region_name="us-east-1")
resp = conn.create_table(
TableName="test-stream-update",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
)
assert "StreamSpecification" not in resp["TableDescription"]
resp = conn.update_table(
TableName="test-stream-update",
StreamSpecification={"StreamEnabled": True, "StreamViewType": "NEW_IMAGE"},
)
assert "StreamSpecification" in resp["TableDescription"]
assert resp["TableDescription"]["StreamSpecification"] == {
"StreamEnabled": True,
"StreamViewType": "NEW_IMAGE",
}
assert "LatestStreamLabel" in resp["TableDescription"]
assert "LatestStreamArn" in resp["TableDescription"]
@mock_dynamodb2 @mock_dynamodb2
def test_query_gsi_with_range_key(): def test_query_gsi_with_range_key():
dynamodb = boto3.client("dynamodb", region_name="us-east-1") dynamodb = boto3.client("dynamodb", region_name="us-east-1")
@ -4314,6 +4220,7 @@ def test_update_expression_with_numeric_literal_instead_of_value():
TableName="moto-test", TableName="moto-test",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
BillingMode="PAY_PER_REQUEST",
) )
try: try:
@ -4338,6 +4245,7 @@ def test_update_expression_with_multiple_set_clauses_must_be_comma_separated():
TableName="moto-test", TableName="moto-test",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
BillingMode="PAY_PER_REQUEST",
) )
try: try:
@ -5123,6 +5031,7 @@ def test_attribute_item_delete():
TableName=name, TableName=name,
AttributeDefinitions=[{"AttributeName": "name", "AttributeType": "S"}], AttributeDefinitions=[{"AttributeName": "name", "AttributeType": "S"}],
KeySchema=[{"AttributeName": "name", "KeyType": "HASH"}], KeySchema=[{"AttributeName": "name", "KeyType": "HASH"}],
BillingMode="PAY_PER_REQUEST",
) )
item_name = "foo" item_name = "foo"

View File

@ -15,6 +15,7 @@ def test_condition_expression_with_dot_in_attr_name():
TableName=table_name, TableName=table_name,
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
BillingMode="PAY_PER_REQUEST",
) )
table = dynamodb.Table(table_name) table = dynamodb.Table(table_name)
@ -247,6 +248,7 @@ def test_condition_expression_numerical_attribute():
TableName="my-table", TableName="my-table",
KeySchema=[{"AttributeName": "partitionKey", "KeyType": "HASH"}], KeySchema=[{"AttributeName": "partitionKey", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "partitionKey", "AttributeType": "S"}], AttributeDefinitions=[{"AttributeName": "partitionKey", "AttributeType": "S"}],
BillingMode="PAY_PER_REQUEST",
) )
table = dynamodb.Table("my-table") table = dynamodb.Table("my-table")
table.put_item(Item={"partitionKey": "pk-pos", "myAttr": 5}) table.put_item(Item={"partitionKey": "pk-pos", "myAttr": 5})
@ -376,6 +378,7 @@ def test_condition_expression_with_reserved_keyword_as_attr_name():
TableName=table_name, TableName=table_name,
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
BillingMode="PAY_PER_REQUEST",
) )
table = dynamodb.Table(table_name) table = dynamodb.Table(table_name)

View File

@ -37,6 +37,7 @@ def test_consumed_capacity_get_unknown_item():
TableName="test_table", TableName="test_table",
KeySchema=[{"AttributeName": "u", "KeyType": "HASH"}], KeySchema=[{"AttributeName": "u", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "u", "AttributeType": "S"}], AttributeDefinitions=[{"AttributeName": "u", "AttributeType": "S"}],
BillingMode="PAY_PER_REQUEST",
) )
response = conn.get_item( response = conn.get_item(
TableName="test_table", TableName="test_table",

View File

@ -0,0 +1,406 @@
import boto3
from botocore.exceptions import ClientError
import sure # noqa # pylint: disable=unused-import
from datetime import datetime
import pytest
from moto import mock_dynamodb2
from moto.core import ACCOUNT_ID
@mock_dynamodb2
def test_create_table_standard():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="messages",
KeySchema=[
{"AttributeName": "id", "KeyType": "HASH"},
{"AttributeName": "subject", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "id", "AttributeType": "S"},
{"AttributeName": "subject", "AttributeType": "S"},
],
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5},
)
actual = client.describe_table(TableName="messages")["Table"]
actual.should.have.key("AttributeDefinitions").equal(
[
{"AttributeName": "id", "AttributeType": "S"},
{"AttributeName": "subject", "AttributeType": "S"},
]
)
actual.should.have.key("CreationDateTime").be.a(datetime)
actual.should.have.key("GlobalSecondaryIndexes").equal([])
actual.should.have.key("LocalSecondaryIndexes").equal([])
actual.should.have.key("ProvisionedThroughput").equal(
{"NumberOfDecreasesToday": 0, "ReadCapacityUnits": 1, "WriteCapacityUnits": 5}
)
actual.should.have.key("TableSizeBytes").equal(0)
actual.should.have.key("TableName").equal("messages")
actual.should.have.key("TableStatus").equal("ACTIVE")
actual.should.have.key("TableArn").equal(
f"arn:aws:dynamodb:us-east-1:{ACCOUNT_ID}:table/messages"
)
actual.should.have.key("KeySchema").equal(
[
{"AttributeName": "id", "KeyType": "HASH"},
{"AttributeName": "subject", "KeyType": "RANGE"},
]
)
actual.should.have.key("ItemCount").equal(0)
@mock_dynamodb2
def test_create_table_with_local_index():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="messages",
KeySchema=[
{"AttributeName": "id", "KeyType": "HASH"},
{"AttributeName": "subject", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "id", "AttributeType": "S"},
{"AttributeName": "subject", "AttributeType": "S"},
{"AttributeName": "threads", "AttributeType": "S"},
],
LocalSecondaryIndexes=[
{
"IndexName": "threads_index",
"KeySchema": [
{"AttributeName": "id", "KeyType": "HASH"},
{"AttributeName": "threads", "KeyType": "RANGE"},
],
"Projection": {"ProjectionType": "ALL"},
}
],
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5},
)
actual = client.describe_table(TableName="messages")["Table"]
actual.should.have.key("AttributeDefinitions").equal(
[
{"AttributeName": "id", "AttributeType": "S"},
{"AttributeName": "subject", "AttributeType": "S"},
{"AttributeName": "threads", "AttributeType": "S"},
]
)
actual.should.have.key("CreationDateTime").be.a(datetime)
actual.should.have.key("GlobalSecondaryIndexes").equal([])
actual.should.have.key("LocalSecondaryIndexes").equal(
[
{
"IndexName": "threads_index",
"KeySchema": [
{"AttributeName": "id", "KeyType": "HASH"},
{"AttributeName": "threads", "KeyType": "RANGE"},
],
"Projection": {"ProjectionType": "ALL"},
}
]
)
actual.should.have.key("ProvisionedThroughput").equal(
{"NumberOfDecreasesToday": 0, "ReadCapacityUnits": 1, "WriteCapacityUnits": 5}
)
actual.should.have.key("TableSizeBytes").equal(0)
actual.should.have.key("TableName").equal("messages")
actual.should.have.key("TableStatus").equal("ACTIVE")
actual.should.have.key("TableArn").equal(
f"arn:aws:dynamodb:us-east-1:{ACCOUNT_ID}:table/messages"
)
actual.should.have.key("KeySchema").equal(
[
{"AttributeName": "id", "KeyType": "HASH"},
{"AttributeName": "subject", "KeyType": "RANGE"},
]
)
actual.should.have.key("ItemCount").equal(0)
@mock_dynamodb2
def test_create_table_with_gsi():
dynamodb = boto3.client("dynamodb", region_name="us-east-1")
table = dynamodb.create_table(
TableName="users",
KeySchema=[
{"AttributeName": "forum_name", "KeyType": "HASH"},
{"AttributeName": "subject", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "forum_name", "AttributeType": "S"},
{"AttributeName": "subject", "AttributeType": "S"},
],
BillingMode="PAY_PER_REQUEST",
GlobalSecondaryIndexes=[
{
"IndexName": "test_gsi",
"KeySchema": [{"AttributeName": "subject", "KeyType": "HASH"}],
"Projection": {"ProjectionType": "ALL"},
}
],
)
table["TableDescription"]["GlobalSecondaryIndexes"].should.equal(
[
{
"KeySchema": [{"KeyType": "HASH", "AttributeName": "subject"}],
"IndexName": "test_gsi",
"Projection": {"ProjectionType": "ALL"},
"IndexStatus": "ACTIVE",
"ProvisionedThroughput": {
"ReadCapacityUnits": 0,
"WriteCapacityUnits": 0,
},
}
]
)
table = dynamodb.create_table(
TableName="users2",
KeySchema=[
{"AttributeName": "forum_name", "KeyType": "HASH"},
{"AttributeName": "subject", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "forum_name", "AttributeType": "S"},
{"AttributeName": "subject", "AttributeType": "S"},
],
BillingMode="PAY_PER_REQUEST",
GlobalSecondaryIndexes=[
{
"IndexName": "test_gsi",
"KeySchema": [{"AttributeName": "subject", "KeyType": "HASH"}],
"Projection": {"ProjectionType": "ALL"},
"ProvisionedThroughput": {
"ReadCapacityUnits": 3,
"WriteCapacityUnits": 5,
},
}
],
)
table["TableDescription"]["GlobalSecondaryIndexes"].should.equal(
[
{
"KeySchema": [{"KeyType": "HASH", "AttributeName": "subject"}],
"IndexName": "test_gsi",
"Projection": {"ProjectionType": "ALL"},
"IndexStatus": "ACTIVE",
"ProvisionedThroughput": {
"ReadCapacityUnits": 3,
"WriteCapacityUnits": 5,
},
}
]
)
@mock_dynamodb2
def test_create_table_with_stream_specification():
conn = boto3.client("dynamodb", region_name="us-east-1")
resp = conn.create_table(
TableName="test-streams",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
StreamSpecification={
"StreamEnabled": True,
"StreamViewType": "NEW_AND_OLD_IMAGES",
},
)
resp["TableDescription"].should.have.key("StreamSpecification")
resp["TableDescription"]["StreamSpecification"].should.equal(
{"StreamEnabled": True, "StreamViewType": "NEW_AND_OLD_IMAGES",}
)
resp["TableDescription"].should.contain("LatestStreamLabel")
resp["TableDescription"].should.contain("LatestStreamArn")
resp = conn.delete_table(TableName="test-streams")
resp["TableDescription"].should.contain("StreamSpecification")
@mock_dynamodb2
def test_create_table_with_tags():
client = boto3.client("dynamodb", region_name="us-east-1")
resp = client.create_table(
TableName="test-streams",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
Tags=[{"Key": "tk", "Value": "tv",}],
)
resp = client.list_tags_of_resource(
ResourceArn=resp["TableDescription"]["TableArn"]
)
resp.should.have.key("Tags").equals([{"Key": "tk", "Value": "tv"}])
@mock_dynamodb2
def test_create_table_pay_per_request():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="test1",
AttributeDefinitions=[
{"AttributeName": "client", "AttributeType": "S"},
{"AttributeName": "app", "AttributeType": "S"},
],
KeySchema=[
{"AttributeName": "client", "KeyType": "HASH"},
{"AttributeName": "app", "KeyType": "RANGE"},
],
BillingMode="PAY_PER_REQUEST",
)
actual = client.describe_table(TableName="test1")["Table"]
actual.should.have.key("BillingModeSummary").equals(
{"BillingMode": "PAY_PER_REQUEST"}
)
actual.should.have.key("ProvisionedThroughput").equals(
{"NumberOfDecreasesToday": 0, "ReadCapacityUnits": 0, "WriteCapacityUnits": 0}
)
@mock_dynamodb2
def test_create_table__provisioned_throughput():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="test1",
AttributeDefinitions=[
{"AttributeName": "client", "AttributeType": "S"},
{"AttributeName": "app", "AttributeType": "S"},
],
KeySchema=[
{"AttributeName": "client", "KeyType": "HASH"},
{"AttributeName": "app", "KeyType": "RANGE"},
],
ProvisionedThroughput={"ReadCapacityUnits": 2, "WriteCapacityUnits": 3},
)
actual = client.describe_table(TableName="test1")["Table"]
actual.should.have.key("BillingModeSummary").equals({"BillingMode": "PROVISIONED"})
actual.should.have.key("ProvisionedThroughput").equals(
{"NumberOfDecreasesToday": 0, "ReadCapacityUnits": 2, "WriteCapacityUnits": 3}
)
@mock_dynamodb2
def test_create_table_without_specifying_throughput():
dynamodb_client = boto3.client("dynamodb", region_name="us-east-1")
with pytest.raises(ClientError) as exc:
dynamodb_client.create_table(
TableName="my-table",
AttributeDefinitions=[
{"AttributeName": "some_field", "AttributeType": "S"}
],
KeySchema=[{"AttributeName": "some_field", "KeyType": "HASH"}],
BillingMode="PROVISIONED",
StreamSpecification={"StreamEnabled": False, "StreamViewType": "NEW_IMAGE"},
)
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.equal(
"One or more parameter values were invalid: ReadCapacityUnits and WriteCapacityUnits must both be specified when BillingMode is PROVISIONED"
)
@mock_dynamodb2
def test_create_table_error_pay_per_request_with_provisioned_param():
client = boto3.client("dynamodb", region_name="us-east-1")
with pytest.raises(ClientError) as exc:
client.create_table(
TableName="test1",
AttributeDefinitions=[
{"AttributeName": "client", "AttributeType": "S"},
{"AttributeName": "app", "AttributeType": "S"},
],
KeySchema=[
{"AttributeName": "client", "KeyType": "HASH"},
{"AttributeName": "app", "KeyType": "RANGE"},
],
ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
BillingMode="PAY_PER_REQUEST",
)
err = exc.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.equal(
"ProvisionedThroughput cannot be specified when BillingMode is PAY_PER_REQUEST"
)
@mock_dynamodb2
def test_create_table_with_ssespecification__false():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="test1",
AttributeDefinitions=[
{"AttributeName": "client", "AttributeType": "S"},
{"AttributeName": "app", "AttributeType": "S"},
],
KeySchema=[
{"AttributeName": "client", "KeyType": "HASH"},
{"AttributeName": "app", "KeyType": "RANGE"},
],
BillingMode="PAY_PER_REQUEST",
SSESpecification={"Enabled": False},
)
actual = client.describe_table(TableName="test1")["Table"]
actual.shouldnt.have.key("SSEDescription")
@mock_dynamodb2
def test_create_table_with_ssespecification__true():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="test1",
AttributeDefinitions=[
{"AttributeName": "client", "AttributeType": "S"},
{"AttributeName": "app", "AttributeType": "S"},
],
KeySchema=[
{"AttributeName": "client", "KeyType": "HASH"},
{"AttributeName": "app", "KeyType": "RANGE"},
],
BillingMode="PAY_PER_REQUEST",
SSESpecification={"Enabled": True},
)
actual = client.describe_table(TableName="test1")["Table"]
actual.should.have.key("SSEDescription")
actual["SSEDescription"].should.have.key("Status").equals("ENABLED")
actual["SSEDescription"].should.have.key("SSEType").equals("KMS")
actual["SSEDescription"].should.have.key("KMSMasterKeyArn").match(
"^arn:aws:kms"
) # Default KMS key for DynamoDB
@mock_dynamodb2
def test_create_table_with_ssespecification__custom_kms_key():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="test1",
AttributeDefinitions=[
{"AttributeName": "client", "AttributeType": "S"},
{"AttributeName": "app", "AttributeType": "S"},
],
KeySchema=[
{"AttributeName": "client", "KeyType": "HASH"},
{"AttributeName": "app", "KeyType": "RANGE"},
],
BillingMode="PAY_PER_REQUEST",
SSESpecification={"Enabled": True, "KMSMasterKeyId": "custom-kms-key"},
)
actual = client.describe_table(TableName="test1")["Table"]
actual.should.have.key("SSEDescription")
actual["SSEDescription"].should.have.key("Status").equals("ENABLED")
actual["SSEDescription"].should.have.key("SSEType").equals("KMS")
actual["SSEDescription"].should.have.key("KMSMasterKeyArn").equals("custom-kms-key")

View File

@ -4,124 +4,12 @@ import boto3
from boto3.dynamodb.conditions import Key from boto3.dynamodb.conditions import Key
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
import sure # noqa # pylint: disable=unused-import import sure # noqa # pylint: disable=unused-import
from datetime import datetime
import pytest import pytest
from moto import mock_dynamodb2 from moto import mock_dynamodb2
from uuid import uuid4 from uuid import uuid4
@mock_dynamodb2
def test_create_table_boto3():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="messages",
KeySchema=[
{"AttributeName": "id", "KeyType": "HASH"},
{"AttributeName": "subject", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "id", "AttributeType": "S"},
{"AttributeName": "subject", "AttributeType": "S"},
],
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5},
)
actual = client.describe_table(TableName="messages")["Table"]
actual.should.have.key("AttributeDefinitions").equal(
[
{"AttributeName": "id", "AttributeType": "S"},
{"AttributeName": "subject", "AttributeType": "S"},
]
)
actual.should.have.key("CreationDateTime").be.a(datetime)
actual.should.have.key("GlobalSecondaryIndexes").equal([])
actual.should.have.key("LocalSecondaryIndexes").equal([])
actual.should.have.key("ProvisionedThroughput").equal(
{"NumberOfDecreasesToday": 0, "ReadCapacityUnits": 1, "WriteCapacityUnits": 5}
)
actual.should.have.key("TableSizeBytes").equal(0)
actual.should.have.key("TableName").equal("messages")
actual.should.have.key("TableStatus").equal("ACTIVE")
actual.should.have.key("TableArn").equal(
"arn:aws:dynamodb:us-east-1:123456789011:table/messages"
)
actual.should.have.key("KeySchema").equal(
[
{"AttributeName": "id", "KeyType": "HASH"},
{"AttributeName": "subject", "KeyType": "RANGE"},
]
)
actual.should.have.key("ItemCount").equal(0)
@mock_dynamodb2
def test_create_table_with_local_index_boto3():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="messages",
KeySchema=[
{"AttributeName": "id", "KeyType": "HASH"},
{"AttributeName": "subject", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "id", "AttributeType": "S"},
{"AttributeName": "subject", "AttributeType": "S"},
{"AttributeName": "threads", "AttributeType": "S"},
],
LocalSecondaryIndexes=[
{
"IndexName": "threads_index",
"KeySchema": [
{"AttributeName": "id", "KeyType": "HASH"},
{"AttributeName": "threads", "KeyType": "RANGE"},
],
"Projection": {"ProjectionType": "ALL"},
}
],
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5},
)
actual = client.describe_table(TableName="messages")["Table"]
actual.should.have.key("AttributeDefinitions").equal(
[
{"AttributeName": "id", "AttributeType": "S"},
{"AttributeName": "subject", "AttributeType": "S"},
{"AttributeName": "threads", "AttributeType": "S"},
]
)
actual.should.have.key("CreationDateTime").be.a(datetime)
actual.should.have.key("GlobalSecondaryIndexes").equal([])
actual.should.have.key("LocalSecondaryIndexes").equal(
[
{
"IndexName": "threads_index",
"KeySchema": [
{"AttributeName": "id", "KeyType": "HASH"},
{"AttributeName": "threads", "KeyType": "RANGE"},
],
"Projection": {"ProjectionType": "ALL"},
}
]
)
actual.should.have.key("ProvisionedThroughput").equal(
{"NumberOfDecreasesToday": 0, "ReadCapacityUnits": 1, "WriteCapacityUnits": 5}
)
actual.should.have.key("TableSizeBytes").equal(0)
actual.should.have.key("TableName").equal("messages")
actual.should.have.key("TableStatus").equal("ACTIVE")
actual.should.have.key("TableArn").equal(
"arn:aws:dynamodb:us-east-1:123456789011:table/messages"
)
actual.should.have.key("KeySchema").equal(
[
{"AttributeName": "id", "KeyType": "HASH"},
{"AttributeName": "subject", "KeyType": "RANGE"},
]
)
actual.should.have.key("ItemCount").equal(0)
@mock_dynamodb2 @mock_dynamodb2
def test_get_item_without_range_key_boto3(): def test_get_item_without_range_key_boto3():
client = boto3.resource("dynamodb", region_name="us-east-1") client = boto3.resource("dynamodb", region_name="us-east-1")
@ -192,83 +80,6 @@ def test_query_filter_boto3():
res["Items"].should.equal([{"pk": "pk", "sk": "sk-1"}, {"pk": "pk", "sk": "sk-2"}]) res["Items"].should.equal([{"pk": "pk", "sk": "sk-1"}, {"pk": "pk", "sk": "sk-2"}])
@mock_dynamodb2
def test_boto3_create_table_with_gsi():
dynamodb = boto3.client("dynamodb", region_name="us-east-1")
table = dynamodb.create_table(
TableName="users",
KeySchema=[
{"AttributeName": "forum_name", "KeyType": "HASH"},
{"AttributeName": "subject", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "forum_name", "AttributeType": "S"},
{"AttributeName": "subject", "AttributeType": "S"},
],
BillingMode="PAY_PER_REQUEST",
GlobalSecondaryIndexes=[
{
"IndexName": "test_gsi",
"KeySchema": [{"AttributeName": "subject", "KeyType": "HASH"}],
"Projection": {"ProjectionType": "ALL"},
}
],
)
table["TableDescription"]["GlobalSecondaryIndexes"].should.equal(
[
{
"KeySchema": [{"KeyType": "HASH", "AttributeName": "subject"}],
"IndexName": "test_gsi",
"Projection": {"ProjectionType": "ALL"},
"IndexStatus": "ACTIVE",
"ProvisionedThroughput": {
"ReadCapacityUnits": 0,
"WriteCapacityUnits": 0,
},
}
]
)
table = dynamodb.create_table(
TableName="users2",
KeySchema=[
{"AttributeName": "forum_name", "KeyType": "HASH"},
{"AttributeName": "subject", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "forum_name", "AttributeType": "S"},
{"AttributeName": "subject", "AttributeType": "S"},
],
BillingMode="PAY_PER_REQUEST",
GlobalSecondaryIndexes=[
{
"IndexName": "test_gsi",
"KeySchema": [{"AttributeName": "subject", "KeyType": "HASH"}],
"Projection": {"ProjectionType": "ALL"},
"ProvisionedThroughput": {
"ReadCapacityUnits": 3,
"WriteCapacityUnits": 5,
},
}
],
)
table["TableDescription"]["GlobalSecondaryIndexes"].should.equal(
[
{
"KeySchema": [{"KeyType": "HASH", "AttributeName": "subject"}],
"IndexName": "test_gsi",
"Projection": {"ProjectionType": "ALL"},
"IndexStatus": "ACTIVE",
"ProvisionedThroughput": {
"ReadCapacityUnits": 3,
"WriteCapacityUnits": 5,
},
}
]
)
@mock_dynamodb2 @mock_dynamodb2
def test_boto3_conditions(): def test_boto3_conditions():
dynamodb = boto3.resource("dynamodb", region_name="us-east-1") dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
@ -1103,8 +914,15 @@ def test_update_table_gsi_create():
table = dynamodb.Table("users") table = dynamodb.Table("users")
table.global_secondary_indexes.should.have.length_of(0) table.global_secondary_indexes.should.have.length_of(0)
table.attribute_definitions.should.have.length_of(2)
table.update( table.update(
AttributeDefinitions=[
{"AttributeName": "forum_name", "AttributeType": "S"},
{"AttributeName": "subject", "AttributeType": "S"},
{"AttributeName": "username", "AttributeType": "S"},
{"AttributeName": "created", "AttributeType": "N"},
],
GlobalSecondaryIndexUpdates=[ GlobalSecondaryIndexUpdates=[
{ {
"Create": { "Create": {
@ -1120,11 +938,13 @@ def test_update_table_gsi_create():
}, },
} }
} }
] ],
) )
table = dynamodb.Table("users") table = dynamodb.Table("users")
table.reload()
table.global_secondary_indexes.should.have.length_of(1) table.global_secondary_indexes.should.have.length_of(1)
table.attribute_definitions.should.have.length_of(4)
gsi_throughput = table.global_secondary_indexes[0]["ProvisionedThroughput"] gsi_throughput = table.global_secondary_indexes[0]["ProvisionedThroughput"]
assert gsi_throughput["ReadCapacityUnits"].should.equal(3) assert gsi_throughput["ReadCapacityUnits"].should.equal(3)
@ -1353,6 +1173,7 @@ def test_update_item_throws_exception_when_updating_hash_or_range_key(
{"AttributeName": "h", "AttributeType": "S"}, {"AttributeName": "h", "AttributeType": "S"},
{"AttributeName": "r", "AttributeType": "S"}, {"AttributeName": "r", "AttributeType": "S"},
], ],
BillingMode="PAY_PER_REQUEST",
) )
initial_val = str(uuid4()) initial_val = str(uuid4())

View File

@ -5,6 +5,7 @@ import pytest
from datetime import datetime from datetime import datetime
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from moto import mock_dynamodb2 from moto import mock_dynamodb2
from moto.core import ACCOUNT_ID
import botocore import botocore
@ -63,7 +64,7 @@ def test_create_table_boto3():
actual.should.have.key("TableName").equal("messages") actual.should.have.key("TableName").equal("messages")
actual.should.have.key("TableStatus").equal("ACTIVE") actual.should.have.key("TableStatus").equal("ACTIVE")
actual.should.have.key("TableArn").equal( actual.should.have.key("TableArn").equal(
"arn:aws:dynamodb:us-east-1:123456789011:table/messages" f"arn:aws:dynamodb:us-east-1:{ACCOUNT_ID}:table/messages"
) )
actual.should.have.key("KeySchema").equal( actual.should.have.key("KeySchema").equal(
[{"AttributeName": "id", "KeyType": "HASH"}] [{"AttributeName": "id", "KeyType": "HASH"}]
@ -93,26 +94,6 @@ def test_delete_table_boto3():
ex.value.response["Error"]["Message"].should.equal("Requested resource not found") ex.value.response["Error"]["Message"].should.equal("Requested resource not found")
@mock_dynamodb2
def test_update_table_throughput_boto3():
conn = boto3.resource("dynamodb", region_name="us-west-2")
table = conn.create_table(
TableName="messages",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
table.provisioned_throughput["ReadCapacityUnits"].should.equal(5)
table.provisioned_throughput["WriteCapacityUnits"].should.equal(5)
table.update(
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 6}
)
table.provisioned_throughput["ReadCapacityUnits"].should.equal(5)
table.provisioned_throughput["WriteCapacityUnits"].should.equal(6)
@mock_dynamodb2 @mock_dynamodb2
def test_item_add_and_describe_and_update_boto3(): def test_item_add_and_describe_and_update_boto3():
conn = boto3.resource("dynamodb", region_name="us-west-2") conn = boto3.resource("dynamodb", region_name="us-west-2")

View File

@ -0,0 +1,80 @@
import boto3
import sure # noqa # pylint: disable=unused-import
from moto import mock_dynamodb2
@mock_dynamodb2
def test_update_table__billing_mode():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="test",
AttributeDefinitions=[
{"AttributeName": "client", "AttributeType": "S"},
{"AttributeName": "app", "AttributeType": "S"},
],
KeySchema=[
{"AttributeName": "client", "KeyType": "HASH"},
{"AttributeName": "app", "KeyType": "RANGE"},
],
BillingMode="PAY_PER_REQUEST",
)
client.update_table(
TableName="test",
BillingMode="PROVISIONED",
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
)
actual = client.describe_table(TableName="test")["Table"]
actual.should.have.key("BillingModeSummary").equals({"BillingMode": "PROVISIONED"})
actual.should.have.key("ProvisionedThroughput").equals(
{"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}
)
@mock_dynamodb2
def test_update_table_throughput():
conn = boto3.resource("dynamodb", region_name="us-west-2")
table = conn.create_table(
TableName="messages",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
table.provisioned_throughput["ReadCapacityUnits"].should.equal(5)
table.provisioned_throughput["WriteCapacityUnits"].should.equal(5)
table.update(
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 6}
)
table.provisioned_throughput["ReadCapacityUnits"].should.equal(5)
table.provisioned_throughput["WriteCapacityUnits"].should.equal(6)
@mock_dynamodb2
def test_update_table__enable_stream():
conn = boto3.client("dynamodb", region_name="us-east-1")
resp = conn.create_table(
TableName="test-stream-update",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
)
assert "StreamSpecification" not in resp["TableDescription"]
resp = conn.update_table(
TableName="test-stream-update",
StreamSpecification={"StreamEnabled": True, "StreamViewType": "NEW_IMAGE"},
)
assert "StreamSpecification" in resp["TableDescription"]
assert resp["TableDescription"]["StreamSpecification"] == {
"StreamEnabled": True,
"StreamViewType": "NEW_IMAGE",
}
assert "LatestStreamLabel" in resp["TableDescription"]
assert "LatestStreamArn" in resp["TableDescription"]