diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_container.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_container.py index fb7780d260ec..ea635bc34b1a 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_container.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_container.py @@ -821,10 +821,12 @@ def query_items( feed_options["correlatedActivityId"] = GenerateGuidId() # Set query with 'query' and 'parameters' from kwargs - if utils.valid_key_value_exist(kwargs, "parameters"): - query = {"query": kwargs.pop("query", None), "parameters": kwargs.pop("parameters", None)} + query_str = kwargs.pop("query", None) + parameters = kwargs.pop("parameters", None) + if parameters is not None: + query = {"query": query_str, "parameters": parameters} else: - query = kwargs.pop("query", None) + query = query_str # Set method to get/cache container properties kwargs["containerProperties"] = self._get_properties_with_options diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/container.py b/sdk/cosmos/azure-cosmos/azure/cosmos/container.py index 2b81cf7b0516..f17e39515fbf 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/container.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/container.py @@ -933,10 +933,12 @@ def query_items( # pylint:disable=docstring-missing-param feed_options["containerRID"] = self.__get_client_container_caches()[self.container_link]["_rid"] # Set query with 'query' and 'parameters' from kwargs - if utils.valid_key_value_exist(kwargs, "parameters"): - query = {"query": kwargs.pop("query", None), "parameters": kwargs.pop("parameters", None)} + query_str = kwargs.pop("query", None) + parameters = kwargs.pop("parameters", None) + if parameters is not None: + query = {"query": query_str, "parameters": parameters} else: - query = kwargs.pop("query", None) + query = query_str # Set range filters for a query. Options are either 'feed_range' or 'partition_key' utils.verify_exclusive_arguments(["feed_range", "partition_key"], **kwargs) diff --git a/sdk/cosmos/azure-cosmos/tests/test_query.py b/sdk/cosmos/azure-cosmos/tests/test_query.py index b268324a77fd..094a9157a7b3 100644 --- a/sdk/cosmos/azure-cosmos/tests/test_query.py +++ b/sdk/cosmos/azure-cosmos/tests/test_query.py @@ -766,6 +766,105 @@ def _MockNextFunction(self): else: raise StopIteration + def test_query_items_with_parameters_none(self): + """Test that query_items handles parameters=None correctly (issue #43662).""" + created_collection = self.created_db.create_container( + "test_params_none_" + str(uuid.uuid4()), PartitionKey(path="/pk")) + + # Create test documents + doc1_id = 'doc1_' + str(uuid.uuid4()) + doc2_id = 'doc2_' + str(uuid.uuid4()) + created_collection.create_item(body={'pk': 'pk1', 'id': doc1_id, 'value': 1}) + created_collection.create_item(body={'pk': 'pk2', 'id': doc2_id, 'value': 2}) + + # Test 1: Explicitly passing parameters=None should not cause TypeError + query = 'SELECT * FROM c' + query_iterable = created_collection.query_items( + query=query, + parameters=None, + enable_cross_partition_query=True + ) + results = list(query_iterable) + self.assertEqual(len(results), 2) + + # Test 2: parameters=None with partition_key should work + query_iterable = created_collection.query_items( + query=query, + parameters=None, + partition_key='pk1' + ) + results = list(query_iterable) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]['id'], doc1_id) + + # Test 3: Verify parameterized query still works with actual parameters + query_with_params = 'SELECT * FROM c WHERE c.value = @value' + query_iterable = created_collection.query_items( + query=query_with_params, + parameters=[{'name': '@value', 'value': 2}], + enable_cross_partition_query=True + ) + results = list(query_iterable) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]['id'], doc2_id) + + # Test 4: Query without parameters argument should work (default behavior) + query_iterable = created_collection.query_items( + query=query, + enable_cross_partition_query=True + ) + results = list(query_iterable) + self.assertEqual(len(results), 2) + + self.created_db.delete_container(created_collection.id) + + def test_query_items_parameters_none_with_options(self): + """Test parameters=None works with various query options.""" + created_collection = self.created_db.create_container( + "test_params_none_opts_" + str(uuid.uuid4()), PartitionKey(path="/pk")) + + # Create multiple test documents + for i in range(5): + doc_id = f'doc_{i}_' + str(uuid.uuid4()) + created_collection.create_item(body={'pk': 'test', 'id': doc_id, 'index': i}) + + # Test with parameters=None and max_item_count + query = 'SELECT * FROM c ORDER BY c.index' + query_iterable = created_collection.query_items( + query=query, + parameters=None, + partition_key='test', + max_item_count=2 + ) + + # Verify pagination works + page_count = 0 + total_items = 0 + for page in query_iterable.by_page(): + page_count += 1 + items = list(page) + total_items += len(items) + self.assertLessEqual(len(items), 2) + + self.assertEqual(total_items, 5) + self.assertGreaterEqual(page_count, 2) # Should have multiple pages + + # Test with parameters=None and populate_query_metrics + query_iterable = created_collection.query_items( + query=query, + parameters=None, + partition_key='test', + populate_query_metrics=True + ) + results = list(query_iterable) + self.assertEqual(len(results), 5) + + # Verify query metrics were populated + metrics_header_name = 'x-ms-documentdb-query-metrics' + self.assertTrue(metrics_header_name in created_collection.client_connection.last_response_headers) + + self.created_db.delete_container(created_collection.id) + if __name__ == "__main__": unittest.main() diff --git a/sdk/cosmos/azure-cosmos/tests/test_query_async.py b/sdk/cosmos/azure-cosmos/tests/test_query_async.py index 6a7985fa77d1..dae9c8835177 100644 --- a/sdk/cosmos/azure-cosmos/tests/test_query_async.py +++ b/sdk/cosmos/azure-cosmos/tests/test_query_async.py @@ -756,6 +756,105 @@ async def _MockExecuteFunctionTimeoutFailoverRetry(self, function, *args, **kwar raise ex_to_raise return await self.OriginalExecuteFunction(function, *args, **kwargs) + async def test_query_items_with_parameters_none_async(self): + """Test that query_items handles parameters=None correctly (issue #43662).""" + created_collection = await self.created_db.create_container( + "test_params_none_" + str(uuid.uuid4()), PartitionKey(path="/pk")) + + # Create test documents + doc1_id = 'doc1_' + str(uuid.uuid4()) + doc2_id = 'doc2_' + str(uuid.uuid4()) + await created_collection.create_item(body={'pk': 'pk1', 'id': doc1_id, 'value': 1}) + await created_collection.create_item(body={'pk': 'pk2', 'id': doc2_id, 'value': 2}) + + # Test 1: Explicitly passing parameters=None should not cause TypeError + query = 'SELECT * FROM c' + query_iterable = created_collection.query_items( + query=query, + parameters=None, + enable_cross_partition_query=True + ) + results = [item async for item in query_iterable] + assert len(results) == 2 + + # Test 2: parameters=None with partition_key should work + query_iterable = created_collection.query_items( + query=query, + parameters=None, + partition_key='pk1' + ) + results = [item async for item in query_iterable] + assert len(results) == 1 + assert results[0]['id'] == doc1_id + + # Test 3: Verify parameterized query still works with actual parameters + query_with_params = 'SELECT * FROM c WHERE c.value = @value' + query_iterable = created_collection.query_items( + query=query_with_params, + parameters=[{'name': '@value', 'value': 2}], + enable_cross_partition_query=True + ) + results = [item async for item in query_iterable] + assert len(results) == 1 + assert results[0]['id'] == doc2_id + + # Test 4: Query without parameters argument should work (default behavior) + query_iterable = created_collection.query_items( + query=query, + enable_cross_partition_query=True + ) + results = [item async for item in query_iterable] + assert len(results) == 2 + + await self.created_db.delete_container(created_collection.id) + + async def test_query_items_parameters_none_with_options_async(self): + """Test parameters=None works with various query options.""" + created_collection = await self.created_db.create_container( + "test_params_none_opts_" + str(uuid.uuid4()), PartitionKey(path="/pk")) + + # Create multiple test documents + for i in range(5): + doc_id = f'doc_{i}_' + str(uuid.uuid4()) + await created_collection.create_item(body={'pk': 'test', 'id': doc_id, 'index': i}) + + # Test with parameters=None and max_item_count + query = 'SELECT * FROM c ORDER BY c.index' + query_iterable = created_collection.query_items( + query=query, + parameters=None, + partition_key='test', + max_item_count=2 + ) + + # Verify pagination works + page_count = 0 + total_items = 0 + async for page in query_iterable.by_page(): + page_count += 1 + items = [item async for item in page] + total_items += len(items) + assert len(items) <= 2 + + assert total_items == 5 + assert page_count >= 2 # Should have multiple pages + + # Test with parameters=None and populate_query_metrics + query_iterable = created_collection.query_items( + query=query, + parameters=None, + partition_key='test', + populate_query_metrics=True + ) + results = [item async for item in query_iterable] + assert len(results) == 5 + + # Verify query metrics were populated + metrics_header_name = 'x-ms-documentdb-query-metrics' + assert metrics_header_name in created_collection.client_connection.last_response_headers + + await self.created_db.delete_container(created_collection.id) + if __name__ == '__main__': unittest.main()