Repository: getmoto/moto

Issue:
DynamoDB2: querying incorrectly parses KeyConditionExpression
This is a bug for moto==1.3.14 .

When using an `AND` condition for DynamoDB moto fails to recognize the `AND` if it is not in all uppercase.  This is more restrictive than Dynamo DB which accepts lowercase `and` and returns the correct details.

AWS also has the lower case `and` usage in their docs. For example https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html .  

This can be easily reproduced by creating a table and querying with a condition that matches the description as mentioned.

```
import boto3
from moto import mock_dynamodb2

table_name = 'Thread'
forum_name = 'ForumName'
subject = 'Subject'
table_definition = {
        "AttributeDefinitions": [
            {
                'AttributeName': forum_name,
                'AttributeType': 'S'
            },
            {
                'AttributeName': subject,
                'AttributeType': 'S'
            }
        ],
        "TableName": table_name,
        "KeySchema": [
            {
                'AttributeName': forum_name,
                'KeyType': 'HASH'
            },
            {
                'AttributeName': subject,
                'KeyType': 'RANGE'
            }
        ],
        "BillingMode": 'PAY_PER_REQUEST',
    }


def create_load_query_delete(lowercase=False, deleteTable=False):
    if lowercase:
        and_condition = 'and'
    else:
        and_condition = 'AND'
    session = boto3.Session(profile_name='default')
    ddb_client = session.client('dynamodb')
    try:
        ddb_client.create_table(**table_definition)
        waiter = ddb_client.get_waiter('table_exists')
        waiter.wait(TableName=table_name)
    except ddb_client.exceptions.ResourceInUseException:
        print("Table exists not creating.")
    ddb_client.put_item(
        TableName=table_name,
        Item={
            forum_name: {'S': 'forum1'},
            subject: {'S': 'subject1'}
        }
    )
    ddb_client.put_item(
        TableName=table_name,
        Item={
            forum_name: {'S': 'forum1'},
            subject: {'S': 'subject2'}
        }
    )
    query_args = {
        'TableName': table_name,
        'KeyConditionExpression': f'{forum_name} = :forum_name {and_condition} {subject} >= :subject',
        'ExpressionAttributeValues': {
            ':forum_name': {
                'S': 'forum1'
            },
            ':subject': {
                'S': 'subject2'
            }
        }
    }
    response = ddb_client.query(**query_args)
    items = response.get('Items', [])
    if deleteTable:
        ddb_client.delete_table(TableName=table_name)
    return items


@mock_dynamodb2
def test_upper_case_mocked():
    assert len(create_load_query_delete()) == 1


@mock_dynamodb2
def test_lower_case_mocked():
    # This test fails
    assert len(create_load_query_delete(lowercase=True)) == 1


def test_upper_case_aws():
    assert len(create_load_query_delete()) == 1


def test_lower_case_aws():
    assert len(create_load_query_delete(lowercase=True, deleteTable=True)) == 1

```


Discussion:

Resolving patch:
diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py
--- a/moto/dynamodb2/responses.py
+++ b/moto/dynamodb2/responses.py
@@ -459,8 +459,10 @@ def query(self):
                 for k, v in six.iteritems(self.body.get("ExpressionAttributeNames", {}))
             )
 
-            if " AND " in key_condition_expression:
-                expressions = key_condition_expression.split(" AND ", 1)
+            if " and " in key_condition_expression.lower():
+                expressions = re.split(
+                    " AND ", key_condition_expression, maxsplit=1, flags=re.IGNORECASE
+                )
 
                 index_hash_key = [key for key in index if key["KeyType"] == "HASH"][0]
                 hash_key_var = reverse_attribute_lookup.get(


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
@@ -1408,6 +1408,13 @@ def test_filter_expression():
     filter_expr.expr(row1).should.be(True)
     filter_expr.expr(row2).should.be(False)
 
+    # lowercase AND test
+    filter_expr = moto.dynamodb2.comparisons.get_filter_expression(
+        "Id > :v0 and Subs < :v1", {}, {":v0": {"N": "5"}, ":v1": {"N": "7"}}
+    )
+    filter_expr.expr(row1).should.be(True)
+    filter_expr.expr(row2).should.be(False)
+
     # OR test
     filter_expr = moto.dynamodb2.comparisons.get_filter_expression(
         "Id = :v0 OR Id=:v1", {}, {":v0": {"N": "5"}, ":v1": {"N": "8"}}
@@ -2719,7 +2726,7 @@ def test_query_gsi_with_range_key():
     res = dynamodb.query(
         TableName="test",
         IndexName="test_gsi",
-        KeyConditionExpression="gsi_hash_key = :gsi_hash_key AND gsi_range_key = :gsi_range_key",
+        KeyConditionExpression="gsi_hash_key = :gsi_hash_key and gsi_range_key = :gsi_range_key",
         ExpressionAttributeValues={
             ":gsi_hash_key": {"S": "key1"},
             ":gsi_range_key": {"S": "range1"},