Repository navigation
Fix the state_changes indexes and add a foreign key on pool-share balances - #704
Conversation
b3105e7 to
ca47c77
Compare
| @@ -0,0 +1,26 @@ | |||
| -- +migrate Up | |||
There was a problem hiding this comment.
why not notransaction here too?
2290cff to
458c7fb
Compare
There was a problem hiding this comment.
Pull request overview
Optimizes state-change queries and enforces pool-share referential integrity.
Changes:
- Uses derived transaction TOIDs to query via the
state_changesprimary key. - Replaces redundant state-change indexes.
- Adds and tests a deferred liquidity-pool balance foreign key.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
internal/db/migrations/2026-08-21.0-statechanges-account-id-index.sql |
Replaces state-change indexes. |
internal/db/migrations/2026-08-13.0-liquidity_pool_balances-pool-fk.sql |
Adds the pool foreign key. |
internal/db/migrations/2025-06-10.4-statechanges.sql |
Documents primary-key lookup behavior. |
internal/data/statechanges.go |
Adds derived to_id query predicates. |
internal/data/statechanges_test.go |
Tests TOID-based operation lookups. |
internal/data/liquidity_pool_balances_test.go |
Tests foreign-key enforcement. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| -- chunk, and compressed chunks prune on the bloom sparse indexes 2025-06-10.4 | ||
| -- declares. The dropped indexes are superseded: operation-scoped lookups derive | ||
| -- their PK prefix from the TOID encoding (to_id = operation_id & ~x'FFF'). | ||
| CREATE INDEX IF NOT EXISTS idx_state_changes_account_id ON state_changes(account_id, ledger_created_at DESC, to_id DESC, operation_id DESC, state_change_id DESC); |
458c7fb to
f5d34f2
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (1)
internal/db/migrations/2026-08-21.0-statechanges-account-id-index.sql:18
- This forward migration builds a multi-column index on the existing
state_changeshypertable inside sql-migrate's default wrapping transaction. PlainCREATE INDEXtakes a lock that conflicts with ingestion writes and holds it until the migration commits, so a production-sized build can stop live ingestion for the full index-build duration. Use the TimescaleDB version's supported online/per-chunk index-build procedure (for example, a nontransactional per-chunk build), and only drop the old indexes after that build completes.
CREATE INDEX IF NOT EXISTS idx_state_changes_account_id ON state_changes(account_id, ledger_created_at DESC, to_id DESC, operation_id DESC, state_change_id DESC);
f5d34f2 to
31c0ac8
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
internal/db/migrations/2026-08-21.0-statechanges-account-id-index.sql:18
- On an existing
state_changeshypertable, ordinaryCREATE INDEXholds write-blocking locks for the full multi-chunk build. This is the forward migration production databases execute, so a large database can stop live ingestion for the duration. Build the index with the TimescaleDB-supported per-chunk/online approach in a no-transaction migration, with retry handling, before dropping the old indexes.
CREATE INDEX IF NOT EXISTS idx_state_changes_account_id ON state_changes(account_id, ledger_created_at DESC, to_id DESC, operation_id DESC, state_change_id DESC);
| FOREIGN KEY (pool_id) REFERENCES liquidity_pools (pool_id) | ||
| DEFERRABLE INITIALLY DEFERRED |
31c0ac8 to
f54b004
Compare
A pool-share balance is always read alongside its pool's reserves, and ingestion writes both tables in one transaction with the pool row first, so a balance row whose pool is missing can only come from an ingestion bug. The foreign key turns that bug into a loud failure at the write instead of a silently broken join at read time, matching the trustline_balances → trustline_assets constraint already in place. DEFERRABLE INITIALLY DEFERRED so the check runs at commit: a pool is deleted only once every share balance referencing it is deleted, and both happen in the same persist transaction. The migration runs notransaction so validation gets its own transaction — ADD CONSTRAINT NOT VALID is metadata-only, and VALIDATE takes just SHARE UPDATE EXCLUSIVE, so ingestion keeps writing through the scan. Orphaned share rows are cleared first: they are unreadable anyway now that the read path joins liquidity_pools, and a single survivor would abort validation and halt the deploy. GetByAccount's test for orphaned rows becomes a test that the insert is rejected, since the row can no longer exist.
…ary key BatchGetByOperationID and BatchGetByOperationIDs bound only operation_id and ledger_created_at, so neither could reach the state_changes primary key — (ledger_created_at, to_id, operation_id, state_change_id) — and both leaned on a standalone index on operation_id. TOID encoding makes the gap closable without extra input: a state change's parent transaction to_id is derivable from its operation ID by bit masking, tx_to_id = operation_id &^ 0xFFF. Binding that derived value turns (ledger_created_at, to_id, operation_id) into a three-column PK prefix, so both queries probe the PK directly. In BatchGetByOperationIDs the extra pinned column also drops out of the ORDER BY, which is now state_change_id alone — the only column that varies within one lateral probe. With operation-scoped lookups on the PK, idx_state_changes_operation_id has no consumer left. idx_state_changes_account_category goes too: the account-scoped queries that used it are served by a narrower index on (account_id, ledger_created_at DESC, to_id DESC, operation_id DESC, state_change_id DESC), whose trailing columns repeat the PK's sort key so an account's page is a heapsort-free prefix scan. Category and reason are left out because an account's rows are few enough to scan and filter on the active chunk, and compressed chunks prune on the bloom sparse indexes. sql-migrate keys applied migrations by filename, so editing 2025-06-10.4 in place only reshapes fresh databases. 2026-08-21.0 converges the ones already past it, and is a no-op everywhere else.
The index swap was expressed twice: 2025-06-10.4 was edited in place to create idx_state_changes_account_id instead of idx_state_changes_operation_id and idx_state_changes_account_category, and 2026-08-21.0 made the same change again for databases already past that filename. Two files then had to be hand-kept in agreement, and 2026-08-21.0's Down recreated indexes 2025-06-10.4 no longer defined. 2025-06-10.4 goes back to creating the original pair, so 2026-08-21.0 is the one place the swap is expressed and every database reaches the final shape the same way. Its CREATE carries IF NOT EXISTS and its DROPs carry IF EXISTS already, so a fresh database builds the pair and converges in the next step, at the cost of two transient index builds at setup. The primary key's comment keeps the note that operation-scoped lookups ride it via the TOID encoding: that describes how the queries are written, not which indexes exist.
f54b004 to
069ff55
Compare
Start here:
internal/db/migrations/2026-08-21.0-statechanges-account-id-index.sql— the only file that changes an existing database.Two schema changes. Each ships with the query change that needs it.
state_changeslookups by operation now use the primary keyBatchGetByOperationIDandBatchGetByOperationIDsonly filtered onoperation_idandledger_created_at. That isn't enough to use the primary key(ledger_created_at, to_id, operation_id, state_change_id), so both queries needed a separate index onoperation_id.We can work out
to_idfrom the operation ID for free, because of how TOIDs are encoded:tx_to_id = operation_id &^ 0xFFF. Adding it to the WHERE clause matches the first three columns of the primary key, so both queries use the PK directly.Two indexes now have nothing using them, and are dropped:
idx_state_changes_operation_id— the primary key covers it.idx_state_changes_account_category— replaced by the smalleridx_state_changes_account_id.sql-migratetracks migrations by filename, so editing2025-06-10.4in place only affects brand-new databases.2026-08-21.0brings existing ones in line and does nothing on new ones.Pool-share balances get a foreign key to their pool
Ingestion writes the pool row first, in the same transaction, so a share row without a pool is always a bug. The foreign key makes it fail loudly on write instead of quietly producing a broken join on read. Same idea as the existing
trustline_balances → trustline_assetsconstraint.It's deferrable, and added as
NOT VALIDthen validated outside a transaction, so ingestion keeps writing while it runs.PR 1 of 6 replacing #684 · schema → fixes → pipeline → parallel writes → table groups → final fixes