From cb635093565b76a6e71ec29b7dff2f91ad4503f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Thu, 25 Feb 2010 16:43:04 +0100 Subject: [PATCH] Add MySQL backend --- CHANGELOG | 1 + INSTALL | 1 + README | 21 +- etc/trytond.conf | 2 +- setup.py | 1 + trytond/backend/__init__.py | 2 + trytond/backend/database.py | 15 +- trytond/backend/mysql/__init__.py | 6 + trytond/backend/mysql/database.py | 305 ++++++++++++++++++ trytond/backend/mysql/fields.py | 148 +++++++++ trytond/backend/mysql/init.sql | 193 ++++++++++++ trytond/backend/mysql/table.py | 412 +++++++++++++++++++++++++ trytond/backend/postgresql/database.py | 4 +- trytond/backend/sqlite/database.py | 4 +- trytond/ir/action.py | 4 + trytond/ir/cron.py | 3 +- trytond/ir/sequence.py | 3 +- 17 files changed, 1112 insertions(+), 13 deletions(-) create mode 100644 trytond/backend/mysql/__init__.py create mode 100644 trytond/backend/mysql/database.py create mode 100644 trytond/backend/mysql/fields.py create mode 100644 trytond/backend/mysql/init.sql create mode 100644 trytond/backend/mysql/table.py diff --git a/CHANGELOG b/CHANGELOG index e0bced7c31..e832365108 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add MySQL backend * Refactoring by validation with pylint * Modify ondelete attribute of Many2One according to required attribute value * Refactor import_data of ModelStorage diff --git a/INSTALL b/INSTALL index 3fd31829fe..8d958b4c5c 100644 --- a/INSTALL +++ b/INSTALL @@ -10,6 +10,7 @@ Prerequisites * python-dateutil (http://labix.org/python-dateutil) * simplejson (http://undefined.org/python/#simplejson) * Optional: psycopg 2 or later (http://www.initd.org/) + * Optional: MySQL-python (http://sourceforge.net/projects/mysql-python/) * Optional: pywebdav 0.9.3 or later (http://code.google.com/p/pywebdav/) * Optional: pydot (http://code.google.com/p/pydot/) * Optional: pytz (http://pytz.sourceforge.net/) diff --git a/README b/README index 81ec86f1eb..47a7e53f5b 100644 --- a/README +++ b/README @@ -3,7 +3,7 @@ trytond The server of the Tryton application platform. A three-tiers high-level general purpose application platform -written in Python and use Postgresql as database engine. +written in Python and use Postgresql as main database engine. It is the core base of an Open Source ERP. It provides modularity, scalability and security. @@ -30,6 +30,25 @@ Package Contents trytond/ trytond sources. +Database Backends +----------------- + +Tryton is mainly developed for PostgreSQL but there is some other backends +available. Here is some warnings about using other backends: + + * MySQL + + * The lock implementation of MySQL requires that Tryton locks all tables. + + * There are problems with floating-point comparisons. + See: http://dev.mysql.com/doc/refman/5.5/en/problems-with-float.html + + * Tryton uses a VARCHAR(255) for Char, Selection and Reference fields. + + * MySQL can not create indexes containing text or blob fields. + + * Tryton uses a DECIMAL(16, 8) for Decimal fields. + Support ------- diff --git a/etc/trytond.conf b/etc/trytond.conf index 7604f29453..855ba8232d 100644 --- a/etc/trytond.conf +++ b/etc/trytond.conf @@ -29,7 +29,7 @@ #webdavport = 8080 # Configure the database type -# allowed values are postgresql, sqlite +# allowed values are postgresql, sqlite, mysql db_type = postgresql # Configure the database connection diff --git a/setup.py b/setup.py index 562b7d2bc2..8190d13a3e 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,7 @@ setup(name=PACKAGE, ] + SIMPLEJSON, extras_require={ 'PostgreSQL': ['psycopg2 >= 2.0'], + 'MySQL': ['MySQL-python'], 'WebDAV': ['PyWebDAV >= 0.9.3'], 'PDF': ['openoffice-python'], 'SSL': ['pyOpenSSL'], diff --git a/trytond/backend/__init__.py b/trytond/backend/__init__.py index 7363cfc299..03ec53fcd6 100644 --- a/trytond/backend/__init__.py +++ b/trytond/backend/__init__.py @@ -4,5 +4,7 @@ from trytond.config import CONFIG if CONFIG['db_type'] == 'sqlite': from sqlite import * +elif CONFIG['db_type'] == 'mysql': + from mysql import * else: from postgresql import * diff --git a/trytond/backend/database.py b/trytond/backend/database.py index 3e1bd1877d..168d1f9624 100644 --- a/trytond/backend/database.py +++ b/trytond/backend/database.py @@ -186,11 +186,11 @@ class CursorInterface(object): :return: an integer ''' - def has_lock(self): + def lock(self, table): ''' - Return True if database handle lock table. + Lock the table - :return: a boolean + :param table: the table name ''' raise NotImplementedError @@ -212,3 +212,12 @@ class CursorInterface(object): :return: a string ''' raise NotImplementedError + + def update_auto_increment(self, table, value): + ''' + Update auto_increment value of table + + :param table: the table name + :param value: the auto_increment value + ''' + pass diff --git a/trytond/backend/mysql/__init__.py b/trytond/backend/mysql/__init__.py new file mode 100644 index 0000000000..9598cf8965 --- /dev/null +++ b/trytond/backend/mysql/__init__.py @@ -0,0 +1,6 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. + +from database import * +from fields import * +from table import * diff --git a/trytond/backend/mysql/database.py b/trytond/backend/mysql/database.py new file mode 100644 index 0000000000..a4775226c7 --- /dev/null +++ b/trytond/backend/mysql/database.py @@ -0,0 +1,305 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. + +from trytond.backend.database import DatabaseInterface, CursorInterface +from trytond.config import CONFIG +import MySQLdb +import MySQLdb.cursors +from MySQLdb import IntegrityError as DatabaseIntegrityError +from MySQLdb import OperationalError as DatabaseOperationalError +import logging +import os +import re +import time +import tempfile + +QUOTE_SEPARATION = re.compile(r"(.*?)('.*?')", re.DOTALL) +DatabaseIntegrityError = None +DatabaseOperationalError = None + + +class Database(DatabaseInterface): + + _list_cache = None + _list_cache_timestamp = None + + def connect(self): + return self + + def cursor(self, autocommit=False): + args = { + 'db': self.database_name, + 'sql_mode': 'traditional,postgresql', + 'use_unicode': True, + 'charset': 'utf8', + } + if CONFIG['db_host']: + args['host'] = CONFIG['db_host'] + if CONFIG['db_port']: + args['port'] = int(CONFIG['db_port']) + if CONFIG['db_user']: + args['user'] = CONFIG['db_user'] + if CONFIG['db_password']: + args['passwd'] = CONFIG['db_password'] + conn = MySQLdb.connect(**args) + return Cursor(conn, self.database_name) + + def close(self): + return + + def create(self, cursor, database_name): + cursor.execute('CREATE DATABASE `' + database_name + '` ' \ + 'DEFAULT CHARACTER SET = \'utf8\'') + + def drop(self, cursor, database_name): + cursor.execute('DROP DATABASE `' + database_name + '`') + + @staticmethod + def dump(database_name): + from trytond.tools import exec_pg_command_pipe + + cmd = ['mysqldump', '--no-create-db'] + if CONFIG['db_user']: + cmd.append('--user=' + CONFIG['db_user']) + if CONFIG['db_host']: + cmd.append('--host=' + CONFIG['db_host']) + if CONFIG['db_port']: + cmd.append('--port=' + CONFIG['db_port']) + if CONFIG['db_password']: + cmd.append('--password=' + CONFIG['db_password']) + cmd.append(database_name) + + pipe = exec_pg_command_pipe(*tuple(cmd)) + pipe.stdin.close() + data = pipe.stdout.read() + res = pipe.wait() + if res: + raise Exception('Couldn\'t dump database!') + return data + + @staticmethod + def restore(database_name, data): + from trytond.tools import exec_pg_command_pipe + + database = Database().connect() + cursor = database.cursor(autocommit=True) + database.create(cursor, database_name) + cursor.commit() + cursor.close() + + cmd = ['mysql'] + if CONFIG['db_user']: + cmd.append('--user=' + CONFIG['db_user']) + if CONFIG['db_host']: + cmd.append('--host=' + CONFIG['db_host']) + if CONFIG['db_port']: + cmd.append('--port=' + CONFIG['db_port']) + if CONFIG['db_password']: + cmd.append('--password=' + CONFIG['db_password']) + cmd.append(database_name) + + fd, file_name = tempfile.mkstemp() + fd = os.fdopen(fd, 'wb+') + fd.write(data) + fd.close() + + cmd.append('<') + cmd.append(file_name) + + args2 = tuple(cmd) + + pipe = exec_pg_command_pipe(*args2) + pipe.stdin.close() + res = pipe.wait() + os.remove(file_name) + if res: + raise Exception('Couldn\'t restore database') + + database = Database(database_name).connect() + cursor = database.cursor() + if not cursor.test(): + cursor.close() + database.close() + raise Exception('Couldn\'t restore database!') + cursor.close() + database.close() + Database._list_cache = None + return True + + @staticmethod + def list(cursor): + now = time.time() + timeout = int(CONFIG['session_timeout']) + res = Database._list_cache + if res and abs(Database._list_cache_timestamp - now) < timeout: + return res + cursor.execute('SHOW DATABASES') + res = [] + for db_name, in cursor.fetchall(): + try: + database = Database(db_name).connect() + except: + continue + cursor2 = database.cursor() + if cursor2.test(): + res.append(db_name) + cursor2.close(close=True) + else: + cursor2.close() + database.close() + Database._list_cache = res + Database._list_cache_timestamp = now + return res + + @staticmethod + def init(cursor): + from trytond.tools import safe_eval + sql_file = os.path.join(os.path.dirname(__file__), 'init.sql') + for line in open(sql_file).read().split(';'): + if (len(line)>0) and (not line.isspace()): + cursor.execute(line) + + for i in ('ir', 'workflow', 'res', 'webdav'): + root_path = os.path.join(os.path.dirname(__file__), '..', '..') + tryton_file = os.path.join(root_path, i, '__tryton__.py') + mod_path = os.path.join(root_path, i) + info = safe_eval(open(tryton_file).read()) + active = info.get('active', False) + if active: + state = 'to install' + else: + state = 'uninstalled' + cursor.execute('INSERT INTO ir_module_module ' \ + '(create_uid, create_date, author, website, name, ' \ + 'shortdesc, description, state) ' \ + 'VALUES (%s, now(), %s, %s, %s, %s, %s, %s)', + (0, info.get('author', ''), + info.get('website', ''), i, info.get('name', False), + info.get('description', ''), state)) + cursor.execute('SELECT LAST_INSERT_ID()') + module_id = cursor.fetchone()[0] + dependencies = info.get('depends', []) + for dependency in dependencies: + cursor.execute('INSERT INTO ir_module_module_dependency ' \ + '(create_uid, create_date, module, name) ' \ + 'VALUES (%s, now(), %s, %s)', + (0, module_id, dependency)) + + +class _Cursor(MySQLdb.cursors.Cursor): + + def __build_dict(self, row): + return dict((desc[0], row[i]) + for i, desc in enumerate(self.description)) + + def dictfetchone(self): + row = self.fetchone() + if row: + return self.__build_dict(row) + else: + return row + + def dictfetchmany(self, size): + rows = self.fetchmany(size) + return [self.__build_dict(row) for row in rows] + + def dictfetchall(self): + rows = self.fetchall() + return [self.__build_dict(row) for row in rows] + + def fetchall(self): + return list(super(_Cursor, self).fetchall()) + + +class Cursor(CursorInterface): + + def __init__(self, conn, database_name): + super(Cursor, self).__init__() + self._conn = conn + self.database_name = database_name + self.dbname = self.database_name #XXX to remove + self.cursor = conn.cursor(_Cursor) + + def __getattr__(self, name): + return getattr(self.cursor, name) + + def execute(self, sql, params=None): + buf = "" + for nquote, quote in QUOTE_SEPARATION.findall(sql+"''"): + nquote = nquote.replace('ilike', 'like') + buf += nquote + quote + sql = buf[:-2] + try: + if params: + res = self.cursor.execute(sql, params) + else: + res = self.cursor.execute(sql) + except Exception, exception: + logger = logging.getLogger('sql') + logger.error('Wrong SQL: ' + sql + ' ' + str(params)) + if isinstance(exception, MySQLdb.Error): + exception.args = exception.args[1:] + raise exception + raise + return res + + def close(self, close=False): + self.cursor.close() + self.rollback() + + def commit(self): + super(Cursor, self).commit() + self._conn.commit() + + def rollback(self): + super(Cursor, self).rollback() + self._conn.rollback() + + def test(self): + self.cursor.execute("SHOW TABLES") + for table, in self.cursor.fetchall(): + if table in ( + 'ir_model', + 'ir_model_field', + 'ir_ui_view', + 'ir_ui_menu', + 'res_user', + 'res_group', + 'wkf', + 'wkf_activity', + 'wkf_transition', + 'wkf_instance', + 'wkf_workitem', + 'wkf_witm_trans', + 'ir_module_module', + 'ir_module_module_dependency', + 'ir_translation', + 'ir_lang', + ): + return True + return False + + def lastid(self): + self.cursor.execute('SELECT LAST_INSERT_ID()') + return self.cursor.fetchone()[0] + + def lock(self, table): + # Lock of table doesn't work because MySQL require + # that the session locks all tables that will be accessed + self.cursor.execute('FLUSH TABLES WITH READ LOCK') + + def has_constraint(self): + return True + + def limit_clause(self, select, limit=None, offset=None): + if offset and limit is None: + limit = 18446744073709551610 #max bigint + if limit is not None: + select += ' LIMIT %d' % limit + if offset is not None and offset != 0: + select += ' OFFSET %d' % offset + return select + + def update_auto_increment(self, table, value): + self.cursor.execute('ALTER TABLE `%s` AUTO_INCREMENT = %%s' % table, + value) diff --git a/trytond/backend/mysql/fields.py b/trytond/backend/mysql/fields.py new file mode 100644 index 0000000000..3f2840ba9c --- /dev/null +++ b/trytond/backend/mysql/fields.py @@ -0,0 +1,148 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. +from trytond.backend import fields + + +class Boolean(fields.Boolean): + + @staticmethod + def sql_type(field): + return ('bool', 'bool') + + @staticmethod + def sql_format(value): + return value and '1' or '0' + + +class Integer(fields.Integer): + + @staticmethod + def sql_type(field): + return ('bigint', 'bigint') + + +class BigInteger(fields.BigInteger): + + @staticmethod + def sql_type(field): + return ('bigint', 'bigint') + + +class Char(fields.Char): + + @staticmethod + def sql_type(field): + if field.size: + return ('varchar', 'varchar(%d)' % (field.size,)) + return ('varchar', 'varchar(255)') + + +class Sha(fields.Sha): + + @staticmethod + def sql_type(field): + return ('varchar', 'varchar(40)') + + +class Text(fields.Text): + + @staticmethod + def sql_type(field): + return ('text', 'text') + + +class Float(fields.Float): + + @staticmethod + def sql_type(field): + return ('float', 'float') + + +class Numeric(fields.Numeric): + + @staticmethod + def sql_type(field): + return ('decimal', 'decimal(16, 8)') + + +class Date(fields.Date): + + @staticmethod + def sql_type(field): + return ('date', 'date') + + +class DateTime(fields.DateTime): + + @staticmethod + def sql_type(field): + return ('timestamp', 'timestamp null') + + +class Timestamp(fields.Timestamp): + + @staticmethod + def sql_type(field): + return ('timestamp', 'timestamp null') + + +class Time(fields.Time): + + @staticmethod + def sql_type(field): + return ('time', 'time') + + +class Binary(fields.Binary): + + @staticmethod + def sql_format(value): + return value or None + + @staticmethod + def sql_type(field): + return ('longblob', 'longblob') + + +class Selection(fields.Selection): + + @staticmethod + def sql_type(field): + return ('varchar', 'varchar(255)') + + +class Reference(fields.Reference): + + @staticmethod + def sql_type(field): + return ('varchar', 'varchar(255)') + + +class Many2One(fields.Many2One): + + @staticmethod + def sql_type(field): + return ('bigint', 'bigint') + +FIELDS = { + 'boolean': Boolean, + 'integer': Integer, + 'biginteger': BigInteger, + 'char': Char, + 'sha': Sha, + 'text': Text, + 'float': Float, + 'numeric': Numeric, + 'date': Date, + 'datetime': DateTime, + 'timestamp': Timestamp, + 'time': Time, + 'binary': Binary, + 'selection': Selection, + 'reference': Reference, + 'many2one': Many2One, + 'one2many': fields.One2Many, + 'many2many': fields.Many2Many, + 'function': fields.Function, + 'property': fields.Property, +} diff --git a/trytond/backend/mysql/init.sql b/trytond/backend/mysql/init.sql new file mode 100644 index 0000000000..fc68a7e924 --- /dev/null +++ b/trytond/backend/mysql/init.sql @@ -0,0 +1,193 @@ +CREATE TABLE ir_model ( + id BIGINT AUTO_INCREMENT NOT NULL, + model VARCHAR(255) NOT NULL, + name VARCHAR(255), + info TEXT, + module VARCHAR(255), + PRIMARY KEY(id) +) ENGINE=InnoDB; + +CREATE TABLE ir_model_field ( + id BIGINT AUTO_INCREMENT NOT NULL, + model BIGINT, + name VARCHAR(255) NOT NULL, + relation VARCHAR(255), + field_description VARCHAR(255), + ttype VARCHAR(255), + help VARCHAR(255), + module VARCHAR(255), + PRIMARY KEY(id), + CONSTRAINT ir_model_field_model_fkey FOREIGN KEY (model) REFERENCES ir_model (id) ON DELETE CASCADE +) ENGINE=InnoDB; + +CREATE TABLE ir_ui_view ( + id BIGINT AUTO_INCREMENT NOT NULL, + model VARCHAR(255) NOT NULL, + type VARCHAR(255), + arch TEXT NOT NULL, + field_childs VARCHAR(255), + priority BIGINT NOT NULL default 0, + PRIMARY KEY(id) +) ENGINE=InnoDB; + +CREATE TABLE ir_ui_menu ( + id BIGINT AUTO_INCREMENT NOT NULL, + parent BIGINT, + name VARCHAR(255) NOT NULL, + icon VARCHAR(255), + PRIMARY KEY (id), + CONSTRAINT ir_ui_menu_parent_fkey FOREIGN KEY (parent) REFERENCES ir_ui_menu (id) ON DELETE SET NULL +) ENGINE=InnoDB; + +CREATE TABLE ir_translation ( + id BIGINT AUTO_INCREMENT NOT NULL, + lang VARCHAR(255), + src TEXT, + name VARCHAR(255) NOT NULL, + res_id BIGINT NOT NULL DEFAULT 0, + value TEXT, + type VARCHAR(255), + module VARCHAR(255), + fuzzy BOOLEAN NOT NULL, + PRIMARY KEY(id) +) ENGINE=InnoDB; + +CREATE TABLE ir_lang ( + id BIGINT AUTO_INCREMENT NOT NULL, + name VARCHAR(255) NOT NULL, + code TEXT NOT NULL, + translatable BOOLEAN NOT NULL, + active BOOLEAN NOT NULL, + direction VARCHAR(255) NOT NULL, + PRIMARY KEY(id) +) ENGINE=InnoDB; + +CREATE TABLE res_user ( + id BIGINT AUTO_INCREMENT NOT NULL, + name VARCHAR(255) NOT NULL, + active BOOLEAN NOT NULL, + login VARCHAR(255) NOT NULL, + password VARCHAR(40), + PRIMARY KEY(id) +) ENGINE=InnoDB; + +ALTER TABLE res_user ADD CONSTRAINT res_user_login_uniq UNIQUE (login); + +INSERT INTO res_user (id, login, password, name, active) VALUES (0, 'root', NULL, 'Root', False); +UPDATE res_user set id = 0; + +CREATE TABLE res_group ( + id BIGINT AUTO_INCREMENT NOT NULL, + name VARCHAR(255) NOT NULL, + PRIMARY KEY(id) +) ENGINE=InnoDB; + +CREATE TABLE `res_user-res_group` ( + uid BIGINT NOT NULL, + gid BIGINT NOT NULL, + CONSTRAINT `res_user-res_group_uid_fkey` FOREIGN KEY (uid) REFERENCES res_user (id) ON DELETE CASCADE, + CONSTRAINT `res_user-res_group_gid_fkey` FOREIGN KEY (gid) REFERENCES res_group (id) ON DELETE CASCADE +) ENGINE=InnoDB; + +CREATE TABLE wkf ( + id BIGINT AUTO_INCREMENT NOT NULL, + name VARCHAR(255), + model VARCHAR(255), + on_create BOOLEAN NOT NULL, + PRIMARY KEY(id) +) ENGINE=InnoDB; + +CREATE TABLE wkf_activity ( + id BIGINT AUTO_INCREMENT NOT NULL, + workflow BIGINT, + subflow BIGINT, + split_mode VARCHAR(255), + join_mode VARCHAR(255), + kind VARCHAR(255) NOT NULL, + name VARCHAR(255), + signal_send VARCHAR(255), + flow_start BOOLEAN NOT NULL, + flow_stop BOOLEAN NOT NULL, + action TEXT, + PRIMARY KEY(id), + CONSTRAINT wkf_activity_workflow_fkey FOREIGN KEY (workflow) REFERENCES wkf (id) ON DELETE CASCADE, + CONSTRAINT wkf_activity_subflow_fkey FOREIGN KEY (subflow) REFERENCES wkf (id) ON DELETE SET NULL +) ENGINE=InnoDB; + +CREATE TABLE wkf_transition ( + id BIGINT AUTO_INCREMENT NOT NULL, + act_from BIGINT, + act_to BIGINT, + `condition` VARCHAR(255), + trigger_expr_id VARCHAR(255), + signal VARCHAR(255), + `group` BIGINT, + PRIMARY KEY(id), + CONSTRAINT wkf_transition_act_from_fkey FOREIGN KEY (act_from) REFERENCES wkf_activity (id) ON DELETE CASCADE, + CONSTRAINT wkf_transition_act_to_fkey FOREIGN KEY (act_to) REFERENCES wkf_activity (id) ON DELETE CASCADE, + CONSTRAINT wkf_transition_group_fkey FOREIGN KEY (`group`) REFERENCES res_group (id) ON DELETE SET NULL +) ENGINE=InnoDB; + +CREATE TABLE wkf_instance ( + id BIGINT AUTO_INCREMENT NOT NULL, + workflow BIGINT, + uid BIGINT DEFAULT 0, + res_id BIGINT NOT NULL DEFAULT 0, + res_type VARCHAR(255) NOT NULL, + state VARCHAR(255) NOT NULL, + PRIMARY KEY(id), + CONSTRAINT wkf_instance_workflow_fkey FOREIGN KEY (workflow) REFERENCES wkf (id) ON DELETE RESTRICT +) ENGINE=InnoDB; + +CREATE TABLE wkf_workitem ( + id BIGINT AUTO_INCREMENT NOT NULL, + activity BIGINT, + instance BIGINT, + subflow BIGINT, + state VARCHAR(255), + PRIMARY KEY(id), + CONSTRAINT wkf_workitem_activity_fkey FOREIGN KEY (activity) REFERENCES wkf_activity (id) ON DELETE CASCADE, + CONSTRAINT wkf_workitem_instance_fkey FOREIGN KEY (instance) REFERENCES wkf_instance (id) ON DELETE CASCADE, + CONSTRAINT wkf_workitem_subflow_fkey FOREIGN KEY (subflow) REFERENCES wkf_instance (id) ON DELETE CASCADE +) ENGINE=InnoDB; + +CREATE TABLE wkf_witm_trans ( + trans_id BIGINT, + inst_id BIGINT, + CONSTRAINT wkf_witm_trans_trans_id_fkey FOREIGN KEY (trans_id) REFERENCES wkf_transition (id) ON DELETE CASCADE, + CONSTRAINT wkf_witm_trans_inst_id_fkey FOREIGN KEY (inst_id) REFERENCES wkf_instance (id) ON DELETE CASCADE +) ENGINE=InnoDB; + +CREATE TABLE ir_module_module ( + id BIGINT AUTO_INCREMENT NOT NULL, + create_uid BIGINT, + create_date TIMESTAMP NOT NULL, + write_date TIMESTAMP NULL, + write_uid BIGINT, + website VARCHAR(255), + name VARCHAR(255) NOT NULL, + author VARCHAR(255), + url VARCHAR(255), + state VARCHAR(255), + shortdesc VARCHAR(255), + description TEXT, + PRIMARY KEY(id), + CONSTRAINT ir_module_module_create_uid_fkey FOREIGN KEY (create_uid) REFERENCES res_user (id) ON DELETE SET NULL, + CONSTRAINT ir_module_module_write_uid_fkey FOREIGN KEY (write_uid) REFERENCES res_user (id) ON DELETE SET NULL +) ENGINE=InnoDB; + +ALTER TABLE ir_module_module ADD CONSTRAINT name_uniq UNIQUE (name); + +CREATE TABLE ir_module_module_dependency ( + id BIGINT AUTO_INCREMENT NOT NULL, + create_uid BIGINT, + create_date TIMESTAMP NOT NULL, + write_date TIMESTAMP NULL, + write_uid BIGINT, + name VARCHAR(255), + module BIGINT, + PRIMARY KEY(id), + CONSTRAINT ir_module_module_dependency_create_uid_fkey FOREIGN KEY (create_uid) REFERENCES res_user (id) ON DELETE SET NULL, + CONSTRAINT ir_module_module_dependency_write_uid_fkey FOREIGN KEY (write_uid) REFERENCES res_user (id) ON DELETE SET NULL, + CONSTRAINT ir_module_module_dependency_module_fkey FOREIGN KEY (module) REFERENCES ir_module_module (id) ON DELETE CASCADE +) ENGINE=InnoDB; diff --git a/trytond/backend/mysql/table.py b/trytond/backend/mysql/table.py new file mode 100644 index 0000000000..e9de67c856 --- /dev/null +++ b/trytond/backend/mysql/table.py @@ -0,0 +1,412 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. + +from trytond.backend.table import TableHandlerInterface +import logging + + +class TableHandler(TableHandlerInterface): + + def __init__(self, cursor, model, module_name=None, history=False): + super(TableHandler, self).__init__(cursor, model, + module_name=module_name, history=history) + self._columns = {} + self._constraints = [] + self._fkeys = [] + self._indexes = [] + self._field2module = {} + self._model = model + + # Create new table if necessary + if not self.table_exist(self.cursor, self.table_name): + if not self.history: + self.cursor.execute('CREATE TABLE `%s` (' \ + 'id BIGINT AUTO_INCREMENT NOT NULL, ' \ + 'PRIMARY KEY(id)' \ + ') ENGINE=InnoDB;' % self.table_name) + else: + self.cursor.execute('CREATE TABLE `%s` (' \ + '__id BIGINT AUTO_INCREMENT NOT NULL, ' \ + 'id BIGINT, ' \ + 'PRIMARY KEY(__id)' \ + ') ENGINE=InnoDB;' % self.table_name) + + self._update_definitions() + if 'id' not in self._columns: + if not self.history: + self.cursor.execute('ALTER TABLE `%s` ' \ + 'ADD COLUMN id BIGINT AUTO_INCREMENT ' \ + 'NOT NULL PRIMARY KEY' % + self.table_name) + else: + self.cursor.execute('ALTER TABLE `%s` ' \ + 'ADD COLUMN id BIGINT' % self.table_name) + self._update_definitions() + if self.history and not '__id' in self._columns: + self.cursor.execute('ALTER TABLE `%s` ' \ + 'ADD COLUMN __id BIGINT AUTO_INCREMENT ' \ + 'NOT NULL PRIMARY KEY' % + self.table_name) + self._update_definitions() + + @staticmethod + def table_exist(cursor, table_name): + cursor.execute("SELECT table_name FROM information_schema.tables " \ + "WHERE table_schema = %s AND table_name = %s", + (cursor.database_name, table_name)) + return bool(cursor.rowcount) + + @staticmethod + def table_rename(cursor, old_name, new_name): + #Rename table + if TableHandler.table_exist(cursor, old_name) and \ + not TableHandler.table_exist(cursor, new_name): + cursor.execute('ALTER TABLE `%s` RENAME TO `%s`' % \ + (old_name, new_name)) + #Rename history table + old_history = old_name + '__history' + new_history = new_name + '__history' + if TableHandler.table_exist(cursor, old_history) and \ + not TableHandler.table_exist(cursor, new_history): + cursor.execute('ALTER TABLE `%s` RENAME TO `%s`' % \ + (old_history, new_history)) + + @staticmethod + def sequence_exist(cursor, sequence_name): + return True + + @staticmethod + def sequence_rename(cursor, old_name, new_name): + pass + + def column_exist(self, column_name): + return column_name in self._columns + + def column_rename(self, old_name, new_name, exception=False): + if self.column_exist(old_name) and \ + not self.column_exist(new_name): + self.cursor.execute('ALTER TABLE `%s` ' \ + 'RENAME COLUMN `%s` TO `%s`' % + (self.table_name, old_name, new_name)) + elif exception and self.column_exist(new_name): + raise Exception('Unable to rename column %s.%s to %s.%s: ' \ + '%s.%s already exist!' % \ + (self.table_name, old_name, self.table_name, new_name, + self.table_name, new_name)) + + def _update_definitions(self): + # Fetch columns definitions from the table + self.cursor.execute("SELECT column_name, character_maximum_length, " \ + "data_type, is_nullable, column_default " \ + "FROM information_schema.columns " \ + "WHERE table_schema = %s AND table_name = %s", + (self.cursor.database_name, self.table_name)) + self._columns = {} + for line in self.cursor.fetchall(): + column, size, typname, nullable, default = line + self._columns[column] = { + 'size': size, + 'typname': typname, + 'nullable': nullable == 'YES' and True or False, + 'default': default, + } + + # fetch constraints for the table + self.cursor.execute("SELECT constraint_name, constraint_type " \ + "FROM information_schema.table_constraints " \ + "WHERE table_schema = %s AND table_name = %s", + (self.cursor.database_name, self.table_name)) + self._constraints = [] + self._fkeys = [] + for line in self.cursor.fetchall(): + conname, contype = line + if contype not in ('PRIMARY KEY', 'FOREIGN KEY'): + self._constraints.append(conname) + elif contype == 'FOREIGN KEY': + self._fkeys.append(conname) + + # Fetch indexes defined for the table + self.cursor.execute('SHOW INDEXES FROM `%s`' % self.table_name) + self._indexes = list(set(x[2] for x in self.cursor.fetchall() + if x[2] != 'PRIMARY')) + + # Keep track of which module created each field + self._field2module = {} + if self.object_name is not None: + self.cursor.execute('SELECT f.name, f.module '\ + 'FROM ir_model_field f '\ + 'JOIN ir_model m on (f.model=m.id) '\ + 'WHERE m.model = %s', + (self.object_name,) + ) + for line in self.cursor.fetchall(): + self._field2module[line[0]] = line[1] + + def alter_size(self, column_name, column_type): + self.cursor.execute('ALTER TABLE `%s` ' \ + 'MODIFY COLUMN `%s` %s' % \ + (self.table_name, column_name, + self._column_definition(column_name))) + self._update_definitions() + + def alter_type(self, column_name, column_type): + self.cursor.execute('ALTER TABLE `%s` ' \ + 'MODIFY COLUMN `%s` %s' % \ + (self.table_name, column_name, + self._column_definition(column_name, typname=column_type))) + self._update_definitions() + + def db_default(self, column_name, value): + self.cursor.execute('ALTER TABLE `%s` ' + 'MODIFY COLUMN `%s` %s' % \ + (self.table_name, column_name, + self._column_definition(column_name, + default=value))) + self._update_definitions() + + def add_raw_column(self, column_name, column_type, column_format, + default_fun=None, field_size=None, migrate=True, string=''): + if self.column_exist(column_name): + if not migrate: + return + base_type = column_type[0].lower() + if base_type != self._columns[column_name]['typname']: + if (self._columns[column_name]['typname'], base_type) in ( + ('varchar', 'text'), + ('text', 'varchar'), + ('date', 'timestamp'), + ('bigint', 'float'), + ('int', 'bigint'), + ('tinyint', 'bool'), + ('decimal', 'numeric'), + ): + self.alter_type(column_name, base_type) + else: + logging.getLogger('init').warning( + 'Unable to migrate column %s on table %s ' \ + 'from %s to %s.' % \ + (column_name, self.table_name, + self._columns[column_name]['typname'], base_type)) + if base_type == 'varchar' \ + and self._columns[column_name]['typname'] == 'varchar': + # Migrate size + if field_size is None: + if self._columns[column_name]['size'] != 255: + self.alter_size(column_name, base_type) + elif self._columns[column_name]['size'] == field_size: + pass + else: + logging.getLogger('init').warning( + 'Unable to migrate column %s on table %s ' \ + 'from varchar(%s) to varchar(%s).' % \ + (column_name, self.table_name, + self._columns[column_name]['size'] > 0 and \ + self._columns[column_name]['size'] or 255, + field_size)) + return + + column_type = column_type[1] + self.cursor.execute('ALTER TABLE `%s` ADD COLUMN `%s` %s' % + (self.table_name, column_name, column_type)) + + if column_format: + # check if table is non-empty: + self.cursor.execute('SELECT 1 FROM `%s` limit 1' % self.table_name) + if self.cursor.rowcount: + # Populate column with default values: + default = None + if default_fun is not None: + default = default_fun(self.cursor, 0, {}) + self.cursor.execute('UPDATE `' + self.table_name + '` '\ + 'SET `' + column_name + '` = %s', + (column_format(default),)) + + self._update_definitions() + + def add_fk(self, column_name, reference, on_delete=None): + if on_delete is None: + on_delete = 'SET NULL' + conname = '%s_%s_fkey' % (self.table_name, column_name) + if conname in self._fkeys: + self.drop_fk(column_name) + self.cursor.execute('ALTER TABLE `%s` ' \ + 'ADD CONSTRAINT `%s` FOREIGN KEY (`%s`) ' \ + 'REFERENCES `%s` (id) ON DELETE %s' % + (self.table_name, conname, column_name, reference, on_delete)) + self._update_definitions() + + def drop_fk(self, column_name, table=None): + conname = '%s_%s_fkey' % (self.table_name, column_name) + if conname not in self._fkeys: + return + self.cursor.execute('ALTER TABLE `%s` ' \ + 'DROP FOREIGN KEY `%s`' % (self.table_name, conname)) + self._update_definitions() + + def index_action(self, column_name, action='add', table=None): + if isinstance(column_name, basestring): + column_name = [column_name] + index_name = (table or self.table_name) + "_" + '_'.join(column_name) \ + + "_index" + + for k in column_name: + if k in self._columns: + if self._columns[k]['typname'] in ('text', 'blob'): + return + + if action == 'add': + if index_name in self._indexes: + return + self.cursor.execute('CREATE INDEX `' + index_name + '` ' \ + 'ON `' + self.table_name + '` ' \ + '( ' + \ + ','.join(['`' + x + '`' for x in column_name]) + \ + ')') + self._update_definitions() + elif action == 'remove': + if len(column_name) == 1: + if self._field2module.get(column_name[0], self.module_name) \ + != self.module_name: + return + + if index_name in self._indexes: + self.cursor.execute('DROP INDEX `%s` ' % (index_name,)) + self._update_definitions() + else: + raise Exception('Index action not supported!') + + def not_null_action(self, column_name, action='add'): + if not self.column_exist(column_name): + return + + if action == 'add': + if not self._columns[column_name]['nullable']: + return + self.cursor.execute('SELECT id FROM `%s` ' \ + 'WHERE `%s` IS NULL' % \ + (self.table_name, column_name)) + if not self.cursor.rowcount: + self.cursor.execute('ALTER TABLE `%s` ' \ + 'MODIFY COLUMN `%s` %s' % + (self.table_name, column_name, + self._column_definition(column_name, + nullable=False))) + self._update_definitions() + else: + logging.getLogger('init').warning( + 'Unable to set column %s ' \ + 'of table %s not null !\n'\ + 'Try to re-run: ' \ + 'trytond.py --update=module\n' \ + 'If it doesn\'t work, update records ' \ + 'and execute manually:\n' \ + 'ALTER TABLE `%s` MODIFY COLUMN `%s` %s' % \ + (column_name, self.table_name, self.table_name, + column_name, self._column_definition(column_name, + nullable=False))) + elif action == 'remove': + if self._columns[column_name]['nullable']: + return + if self._field2module.get(column_name, self.module_name) \ + != self.module_name: + return + self.cursor.execute('ALTER TABLE `%s` ' \ + 'MODIFY COLUMN `%s` %s' % + (self.table_name, column_name, + self._column_definition(column_name, nullable=True))) + self._update_definitions() + else: + raise Exception('Not null action not supported!') + + def add_constraint(self, ident, constraint, exception=False): + ident = self.table_name + "_" + ident + if ident in self._constraints: + # This constrain already exists + return + try: + self.cursor.execute('ALTER TABLE `%s` ' \ + 'ADD CONSTRAINT `%s` %s' % \ + (self.table_name, ident, constraint,)) + except: + if exception: + raise + logging.getLogger('init').warning( + 'unable to add \'%s\' constraint on table %s !\n' \ + 'If you want to have it, you should update the records ' \ + 'and execute manually:\n'\ + 'ALTER table `%s` ADD CONSTRAINT `%s` %s' % \ + (constraint, self.table_name, self.table_name, + ident, constraint,)) + self._update_definitions() + + def drop_constraint(self, ident, exception=False, table=None): + ident = (table or self.table_name) + "_" + ident + if ident not in self._constraints: + return + try: + self.cursor.execute('ALTER TABLE `%s` ' \ + 'DROP CONSTRAINT `%s`' % \ + (self.table_name, ident)) + except: + if exception: + raise + logging.getLogger('init').warning( + 'unable to drop \'%s\' constraint on table %s!' % \ + (ident, self.table_name)) + self._update_definitions() + + def drop_column(self, column_name, exception=False): + if not self.column_exist(column_name): + return + try: + self.cursor.execute( + 'ALTER TABLE `%s` DROP COLUMN `%s`' % + (self.table_name, column_name)) + + except: + if exception: + raise + logging.getLogger('init').warning( + 'unable to drop \'%s\' column on table %s!' % \ + (column_name, self.table_name)) + self._update_definitions() + + @staticmethod + def drop_table(cursor, model, table, cascade=False): + cursor.execute('DELETE from ir_model_data where '\ + 'model = %s', model) + + query = 'DROP TABLE `%s`' % table + if cascade: + query = query + ' CASCADE' + cursor.execute(query) + + def _column_definition(self, column_name, typname=None, nullable=None, + size=None, default=None): + if typname is None: + typname = self._columns[column_name]['typname'] + if nullable is None: + nullable = self._columns[column_name]['nullable'] + if size is None: + size = self._columns[column_name]['size'] + if default is None: + default = self._columns[column_name]['default'] + res = '' + if typname == 'varchar': + if int(size) > 255: + size = 255 + res = 'varchar(%s)' % str(size) + elif typname == 'decimal': + res = 'decimal(16, 8)' + else: + res = typname + # Default value for timestamp doesn't work + if typname == 'timestamp' and not nullable: + nullable = True + if nullable: + res += ' NULL' + else: + res += ' NOT NULL' + if default is not None: + res += ' DEFAULT %s' % default + return res diff --git a/trytond/backend/postgresql/database.py b/trytond/backend/postgresql/database.py index f49cf3e419..efe4282b5f 100644 --- a/trytond/backend/postgresql/database.py +++ b/trytond/backend/postgresql/database.py @@ -380,8 +380,8 @@ class Cursor(CursorInterface): self.cursor.execute('SELECT last_value FROM "' + table + '_id_seq"') return self.cursor.fetchone()[0] - def has_lock(self): - return True + def lock(self, table): + self.cursor.execute('LOCK "%s"' % table) def has_constraint(self): return True diff --git a/trytond/backend/sqlite/database.py b/trytond/backend/sqlite/database.py index e27eee4deb..393170ac89 100644 --- a/trytond/backend/sqlite/database.py +++ b/trytond/backend/sqlite/database.py @@ -328,8 +328,8 @@ class Cursor(CursorInterface): self.cursor.execute('SELECT last_insert_rowid()') return self.cursor.fetchone()[0] - def has_lock(self): - return False + def lock(self, table): + pass def has_constraint(self): return False diff --git a/trytond/ir/action.py b/trytond/ir/action.py index 0fdc54978d..e8b2650fb0 100644 --- a/trytond/ir/action.py +++ b/trytond/ir/action.py @@ -349,6 +349,7 @@ class ActionReport(ModelSQL, ModelView): new_id = report.action.id cursor.execute('UPDATE "' + self._table + '" SET id = %s ' \ 'WHERE id = %s', (report.action.id, report.id)) + cursor.update_auto_increment(self._table, report.action.id) ModelStorage.delete(self, cursor, user, report.id) self.write(cursor, user, new_id, later, context=context) return new_id @@ -541,6 +542,7 @@ class ActionActWindow(ModelSQL, ModelView): new_id = act_window.action.id cursor.execute('UPDATE "' + self._table + '" SET id = %s ' \ 'WHERE id = %s', (act_window.action.id, act_window.id)) + cursor.update_auto_increment(self._table, act_window.action.id) ModelStorage.delete(self, cursor, user, act_window.id) self.write(cursor, user, new_id, later, context=context) return new_id @@ -618,6 +620,7 @@ class ActionWizard(ModelSQL, ModelView): new_id = wizard.action.id cursor.execute('UPDATE "' + self._table + '" SET id = %s ' \ 'WHERE id = %s', (wizard.action.id, wizard.id)) + cursor.update_auto_increment(self._table, wizard.action.id) ModelStorage.delete(self, cursor, user, wizard.id) self.write(cursor, user, new_id, later, context=context) return new_id @@ -763,6 +766,7 @@ class ActionURL(ModelSQL, ModelView): new_id = url.action.id cursor.execute('UPDATE "' + self._table + '" SET id = %s ' \ 'WHERE id = %s', (url.action.id, url.id)) + cursor.update_auto_increment(self._table, url.action.id) ModelStorage.delete(self, cursor, user, url.id) self.write(cursor, user, new_id, later, context=context) return new_id diff --git a/trytond/ir/cron.py b/trytond/ir/cron.py index 4d66fb4edb..06def37545 100644 --- a/trytond/ir/cron.py +++ b/trytond/ir/cron.py @@ -155,8 +155,7 @@ class Cron(ModelSQL, ModelView): return try: try: - if cursor.has_lock(): - cursor.execute('LOCK TABLE ir_cron') + cursor.lock(self._table) cursor.execute('SELECT * FROM ir_cron ' \ 'WHERE numbercall <> 0 ' \ 'AND active ' \ diff --git a/trytond/ir/sequence.py b/trytond/ir/sequence.py index ec0bbf2611..368d28daae 100644 --- a/trytond/ir/sequence.py +++ b/trytond/ir/sequence.py @@ -149,8 +149,7 @@ class SequenceStrict(Sequence): _description = __doc__ def get_id(self, cursor, user, clause, context=None): - if cursor.has_lock(): - cursor.execute('LOCK TABLE "' + self._table + '"') + cursor.lock(self._table) return super(SequenceStrict, self).get_id(cursor, user, clause, context=context)