diff --git a/docs/docs/services/dynamodb.rst b/docs/docs/services/dynamodb.rst index 16a3b139d..0122b9799 100644 --- a/docs/docs/services/dynamodb.rst +++ b/docs/docs/services/dynamodb.rst @@ -61,7 +61,7 @@ dynamodb - [X] put_item - [X] query - [X] restore_table_from_backup -- [ ] restore_table_to_point_in_time +- [X] restore_table_to_point_in_time - [X] scan - [X] tag_resource - [X] transact_get_items diff --git a/moto/dynamodb2/models/__init__.py b/moto/dynamodb2/models/__init__.py index 388d82067..c1576db7a 100644 --- a/moto/dynamodb2/models/__init__.py +++ b/moto/dynamodb2/models/__init__.py @@ -1020,6 +1020,36 @@ class RestoredTable(Table): return result +class RestoredPITTable(Table): + def __init__(self, name, source): + params = self._parse_params_from_table(source) + super(RestoredPITTable, self).__init__(name, **params) + self.indexes = copy.deepcopy(source.indexes) + self.global_indexes = copy.deepcopy(source.global_indexes) + self.items = copy.deepcopy(source.items) + # Restore Attrs + self.source_table_arn = source.table_arn + self.restore_date_time = self.created_at + + @staticmethod + def _parse_params_from_table(table): + params = { + "schema": copy.deepcopy(table.schema), + "attr": copy.deepcopy(table.attr), + "throughput": copy.deepcopy(table.throughput), + } + return params + + def describe(self, base_key="TableDescription"): + result = super(RestoredPITTable, self).describe(base_key=base_key) + result[base_key]["RestoreSummary"] = { + "SourceTableArn": self.source_table_arn, + "RestoreDateTime": unix_time(self.restore_date_time), + "RestoreInProgress": False, + } + return result + + class Backup(object): def __init__( self, backend, name, table, status=None, type_=None, @@ -1700,6 +1730,22 @@ class DynamoDBBackend(BaseBackend): self.tables[target_table_name] = new_table return new_table + """ + Currently this only accepts the source and target table elements, and will + copy all items from the source without respect to other arguments. + """ + + def restore_table_to_point_in_time(self, target_table_name, source_table_name): + source = self.get_table(source_table_name) + if source is None: + raise KeyError() + existing_table = self.get_table(target_table_name) + if existing_table is not None: + raise ValueError() + new_table = RestoredPITTable(target_table_name, source) + self.tables[target_table_name] = new_table + return new_table + ###################### # LIST of methods where the logic completely resides in responses.py # Duplicated here so that the implementation coverage script is aware diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 7ea3936c5..fba153c6f 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -1203,3 +1203,19 @@ class DynamoHandler(BaseResponse): except ValueError: er = "com.amazonaws.dynamodb.v20111205#TableAlreadyExistsException" return self.error(er, "Table already exists: %s" % target_table_name) + + def restore_table_to_point_in_time(self): + body = self.body + target_table_name = body.get("TargetTableName") + source_table_name = body.get("SourceTableName") + try: + restored_table = self.dynamodb_backend.restore_table_to_point_in_time( + target_table_name, source_table_name + ) + return dynamo_json_dump(restored_table.describe()) + except KeyError: + er = "com.amazonaws.dynamodb.v20111205#SourceTableNotFoundException" + return self.error(er, "Source table not found: %s" % source_table_name) + except ValueError: + er = "com.amazonaws.dynamodb.v20111205#TableAlreadyExistsException" + return self.error(er, "Table already exists: %s" % target_table_name) diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index dbe238b23..69217d7f7 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -5524,6 +5524,76 @@ def test_restore_table_from_backup(): summary.should.have.key("RestoreInProgress").should.equal(False) +@mock_dynamodb2 +def test_restore_table_to_point_in_time(): + client = boto3.client("dynamodb", "us-east-1") + table_name = "test-table" + resp = client.create_table( + TableName=table_name, + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + table = resp.get("TableDescription") + for i in range(5): + client.put_item(TableName=table_name, Item={"id": {"S": "item %d" % i}}) + + restored_table_name = "restored-from-pit" + restored = client.restore_table_to_point_in_time( + TargetTableName=restored_table_name, SourceTableName=table_name + ).get("TableDescription") + restored.should.have.key("TableName").should.equal(restored_table_name) + restored.should.have.key("KeySchema").should.equal(table["KeySchema"]) + restored.should.have.key("TableStatus") + restored.should.have.key("ItemCount").should.equal(5) + restored.should.have.key("TableArn").should.contain(restored_table_name) + restored.should.have.key("RestoreSummary").should.be.a(dict) + summary = restored.get("RestoreSummary") + summary.should.have.key("SourceTableArn").should.equal(table["TableArn"]) + summary.should.have.key("RestoreDateTime").should.be.a(datetime) + summary.should.have.key("RestoreInProgress").should.equal(False) + + +@mock_dynamodb2 +def test_restore_table_to_point_in_time_raises_error_when_source_not_exist(): + client = boto3.client("dynamodb", "us-east-1") + table_name = "test-table" + restored_table_name = "restored-from-pit" + with pytest.raises(ClientError) as ex: + client.restore_table_to_point_in_time( + TargetTableName=restored_table_name, SourceTableName=table_name + ) + error = ex.value.response["Error"] + error["Code"].should.equal("SourceTableNotFoundException") + error["Message"].should.equal("Source table not found: %s" % table_name) + + +@mock_dynamodb2 +def test_restore_table_to_point_in_time_raises_error_when_dest_exist(): + client = boto3.client("dynamodb", "us-east-1") + table_name = "test-table" + restored_table_name = "restored-from-pit" + client.create_table( + TableName=table_name, + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + client.create_table( + TableName=restored_table_name, + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + with pytest.raises(ClientError) as ex: + client.restore_table_to_point_in_time( + TargetTableName=restored_table_name, SourceTableName=table_name + ) + error = ex.value.response["Error"] + error["Code"].should.equal("TableAlreadyExistsException") + error["Message"].should.equal("Table already exists: %s" % restored_table_name) + + @mock_dynamodb2 def test_delete_non_existent_backup_raises_error(): client = boto3.client("dynamodb", "us-east-1")