From 6a601d7d5c0e2556ebc251035a718d45e0da935a Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Sun, 3 Nov 2019 14:02:25 +0000 Subject: [PATCH 1/3] #2527 - DynamoDB - Allow nested attributes in ProjectionExpressions --- moto/dynamodb2/models.py | 42 ++-- moto/dynamodb2/responses.py | 33 +--- tests/test_dynamodb2/test_dynamodb.py | 268 ++++++++++++++++++++++++++ 3 files changed, 305 insertions(+), 38 deletions(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index cd49d7b1..0854f428 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -107,6 +107,19 @@ class DynamoType(object): else: self.value.pop(key) + def filter(self, projection_expressions): + nested_projections = [expr[0:expr.index('.')] for expr in projection_expressions if '.' in expr] + if self.is_map(): + expressions_to_delete = [] + for attr in self.value: + if attr not in projection_expressions and attr not in nested_projections: + expressions_to_delete.append(attr) + elif attr in nested_projections: + relevant_expressions = [expr[len(attr + '.'):] for expr in projection_expressions if expr.startswith(attr + '.')] + self.value[attr].filter(relevant_expressions) + for expr in expressions_to_delete: + self.value.pop(expr) + def __hash__(self): return hash((self.type, self.value)) @@ -477,6 +490,18 @@ class Item(BaseModel): "%s action not support for update_with_attribute_updates" % action ) + # Filter using projection_expression + # Ensure a deep copy is used to filter, otherwise actual data will be removed + def filter(self, projection_expression): + expressions = [x.strip() for x in projection_expression.split(',')] + top_level_expressions = [expr[0:expr.index('.')] for expr in expressions if '.' in expr] + for attr in list(self.attrs): + if attr not in expressions and attr not in top_level_expressions: + self.attrs.pop(attr) + if attr in top_level_expressions: + relevant_expressions = [expr[len(attr + '.'):] for expr in expressions if expr.startswith(attr + '.')] + self.attrs[attr].filter(relevant_expressions) + class StreamRecord(BaseModel): def __init__(self, table, stream_type, event_name, old, new, seq): @@ -774,11 +799,8 @@ class Table(BaseModel): result = self.items[hash_key] if projection_expression and result: - expressions = [x.strip() for x in projection_expression.split(",")] result = copy.deepcopy(result) - for attr in list(result.attrs): - if attr not in expressions: - result.attrs.pop(attr) + result.filter(projection_expression) if not result: raise KeyError @@ -911,13 +933,10 @@ class Table(BaseModel): if filter_expression is not None: results = [item for item in results if filter_expression.expr(item)] + results = copy.deepcopy(results) if projection_expression: - expressions = [x.strip() for x in projection_expression.split(",")] - results = copy.deepcopy(results) for result in results: - for attr in list(result.attrs): - if attr not in expressions: - result.attrs.pop(attr) + result.filter(projection_expression) results, last_evaluated_key = self._trim_results( results, limit, exclusive_start_key @@ -1004,12 +1023,9 @@ class Table(BaseModel): results.append(item) if projection_expression: - expressions = [x.strip() for x in projection_expression.split(",")] results = copy.deepcopy(results) for result in results: - for attr in list(result.attrs): - if attr not in expressions: - result.attrs.pop(attr) + result.filter(projection_expression) results, last_evaluated_key = self._trim_results( results, limit, exclusive_start_key, index_name diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index fd1d19ff..9c7bcd1f 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -346,9 +346,7 @@ class DynamoHandler(BaseResponse): projection_expression = self.body.get("ProjectionExpression") expression_attribute_names = self.body.get("ExpressionAttributeNames", {}) - projection_expression = self._adjust_projection_expression( - projection_expression, expression_attribute_names - ) + projection_expression = self._adjust_projection_expression(projection_expression, expression_attribute_names) try: item = self.dynamodb_backend.get_item(name, key, projection_expression) @@ -415,9 +413,7 @@ class DynamoHandler(BaseResponse): filter_expression = self.body.get("FilterExpression") expression_attribute_values = self.body.get("ExpressionAttributeValues", {}) - projection_expression = self._adjust_projection_expression( - projection_expression, expression_attribute_names - ) + projection_expression = self._adjust_projection_expression(projection_expression, expression_attribute_names) filter_kwargs = {} @@ -571,25 +567,12 @@ class DynamoHandler(BaseResponse): return dynamo_json_dump(result) - def _adjust_projection_expression( - self, projection_expression, expression_attribute_names - ): - if projection_expression and expression_attribute_names: - expressions = [x.strip() for x in projection_expression.split(",")] - projection_expr = None - for expression in expressions: - if projection_expr is not None: - projection_expr = projection_expr + ", " - else: - projection_expr = "" - - if expression in expression_attribute_names: - projection_expr = ( - projection_expr + expression_attribute_names[expression] - ) - else: - projection_expr = projection_expr + expression - return projection_expr + def _adjust_projection_expression(self, projection_expression, expr_attr_names): + def _adjust(expression): + return expr_attr_names[expression] if expression in expr_attr_names else expression + if projection_expression and expr_attr_names: + expressions = [x.strip() for x in projection_expression.split(',')] + return ','.join(['.'.join([_adjust(expr) for expr in nested_expr.split('.')]) for nested_expr in expressions]) return projection_expression diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index f6ed4f13..2d47a83e 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -559,6 +559,178 @@ def test_basic_projection_expressions_using_scan(): assert "forum_name" in results["Items"][1] +@mock_dynamodb2 +def test_nested_projection_expression_using_get_item(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB table. + dynamodb.create_table(TableName='users', + KeySchema=[{'AttributeName': 'forum_name', 'KeyType': 'HASH'}], + AttributeDefinitions=[{'AttributeName': 'forum_name', 'AttributeType': 'S'}], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) + table = dynamodb.Table('users') + table.put_item(Item={'forum_name': 'key1', 'nested': {'level1': {'id': 'id1', 'att': 'irrelevant'}, + 'level2': {'id': 'id2', 'include': 'all'}, + 'level3': {'id': 'irrelevant'}}, + 'foo': 'bar'}) + table.put_item(Item={'forum_name': 'key2', 'nested': {'id': 'id2', 'incode': 'code2'}, 'foo': 'bar'}) + + # Test a get_item returning all items + result = table.get_item(Key={'forum_name': 'key1'}, + ProjectionExpression='nested.level1.id, nested.level2')['Item'] + result.should.equal({'nested': {'level1': {'id': 'id1'}, + 'level2': {'id': 'id2', 'include': 'all'}}}) + # Assert actual data has not been deleted + result = table.get_item(Key={'forum_name': 'key1'})['Item'] + result.should.equal({u'foo': u'bar', + u'forum_name': u'key1', + u'nested': {u'level1': {u'id': u'id1', u'att': u'irrelevant'}, + u'level2': {u'id': u'id2', u'include': u'all'}, + u'level3': {u'id': u'irrelevant'}}}) + + +@mock_dynamodb2 +def test_basic_projection_expressions_using_query(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB 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'}], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) + table = dynamodb.Table('users') + table.put_item(Item={'forum_name': 'the-key', 'subject': '123', 'body': 'some test message'}) + table.put_item(Item={'forum_name': 'not-the-key', 'subject': '123', 'body': 'some other test message'}) + + # Test a query returning all items + result = table.query(KeyConditionExpression=Key('forum_name').eq('the-key'), + ProjectionExpression='body, subject')['Items'][0] + + assert 'body' in result + assert result['body'] == 'some test message' + assert 'subject' in result + assert 'forum_name' not in result + + table.put_item(Item={'forum_name': 'the-key', 'subject': '1234', 'body': 'yet another test message'}) + + items = table.query(KeyConditionExpression=Key('forum_name').eq('the-key'), + ProjectionExpression='body')['Items'] + + assert 'body' in items[0] + assert 'subject' not in items[0] + assert items[0]['body'] == 'some test message' + assert 'body' in items[1] + assert 'subject' not in items[1] + assert items[1]['body'] == 'yet another test message' + + # The projection expression should not remove data from storage + items = table.query(KeyConditionExpression=Key('forum_name').eq('the-key'))['Items'] + assert 'subject' in items[0] + assert 'body' in items[1] + assert 'forum_name' in items[1] + + +@mock_dynamodb2 +def test_nested_projection_expression_using_query(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB table. + dynamodb.create_table(TableName='users', + KeySchema=[{'AttributeName': 'forum_name', 'KeyType': 'HASH'}], + AttributeDefinitions=[{'AttributeName': 'forum_name', 'AttributeType': 'S'}], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) + table = dynamodb.Table('users') + table.put_item(Item={'forum_name': 'key1', 'nested': {'level1': {'id': 'id1', 'att': 'irrelevant'}, + 'level2': {'id': 'id2', 'include': 'all'}, + 'level3': {'id': 'irrelevant'}}, + 'foo': 'bar'}) + table.put_item(Item={'forum_name': 'key2', 'nested': {'id': 'id2', 'incode': 'code2'}, 'foo': 'bar'}) + + # Test a query returning all items + result = table.query(KeyConditionExpression=Key('forum_name').eq('key1'), + ProjectionExpression="nested.level1.id, nested.level2")['Items'][0] + + assert 'nested' in result + result['nested'].should.equal({'level1': {'id': 'id1'}, + 'level2': {'id': 'id2', 'include': 'all'}}) + assert 'foo' not in result + # Assert actual data has not been deleted + result = table.query(KeyConditionExpression=Key('forum_name').eq('key1'))['Items'][0] + result.should.equal({u'foo': u'bar', + u'forum_name': u'key1', + u'nested': {u'level1': {u'id': u'id1', u'att': u'irrelevant'}, + u'level2': {u'id': u'id2', u'include': u'all'}, + u'level3': {u'id': u'irrelevant'}}}) + + +@mock_dynamodb2 +def test_basic_projection_expressions_using_scan(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB 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'}], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) + table = dynamodb.Table('users') + + table.put_item(Item={'forum_name': 'the-key', 'subject': '123', 'body': 'some test message'}) + table.put_item(Item={'forum_name': 'not-the-key', 'subject': '123', 'body': 'some other test message'}) + # Test a scan returning all items + results = table.scan(FilterExpression=Key('forum_name').eq('the-key'), + ProjectionExpression='body, subject')['Items'] + + results.should.equal([{u'body': u'some test message', u'subject': u'123'}]) + + table.put_item(Item={'forum_name': 'the-key', 'subject': '1234', 'body': 'yet another test message'}) + + results = table.scan(FilterExpression=Key('forum_name').eq('the-key'), + ProjectionExpression='body')['Items'] + + results.should.equal([{u'body': u'yet another test message'}, + {u'body': u'some test message'}]) + + # The projection expression should not remove data from storage + results = table.query(KeyConditionExpression=Key('forum_name').eq('the-key')) + assert 'subject' in results['Items'][0] + assert 'body' in results['Items'][1] + assert 'forum_name' in results['Items'][1] + + +@mock_dynamodb2 +def test_nested_projection_expression_using_scan(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB table. + dynamodb.create_table(TableName='users', + KeySchema=[{'AttributeName': 'forum_name', 'KeyType': 'HASH'}], + AttributeDefinitions=[{'AttributeName': 'forum_name', 'AttributeType': 'S'}], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) + table = dynamodb.Table('users') + table.put_item(Item={'forum_name': 'key1', 'nested': {'level1': {'id': 'id1', 'att': 'irrelevant'}, + 'level2': {'id': 'id2', 'include': 'all'}, + 'level3': {'id': 'irrelevant'}}, + 'foo': 'bar'}) + table.put_item(Item={'forum_name': 'key2', 'nested': {'id': 'id2', 'incode': 'code2'}, 'foo': 'bar'}) + + # Test a scan + results = table.scan(FilterExpression=Key('forum_name').eq('key1'), + ProjectionExpression='nested.level1.id, nested.level2')['Items'] + results.should.equal([{'nested': {'level1': {'id': 'id1'}, + 'level2': {'include': 'all', 'id': 'id2'}}}]) + # Assert original data is still there + results = table.scan(FilterExpression=Key('forum_name').eq('key1'))['Items'] + results.should.equal([{'forum_name': 'key1', + 'foo': 'bar', + 'nested': {'level1': {'att': 'irrelevant', 'id': 'id1'}, + 'level2': {'include': 'all', 'id': 'id2'}, + 'level3': {'id': 'irrelevant'}}}]) + + @mock_dynamodb2 def test_basic_projection_expression_using_get_item_with_attr_expression_names(): dynamodb = boto3.resource("dynamodb", region_name="us-east-1") @@ -658,6 +830,71 @@ def test_basic_projection_expressions_using_query_with_attr_expression_names(): assert results["Items"][0]["attachment"] == "something" +@mock_dynamodb2 +def test_nested_projection_expression_using_get_item_with_attr_expression(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB table. + dynamodb.create_table(TableName='users', + KeySchema=[{'AttributeName': 'forum_name', 'KeyType': 'HASH'}], + AttributeDefinitions=[{'AttributeName': 'forum_name', 'AttributeType': 'S'}], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) + table = dynamodb.Table('users') + table.put_item(Item={'forum_name': 'key1', 'nested': {'level1': {'id': 'id1', 'att': 'irrelevant'}, + 'level2': {'id': 'id2', 'include': 'all'}, + 'level3': {'id': 'irrelevant'}}, + 'foo': 'bar'}) + table.put_item(Item={'forum_name': 'key2', 'nested': {'id': 'id2', 'incode': 'code2'}, 'foo': 'bar'}) + + # Test a get_item returning all items + result = table.get_item(Key={'forum_name': 'key1'}, + ProjectionExpression='#nst.level1.id, #nst.#lvl2', + ExpressionAttributeNames={'#nst': 'nested', '#lvl2': 'level2'})['Item'] + result.should.equal({'nested': {'level1': {'id': 'id1'}, + 'level2': {'id': 'id2', 'include': 'all'}}}) + # Assert actual data has not been deleted + result = table.get_item(Key={'forum_name': 'key1'})['Item'] + result.should.equal({u'foo': u'bar', + u'forum_name': u'key1', + u'nested': {u'level1': {u'id': u'id1', u'att': u'irrelevant'}, + u'level2': {u'id': u'id2', u'include': u'all'}, + u'level3': {u'id': u'irrelevant'}}}) + + +@mock_dynamodb2 +def test_nested_projection_expression_using_query_with_attr_expression_names(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB table. + dynamodb.create_table(TableName='users', + KeySchema=[{'AttributeName': 'forum_name', 'KeyType': 'HASH'}], + AttributeDefinitions=[{'AttributeName': 'forum_name', 'AttributeType': 'S'}], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) + table = dynamodb.Table('users') + table.put_item(Item={'forum_name': 'key1', 'nested': {'level1': {'id': 'id1', 'att': 'irrelevant'}, + 'level2': {'id': 'id2', 'include': 'all'}, + 'level3': {'id': 'irrelevant'}}, + 'foo': 'bar'}) + table.put_item(Item={'forum_name': 'key2', 'nested': {'id': 'id2', 'incode': 'code2'}, 'foo': 'bar'}) + + # Test a query returning all items + result = table.query(KeyConditionExpression=Key('forum_name').eq('key1'), + ProjectionExpression="#nst.level1.id, #nst.#lvl2", + ExpressionAttributeNames={'#nst': 'nested', '#lvl2': 'level2'})['Items'][0] + + assert 'nested' in result + result['nested'].should.equal({'level1': {'id': 'id1'}, + 'level2': {'id': 'id2', 'include': 'all'}}) + assert 'foo' not in result + # Assert actual data has not been deleted + result = table.query(KeyConditionExpression=Key('forum_name').eq('key1'))['Items'][0] + result.should.equal( {u'foo': u'bar', + u'forum_name': u'key1', + u'nested': {u'level1': {u'id': u'id1', u'att': u'irrelevant'}, + u'level2': {u'id': u'id2', u'include': u'all'}, + u'level3': {u'id': u'irrelevant'}}}) + + @mock_dynamodb2 def test_basic_projection_expressions_using_scan_with_attr_expression_names(): dynamodb = boto3.resource("dynamodb", region_name="us-east-1") @@ -719,6 +956,37 @@ def test_basic_projection_expressions_using_scan_with_attr_expression_names(): assert "form_name" not in results["Items"][0] +@mock_dynamodb2 +def test_nested_projection_expression_using_scan_with_attr_expression_names(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB table. + dynamodb.create_table(TableName='users', + KeySchema=[{'AttributeName': 'forum_name', 'KeyType': 'HASH'}], + AttributeDefinitions=[{'AttributeName': 'forum_name', 'AttributeType': 'S'}], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) + table = dynamodb.Table('users') + table.put_item(Item={'forum_name': 'key1', 'nested': {'level1': {'id': 'id1', 'att': 'irrelevant'}, + 'level2': {'id': 'id2', 'include': 'all'}, + 'level3': {'id': 'irrelevant'}}, + 'foo': 'bar'}) + table.put_item(Item={'forum_name': 'key2', 'nested': {'id': 'id2', 'incode': 'code2'}, 'foo': 'bar'}) + + # Test a scan + results = table.scan(FilterExpression=Key('forum_name').eq('key1'), + ProjectionExpression='nested.level1.id, nested.level2', + ExpressionAttributeNames={'#nst': 'nested', '#lvl2': 'level2'})['Items'] + results.should.equal([{'nested': {'level1': {'id': 'id1'}, + 'level2': {'include': 'all', 'id': 'id2'}}}]) + # Assert original data is still there + results = table.scan(FilterExpression=Key('forum_name').eq('key1'))['Items'] + results.should.equal([{'forum_name': 'key1', + 'foo': 'bar', + 'nested': {'level1': {'att': 'irrelevant', 'id': 'id1'}, + 'level2': {'include': 'all', 'id': 'id2'}, + 'level3': {'id': 'irrelevant'}}}]) + + @mock_dynamodb2 def test_put_item_returns_consumed_capacity(): dynamodb = boto3.resource("dynamodb", region_name="us-east-1") From 5876dc01864d2f19b73e4555ec88fa045b88e240 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Sun, 3 Nov 2019 07:14:19 -0800 Subject: [PATCH 2/3] Fix test - List order does not matter for projectionexpression --- tests/test_dynamodb2/test_dynamodb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index 2d47a83e..d6802f66 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -691,8 +691,8 @@ def test_basic_projection_expressions_using_scan(): results = table.scan(FilterExpression=Key('forum_name').eq('the-key'), ProjectionExpression='body')['Items'] - results.should.equal([{u'body': u'yet another test message'}, - {u'body': u'some test message'}]) + assert {u'body': u'some test message'} in results + assert {u'body': u'yet another test message'} in results # The projection expression should not remove data from storage results = table.query(KeyConditionExpression=Key('forum_name').eq('the-key')) From eaa23800bd0ac722b279ad1a39f7b49d7fbf1e37 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Sun, 3 Nov 2019 07:33:27 -0800 Subject: [PATCH 3/3] Linting --- moto/dynamodb2/models.py | 27 +- moto/dynamodb2/responses.py | 24 +- tests/test_dynamodb2/test_dynamodb.py | 581 ++++++++++++++++++-------- 3 files changed, 437 insertions(+), 195 deletions(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 0854f428..8a061041 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -108,14 +108,23 @@ class DynamoType(object): self.value.pop(key) def filter(self, projection_expressions): - nested_projections = [expr[0:expr.index('.')] for expr in projection_expressions if '.' in expr] + nested_projections = [ + expr[0 : expr.index(".")] for expr in projection_expressions if "." in expr + ] if self.is_map(): expressions_to_delete = [] for attr in self.value: - if attr not in projection_expressions and attr not in nested_projections: + if ( + attr not in projection_expressions + and attr not in nested_projections + ): expressions_to_delete.append(attr) elif attr in nested_projections: - relevant_expressions = [expr[len(attr + '.'):] for expr in projection_expressions if expr.startswith(attr + '.')] + relevant_expressions = [ + expr[len(attr + ".") :] + for expr in projection_expressions + if expr.startswith(attr + ".") + ] self.value[attr].filter(relevant_expressions) for expr in expressions_to_delete: self.value.pop(expr) @@ -493,13 +502,19 @@ class Item(BaseModel): # Filter using projection_expression # Ensure a deep copy is used to filter, otherwise actual data will be removed def filter(self, projection_expression): - expressions = [x.strip() for x in projection_expression.split(',')] - top_level_expressions = [expr[0:expr.index('.')] for expr in expressions if '.' in expr] + expressions = [x.strip() for x in projection_expression.split(",")] + top_level_expressions = [ + expr[0 : expr.index(".")] for expr in expressions if "." in expr + ] for attr in list(self.attrs): if attr not in expressions and attr not in top_level_expressions: self.attrs.pop(attr) if attr in top_level_expressions: - relevant_expressions = [expr[len(attr + '.'):] for expr in expressions if expr.startswith(attr + '.')] + relevant_expressions = [ + expr[len(attr + ".") :] + for expr in expressions + if expr.startswith(attr + ".") + ] self.attrs[attr].filter(relevant_expressions) diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 9c7bcd1f..0e39a1da 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -346,7 +346,9 @@ class DynamoHandler(BaseResponse): projection_expression = self.body.get("ProjectionExpression") expression_attribute_names = self.body.get("ExpressionAttributeNames", {}) - projection_expression = self._adjust_projection_expression(projection_expression, expression_attribute_names) + projection_expression = self._adjust_projection_expression( + projection_expression, expression_attribute_names + ) try: item = self.dynamodb_backend.get_item(name, key, projection_expression) @@ -413,7 +415,9 @@ class DynamoHandler(BaseResponse): filter_expression = self.body.get("FilterExpression") expression_attribute_values = self.body.get("ExpressionAttributeValues", {}) - projection_expression = self._adjust_projection_expression(projection_expression, expression_attribute_names) + projection_expression = self._adjust_projection_expression( + projection_expression, expression_attribute_names + ) filter_kwargs = {} @@ -569,10 +573,20 @@ class DynamoHandler(BaseResponse): def _adjust_projection_expression(self, projection_expression, expr_attr_names): def _adjust(expression): - return expr_attr_names[expression] if expression in expr_attr_names else expression + return ( + expr_attr_names[expression] + if expression in expr_attr_names + else expression + ) + if projection_expression and expr_attr_names: - expressions = [x.strip() for x in projection_expression.split(',')] - return ','.join(['.'.join([_adjust(expr) for expr in nested_expr.split('.')]) for nested_expr in expressions]) + expressions = [x.strip() for x in projection_expression.split(",")] + return ",".join( + [ + ".".join([_adjust(expr) for expr in nested_expr.split(".")]) + for nested_expr in expressions + ] + ) return projection_expression diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index d6802f66..d492b013 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -561,174 +561,304 @@ def test_basic_projection_expressions_using_scan(): @mock_dynamodb2 def test_nested_projection_expression_using_get_item(): - dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + dynamodb = boto3.resource("dynamodb", region_name="us-east-1") # Create the DynamoDB table. - dynamodb.create_table(TableName='users', - KeySchema=[{'AttributeName': 'forum_name', 'KeyType': 'HASH'}], - AttributeDefinitions=[{'AttributeName': 'forum_name', 'AttributeType': 'S'}], - ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) - table = dynamodb.Table('users') - table.put_item(Item={'forum_name': 'key1', 'nested': {'level1': {'id': 'id1', 'att': 'irrelevant'}, - 'level2': {'id': 'id2', 'include': 'all'}, - 'level3': {'id': 'irrelevant'}}, - 'foo': 'bar'}) - table.put_item(Item={'forum_name': 'key2', 'nested': {'id': 'id2', 'incode': 'code2'}, 'foo': 'bar'}) + dynamodb.create_table( + TableName="users", + KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + table = dynamodb.Table("users") + table.put_item( + Item={ + "forum_name": "key1", + "nested": { + "level1": {"id": "id1", "att": "irrelevant"}, + "level2": {"id": "id2", "include": "all"}, + "level3": {"id": "irrelevant"}, + }, + "foo": "bar", + } + ) + table.put_item( + Item={ + "forum_name": "key2", + "nested": {"id": "id2", "incode": "code2"}, + "foo": "bar", + } + ) # Test a get_item returning all items - result = table.get_item(Key={'forum_name': 'key1'}, - ProjectionExpression='nested.level1.id, nested.level2')['Item'] - result.should.equal({'nested': {'level1': {'id': 'id1'}, - 'level2': {'id': 'id2', 'include': 'all'}}}) + result = table.get_item( + Key={"forum_name": "key1"}, + ProjectionExpression="nested.level1.id, nested.level2", + )["Item"] + result.should.equal( + {"nested": {"level1": {"id": "id1"}, "level2": {"id": "id2", "include": "all"}}} + ) # Assert actual data has not been deleted - result = table.get_item(Key={'forum_name': 'key1'})['Item'] - result.should.equal({u'foo': u'bar', - u'forum_name': u'key1', - u'nested': {u'level1': {u'id': u'id1', u'att': u'irrelevant'}, - u'level2': {u'id': u'id2', u'include': u'all'}, - u'level3': {u'id': u'irrelevant'}}}) + result = table.get_item(Key={"forum_name": "key1"})["Item"] + result.should.equal( + { + "foo": "bar", + "forum_name": "key1", + "nested": { + "level1": {"id": "id1", "att": "irrelevant"}, + "level2": {"id": "id2", "include": "all"}, + "level3": {"id": "irrelevant"}, + }, + } + ) @mock_dynamodb2 def test_basic_projection_expressions_using_query(): - dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + dynamodb = boto3.resource("dynamodb", region_name="us-east-1") # Create the DynamoDB 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'}], - ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) - table = dynamodb.Table('users') - table.put_item(Item={'forum_name': 'the-key', 'subject': '123', 'body': 'some test message'}) - table.put_item(Item={'forum_name': 'not-the-key', 'subject': '123', 'body': 'some other test message'}) + 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"}, + ], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + table = dynamodb.Table("users") + table.put_item( + Item={"forum_name": "the-key", "subject": "123", "body": "some test message"} + ) + table.put_item( + Item={ + "forum_name": "not-the-key", + "subject": "123", + "body": "some other test message", + } + ) # Test a query returning all items - result = table.query(KeyConditionExpression=Key('forum_name').eq('the-key'), - ProjectionExpression='body, subject')['Items'][0] + result = table.query( + KeyConditionExpression=Key("forum_name").eq("the-key"), + ProjectionExpression="body, subject", + )["Items"][0] - assert 'body' in result - assert result['body'] == 'some test message' - assert 'subject' in result - assert 'forum_name' not in result + assert "body" in result + assert result["body"] == "some test message" + assert "subject" in result + assert "forum_name" not in result - table.put_item(Item={'forum_name': 'the-key', 'subject': '1234', 'body': 'yet another test message'}) + table.put_item( + Item={ + "forum_name": "the-key", + "subject": "1234", + "body": "yet another test message", + } + ) - items = table.query(KeyConditionExpression=Key('forum_name').eq('the-key'), - ProjectionExpression='body')['Items'] + items = table.query( + KeyConditionExpression=Key("forum_name").eq("the-key"), + ProjectionExpression="body", + )["Items"] - assert 'body' in items[0] - assert 'subject' not in items[0] - assert items[0]['body'] == 'some test message' - assert 'body' in items[1] - assert 'subject' not in items[1] - assert items[1]['body'] == 'yet another test message' + assert "body" in items[0] + assert "subject" not in items[0] + assert items[0]["body"] == "some test message" + assert "body" in items[1] + assert "subject" not in items[1] + assert items[1]["body"] == "yet another test message" # The projection expression should not remove data from storage - items = table.query(KeyConditionExpression=Key('forum_name').eq('the-key'))['Items'] - assert 'subject' in items[0] - assert 'body' in items[1] - assert 'forum_name' in items[1] + items = table.query(KeyConditionExpression=Key("forum_name").eq("the-key"))["Items"] + assert "subject" in items[0] + assert "body" in items[1] + assert "forum_name" in items[1] @mock_dynamodb2 def test_nested_projection_expression_using_query(): - dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + dynamodb = boto3.resource("dynamodb", region_name="us-east-1") # Create the DynamoDB table. - dynamodb.create_table(TableName='users', - KeySchema=[{'AttributeName': 'forum_name', 'KeyType': 'HASH'}], - AttributeDefinitions=[{'AttributeName': 'forum_name', 'AttributeType': 'S'}], - ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) - table = dynamodb.Table('users') - table.put_item(Item={'forum_name': 'key1', 'nested': {'level1': {'id': 'id1', 'att': 'irrelevant'}, - 'level2': {'id': 'id2', 'include': 'all'}, - 'level3': {'id': 'irrelevant'}}, - 'foo': 'bar'}) - table.put_item(Item={'forum_name': 'key2', 'nested': {'id': 'id2', 'incode': 'code2'}, 'foo': 'bar'}) + dynamodb.create_table( + TableName="users", + KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + table = dynamodb.Table("users") + table.put_item( + Item={ + "forum_name": "key1", + "nested": { + "level1": {"id": "id1", "att": "irrelevant"}, + "level2": {"id": "id2", "include": "all"}, + "level3": {"id": "irrelevant"}, + }, + "foo": "bar", + } + ) + table.put_item( + Item={ + "forum_name": "key2", + "nested": {"id": "id2", "incode": "code2"}, + "foo": "bar", + } + ) # Test a query returning all items - result = table.query(KeyConditionExpression=Key('forum_name').eq('key1'), - ProjectionExpression="nested.level1.id, nested.level2")['Items'][0] + result = table.query( + KeyConditionExpression=Key("forum_name").eq("key1"), + ProjectionExpression="nested.level1.id, nested.level2", + )["Items"][0] - assert 'nested' in result - result['nested'].should.equal({'level1': {'id': 'id1'}, - 'level2': {'id': 'id2', 'include': 'all'}}) - assert 'foo' not in result + assert "nested" in result + result["nested"].should.equal( + {"level1": {"id": "id1"}, "level2": {"id": "id2", "include": "all"}} + ) + assert "foo" not in result # Assert actual data has not been deleted - result = table.query(KeyConditionExpression=Key('forum_name').eq('key1'))['Items'][0] - result.should.equal({u'foo': u'bar', - u'forum_name': u'key1', - u'nested': {u'level1': {u'id': u'id1', u'att': u'irrelevant'}, - u'level2': {u'id': u'id2', u'include': u'all'}, - u'level3': {u'id': u'irrelevant'}}}) + result = table.query(KeyConditionExpression=Key("forum_name").eq("key1"))["Items"][ + 0 + ] + result.should.equal( + { + "foo": "bar", + "forum_name": "key1", + "nested": { + "level1": {"id": "id1", "att": "irrelevant"}, + "level2": {"id": "id2", "include": "all"}, + "level3": {"id": "irrelevant"}, + }, + } + ) @mock_dynamodb2 def test_basic_projection_expressions_using_scan(): - dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + dynamodb = boto3.resource("dynamodb", region_name="us-east-1") # Create the DynamoDB 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'}], - ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) - table = dynamodb.Table('users') + 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"}, + ], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + table = dynamodb.Table("users") - table.put_item(Item={'forum_name': 'the-key', 'subject': '123', 'body': 'some test message'}) - table.put_item(Item={'forum_name': 'not-the-key', 'subject': '123', 'body': 'some other test message'}) + table.put_item( + Item={"forum_name": "the-key", "subject": "123", "body": "some test message"} + ) + table.put_item( + Item={ + "forum_name": "not-the-key", + "subject": "123", + "body": "some other test message", + } + ) # Test a scan returning all items - results = table.scan(FilterExpression=Key('forum_name').eq('the-key'), - ProjectionExpression='body, subject')['Items'] + results = table.scan( + FilterExpression=Key("forum_name").eq("the-key"), + ProjectionExpression="body, subject", + )["Items"] - results.should.equal([{u'body': u'some test message', u'subject': u'123'}]) + results.should.equal([{"body": "some test message", "subject": "123"}]) - table.put_item(Item={'forum_name': 'the-key', 'subject': '1234', 'body': 'yet another test message'}) + table.put_item( + Item={ + "forum_name": "the-key", + "subject": "1234", + "body": "yet another test message", + } + ) - results = table.scan(FilterExpression=Key('forum_name').eq('the-key'), - ProjectionExpression='body')['Items'] + results = table.scan( + FilterExpression=Key("forum_name").eq("the-key"), ProjectionExpression="body" + )["Items"] - assert {u'body': u'some test message'} in results - assert {u'body': u'yet another test message'} in results + assert {"body": "some test message"} in results + assert {"body": "yet another test message"} in results # The projection expression should not remove data from storage - results = table.query(KeyConditionExpression=Key('forum_name').eq('the-key')) - assert 'subject' in results['Items'][0] - assert 'body' in results['Items'][1] - assert 'forum_name' in results['Items'][1] + results = table.query(KeyConditionExpression=Key("forum_name").eq("the-key")) + assert "subject" in results["Items"][0] + assert "body" in results["Items"][1] + assert "forum_name" in results["Items"][1] @mock_dynamodb2 def test_nested_projection_expression_using_scan(): - dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + dynamodb = boto3.resource("dynamodb", region_name="us-east-1") # Create the DynamoDB table. - dynamodb.create_table(TableName='users', - KeySchema=[{'AttributeName': 'forum_name', 'KeyType': 'HASH'}], - AttributeDefinitions=[{'AttributeName': 'forum_name', 'AttributeType': 'S'}], - ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) - table = dynamodb.Table('users') - table.put_item(Item={'forum_name': 'key1', 'nested': {'level1': {'id': 'id1', 'att': 'irrelevant'}, - 'level2': {'id': 'id2', 'include': 'all'}, - 'level3': {'id': 'irrelevant'}}, - 'foo': 'bar'}) - table.put_item(Item={'forum_name': 'key2', 'nested': {'id': 'id2', 'incode': 'code2'}, 'foo': 'bar'}) + dynamodb.create_table( + TableName="users", + KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + table = dynamodb.Table("users") + table.put_item( + Item={ + "forum_name": "key1", + "nested": { + "level1": {"id": "id1", "att": "irrelevant"}, + "level2": {"id": "id2", "include": "all"}, + "level3": {"id": "irrelevant"}, + }, + "foo": "bar", + } + ) + table.put_item( + Item={ + "forum_name": "key2", + "nested": {"id": "id2", "incode": "code2"}, + "foo": "bar", + } + ) # Test a scan - results = table.scan(FilterExpression=Key('forum_name').eq('key1'), - ProjectionExpression='nested.level1.id, nested.level2')['Items'] - results.should.equal([{'nested': {'level1': {'id': 'id1'}, - 'level2': {'include': 'all', 'id': 'id2'}}}]) + results = table.scan( + FilterExpression=Key("forum_name").eq("key1"), + ProjectionExpression="nested.level1.id, nested.level2", + )["Items"] + results.should.equal( + [ + { + "nested": { + "level1": {"id": "id1"}, + "level2": {"include": "all", "id": "id2"}, + } + } + ] + ) # Assert original data is still there - results = table.scan(FilterExpression=Key('forum_name').eq('key1'))['Items'] - results.should.equal([{'forum_name': 'key1', - 'foo': 'bar', - 'nested': {'level1': {'att': 'irrelevant', 'id': 'id1'}, - 'level2': {'include': 'all', 'id': 'id2'}, - 'level3': {'id': 'irrelevant'}}}]) + results = table.scan(FilterExpression=Key("forum_name").eq("key1"))["Items"] + results.should.equal( + [ + { + "forum_name": "key1", + "foo": "bar", + "nested": { + "level1": {"att": "irrelevant", "id": "id1"}, + "level2": {"include": "all", "id": "id2"}, + "level3": {"id": "irrelevant"}, + }, + } + ] + ) @mock_dynamodb2 @@ -832,67 +962,117 @@ def test_basic_projection_expressions_using_query_with_attr_expression_names(): @mock_dynamodb2 def test_nested_projection_expression_using_get_item_with_attr_expression(): - dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + dynamodb = boto3.resource("dynamodb", region_name="us-east-1") # Create the DynamoDB table. - dynamodb.create_table(TableName='users', - KeySchema=[{'AttributeName': 'forum_name', 'KeyType': 'HASH'}], - AttributeDefinitions=[{'AttributeName': 'forum_name', 'AttributeType': 'S'}], - ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) - table = dynamodb.Table('users') - table.put_item(Item={'forum_name': 'key1', 'nested': {'level1': {'id': 'id1', 'att': 'irrelevant'}, - 'level2': {'id': 'id2', 'include': 'all'}, - 'level3': {'id': 'irrelevant'}}, - 'foo': 'bar'}) - table.put_item(Item={'forum_name': 'key2', 'nested': {'id': 'id2', 'incode': 'code2'}, 'foo': 'bar'}) + dynamodb.create_table( + TableName="users", + KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + table = dynamodb.Table("users") + table.put_item( + Item={ + "forum_name": "key1", + "nested": { + "level1": {"id": "id1", "att": "irrelevant"}, + "level2": {"id": "id2", "include": "all"}, + "level3": {"id": "irrelevant"}, + }, + "foo": "bar", + } + ) + table.put_item( + Item={ + "forum_name": "key2", + "nested": {"id": "id2", "incode": "code2"}, + "foo": "bar", + } + ) # Test a get_item returning all items - result = table.get_item(Key={'forum_name': 'key1'}, - ProjectionExpression='#nst.level1.id, #nst.#lvl2', - ExpressionAttributeNames={'#nst': 'nested', '#lvl2': 'level2'})['Item'] - result.should.equal({'nested': {'level1': {'id': 'id1'}, - 'level2': {'id': 'id2', 'include': 'all'}}}) + result = table.get_item( + Key={"forum_name": "key1"}, + ProjectionExpression="#nst.level1.id, #nst.#lvl2", + ExpressionAttributeNames={"#nst": "nested", "#lvl2": "level2"}, + )["Item"] + result.should.equal( + {"nested": {"level1": {"id": "id1"}, "level2": {"id": "id2", "include": "all"}}} + ) # Assert actual data has not been deleted - result = table.get_item(Key={'forum_name': 'key1'})['Item'] - result.should.equal({u'foo': u'bar', - u'forum_name': u'key1', - u'nested': {u'level1': {u'id': u'id1', u'att': u'irrelevant'}, - u'level2': {u'id': u'id2', u'include': u'all'}, - u'level3': {u'id': u'irrelevant'}}}) + result = table.get_item(Key={"forum_name": "key1"})["Item"] + result.should.equal( + { + "foo": "bar", + "forum_name": "key1", + "nested": { + "level1": {"id": "id1", "att": "irrelevant"}, + "level2": {"id": "id2", "include": "all"}, + "level3": {"id": "irrelevant"}, + }, + } + ) @mock_dynamodb2 def test_nested_projection_expression_using_query_with_attr_expression_names(): - dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + dynamodb = boto3.resource("dynamodb", region_name="us-east-1") # Create the DynamoDB table. - dynamodb.create_table(TableName='users', - KeySchema=[{'AttributeName': 'forum_name', 'KeyType': 'HASH'}], - AttributeDefinitions=[{'AttributeName': 'forum_name', 'AttributeType': 'S'}], - ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) - table = dynamodb.Table('users') - table.put_item(Item={'forum_name': 'key1', 'nested': {'level1': {'id': 'id1', 'att': 'irrelevant'}, - 'level2': {'id': 'id2', 'include': 'all'}, - 'level3': {'id': 'irrelevant'}}, - 'foo': 'bar'}) - table.put_item(Item={'forum_name': 'key2', 'nested': {'id': 'id2', 'incode': 'code2'}, 'foo': 'bar'}) + dynamodb.create_table( + TableName="users", + KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + table = dynamodb.Table("users") + table.put_item( + Item={ + "forum_name": "key1", + "nested": { + "level1": {"id": "id1", "att": "irrelevant"}, + "level2": {"id": "id2", "include": "all"}, + "level3": {"id": "irrelevant"}, + }, + "foo": "bar", + } + ) + table.put_item( + Item={ + "forum_name": "key2", + "nested": {"id": "id2", "incode": "code2"}, + "foo": "bar", + } + ) # Test a query returning all items - result = table.query(KeyConditionExpression=Key('forum_name').eq('key1'), - ProjectionExpression="#nst.level1.id, #nst.#lvl2", - ExpressionAttributeNames={'#nst': 'nested', '#lvl2': 'level2'})['Items'][0] + result = table.query( + KeyConditionExpression=Key("forum_name").eq("key1"), + ProjectionExpression="#nst.level1.id, #nst.#lvl2", + ExpressionAttributeNames={"#nst": "nested", "#lvl2": "level2"}, + )["Items"][0] - assert 'nested' in result - result['nested'].should.equal({'level1': {'id': 'id1'}, - 'level2': {'id': 'id2', 'include': 'all'}}) - assert 'foo' not in result + assert "nested" in result + result["nested"].should.equal( + {"level1": {"id": "id1"}, "level2": {"id": "id2", "include": "all"}} + ) + assert "foo" not in result # Assert actual data has not been deleted - result = table.query(KeyConditionExpression=Key('forum_name').eq('key1'))['Items'][0] - result.should.equal( {u'foo': u'bar', - u'forum_name': u'key1', - u'nested': {u'level1': {u'id': u'id1', u'att': u'irrelevant'}, - u'level2': {u'id': u'id2', u'include': u'all'}, - u'level3': {u'id': u'irrelevant'}}}) + result = table.query(KeyConditionExpression=Key("forum_name").eq("key1"))["Items"][ + 0 + ] + result.should.equal( + { + "foo": "bar", + "forum_name": "key1", + "nested": { + "level1": {"id": "id1", "att": "irrelevant"}, + "level2": {"id": "id2", "include": "all"}, + "level3": {"id": "irrelevant"}, + }, + } + ) @mock_dynamodb2 @@ -958,33 +1138,66 @@ def test_basic_projection_expressions_using_scan_with_attr_expression_names(): @mock_dynamodb2 def test_nested_projection_expression_using_scan_with_attr_expression_names(): - dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + dynamodb = boto3.resource("dynamodb", region_name="us-east-1") # Create the DynamoDB table. - dynamodb.create_table(TableName='users', - KeySchema=[{'AttributeName': 'forum_name', 'KeyType': 'HASH'}], - AttributeDefinitions=[{'AttributeName': 'forum_name', 'AttributeType': 'S'}], - ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5}) - table = dynamodb.Table('users') - table.put_item(Item={'forum_name': 'key1', 'nested': {'level1': {'id': 'id1', 'att': 'irrelevant'}, - 'level2': {'id': 'id2', 'include': 'all'}, - 'level3': {'id': 'irrelevant'}}, - 'foo': 'bar'}) - table.put_item(Item={'forum_name': 'key2', 'nested': {'id': 'id2', 'incode': 'code2'}, 'foo': 'bar'}) + dynamodb.create_table( + TableName="users", + KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + table = dynamodb.Table("users") + table.put_item( + Item={ + "forum_name": "key1", + "nested": { + "level1": {"id": "id1", "att": "irrelevant"}, + "level2": {"id": "id2", "include": "all"}, + "level3": {"id": "irrelevant"}, + }, + "foo": "bar", + } + ) + table.put_item( + Item={ + "forum_name": "key2", + "nested": {"id": "id2", "incode": "code2"}, + "foo": "bar", + } + ) # Test a scan - results = table.scan(FilterExpression=Key('forum_name').eq('key1'), - ProjectionExpression='nested.level1.id, nested.level2', - ExpressionAttributeNames={'#nst': 'nested', '#lvl2': 'level2'})['Items'] - results.should.equal([{'nested': {'level1': {'id': 'id1'}, - 'level2': {'include': 'all', 'id': 'id2'}}}]) + results = table.scan( + FilterExpression=Key("forum_name").eq("key1"), + ProjectionExpression="nested.level1.id, nested.level2", + ExpressionAttributeNames={"#nst": "nested", "#lvl2": "level2"}, + )["Items"] + results.should.equal( + [ + { + "nested": { + "level1": {"id": "id1"}, + "level2": {"include": "all", "id": "id2"}, + } + } + ] + ) # Assert original data is still there - results = table.scan(FilterExpression=Key('forum_name').eq('key1'))['Items'] - results.should.equal([{'forum_name': 'key1', - 'foo': 'bar', - 'nested': {'level1': {'att': 'irrelevant', 'id': 'id1'}, - 'level2': {'include': 'all', 'id': 'id2'}, - 'level3': {'id': 'irrelevant'}}}]) + results = table.scan(FilterExpression=Key("forum_name").eq("key1"))["Items"] + results.should.equal( + [ + { + "forum_name": "key1", + "foo": "bar", + "nested": { + "level1": {"att": "irrelevant", "id": "id1"}, + "level2": {"include": "all", "id": "id2"}, + "level3": {"id": "irrelevant"}, + }, + } + ] + ) @mock_dynamodb2