Skip to content

feat(db): add network column across all Prisma models (#159) - #169

Merged
Miracle656 merged 2 commits into
Miracle656:mainfrom
Salmatcre8:feat/network-column
Aug 30, 2026
Merged

feat(db): add network column across all Prisma models (#159)#169
Miracle656 merged 2 commits into
Miracle656:mainfrom
Salmatcre8:feat/network-column

Conversation

@Salmatcre8

Copy link
Copy Markdown
Contributor

Closes #159.

What changed

Every model now carries network ("testnet" | "mainnet"), folded into every uniqueness constraint and every query.

Model Before After
TokenTransfer / HostFnLog / NftTransfer eventId @unique @@unique([network, eventId])
NftMetadata @@unique([contractId, tokenId]) @@unique([network, contractId, tokenId])
AccountSummary @@unique([address, contractId]) @@unique([network, address, contractId])
IndexerCheckpoint batchId @unique @@unique([network, batchId])
IndexerState id Int @id @default(1) singleton network String @id
BackfillCursor id Int @id @default(1) singleton network String @id
WebhookSubscription / WebhookDelivery / RetentionJobRun network column

network also leads every composite index. Every query filters on it now, so a trailing position would leave Postgres unable to use the index for that filter — the column would be recorded but not exploited.

src/network.ts is the single place that decides what "the current network" means. Every db.ts function takes an optional trailing network defaulting to STELLAR_NETWORK, so existing single-network callers are untouched and #161 / #163 have somewhere explicit to pass.

Two bugs the type checker could not catch

network carries DEFAULT 'testnet'. That makes a forgotten write compile, typecheck, and succeed — while filing mainnet events under testnet. Both instances found:

  1. commitBatch wrote three tables with no network. Now stamped explicitly on every row.
  2. updateAccountSummaries in checkpoint.ts was a byte-identical copy of upsertAccountSummaries in db.ts, raw ON CONFLICT included. That target must name the same columns as the unique index, which widened here — so the leftover copy would have thrown no unique or exclusion constraint matching the ON CONFLICT specification on every write, not merely mis-scoped the aggregate. It now delegates to the single implementation (−80 lines).

rollbackToLedger is the other one worth a look: ledger sequences are per-chain and testnet runs far ahead of mainnet, so its unscoped ledger > target deletes would have wiped real mainnet history during a testnet reorg. Now scoped, with a test.

The migration is hand-written, deliberately

prisma migrate diff emits this for IndexerState and BackfillCursor, because the new column is their primary key:

ALTER TABLE "wraith"."IndexerState" DROP CONSTRAINT "IndexerState_pkey",
DROP COLUMN "id", ADD COLUMN "network" TEXT NOT NULL, ...

Postgres rejects a NOT NULL column with no default on a non-empty table. Verified rather than assumed:

ERROR:  column "network" of relation "pkproof" contains null values

Both tables are therefore added with a default, populated, then the default dropped before the key is applied — which preserves the indexer cursor instead of forcing a re-index from genesis.

⚠️ Regenerating this migration with prisma migrate dev would silently restore the broken form.

Verification

Against a real Postgres 15 instance:

  1. Pushed the pre-Add a network column across all Prisma models #159 schema (matching what is deployed).
  2. Seeded IndexerState (lastIndexedLedger = 987654), BackfillCursor, and event rows.
  3. Applied the migration — clean, no errors.
  4. Both cursors survived as network='testnet', values intact.
  5. prisma migrate diff --from-url <migrated db> --to-schema-datamodel-- This is an empty migration. — zero drift, so the SQL lands exactly on the target schema.
 network | lastIndexedLedger        network | nextLedger
---------+-------------------      ---------+------------
 testnet |            987654        testnet |        450

Tests: 270 pass, was 244. tsc --noEmit clean.

The 26 new tests assert the predicate is present in each where clause, not that queries merely succeed — the whole failure mode here is queries that succeed with the wrong rows. Mutation-checked: deleting one network filter from queryTransfers fails three of them.

Notes for the reviewer

  • No baseline migration exists in this repoprisma/migrations/ starts at add_backfill_cursor and the base tables came from db push. So migrate deploy on a genuinely empty database cannot work today, for reasons that predate this PR. I verified against the deployed shape instead (step 1 above). Worth its own issue.
  • coverage/ is not in .gitignore and is easy to sweep into a commit accidentally. Left alone here as unrelated.
  • Nothing else is in this diff: no lockfile changes, no reformatting.

Acceptance criteria

  • Every model carries network; all @unique/@@unique keys include it
  • IndexerState/BackfillCursor are per-network, not singleton rows
  • Migration applies cleanly (verified on Postgres 15; zero drift afterwards)
  • Existing queries updated and tests green (270 pass)

Joined the contributor Telegram.

Wraith's schema had no network dimension, so a mainnet instance and a
testnet instance could not share a database. Three collisions made it
impossible rather than merely untidy:

  - eventId was globally @unique on TokenTransfer/HostFnLog/NftTransfer.
    It is an RPC paging token, unique only within a network, so the same
    token on both chains silently overwrote one network's row.
  - IndexerState and BackfillCursor were singleton rows (@id @default(1)).
    One cursor cannot describe how far two independent chains have been
    indexed.
  - AccountSummary and NftMetadata keyed on (address, contractId) and
    (contractId, tokenId), merging two chains into one aggregate.

Every model now carries `network` ("testnet" | "mainnet"), folded into
every uniqueness constraint and every query. IndexerState and
BackfillCursor are keyed by network instead of the singleton id.

network also leads every composite index. Each query filters on it now,
and a trailing position would leave Postgres unable to use the index for
that filter.

src/network.ts is the single place that decides what "the current
network" means. Every db.ts function takes an optional trailing network
defaulting to STELLAR_NETWORK, so existing single-network callers are
unchanged and Miracle656#161/Miracle656#163 have somewhere explicit to pass.

Two things the type checker could not have caught:

  - commitBatch wrote transfers, NFT transfers and host-fn logs without
    a network. The column has a DEFAULT of 'testnet', so that compiles
    and files mainnet events under testnet. Now stamped explicitly.
  - updateAccountSummaries in checkpoint.ts was a byte-identical copy of
    upsertAccountSummaries in db.ts, including the raw ON CONFLICT
    target. That target must name the same columns as the unique index,
    which widened here, so the copy would have thrown on every write.
    It now delegates to the single implementation.

The migration is hand-written. `prisma migrate diff` emits
ADD COLUMN "network" TEXT NOT NULL with no default for IndexerState and
BackfillCursor, because the new column is their primary key. Postgres
rejects that on a non-empty table, verified:

  ERROR: column "network" of relation "pkproof" contains null values

Both are therefore added WITH a default, populated, then the default is
dropped before the key is applied, which preserves the indexer cursor
instead of forcing a re-index from genesis.

Verified against a Postgres 15 instance: pushed the pre-Miracle656#159 schema,
seeded IndexerState (lastIndexedLedger 987654) and BackfillCursor, applied
the migration, and confirmed both survived as network='testnet'.
`prisma migrate diff --from-url` against the result reports an empty
migration, so the SQL lands exactly on the target schema with no drift.

Tests: 270 pass (was 244). The 26 new ones assert the predicate is
present in each where clause rather than that queries merely succeed —
deleting one network filter from queryTransfers fails three of them.

Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
@drips-wave

drips-wave Bot commented Aug 30, 2026

Copy link
Copy Markdown

@Salmatcre8 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

The integration suite still created and read IndexerState with the old
singleton key:

  prisma.indexerState.create({ data: { id: 1, lastIndexedLedger: 101 } })
  prisma.indexerState.findUnique({ where: { id: 1 } })

`id` no longer exists on that model, so every integration file failed at
setup with PrismaClientValidationError.

Why the unit run and typecheck missed it: tsconfig.json has
`include: ["src/**/*"]`, so tests/ is never typechecked, and the
integration suite is a separate vitest config that does not run in the
unit job. `tsc --noEmit` was clean and 270 unit tests passed while six
integration suites were broken.

Widening `include` to tests/ conflicts with `rootDir: "src"`, so a proper
fix needs a separate tsconfig for typechecking tests — worth its own
issue rather than changing the build layout here.

Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
@Miracle656
Miracle656 merged commit 03caff1 into Miracle656:main Aug 30, 2026
4 checks passed
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.

Add a network column across all Prisma models

2 participants