Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions integration/test_collection_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import weaviate
import weaviate.classes as wvc
from integration.conftest import (
ClientFactory,
CollectionFactory,
OpenAICollection,
_sanitize_collection_name,
Expand Down Expand Up @@ -1950,3 +1951,108 @@ 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"]
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)
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)
54 changes: 47 additions & 7 deletions test/collection/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -2651,7 +2652,7 @@ def test_config_with_vectors(vector_config: List[_VectorConfigCreate], expected:
),
{
"enabled": True,
"timeToLive": 5400,
"defaultTtl": 5400,
"filterExpiredObjects": True,
"deleteOn": "releaseDate",
},
Expand All @@ -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)
Expand All @@ -2680,7 +2681,7 @@ def test_config_with_vectors(vector_config: List[_VectorConfigCreate], expected:
),
{
"enabled": True,
"timeToLive": -3600,
"defaultTtl": -3600,
"filterExpiredObjects": True,
"deleteOn": "eventDate",
},
Expand All @@ -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(
Expand Down
16 changes: 16 additions & 0 deletions weaviate/collections/classes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Comment on lines +1941 to +1947
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deleteOn value mapping (creationTime/updateTime <-> _creationTimeUnix/_lastUpdateTimeUnix) is now duplicated here and in config_methods._get_object_ttl_config. To reduce the chance of the two drifting over time, consider centralizing this mapping (e.g., module-level constants or a small helper used by both serialization and deserialization).

Copilot uses AI. Check for mistakes.
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

Expand Down
Loading