Composite lead indexes, and unblock the run that creates them

FeatureInitializerService
Shipped
August 20, 2026 at 11:29 PM UTC
Author
Kamo
Commit
918de6e

The /leads grid now filters and pages in the database instead of shipping the whole organisation to the browser. With only IX_LEADS_ORG_ID that would relocate the scan rather than remove it: the planner still reads every lead in the tenant and discards what the filter rejects. Four indexes, each leading with ORG_ID so Yugabyte keeps hash-partitioning on the tenant and then narrows: assignment (the filter behind the original report — 801 of 827 leads in the largest org were unassigned, and a member filtering to their own book wanted seven rows out of 1,427), market and status (the tab bar, applied on essentially every load), and dateCreated (the default sort and the date-range filter). Hibernate's ddl-auto would create them from the @Index annotations in the same boot, but it emits CREATE INDEX without IF NOT EXISTS and swallows the failure, which makes 'did it actually land' unanswerable from the log. Explicit, idempotent and retried here instead. Purely additive, so the gate-the-DROP rule does not apply — but the 40001 retry does, since this contends with that same ddl-auto pass moments earlier. IX_LEADS_ORG_ID is left in place though now redundant: dropping an index a racing pass may have just failed to recreate is how a table ends up with neither. LeadContactPointSeedRunner had to change for any of this to run at all. It paged with findAll(Pageable), which Spring Data executes as a content SELECT plus a COUNT in one read-only transaction — so the COUNT is never the first statement, and Yugabyte can only transparently retry a read restart on the first statement. Because the runner writes contact points between pages it manufactures its own restart. Latent until there were enough unseeded leads to matter; at 851 it failed outright: 40001 Restart read required ... query layer retry isn't possible on: select count(l1_0.uid) from leads That aborted the whole run at order 99 and took every later migration with it, including this one. Replaced with a keyset scan (uid > :afterUid, one statement per batch, no COUNT), which also survives inserts mid-scan in a way OFFSET paging never did.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing