├── registration.php ├── DB ├── PostgresQuote.php ├── Adapter │ └── Pdo │ │ ├── Functions │ │ ├── Transaction.php │ │ ├── DDLCache.php │ │ └── Fixes.php │ │ ├── PostgresFactory.php │ │ └── Postgres.php ├── Model │ └── Sequence.php └── Postgres.php ├── etc ├── module.xml └── di.xml ├── composer.json ├── Compat └── AsynchronousOperations │ └── Model │ └── BulkStatus │ └── CalculatedStatusSql.php ├── misc ├── app_etc_di.patch ├── mysql_to_pgsql.sh └── convert_sequences.php ├── README.md └── LICENSE /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kirmorozov/mage-postgres-compatibility", 3 | "description": "Magento Compatibility with Postgres", 4 | "require": { 5 | "php": ">=7.4.0", 6 | "magento/framework": "*", 7 | "ext-pdo_pgsql": "*" 8 | }, 9 | "type": "magento2-module", 10 | "license": [ 11 | "OSL-3.0", 12 | "AFL-3.0" 13 | ], 14 | "autoload": { 15 | "files": [ 16 | "registration.php" 17 | ], 18 | "psr-4": { 19 | "Morozov\\PgCompat\\": "" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /Compat/AsynchronousOperations/Model/BulkStatus/CalculatedStatusSql.php: -------------------------------------------------------------------------------- 1 | _transactionLevel == 0) { 9 | parent::beginTransaction(); 10 | } 11 | $this->_transactionLevel++; 12 | return $this; 13 | } 14 | 15 | public function commit() 16 | { 17 | if ($this->_transactionLevel == 1) { 18 | parent::commit(); 19 | } 20 | $this->_transactionLevel--; 21 | return $this; 22 | } 23 | 24 | public function rollBack() 25 | { 26 | parent::rollBack(); 27 | if ($this->_transactionLevel == 0) { 28 | throw new \Exception("Symetric transaction"); 29 | } 30 | $this->_transactionLevel--; 31 | return $this; 32 | } 33 | 34 | 35 | /** 36 | * Get adapter transaction level state. Return 0 if all transactions are complete 37 | * 38 | * @return int 39 | */ 40 | public function getTransactionLevel() 41 | { 42 | return $this->_transactionLevel; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /DB/Adapter/Pdo/Functions/DDLCache.php: -------------------------------------------------------------------------------- 1 | _cacheAdapter = $cacheAdapter; 10 | return $this; 11 | } 12 | 13 | public function allowDdlCache() 14 | { 15 | throw new \RuntimeException('Not implemented ' . self::class . '::allowDdlCache()'); 16 | } 17 | 18 | public function disallowDdlCache() 19 | { 20 | throw new \RuntimeException('Not implemented ' . self::class . '::disallowDdlCache()'); 21 | } 22 | 23 | public function resetDdlCache($tableName = null, $schemaName = null) 24 | { 25 | throw new \RuntimeException('Not implemented ' . self::class . '::resetDdlCache()'); 26 | } 27 | 28 | public function saveDdlCache($tableCacheKey, $ddlType, $data) 29 | { 30 | // Todo: add implementation 31 | return $this; 32 | } 33 | 34 | public function loadDdlCache($tableCacheKey, $ddlType) 35 | { 36 | // Todo: add implementation 37 | return false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /misc/app_etc_di.patch: -------------------------------------------------------------------------------- 1 | Index: app/etc/di.xml 2 | IDEA additional info: 3 | Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP 4 | <+>UTF-8 5 | =================================================================== 6 | --- app/etc/di.xml (date 1611159395695) 7 | +++ app/etc/di.xml (date 1611159395695) 8 | @@ -108,7 +108,8 @@ 9 | 10 | 11 | 12 | - 13 | + 14 | + 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /misc/mysql_to_pgsql.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # shellcheck disable=SC2016 4 | echo "drop database m2_core_std; create database m2_core_std;" | mysql -h127.0.0.1 -u root -p123123qa 5 | mysqldump -h 127.0.0.1 -u root -p123123qa m2_core | \ 6 | sed 's/ unsigned / /g' | \ 7 | sed 's/ tinyint(1) / tinyint /g' | \ 8 | sed 's/KEY `EAV_ATTRIBUTE_FRONTEND_INPUT_ENTITY_TYPE_ID_IS_USER_DEFINED/KEY `EAV_ATTR_FRONTEND_INPUT_IS_USER_DEFINED/g' | \ 9 | sed 's/KEY `EAV_ATTRIBUTE_GROUP_ATTRIBUTE_/KEY `EAV_ATTR_GRP_ATTR_/g' | \ 10 | sed 's/KEY `CMS_PAGE_TITLE_META_KEYWORDS_META_DESCRIPTION_IDENTIFIER_CONTENT/KEY `CMS_PAGE_TITLE_META_DESCR_ID_CONTENT/g' | \ 11 | sed 's/KEY `CATALOG_CATEGORY_ENTITY_/KEY `CCE_/g' | \ 12 | sed 's/KEY `CATALOG_PRODUCT_ENTITY_/KEY `CPE_/g' | \ 13 | sed 's/KEY `CATALOG_PRODUCT_FRONTEND_ACTION_/KEY `CPFA_/g' | \ 14 | sed 's/KEY `CAT_CTGR_PRD_IDX_STORE/KEY `CCPIS/g' | \ 15 | sed 's/KEY `CAT_PRD_ENTT_MDA_GLR_VAL_/KEY `CPEMGV_/g' | \ 16 | sed 's/KEY `CUSTOMER_ADDRESS_ENTITY_/KEY `CAE_/g' | \ 17 | sed 's/KEY `CATALOGSEARCH_RECOMMENDATIONS_RELATION_/KEY `CSRR_/g' | \ 18 | mysql -h 127.0.0.1 -u root -p123123qa m2_core_std 19 | 20 | echo "alter table captcha_log modify type smallint default 0 not null comment 'Type';" | mysql -h127.0.0.1 -u root -p123123qa m2_core_std 21 | echo "alter table ui_bookmark alter column current set default 0;" | mysql -h127.0.0.1 -u root -p123123qa m2_core_std 22 | 23 | docker run --rm --name pgloader dimitri/pgloader:latest \ 24 | pgloader mysql://root:123123qa@172.17.0.2:3306/m2_core_std postgresql://m2_core:123123qa@172.17.0.5:5432/m2_core 25 | -------------------------------------------------------------------------------- /DB/Adapter/Pdo/PostgresFactory.php: -------------------------------------------------------------------------------- 1 | objectManager = $objectManager; 29 | } 30 | 31 | /** 32 | * Create instance of Postgres adapter 33 | * 34 | * @param string $className 35 | * @param array $config 36 | * @param LoggerInterface|null $logger 37 | * @param SelectFactory|null $selectFactory 38 | * @return Postgres 39 | * @throws \InvalidArgumentException 40 | */ 41 | public function create( 42 | $className, 43 | array $config, 44 | LoggerInterface $logger = null, 45 | SelectFactory $selectFactory = null 46 | ) { 47 | if (!in_array(Postgres::class, class_parents($className, true) + [$className => $className])) { 48 | throw new \InvalidArgumentException('Invalid class, ' . $className . ' must extend ' . Postgres::class . '.'); 49 | } 50 | $arguments = [ 51 | 'config' => $config 52 | ]; 53 | if ($logger) { 54 | $arguments['logger'] = $logger; 55 | } 56 | if ($selectFactory) { 57 | $arguments['selectFactory'] = $selectFactory; 58 | } 59 | return $this->objectManager->create( 60 | $className, 61 | $arguments 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /DB/Model/Sequence.php: -------------------------------------------------------------------------------- 1 | meta = $meta; 53 | $this->connection = $resource->getConnection('sales'); 54 | $this->pattern = $pattern; 55 | } 56 | 57 | /** 58 | * Retrieve current value 59 | * 60 | * @return string 61 | */ 62 | public function getCurrentValue() 63 | { 64 | if (!isset($this->lastIncrementId)) { 65 | return null; 66 | } 67 | 68 | return sprintf( 69 | $this->pattern, 70 | $this->meta->getActiveProfile()->getPrefix(), 71 | $this->calculateCurrentValue(), 72 | $this->meta->getActiveProfile()->getSuffix() 73 | ); 74 | } 75 | 76 | /** 77 | * Retrieve next value 78 | * 79 | * @return string 80 | */ 81 | public function getNextValue() 82 | { 83 | $this->lastIncrementId = $this->connection->fetchOne("select nextval('{$this->meta->getSequenceTable()}'::regclass)"); 84 | return $this->getCurrentValue(); 85 | } 86 | 87 | /** 88 | * Calculate current value depends on start value 89 | * 90 | * @return string 91 | */ 92 | private function calculateCurrentValue() 93 | { 94 | return ($this->lastIncrementId - $this->meta->getActiveProfile()->getStartValue()) 95 | * $this->meta->getActiveProfile()->getStep() + $this->meta->getActiveProfile()->getStartValue(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Postgres Compatibility for Magento 2 | ======== 3 | 4 | ### Motivation 5 | Back in 2010 Magento 1.4->1.6 was rewritten to have Zend_Select everywhere.\ 6 | It was done as a project for compatibility with SQL Server and Oracle.\ 7 | Postgres was not in scope at that time. Even after Magento 2 moved to Github, it never came in.\ 8 | Adapter implementation is not there because of its low level and complexity.\ 9 | So this module provides baseline for further development.\ 10 | 11 | There were updates to Magento 2 that made some part incompatible but legacy of 2010 is still there. 12 | 13 | ### Current state of implementation 14 | 15 | Current state is a `proof of concept` and checkout with simple products works.\ 16 | At this moment `bin/magento` works with some commands, \ 17 | like `store:list`,`store:website:list`,`config:show`.\ 18 | Login into admin works, homepage works, CMS Editing works.\ 19 | Visiting orders, invoices, shipments, settings.\ 20 | Catalog and Cart Rules, visited, but did not check in action. 21 | 22 | Basic CRUD suppose to work fine. 23 | 24 | ### Pain points 25 | [ ] DDL Cache (It used Redis) \ 26 | [x] Identities and autoincrements.(script in misc)\ 27 | [ ] Insert on duplicate.\ 28 | [x] Fancy catalog collection::getSize(); (`compatibility patch`) 29 | [x] EAV Unions. (`compatibility patch`) \ 30 | [ ] Indexers and Reports.\ 31 | [ ] Magento relies on custom implementation of Material Views for Mysql.\ 32 | [ ] Time Zone, database uses GMT.\ 33 | [ ] Functions that do not exist in Postgres. added `getFieldSql()` with `compatibility patch` for sort calls.\ 34 | [x] Replace sequence_% tables with real sequences. 35 | 36 | ### Make it work 37 | 38 | 0. Prepare working Magento with MySQL database. 39 | 1. Use script misc/mysql_to_pgsql.sh as an example for migration of your database. 40 | 2. Use script `php convert_sequences.php` to restore identities/auto-increments for primary keys. 41 | 3. Apply some patches to the core from [Postgres Compatibility Branch](https://github.com/kirmorozov/magento2/tree/2.4-postgres-compatibility) 42 | 4. Change app/etc/env.php 43 | 5. Enjoy. 44 | 6. Fix 'Not implemented Morozov\PgCompat\DB\Adapter\Pdo\Postgres...' exception. 45 | 7. Go to 5 :) 46 | 47 | 48 | ### License 49 | Copyright 2021 Kirill Morozov 50 | 51 | Licensed under the Apache License, Version 2.0 (the "License"); 52 | you may not use this file except in compliance with the License. 53 | You may obtain a copy of the License at 54 | 55 | http://www.apache.org/licenses/LICENSE-2.0 56 | 57 | Unless required by applicable law or agreed to in writing, software 58 | distributed under the License is distributed on an "AS IS" BASIS, 59 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 60 | See the License for the specific language governing permissions and 61 | limitations under the License. 62 | -------------------------------------------------------------------------------- /misc/convert_sequences.php: -------------------------------------------------------------------------------- 1 | query($getIdentitiesSql); 30 | $lines = $res->fetchAll(\PDO::FETCH_ASSOC); 31 | 32 | //$matches = []; 33 | 34 | $seqs = []; 35 | foreach ($lines as $line) { 36 | 37 | preg_match('#\.([^ ]*).*\(([^\)]*id)\)#', $line['indexdef'], $matches); 38 | 39 | if (isset($matches[1])) { 40 | $seqs [] = ['table' => $matches[1], 'col' => $matches[2], 'schema' => $line['schemaname']]; 41 | } 42 | } 43 | $seqs [] = ['table' => 'shipping_tablerate', 'col' => 'pk', 'schema' => $seqs[0]['schema']]; 44 | $seqs [] = ['table' => 'quote_id_mask', 'col' => 'entity_id', 'schema' => $seqs[0]['schema']]; 45 | 46 | foreach ($seqs as $seq) { 47 | $maxValSQL = "select max({$seq['col']}) as mx, count(1) as cnt from {$seq['table']} "; 48 | $r1 = $pdo->query($maxValSQL); 49 | $r = $r1->fetchAll(\PDO::FETCH_ASSOC); 50 | $mx = 0; 51 | $cnt = 0; 52 | if (count($r)) { 53 | $mx = $r[0]['mx']; 54 | $cnt = $r[0]['cnt']; 55 | } 56 | $createSql = "create sequence {$seq['table']}_seq "; 57 | if ($mx) { 58 | $mx++; 59 | $createSql .= " start with {$mx}"; 60 | } 61 | 62 | $r1 = $pdo->query($createSql); 63 | $r1 = $pdo->query("alter table {$seq['table']} alter column {$seq['col']} 64 | set default nextval('{$seq['schema']}.{$seq['table']}_seq')"); 65 | $r1 = $pdo->query("alter sequence {$seq['table']}_seq owned by {$seq['table']}.{$seq['col']}"); 66 | } 67 | 68 | 69 | $res = $pdo->query("select * from pg_catalog.pg_tables where tablename like 'sequence_%';"); 70 | 71 | foreach ($res->fetchAll(\PDO::FETCH_ASSOC) as $seq) { 72 | $maxValSQL = "select max(sequence_value) as mx from {$seq['tablename']};"; 73 | $r1 = $pdo->query($maxValSQL); 74 | $r = $r1->fetchAll(\PDO::FETCH_ASSOC); 75 | $mx = 0; 76 | if (count($r)) { 77 | $mx = $r[0]['mx']; 78 | } 79 | 80 | $createSql = "create sequence {$seq['tablename']} "; 81 | if ($mx) { 82 | $mx++; 83 | $createSql .= " start with {$mx}"; 84 | } 85 | $r0 = $pdo->query("DROP TABLE {$seq['schemaname']}.{$seq['tablename']};"); 86 | $r1 = $pdo->query($createSql); 87 | 88 | } 89 | 90 | -------------------------------------------------------------------------------- /DB/Postgres.php: -------------------------------------------------------------------------------- 1 | connectionConfig = $this->getValidConfig($config); 38 | $this->PostgresFactory = $PostgresFactory ?: ObjectManager::getInstance()->get(PostgresFactory::class); 39 | parent::__construct(); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function getConnection(DB\LoggerInterface $logger = null, SelectFactory $selectFactory = null) 46 | { 47 | $connection = $this->getDbConnectionInstance($logger, $selectFactory); 48 | 49 | $profiler = $connection->getProfiler(); 50 | if ($profiler instanceof DB\Profiler) { 51 | $profiler->setType($this->connectionConfig['type']); 52 | $profiler->setHost($this->connectionConfig['host']); 53 | } 54 | 55 | return $connection; 56 | } 57 | 58 | /** 59 | * Create and return database connection object instance 60 | * 61 | * @param DB\LoggerInterface|null $logger 62 | * @param SelectFactory|null $selectFactory 63 | * @return \Magento\Framework\DB\Adapter\Pdo\Mysql 64 | */ 65 | protected function getDbConnectionInstance(DB\LoggerInterface $logger = null, SelectFactory $selectFactory = null) 66 | { 67 | return $this->PostgresFactory->create( 68 | $this->getDbConnectionClassName(), 69 | $this->connectionConfig, 70 | $logger, 71 | $selectFactory 72 | ); 73 | } 74 | 75 | /** 76 | * Retrieve DB connection class name 77 | * 78 | * @return string 79 | */ 80 | protected function getDbConnectionClassName() 81 | { 82 | return Adapter\Pdo\Postgres::class; 83 | } 84 | 85 | /** 86 | * Validates the config and adds default options, if any is missing 87 | * 88 | * @param array $config 89 | * @return array 90 | */ 91 | private function getValidConfig(array $config) 92 | { 93 | $default = ['initStatements' => 'SET NAMES utf8', 'type' => 'pdo_postgres', 'active' => false]; 94 | foreach ($default as $key => $value) { 95 | if (!isset($config[$key])) { 96 | $config[$key] = $value; 97 | } 98 | } 99 | $required = ['host']; 100 | foreach ($required as $name) { 101 | if (!isset($config[$name])) { 102 | throw new \InvalidArgumentException("Postgres adapter: Missing required configuration option '$name'"); 103 | } 104 | } 105 | 106 | if (isset($config['port'])) { 107 | throw new \InvalidArgumentException( 108 | "Port must be configured within host (like '$config[host]:$config[port]') parameter, not within port" 109 | ); 110 | } 111 | 112 | $config['active'] = !( 113 | $config['active'] === 'false' 114 | || $config['active'] === false 115 | || $config['active'] === '0' 116 | ); 117 | 118 | return $config; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /DB/Adapter/Pdo/Functions/Fixes.php: -------------------------------------------------------------------------------- 1 | string; name of database or schema 18 | * TABLE_NAME => string; 19 | * COLUMN_NAME => string; column name 20 | * COLUMN_POSITION => number; ordinal position of column in table 21 | * DATA_TYPE => string; SQL datatype name of column 22 | * DEFAULT => string; default expression of column, null if none 23 | * NULLABLE => boolean; true if column can have nulls 24 | * LENGTH => number; length of CHAR/VARCHAR 25 | * SCALE => number; scale of NUMERIC/DECIMAL 26 | * PRECISION => number; precision of NUMERIC/DECIMAL 27 | * UNSIGNED => boolean; unsigned property of an integer type 28 | * PRIMARY => boolean; true if column is part of the primary key 29 | * PRIMARY_POSITION => integer; position of column in primary key 30 | * IDENTITY => integer; true if column is auto-generated with unique values 31 | * 32 | * @todo Discover integer unsigned property. 33 | * 34 | * @param string $tableName 35 | * @param string $schemaName OPTIONAL 36 | * @return array 37 | */ 38 | public function describeTable($tableName, $schemaName = null) 39 | { 40 | $sql = "SELECT 41 | a.attnum, 42 | n.nspname, 43 | c.relname, 44 | a.attname AS colname, 45 | t.typname AS type, 46 | a.atttypmod, 47 | FORMAT_TYPE(a.atttypid, a.atttypmod) AS complete_type, 48 | pg_get_expr(d.adbin, d.adrelid) AS default_value, 49 | a.attnotnull AS notnull, 50 | a.attlen AS length, 51 | co.contype, 52 | ARRAY_TO_STRING(co.conkey, ',') AS conkey 53 | FROM pg_attribute AS a 54 | JOIN pg_class AS c ON a.attrelid = c.oid 55 | JOIN pg_namespace AS n ON c.relnamespace = n.oid 56 | JOIN pg_type AS t ON a.atttypid = t.oid 57 | LEFT OUTER JOIN pg_constraint AS co ON (co.conrelid = c.oid 58 | AND a.attnum = ANY(co.conkey) AND co.contype = 'p') 59 | LEFT OUTER JOIN pg_attrdef AS d ON d.adrelid = c.oid AND d.adnum = a.attnum 60 | WHERE a.attnum > 0 AND c.relname = ".$this->quote($tableName); 61 | if ($schemaName) { 62 | $sql .= " AND n.nspname = ".$this->quote($schemaName); 63 | } 64 | $sql .= ' ORDER BY a.attnum'; 65 | 66 | $stmt = $this->query($sql); 67 | 68 | // Use FETCH_NUM so we are not dependent on the CASE attribute of the PDO connection 69 | $result = $stmt->fetchAll(\Zend_Db::FETCH_NUM); 70 | 71 | $attnum = 0; 72 | $nspname = 1; 73 | $relname = 2; 74 | $colname = 3; 75 | $type = 4; 76 | $atttypemod = 5; 77 | $complete_type = 6; 78 | $default_value = 7; 79 | $notnull = 8; 80 | $length = 9; 81 | $contype = 10; 82 | $conkey = 11; 83 | 84 | $desc = array(); 85 | foreach ($result as $key => $row) { 86 | $defaultValue = $row[$default_value]; 87 | if ($row[$type] == 'varchar' || $row[$type] == 'bpchar' ) { 88 | if (preg_match('/character(?: varying)?(?:\((\d+)\))?/', $row[$complete_type], $matches)) { 89 | if (isset($matches[1])) { 90 | $row[$length] = $matches[1]; 91 | } else { 92 | $row[$length] = null; // unlimited 93 | } 94 | } 95 | if (preg_match("/^'(.*?)'::(?:character varying|bpchar)$/", $defaultValue, $matches)) { 96 | $defaultValue = $matches[1]; 97 | } 98 | } 99 | list($primary, $primaryPosition, $identity) = array(false, null, false); 100 | if ($row[$contype] == 'p') { 101 | $primary = true; 102 | $primaryPosition = array_search($row[$attnum], explode(',', $row[$conkey])) + 1; 103 | $identity = (bool) (preg_match('/^nextval/', $row[$default_value])); 104 | } 105 | 106 | if ($row[$type] == 'int2' || $row[$type] == 'int4') { 107 | $row[$type] = 'smallint'; 108 | } 109 | 110 | $desc[$this->foldCase($row[$colname])] = array( 111 | 'SCHEMA_NAME' => $this->foldCase($row[$nspname]), 112 | 'TABLE_NAME' => $this->foldCase($row[$relname]), 113 | 'COLUMN_NAME' => $this->foldCase($row[$colname]), 114 | 'COLUMN_POSITION' => $row[$attnum], 115 | 'DATA_TYPE' => $row[$type], 116 | 'DEFAULT' => $defaultValue, 117 | 'NULLABLE' => (bool) ($row[$notnull] != 't'), 118 | 'LENGTH' => $row[$length], 119 | 'SCALE' => null, // @todo 120 | 'PRECISION' => null, // @todo 121 | 'UNSIGNED' => null, // @todo 122 | 'PRIMARY' => $primary, 123 | 'PRIMARY_POSITION' => $primaryPosition, 124 | 'IDENTITY' => $identity 125 | ); 126 | } 127 | return $desc; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2025 Kirill Morozov 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /DB/Adapter/Pdo/Postgres.php: -------------------------------------------------------------------------------- 1 | string = $string; 70 | $this->dateTime = $dateTime; 71 | $this->logger = $logger; 72 | $this->selectFactory = $selectFactory; 73 | $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); 74 | $this->exceptionMap = [ 75 | // SQLSTATE[HY000]: General error: 2006 MySQL server has gone away 76 | 2006 => ConnectionException::class, 77 | // SQLSTATE[HY000]: General error: 2013 Lost connection to MySQL server during query 78 | 2013 => ConnectionException::class, 79 | // SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded 80 | 1205 => LockWaitException::class, 81 | // SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock 82 | 1213 => DeadlockException::class, 83 | // SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 84 | 1062 => DuplicateException::class, 85 | // SQLSTATE[42S02]: Base table or view not found: 1146 86 | 1146 => TableNotFoundException::class, 87 | ]; 88 | try { 89 | parent::__construct($config); 90 | } catch (Zend_Db_Adapter_Exception $e) { 91 | throw new \InvalidArgumentException($e->getMessage(), $e->getCode(), $e); 92 | } 93 | } 94 | 95 | protected function _connect() 96 | { 97 | unset($this->_config['model'], $this->_config['engine'], $this->_config['active'], $this->_config['type']); 98 | $initStatements = ""; 99 | if (isset($this->_config['initStatements'])) { 100 | $initStatements = $this->_config['initStatements']; 101 | unset($this->_config['initStatements']); 102 | } 103 | parent::_connect(); 104 | 105 | if (!empty($initStatements)) { 106 | $this->query($initStatements); 107 | } 108 | } 109 | 110 | public function select() 111 | { 112 | return $this->selectFactory->create($this); 113 | } 114 | 115 | /** 116 | * Quotes a value and places into a piece of text at a placeholder. 117 | * 118 | * Method revrited for handle empty arrays in value param 119 | * 120 | * @param string $text The text with a placeholder. 121 | * @param array|null|int|string|float|Expression|Select|\DateTimeInterface $value The value to quote. 122 | * @param int|string|null $type OPTIONAL SQL datatype of the given value e.g. Zend_Db::FLOAT_TYPE or "INT" 123 | * @param integer $count OPTIONAL count of placeholders to replace 124 | * @return string An SQL-safe quoted value placed into the original text. 125 | */ 126 | public function quoteInto($text, $value, $type = null, $count = null) 127 | { 128 | if (is_array($value) && empty($value)) { 129 | $value = new \Zend_Db_Expr('NULL'); 130 | } 131 | 132 | if ($value instanceof \DateTimeInterface) { 133 | $value = $value->format('Y-m-d H:i:s'); 134 | } 135 | 136 | return parent::quoteInto($text, $value, $type, $count); 137 | } 138 | 139 | public function newTable($tableName = null, $schemaName = null) 140 | { 141 | throw new \RuntimeException('Not implemented ' . self::class . '::newTable()'); 142 | } 143 | 144 | public function createTable(Table $table) 145 | { 146 | throw new \RuntimeException('Not implemented ' . self::class . '::createTable()'); 147 | } 148 | 149 | public function dropTable($tableName, $schemaName = null) 150 | { 151 | throw new \RuntimeException('Not implemented ' . self::class . '::dropTable()'); 152 | } 153 | 154 | public function createTemporaryTable(Table $table) 155 | { 156 | throw new \RuntimeException('Not implemented ' . self::class . '::createTemporaryTable()'); 157 | } 158 | 159 | public function createTemporaryTableLike($temporaryTableName, $originTableName, $ifNotExists = false) 160 | { 161 | throw new \RuntimeException('Not implemented ' . self::class . '::createTemporaryTableLike()'); 162 | } 163 | 164 | public function dropTemporaryTable($tableName, $schemaName = null) 165 | { 166 | throw new \RuntimeException('Not implemented ' . self::class . '::dropTemporaryTable()'); 167 | } 168 | 169 | public function renameTablesBatch(array $tablePairs) 170 | { 171 | throw new \RuntimeException('Not implemented ' . self::class . '::renameTablesBatch()'); 172 | } 173 | 174 | public function truncateTable($tableName, $schemaName = null) 175 | { 176 | throw new \RuntimeException('Not implemented ' . self::class . '::truncateTable()'); 177 | } 178 | 179 | public function isTableExists($tableName, $schemaName = null) 180 | { 181 | return count($this->query("SELECT 1 FROM pg_tables WHERE tablename = ? ", [$tableName])->fetchAll()); 182 | } 183 | 184 | public function showTableStatus($tableName, $schemaName = null) 185 | { 186 | throw new \RuntimeException('Not implemented ' . self::class . '::showTableStatus()'); 187 | } 188 | 189 | public function createTableByDdl($tableName, $newTableName) 190 | { 191 | throw new \RuntimeException('Not implemented ' . self::class . '::createTableByDdl()'); 192 | } 193 | 194 | public function modifyColumnByDdl($tableName, $columnName, $definition, $flushData = false, $schemaName = null) 195 | { 196 | throw new \RuntimeException('Not implemented ' . self::class . '::modifyColumnByDdl()'); 197 | } 198 | 199 | public function renameTable($oldTableName, $newTableName, $schemaName = null) 200 | { 201 | throw new \RuntimeException('Not implemented ' . self::class . '::renameTable()'); 202 | } 203 | 204 | public function addColumn($tableName, $columnName, $definition, $schemaName = null) 205 | { 206 | throw new \RuntimeException('Not implemented ' . self::class . '::addColumn()'); 207 | } 208 | 209 | public function changeColumn($tableName, $oldColumnName, $newColumnName, $definition, $flushData = false, $schemaName = null) 210 | { 211 | throw new \RuntimeException('Not implemented ' . self::class . '::changeColumn()'); 212 | } 213 | 214 | public function modifyColumn($tableName, $columnName, $definition, $flushData = false, $schemaName = null) 215 | { 216 | throw new \RuntimeException('Not implemented ' . self::class . '::modifyColumn()'); 217 | } 218 | 219 | public function dropColumn($tableName, $columnName, $schemaName = null) 220 | { 221 | throw new \RuntimeException('Not implemented ' . self::class . '::dropColumn()'); 222 | } 223 | 224 | public function tableColumnExists($tableName, $columnName, $schemaName = null) 225 | { 226 | throw new \RuntimeException('Not implemented ' . self::class . '::tableColumnExists()'); 227 | } 228 | 229 | public function addIndex($tableName, $indexName, $fields, $indexType = self::INDEX_TYPE_INDEX, $schemaName = null) 230 | { 231 | throw new \RuntimeException('Not implemented ' . self::class . '::addIndex()'); 232 | } 233 | 234 | public function dropIndex($tableName, $keyName, $schemaName = null) 235 | { 236 | throw new \RuntimeException('Not implemented ' . self::class . '::dropIndex()'); 237 | } 238 | 239 | public function getIndexList($tableName, $schemaName = null) 240 | { 241 | $cacheKey = $tableName; 242 | $ddl = $this->loadDdlCache($cacheKey, self::DDL_INDEX); 243 | if ($ddl === false) { 244 | $ddl = []; 245 | 246 | $sql = "select 247 | i.relname as key_name, 248 | case 249 | when ix.indisprimary = true then 'primary' 250 | when ix.indisunique = true then 'unique' 251 | else 'index' 252 | end as index_type, 253 | a.attname as column_name 254 | from 255 | pg_class t, 256 | pg_class i, 257 | pg_index ix, 258 | pg_attribute a 259 | where 260 | t.oid = ix.indrelid 261 | and i.oid = ix.indexrelid 262 | and a.attrelid = t.oid 263 | and a.attnum = ANY(ix.indkey) 264 | and t.relkind = 'r' 265 | and t.relname like ? 266 | order by 267 | t.relname, 268 | i.relname; 269 | "; 270 | foreach ($this->fetchAll($sql, [$tableName]) as $row) { 271 | $fieldKeyName = 'key_name'; 272 | $fieldColumn = 'column_name'; 273 | $fieldIndexType = 'index_type'; 274 | 275 | if ($row[$fieldIndexType] == AdapterInterface::INDEX_TYPE_PRIMARY) { 276 | $indexType = AdapterInterface::INDEX_TYPE_PRIMARY; 277 | } elseif ($row[$fieldIndexType] == AdapterInterface::INDEX_TYPE_UNIQUE) { 278 | $indexType = AdapterInterface::INDEX_TYPE_UNIQUE; 279 | } elseif ($row[$fieldIndexType] == AdapterInterface::INDEX_TYPE_FULLTEXT) { 280 | // TODO: Add FULLTEXT search 281 | $indexType = AdapterInterface::INDEX_TYPE_FULLTEXT; 282 | } else { 283 | $indexType = AdapterInterface::INDEX_TYPE_INDEX; 284 | } 285 | 286 | $upperKeyName = strtolower($row[$fieldKeyName]); 287 | if (isset($ddl[$upperKeyName])) { 288 | $ddl[$upperKeyName]['fields'][] = $row[$fieldColumn]; // for compatible 289 | $ddl[$upperKeyName]['COLUMNS_LIST'][] = $row[$fieldColumn]; 290 | } else { 291 | $ddl[$upperKeyName] = [ 292 | 'SCHEMA_NAME' => $schemaName, 293 | 'TABLE_NAME' => $tableName, 294 | 'KEY_NAME' => $row[$fieldKeyName], 295 | 'COLUMNS_LIST' => [$row[$fieldColumn]], 296 | 'INDEX_TYPE' => $indexType, 297 | 'INDEX_METHOD' => $row[$fieldIndexType], 298 | 'type' => strtolower($indexType), // for compatibility 299 | 'fields' => [$row[$fieldColumn]], // for compatibility 300 | ]; 301 | } 302 | } 303 | $this->saveDdlCache($cacheKey, self::DDL_INDEX, $ddl); 304 | } 305 | 306 | return $ddl; 307 | } 308 | 309 | public function addForeignKey($fkName, $tableName, $columnName, $refTableName, $refColumnName, $onDelete = self::FK_ACTION_CASCADE, $purge = false, $schemaName = null, $refSchemaName = null) 310 | { 311 | throw new \RuntimeException('Not implemented ' . self::class . '::addForeignKey()'); 312 | } 313 | 314 | public function dropForeignKey($tableName, $fkName, $schemaName = null) 315 | { 316 | throw new \RuntimeException('Not implemented ' . self::class . '::dropForeignKey()'); 317 | } 318 | 319 | public function getForeignKeys($tableName, $schemaName = null) 320 | { 321 | throw new \RuntimeException('Not implemented ' . self::class . '::getForeignKeys()'); 322 | } 323 | 324 | public function insertOnDuplicate($table, array $data, array $fields = []) 325 | { 326 | $indexes = array_filter($this->getIndexList($table), function ($idx) { 327 | return in_array($idx['INDEX_TYPE'], [AdapterInterface::INDEX_TYPE_PRIMARY, AdapterInterface::INDEX_TYPE_UNIQUE]); 328 | }); 329 | $cols = []; 330 | $vals = ""; 331 | if (is_array(reset($data))) { 332 | $cols = array_keys($data[0]); 333 | $vals = implode( 334 | '), (', 335 | array_map(function ($row) { 336 | return implode(', ', array_map(function ($val) { 337 | return $this->quote($val); 338 | }, $row)); 339 | }, $data) 340 | ); 341 | } else { 342 | $cols = array_keys($data); 343 | $vals = implode(', ', array_map(function ($val) { 344 | return $this->quote($val); 345 | }, $data)); 346 | } 347 | 348 | if (empty($fields)) { 349 | $fields = $cols; 350 | } 351 | 352 | $sql = "INSERT INTO " 353 | . $this->quoteIdentifier($table, true) . ' as _tgt' 354 | . ' (' . implode(', ', $cols) . ') ' 355 | . ' VALUES (' . $vals . ')'; 356 | 357 | foreach ($indexes as $index) { 358 | $condition = array_map(function ($col) { 359 | return $this->quoteIdentifier($col); 360 | }, $index['COLUMNS_LIST']); 361 | $sql .= "\n ON CONFLICT (" . implode(', ', $condition) . ") "; 362 | $updateExprs = []; 363 | foreach ($fields as $k => $v) { 364 | if (!is_numeric($k)) { 365 | $field = $this->quoteIdentifier($k); 366 | if ($v instanceof \Zend_Db_Expr) { 367 | $value = '_tgt.' . $v->__toString(); 368 | } elseif ($v instanceof \Laminas\Db\Sql\Expression) { 369 | $value = '_tgt.' . $v->getExpression(); 370 | } elseif (is_string($v)) { 371 | $value = 'excluded.' . $this->quoteIdentifier($v); 372 | } elseif (is_numeric($v)) { 373 | $value = $this->quoteInto('?', $v); 374 | } 375 | } elseif (is_string($v)) { 376 | $value = "excluded.{$this->quoteIdentifier($v)}"; 377 | $field = $this->quoteIdentifier($v); 378 | } 379 | if ($field && is_string($value) && $value !== '') { 380 | $updateExprs[] = "$field = $value"; 381 | } 382 | } 383 | $sql .= "DO UPDATE SET " . implode(', ', $updateExprs); 384 | } 385 | 386 | $res = $this->query($sql); 387 | return $res->rowCount(); 388 | } 389 | 390 | /** 391 | * Inserts a table multiply rows with specified data. 392 | * 393 | * @param string|array|\Zend_Db_Expr $table The table to insert data into. 394 | * @param array $data Column-value pairs or array of Column-value pairs. 395 | * @return int The number of affected rows. 396 | * @throws \Zend_Db_Exception 397 | */ 398 | public function insertMultiple($table, array $data) 399 | { 400 | $row = reset($data); 401 | // support insert syntaxes 402 | if (!is_array($row)) { 403 | return $this->insert($table, $data); 404 | } 405 | 406 | $res = 0; 407 | foreach ($data as $row) { 408 | $res +=$this->insert($table, $row); 409 | } 410 | return $res; 411 | } 412 | 413 | public function insertArray($table, array $columns, array $data) 414 | { 415 | $res = 0; 416 | foreach ($data as $row) { 417 | $res += $this->insert($table, array_combine($columns, $row)); 418 | } 419 | return $res; 420 | } 421 | 422 | public function insertForce($table, array $bind) 423 | { 424 | throw new \RuntimeException('Not implemented ' . self::class . '::insertForce()'); 425 | } 426 | 427 | /** 428 | * Format Date to internal database date format 429 | * 430 | * @param int|string|\DateTimeInterface $date 431 | * @param bool $includeTime 432 | * @return \Zend_Db_Expr 433 | */ 434 | public function formatDate($date, $includeTime = true) 435 | { 436 | $date = $this->dateTime->formatDate($date, $includeTime); 437 | 438 | if ($date === null) { 439 | return new \Zend_Db_Expr('NULL'); 440 | } 441 | 442 | return new \Zend_Db_Expr($this->quote($date)); 443 | } 444 | 445 | public function startSetup() 446 | { 447 | throw new \RuntimeException('Not implemented ' . self::class . '::startSetup()'); 448 | } 449 | 450 | public function endSetup() 451 | { 452 | throw new \RuntimeException('Not implemented ' . self::class . '::endSetup()'); 453 | } 454 | 455 | use Functions\DDLCache; 456 | 457 | /** 458 | * Build SQL statement for condition 459 | * 460 | * If $condition integer or string - exact value will be filtered ('eq' condition) 461 | * 462 | * If $condition is array is - one of the following structures is expected: 463 | * - array("from" => $fromValue, "to" => $toValue) 464 | * - array("eq" => $equalValue) 465 | * - array("neq" => $notEqualValue) 466 | * - array("like" => $likeValue) 467 | * - array("in" => array($inValues)) 468 | * - array("nin" => array($notInValues)) 469 | * - array("notnull" => $valueIsNotNull) 470 | * - array("null" => $valueIsNull) 471 | * - array("gt" => $greaterValue) 472 | * - array("lt" => $lessValue) 473 | * - array("gteq" => $greaterOrEqualValue) 474 | * - array("lteq" => $lessOrEqualValue) 475 | * - array("finset" => $valueInSet) 476 | * - array("nfinset" => $valueNotInSet) 477 | * - array("regexp" => $regularExpression) 478 | * - array("seq" => $stringValue) 479 | * - array("sneq" => $stringValue) 480 | * 481 | * If non matched - sequential array is expected and OR conditions 482 | * will be built using above mentioned structure 483 | * 484 | * @param string $fieldName 485 | * @param integer|string|array $condition 486 | * @return string 487 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 488 | */ 489 | public function prepareSqlCondition($fieldName, $condition) 490 | { 491 | $conditionKeyMap = [ 492 | 'eq' => "{{fieldName}} = ?", 493 | 'neq' => "{{fieldName}} != ?", 494 | 'like' => "{{fieldName}} LIKE ?", 495 | 'nlike' => "{{fieldName}} NOT LIKE ?", 496 | 'in' => "{{fieldName}} IN(?)", 497 | 'nin' => "{{fieldName}} NOT IN(?)", 498 | 'is' => "{{fieldName}} IS ?", 499 | 'notnull' => "{{fieldName}} IS NOT NULL", 500 | 'null' => "{{fieldName}} IS NULL", 501 | 'gt' => "{{fieldName}} > ?", 502 | 'lt' => "{{fieldName}} < ?", 503 | 'gteq' => "{{fieldName}} >= ?", 504 | 'lteq' => "{{fieldName}} <= ?", 505 | 'finset' => "? = ANY (string_to_array({{fieldName}},','))", 506 | 'nfinset' => "NOT( ? = ANY (string_to_array({{fieldName}},',')))", 507 | 'regexp' => "{{fieldName}} REGEXP ?", // TODO: implement REGEXP search 508 | 'from' => "{{fieldName}} >= ?", 509 | 'to' => "{{fieldName}} <= ?", 510 | 'seq' => null, 511 | 'sneq' => null, 512 | 'ntoa' => "INET_NTOA({{fieldName}}) LIKE ?", // TODO: implement INET_NTOA 513 | ]; 514 | 515 | $query = ''; 516 | if (is_array($condition)) { 517 | $key = key(array_intersect_key($condition, $conditionKeyMap)); 518 | 519 | if (isset($condition['from']) || isset($condition['to'])) { 520 | if (isset($condition['from'])) { 521 | $from = $this->_prepareSqlDateCondition($condition, 'from'); 522 | $query = $this->_prepareQuotedSqlCondition($conditionKeyMap['from'], $from, $fieldName); 523 | } 524 | 525 | if (isset($condition['to'])) { 526 | $query .= empty($query) ? '' : ' AND '; 527 | $to = $this->_prepareSqlDateCondition($condition, 'to'); 528 | $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); 529 | } 530 | } elseif (array_key_exists($key, $conditionKeyMap)) { 531 | $value = $condition[$key]; 532 | if (($key == 'seq') || ($key == 'sneq')) { 533 | $key = $this->_transformStringSqlCondition($key, $value); 534 | } 535 | if (($key == 'in' || $key == 'nin') && is_string($value)) { 536 | $value = explode(',', $value); 537 | } 538 | $query = $this->_prepareQuotedSqlCondition($conditionKeyMap[$key], $value, $fieldName); 539 | } else { 540 | $queries = []; 541 | foreach ($condition as $orCondition) { 542 | $queries[] = sprintf('(%s)', $this->prepareSqlCondition($fieldName, $orCondition)); 543 | } 544 | 545 | $query = sprintf('(%s)', implode(' OR ', $queries)); 546 | } 547 | } else { 548 | $query = $this->_prepareQuotedSqlCondition($conditionKeyMap['eq'], (string)$condition, $fieldName); 549 | } 550 | 551 | return $query; 552 | } 553 | 554 | /** 555 | * Prepare Sql condition 556 | * 557 | * @param string $text Condition value 558 | * @param mixed $value 559 | * @param string $fieldName 560 | * @return string 561 | */ 562 | protected function _prepareQuotedSqlCondition($text, $value, $fieldName) 563 | { 564 | $sql = $this->quoteInto($text, $value); 565 | $sql = str_replace('{{fieldName}}', $fieldName, $sql); 566 | return $sql; 567 | } 568 | 569 | /** 570 | * Prepare sql date condition 571 | * 572 | * @param array $condition 573 | * @param string $key 574 | * @return string 575 | */ 576 | protected function _prepareSqlDateCondition($condition, $key) 577 | { 578 | if (empty($condition['date'])) { 579 | if (empty($condition['datetime'])) { 580 | $result = $condition[$key]; 581 | } else { 582 | $result = $this->formatDate($condition[$key]); 583 | } 584 | } else { 585 | $result = $this->formatDate($condition[$key]); 586 | } 587 | 588 | return $result; 589 | } 590 | 591 | /** 592 | * Transforms sql condition key 'seq' / 'sneq' that is used for comparing string values to its analog: 593 | * - 'null' / 'notnull' for empty strings 594 | * - 'eq' / 'neq' for non-empty strings 595 | * 596 | * @param string $conditionKey 597 | * @param mixed $value 598 | * @return string 599 | */ 600 | protected function _transformStringSqlCondition($conditionKey, $value) 601 | { 602 | $value = (string)$value; 603 | if ($value == '') { 604 | return ($conditionKey == 'seq') ? 'null' : 'notnull'; 605 | } else { 606 | return ($conditionKey == 'seq') ? 'eq' : 'neq'; 607 | } 608 | } 609 | 610 | /** 611 | * Prepare value for save in column 612 | * 613 | * Return converted to column data type value 614 | * 615 | * @param array $column the column describe array 616 | * @param mixed $value 617 | * @return mixed 618 | * 619 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 620 | * @SuppressWarnings(PHPMD.NPathComplexity) 621 | */ 622 | public function prepareColumnValue(array $column, $value) 623 | { 624 | if ($value instanceof \Zend_Db_Expr) { 625 | return $value; 626 | } 627 | if ($value instanceof Parameter) { 628 | return $value; 629 | } 630 | 631 | // return original value if invalid column describe data 632 | if (!isset($column['DATA_TYPE'])) { 633 | return $value; 634 | } 635 | 636 | // return null 637 | if ($value === null && $column['NULLABLE']) { 638 | return null; 639 | } 640 | 641 | switch ($column['DATA_TYPE']) { 642 | case 'smallint': 643 | case 'int': 644 | $value = (int)$value; 645 | break; 646 | case 'bigint': 647 | if (!is_integer($value)) { 648 | $value = sprintf('%.0f', (float)$value); 649 | } 650 | break; 651 | 652 | case 'decimal': 653 | $precision = 10; 654 | $scale = 0; 655 | if (isset($column['SCALE'])) { 656 | $scale = $column['SCALE']; 657 | } 658 | if (isset($column['PRECISION'])) { 659 | $precision = $column['PRECISION']; 660 | } 661 | $format = sprintf('%%%d.%dF', $precision - $scale, $scale); 662 | $value = (float)sprintf($format, $value); 663 | break; 664 | 665 | case 'float': 666 | $value = (float)sprintf('%F', $value); 667 | break; 668 | 669 | case 'date': 670 | $value = $this->formatDate($value, false); 671 | break; 672 | case 'datetime': 673 | case 'timestamp': 674 | $value = $this->formatDate($value); 675 | break; 676 | 677 | case 'varchar': 678 | case 'mediumtext': 679 | case 'text': 680 | case 'longtext': 681 | $value = (string)$value; 682 | if ($column['NULLABLE'] && $value == '') { 683 | $value = null; 684 | } 685 | break; 686 | 687 | case 'varbinary': 688 | case 'mediumblob': 689 | case 'blob': 690 | case 'longblob': 691 | // No special processing for MySQL is needed 692 | break; 693 | } 694 | 695 | return $value; 696 | } 697 | 698 | /** 699 | * Generate fragment of SQL, that check condition and return true or false value 700 | * 701 | * @param \Zend_Db_Expr|\Magento\Framework\DB\Select|string $expression 702 | * @param string $true true value 703 | * @param string $false false value 704 | * @return \Zend_Db_Expr 705 | */ 706 | public function getCheckSql($expression, $true, $false) 707 | { 708 | if ($expression instanceof \Zend_Db_Expr || $expression instanceof \Zend_Db_Select) { 709 | $expression = sprintf("CASE WHEN (%s) THEN %s ELSE %s END", $expression, $true, $false); 710 | } else { 711 | $expression = sprintf("CASE WHEN %s THEN %s ELSE %s END", $expression, $true, $false); 712 | } 713 | 714 | return new \Zend_Db_Expr($expression); 715 | } 716 | 717 | /** 718 | * Returns valid IFNULL expression 719 | * 720 | * @param \Zend_Db_Expr|\Magento\Framework\DB\Select|string $expression 721 | * @param string|int $value OPTIONAL. Applies when $expression is NULL 722 | * @return \Zend_Db_Expr 723 | */ 724 | public function getIfNullSql($expression, $value = 0) 725 | { 726 | if ($expression instanceof \Zend_Db_Expr || $expression instanceof \Zend_Db_Select) { 727 | $expression = sprintf("COALESCE((%s), %s)", $expression, $value); 728 | } else { 729 | $expression = sprintf("COALESCE(%s, %s)", $expression, $value); 730 | } 731 | 732 | return new \Zend_Db_Expr($expression); 733 | } 734 | 735 | /** 736 | * Generate fragment of SQL, that combine together (concatenate) the results from data array 737 | * 738 | * All arguments in data must be quoted 739 | * 740 | * @param string[] $data 741 | * @param string $separator concatenate with separator 742 | * @return \Zend_Db_Expr 743 | */ 744 | public function getConcatSql(array $data, $separator = null) 745 | { 746 | $format = empty($separator) ? 'CONCAT(%s)' : "CONCAT_WS('{$separator}', %s)"; 747 | return new \Zend_Db_Expr(sprintf($format, implode(', ', $data))); 748 | } 749 | 750 | public function getLengthSql($string) 751 | { 752 | throw new \RuntimeException('Not implemented ' . self::class . '::getLengthSql()'); 753 | } 754 | 755 | public function getLeastSql(array $data) 756 | { 757 | return new \Zend_Db_Expr('LEAST(' . implode(', ', $data) . ')'); 758 | } 759 | 760 | public function getFieldSql($field, array $sequence) 761 | { 762 | $sql = "CASE "; 763 | foreach ($sequence as $i => $v) { 764 | $sql .= "WHEN $field = $v THEN $i "; 765 | } 766 | $sql .= " END"; 767 | 768 | return new \Zend_Db_Expr($sql); 769 | } 770 | 771 | public function getGreatestSql(array $data) 772 | { 773 | throw new \RuntimeException('Not implemented ' . self::class . '::getGreatestSql()'); 774 | } 775 | 776 | public function getDateAddSql($date, $interval, $unit) 777 | { 778 | throw new \RuntimeException('Not implemented ' . self::class . '::getDateAddSql()'); 779 | } 780 | 781 | public function getDateSubSql($date, $interval, $unit) 782 | { 783 | throw new \RuntimeException('Not implemented ' . self::class . '::getDateSubSql()'); 784 | } 785 | 786 | public function getDateFormatSql($date, $format) 787 | { 788 | throw new \RuntimeException('Not implemented ' . self::class . '::getDateFormatSql()'); 789 | } 790 | 791 | public function getDatePartSql($date) 792 | { 793 | throw new \RuntimeException('Not implemented ' . self::class . '::getDatePartSql()'); 794 | } 795 | 796 | public function getSubstringSql($stringExpression, $pos, $len = null) 797 | { 798 | throw new \RuntimeException('Not implemented ' . self::class . '::getSubstringSql()'); 799 | } 800 | 801 | public function getStandardDeviationSql($expressionField) 802 | { 803 | throw new \RuntimeException('Not implemented ' . self::class . '::getStandardDeviationSql()'); 804 | } 805 | 806 | public function getDateExtractSql($date, $unit) 807 | { 808 | throw new \RuntimeException('Not implemented ' . self::class . '::getDateExtractSql()'); 809 | } 810 | 811 | /** 812 | * Generates case SQL fragment 813 | * 814 | * Generate fragment of SQL, that check value against multiple condition cases 815 | * and return different result depends on them 816 | * 817 | * @param string $valueName Name of value to check 818 | * @param array $casesResults Cases and results 819 | * @param string $defaultValue value to use if value doesn't confirm to any cases 820 | * @return \Zend_Db_Expr 821 | */ 822 | public function getCaseSql($valueName, $casesResults, $defaultValue = null) 823 | { 824 | $expression = 'CASE ' . $valueName; 825 | foreach ($casesResults as $case => $result) { 826 | $expression .= ' WHEN ' . $case . ' THEN ' . $result; 827 | } 828 | if ($defaultValue !== null) { 829 | $expression .= ' ELSE ' . $defaultValue; 830 | } 831 | $expression .= ' END'; 832 | 833 | return new \Zend_Db_Expr($expression); 834 | } 835 | 836 | public function getTableName($tableName) 837 | { 838 | return $tableName; 839 | } 840 | 841 | public function getTriggerName($tableName, $time, $event) 842 | { 843 | throw new \RuntimeException('Not implemented ' . self::class . '::getTriggerName()'); 844 | } 845 | 846 | public function getIndexName($tableName, $fields, $indexType = '') 847 | { 848 | throw new \RuntimeException('Not implemented ' . self::class . '::getIndexName()'); 849 | } 850 | 851 | public function getForeignKeyName($priTableName, $priColumnName, $refTableName, $refColumnName) 852 | { 853 | throw new \RuntimeException('Not implemented ' . self::class . '::getForeignKeyName()'); 854 | } 855 | 856 | public function disableTableKeys($tableName, $schemaName = null) 857 | { 858 | throw new \RuntimeException('Not implemented ' . self::class . '::disableTableKeys()'); 859 | } 860 | 861 | public function enableTableKeys($tableName, $schemaName = null) 862 | { 863 | throw new \RuntimeException('Not implemented ' . self::class . '::enableTableKeys()'); 864 | } 865 | 866 | public function selectsByRange($rangeField, \Magento\Framework\DB\Select $select, $stepCount = 100) 867 | { 868 | throw new \RuntimeException('Not implemented ' . self::class . '::selectsByRange()'); 869 | } 870 | 871 | public function insertFromSelect(\Magento\Framework\DB\Select $select, $table, array $fields = [], $mode = false) 872 | { 873 | $sql = "INSERT INTO {$table} (" . implode(', ', $fields) . ")" . $select; 874 | // TODO: Handle empty fields, and mode On_duplicate; 875 | return $sql; 876 | } 877 | 878 | public function updateFromSelect(\Magento\Framework\DB\Select $select, $table) 879 | { 880 | throw new \RuntimeException('Not implemented ' . self::class . '::updateFromSelect()'); 881 | } 882 | 883 | public function deleteFromSelect(\Magento\Framework\DB\Select $select, $table) 884 | { 885 | throw new \RuntimeException('Not implemented ' . self::class . '::deleteFromSelect()'); 886 | } 887 | 888 | public function getTablesChecksum($tableNames, $schemaName = null) 889 | { 890 | throw new \RuntimeException('Not implemented ' . self::class . '::getTablesChecksum()'); 891 | } 892 | 893 | public function supportStraightJoin() 894 | { 895 | throw new \RuntimeException('Not implemented ' . self::class . '::supportStraightJoin()'); 896 | } 897 | 898 | public function orderRand(\Magento\Framework\DB\Select $select, $field = null) 899 | { 900 | throw new \RuntimeException('Not implemented ' . self::class . '::orderRand()'); 901 | } 902 | 903 | public function forUpdate($sql) 904 | { 905 | throw new \RuntimeException('Not implemented ' . self::class . '::forUpdate()'); 906 | } 907 | 908 | public function getPrimaryKeyName($tableName, $schemaName = null) 909 | { 910 | $indexes = $this->getIndexList($tableName, $schemaName); 911 | $data = array_filter($indexes, function ($x) { 912 | return $x['INDEX_TYPE'] == 'primary'; 913 | }); 914 | $mainKey = reset($data); 915 | return $mainKey['KEY_NAME']; 916 | } 917 | 918 | public function decodeVarbinary($value) 919 | { 920 | throw new \RuntimeException('Not implemented ' . self::class . '::decodeVarbinary()'); 921 | } 922 | 923 | use Functions\Transaction; 924 | 925 | public function createTrigger(\Magento\Framework\DB\Ddl\Trigger $trigger) 926 | { 927 | throw new \RuntimeException('Not implemented ' . self::class . '::createTrigger()'); 928 | } 929 | 930 | public function dropTrigger($triggerName, $schemaName = null) 931 | { 932 | throw new \RuntimeException('Not implemented ' . self::class . '::dropTrigger()'); 933 | } 934 | 935 | public function getTables($likeCondition = null) 936 | { 937 | throw new \RuntimeException('Not implemented ' . self::class . '::getTables()'); 938 | } 939 | 940 | /** 941 | * Returns auto increment field if exists 942 | * 943 | * @param string $tableName 944 | * @param string|null $schemaName 945 | * @return string|bool 946 | * @since 100.1.0 947 | */ 948 | public function getAutoIncrementField($tableName, $schemaName = null) 949 | { 950 | $indexName = $this->getPrimaryKeyName($tableName, $schemaName); 951 | $indexes = $this->getIndexList($tableName); 952 | if ($indexName && count($indexes[$indexName]['COLUMNS_LIST']) == 1) { 953 | return current($indexes[$indexName]['COLUMNS_LIST']); 954 | } 955 | return false; 956 | } 957 | } 958 | --------------------------------------------------------------------------------