DynamoDB improvements (#4849)
This commit is contained in:
parent
b28d763c08
commit
e5c8cf058c
@ -205,6 +205,13 @@ class MultipleTransactionsException(MockValidationException):
|
||||
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):
|
||||
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
|
||||
|
@ -24,6 +24,7 @@ from moto.dynamodb2.exceptions import (
|
||||
EmptyKeyAttributeException,
|
||||
InvalidAttributeTypeError,
|
||||
MultipleTransactionsException,
|
||||
TooManyTransactionsException,
|
||||
)
|
||||
from moto.dynamodb2.models.utilities import bytesize
|
||||
from moto.dynamodb2.models.dynamo_type import DynamoType
|
||||
@ -388,12 +389,16 @@ class Table(CloudFormationModel):
|
||||
def __init__(
|
||||
self,
|
||||
table_name,
|
||||
region,
|
||||
schema=None,
|
||||
attr=None,
|
||||
throughput=None,
|
||||
billing_mode=None,
|
||||
indexes=None,
|
||||
global_indexes=None,
|
||||
streams=None,
|
||||
sse_specification=None,
|
||||
tags=None,
|
||||
):
|
||||
self.name = table_name
|
||||
self.attr = attr
|
||||
@ -417,8 +422,9 @@ class Table(CloudFormationModel):
|
||||
self.table_key_attrs = [
|
||||
key for key in (self.hash_key_attr, self.range_key_attr) if key
|
||||
]
|
||||
self.billing_mode = billing_mode
|
||||
if throughput is None:
|
||||
self.throughput = {"WriteCapacityUnits": 10, "ReadCapacityUnits": 10}
|
||||
self.throughput = {"WriteCapacityUnits": 0, "ReadCapacityUnits": 0}
|
||||
else:
|
||||
self.throughput = throughput
|
||||
self.throughput["NumberOfDecreasesToday"] = 0
|
||||
@ -433,11 +439,14 @@ class Table(CloudFormationModel):
|
||||
self.created_at = datetime.datetime.utcnow()
|
||||
self.items = defaultdict(dict)
|
||||
self.table_arn = self._generate_arn(table_name)
|
||||
self.tags = []
|
||||
self.tags = tags or []
|
||||
self.ttl = {
|
||||
"TimeToLiveStatus": "DISABLED" # One of 'ENABLING'|'DISABLING'|'ENABLED'|'DISABLED',
|
||||
# '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.lambda_event_source_mappings = {}
|
||||
self.continuous_backups = {
|
||||
@ -446,6 +455,32 @@ class Table(CloudFormationModel):
|
||||
"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
|
||||
def has_cfn_attr(cls, attribute):
|
||||
@ -466,7 +501,7 @@ class Table(CloudFormationModel):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def key_attributes(self):
|
||||
def attribute_keys(self):
|
||||
# A set of all the hash or range attributes for all indexes
|
||||
def keys_from_index(idx):
|
||||
schema = idx.schema
|
||||
@ -519,7 +554,7 @@ class Table(CloudFormationModel):
|
||||
return table
|
||||
|
||||
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):
|
||||
self.stream_specification = streams
|
||||
@ -529,14 +564,13 @@ class Table(CloudFormationModel):
|
||||
self.stream_shard = StreamShard(self)
|
||||
else:
|
||||
self.stream_specification = {"StreamEnabled": False}
|
||||
self.latest_stream_label = None
|
||||
self.stream_shard = None
|
||||
|
||||
def describe(self, base_key="TableDescription"):
|
||||
results = {
|
||||
base_key: {
|
||||
"AttributeDefinitions": self.attr,
|
||||
"ProvisionedThroughput": self.throughput,
|
||||
"BillingModeSummary": {"BillingMode": self.billing_mode},
|
||||
"TableSizeBytes": 0,
|
||||
"TableName": self.name,
|
||||
"TableStatus": "ACTIVE",
|
||||
@ -550,13 +584,19 @@ class Table(CloudFormationModel):
|
||||
"LocalSecondaryIndexes": [index.describe() for index in self.indexes],
|
||||
}
|
||||
}
|
||||
if self.latest_stream_label:
|
||||
results[base_key]["LatestStreamLabel"] = self.latest_stream_label
|
||||
results[base_key][
|
||||
"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.latest_stream_label:
|
||||
results[base_key]["LatestStreamLabel"] = self.latest_stream_label
|
||||
results[base_key]["LatestStreamArn"] = (
|
||||
self.table_arn + "/stream/" + self.latest_stream_label
|
||||
)
|
||||
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
|
||||
|
||||
def __len__(self):
|
||||
@ -988,9 +1028,9 @@ class Table(CloudFormationModel):
|
||||
|
||||
|
||||
class RestoredTable(Table):
|
||||
def __init__(self, name, backup):
|
||||
def __init__(self, name, region, 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.global_indexes = copy.deepcopy(backup.table.global_indexes)
|
||||
self.items = copy.deepcopy(backup.table.items)
|
||||
@ -1020,9 +1060,9 @@ class RestoredTable(Table):
|
||||
|
||||
|
||||
class RestoredPITTable(Table):
|
||||
def __init__(self, name, source):
|
||||
def __init__(self, name, region, 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.global_indexes = copy.deepcopy(source.global_indexes)
|
||||
self.items = copy.deepcopy(source.items)
|
||||
@ -1145,7 +1185,7 @@ class DynamoDBBackend(BaseBackend):
|
||||
def create_table(self, name, **params):
|
||||
if name in self.tables:
|
||||
return None
|
||||
table = Table(name, **params)
|
||||
table = Table(name, region=self.region_name, **params)
|
||||
self.tables[name] = table
|
||||
return table
|
||||
|
||||
@ -1205,12 +1245,24 @@ class DynamoDBBackend(BaseBackend):
|
||||
table = self.tables[name]
|
||||
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)
|
||||
if attr_definitions:
|
||||
table.attr = attr_definitions
|
||||
if global_index:
|
||||
table = self.update_table_global_indexes(name, global_index)
|
||||
if throughput:
|
||||
table = self.update_table_throughput(name, throughput)
|
||||
if billing_mode:
|
||||
table = self.update_table_billing_mode(name, billing_mode)
|
||||
if stream_spec:
|
||||
table = self.update_table_streams(name, stream_spec)
|
||||
return table
|
||||
@ -1220,6 +1272,11 @@ class DynamoDBBackend(BaseBackend):
|
||||
table.throughput = throughput
|
||||
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):
|
||||
table = self.tables[name]
|
||||
if (
|
||||
@ -1495,7 +1552,7 @@ class DynamoDBBackend(BaseBackend):
|
||||
item = table.get_item(hash_value, range_value)
|
||||
|
||||
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:
|
||||
validator = UpdateExpressionValidator(
|
||||
@ -1568,6 +1625,8 @@ class DynamoDBBackend(BaseBackend):
|
||||
return table.ttl
|
||||
|
||||
def transact_write_items(self, transact_items):
|
||||
if len(transact_items) > 25:
|
||||
raise TooManyTransactionsException()
|
||||
# Create a backup in case any of the transactions fail
|
||||
original_table_state = copy.deepcopy(self.tables)
|
||||
target_items = set()
|
||||
@ -1739,7 +1798,9 @@ class DynamoDBBackend(BaseBackend):
|
||||
existing_table = self.get_table(target_table_name)
|
||||
if existing_table is not None:
|
||||
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
|
||||
return new_table
|
||||
|
||||
@ -1755,7 +1816,9 @@ class DynamoDBBackend(BaseBackend):
|
||||
existing_table = self.get_table(target_table_name)
|
||||
if existing_table is not None:
|
||||
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
|
||||
return new_table
|
||||
|
||||
|
@ -410,6 +410,6 @@ class UpdateExpressionValidator(Validator):
|
||||
UpdateExpressionFunctionEvaluator(),
|
||||
NoneExistingPathChecker(),
|
||||
ExecuteOperations(),
|
||||
EmptyStringKeyValueValidator(self.table.key_attributes),
|
||||
EmptyStringKeyValueValidator(self.table.attribute_keys),
|
||||
]
|
||||
return processors
|
||||
|
@ -69,7 +69,7 @@ def include_consumed_capacity(val=1.0):
|
||||
|
||||
def put_has_empty_keys(field_updates, table):
|
||||
if table:
|
||||
key_names = table.key_attributes
|
||||
key_names = table.attribute_keys
|
||||
|
||||
# string/binary fields with empty string as value
|
||||
empty_str_fields = [
|
||||
@ -168,12 +168,20 @@ class DynamoHandler(BaseResponse):
|
||||
er = "com.amazonaws.dynamodb.v20111205#ValidationException"
|
||||
return self.error(
|
||||
er,
|
||||
"ProvisionedThroughput cannot be specified \
|
||||
when BillingMode is PAY_PER_REQUEST",
|
||||
"ProvisionedThroughput cannot be specified when BillingMode is PAY_PER_REQUEST",
|
||||
)
|
||||
throughput = None
|
||||
billing_mode = "PAY_PER_REQUEST"
|
||||
else: # Provisioned (default billing mode)
|
||||
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
|
||||
key_schema = body["KeySchema"]
|
||||
# getting attribute definition
|
||||
@ -218,6 +226,8 @@ class DynamoHandler(BaseResponse):
|
||||
)
|
||||
# get the stream specification
|
||||
streams = body.get("StreamSpecification")
|
||||
# Get any tags
|
||||
tags = body.get("Tags", [])
|
||||
|
||||
table = self.dynamodb_backend.create_table(
|
||||
table_name,
|
||||
@ -227,6 +237,9 @@ class DynamoHandler(BaseResponse):
|
||||
global_indexes=global_indexes,
|
||||
indexes=local_secondary_indexes,
|
||||
streams=streams,
|
||||
billing_mode=billing_mode,
|
||||
sse_specification=sse_spec,
|
||||
tags=tags,
|
||||
)
|
||||
if table is not None:
|
||||
return dynamo_json_dump(table.describe())
|
||||
@ -339,14 +352,18 @@ class DynamoHandler(BaseResponse):
|
||||
|
||||
def update_table(self):
|
||||
name = self.body["TableName"]
|
||||
attr_definitions = self.body.get("AttributeDefinitions", None)
|
||||
global_index = self.body.get("GlobalSecondaryIndexUpdates", None)
|
||||
throughput = self.body.get("ProvisionedThroughput", None)
|
||||
billing_mode = self.body.get("BillingMode", None)
|
||||
stream_spec = self.body.get("StreamSpecification", None)
|
||||
try:
|
||||
table = self.dynamodb_backend.update_table(
|
||||
name=name,
|
||||
attr_definitions=attr_definitions,
|
||||
global_index=global_index,
|
||||
throughput=throughput,
|
||||
billing_mode=billing_mode,
|
||||
stream_spec=stream_spec,
|
||||
)
|
||||
return dynamo_json_dump(table.describe())
|
||||
|
@ -115,6 +115,7 @@ def test_invoke_function_from_dynamodb_put():
|
||||
"StreamEnabled": True,
|
||||
"StreamViewType": "NEW_AND_OLD_IMAGES",
|
||||
},
|
||||
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5},
|
||||
)
|
||||
|
||||
conn = boto3.client("lambda", region_name="us-east-1")
|
||||
@ -165,6 +166,7 @@ def test_invoke_function_from_dynamodb_update():
|
||||
"StreamEnabled": True,
|
||||
"StreamViewType": "NEW_AND_OLD_IMAGES",
|
||||
},
|
||||
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5},
|
||||
)
|
||||
|
||||
conn = boto3.client("lambda", region_name="us-east-1")
|
||||
|
@ -6,6 +6,7 @@ from moto.dynamodb2.models import Table
|
||||
def table():
|
||||
return Table(
|
||||
"Forums",
|
||||
region="us-east-1",
|
||||
schema=[
|
||||
{"KeyType": "HASH", "AttributeName": "forum_name"},
|
||||
{"KeyType": "RANGE", "AttributeName": "subject"},
|
||||
|
@ -502,3 +502,35 @@ def test_multiple_transactions_on_same_item():
|
||||
err["Message"].should.equal(
|
||||
"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")
|
||||
|
||||
|
||||
@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
|
||||
def test_duplicate_create():
|
||||
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
|
||||
def test_query_gsi_with_range_key():
|
||||
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",
|
||||
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
|
||||
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
|
||||
BillingMode="PAY_PER_REQUEST",
|
||||
)
|
||||
|
||||
try:
|
||||
@ -4338,6 +4245,7 @@ def test_update_expression_with_multiple_set_clauses_must_be_comma_separated():
|
||||
TableName="moto-test",
|
||||
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
|
||||
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
|
||||
BillingMode="PAY_PER_REQUEST",
|
||||
)
|
||||
|
||||
try:
|
||||
@ -5123,6 +5031,7 @@ def test_attribute_item_delete():
|
||||
TableName=name,
|
||||
AttributeDefinitions=[{"AttributeName": "name", "AttributeType": "S"}],
|
||||
KeySchema=[{"AttributeName": "name", "KeyType": "HASH"}],
|
||||
BillingMode="PAY_PER_REQUEST",
|
||||
)
|
||||
|
||||
item_name = "foo"
|
||||
|
@ -15,6 +15,7 @@ def test_condition_expression_with_dot_in_attr_name():
|
||||
TableName=table_name,
|
||||
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
|
||||
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
|
||||
BillingMode="PAY_PER_REQUEST",
|
||||
)
|
||||
table = dynamodb.Table(table_name)
|
||||
|
||||
@ -247,6 +248,7 @@ def test_condition_expression_numerical_attribute():
|
||||
TableName="my-table",
|
||||
KeySchema=[{"AttributeName": "partitionKey", "KeyType": "HASH"}],
|
||||
AttributeDefinitions=[{"AttributeName": "partitionKey", "AttributeType": "S"}],
|
||||
BillingMode="PAY_PER_REQUEST",
|
||||
)
|
||||
table = dynamodb.Table("my-table")
|
||||
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,
|
||||
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
|
||||
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
|
||||
BillingMode="PAY_PER_REQUEST",
|
||||
)
|
||||
table = dynamodb.Table(table_name)
|
||||
|
||||
|
@ -37,6 +37,7 @@ def test_consumed_capacity_get_unknown_item():
|
||||
TableName="test_table",
|
||||
KeySchema=[{"AttributeName": "u", "KeyType": "HASH"}],
|
||||
AttributeDefinitions=[{"AttributeName": "u", "AttributeType": "S"}],
|
||||
BillingMode="PAY_PER_REQUEST",
|
||||
)
|
||||
response = conn.get_item(
|
||||
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 botocore.exceptions import ClientError
|
||||
import sure # noqa # pylint: disable=unused-import
|
||||
from datetime import datetime
|
||||
import pytest
|
||||
|
||||
from moto import mock_dynamodb2
|
||||
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
|
||||
def test_get_item_without_range_key_boto3():
|
||||
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"}])
|
||||
|
||||
|
||||
@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
|
||||
def test_boto3_conditions():
|
||||
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
|
||||
@ -1103,8 +914,15 @@ def test_update_table_gsi_create():
|
||||
table = dynamodb.Table("users")
|
||||
|
||||
table.global_secondary_indexes.should.have.length_of(0)
|
||||
table.attribute_definitions.should.have.length_of(2)
|
||||
|
||||
table.update(
|
||||
AttributeDefinitions=[
|
||||
{"AttributeName": "forum_name", "AttributeType": "S"},
|
||||
{"AttributeName": "subject", "AttributeType": "S"},
|
||||
{"AttributeName": "username", "AttributeType": "S"},
|
||||
{"AttributeName": "created", "AttributeType": "N"},
|
||||
],
|
||||
GlobalSecondaryIndexUpdates=[
|
||||
{
|
||||
"Create": {
|
||||
@ -1120,11 +938,13 @@ def test_update_table_gsi_create():
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
table = dynamodb.Table("users")
|
||||
table.reload()
|
||||
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"]
|
||||
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": "r", "AttributeType": "S"},
|
||||
],
|
||||
BillingMode="PAY_PER_REQUEST",
|
||||
)
|
||||
|
||||
initial_val = str(uuid4())
|
||||
|
@ -5,6 +5,7 @@ import pytest
|
||||
from datetime import datetime
|
||||
from botocore.exceptions import ClientError
|
||||
from moto import mock_dynamodb2
|
||||
from moto.core import ACCOUNT_ID
|
||||
import botocore
|
||||
|
||||
|
||||
@ -63,7 +64,7 @@ def test_create_table_boto3():
|
||||
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"
|
||||
f"arn:aws:dynamodb:us-east-1:{ACCOUNT_ID}:table/messages"
|
||||
)
|
||||
actual.should.have.key("KeySchema").equal(
|
||||
[{"AttributeName": "id", "KeyType": "HASH"}]
|
||||
@ -93,26 +94,6 @@ def test_delete_table_boto3():
|
||||
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
|
||||
def test_item_add_and_describe_and_update_boto3():
|
||||
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