From 74ed36e41672f7d10adc5e5b094be6b17ceacc85 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Sat, 31 Mar 2012 02:42:15 +0200 Subject: [PATCH] [IMP] orm,ir.model: improve registration and deletion of schema ext_ids Also handle the very common cases of inheriting classes that must *not* trigger deletion of the m2m tables and constraints of their parent classes! bzr revid: odo@openerp.com-20120331004215-413lcxaxvg0u6oxw --- openerp/addons/base/ir/ir_model.py | 88 +++++++++++++++++++----------- openerp/osv/orm.py | 42 +++++++------- 2 files changed, 75 insertions(+), 55 deletions(-) diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 845dedc7a51..01edded16b7 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -26,10 +26,11 @@ import types from openerp.osv import fields,osv from openerp import netsvc, pooler, tools -from openerp.osv.orm import except_orm, browse_record from openerp.tools.safe_eval import safe_eval as eval from openerp.tools import config from openerp.tools.translate import _ +from openerp.osv.orm import except_orm, browse_record, EXT_ID_PREFIX_FK, \ + EXT_ID_PREFIX_M2M_TABLE, EXT_ID_PREFIX_CONSTRAINT _logger = logging.getLogger(__name__) @@ -830,6 +831,16 @@ class ir_model_data(osv.osv): return True def _module_data_uninstall(self, cr, uid, ids, context=None): + """Deletes all the records referenced by the ir.model.data entries + ``ids`` along with their corresponding database backed (including + dropping tables, columns, FKs, etc, as long as there is no other + ir.model.data entry holding a reference to them (which indicates that + they are still owned by another module). + Attempts to perform the deletion in an appropriate order to maximize + the chance of gracefully deleting all records. + This step is performed as part of the full uninstallation of a module. + """ + if uid != 1 and not self.pool.get('ir.model.access').check_groups(cr, uid, "base.group_system"): raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module'))) @@ -846,30 +857,42 @@ class ir_model_data(osv.osv): model = data.model res_id = data.res_id model_obj = self.pool.get(model) - name = data.name - # FIXME: replace custom keys with constants - if str(name).startswith('foreign_key_'): - name = name[12:] - # test if FK exists - cr.execute('select conname from pg_constraint where contype=%s and conname=%s',('f', name),) - if cr.fetchall(): - cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model,name),) - _logger.info('Drop FK CONSTRAINT %s@%s', name, model) + name = tools.ustr(data.name) + + if name.startswith(EXT_ID_PREFIX_FK) or name.startswith(EXT_ID_PREFIX_M2M_TABLE)\ + or name.startswith(EXT_ID_PREFIX_CONSTRAINT): + # double-check we are really going to delete all the owners of this schema element + cr.execute("""SELECT id from ir_model_data where name = %s and res_id IS NULL""", (data.name,)) + external_ids = [x[0] for x in cr.fetchall()] + if (set(external_ids)-ids_set): + # as installed modules have defined this element we must not delete it! + continue + + if name.startswith(EXT_ID_PREFIX_FK): + name = name[len(EXT_ID_PREFIX_FK):] + # test if FK exists on this table (it could be on a related m2m table, in which case we ignore it) + cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid) + WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table)) + if cr.fetchone(): + cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),) + _logger.info('Dropped FK CONSTRAINT %s@%s', name, model) continue - if str(name).startswith('table_'): - cr.execute("SELECT table_name FROM information_schema.tables WHERE table_name='%s'"%(name[6:])) - column_name = cr.fetchone() - if column_name: - to_drop_table.append(name[6:]) + if name.startswith(EXT_ID_PREFIX_M2M_TABLE): + name = name[len(EXT_ID_PREFIX_M2M_TABLE):] + cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,)) + if cr.fetchone() and not name in to_drop_table: + to_drop_table.append(name) continue - if str(name).startswith('constraint_'): + if name.startswith(EXT_ID_PREFIX_CONSTRAINT): + name = name[len(EXT_ID_PREFIX_CONSTRAINT):] # test if constraint exists - cr.execute('select conname from pg_constraint where contype=%s and conname=%s',('u', name),) - if cr.fetchall(): - cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table,name[11:]),) - _logger.info('Drop CONSTRAINT %s@%s', name[11:], model) + cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid) + WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table)) + if cr.fetchone(): + cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),) + _logger.info('Dropped CONSTRAINT %s@%s', name, model) continue pair_to_unlink = (model, res_id) @@ -877,21 +900,24 @@ class ir_model_data(osv.osv): to_unlink.append(pair_to_unlink) if model == 'workflow.activity': + # Special treatment for workflow activities: temporarily revert their + # incoming transition and trigger an update to force all workflow items + # to move out before deleting them cr.execute('select res_type,res_id from wkf_instance where id IN (select inst_id from wkf_workitem where act_id=%s)', (res_id,)) wkf_todo.extend(cr.fetchall()) cr.execute("update wkf_transition set condition='True', group_id=NULL, signal=NULL,act_to=act_from,act_from=%s where act_to=%s", (res_id,res_id)) + wf_service = netsvc.LocalService("workflow") for model,res_id in wkf_todo: - wf_service = netsvc.LocalService("workflow") try: wf_service.trg_write(uid, model, res_id, cr) except: - _logger.info('Unable to process workflow %s@%s', res_id, model) + _logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model) - # drop relation .table - for model in to_drop_table: - cr.execute('DROP TABLE %s CASCADE'% (model),) - _logger.info('Dropping table %s', model) + # drop m2m relation tables + for table in to_drop_table: + cr.execute('DROP TABLE %s CASCADE'% (table),) + _logger.info('Dropped table %s', table) for (model, res_id) in to_unlink: if model in ('ir.model','ir.model.fields', 'ir.model.data'): @@ -938,13 +964,11 @@ class ir_model_data(osv.osv): """ if not modules: return True - modules = list(modules) - module_in = ",".join(["%s"] * len(modules)) - process_query = 'select id,name,model,res_id,module from ir_model_data where module IN (' + module_in + ')' - process_query+= ' and noupdate=%s' to_unlink = [] - cr.execute(process_query, modules + [False]) - for (id, name, model, res_id,module) in cr.fetchall(): + cr.execute("""SELECT id,name,model,res_id,module FROM ir_model_data + WHERE module IN %s AND res_id IS NOT NULL AND noupdate=%s""", + (tuple(modules), False)) + for (id, name, model, res_id, module) in cr.fetchall(): if (module,name) not in self.loads: to_unlink.append((model,res_id)) if not config.get('import_partial'): diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 5e2b9e5ae4c..cad676bbdce 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -70,6 +70,11 @@ _schema = logging.getLogger(__name__ + '.schema') # List of etree._Element subclasses that we choose to ignore when parsing XML. from openerp.tools import SKIPPED_ELEMENT_TYPES +# Prefixes for external IDs of schema elements +EXT_ID_PREFIX_FK = "_foreign_key_" +EXT_ID_PREFIX_M2M_TABLE = "_m2m_rel_table_" +EXT_ID_PREFIX_CONSTRAINT = "_constraint_" + regex_order = re.compile('^(([a-z0-9_]+|"[a-z0-9_]+")( *desc| *asc)?( *, *|))+$', re.I) regex_object_name = re.compile(r'^[a-z0-9_.]+$') @@ -100,10 +105,10 @@ def transfer_node_to_modifiers(node, modifiers, context=None, in_tree_view=False if node.get('states'): if 'invisible' in modifiers and isinstance(modifiers['invisible'], list): - # TODO combine with AND or OR, use implicit AND for now. - modifiers['invisible'].append(('state', 'not in', node.get('states').split(','))) + # TODO combine with AND or OR, use implicit AND for now. + modifiers['invisible'].append(('state', 'not in', node.get('states').split(','))) else: - modifiers['invisible'] = [('state', 'not in', node.get('states').split(','))] + modifiers['invisible'] = [('state', 'not in', node.get('states').split(','))] for a in ('invisible', 'readonly', 'required'): if node.get(a): @@ -2713,6 +2718,14 @@ class BaseModel(object): _schema.debug("Table '%s': column '%s': dropped NOT NULL constraint", self._table, column['attname']) + # quick creation of ir.model.data entry to make uninstall of schema elements easier + def _make_ext_id(self, cr, ext_id): + cr.execute('SELECT 1 FROM ir_model_data WHERE name=%s AND module=%s', (ext_id, self._module)) + if not cr.rowcount: + cr.execute("""INSERT INTO ir_model_data (name,date_init,date_update,module,model) + VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC', %s, %s)""", + (ext_id, self._module, self._name)) + # checked version: for direct m2o starting from `self` def _m2o_add_foreign_key_checked(self, source_field, dest_model, ondelete): assert self.is_transient() or not dest_model.is_transient(), \ @@ -3052,12 +3065,7 @@ class BaseModel(object): """ Create the foreign keys recorded by _auto_init. """ for t, k, r, d in self._foreign_keys: cr.execute('ALTER TABLE "%s" ADD FOREIGN KEY ("%s") REFERENCES "%s" ON DELETE %s' % (t, k, r, d)) - name_id = "foreign_key_"+t+"_"+k+"_fkey" - cr.execute('select * from ir_model_data where name=%s and module=%s', (name_id, self._module)) - if not cr.rowcount: - cr.execute("INSERT INTO ir_model_data (name,date_init,date_update,module, model) VALUES (%s, now(), now(), %s, %s)", \ - (name_id, self._module, t) - ) + self._make_ext_id(cr, "%s%s_%s_fkey" % (EXT_ID_PREFIX_FK, t, k)) cr.commit() del self._foreign_keys @@ -3146,6 +3154,7 @@ class BaseModel(object): def _m2m_raise_or_create_relation(self, cr, f): m2m_tbl, col1, col2 = f._sql_names(self) + self._make_ext_id(cr, EXT_ID_PREFIX_M2M_TABLE + m2m_tbl) cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (m2m_tbl,)) if not cr.dictfetchall(): if not self.pool.get(f._obj): @@ -3153,14 +3162,6 @@ class BaseModel(object): dest_model = self.pool.get(f._obj) ref = dest_model._table cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s")) WITH OIDS' % (m2m_tbl, col1, col2, col1, col2)) - #create many2many references - name_id = 'table_'+m2m_tbl - cr.execute('select * from ir_model_data where name=%s and module=%s', (name_id, self._module)) - if not cr.rowcount: - cr.execute("INSERT INTO ir_model_data (name,date_init,date_update,module, model) VALUES (%s, now(), now(), %s, %s)", \ - (name_id, self._module, self._name) - ) - # self.pool.get('ir.model.data')._update(cr, 1, self._name, self._module, {}, 'table_'+m2m_tbl, store=True, noupdate=False, mode='init', res_id=False, context=None) # create foreign key references with ondelete=cascade, unless the targets are SQL views cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (ref,)) if not cr.fetchall(): @@ -3189,6 +3190,7 @@ class BaseModel(object): for (key, con, _) in self._sql_constraints: conname = '%s_%s' % (self._table, key) + self._make_ext_id(cr, EXT_ID_PREFIX_CONSTRAINT + conname) cr.execute("SELECT conname, pg_catalog.pg_get_constraintdef(oid, true) as condef FROM pg_constraint where conname=%s", (conname,)) existing_constraints = cr.dictfetchall() sql_actions = { @@ -3229,12 +3231,6 @@ class BaseModel(object): cr.execute(sql_action['query']) cr.commit() _schema.debug(sql_action['msg_ok']) - name_id = 'constraint_'+ conname - cr.execute('select * from ir_model_data where name=%s and module=%s', (name_id, module)) - if not cr.rowcount: - cr.execute("INSERT INTO ir_model_data (name,date_init,date_update,module, model) VALUES (%s, now(), now(), %s, %s)", \ - (name_id, module, self._name) - ) except: _schema.warning(sql_action['msg_err']) cr.rollback()