Skip to content

Guard catalog pricing rule lookup during migrations#4309

Open
boboldehampsink wants to merge 6 commits into
craftcms:5.xfrom
boboldehampsink:fix/catalog-pricing-rules-migration-table-check
Open

Guard catalog pricing rule lookup during migrations#4309
boboldehampsink wants to merge 6 commits into
craftcms:5.xfrom
boboldehampsink:fix/catalog-pricing-rules-migration-table-check

Conversation

@boboldehampsink

Copy link
Copy Markdown
Contributor

Summary

  • Return false from CatalogPricingRules::hasCatalogPricingRules() when the catalog pricing rules table has not been created yet.
  • Prevent product/purchasable queries from touching commerce_catalogpricingrules while Craft/Commerce migrations are still being applied.

Context

During a Craft 5 / Commerce 5 upgrade, a Craft CMS migration can save single entries and serialize product relation fields before Commerce has applied m221026_105212_add_catalog_pricing_table. That path calls ProductQuery::beforePrepare(), which calls hasCatalogPricingRules(), and currently attempts exists() against commerce_catalogpricingrules even when the table does not exist yet.

Treating the missing table as “no catalog pricing rules yet” lets migrations continue until Commerce creates the table normally.

@boboldehampsink boboldehampsink requested a review from a team as a code owner June 3, 2026 09:05
@boboldehampsink

Copy link
Copy Markdown
Contributor Author

Added a second guard for the same migration-order path.

After the catalog pricing rules table guard, the upgrade continued until product relation serialization hit ProductQuery joins against commerce_site_stores before m230110_052712_site_stores had created it. The latest commit now gates store-dependent joins/selects in ProductQuery and PurchasableQuery, and also avoids the later inventory-item join until commerce_inventoryitems exists.

@boboldehampsink

Copy link
Copy Markdown
Contributor Author

Added another migration-order guard.

The next failure was from ProductQuery selecting Commerce 5 purchasable detail columns (weight, length, width, height) before m221025_083940_add_purchasables_stores_table had moved those columns from variants onto commerce_purchasables. The latest commit selects NULL for those fields until the columns exist, and aborts only when the query is explicitly filtering by unavailable purchasable detail columns.

@boboldehampsink

Copy link
Copy Markdown
Contributor Author

Added a migration data-repair fix for m240219_194855_donation_multi_store.

The migration assumes every commerce_donations row has a matching commerce_purchasables row before inserting into commerce_purchasables_stores. On the upgrade database, a donation row existed without the base purchasable row, causing a foreign key violation. The latest commit backfills the missing purchasable record from the donation row before creating store-specific purchasable rows.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens Commerce’s catalog pricing/store-aware query paths so Craft/Commerce upgrades can run Craft migrations without triggering SQL against Commerce tables/columns that may not exist yet.

Changes:

  • Guard CatalogPricingRules::hasCatalogPricingRules() so missing commerce_catalogpricingrules is treated as “no rules yet”.
  • Add schema-existence guards in PurchasableQuery/ProductQuery to avoid joining store/inventory/catalog-pricing tables during partial schema states.
  • Ensure the donation multi-store migration creates a corresponding commerce_purchasables row when missing.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/services/CatalogPricingRules.php Avoids querying catalog pricing rules when the table hasn’t been created yet.
src/migrations/m240219_194855_donation_multi_store.php Ensures donations have a purchasable row before creating per-store purchasable-store rows.
src/elements/db/PurchasableQuery.php Adds guards/fallback selects to prevent store/inventory/catalog pricing joins when schema isn’t ready.
src/elements/db/ProductQuery.php Adds guards/fallback selects to prevent store/catalog pricing joins when schema isn’t ready.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 54 to 57
if ($this->_hasCatalogPricingRules === null) {
$this->_hasCatalogPricingRules = $this->_createCatalogPricingRuleQuery()->exists();
$this->_hasCatalogPricingRules = Craft::$app->getDb()->tableExists(Table::CATALOG_PRICING_RULES) &&
$this->_createCatalogPricingRuleQuery()->exists();
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in f382275. hasCatalogPricingRules() now returns false early while commerce_catalogpricingrules is missing, without writing to the _hasCatalogPricingRules cache. Once the table exists later in the same migration/request, the method will query and memoize the real result.

Comment on lines +88 to +96
if ($this->db->columnExists(Table::PURCHASABLES, 'taxCategoryId')) {
$purchasable['taxCategoryId'] = (new Query())
->select('id')
->from(Table::TAXCATEGORIES)
->orderBy(['id' => SORT_ASC])
->scalar();
}

$this->insert(Table::PURCHASABLES, $purchasable);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in f382275. The donation purchasable backfill now includes dateCreated, dateUpdated, and a generated uid. It also checks for an available tax category before setting taxCategoryId, and throws a clear migration exception if one cannot be found rather than attempting an invalid insert.

Comment thread src/elements/db/PurchasableQuery.php Outdated
Comment on lines +739 to +743
new Expression('NULL as [[availableForPurchase]]'),
new Expression('NULL as [[basePrice]]'),
new Expression('NULL as [[basePromotionalPrice]]'),
new Expression('NULL as [[freeShipping]]'),
new Expression('NULL as [[maxQty]]'),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in f382275. The missing-store-table fallback now selects boolean-safe defaults for availableForPurchase and freeShipping (1 and 0) instead of NULL, so hydration won’t assign null to non-nullable bool properties.

Comment on lines +744 to +748
new Expression('NULL as [[minQty]]'),
new Expression('NULL as [[inventoryTracked]]'),
new Expression('NULL as [[allowOutOfStockPurchases]]'),
new Expression('NULL as [[promotable]]'),
new Expression('NULL as [[shippingCategoryId]]'),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in f382275. The fallback selects 0 for inventoryTracked, allowOutOfStockPurchases, and promotable instead of NULL, matching the non-nullable bool properties on Purchasable.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants