DynamoDB improvements (#4849)
This commit is contained in:
parent
b28d763c08
commit
e5c8cf058c
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
|
@ -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")
|
||||||
|
@ -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"},
|
||||||
|
@ -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")
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
406
tests/test_dynamodb2/test_dynamodb_create_table.py
Normal file
406
tests/test_dynamodb2/test_dynamodb_create_table.py
Normal 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")
|
@ -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())
|
||||||
|
@ -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")
|
||||||
|
80
tests/test_dynamodb2/test_dynamodb_update_table.py
Normal file
80
tests/test_dynamodb2/test_dynamodb_update_table.py
Normal 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"]
|
Loading…
Reference in New Issue
Block a user