From 68db48ad1b37cb9bef7703604574497a5bbf42b5 Mon Sep 17 00:00:00 2001 From: kzahiri1 Date: Fri, 2 Oct 2026 16:31:53 -0700 Subject: [PATCH 1/2] fix(config): write object TTL updates under objectTtlConfig The update path serialized objectTTLConfig while create and the server use objectTtlConfig, so existing TTL fields were dropped on merge. Co-authored-by: Cursor --- test/collection/test_config.py | 22 ++++++++++++++++++++++ weaviate/collections/classes/config.py | 9 +++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/test/collection/test_config.py b/test/collection/test_config.py index 360a0d138..fa02a918e 100644 --- a/test/collection/test_config.py +++ b/test/collection/test_config.py @@ -14,6 +14,7 @@ Vectorizers, _AsyncReplicationConfig, _CollectionConfigCreate, + _CollectionConfigUpdate, _GenerativeProvider, _ReplicationConfig, _ReplicationConfigUpdate, @@ -30,6 +31,7 @@ VectorDistances, ) from weaviate.collections.classes.config_vectors import _VectorConfigCreate +from weaviate.collections.classes.config_object_ttl import ObjectTTLConfigUpdate from weaviate.exceptions import WeaviateInsertInvalidPropertyError DEFAULTS = { @@ -3529,3 +3531,23 @@ def test_reconfigure_inverted_index_without_stopword_presets_leaves_existing(sel } merged = rc.merge_with_existing(existing) assert merged["stopwordPresets"] == {"fr": ["le", "la"]} + + +def test_object_ttl_update_uses_server_schema_key(): + stored = { + "objectTtlConfig": { + "enabled": True, + "deleteOn": "expiresAt", + "defaultTtl": 3600, + "filterExpiredObjects": False, + } + } + update = _CollectionConfigUpdate( + object_ttl_config=ObjectTTLConfigUpdate(enabled=True, filterExpiredObjects=True) + ) + merged = update.merge_with_existing(stored) + + assert "objectTTLConfig" not in merged + assert merged["objectTtlConfig"]["filterExpiredObjects"] is True + assert merged["objectTtlConfig"]["defaultTtl"] == 3600 + assert merged["objectTtlConfig"]["deleteOn"] == "expiresAt" diff --git a/weaviate/collections/classes/config.py b/weaviate/collections/classes/config.py index 390578756..7b088ae72 100644 --- a/weaviate/collections/classes/config.py +++ b/weaviate/collections/classes/config.py @@ -1616,7 +1616,7 @@ class _CollectionConfigUpdate(_ConfigUpdateModel): invertedIndexConfig: Optional[InvertedIndexConfigUpdate] = Field( default=None, alias="inverted_index_config" ) - objectTTLConfig: Optional[ObjectTTLConfigUpdate] = Field( + objectTtlConfig: Optional[ObjectTTLConfigUpdate] = Field( default=None, alias="object_ttl_config" ) replicationConfig: Optional[ReplicationConfigUpdate] = Field( @@ -1745,10 +1745,11 @@ def merge_with_existing(self, schema: Dict[str, Any]) -> Dict[str, Any]: schema["multiTenancyConfig"] = self.multiTenancyConfig.merge_with_existing( schema["multiTenancyConfig"] ) - if self.objectTTLConfig is not None: - schema["objectTTLConfig"] = self.objectTTLConfig.merge_with_existing( - schema.get("objectTTLConfig", {}) + if self.objectTtlConfig is not None: + schema["objectTtlConfig"] = self.objectTtlConfig.merge_with_existing( + schema.get("objectTtlConfig") or schema.get("objectTTLConfig") or {} ) + schema.pop("objectTTLConfig", None) if self.vectorIndexConfig is not None: self.__check_quantizers(self.vectorIndexConfig.quantizer, schema["vectorIndexConfig"]) schema["vectorIndexConfig"] = self.vectorIndexConfig.merge_with_existing( From a57672502ab3ad47e9a665149c80876a09594df4 Mon Sep 17 00:00:00 2001 From: Ivan Despot <66276597+g-despot@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:59:36 +0200 Subject: [PATCH 2/2] fix(config): merge object TTL updates from the stored objectTtlConfig only The server never sends objectTTLConfig, so the fallback and the pop could not run. Read the stored block the way the other config arms do. Move the test next to the other merge_with_existing cases and cover the first-time enable and the disable paths too. --- test/collection/test_config.py | 22 ----------- test/collection/test_config_update.py | 52 ++++++++++++++++++++++++++ weaviate/collections/classes/config.py | 3 +- 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/test/collection/test_config.py b/test/collection/test_config.py index fa02a918e..360a0d138 100644 --- a/test/collection/test_config.py +++ b/test/collection/test_config.py @@ -14,7 +14,6 @@ Vectorizers, _AsyncReplicationConfig, _CollectionConfigCreate, - _CollectionConfigUpdate, _GenerativeProvider, _ReplicationConfig, _ReplicationConfigUpdate, @@ -31,7 +30,6 @@ VectorDistances, ) from weaviate.collections.classes.config_vectors import _VectorConfigCreate -from weaviate.collections.classes.config_object_ttl import ObjectTTLConfigUpdate from weaviate.exceptions import WeaviateInsertInvalidPropertyError DEFAULTS = { @@ -3531,23 +3529,3 @@ def test_reconfigure_inverted_index_without_stopword_presets_leaves_existing(sel } merged = rc.merge_with_existing(existing) assert merged["stopwordPresets"] == {"fr": ["le", "la"]} - - -def test_object_ttl_update_uses_server_schema_key(): - stored = { - "objectTtlConfig": { - "enabled": True, - "deleteOn": "expiresAt", - "defaultTtl": 3600, - "filterExpiredObjects": False, - } - } - update = _CollectionConfigUpdate( - object_ttl_config=ObjectTTLConfigUpdate(enabled=True, filterExpiredObjects=True) - ) - merged = update.merge_with_existing(stored) - - assert "objectTTLConfig" not in merged - assert merged["objectTtlConfig"]["filterExpiredObjects"] is True - assert merged["objectTtlConfig"]["defaultTtl"] == 3600 - assert merged["objectTtlConfig"]["deleteOn"] == "expiresAt" diff --git a/test/collection/test_config_update.py b/test/collection/test_config_update.py index c7ddd6ab4..073b645b9 100644 --- a/test/collection/test_config_update.py +++ b/test/collection/test_config_update.py @@ -1,7 +1,10 @@ +from typing import Optional + import pytest from test.collection.schema import multi_vector_schema from weaviate.collections.classes.config import ( + ObjectTTLConfigUpdate, Reconfigure, _CollectionConfigUpdate, ) @@ -162,6 +165,55 @@ def test_replication_async_config_reset_all_fields() -> None: assert result["asyncConfig"] == {} +@pytest.mark.parametrize( + "stored,update,expected", + [ + ( + { + "enabled": True, + "deleteOn": "expiresAt", + "defaultTtl": 3600, + "filterExpiredObjects": False, + }, + ObjectTTLConfigUpdate(enabled=True, filterExpiredObjects=True), + { + "enabled": True, + "deleteOn": "expiresAt", + "defaultTtl": 3600, + "filterExpiredObjects": True, + }, + ), + ( + None, + Reconfigure.ObjectTTL.delete_by_update_time(time_to_live=3600), + {"enabled": True, "deleteOn": "_lastUpdateTimeUnix", "defaultTtl": 3600}, + ), + ( + { + "enabled": True, + "deleteOn": "expiresAt", + "defaultTtl": 3600, + "filterExpiredObjects": False, + }, + Reconfigure.ObjectTTL.disable(), + { + "enabled": False, + "deleteOn": "expiresAt", + "defaultTtl": 3600, + "filterExpiredObjects": False, + }, + ), + ], +) +def test_object_ttl_update_merges_into_objectTtlConfig( + stored: Optional[dict], update: ObjectTTLConfigUpdate, expected: dict +) -> None: + schema = {"class": "Test"} if stored is None else {"class": "Test", "objectTtlConfig": stored} + merged = _CollectionConfigUpdate(object_ttl_config=update).merge_with_existing(schema) + assert "objectTTLConfig" not in merged + assert merged["objectTtlConfig"] == expected + + def _hfresh_schema(rescore_limit: int = 20) -> dict: """An HFresh schema, which mandates RQ and so carries no pq/bq/sq blocks.""" return { diff --git a/weaviate/collections/classes/config.py b/weaviate/collections/classes/config.py index 7b088ae72..62761a3ca 100644 --- a/weaviate/collections/classes/config.py +++ b/weaviate/collections/classes/config.py @@ -1747,9 +1747,8 @@ def merge_with_existing(self, schema: Dict[str, Any]) -> Dict[str, Any]: ) if self.objectTtlConfig is not None: schema["objectTtlConfig"] = self.objectTtlConfig.merge_with_existing( - schema.get("objectTtlConfig") or schema.get("objectTTLConfig") or {} + schema.get("objectTtlConfig", {}) ) - schema.pop("objectTTLConfig", None) if self.vectorIndexConfig is not None: self.__check_quantizers(self.vectorIndexConfig.quantizer, schema["vectorIndexConfig"]) schema["vectorIndexConfig"] = self.vectorIndexConfig.merge_with_existing(