Skip to content

feat(gfql): physical adjacency indexes for O(degree) seeded traversal#1658

Merged
lmeyerov merged 20 commits into
masterfrom
dev/gfql-seeded-traversal-index
Jul 9, 2026
Merged

feat(gfql): physical adjacency indexes for O(degree) seeded traversal#1658
lmeyerov merged 20 commits into
masterfrom
dev/gfql-seeded-traversal-index

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Stack position

Stacks on #1667 (polars-engine followups). Full spine:
master → #1666 (3VL) → #1660 (polars engine + GPU target; #1655 folded in) → #1667 (followups) → **this PR**.
Restacked 2026-07-02: the followups PR proved independent of this index layer, so the most
experimental PR (this one) moved to the top of the spine — reviewable against the final
engine surface its hooks land on. Additive — no change to default (un-indexed) behavior
on any engine.

What this does

Opt-in, pay-as-you-go CSR adjacency / node-id indexes → seeded traversal becomes
O(degree), flat in graph size. Sidecar over edge row positions (never reorders
.edges/.nodes), fingerprint-validated by object IDENTITY (a .edges() rebind safely
invalidates → scan fallback, never a wrong answer). Three uniform surfaces (Python /
Cypher DDL CREATE/DROP/SHOW GFQL INDEX / JSON wire) + gfql(..., index_policy=...).
Engine-polymorphic: numpy host arrays for pandas/polars, cupy on-device for cudf.
Hooked at every seeded scan site; falls back to scan for uncovered features
(edge/source/dest match, target_wave_front, min_hops>1, labeling, missing/duplicate
node ids, missing endpoints).

⚠️ Correctness hardening (takeover)

A 2-agent adversarial review reproduced wrong-answer divergences from the un-indexed
scan oracle
that the original parity scenarios missed (the cost gate masks them unless
≥3 distinct seed keys hit the index path). All fixed — the index now matches scan or
falls back, never a wrong answer (+6 engine-parametrized differential regression tests):

  • max_hops was silently ignored (hop(max_hops=3) ran 1 hop) → now honored.
  • duplicate node ids corrupted the node-id index → first-row-per-key + refuse/fallback.
  • edge endpoints absent from the node table were dropped (scan synthesizes them) → fallback.
  • node-row order, id(df) fingerprint recycling, int32/int64 seed narrowing, and a
    polars/cudf seed-conversion crash under force → all fixed.
  • review wave: polars is_in deprecation → order-preserving semi-join; DROP GFQL INDEX <custom-name> silent no-op → resolve-by-name + raise.

Results (dgx-spark, deg-8, warm median) — OUR reproduced numbers

Every benchmarked cell is GUARDED: the timing is discarded unless (a) the index path
was actually taken (index_trace) AND (b) the index result == the scan result. No cell
failed either guard.

Seeded warm latency is FLAT in N (O(degree)) while the scan is O(E):

nodes / edges GFQL-pandas idx GFQL-polars idx scan (pandas) speedup
0.8M / 6.4M 0.124 ms 0.142 ms 105 ms 849×
8M / 64M 0.122 ms 0.218 ms 1045 ms 8553×

(10× more edges → pandas index latency 0.124 → 0.122 ms = flat, while the scan grows 10× = O(E). That is the O(degree) proof.)

cuDF / polars-GPU are flat but floored ~3 ms (cudf) / sub-ms (polars-gpu) by GPU kernel
launch — selective traversal is an indexing problem, not a compute one (CPU wins it).

vs graph DBs (0.8M, identical graph/seed, matched answer counts):

task GFQL-pandas kuzu (CSR) neo4j vs kuzu vs neo4j
SEL1 (1-hop) 0.123 ms 1.15 ms 1.45 ms 9.4× 11.8×
SEL2 (1–2 hop) 0.150 ms 4.25 ms 2.54 ms 28× 16.9×

(Adversarial note: the original neo4j SEL2 query was exactly-2-hop — 70 rows vs the
1–2-hop 78 — and undercounted; re-run with *1..2 for a fair 78-row comparison.)

