Repository: getmoto/moto

Issue:
mock_dynamodb2 ProjectionExpression returns empty Items when Querying for Nested fields

### Expected
Moto would produce a item with matched fields from the projection expression when using a the nested / dynamodb dereferencing syntax. 

### What Happens 
When running a query that includes a ProjectionExpression with nested field an Item dict is returned, but this Item is empty and does not contain the valid keys

This is very similar to #1711 except this is for ProjectionExpressions

### moto version
```json
        "moto": {
            "hashes": [
                "sha256:79d0ad6db3793a308b3b36973a5e9ee4cded80184fb4ddecb5ec0c601d3ac555",
                "sha256:bd5f2d2fcd569e52f8e05cff7a85d24880192e996968b720296ba4a9503dcbf3"
            ],
            "index": "pypi",
            "version": "==1.3.14.dev466"
        },
```

### Reproduction 
(requires docker, boto3 and moto)

```python
#!/usr/bin/env python

from contextlib import contextmanager
from pprint import pprint

import boto3
import docker
import moto

from boto3.dynamodb.conditions import Key

table_name = "test"
table_options = dict(
    TableName=table_name,
    AttributeDefinitions=[
        {"AttributeName": "pk", "AttributeType": "S"},
        {"AttributeName": "sk-1", "AttributeType": "S"},
        {"AttributeName": "sk-2", "AttributeType": "S"},
        {"AttributeName": "sk-3", "AttributeType": "S"},
        {"AttributeName": "sk-4", "AttributeType": "S"},
    ],
    KeySchema=[{"AttributeName": "pk", "KeyType": "HASH"}],
    GlobalSecondaryIndexes=[
        {
            "IndexName": "GSI-1",
            "KeySchema": [{"AttributeName": "sk-4", "KeyType": "HASH"}, {"AttributeName": "pk", "KeyType": "RANGE"}],
            "Projection": {"ProjectionType": "INCLUDE", "NonKeyAttributes": ["data"]},
        },
        {
            "IndexName": "GSI-2",
            "KeySchema": [{"AttributeName": "sk-1", "KeyType": "HASH"}, {"AttributeName": "sk-3", "KeyType": "RANGE"}],
            "Projection": {"ProjectionType": "INCLUDE", "NonKeyAttributes": ["data"]},
        },
        {
            "IndexName": "GSI-3",
            "KeySchema": [{"AttributeName": "sk-2", "KeyType": "HASH"}, {"AttributeName": "sk-3", "KeyType": "RANGE"}],
            "Projection": {"ProjectionType": "INCLUDE", "NonKeyAttributes": ["data"]},
        },
    ],
    BillingMode="PAY_PER_REQUEST",
)

table_data = [ {
        "pk": {
            "S": "HINT#d72f3d4f-0fc3-4e64-86d9-27a91125a549"
        },
        "sk-1": {
            "S": "85d11709-7b87-4eef-8c80-6a670810dfe0#555e69a3-b533-40de-a004-f3c7f57dd3df"
        },
        "sk-2": {
            "S": "555e69a3-b533-40de-a004-f3c7f57dd3df"
        },
        "sk-3": {
            "S": "HINT#10:07:13:17"
        },
        "sk-4": {
            "S": "85d11709-7b87-4eef-8c80-6a670810dfe0#HINT"
        },
        "data": {
            "M": {
                "id": {
                    "S": "d72f3d4f-0fc3-4e64-86d9-27a91125a549"
                },
                "incode": {
                    "S": "10:07:13:17"
                },
                "outcode": {
                    "S": "10:13:5101"
                }
            }
        }
    }
]

query = {
    "TableName": "test",
    "IndexName": "GSI-2",
    "Limit": 25,
    "KeyConditionExpression": Key("sk-1").eq(
        "85d11709-7b87-4eef-8c80-6a670810dfe0#555e69a3-b533-40de-a004-f3c7f57dd3df"
    )
    & Key("sk-3").gte("-1"),
    "ProjectionExpression": "#data.#incode",
    "ExpressionAttributeNames": {"#data": "data", "#incode": "incode"},
}

@contextmanager
def dynamodb_local():
    client = docker.from_env()
    containers = client.containers
    image = "amazon/dynamodb-local"
    try:
        container = containers.run(
            image,
            auto_remove=True,
            detach=True,
            ports={"8000/tcp": "8000"}
        )
        yield container
    finally:
        container.stop()


with dynamodb_local() as ddb_local:
    print("DynamoDB Local")
    client = boto3.client("dynamodb", endpoint_url='http://localhost:8000')
    table = client.create_table(**table_options).get("TableDescription")
    resource = boto3.resource("dynamodb", endpoint_url="http://localhost:8000").Table(table_name)
    for item in table_data:
        client.put_item(TableName=table_name, Item=item)
    pprint(resource.query(**query))

with moto.mock_dynamodb2() as ddb_moto:
    print("Moto")
    client = boto3.client("dynamodb")
    table = client.create_table(**table_options).get("TableDescription")
    resource = boto3.resource("dynamodb").Table(table_name)
    for item in table_data:
        client.put_item(TableName=table_name, Item=item)
    pprint(resource.query(**query))
```

