Declare the migration-created indexes on their entities - #4597
Conversation
Three merged migrations (1785460000000, 1785470000000, 1785510000000) created eleven indexes in SQL but never declared them on the TypeORM entities. RdbmsSchemaBuilder.dropOldIndices drops any database index with no same-named entry in the entity metadata, so a schema-generation run would have removed them and silently undone the migrations. The decorators reproduce the migrations' names byte-for-byte (the migrations deliberately used TypeORM's deterministic naming), so this is a no-op against the database. 1785520000000's log index is deliberately NOT declared: it carries an INCLUDE clause TypeORM cannot express, so declaring its key columns alone would make schema generation drop and recreate it without the INCLUDE. Recorded as migration-owned on the entity instead. Adds a parity spec that sweeps every migration for CREATE INDEX and compares against real TypeORM entity metadata, so the same drift cannot recur silently. Three virtual_iban_issuance_intent indexes that already drifted on develop are enumerated as exemptions rather than fixed here. Also fixes a positional index lookup in payout-order.entity.spec.ts that class-decorator bottom-up registration would otherwise have broken.
|
7 full review passes, two independent lenses per pass — correctness/schema-safety and conformance/regression — each re-run from scratch against the merge base rather than the incremental delta. I had declared the The parity spec keyed entity metadata last-wins on table name. Three tables use single-table inheritance — The migration sweep processed every Index columns were originally resolved through a Proxy standing in for Expectations came from a hardcoded list of migrations. Two further index migrations landed on develop while the branch was open and the list did not cover them, so the branch was rebased mid-review and the list replaced with a directory sweep. Two assertions were tautological. The name comparison re-derived the migration's own literal from the migration's own table and columns, so it could only fail on a stray The exemption rationale was wrong twice over. Six custom-named indexes were recorded as inexpressible by TypeORM — they are expressible, and a reviewer declared all six to prove it. The actual constraint is that declaring them requires passing the migration's custom name, which CONTRIBUTING forbids. Four reasons also asserted the indexes predated the naming convention; they postdate it by five to ten weeks.
Smaller items: a Three latent gaps are known and left in place: a migration whose |
Why
PRs #4480, #4484 and #4497 added eleven database indexes via migration, in raw SQL, without declaring them on the corresponding TypeORM entities.
That is not cosmetic.
RdbmsSchemaBuilder.dropOldIndicesdrops any database index that has no same-named entry in the entity metadata:So a schema-generation run would have removed all eleven and silently undone those PRs.
synchronizeis env-gated (src/config/config.ts,SQL_SYNCHRONIZE) and off in deployed infra, butnpm run migrationgenerates from the same entity diff — it would have emitted eleven spuriousDROP INDEXstatements into the next migration anyone generated.What changed
Nine entities gain class-level
@Indexdeclarations for the indexes their migrations already created:1785460000000-AddLedgerContentChangeScanIndexes(updated, id)keyset indexes for the per-minute ledger content-change scan1785470000000-AddTradingOrderCreatedIndextrading_order (created)1785510000000-AddTradingOrderRuleIdIndextrading_order (tradingRuleId, id)This is a no-op against the database. Those migrations deliberately used TypeORM's deterministic naming (
IDX_+ first 26 hex ofsha1(table + '_' + columns.sort().join('_'))), so the decorators reproduce the existing names byte-for-byte. Verified against realEntityMetadata— resolved name, ordereddatabaseNames and uniqueness match the SQL for all eleven, including the relation form@Index((o) => [o.tradingRule, o.id]), which TypeORM resolves to(tradingRuleId, id).Deliberately not declared
1785520000000'slogindex is a covering index —INCLUDE ("totalBalanceChf", "btcPriceChf")— which TypeORM cannot express. Declaring its five key columns would be worse than leaving it alone: Postgres counts INCLUDE columns inpg_index.indkey, so TypeORM's loader reports seven columns against a five-column declaration,dropOldIndicesdrops on the count mismatch, andcreateNewIndicesrecreates it without the INCLUDE — silently losing the index-only scan the migration was written for. Recorded as migration-owned on the entity instead.The regression guard
src/shared/utils/__tests__/migration-index-parity.spec.tssweeps every migration forCREATE INDEX(applying DROP/CREATE in source order), builds real TypeORM entity metadata, and compares resolved names, column order,databaseNames and uniqueness. Reading expectations out of the SQL rather than restating them is what makes it independent of how a decorator happens to be written — it sees through relations, inheritedIEntitycolumns and@Column({ name })overrides.A curated list of migrations was the first design and was wrong: it silently stops covering each new index migration, which is precisely how this drift got in. Two more index migrations landed on develop while this branch was open.
Ten indexes are exempt, each with a recorded reason, and the exemption list is self-retiring — declare one and its entry must be removed or the spec fails. Three of them are pre-existing drift on develop (
virtual_iban_issuance_intent, where the entity declares the same columns but TypeORM generates a different name, so the migration's index is orphaned); fixing those belongs to the owning subdomain.Also
payout-order.entity.spec.tsselected its index positionally (.find(i => typeof i.columns === 'function')). Class decorators apply bottom-up, so the new(updated, id)decorator registers first and that lookup would have returned the wrong index, failingexpect(index.unique).toBe(true). Now selects by resolved columns.