Skip to content

❌ ValueError: Cannot parse condition starting at: + :delta >= :zero ​

Clase 8 · src/inventory/app.py (y su copia en console_lambda/lambda_function.py) · update_inventory()

🧨 Qué pasó ​

python
result = table.update_item(
    Key={"product_id": product_id},
    UpdateExpression="SET available_stock = available_stock + :delta",
    ConditionExpression=(
        "attribute_exists(product_id) AND "
        "available_stock + :delta >= :zero"
    ),
    ExpressionAttributeValues={
        ":delta": Decimal(delta_stock),
        ":zero": Decimal(0),
    },
    ReturnValues="ALL_NEW",
)

Contra una tabla DynamoDB real (verificado con moto, que emula el servicio real de AWS byte a byte en su parser de expresiones):

ValueError: Cannot parse condition starting at: + :delta >= :zero

🔍 Causa ​

DynamoDB tiene dos lenguajes de expresión distintos, con reglas distintas:

ExpresiónPara qué sirve¿Admite aritmética (+, -)?
UpdateExpressionDescribir qué cambiar en un item (SET, ADD, REMOVE)✅ Sí — SET x = x + :delta es válido
ConditionExpressionDescribir una comparación que debe cumplirse antes de aplicar la operación❌ No — solo compara un path o un value contra otro

available_stock + :delta no es ni un nombre de atributo (path) ni un valor (value) — es una expresión aritmética, y la gramática de ConditionExpression simplemente no la reconoce como operando válido. El parser lee available_stock como un path correcto, y se rompe apenas encuentra el + después.

✅ Solución ​

Calcular el umbral necesario en Python, antes de armar la condición, y comparar el atributo directo contra ese valor ya calculado — toda la aritmética queda dentro del UpdateExpression (donde sí es válida), nunca dentro del ConditionExpression:

python
result = table.update_item(
    Key={"product_id": product_id},
    UpdateExpression="SET available_stock = available_stock + :delta",   # la suma va acá
    ConditionExpression=(
        "attribute_exists(product_id) AND "
        "available_stock >= :min_needed"       # acá solo comparación directa
    ),
    ExpressionAttributeValues={
        ":delta": Decimal(delta_stock),
        # si delta es negativo, hace falta AL MENOS esa cantidad disponible;
        # si delta es positivo (sumar stock), la condición es siempre verdadera
        ":min_needed": Decimal(-delta_stock) if delta_stock < 0 else Decimal(0),
    },
    ReturnValues="ALL_NEW",
)

Verificado con moto simulando la tabla real: delta_stock=-4 sobre available_stock=20 actualiza a 16 (200 OK); delta_stock=-999 sobre el mismo item lanza ConditionalCheckFailedException (409, como se espera); delta_stock=5 (sumar stock) siempre pasa.

💡 Por qué reserve_inventory (la otra operación condicional del mismo archivo) nunca tuvo este bug: su condición ya era "attribute_exists(product_id) AND available_stock >= :q" — comparaba el atributo directo contra un valor, sin sumarle nada adentro de la expresión. El problema es específico de mezclar una resta/suma DENTRO del ConditionExpression.

⚠️ Este mismo patrón está duplicado en dos archivos del proyecto de la sesión (src/inventory/app.py y console_lambda/lambda_function.py) — ver Clase 8 → sección 8 sobre por qué mantener dos copias del mismo código facilita que un bug corregido en un lado quede sin corregir en el otro.

✅ Confirmado en AWS real (no solo simulado) ​

El fix se aplicó al src/inventory/app.py real del repo de la sesión, y Styp lo desplegó con sam build && sam deploy en su propia cuenta AWS (us-east-2). La cadena completa contra la URL pública real del API Gateway dio exactamente lo esperado:

bash
curl -X POST .../inventory -d '{"product_id":"PROD-001","product_name":"Laptop empresarial","available_stock":20}'
# → 201, available_stock: 20, reserved_stock: 0

curl -X PATCH .../inventory/PROD-001 -d '{"delta_stock": -4}'
# → 200, available_stock: 16   ← antes del fix, esto daba el error de sintaxis

curl -X PATCH .../inventory/PROD-001/reserve -d '{"quantity": 3}'
# → 200, available_stock: 13, reserved_stock: 3

console_lambda/lambda_function.py (la copia de consola, sección 8) sigue con el bug sin corregir a propósito — sigue siendo el Ejercicio 19 de la Clase 8 (aplicar el mismo fix ahí, como práctica).

📎 Apuntes relacionados ​