Repository: getmoto/moto (python)

Issue:
[Regression in UpdateItem: Invalid UpdateExpression Error (5.1.2)] - [DynamoDB]: Path overlap check for valid updates
Hi,

After upgrading to `moto` 5.1.2 (PR #8648), I encountered an issue where valid `UpdateItem` operations fail in `moto` with a `ValidationException`, even though they work fine with real DynamoDB tables.

**Expected Behavior**
- The update operation should succeed, as it does on a real DynamoDB table.

**Actual Behavior**
- The update fails in `moto` with the following error:  

```plaintext
botocore.exceptions.ClientError: An error occurred (ValidationException) when calling the UpdateItem operation: 
Invalid UpdateExpression: Two document paths overlap with each other; must remove or rewrite one of these paths; 
path one: [nested], path two: [nested]
```

**Minimal Reproducible Test Case**

```python
import os
from typing import TypeVar, Generator

import boto3
import pytest
from moto import mock_aws
from boto3.session import Session
from mypy_boto3_dynamodb.service_resource import Table, DynamoDBServiceResource

T = TypeVar("T")

YieldFixture = Generator[T, None, None]


@pytest.fixture(scope="session")
def region_name() -> str:
    return "us-east-1"


@pytest.fixture(scope="session")
def aws_credentials(region_name: str):
    """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"
    os.environ["AWS_DEFAULT_REGION"] = region_name


@pytest.fixture(scope="session")
def dynamodb_resource(aws_credentials, region_name: str) -> YieldFixture[DynamoDBServiceResource]:
    with mock_aws():
        yield boto3.resource(service_name="dynamodb", region_name=region_name)


@pytest.fixture
def moto_dynamodb_table(dynamodb_resource: DynamoDBServiceResource) -> Table:
    return dynamodb_resource.create_table(
        TableName="dev-table",
        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
        AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
        BillingMode="PAY_PER_REQUEST",
    )


@pytest.fixture
def real_dynamodb_table() -> Table:
    # This is the real table in the AWS account, need to be created upfront
    return Session(profile_name="<your-valid-profile>").resource("dynamodb").Table("dev-table")


def test_update_item_moto(moto_dynamodb_table: Table):
    nested = {"key1": "value1", "key2": "value2", "key3": "value3"}
    item = {"id": "1", "name": "test", "nested": nested}
    moto_dynamodb_table.put_item(Item=item)
    moto_dynamodb_table.update_item(
        Key={"id": "1"},
        UpdateExpression="SET #BPGKOx.#IMAz=:key1,#BPGKOx.#dCtc=:key2",
        ExpressionAttributeNames={"#BPGKOx": "nested", "#IMAz": "key1", "#dCtc": "key2"},
        ExpressionAttributeValues={":key1": "new value1", ":key2": "new value2"},
    )
    item = moto_dynamodb_table.get_item(Key={"id": "1"})["Item"]
    assert item["nested"] == {"key1": "new value1", "key2": "new value2", "key3": "value3"}


def test_update_item_real(real_dynamodb_table: Table):
    nested = {"key1": "value1", "key2": "value2", "key3": "value3"}
    item = {"id": "1", "name": "test", "nested": nested}
    real_dynamodb_table.put_item(Item=item)
    real_dynamodb_table.update_item(
        Key={"id": "1"},
        UpdateExpression="SET #BPGKOx.#IMAz=:key1,#BPGKOx.#dCtc=:key2",
        ExpressionAttributeNames={"#BPGKOx": "nested", "#IMAz": "key1", "#dCtc": "key2"},
        ExpressionAttributeValues={":key1": "new value1", ":key2": "new value2"},
    )
    item = real_dynamodb_table.get_item(Key={"id": "1"})["Item"]
    assert item["nested"] == {"key1": "new value1", "key2": "new value2", "key3": "value3"}
```

Environment Details

Moto Version: 5.1.2
Python Version: 3.12

Thanks for your time! Let me know if you need any further details. 😊

Pull request description:
DynamoDB: Dont trigger Path overlap on expressions with shared root
Fixes #8740 

If we only have a root name (and not an expression, so 'path' instead of '#path'), AWS only checks for direct matches, not path overlaps. However, this code path should trigger only if we have a root name (`root`, one child) - not with a nested name (`root.child`, which resolves to three children)

@BlizzardOf - considering it's a minor adjustment to [your earlier PR](https://github.com/getmoto/moto/pull/8648) where we introduced path overlap checks, do you want to review this?

Patch:
diff --git a/moto/dynamodb/parsing/validators.py b/moto/dynamodb/parsing/validators.py
index 0304be217..db0963f38 100644
--- a/moto/dynamodb/parsing/validators.py
+++ b/moto/dynamodb/parsing/validators.py
@@ -478,7 +478,12 @@ class OverlappingPathsValidator(DepthFirstTraverser):  # type: ignore[misc]
         for action in actions:
             path = action.children[0]
             path_name = path.children[0]
-            if isinstance(path_name, ExpressionAttributeName):
+            # for some reason, if we only have a root expression ('#path1', '#path2'),
+            # AWS only checks for direct matches, not path overlaps
+            if (
+                isinstance(path_name, ExpressionAttributeName)
+                and len(path.children) == 1
+            ):
                 expression_attribute_name_paths.append(
                     self.expression_attribute_names[
                         path_name.get_attribute_name_placeholder()
@@ -487,8 +492,6 @@ class OverlappingPathsValidator(DepthFirstTraverser):  # type: ignore[misc]
             else:
                 plaintext_paths.append(path.to_str())
 
-        # for some reason, AWS only checks expression attribute names for
-        # direct matches, not path overlaps
         overlapping_paths = find_path_overlaps(plaintext_paths)
         if overlapping_paths:
             # There might be more than one attribute duplicated:


Test patch:
diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
index 1a5411582..51769d4fc 100644
--- a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
+++ b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
@@ -367,6 +367,28 @@ class TestUpdateExpressionClausesWithClashingExpressions(BaseTest):
             "Invalid UpdateExpression: Two document paths overlap with each other;"
         )
 
+    def test_update_item_with_shared_root_but_different_overall(self):
+        # Can update an item where the first part of the expressions is duplicate but not the rest
+        nested = {"key1": "value1", "key2": "value2", "key3": "value3"}
+        item = {"pk": "1", "name": "test", "nested": nested}
+        self.table.put_item(Item=item)
+        self.table.update_item(
+            Key={"pk": "1"},
+            UpdateExpression="SET #root.#child1=:key1,#root.#child2=:key2",
+            ExpressionAttributeNames={
+                "#root": "nested",
+                "#child1": "key1",
+                "#child2": "key2",
+            },
+            ExpressionAttributeValues={":key1": "new value1", ":key2": "new value2"},
+        )
+        item = self.table.get_item(Key={"pk": "1"})["Item"]
+        assert item["nested"] == {
+            "key1": "new value1",
+            "key2": "new value2",
+            "key3": "value3",
+        }
+
     def test_delete_and_add_on_different_set(self):
         self.table.update_item(
             Key={"pk": "the-key"},