From 236aa8058ed9cd79785319d8a6692d21aeaa29a3 Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Fri, 20 Feb 2026 16:53:20 -0300 Subject: [PATCH 1/6] Fix Object TTL schema export to match server json --- integration/test_collection_config.py | 89 ++++++++++++++++++++++++++ test/collection/test_config.py | 54 ++++++++++++++-- weaviate/collections/classes/config.py | 16 +++++ 3 files changed, 152 insertions(+), 7 deletions(-) diff --git a/integration/test_collection_config.py b/integration/test_collection_config.py index 1ed1df103..0bbe1d257 100644 --- a/integration/test_collection_config.py +++ b/integration/test_collection_config.py @@ -7,6 +7,7 @@ import weaviate import weaviate.classes as wvc from integration.conftest import ( + ClientFactory, CollectionFactory, OpenAICollection, _sanitize_collection_name, @@ -1950,3 +1951,91 @@ def test_object_ttl_update(collection_factory: CollectionFactory) -> None: ) conf = collection.config.get() assert conf.object_ttl_config is None + + +def test_object_ttl_roundtrip_from_dict( + collection_factory: CollectionFactory, client_factory: ClientFactory +) -> None: + dummy = collection_factory("dummy") + if dummy._connection._weaviate_version.is_lower_than(1, 35, 0): + pytest.skip("object ttl is not supported in Weaviate versions lower than 1.35.0") + + client = client_factory() + + # (schema_to_create, expected_object_ttl_config_dict) + test_cases = [ + # deleteOn: _creationTimeUnix + ( + { + "class": "CollectionTTLRoundtripCreation", + "objectTtlConfig": { + "enabled": True, + "defaultTtl": 60, + "deleteOn": "_creationTimeUnix", + "filterExpiredObjects": True, + }, + }, + { + "enabled": True, + "defaultTtl": 60, + "deleteOn": "_creationTimeUnix", + "filterExpiredObjects": True, + }, + ), + # deleteOn: _lastUpdateTimeUnix + ( + { + "class": "CollectionTTLRoundtripUpdate", + "objectTtlConfig": { + "enabled": True, + "defaultTtl": 3600, + "deleteOn": "_lastUpdateTimeUnix", + "filterExpiredObjects": True, + }, + }, + { + "enabled": True, + "defaultTtl": 3600, + "deleteOn": "_lastUpdateTimeUnix", + "filterExpiredObjects": True, + }, + ), + # deleteOn: custom date property + ( + { + "class": "CollectionTTLRoundtripDateProp", + "properties": [ + { + "name": "reference_date", + "dataType": ["date"], + } + ], + "objectTtlConfig": { + "enabled": True, + "defaultTtl": 123, + "deleteOn": "reference_date", + "filterExpiredObjects": True, + }, + }, + { + "enabled": True, + "defaultTtl": 123, + "deleteOn": "reference_date", + "filterExpiredObjects": True, + }, + ), + ] + + for schema, expected_ttl_dict in test_cases: + name = schema["class"] + client.collections.delete(name) + try: + client.collections.create_from_dict(schema) + config = client.collections.export_config(name) + assert config.object_ttl_config is not None, f"object_ttl_config is None for {name}" + assert config.object_ttl_config.to_dict() == expected_ttl_dict, ( + f"Round-trip mismatch for {name}: " + f"got {config.object_ttl_config.to_dict()}, expected {expected_ttl_dict}" + ) + finally: + client.collections.delete(name) diff --git a/test/collection/test_config.py b/test/collection/test_config.py index b188a0bad..b84ede93c 100644 --- a/test/collection/test_config.py +++ b/test/collection/test_config.py @@ -16,6 +16,7 @@ _RerankerProvider, _VectorizerConfigCreate, ) +from weaviate.collections.classes.config_methods import _get_object_ttl_config from weaviate.collections.classes.config_named_vectors import _NamedVectorConfigCreate from weaviate.collections.classes.config_vectorizers import ( Multi2VecField, @@ -2621,9 +2622,9 @@ def test_config_with_vectors(vector_config: List[_VectorConfigCreate], expected: ), { "enabled": True, - "timeToLive": 86400, + "defaultTtl": 86400, "filterExpiredObjects": True, - "deleteOn": "creationTime", + "deleteOn": "_creationTimeUnix", }, ), # delete_by_update_time @@ -2636,9 +2637,9 @@ def test_config_with_vectors(vector_config: List[_VectorConfigCreate], expected: ), { "enabled": True, - "timeToLive": 604800, + "defaultTtl": 604800, "filterExpiredObjects": False, - "deleteOn": "updateTime", + "deleteOn": "_lastUpdateTimeUnix", }, ), # delete_by_date_property @@ -2651,7 +2652,7 @@ def test_config_with_vectors(vector_config: List[_VectorConfigCreate], expected: ), { "enabled": True, - "timeToLive": 5400, + "defaultTtl": 5400, "filterExpiredObjects": True, "deleteOn": "releaseDate", }, @@ -2667,7 +2668,7 @@ def test_config_with_vectors(vector_config: List[_VectorConfigCreate], expected: { "enabled": True, "filterExpiredObjects": False, - "deleteOn": "creationTime", + "deleteOn": "_creationTimeUnix", }, ), # negative offset (delete_by_date_property with offset before date) @@ -2680,7 +2681,7 @@ def test_config_with_vectors(vector_config: List[_VectorConfigCreate], expected: ), { "enabled": True, - "timeToLive": -3600, + "defaultTtl": -3600, "filterExpiredObjects": True, "deleteOn": "eventDate", }, @@ -2694,6 +2695,45 @@ def test_object_ttl_config_to_dict(ttl_config: _ObjectTTLConfig, expected: dict) assert ttl_config.to_dict() == expected +TEST_OBJECT_TTL_ROUNDTRIP_PARAMETERS = [ + # _creationTimeUnix round-trip + { + "objectTtlConfig": { + "enabled": True, + "defaultTtl": 60, + "deleteOn": "_creationTimeUnix", + "filterExpiredObjects": True, + } + }, + # _lastUpdateTimeUnix round-trip + { + "objectTtlConfig": { + "enabled": True, + "defaultTtl": 3600, + "deleteOn": "_lastUpdateTimeUnix", + "filterExpiredObjects": True, + } + }, + # custom date property round-trip + { + "objectTtlConfig": { + "enabled": True, + "defaultTtl": 123, + "deleteOn": "reference_date", + "filterExpiredObjects": True, + } + }, +] + + +@pytest.mark.parametrize("schema", TEST_OBJECT_TTL_ROUNDTRIP_PARAMETERS) +def test_object_ttl_config_roundtrip(schema: dict) -> None: + """Test that deserializing an objectTtlConfig and calling to_dict() produces the original dict.""" + ttl_config = _get_object_ttl_config(schema) + assert ttl_config is not None + assert ttl_config.to_dict() == schema["objectTtlConfig"] + + def test_nested_property_with_id_name_is_allowed() -> None: """A nested property named 'id' must not raise — only top-level 'id' is reserved.""" prop = Property( diff --git a/weaviate/collections/classes/config.py b/weaviate/collections/classes/config.py index 6d8d5bdf1..c45f5b98e 100644 --- a/weaviate/collections/classes/config.py +++ b/weaviate/collections/classes/config.py @@ -1938,6 +1938,22 @@ class _ObjectTTLConfig(_ConfigBase): filter_expired_objects: bool delete_on: Union[str, Literal["updateTime"], Literal["creationTime"]] + def to_dict(self) -> dict: + delete_on = self.delete_on + if delete_on == "creationTime": + delete_on = "_creationTimeUnix" + elif delete_on == "updateTime": + delete_on = "_lastUpdateTimeUnix" + + out: dict = { + "enabled": self.enabled, + "filterExpiredObjects": self.filter_expired_objects, + "deleteOn": delete_on, + } + if self.time_to_live is not None: + out["defaultTtl"] = int(self.time_to_live.total_seconds()) + return out + ObjectTTLConfig = _ObjectTTLConfig From 59a7f94ff14f397782ccec5545cae6e0a2f74307 Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Fri, 20 Feb 2026 18:40:12 -0300 Subject: [PATCH 2/6] improve test to actually test the reimport --- integration/test_collection_config.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/integration/test_collection_config.py b/integration/test_collection_config.py index 0bbe1d257..7a681d174 100644 --- a/integration/test_collection_config.py +++ b/integration/test_collection_config.py @@ -2028,7 +2028,9 @@ def test_object_ttl_roundtrip_from_dict( for schema, expected_ttl_dict in test_cases: name = schema["class"] + reimport_name = name + "Reimport" client.collections.delete(name) + client.collections.delete(reimport_name) try: client.collections.create_from_dict(schema) config = client.collections.export_config(name) @@ -2037,5 +2039,20 @@ def test_object_ttl_roundtrip_from_dict( f"Round-trip mismatch for {name}: " f"got {config.object_ttl_config.to_dict()}, expected {expected_ttl_dict}" ) + + # Guard against schema round-trip regression (#1957): + # export the full collection schema dict and re-import it. + exported_dict = config.to_dict() + exported_dict["class"] = reimport_name + client.collections.create_from_dict(exported_dict) + reimport_config = client.collections.export_config(reimport_name) + assert reimport_config.object_ttl_config is not None, ( + f"object_ttl_config is None after schema round-trip for {reimport_name}" + ) + assert reimport_config.object_ttl_config.to_dict() == expected_ttl_dict, ( + f"Schema round-trip mismatch for {reimport_name}: " + f"got {reimport_config.object_ttl_config.to_dict()}, expected {expected_ttl_dict}" + ) finally: client.collections.delete(name) + client.collections.delete(reimport_name) From bd3e34860ac29122774e7df857e8e4e1905d2365 Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Wed, 25 Feb 2026 17:09:55 -0300 Subject: [PATCH 3/6] Improve test_object_ttl_roundtrip_from_dict --- integration/test_collection_config.py | 121 ++++++++++++-------------- 1 file changed, 54 insertions(+), 67 deletions(-) diff --git a/integration/test_collection_config.py b/integration/test_collection_config.py index 7a681d174..483b0b9b9 100644 --- a/integration/test_collection_config.py +++ b/integration/test_collection_config.py @@ -1953,27 +1953,16 @@ def test_object_ttl_update(collection_factory: CollectionFactory) -> None: assert conf.object_ttl_config is None -def test_object_ttl_roundtrip_from_dict( - collection_factory: CollectionFactory, client_factory: ClientFactory -) -> None: - dummy = collection_factory("dummy") - if dummy._connection._weaviate_version.is_lower_than(1, 35, 0): - pytest.skip("object ttl is not supported in Weaviate versions lower than 1.35.0") - - client = client_factory() - - # (schema_to_create, expected_object_ttl_config_dict) - test_cases = [ +@pytest.mark.parametrize( + "create_kwargs,expected_ttl_dict", + [ # deleteOn: _creationTimeUnix ( { - "class": "CollectionTTLRoundtripCreation", - "objectTtlConfig": { - "enabled": True, - "defaultTtl": 60, - "deleteOn": "_creationTimeUnix", - "filterExpiredObjects": True, - }, + "object_ttl_config": Configure.ObjectTTL.delete_by_creation_time( + time_to_live=60, + filter_expired_objects=True, + ), }, { "enabled": True, @@ -1985,13 +1974,10 @@ def test_object_ttl_roundtrip_from_dict( # deleteOn: _lastUpdateTimeUnix ( { - "class": "CollectionTTLRoundtripUpdate", - "objectTtlConfig": { - "enabled": True, - "defaultTtl": 3600, - "deleteOn": "_lastUpdateTimeUnix", - "filterExpiredObjects": True, - }, + "object_ttl_config": Configure.ObjectTTL.delete_by_update_time( + time_to_live=3600, + filter_expired_objects=True, + ), }, { "enabled": True, @@ -2003,19 +1989,12 @@ def test_object_ttl_roundtrip_from_dict( # deleteOn: custom date property ( { - "class": "CollectionTTLRoundtripDateProp", - "properties": [ - { - "name": "reference_date", - "dataType": ["date"], - } - ], - "objectTtlConfig": { - "enabled": True, - "defaultTtl": 123, - "deleteOn": "reference_date", - "filterExpiredObjects": True, - }, + "properties": [Property(name="reference_date", data_type=DataType.DATE)], + "object_ttl_config": Configure.ObjectTTL.delete_by_date_property( + property_name="reference_date", + ttl_offset=123, + filter_expired_objects=True, + ), }, { "enabled": True, @@ -2024,35 +2003,43 @@ def test_object_ttl_roundtrip_from_dict( "filterExpiredObjects": True, }, ), - ] + ], +) +def test_object_ttl_roundtrip_from_dict( + client_factory: ClientFactory, + create_kwargs: dict, + expected_ttl_dict: dict, +) -> None: + client = client_factory() + if client._connection._weaviate_version.is_lower_than(1, 35, 0): + pytest.skip("object ttl is not supported in Weaviate versions lower than 1.35.0") + + name = "TTLRoundtrip" + reimport_name = "TTLRoundtripReimport" + client.collections.delete(name) + client.collections.delete(reimport_name) + try: + collection = client.collections.create(name=name, **create_kwargs) + config = collection.config.get() + assert config.object_ttl_config is not None, f"object_ttl_config is None for {name}" + assert config.object_ttl_config.to_dict() == expected_ttl_dict, ( + f"Round-trip mismatch for {name}: " + f"got {config.object_ttl_config.to_dict()}, expected {expected_ttl_dict}" + ) - for schema, expected_ttl_dict in test_cases: - name = schema["class"] - reimport_name = name + "Reimport" + # Guard against schema round-trip regression (#1957): + # export the full collection schema dict and re-import it. + exported_dict = config.to_dict() + exported_dict["class"] = reimport_name + client.collections.create_from_dict(exported_dict) + reimport_config = client.collections.export_config(reimport_name) + assert reimport_config.object_ttl_config is not None, ( + f"object_ttl_config is None after schema round-trip for {reimport_name}" + ) + assert reimport_config.object_ttl_config.to_dict() == expected_ttl_dict, ( + f"Schema round-trip mismatch for {reimport_name}: " + f"got {reimport_config.object_ttl_config.to_dict()}, expected {expected_ttl_dict}" + ) + finally: client.collections.delete(name) client.collections.delete(reimport_name) - try: - client.collections.create_from_dict(schema) - config = client.collections.export_config(name) - assert config.object_ttl_config is not None, f"object_ttl_config is None for {name}" - assert config.object_ttl_config.to_dict() == expected_ttl_dict, ( - f"Round-trip mismatch for {name}: " - f"got {config.object_ttl_config.to_dict()}, expected {expected_ttl_dict}" - ) - - # Guard against schema round-trip regression (#1957): - # export the full collection schema dict and re-import it. - exported_dict = config.to_dict() - exported_dict["class"] = reimport_name - client.collections.create_from_dict(exported_dict) - reimport_config = client.collections.export_config(reimport_name) - assert reimport_config.object_ttl_config is not None, ( - f"object_ttl_config is None after schema round-trip for {reimport_name}" - ) - assert reimport_config.object_ttl_config.to_dict() == expected_ttl_dict, ( - f"Schema round-trip mismatch for {reimport_name}: " - f"got {reimport_config.object_ttl_config.to_dict()}, expected {expected_ttl_dict}" - ) - finally: - client.collections.delete(name) - client.collections.delete(reimport_name) From de7f82bd3c986b9ad26ab935265ab08925dbe409 Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Thu, 26 Feb 2026 09:22:54 -0300 Subject: [PATCH 4/6] Good old linting --- integration/test_collection_config.py | 1 - test/collection/test_config.py | 1 - 2 files changed, 2 deletions(-) diff --git a/integration/test_collection_config.py b/integration/test_collection_config.py index b85a0e88d..b545b99cb 100644 --- a/integration/test_collection_config.py +++ b/integration/test_collection_config.py @@ -7,7 +7,6 @@ import weaviate import weaviate.classes as wvc from integration.conftest import ( - ClientFactory, CollectionFactory, OpenAICollection, _sanitize_collection_name, diff --git a/test/collection/test_config.py b/test/collection/test_config.py index f2fcc3037..97ae5aa73 100644 --- a/test/collection/test_config.py +++ b/test/collection/test_config.py @@ -20,7 +20,6 @@ _ReplicationConfigCreate, ReplicationDeletionStrategy, ) -from weaviate.collections.classes.config_methods import _get_object_ttl_config from weaviate.collections.classes.config_named_vectors import _NamedVectorConfigCreate from weaviate.collections.classes.config_vectorizers import ( Multi2VecField, From 59db1b87de153df68239aad8a0b4dca7a3d3cef9 Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Thu, 26 Feb 2026 11:48:07 -0300 Subject: [PATCH 5/6] Keep only integration tests --- integration/test_collection_config.py | 25 ++++++++ test/collection/test_config.py | 86 +-------------------------- 2 files changed, 26 insertions(+), 85 deletions(-) diff --git a/integration/test_collection_config.py b/integration/test_collection_config.py index b545b99cb..d0693ee33 100644 --- a/integration/test_collection_config.py +++ b/integration/test_collection_config.py @@ -1953,6 +1953,31 @@ def test_object_ttl_update(collection_factory: CollectionFactory) -> None: assert conf.object_ttl_config is None +def test_object_ttl_roundtrip_from_dict(collection_factory: CollectionFactory) -> None: + dummy = collection_factory("dummy") + if dummy._connection._weaviate_version.is_lower_than(1, 35, 0): + pytest.skip("object ttl is not supported in Weaviate versions lower than 1.35.0") + + collection = collection_factory( + object_ttl=Configure.ObjectTTL.delete_by_creation_time( + time_to_live=datetime.timedelta(seconds=60), + filter_expired_objects=True, + ), + ) + config = collection.config.get() + assert config.object_ttl_config is not None + + name = f"TestObjectTTLRoundtrip{collection.name}" + config.name = name + with weaviate.connect_to_local() as client: + client.collections.delete(name) + client.collections.create_from_dict(config.to_dict()) + new = client.collections.use(name).config.get() + assert config == new + assert config.to_dict() == new.to_dict() + client.collections.delete(name) + + @pytest.mark.parametrize("index_name", ["filterable", "searchable", "rangeFilters"]) def test_delete_property_index( index_name: IndexName, collection_factory: CollectionFactory diff --git a/test/collection/test_config.py b/test/collection/test_config.py index 97ae5aa73..2cef4dd2d 100644 --- a/test/collection/test_config.py +++ b/test/collection/test_config.py @@ -1,4 +1,3 @@ -from datetime import timedelta from typing import List, Union import pytest @@ -14,7 +13,7 @@ Vectorizers, _CollectionConfigCreate, _GenerativeProvider, - _ObjectTTLConfig, + _RerankerProvider, _VectorizerConfigCreate, _ReplicationConfigCreate, @@ -2614,89 +2613,6 @@ def test_config_with_vectors(vector_config: List[_VectorConfigCreate], expected: } -TEST_OBJECT_TTL_CONFIG_TO_DICT_PARAMETERS = [ - # delete_by_creation_time - ( - _ObjectTTLConfig( - enabled=True, - time_to_live=timedelta(hours=24), - filter_expired_objects=True, - delete_on="creationTime", - ), - { - "enabled": True, - "defaultTtl": 86400, - "filterExpiredObjects": True, - "deleteOn": "_creationTimeUnix", - }, - ), - # delete_by_update_time - ( - _ObjectTTLConfig( - enabled=True, - time_to_live=timedelta(days=7), - filter_expired_objects=False, - delete_on="updateTime", - ), - { - "enabled": True, - "defaultTtl": 604800, - "filterExpiredObjects": False, - "deleteOn": "_lastUpdateTimeUnix", - }, - ), - # delete_by_date_property - ( - _ObjectTTLConfig( - enabled=True, - time_to_live=timedelta(hours=1, minutes=30), - filter_expired_objects=True, - delete_on="releaseDate", - ), - { - "enabled": True, - "defaultTtl": 5400, - "filterExpiredObjects": True, - "deleteOn": "releaseDate", - }, - ), - # None time_to_live - ( - _ObjectTTLConfig( - enabled=True, - time_to_live=None, - filter_expired_objects=False, - delete_on="creationTime", - ), - { - "enabled": True, - "filterExpiredObjects": False, - "deleteOn": "_creationTimeUnix", - }, - ), - # negative offset (delete_by_date_property with offset before date) - ( - _ObjectTTLConfig( - enabled=True, - time_to_live=timedelta(seconds=-3600), - filter_expired_objects=True, - delete_on="eventDate", - ), - { - "enabled": True, - "defaultTtl": -3600, - "filterExpiredObjects": True, - "deleteOn": "eventDate", - }, - ), -] - - -@pytest.mark.parametrize("ttl_config,expected", TEST_OBJECT_TTL_CONFIG_TO_DICT_PARAMETERS) -def test_object_ttl_config_to_dict(ttl_config: _ObjectTTLConfig, expected: dict) -> None: - """Test that _ObjectTTLConfig.to_dict() properly converts timedelta to seconds.""" - assert ttl_config.to_dict() == expected - TEST_CONFIGURE_WITH_REPLICATION_PARAMETERS = [ (Configure.replication(), {}), From b078f7888b56d536c9782731825269966fbb44c3 Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Thu, 26 Feb 2026 11:52:37 -0300 Subject: [PATCH 6/6] linting... --- test/collection/test_config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/collection/test_config.py b/test/collection/test_config.py index 2cef4dd2d..65af47504 100644 --- a/test/collection/test_config.py +++ b/test/collection/test_config.py @@ -13,7 +13,6 @@ Vectorizers, _CollectionConfigCreate, _GenerativeProvider, - _RerankerProvider, _VectorizerConfigCreate, _ReplicationConfigCreate, @@ -2613,7 +2612,6 @@ def test_config_with_vectors(vector_config: List[_VectorConfigCreate], expected: } - TEST_CONFIGURE_WITH_REPLICATION_PARAMETERS = [ (Configure.replication(), {}), (