Skip to content

consistent KeyErrors across backends - #332

Open
TeunHuijben wants to merge 1 commit into
royerlab:mainfrom
TeunHuijben:fix/consistent-missing-attr-key-error
Open

consistent KeyErrors across backends#332
TeunHuijben wants to merge 1 commit into
royerlab:mainfrom
TeunHuijben:fix/consistent-missing-attr-key-error

Conversation

@TeunHuijben

Copy link
Copy Markdown
Contributor

Raise a consistent KeyError for missing attribute keys on all backends

Asking for an attribute key that doesn't exist raised a different exception depending on which
backend you were using:

missing node key missing edge key
SQLGraph AttributeError AttributeError
IndexedRXGraph KeyError ColumnNotFoundError
RustWorkXGraph KeyError KeyError (but ColumnNotFoundError via filter())

AttributeError leaks out of SQLAlchemy's getattr, and polars' ColumnNotFoundError isn't a
KeyError subclass, so except KeyError didn't catch every case. Code that worked against the
in-memory backends broke as soon as you switched it to SQL.

funtracks is the reason this goes first: its tests assert KeyError for a missing attribute key
and currently fail only on SQL.

What changed

All read paths now raise KeyError, with a message that names the unknown key and lists the valid
ones.

  • Added BaseGraph._validate_attr_keys(attr_keys, mode). It's the read-path counterpart of the
    existing _validate_attributes, which guards writes and raises ValueError. Reads raise
    KeyError instead because asking for a key that isn't there is a lookup miss, like
    dict["missing"], while passing an undeclared key to a write is a bad argument.
  • Called it from the read chokepoints in both backends: 4 places in _sql_graph.py, 6 in
    _rustworkx_graph.py. SQL needs fewer because all its reads funnel through one column resolver;
    rustworkx has separate implementations for graph-level edge reads, filter-level edge reads, and
    the neighbour walk in _get_neighbors.
  • filter() validates at construction rather than at collect time, so a typo points at the
    filter() call instead of somewhere further downstream.

Covered: node_attrs, edge_attrs, nodes[id][key], edges[id][key],
filter(NodeAttr/EdgeAttr(...)), successors, predecessors, and the filter-level
node_attrs / edge_attrs.

While writing the tests I found one path that wasn't in the original bug report:
filter().edge_attrs raised ColumnNotFoundError on RustWorkXGraph too, not just
IndexedRXGraph.

Not in scope

Other backend inconsistencies we found but are handling separately: update_node_attrs silently
discarding a write with an unknown key on SQL, add_edge creating dangling edges on SQL, and the
differing exception types for a missing node/edge id.

Tests

  • New parametrized test covering all 10 read paths × 3 backends.
  • Tests for the error message content, and for filter() failing eagerly.
  • A guard that valid keys are still accepted, including id columns, attr_keys=None, and
    attr_keys passed as a bare string.
  • Tightened an existing test that accepted (KeyError, AttributeError) or no error at all.

Full suite: 1257 passed, 13 skipped, 1 xfailed.

Performance

No measurable cost. The check is one set lookup per call against the already-cached schema dict,
not per row. A/B on node_attrs over 20k nodes: SQL 15.4 → 14.6 ms/call, rustworkx
1.61 → 1.70 ms/call, both within run-to-run noise.

Breaking change

Anyone catching AttributeError for a missing attribute key needs to catch KeyError now.

@TeunHuijben
TeunHuijben requested a review from JoOkuma July 30, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant