UPSTREAM FIX

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:update

Steps to reproduce

1. Add a module with [JoinProcessor](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/searching-with-repositories.html#join-processor) for the OrderRepository using the files below:
<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 Details

In 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 the
echo '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.

LineCode match per tagTests
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
QuestionAnswerProbabilitiesConfidence
Change kindbugfixbugfix 96.9%, refactor 1.2%, tests_only 0.8%, feature 0.6%, dependency 0.3%, docs_only 0.3%92.6%
Areaframeworkframework 91.6%, graphql_api 2.3%, admin 1.7%81.8%
Reported versionunspecifiedunspecified 45.0%, 2.4.0 3.5%, 2.4.8-p3 1.4%19.8%
Scope0.54 of 20 55.8%, 1 34.4%, 2 9.8%15.9%
Risk0.29 of 20 74.6%, 1 21.6%, 2 3.8%40.8%
Worth backporting1.45 of 22 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.

Packages (1): magento/framework
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.

Sources

Łukasz Bajsarowicz
Built by

Łukasz Bajsarowicz, e-commerce architect

Magento and Adobe Commerce architecture, upgrades, performance and audits for merchants and agencies since 2015; magento.watch is the tooling I use on those projects.

Open source, maintained on weekends.