fix(kg): tighten temporal handling and cleanup

This commit is contained in:
fatkobra 2026-05-08 15:27:54 +00:00
parent 29c0c8059e
commit 1fbcb739c9
4 changed files with 23 additions and 26 deletions

View File

@ -93,14 +93,16 @@ def sanitize_kg_value(value: str, field_name: str = "value") -> str:
# Accepted:
# YYYY-MM-DD
# YYYY-MM-DDTHH:MM:SSZ
# YYYY-MM-DDTHH:MM:SS+00:00 (normalized to ...Z)
#
# Rejected:
# partial dates, naive datetimes, timezone offsets, fractional seconds,
# and SQLite-style space-separated datetimes.
# partial dates, naive datetimes, non-UTC timezone offsets, fractional
# seconds, and SQLite-style space-separated datetimes.
_ISO_DATE_RE = re.compile(r"^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$")
_ISO_UTC_DATETIME_RE = re.compile(
r"^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])" r"T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\dZ$"
r"^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])"
r"T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:Z|\+00:00)$"
)
@ -127,6 +129,7 @@ def sanitize_iso_temporal(value, field_name: str = "date"):
- ``YYYY-MM-DD``
- ``YYYY-MM-DDTHH:MM:SSZ``
- ``YYYY-MM-DDTHH:MM:SS+00:00`` normalized to ``...Z``
Partial dates are rejected because KG queries compare TEXT temporal values.
Non-canonical datetime forms are rejected because mixed temporal string
@ -148,6 +151,9 @@ def sanitize_iso_temporal(value, field_name: str = "date"):
"(expected YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ)"
) from None
if value.endswith("+00:00"):
value = f"{value[:-6]}Z"
return value

View File

@ -81,20 +81,6 @@ def _temporal_end_key(value: Optional[str]) -> Optional[str]:
return value
def _triple_valid_at(valid_from: Optional[str], valid_to: Optional[str], as_of: str) -> bool:
as_of_key = _temporal_start_key(as_of)
valid_from_key = _temporal_start_key(valid_from)
valid_to_key = _temporal_end_key(valid_to)
if valid_from_key is not None and valid_from_key > as_of_key:
return False
if valid_to_key is not None and valid_to_key < as_of_key:
return False
return True
def _sql_temporal_start_expr(column: str) -> str:
"""SQLite expression for comparing valid_from-style temporal values."""
@ -230,15 +216,6 @@ class KnowledgeGraph:
self.close()
return False
def __del__(self):
"""Best-effort cleanup for callers/tests that forget to call close()."""
try:
self.close()
except Exception:
# Destructors must never raise, especially during interpreter
# shutdown when module globals may already be partially torn down.
pass
def _entity_id(self, name: str) -> str:
return name.lower().replace(" ", "_").replace("'", "")

View File

@ -42,6 +42,16 @@ def _reset_mcp_cache():
try:
from mempalace import mcp_server
for kg in list(getattr(mcp_server, "_kg_by_path", {}).values()):
close = getattr(kg, "close", None)
if close is not None:
try:
close()
except Exception:
pass
if hasattr(mcp_server, "_kg_by_path"):
mcp_server._kg_by_path.clear()
mcp_server._client_cache = None
mcp_server._collection_cache = None
except (ImportError, AttributeError):

View File

@ -341,3 +341,7 @@ def test_iso_temporal_rejects_invalid_calendar_date():
def test_iso_temporal_error_names_field():
with pytest.raises(ValueError, match="as_of"):
sanitize_iso_temporal("2026-05-06T14:23", "as_of")
def test_iso_temporal_normalizes_plus_zero_offset_to_z():
assert sanitize_iso_temporal("2026-05-06T14:23:00+00:00") == "2026-05-06T14:23:00Z"