Skip to content

fix(pricing): correct inverted partial predicate on price_list_rule index (up to 50x in B2B scenario) - #16289

Open
docloulou wants to merge 1 commit into
medusajs:developfrom
docloulou:fix/pricing-price-list-rule-index-predicate
Open

fix(pricing): correct inverted partial predicate on price_list_rule index (up to 50x in B2B scenario)#16289
docloulou wants to merge 1 commit into
medusajs:developfrom
docloulou:fix/pricing-price-list-rule-index-predicate

Conversation

@docloulou

@docloulou docloulou commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

What — What changes are introduced in this PR?

A migration correcting the partial predicate on IDX_price_list_rule_price_list_id, which has indexed only soft-deleted rows since #6732:

-- today
CREATE INDEX "IDX_price_list_rule_price_list_id"
  ON "price_list_rule" (price_list_id) WHERE deleted_at IS NOT NULL;

The PriceListRule model declared where: "deleted_at IS NULL" in that same commit and still does (models/price-list-rule.ts:14-17), so this aligns the database with the model. No model, entity, or API change.

Why — Why are these changes relevant or necessary?

calculatePrices resolves applicable price lists with a correlated subquery filtering plr.deleted_at IS NULL, which an IS NOT NULL index cannot serve. The planner sequentially scans price_list_rule once per candidate price row, so cost grows with the tenant's total price-list count rather than with the page — the B2B shape of one negotiated price list per customer.

It only pays off when the pricing context carries a rule attribute, the price lists actually have rules, and the rule table is large enough for the planner to switch to an index scan. Both negative cases are measured below.

How — How have these changes been implemented?

DROP then CREATE: the name already exists, so CREATE INDEX IF NOT EXISTS alone is a no-op and cannot change a predicate. The replacement is built under a scratch name and swapped in by rename, which keeps ACCESS EXCLUSIVE to the rename only — read stall 1468 ms → 68 ms at 1M rows under concurrent load. Written by hand because MikroORM's differ compares index names and columns but not partial predicates: migration:create emits byte-identical output against a broken and a corrected schema. Same form as customer/Migration20251010130829.ts.

Testing — How have these changes been tested, or how can the reviewer test the feature?

133/133 pricing integration tests pass, including the 49 calculate-price.spec.ts cases covering price-list rules and customer groups. Applied through the real migration runner: [migrator] Applied 'Migration20260802103000', resulting predicate WHERE (deleted_at IS NULL), down() round-trips, no residual drift.

cd packages/modules/pricing && yarn test:integration

Benchmark — standalone, psql only, no Node or Medusa app; builds a scratch database and measures the query under both predicates.

===> bench-price-list-rule-index.sh


Examples

No API change. Cost of one calculatePrices call — PostgreSQL 16.14, 20,000 price sets, 340,000 price rows, 50-variant page. Only the rule table varies:

price lists rule rows before after
150 120 3.16 ms 2.97 ms Seq Scan both — no gain
25,000 20,000 136.93 ms 2.51 ms SeqIndex Scan, 54x
25,000 0 6.59 ms 6.58 ms no rules — subquery never runs

The broken path grows linearly with rule count while the corrected one stays flat. Small or unruled tenants gain nothing from this change.


Checklist

  • I have added a changeset for this PR — patch on @medusajs/pricing
  • The changes are covered by relevant tests — the existing pricing integration suite covers the resolution semantics this index serves
  • I have verified the code works as intended locally
  • I have linked the related issue(s) if applicable

…ndex

"IDX_price_list_rule_price_list_id" was created by the initial 2023
migration with `WHERE deleted_at IS NOT NULL`, so it only ever indexed
soft-deleted rows. PricingRepository.calculatePrices resolves active
price lists with `plr.price_list_id = pl.id AND plr.deleted_at IS NULL`,
which the index could never serve, leaving the planner to sequentially
scan price_list_rule once per candidate price row.

