|
29.Oct.2015: Note
Thanks for your reading of my old contents. Feasibility study of PostgreSQL supportIn order to support PostgreSQL, I made a feasibility study for about couple of weeks.
I write the conclusion first: Findings and ProblemsBut not all, I think I got some findings and was able to extract a lot of problems. I used PrestaShop Version 1.5.3.1. 1. Back quote (`) problem (non-standard: MySQL expand)
The first difficulty was a huge number of back quote(`).
[< MySQL >] SELECT `language_code` FROM `'._DB_PREFIX_.'lang` WHERE `iso_code` = \''.pSQL(strtolower($iso_code)).'\'' I had to rewrite this as follow: [< PostgreSQL >] SELECT language_code FROM '._DB_PREFIX_.'lang WHERE iso_code = \''.pSQL(strtolower($iso_code)).'\''
It's simple. However, deletion of a huge number of back quote was really hard work.
2. Table Definitions
I was able to rewrite all CREATE TABLE queries in "install/data/db_structure.sql".
2.1. Data Type
PostgreSQL does not support UNSIGNED attribute. Therefore, I was emulated using check constraint. [< MySQL >] CREATE TABLE unsigned_table ( col int unsigned ); [< PostgreSQL >] CREATE TABLE unsigned_table ( col int check(0 <= col) ); All auto_increment integer type have been rewritten to Serial type. [< MySQL >] CREATE TABLE test_table ( col int auto_increment, data int ); [< PostgreSQL >] CREATE TABLE test_table ( col serial, data int ); /* for last_insert_id() function */ ALTER SEQUENCE IF EXISTS test_table_col_seq RENAME TO test_table_auto_increment;
PostgreSQL does not support ENUM type.
I made new types using CREATE TYPE queries.
[< MySQL >] CREATE TABLE `new_type_test` ( `id` ENUM('red', 'green', 'blue') NOT NULL DEFAULT 'red' ); [< PostgreSQL >] CREATE TYPE color_type AS ENUM ('red', 'green', 'blue'); CREATE TABLE new_type_test ( id color_type NOT NULL DEFAULT 'red' ); I made nine types below: CREATE TYPE group_type AS ENUM ('select', 'radio', 'color'); CREATE TYPE type AS ENUM('products', 'categories', 'attributes', 'manufacturers', 'suppliers'); CREATE TYPE status AS enum('open','closed','pending1','pending2'); CREATE TYPE redirect_type AS ENUM('', '404', '301', '302'); CREATE TYPE condition AS ENUM('new', 'used', 'refurbished'); CREATE TYPE visibility AS ENUM('both', 'catalog', 'search', 'none'); CREATE TYPE reduction_type AS ENUM('amount', 'percentage'); CREATE TYPE method AS enum('GET','POST','PUT','DELETE','HEAD'); CREATE TYPE management_type AS ENUM('WA', 'FIFO', 'LIFO'); 2.2. CREATE TABLEIn the table definitions, indices have to be defined separately. I show two examples: [< MySQL >] CREATE TABLE `PREFIX_accessory` ( `id_product_1` int(10) unsigned NOT NULL, `id_product_2` int(10) unsigned NOT NULL, KEY `accessory_product` (`id_product_1`,`id_product_2`) ) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8; [< PostgreSQL >] CREATE TABLE PREFIX_accessory ( id_product_1 int NOT NULL check(0 <= id_product_1), id_product_2 int NOT NULL check(0 <= id_product_2) ); CREATE INDEX PREFIX_accessory_accessory_product1 ON PREFIX_accessory (id_product_1,id_product_2); [< MySQL >] CREATE TABLE `PREFIX_attribute_impact` ( `id_attribute_impact` int(10) unsigned NOT NULL auto_increment, `id_product` int(11) unsigned NOT NULL, `id_attribute` int(11) unsigned NOT NULL, `weight` DECIMAL(20,6) NOT NULL, `price` decimal(17,2) NOT NULL, PRIMARY KEY (`id_attribute_impact`), UNIQUE KEY `id_product` (`id_product`,`id_attribute`) ) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8; [< PostgreSQL >] CREATE TABLE PREFIX_attribute_impact ( id_attribute_impact serial NOT NULL , id_product int NOT NULL check(0 <= id_product), id_attribute int NOT NULL check(0 <= id_attribute), weight DECIMAL(20,6) NOT NULL, price decimal(17,2) NOT NULL, PRIMARY KEY (id_attribute_impact), CONSTRAINT id_product UNIQUE (id_product,id_attribute) ); 3. INSERTThere are some problems. Especially the Db::insert method is troublesome. 3.1. INSERT ON DUPLICATE KEY UPDATE (non-standard: MySQL expand)
INSERT ON DUPLICATE KEY UPDATE queries are used in about 10 places.
This statement is non-standard, but those were able to rewrite easily.
[< Original: classes/Product.php:3146 >] public static function addFeatureProductImport($id_product, $id_feature, $id_feature_value) { return Db::getInstance()->execute(' INSERT INTO `'._DB_PREFIX_.'feature_product` (`id_feature`, `id_product`, `id_feature_value`) VALUES ('.(int)$id_feature.', '.(int)$id_product.', '.(int)$id_feature_value.') ON DUPLICATE KEY UPDATE `id_feature_value` = '.(int)$id_feature_value ); } [< After change >] public static function addFeatureProductImport($id_product, $id_feature, $id_feature_value) { $count = Db::getInstance()->getValue('SELECT count(*) FROM '._DB_PREFIX_.'feature_product WHERE id_feature = ' . (int)$id_feature . ' AND id_product = '. (int)$id_product); if ($count == 0) { return Db::getInstance()->execute('INSERT INTO '._DB_PREFIX_.'feature_product (id_feature, id_product, id_feature_value) VALUES ('.(int)$id_feature.', '.(int)$id_product.', '.(int)$id_feature_value.')'); } else { return Db::getInstance()->execute('UPDATE '._DB_PREFIX_.'feature_product SET id_feature_value = ' . (int)$id_feature_value . ' WHERE id_deature = ' . (int)$id_feature . ' AND id_product = ' . (int)$id_product); } } 3.2. INSERT IGNORE (non-standard: MySQL expand)
First of all, I can not understand why MySQL supports INSERT IGNORE statement.
INSERT IGNORE queries are used in more than twenty places, they are rewritable. I show two examples. [< Original: classes/Country.php:358 >] $sql = false; foreach ($shops as $id_shop) foreach ($countries as $country) foreach ($modules as $module) $sql .= '('.(int)$module['id_module'].', '.(int)$id_shop.', '.(int)$country['id_country'].'),'; if ($sql) { $sql = 'INSERT IGNORE INTO `'._DB_PREFIX_.'module_country` (`id_module`, `id_shop`, `id_country`) VALUES '.rtrim($sql, ','); return Db::getInstance()->execute($sql); } else return true; [< After change>] foreach ($shops as $id_shop) { foreach ($countries as $country) { foreach ($modules as $module) { $sql = 'INSERT INTO '._DB_PREFIX_.'module_country (id_module, id_shop, id_country) VALUES '; $sql .= '('.(int)$module['id_module'].', '.(int)$id_shop.', '.(int)$country['id_country'].')'; Db::getInstance()->execute($sql); } } } return true; [< Original: classes/shop/Shop.php:1039 >] foreach() { ... $keys = implode('`, `', array_keys($res)); $sql = 'INSERT IGNORE INTO `'._DB_PREFIX_.$table_name.'` (`'.$keys.'`, `id_category_default`, '.$id.') (SELECT `'.$keys.'`, '.(int)$categories[0].', '.(int)$this->id.' FROM '._DB_PREFIX_.$table_name.' WHERE `'.$id.'` = '.(int)$old_id.')'; ... } Db::getInstance()->execute($sql); [< After change >] foreach() { ... $keys = implode(', ', array_keys($res)); $sql = 'SELECT '.$keys.', '.(int)$categories[0].', '.(int)$this->id.' FROM '._DB_PREFIX_.$table_name.' WHERE '.$id.' = '.(int)$old_id; $result = Db::getInstance()->executeS($sql); foreach($result as $row) { $sql = 'INSERT INTO '._DB_PREFIX_.$table_name.' ('.$keys.', id_category_default, '.$id.') VALUES('; foreach($keys as $key) $sql .= '\'' . $row[$key] . '\', '; $sql .= '\'' .(int)$categories[0]. '\', ' . (int)$this->id . '\')'; Db::getInstance()->execute($sql); .... }
INSERT IGNORE statement is used in the Db::insert method.
3.3. "INSERT INTO table () VALUES ()" query problem (non-standard: MySQL expand)When installing PrestaShop, the INSERT statement with no value is executed. I show database's log file: 15 Query INSERT INTO `ps_profile` () VALUES ()
Of course, this is non-standard INSERT statement,
only MySQL is able to execute this.
4. REPLACE (non-standard: MySQL expand)REPLACE statement is useful, but this is non-standard. Fortunately, it has not been used much.
There are two explicit REPLACE statements in the source code. Rewriting was somewhat complicated. [< Original classes/ProductSale.php:33 >] public static function fillProductSales() { $sql = 'REPLACE INTO '._DB_PREFIX_.'product_sale (`id_product`, `quantity`, `sale_nbr`, `date_upd`) SELECT od.product_id, COUNT(od.product_id), SUM(od.product_quantity), NOW() FROM '._DB_PREFIX_.'order_detail od GROUP BY od.product_id'; return Db::getInstance()->execute($sql); } [< After change >] public static function fillProductSales() { $ret = true; $sql = 'SELECT od.product_id as id_product, COUNT(od.product_id) as quantity, SUM(od.product_quantity) as sale_nbr, NOW() as date_upd FROM '._DB_PREFIX_.'order_detail od GROUP BY od.product_id'; $result = Db::getInstance()->executeS($sql); foreach($result as $row) { $sql = 'SELECT count(*) FROM ' ._DB_PREFIX_.'product_sale WHERE id_product = ' .$row['id_product']; $count = Db::getInstance()->getValue($sql); if ($count == 0) { $sql = 'INSERT INTO '._DB_PREFIX_.'product_sale (id_product, quantity, sale_nbr, date_upd) VALUES ('; $sql .= '\''.$row['id_product'].'\', \''.$row['quantity'].'\', \''.$row['sale_nbr'].'\', \''.$row['date_upd'].'\')'; $ret &= Db::getInstance()->execute($sql); } else { $sql = 'UPDATE '._DB_PREFIX_.'product_sale SET id_product = \'' .$row['quantity']. '\', sale_nbr = \'' .$row['sale_nbr']. '\', date_upd = \\'' .$row['date_upd'] . '\ WHERE id_product = \'' .$row['id_product']; $ret &= Db::getInstance()->execute($sql); } } return $ret; [< Original classes/Tab.php:125 >] /* Query definition */ $query = 'REPLACE INTO `'._DB_PREFIX_.'access` (`id_profile`, `id_tab`, `view`, `add`, `edit`, `delete`) VALUES '; $query .= '(1, '.(int)$id_tab.', 1, 1, 1, 1),'; foreach ($profiles as $profile) { $rights = $profile['id_profile'] == $context->employee->id_profile ? 1 : 0; $query .= '('.(int)$profile['id_profile'].', '.(int)$id_tab.', '.(int)$rights.', ' .(int)$rights.', '.(int)$rights.', '.(int)$rights.'),'; } $query = trim($query, ', '); return Db::getInstance()->execute($query); [< After change >] $ret = true; $sql = 'SELECT count(*) FROM ' ._DB_PREFIX_. 'access WHERE id_profile = 1 AND id_tab = ' . (int)$id_tab; $count = Db::getInstance()->getValue($sql); if ($count == 0) { $sql = 'INSERT INTO '._DB_PREFIX_.'access (id_profile, id_tab, view, add, edit, delete) VALUES '; $sql .= '(1, '.(int)$id_tab.', 1, 1, 1, 1),'; $ret &= Db::getInstance()->execute($sql); } else { $sql = 'UPDATE '._DB_PREFIX_.'access SET view = 1, add = 1, edit = 1, delete = 1 WHERE id_profile = 1 AND id_tab = '; $sql.= (int)$id_tab; $ret &= Db::getInstance()->execute($sql); } foreach ($profiles as $profile) { $rights = $profile['id_profile'] == $context->employee->id_profile ? 1 : 0; $sql = 'SELECT count(*) FROM ' ._DB_PREFIX_. 'access WHERE id_profile = ' .(int)$profile['id_profile']. ' AND id_tab = ' . (int)$id_tab; $count = Db::getInstance()->getValue($sql); if ($count == 0) { $sql = 'INSERT INTO '._DB_PREFIX_.'access (id_profile, id_tab, view, add, edit, delete) VALUES '; $sql .= '('.(int)$profle['id_profile'].','.(int)$id_tab.','; $sql .= (int)$rights.','.(int)$rights.','.(int)$rights.','.(int)$rights.'),'; $ret &= Db::getInstance()->execute($sql); } else { $sql = 'UPDATE '._DB_PREFIX_.'access SET view = '.(int)$rights.', add = '.(int)$rights.', edit = '; $sql .= (int)$rights.', delete = '.(int)$rights; $sql .= ' WHERE id_profile = ' .(int)$profile['id_profile'] . ' AND id_tab = ' . (int)$id_tab; $ret &= Db::getInstance()->execute($sql); } } return $ret;
REPLACE statement is used when data of access.xml is populated in the database (See install/classes/xmlLoader.php:404, and next, the Db::insert method is invoked.).
5. SELECT
Almost SELECT queries did not need to be rewritten.
5.1. Last_Insert_Id function (non-standard: MySQL expand)MySQL's last_insert_id function is wrapped in Db::Insert_ID method. This method is used in over forty places, I was able to rewrite almost queries by expand this method. I rewrote this method and source code. I show one example. [< Original classes/db/Db.php:131 >] abstract public function Insert_ID(); [< After change >] abstract public function Insert_ID($table = null); [< Original classes/db/DbPDO.php:104 >] public function Insert_ID() { return $this->link->lastInsertId(); } [< After change >] public function Insert_ID($table = null) { if ($table == null) return $this->link->lastInsertId(); else return $this->getValue('SELECT currval\'' . _DB_PREFIX_. $table . '_auto_increment\')'); } [< Original classes/Product.php:3396 >] if (!Db::getInstance()->execute(' INSERT INTO `'._DB_PREFIX_.'customization_field` (`id_product`, `type`, `required`) VALUES ('.(int)$this->id.', '.(int)$type.', 0)') || !$id_customization_field = (int)Db::getInstance()->Insert_ID()) return false; [< After change >] if (!Db::getInstance()->execute(' INSERT INTO `'._DB_PREFIX_.'customization_field` (`id_product`, `type`, `required`) VALUES ('.(int)$this->id.', '.(int)$type.', 0)') || !$id_customization_field = (int)Db::getInstance()->Insert_ID('customization_field')) return false; This method is effective in most places. However, I do not know how to rewrite the following seven places.
5.2. "SELECT SQL_CALC_FOUND_ROWS" query (non-standard: MySQL expand)
"SELECT SQL_CALC_FOUND_ROWS" queries are used in nine places. This is also non-standard statement.
5.3. control flow function: IF(), IFNULL()Control flow functions IF() and IFNULL() are used in seviral places in the source code. These can be rewritten in CASE statement which is supported by MySQL and PostgreSQL. [< Original: classes/Configuration.php:113 >] $sql = 'SELECT c.`name`, cl.`id_lang`, IF(cl.`id_lang` IS NULL, c.`value`, cl.`value`) AS value, c.id_shop_group, c.id_shop FROM `'._DB_PREFIX_.'configuration` c LEFT JOIN `'._DB_PREFIX_.'configuration_lang` cl ON (c.id_configuration = cl.id_configuration)'; [< After change >] $sql = 'SELECT c.name, cl.id_lang, CASE WHEN cl.id_lang = NULL THEN c.value ELSE cl.value END AS value, c.id_shop_group, c.id_shop FROM `'._DB_PREFIX_.'configuration` c LEFT JOIN `'._DB_PREFIX_.'configuration_lang` cl ON (c.id_configuration = cl.id_configuration)'; 6. UPDATE LIMIT, DELETE LIMIT statements (non-standard: MySQL expand)
"UPDATE LIMIT" statement is of course non-standard.
30 Query UPDATE `ps_configuration` SET `value` = '1',`date_upd` = '2013-03-17 22:50:15' WHERE `name` = 'MOD_BLOCKTOPMENU_SEARCH' AND id_shop_group IS NULL AND id_shop IS NULL LIMIT 1 30 Query UPDATE `ps_configuration` SET `value` = '2',`date_upd` = '2013-03-17 22:50:15' WHERE `name` = 'PRODUCTS_VIEWED_NBR' AND id_shop_group IS NULL AND id_shop IS NULL LIMIT 1 30 Query UPDATE `ps_configuration` SET `value` = 'graphartichow',`date_upd` = '2013-03-17 22:50:15' WHERE `name` = 'PS_STATS_RENDER' AND id_shop_group IS NULL AND id_shop IS NULL LIMIT 1 30 Query UPDATE `ps_configuration` SET `value` = 'graphgooglechart',`date_upd` = '2013-03-17 22:50:15' WHERE `name` = 'PS_STATS_RENDER' AND id_shop_group IS NULL AND id_shop IS NULL LIMIT 1 ... to be continued ..
I have no idea how to rewrite this, because I do not understand this logic.
7. SHOW commandsSHOW TABLES and SHOW COLUMNS FROM commands can be emulated by PL/pgSQL functions. 7.1. SHOW TABLESSHOW TABLE command is used in about ten places in the source code. I defined two functions called "show_tables", and rewrote SHOW TABLE command using those functions.
CREATE OR REPLACE FUNCTION show_tables (text) RETURNS SETOF name AS $$ /* $1 = pattern */ SELECT c.relname as "Name" FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN ('r','s','') AND n.nspname !~ '^pg_toast' AND pg_catalog.pg_table_is_visible(c.oid) AND c.relname LIKE $1 ; $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION show_tables () RETURNS SETOF name AS $$ SELECT c.relname as "Name" FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN ('r','s','') AND n.nspname !~ '^pg_toast' AND pg_catalog.pg_table_is_visible(c.oid); $$ LANGUAGE SQL; I show two examples: [< Original classes/Language.php:472 >] $result = Db::getInstance()->executeS('SHOW TABLES FROM `'._DB_NAME_.'`'); [< After change >] $result = Db::getInstance()->executeS('SELECT SHOW_TABLES()'); [< Original classes/Language.php:391 >] $tables = Db::getInstance()->executeS('SHOW TABLES LIKE \''.str_replace('_', '\\_', _DB_PREFIX_).'%\_lang\' '); [< After change >] $tables = Db::getInstance()->executeS('SELECT SHOW_TABLES(\''.str_replace('_', '\\_', _DB_PREFIX_).'%\_lang\') ');
7.2. SHOW COLUMNS FROMSHOW COLUMNS (also SHOW FIELDS) command is used in about ten places in the source code. I defined a function called "show_columns_from", and rewrote SHOW COLUMNS command using this function.
CREATE OR REPLACE FUNCTION show_columns_from (name) RETURNS SETOF record AS $$ /* $1 = relname */ SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod) FROM pg_catalog.pg_attribute a WHERE a.attrelid = (SELECT c.oid FROM pg_catalog.pg_class c WHERE c.relname = $1 AND pg_catalog.pg_table_is_visible(c.oid)) AND a.attnum > 0 AND NOT a.attisdropped; $$ LANGUAGE SQL; I show one example: [< Original classes/Language.php:412 >] $columns = Db::getInstance()->executeS('SHOW COLUMNS FROM `'.$name.'`'); [< After change >] $columns = Db::getInstance()->executeS('SELECT * FROM SHOW_COLUMNS_FROM(\'' .$name. '\') AS (Field name, Type text)'); Conclusions
I think that more than 95% of queries which are executed while PrestaShop is running are SELECT statements.
And then, almost PrestaShop's SELECT statements can be executed in PostgreSQL (if back quotes are deleted).
I would like to show three important issues:
Indeed, those are not easy tasks.
However, I think that it is able to finish by a few engineers.
Suzuki Hironobu
APPENDIX: Source Code and InstallationWarning:This source code is junk. This is not work, and is only for confirmation.
# createdb prestashop_db -E utf8 # psql prestashop_db -f installpath/prestashop/func.sql Remark: connect to 127.0.0.1 or your server's IP address. Don't connect to "localhost".
Last-modified: 2013-3-29
|