From 236aa8058ed9cd79785319d8a6692d21aeaa29a3 Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Fri, 20 Feb 2026 16:53:20 -0300 Subject: [PATCH 1/2] 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/2] 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)