From 8aded3b036fe072572f2044e6b5de4001f242bc5 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Fri, 24 Apr 2026 18:59:45 +0800 Subject: [PATCH 1/6] 4.0: adapting API and tests --- arango/database.py | 1 - tests/conftest.py | 18 ++++++++++-------- tests/test_document.py | 18 +++++++++++++++--- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/arango/database.py b/arango/database.py index 2ba1129b..035dd419 100644 --- a/arango/database.py +++ b/arango/database.py @@ -1554,7 +1554,6 @@ def response_handler(resp: Response) -> Jsons: "name": col["name"], "system": col["isSystem"], "type": StandardCollection.types[col["type"]], - "status": StandardCollection.statuses[col["status"]], } for col in resp.body["result"] ] diff --git a/tests/conftest.py b/tests/conftest.py index 1483ce91..f3f77f21 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -138,6 +138,7 @@ def pytest_configure(config): ) db_version = sys_db.version() + global_data.db_version = version.parse(db_version.split("-")[0]) # Create a user and non-system database for testing. username = generate_username() @@ -161,8 +162,9 @@ def pytest_configure(config): col_name = generate_col_name() tst_col = tst_db.create_collection(col_name, edge=False) - tst_col.add_index({"type": "skiplist", "fields": ["val"]}) - tst_col.add_index({"type": "fulltext", "fields": ["text"]}) + tst_col.add_index({"type": "persistent", "fields": ["val"]}) + if global_data.db_version < version.parse("4.0.0"): + tst_col.add_index({"type": "fulltext", "fields": ["text"]}) geo_index = tst_col.add_index({"type": "geo", "fields": ["loc"]}) # Create a legacy edge collection for testing. @@ -189,7 +191,6 @@ def pytest_configure(config): global_data.username = username global_data.password = password global_data.db_name = tst_db_name - global_data.db_version = version.parse(db_version.split("-")[0]) global_data.sys_db = sys_db global_data.tst_db = tst_db global_data.bad_db = bad_db @@ -220,11 +221,12 @@ def pytest_unconfigure(*_): # pragma: no cover # Remove all test async jobs. sys_db.clear_async_jobs() - # Remove all test tasks. - for task in sys_db.tasks(): - task_name = task["name"] - if task_name.startswith("test_task"): - sys_db.delete_task(task_name, ignore_missing=True) + if global_data.db_version < version.parse("4.0.0"): + # Remove all test tasks. + for task in sys_db.tasks(): + task_name = task["name"] + if task_name.startswith("test_task"): + sys_db.delete_task(task_name, ignore_missing=True) # Remove all test users. for user in sys_db.users(): diff --git a/tests/test_document.py b/tests/test_document.py index 0dbca038..74eb75f6 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -1278,7 +1278,10 @@ def test_document_find(col, bad_col, docs): assert len(list(col.find({"foo.bar": "baz"}))) == 1 -def test_document_find_near(col, bad_col, docs): +def test_document_find_near(db_version, col, bad_col, docs): + if db_version >= version.parse("4.0.0"): + pytest.skip("Not tested in ArangoDB 4.0 and above") + col.import_bulk(docs) # Test find_near with default options @@ -1362,7 +1365,10 @@ def test_document_find_in_range(col, bad_col, docs): assert err.value.error_code in {11, 1228} -def test_document_find_in_radius(col, bad_col): +def test_document_find_in_radius(db_version, col, bad_col): + if db_version >= version.parse("4.0.0"): + pytest.skip("Not tested in ArangoDB 4.0 and above") + doc1 = {"_key": "1", "loc": [1, 1]} doc2 = {"_key": "2", "loc": [1, 4]} doc3 = {"_key": "3", "loc": [4, 1]} @@ -1523,7 +1529,13 @@ def test_document_find_in_box(db, col, bad_col, geo, cluster): assert err.value.error_code in {11, 1228} -def test_document_find_by_text(col, docs): +def test_document_find_by_text(db_version, col, docs): + if db_version >= version.parse("4.0.0"): + pytest.skip( + "Fulltext indexes are no longer supported and have been replaced" + "by ArangoSearch (inverted indexes)" + ) + col.import_bulk(docs) # Test find_by_text with default options From 4321e4473b0e932e808d0e7feaf6f3dcb6996bd7 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Fri, 24 Apr 2026 19:00:43 +0800 Subject: [PATCH 2/6] 4.0: deprecated find_near and find_in_range --- arango/collection.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/arango/collection.py b/arango/collection.py index c62840a9..8c4ab435 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -866,6 +866,9 @@ def find_near( :rtype: arango.cursor.Cursor :raises arango.exceptions.DocumentGetError: If retrieval fails. """ + m = "find_near is deprecated in ArangoDB 4.0" + warn(m, DeprecationWarning, stacklevel=2) + assert isinstance(latitude, Number), "latitude must be a number" assert isinstance(longitude, Number), "longitude must be a number" assert is_none_or_int(limit), "limit must be a non-negative int" @@ -988,6 +991,9 @@ def find_in_radius( :rtype: arango.cursor.Cursor :raises arango.exceptions.DocumentGetError: If retrieval fails. """ + m = "find_in_radius is deprecated in ArangoDB 4.0" + warn(m, DeprecationWarning, stacklevel=2) + assert isinstance(latitude, Number), "latitude must be a number" assert isinstance(longitude, Number), "longitude must be a number" assert isinstance(radius, Number), "radius must be a number" From 77930cd33bdd8f440adfc536558011e4f8c51ba0 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Fri, 24 Apr 2026 20:03:29 +0800 Subject: [PATCH 3/6] 4.0: deprecating wal methods, override for documents, and more test fixes --- arango/collection.py | 151 +----------------------------- arango/database.py | 5 +- arango/wal.py | 18 +++- tests/test_aql.py | 5 +- tests/test_cluster.py | 4 +- tests/test_collection.py | 30 +++--- tests/test_database.py | 194 +++++++++++++++++++-------------------- tests/test_document.py | 8 +- tests/test_foxx.py | 31 +++++-- tests/test_index.py | 107 +-------------------- tests/test_task.py | 6 +- tests/test_wal.py | 48 ---------- 12 files changed, 180 insertions(+), 427 deletions(-) diff --git a/arango/collection.py b/arango/collection.py index 8c4ab435..5dadc1e7 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -1341,112 +1341,6 @@ def response_handler(resp: Response) -> Json: return self._execute(request, response_handler) - def add_hash_index( - self, - fields: Sequence[str], - unique: Optional[bool] = None, - sparse: Optional[bool] = None, - deduplicate: Optional[bool] = None, - name: Optional[str] = None, - in_background: Optional[bool] = None, - ) -> Result[Json]: - """Create a new hash index. - - .. warning:: - - The index types `hash` and `skiplist` are aliases for the persistent - index type and should no longer be used to create new indexes. The - aliases will be removed in a future version. - - :param fields: Document fields to index. - :type fields: [str] - :param unique: Whether the index is unique. - :type unique: bool | None - :param sparse: If set to True, documents with None in the field - are also indexed. If set to False, they are skipped. - :type sparse: bool | None - :param deduplicate: If set to True, inserting duplicate index values - from the same document triggers unique constraint errors. - :type deduplicate: bool | None - :param name: Optional name for the index. - :type name: str | None - :param in_background: Do not hold the collection lock. - :type in_background: bool | None - :return: New index details. - :rtype: dict - :raise arango.exceptions.IndexCreateError: If create fails. - """ - m = "add_hash_index is deprecated. Using add_index with {'type': 'hash'} instead." # noqa: E501 - warn(m, DeprecationWarning, stacklevel=2) - - data: Json = {"type": "hash", "fields": fields} - - if unique is not None: - data["unique"] = unique - if sparse is not None: - data["sparse"] = sparse - if deduplicate is not None: - data["deduplicate"] = deduplicate - if name is not None: - data["name"] = name - if in_background is not None: - data["inBackground"] = in_background - - return self.add_index(data, formatter=True) - - def add_skiplist_index( - self, - fields: Sequence[str], - unique: Optional[bool] = None, - sparse: Optional[bool] = None, - deduplicate: Optional[bool] = None, - name: Optional[str] = None, - in_background: Optional[bool] = None, - ) -> Result[Json]: - """Create a new skiplist index. - - .. warning:: - - The index types `hash` and `skiplist` are aliases for the persistent - index type and should no longer be used to create new indexes. The - aliases will be removed in a future version. - - :param fields: Document fields to index. - :type fields: [str] - :param unique: Whether the index is unique. - :type unique: bool | None - :param sparse: If set to True, documents with None in the field - are also indexed. If set to False, they are skipped. - :type sparse: bool | None - :param deduplicate: If set to True, inserting duplicate index values - from the same document triggers unique constraint errors. - :type deduplicate: bool | None - :param name: Optional name for the index. - :type name: str | None - :param in_background: Do not hold the collection lock. - :type in_background: bool | None - :return: New index details. - :rtype: dict - :raise arango.exceptions.IndexCreateError: If create fails. - """ - m = "add_skiplist_index is deprecated. Using add_index with {'type': 'skiplist'} instead." # noqa: E501 - warn(m, DeprecationWarning, stacklevel=2) - - data: Json = {"type": "skiplist", "fields": fields} - - if unique is not None: - data["unique"] = unique - if sparse is not None: - data["sparse"] = sparse - if deduplicate is not None: - data["deduplicate"] = deduplicate - if name is not None: - data["name"] = name - if in_background is not None: - data["inBackground"] = in_background - - return self.add_index(data, formatter=True) - def add_geo_index( self, fields: Fields, @@ -1493,45 +1387,6 @@ def add_geo_index( return self.add_index(data, formatter=True) - def add_fulltext_index( - self, - fields: Sequence[str], - min_length: Optional[int] = None, - name: Optional[str] = None, - in_background: Optional[bool] = None, - ) -> Result[Json]: - """Create a new fulltext index. - - .. warning:: - This method is deprecated since ArangoDB 3.10 and will be removed - in a future version of the driver. - - :param fields: Document fields to index. - :type fields: [str] - :param min_length: Minimum number of characters to index. - :type min_length: int | None - :param name: Optional name for the index. - :type name: str | None - :param in_background: Do not hold the collection lock. - :type in_background: bool | None - :return: New index details. - :rtype: dict - :raise arango.exceptions.IndexCreateError: If create fails. - """ - m = "add_fulltext_index is deprecated. Using add_index with {'type': 'fulltext'} instead." # noqa: E501 - warn(m, DeprecationWarning, stacklevel=2) - - data: Json = {"type": "fulltext", "fields": fields} - - if min_length is not None: - data["minLength"] = min_length - if name is not None: - data["name"] = name - if in_background is not None: - data["inBackground"] = in_background - - return self.add_index(data, formatter=True) - def add_persistent_index( self, fields: Sequence[str], @@ -1796,7 +1651,8 @@ def insert_many( can be used to save resources. :type silent: bool :param overwrite: If set to True, operation does not fail on duplicate - keys and the existing documents are replaced. + keys and the existing documents are replaced. **Removed** in ArangoDB + v4.0.0. Use overwrite_mode instead. :type overwrite: bool :param return_old: Include body of the old documents if replaced. Applies only when value of **overwrite** is set to True. @@ -2617,7 +2473,8 @@ def insert( can be used to save resources. :type silent: bool :param overwrite: If set to True, operation does not fail on duplicate - key and existing document is overwritten (replace-insert). + key and existing document is overwritten (replace-insert). **Removed** + in ArangoDB v4.0.0. Use overwrite_mode instead. :type overwrite: bool :param return_old: Include body of the old document if overwritten. Ignored if parameter **silent** is set to True. diff --git a/arango/database.py b/arango/database.py index 035dd419..bf1834a2 100644 --- a/arango/database.py +++ b/arango/database.py @@ -573,6 +573,9 @@ def required_db_version(self) -> Result[str]: :rtype: str :raise arango.exceptions.ServerRequiredDBVersionError: If retrieval fails. """ + m = "target-version is removed in ArangoDB 4.0" + warn(m, DeprecationWarning, stacklevel=2) + request = Request(method="get", endpoint="/_admin/database/target-version") def response_handler(resp: Response) -> str: @@ -1115,7 +1118,7 @@ def metrics(self) -> Result[str]: :rtype: str :raise arango.exceptions.ServerMetricsError: If operation fails. """ - request = Request(method="get", endpoint="/_admin/metrics/v2") + request = Request(method="get", endpoint="/_admin/metrics") def response_handler(resp: Response) -> str: if resp.is_success: diff --git a/arango/wal.py b/arango/wal.py index 9ad089f5..936eced1 100644 --- a/arango/wal.py +++ b/arango/wal.py @@ -1,6 +1,7 @@ __all__ = ["WAL"] from typing import Optional +from warnings import warn from arango.api import ApiGroup from arango.exceptions import ( @@ -28,12 +29,15 @@ class WAL(ApiGroup): # pragma: no cover """WAL (Write-Ahead Log) API wrapper.""" def properties(self) -> Result[Json]: - """Return WAL properties. + """Return WAL properties. **Removed** in ArangoDB v4.0.0. :return: WAL properties. :rtype: dict :raise arango.exceptions.WALPropertiesError: If retrieval fails. """ + m = "/_admin/wal/properties was removed in ArangoDB v4.0.0." + warn(m, DeprecationWarning, stacklevel=2) + request = Request(method="get", endpoint="/_admin/wal/properties") def response_handler(resp: Response) -> Json: @@ -52,7 +56,7 @@ def configure( throttle_wait: Optional[int] = None, throttle_limit: Optional[int] = None, ) -> Result[Json]: - """Configure WAL properties. + """Configure WAL properties. **Removed in ArangoDB v4.0.0. :param oversized_ops: If set to True, operations bigger than a single log file are allowed to be executed and stored. @@ -74,6 +78,9 @@ def configure( :rtype: dict :raise arango.exceptions.WALConfigureError: If operation fails. """ + m = "/_admin/wal/properties was removed in ArangoDB v4.0.0." + warn(m, DeprecationWarning, stacklevel=2) + data: Json = {} if oversized_ops is not None: data["allowOversizeEntries"] = oversized_ops @@ -98,7 +105,9 @@ def response_handler(resp: Response) -> Json: return self._execute(request, response_handler) def transactions(self) -> Result[Json]: - """Return details on currently running WAL transactions. + """**Removed** in ArangoDB v4.0.0. + + Return details on currently running WAL transactions. Fields in the returned details are as follows: @@ -118,6 +127,9 @@ def transactions(self) -> Result[Json]: :rtype: dict :raise arango.exceptions.WALTransactionListError: If retrieval fails. """ + m = "/_admin/wal/transactions was removed in ArangoDB v4.0.0." + warn(m, DeprecationWarning, stacklevel=2) + request = Request(method="get", endpoint="/_admin/wal/transactions") def response_handler(resp: Response) -> Json: diff --git a/tests/test_aql.py b/tests/test_aql.py index 61d07d8c..30f6da5e 100644 --- a/tests/test_aql.py +++ b/tests/test_aql.py @@ -284,7 +284,10 @@ def test_aql_query_force_one_shard_attribute_value(db, skip_tests, cluster): assert len(results) == 0 -def test_aql_function_management(db, bad_db): +def test_aql_function_management(db, bad_db, db_version): + if db_version >= version.parse("4.0.0"): + pytest.skip("Javascript is not available in ArangoDB v4.0") + fn_group = "functions::temperature" fn_name_1 = "functions::temperature::celsius_to_fahrenheit" fn_body_1 = "function (celsius) { return celsius * 1.8 + 32; }" diff --git a/tests/test_cluster.py b/tests/test_cluster.py index 0a4cd19f..0ae0a385 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -99,7 +99,9 @@ def test_cluster_server_engine(sys_db, bad_db, cluster): assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} -def test_cluster_server_statistics(sys_db, bad_db, cluster): +def test_cluster_server_statistics(sys_db, bad_db, cluster, db_version): + if db_version >= version.parse("4.0.0"): + pytest.skip("Server statistics endpoint is removed in ArangoDB v4.0") if not cluster: pytest.skip("Only tested in a cluster setup") diff --git a/tests/test_collection.py b/tests/test_collection.py index ad5bc581..2ab44576 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -1,6 +1,7 @@ import time import pytest +from packaging import version from arango.client import ArangoClient from arango.collection import StandardCollection @@ -39,7 +40,7 @@ def test_collection_attributes(db, col, username): assert repr(col) == f"" -def test_collection_misc_methods(col, bad_col, cluster): +def test_collection_misc_methods(col, bad_col, cluster, db_version): # Test get properties properties = col.properties() assert properties["name"] == col.name @@ -101,21 +102,22 @@ def test_collection_misc_methods(col, bad_col, cluster): bad_col.revision() assert err.value.error_code in {11, 1228} - # Test load into memory - assert col.load() is True + if db_version < version.parse("4.0.0"): + # Test load into memory + assert col.load() is True - # Test load with bad collection - with assert_raises(CollectionLoadError) as err: - bad_col.load() - assert err.value.error_code in {11, 1228} + # Test load with bad collection + with assert_raises(CollectionLoadError) as err: + bad_col.load() + assert err.value.error_code in {11, 1228} - # Test unload from memory - assert col.unload() is True + # Test unload from memory + assert col.unload() is True - # Test unload with bad collection - with assert_raises(CollectionUnloadError) as err: - bad_col.unload() - assert err.value.error_code in {11, 1228} + # Test unload with bad collection + with assert_raises(CollectionUnloadError) as err: + bad_col.unload() + assert err.value.error_code in {11, 1228} if cluster: col.insert({}) @@ -162,7 +164,7 @@ def test_collection_misc_methods(col, bad_col, cluster): # Test collection info info = col.info() - assert set(info.keys()) == {"id", "name", "system", "type", "status", "global_id"} + assert set(info.keys()) == {"id", "name", "system", "type", "global_id"} assert info["name"] == col.name assert info["system"] is False diff --git a/tests/test_database.py b/tests/test_database.py index 1b9cf958..8d44338d 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -101,13 +101,14 @@ def test_database_misc_methods(client, sys_db, db, bad_db, cluster, secret, db_v bad_db.details() assert err.value.error_code in {11, 1228} - # Test get server required database version - required_version = db.required_db_version() - assert isinstance(required_version, str) + if db_version < version.parse("4.0.0"): + # Test get server required database version + required_version = db.required_db_version() + assert isinstance(required_version, str) - # Test get server target version with bad database - with assert_raises(ServerRequiredDBVersionError): - bad_db.required_db_version() + # Test get server target version with bad database + with assert_raises(ServerRequiredDBVersionError): + bad_db.required_db_version() # Test get server metrics metrics = db.metrics() @@ -118,23 +119,24 @@ def test_database_misc_methods(client, sys_db, db, bad_db, cluster, secret, db_v bad_db.metrics() assert err.value.error_code in {11, 1228} - # Test get server statistics - statistics = db.statistics(description=False) - assert isinstance(statistics, dict) - assert "time" in statistics - assert "system" in statistics - assert "server" in statistics - - # Test get server statistics with description - description = db.statistics(description=True) - assert isinstance(description, dict) - assert "figures" in description - assert "groups" in description - - # Test get server statistics with bad database - with assert_raises(ServerStatisticsError) as err: - bad_db.statistics() - assert err.value.error_code in {11, 1228} + if db_version < version.parse("4.0.0"): + # Test get server statistics + statistics = db.statistics(description=False) + assert isinstance(statistics, dict) + assert "time" in statistics + assert "system" in statistics + assert "server" in statistics + + # Test get server statistics with description + description = db.statistics(description=True) + assert isinstance(description, dict) + assert "figures" in description + assert "groups" in description + + # Test get server statistics with bad database + with assert_raises(ServerStatisticsError) as err: + bad_db.statistics() + assert err.value.error_code in {11, 1228} # Test get server role assert db.role() in {"SINGLE", "COORDINATOR", "PRIMARY", "SECONDARY", "UNDEFINED"} @@ -160,10 +162,11 @@ def test_database_misc_methods(client, sys_db, db, bad_db, cluster, secret, db_v # Test get server status status = db.status() assert "host" in status - assert "operation_mode" in status + if db_version < version.parse("4.0.0"): + assert "operation_mode" in status + assert "write_ops_enabled" in status["server_info"] assert "server_info" in status assert "read_only" in status["server_info"] - assert "write_ops_enabled" in status["server_info"] assert "version" in status # Test get status with bad database @@ -179,78 +182,74 @@ def test_database_misc_methods(client, sys_db, db, bad_db, cluster, secret, db_v bad_db.time() assert err.value.error_code in {11, 1228} - # Test echo (get last request) - last_request = db.echo() - assert "protocol" in last_request - assert "user" in last_request - assert "requestType" in last_request - assert "rawRequestBody" in last_request + if db_version < version.parse("4.0.0"): + # Test echo (get last request) + last_request = db.echo() + assert "protocol" in last_request + assert "user" in last_request + assert "requestType" in last_request + assert "rawRequestBody" in last_request + + # Test echo with bad database + with assert_raises(ServerEchoError) as err: + bad_db.echo() + assert err.value.error_code in {11, 1228} + + # Test echo (forward request) + body = "request goes here" + echo = db.echo(body) + assert isinstance(echo, dict) + assert echo["requestBody"] == body + + # Test read_log with default parameters + log = sys_db.read_log(upto="fatal") + assert "lid" in log + assert "level" in log + assert "text" in log + assert "total_amount" in log + + log_entry = sys_db.read_log_entries(upto="fatal") + assert "total" in log_entry + assert "messages" in log_entry + + kwargs = { + "level": "error", + "start": 0, + "size": 100000, + "offset": 0, + "search": "test", + "sort": "desc", + } + + # Test read_log with specific parameters + # Deprecated in 3.8.0 + log = sys_db.read_log(**kwargs) + assert "lid" in log + assert "level" in log + assert "text" in log + assert "total_amount" in log - # Test echo with bad database - with assert_raises(ServerEchoError) as err: - bad_db.echo() - assert err.value.error_code in {11, 1228} + log_entry = sys_db.read_log_entries(**kwargs) + assert "total" in log_entry + assert "messages" in log_entry - # Test echo (forward request) - body = "request goes here" - echo = db.echo(body) - assert isinstance(echo, dict) - assert echo["requestBody"] == body - - # Test read_log with default parameters - # Deprecated in 3.8.0 - # TODO: Remove in future release - log = sys_db.read_log(upto="fatal") - assert "lid" in log - assert "level" in log - assert "text" in log - assert "total_amount" in log - - log_entry = sys_db.read_log_entries(upto="fatal") - assert "total" in log_entry - assert "messages" in log_entry - - kwargs = { - "level": "error", - "start": 0, - "size": 100000, - "offset": 0, - "search": "test", - "sort": "desc", - } - - # Test read_log with specific parameters - # Deprecated in 3.8.0 - # TODO: Remove in future release - log = sys_db.read_log(**kwargs) - assert "lid" in log - assert "level" in log - assert "text" in log - assert "total_amount" in log - - log_entry = sys_db.read_log_entries(**kwargs) - assert "total" in log_entry - assert "messages" in log_entry - - # Test read_log with bad database - # Deprecated in 3.8.0 - # TODO: Remove in future release - with assert_raises(ServerReadLogError) as err: - bad_db.read_log() - assert err.value.error_code in {11, 1228} + with assert_raises(ServerReadLogError) as err: + bad_db.read_log() + assert err.value.error_code in {11, 1228} - # Test read_log_entries with bad database - with assert_raises(ServerReadLogError) as err: - bad_db.read_log_entries() - assert err.value.error_code in {11, 1228} + # Test read_log_entries with bad database + with assert_raises(ServerReadLogError) as err: + bad_db.read_log_entries() + assert err.value.error_code in {11, 1228} - # Test reload routing - assert isinstance(db.reload_routing(), bool) + # Test reload routing + # Deprecated in ArangoDB 4.0 + assert isinstance(db.reload_routing(), bool) - # Test reload routing with bad database - with assert_raises(ServerReloadRoutingError) as err: - bad_db.reload_routing() - assert err.value.error_code in {11, 1228} + # Test reload routing with bad database + with assert_raises(ServerReloadRoutingError) as err: + bad_db.reload_routing() + assert err.value.error_code in {11, 1228} # Test get log levels default_log_levels = sys_db.log_levels() @@ -339,11 +338,12 @@ def test_database_misc_methods(client, sys_db, db, bad_db, cluster, secret, db_v assert "deployment" in info assert "date" in info - # Test execute JavaScript code - assert db.execute(1) is None - assert db.execute(None) == {"error": False, "code": 200} - assert db.execute("") == {"error": False, "code": 200} - assert db.execute("return 1") == 1 + if db_version < version.parse("4.0.0"): + # Test execute JavaScript code + assert db.execute(1) is None + assert db.execute(None) == {"error": False, "code": 200} + assert db.execute("") == {"error": False, "code": 200} + assert db.execute("return 1") == 1 # Test database compact with assert_raises(DatabaseCompactError) as err: diff --git a/tests/test_document.py b/tests/test_document.py index 74eb75f6..b09f4545 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -109,7 +109,9 @@ def test_document_insert(col, docs): # Test insert replace doc = {"_key": doc["_key"], "foo": {"bar": 1}, "baz": None} - result = col.insert(document=doc, overwrite=True, return_old=True, return_new=True) + result = col.insert( + document=doc, overwrite_mode="replace", return_old=True, return_new=True + ) assert result["new"]["foo"] == {"bar": 1} assert result["new"]["baz"] is None assert "val" in result["old"] @@ -121,7 +123,6 @@ def test_document_insert(col, docs): document=doc, return_old=True, return_new=True, - overwrite=True, overwrite_mode="ignore", keep_none=False, merge=True, @@ -136,7 +137,6 @@ def test_document_insert(col, docs): document=doc, return_old=True, return_new=True, - overwrite=True, overwrite_mode="conflict", keep_none=False, merge=True, @@ -233,7 +233,7 @@ def test_document_insert_many(col, bad_col, docs): assert "[HTTP 202][ERR 1210]" in error.message # Test insert_many with overwrite and return_old set to True - results = col.insert_many(docs, overwrite=True, return_old=True) + results = col.insert_many(docs, overwrite_mode="replace", return_old=True) for result, doc in zip(results, docs): assert not isinstance(result, DocumentInsertError) assert isinstance(result["old"], dict) diff --git a/tests/test_foxx.py b/tests/test_foxx.py index 892cfc60..bfb1d741 100644 --- a/tests/test_foxx.py +++ b/tests/test_foxx.py @@ -2,6 +2,7 @@ import os import pytest +from packaging import version from arango.exceptions import ( FoxxCommitError, @@ -32,14 +33,20 @@ service_name = "test" -def test_foxx_attributes(db, skip_tests): +def test_foxx_attributes(db, skip_tests, db_version): + if db_version >= version.parse("4.0.0"): + pytest.skip("Foxx API has been removed in ArangoDB v4.0") if "foxx" in skip_tests: pytest.skip("Skipping foxx tests") assert isinstance(db.foxx, Foxx) assert repr(db.foxx) == f"" -def test_foxx_service_management_json(db, bad_db, cluster, skip_tests, foxx_path): +def test_foxx_service_management_json( + db, bad_db, cluster, skip_tests, foxx_path, db_version +): + if db_version >= version.parse("4.0.0"): + pytest.skip("Foxx API has been removed in ArangoDB v4.0") if "foxx" in skip_tests: pytest.skip("Skipping foxx tests") if cluster: @@ -150,7 +157,9 @@ def test_foxx_service_management_json(db, bad_db, cluster, skip_tests, foxx_path assert err.value.error_code == 3009 -def test_foxx_service_management_file(db, cluster, skip_tests): +def test_foxx_service_management_file(db, cluster, skip_tests, db_version): + if db_version >= version.parse("4.0.0"): + pytest.skip("Foxx API has been removed in ArangoDB v4.0") if "foxx" in skip_tests: pytest.skip("Skipping foxx tests") if cluster: @@ -240,7 +249,9 @@ def test_foxx_service_management_file(db, cluster, skip_tests): assert service_mount not in extract("mount", db.foxx.services()) -def test_foxx_config_management(db, cluster, skip_tests, foxx_path): +def test_foxx_config_management(db, cluster, skip_tests, foxx_path, db_version): + if db_version >= version.parse("4.0.0"): + pytest.skip("Foxx API has been removed in ArangoDB v4.0") if "foxx" in skip_tests: pytest.skip("Skipping foxx tests") if cluster: @@ -281,7 +292,9 @@ def test_foxx_config_management(db, cluster, skip_tests, foxx_path): assert err.value.error_code == 3009 -def test_foxx_dependency_management(db, cluster, skip_tests, foxx_path): +def test_foxx_dependency_management(db, cluster, skip_tests, foxx_path, db_version): + if db_version >= version.parse("4.0.0"): + pytest.skip("Foxx API has been removed in ArangoDB v4.0") if "foxx" in skip_tests: pytest.skip("Skipping foxx tests") if cluster: @@ -318,7 +331,9 @@ def test_foxx_dependency_management(db, cluster, skip_tests, foxx_path): assert err.value.error_code == 3009 -def test_foxx_development_toggle(db, cluster, skip_tests, foxx_path): +def test_foxx_development_toggle(db, cluster, skip_tests, foxx_path, db_version): + if db_version >= version.parse("4.0.0"): + pytest.skip("Foxx API has been removed in ArangoDB v4.0") if "foxx" in skip_tests: pytest.skip("Skipping foxx tests") if cluster: @@ -357,7 +372,9 @@ def test_foxx_development_toggle(db, cluster, skip_tests, foxx_path): assert err.value.error_code == 3009 -def test_foxx_misc_functions(db, bad_db, cluster, skip_tests, foxx_path): +def test_foxx_misc_functions(db, bad_db, cluster, skip_tests, foxx_path, db_version): + if db_version >= version.parse("4.0.0"): + pytest.skip("Foxx API has been removed in ArangoDB v4.0") if "foxx" in skip_tests: pytest.skip("Skipping foxx tests") if cluster: diff --git a/tests/test_index.py b/tests/test_index.py index c756b1e9..0dfd6add 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -46,70 +46,6 @@ def test_get_index(icol, bad_col): assert err.value.error_code == 1212 -def test_add_hash_index(icol): - icol = icol - - fields = ["attr1", "attr2"] - result = icol.add_index( - { - "type": "hash", - "fields": fields, - "unique": True, - "sparse": True, - "deduplicate": True, - "name": "hash_index", - "inBackground": False, - } - ) - - expected_index = { - "sparse": True, - "type": "hash", - "fields": fields, - "unique": True, - "deduplicate": True, - "name": "hash_index", - } - for key, value in expected_index.items(): - assert result[key] == value - - assert result["id"] in extract("id", icol.indexes()) - - # Clean up the index - icol.delete_index(result["id"]) - - -def test_add_skiplist_index(icol): - fields = ["attr1", "attr2"] - result = icol.add_index( - { - "type": "skiplist", - "fields": fields, - "unique": True, - "sparse": True, - "deduplicate": True, - "name": "skiplist_index", - "inBackground": False, - } - ) - - expected_index = { - "sparse": True, - "type": "skiplist", - "fields": ["attr1", "attr2"], - "unique": True, - "deduplicate": True, - "name": "skiplist_index", - } - for key, value in expected_index.items(): - assert result[key] == value - - assert result["id"] in extract("id", icol.indexes()) - - # Clean up the index - icol.delete_index(result["id"]) - - def test_add_geo_index(icol): # Test add geo index with one attribute result = icol.add_index( @@ -163,39 +99,6 @@ def test_add_geo_index(icol): icol.delete_index(result["id"]) -def test_add_fulltext_index(icol): - # Test add fulltext index with one attributes - result = icol.add_index( - { - "type": "fulltext", - "fields": ["attr1"], - "minLength": 10, - "name": "fulltext_index", - "inBackground": True, - } - ) - expected_index = { - "sparse": True, - "type": "fulltext", - "fields": ["attr1"], - "minLength": 10, - "unique": False, - "name": "fulltext_index", - } - for key, value in expected_index.items(): - assert result[key] == value - - assert result["id"] in extract("id", icol.indexes()) - - # Test add fulltext index with two attributes (should fail) - with assert_raises(IndexCreateError) as err: - icol.add_index({"type": "fulltext", "fields": ["attr1", "attr2"]}) - assert err.value.error_code == 10 - - # Clean up the index - icol.delete_index(result["id"]) - - def test_add_persistent_index(icol): # Test add persistent index with two attributes result = icol.add_index( @@ -373,12 +276,10 @@ def test_add_vector_index(col): def test_delete_index(icol, bad_col): old_indexes = set(extract("id", icol.indexes())) - hash_index = {"type": "hash", "fields": ["attr1", "attr2"], "unique": True} - icol.add_index(hash_index) - skiplist_Index = {"type": "skiplist", "fields": ["attr3", "attr4"], "unique": True} - icol.add_index(skiplist_Index) - fulltext_index = {"type": "fulltext", "fields": ["attr5"], "min_length": 10} - icol.add_index(fulltext_index) + index1 = {"type": "persistent", "fields": ["attr1", "attr2"], "unique": True} + icol.add_index(index1) + index2 = {"type": "persistent", "fields": ["attr3", "attr4"], "unique": True} + icol.add_index(index2) new_indexes = set(extract("id", icol.indexes())) assert new_indexes.issuperset(old_indexes) diff --git a/tests/test_task.py b/tests/test_task.py index 9bfa48f6..8101f2ea 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -1,4 +1,5 @@ import pytest +from packaging import version from arango.exceptions import ( TaskCreateError, @@ -9,7 +10,10 @@ from tests.helpers import assert_raises, extract, generate_task_id, generate_task_name -def test_task_management(sys_db, db, bad_db, skip_tests): +def test_task_management(sys_db, db, bad_db, skip_tests, db_version): + if db_version >= version.parse("4.0.0"): + pytest.skip("The tasks feature is deprecated and removed in ArangoDB v4.0") + if "task" in skip_tests: pytest.skip("Skipping task tests") diff --git a/tests/test_wal.py b/tests/test_wal.py index 08c69850..2b2336e5 100644 --- a/tests/test_wal.py +++ b/tests/test_wal.py @@ -2,13 +2,11 @@ from arango.errno import DATABASE_NOT_FOUND, FORBIDDEN, HTTP_UNAUTHORIZED from arango.exceptions import ( - WALConfigureError, WALFlushError, WALLastTickError, WALPropertiesError, WALTailError, WALTickRangesError, - WALTransactionListError, ) from tests.helpers import assert_raises @@ -20,52 +18,6 @@ def test_wal_misc_methods(sys_db, bad_db): if wal_err.http_code == 501: pytest.skip("WAL not implemented") - # Test get properties - properties = sys_db.wal.properties() - assert "oversized_ops" in properties - assert "log_size" in properties - assert "historic_logs" in properties - assert "reserve_logs" in properties - assert "throttle_wait" in properties - assert "throttle_limit" in properties - - # Test get properties with bad database - with assert_raises(WALPropertiesError) as err: - bad_db.wal.properties() - assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} - - # Test configure properties - sys_db.wal.configure( - historic_logs=15, - oversized_ops=False, - log_size=30000000, - reserve_logs=5, - throttle_limit=0, - throttle_wait=16000, - ) - properties = sys_db.wal.properties() - assert properties["historic_logs"] == 15 - assert properties["oversized_ops"] is False - assert properties["log_size"] == 30000000 - assert properties["reserve_logs"] == 5 - assert properties["throttle_limit"] == 0 - assert properties["throttle_wait"] == 16000 - - # Test configure properties with bad database - with assert_raises(WALConfigureError) as err: - bad_db.wal.configure(log_size=2000000) - assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} - - # Test get transactions - result = sys_db.wal.transactions() - assert "count" in result - assert "last_collected" in result - - # Test get transactions with bad database - with assert_raises(WALTransactionListError) as err: - bad_db.wal.transactions() - assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} - # Test flush result = sys_db.wal.flush(garbage_collect=False, sync=False) assert isinstance(result, bool) From e94ac3735982d1e817551dce93e9ecfb4bbeedeb Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Fri, 24 Apr 2026 20:05:37 +0800 Subject: [PATCH 4/6] 4.0: updating documentation --- README.md | 2 +- docs/graph.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d90d8458..6335575b 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ edges.insert({"_from": "students/03", "_to": "lectures/CSC101"}) # Traverse the graph in outbound direction, breath-first. query = """ FOR v, e, p IN 1..3 OUTBOUND 'students/01' GRAPH 'school' - OPTIONS { bfs: true, uniqueVertices: 'global' } + OPTIONS { order: 'bfs', uniqueVertices: 'global' } RETURN {vertex: v, edge: e, path: p} """ cursor = db.aql.execute(query) diff --git a/docs/graph.rst b/docs/graph.rst index 0645d5b6..88d0bcc0 100644 --- a/docs/graph.rst +++ b/docs/graph.rst @@ -377,7 +377,7 @@ over edges and vertices using various algorithms. # AQL to perform a graph traversal query = """ FOR v, e, p IN 1..3 OUTBOUND 'teachers/jon' GRAPH 'school' - OPTIONS { bfs: true, uniqueVertices: 'global' } + OPTIONS { order: 'bfs', uniqueVertices: 'global' } RETURN {vertex: v, edge: e, path: p} """ From bc60f0fa5580f3e52dbd35b1c1b5174346261fc6 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Fri, 24 Apr 2026 20:22:29 +0800 Subject: [PATCH 5/6] 4.0: test fix --- tests/test_collection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_collection.py b/tests/test_collection.py index 2ab44576..02739450 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -164,7 +164,8 @@ def test_collection_misc_methods(col, bad_col, cluster, db_version): # Test collection info info = col.info() - assert set(info.keys()) == {"id", "name", "system", "type", "global_id"} + for key in ["id", "name", "system", "type", "global_id"]: + assert key in info assert info["name"] == col.name assert info["system"] is False From 1ceb86ac7700037b3f0117d29d3115d31a434180 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Fri, 24 Apr 2026 21:59:01 +0800 Subject: [PATCH 6/6] 4.0: deprecated logger-first-tick --- arango/replication.py | 4 ++++ tests/test_replication.py | 13 ------------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/arango/replication.py b/arango/replication.py index d5fae457..308f9c2e 100644 --- a/arango/replication.py +++ b/arango/replication.py @@ -1,6 +1,7 @@ __all__ = ["Replication"] from typing import Optional, Sequence +from warnings import warn from arango.api import ApiGroup from arango.exceptions import ( @@ -325,6 +326,9 @@ def logger_first_tick(self) -> Result[str]: :rtype: str :raise arango.exceptions.ReplicationLoggerFirstTickError: If retrieval fails. """ + m = "/_api/replication/logger-first-tick endpoint is removed in ArangoDB v4.0" + warn(m, DeprecationWarning, stacklevel=2) + request = Request( method="get", endpoint="/_api/replication/logger-first-tick", diff --git a/tests/test_replication.py b/tests/test_replication.py index 2136b97a..c89ee80e 100644 --- a/tests/test_replication.py +++ b/tests/test_replication.py @@ -19,7 +19,6 @@ ReplicationDumpBatchExtendError, ReplicationDumpError, ReplicationInventoryError, - ReplicationLoggerFirstTickError, ReplicationLoggerStateError, ReplicationMakeSlaveError, ReplicationServerIDError, @@ -106,18 +105,6 @@ def test_replication_logger_state(sys_db, bad_db, cluster): assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} -def test_replication_first_tick(sys_db, bad_db, cluster): - if cluster: - pytest.skip("Not tested in a cluster setup") - - result = sys_db.replication.logger_first_tick() - assert isinstance(result, str) - - with assert_raises(ReplicationLoggerFirstTickError) as err: - bad_db.replication.logger_first_tick() - assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} - - def test_replication_applier(sys_db, bad_db, url, cluster): if cluster: pytest.skip("Not tested in a cluster setup")