Skip to content

DynamoDBDocumentClient: QueryCommand fails with ValidationException operand type is incorrectly inferred as Map (M)  #7012

@tas-oikawa

Description

@tas-oikawa

Checkboxes for prior research

Describe the bug

After upgrading @aws-sdk/client-dynamodb and @aws-sdk/lib-dynamodb to version 3.787.0, some of our test cases started throwing the following error:

Caught expected error: Invalid KeyConditionExpression: Incorrect operand type for operator or function; operator or function: begins_with, operand type: M

We did not encounter this issue when using version 3.767.0.

This error occurs when we reuse the same QueryCommand instance for pagination.
Specifically, we execute a query, get the LastEvaluatedKey from the response, assign it to the ExclusiveStartKey of the same QueryCommand instance, and then execute the query again.
At this point, the error occurs.

Regression Issue

  • Select this option if this issue appears to be a regression.

SDK version number

@aws-sdk/package-name@version, ...

Which JavaScript Runtime is this issue in?

Node.js

Details of the browser/Node.js/ReactNative version

v20.16.0

Reproduction Steps

I created some test cases to reproduce this issue.

package.json

{
  "name": "marshall_test_repo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "vitest"
  },
  "private": true,
  "dependencies": {
    "@aws-sdk/client-dynamodb": "3.787.0",
    "@aws-sdk/lib-dynamodb": "3.787.0",
    "dynalite": "^3.2.2",
    "vitest": "^3.1.1"
  },
  "vitest": {
    "testMatch": [
      "**/*.test.ts"
    ]
  }
}

dynamodb.test.ts

import {
  DynamoDBClient,
  CreateTableCommand,
} from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  BatchWriteCommand,
  QueryCommand,
} from "@aws-sdk/lib-dynamodb";
import dynalite from "dynalite";
import { beforeAll, afterAll, describe, it, expect } from "vitest";

const dynaliteServer = dynalite({ createTableMs: 0 });
const PORT = 4567;
const TABLE_NAME = "TestTable";

const client = new DynamoDBClient({
  endpoint: `http://localhost:${PORT}`,
  region: "us-east-1",
});
const ddbDocClient = DynamoDBDocumentClient.from(client);

beforeAll(async () => {
  await new Promise((resolve) => dynaliteServer.listen(PORT, resolve));
  await client.send(
    new CreateTableCommand({
      TableName: TABLE_NAME,
      KeySchema: [
        { AttributeName: "PK", KeyType: "HASH" },
        { AttributeName: "SK", KeyType: "RANGE" },
      ],
      AttributeDefinitions: [
        { AttributeName: "PK", AttributeType: "S" },
        { AttributeName: "SK", AttributeType: "S" },
        {
          AttributeName: "Data",
          AttributeType: "S",
        },
      ],
      GlobalSecondaryIndexes: [
        {
          IndexName: "GSI1",
          KeySchema: [
            {
              AttributeName: "SK",
              KeyType: "HASH",
            },
            {
              AttributeName: "Data",
              KeyType: "RANGE",
            },
          ],
          Projection: {
            ProjectionType: "ALL",
          },
        },
      ],
      BillingMode: "PAY_PER_REQUEST",
    })
  );
});

afterAll(async () => {
  await new Promise((resolve) => dynaliteServer.close(resolve));
});

describe("DynamoDB behavior tests with ExclusiveStartKey", () => {
  it("should error when use same query input", async () => {
    const items = [
      {
        PK: "a#123",
        SK: "b#123",
        Data: "c#123",
      },
      {
        PK: "a#987",
        SK: "b#123",
        Data: "c#123",
      },
    ];

    // Insert test data
    await ddbDocClient.send(
      new BatchWriteCommand({
        RequestItems: {
          [TABLE_NAME]: items.map((item) => ({ PutRequest: { Item: item } })),
        },
      })
    );

    const query = new QueryCommand({
      TableName: TABLE_NAME,
      IndexName: "GSI1",
      Select: "ALL_ATTRIBUTES",
      ExpressionAttributeNames: {
        "#GSI1PK": "SK",
        "#GSI1SK": "Data",
      },
      ExpressionAttributeValues: {
        ":GSI1PK": "b#123",
        ":GSI1SK": "c#",
      },
      KeyConditionExpression:
        "#GSI1PK = :GSI1PK and begins_with(#GSI1SK, :GSI1SK)",
      Limit: 1,
    });

    // First query to get LastEvaluatedKey
    const firstQuery = await ddbDocClient.send(
      query
    );

    const lastKey = firstQuery.LastEvaluatedKey;

    query.input.ExclusiveStartKey = lastKey;
    expect(lastKey).toBeDefined();

  
    let errorCaught = false;
    let count = 1;
    try {
      const result = await ddbDocClient.send(
        query
      );
    } catch (err: any) {
      errorCaught = true;
      console.log("Caught expected error:", err.message);
    }

    expect(errorCaught).toBe(false); // We expect this to be false
  });
});

Observed Behavior

Caught expected error: Invalid KeyConditionExpression: Incorrect operand type for operator or function; operator or function: begins_with, operand type: M

Expected Behavior

The test cases pass with no errors.

Possible Solution

No response

Additional Information/Context

No response

Metadata

Metadata

Assignees

Labels

bugThis issue is a bug.closed-for-stalenessguidanceGeneral information and guidance, answers to FAQs, or recommended best practices/resources.potential-regressionMarking this issue as a potential regression to be checked by team memberworkaround-availableThis issue has a work around available.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions