Skip to content

Search cutoff is wrong for snapshots that only exist on a non-main branch #131

Open

Description

@YanivZalach

Concept

search_cutoff needs, for the selected start/end snapshot, the window of metadata.json files that reference it. Today it gets that window by querying metadata_log_entries filtered on latest_snapshot_id.

latest_snapshot_id is main-branch resolved: it is whatever the current-snapshot pointer was at the moment each metadata.json was written. A snapshot that only ever lived on a branch (or a tag) is never that value.

Problem

For a branch-only snapshot, both lookups in backend/search_cutoff/find_search_cutoff.py return NULL and the code silently falls back to the snapshot's own committed_at:

  • _get_start_cutoffs line 97
  • _get_end_cutoffs line 141

Consequences:

  • Start ends up roughly right, off by at most one metadata file.
  • End is genuinely wrong. Every metadata.json written after the branch commit that still carries the ref falls outside the window.

User-visible symptom: metadata file nodes are missing from the graph when the selected range ends on a snapshot that lives on a non-main branch.

Root cause is an Iceberg metadata-table gap: no metadata table exposes branch refs per metadata file. refs gives only the current refs, metadata_log_entries gives only the main pointer. The branch-to-snapshot mapping over time exists only inside the metadata.json files themselves.

How to solve

Keep the current fast path. Add a fallback that runs only when the metadata_log_entries lookup comes back empty, which is already the exact signal for "this snapshot was never on main". No branch-detection heuristic is needed.

The fallback reads the metadata files directly:

  1. Take candidate file paths from metadata_log_entries, starting one entry before the snapshot's committed_at (absorbs the skew between snapshot commit time and metadata write time) and walking forward.
  2. Read them in a single spark.read.schema(...).json(paths) with a minimal explicit schema: refs as map<string, struct<snapshot-id: bigint>> plus last-updated-ms. One pass, no per-file round trip.
  3. Keep rows whose refs values contain the selected snapshot id.
  4. MIN(last-updated-ms) is the start metadata cutoff, MAX(...) is the end. If the newest metadata file still matches, the end stays open (arrow.Arrow.max).

Doing it as one min/max instead of a sequential walk also handles a ref that disappears and reappears, without extra code.

Constraints

  • Bound the candidate set with a new MAX_* env var defined in backend/env.py, so a table with thousands of metadata files cannot turn this into a full scan.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendbugSomething isn't working

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions