DynamoDB improvements (#4849)

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

View File

@ -205,6 +205,13 @@ class MultipleTransactionsException(MockValidationException):
super().__init__(self.msg)
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

View File

@ -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

View File

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

View File

@ -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())

View File

@ -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")

View File

@ -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"},

View File

@ -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")

View File

@ -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"

View File

@ -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)

View File

@ -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",

View File

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

View File

@ -4,124 +4,12 @@ import boto3
from boto3.dynamodb.conditions import Key
from 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())

View File

@ -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")

View File

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