diff --git a/awswrangler/dynamodb/_read.py b/awswrangler/dynamodb/_read.py index cee3d11a8..0866c45af 100644 --- a/awswrangler/dynamodb/_read.py +++ b/awswrangler/dynamodb/_read.py @@ -392,7 +392,18 @@ def _read_items( items = _read_batch_items(table_name, dynamodb_client, chunked, **kwargs) else: - if limit: + if limit is not None: + if limit < 0: + raise exceptions.InvalidArgumentValue("`max_items_evaluated` must be greater than or equal to 0.") + if limit == 0: + empty_items: _ItemsListType = [] + if chunked: + return _convert_items_chunked( + items_iterator=iter([empty_items]), + as_dataframe=as_dataframe, + arrow_kwargs=arrow_kwargs, + ) + return _convert_items(items=empty_items, as_dataframe=as_dataframe, arrow_kwargs=arrow_kwargs) kwargs["Limit"] = limit _logger.debug("`max_items_evaluated` argument detected, setting use_threads to False") use_threads = False @@ -744,12 +755,20 @@ def read_items( # noqa: PLR0912, PLR0915 **kwargs.get("ExpressionAttributeValues", {}), **_serialize_item(expression_attribute_values, serializer), } - if max_items_evaluated: + if max_items_evaluated is not None: kwargs["Limit"] = max_items_evaluated _logger.debug("DynamoDB scan/query kwargs: %s", kwargs) # If kwargs are sufficiently informative, proceed with actual read op - if any((partition_values, key_condition_expression, filter_expression, allow_full_scan, max_items_evaluated)): + if any( + ( + partition_values, + key_condition_expression, + filter_expression, + allow_full_scan, + max_items_evaluated is not None, + ) + ): return _read_items( table_name=table_name, as_dataframe=as_dataframe, diff --git a/tests/unit/test_moto.py b/tests/unit/test_moto.py index 6e7e23bb1..d60fe0d1c 100644 --- a/tests/unit/test_moto.py +++ b/tests/unit/test_moto.py @@ -844,3 +844,31 @@ def fake_start(*, sql: str, **_) -> str: # The intended LOCATION (un-doubled quotes) is the only top-level clause. assert "LOCATION 's3://intended/output/'" in sql assert "LOCATION 's3://other/'" not in sql + + +def test_dynamodb_read_items_max_items_evaluated_zero(moto_dynamodb_client, moto_dynamodb_table) -> None: + items = [{"key": 1, "value": "A"}, {"key": 2, "value": "B"}] + wr.dynamodb.put_items(items=items, table_name=moto_dynamodb_table) + + # 1. max_items_evaluated=0 without allow_full_scan + df0 = wr.dynamodb.read_items(table_name=moto_dynamodb_table, max_items_evaluated=0) + assert isinstance(df0, pd.DataFrame) + assert len(df0) == 0 + + # 2. max_items_evaluated=0 with allow_full_scan=True + df0_scan = wr.dynamodb.read_items(table_name=moto_dynamodb_table, max_items_evaluated=0, allow_full_scan=True) + assert isinstance(df0_scan, pd.DataFrame) + assert len(df0_scan) == 0 + + # 3. max_items_evaluated=0 as_dataframe=False + items0 = wr.dynamodb.read_items(table_name=moto_dynamodb_table, max_items_evaluated=0, as_dataframe=False) + assert items0 == [] + + # 4. max_items_evaluated=0 chunked=True + chunks = list(wr.dynamodb.read_items(table_name=moto_dynamodb_table, max_items_evaluated=0, chunked=True)) + assert len(chunks) == 1 + assert len(chunks[0]) == 0 + + # 5. max_items_evaluated=-1 raises InvalidArgumentValue + with pytest.raises(wr.exceptions.InvalidArgumentValue): + wr.dynamodb.read_items(table_name=moto_dynamodb_table, max_items_evaluated=-1)