feat(db): add network column across all Prisma models (#159) - #169
Merged
Conversation
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
|
@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! 🚀 |
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
4 tasks
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #159.
What changed
Every model now carries
network("testnet" | "mainnet"), folded into every uniqueness constraint and every query.eventId @unique@@unique([network, eventId])@@unique([contractId, tokenId])@@unique([network, contractId, tokenId])@@unique([address, contractId])@@unique([network, address, contractId])batchId @unique@@unique([network, batchId])id Int @id @default(1)singletonnetwork String @idid Int @id @default(1)singletonnetwork String @idnetworkcolumnnetworkalso 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.tsis the single place that decides what "the current network" means. Everydb.tsfunction takes an optional trailingnetworkdefaulting toSTELLAR_NETWORK, so existing single-network callers are untouched and #161 / #163 have somewhere explicit to pass.Two bugs the type checker could not catch
networkcarriesDEFAULT 'testnet'. That makes a forgotten write compile, typecheck, and succeed — while filing mainnet events under testnet. Both instances found:commitBatchwrote three tables with no network. Now stamped explicitly on every row.updateAccountSummariesincheckpoint.tswas a byte-identical copy ofupsertAccountSummariesindb.ts, rawON CONFLICTincluded. That target must name the same columns as the unique index, which widened here — so the leftover copy would have thrownno unique or exclusion constraint matching the ON CONFLICT specificationon every write, not merely mis-scoped the aggregate. It now delegates to the single implementation (−80 lines).rollbackToLedgeris the other one worth a look: ledger sequences are per-chain and testnet runs far ahead of mainnet, so its unscopedledger > targetdeletes would have wiped real mainnet history during a testnet reorg. Now scoped, with a test.The migration is hand-written, deliberately
prisma migrate diffemits this for IndexerState and BackfillCursor, because the new column is their primary key:Postgres rejects a
NOT NULLcolumn with no default on a non-empty table. Verified rather than assumed: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.
Verification
Against a real Postgres 15 instance:
networkcolumn across all Prisma models #159 schema (matching what is deployed).IndexerState(lastIndexedLedger = 987654),BackfillCursor, and event rows.network='testnet', values intact.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.Tests: 270 pass, was 244.
tsc --noEmitclean.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
queryTransfersfails three of them.Notes for the reviewer
prisma/migrations/starts atadd_backfill_cursorand the base tables came fromdb push. Somigrate deployon 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.gitignoreand is easy to sweep into a commit accidentally. Left alone here as unrelated.Acceptance criteria
network; all@unique/@@uniquekeys include itIndexerState/BackfillCursorare per-network, not singleton rowsJoined the contributor Telegram.