Skip to content

ANN rewrite misses the index unless the table is referenced with all three parts #32

Description

@rohan-hotdata

Summary

The ANN rewrite fires only when the query spells the table with a fully qualified
three-part reference
(catalog.schema.table). A two-part (schema.table) or bare
(table) reference silently misses the index lookup and falls back to a full scan —
correct results, no error, no warning, no log line.

The registry key is built from the table reference exactly as written in the query,
while registration keys the index by its resolved name. Only one of the two sides
normalizes, so the keys agree only by accident of how the user typed the FROM clause.

Reproduction

Verified against production (api.hotdata.dev, workspace roh-workspace) on a managed
database table with a status=ready cosine index over a 1536-dim List(Float32) column.
Same index, same query, same session — only the FROM clause differs:

FROM clause Plan
public.clean_docs DataSourceExec … file_type=liquid_parquet — full scan
"default"."public"."clean_docs" USearchExec: k=5, filtered=false

The two-part form was re-run after the three-part form succeeded, so the index was
loaded and warm in the registry. It still full-scanned. This is deterministic on the
shape of the reference, not a lazy-load or cache-warmth artifact.

Root cause

Two sides must agree on a registry key, and only one normalizes.

Lookup (this crate) — does not normalize. The rule builds its key from the raw
TableReference carried in the plan. table_ref_to_str (src/rule.rs:390) joins
whichever parts happen to be present:

  • Fullcatalog::schema::table (3 parts)
  • Partialschema::table (2 parts)
  • Baretable (1 part)

src/rule.rs:209 then appends the column, producing a 4-, 3-, or 2-part key.

DataFusion preserves the reference as written — it resolves the name to find the table
provider, but does not rewrite TableScan.table_name into a canonical form. Observable
in the two plans above: TableScan: public.clean_docs vs
TableScan: default.public.clean_docs.

Registration (runtimedb) — normalizes. refresh_for_tables
(runtimedb/src/vector/mod.rs:1373) calls
normalize_table_ref(table_ref, default_catalog, default_schema) to fill in the missing
catalog/schema, translates the visible catalog alias to the real connection name, and
registers under a always-4-part key (mod.rs:1686):

__db_h4tn5esw2roy7zg4sh1fqv8rov::public::clean_docs::embedding

So a two-part reference produces public::clean_docs::embedding and misses the map.
It never even reaches the alias translation: ScopedVectorIndexResolver::translate
(mod.rs:1211) only rewrites keys of exactly 4 parts and passes anything shorter through
untouched (mod.rs:1213).

peek returns None, build_rewrite bails at src/rule.rs:212, and because that is the
same return path used for legitimate declines (metric mismatch, DESC, SELECT *,
stacked filters), nothing is logged.

Fix

Resolve the table reference against the session defaults before building the key, so all
three written forms collapse to the same canonical 4-part key and the lookup side becomes
symmetric with the registration side.

DataFusion provides both halves already, and the rule currently discards them:

  • TableReference::resolve(default_catalog, default_schema) returns a
    ResolvedTableReference, which is three parts by construction.
  • OptimizerConfig::options() exposes catalog.default_catalog and
    catalog.default_schema; runtimedb sets both per query
    (runtimedb/src/engine/query.rs:1179-1180).

The rule already receives that config and throws it away — the parameter is named
_config at src/rule.rs:344. It needs threading from rewrite() through try_match
into build_rewrite.

Note this makes index matching agree with table resolution; it does not make more names
valid. A bare reference still resolves against the default schema, so on a database whose
default schema is main while the table lives in public, FROM clean_docs continues to
miss — exactly as plain SQL does.

Also worth fixing: the silence

The miss is indistinguishable from a legitimate decline. A debug! (or trace!) on
"distance shape matched but no index registered under key X" would have made this a
five-minute diagnosis. Worth adding alongside the fix, independent of it.

Impact

Anything that emits a short reference silently loses acceleration:

  • hand-written SQL via the CLI or execute_sql
  • LLM-authored SQL in agent tool surfaces — notably hotdata-langchain's
    tools.py:73, whose prompt currently tells the model "schema-qualified names also
    resolve", which is true for correctness and misleading for performance
    (tracked separately in hotdata-langchain)

Not affected, both verified by EXPLAIN against production:

  • hotdata-ibis — always emits the catalog explicitly
    (sg.table(table_name, db=schema_name, catalog=conn), backend.py:455)
  • hotdata-langchain's vectorstore and search paths — table_ref hardcodes the
    three-part form (vectorstore.py:171), and search.py:28 rejects anything else

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions