Repository: getmoto/moto

Issue:
DynamoDB update_item does not update item on value type change (regression in 1.3.14)
Our automated tests ran with moto 1.3.14 today and we experience several unexpected test failures, all relating to DynamoDB. From the cases I looked at, it seems moto's dynamodb now fails to update an item's value when its type changes. Consider this test (works in 1.3.13, fails in 1.3.14). All tests run on Python 3.7.5.

```
import moto

with moto.mock_dynamodb2():
    dynamo = boto3.resource("dynamodb", region_name="eu-central-1")
    dynamo.create_table(
        AttributeDefinitions=[{"AttributeName": "job_id", "AttributeType": "S"}],
        TableName="origin-rbu-dev",
        KeySchema=[{"AttributeName": "job_id", "KeyType": "HASH"}],
        ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
    )
    table = dynamo.Table("origin-rbu-dev")
    table.put_item(Item={"job_id": "a", "output": None})
    table.update_item(
                Key=dict(job_id="a"),
                UpdateExpression="SET output = :output",
                ExpressionAttributeValues={
                    ":output": "updated",
                },
    )
    assert table.scan()["Items"][0]["output"] == "updated"
```

Bisecting yielded this commit:
64cf1fc2c9825a029402a347d2c4977c04c58dbb is the first bad commit
commit 64cf1fc2c9825a029402a347d2c4977c04c58dbb
Author: Bert Blommers <bblommers@users.noreply.github.com>
Date:   Tue Oct 22 20:40:41 2019 +0100

    Refactor DynamoDB update expressions (#2497)
    
    * Refactor DynamoDB.update to use recursive method for nested updates
    
    * Simplify DynamoDB.update_item logic




Discussion:
ping #2497 / @bblommers 

Resolving patch:
diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py
--- a/moto/dynamodb2/models.py
+++ b/moto/dynamodb2/models.py
@@ -77,6 +77,7 @@ def set(self, key, new_value, index=None):
             attr, list_index = attribute_is_list(attr)
             if not key:
                 # {'S': value} ==> {'S': new_value}
+                self.type = new_value.type
                 self.value = new_value.value
             else:
                 if attr not in self.value:  # nonexistingattribute


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
@@ -3319,3 +3319,66 @@ def _create_user_table():
         TableName="users", Item={"username": {"S": "user3"}, "foo": {"S": "bar"}}
     )
     return client
+
+
+@mock_dynamodb2
+def test_update_item_if_original_value_is_none():
+    dynamo = boto3.resource("dynamodb", region_name="eu-central-1")
+    dynamo.create_table(
+        AttributeDefinitions=[{"AttributeName": "job_id", "AttributeType": "S"}],
+        TableName="origin-rbu-dev",
+        KeySchema=[{"AttributeName": "job_id", "KeyType": "HASH"}],
+        ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
+    )
+    table = dynamo.Table("origin-rbu-dev")
+    table.put_item(Item={"job_id": "a", "job_name": None})
+    table.update_item(
+        Key={"job_id": "a"},
+        UpdateExpression="SET job_name = :output",
+        ExpressionAttributeValues={":output": "updated",},
+    )
+    table.scan()["Items"][0]["job_name"].should.equal("updated")
+
+
+@mock_dynamodb2
+def test_update_nested_item_if_original_value_is_none():
+    dynamo = boto3.resource("dynamodb", region_name="eu-central-1")
+    dynamo.create_table(
+        AttributeDefinitions=[{"AttributeName": "job_id", "AttributeType": "S"}],
+        TableName="origin-rbu-dev",
+        KeySchema=[{"AttributeName": "job_id", "KeyType": "HASH"}],
+        ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
+    )
+    table = dynamo.Table("origin-rbu-dev")
+    table.put_item(Item={"job_id": "a", "job_details": {"job_name": None}})
+    table.update_item(
+        Key={"job_id": "a"},
+        UpdateExpression="SET job_details.job_name = :output",
+        ExpressionAttributeValues={":output": "updated",},
+    )
+    table.scan()["Items"][0]["job_details"]["job_name"].should.equal("updated")
+
+
+@mock_dynamodb2
+def test_allow_update_to_item_with_different_type():
+    dynamo = boto3.resource("dynamodb", region_name="eu-central-1")
+    dynamo.create_table(
+        AttributeDefinitions=[{"AttributeName": "job_id", "AttributeType": "S"}],
+        TableName="origin-rbu-dev",
+        KeySchema=[{"AttributeName": "job_id", "KeyType": "HASH"}],
+        ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
+    )
+    table = dynamo.Table("origin-rbu-dev")
+    table.put_item(Item={"job_id": "a", "job_details": {"job_name": {"nested": "yes"}}})
+    table.put_item(Item={"job_id": "b", "job_details": {"job_name": {"nested": "yes"}}})
+    table.update_item(
+        Key={"job_id": "a"},
+        UpdateExpression="SET job_details.job_name = :output",
+        ExpressionAttributeValues={":output": "updated"},
+    )
+    table.get_item(Key={"job_id": "a"})["Item"]["job_details"][
+        "job_name"
+    ].should.be.equal("updated")
+    table.get_item(Key={"job_id": "b"})["Item"]["job_details"][
+        "job_name"
+    ].should.be.equal({"nested": "yes"})