Skip to content

envio subgraph diff: parity report against a graph-node endpoint - #1731

Closed
moose-code wants to merge 9 commits into
claude/hyperindex-subgraph-mode-bs67pqfrom
moose/subgraph-diff
Closed

moose-code wants to merge 9 commits into
claude/hyperindex-subgraph-mode-bs67pqfrom
moose/subgraph-diff

Conversation

@moose-code

@moose-code moose-code commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Adds envio subgraph diff <endpoint>: a parity report between a local subgraph-mode indexer and the graph-node endpoint a team is migrating from, at one pinned block. Targets #1626's branch.

What it does

  • Pins block N = the indexer's envio_chains.progress_block, read in one read-only snapshot. The reference is queried with block: {number: N} and checked with _meta. It refuses (exit 2) a reference that hasn't reached N or reports indexing errors.
  • For each entity type in subgraph.yaml's schema, it compares the full id sets (paged with an id_gt cursor), then field-by-field on a deterministic sample (sha256 of the id), normalised to graph-node's encodings.
  • --ours <graph endpoint> compares two Graph endpoints instead of Postgres, for example envio-serve's subgraph_api.
  • Output is a table, or --json (stable schemaVersion: 1). Exit codes: 0 match, 1 mismatch, 2 couldn't compare.
envio subgraph diff <endpoint> [--ours <endpoint>] [--block N] [--sample 100] [--type A,B]
  [--tolerance 0.1] [--max-field-mismatches 0] [--concurrency 4] [--max-entities 0]
  [--header 'K: V'] [--allow-indexing-errors] [--timeout 60] [--json]

Tried on real data

Run Block Result
Nouns (subgraph mode) vs a live nouns-mainnet deployment 26,192,735 MATCH: 25 types, ~100k ids, 2,067 sampled, 0 field mismatches, 40 s
ENS (subgraph mode) vs api.thegraph.com/.../ensdomains/ens 14,056,428 MISMATCH: 8 types match, 12 mismatch, 7 partial. Catches two subgraph-mode bugs: the address-less Resolver data source never runs, and labels are unhealed (ENSRainbow is gone)

Notes

  • Lives in packages/cli, because the schema translation and .env loading are already there. Adds tokio-postgres, plus an Exit{code} command in Bin.res so Node can exit 2.
  • Limitations:
    • About 1 request per 1,000 reference entities, so --max-entities caps big types and marks them incomplete.
    • Derived fields aren't compared.
    • Without introspection, collection names use simple plural rules.
    • Single chain only.
  • Tests: 22 diff tests (pagination, cursors, 429 retry, normalisation, mock graph-node runs in both modes, a Postgres fixture test behind ENVIO_DIFF_TEST_PG). cargo fmt and clippy --all-targets pass. Help docs, snapshot and docs/subgraph-compat.md §6c are updated.

🤖 Generated with Claude Code

moose-code and others added 5 commits October 14, 2026 19:12
Run inside a subgraph project, it compares the indexer's own Postgres (the
same ENVIO_PG_* settings, read in one REPEATABLE READ snapshot) with the
subgraph's old graph-node endpoint, pinned with `block: {number: N}` to the
block the indexer has processed. `--ours <graph-endpoint>` compares a Graph
endpoint of ours (envio-serve) instead.

Per entity type from the subgraph schema it pages every reference id by
cursor, diffs the id sets, and compares a deterministic sample (sha256 of
the id) field by field after normalizing both sides to graph-node's
encodings: BigInt/Int8/Timestamp as integers, BigDecimal normalized to 34
significant digits, Bytes lowercase hex, relations by id, stored entity
lists as sorted id sets; derived fields are skipped. A field graph-node
can't resolve is dropped and reported instead of failing the type.

