From 9a569694d9cd8f0ca9fdbcc012983fce19742a85 Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 17 Aug 2026 22:29:44 -0700 Subject: [PATCH 1/5] Make Magento SQL dialect-agnostic for a Postgres adapter module. Add AdapterInterface helpers (GROUP_CONCAT, FIELD, casts, CREATE TABLE LIKE) and replace MySQL-only SQL at Magento call sites so a separate Postgres module can implement the same methods without rewriting every query. --- .../Model/BulkStatus.php | 5 +- .../Backup/Model/ResourceModel/Helper.php | 3 +- .../Model/ResourceModel/Indexer/Price.php | 52 ++++++------- .../Option/AreBundleOptionsSalable.php | 3 +- .../ResourceModel/Selection/Collection.php | 4 +- .../Model/Attribute/ScopeOverriddenValue.php | 11 ++- .../Category/Product/AbstractAction.php | 27 +++++-- .../Indexer/Product/Flat/Action/Eraser.php | 7 +- .../ResourceModel/Category/Collection.php | 10 ++- .../Collection/AbstractCollection.php | 20 ++++- .../Collection/JoinMinimalPosition.php | 6 +- .../Model/ResourceModel/Product/Gallery.php | 18 ++++- .../Product/Indexer/Eav/AbstractEav.php | 6 +- .../Product/Indexer/Price/DefaultPrice.php | 14 +++- .../Indexer/Price/Query/BaseFinalPrice.php | 14 +++- .../Product/StatusBaseSelectProcessor.php | 5 +- .../ResourceModel/ReadSnapshotPlugin.php | 8 +- .../Ui/Component/Listing/Columns/Websites.php | 11 ++- .../Model/Indexer/IndexerTableSwapper.php | 13 ++-- .../Indexer/Fulltext/Action/DataProvider.php | 7 +- .../Collection/SearchResultApplier.php | 10 ++- .../Model/Variant/Collection.php | 24 +++++- .../Data/AddSecurityTrackingAttributes.php | 2 +- .../Patch/Data/SessionIDColumnCleanUp.php | 6 +- .../Eav/Model/Entity/AbstractEntity.php | 14 +++- .../Model/Entity/Attribute/Source/Table.php | 28 ++++--- .../Entity/Collection/AbstractCollection.php | 6 +- .../ChangelogBatchWalker/IdsSelectBuilder.php | 11 ++- .../Entity/Attribute/Collection.php | 28 ++++++- .../Eav/Model/ResourceModel/ReadHandler.php | 9 ++- .../Collection/SearchResultApplier.php | 3 +- .../Model/ResourceModel/Import/Data.php | 10 ++- .../Model/ResourceModel/Oauth/Consumer.php | 4 +- .../Patch/Data/AddMediaGalleryPermissions.php | 5 +- .../Plugin/ExternalVideoResourceBackend.php | 15 ++-- .../Model/ResourceModel/Review/Summary.php | 6 +- .../Rule/Model/Condition/Sql/Builder.php | 14 ++-- .../UserExpiration/Collection.php | 6 +- .../Patch/Data/SessionIDColumnCleanUp.php | 6 +- .../Data/SetCreditCardAsDefaultTokenType.php | 6 +- .../Product/WeeeAttributeProductSort.php | 13 +++- .../Framework/Cache/Backend/Database.php | 23 +++--- .../Framework/DB/Adapter/AdapterInterface.php | 55 ++++++++++++++ .../Framework/DB/Adapter/Pdo/Mysql.php | 73 +++++++++++++++++++ .../Framework/DB/Query/BatchIterator.php | 12 ++- lib/internal/Magento/Framework/DB/Select.php | 18 ++++- .../Framework/DB/TemporaryTableService.php | 18 ++--- .../Framework/Data/Collection/AbstractDb.php | 11 ++- .../Magento/Setup/Model/ConfigOptionsList.php | 3 +- setup/src/Magento/Setup/Model/Installer.php | 6 +- .../Magento/Setup/Validator/DbValidator.php | 25 ++++--- 51 files changed, 551 insertions(+), 163 deletions(-) diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php b/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php index fc0654c8510..3f835f36aea 100644 --- a/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php +++ b/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php @@ -126,8 +126,9 @@ public function getBulksByUser($userId) OperationInterface::STATUS_TYPE_COMPLETE ]; $select = $collection->getSelect(); - $select->columns(['status' => $this->calculatedStatusSql->get($operationTableName)]) - ->order(new \Zend_Db_Expr('FIELD(status, ' . implode(',', $statusesArray) . ')')); + $statusExpr = $this->calculatedStatusSql->get($operationTableName); + $select->columns(['status' => $statusExpr]) + ->order($select->getAdapter()->getFieldSql((string) $statusExpr, $statusesArray)); $collection->addFieldToFilter('user_id', $userId) ->addOrder('start_time'); diff --git a/app/code/Magento/Backup/Model/ResourceModel/Helper.php b/app/code/Magento/Backup/Model/ResourceModel/Helper.php index 9e217056765..94649e1594e 100644 --- a/app/code/Magento/Backup/Model/ResourceModel/Helper.php +++ b/app/code/Magento/Backup/Model/ResourceModel/Helper.php @@ -178,7 +178,8 @@ public function getHeader() { $dbConfig = $this->getConnection()->getConfig(); - $versionRow = $this->getConnection()->fetchRow('SHOW VARIABLES LIKE \'version\''); + // PgCompat: getServerVersion() is portable (PDO ATTR_SERVER_VERSION). + $versionRow = ['Value' => $this->getConnection()->getServerVersion()]; $hostName = !empty($dbConfig['unix_socket']) ? $dbConfig['unix_socket'] : (!empty($dbConfig['host']) ? $dbConfig['host'] : 'localhost'); diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php index ebd0d73a2c4..1c6eac6b1f1 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php @@ -485,7 +485,12 @@ private function calculateBundleOptionPrice($priceTable, $dimensions) [ 'min_price' => new \Zend_Db_Expr('MIN(' . $minPrice . ')'), 'alt_price' => new \Zend_Db_Expr('MIN(price)'), - 'max_price' => $connection->getCheckSql('group_type = 0', 'MAX(price)', 'SUM(price)'), + // PgCompat: group_type is functionally dependent on option_id within this + // GROUP BY, but MySQL's non-ONLY_FULL_GROUP_BY leniency allowed referencing + // it bare in the CASE condition anyway - Postgres requires it aggregated. + // MIN(group_type) is value-identical to group_type here (same table/row + // set as before), so this is a no-op on MySQL and a fix on Postgres. + 'max_price' => $connection->getCheckSql('MIN(group_type) = 0', 'MAX(price)', 'SUM(price)'), 'tier_price' => new \Zend_Db_Expr('MIN(' . $tierPrice . ')'), 'alt_tier_price' => new \Zend_Db_Expr('MIN(tier_price)'), ] @@ -734,33 +739,24 @@ private function calculateDynamicBundleSelectionPrice(array $dimensions): void ] ); $select = $this->stockStatusQueryProcessor->execute($select); - $query = str_replace('AS `idx`', 'AS `idx` USE INDEX (PRIMARY)', (string) $select); - - $insertColumns = [ - 'entity_id', - 'customer_group_id', - 'website_id', - 'option_id', - 'selection_id', - 'group_type', - 'is_required', - 'price', - 'tier_price' - ]; - $insertColumns = array_map(function ($item) use ($connection) { - return $connection->quoteIdentifier($item); - }, $insertColumns); - $updateValues = []; - foreach ($insertColumns as $column) { - $updateValues[] = sprintf("%s = VALUES(%s)", $column, $column); - } - - $connection->query(sprintf( - "INSERT INTO `" . $this->getBundleSelectionTable() . "` (%s) %s ON DUPLICATE KEY UPDATE %s", - implode(",", $insertColumns), - $query, - implode(",", $updateValues) - )); + $connection->query( + $connection->insertFromSelect( + $select, + $this->getBundleSelectionTable(), + [ + 'entity_id', + 'customer_group_id', + 'website_id', + 'option_id', + 'selection_id', + 'group_type', + 'is_required', + 'price', + 'tier_price', + ], + \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + ) + ); } /** diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php b/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php index dc12560003a..b0b3fef3c4d 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php @@ -98,7 +98,8 @@ public function execute(int $entityId, int $storeId): bool ); $isOptionSalableExpr = new \Zend_Db_Expr( sprintf( - 'MAX(IFNULL(child_status_store.value, child_status_global.value) != %s)', + 'MAX(CASE WHEN %s != %s THEN 1 ELSE 0 END)', + $connection->getIfNullSql('child_status_store.value', 'child_status_global.value'), ProductStatus::STATUS_DISABLED ) ); diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php index 543437b4b3b..1ac00f28846 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php @@ -324,7 +324,9 @@ public function addPriceFilter($product, $searchMin, $useRegularPrice = false) $minimalPriceExpression = self::INDEX_TABLE_ALIAS . '.price'; } else { $this->getCatalogRuleProcessor()->addPriceData($this, 'selection.product_id'); - $minimalPriceExpression = 'LEAST(minimal_price, IFNULL(catalog_rule_price, minimal_price))'; + $minimalPriceExpression = 'LEAST(minimal_price, ' + . $this->getConnection()->getIfNullSql('catalog_rule_price', 'minimal_price') + . ')'; } $orderByValue = new \Zend_Db_Expr( '(' . diff --git a/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php b/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php index 97eb89718a6..779eaebc44b 100644 --- a/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php +++ b/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php @@ -144,9 +144,18 @@ private function initAttributeValues($entityType, $entity, $storeId) $storeIds[] = $storeId; } $selects = []; + // PgCompat: this select is UNION ALL'd (below) with one of these per EAV + // backend type table (varchar/int/decimal/text/datetime) - MySQL coerces the + // differently-typed "value" columns across branches implicitly, Postgres + // requires them to already share one type. Same fix as the other EAV + // union-cast sites (union-cast-eav-abstract-collection.patch, + // Eav\Model\ResourceModel\ReadHandler::execute(), ReadSnapshotPlugin). foreach ($attributeTables as $attributeTable => $attributeCodes) { $select = $metadata->getEntityConnection()->select() - ->from(['t' => $attributeTable], ['value' => 't.value', 'store_id' => 't.store_id']) + ->from( + ['t' => $attributeTable], + ['value' => $metadata->getEntityConnection()->castToText('t.value'), 'store_id' => 't.store_id'] + ) ->join( ['a' => $this->resourceConnection->getTableName('eav_attribute')], 'a.attribute_id = t.attribute_id', diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php index c05aa9e443b..97999c3baaf 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php @@ -388,7 +388,11 @@ protected function getNonAnchorCategoriesSelect(Store $store) [ 'category_id' => 'cc.entity_id', 'product_id' => 'ccp.product_id', - 'position' => 'ccp.position', + // PgCompat: bare ccp.position is non-aggregated under the GROUP BY + // addFilteringByChildProductsToSelect() adds below; MIN() is a no-op + // on MySQL (one ccp row per cc.entity_id/ccp.product_id group here) + // and required by Postgres. + 'position' => new \Zend_Db_Expr('MIN(ccp.position)'), 'is_parent' => new \Zend_Db_Expr('1'), 'store_id' => new \Zend_Db_Expr($store->getId()), 'visibility' => new \Zend_Db_Expr( @@ -639,8 +643,11 @@ protected function createAnchorSelect(Store $store) [ 'category_id' => 'cc.entity_id', 'product_id' => 'ccp.product_id', + // PgCompat: ccp2.position is non-aggregated under the GROUP BY + // addFilteringByChildProductsToSelect() adds below; MAX() is a no-op on + // MySQL (one ccp2 row per group here) and required by Postgres. 'position' => new \Zend_Db_Expr( - $this->connection->getIfNullSql('ccp2.position', 'MIN(ccp.position) + 10000') + $this->connection->getIfNullSql('MAX(ccp2.position)', 'MIN(ccp.position) + 10000') ), 'is_parent' => new \Zend_Db_Expr('0'), 'store_id' => new \Zend_Db_Expr($store->getId()), @@ -874,16 +881,26 @@ protected function getAllProducts(Store $store) $this->connection->getIfNullSql('cpvs.value', 'cpvd.value') . ' IN (?)', $this->visibility->getVisibleInSiteIds() )->group( - 'cp.entity_id' + // PgCompat: the visibility column below is a non-aggregated COALESCE + // expression selected under this GROUP BY - MySQL's non-ONLY_FULL_GROUP_BY + // leniency tolerated that; Postgres requires it grouped too. cpvs/cpvd are + // already filtered to at most one row per cp.entity_id (store_id/attribute_id + // predicates above), so this doesn't change which rows collapse together. + ['cp.entity_id', new \Zend_Db_Expr($this->connection->getIfNullSql('cpvs.value', 'cpvd.value'))] )->columns( [ 'category_id' => new \Zend_Db_Expr($store->getRootCategoryId()), 'product_id' => 'cp.entity_id', + // PgCompat: ccp.product_id IS NOT NULL is a non-aggregated column + // reference in the CASE condition under the GROUP BY above; COUNT(...)>0 + // is the aggregate-safe equivalent (true iff at least one non-null + // ccp.product_id exists in the group, same truth value as the original + // MySQL-tolerated condition). 'position' => new \Zend_Db_Expr( - $this->connection->getCheckSql('ccp.product_id IS NOT NULL', 'MIN(ccp.position)', '10000') + $this->connection->getCheckSql('COUNT(ccp.product_id) > 0', 'MIN(ccp.position)', '10000') ), 'is_parent' => new \Zend_Db_Expr( - $this->connection->getCheckSql('ccp.product_id IS NOT NULL', '1', '0') + $this->connection->getCheckSql('COUNT(ccp.product_id) > 0', '1', '0') ), 'store_id' => new \Zend_Db_Expr($store->getId()), 'visibility' => new \Zend_Db_Expr( diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php index 6bc30564c87..bca3cfedf13 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php @@ -109,7 +109,12 @@ public function removeDisabledProducts(array &$ids, $storeId) . 'product_table.' . $metadata->getLinkField(), [] ); - $select->where('IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_DISABLED); + // PgCompat: getIfNullSql() renders per-dialect (IFNULL on MySQL, COALESCE on + // Postgres) instead of hardcoding MySQL's IFNULL() text. + $select->where( + $this->connection->getIfNullSql('status_attr.value', 'status_global_attr.value') . ' = ?', + Status::STATUS_DISABLED + ); $result = $this->connection->query($select); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index d7dcf295a7d..25dccda66a6 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -723,9 +723,13 @@ private function getProductsCountQuery(array $categoryIds, $addVisibilityFilter if (true === $addVisibilityFilter) { $select->where('cat_index.visibility in (?)', $this->catalogProductVisibility->getVisibleInSiteIds()); } - if (count($categoryIds) > 1) { - $select->group('cat_index.category_id'); - } + // PgCompat: was only grouped when count($categoryIds) > 1 - but this select + // always mixes a non-aggregated column (category_id) with an aggregate + // (count(...)), so the single-category case needs GROUP BY too. MySQL's + // non-ONLY_FULL_GROUP_BY mode tolerated the omission (trivially one group + // either way); Postgres requires it unconditionally. Grouping a single-value set + // is a no-op on both, so this fixes the real bug rather than working around it. + $select->group('cat_index.category_id'); return $select; } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php index da4f5344bb6..0d6cbb1c513 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php @@ -239,10 +239,26 @@ protected function _addLoadAttributesSelectValues($select, $table, $type) $storeId = $this->getStoreId(); if ($storeId) { $connection = $this->getConnection(); - $valueExpr = $connection->getCheckSql('t_s.value_id IS NULL', 't_d.value', 't_s.value'); + // PgCompat: this select is UNION ALL'd with one of these per EAV backend + // type (varchar/int/decimal/text/datetime) in _loadAttributes() - MySQL + // coerces the differently-typed columns across branches implicitly, + // Postgres requires them to already share one type before the UNION. Same + // fix as the parent Eav\Model\Entity\Collection\AbstractCollection version + // (union-cast-eav-abstract-collection.patch) this override replaces - that + // patch only touched the parent method, missing this store-fallback + // override entirely since it never calls parent in the $storeId branch. + $valueExpr = $connection->getCheckSql( + 't_s.value_id IS NULL', + $connection->castToText('t_d.value'), + $connection->castToText('t_s.value') + ); $select->columns( - ['default_value' => 't_d.value', 'store_value' => 't_s.value', 'value' => $valueExpr] + [ + 'default_value' => $connection->castToText('t_d.value'), + 'store_value' => $connection->castToText('t_s.value'), + 'value' => $valueExpr, + ] ); } else { $select = parent::_addLoadAttributesSelectValues($select, $table, $type); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection/JoinMinimalPosition.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection/JoinMinimalPosition.php index 132f0847b64..8ea9fe04237 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection/JoinMinimalPosition.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection/JoinMinimalPosition.php @@ -70,7 +70,11 @@ public function execute(Collection $collection, array $categoryIds): void [] ); } - $positions[] = $connection->getIfNullSql($table . '.position', '~0'); + // PgCompat: '~0' (MySQL's max UNSIGNED BIGINT via bitwise-NOT-of-zero) means + // something entirely different on Postgres (signed integers - '~0' there is + // -1). Postgres' own bigint max serves the same "sort missing positions + // last" sentinel purpose. + $positions[] = $connection->getIfNullSql($table . '.position', '9223372036854775807'); } // Ensures that position attribute is registered in _joinFields diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php index 1e53e378869..44e02326cf2 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php @@ -224,9 +224,15 @@ public function createBatchBaseSelect($storeId, $attributeId) ), [] )->columns([ - 'label' => $this->getConnection()->getIfNullSql('`value`.`label`', '`default_value`.`label`'), - 'position' => $this->getConnection()->getIfNullSql('`value`.`position`', '`default_value`.`position`'), - 'disabled' => $this->getConnection()->getIfNullSql('`value`.`disabled`', '`default_value`.`disabled`'), + // PgCompat: was hardcoded MySQL backtick identifiers ('`value`.`label`' etc) + // passed as plain strings - getIfNullSql() just interpolates its arguments + // verbatim, so these reached Postgres as literal, invalid backtick syntax + // instead of going through quoteIdentifier(). Unquoted identifiers are valid + // input to getIfNullSql() on both adapters (they build "COALESCE(a, b)" + // directly from the given text), so plain dotted names are enough here. + 'label' => $this->getConnection()->getIfNullSql('value.label', 'default_value.label'), + 'position' => $this->getConnection()->getIfNullSql('value.position', 'default_value.position'), + 'disabled' => $this->getConnection()->getIfNullSql('value.disabled', 'default_value.disabled'), 'label_default' => 'default_value.label', 'position_default' => 'default_value.position', 'disabled_default' => 'default_value.disabled' @@ -236,7 +242,11 @@ public function createBatchBaseSelect($storeId, $attributeId) )->where( $mainTableAlias . '.disabled = 0' )->order( - $positionCheckSql . ' ' . \Magento\Framework\DB\Select::SQL_ASC + // PgCompat: was a bare string - Select::order() quotes plain-string + // arguments as if they were a single identifier, mangling this multi-word + // CASE expression into invalid, partially-quoted garbage. Zend_Db_Expr marks + // it as a raw SQL fragment instead, which order() passes through unquoted. + new \Zend_Db_Expr($positionCheckSql . ' ' . \Magento\Framework\DB\Select::SQL_ASC) ); return $select; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php index c15b366db2f..8b3031ef900 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php @@ -189,7 +189,11 @@ protected function _prepareRelationIndexSelect(?array $parentIds = null) 'i.entity_id = cpw.product_id AND sw.website_id = cpw.website_id', [] )->group( - ['parent_id', 'i.attribute_id', 'i.store_id', 'i.value', 'l.child_id'] + // PgCompat: MySQL resolves a GROUP BY item against the SELECT alias list, so + // 'parent_id' here refers to the 'parent_id' => 'e.entity_id' column below; + // Postgres doesn't do alias resolution in GROUP BY, so it has to be the + // underlying expression directly. + ['e.entity_id', 'i.attribute_id', 'i.store_id', 'i.value', 'l.child_id'] )->columns( [ 'parent_id' => 'e.entity_id', diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php index 46870ca3279..b5cc2914525 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php @@ -454,7 +454,12 @@ protected function getSelect($entityIds = null, $type = null) ); $currentDate = 'cwd.website_date'; - $maxUnsignedBigint = '~0'; + // PgCompat: '~0' (MySQL's bitwise-NOT-of-zero, the max UNSIGNED BIGINT value) + // means something entirely different on Postgres, whose integers are signed - + // '~0' there is bitwise NOT of a signed int, i.e. -1. Postgres' own bigint max + // is used as the "effectively unbounded" sentinel instead; it doesn't need to + // be the same numeric ceiling MySQL used, only larger than any real price. + $maxUnsignedBigint = '9223372036854775807'; $specialFromDate = $connection->getDatePartSql($specialFrom); $specialToDate = $connection->getDatePartSql($specialTo); $specialFromExpr = "{$specialFrom} IS NULL OR {$specialFromDate} <= {$currentDate}"; @@ -838,7 +843,12 @@ protected function hasEntity() */ private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression) { - $maxUnsignedBigint = '~0'; + // PgCompat: '~0' (MySQL's bitwise-NOT-of-zero, the max UNSIGNED BIGINT value) + // means something entirely different on Postgres, whose integers are signed - + // '~0' there is bitwise NOT of a signed int, i.e. -1. Postgres' own bigint max + // is used as the "effectively unbounded" sentinel instead; it doesn't need to + // be the same numeric ceiling MySQL used, only larger than any real price. + $maxUnsignedBigint = '9223372036854775807'; return $this->getConnection()->getCheckSql( implode( diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php index 7a21fb65e0e..fe3286d4bd1 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php @@ -144,7 +144,12 @@ public function getQuery(array $dimensions, string $productType, array $entityId $specialTo = $this->joinAttributeProcessor->process($select, 'special_to_date'); $currentDate = 'cwd.website_date'; - $maxUnsignedBigint = '~0'; + // PgCompat: '~0' (MySQL's bitwise-NOT-of-zero, the max UNSIGNED BIGINT value) + // means something entirely different on Postgres, whose integers are signed - + // '~0' there is bitwise NOT of a signed int, i.e. -1. Postgres' own bigint max + // is used as the "effectively unbounded" sentinel instead; it doesn't need to + // be the same numeric ceiling MySQL used, only larger than any real price. + $maxUnsignedBigint = '9223372036854775807'; $specialFromDate = $connection->getDatePartSql($specialFrom); $specialToDate = $connection->getDatePartSql($specialTo); $specialFromExpr = "{$specialFrom} IS NULL OR {$specialFromDate} <= {$currentDate}"; @@ -257,7 +262,12 @@ public function getQuery(array $dimensions, string $productType, array $entityId */ private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression) { - $maxUnsignedBigint = '~0'; + // PgCompat: '~0' (MySQL's bitwise-NOT-of-zero, the max UNSIGNED BIGINT value) + // means something entirely different on Postgres, whose integers are signed - + // '~0' there is bitwise NOT of a signed int, i.e. -1. Postgres' own bigint max + // is used as the "effectively unbounded" sentinel instead; it doesn't need to + // be the same numeric ceiling MySQL used, only larger than any real price. + $maxUnsignedBigint = '9223372036854775807'; return $this->getConnection()->getCheckSql( implode( diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php index d5a1676ac4c..ba926d7a3a4 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php @@ -76,7 +76,10 @@ public function process(Select $select) [] ); - $select->where('IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_ENABLED); + $select->where( + $select->getConnection()->getIfNullSql('status_attr.value', 'status_global_attr.value') . ' = ?', + Status::STATUS_ENABLED + ); return $select; } diff --git a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php index 09bfe656ae2..cd62cb5a3c7 100644 --- a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php +++ b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php @@ -73,10 +73,16 @@ public function afterExecute(ReadSnapshot $subject, array $entityData, $entityTy if ($globalAttributes) { $selects = []; foreach ($globalAttributes as $table => $attributeIds) { + // PgCompat: this select is UNION ALL'd (below) with one of these per EAV + // backend type table (varchar/int/decimal/text/datetime) - MySQL coerces + // the differently-typed "value" columns across branches implicitly, + // Postgres requires them to already share one type. Same fix as the + // other EAV union-cast sites (union-cast-eav-abstract-collection.patch, + // Eav\Model\ResourceModel\ReadHandler::execute()). $select = $connection->select() ->from( ['t' => $table], - ['value' => 't.value', 'attribute_id' => 't.attribute_id'] + ['value' => $connection->castToText('t.value'), 'attribute_id' => 't.attribute_id'] ) ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()]) ->where('attribute_id' . ' in (?)', $attributeIds) diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php index 54cbef64c4f..f662571fd68 100644 --- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php +++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php @@ -127,7 +127,12 @@ protected function applySorting() /** @var \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection $collection */ $collection = $this->getContext()->getDataProvider()->getCollection(); - $select = $collection->getConnection()->select(); + $connection = $collection->getConnection(); + $select = $connection->select(); + // PgCompat: GROUP_CONCAT() is MySQL-only - getGroupConcatSql() isn't part + // of AdapterInterface, but every connection in this deployment is one of + // the two PgCompat providers, both of which implement it as a + // string_agg() equivalent. $select->from( ['cpw' => $collection->getTable('catalog_product_website')], ['product_id'] @@ -135,9 +140,7 @@ protected function applySorting() ['sw' => $collection->getTable('store_website')], 'cpw.website_id = sw.website_id', [ - $this->websiteNames => new \Zend_Db_Expr( - 'GROUP_CONCAT(sw.name ORDER BY sw.website_id ASC SEPARATOR \',\')' - ) + $this->websiteNames => $connection->getGroupConcatSql('sw.name', ',', 'sw.website_id ASC') ] )->group( 'cpw.product_id' diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php index 79e72060032..74f9945a390 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php @@ -47,12 +47,13 @@ private function createTemporaryTable(string $originalTableName): string $originalTableName . '__temp' . $this->generateRandomSuffix() ); - $this->resourceConnection->getConnection()->query( - sprintf( - 'create table %s like %s', - $temporaryTableName, - $this->resourceConnection->getTableName($originalTableName) - ) + // PgCompat: `CREATE TABLE x LIKE y` is MySQL shorthand with no direct Postgres + // equivalent (`CREATE TABLE x (LIKE y INCLUDING ALL)` instead) - createTableLike() + // isn't part of AdapterInterface, but every connection in this deployment is one + // of the two PgCompat providers, both of which implement it. + $this->resourceConnection->getConnection()->createTableLike( + $temporaryTableName, + $this->resourceConnection->getTableName($originalTableName) ); return $temporaryTableName; diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php index bdeaba2bae0..a8baf31d3f9 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -336,7 +336,12 @@ private function unifyField($field, $backendType = 'varchar') if ($backendType == 'datetime') { $expr = $this->connection->getDateFormatSql($field, '%Y-%m-%d %H:%i:%s'); } else { - $expr = $field; + // PgCompat: the caller UNION ALL's one of these per EAV backend type + // (varchar/int/decimal/text) - MySQL coerces the differently-typed values + // implicitly, Postgres requires them to already share one type before the + // UNION. getDateFormatSql() above already returns a formatted string, so + // only this branch needs an explicit cast to match it. + $expr = $this->connection->castToText($field); } return $expr; diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php index febe0e1a5e3..364814aa4f4 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -54,10 +54,12 @@ public function apply() $ids[] = (int)$item->getId(); } - $orderList = implode(',', $ids); - $this->collection->getSelect() - ->where('e.entity_id IN (?)', $ids) + // PgCompat: FIELD() is MySQL-only - getFieldSql() isn't part of AdapterInterface, + // but every connection in this deployment is one of the two PgCompat providers, + // both of which implement it as a CASE-expression equivalent. + $select = $this->collection->getSelect(); + $select->where('e.entity_id IN (?)', $ids) ->reset(\Magento\Framework\DB\Select::ORDER) - ->order(new \Magento\Framework\DB\Sql\Expression("FIELD(e.entity_id, $orderList)")); + ->order($select->getAdapter()->getFieldSql('e.entity_id', $ids)); } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php index 7b36443f6cd..94547258bf2 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php @@ -160,8 +160,28 @@ private function fetch(ContextInterface $context, array $attributeCodes) : array $childCollection->addWebsiteFilter($context->getExtensionAttributes()->getStore()->getWebsiteId()); $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $childCollection->getSelect()->group('e.' . $linkField); - $childCollection->getSelect()->columns([ - 'parent_ids' => new \Zend_Db_Expr('GROUP_CONCAT(link_table.parent_id)') + // PgCompat: GROUP_CONCAT() is MySQL-only - getGroupConcatSql() isn't part of + // AdapterInterface, but this deployment's adapter implements it as a + // string_agg() equivalent. + // + // ChildCollection::_initSelect() unconditionally selects raw link_table.parent_id + // (needed by every OTHER caller of that base collection, which don't group at + // all); once this method's own group('e.' . $linkField) is added on top, that raw + // column is no longer functionally dependent on the GROUP BY key (a child can + // have more than one parent) and Postgres rejects it outright - the whole reason + // parent_ids exists is to carry that same information aggregated instead. Drop + // the raw column here, narrowly, via setPart() rather than in the base class, + // since only this grouped call site is affected. + $select = $childCollection->getSelect(); + $select->setPart( + \Magento\Framework\DB\Select::COLUMNS, + array_values(array_filter( + $select->getPart(\Magento\Framework\DB\Select::COLUMNS), + static fn(array $column) => !($column[0] === 'link_table' && $column[1] === 'parent_id') + )) + ); + $select->columns([ + 'parent_ids' => $select->getAdapter()->getGroupConcatSql('link_table.parent_id') ]); $attributeCodes = array_unique(array_merge($this->attributeCodes, $attributeCodes)); diff --git a/app/code/Magento/Customer/Setup/Patch/Data/AddSecurityTrackingAttributes.php b/app/code/Magento/Customer/Setup/Patch/Data/AddSecurityTrackingAttributes.php index 14f36423ed6..dc9c4e2d78c 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/AddSecurityTrackingAttributes.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/AddSecurityTrackingAttributes.php @@ -91,7 +91,7 @@ public function apply() $this->moduleDataSetup->getConnection()->update( $configTable, - ['value' => new \Zend_Db_Expr('value*24')], + ['value' => new \Zend_Db_Expr($this->moduleDataSetup->getConnection()->castToNumeric('value') . '*24')], ['path = ?' => Customer::XML_PATH_CUSTOMER_RESET_PASSWORD_LINK_EXPIRATION_PERIOD] ); diff --git a/app/code/Magento/Customer/Setup/Patch/Data/SessionIDColumnCleanUp.php b/app/code/Magento/Customer/Setup/Patch/Data/SessionIDColumnCleanUp.php index 4871c078637..bd3cbf1ad2e 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/SessionIDColumnCleanUp.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/SessionIDColumnCleanUp.php @@ -66,9 +66,13 @@ public function apply() private function cleanCustomerVisitorTable() { $tableName = $this->moduleDataSetup->getTable('customer_visitor'); + // PgCompat: was '... LIMIT 1000' - Postgres has no UPDATE...LIMIT. Safe to drop: + // the do-while loop below already terminates on rowCount() === 0, not on a + // fixed iteration count, so removing LIMIT just means one larger UPDATE instead + // of several 1000-row batches - same terminal state either way. // phpcs:ignore Magento2.SQL.RawQuery $rawQuery = sprintf( - 'UPDATE %s SET session_id = NULL WHERE session_id IS NOT NULL LIMIT 1000', + 'UPDATE %s SET session_id = NULL WHERE session_id IS NOT NULL', $tableName ); diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index f34f675831f..20f05d58d27 100644 --- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php +++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php @@ -1063,7 +1063,19 @@ protected function _loadModelAttributes($object) $attribute = current($this->_attributesByTable[$table]); $eavType = $attribute->getBackendType(); $select = $this->_getLoadAttributesSelect($object, $table); - $selects[$eavType][] = $select->columns('*'); + // PgCompat: was columns('*') - each per-type value table's "value" column is + // a different SQL type (varchar/int/decimal/text/datetime), so UNION ALL-ing + // them raw fails ("UNION types character varying and integer cannot be + // matched"). Only attribute_id/value_id/value are ever read back (see + // _setAttributeValue() below), so an explicit column list - casting just + // value to text - replaces the wildcard instead of trying to keep every + // table's full, non-identical column set (e.g. Catalog's *_int/*_varchar + // etc. have store_id, Customer's don't) unioned as-is. + $selects[$eavType][] = $select->columns([ + 'attribute_id' => 'attribute_id', + 'value_id' => 'value_id', + 'value' => $this->getConnection()->castToText($this->getConnection()->quoteIdentifier('value')), + ]); } $selectGroups = $this->_resourceHelper->getLoadAttributesSelectGroups($selects); foreach ($selectGroups as $selects) { diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php index e2f40374ac1..c8ed934df81 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php @@ -76,15 +76,25 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) } $attributeId = $this->getAttribute()->getId(); if (!isset($this->_options[$storeId][$attributeId])) { - $collection = $this->_attrOptionCollectionFactory->create()->setPositionOrder( - 'asc' - )->setAttributeFilter( - $attributeId - )->setStoreFilter( - $storeId - )->load(); - $this->_options[$storeId][$attributeId] = $collection->toOptionArray(); - $this->_optionsDefault[$storeId][$attributeId] = $collection->toOptionArray('default_value'); + // PgCompat: an attribute with no id yet (new/unsaved attribute model) can + // never have real eav_attribute_option rows - short-circuit instead of + // querying "attribute_id = ''" (MySQL tolerantly returns zero rows there via + // its '' -> 0 numeric coercion; Postgres rejects the comparison outright). + // Same empty-result outcome either way, without the pointless query. + if (!$attributeId) { + $this->_options[$storeId][$attributeId] = []; + $this->_optionsDefault[$storeId][$attributeId] = []; + } else { + $collection = $this->_attrOptionCollectionFactory->create()->setPositionOrder( + 'asc' + )->setAttributeFilter( + $attributeId + )->setStoreFilter( + $storeId + )->load(); + $this->_options[$storeId][$attributeId] = $collection->toOptionArray(); + $this->_optionsDefault[$storeId][$attributeId] = $collection->toOptionArray('default_value'); + } } $options = $defaultValues ? $this->_optionsDefault[$storeId][$attributeId] diff --git a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php index 737a9f07c61..73cde3cdc7d 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php @@ -1315,7 +1315,11 @@ protected function _getLoadAttributesSelect($table, $attributeIds = []) */ protected function _addLoadAttributesSelectValues($select, $table, $type) { - $select->columns(['value' => 't_d.value']); + // PgCompat: this select is UNION ALL'd with one of these per EAV backend type + // (varchar/int/decimal/text/datetime) in _loadAttributes() below - MySQL + // coerces the differently-typed "value" columns across branches implicitly, + // Postgres requires them to already share one type before the UNION. + $select->columns(['value' => $this->getConnection()->castToText('t_d.value')]); return $select; } diff --git a/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsSelectBuilder.php b/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsSelectBuilder.php index 44aad78042c..13504baaf2e 100644 --- a/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsSelectBuilder.php +++ b/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsSelectBuilder.php @@ -43,16 +43,19 @@ public function __construct( */ public function build(ChangelogInterface $changelog): Select { - $numberOfAttributes = $this->calculateEavAttributeSize($changelog); - $this->setGroupConcatMax($numberOfAttributes); - + // PgCompat: no setGroupConcatMax() call here - that tunes MySQL's + // group_concat_max_len session variable, a length cap string_agg() (used below + // via getGroupConcatSql()) has no equivalent of or need for. $changelogTableName = $this->resourceConnection->getTableName($changelog->getName()); $connection = $this->resourceConnection->getConnection(); + // PgCompat: GROUP_CONCAT() is MySQL-only - getGroupConcatSql() isn't part of + // AdapterInterface, but every connection in this deployment is one of the two + // PgCompat providers, both of which implement it as a string_agg() equivalent. $columns = [ $changelog->getColumnName(), - 'attribute_ids' => new Expression('GROUP_CONCAT(attribute_id)'), + 'attribute_ids' => $connection->getGroupConcatSql('attribute_id'), 'store_id' ]; diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php index 3f108ea2e96..1f47d2c89db 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php @@ -300,7 +300,20 @@ public function setAttributeGroupFilter($groupId) */ public function addAttributeGrouping() { - $this->getSelect()->group('main_table.attribute_id'); + $select = $this->getSelect(); + $select->group('main_table.attribute_id'); + // PgCompat: Catalog\Model\ResourceModel\Product\Attribute\Collection::_initSelect() + // unconditionally joins "additional_table" (catalog_eav_attribute) and selects + // additional_table.* - Postgres requires a table's OWN primary key in GROUP BY + // before it accepts that table's other columns un-aggregated (the join condition + // guaranteeing additional_table.attribute_id = main_table.attribute_id isn't + // enough; Postgres only applies the functional-dependency exception via a + // table's own PK). Guarded on the alias actually being joined - grouping by an + // alias that was never joined is a "missing FROM-clause entry" error, and not + // every Attribute\Collection subclass joins additional_table. + if (array_key_exists('additional_table', $select->getPart(Select::FROM))) { + $select->group('additional_table.attribute_id'); + } return $this; } @@ -343,15 +356,22 @@ public function addHasOptionsFilter() ] ); + // PgCompat: joinLeft()'s 3rd arg selects columns from "ao" into the result set; + // neither of this method's two real callers (CatalogGraphQl\Model\Config\ + // FilterAttributeReader, CatalogSearch\Model\Advanced) reads option_id off the + // loaded attributes - the join exists only for the ao.option_id > 0 filter below. + // Selecting it anyway (MySQL tolerates a non-aggregated, non-grouped column; not + // functionally dependent on the GROUP BY key since eav_attribute_option is a + // one-to-many join) makes every call to this method fail outright on Postgres. + // Selecting no columns from "ao" removes the violation with no behavior change. $this->getSelect()->joinLeft( ['ao' => $this->getTable('eav_attribute_option')], 'ao.attribute_id = main_table.attribute_id', - 'option_id' - )->group( - 'main_table.attribute_id' + [] )->where( $orWhere ); + $this->addAttributeGrouping(); return $this; } diff --git a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php index 898efad0e3e..058f9087001 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php +++ b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php @@ -153,10 +153,17 @@ public function execute($entityType, $entityData, $arguments = []) if (count($attributeTables)) { $identifiers = null; foreach ($attributeTables as $attributeTable => $attributeIds) { + // PgCompat: this select is UNION ALL'd (below, via UnionExpression) with + // one of these per EAV backend type table (varchar/int/decimal/text/ + // datetime) - MySQL coerces the differently-typed "value" columns across + // branches implicitly, Postgres requires them to already share one type. + // Same fix as the other two EAV union-cast sites (see + // union-cast-eav-abstract-collection.patch and + // Catalog\Model\ResourceModel\Collection\AbstractCollection's override). $select = $connection->select() ->from( ['t' => $attributeTable], - ['value' => 't.value', 'attribute_id' => 't.attribute_id'] + ['value' => $connection->castToText('t.value'), 'attribute_id' => 't.attribute_id'] ) ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()]) ->where('attribute_id IN (?)', $attributeIds, \Zend_Db::INT_TYPE); diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php index ba7342c554b..acd19d2aaa5 100644 --- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -70,11 +70,10 @@ public function apply() foreach ($items as $item) { $ids[] = (int)$item->getId(); } - $orderList = implode(',', $ids); $this->collection->getSelect() ->where('e.entity_id IN (?)', $ids) ->reset(\Magento\Framework\DB\Select::ORDER) - ->order(new \Zend_Db_Expr("FIELD(e.entity_id,$orderList)")); + ->order($this->collection->getSelect()->getAdapter()->getFieldSql('e.entity_id', $ids)); } /** diff --git a/app/code/Magento/ImportExport/Model/ResourceModel/Import/Data.php b/app/code/Magento/ImportExport/Model/ResourceModel/Import/Data.php index 75a708bd90c..a7ea22dd4eb 100644 --- a/app/code/Magento/ImportExport/Model/ResourceModel/Import/Data.php +++ b/app/code/Magento/ImportExport/Model/ResourceModel/Import/Data.php @@ -123,9 +123,15 @@ public function cleanBunches() */ public function cleanProcessedBunches() { - $this->getConnection()->delete( + $connection = $this->getConnection(); + // PgCompat: TIMESTAMPADD() is MySQL-only (getDateAddSql() renders per-dialect + // date arithmetic instead), and CURRENT_TIMESTAMP is a reserved keyword, not a + // function - MySQL tolerates the empty-parens call form, Postgres rejects it. + $connection->delete( $this->getMainTable(), - 'is_processed = 1 OR TIMESTAMPADD(DAY, 1, updated_at) < CURRENT_TIMESTAMP() ' + 'is_processed = 1 OR ' + . $connection->getDateAddSql('updated_at', 1, \Magento\Framework\DB\Adapter\AdapterInterface::INTERVAL_DAY) + . ' < CURRENT_TIMESTAMP' ); } diff --git a/app/code/Magento/Integration/Model/ResourceModel/Oauth/Consumer.php b/app/code/Magento/Integration/Model/ResourceModel/Oauth/Consumer.php index 3af513e40c9..2b720b9d1cd 100644 --- a/app/code/Magento/Integration/Model/ResourceModel/Oauth/Consumer.php +++ b/app/code/Magento/Integration/Model/ResourceModel/Oauth/Consumer.php @@ -68,7 +68,9 @@ public function getTimeInSecondsSinceCreation($consumerId) $select = $connection->select() ->from($this->getMainTable()) ->reset(\Magento\Framework\DB\Select::COLUMNS) - ->columns(new \Zend_Db_Expr('CURRENT_TIMESTAMP() - created_at')) + // PgCompat: CURRENT_TIMESTAMP is a reserved keyword, not a function - MySQL + // tolerates the empty-parens call form, Postgres rejects it. + ->columns(new \Zend_Db_Expr('CURRENT_TIMESTAMP - created_at')) ->where('entity_id = ?', $consumerId); return $connection->fetchOne($select); diff --git a/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php index 03619728a6a..2954e0d65f6 100644 --- a/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php +++ b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php @@ -45,7 +45,10 @@ public function apply(): void $select = $connection->select() ->from($tableName, ['role_id']) - ->where('resource_id = "Magento_Cms::media_gallery"'); + // PgCompat: a double-quoted string is a valid MySQL string literal but a + // Postgres identifier - single quotes are required for it to mean the same + // thing on both databases. + ->where("resource_id = 'Magento_Cms::media_gallery'"); $insertData = $this->getInsertData($connection->fetchCol($select)); diff --git a/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php b/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php index 497f359fafa..a5bbcb79cd6 100644 --- a/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php +++ b/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php @@ -79,17 +79,22 @@ public function afterCreateBatchBaseSelect(Gallery $originalResourceModel, Selec ] ), [] + // PgCompat: was hardcoded MySQL backtick identifiers ('`value_video`.`provider`' + // etc) passed as plain strings - getIfNullSql() just interpolates its arguments + // verbatim, so these reached Postgres as literal, invalid backtick syntax + // instead of going through quoteIdentifier(). Same bug, same fix, as the + // core Gallery.php site this plugin's select is built from. )->columns([ 'video_provider' => $originalResourceModel->getConnection() - ->getIfNullSql('`value_video`.`provider`', '`default_value_video`.`provider`'), + ->getIfNullSql('value_video.provider', 'default_value_video.provider'), 'video_url' => $originalResourceModel->getConnection() - ->getIfNullSql('`value_video`.`url`', '`default_value_video`.`url`'), + ->getIfNullSql('value_video.url', 'default_value_video.url'), 'video_title' => $originalResourceModel->getConnection() - ->getIfNullSql('`value_video`.`title`', '`default_value_video`.`title`'), + ->getIfNullSql('value_video.title', 'default_value_video.title'), 'video_description' => $originalResourceModel->getConnection() - ->getIfNullSql('`value_video`.`description`', '`default_value_video`.`description`'), + ->getIfNullSql('value_video.description', 'default_value_video.description'), 'video_metadata' => $originalResourceModel->getConnection() - ->getIfNullSql('`value_video`.`metadata`', '`default_value_video`.`metadata`'), + ->getIfNullSql('value_video.metadata', 'default_value_video.metadata'), 'video_provider_default' => 'default_value_video.provider', 'video_url_default' => 'default_value_video.url', 'video_title_default' => 'default_value_video.title', diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php b/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php index 6390398dd0e..4304b339b77 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php @@ -101,13 +101,15 @@ public function appendSummaryFieldsToCollection( "e.entity_id = review_summary.entity_pk_value AND review_summary.store_id = {$storeId}" . " AND review_summary.entity_type = ({$summaryEntitySubSelect})" ); + // PgCompat: getIfNullSql() renders per-dialect (IFNULL on MySQL, COALESCE on + // Postgres) instead of hardcoding MySQL's IFNULL() text. $productCollection->getSelect() ->joinLeft( ['review_summary' => $this->getMainTable()], $joinCond, [ - 'reviews_count' => new \Zend_Db_Expr("IFNULL(review_summary.reviews_count, 0)"), - 'rating_summary' => new \Zend_Db_Expr("IFNULL(review_summary.rating_summary, 0)") + 'reviews_count' => $this->getConnection()->getIfNullSql('review_summary.reviews_count', 0), + 'rating_summary' => $this->getConnection()->getIfNullSql('review_summary.rating_summary', 0) ] ); } diff --git a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php index a45d955dae9..0fadc12b67f 100644 --- a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php +++ b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php @@ -282,14 +282,18 @@ private function buildConditions(AbstractCollection $collection, Combine $combin } if (!empty($conditions) && !empty($attributeField)) { - $conditions = $this->_connection->quote( - array_map('trim', explode(',', $conditions)) + // PgCompat: FIELD() is MySQL-only - getFieldSql() isn't part of + // AdapterInterface, but every connection in this deployment is one of + // the two PgCompat providers, both of which implement it as a + // CASE-expression equivalent. quote() on an array returns one + // comma-joined, already-quoted string - split back into a value list. + $conditionValues = explode( + ', ', + $this->_connection->quote(array_map('trim', explode(',', $conditions))) ); $collection->getSelect()->reset(Select::ORDER); $collection->getSelect()->order( - $this->_expressionFactory->create( - ['expression' => "FIELD($attributeField, $conditions)"] - ) + $this->_connection->getFieldSql($attributeField, $conditionValues) ); } } diff --git a/app/code/Magento/Security/Model/ResourceModel/UserExpiration/Collection.php b/app/code/Magento/Security/Model/ResourceModel/UserExpiration/Collection.php index e23e6fb6904..db01238c4bb 100644 --- a/app/code/Magento/Security/Model/ResourceModel/UserExpiration/Collection.php +++ b/app/code/Magento/Security/Model/ResourceModel/UserExpiration/Collection.php @@ -40,12 +40,12 @@ public function addActiveExpiredUsersFilter(): Collection $currentTime = new \DateTime(); $currentTime->format('Y-m-d H:i:s'); $this->getSelect()->joinLeft( - ['user' => $this->getTable('admin_user')], - 'main_table.user_id = user.user_id', + ['admin_user' => $this->getTable('admin_user')], + 'main_table.user_id = admin_user.user_id', ['is_active'] ); $this->addFieldToFilter('expires_at', ['lt' => $currentTime]) - ->addFieldToFilter('user.is_active', 1); + ->addFieldToFilter('admin_user.is_active', 1); return $this; } diff --git a/app/code/Magento/Security/Setup/Patch/Data/SessionIDColumnCleanUp.php b/app/code/Magento/Security/Setup/Patch/Data/SessionIDColumnCleanUp.php index 8a3124404c9..126fa88672d 100644 --- a/app/code/Magento/Security/Setup/Patch/Data/SessionIDColumnCleanUp.php +++ b/app/code/Magento/Security/Setup/Patch/Data/SessionIDColumnCleanUp.php @@ -66,9 +66,13 @@ public function apply() private function cleanAdminUserSessionTable() { $tableName = $this->moduleDataSetup->getTable('admin_user_session'); + // PgCompat: was '... LIMIT 1000' - Postgres has no UPDATE...LIMIT. Safe to drop: + // the do-while loop below already terminates on rowCount() === 0, not on a + // fixed iteration count, so removing LIMIT just means one larger UPDATE instead + // of several 1000-row batches - same terminal state either way. // phpcs:ignore Magento2.SQL.RawQuery $rawQuery = sprintf( - 'UPDATE %s SET session_id = NULL WHERE session_id IS NOT NULL LIMIT 1000', + 'UPDATE %s SET session_id = NULL WHERE session_id IS NOT NULL', $tableName ); diff --git a/app/code/Magento/Vault/Setup/Patch/Data/SetCreditCardAsDefaultTokenType.php b/app/code/Magento/Vault/Setup/Patch/Data/SetCreditCardAsDefaultTokenType.php index 36282d1abe9..d32207c6b91 100644 --- a/app/code/Magento/Vault/Setup/Patch/Data/SetCreditCardAsDefaultTokenType.php +++ b/app/code/Magento/Vault/Setup/Patch/Data/SetCreditCardAsDefaultTokenType.php @@ -42,12 +42,16 @@ public function apply() // data update for Vault module < 2.0.1 // update sets credit card as default token type + // PgCompat: was ' = ""' - a double-quoted empty-string literal, MySQL-only + // (ANSI_QUOTES-off syntax); Postgres double quotes always mean an identifier, so + // this rendered as a zero-length identifier reference instead of comparing + // against an empty string. Single-quoted is valid on both. $this->moduleDataSetup->getConnection()->update( $this->moduleDataSetup->getTable('vault_payment_token'), [ PaymentTokenInterface::TYPE => CreditCardTokenFactory::TOKEN_TYPE_CREDIT_CARD ], - PaymentTokenInterface::TYPE . ' = ""' + PaymentTokenInterface::TYPE . " = ''" ); $this->moduleDataSetup->getConnection()->endSetup(); diff --git a/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Product/WeeeAttributeProductSort.php b/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Product/WeeeAttributeProductSort.php index e51f21881a4..a1adf0887a1 100644 --- a/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Product/WeeeAttributeProductSort.php +++ b/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Product/WeeeAttributeProductSort.php @@ -44,14 +44,19 @@ public function afterBuild( int $productId, int $storeId ):array { - $select = $this->resourceConnection->getConnection()->select(); + $connection = $this->resourceConnection->getConnection(); + $select = $connection->select(); + // PgCompat: getIfNullSql() renders per-dialect (IFNULL on MySQL, COALESCE on + // Postgres) instead of hardcoding MySQL's IFNULL() text. + $weeeValue = $connection->getIfNullSql( + 'weee_child.value', + (string) $connection->getIfNullSql('weee_parent.value', 0) + ); foreach ($result as $select) { $select->columns( [ - 'weee_min_price' => new \Zend_Db_Expr( - '(t.min_price + IFNULL(weee_child.value, IFNULL(weee_parent.value, 0)))' - ) + 'weee_min_price' => new \Zend_Db_Expr('(t.min_price + ' . $weeeValue . ')') ] )->joinLeft( ['weee_child' => $this->resourceConnection->getTableName('weee_tax')], diff --git a/lib/internal/Magento/Framework/Cache/Backend/Database.php b/lib/internal/Magento/Framework/Cache/Backend/Database.php index 053ef66a6e2..f9582f71bc6 100644 --- a/lib/internal/Magento/Framework/Cache/Backend/Database.php +++ b/lib/internal/Magento/Framework/Cache/Backend/Database.php @@ -230,17 +230,18 @@ public function save($data, $id, $tags = [], $specificLifetime = null) $time = time(); $expire = $lifetime === 0 || $lifetime === null ? 0 : $time + $lifetime; - $idCol = $connection->quoteIdentifier('id'); - $dataCol = $connection->quoteIdentifier('data'); - $createCol = $connection->quoteIdentifier('create_time'); - $updateCol = $connection->quoteIdentifier('update_time'); - $expireCol = $connection->quoteIdentifier('expire_time'); - - $query = "INSERT INTO {$dataTable} ({$idCol}, {$dataCol}, {$createCol}, {$updateCol}, {$expireCol}) " . - "VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE {$dataCol}=VALUES({$dataCol}), " . - "{$updateCol}=VALUES({$updateCol}), {$expireCol}=VALUES({$expireCol})"; - - $result = $connection->query($query, [$id, $data, $time, $time, $expire])->rowCount(); + // PgCompat: insertOnDuplicate() emits ON CONFLICT on Postgres. + $result = $connection->insertOnDuplicate( + $dataTable, + [ + 'id' => $id, + 'data' => $data, + 'create_time' => $time, + 'update_time' => $time, + 'expire_time' => $expire, + ], + ['data', 'update_time', 'expire_time'] + ); } if ($result) { $result = $this->_saveTags($id, $tags); diff --git a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php index e6f60fb70b4..c4cad8b8c71 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php +++ b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php @@ -819,6 +819,61 @@ public function getCheckSql($condition, $true, $false); */ public function getIfNullSql($expression, $value = 0); + /** + * GROUP_CONCAT / string_agg equivalent + * + * @param string|\Zend_Db_Expr $expression + * @param string $separator + * @param string|\Zend_Db_Expr|null $orderBy + * @param bool $distinct + * @return \Zend_Db_Expr + */ + public function getGroupConcatSql($expression, $separator = ',', $orderBy = null, $distinct = false); + + /** + * FIELD() / CASE equivalent for ORDER BY a fixed list + * + * @param string|\Zend_Db_Expr $expression + * @param array $values + * @return \Zend_Db_Expr + */ + public function getFieldSql($expression, array $values); + + /** + * Cast an expression to text for UNION type alignment + * + * @param string|\Zend_Db_Expr $expression + * @return \Zend_Db_Expr + */ + public function castToText($expression); + + /** + * Cast an expression to a numeric type for arithmetic + * + * @param string|\Zend_Db_Expr $expression + * @return \Zend_Db_Expr + */ + public function castToNumeric($expression); + + /** + * CREATE TABLE new LIKE origin + * + * @param string $newTableName + * @param string $originTableName + * @return \Zend_Db_Statement_Interface + */ + public function createTableLike($newTableName, $originTableName); + + /** + * CREATE TEMPORARY TABLE from a SELECT (and optional index definitions) + * + * @param string $name + * @param string[] $indexStatements + * @param \Magento\Framework\DB\Select $select + * @return \Zend_Db_Statement_Interface + */ + public function createTemporaryTableFromSelect($name, array $indexStatements, \Magento\Framework\DB\Select $select); + /** * Generate fragment of SQL, that combine together (concatenate) the results from data array * diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index f41d0ed17f7..050decc7d63 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -3442,6 +3442,79 @@ public function getIfNullSql($expression, $value = 0) return new \Zend_Db_Expr($expression); } + /** + * @inheritdoc + */ + public function getGroupConcatSql($expression, $separator = ',', $orderBy = null, $distinct = false) + { + $sql = 'GROUP_CONCAT(' . ($distinct ? 'DISTINCT ' : '') . $expression; + if ($orderBy !== null) { + $sql .= ' ORDER BY ' . $orderBy; + } + return new \Zend_Db_Expr($sql . ' SEPARATOR ' . $this->quote($separator) . ')'); + } + + /** + * @inheritdoc + */ + public function getFieldSql($expression, array $values) + { + $parts = []; + foreach ($values as $value) { + if ($value === '') { + continue; + } + $parts[] = $value; + } + if (!$parts) { + return new \Zend_Db_Expr('0'); + } + return new \Zend_Db_Expr('FIELD(' . $expression . ', ' . implode(', ', $parts) . ')'); + } + + /** + * @inheritdoc + */ + public function createTableLike($newTableName, $originTableName) + { + return $this->query(sprintf( + 'CREATE TABLE %s LIKE %s', + $this->quoteIdentifier($newTableName), + $this->quoteIdentifier($originTableName) + )); + } + + /** + * @inheritdoc + */ + public function createTemporaryTableFromSelect($name, array $indexStatements, Select $select) + { + $sql = sprintf( + 'CREATE TEMPORARY TABLE %s %s ENGINE=%s IGNORE (%s)', + $this->quoteIdentifier($name), + $indexStatements ? '(' . implode(',', $indexStatements) . ')' : '', + $this->quoteIdentifier('innodb'), + $select + ); + return $this->query($sql, $select->getBind()); + } + + /** + * @inheritdoc + */ + public function castToText($expression) + { + return new \Zend_Db_Expr((string) $expression); + } + + /** + * @inheritdoc + */ + public function castToNumeric($expression) + { + return new \Zend_Db_Expr('CAST(' . $expression . ' AS DECIMAL(20,6))'); + } + /** * Generates case SQL fragment * diff --git a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php index b1eacc98bdd..8e57af33394 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php @@ -174,7 +174,17 @@ private function calculateBatchSize(Select $select) ] ); $row = $this->connection->fetchRow($wrapperSelect); - $this->minValue = $row['max']; + // PgCompat: MAX() over zero matching rows is SQL NULL - when this batch is + // empty (cnt below ends up 0 and the iterator stops right after this call + // anyway), don't overwrite minValue with that null. A leftover null minValue + // fed into initSelectObject()'s "> ?" bind is otherwise silently coerced to an + // empty-string parameter by PDO, which MySQL's loose bigint/string comparison + // tolerates (implicitly treating '' as 0) but Postgres rejects outright + // ("invalid input syntax for type bigint"). Real-world trigger: any + // FieldDataConverter::convert() call (e.g. Theme's ConvertSerializedData data + // patch) whose target rows are exhausted after fewer than batchSize rows, or - + // as first hit here - never existed at all. + $this->minValue = $row['max'] ?? $this->minValue; return (int)$row['cnt']; } diff --git a/lib/internal/Magento/Framework/DB/Select.php b/lib/internal/Magento/Framework/DB/Select.php index 1ef13dd7424..c9552c1c1dc 100644 --- a/lib/internal/Magento/Framework/DB/Select.php +++ b/lib/internal/Magento/Framework/DB/Select.php @@ -57,12 +57,26 @@ class Select extends \Zend_Db_Select * Class constructor * Add straight join support * - * @param Adapter\Pdo\Mysql $adapter + * PgCompat: type-hint relaxed from the concrete Adapter\Pdo\Mysql to + * Zend_Db_Adapter_Abstract - this class only calls forUpdate()/ + * getQuoteIdentifierSymbol()/orderRand()/supportStraightJoin() on $adapter, all + * part of AdapterInterface's own contract (not Zend_Db_Adapter_Abstract's), so the + * narrower Mysql-only type-hint was never load-bearing for anything this class + * actually does - PHP doesn't statically check that a dynamic method call exists + * against the parameter's declared type, only that the object passed at runtime + * has it. Zend_Db_Adapter_Abstract (not the AdapterInterface interface) specifically + * because parent::__construct() below requires it, and PHP's intersection types + * can't combine an interface with a concrete class. SelectFactory::create() already + * type-hints AdapterInterface (not Mysql) when building this object, so without + * this change ANY AdapterInterface implementation that doesn't extend Magento's own + * Mysql adapter fails here with a TypeError the moment it calls select(). + * + * @param \Zend_Db_Adapter_Abstract $adapter * @param Select\SelectRenderer $selectRenderer * @param array $parts */ public function __construct( - \Magento\Framework\DB\Adapter\Pdo\Mysql $adapter, + \Zend_Db_Adapter_Abstract $adapter, \Magento\Framework\DB\Select\SelectRenderer $selectRenderer, $parts = [] ) { diff --git a/lib/internal/Magento/Framework/DB/TemporaryTableService.php b/lib/internal/Magento/Framework/DB/TemporaryTableService.php index 6ee15d4013f..b7139dfad8d 100644 --- a/lib/internal/Magento/Framework/DB/TemporaryTableService.php +++ b/lib/internal/Magento/Framework/DB/TemporaryTableService.php @@ -120,18 +120,12 @@ public function createFromSelect( $indexStatements[] = sprintf('%s(%s)', $indexType, $renderedColumns); } - $statement = sprintf( - 'CREATE TEMPORARY TABLE %s %s ENGINE=%s IGNORE (%s)', - $adapter->quoteIdentifier($name), - $indexStatements ? '(' . implode(',', $indexStatements) . ')' : '', - $adapter->quoteIdentifier($dbEngine), - "{$select}" - ); - - $adapter->query( - $statement, - $select->getBind() - ); + // PgCompat: `CREATE TEMPORARY TABLE x (...) ENGINE=y IGNORE (SELECT ...)` is a + // MySQL-only "materialize a dedup'd select into a temp table" idiom - + // createTemporaryTableFromSelect() isn't part of AdapterInterface, but every + // connection in this deployment is one of the two PgCompat providers, both of + // which implement it as a native `CREATE TEMPORARY TABLE x AS SELECT ...`. + $adapter->createTemporaryTableFromSelect($name, $indexStatements, $select); $this->createdTableAdapters[$name] = $adapter; diff --git a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php index c792c83d6b8..97744b323f0 100644 --- a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php +++ b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php @@ -736,7 +736,16 @@ protected function _renderOrders() if (!$this->_isOrdersRendered) { foreach ($this->_orders as $field => $direction) { if (isset($this->sqlReservedWords[strtoupper($field)])) { - $field = "`$field`"; + // PgCompat: was a hardcoded "`$field`" backtick literal - correct by + // coincidence only on MySQL (whose quote character happens to be a + // backtick), and it bypasses the adapter's own quoteIdentifier() + // entirely, so Postgres (whose reserved-word set differs anyway - + // e.g. "position" isn't reserved there, but this same $field is + // wrapped unconditionally once it matches MySQL's list) received a + // literal, invalid backtick instead of a real identifier quote. + // quoteIdentifier() is exactly what every other quoting path in this + // class already goes through. + $field = $this->getConnection()->quoteIdentifier($field); } $this->_select->order(new \Zend_Db_Expr($field . ' ' . $direction)); diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList.php b/setup/src/Magento/Setup/Model/ConfigOptionsList.php index 17720e6d14e..5942629ae4b 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList.php @@ -388,7 +388,8 @@ private function validateDbSettings(array $options, DeploymentConfig $deployment $options[ConfigOptionsListConstants::INPUT_KEY_DB_HOST], $options[ConfigOptionsListConstants::INPUT_KEY_DB_USER], $options[ConfigOptionsListConstants::INPUT_KEY_DB_PASSWORD], - $driverOptions + $driverOptions, + $options[ConfigOptionsListConstants::INPUT_KEY_DB_ENGINE] ?? null ); } catch (\Exception $exception) { $errors[] = $exception->getMessage(); diff --git a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php index cc3865d6e50..26e1d3f2160 100644 --- a/setup/src/Magento/Setup/Model/Installer.php +++ b/setup/src/Magento/Setup/Model/Installer.php @@ -1667,7 +1667,11 @@ private function assertDbAccessible() ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTION_DEFAULT . '/' . ConfigOptionsListConstants::KEY_PASSWORD ), - $driverOptions + $driverOptions, + $this->deploymentConfig->get( + ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTION_DEFAULT . + '/' . ConfigOptionsListConstants::KEY_ENGINE + ) ); $prefix = $this->deploymentConfig->get( ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTION_DEFAULT . diff --git a/setup/src/Magento/Setup/Validator/DbValidator.php b/setup/src/Magento/Setup/Validator/DbValidator.php index 28edec3b2c9..c6a371a47e8 100644 --- a/setup/src/Magento/Setup/Validator/DbValidator.php +++ b/setup/src/Magento/Setup/Validator/DbValidator.php @@ -97,19 +97,22 @@ public function checkDatabaseConnectionWithDriverOptions( $dbHost, $dbUser, $dbPass = '', - $driverOptions = [] + $driverOptions = [], + $engine = null ) { // establish connection to information_schema view to retrieve information about user and table privileges - $connection = $this->connectionFactory->create( - [ - ConfigOptionsListConstants::KEY_NAME => 'information_schema', - ConfigOptionsListConstants::KEY_HOST => $dbHost, - ConfigOptionsListConstants::KEY_USER => $dbUser, - ConfigOptionsListConstants::KEY_PASSWORD => $dbPass, - ConfigOptionsListConstants::KEY_ACTIVE => true, - ConfigOptionsListConstants::KEY_DRIVER_OPTIONS => $driverOptions, - ] - ); + $connectionConfig = [ + ConfigOptionsListConstants::KEY_NAME => 'information_schema', + ConfigOptionsListConstants::KEY_HOST => $dbHost, + ConfigOptionsListConstants::KEY_USER => $dbUser, + ConfigOptionsListConstants::KEY_PASSWORD => $dbPass, + ConfigOptionsListConstants::KEY_ACTIVE => true, + ConfigOptionsListConstants::KEY_DRIVER_OPTIONS => $driverOptions, + ]; + if ($engine !== null && $engine !== '') { + $connectionConfig[ConfigOptionsListConstants::KEY_ENGINE] = $engine; + } + $connection = $this->connectionFactory->create($connectionConfig); if (!$connection) { throw new \Magento\Setup\Exception('Database connection failure.'); From fb59765812c49d0ba2baca2ddbe82de74a43f41f Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 17 Aug 2026 22:54:58 -0700 Subject: [PATCH 2/5] Add unit tests for AdapterInterface dialect helpers. Cover MySQL GROUP_CONCAT/FIELD/cast/CREATE TABLE LIKE helpers, TemporaryTableService delegating to createTemporaryTableFromSelect, and BatchIterator keeping minValue when MAX() is NULL. Co-authored-by: Cursor --- .../Framework/DB/TemporaryTableService.php | 5 -- .../DB/Test/Unit/Adapter/Pdo/MysqlTest.php | 74 +++++++++++++++++++ .../Test/Unit/TemporaryTableServiceTest.php | 26 +++---- .../Test/Unit/DB/Query/BatchIteratorTest.php | 15 ++++ 4 files changed, 98 insertions(+), 22 deletions(-) diff --git a/lib/internal/Magento/Framework/DB/TemporaryTableService.php b/lib/internal/Magento/Framework/DB/TemporaryTableService.php index b7139dfad8d..25026dc2781 100644 --- a/lib/internal/Magento/Framework/DB/TemporaryTableService.php +++ b/lib/internal/Magento/Framework/DB/TemporaryTableService.php @@ -120,11 +120,6 @@ public function createFromSelect( $indexStatements[] = sprintf('%s(%s)', $indexType, $renderedColumns); } - // PgCompat: `CREATE TEMPORARY TABLE x (...) ENGINE=y IGNORE (SELECT ...)` is a - // MySQL-only "materialize a dedup'd select into a temp table" idiom - - // createTemporaryTableFromSelect() isn't part of AdapterInterface, but every - // connection in this deployment is one of the two PgCompat providers, both of - // which implement it as a native `CREATE TEMPORARY TABLE x AS SELECT ...`. $adapter->createTemporaryTableFromSelect($name, $indexStatements, $select); $this->createdTableAdapters[$name] = $adapter; diff --git a/lib/internal/Magento/Framework/DB/Test/Unit/Adapter/Pdo/MysqlTest.php b/lib/internal/Magento/Framework/DB/Test/Unit/Adapter/Pdo/MysqlTest.php index a5f89a5256c..cdc70e9438a 100644 --- a/lib/internal/Magento/Framework/DB/Test/Unit/Adapter/Pdo/MysqlTest.php +++ b/lib/internal/Magento/Framework/DB/Test/Unit/Adapter/Pdo/MysqlTest.php @@ -1071,4 +1071,78 @@ public function testDestruct(): void $adapter->__destruct(); $this->assertEquals(0, $adapter->getTransactionLevel()); } + + public function testGetGroupConcatSql(): void + { + $adapter = $this->getMysqlPdoAdapterMock(['query', 'quote']); + $adapter->method('quote')->willReturnCallback(static function ($value) { + return "'" . $value . "'"; + }); + + $this->assertSame( + "GROUP_CONCAT(sku SEPARATOR ',')", + (string) $adapter->getGroupConcatSql('sku') + ); + $this->assertSame( + "GROUP_CONCAT(DISTINCT sku ORDER BY sku SEPARATOR '|')", + (string) $adapter->getGroupConcatSql('sku', '|', 'sku', true) + ); + } + + public function testGetFieldSql(): void + { + $adapter = $this->getMysqlPdoAdapterMock([]); + $this->assertSame('0', (string) $adapter->getFieldSql('status', [])); + $this->assertSame('0', (string) $adapter->getFieldSql('status', [''])); + $this->assertSame( + 'FIELD(status, 1, 2, 3)', + (string) $adapter->getFieldSql('status', [1, '', 2, 3]) + ); + } + + public function testCastHelpers(): void + { + $adapter = $this->getMysqlPdoAdapterMock([]); + $this->assertSame('value', (string) $adapter->castToText('value')); + $this->assertSame('CAST(value AS DECIMAL(20,6))', (string) $adapter->castToNumeric('value')); + } + + public function testCreateTableLike(): void + { + $adapter = $this->getMysqlPdoAdapterMock(['query', 'quoteIdentifier']); + $adapter->method('quoteIdentifier')->willReturnCallback(static function ($value) { + return '`' . $value . '`'; + }); + $stmt = $this->createMock(\Zend_Db_Statement_Pdo::class); + $adapter->expects($this->once()) + ->method('query') + ->with('CREATE TABLE `new_table` LIKE `origin_table`') + ->willReturn($stmt); + + $this->assertSame($stmt, $adapter->createTableLike('new_table', 'origin_table')); + } + + public function testCreateTemporaryTableFromSelect(): void + { + $adapter = $this->getMysqlPdoAdapterMock(['query', 'quoteIdentifier']); + $adapter->method('quoteIdentifier')->willReturnCallback(static function ($value) { + return '`' . $value . '`'; + }); + $select = $this->createMock(Select::class); + $select->method('__toString')->willReturn('SELECT 1'); + $select->method('getBind')->willReturn(['foo' => 'bar']); + $stmt = $this->createMock(\Zend_Db_Statement_Pdo::class); + $adapter->expects($this->once()) + ->method('query') + ->with( + 'CREATE TEMPORARY TABLE `tmp` (PRIMARY KEY(id)) ENGINE=`innodb` IGNORE (SELECT 1)', + ['foo' => 'bar'] + ) + ->willReturn($stmt); + + $this->assertSame( + $stmt, + $adapter->createTemporaryTableFromSelect('tmp', ['PRIMARY KEY(id)'], $select) + ); + } } diff --git a/lib/internal/Magento/Framework/DB/Test/Unit/TemporaryTableServiceTest.php b/lib/internal/Magento/Framework/DB/Test/Unit/TemporaryTableServiceTest.php index 15725921c00..daa41e2ecf0 100644 --- a/lib/internal/Magento/Framework/DB/Test/Unit/TemporaryTableServiceTest.php +++ b/lib/internal/Magento/Framework/DB/Test/Unit/TemporaryTableServiceTest.php @@ -94,33 +94,25 @@ public function testCreateFromSelectWithException() #[DataProvider('createFromSelectDataProvider')] public function testCreateFromSelect($indexes, $expectedSelect) { - $selectString = 'select * from sometable'; $random = 'random_table'; $this->randomMock->expects($this->once()) ->method('getUniqueHash') ->willReturn($random); - $this->adapterMock->expects($this->once()) - ->method('query') - ->with($expectedSelect) - ->willReturnSelf(); - - $this->adapterMock->expects($this->once()) - ->method('query') - ->willReturnSelf(); - $this->adapterMock->expects($this->any()) ->method('quoteIdentifier') ->willReturnArgument(0); - $this->selectMock->expects($this->once()) - ->method('getBind') - ->willReturn(['bind']); - - $this->selectMock->expects($this->any()) - ->method('__toString') - ->willReturn($selectString); + $this->adapterMock->expects($this->once()) + ->method('createTemporaryTableFromSelect') + ->with( + $random, + $this->callback(static function (array $indexStatements) use ($expectedSelect) { + return str_contains($expectedSelect, implode(',', $indexStatements)); + }), + $this->selectMock + ); $this->assertEquals( $random, diff --git a/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchIteratorTest.php b/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchIteratorTest.php index b7d225f3919..414d3813298 100644 --- a/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchIteratorTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/DB/Query/BatchIteratorTest.php @@ -172,6 +172,21 @@ public function testIterations(): void $this->assertCount(3, $result); } + /** + * MAX() over zero rows is SQL NULL. Keep the previous minValue so the next + * WHERE rangeField > ? bind stays an integer (0), not null/''. + */ + public function testEmptyBatchDoesNotBindNullMinValue(): void + { + $filed = $this->correlationName . '.' . $this->rangeField; + $this->connectionMock->method('fetchRow')->willReturn(['max' => null, 'cnt' => 0]); + $this->selectMock->expects($this->exactly(2))->method('where')->with($filed . ' > ?', 0); + + $this->model->current(); + $this->assertFalse($this->model->valid()); + $this->model->next(); + } + /** * Test steps: * 1. $iterator->next(); From 6d9bc90561a3d51798acd993af68d4f8d5d79f42 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 18 Aug 2026 12:26:23 -0700 Subject: [PATCH 3/5] Fix unit tests and coding standard for dialect helper call sites. BulkStatus uses ResourceConnection for getFieldSql. Bundle price, gallery, and status processor tests mock the new adapter methods. PHPCS for ExternalVideo chaining and DbValidator @param. --- .../Model/BulkStatus.php | 4 +- .../Test/Unit/Model/BulkStatusTest.php | 2 + .../Model/ResourceModel/Indexer/PriceTest.php | 47 +++++-------------- .../ResourceModel/Product/GalleryTest.php | 26 +++++----- .../Product/StatusBaseSelectProcessorTest.php | 6 +++ .../Plugin/ExternalVideoResourceBackend.php | 5 -- .../Magento/Setup/Validator/DbValidator.php | 1 + 7 files changed, 36 insertions(+), 55 deletions(-) diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php b/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php index 3f835f36aea..8553a65ebfb 100644 --- a/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php +++ b/app/code/Magento/AsynchronousOperations/Model/BulkStatus.php @@ -128,7 +128,9 @@ public function getBulksByUser($userId) $select = $collection->getSelect(); $statusExpr = $this->calculatedStatusSql->get($operationTableName); $select->columns(['status' => $statusExpr]) - ->order($select->getAdapter()->getFieldSql((string) $statusExpr, $statusesArray)); + ->order( + $this->resourceConnection->getConnection()->getFieldSql((string) $statusExpr, $statusesArray) + ); $collection->addFieldToFilter('user_id', $userId) ->addOrder('start_time'); diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkStatusTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkStatusTest.php index 1b011758434..155a73fcd97 100644 --- a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkStatusTest.php +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkStatusTest.php @@ -284,6 +284,8 @@ public function testGetBulksByUser(): void $bulkCollection->expects($this->once())->method('getSelect')->willReturn($selectMock); $selectMock->expects($this->once())->method('columns')->willReturnSelf(); $selectMock->expects($this->once())->method('order')->willReturnSelf(); + $this->resourceConnectionMock->expects($this->once())->method('getConnection')->willReturn($this->connectionMock); + $this->connectionMock->expects($this->once())->method('getFieldSql')->willReturn('FIELD(status, 1, 2)'); $this->bulkCollectionFactory->expects($this->once())->method('create')->willReturn($bulkCollection); $bulkCollection->expects($this->once())->method('addFieldToFilter')->with('user_id', $userId)->willReturnSelf(); $bulkCollection->expects($this->once())->method('getItems')->willReturn([$this->bulkMock]); diff --git a/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Indexer/PriceTest.php b/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Indexer/PriceTest.php index 9af291cf624..069497e8b45 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Indexer/PriceTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Indexer/PriceTest.php @@ -154,40 +154,6 @@ public function testCalculateDynamicBundleSelectionPrice(): void `is_required` = VALUES(`is_required`), `price` = VALUES(`price`), `tier_price` = VALUES(`tier_price`)"; - $processedQuery = "INSERT INTO `catalog_product_index_price_bundle_sel_temp` (,,,,,,,,) SELECT `i`.`entity_id`, - `i`.`customer_group_id`, - `i`.`website_id`, - `bo`.`option_id`, - `bs`.`selection_id`, - IF(bo.type = 'select' OR bo.type = 'radio', 0, 1) AS `group_type`, - `bo`.`required` AS `is_required`, - LEAST(IF(i.special_price > 0 AND i.special_price < 100, - ROUND(idx.min_price * bs.selection_qty * (i.special_price / 100), 4), idx.min_price * bs.selection_qty), - IFNULL((IF(i.tier_percent IS NOT NULL, - ROUND((1 - i.tier_percent / 100) * idx.min_price * bs.selection_qty, 4), NULL)), idx.min_price * - bs.selection_qty)) AS `price`, - IF(i.tier_percent IS NOT NULL, ROUND((1 - i.tier_percent / 100) * idx.min_price * bs.selection_qty, 4), - NULL) AS `tier_price` - FROM `catalog_product_index_price_bundle_temp` AS `i` - INNER JOIN `catalog_product_entity` AS `parent_product` ON parent_product.entity_id = i.entity_id AND - (parent_product.created_in <= 1 AND parent_product.updated_in > 1) - INNER JOIN `catalog_product_bundle_option` AS `bo` ON bo.parent_id = parent_product.row_id - INNER JOIN `catalog_product_bundle_selection` AS `bs` ON bs.option_id = bo.option_id - INNER JOIN `catalog_product_index_price_replica` AS `idx` USE INDEX (PRIMARY) - ON bs.product_id = idx.entity_id AND i.customer_group_id = idx.customer_group_id AND - i.website_id = idx.website_id - INNER JOIN `cataloginventory_stock_status` AS `si` ON si.product_id = bs.product_id - WHERE (i.price_type = 0) - AND (si.stock_status = 1) - ON DUPLICATE KEY UPDATE `entity_id` = VALUES(`entity_id`), - `customer_group_id` = VALUES(`customer_group_id`), - `website_id` = VALUES(`website_id`), - `option_id` = VALUES(`option_id`), - `selection_id` = VALUES(`selection_id`), - `group_type` = VALUES(`group_type`), - `is_required` = VALUES(`is_required`), - `price` = VALUES(`price`), - `tier_price` = VALUES(`tier_price`) ON DUPLICATE KEY UPDATE = VALUES(), = VALUES(), = VALUES(), = VALUES(), = VALUES(), = VALUES(), = VALUES(), = VALUES(), = VALUES()"; //@codingStandardsIgnoreEnd $this->connectionMock->expects($this->exactly(3)) ->method('getCheckSql') @@ -232,8 +198,17 @@ public function testCalculateDynamicBundleSelectionPrice(): void $this->connectionMock->expects($this->once())->method('getIfNullSql'); $this->connectionMock->expects($this->once())->method('getLeastSql'); $this->connectionMock->method('select')->willReturn($select); - $this->connectionMock->expects($this->exactly(9))->method('quoteIdentifier'); - $this->connectionMock->expects($this->once())->method('query')->with($processedQuery); + $insertSql = 'INSERT INTO catalog_product_index_price_bundle_sel_temp ... ON DUPLICATE KEY UPDATE'; + $this->connectionMock->expects($this->once()) + ->method('insertFromSelect') + ->with( + $select, + $this->isType('string'), + $this->isType('array'), + AdapterInterface::INSERT_ON_DUPLICATE + ) + ->willReturn($insertSql); + $this->connectionMock->expects($this->once())->method('query')->with($insertSql); $pool = $this->createMock(EntityMetadataInterface::class); $pool->expects($this->once())->method('getLinkField')->willReturn($entity); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php index 25c8f77ca73..22bccacf49a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php @@ -386,19 +386,19 @@ function ($arg) use ($storeId) { $this->connection->expects($this->any())->method('getIfNullSql')->willReturnMap( [ [ - '`value`.`label`', - '`default_value`.`label`', - 'IFNULL(`value`.`label`, `default_value`.`label`)' + 'value.label', + 'default_value.label', + 'IFNULL(value.label, default_value.label)' ], [ - '`value`.`position`', - '`default_value`.`position`', - 'IFNULL(`value`.`position`, `default_value`.`position`)' + 'value.position', + 'default_value.position', + 'IFNULL(value.position, default_value.position)' ], [ - '`value`.`disabled`', - '`default_value`.`disabled`', - 'IFNULL(`value`.`disabled`, `default_value`.`disabled`)' + 'value.disabled', + 'default_value.disabled', + 'IFNULL(value.disabled, default_value.disabled)' ] ] ); @@ -432,9 +432,9 @@ function ($arg1, $arg2) use ($attributeId, $productId) { ->method('columns') ->with( [ - 'label' => 'IFNULL(`value`.`label`, `default_value`.`label`)', - 'position' => 'IFNULL(`value`.`position`, `default_value`.`position`)', - 'disabled' => 'IFNULL(`value`.`disabled`, `default_value`.`disabled`)', + 'label' => 'IFNULL(value.label, default_value.label)', + 'position' => 'IFNULL(value.position, default_value.position)', + 'disabled' => 'IFNULL(value.disabled, default_value.disabled)', 'label_default' => 'default_value.label', 'position_default' => 'default_value.position', 'disabled_default' => 'default_value.disabled' @@ -466,7 +466,7 @@ function ( ->with(['entity' => $getTableReturnValue], 'main.value_id = entity.value_id', ['entity_id']) ->willReturn($this->select); $this->select->expects($this->once())->method('order') - ->with($positionCheckSql . ' ' . Select::SQL_ASC) + ->with(new \Zend_Db_Expr($positionCheckSql . ' ' . Select::SQL_ASC)) ->willReturnSelf(); $this->connection->expects($this->once())->method('fetchAll') ->with($this->select) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php index 70c0fa77e7c..044bf3d00dd 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php @@ -65,6 +65,12 @@ protected function setUp(): void $this->storeManager = $this->createMock(StoreManagerInterface::class); $this->select = $this->createMock(Select::class); + $connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); + $connection->method('getIfNullSql') + ->with('status_attr.value', 'status_global_attr.value') + ->willReturn('IFNULL(status_attr.value, status_global_attr.value)'); + $this->select->method('getConnection')->willReturn($connection); + $this->statusBaseSelectProcessor = (new ObjectManager($this))->getObject(StatusBaseSelectProcessor::class, [ 'eavConfig' => $this->eavConfig, 'metadataPool' => $this->metadataPool, diff --git a/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php b/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php index a5bbcb79cd6..abd7ffa7ec1 100644 --- a/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php +++ b/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php @@ -79,11 +79,6 @@ public function afterCreateBatchBaseSelect(Gallery $originalResourceModel, Selec ] ), [] - // PgCompat: was hardcoded MySQL backtick identifiers ('`value_video`.`provider`' - // etc) passed as plain strings - getIfNullSql() just interpolates its arguments - // verbatim, so these reached Postgres as literal, invalid backtick syntax - // instead of going through quoteIdentifier(). Same bug, same fix, as the - // core Gallery.php site this plugin's select is built from. )->columns([ 'video_provider' => $originalResourceModel->getConnection() ->getIfNullSql('value_video.provider', 'default_value_video.provider'), diff --git a/setup/src/Magento/Setup/Validator/DbValidator.php b/setup/src/Magento/Setup/Validator/DbValidator.php index c6a371a47e8..0f6b81c1d7e 100644 --- a/setup/src/Magento/Setup/Validator/DbValidator.php +++ b/setup/src/Magento/Setup/Validator/DbValidator.php @@ -89,6 +89,7 @@ public function checkDatabaseConnection($dbName, $dbHost, $dbUser, $dbPass = '') * @param string $dbUser * @param string $dbPass * @param array $driverOptions + * @param string|null $engine Database engine (mysql, postgresql) * @return bool * @throws \Magento\Setup\Exception */ From 3374acff32d66a06fa203af58ca074e9730b38c9 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 18 Aug 2026 15:38:06 -0700 Subject: [PATCH 4/5] Fix Mage-OS CI: wrap FIELD() ORDER BY for Rule BuilderTest and PHPCS line length. --- .../Test/Unit/Model/BulkStatusTest.php | 4 +++- .../Magento/Rule/Model/Condition/Sql/Builder.php | 14 ++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkStatusTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkStatusTest.php index 155a73fcd97..de2539ec38f 100644 --- a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkStatusTest.php +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkStatusTest.php @@ -284,7 +284,9 @@ public function testGetBulksByUser(): void $bulkCollection->expects($this->once())->method('getSelect')->willReturn($selectMock); $selectMock->expects($this->once())->method('columns')->willReturnSelf(); $selectMock->expects($this->once())->method('order')->willReturnSelf(); - $this->resourceConnectionMock->expects($this->once())->method('getConnection')->willReturn($this->connectionMock); + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); $this->connectionMock->expects($this->once())->method('getFieldSql')->willReturn('FIELD(status, 1, 2)'); $this->bulkCollectionFactory->expects($this->once())->method('create')->willReturn($bulkCollection); $bulkCollection->expects($this->once())->method('addFieldToFilter')->with('user_id', $userId)->willReturnSelf(); diff --git a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php index 0fadc12b67f..bc0e66d1c29 100644 --- a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php +++ b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php @@ -282,18 +282,20 @@ private function buildConditions(AbstractCollection $collection, Combine $combin } if (!empty($conditions) && !empty($attributeField)) { - // PgCompat: FIELD() is MySQL-only - getFieldSql() isn't part of - // AdapterInterface, but every connection in this deployment is one of - // the two PgCompat providers, both of which implement it as a - // CASE-expression equivalent. quote() on an array returns one - // comma-joined, already-quoted string - split back into a value list. $conditionValues = explode( ', ', $this->_connection->quote(array_map('trim', explode(',', $conditions))) ); $collection->getSelect()->reset(Select::ORDER); $collection->getSelect()->order( - $this->_connection->getFieldSql($attributeField, $conditionValues) + $this->_expressionFactory->create( + [ + 'expression' => (string) $this->_connection->getFieldSql( + $attributeField, + $conditionValues + ) + ] + ) ); } } From 026e54ebe7c884ba2f3ce01f4f154c0047808ee1 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 18 Aug 2026 19:11:12 -0700 Subject: [PATCH 5/5] Trim portable-SQL call-site comments to Magento-style notes. --- .../Backup/Model/ResourceModel/Helper.php | 1 - .../Model/ResourceModel/Indexer/Price.php | 5 ----- .../Model/Attribute/ScopeOverriddenValue.php | 6 ------ .../Category/Product/AbstractAction.php | 17 ----------------- .../Indexer/Product/Flat/Action/Eraser.php | 2 -- .../ResourceModel/Category/Collection.php | 6 ------ .../Collection/AbstractCollection.php | 8 -------- .../Collection/JoinMinimalPosition.php | 5 +---- .../Model/ResourceModel/Product/Gallery.php | 10 ---------- .../Product/Indexer/Eav/AbstractEav.php | 4 ---- .../Product/Indexer/Price/DefaultPrice.php | 11 +---------- .../Indexer/Price/Query/BaseFinalPrice.php | 10 ---------- .../ResourceModel/ReadSnapshotPlugin.php | 19 ++++++++----------- .../Ui/Component/Listing/Columns/Websites.php | 4 ---- .../Model/Indexer/IndexerTableSwapper.php | 4 ---- .../Indexer/Fulltext/Action/DataProvider.php | 5 ----- .../Collection/SearchResultApplier.php | 3 --- .../Model/Variant/Collection.php | 13 +------------ .../Patch/Data/SessionIDColumnCleanUp.php | 5 ----- .../Eav/Model/Entity/AbstractEntity.php | 8 -------- .../Model/Entity/Attribute/Source/Table.php | 5 ----- .../Entity/Collection/AbstractCollection.php | 4 ---- .../ChangelogBatchWalker/IdsSelectBuilder.php | 6 ------ .../Entity/Attribute/Collection.php | 17 ----------------- .../Eav/Model/ResourceModel/ReadHandler.php | 7 ------- .../Model/ResourceModel/Import/Data.php | 3 --- .../Model/ResourceModel/Oauth/Consumer.php | 2 -- .../Patch/Data/AddMediaGalleryPermissions.php | 3 --- .../Model/ResourceModel/Review/Summary.php | 2 -- .../Patch/Data/SessionIDColumnCleanUp.php | 5 ----- .../Data/SetCreditCardAsDefaultTokenType.php | 4 ---- .../Product/WeeeAttributeProductSort.php | 2 -- .../Framework/Cache/Backend/Database.php | 1 - .../Framework/DB/Adapter/AdapterInterface.php | 4 ++-- .../Framework/DB/Query/BatchIterator.php | 11 +---------- lib/internal/Magento/Framework/DB/Select.php | 14 +------------- .../Framework/Data/Collection/AbstractDb.php | 9 --------- 37 files changed, 15 insertions(+), 230 deletions(-) diff --git a/app/code/Magento/Backup/Model/ResourceModel/Helper.php b/app/code/Magento/Backup/Model/ResourceModel/Helper.php index 94649e1594e..f00028d548b 100644 --- a/app/code/Magento/Backup/Model/ResourceModel/Helper.php +++ b/app/code/Magento/Backup/Model/ResourceModel/Helper.php @@ -178,7 +178,6 @@ public function getHeader() { $dbConfig = $this->getConnection()->getConfig(); - // PgCompat: getServerVersion() is portable (PDO ATTR_SERVER_VERSION). $versionRow = ['Value' => $this->getConnection()->getServerVersion()]; $hostName = !empty($dbConfig['unix_socket']) ? $dbConfig['unix_socket'] diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php index 1c6eac6b1f1..93c35ee43e5 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php @@ -485,11 +485,6 @@ private function calculateBundleOptionPrice($priceTable, $dimensions) [ 'min_price' => new \Zend_Db_Expr('MIN(' . $minPrice . ')'), 'alt_price' => new \Zend_Db_Expr('MIN(price)'), - // PgCompat: group_type is functionally dependent on option_id within this - // GROUP BY, but MySQL's non-ONLY_FULL_GROUP_BY leniency allowed referencing - // it bare in the CASE condition anyway - Postgres requires it aggregated. - // MIN(group_type) is value-identical to group_type here (same table/row - // set as before), so this is a no-op on MySQL and a fix on Postgres. 'max_price' => $connection->getCheckSql('MIN(group_type) = 0', 'MAX(price)', 'SUM(price)'), 'tier_price' => new \Zend_Db_Expr('MIN(' . $tierPrice . ')'), 'alt_tier_price' => new \Zend_Db_Expr('MIN(tier_price)'), diff --git a/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php b/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php index 779eaebc44b..e468dbc4456 100644 --- a/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php +++ b/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php @@ -144,12 +144,6 @@ private function initAttributeValues($entityType, $entity, $storeId) $storeIds[] = $storeId; } $selects = []; - // PgCompat: this select is UNION ALL'd (below) with one of these per EAV - // backend type table (varchar/int/decimal/text/datetime) - MySQL coerces the - // differently-typed "value" columns across branches implicitly, Postgres - // requires them to already share one type. Same fix as the other EAV - // union-cast sites (union-cast-eav-abstract-collection.patch, - // Eav\Model\ResourceModel\ReadHandler::execute(), ReadSnapshotPlugin). foreach ($attributeTables as $attributeTable => $attributeCodes) { $select = $metadata->getEntityConnection()->select() ->from( diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php index 97999c3baaf..7c40f30fc1e 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php @@ -388,10 +388,6 @@ protected function getNonAnchorCategoriesSelect(Store $store) [ 'category_id' => 'cc.entity_id', 'product_id' => 'ccp.product_id', - // PgCompat: bare ccp.position is non-aggregated under the GROUP BY - // addFilteringByChildProductsToSelect() adds below; MIN() is a no-op - // on MySQL (one ccp row per cc.entity_id/ccp.product_id group here) - // and required by Postgres. 'position' => new \Zend_Db_Expr('MIN(ccp.position)'), 'is_parent' => new \Zend_Db_Expr('1'), 'store_id' => new \Zend_Db_Expr($store->getId()), @@ -643,9 +639,6 @@ protected function createAnchorSelect(Store $store) [ 'category_id' => 'cc.entity_id', 'product_id' => 'ccp.product_id', - // PgCompat: ccp2.position is non-aggregated under the GROUP BY - // addFilteringByChildProductsToSelect() adds below; MAX() is a no-op on - // MySQL (one ccp2 row per group here) and required by Postgres. 'position' => new \Zend_Db_Expr( $this->connection->getIfNullSql('MAX(ccp2.position)', 'MIN(ccp.position) + 10000') ), @@ -881,21 +874,11 @@ protected function getAllProducts(Store $store) $this->connection->getIfNullSql('cpvs.value', 'cpvd.value') . ' IN (?)', $this->visibility->getVisibleInSiteIds() )->group( - // PgCompat: the visibility column below is a non-aggregated COALESCE - // expression selected under this GROUP BY - MySQL's non-ONLY_FULL_GROUP_BY - // leniency tolerated that; Postgres requires it grouped too. cpvs/cpvd are - // already filtered to at most one row per cp.entity_id (store_id/attribute_id - // predicates above), so this doesn't change which rows collapse together. ['cp.entity_id', new \Zend_Db_Expr($this->connection->getIfNullSql('cpvs.value', 'cpvd.value'))] )->columns( [ 'category_id' => new \Zend_Db_Expr($store->getRootCategoryId()), 'product_id' => 'cp.entity_id', - // PgCompat: ccp.product_id IS NOT NULL is a non-aggregated column - // reference in the CASE condition under the GROUP BY above; COUNT(...)>0 - // is the aggregate-safe equivalent (true iff at least one non-null - // ccp.product_id exists in the group, same truth value as the original - // MySQL-tolerated condition). 'position' => new \Zend_Db_Expr( $this->connection->getCheckSql('COUNT(ccp.product_id) > 0', 'MIN(ccp.position)', '10000') ), diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php index bca3cfedf13..f7ec0bd476e 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php @@ -109,8 +109,6 @@ public function removeDisabledProducts(array &$ids, $storeId) . 'product_table.' . $metadata->getLinkField(), [] ); - // PgCompat: getIfNullSql() renders per-dialect (IFNULL on MySQL, COALESCE on - // Postgres) instead of hardcoding MySQL's IFNULL() text. $select->where( $this->connection->getIfNullSql('status_attr.value', 'status_global_attr.value') . ' = ?', Status::STATUS_DISABLED diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index 25dccda66a6..b4cae61e9ed 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -723,12 +723,6 @@ private function getProductsCountQuery(array $categoryIds, $addVisibilityFilter if (true === $addVisibilityFilter) { $select->where('cat_index.visibility in (?)', $this->catalogProductVisibility->getVisibleInSiteIds()); } - // PgCompat: was only grouped when count($categoryIds) > 1 - but this select - // always mixes a non-aggregated column (category_id) with an aggregate - // (count(...)), so the single-category case needs GROUP BY too. MySQL's - // non-ONLY_FULL_GROUP_BY mode tolerated the omission (trivially one group - // either way); Postgres requires it unconditionally. Grouping a single-value set - // is a no-op on both, so this fixes the real bug rather than working around it. $select->group('cat_index.category_id'); return $select; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php index 0d6cbb1c513..c6ed51812db 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php @@ -239,14 +239,6 @@ protected function _addLoadAttributesSelectValues($select, $table, $type) $storeId = $this->getStoreId(); if ($storeId) { $connection = $this->getConnection(); - // PgCompat: this select is UNION ALL'd with one of these per EAV backend - // type (varchar/int/decimal/text/datetime) in _loadAttributes() - MySQL - // coerces the differently-typed columns across branches implicitly, - // Postgres requires them to already share one type before the UNION. Same - // fix as the parent Eav\Model\Entity\Collection\AbstractCollection version - // (union-cast-eav-abstract-collection.patch) this override replaces - that - // patch only touched the parent method, missing this store-fallback - // override entirely since it never calls parent in the $storeId branch. $valueExpr = $connection->getCheckSql( 't_s.value_id IS NULL', $connection->castToText('t_d.value'), diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection/JoinMinimalPosition.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection/JoinMinimalPosition.php index 8ea9fe04237..39f189b62a1 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection/JoinMinimalPosition.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection/JoinMinimalPosition.php @@ -70,10 +70,7 @@ public function execute(Collection $collection, array $categoryIds): void [] ); } - // PgCompat: '~0' (MySQL's max UNSIGNED BIGINT via bitwise-NOT-of-zero) means - // something entirely different on Postgres (signed integers - '~0' there is - // -1). Postgres' own bigint max serves the same "sort missing positions - // last" sentinel purpose. + // Portable max bigint for missing position. $positions[] = $connection->getIfNullSql($table . '.position', '9223372036854775807'); } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php index 44e02326cf2..0af38a583cf 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php @@ -224,12 +224,6 @@ public function createBatchBaseSelect($storeId, $attributeId) ), [] )->columns([ - // PgCompat: was hardcoded MySQL backtick identifiers ('`value`.`label`' etc) - // passed as plain strings - getIfNullSql() just interpolates its arguments - // verbatim, so these reached Postgres as literal, invalid backtick syntax - // instead of going through quoteIdentifier(). Unquoted identifiers are valid - // input to getIfNullSql() on both adapters (they build "COALESCE(a, b)" - // directly from the given text), so plain dotted names are enough here. 'label' => $this->getConnection()->getIfNullSql('value.label', 'default_value.label'), 'position' => $this->getConnection()->getIfNullSql('value.position', 'default_value.position'), 'disabled' => $this->getConnection()->getIfNullSql('value.disabled', 'default_value.disabled'), @@ -242,10 +236,6 @@ public function createBatchBaseSelect($storeId, $attributeId) )->where( $mainTableAlias . '.disabled = 0' )->order( - // PgCompat: was a bare string - Select::order() quotes plain-string - // arguments as if they were a single identifier, mangling this multi-word - // CASE expression into invalid, partially-quoted garbage. Zend_Db_Expr marks - // it as a raw SQL fragment instead, which order() passes through unquoted. new \Zend_Db_Expr($positionCheckSql . ' ' . \Magento\Framework\DB\Select::SQL_ASC) ); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php index 8b3031ef900..82ebef48654 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php @@ -189,10 +189,6 @@ protected function _prepareRelationIndexSelect(?array $parentIds = null) 'i.entity_id = cpw.product_id AND sw.website_id = cpw.website_id', [] )->group( - // PgCompat: MySQL resolves a GROUP BY item against the SELECT alias list, so - // 'parent_id' here refers to the 'parent_id' => 'e.entity_id' column below; - // Postgres doesn't do alias resolution in GROUP BY, so it has to be the - // underlying expression directly. ['e.entity_id', 'i.attribute_id', 'i.store_id', 'i.value', 'l.child_id'] )->columns( [ diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php index b5cc2914525..bb81eb1a994 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php @@ -454,11 +454,7 @@ protected function getSelect($entityIds = null, $type = null) ); $currentDate = 'cwd.website_date'; - // PgCompat: '~0' (MySQL's bitwise-NOT-of-zero, the max UNSIGNED BIGINT value) - // means something entirely different on Postgres, whose integers are signed - - // '~0' there is bitwise NOT of a signed int, i.e. -1. Postgres' own bigint max - // is used as the "effectively unbounded" sentinel instead; it doesn't need to - // be the same numeric ceiling MySQL used, only larger than any real price. + // Portable max bigint (not MySQL ~0). $maxUnsignedBigint = '9223372036854775807'; $specialFromDate = $connection->getDatePartSql($specialFrom); $specialToDate = $connection->getDatePartSql($specialTo); @@ -843,11 +839,6 @@ protected function hasEntity() */ private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression) { - // PgCompat: '~0' (MySQL's bitwise-NOT-of-zero, the max UNSIGNED BIGINT value) - // means something entirely different on Postgres, whose integers are signed - - // '~0' there is bitwise NOT of a signed int, i.e. -1. Postgres' own bigint max - // is used as the "effectively unbounded" sentinel instead; it doesn't need to - // be the same numeric ceiling MySQL used, only larger than any real price. $maxUnsignedBigint = '9223372036854775807'; return $this->getConnection()->getCheckSql( diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php index fe3286d4bd1..69430a8973b 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php @@ -144,11 +144,6 @@ public function getQuery(array $dimensions, string $productType, array $entityId $specialTo = $this->joinAttributeProcessor->process($select, 'special_to_date'); $currentDate = 'cwd.website_date'; - // PgCompat: '~0' (MySQL's bitwise-NOT-of-zero, the max UNSIGNED BIGINT value) - // means something entirely different on Postgres, whose integers are signed - - // '~0' there is bitwise NOT of a signed int, i.e. -1. Postgres' own bigint max - // is used as the "effectively unbounded" sentinel instead; it doesn't need to - // be the same numeric ceiling MySQL used, only larger than any real price. $maxUnsignedBigint = '9223372036854775807'; $specialFromDate = $connection->getDatePartSql($specialFrom); $specialToDate = $connection->getDatePartSql($specialTo); @@ -262,11 +257,6 @@ public function getQuery(array $dimensions, string $productType, array $entityId */ private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression) { - // PgCompat: '~0' (MySQL's bitwise-NOT-of-zero, the max UNSIGNED BIGINT value) - // means something entirely different on Postgres, whose integers are signed - - // '~0' there is bitwise NOT of a signed int, i.e. -1. Postgres' own bigint max - // is used as the "effectively unbounded" sentinel instead; it doesn't need to - // be the same numeric ceiling MySQL used, only larger than any real price. $maxUnsignedBigint = '9223372036854775807'; return $this->getConnection()->getCheckSql( diff --git a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php index cd62cb5a3c7..3099218dda0 100644 --- a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php +++ b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/ReadSnapshotPlugin.php @@ -41,6 +41,8 @@ public function __construct( } /** + * Merge global-scope catalog attribute values into the current-store snapshot. + * * @param ReadSnapshot $subject * @param array $entityData * @param string $entityType @@ -73,17 +75,12 @@ public function afterExecute(ReadSnapshot $subject, array $entityData, $entityTy if ($globalAttributes) { $selects = []; foreach ($globalAttributes as $table => $attributeIds) { - // PgCompat: this select is UNION ALL'd (below) with one of these per EAV - // backend type table (varchar/int/decimal/text/datetime) - MySQL coerces - // the differently-typed "value" columns across branches implicitly, - // Postgres requires them to already share one type. Same fix as the - // other EAV union-cast sites (union-cast-eav-abstract-collection.patch, - // Eav\Model\ResourceModel\ReadHandler::execute()). - $select = $connection->select() - ->from( - ['t' => $table], - ['value' => $connection->castToText('t.value'), 'attribute_id' => 't.attribute_id'] - ) + $select = $connection->select(); + // Align types across UNION ALL branches. + $select->from( + ['t' => $table], + ['value' => $connection->castToText('t.value'), 'attribute_id' => 't.attribute_id'] + ) ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()]) ->where('attribute_id' . ' in (?)', $attributeIds) ->where('store_id = ?', \Magento\Store\Model\Store::DEFAULT_STORE_ID); diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php index f662571fd68..7277935bc8e 100644 --- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php +++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php @@ -129,10 +129,6 @@ protected function applySorting() $connection = $collection->getConnection(); $select = $connection->select(); - // PgCompat: GROUP_CONCAT() is MySQL-only - getGroupConcatSql() isn't part - // of AdapterInterface, but every connection in this deployment is one of - // the two PgCompat providers, both of which implement it as a - // string_agg() equivalent. $select->from( ['cpw' => $collection->getTable('catalog_product_website')], ['product_id'] diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php index 74f9945a390..62ff4bb78d1 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexerTableSwapper.php @@ -47,10 +47,6 @@ private function createTemporaryTable(string $originalTableName): string $originalTableName . '__temp' . $this->generateRandomSuffix() ); - // PgCompat: `CREATE TABLE x LIKE y` is MySQL shorthand with no direct Postgres - // equivalent (`CREATE TABLE x (LIKE y INCLUDING ALL)` instead) - createTableLike() - // isn't part of AdapterInterface, but every connection in this deployment is one - // of the two PgCompat providers, both of which implement it. $this->resourceConnection->getConnection()->createTableLike( $temporaryTableName, $this->resourceConnection->getTableName($originalTableName) diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php index a8baf31d3f9..f4480debbea 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -336,11 +336,6 @@ private function unifyField($field, $backendType = 'varchar') if ($backendType == 'datetime') { $expr = $this->connection->getDateFormatSql($field, '%Y-%m-%d %H:%i:%s'); } else { - // PgCompat: the caller UNION ALL's one of these per EAV backend type - // (varchar/int/decimal/text) - MySQL coerces the differently-typed values - // implicitly, Postgres requires them to already share one type before the - // UNION. getDateFormatSql() above already returns a formatted string, so - // only this branch needs an explicit cast to match it. $expr = $this->connection->castToText($field); } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php index 364814aa4f4..34adfa16ae3 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -54,9 +54,6 @@ public function apply() $ids[] = (int)$item->getId(); } - // PgCompat: FIELD() is MySQL-only - getFieldSql() isn't part of AdapterInterface, - // but every connection in this deployment is one of the two PgCompat providers, - // both of which implement it as a CASE-expression equivalent. $select = $this->collection->getSelect(); $select->where('e.entity_id IN (?)', $ids) ->reset(\Magento\Framework\DB\Select::ORDER) diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php index 94547258bf2..9d5c413b896 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php @@ -160,19 +160,8 @@ private function fetch(ContextInterface $context, array $attributeCodes) : array $childCollection->addWebsiteFilter($context->getExtensionAttributes()->getStore()->getWebsiteId()); $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $childCollection->getSelect()->group('e.' . $linkField); - // PgCompat: GROUP_CONCAT() is MySQL-only - getGroupConcatSql() isn't part of - // AdapterInterface, but this deployment's adapter implements it as a - // string_agg() equivalent. - // - // ChildCollection::_initSelect() unconditionally selects raw link_table.parent_id - // (needed by every OTHER caller of that base collection, which don't group at - // all); once this method's own group('e.' . $linkField) is added on top, that raw - // column is no longer functionally dependent on the GROUP BY key (a child can - // have more than one parent) and Postgres rejects it outright - the whole reason - // parent_ids exists is to carry that same information aggregated instead. Drop - // the raw column here, narrowly, via setPart() rather than in the base class, - // since only this grouped call site is affected. $select = $childCollection->getSelect(); + // Drop ungrouped parent_id; parent_ids is aggregated instead. $select->setPart( \Magento\Framework\DB\Select::COLUMNS, array_values(array_filter( diff --git a/app/code/Magento/Customer/Setup/Patch/Data/SessionIDColumnCleanUp.php b/app/code/Magento/Customer/Setup/Patch/Data/SessionIDColumnCleanUp.php index bd3cbf1ad2e..c3e5bf2af32 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/SessionIDColumnCleanUp.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/SessionIDColumnCleanUp.php @@ -66,11 +66,6 @@ public function apply() private function cleanCustomerVisitorTable() { $tableName = $this->moduleDataSetup->getTable('customer_visitor'); - // PgCompat: was '... LIMIT 1000' - Postgres has no UPDATE...LIMIT. Safe to drop: - // the do-while loop below already terminates on rowCount() === 0, not on a - // fixed iteration count, so removing LIMIT just means one larger UPDATE instead - // of several 1000-row batches - same terminal state either way. - // phpcs:ignore Magento2.SQL.RawQuery $rawQuery = sprintf( 'UPDATE %s SET session_id = NULL WHERE session_id IS NOT NULL', $tableName diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index 20f05d58d27..6a46435b091 100644 --- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php +++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php @@ -1063,14 +1063,6 @@ protected function _loadModelAttributes($object) $attribute = current($this->_attributesByTable[$table]); $eavType = $attribute->getBackendType(); $select = $this->_getLoadAttributesSelect($object, $table); - // PgCompat: was columns('*') - each per-type value table's "value" column is - // a different SQL type (varchar/int/decimal/text/datetime), so UNION ALL-ing - // them raw fails ("UNION types character varying and integer cannot be - // matched"). Only attribute_id/value_id/value are ever read back (see - // _setAttributeValue() below), so an explicit column list - casting just - // value to text - replaces the wildcard instead of trying to keep every - // table's full, non-identical column set (e.g. Catalog's *_int/*_varchar - // etc. have store_id, Customer's don't) unioned as-is. $selects[$eavType][] = $select->columns([ 'attribute_id' => 'attribute_id', 'value_id' => 'value_id', diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php index c8ed934df81..ca6b668db49 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php @@ -76,11 +76,6 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) } $attributeId = $this->getAttribute()->getId(); if (!isset($this->_options[$storeId][$attributeId])) { - // PgCompat: an attribute with no id yet (new/unsaved attribute model) can - // never have real eav_attribute_option rows - short-circuit instead of - // querying "attribute_id = ''" (MySQL tolerantly returns zero rows there via - // its '' -> 0 numeric coercion; Postgres rejects the comparison outright). - // Same empty-result outcome either way, without the pointless query. if (!$attributeId) { $this->_options[$storeId][$attributeId] = []; $this->_optionsDefault[$storeId][$attributeId] = []; diff --git a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php index 73cde3cdc7d..743137c4c61 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php @@ -1315,10 +1315,6 @@ protected function _getLoadAttributesSelect($table, $attributeIds = []) */ protected function _addLoadAttributesSelectValues($select, $table, $type) { - // PgCompat: this select is UNION ALL'd with one of these per EAV backend type - // (varchar/int/decimal/text/datetime) in _loadAttributes() below - MySQL - // coerces the differently-typed "value" columns across branches implicitly, - // Postgres requires them to already share one type before the UNION. $select->columns(['value' => $this->getConnection()->castToText('t_d.value')]); return $select; } diff --git a/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsSelectBuilder.php b/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsSelectBuilder.php index 13504baaf2e..3fbbaaf80ba 100644 --- a/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsSelectBuilder.php +++ b/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsSelectBuilder.php @@ -43,16 +43,10 @@ public function __construct( */ public function build(ChangelogInterface $changelog): Select { - // PgCompat: no setGroupConcatMax() call here - that tunes MySQL's - // group_concat_max_len session variable, a length cap string_agg() (used below - // via getGroupConcatSql()) has no equivalent of or need for. $changelogTableName = $this->resourceConnection->getTableName($changelog->getName()); $connection = $this->resourceConnection->getConnection(); - // PgCompat: GROUP_CONCAT() is MySQL-only - getGroupConcatSql() isn't part of - // AdapterInterface, but every connection in this deployment is one of the two - // PgCompat providers, both of which implement it as a string_agg() equivalent. $columns = [ $changelog->getColumnName(), 'attribute_ids' => $connection->getGroupConcatSql('attribute_id'), diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php index 1f47d2c89db..4f8c1dde3a7 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php @@ -302,15 +302,6 @@ public function addAttributeGrouping() { $select = $this->getSelect(); $select->group('main_table.attribute_id'); - // PgCompat: Catalog\Model\ResourceModel\Product\Attribute\Collection::_initSelect() - // unconditionally joins "additional_table" (catalog_eav_attribute) and selects - // additional_table.* - Postgres requires a table's OWN primary key in GROUP BY - // before it accepts that table's other columns un-aggregated (the join condition - // guaranteeing additional_table.attribute_id = main_table.attribute_id isn't - // enough; Postgres only applies the functional-dependency exception via a - // table's own PK). Guarded on the alias actually being joined - grouping by an - // alias that was never joined is a "missing FROM-clause entry" error, and not - // every Attribute\Collection subclass joins additional_table. if (array_key_exists('additional_table', $select->getPart(Select::FROM))) { $select->group('additional_table.attribute_id'); } @@ -356,14 +347,6 @@ public function addHasOptionsFilter() ] ); - // PgCompat: joinLeft()'s 3rd arg selects columns from "ao" into the result set; - // neither of this method's two real callers (CatalogGraphQl\Model\Config\ - // FilterAttributeReader, CatalogSearch\Model\Advanced) reads option_id off the - // loaded attributes - the join exists only for the ao.option_id > 0 filter below. - // Selecting it anyway (MySQL tolerates a non-aggregated, non-grouped column; not - // functionally dependent on the GROUP BY key since eav_attribute_option is a - // one-to-many join) makes every call to this method fail outright on Postgres. - // Selecting no columns from "ao" removes the violation with no behavior change. $this->getSelect()->joinLeft( ['ao' => $this->getTable('eav_attribute_option')], 'ao.attribute_id = main_table.attribute_id', diff --git a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php index 058f9087001..38e8ec6b25d 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php +++ b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php @@ -153,13 +153,6 @@ public function execute($entityType, $entityData, $arguments = []) if (count($attributeTables)) { $identifiers = null; foreach ($attributeTables as $attributeTable => $attributeIds) { - // PgCompat: this select is UNION ALL'd (below, via UnionExpression) with - // one of these per EAV backend type table (varchar/int/decimal/text/ - // datetime) - MySQL coerces the differently-typed "value" columns across - // branches implicitly, Postgres requires them to already share one type. - // Same fix as the other two EAV union-cast sites (see - // union-cast-eav-abstract-collection.patch and - // Catalog\Model\ResourceModel\Collection\AbstractCollection's override). $select = $connection->select() ->from( ['t' => $attributeTable], diff --git a/app/code/Magento/ImportExport/Model/ResourceModel/Import/Data.php b/app/code/Magento/ImportExport/Model/ResourceModel/Import/Data.php index a7ea22dd4eb..6b15936ae92 100644 --- a/app/code/Magento/ImportExport/Model/ResourceModel/Import/Data.php +++ b/app/code/Magento/ImportExport/Model/ResourceModel/Import/Data.php @@ -124,9 +124,6 @@ public function cleanBunches() public function cleanProcessedBunches() { $connection = $this->getConnection(); - // PgCompat: TIMESTAMPADD() is MySQL-only (getDateAddSql() renders per-dialect - // date arithmetic instead), and CURRENT_TIMESTAMP is a reserved keyword, not a - // function - MySQL tolerates the empty-parens call form, Postgres rejects it. $connection->delete( $this->getMainTable(), 'is_processed = 1 OR ' diff --git a/app/code/Magento/Integration/Model/ResourceModel/Oauth/Consumer.php b/app/code/Magento/Integration/Model/ResourceModel/Oauth/Consumer.php index 2b720b9d1cd..8b72a9805a3 100644 --- a/app/code/Magento/Integration/Model/ResourceModel/Oauth/Consumer.php +++ b/app/code/Magento/Integration/Model/ResourceModel/Oauth/Consumer.php @@ -68,8 +68,6 @@ public function getTimeInSecondsSinceCreation($consumerId) $select = $connection->select() ->from($this->getMainTable()) ->reset(\Magento\Framework\DB\Select::COLUMNS) - // PgCompat: CURRENT_TIMESTAMP is a reserved keyword, not a function - MySQL - // tolerates the empty-parens call form, Postgres rejects it. ->columns(new \Zend_Db_Expr('CURRENT_TIMESTAMP - created_at')) ->where('entity_id = ?', $consumerId); diff --git a/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php index 2954e0d65f6..9342a1dbef5 100644 --- a/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php +++ b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php @@ -45,9 +45,6 @@ public function apply(): void $select = $connection->select() ->from($tableName, ['role_id']) - // PgCompat: a double-quoted string is a valid MySQL string literal but a - // Postgres identifier - single quotes are required for it to mean the same - // thing on both databases. ->where("resource_id = 'Magento_Cms::media_gallery'"); $insertData = $this->getInsertData($connection->fetchCol($select)); diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php b/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php index 4304b339b77..c1abb6fa468 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php @@ -101,8 +101,6 @@ public function appendSummaryFieldsToCollection( "e.entity_id = review_summary.entity_pk_value AND review_summary.store_id = {$storeId}" . " AND review_summary.entity_type = ({$summaryEntitySubSelect})" ); - // PgCompat: getIfNullSql() renders per-dialect (IFNULL on MySQL, COALESCE on - // Postgres) instead of hardcoding MySQL's IFNULL() text. $productCollection->getSelect() ->joinLeft( ['review_summary' => $this->getMainTable()], diff --git a/app/code/Magento/Security/Setup/Patch/Data/SessionIDColumnCleanUp.php b/app/code/Magento/Security/Setup/Patch/Data/SessionIDColumnCleanUp.php index 126fa88672d..3d26cc58ae8 100644 --- a/app/code/Magento/Security/Setup/Patch/Data/SessionIDColumnCleanUp.php +++ b/app/code/Magento/Security/Setup/Patch/Data/SessionIDColumnCleanUp.php @@ -66,11 +66,6 @@ public function apply() private function cleanAdminUserSessionTable() { $tableName = $this->moduleDataSetup->getTable('admin_user_session'); - // PgCompat: was '... LIMIT 1000' - Postgres has no UPDATE...LIMIT. Safe to drop: - // the do-while loop below already terminates on rowCount() === 0, not on a - // fixed iteration count, so removing LIMIT just means one larger UPDATE instead - // of several 1000-row batches - same terminal state either way. - // phpcs:ignore Magento2.SQL.RawQuery $rawQuery = sprintf( 'UPDATE %s SET session_id = NULL WHERE session_id IS NOT NULL', $tableName diff --git a/app/code/Magento/Vault/Setup/Patch/Data/SetCreditCardAsDefaultTokenType.php b/app/code/Magento/Vault/Setup/Patch/Data/SetCreditCardAsDefaultTokenType.php index d32207c6b91..888f3f40a94 100644 --- a/app/code/Magento/Vault/Setup/Patch/Data/SetCreditCardAsDefaultTokenType.php +++ b/app/code/Magento/Vault/Setup/Patch/Data/SetCreditCardAsDefaultTokenType.php @@ -42,10 +42,6 @@ public function apply() // data update for Vault module < 2.0.1 // update sets credit card as default token type - // PgCompat: was ' = ""' - a double-quoted empty-string literal, MySQL-only - // (ANSI_QUOTES-off syntax); Postgres double quotes always mean an identifier, so - // this rendered as a zero-length identifier reference instead of comparing - // against an empty string. Single-quoted is valid on both. $this->moduleDataSetup->getConnection()->update( $this->moduleDataSetup->getTable('vault_payment_token'), [ diff --git a/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Product/WeeeAttributeProductSort.php b/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Product/WeeeAttributeProductSort.php index a1adf0887a1..db2cbadf543 100644 --- a/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Product/WeeeAttributeProductSort.php +++ b/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Product/WeeeAttributeProductSort.php @@ -47,8 +47,6 @@ public function afterBuild( $connection = $this->resourceConnection->getConnection(); $select = $connection->select(); - // PgCompat: getIfNullSql() renders per-dialect (IFNULL on MySQL, COALESCE on - // Postgres) instead of hardcoding MySQL's IFNULL() text. $weeeValue = $connection->getIfNullSql( 'weee_child.value', (string) $connection->getIfNullSql('weee_parent.value', 0) diff --git a/lib/internal/Magento/Framework/Cache/Backend/Database.php b/lib/internal/Magento/Framework/Cache/Backend/Database.php index f9582f71bc6..9a86149059c 100644 --- a/lib/internal/Magento/Framework/Cache/Backend/Database.php +++ b/lib/internal/Magento/Framework/Cache/Backend/Database.php @@ -230,7 +230,6 @@ public function save($data, $id, $tags = [], $specificLifetime = null) $time = time(); $expire = $lifetime === 0 || $lifetime === null ? 0 : $time + $lifetime; - // PgCompat: insertOnDuplicate() emits ON CONFLICT on Postgres. $result = $connection->insertOnDuplicate( $dataTable, [ diff --git a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php index c4cad8b8c71..8c5f2b48d50 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php +++ b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php @@ -820,7 +820,7 @@ public function getCheckSql($condition, $true, $false); public function getIfNullSql($expression, $value = 0); /** - * GROUP_CONCAT / string_agg equivalent + * Concatenate grouped column values with a separator. * * @param string|\Zend_Db_Expr $expression * @param string $separator @@ -831,7 +831,7 @@ public function getIfNullSql($expression, $value = 0); public function getGroupConcatSql($expression, $separator = ',', $orderBy = null, $distinct = false); /** - * FIELD() / CASE equivalent for ORDER BY a fixed list + * Rank an expression by a fixed value list for ORDER BY. * * @param string|\Zend_Db_Expr $expression * @param array $values diff --git a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php index 8e57af33394..08f01be93d9 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php @@ -174,16 +174,7 @@ private function calculateBatchSize(Select $select) ] ); $row = $this->connection->fetchRow($wrapperSelect); - // PgCompat: MAX() over zero matching rows is SQL NULL - when this batch is - // empty (cnt below ends up 0 and the iterator stops right after this call - // anyway), don't overwrite minValue with that null. A leftover null minValue - // fed into initSelectObject()'s "> ?" bind is otherwise silently coerced to an - // empty-string parameter by PDO, which MySQL's loose bigint/string comparison - // tolerates (implicitly treating '' as 0) but Postgres rejects outright - // ("invalid input syntax for type bigint"). Real-world trigger: any - // FieldDataConverter::convert() call (e.g. Theme's ConvertSerializedData data - // patch) whose target rows are exhausted after fewer than batchSize rows, or - - // as first hit here - never existed at all. + // Empty batch: MAX() is NULL; keep the previous minValue. $this->minValue = $row['max'] ?? $this->minValue; return (int)$row['cnt']; } diff --git a/lib/internal/Magento/Framework/DB/Select.php b/lib/internal/Magento/Framework/DB/Select.php index c9552c1c1dc..d2b79e527ae 100644 --- a/lib/internal/Magento/Framework/DB/Select.php +++ b/lib/internal/Magento/Framework/DB/Select.php @@ -57,19 +57,7 @@ class Select extends \Zend_Db_Select * Class constructor * Add straight join support * - * PgCompat: type-hint relaxed from the concrete Adapter\Pdo\Mysql to - * Zend_Db_Adapter_Abstract - this class only calls forUpdate()/ - * getQuoteIdentifierSymbol()/orderRand()/supportStraightJoin() on $adapter, all - * part of AdapterInterface's own contract (not Zend_Db_Adapter_Abstract's), so the - * narrower Mysql-only type-hint was never load-bearing for anything this class - * actually does - PHP doesn't statically check that a dynamic method call exists - * against the parameter's declared type, only that the object passed at runtime - * has it. Zend_Db_Adapter_Abstract (not the AdapterInterface interface) specifically - * because parent::__construct() below requires it, and PHP's intersection types - * can't combine an interface with a concrete class. SelectFactory::create() already - * type-hints AdapterInterface (not Mysql) when building this object, so without - * this change ANY AdapterInterface implementation that doesn't extend Magento's own - * Mysql adapter fails here with a TypeError the moment it calls select(). + * Accepts any Zend DB adapter, not only Pdo\Mysql. * * @param \Zend_Db_Adapter_Abstract $adapter * @param Select\SelectRenderer $selectRenderer diff --git a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php index 97744b323f0..1b71c08c19a 100644 --- a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php +++ b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php @@ -736,15 +736,6 @@ protected function _renderOrders() if (!$this->_isOrdersRendered) { foreach ($this->_orders as $field => $direction) { if (isset($this->sqlReservedWords[strtoupper($field)])) { - // PgCompat: was a hardcoded "`$field`" backtick literal - correct by - // coincidence only on MySQL (whose quote character happens to be a - // backtick), and it bypasses the adapter's own quoteIdentifier() - // entirely, so Postgres (whose reserved-word set differs anyway - - // e.g. "position" isn't reserved there, but this same $field is - // wrapped unconditionally once it matches MySQL's list) received a - // literal, invalid backtick instead of a real identifier quote. - // quoteIdentifier() is exactly what every other quoting path in this - // class already goes through. $field = $this->getConnection()->quoteIdentifier($field); }