Repository: getmoto/moto

Issue:
Dynamodb2: moto fails to throw error for multiple 'ADD' sections in UpdateExpression
Moto's dynamodb2 currently doesn't throw an error for this situation, where as the real dynamodb does.

For a query like so:

```python
table.update_item(
  Key={...},
  UpdateExpression='ADD x :one SET a = :a ADD y :one',
  ExpressionAttributeValues={':one': 1, ':a': 'lore ipsum'},
)
```

the real dynamo will return the error:

```txt
An error occurred (ValidationException) when calling the UpdateItem operation: Invalid UpdateExpression: The "ADD" section can only be used once in an update expression;
```

Whereas moto will happily process the query.


Discussion:

Resolving patch:
diff --git a/moto/dynamodb2/exceptions.py b/moto/dynamodb2/exceptions.py
--- a/moto/dynamodb2/exceptions.py
+++ b/moto/dynamodb2/exceptions.py
@@ -204,3 +204,10 @@ class UpdateHashRangeKeyException(MockValidationException):
 
     def __init__(self, key_name):
         super(UpdateHashRangeKeyException, self).__init__(self.msg.format(key_name))
+
+
+class TooManyAddClauses(InvalidUpdateExpression):
+    msg = 'The "ADD" section can only be used once in an update expression;'
+
+    def __init__(self):
+        super(TooManyAddClauses, self).__init__(self.msg)
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
@@ -1388,6 +1388,7 @@ def update_item(
             # Parse expression to get validation errors
             update_expression_ast = UpdateExpressionParser.make(update_expression)
             update_expression = re.sub(r"\s*([=\+-])\s*", "\\1", update_expression)
+            update_expression_ast.validate()
 
         if all([table.hash_key_attr in key, table.range_key_attr in key]):
             # Covers cases where table has hash and range keys, ``key`` param
diff --git a/moto/dynamodb2/parsing/ast_nodes.py b/moto/dynamodb2/parsing/ast_nodes.py
--- a/moto/dynamodb2/parsing/ast_nodes.py
+++ b/moto/dynamodb2/parsing/ast_nodes.py
@@ -3,6 +3,7 @@
 from collections import deque
 
 from moto.dynamodb2.models import DynamoType
+from ..exceptions import TooManyAddClauses
 
 
 class Node(metaclass=abc.ABCMeta):
@@ -20,6 +21,21 @@ def __init__(self, children=None):
     def set_parent(self, parent_node):
         self.parent = parent_node
 
+    def validate(self):
+        if self.type == "UpdateExpression":
+            nr_of_clauses = len(self.find_clauses(UpdateExpressionAddClause))
+            if nr_of_clauses > 1:
+                raise TooManyAddClauses()
+
+    def find_clauses(self, clause_type):
+        clauses = []
+        for child in self.children or []:
+            if isinstance(child, clause_type):
+                clauses.append(child)
+            elif isinstance(child, Expression):
+                clauses.extend(child.find_clauses(clause_type))
+        return clauses
+
 
 class LeafNode(Node):
     """A LeafNode is a Node where none of the children are Nodes themselves."""


Test patch:
diff --git a/tests/test_dynamodb2/test_dynamodb_exceptions.py b/tests/test_dynamodb2/test_dynamodb_exceptions.py
--- a/tests/test_dynamodb2/test_dynamodb_exceptions.py
+++ b/tests/test_dynamodb2/test_dynamodb_exceptions.py
@@ -147,3 +147,25 @@ def test_empty_expressionattributenames_with_projection():
     err = exc.value.response["Error"]
     err["Code"].should.equal("ValidationException")
     err["Message"].should.equal("ExpressionAttributeNames must not be empty")
+
+
+@mock_dynamodb2
+def test_update_item_range_key_set():
+    ddb = boto3.resource("dynamodb", region_name="us-east-1")
+
+    # Create the DynamoDB table.
+    table = ddb.create_table(
+        TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
+    )
+
+    with pytest.raises(ClientError) as exc:
+        table.update_item(
+            Key={"partitionKey": "the-key"},
+            UpdateExpression="ADD x :one SET a = :a ADD y :one",
+            ExpressionAttributeValues={":one": 1, ":a": "lore ipsum"},
+        )
+    err = exc.value.response["Error"]
+    err["Code"].should.equal("ValidationException")
+    err["Message"].should.equal(
+        'Invalid UpdateExpression: The "ADD" section can only be used once in an update expression;'
+    )