Skip to content
Merged
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
14 changes: 11 additions & 3 deletions src/surreal_memory/storage/surrealdb/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -1278,9 +1278,14 @@ async def find_neurons(

if time_range is not None:
start, end = time_range
# Bind datetimes, not ISO strings: created_at is a datetime column, and
# SurrealDB resolves a cross-type comparison by type rank rather than by
# value, so a string bound here makes the predicate CONSTANT - and which
# constant depends on the operator: `datetime >= string` is always true,
# `datetime <= string` always false. Here the two combine to match nothing.
conditions.append("created_at >= $time_start AND created_at <= $time_end")
params["time_start"] = start.isoformat()
params["time_end"] = end.isoformat()
params["time_start"] = start
params["time_end"] = end

if ephemeral is not None:
conditions.append("ephemeral = $ephemeral")
Expand Down Expand Up @@ -2726,7 +2731,10 @@ async def get_enhanced_stats(
"SELECT count() AS c FROM fiber "
"WHERE brain_id = $bid AND created_at >= $today GROUP ALL",
bid=brain_id,
today=today.isoformat(),
# Bind the datetime, not an ISO string - see find_neurons above.
# Here the comparison is one-sided (>=), where a string bound makes
# the predicate constantly TRUE, so the count silently became "all".
today=today,
),
self._query(
"SELECT * FROM neuron_state WHERE brain_id = $bid "
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/_surrealdb_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"parity-test-surreal", # test_get_project_memories.py
"snapshot-roundtrip-live", # test_surrealdb_export_import_live.py
"snapshot-roundtrip-live-target", # test_surrealdb_export_import_live.py
"neuron-snapshot-live", # test_surrealdb_neuron_snapshot_live.py
"time-range-filter-live", # test_surrealdb_time_range_filter_live.py
"pinned-expiry-test-9f3a1c", # test_surrealdb_expiry_respects_pinned_live.py
"bug006-tm-delete-id-live", # test_surrealdb_typed_memory_delete_id_live.py
"kw-df-batch-live-4b8d2e", # test_surrealdb_keyword_df_live.py
Expand Down
138 changes: 138 additions & 0 deletions tests/unit/test_surrealdb_time_range_filter_live.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""Regression: find_neurons(time_range=...) must select by value, not by type rank.

`created_at` is a datetime column, but the query bound `$time_start`/`$time_end` as
ISO strings. SurrealDB resolves a comparison between two different types by type
rank rather than by value, so `created_at >= $time_start AND created_at <= $time_end`
became a constant predicate and the filter matched nothing at all - including rows
squarely inside the window.

The in-memory backend compares Python objects and so cannot reproduce this; only a
live engine can. The test is skipped when SURREALDB_URL is unset so CI without
docker still passes.
"""

from __future__ import annotations

import dataclasses
import os
from datetime import timedelta

import pytest

from surreal_memory.core.brain import Brain
from surreal_memory.core.fiber import Fiber
from surreal_memory.core.neuron import Neuron, NeuronType
from surreal_memory.utils.timeutils import utcnow
from tests.unit._surrealdb_live import cleanup_live_brains, ensure_real_surrealdb_sdk

SURREALDB_URL = os.getenv("SURREALDB_URL")

pytestmark = pytest.mark.skipif(
not SURREALDB_URL,
reason="requires SURREALDB_URL env var pointing to a running SurrealDB",
)


@pytest.fixture
async def surrealdb_storage(): # type: ignore[no-untyped-def]
ensure_real_surrealdb_sdk()
from surreal_memory.storage.surrealdb.store import SurrealDBStorage

storage = SurrealDBStorage(url=SURREALDB_URL)
await storage.initialize()
brain = Brain.create(name="time-range-filter-live")
await storage.save_brain(brain)
storage.set_brain(brain.id)
yield storage
try:
await cleanup_live_brains(storage, own_brain_id=brain.id)
except Exception:
pass
try:
await storage.close()
except Exception:
pass


@pytest.mark.asyncio
async def test_time_range_returns_rows_inside_the_window(surrealdb_storage) -> None: # type: ignore[no-untyped-def]
"""A window around now returns the neurons created now."""
now = utcnow()
for i in range(3):
await surrealdb_storage.add_neuron(
Neuron.create(type=NeuronType.CONCEPT, content=f"time-range probe {i}")
)

found = await surrealdb_storage.find_neurons(
time_range=(now - timedelta(hours=1), now + timedelta(hours=1)), limit=100
)
assert len(found) == 3, "a window containing the rows returned nothing"


@pytest.mark.asyncio
async def test_time_range_excludes_rows_outside_the_window(surrealdb_storage) -> None: # type: ignore[no-untyped-def]
"""Windows in the far past and far future match nothing - the filter narrows."""
now = utcnow()
await surrealdb_storage.add_neuron(
Neuron.create(type=NeuronType.CONCEPT, content="time-range probe outside")
)

future_start = now + timedelta(days=1)
future = await surrealdb_storage.find_neurons(
time_range=(future_start, future_start + timedelta(days=1)), limit=100
)
assert future == []

past_start = now - timedelta(days=2)
past = await surrealdb_storage.find_neurons(
time_range=(past_start, past_start + timedelta(days=1)), limit=100
)
assert past == []


@pytest.mark.asyncio
async def test_time_neuron_lookup_can_reach_storage(surrealdb_storage) -> None: # type: ignore[no-untyped-def]
"""The pipeline's TIME lookup can see a neuron through this filter at all.

This asserts reachability, not de-duplication: `_find_similar_time_neuron`
compares `created_at` (insert time) against a window around the hint's
midpoint (referenced time), which are different quantities, so it only
coincides when a neuron was inserted near the referenced moment. While the
filter matched nothing, the lookup could never return anything at all.
"""
from surreal_memory.engine.pipeline_steps import _find_similar_time_neuron

now = utcnow()
await surrealdb_storage.add_neuron(
Neuron.create(type=NeuronType.TIME, content="this afternoon")
)

existing = await _find_similar_time_neuron(surrealdb_storage, now)
assert existing is not None, "the TIME lookup could not see a neuron inserted just now"


@pytest.mark.asyncio
async def test_today_fibers_count_counts_only_today(surrealdb_storage) -> None: # type: ignore[no-untyped-def]
"""`today_fibers_count` must not count fibers created before today.

The same string-bound comparison appeared in get_enhanced_stats, but with a
one-sided `>=`, where the constant predicate is always TRUE - so the counter
reported every fiber the brain had ever held as created today. It surfaces in
`smem info` and on the dashboard.
"""
neuron = Neuron.create(type=NeuronType.CONCEPT, content="fiber anchor")
await surrealdb_storage.add_neuron(neuron)

fresh = Fiber.create(
neuron_ids={neuron.id}, synapse_ids=set(), anchor_neuron_id=neuron.id, summary="today"
)
await surrealdb_storage.add_fiber(fresh)

old = Fiber.create(
neuron_ids={neuron.id}, synapse_ids=set(), anchor_neuron_id=neuron.id, summary="old"
)
old = dataclasses.replace(old, created_at=utcnow() - timedelta(days=10))
await surrealdb_storage.add_fiber(old)

stats = await surrealdb_storage.get_enhanced_stats(surrealdb_storage.current_brain_id)
assert stats["today_fibers_count"] == 1, "the counter included a fiber created ten days ago"
Loading