Prints a table or, with --json, a stable schemaVersion 1 report. Exit 0 on a
match within --tolerance (0.1% per type) and --max-field-mismatches (0),
1 on a mismatch, 2 when it couldn't compare. The new `exit` runtime command
carries that code through the Node host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Public endpoints behind a shared key (ENS's community key) answer 429 for
longer than five retries cover; eight with the 30s backoff cap ride out about
a minute and a half.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A type whose reference listing stopped at --max-entities with nothing
differing so far wasn't compared fully, but nothing failed either; agents
reading the JSON need to tell the two apart. Both still exit 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dropping a field the reference can't resolve keeps a defect in the old
host's data from hiding the rest of the type. A field ours fails on is
different: every frontend query that selects it breaks, so the type is a
mismatch and the field is listed as unservable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 54711222-3ada-4de3-9be8-8acdcafed37a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@moose-code

Copy link
Copy Markdown
Member Author

@DZakh: two PRs into your #1626 branch from the subgraph-hosting hackathon, for review when you have time. No merge needed during the hack.

🤖 Generated with Claude Code

@moose-code
moose-code requested a review from DZakh October 14, 2026 19:49
moose-code and others added 3 commits October 14, 2026 21:31
…es it

graph-node resolves a relation by joining the target's table, so a nullable
relation whose entity doesn't exist comes back null, and a list of ids
leaves such ids out (relational_queries.rs, child types D and C). The
Postgres side compared the stored id instead, which was the only cause of
the Aave v3 (User.eModeCategoryId → EModeCategory "0") and Klima
(Activity.user, 275k rows) mismatches. It now joins the same way and
counts what it nulled or dropped per field as a note (`danglingRelations`),
not a mismatch. Non-null relations keep failing graph-node's query, which
the first batch already handles; interface targets have no table and are
compared as stored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rence

Through a gateway, the id_gt-paged listing can come back short: two
listings of the same Request Network Payment set lost 24 and 81 ids that
id_in finds, so ours' rows were reported as "only ours". Indexers behind
one gateway can also answer the same pinned query differently (Nouns
Proposal 708: 142/13, then 147/14 at the same block).

Every id on one side only (up to 1000 per type and side) is now asked for
by id_in at the same block on the side missing it, and every sampled
entity that differs or wasn't returned is fetched again, under its own
alias so a caching proxy doesn't replay the first answer. Only what the
second answer still shows is reported; what it settled is counted under
`recheck` in the JSON and "Notes" in the table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…introspect

envio-serve answers no introspection, so the diff guessed ours' collection
names with a hand-rolled plural and missed Aave v3's
ATokenBalanceHistoryItem (atokenBalanceHistoryItems), EModeCategory
(emodeCategories) and Nouns' DynamicQuorumParams
(dynamicQuorumParams_collection): those types were never reached.

A side without introspection now uses the other side's names, and when
neither has them, graph-node's own rule: camel_cased_names
(graph/src/data/graphql/ext.rs) on Inflector =0.11.4, the version
graph-node pins, with `_collection` when the plural is the singular.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@moose-code

Copy link
Copy Markdown
Member Author

3 diff-tool fixes (head 01e58235d), each with a test:

  • e445c8002: a relation to a missing entity is read the way graph-node serves it. It's reported as danglingRelations, not as a mismatch.
  • 35d0fefa3: before reporting a difference, the reference is asked again by id_in at the pinned block, so an only-ours or only-theirs id is confirmed.
  • 01e58235d: when one side can't be introspected, collections are named the way graph-node names them.

This removes the false positives seen on Aave v3 and Klima. cargo test --lib passes 1022.

🤖 Generated with Claude Code

moose-code added a commit that referenced this pull request Oct 14, 2026
envio subgraph diff beside sg-codegen's subgraph-codegen: module list, compat docs §6b/§6c, CLI help and snapshot regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DZakh

DZakh commented Oct 22, 2026

Copy link
Copy Markdown
Member

Merged

@DZakh DZakh closed this Oct 22, 2026

This branch was successfully deployed

1 active deployment
internal — 83138689 Deployed Oct 15, 2026 by moose-code via authorize #3737
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.

2 participants