#!/usr/bin/env bats # DynamoDB tests setup() { load 'test_helper/common-setup' TABLE_NAME="bats-test-table-$(unique_name)" } teardown() { aws_cmd dynamodb delete-table --table-name "$TABLE_NAME" >/dev/null 2>&1 || true } @test "DynamoDB: create table" { run aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \ --billing-mode PAY_PER_REQUEST assert_success status=$(json_get "$output" '.TableDescription.TableStatus') [ "$status" = "ACTIVE" ] || [ "$status" = "CREATING" ] } @test "DynamoDB: describe table" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME" assert_success name=$(json_get "$output" '.Table.TableName') [ "$name" = "$TABLE_NAME" ] } @test "DynamoDB: describe table by ARN" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" table_arn="arn:aws:dynamodb:${AWS_REGION:-us-east-1}:000000000000:table/$TABLE_NAME" run aws_cmd dynamodb describe-table --table-name "$table_arn" assert_success name=$(json_get "$output" '.Table.TableName') [ "$name" = "$TABLE_NAME" ] } @test "DynamoDB: list tables" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" run aws_cmd dynamodb list-tables assert_success found=$(echo "$output" | jq --arg name "$TABLE_NAME" '.TableNames | any(. == $name)') [ "$found" = "true" ] } @test "DynamoDB: put and get item" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" run aws_cmd dynamodb put-item \ --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"user#1"},"name":{"S":"Alice"}}' assert_success run aws_cmd dynamodb get-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"user#1"}}' assert_success name=$(json_get "$output" '.Item.name.S') [ "$name" = "Alice" ] } @test "DynamoDB: update item" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" aws_cmd dynamodb put-item \ --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"user#1"},"name":{"S":"Alice"}}' >/dev/null run aws_cmd dynamodb update-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"user#1"}}' \ --update-expression 'SET #n = :v' \ --expression-attribute-names '{"#n":"name"}' \ --expression-attribute-values '{":v":{"S":"Bob"}}' assert_success run aws_cmd dynamodb get-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"user#1"}}' assert_success name=$(json_get "$output" '.Item.name.S') [ "$name" = "Bob" ] } @test "DynamoDB: ADD resolves nested placeholder paths" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" aws_cmd dynamodb put-item \ --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-1"},"functionInvocationSummaries":{"M":{"ISA_10136":{"M":{"eventsAvailable":{"N":"0"}}}}}}' >/dev/null run aws_cmd dynamodb update-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"item-1"}}' \ --update-expression 'ADD #fis.#isa.#ea :one' \ --expression-attribute-names '{"#fis":"functionInvocationSummaries","#isa":"ISA_10136","#ea":"eventsAvailable"}' \ --expression-attribute-values '{":one":{"N":"1"}}' assert_success run aws_cmd dynamodb get-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"item-1"}}' assert_success nested_counter=$(json_get "$output" '.Item.functionInvocationSummaries.M.ISA_10136.M.eventsAvailable.N') [ "$nested_counter" = "1" ] phantom_attribute=$(echo "$output" | jq '.Item | has("#fis.#isa.#ea")') [ "$phantom_attribute" = "false" ] } @test "DynamoDB: DELETE resolves nested placeholder paths" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" aws_cmd dynamodb put-item \ --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-1"},"settings":{"M":{"labels":{"SS":["keep","drop"]}}}}' >/dev/null run aws_cmd dynamodb update-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"item-1"}}' \ --update-expression 'DELETE #settings.#labels :labels' \ --expression-attribute-names '{"#settings":"settings","#labels":"labels"}' \ --expression-attribute-values '{":labels":{"SS":["drop"]}}' assert_success run aws_cmd dynamodb get-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"item-1"}}' assert_success labels=$(echo "$output" | jq -c '.Item.settings.M.labels.SS') [ "$labels" = '["keep"]' ] phantom_attribute=$(echo "$output" | jq '.Item | has("#settings.#labels")') [ "$phantom_attribute" = "false" ] run aws_cmd dynamodb update-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"item-1"}}' \ --update-expression 'DELETE #settings.#labels :labels' \ --expression-attribute-names '{"#settings":"settings","#labels":"labels"}' \ --expression-attribute-values '{":labels":{"SS":["keep"]}}' assert_success run aws_cmd dynamodb get-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"item-1"}}' assert_success nested_attribute=$(echo "$output" | jq '.Item.settings.M | has("labels")') [ "$nested_attribute" = "false" ] run aws_cmd dynamodb update-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"item-1"}}' \ --update-expression 'DELETE #settings.#labels :labels' \ --expression-attribute-names '{"#settings":"settings","#labels":"labels"}' \ --expression-attribute-values '{":labels":{"SS":["missing"]}}' assert_success } @test "DynamoDB: scan table" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" aws_cmd dynamodb put-item --table-name "$TABLE_NAME" --item '{"pk":{"S":"user#1"},"name":{"S":"Alice"}}' >/dev/null aws_cmd dynamodb put-item --table-name "$TABLE_NAME" --item '{"pk":{"S":"user#2"},"name":{"S":"Bob"}}' >/dev/null run aws_cmd dynamodb scan --table-name "$TABLE_NAME" assert_success count=$(json_get "$output" '.Count') [ "$count" -ge 2 ] } @test "DynamoDB: query table" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" aws_cmd dynamodb put-item --table-name "$TABLE_NAME" --item '{"pk":{"S":"user#1"},"sk":{"S":"profile"},"name":{"S":"Alice"}}' >/dev/null run aws_cmd dynamodb query \ --table-name "$TABLE_NAME" \ --key-condition-expression 'pk = :pk' \ --expression-attribute-values '{":pk":{"S":"user#1"}}' assert_success count=$(json_get "$output" '.Count') [ "$count" -ge 1 ] } @test "DynamoDB: delete item" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" aws_cmd dynamodb put-item --table-name "$TABLE_NAME" --item '{"pk":{"S":"user#1"}}' >/dev/null run aws_cmd dynamodb delete-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"user#1"}}' assert_success } @test "DynamoDB: delete table" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" run aws_cmd dynamodb delete-table --table-name "$TABLE_NAME" assert_success } @test "DynamoDB: update and describe continuous backups" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" run aws_cmd dynamodb describe-continuous-backups --table-name "$TABLE_NAME" assert_success status=$(json_get "$output" '.ContinuousBackupsDescription.ContinuousBackupsStatus') [ "$status" = "ENABLED" ] pitr_status=$(json_get "$output" '.ContinuousBackupsDescription.PointInTimeRecoveryDescription.PointInTimeRecoveryStatus') [ "$pitr_status" = "DISABLED" ] missing_period=$(echo "$output" | jq '.ContinuousBackupsDescription.PointInTimeRecoveryDescription | has("RecoveryPeriodInDays")') [ "$missing_period" = "false" ] run aws_cmd dynamodb update-continuous-backups \ --table-name "$TABLE_NAME" \ --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true assert_success updated_status=$(json_get "$output" '.ContinuousBackupsDescription.PointInTimeRecoveryDescription.PointInTimeRecoveryStatus') [ "$updated_status" = "ENABLED" ] recovery_period=$(json_get "$output" '.ContinuousBackupsDescription.PointInTimeRecoveryDescription.RecoveryPeriodInDays') [ "$recovery_period" = "35" ] } # --- DynamoDB GSI/LSI Tests --- @test "DynamoDB: create table with GSI and LSI" { run aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions \ AttributeName=pk,AttributeType=S \ AttributeName=sk,AttributeType=S \ AttributeName=gsiPk,AttributeType=S \ AttributeName=lsiSk,AttributeType=S \ --key-schema \ AttributeName=pk,KeyType=HASH \ AttributeName=sk,KeyType=RANGE \ --global-secondary-indexes \ 'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \ --local-secondary-indexes \ 'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \ --billing-mode PAY_PER_REQUEST assert_success } @test "DynamoDB: GSI count is 1" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions \ AttributeName=pk,AttributeType=S \ AttributeName=sk,AttributeType=S \ AttributeName=gsiPk,AttributeType=S \ AttributeName=lsiSk,AttributeType=S \ --key-schema \ AttributeName=pk,KeyType=HASH \ AttributeName=sk,KeyType=RANGE \ --global-secondary-indexes \ 'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \ --local-secondary-indexes \ 'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME" assert_success count=$(echo "$output" | jq '.Table.GlobalSecondaryIndexes | length') [ "$count" = "1" ] } @test "DynamoDB: GSI name is gsi-1" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions \ AttributeName=pk,AttributeType=S \ AttributeName=sk,AttributeType=S \ AttributeName=gsiPk,AttributeType=S \ AttributeName=lsiSk,AttributeType=S \ --key-schema \ AttributeName=pk,KeyType=HASH \ AttributeName=sk,KeyType=RANGE \ --global-secondary-indexes \ 'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \ --local-secondary-indexes \ 'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME" assert_success name=$(echo "$output" | jq -r '.Table.GlobalSecondaryIndexes[0].IndexName') [ "$name" = "gsi-1" ] } @test "DynamoDB: GSI projection is ALL" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions \ AttributeName=pk,AttributeType=S \ AttributeName=sk,AttributeType=S \ AttributeName=gsiPk,AttributeType=S \ AttributeName=lsiSk,AttributeType=S \ --key-schema \ AttributeName=pk,KeyType=HASH \ AttributeName=sk,KeyType=RANGE \ --global-secondary-indexes \ 'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \ --local-secondary-indexes \ 'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME" assert_success proj=$(echo "$output" | jq -r '.Table.GlobalSecondaryIndexes[0].Projection.ProjectionType') [ "$proj" = "ALL" ] } @test "DynamoDB: LSI count is 1" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions \ AttributeName=pk,AttributeType=S \ AttributeName=sk,AttributeType=S \ AttributeName=gsiPk,AttributeType=S \ AttributeName=lsiSk,AttributeType=S \ --key-schema \ AttributeName=pk,KeyType=HASH \ AttributeName=sk,KeyType=RANGE \ --global-secondary-indexes \ 'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \ --local-secondary-indexes \ 'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME" assert_success count=$(echo "$output" | jq '.Table.LocalSecondaryIndexes | length') [ "$count" = "1" ] } @test "DynamoDB: LSI name is lsi-1" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions \ AttributeName=pk,AttributeType=S \ AttributeName=sk,AttributeType=S \ AttributeName=gsiPk,AttributeType=S \ AttributeName=lsiSk,AttributeType=S \ --key-schema \ AttributeName=pk,KeyType=HASH \ AttributeName=sk,KeyType=RANGE \ --global-secondary-indexes \ 'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \ --local-secondary-indexes \ 'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME" assert_success name=$(echo "$output" | jq -r '.Table.LocalSecondaryIndexes[0].IndexName') [ "$name" = "lsi-1" ] } @test "DynamoDB: LSI projection is KEYS_ONLY" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions \ AttributeName=pk,AttributeType=S \ AttributeName=sk,AttributeType=S \ AttributeName=gsiPk,AttributeType=S \ AttributeName=lsiSk,AttributeType=S \ --key-schema \ AttributeName=pk,KeyType=HASH \ AttributeName=sk,KeyType=RANGE \ --global-secondary-indexes \ 'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \ --local-secondary-indexes \ 'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" run aws_cmd dynamodb describe-table --table-name "$TABLE_NAME" assert_success proj=$(echo "$output" | jq -r '.Table.LocalSecondaryIndexes[0].Projection.ProjectionType') [ "$proj" = "KEYS_ONLY" ] } @test "DynamoDB: query GSI sparse index" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions \ AttributeName=pk,AttributeType=S \ AttributeName=sk,AttributeType=S \ AttributeName=gsiPk,AttributeType=S \ --key-schema \ AttributeName=pk,KeyType=HASH \ AttributeName=sk,KeyType=RANGE \ --global-secondary-indexes \ 'IndexName=gsi-1,KeySchema=[{AttributeName=gsiPk,KeyType=HASH},{AttributeName=sk,KeyType=RANGE}],Projection={ProjectionType=ALL}' \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" # Put 2 items with gsiPk aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-1"},"sk":{"S":"rev-1"},"gsiPk":{"S":"group-A"}}' >/dev/null aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-2"},"sk":{"S":"rev-1"},"gsiPk":{"S":"group-A"}}' >/dev/null # Put 1 item without gsiPk (sparse) aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-3"},"sk":{"S":"rev-1"},"data":{"S":"no-gsi"}}' >/dev/null # Query GSI - should return only the 2 items with gsiPk run aws_cmd dynamodb query \ --table-name "$TABLE_NAME" \ --index-name gsi-1 \ --key-condition-expression 'gsiPk = :gpk' \ --expression-attribute-values '{":gpk":{"S":"group-A"}}' assert_success count=$(json_get "$output" '.Count') [ "$count" = "2" ] } @test "DynamoDB: query LSI" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions \ AttributeName=pk,AttributeType=S \ AttributeName=sk,AttributeType=S \ AttributeName=lsiSk,AttributeType=S \ --key-schema \ AttributeName=pk,KeyType=HASH \ AttributeName=sk,KeyType=RANGE \ --local-secondary-indexes \ 'IndexName=lsi-1,KeySchema=[{AttributeName=pk,KeyType=HASH},{AttributeName=lsiSk,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}' \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-1"},"sk":{"S":"rev-1"},"lsiSk":{"S":"2024-01-01"}}' >/dev/null aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-1"},"sk":{"S":"rev-2"},"lsiSk":{"S":"2024-01-02"}}' >/dev/null # Query LSI with range condition run aws_cmd dynamodb query \ --table-name "$TABLE_NAME" \ --index-name lsi-1 \ --key-condition-expression 'pk = :pk AND lsiSk > :d' \ --expression-attribute-values '{":pk":{"S":"item-1"},":d":{"S":"2024-01-00"}}' assert_success count=$(json_get "$output" '.Count') [ "$count" = "2" ] } # --- DynamoDB FilterExpression Tests --- @test "DynamoDB: query with FilterExpression" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" # Put items with different status values aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"user#1"},"sk":{"S":"order#1"},"status":{"S":"pending"}}' >/dev/null aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"user#1"},"sk":{"S":"order#2"},"status":{"S":"complete"}}' >/dev/null aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"user#1"},"sk":{"S":"order#3"},"status":{"S":"pending"}}' >/dev/null # Query with FilterExpression to get only pending run aws_cmd dynamodb query \ --table-name "$TABLE_NAME" \ --key-condition-expression 'pk = :pk' \ --filter-expression '#s = :status' \ --expression-attribute-names '{"#s":"status"}' \ --expression-attribute-values '{":pk":{"S":"user#1"},":status":{"S":"pending"}}' assert_success count=$(json_get "$output" '.Count') [ "$count" = "2" ] } @test "DynamoDB: query FilterExpression with limit" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" # Put multiple items for i in 1 2 3 4 5; do aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item "{\"pk\":{\"S\":\"user#1\"},\"sk\":{\"S\":\"order#$i\"},\"status\":{\"S\":\"pending\"}}" >/dev/null done # Query with limit run aws_cmd dynamodb query \ --table-name "$TABLE_NAME" \ --key-condition-expression 'pk = :pk' \ --expression-attribute-values '{":pk":{"S":"user#1"}}' \ --limit 2 assert_success count=$(json_get "$output" '.Count') [ "$count" = "2" ] } @test "DynamoDB: query FilterExpression pagination" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" # Put multiple items for i in 1 2 3 4 5; do aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item "{\"pk\":{\"S\":\"user#1\"},\"sk\":{\"S\":\"order#$i\"},\"status\":{\"S\":\"pending\"}}" >/dev/null done # First page run aws_cmd dynamodb query \ --table-name "$TABLE_NAME" \ --key-condition-expression 'pk = :pk' \ --expression-attribute-values '{":pk":{"S":"user#1"}}' \ --limit 2 assert_success # Check that LastEvaluatedKey exists for pagination lek=$(echo "$output" | jq '.LastEvaluatedKey') [ "$lek" != "null" ] # Get next page run aws_cmd dynamodb query \ --table-name "$TABLE_NAME" \ --key-condition-expression 'pk = :pk' \ --expression-attribute-values '{":pk":{"S":"user#1"}}' \ --limit 2 \ --exclusive-start-key "$lek" assert_success count=$(json_get "$output" '.Count') [ "$count" = "2" ] } # --- DynamoDB Advanced Filter Tests --- @test "DynamoDB: scan with contains on List" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-1"},"tags":{"L":[{"S":"foo"},{"S":"bar"}]}}' >/dev/null aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-2"},"tags":{"L":[{"S":"baz"},{"S":"qux"}]}}' >/dev/null run aws_cmd dynamodb scan \ --table-name "$TABLE_NAME" \ --filter-expression 'contains(tags, :tag)' \ --expression-attribute-values '{":tag":{"S":"foo"}}' assert_success count=$(json_get "$output" '.Count') [ "$count" = "1" ] } @test "DynamoDB: scan with BOOL filter" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-1"},"active":{"BOOL":true}}' >/dev/null aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-2"},"active":{"BOOL":false}}' >/dev/null run aws_cmd dynamodb scan \ --table-name "$TABLE_NAME" \ --filter-expression 'active = :val' \ --expression-attribute-values '{":val":{"BOOL":true}}' assert_success count=$(json_get "$output" '.Count') [ "$count" = "1" ] } @test "DynamoDB: scan with attribute_exists on nested Map" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-1"},"meta":{"M":{"created":{"S":"2024-01-01"}}}}' >/dev/null aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-2"},"other":{"S":"data"}}' >/dev/null run aws_cmd dynamodb scan \ --table-name "$TABLE_NAME" \ --filter-expression 'attribute_exists(meta.created)' assert_success count=$(json_get "$output" '.Count') [ "$count" = "1" ] } # --- DynamoDB Kinesis Streaming Destination Tests --- @test "DynamoDB: enable kinesis streaming destination" { STREAM_NAME="bats-kinesis-$(unique_name)" aws_cmd kinesis create-stream --stream-name "$STREAM_NAME" --shard-count 1 >/dev/null aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES >/dev/null ddb_wait_table "$TABLE_NAME" STREAM_ARN=$(aws_cmd kinesis describe-stream-summary --stream-name "$STREAM_NAME" | jq -r '.StreamDescriptionSummary.StreamARN') run aws_cmd dynamodb enable-kinesis-streaming-destination \ --table-name "$TABLE_NAME" \ --stream-arn "$STREAM_ARN" assert_success status=$(json_get "$output" '.DestinationStatus') [ "$status" = "ACTIVE" ] aws_cmd kinesis delete-stream --stream-name "$STREAM_NAME" >/dev/null 2>&1 || true } @test "DynamoDB: describe kinesis streaming destination" { STREAM_NAME="bats-kinesis-$(unique_name)" aws_cmd kinesis create-stream --stream-name "$STREAM_NAME" --shard-count 1 >/dev/null aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES >/dev/null ddb_wait_table "$TABLE_NAME" STREAM_ARN=$(aws_cmd kinesis describe-stream-summary --stream-name "$STREAM_NAME" | jq -r '.StreamDescriptionSummary.StreamARN') aws_cmd dynamodb enable-kinesis-streaming-destination \ --table-name "$TABLE_NAME" \ --stream-arn "$STREAM_ARN" >/dev/null run aws_cmd dynamodb describe-kinesis-streaming-destination \ --table-name "$TABLE_NAME" assert_success count=$(echo "$output" | jq '.KinesisDataStreamDestinations | length') [ "$count" = "1" ] dest_status=$(echo "$output" | jq -r '.KinesisDataStreamDestinations[0].DestinationStatus') [ "$dest_status" = "ACTIVE" ] aws_cmd kinesis delete-stream --stream-name "$STREAM_NAME" >/dev/null 2>&1 || true } @test "DynamoDB: disable kinesis streaming destination" { STREAM_NAME="bats-kinesis-$(unique_name)" aws_cmd kinesis create-stream --stream-name "$STREAM_NAME" --shard-count 1 >/dev/null aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES >/dev/null ddb_wait_table "$TABLE_NAME" STREAM_ARN=$(aws_cmd kinesis describe-stream-summary --stream-name "$STREAM_NAME" | jq -r '.StreamDescriptionSummary.StreamARN') aws_cmd dynamodb enable-kinesis-streaming-destination \ --table-name "$TABLE_NAME" \ --stream-arn "$STREAM_ARN" >/dev/null run aws_cmd dynamodb disable-kinesis-streaming-destination \ --table-name "$TABLE_NAME" \ --stream-arn "$STREAM_ARN" assert_success status=$(json_get "$output" '.DestinationStatus') [ "$status" = "DISABLED" ] aws_cmd kinesis delete-stream --stream-name "$STREAM_NAME" >/dev/null 2>&1 || true } @test "DynamoDB: scan with contains on String Set" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-1"},"roles":{"SS":["admin","user"]}}' >/dev/null aws_cmd dynamodb put-item --table-name "$TABLE_NAME" \ --item '{"pk":{"S":"item-2"},"roles":{"SS":["guest"]}}' >/dev/null run aws_cmd dynamodb scan \ --table-name "$TABLE_NAME" \ --filter-expression 'contains(#r, :role)' \ --expression-attribute-names '{"#r":"roles"}' \ --expression-attribute-values '{":role":{"S":"admin"}}' assert_success count=$(json_get "$output" '.Count') [ "$count" = "1" ] } # --- DynamoDB REMOVE nested map key tests (GH #402) --- @test "DynamoDB: REMOVE key from nested map" { aws_cmd dynamodb create-table \ --table-name "$TABLE_NAME" \ --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \ --key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \ --billing-mode PAY_PER_REQUEST >/dev/null ddb_wait_table "$TABLE_NAME" # Set a map attribute with a key aws_cmd dynamodb update-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"user1"},"sk":{"S":"sort1"}}' \ --update-expression 'SET ratings = :ratings' \ --expression-attribute-values '{":ratings":{"M":{"foo":{"S":"5"},"bar":{"S":"3"}}}}' >/dev/null # REMOVE ratings.foo aws_cmd dynamodb update-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"user1"},"sk":{"S":"sort1"}}' \ --update-expression 'REMOVE ratings.foo' >/dev/null # Verify foo is removed but bar remains run aws_cmd dynamodb get-item \ --table-name "$TABLE_NAME" \ --key '{"pk":{"S":"user1"},"sk":{"S":"sort1"}}' assert_success foo=$(echo "$output" | jq -r '.Item.ratings.M.foo // "null"') bar=$(echo "$output" | jq -r '.Item.ratings.M.bar.S') [ "$foo" = "null" ] [ "$bar" = "3" ] }