Tests

  • graphistry/tests/compute/gfql/index/test_index.py: 75 passed on dgx (pandas/cudf/
    polars/polars-gpu), incl. 6 new adversarial regression tests (max_hops, dup ids, missing
    endpoint, node order, mixed dtype, stale rebind) + drop-by-custom-name.
  • No regressions in the existing hop/chain/cypher suites.

🤖 Generated with Claude Code

Review notes

  • 8 conventional commits (adversarial-review fix commits folded into the feature commit).
  • Team-polish pass (dc4f9da3): DROP GFQL INDEX [IF EXISTS] semantics wired end-to-end (wire missing_ok was serialized-but-ignored; plain DROP of a missing index now raises, IF EXISTS is a no-op, +test); dead utility code removed; internal review-wave codes scrubbed from production comments; real docstrings on the ComputeMixin index API incl. the previously-undocumented gfql(index_policy=...); benchmark catalog added to benchmarks/gfql/README.md (which harness pair backs the published vs-Kuzu claims); bulk-OLAP polars-gpu cell fixed to GPU-or-error.

lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov
lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 446d419 to 1e4cde8 Compare June 28, 2026 17:41
@lmeyerov
lmeyerov force-pushed the dev/gfql-lazy-gpu branch from 194671a to e5321e4 Compare June 28, 2026 20:00
lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov
lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 1e4cde8 to dec68fc Compare June 28, 2026 20:00
@lmeyerov
lmeyerov force-pushed the dev/gfql-lazy-gpu branch from e5321e4 to 5c60a46 Compare June 28, 2026 20:27
lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov
lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from a5e70f7 to 9cbe4d7 Compare June 28, 2026 20:27
lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov
lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 9cbe4d7 to 073eb80 Compare June 28, 2026 20:46
@lmeyerov
lmeyerov force-pushed the dev/gfql-lazy-gpu branch from 36cf1fd to 7207dc8 Compare June 28, 2026 20:51
lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov
lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 073eb80 to bb4d7e3 Compare June 28, 2026 20:51
@lmeyerov
lmeyerov force-pushed the dev/gfql-lazy-gpu branch from 7207dc8 to 8591416 Compare June 28, 2026 21:02
lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov
lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from bb4d7e3 to 648f10d Compare June 28, 2026 21:02
@lmeyerov
lmeyerov force-pushed the dev/gfql-lazy-gpu branch from 8591416 to 3d47a07 Compare June 28, 2026 21:59
lmeyerov added a commit that referenced this pull request Jun 28, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov
lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 648f10d to 8759fd4 Compare June 28, 2026 22:00
lmeyerov added a commit that referenced this pull request Jun 29, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov
lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 8759fd4 to 9014160 Compare June 29, 2026 03:06
@lmeyerov
lmeyerov force-pushed the dev/gfql-lazy-gpu branch from 16d5beb to eea6c96 Compare June 29, 2026 03:14
lmeyerov added a commit that referenced this pull request Jun 29, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov
lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from 9014160 to ca5dfab Compare June 29, 2026 03:14
@lmeyerov
lmeyerov force-pushed the dev/gfql-lazy-gpu branch from eea6c96 to 38a08e7 Compare June 29, 2026 23:08
lmeyerov added a commit that referenced this pull request Jun 29, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov
lmeyerov force-pushed the dev/gfql-seeded-traversal-index branch from ca5dfab to b65ca7f Compare June 29, 2026 23:08
@lmeyerov
lmeyerov force-pushed the dev/gfql-lazy-gpu branch from 38a08e7 to c5bc2fd Compare July 1, 2026 01:20
lmeyerov added a commit that referenced this pull request Jul 1, 2026
…(takeover)