The PriceListRule model has always declared `where: "deleted_at IS NULL"`
on this index, so the migration aligns the database with the model rather
than changing it. MikroORM's differ compares index names and columns but
not partial predicates, so `migration:create" cannot detect or repair the
drift - the DDL is written by hand, matching existing precedent in the
repo (customer/Migration20251010130829.ts, fulfillment/Migration20250120115002.ts).

The replacement index is built under a scratch name and swapped in by
rename so concurrent reads stay live for all but the final rename.
@changeset-bot

changeset-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 87d529c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 79 packages
Name Type
@medusajs/pricing Patch
@medusajs/medusa Patch
@medusajs/test-utils Patch
@medusajs/loyalty-plugin Patch
@medusajs/medusa-oas-cli Patch
integration-tests-http Patch
@medusajs/analytics Patch
@medusajs/api-key Patch
@medusajs/auth Patch
@medusajs/caching Patch
@medusajs/cart Patch
@medusajs/currency Patch
@medusajs/customer Patch
@medusajs/file Patch
@medusajs/fulfillment Patch
@medusajs/index Patch
@medusajs/inventory Patch
@medusajs/link-modules Patch
@medusajs/locking Patch
@medusajs/notification Patch
@medusajs/order Patch
@medusajs/payment Patch
@medusajs/product Patch
@medusajs/promotion Patch
@medusajs/rbac Patch
@medusajs/region Patch
@medusajs/sales-channel Patch
@medusajs/settings Patch
@medusajs/stock-location Patch
@medusajs/store Patch
@medusajs/tax Patch
@medusajs/translation Patch
@medusajs/user Patch
@medusajs/workflow-engine-inmemory Patch
@medusajs/workflow-engine-redis Patch
@medusajs/draft-order Patch
@medusajs/oas-github-ci Patch
@medusajs/cache-inmemory Patch
@medusajs/cache-redis Patch
@medusajs/event-bus-local Patch
@medusajs/event-bus-redis Patch
@medusajs/analytics-local Patch
@medusajs/analytics-posthog Patch
@medusajs/auth-emailpass Patch
@medusajs/auth-github Patch
@medusajs/auth-google Patch
@medusajs/caching-redis Patch
@medusajs/file-local Patch
@medusajs/file-s3 Patch
@medusajs/fulfillment-manual Patch
@medusajs/locking-postgres Patch
@medusajs/locking-redis Patch
@medusajs/notification-local Patch
@medusajs/notification-sendgrid Patch
@medusajs/payment-stripe Patch
@medusajs/core-flows Patch
@medusajs/framework Patch
@medusajs/js-sdk Patch
@medusajs/modules-sdk Patch
@medusajs/orchestration Patch
@medusajs/query Patch
@medusajs/types Patch
@medusajs/utils Patch
@medusajs/workflows-sdk Patch
@medusajs/http-types-generator Patch
@medusajs/cli Patch
@medusajs/deps Patch
@medusajs/eslint-plugin Patch
@medusajs/telemetry Patch
@medusajs/admin-bundler Patch
@medusajs/admin-sdk Patch
@medusajs/admin-shared Patch
@medusajs/admin-vite-plugin Patch
@medusajs/dashboard Patch
@medusajs/icons Patch
@medusajs/toolbox Patch
@medusajs/ui-preset Patch
create-medusa-app Patch
@medusajs/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@docloulou docloulou changed the title fix(pricing): correct inverted partial predicate on price_list_rule index fix(pricing): correct inverted partial predicate on price_list_rule index (up to 50x in B2B scenario) Aug 2, 2026
@docloulou
docloulou marked this pull request as ready for review August 2, 2026 21:38
@docloulou
docloulou requested a review from a team as a code owner August 2, 2026 21:38
@medusa-os-bot

medusa-os-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

Thanks for the contribution! Initial automated review looks good.

Fixes a verified database index bug: the original migration created IDX_price_list_rule_price_list_id with an inverted predicate (IS NOT NULL), meaning only soft-deleted rows were indexed. The corrective migration uses a create-then-rename strategy to keep read stall minimal. Bug confirmed in Migration20230929122253.ts:156. Model definition was always correct. Changeset present (patch on @medusajs/pricing). All PR template sections filled. Migration follows established conventions. No security, performance, or correctness concerns. Note: no linked issue for a non-trivial bug fix; contribution guidelines expect one.

Triggered by: PR marked as ready for review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant