DEV Community

Mads Hansen
Mads Hansen

Posted on

Zero rows is not proof that nothing exists

“No failed payments exist” is a much stronger claim than “this query returned zero rows.”

The result may be empty because:

  • tenant or environment scope was wrong
  • a partition has not arrived
  • one source timed out
  • a join removed unmatched records
  • authorization hid the matching rows
  • pagination or a timeout ended the search

So an MCP database result should distinguish:

  • none found in the complete authorized population
  • no visible matches in the caller's scope
  • source unavailable
  • incomplete search
  • not answerable
  • unknown

The final prose must preserve that state. “No visible matches” must never become “none exist.”

A negative-answer receipt should carry normalized scope, effective identity and policy, expected and observed sources, watermarks, counts before and after material joins, NULL/unmatched counts, pagination and truncation state, metric version, and trace ID.

Test the empty path deliberately: delayed sources, known hidden rows, unmatched foreign keys, timezone boundaries, stale definitions, and forced timeouts.

An empty result is data. An absence claim is a conclusion that needs evidence.

Full guide: Prove negative database answers before saying none exist

Top comments (1)

Collapse
 
yuhaixia profile image
Yuhai Xia

Semantic retrieval adds a seventh cause to that list and it's the one that fooled me longest. A vector search can come back empty because the match scored below a threshold, which means the answer was sitting right there and the ranker declined to show it. SQL fails the same way twice. Embedding recall doesn't. It never tells me it declined.

I run a memory server, so this is my entire failure surface. Two things I added after getting burned.

Every recall result I return carries the true total, not the returned count. "Here are 5" and "here are 5 of a real 89" are different answers, and only one of them is safe to summarize from. My enumerations come back with an explicit truncated flag for the same reason. A truncated list read as a complete one is your "none exist" in another costume.

I also stopped letting an empty semantic search stand as evidence. My rule now is that a literal exact-match pass has to run before anything may report nothing found. No LLM in that path. Plain substring, real total. It catches the case where the words are sitting in a stored record and the embedding simply didn't rank it. That happened often enough that I made it a rule instead of a habit.

The receipt is the part I'd take from you. I have most of those fields already. They're scattered across my responses. I've never shipped them as one object.