Repository: getmoto/moto

Issue:
Deleting element from string set with mock_dynamodb2 doesn't match AWS
Versions: python==3.8.5, moto[iam]==1.3.15

When deleting the final element from a string set attribute in DynamoDB on AWS via `update_item`, the attribute is dropped. This behaviour is not specified in the AWS docs, but is observed in production.

In `moto` however, the string set attribute remains present but without any entries. Therefore `moto` is not matching production AWS behaviour. It is certainly a question, given this is undocumented behaviour, whether `moto` should change to match.

Consider the following code:
```python
import boto3
import moto
import os
import pytest

TABLE_NAME = 'table'

@pytest.fixture(scope='function')
def aws_credentials():
	"""Mocked AWS Credentials for moto."""
	os.environ['AWS_ACCESS_KEY_ID'] = 'testing'
	os.environ['AWS_SECRET_ACCESS_KEY'] = 'testing'
	os.environ['AWS_SECURITY_TOKEN'] = 'testing'
	os.environ['AWS_SESSION_TOKEN'] = 'testing'

@pytest.fixture(scope='function')
def dynamodb(aws_credentials):
	with moto.mock_dynamodb2():
		client = boto3.client('dynamodb', region_name='eu-west-1')
		client.create_table(
			TableName=TABLE_NAME,
			KeySchema=[
				{
					'AttributeName': 'customer',
					'KeyType': 'HASH',
				},
			],
			AttributeDefinitions=[
				{
					'AttributeName': 'customer',
					'AttributeType': 'S',
				},
			],
		)
		yield client

def test_conidtional_delete(dynamodb):
	customer_name = "Joe Bloggs"
	order = "An Order"
	dynamodb.update_item(
		TableName=TABLE_NAME,
		Key={'customer': {"S": customer_name}},
		UpdateExpression=f"ADD orders :order",
		ExpressionAttributeValues={
			':order': {"SS": [order]},
		},
	)

	response = dynamodb.scan(
		TableName=TABLE_NAME,
		ProjectionExpression='customer, orders',
	)

	print(f"Response 1: {response}")

	dynamodb.update_item(
		TableName=TABLE_NAME,
		Key={'customer': {"S": customer_name}},
		UpdateExpression=f"DELETE orders :order",
		ExpressionAttributeValues={
			':order': {"SS": [order]},
		},
	)

	response = dynamodb.scan(
		TableName=TABLE_NAME,
		ProjectionExpression='customer, orders',
	)

	print(f"Response 2: {response}")

```

Which outputs the following:
```
Response 1: {'Items': [{'customer': {'S': 'Joe Bloggs'}, 'orders': {'SS': ['An Order']}}], 'Count': 1, 'ScannedCount': 1, ...}
Response 2: {'Items': [{'customer': {'S': 'Joe Bloggs'}, 'orders': {'SS': []}}], 'Count': 1, 'ScannedCount': 1, ...}
```

When executed against production AWS however, the responses are:
```
Response 1: {'Items': [{'customer': {'S': 'Joe Bloggs'}, 'orders': {'SS': ['An Order']}}], 'Count': 1, 'ScannedCount': 1, ...}
Response 2: {'Items': [{'customer': {'S': 'Joe Bloggs'}}], 'Count': 1, 'ScannedCount': 1, ...}
```


Discussion:
Thanks for raising this @jamesgunnfiveai - will mark this as an enhancement.

Resolving patch:
diff --git a/moto/dynamodb2/parsing/executors.py b/moto/dynamodb2/parsing/executors.py
--- a/moto/dynamodb2/parsing/executors.py
+++ b/moto/dynamodb2/parsing/executors.py
@@ -161,6 +161,13 @@ def execute(self, item):
                 # DynamoDB does not mind if value is not present
                 pass
 
+        # DynamoDB does not support empty sets.  If we've deleted
+        # the last item in the set, we have to remove the attribute.
+        if not string_set_list:
+            element = self.get_element_to_action()
+            container = self.get_item_before_end_of_path(item)
+            container.pop(element.get_attribute_name())
+
 
 class RemoveExecutor(NodeExecutor):
     def execute(self, item):


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
@@ -5445,3 +5445,37 @@ def test_lsi_projection_type_keys_only():
     items[0].should.equal(
         {"partitionKey": "pk-1", "sortKey": "sk-1", "lsiK1SortKey": "lsi-sk"}
     )
+
+
+@mock_dynamodb2
+def test_set_attribute_is_dropped_if_empty_after_update_expression():
+    table_name, item_key, set_item = "test-table", "test-id", "test-data"
+    client = boto3.client("dynamodb", region_name="us-east-1")
+    client.create_table(
+        TableName=table_name,
+        KeySchema=[{"AttributeName": "customer", "KeyType": "HASH"}],
+        AttributeDefinitions=[{"AttributeName": "customer", "AttributeType": "S"}],
+        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
+    )
+
+    client.update_item(
+        TableName=table_name,
+        Key={"customer": {"S": item_key}},
+        UpdateExpression="ADD orders :order",
+        ExpressionAttributeValues={":order": {"SS": [set_item]}},
+    )
+    resp = client.scan(TableName=table_name, ProjectionExpression="customer, orders")
+    item = resp["Items"][0]
+    item.should.have.key("customer")
+    item.should.have.key("orders")
+
+    client.update_item(
+        TableName=table_name,
+        Key={"customer": {"S": item_key}},
+        UpdateExpression="DELETE orders :order",
+        ExpressionAttributeValues={":order": {"SS": [set_item]}},
+    )
+    resp = client.scan(TableName=table_name, ProjectionExpression="customer, orders")
+    item = resp["Items"][0]
+    item.should.have.key("customer")
+    item.should_not.have.key("orders")