Files
scylladb/test/alternator/test_write_isolation.py
Avi Kivity 0ae22a09d4 LICENSE: Update to version 1.1
Updated terms of non-commercial use (must be a never-customer).
2026-04-12 19:46:33 +03:00

483 lines
26 KiB
Python

# Copyright 2025-present ScyllaDB
#
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.1
# Tests for the operations allowed under the four different write isolation
# modes describe in docs/alternator/new-apis.md. We only test here which
# operations are allowed or not allowed in each write isolation mode -
# we do NOT test here the actual isolation provided in these modes between
# different concurrent writes! We will need to check that in a different
# test framework that is allows testing Alternator concurrency and
# consistency (see suggestion in #6350).
#
# We also don't check here the various corner cases of the operation to *set*
# the write isolation - this is done using tags and tested in test_tag.py.
#
# The four write isolation modes fall into two types:
#
# 1. The mode `forbid_rmw` forbids any type of write operation which requires
# a read before the write (a.k.a. read-modify-write or RMW).
# As we'll test below, the forbidden operations include writes with
# conditions, certain update operations (but not all), and writes returning
# pre-write values. We need to check what kinds of writes are forbidden,
# but also what is not forbidden.
#
# 2. The modes `always_use_lwt`, `only_rmw_uses_lwt`, and `unsafe_rmw`
# allow any write operation, including those that need a read-before-write.
# Below we'll call these three modes "permit rmw" modes.
#
# The result of each allowed operation is the same in all modes that allow
# it (remember we don't check concurrent updates). We want to verify this
# because operations have slightly different code paths for different modes.
# However, note that it is only necessary to check here major feature
# interaction - for example PutItem with or without a ConditionExpression -
# we don't need to check every kind of syntax in ConditionExpression
# because we already test this elsewhere (test_condition_expression.py).
# We don't expect Alternator's handling of different ConditionExpression
# operators to depend on any way on the write isolation mode. Conversely,
# as we'll see below, different UpdateExpressions features do need - or
# don't need - a read before write, so we'll need to check those specific
# UpdateExpressions.
#
# As the name "write isolation" suggests, Alternator guarantees that the
# write isolation mode only affects *writes* - so we don't need to test here
# its effect on read operations. This is not entirely obvious - when we
# started working on Alternator, we thought we might need to use LWT for
# reads as well. But at the end, we decided not to, and the name "write
# isolation mode" now guarantees that it indeed affects only writes.
# Reads are the same in all four modes, and do not use LWT, and we don't
# need to test them here.
#############################################################################
import pytest
from botocore.exceptions import ClientError
from .util import create_test_table, random_string, new_test_table
@pytest.fixture(scope="function", autouse=True)
def all_tests_are_scylla_only(scylla_only):
pass
# The error that RMW (read-modify-write) operations return in forbid_rmw
# write isolation mode:
rmw_forbidden='ValidationException.*write isolation policy'
# The "table_*" fixtures are the same as test_table_s (table with a string
# hash key), except with each a different write isolation mode.
# Because write isolation modes are a Scylla-only feature, these fixtures
# are marked scylla_only, so all tests that use one of them are skipped
# when not running against Scylla.
@pytest.fixture(scope='module')
def table_forbid_rmw(dynamodb, scylla_only):
table = create_test_table(dynamodb,
Tags=[{'Key': 'system:write_isolation', 'Value': 'forbid_rmw'}],
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, ],
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ])
yield table
table.delete()
@pytest.fixture(scope='module')
def table_always_use_lwt(dynamodb, scylla_only):
table = create_test_table(dynamodb,
Tags=[{'Key': 'system:write_isolation', 'Value': 'always_use_lwt'}],
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, ],
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ])
yield table
table.delete()
@pytest.fixture(scope='module')
def table_only_rmw_uses_lwt(dynamodb, scylla_only):
table = create_test_table(dynamodb,
Tags=[{'Key': 'system:write_isolation', 'Value': 'only_rmw_uses_lwt'}],
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, ],
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ])
yield table
table.delete()
@pytest.fixture(scope='module')
def table_unsafe_rmw(dynamodb, scylla_only):
table = create_test_table(dynamodb,
Tags=[{'Key': 'system:write_isolation', 'Value': 'unsafe_rmw'}],
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, ],
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ])
yield table
table.delete()
# "permit rwm" write isolation modes are the three modes besides forbid_rmw.
# These three modes permit all read-modify-write operations. Although these
# modes may isolate concurrent writes differently, there is no difference
# between them when writes are not concurrent.
@pytest.fixture(scope='module')
def tables_permit_rmw(table_always_use_lwt, table_only_rmw_uses_lwt, table_unsafe_rmw):
yield [table_always_use_lwt, table_only_rmw_uses_lwt, table_unsafe_rmw]
#############################################################################
# "ConditionExpression" tests.
# ConditionExpression applies to PutItem, DeleteItem and UpdateItem.
# Without ConditionExpression, these operations work on all modes. With
# ConditionExpression, they work on the three permit modes, and fail on
# the forbid mode.
# PutItem & ConditionExpression:
def test_isolation_putitem_conditionexpression(table_forbid_rmw, tables_permit_rmw):
# Without ConditionExpression, PutItem works correctly on all modes
p = random_string()
for table in tables_permit_rmw + [table_forbid_rmw]:
table.put_item(Item={'p': p, 'a': 1})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
# With ConditionExpression, PutItem works correctly on permit modes,
# fails on forbid mode.
for table in tables_permit_rmw:
table.put_item(Item={'p': p, 'a': 2},
ConditionExpression='a = :oldval',
ExpressionAttributeValues={':oldval': 1})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2}
with pytest.raises(ClientError, match=rmw_forbidden):
table_forbid_rmw.put_item(Item={'p': p, 'a': 2},
ConditionExpression='a = :oldval',
ExpressionAttributeValues={':oldval': 1})
# DeleteItem & ConditionExpression:
def test_isolation_deleteitem_conditionexpression(table_forbid_rmw, tables_permit_rmw):
# Without ConditionExpression, DeleteItem works correctly on all modes
p = random_string()
for table in tables_permit_rmw + [table_forbid_rmw]:
table.put_item(Item={'p': p, 'a': 1})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
table.delete_item(Key={'p': p})
assert 'Item' not in table.get_item(Key={'p': p}, ConsistentRead=True)
# With ConditionExpression, DeleteItem works correctly on permit modes,
# fails on forbid mode.
for table in tables_permit_rmw:
table.put_item(Item={'p': p, 'a': 1})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
table.delete_item(Key={'p': p},
ConditionExpression='a = :oldval',
ExpressionAttributeValues={':oldval': 1})
assert 'Item' not in table.get_item(Key={'p': p}, ConsistentRead=True)
with pytest.raises(ClientError, match=rmw_forbidden):
table_forbid_rmw.delete_item(Key={'p': p},
ConditionExpression='a = :oldval',
ExpressionAttributeValues={':oldval': 1})
# UpdateItem & ConditionExpression:
# Note that an UpdateItem always needs some sort of update expression, so
# we deliberately test with an update expression that doesn't itself need
# a read-before-write (we'll check more elaborate UpdateExpression below).
def test_isolation_updateitem_conditionexpression(table_forbid_rmw, tables_permit_rmw):
# Without ConditionExpression, UpdateItem (with a simple update expression
# not requiring read-before-write) works correctly on all modes
p = random_string()
for table in tables_permit_rmw + [table_forbid_rmw]:
table.update_item(Key={'p': p},
UpdateExpression='SET a = :val',
ExpressionAttributeValues={':val': 1})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
# With ConditionExpression, UpdateItem works correctly on permit modes,
# fails on forbid mode.
for table in tables_permit_rmw:
table.update_item(Key={'p': p},
UpdateExpression='SET a = :newval',
ConditionExpression='a = :oldval',
ExpressionAttributeValues={':oldval': 1, ':newval': 2})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2}
with pytest.raises(ClientError, match=rmw_forbidden):
table_forbid_rmw.update_item(Key={'p': p},
UpdateExpression='SET a = :newval',
ConditionExpression='a = :oldval',
ExpressionAttributeValues={':oldval': 1, ':newval': 2})
#############################################################################
# "Expected" tests. Expected is the old version of ConditionExpression and
# abides by the same rules - so we have very similar tests to those of
# ConditionExpression above. We don't need to test the "without Expected"
# cases, as "without Expected" is the same as "without ConditionExpression" :-)
# PutItem & Expected:
def test_isolation_putitem_expected(table_forbid_rmw, tables_permit_rmw):
# With Expected, PutItem works correctly on permit modes,
# fails on forbid mode.
p = random_string()
for table in tables_permit_rmw:
table.put_item(Item={'p': p, 'a': 1})
table.put_item(Item={'p': p, 'a': 2},
Expected={'a': {'ComparisonOperator': 'EQ', 'AttributeValueList': [1]}})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2}
with pytest.raises(ClientError, match=rmw_forbidden):
table_forbid_rmw.put_item(Item={'p': p, 'a': 2},
Expected={'a': {'ComparisonOperator': 'EQ', 'AttributeValueList': [1]}})
# DeleteItem & Expected:
def test_isolation_deleteitem_expected(table_forbid_rmw, tables_permit_rmw):
# With Expected, DeleteItem works correctly on permit modes,
# fails on forbid mode.
p = random_string()
for table in tables_permit_rmw:
table.put_item(Item={'p': p, 'a': 1})
table.delete_item(Key={'p': p},
Expected={'a': {'ComparisonOperator': 'EQ', 'AttributeValueList': [1]}})
assert 'Item' not in table.get_item(Key={'p': p}, ConsistentRead=True)
with pytest.raises(ClientError, match=rmw_forbidden):
table_forbid_rmw.delete_item(Key={'p': p},
Expected={'a': {'ComparisonOperator': 'EQ', 'AttributeValueList': [1]}})
# UpdateItem & Expected:
def test_isolation_updateitem_expected(table_forbid_rmw, tables_permit_rmw):
# With Expected, UpdateItem works correctly on permit modes,
# fails on forbid mode.
p = random_string()
for table in tables_permit_rmw:
table.put_item(Item={'p': p, 'a': 1})
table.update_item(Key={'p': p},
AttributeUpdates={'a': {'Value': 2, 'Action': 'PUT'}},
Expected={'a': {'ComparisonOperator': 'EQ', 'AttributeValueList': [1]}})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2}
with pytest.raises(ClientError, match=rmw_forbidden):
table_forbid_rmw.update_item(Key={'p': p},
AttributeUpdates={'a': {'Value': 2, 'Action': 'PUT'}},
Expected={'a': {'ComparisonOperator': 'EQ', 'AttributeValueList': [1]}})
#############################################################################
# "UpdateExpression" tests.
# Obviously, only the UpdateItem operation supports UpdateExpression.
# As we'll see now, certain expressions need to read attributes from the
# existing item and require read-modify-write (so don't work on forbid_rmw
# mode), but other expressions don't.
# Test "write only" update expressions, that do not need to read the old
# value of the item, so work on all write isolation modes:
def test_isolation_updateexpression_write_only(table_forbid_rmw, tables_permit_rmw):
p = random_string()
for table in tables_permit_rmw + [table_forbid_rmw]:
table.update_item(Key={'p': p},
UpdateExpression='SET a = :val',
ExpressionAttributeValues={':val': 1})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
table.update_item(Key={'p': p},
UpdateExpression='REMOVE a')
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p}
# Test rmw update expressions, that need to read the old value of the item,
# so work on all write isolation modes except forbid_rmw
def test_isolation_updateexpression_rmw(table_forbid_rmw, tables_permit_rmw):
p = random_string()
for table in tables_permit_rmw:
table.put_item(Item={'p': p, 'a': 1})
# A SET with an attribute in the RHS (right-hand side) needs to read
# that attribute
table.update_item(Key={'p': p},
UpdateExpression='SET a = a + :incr',
ExpressionAttributeValues={':incr': 1})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2}
# An ADD is also a read-modify-write operation
table.update_item(Key={'p': p},
UpdateExpression='ADD a :incr',
ExpressionAttributeValues={':incr': 1})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 3}
# A SET with a top-level attribute in the LHS doesn't need to read
# that attribute (we checked this in the previous test), however
# if the attribute is a document path, we do need to read the old
# item because Scylla needs to read the full top-level attribute,
# modify only a part of it, and write it back.
table.put_item(Item={'p': p, 'a': {'x': 'y'}})
table.update_item(Key={'p': p},
UpdateExpression='SET a.b = :val',
ExpressionAttributeValues={':val': 1})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': {'x': 'y', 'b': 1}}
# A DELETE (removing an element from a set) also requires a read.
table.put_item(Item={'p': p, 'a': set([2, 4, 6])})
table.update_item(Key={'p': p},
UpdateExpression='DELETE a :val',
ExpressionAttributeValues={':val': set([2])})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': set([4,6])}
# Check that the same things don't work in forbid_rmw mode:
table = table_forbid_rmw
table.put_item(Item={'p': p, 'a': 1})
with pytest.raises(ClientError, match=rmw_forbidden):
table.update_item(Key={'p': p},
UpdateExpression='SET a = a + :incr',
ExpressionAttributeValues={':incr': 1})
with pytest.raises(ClientError, match=rmw_forbidden):
table.update_item(Key={'p': p},
UpdateExpression='ADD a :incr',
ExpressionAttributeValues={':incr': 1})
table.put_item(Item={'p': p, 'a': {'x': 'y'}})
with pytest.raises(ClientError, match=rmw_forbidden):
table.update_item(Key={'p': p},
UpdateExpression='SET a.b = :val',
ExpressionAttributeValues={':val': 1})
table.put_item(Item={'p': p, 'a': set([2, 4, 6])})
with pytest.raises(ClientError, match=rmw_forbidden):
table.update_item(Key={'p': p},
UpdateExpression='DELETE a :val',
ExpressionAttributeValues={':val': set([2])})
#############################################################################
# "AttributeUpdates" tests. These are the old version of UpdateExpression,
# and also mix RMW operations (ADD), with non-RWM (PUT, DELETE)
# Test PUT and DELETE updates, that do not need to read the old
# value of the item, so work on all write isolation modes:
def test_isolation_attributeupdates_write_only(table_forbid_rmw, tables_permit_rmw):
p = random_string()
for table in tables_permit_rmw + [table_forbid_rmw]:
table.update_item(Key={'p': p},
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
table.update_item(Key={'p': p},
AttributeUpdates={'a': {'Action': 'DELETE'}})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p}
# Test ADD updates, that need to read the old value of the item,
# so work on all write isolation modes except forbid_rmw
def test_isolation_attributeupdates_rmw(table_forbid_rmw, tables_permit_rmw):
p = random_string()
for table in tables_permit_rmw:
table.put_item(Item={'p': p, 'a': 1})
table.update_item(Key={'p': p},
AttributeUpdates={'a': {'Value': 1, 'Action': 'ADD'}})
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2}
with pytest.raises(ClientError, match=rmw_forbidden):
table_forbid_rmw.update_item(Key={'p': p},
AttributeUpdates={'a': {'Value': 1, 'Action': 'ADD'}})
#############################################################################
# "ReturnValues" tests. ALL_OLD, ALL_NEW, UPDATED_OLD requires RMW,
# but NONE and UPDATED_NEW do not.
# ReturnValues is supported by the PutItem, DeleteItem and UpdateItem
# operations.
# PutItem & ReturnValues:
# PutItem supports only ReturnValues = NONE or ALL_OLD
def test_isolation_putitem_returnvalues(table_forbid_rmw, tables_permit_rmw):
# With ReturnValues=NONE, PutItem works correctly on all modes
p = random_string()
for table in tables_permit_rmw + [table_forbid_rmw]:
table.put_item(Item={'p': p, 'a': 1}, ReturnValues='NONE')
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
# With ReturnValues=ALL_OLD, PutItem works correctly (also returning the
# old item) on permit modes, and fails on forbid mode.
for table in tables_permit_rmw:
ret = table.put_item(Item={'p': p, 'a': 2}, ReturnValues='ALL_OLD')
assert ret['Attributes'] == {'p': p, 'a': 1}
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2}
with pytest.raises(ClientError, match=rmw_forbidden):
table_forbid_rmw.put_item(Item={'p': p, 'a': 2}, ReturnValues='ALL_OLD')
# DeleteItem & ReturnValues:
# DeleteItem supports only ReturnValues = NONE, ALL_OLD
def test_isolation_deleteitem_returnvalues(table_forbid_rmw, tables_permit_rmw):
# With ReturnValues=NONE, DeleteItem works correctly on all modes
p = random_string()
for table in tables_permit_rmw + [table_forbid_rmw]:
table.put_item(Item={'p': p, 'a': 1})
table.delete_item(Key={'p': p}, ReturnValues='NONE')
assert 'Item' not in table.get_item(Key={'p': p}, ConsistentRead=True)
# With ReturnValues=ALL_OLD, DeleteItems works correctly (also returning
# the old item) on permit modes, and fails on forbid mode.
for table in tables_permit_rmw:
table.put_item(Item={'p': p, 'a': 1})
ret = table.delete_item(Key={'p': p}, ReturnValues='ALL_OLD')
assert ret['Attributes'] == {'p': p, 'a': 1}
assert 'Item' not in table.get_item(Key={'p': p}, ConsistentRead=True)
with pytest.raises(ClientError, match=rmw_forbidden):
table_forbid_rmw.delete_item(Key={'p': p}, ReturnValues='ALL_OLD')
# UpdateItem & ReturnValues:
# UpdateItem supports all ReturnValues values - NONE, ALL_OLD, UPDATED_OLD,
# ALL_NEW, and UPDATED_NEW. Some of them requiring rwm, some not:
def test_isolation_updateitem_returnvalues(table_forbid_rmw, tables_permit_rmw):
# With ReturnValues=NONE or UPDATED_NEW, UpdateItem works correctly on
# all modes - there is no need to read the old item so it's not a
# read-modify-write operation.
p = random_string()
for table in tables_permit_rmw + [table_forbid_rmw]:
table.update_item(Key={'p': p},
UpdateExpression='SET a = :val',
ExpressionAttributeValues={':val': 1},
ReturnValues='NONE')
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
ret = table.update_item(Key={'p': p},
UpdateExpression='SET a = :val',
ExpressionAttributeValues={':val': 2},
ReturnValues='UPDATED_NEW')
assert ret['Attributes'] == {'a': 2}
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2}
# All other ReturnValues modes need to read the old item, so work
# correctly on all write isolation modes except forbid:
for table in tables_permit_rmw:
ret = table.update_item(Key={'p': p},
UpdateExpression='SET a = :val',
ExpressionAttributeValues={':val': 3},
ReturnValues='ALL_OLD')
assert ret['Attributes'] == {'p': p, 'a': 2}
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 3}
ret = table.update_item(Key={'p': p},
UpdateExpression='SET a = :val',
ExpressionAttributeValues={':val': 4},
ReturnValues='UPDATED_OLD')
assert ret['Attributes'] == {'a': 3}
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 4}
ret = table.update_item(Key={'p': p},
UpdateExpression='SET a = :val',
ExpressionAttributeValues={':val': 5},
ReturnValues='ALL_NEW')
assert ret['Attributes'] == {'p': p, 'a': 5}
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 5}
for returnvalues in ['ALL_OLD', 'UPDATED_OLD', 'ALL_NEW']:
with pytest.raises(ClientError, match=rmw_forbidden):
table_forbid_rmw.update_item(Key={'p': p},
UpdateExpression='SET a = :val',
ExpressionAttributeValues={':val': 1},
ReturnValues=returnvalues)
#############################################################################
# BatchWriteItem tests.
# BatchWriteItem writes are always pure write - never RMW (read-modify-write)
# operations - because none of the RMW options are supported: Batch writes
# don't support an UpdateExpression, a ConditionExpression or ReturnValues.
# Still, even in the pure write case, the write code paths are different for
# the different write isolation modes, and we need to exercise them.
# For completeness, this test exercises a single batch with more than one
# partition, more than one clustering key in the same partition, and a
# combination of PutRequest and DeleteRequest.
def test_isolation_batchwriteitem(dynamodb):
# Unfortunately we can't use the four table fixtures that all other tests
# use, because those fixtures only have a partition key and we also want
# a sort key (so we can test the case of multiple items in the same
# partition). So we have to create four new tables just for this test.
for mode in ['only_rmw_uses_lwt', 'always_use_lwt', 'unsafe_rmw', 'forbid_rmw']:
with new_test_table(dynamodb,
Tags=[{'Key': 'system:write_isolation', 'Value': mode}],
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' },
{ 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
AttributeDefinitions=[
{ 'AttributeName': 'p', 'AttributeType': 'S' },
{ 'AttributeName': 'c', 'AttributeType': 'S' } ]) as table:
p1 = random_string()
p2 = random_string()
# Set up two items in p1, only one of them will be deleted later
table.put_item(Item={'p': p1, 'c': 'item1', 'x': 'hello'})
assert table.get_item(Key={'p': p1, 'c': 'item1'}, ConsistentRead=True)['Item'] == {'p': p1, 'c': 'item1', 'x': 'hello'}
table.put_item(Item={'p': p1, 'c': 'item2', 'x': 'hi'})
assert table.get_item(Key={'p': p1, 'c': 'item2'}, ConsistentRead=True)['Item'] == {'p': p1, 'c': 'item2', 'x': 'hi'}
# Perform the batch write, writing to two different partitions
# (p1 and p2), multiple items in one partition (p1), and
# one of the writes is a DeleteRequest (of item1 that we wrote
# above).
table.meta.client.batch_write_item(RequestItems = {
table.name: [
{'PutRequest': {'Item': {'p': p1, 'c': 'item3', 'x': 'dog'}}},
{'PutRequest': {'Item': {'p': p1, 'c': 'item4', 'x': 'cat'}}},
{'DeleteRequest': {'Key': {'p': p1, 'c': 'item1'}}},
{'PutRequest': {'Item': {'p': p2, 'c': 'item5', 'x': 'mouse'}}}
]})
# After the batch write, item1 will be gone, item2..item5 should
# exist with the right content.
assert 'Item' not in table.get_item(Key={'p': p1, 'c': 'item1'}, ConsistentRead=True)
assert table.get_item(Key={'p': p1, 'c': 'item2'}, ConsistentRead=True)['Item'] == {'p': p1, 'c': 'item2', 'x': 'hi'}
assert table.get_item(Key={'p': p1, 'c': 'item3'}, ConsistentRead=True)['Item'] == {'p': p1, 'c': 'item3', 'x': 'dog'}
assert table.get_item(Key={'p': p1, 'c': 'item4'}, ConsistentRead=True)['Item'] == {'p': p1, 'c': 'item4', 'x': 'cat'}
assert table.get_item(Key={'p': p2, 'c': 'item5'}, ConsistentRead=True)['Item'] == {'p': p2, 'c': 'item5', 'x': 'mouse'}