Took over the seeded-traversal CSR index (#1658). A 2-agent adversarial review
(architecture + correctness) reproduced multiple wrong-answer divergences from the
un-indexed scan oracle that the original parity scenarios missed (the cost gate
masks them unless >=3 distinct seed keys hit the index path). All fixed; the index
now matches scan or falls back — never a wrong answer. +6 engine-parametrized
differential regression tests.

BLOCKERs:
- B1 max_hops silently ignored: the coverage gate omitted max_hops, so the index
  ran 'hops' (default 1) while the scan ran 'max_hops or hops'. Now resolve
  eff_hops = max_hops or hops in maybe_index_hop.
- B2 duplicate node ids corrupted the node-id index: build treated a per-row
  (length-E) row_positions array as per-key (length-U). Now collapse to the first
  row per key + refuse (None) on non-unique ids; gfql_index_all skips node_id
  gracefully; traverse falls back when the output would carry duplicate id rows
  (the scan dedups output nodes by id).
- B3 endpoints absent from the node table were dropped: the scan synthesizes a row
  per edge endpoint; the index only materialized existing rows. Now detect missing
  ids and fall back to scan.

IMPORTANTs:
- I4 node-row order: node-id-index materialization returned sorted-by-id order;
  now sort row positions ascending to preserve the .nodes table order.
- I5 id(df) fingerprint staleness: an object id can be recycled after GC, so a new
  same-shape frame could pass validation -> stale index -> wrong answer. Now hold a
  strong ref + validate by object identity (recycle-proof).
- I6 frontier dtype narrowing: an int64 id seed cast to int32 keys wraps and false-
  matches. Now promote both sides to a common dtype (never narrow).

Latent integration bugs surfaced under force policy:
- the polars/cudf index hooks passed un-converted pandas seeds to col_to_array
  (which assumes engine-native frames) -> AttributeError. Now normalize seeds via
  df_to_engine at the index entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
g: Any,
query: object,
*,
index_policy: str = "use",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

literal type?

Comment thread graphistry/compute/gfql/index/types.py Outdated
FrameLike = DataFrameT


class ArrayLike(Protocol):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move

Comment thread graphistry/compute/gfql/index/explain.py Outdated
# Type variable for return type preservation in predicates
T = TypeVar('T')

class ArrayLike(Protocol):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird location

@lmeyerov
lmeyerov merged commit 8378230 into master Jul 9, 2026
77 checks passed
@lmeyerov
lmeyerov deleted the dev/gfql-seeded-traversal-index branch July 9, 2026 00:19
lmeyerov added a commit that referenced this pull request Jul 16, 2026
…-identity

gfql: engage the #1658 seeded index on Polars/cuDF (identity-stable coercion #1726 + small-frontier cost gate)
pull Bot pushed a commit to admariner/pygraphistry that referenced this pull request Jul 16, 2026
…ngages on polars (graphistry#1726)

_pl_nan_to_null returned a fresh polars frame whenever any Float32/Float64 column
existed (keyed on column presence, not actual NaN), churning frame identity so the
graphistry#1658 CSR index's `source_ref is df` check failed -> polars/polars-gpu direct hops
always fell back to the O(E) scan. Fix: probe is_nan().any() per float column
(dtype-gated) and return the SAME frame object when no NaN is present; rebuild only
NaN-carrying columns when present. Value-identical (fill_nan(None) is a no-op on
NaN-free columns); only object identity changes -> the resident index now engages.
Verified on the real SNB graph: polars flipped 120/120-scan -> 54-index/12-scan
(remaining 12 = cost-gate, separate). Adds pure-polars TestPlNanToNull.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Myz4YLr6FhjyH7h7dHHTBV
lmeyerov added a commit that referenced this pull request Jul 19, 2026
…edges

The native polars lazy chain executor (lazy/engine/polars/chain.py) attaches its
synthetic edge id via with_row_index -> a fresh edge frame -> the #1658 identity
guard missed, so typed-edge chains scanned on polars/polars-gpu (untyped already
engaged). Apply the same rebind_edges fix at the polars augmentation site.

Now ALL FOUR engines engage the index for typed AND untyped seeded chain/Cypher
hops. Measured (dgx, 500k/4M/4-types, index==scan): polars typed 1.3x / untyped
5.4x; polars-gpu typed engaged / untyped 12.4x. Broadened the engagement test to
all engines; 138 index tests pass (109 existing + 29 new), 4-engine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
lmeyerov added a commit that referenced this pull request Jul 19, 2026
…das/cuDF (incl. typed edges)

A seeded hop expressed as a gfql()/Cypher chain (not a direct g.hop) always
scanned on pandas/cuDF even with a resident index: _chain_impl attaches a
synthetic per-edge id column (copy(deep=False)+assign) -> a fresh edge-frame
object -> the index's `source_ref is df` identity guard missed.

- registry.rebind_edges: re-point edge adjacency indexes at the augmented frame
  (safe: shallow copy preserves src/dst by value; fingerprint unchanged by an
  added column). Called in chain.py right after the edge-index augmentation.
- Typed edges: relax _hop_is_index_coverable for simple scalar-equality edge_match
  on the wavefront path; index_seeded_hop applies the edge predicate to CSR-matched
  rows each hop, parity-exact with the scan's filter_edges_by_dict.
- Predicate/membership edge_match, edge_query, and the direct-hop path stay on scan.

Measured (dgx, 500k/4M/4-types, warm median, index==scan): pandas typed 2.1x /
untyped 2.7x; cuDF typed 1.9x / untyped 2.0x. polars/polars-gpu untyped already
engage via the native lazy executor; typed polars is a tracked follow-up.

27 new 4-engine differential + engagement tests; existing 109-test index suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
lmeyerov added a commit that referenced this pull request Jul 19, 2026
…edges

The native polars lazy chain executor (lazy/engine/polars/chain.py) attaches its
synthetic edge id via with_row_index -> a fresh edge frame -> the #1658 identity
guard missed, so typed-edge chains scanned on polars/polars-gpu (untyped already
engaged). Apply the same rebind_edges fix at the polars augmentation site.

Now ALL FOUR engines engage the index for typed AND untyped seeded chain/Cypher
hops. Measured (dgx, 500k/4M/4-types, index==scan): polars typed 1.3x / untyped
5.4x; polars-gpu typed engaged / untyped 12.4x. Broadened the engagement test to
all engines; 138 index tests pass (109 existing + 29 new), 4-engine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
lmeyerov added a commit that referenced this pull request Jul 19, 2026
…das/cuDF (incl. typed edges)

A seeded hop expressed as a gfql()/Cypher chain (not a direct g.hop) always
scanned on pandas/cuDF even with a resident index: _chain_impl attaches a
synthetic per-edge id column (copy(deep=False)+assign) -> a fresh edge-frame
object -> the index's `source_ref is df` identity guard missed.

- registry.rebind_edges: re-point edge adjacency indexes at the augmented frame
  (safe: shallow copy preserves src/dst by value; fingerprint unchanged by an
  added column). Called in chain.py right after the edge-index augmentation.
- Typed edges: relax _hop_is_index_coverable for simple scalar-equality edge_match
  on the wavefront path; index_seeded_hop applies the edge predicate to CSR-matched
  rows each hop, parity-exact with the scan's filter_edges_by_dict.
- Predicate/membership edge_match, edge_query, and the direct-hop path stay on scan.

Measured (dgx, 500k/4M/4-types, warm median, index==scan): pandas typed 2.1x /
untyped 2.7x; cuDF typed 1.9x / untyped 2.0x. polars/polars-gpu untyped already
engage via the native lazy executor; typed polars is a tracked follow-up.

27 new 4-engine differential + engagement tests; existing 109-test index suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
lmeyerov added a commit that referenced this pull request Jul 19, 2026
…edges

The native polars lazy chain executor (lazy/engine/polars/chain.py) attaches its
synthetic edge id via with_row_index -> a fresh edge frame -> the #1658 identity
guard missed, so typed-edge chains scanned on polars/polars-gpu (untyped already
engaged). Apply the same rebind_edges fix at the polars augmentation site.

Now ALL FOUR engines engage the index for typed AND untyped seeded chain/Cypher
hops. Measured (dgx, 500k/4M/4-types, index==scan): polars typed 1.3x / untyped
5.4x; polars-gpu typed engaged / untyped 12.4x. Broadened the engagement test to
all engines; 138 index tests pass (109 existing + 29 new), 4-engine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
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