Repository: getmoto/moto

Issue:
dynamodb2 update_item error occurring on non existing items creates item
## item should not get created

**how to reproduce the issue**
Have a non existing item and try to update it with the condition of an attribute not being existent.
Try to retrieve the item.

Example data:
table with primary: `primary_partition_key`
update expression:
```
{'Key': {'primary_partition_key': 'some_identification_string'}, 'UpdateExpression': 'set #AA.#AB = :aa', 'ExpressionAttributeValues': {':aa': 'abc'}, 'ExpressionAttributeNames': {'#AA': 'some_dict', '#AB': 'key1'}, 'ConditionExpression': 'attribute_not_exists(#AA.#AB)'}
```


code:
```
table = resource("dynamodb").Table("table_name")
table.update_item(**update_expression)
item = table.get_item(Key="primary_partition_key")
```

**expected behavior**
the get_item operation should not contain any items

**actual behavior**
an item get's returned with only the primary_key as value


**Versions**
used as decorator mock
moto: 2.0.0 (installation already by version 1.2.x, clean installed before raising issue)
boto3: 1.17.15
botocore: 1.20.15
Maybe related to issue #3635 



Discussion:
Hi @Janluak, I can't reproduce this I'm afraid.

The test case that I use:
```
@mock_dynamodb2
def test_3729():
    name = "TestTable"
    conn = boto3.client("dynamodb", region_name="us-west-2")
    hkey = "primary_partition_key"
    conn.create_table(
        TableName=name,
        KeySchema=[{"AttributeName": hkey, "KeyType": "HASH"}],
        AttributeDefinitions=[{"AttributeName": hkey, "AttributeType": "S"}],
        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
    )
    update_expression = {'Key': {hkey: 'some_identification_string'},
                         'UpdateExpression': 'set #AA.#AB = :aa',
                         'ExpressionAttributeValues': {':aa': 'abc'},
                         'ExpressionAttributeNames': {'#AA': 'some_dict', '#AB': 'key1'},
                         'ConditionExpression': 'attribute_not_exists(#AA.#AB)'}
    table = boto3.resource("dynamodb", region_name="us-west-2").Table(name)
    table.update_item(**update_expression)
```
This results in this error:
`An error occurred (ValidationException) when calling the UpdateItem operation: The document path provided in the update expression is invalid for update`

Note that this is the wrong error; AWS gives a `ResourceNotFoundException: Requested resource not found`. 
But it does fail for me, as it should.
Hey @bblommers,
thanks for checking. I got again into debugging and in deed, the exception is raised.
Yet, even though the exception get's raised the item get's created:
At least for me a scan operation afterwards returns an item with only the primary key. When running directly against a real instance of ddb there is no item.
Could you please check if you have the same behavior?
Ah,  you're right  @Janluak - I'm seeing the same thing. Curious, I just assumed it would fail early and not persist anything.

Marking it as a bug.

Resolving patch:
diff --git a/moto/dynamodb2/models/__init__.py b/moto/dynamodb2/models/__init__.py
--- a/moto/dynamodb2/models/__init__.py
+++ b/moto/dynamodb2/models/__init__.py
@@ -1407,6 +1407,22 @@ def update_item(
 
         # Update does not fail on new items, so create one
         if item is None:
+            if update_expression:
+                # Validate AST before creating anything
+                item = Item(
+                    hash_value,
+                    table.hash_key_type,
+                    range_value,
+                    table.range_key_type,
+                    attrs={},
+                )
+                UpdateExpressionValidator(
+                    update_expression_ast,
+                    expression_attribute_names=expression_attribute_names,
+                    expression_attribute_values=expression_attribute_values,
+                    item=item,
+                    table=table,
+                ).validate()
             data = {table.hash_key_attr: {hash_value.type: hash_value.value}}
             if range_value:
                 data.update(


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
@@ -6245,3 +6245,35 @@ def test_describe_endpoints(region):
             },
         ]
     )
+
+
+@mock_dynamodb2
+def test_update_non_existing_item_raises_error_and_does_not_contain_item_afterwards():
+    """
+    https://github.com/spulec/moto/issues/3729
+    Exception is raised, but item was persisted anyway
+    Happened because we would create a placeholder, before validating/executing the UpdateExpression
+    :return:
+    """
+    name = "TestTable"
+    conn = boto3.client("dynamodb", region_name="us-west-2")
+    hkey = "primary_partition_key"
+    conn.create_table(
+        TableName=name,
+        KeySchema=[{"AttributeName": hkey, "KeyType": "HASH"}],
+        AttributeDefinitions=[{"AttributeName": hkey, "AttributeType": "S"}],
+        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
+    )
+    update_expression = {
+        "Key": {hkey: "some_identification_string"},
+        "UpdateExpression": "set #AA.#AB = :aa",
+        "ExpressionAttributeValues": {":aa": "abc"},
+        "ExpressionAttributeNames": {"#AA": "some_dict", "#AB": "key1"},
+        "ConditionExpression": "attribute_not_exists(#AA.#AB)",
+    }
+    table = boto3.resource("dynamodb", region_name="us-west-2").Table(name)
+    with pytest.raises(ClientError) as err:
+        table.update_item(**update_expression)
+    err.value.response["Error"]["Code"].should.equal("ValidationException")
+
+    conn.scan(TableName=name)["Items"].should.have.length_of(0)