Repository: getmoto/moto

Issue:
dynamodb2 UpdateExpression "REMOVE" not working for nested attributes.
table.update_item(...
            UpdateExpression='REMOVE attra.#attrb',
            ExpressionAttributeNames={
                "#attrb": 'value'
            }
        )

Expected:  attra.attrb removed from my item
Result: attra.attrb not removed and no error.

CODE: dynamodb2/models.py line 147

                if action == "REMOVE":
                    self.attrs.pop(value, None) -- value = "atta.attr.b"



Discussion:
Thanks; setting as a feature request.
This was driving me crazy trying to figure out why it wasn't working in my test

Resolving patch:
diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py
--- a/moto/dynamodb2/models.py
+++ b/moto/dynamodb2/models.py
@@ -182,7 +182,33 @@ def update(self, update_expression, expression_attribute_names, expression_attri
                     value = re.sub(r'{0}\b'.format(k), v, value)
 
                 if action == "REMOVE":
-                    self.attrs.pop(value, None)
+                    key = value
+                    if '.' not in key:
+                        self.attrs.pop(value, None)
+                    else:
+                        # Handle nested dict updates
+                        key_parts = key.split('.')
+                        attr = key_parts.pop(0)
+                        if attr not in self.attrs:
+                            raise ValueError
+
+                        last_val = self.attrs[attr].value
+                        for key_part in key_parts[:-1]:
+                            # Hack but it'll do, traverses into a dict
+                            last_val_type = list(last_val.keys())
+                            if last_val_type and last_val_type[0] == 'M':
+                                last_val = last_val['M']
+
+                            if key_part not in last_val:
+                                last_val[key_part] = {'M': {}}
+
+                            last_val = last_val[key_part]
+
+                        last_val_type = list(last_val.keys())
+                        if last_val_type and last_val_type[0] == 'M':
+                            last_val['M'].pop(key_parts[-1], None)
+                        else:
+                            last_val.pop(key_parts[-1], None)
                 elif action == 'SET':
                     key, value = value.split("=", 1)
                     key = key.strip()


Test patch:
diff --git a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py
--- a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py
+++ b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py
@@ -450,6 +450,70 @@ def test_update_item_remove():
     })
 
 
+@mock_dynamodb2_deprecated
+def test_update_item_nested_remove():
+    conn = boto.dynamodb2.connect_to_region("us-east-1")
+    table = Table.create('messages', schema=[
+        HashKey('username')
+    ])
+
+    data = {
+        'username': "steve",
+        'Meta': {
+            'FullName': 'Steve Urkel'    
+        }
+    }
+    table.put_item(data=data)
+    key_map = {
+        'username': {"S": "steve"}
+    }
+
+    # Then remove the Meta.FullName field
+    conn.update_item("messages", key_map,
+                     update_expression="REMOVE Meta.FullName")
+
+    returned_item = table.get_item(username="steve")
+    dict(returned_item).should.equal({
+        'username': "steve",
+        'Meta': {}
+    })
+
+
+@mock_dynamodb2_deprecated
+def test_update_item_double_nested_remove():
+    conn = boto.dynamodb2.connect_to_region("us-east-1")
+    table = Table.create('messages', schema=[
+        HashKey('username')
+    ])
+
+    data = {
+        'username': "steve",
+        'Meta': {
+            'Name': {
+                'First': 'Steve',
+                'Last': 'Urkel'
+            }
+        }
+    }
+    table.put_item(data=data)
+    key_map = {
+        'username': {"S": "steve"}
+    }
+
+    # Then remove the Meta.FullName field
+    conn.update_item("messages", key_map,
+                     update_expression="REMOVE Meta.Name.First")
+
+    returned_item = table.get_item(username="steve")
+    dict(returned_item).should.equal({
+        'username': "steve",
+        'Meta': {
+            'Name': {
+                'Last': 'Urkel'
+            }
+        }
+    })
+
 @mock_dynamodb2_deprecated
 def test_update_item_set():
     conn = boto.dynamodb2.connect_to_region("us-east-1")