magento2-37550: JoinProcessor join definitions are only applied the first time per request
Community fix magento2-37550 merged into magento/magento2 on 2025-02-17, released in 2.4.8; applies cleanly to 27 releases from 2.4.6 to 2.4.7-p10.
JoinProcessor join definitions are only applied the first time per request edited
- Pull request title
- JoinProcessor caching issue
- Pull request
- magento/magento2#37550
- Issues
- #27504 human
- Author
- @ouzned
- Merged
- 2025-02-17
- Fixed in
- 2.4.8
- Reported on
- 2.4.0
- Categories
- —
- Components
- magento/framework
Labels
- Area
- Order
- Component
- Cache
- Priority
- P1
- Severity
- —
- Reported on (labels)
- 2.4.0, 2.4.x
Issue
Title and steps come from the upstream issue and pull request.
Description
Searching for order #001 ... Not Found
Searching for order #002 ...
In Mysql.php line 110:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'store.group_id' in 'where clause', query was: SELECT `main_table`.* FROM `sales_order` AS `main_table` WHERE ((`
increment_id` = '002')) AND ((`store`.`group_id` = 1))
In Mysql.php line 91:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'store.group_id' in 'where clause'
order:updateSteps to reproduce
<details><summary>app/code/SomeVendor/SomeModule/registration.php</summary>
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'SomeVendor_SomeModule', __DIR__);
</details><details><summary>app/code/SomeVendor/SomeModule/etc/module.xml</summary>
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="SomeVendor_SomeModule" />
</config>
</details><details><summary>app/code/SomeVendor/SomeModule/etc/di.xml</summary>
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Framework\Console\CommandListInterface">
<arguments>
<argument name="commands" xsi:type="array">
<item name="order:update" xsi:type="object">SomeVendor\SomeModule\Console\Command\OrderUpdate</item>
</argument>
</arguments>
</type>
<type name="SomeVendor\SomeModule\Console\Command\OrderUpdate">
<arguments>
<argument name="orderRepository" xsi:type="object">SomeVendor\SomeModule\Model\OrderRepository</argument>
</arguments>
</type>
<virtualType name="SomeVendor\SomeModule\Model\OrderRepository" type="Magento\Sales\Model\OrderRepository">
<arguments>
<argument name="collectionProcessor" xsi:type="object">SomeVendor\SomeModule\Model\OrderRepository\CollectionProcessor</argument>
</arguments>
</virtualType>
<virtualType name="SomeVendor\SomeModule\Model\OrderRepository\CollectionProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor">
<arguments>
<argument name="processors" xsi:type="array">
<item name="joins" xsi:type="object">SomeVendor\SomeModule\Model\Api\SearchCriteria\CollectionProcessor\StoreViewJoinProcessor</item>
</argument>
</arguments>
</virtualType>
<virtualType name="SomeVendor\SomeModule\Model\Api\SearchCriteria\CollectionProcessor\StoreViewJoinProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\JoinProcessor">
<arguments>
<argument name="customJoins" xsi:type="array">
<item name="store.group_id" xsi:type="object">SomeVendor\SomeModule\Model\Api\SearchCriteria\JoinProcessor\Store</item>
</argument>
</arguments>
</virtualType>
</config>
</details><details><summary>app/code/SomeVendor/SomeModule/Model/Api/SearchCriteria/JoinProcessor/Store.php</summary>
<?php
/**
* Store Join Processor
*/
declare(strict_types = 1);
namespace SomeVendor\SomeModule\Model\Api\SearchCriteria\JoinProcessor;
use Magento\Framework\Api\SearchCriteria\CollectionProcessor\JoinProcessor\CustomJoinInterface;
use Magento\Framework\Data\Collection\AbstractDb;
/**
* Store Join Processor
*/
class Store implements CustomJoinInterface
{
/**
* @inheritDoc
*/
public function apply(AbstractDb $collection)
{
$collection->join(
['store' => $collection->getResource()->getTable('store')],
'store.store_id = main_table.store_id',
[]
);
return true;
}
}
</details><details><summary>app/code/SomeVendor/SomeModule/Console/Command/OrderUpdate.php</summary>
<?php
/**
* Order Update CLI
*/
declare(strict_types = 1);
namespace SomeVendor\SomeModule\Console\Command;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Order Update CLI
*/
class OrderUpdate extends Command
{
/** @var OrderRepositoryInterface */
protected $orderRepository;
/** @var SearchCriteriaBuilder */
protected $searchCriteriaBuilder;
/**
* OrderUpdate constructor.
*
* @param OrderRepositoryInterface $orderRepository
* @param SearchCriteriaBuilder $searchCriteriaBuilder
*/
public function __construct(
OrderRepositoryInterface $orderRepository,
SearchCriteriaBuilder $searchCriteriaBuilder
) {
$this->orderRepository = $orderRepository;
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
parent::__construct();
}
/**
* @inheritDoc
*/
protected function configure()
{
$this->setName('order:update');
$this->setDescription('Order Update');
parent::configure();
}
/**
* @inheritDoc
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
foreach (['001', '002'] as $orderId) {
echo 'Searching for order #' . $orderId . ' ... ';
$order = $this->searchOrder($orderId, 1);
echo $order ? ' Found' : ' Not Found';
echo PHP_EOL;
}
}
/**
* Search Order
*
* @param string $incrementId
* @param int $storeId
*
* @return OrderInterface|null
*/
protected function searchOrder(string $incrementId, int $storeId): ?OrderInterface
{
$searchCriteria = $this->searchCriteriaBuilder
->addFilter('increment_id', $incrementId)
->addFilter('store.group_id', $storeId)
->create();
$orders = $this->orderRepository->getList($searchCriteria)->getItems();
return current($orders) ?: null;
}
}
</details>2. Run the following CLI commands:
php bin/magento module:enable SomeVendor_SomeModule
php bin/magento setup:upgrade
php bin/magento cache:clean
php bin/magento order:update
Expected result
Searching for order #001 ... Not Found Searching for order #002 ... Not Found
Actual result
Searching for order #001 ... Not Found
Searching for order #002 ...
In Mysql.php line 110:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'store.group_id' in 'where clause', query was: SELECT `main_table`.* FROM `sales_order` AS `main_table` WHERE ((`
increment_id` = '002')) AND ((`store`.`group_id` = 1))
In Mysql.php line 91:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'store.group_id' in 'where clause'
order:update### More DetailsIn the second
foreach iteration the JoinProcessor it not applied because it is marked as already applied in the $this->appliedFields property of the [Magento\Framework\Api\SearchCriteria\CollectionProcessor\JoinProcessor](https://github.com/magento/magento2/blob/2.4-develop/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/JoinProcessor.php#L58) class. It looks right, but in the second iteration we have a fresh new instance of the order collection where is no JOIN which was added in the first iteration. You may check it with theecho 'Collection ID: ' . spl_object_id($collection) . PHP_EOL;---
Please provide [Severity](https://devdocs.magento.com/guides/v2.4/contributor-guide/contributing.html#backlog) assessment for the Issue as Reporter. This information will help during Confirmation and Issue triage processes.
- [ ] Severity: S0 _- Affects critical data or functionality and leaves users without workaround._
- [ ] Severity: S1 _- Affects critical data or functionality and forces users to employ a workaround._
- [x] Severity: S2 _- Affects non-critical data or functionality and forces users to employ a workaround._
- [ ] Severity: S3 _- Affects non-critical data or functionality and does not force users to employ a workaround._
- [ ] Severity: S4 _- Affects aesthetics, professional look and feel, “quality” or “usability”._
Taken from the upstream issue.
Code match per tag
Each tag was checked with git apply --check against that tag's files. A clean match means the change applies; it is not a test result. Tags that already contain the fix are marked.
| Line | Code match per tag | Tests |
|---|---|---|
| 2.4.6 | 2.4.6 clean 2.4.6-p1 clean 2.4.6-p2 clean 2.4.6-p3 clean 2.4.6-p4 clean 2.4.6-p5 clean 2.4.6-p6 clean 2.4.6-p7 clean 2.4.6-p8 clean 2.4.6-p9 clean 2.4.6-p10 clean 2.4.6-p11 clean 2.4.6-p12 clean 2.4.6-p13 clean 2.4.6-p14 clean 2.4.6-p15 clean | 2.4.6: no test data 2.4.6-p1: no test data 2.4.6-p2: no test data 2.4.6-p3: no test data 2.4.6-p4: no test data 2.4.6-p5: no test data 2.4.6-p6: no test data 2.4.6-p7: no test data 2.4.6-p8: no test data 2.4.6-p9: no test data 2.4.6-p10: no test data 2.4.6-p11: no test data 2.4.6-p12: no test data 2.4.6-p13: no test data 2.4.6-p14: no test data 2.4.6-p15: no test data |
| 2.4.7 | 2.4.7 clean 2.4.7-p1 clean 2.4.7-p2 clean 2.4.7-p3 clean 2.4.7-p4 clean 2.4.7-p5 clean 2.4.7-p6 clean 2.4.7-p7 clean 2.4.7-p8 clean 2.4.7-p9 clean 2.4.7-p10 clean | 2.4.7: no test data 2.4.7-p1: no test data 2.4.7-p2: no test data 2.4.7-p3: no test data 2.4.7-p4: no test data 2.4.7-p5: no test data 2.4.7-p6: no test data 2.4.7-p7: no test data 2.4.7-p8: no test data 2.4.7-p9: no test data 2.4.7-p10: fails before, passes afterintegration: fails before, passes after |
| 2.4.8 | 2.4.8 conflictcontains the fix 2.4.8-p1 conflictcontains the fix 2.4.8-p2 conflictcontains the fix 2.4.8-p3 conflictcontains the fix 2.4.8-p4 conflictcontains the fix 2.4.8-p5 conflictcontains the fix | 2.4.8: no test data 2.4.8-p1: no test data 2.4.8-p2: no test data 2.4.8-p3: no test data 2.4.8-p4: no test data 2.4.8-p5: no test data |
| 2.4.9 | 2.4.9 conflictcontains the fix | 2.4.9: no test data |
Triage
Model @cf/cloudflare/clef. Probability this is a bug fix: 97.5%. Probability it is security relevant: 0.5%.
Show the model's answers and probabilities
| Question | Answer | Probabilities | Confidence |
|---|---|---|---|
| Change kind | bugfix | bugfix 96.9%, refactor 1.2%, tests_only 0.8%, feature 0.6%, dependency 0.3%, docs_only 0.3% | 92.6% |
| Area | framework | framework 91.6%, graphql_api 2.3%, admin 1.7% | 81.8% |
| Reported version | unspecified | unspecified 45.0%, 2.4.0 3.5%, 2.4.8-p3 1.4% | 19.8% |
| Scope | 0.54 of 2 | 0 55.8%, 1 34.4%, 2 9.8% | 15.9% |
| Risk | 0.29 of 2 | 0 74.6%, 1 21.6%, 2 3.8% | 40.8% |
| Worth backporting | 1.45 of 2 | 2 57.9%, 1 29.6%, 0 12.6% | 15.7% |
Download
For cweagans/composer-patches, choose a version below and download the bundle. Copy its magento2-37550/ folder into patches/composer/, merge composer.patches.json into composer.json, then run composer install. Test files are always removed; paths are relative to each package root, using the default -p1 level.
Bundle README (what the ZIP ships)
# magento2-37550 Community fix merged upstream into magento/magento2, adapted by magento.watch. This is not a patch published by Adobe. Pull request: https://github.com/magento/magento2/pull/37550 Issue: https://github.com/magento/magento2/issues/27504 Author: @ouzned Source commit: 24de63e9f50e35c2ec65bc1cb8ffd271313acaeb Modifications: test files and documentation removed, paths rewritten relative to each Composer package. Licence: OSL-3.0 / AFL-3.0, as the original Magento Open Source code. Maintainer: Łukasz Bajsarowicz (@lbajsarowicz)
Licence: Magento Open Source code under OSL-3.0 and AFL-3.0. The bundle carries the original author, source commit and the list of modifications.
