Skip to content

Commit daba485

Browse files
committed
fix(models): allow nullable created_at/updated_at in BaseRecord
When records are deserialized from external sources (databases, APIs, JSON responses), created_at and updated_at may legitimately be None. Previously this raised a pydantic.ValidationError because the fields were typed as datetime (non-nullable). Change both fields in BaseRecord from datetime to datetime | None. The default_factory still auto-populates with pendulum.now('UTC') for freshly created objects, so existing code is unaffected. This fixes the ValidationError reported in #8 where retrieving memory categories with None timestamps caused 16 validation errors. Closes #8
1 parent de30a37 commit daba485

File tree

2 files changed

+102
-2
lines changed

2 files changed

+102
-2
lines changed

src/memu/database/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ class BaseRecord(BaseModel):
3636
"""Backend-agnostic record interface."""
3737

3838
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
39-
created_at: datetime = Field(default_factory=lambda: pendulum.now("UTC"))
40-
updated_at: datetime = Field(default_factory=lambda: pendulum.now("UTC"))
39+
created_at: datetime | None = Field(default_factory=lambda: pendulum.now("UTC"))
40+
updated_at: datetime | None = Field(default_factory=lambda: pendulum.now("UTC"))
4141

4242

4343
class ToolCallResult(BaseModel):

tests/test_nullable_timestamps.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Unit tests for BaseRecord nullable datetime fields (issue #8 fix).
2+
3+
When records are deserialized from external sources (databases, APIs, JSON),
4+
``created_at`` and ``updated_at`` may be ``None``. Previously this raised a
5+
``pydantic.ValidationError`` because the fields were typed as ``datetime``
6+
(non-nullable). After the fix they accept ``None`` gracefully while still
7+
auto-populating with ``pendulum.now("UTC")`` for freshly created objects.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from datetime import datetime
13+
14+
import pytest
15+
16+
from memu.database.models import BaseRecord, CategoryItem, MemoryCategory, MemoryItem, Resource
17+
18+
19+
class TestBaseRecordNullableDatetime:
20+
"""Verify that ``created_at`` and ``updated_at`` accept ``None``."""
21+
22+
def test_default_factory_populates_datetime(self):
23+
"""Freshly created records should still get auto-populated timestamps."""
24+
record = BaseRecord()
25+
assert isinstance(record.created_at, datetime)
26+
assert isinstance(record.updated_at, datetime)
27+
28+
def test_explicit_none_accepted(self):
29+
"""Passing ``None`` explicitly must not raise a ValidationError."""
30+
record = BaseRecord(created_at=None, updated_at=None)
31+
assert record.created_at is None
32+
assert record.updated_at is None
33+
34+
def test_partial_none_accepted(self):
35+
"""One field ``None``, the other auto-populated — both valid."""
36+
record = BaseRecord(created_at=None)
37+
assert record.created_at is None
38+
assert isinstance(record.updated_at, datetime)
39+
40+
def test_memory_item_with_none_timestamps(self):
41+
"""MemoryItem inherits BaseRecord; should tolerate ``None`` timestamps."""
42+
item = MemoryItem(
43+
resource_id=None,
44+
memory_type="knowledge",
45+
summary="test memory",
46+
created_at=None,
47+
updated_at=None,
48+
)
49+
assert item.created_at is None
50+
assert item.updated_at is None
51+
52+
def test_memory_category_with_none_timestamps(self):
53+
"""MemoryCategory inherits BaseRecord; should tolerate ``None`` timestamps."""
54+
cat = MemoryCategory(
55+
name="test",
56+
description="test category",
57+
created_at=None,
58+
updated_at=None,
59+
)
60+
assert cat.created_at is None
61+
62+
def test_resource_with_none_timestamps(self):
63+
"""Resource inherits BaseRecord; should tolerate ``None`` timestamps."""
64+
res = Resource(
65+
url="https://example.com",
66+
modality="text",
67+
local_path="/tmp/file.txt",
68+
created_at=None,
69+
updated_at=None,
70+
)
71+
assert res.created_at is None
72+
73+
def test_category_item_with_none_timestamps(self):
74+
"""CategoryItem inherits BaseRecord; should tolerate ``None`` timestamps."""
75+
ci = CategoryItem(
76+
item_id="item-1",
77+
category_id="cat-1",
78+
created_at=None,
79+
updated_at=None,
80+
)
81+
assert ci.created_at is None
82+
83+
def test_model_dump_includes_none(self):
84+
"""Serialized output should faithfully represent ``None`` values."""
85+
record = BaseRecord(created_at=None, updated_at=None)
86+
data = record.model_dump()
87+
assert data["created_at"] is None
88+
assert data["updated_at"] is None
89+
90+
def test_model_validate_with_none(self):
91+
"""``model_validate`` from raw dict with ``None`` timestamps must work."""
92+
data = {
93+
"id": "abc-123",
94+
"created_at": None,
95+
"updated_at": None,
96+
}
97+
record = BaseRecord.model_validate(data)
98+
assert record.id == "abc-123"
99+
assert record.created_at is None
100+
assert record.updated_at is None

0 commit comments

Comments
 (0)