1

I'm working a use case where a dynamoDB update should:

  1. Dynamically upsert (update if present, insert if not present) an item, without hardcoding the item's components.
  2. Use the DynamoDB Document Client for simplicity
  3. And in the same atomic operation, update a simple counter

I started with an excellent utility method by Daniel Barrel at https://stackoverflow.com/a/63511693/15369972 that provides a general utility method for the update with dynamic values, but without the atomic counter.

I've attempted to add the atomic counter capability by adding the counter and its incrementor into the parameter objects after the dynamic values are loaded, but am getting a static value in the counter on update instead of a value that increments by one with each call.

Where is this going wrong? I call the modified update function with a table name, a dynamic javascript object, and an array containing the hash and sort key:

await update(tableName, jsonObjectToStore, ['myHashKey', 'mySortKey'])

And the modified update method that's not incrementing as I'd like, is:

async function update (tableName, item, idAttributeNames) {

    var params = {
        TableName: tableName,
        Key: {},
        ExpressionAttributeValues: {},
        ExpressionAttributeNames: {},
        UpdateExpression: "",
        ReturnValues: "UPDATED_NEW"
    };

    for (const attname of idAttributeNames) {
        params["Key"][attname] = item[attname];
    }

    let prefix = "set ";
    let attributes = Object.keys(item);
    for (let i=0; i<attributes.length; i++) {
        let attribute = attributes[i];
        if (!idAttributeNames.includes(attribute)) {
            params["UpdateExpression"] += prefix + "#" + attribute + " = :" + attribute;
            params["ExpressionAttributeValues"][":" + attribute] = item[attribute];
            params["ExpressionAttributeNames"]["#" + attribute] = attribute;
            prefix = ", ";
        }
    }
    
    // Add the counter
    params["UpdateExpression"] +=  ", #nImports = :nImports + :incr";
    console.log(params["UpdateExpression"])
    
    console.log(params["ExpressionAttributeValues"])
    params["ExpressionAttributeValues"][":incr"] = 1;
    params["ExpressionAttributeValues"][":nImports"] = 0;
    console.log(params["ExpressionAttributeValues"])
        
    console.log(params["ExpressionAttributeNames"])
    params["ExpressionAttributeNames"]["#nImports"] = 'nImports'
    console.log(params["ExpressionAttributeNames"])
    
    await docClient.update

    return await docClient.update(params).promise();
}
rpc
  • 79
  • 7
  • `params["UpdateExpression"] += ", SET nImports = nImports + :incr";` – hoangdv Oct 11 '21 at 01:28
  • Sadly, does not work but appreciate the thought. Error is ```INFO ValidationException: Invalid UpdateExpression: Syntax error; token: "SET", near: ", SET nImports"```. Reason is that the SET is already present at the beginning of the whole string of the UpdateExpression, and encompasses the nImports at the end. Therefore adding an extra 'SET' midstring creates the syntax error – rpc Oct 11 '21 at 14:11
  • Then, let's remove "SET" – hoangdv Oct 12 '21 at 01:01
  • Sorry Hoang, I should have been more clear. Inserting SET breaks the syntax. Removing it gives the original code I wrote, which produces a static value in the counter, instead of incrementing the counter. There's something I'm missing in the arithmetic expression that updates the counter or something similar that is delivering a static value. – rpc Oct 12 '21 at 01:13
  • `params["UpdateExpression"] += ", nImports = nImports + :incr"; ` – hoangdv Oct 12 '21 at 01:18
  • Sorry, tried that variant too. None work – rpc Oct 14 '21 at 21:33

2 Answers2

0

Worked with AWS Support to find a reasonable solution. They also were not sure how to do an atomic counter using the ddb document client (as opposed to the low level client which has many documented examples) but suggested the ADD command, which has the side effect of an atomic update on a numeric field.

So, with the example below, we construct our dynamic update from the object to be stored, then append the ADD statement in the update expression (without a comma!), and add what is in effect a numeric incrementor to the ExpressionAttributeValues for nImports. Like this, which should be a complete working lambda example. There's a few console.log statements to show what's happening:

const AWS = require('aws-sdk');
const docClient = new AWS.DynamoDB.DocumentClient();
async function update (tableName, item, idAttributeNames) {
    var params = {
        TableName: tableName,
        Key: {},
        ExpressionAttributeValues: {},
        ExpressionAttributeNames: {},
        UpdateExpression: "",
        ReturnValues: "UPDATED_NEW"
    };

    for (const attname of idAttributeNames) {
        params["Key"][attname] = item[attname];
    }

    let prefix = "set ";
    let attributes = Object.keys(item);
    for (let i=0; i<attributes.length; i++) {
        let attribute = attributes[i];
        if (!idAttributeNames.includes(attribute)) {
            params["UpdateExpression"] += prefix + "#" + attribute + " = :" + attribute;
            params["ExpressionAttributeValues"][":" + attribute] = item[attribute];
            params["ExpressionAttributeNames"]["#" + attribute] = attribute;
            prefix = ", ";
        }
    }
    console.log('params before adding atomic counter is:', params)
    // Add the counter using the ADD syntax
    params["UpdateExpression"] +=  " ADD #nImports :nImports"
    params["ExpressionAttributeValues"][":nImports"] = 1;
    params["ExpressionAttributeNames"]["#nImports"] = 'nImports'

    console.log('params after adding atomic counter is:', params)
    
    try {
        const result = await docClient.update(params).promise();
        console.log('after await, result is ', result);
        return result;
    } catch (err) {
        console.log('err is ', err)
    }
};



exports.handler = async (event) => {
    
    const item = {title: 'sometitle', site_url: "www.amazon.com", key: "G"};
    const body = await update('test_table', item, ['title', 'site_url']);
    const response = {
        statusCode: 200,
        body: JSON.stringify(body),
    };
    return response;
}
rpc
  • 79
  • 7
0

The kind folks at AWS did a little more digging, and also pointed out an error in the initial code that, when corrected, should increment as desired using the SET operator.

Basically the original code didn't properly target the variable for increment. So a corrected version where we add the incremented variable should be:

    console.log('params before adding atomic counter is:', params)
    // Add the counter
    params["UpdateExpression"] += ", #nImports = #nImports + :incr";
    params["ExpressionAttributeValues"][":incr"] = 1;
    //params["ExpressionAttributeValues"][":nImports"] = 0;
    params["ExpressionAttributeNames"]["#nImports"] = 'nImports'

    console.log('params after adding atomic counter is:', params)```

I'm sticking with the original ADD answer because I like the differentiation it gives over the properties inserted by the SET, but both seem valid and I wanted to include the correction as well
rpc
  • 79
  • 7