{
    "componentChunkName": "component---src-templates-mdx-content-page-js",
    "path": "/dynamodb-update-expression/",
    "result": {"data":{"mdx":{"body":"var _excluded = [\"components\"];\n\nfunction _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"path\": \"/dynamodb-update-expression/\",\n  \"publishedDate\": \"2026-10-23\",\n  \"updatedDate\": \"2026-10-23\",\n  \"metaTitle\": \"DynamoDB UpdateExpression: Examples and Error Fixes\",\n  \"metaDescription\": \"Use SET, REMOVE, ADD and DELETE with tested DynamoDB update expressions, nested maps, lists, conditional writes, return values and dynamic builders.\",\n  \"title\": \"DynamoDB update expressions: SET, REMOVE, ADD and DELETE\",\n  \"authorName\": \"Rafal Wilinski\",\n  \"authorLink\": \"https://x.com/rafalwilinski\",\n  \"authorImage\": \"/images/authors/rafal-wilinski.jpg\",\n  \"image\": \"/images/posts/dynamodb-update-expression/social.png\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, _excluded);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"A DynamoDB \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"UpdateExpression\"), \" changes attributes on one item identified by its complete primary key. Use \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET\"), \" to assign values, \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"REMOVE\"), \" to delete attributes, \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ADD\"), \" to increment numbers or merge sets, and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"DELETE\"), \" to subtract set members. You can combine all four clauses in one atomic \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"UpdateItem\"), \" request, with each clause keyword appearing at most once. If the item does not exist, the update can create it; add a \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ConditionExpression\"), \" when creation or an unexpected overwrite must be rejected.\"), mdx(\"p\", null, \"The examples below ran against DynamoDB Local on port 8000. Their outputs show actual item contents, including failed updates that left the item unchanged. Local tests verify expression behavior; billing and service limits are checked against AWS documentation.\"), mdx(\"h2\", null, \"DynamoDB update expression syntax\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Clause\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"What it changes\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Example shape\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"SET\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Any supported attribute type; numeric arithmetic; list and map paths\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"SET #status = :status, #stock = #stock + :delta\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"REMOVE\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Whole attributes, map members, list elements\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"REMOVE #obsolete, #items[1]\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"ADD\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Numbers or sets at the top level\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"ADD #stock :delta, #tags :newTags\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"DELETE\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Members of a top-level set\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"DELETE #tags :oldTags\"))))), mdx(\"p\", null, \"Separate actions \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"inside a clause\"), \" with commas. Separate different clauses with spaces. Clause order in the string is flexible. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET a = :a SET b = :b\"), \" is invalid; use one \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET\"), \" containing both assignments. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"DELETE\"), \" here removes set members; deleting an entire item uses the separate \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"DeleteItem\"), \" API.\"), mdx(\"p\", null, mdx(\"img\", {\n    parentName: \"p\",\n    \"src\": \"/images/posts/dynamodb-update-expression/updexpr-anatomy.svg\",\n    \"alt\": \"An UpdateExpression combines four clauses, with name aliases resolving to item attributes and value tokens resolving to typed inputs.\"\n  })), mdx(\"p\", null, \"A name beginning with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"#\"), \" belongs in \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ExpressionAttributeNames\"), \". A value beginning with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \":\"), \" belongs in \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ExpressionAttributeValues\"), \". They are separate maps, and their entries must match the tokens actually used in the update or condition. Alias \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-reserved-words/\"\n  }, \"reserved words\"), \" such as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"status\"), \", and names containing punctuation. Alias each segment of a nested path separately: \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"#profile.#city\"), \". Mapping one alias to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"profile.city\"), \" addresses a literal attribute with a dot in its name.\"), mdx(\"p\", null, \"The \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html\"\n  }, \"AWS update-expression reference\"), \" defines the grammar. Unlike \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-putitem-vs-updateitem/\"\n  }, \"PutItem\"), \", an update preserves attributes it does not target. Replacing a whole map with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET\"), \", however, replaces that map's contents, including members omitted from the new value.\"), mdx(\"h2\", null, \"Run the examples locally\"), mdx(\"p\", null, \"Use Node.js 20 or later, a disposable directory and a fresh local container. These commands install dependencies there, with no AWS account required:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-bash\"\n  }, \"# Run in a new directory, with Docker, Node.js and AWS CLI v2 installed.\\ndocker run -d --rm --name dynamodb-local -p 8000:8000 \\\\\\n  amazon/dynamodb-local:latest -jar DynamoDBLocal.jar \\\\\\n  -inMemory -sharedDb -disableTelemetry\\nnpm init -y\\nnpm pkg set type=module\\nnpm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb @aws-sdk/util-dynamodb\\nnpm install --save-dev tsx typescript @types/node\\npython3 -m venv venv\\nvenv/bin/pip install boto3\\n\")), mdx(\"p\", null, \"Save this shared fixture as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"setup.mjs\"), \". It creates \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"UpdateExpressionDemo\"), \", whose only key is the String attribute \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"pk\"), \". On a table with a sort key, supply both key attributes in every request.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import {\\n  CreateTableCommand, DynamoDBClient, waitUntilTableExists,\\n} from \\\"@aws-sdk/client-dynamodb\\\"\\nimport {\\n  DeleteCommand, DynamoDBDocumentClient, GetCommand, PutCommand, UpdateCommand,\\n} from \\\"@aws-sdk/lib-dynamodb\\\"\\nimport { unmarshall } from \\\"@aws-sdk/util-dynamodb\\\"\\n\\nexport const low = new DynamoDBClient({\\n  endpoint: \\\"http://localhost:8000\\\",\\n  region: \\\"us-east-1\\\",\\n  credentials: { accessKeyId: \\\"fake\\\", secretAccessKey: \\\"fake\\\" },\\n})\\nexport const doc = DynamoDBDocumentClient.from(low)\\nexport const TableName = \\\"UpdateExpressionDemo\\\"\\nexport const Key = { pk: \\\"item-1\\\" }\\ntry {\\n  await low.send(new CreateTableCommand({\\n    TableName,\\n    KeySchema: [{ AttributeName: \\\"pk\\\", KeyType: \\\"HASH\\\" }],\\n    AttributeDefinitions: [{ AttributeName: \\\"pk\\\", AttributeType: \\\"S\\\" }],\\n    BillingMode: \\\"PAY_PER_REQUEST\\\",\\n  }))\\n} catch (error) {\\n  if (error.name !== \\\"ResourceInUseException\\\") throw error\\n}\\nawait waitUntilTableExists({ client: low, maxWaitTime: 30 }, { TableName })\\n\\nexport const read = async () => (await doc.send(new GetCommand({\\n  TableName, Key, ConsistentRead: true,\\n}))).Item ?? null\\n\\nexport function normalize(value) {\\n  if (value instanceof Set) return { $set: [...value].sort() }\\n  if (Array.isArray(value)) return value.map(normalize)\\n  if (value && typeof value === \\\"object\\\") {\\n    return Object.fromEntries(Object.keys(value).sort()\\n      .map(key => [key, normalize(value[key])]))\\n  }\\n  return value\\n}\\nexport const print = value => console.log(JSON.stringify(normalize(value)))\\n\\n// Each example replaces only item-1 in this disposable local table.\\nexport async function example(seed, request, options = {}) {\\n  if (seed === null) await doc.send(new DeleteCommand({ TableName, Key }))\\n  else await doc.send(new PutCommand({ TableName, Item: { ...seed, ...Key } }))\\n  const before = await read()\\n  let result\\n  try {\\n    result = await doc.send(new UpdateCommand({\\n      ReturnValues: \\\"ALL_NEW\\\", ...request, TableName, Key,\\n    }))\\n  } catch (error) {\\n    if (!options.expectError) throw error\\n    print({ before, error: `${error.name}: ${error.message}`, after: await read(),\\n      ...(error.Item ? { failureItem: unmarshall(error.Item) } : {}) })\\n    return\\n  }\\n  if (options.expectError) throw new Error(\\\"Expected the update to fail\\\")\\n  const after = await read()\\n  print({ before, after, ...(options.showReturn\\n    ? { mode: request.ReturnValues, returned: result.Attributes ?? null } : {}) })\\n  return after\\n}\\n\")), mdx(\"p\", null, \"Every JavaScript example imports that file. Save each block under its stated filename and run \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"node filename.mjs\"), \". The fixture replaces \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"item-1\"), \" before each case, then reads it before and after the update. Those reads are test instrumentation; an application can normally use the update's \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ReturnValues\"), \" without extra reads.\"), mdx(\"p\", null, \"Output keys are sorted for reproducibility. A displayed \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"{\\\"$set\\\":[...]}\"), \" represents an SDK \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"Set\"), \", with sorted members for display only; DynamoDB sets have no order. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"before: null\"), \" means the item was absent. This display convention is not a DynamoDB storage format.\"), mdx(\"h2\", null, \"Combine SET, REMOVE, ADD and DELETE in one update\"), mdx(\"p\", null, \"Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"clauses.mjs\"), \". This publishes an item, removes a temporary flag, adds stock and removes an old tag in one request:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import { example } from \\\"./setup.mjs\\\"\\n\\nawait example({\\n  status: \\\"draft\\\", obsolete: true, stock: 5,\\n  tags: new Set([\\\"sale\\\", \\\"old\\\"]),\\n}, {\\n  UpdateExpression: \\\"SET #s = :s REMOVE #o ADD #n :n DELETE #t :t\\\",\\n  ExpressionAttributeNames: {\\n    \\\"#s\\\": \\\"status\\\", \\\"#o\\\": \\\"obsolete\\\", \\\"#n\\\": \\\"stock\\\", \\\"#t\\\": \\\"tags\\\",\\n  },\\n  ExpressionAttributeValues: { \\\":s\\\": \\\"live\\\", \\\":n\\\": 2, \\\":t\\\": new Set([\\\"old\\\"]) },\\n})\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"live\\\",\\\"stock\\\":7,\\\"tags\\\":{\\\"$set\\\":[\\\"sale\\\"]}},\\\"before\\\":{\\\"obsolete\\\":true,\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"draft\\\",\\\"stock\\\":5,\\\"tags\\\":{\\\"$set\\\":[\\\"old\\\",\\\"sale\\\"]}}}\\n\")), mdx(\"p\", null, mdx(\"img\", {\n    parentName: \"p\",\n    \"src\": \"/images/posts/dynamodb-update-expression/updexpr-before-after.svg\",\n    \"alt\": \"One atomic update changes status from draft to live, removes obsolete, increments stock from 5 to 7 and removes old from the tags set.\"\n  })), mdx(\"p\", null, \"The four changes succeed together. A malformed action or a failed condition prevents the whole update.\"), mdx(\"h2\", null, \"SET: assignments, arithmetic and if_not_exists\"), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET\"), \" overwrites an existing value or creates an attribute. Arithmetic supports addition and subtraction on Numbers. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"if_not_exists(path, fallback)\"), \" supplies a default when that attribute is absent; an existing \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"NULL\"), \" attribute still exists and does not select the fallback.\"), mdx(\"p\", null, \"Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"set.mjs\"), \". Both cases subtract stock, but only the first writes \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"created\"), \":\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import { example } from \\\"./setup.mjs\\\"\\n\\nfor (const seed of [{ stock: 5 }, { stock: 5, created: \\\"original\\\" }]) {\\n  await example(seed, {\\n    UpdateExpression: \\\"SET #n = #n - :n, #c = if_not_exists(#c, :c)\\\",\\n    ExpressionAttributeNames: { \\\"#n\\\": \\\"stock\\\", \\\"#c\\\": \\\"created\\\" },\\n    ExpressionAttributeValues: { \\\":n\\\": 2, \\\":c\\\": \\\"2026-10-23\\\" },\\n  })\\n}\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"created\\\":\\\"2026-10-23\\\",\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":3},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":5}}\\n{\\\"after\\\":{\\\"created\\\":\\\"original\\\",\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":3},\\\"before\\\":{\\\"created\\\":\\\"original\\\",\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":5}}\\n\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"if_not_exists\"), \" is a function inside \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET\"), \", not a substitute for an update condition. Other actions in the expression still run when the attribute exists. Function names are case-sensitive.\"), mdx(\"h3\", null, \"Append or prepend a DynamoDB list\"), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"list_append(left, right)\"), \" concatenates two Lists. Put the existing list on the left to append, or on the right to prepend. An Array in the JavaScript document client is a List; \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"new Set(...)\"), \" is a different \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-data-types/\"\n  }, \"DynamoDB data type\"), \".\"), mdx(\"p\", null, \"Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"lists.mjs\"), \". The first loop handles both an absent list and an existing list. The last case replaces index zero and writes beyond the list's end:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import { example } from \\\"./setup.mjs\\\"\\n\\nfor (const seed of [{}, { items: [\\\"pen\\\"] }]) {\\n  await example(seed, {\\n    UpdateExpression: \\\"SET #l = list_append(if_not_exists(#l, :empty), :new)\\\",\\n    ExpressionAttributeNames: { \\\"#l\\\": \\\"items\\\" },\\n    ExpressionAttributeValues: { \\\":empty\\\": [], \\\":new\\\": [\\\"book\\\"] },\\n  })\\n}\\nawait example({ items: [\\\"pen\\\"] }, {\\n  UpdateExpression: \\\"SET #l = list_append(:new, #l)\\\",\\n  ExpressionAttributeNames: { \\\"#l\\\": \\\"items\\\" },\\n  ExpressionAttributeValues: { \\\":new\\\": [\\\"book\\\"] },\\n})\\nawait example({ items: [\\\"pen\\\", \\\"book\\\"] }, {\\n  UpdateExpression: \\\"SET #l[0] = :replacement, #l[8] = :extra\\\",\\n  ExpressionAttributeNames: { \\\"#l\\\": \\\"items\\\" },\\n  ExpressionAttributeValues: { \\\":replacement\\\": \\\"pencil\\\", \\\":extra\\\": \\\"bag\\\" },\\n})\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"items\\\":[\\\"book\\\"],\\\"pk\\\":\\\"item-1\\\"},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\"}}\\n{\\\"after\\\":{\\\"items\\\":[\\\"pen\\\",\\\"book\\\"],\\\"pk\\\":\\\"item-1\\\"},\\\"before\\\":{\\\"items\\\":[\\\"pen\\\"],\\\"pk\\\":\\\"item-1\\\"}}\\n{\\\"after\\\":{\\\"items\\\":[\\\"book\\\",\\\"pen\\\"],\\\"pk\\\":\\\"item-1\\\"},\\\"before\\\":{\\\"items\\\":[\\\"pen\\\"],\\\"pk\\\":\\\"item-1\\\"}}\\n{\\\"after\\\":{\\\"items\\\":[\\\"pencil\\\",\\\"book\\\",\\\"bag\\\"],\\\"pk\\\":\\\"item-1\\\"},\\\"before\\\":{\\\"items\\\":[\\\"pen\\\",\\\"book\\\"],\\\"pk\\\":\\\"item-1\\\"}}\\n\")), mdx(\"p\", null, \"Indexes start at zero. Writing a nonexistent index appends the element; it does not create empty slots up to that index. Use \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"list_append\"), \" when appending is the intent. Concurrent position-based edits can target a different logical element after another writer changes the list; protect such edits with a version condition when identity matters.\"), mdx(\"h3\", null, \"Update a nested map attribute\"), mdx(\"p\", null, \"Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"nested.mjs\"), \":\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import { example } from \\\"./setup.mjs\\\"\\n\\nawait example({ profile: { city: \\\"Paris\\\", tier: \\\"free\\\" } }, {\\n  UpdateExpression: \\\"SET #p.#c = :c\\\",\\n  ExpressionAttributeNames: { \\\"#p\\\": \\\"profile\\\", \\\"#c\\\": \\\"city\\\" },\\n  ExpressionAttributeValues: { \\\":c\\\": \\\"Rome\\\" },\\n})\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"profile\\\":{\\\"city\\\":\\\"Rome\\\",\\\"tier\\\":\\\"free\\\"}},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"profile\\\":{\\\"city\\\":\\\"Paris\\\",\\\"tier\\\":\\\"free\\\"}}}\\n\")), mdx(\"p\", null, \"The sibling \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"tier\"), \" survives. The parent \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"profile\"), \" must already be a Map; DynamoDB does not construct intermediate maps for \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"profile.city\"), \". \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"if_not_exists(profile.city, :city)\"), \" cannot repair a missing parent. The troubleshooting section shows the error and a separate initialization step.\"), mdx(\"h2\", null, \"REMOVE: delete attributes and list elements\"), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"REMOVE\"), \" takes paths without values. Removing a missing top-level attribute is a no-op. Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"remove.mjs\"), \":\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import { example } from \\\"./setup.mjs\\\"\\n\\nawait example({\\n  obsolete: true, profile: { city: \\\"Paris\\\", tier: \\\"free\\\" },\\n  items: [\\\"pen\\\", \\\"book\\\", \\\"bag\\\", \\\"ruler\\\"],\\n}, {\\n  UpdateExpression: \\\"REMOVE #o, #p.#c, #l[1], #l[2], #missing\\\",\\n  ExpressionAttributeNames: {\\n    \\\"#o\\\": \\\"obsolete\\\", \\\"#p\\\": \\\"profile\\\", \\\"#c\\\": \\\"city\\\",\\n    \\\"#l\\\": \\\"items\\\", \\\"#missing\\\": \\\"notPresent\\\",\\n  },\\n})\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"items\\\":[\\\"pen\\\",\\\"ruler\\\"],\\\"pk\\\":\\\"item-1\\\",\\\"profile\\\":{\\\"tier\\\":\\\"free\\\"}},\\\"before\\\":{\\\"items\\\":[\\\"pen\\\",\\\"book\\\",\\\"bag\\\",\\\"ruler\\\"],\\\"obsolete\\\":true,\\\"pk\\\":\\\"item-1\\\",\\\"profile\\\":{\\\"city\\\":\\\"Paris\\\",\\\"tier\\\":\\\"free\\\"}}}\\n\")), mdx(\"p\", null, \"Both list indexes refer to the original list: \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"book\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"bag\"), \" disappear, then the remaining elements shift together. No need to reverse the indexes within one expression. Across separate requests, indexes can shift between calls. Removing a nested member also requires a valid parent path.\"), mdx(\"h2\", null, \"ADD: increment a counter or add to a set\"), mdx(\"p\", null, \"A Number operand increments the existing Number. A negative operand decrements it. If the attribute is absent, DynamoDB starts from zero. A Set operand unions its members with the stored set, without duplicates; an absent set is created. Both sets must have the same member type: String, Number or Binary.\"), mdx(\"p\", null, \"Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"add.mjs\"), \". The final case uses \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET\"), \" arithmetic for a nested counter because \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ADD\"), \" only accepts top-level paths:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import { example } from \\\"./setup.mjs\\\"\\n\\nfor (const seed of [{}, { stock: 5, tags: new Set([\\\"sale\\\"]) }]) {\\n  await example(seed, {\\n    UpdateExpression: \\\"ADD #n :n, #t :t\\\",\\n    ExpressionAttributeNames: { \\\"#n\\\": \\\"stock\\\", \\\"#t\\\": \\\"tags\\\" },\\n    ExpressionAttributeValues: { \\\":n\\\": 2, \\\":t\\\": new Set([\\\"sale\\\", \\\"new\\\"]) },\\n  })\\n}\\nawait example({ profile: {} }, {\\n  UpdateExpression: \\\"SET #p.#n = if_not_exists(#p.#n, :zero) + :n\\\",\\n  ExpressionAttributeNames: { \\\"#p\\\": \\\"profile\\\", \\\"#n\\\": \\\"visits\\\" },\\n  ExpressionAttributeValues: { \\\":zero\\\": 0, \\\":n\\\": 1 },\\n})\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":2,\\\"tags\\\":{\\\"$set\\\":[\\\"new\\\",\\\"sale\\\"]}},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\"}}\\n{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":7,\\\"tags\\\":{\\\"$set\\\":[\\\"new\\\",\\\"sale\\\"]}},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":5,\\\"tags\\\":{\\\"$set\\\":[\\\"sale\\\"]}}}\\n{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"profile\\\":{\\\"visits\\\":1}},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"profile\\\":{}}}\\n\")), mdx(\"p\", null, \"The counter operation is atomic, so it avoids a read-then-write lost increment. It is \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"not idempotent\"), \": retrying a successful increment increments again. The same applies to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET #n = #n + :n\"), \" and to list appends. A clause named \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET\"), \" does not itself make an operation safe to replay. Use a conditional version or an application idempotency design when a timeout leaves success uncertain. See \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-errors/dynamodb-atomic-counter-not-working/\"\n  }, \"atomic counter troubleshooting\"), \" for missing attributes and type mismatches.\"), mdx(\"h2\", null, \"DELETE: remove members from a set\"), mdx(\"p\", null, \"Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"delete.mjs\"), \":\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import { example } from \\\"./setup.mjs\\\"\\n\\nfor (const tags of [new Set([\\\"old\\\", \\\"sale\\\"]), new Set([\\\"old\\\"])]) {\\n  await example({ tags }, {\\n    UpdateExpression: \\\"DELETE #t :t\\\",\\n    ExpressionAttributeNames: { \\\"#t\\\": \\\"tags\\\" },\\n    ExpressionAttributeValues: { \\\":t\\\": new Set([\\\"old\\\"]) },\\n  })\\n}\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"tags\\\":{\\\"$set\\\":[\\\"sale\\\"]}},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"tags\\\":{\\\"$set\\\":[\\\"old\\\",\\\"sale\\\"]}}}\\n{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\"},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"tags\\\":{\\\"$set\\\":[\\\"old\\\"]}}}\\n\")), mdx(\"p\", null, \"The first case keeps \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"sale\"), \"; the second removes the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"tags\"), \" attribute because no members remain. You cannot pass an empty set as the operand. Use \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"REMOVE #tags\"), \" to remove the whole attribute, and a List plus \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET\"), \"/\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"REMOVE\"), \" when order or duplicate entries matter.\"), mdx(\"h2\", null, \"How are multiple actions evaluated?\"), mdx(\"p\", null, \"Right-hand attribute references use the item's values \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"before the update\"), \", even if another action changes or removes the referenced attribute. The expression is not a sequence of JavaScript assignments.\"), mdx(\"p\", null, \"Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"evaluation.mjs\"), \":\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import { example } from \\\"./setup.mjs\\\"\\n\\nawait example({ stock: 5 }, {\\n  UpdateExpression: \\\"SET #n = #n + :n, #previous = #n\\\",\\n  ExpressionAttributeNames: { \\\"#n\\\": \\\"stock\\\", \\\"#previous\\\": \\\"previousStock\\\" },\\n  ExpressionAttributeValues: { \\\":n\\\": 2 },\\n})\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"previousStock\\\":5,\\\"stock\\\":7},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":5}}\\n\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"previousStock\"), \" becomes \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"5\"), \", not \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"7\"), \". Referencing a path is allowed; assigning overlapping target paths is not. For example, trying to set both \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"profile\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"profile.city\"), \" in one expression is rejected. Reordering clauses cannot resolve that overlap.\"), mdx(\"h2\", null, \"UpdateItem upserts unless a condition prevents it\"), mdx(\"p\", null, \"Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"upsert.mjs\"), \". Both cases start with no item. The second adds an existence condition:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import { example } from \\\"./setup.mjs\\\"\\n\\nconst request = {\\n  UpdateExpression: \\\"SET #s = :s\\\",\\n  ExpressionAttributeNames: { \\\"#s\\\": \\\"status\\\" },\\n  ExpressionAttributeValues: { \\\":s\\\": \\\"live\\\" },\\n}\\nawait example(null, request)\\nawait example(null, {\\n  ...request,\\n  ConditionExpression: \\\"attribute_exists(#pk)\\\",\\n  ExpressionAttributeNames: { ...request.ExpressionAttributeNames, \\\"#pk\\\": \\\"pk\\\" },\\n}, { expectError: true })\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"live\\\"},\\\"before\\\":null}\\n{\\\"after\\\":null,\\\"before\\\":null,\\\"error\\\":\\\"ConditionalCheckFailedException: The conditional request failed\\\"}\\n\")), mdx(\"p\", null, \"The first creates the key and the assigned attributes. The second fails without creating anything. An upsert still needs a valid expression: arithmetic on an absent Number without a default, or an update through a missing map, can fail.\"), mdx(\"h3\", null, \"Conditional updates and optimistic locking\"), mdx(\"p\", null, \"A condition is tested atomically with the write. The \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-conditional-write-and-update/\"\n  }, \"conditional update guide\"), \" covers existence and value checks. For \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-locking/\"\n  }, \"optimistic locking\"), \", compare the version you read and increment it in the same update.\"), mdx(\"p\", null, \"Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"conditional.mjs\"), \". These independent cases start at version \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"3\"), \"; one caller expects that version and the other expects an older version:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import { example } from \\\"./setup.mjs\\\"\\n\\nfor (const expected of [3, 2]) {\\n  await example({ stock: 5, version: 3 }, {\\n    UpdateExpression: \\\"SET #n = #n - :qty, #v = #v + :one\\\",\\n    ConditionExpression: \\\"attribute_exists(#pk) AND #v = :expected AND #n >= :qty\\\",\\n    ExpressionAttributeNames: { \\\"#pk\\\": \\\"pk\\\", \\\"#n\\\": \\\"stock\\\", \\\"#v\\\": \\\"version\\\" },\\n    ExpressionAttributeValues: { \\\":qty\\\": 2, \\\":one\\\": 1, \\\":expected\\\": expected },\\n    ReturnValuesOnConditionCheckFailure: \\\"ALL_OLD\\\",\\n  }, { expectError: expected === 2 })\\n}\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":3,\\\"version\\\":4},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":5,\\\"version\\\":3}}\\n{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":5,\\\"version\\\":3},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":5,\\\"version\\\":3},\\\"error\\\":\\\"ConditionalCheckFailedException: The conditional request failed\\\",\\\"failureItem\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":5,\\\"version\\\":3}}\\n\")), mdx(\"p\", null, \"The failed request leaves stock and version unchanged. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ReturnValuesOnConditionCheckFailure: \\\"ALL_OLD\\\"\"), \" returns the item that failed the condition in the exception's \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"Item\"), \" field. In the tested JavaScript SDK, that exception field is low-level DynamoDB JSON, so the fixture calls \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"unmarshall(error.Item)\"), \". It can be absent when the item does not exist. Handle the conflict in application logic rather than blindly repeating the same condition.\"), mdx(\"p\", null, \"Keeping historical versions is a separate requirement from rejecting stale writes; see \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-versioning/\"\n  }, \"DynamoDB data versioning\"), \" for that model.\"), mdx(\"h2\", null, \"ReturnValues: what does UpdateItem return?\"), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ReturnValues\"), \" controls a successful response's \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"Attributes\"), \" field. Returned attributes are strongly consistent and do not consume read capacity. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ReturnValuesOnConditionCheckFailure\"), \" is separate and accepts only \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ALL_OLD\"), \" or \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"NONE\"), \". These behaviors are specified in the \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html\"\n  }, \"UpdateItem API reference\"), \".\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"th\"\n  }, \"ReturnValues\")), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Returned attributes\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"NONE\"), \" (default)\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"No \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"Attributes\"), \" field\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"ALL_OLD\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Entire item before the update\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"UPDATED_OLD\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Previous values of affected attributes\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"ALL_NEW\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Entire item after the update\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"UPDATED_NEW\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"New values of affected attributes that remain present\")))), mdx(\"p\", null, \"Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"returns.mjs\"), \". Every iteration resets the same starting item:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import { example } from \\\"./setup.mjs\\\"\\n\\nfor (const ReturnValues of [\\\"NONE\\\", \\\"ALL_OLD\\\", \\\"UPDATED_OLD\\\", \\\"ALL_NEW\\\", \\\"UPDATED_NEW\\\"]) {\\n  await example({ status: \\\"draft\\\", obsolete: true, stock: 5 }, {\\n    UpdateExpression: \\\"SET #s = :s REMOVE #o\\\",\\n    ExpressionAttributeNames: { \\\"#s\\\": \\\"status\\\", \\\"#o\\\": \\\"obsolete\\\" },\\n    ExpressionAttributeValues: { \\\":s\\\": \\\"live\\\" },\\n    ReturnValues,\\n  }, { showReturn: true })\\n}\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"live\\\",\\\"stock\\\":5},\\\"before\\\":{\\\"obsolete\\\":true,\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"draft\\\",\\\"stock\\\":5},\\\"mode\\\":\\\"NONE\\\",\\\"returned\\\":null}\\n{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"live\\\",\\\"stock\\\":5},\\\"before\\\":{\\\"obsolete\\\":true,\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"draft\\\",\\\"stock\\\":5},\\\"mode\\\":\\\"ALL_OLD\\\",\\\"returned\\\":{\\\"obsolete\\\":true,\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"draft\\\",\\\"stock\\\":5}}\\n{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"live\\\",\\\"stock\\\":5},\\\"before\\\":{\\\"obsolete\\\":true,\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"draft\\\",\\\"stock\\\":5},\\\"mode\\\":\\\"UPDATED_OLD\\\",\\\"returned\\\":{\\\"obsolete\\\":true,\\\"status\\\":\\\"draft\\\"}}\\n{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"live\\\",\\\"stock\\\":5},\\\"before\\\":{\\\"obsolete\\\":true,\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"draft\\\",\\\"stock\\\":5},\\\"mode\\\":\\\"ALL_NEW\\\",\\\"returned\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"live\\\",\\\"stock\\\":5}}\\n{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"live\\\",\\\"stock\\\":5},\\\"before\\\":{\\\"obsolete\\\":true,\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"draft\\\",\\\"stock\\\":5},\\\"mode\\\":\\\"UPDATED_NEW\\\",\\\"returned\\\":{\\\"status\\\":\\\"live\\\"}}\\n\")), mdx(\"p\", null, \"The fixture prints \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"returned: null\"), \" for an absent \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"Attributes\"), \" field. Notice that \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"UPDATED_OLD\"), \" includes the removed \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"obsolete\"), \" attribute, while \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"UPDATED_NEW\"), \" omits it and the untouched \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stock\"), \" attribute. Neither option is a general-purpose record of deletions and additions; use both images in your own audit workflow if needed.\"), mdx(\"h2\", null, \"SDK v3 document client versus low-level DynamoDB JSON\"), mdx(\"p\", null, \"Use \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"UpdateCommand\"), \" from \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"@aws-sdk/lib-dynamodb\"), \" with ordinary JavaScript values, or \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"UpdateItemCommand\"), \" from \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"@aws-sdk/client-dynamodb\"), \" with explicit type descriptors. Do not feed \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"{ N: \\\"2\\\" }\"), \" to a document-client numeric operand: it represents a Map there. The \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/programming-with-javascript.html\"\n  }, \"AWS JavaScript programming guide\"), \" explains the two layers and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"marshall\"), \"/\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"unmarshall\"), \".\"), mdx(\"p\", null, \"Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"low-level.mjs\"), \":\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import { UpdateItemCommand } from \\\"@aws-sdk/client-dynamodb\\\"\\nimport { PutCommand } from \\\"@aws-sdk/lib-dynamodb\\\"\\nimport { unmarshall } from \\\"@aws-sdk/util-dynamodb\\\"\\nimport { low, doc, TableName, Key, read, print } from \\\"./setup.mjs\\\"\\n\\nawait doc.send(new PutCommand({ TableName, Item: { ...Key, stock: 5 } }))\\nconst before = await read()\\nconst result = await low.send(new UpdateItemCommand({\\n  TableName,\\n  Key: { pk: { S: \\\"item-1\\\" } },\\n  UpdateExpression: \\\"ADD #n :n\\\",\\n  ExpressionAttributeNames: { \\\"#n\\\": \\\"stock\\\" },\\n  ExpressionAttributeValues: { \\\":n\\\": { N: \\\"2\\\" } },\\n  ReturnValues: \\\"ALL_NEW\\\",\\n}))\\nprint({ before, after: unmarshall(result.Attributes) })\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":7},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"stock\\\":5}}\\n\")), mdx(\"p\", null, \"Numbers on the low-level wire are strings under \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"N\"), \". The command still increments a Number, not a String.\"), mdx(\"h3\", null, \"AWS CLI v2 example\"), mdx(\"p\", null, \"After running \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"setup.mjs\"), \", save this as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"cli.sh\"), \" and run \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"bash cli.sh\"), \". The first printed item is the strongly consistent read before the update; the second is \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ALL_NEW\"), \" afterwards. Every call targets the local endpoint with dummy credentials.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-bash\"\n  }, \"export AWS_ACCESS_KEY_ID=fake AWS_SECRET_ACCESS_KEY=fake\\nexport AWS_DEFAULT_REGION=us-east-1 AWS_PAGER=\\\"\\\"\\nunset AWS_SESSION_TOKEN AWS_PROFILE\\n\\nddb() { aws dynamodb --endpoint-url http://localhost:8000 --region us-east-1 \\\"$@\\\"; }\\nddb put-item --table-name UpdateExpressionDemo \\\\\\n  --item '{\\\"pk\\\":{\\\"S\\\":\\\"item-1\\\"},\\\"stock\\\":{\\\"N\\\":\\\"5\\\"}}'\\nddb get-item --table-name UpdateExpressionDemo \\\\\\n  --key '{\\\"pk\\\":{\\\"S\\\":\\\"item-1\\\"}}' --consistent-read --query Item --output json\\nddb update-item --table-name UpdateExpressionDemo \\\\\\n  --key '{\\\"pk\\\":{\\\"S\\\":\\\"item-1\\\"}}' \\\\\\n  --update-expression 'ADD #n :n' \\\\\\n  --expression-attribute-names '{\\\"#n\\\":\\\"stock\\\"}' \\\\\\n  --expression-attribute-values '{\\\":n\\\":{\\\"N\\\":\\\"2\\\"}}' \\\\\\n  --return-values ALL_NEW --query Attributes --output json\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\n    \\\"stock\\\": {\\n        \\\"N\\\": \\\"5\\\"\\n    },\\n    \\\"pk\\\": {\\n        \\\"S\\\": \\\"item-1\\\"\\n    }\\n}\\n{\\n    \\\"stock\\\": {\\n        \\\"N\\\": \\\"7\\\"\\n    },\\n    \\\"pk\\\": {\\n        \\\"S\\\": \\\"item-1\\\"\\n    }\\n}\\n\")), mdx(\"h2\", null, \"Build an UpdateExpression dynamically from a partial object\"), mdx(\"p\", null, \"A patch builder needs explicit meanings for omission, null and removal. These helpers treat each object key as a \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"literal top-level attribute name\"), \". They alias every name, generate one clause per action type, reject primary-key changes, and omit \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ExpressionAttributeValues\"), \" for remove-only patches.\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Patch input\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Meaning\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Missing property; JavaScript \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"undefined\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Leave it unchanged\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"JavaScript \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"null\"), \"; Python \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"None\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Store DynamoDB \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"NULL\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"REMOVE\"), \" sentinel\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Delete the attribute\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Map/object or List/Array\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Replace that whole value\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Name containing \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \".\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Literal name, not a nested path\")))), mdx(\"p\", null, \"These are expression builders, not schema validators. Validate types, permitted fields and expression size at your application's boundary. Pass both key names if your table has a sort key. For a nested patch, build separate aliases for explicit path segments and reject ancestor/descendant target collisions; do not split arbitrary user-supplied names on dots.\"), mdx(\"h3\", null, \"TypeScript builder\"), mdx(\"p\", null, \"Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"builder.ts\"), \":\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-ts\"\n  }, \"import type { NativeAttributeValue } from \\\"@aws-sdk/util-dynamodb\\\"\\n\\nexport const REMOVE = Symbol(\\\"REMOVE\\\")\\ntype Patch = Record<string, NativeAttributeValue | typeof REMOVE | undefined>\\n\\nexport function buildUpdate(patch: Patch, keyNames: readonly string[] = [\\\"pk\\\"]) {\\n  const names: Record<string, string> = {}\\n  const values: Record<string, NativeAttributeValue> = {}\\n  const sets: string[] = []\\n  const removes: string[] = []\\n  for (const [name, value] of Object.entries(patch)) {\\n    if (value === undefined) continue\\n    if (!name || keyNames.includes(name)) throw new Error(`Cannot patch: ${name}`)\\n    const i = Object.keys(names).length\\n    const alias = `#n${i}`\\n    names[alias] = name\\n    if (value === REMOVE) removes.push(alias)\\n    else {\\n      const token = `:v${i}`\\n      sets.push(`${alias} = ${token}`)\\n      values[token] = value\\n    }\\n  }\\n  const clauses = [\\n    sets.length ? `SET ${sets.join(\\\", \\\")}` : \\\"\\\",\\n    removes.length ? `REMOVE ${removes.join(\\\", \\\")}` : \\\"\\\",\\n  ].filter(Boolean)\\n  if (!clauses.length) throw new Error(\\\"No changes\\\")\\n  return {\\n    UpdateExpression: clauses.join(\\\" \\\"),\\n    ExpressionAttributeNames: names,\\n    ...(sets.length ? { ExpressionAttributeValues: values } : {}),\\n  }\\n}\\n\")), mdx(\"p\", null, \"Save this as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"builder-demo.ts\"), \" and run \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"npx tsx builder-demo.ts\"), \":\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-ts\"\n  }, \"import { buildUpdate, REMOVE } from \\\"./builder.ts\\\"\\nimport { example, print } from \\\"./setup.mjs\\\"\\n\\nconst patch = buildUpdate({\\n  status: \\\"live\\\", note: null, obsolete: REMOVE,\\n  \\\"audit.label\\\": \\\"checked\\\", untouched: undefined,\\n})\\nprint(patch)\\nawait example({ status: \\\"draft\\\", obsolete: true }, patch)\\nawait example({ obsolete: true }, buildUpdate({ obsolete: REMOVE }))\\nfor (const invalid of [{}, { pk: \\\"different-key\\\" }]) {\\n  try { buildUpdate(invalid) }\\n  catch (error) { print({ error: (error as Error).message }) }\\n}\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"ExpressionAttributeNames\\\":{\\\"#n0\\\":\\\"status\\\",\\\"#n1\\\":\\\"note\\\",\\\"#n2\\\":\\\"obsolete\\\",\\\"#n3\\\":\\\"audit.label\\\"},\\\"ExpressionAttributeValues\\\":{\\\":v0\\\":\\\"live\\\",\\\":v1\\\":null,\\\":v3\\\":\\\"checked\\\"},\\\"UpdateExpression\\\":\\\"SET #n0 = :v0, #n1 = :v1, #n3 = :v3 REMOVE #n2\\\"}\\n{\\\"after\\\":{\\\"audit.label\\\":\\\"checked\\\",\\\"note\\\":null,\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"live\\\"},\\\"before\\\":{\\\"obsolete\\\":true,\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"draft\\\"}}\\n{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\"},\\\"before\\\":{\\\"obsolete\\\":true,\\\"pk\\\":\\\"item-1\\\"}}\\n{\\\"error\\\":\\\"No changes\\\"}\\n{\\\"error\\\":\\\"Cannot patch: pk\\\"}\\n\")), mdx(\"p\", null, \"The output demonstrates the literal \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"audit.label\"), \" name, stored null, an omitted undefined value, a remove-only update, and rejection of an empty patch and a primary-key change. To modify a sibling inside a Map, use a nested path as shown earlier; passing a partial Map to this helper replaces the existing Map.\"), mdx(\"h3\", null, \"Python builder with boto3 Table.update_item\"), mdx(\"p\", null, \"Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"builder.py\"), \" and run \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"venv/bin/python builder.py\"), \" after creating the table with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"setup.mjs\"), \". This uses boto3's \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/boto3/latest/reference/services/dynamodb/table/update_item.html\"\n  }, \"resource \", mdx(\"inlineCode\", {\n    parentName: \"a\"\n  }, \"Table.update_item\")), \", so values are native Python values. Use \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"Decimal\"), \" for non-integer numbers instead of Python \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"float\"), \"; the JSON formatter below renders non-integral Decimals as strings to avoid display precision loss.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-python\"\n  }, \"import json\\nfrom decimal import Decimal\\nimport boto3\\n\\nREMOVE = object()\\n\\n\\ndef build_update(patch, key_names=(\\\"pk\\\",)):\\n    names, values, sets, removes = {}, {}, [], []\\n    for name, value in patch.items():\\n        if not isinstance(name, str) or not name or name in key_names:\\n            raise ValueError(f\\\"Cannot patch: {name}\\\")\\n        i = len(names)\\n        alias = f\\\"#n{i}\\\"\\n        names[alias] = name\\n        if value is REMOVE:\\n            removes.append(alias)\\n        else:\\n            token = f\\\":v{i}\\\"\\n            sets.append(f\\\"{alias} = {token}\\\")\\n            values[token] = value\\n    clauses = []\\n    if sets:\\n        clauses.append(\\\"SET \\\" + \\\", \\\".join(sets))\\n    if removes:\\n        clauses.append(\\\"REMOVE \\\" + \\\", \\\".join(removes))\\n    if not clauses:\\n        raise ValueError(\\\"No changes\\\")\\n    result = {\\\"UpdateExpression\\\": \\\" \\\".join(clauses), \\\"ExpressionAttributeNames\\\": names}\\n    if values:\\n        result[\\\"ExpressionAttributeValues\\\"] = values\\n    return result\\n\\n\\ndef normalize(value):\\n    if isinstance(value, Decimal):\\n        return int(value) if value == value.to_integral_value() else str(value)\\n    if isinstance(value, set):\\n        return {\\\"$set\\\": sorted(normalize(x) for x in value)}\\n    if isinstance(value, dict):\\n        return {k: normalize(v) for k, v in value.items()}\\n    if isinstance(value, list):\\n        return [normalize(v) for v in value]\\n    return value\\n\\n\\ntable = boto3.resource(\\n    \\\"dynamodb\\\", endpoint_url=\\\"http://localhost:8000\\\", region_name=\\\"us-east-1\\\",\\n    aws_access_key_id=\\\"fake\\\", aws_secret_access_key=\\\"fake\\\",\\n).Table(\\\"UpdateExpressionDemo\\\")\\nkey = {\\\"pk\\\": \\\"item-1\\\"}\\n\\nfor patch in [\\n    {\\\"status\\\": \\\"live\\\", \\\"note\\\": None, \\\"obsolete\\\": REMOVE, \\\"audit.label\\\": \\\"checked\\\"},\\n    {\\\"obsolete\\\": REMOVE},\\n]:\\n    table.put_item(Item={**key, \\\"status\\\": \\\"draft\\\", \\\"obsolete\\\": True})\\n    before = table.get_item(Key=key, ConsistentRead=True)[\\\"Item\\\"]\\n    result = table.update_item(Key=key, **build_update(patch), ReturnValues=\\\"ALL_NEW\\\")\\n    print(json.dumps(normalize({\\\"before\\\": before, \\\"after\\\": result[\\\"Attributes\\\"]}),\\n                     sort_keys=True, separators=(\\\",\\\", \\\":\\\")))\\nfor invalid in [{}, {\\\"pk\\\": \\\"different-key\\\"}]:\\n    try:\\n        build_update(invalid)\\n    except ValueError as error:\\n        print(json.dumps({\\\"error\\\": str(error)}, separators=(\\\",\\\", \\\":\\\")))\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"audit.label\\\":\\\"checked\\\",\\\"note\\\":null,\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"live\\\"},\\\"before\\\":{\\\"obsolete\\\":true,\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"draft\\\"}}\\n{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"draft\\\"},\\\"before\\\":{\\\"obsolete\\\":true,\\\"pk\\\":\\\"item-1\\\",\\\"status\\\":\\\"draft\\\"}}\\n{\\\"error\\\":\\\"No changes\\\"}\\n{\\\"error\\\":\\\"Cannot patch: pk\\\"}\\n\")), mdx(\"p\", null, \"Python has no JavaScript-style \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"undefined\"), \"; omit a dictionary key to leave the attribute unchanged. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"None\"), \" writes \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"NULL\"), \", and only the sentinel requests removal.\"), mdx(\"h2\", null, \"Common UpdateExpression errors and their fixes\"), mdx(\"p\", null, \"The following messages were reproduced from DynamoDB Local. Each failing request below started and ended with exactly \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"{\\\"pk\\\":\\\"item-1\\\"}\"), \"; no requested change was applied. The \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ValidationException\"), \" prefix is the exception name. SDK wrappers may format their surrounding message differently.\"), mdx(\"h3\", null, \"Two document paths overlap\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"ValidationException: Invalid UpdateExpression: Two document paths overlap with each other; must remove or rewrite one of these paths; path one: [profile], path two: [profile, city]\\n\")), mdx(\"p\", null, \"The failing expression was \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET #p = :p, #p.#c = :c\"), \", with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"#p\"), \" mapped to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"profile\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"#c\"), \" to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"city\"), \". Set the whole Map once, or update only its children when the Map already exists. Parent initialization and child updates must be separate requests.\"), mdx(\"h3\", null, \"The document path is invalid for update\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"ValidationException: The document path provided in the update expression is invalid for update\\n\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET #p.#c = :c\"), \" failed because \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"profile\"), \" was absent. Initialize the Map separately as shown below. An existing parent of the wrong type also needs correction.\"), mdx(\"h3\", null, \"Invalid UpdateExpression: Syntax error\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"ValidationException: Invalid UpdateExpression: Syntax error; token: \\\"<EOF>\\\", near: \\\"=\\\"\\n\")), mdx(\"p\", null, \"The incomplete expression \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET #s =\"), \" has no operand. Supply a value token and its mapping, as in the upsert example. Also check commas and parentheses.\"), mdx(\"h3\", null, \"Attribute name is a reserved keyword\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"ValidationException: Invalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: status\\n\")), mdx(\"p\", null, \"The failing expression used \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET status = :s\"), \". Change it to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET #s = :s\"), \" with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ExpressionAttributeNames\"), \" mapping \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"#s\"), \" to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"status\"), \".\"), mdx(\"h3\", null, \"The SET section can only be used once\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"ValidationException: Invalid UpdateExpression: The \\\"SET\\\" section can only be used once in an update expression;\\n\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET #s = :s SET #n = :n\"), \" repeats the clause keyword. Use \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET #s = :s, #n = :n\"), \" instead.\"), mdx(\"h3\", null, \"An expression attribute value is not defined\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"ValidationException: Invalid UpdateExpression: An expression attribute value used in expression is not defined; attribute value: :missing\\n\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET #s = :missing\"), \" used an unmapped token. Add \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \":missing\"), \" to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ExpressionAttributeValues\"), \", or correct the expression to use the token you actually supplied.\"), mdx(\"h3\", null, \"The expression refers to an attribute that does not exist\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"ValidationException: The provided expression refers to an attribute that does not exist in the item\\n\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET #l = list_append(#l, :new)\"), \" failed with no \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"items\"), \" attribute. Use \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"if_not_exists(#l, :empty)\"), \" with an empty List, as in \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"lists.mjs\"), \". An existing non-List value needs a type correction instead.\"), mdx(\"h3\", null, \"Cannot update a primary-key attribute\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"ValidationException: One or more parameter values were invalid: Cannot update attribute pk. This attribute is part of the key\\n\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET #pk = :pk\"), \" tried to change \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"pk\"), \". The key belongs in \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"Key\"), \", not among the update targets. Moving an item to another key requires creating the new item and deleting the old one, with a transaction if they must succeed together.\"), mdx(\"h3\", null, \"An empty set is invalid\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"ValidationException: ExpressionAttributeValues contains invalid value: One or more parameter values were invalid: An string set  may not be empty for key :empty\\n\")), mdx(\"p\", null, \"This exact message came from a low-level \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ADD #t :empty\"), \" request with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \":empty\"), \" set to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SS: []\"), \". An empty JavaScript Set can instead fail earlier during marshalling. Omit an empty union/subtraction operation, or use \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"REMOVE\"), \" to delete the entire stored Set. Do not convert an empty Set to NULL unless that type change is intended.\"), mdx(\"h3\", null, \"Fix a missing parent map\"), mdx(\"p\", null, \"Create the parent as part of the original item whenever possible. For existing data with an optional parent, initialize it with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"if_not_exists\"), \" in one request, then update the child in another. Save as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"fix-parent.mjs\"), \"; the fixture shows the state transition for each request:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-js\"\n  }, \"import { example } from \\\"./setup.mjs\\\"\\n\\n// Seed a missing map, without replacing an existing one.\\nconst initialized = await example({}, {\\n  UpdateExpression: \\\"SET #p = if_not_exists(#p, :empty)\\\",\\n  ExpressionAttributeNames: { \\\"#p\\\": \\\"profile\\\" },\\n  ExpressionAttributeValues: { \\\":empty\\\": {} },\\n})\\nawait example(initialized, {\\n  UpdateExpression: \\\"SET #p.#c = :c\\\",\\n  ExpressionAttributeNames: { \\\"#p\\\": \\\"profile\\\", \\\"#c\\\": \\\"city\\\" },\\n  ExpressionAttributeValues: { \\\":c\\\": \\\"Rome\\\" },\\n})\\n\")), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"profile\\\":{}},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\"}}\\n{\\\"after\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"profile\\\":{\\\"city\\\":\\\"Rome\\\"}},\\\"before\\\":{\\\"pk\\\":\\\"item-1\\\",\\\"profile\\\":{}}}\\n\")), mdx(\"p\", null, \"Those are two operations, not one atomic unit. A concurrent writer can change or remove the map between them; add an appropriate condition and handle failure, or enforce the map's presence in your item schema. Trying to combine parent initialization and child assignment in one expression still produces the overlap error. If replacing the whole Map is intended, assign the complete Map once.\"), mdx(\"h2\", null, \"How update writes are billed\"), mdx(\"p\", null, \"Updating one small attribute does not mean paying only for that attribute. DynamoDB uses the larger item size before or after an \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"UpdateItem\"), \", rounded up in 1 KB units. A change from a 2.2 KB item to a 2.3 KB item therefore uses three ordinary write units; deleting attributes from 2.3 KB down to 0.8 KB still uses three for that operation. A transactional write uses twice the ordinary units. Index maintenance can add write consumption.\"), mdx(\"p\", null, \"Failed conditions can also consume write capacity. Returning attributes does not add a read-capacity charge. AWS documents this in \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/read-write-operations.html\"\n  }, \"read and write operation accounting\"), \". Use \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ReturnConsumedCapacity: \\\"INDEXES\\\"\"), \" to observe table and index consumption on service requests; DynamoDB Local is not a production billing benchmark.\"), mdx(\"h2\", null, \"Expression and item limits\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Constraint\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Limit\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Each expression string\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"4 KB\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Each substitution token, such as \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"#name\"), \" or \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \":value\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"255 bytes\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Total expression substitution variables\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"2 MB\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Operators and functions per expression\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"300\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Document-path dereferences\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"32\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Item size after the update\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"400 KB, including names and values\")))), mdx(\"p\", null, \"These are fixed constraints in the \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Constraints.html\"\n  }, \"AWS constraints reference\"), \" and \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.Attributes.html\"\n  }, \"document-path rules\"), \". The token limit does not cap a stored String value at 255 bytes. The operator limit covers condition, filter and update expressions; assignment \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"=\"), \" inside \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SET\"), \" does not count, while arithmetic, functions and condition comparisons do. A generated patch must fit all applicable limits.\"), mdx(\"h2\", null, \"Can DynamoDB update many items with one expression?\"), mdx(\"p\", null, \"An \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"UpdateExpression\"), \" addresses one item. DynamoDB has no arbitrary multi-item \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"UPDATE ... WHERE\"), \" operation. Even \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ql-reference.update.html\"\n  }, \"PartiQL UPDATE\"), \" must resolve to one primary key.\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Need\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Approach\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Change independent known keys\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Issue individual \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"UpdateItem\"), \" requests with bounded parallelism and per-item error handling\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Find matching items first\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Query a suitable key/index, paginate the result, then update each full key; recheck mutable predicates with a condition\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Send several PartiQL updates together\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"BatchExecuteStatement\"), \", up to 25 write statements; inspect each result for errors\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Make several updates succeed together\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"TransactWriteItems\"), \", up to 100 distinct items and 4 MB, within one account and Region\")))), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"BatchWriteItem\"), \" supports puts and deletes, not updates. The \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-batch-write-update-delete/\"\n  }, \"batch write and update guide\"), \" covers those differences. Use \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-transactions/\"\n  }, \"transactions\"), \" when partial completion would break an invariant; do not include two actions for the same item in one transaction. The \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchExecuteStatement.html\"\n  }, \"batch statement API\"), \" and \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html\"\n  }, \"transaction API\"), \" specify these boundaries.\"), mdx(\"p\", null, \"For an occasional manual correction, \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-bulk-edit/\"\n  }, \"Dynobase bulk editing\"), \" provides bulk edits, undo and item history without writing a custom patch script.\"), mdx(\"p\", null, \"When finished with the examples, stop the disposable container with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"docker stop dynamodb-local\"), \"; its in-memory table disappears.\"), mdx(FAQ, {\n    questions: [{\n      question: \"What is a DynamoDB UpdateExpression?\",\n      answer: \"It is the expression passed to UpdateItem to change attributes on one item. It can contain SET, REMOVE, ADD and DELETE clauses, each at most once, with comma-separated actions within a clause.\"\n    }, {\n      question: \"How do I append to a DynamoDB list if it does not exist?\",\n      answer: \"Use SET with list_append(if_not_exists(path, :empty), :new), where :empty and :new are Lists. For a nested path, its parent Map must already exist.\"\n    }, {\n      question: \"Can DynamoDB update a nested map without replacing it?\",\n      answer: \"Yes. SET a nested path, using a separate name alias for each path segment. Sibling attributes remain unchanged, but every intermediate parent must exist with the correct type.\"\n    }, {\n      question: \"Does UpdateItem create an item if it does not exist?\",\n      answer: \"Yes, when the expression can be evaluated on an absent item. Add a ConditionExpression checking attribute_exists on the partition-key attribute to require an existing item.\"\n    }, {\n      question: \"What is the difference between REMOVE and DELETE?\",\n      answer: \"REMOVE deletes an attribute or a List element. DELETE subtracts members from a top-level Set. Neither clause deletes the whole item; use DeleteItem for that.\"\n    }, {\n      question: \"Can I update multiple items with one UpdateExpression?\",\n      answer: \"No. Each UpdateItem needs one complete primary key. Use bounded parallel updates for independent changes or TransactWriteItems for changes that must succeed together; BatchWriteItem does not support updates.\"\n    }],\n    mdxType: \"FAQ\"\n  }));\n}\n;\nMDXContent.isMDXComponent = true;","frontmatter":{"publishedDate":"2026-10-23","updatedDate":"2026-10-23","path":"/dynamodb-update-expression/","metaTitle":"DynamoDB UpdateExpression: Examples and Error Fixes","metaDescription":"Use SET, REMOVE, ADD and DELETE with tested DynamoDB update expressions, nested maps, lists, conditional writes, return values and dynamic builders.","title":"DynamoDB update expressions: SET, REMOVE, ADD and DELETE","authorName":"Rafal Wilinski","authorLink":"https://x.com/rafalwilinski","authorImage":"/images/authors/rafal-wilinski.jpg","image":"/images/posts/dynamodb-update-expression/social.png"}}},"pageContext":{}},
    "staticQueryHashes": ["3649515864"]}