### Comparative Output

The output from dynamodb local includes the incode, moto returns an empty item
```
DynamoDB Local
{'Count': 1,
 'Items': [{'data': {'incode': '10:07:13:17'}}],
 'ResponseMetadata': {'HTTPHeaders': {'content-length': '84',
                                      'content-type': 'application/x-amz-json-1.0',
                                      'server': 'Jetty(8.1.12.v20130726)',
                                      'x-amz-crc32': '1714179601',
                                      'x-amzn-requestid': '0d1555c7-31c1-44eb-b971-4b99d3a869ae'},
                      'HTTPStatusCode': 200,
                      'RequestId': '0d1555c7-31c1-44eb-b971-4b99d3a869ae',
                      'RetryAttempts': 0},
 'ScannedCount': 1}
Moto
{'ConsumedCapacity': {'CapacityUnits': 1, 'TableName': 'test'},
 'Count': 1,
 'Items': [{}],
 'ResponseMetadata': {'HTTPHeaders': {'server': 'amazon.com',
                                      'x-amzn-requestid': 'GPG7FSZ41VQ71ROXUGUTVVTC50LDFIAVIYQWG7LWA3RMIG1UHCOX'},
                      'HTTPStatusCode': 200,
                      'RequestId': 'GPG7FSZ41VQ71ROXUGUTVVTC50LDFIAVIYQWG7LWA3RMIG1UHCOX',
                      'RetryAttempts': 0},
 'ScannedCount': 1}
```


Discussion:

Resolving patch:
diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py
--- a/moto/dynamodb2/models.py
+++ b/moto/dynamodb2/models.py
@@ -107,6 +107,28 @@ def delete(self, key, index=None):
             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 +499,24 @@ def update_with_attribute_updates(self, attribute_updates):
                     "%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 +814,8 @@ def get_item(self, hash_key, range_key=None, projection_expression=None):
                 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 +948,10 @@ def query(
         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 +1038,9 @@ def scan(
                 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
--- a/moto/dynamodb2/responses.py
+++ b/moto/dynamodb2/responses.py
@@ -571,25 +571,22 @@ def query(self):
 
         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 = ""
+    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 expression in expression_attribute_names:
-                    projection_expr = (
-                        projection_expr + expression_attribute_names[expression]
-                    )
-                else:
-                    projection_expr = projection_expr + expression
-            return projection_expr
+        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
 


Test patch:
diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py
--- a/tests/test_dynamodb2/test_dynamodb.py
+++ b/tests/test_dynamodb2/test_dynamodb.py
@@ -559,6 +559,308 @@ 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(
+        {
+            "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")
+
+    # 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(
+        {
+            "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")
+
+    # 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([{"body": "some test message", "subject": "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"]
+
+    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]
+
+
+@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 +960,121 @@ 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(
+        {
+            "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")
+
+    # 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(
+        {
+            "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_with_attr_expression_names():
     dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
@@ -719,6 +1136,70 @@ 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")