From c605ffa62aae3993b4603fd0ad9d072530220339 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 9 Feb 2017 14:05:39 +0100 Subject: [PATCH 01/15] [IMP] base: remove management of deprecated indexes Those indexes have been removed in version 10.0 or before, dropping them on module upgrade is no longer necessary. --- odoo/addons/base/ir/ir_filters.py | 1 - odoo/addons/base/ir/ir_translation.py | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/odoo/addons/base/ir/ir_filters.py b/odoo/addons/base/ir/ir_filters.py index a25e734c06e..bfed5939fa6 100644 --- a/odoo/addons/base/ir/ir_filters.py +++ b/odoo/addons/base/ir/ir_filters.py @@ -150,7 +150,6 @@ class IrFilters(models.Model): def _auto_init(self): result = super(IrFilters, self)._auto_init() # Use unique index to implement unique constraint on the lowercase name (not possible using a constraint) - self._cr.execute("DROP INDEX IF EXISTS ir_filters_name_model_uid_unique_index") # drop old index w/o action self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_filters_name_model_uid_unique_action_index'") if not self._cr.fetchone(): self._cr.execute("""CREATE UNIQUE INDEX "ir_filters_name_model_uid_unique_action_index" ON ir_filters diff --git a/odoo/addons/base/ir/ir_translation.py b/odoo/addons/base/ir/ir_translation.py index 5c63e3c7244..de639e10abf 100644 --- a/odoo/addons/base/ir/ir_translation.py +++ b/odoo/addons/base/ir/ir_translation.py @@ -260,16 +260,6 @@ class IrTranslation(models.Model): cr.execute("SELECT indexname FROM pg_indexes WHERE indexname LIKE 'ir_translation_%'") indexes = [row[0] for row in cr.fetchall()] - # Removed because there is a size limit on btree indexed values (problem with column src): - # cr.execute('CREATE INDEX ir_translation_ltns ON ir_translation (name, lang, type, src)') - # cr.execute('CREATE INDEX ir_translation_lts ON ir_translation (lang, type, src)') - # - # Removed because hash indexes are not compatible with postgres streaming replication: - # cr.execute('CREATE INDEX ir_translation_src_hash_idx ON ir_translation USING hash (src)') - if set(indexes) & set(['ir_translation_ltns', 'ir_translation_lts', 'ir_translation_src_hash_idx']): - cr.execute('DROP INDEX IF EXISTS ir_translation_ltns, ir_translation_lts, ir_translation_src_hash_idx') - cr.commit() - # Add separate md5 index on src (no size limit on values, and good performance). if 'ir_translation_src_md5' not in indexes: cr.execute('CREATE INDEX ir_translation_src_md5 ON ir_translation (md5(src))') From 3ae219f7730ae960bc1e171aaec25f7849c3ae98 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 31 Jan 2017 14:05:58 +0100 Subject: [PATCH 02/15] [IMP] base: minimize recomputations triggered by model and field reflection --- odoo/addons/base/ir/ir_model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index e810be3ab7b..b3c35c673e5 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -232,7 +232,7 @@ class IrModel(models.Model): query_insert(cr, self._table, params) record = self.browse(cr.fetchone()) - self._context['todo'].append((10, record.modified, [['name', 'info', 'transient']])) + self._context['todo'].append((10, record.modified, [set(params) - {'model', 'state'}])) if model._module == self._context.get('module'): # self._module is the name of the module that last extended self @@ -707,13 +707,13 @@ class IrModelFields(models.Model): existing[field.name] = dict(vals, id=record.id) return record - if not all(existing_data[key] == val for key, val in vals.iteritems()): + diff = {key for key, val in vals.items() if existing_data[key] != val} + if diff: cr = self.env.cr # update the entry in this table query_update(cr, self._table, vals, ['model', 'name']) record = self.browse(cr.fetchone()) - modified = set(vals) - set(['model', 'name']) - self._context['todo'].append((20, record.modified, [modified])) + self._context['todo'].append((20, record.modified, [diff])) # update existing (for recursive calls) existing_data.update(vals) return record From 3214ca98206e5f99e2ab1d471ce1ef543057c9f6 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 14 Feb 2017 10:36:52 +0100 Subject: [PATCH 03/15] [ADD] test_new_api: add checks on a model's schema --- odoo/addons/test_new_api/__init__.py | 1 + odoo/addons/test_new_api/tests/__init__.py | 1 + odoo/addons/test_new_api/tests/test_schema.py | 534 ++++++++++++++++++ 3 files changed, 536 insertions(+) create mode 100644 odoo/addons/test_new_api/tests/test_schema.py diff --git a/odoo/addons/test_new_api/__init__.py b/odoo/addons/test_new_api/__init__.py index 89d26e2f597..361c3498d66 100644 --- a/odoo/addons/test_new_api/__init__.py +++ b/odoo/addons/test_new_api/__init__.py @@ -1,2 +1,3 @@ # -*- coding: utf-8 -*- import models +import tests diff --git a/odoo/addons/test_new_api/tests/__init__.py b/odoo/addons/test_new_api/tests/__init__.py index b90ccc96825..b368bc5a45d 100644 --- a/odoo/addons/test_new_api/tests/__init__.py +++ b/odoo/addons/test_new_api/tests/__init__.py @@ -6,3 +6,4 @@ from . import test_attributes from . import test_one2many from . import test_ui from . import test_domain +from . import test_schema diff --git a/odoo/addons/test_new_api/tests/test_schema.py b/odoo/addons/test_new_api/tests/test_schema.py new file mode 100644 index 00000000000..45aea9c0494 --- /dev/null +++ b/odoo/addons/test_new_api/tests/test_schema.py @@ -0,0 +1,534 @@ +# -*- coding: utf-8 -*- +from odoo.models import MetaModel +from odoo.tests import common + + +class TestReflection(common.TransactionCase): + """ Test the reflection into 'ir.model', 'ir.model.fields', etc. """ + + def test_models_fields(self): + """ check that all models and fields are reflected as expected. """ + # retrieve the models defined in this module, and check them + model_names = {cls._name for cls in MetaModel.module_to_models['test_new_api']} + ir_models = self.env['ir.model'].search([('model', 'in', list(model_names))]) + self.assertEqual(len(ir_models), len(model_names)) + for ir_model in ir_models: + model = self.env[ir_model.model] + self.assertEqual(ir_model.name, model._description or False) + self.assertEqual(ir_model.state, 'manual' if model._custom else 'base') + self.assertEqual(ir_model.transient, bool(model._transient)) + self.assertItemsEqual(ir_model.mapped('field_id.name'), list(model._fields)) + for ir_field in ir_model.field_id: + field = model._fields[ir_field.name] + self.assertEqual(ir_field.model, field.model_name) + self.assertEqual(ir_field.field_description, field.string) + self.assertEqual(ir_field.help, field.help or False) + self.assertEqual(ir_field.ttype, field.type) + self.assertEqual(ir_field.state, 'manual' if field.manual else 'base') + self.assertEqual(ir_field.index, bool(field.index)) + self.assertEqual(ir_field.store, bool(field.store)) + self.assertEqual(ir_field.copy, bool(field.copy)) + self.assertEqual(ir_field.related, bool(field.related) and ".".join(field.related)) + self.assertEqual(ir_field.readonly, bool(field.readonly)) + self.assertEqual(ir_field.required, bool(field.required)) + self.assertEqual(ir_field.selectable, bool(field.search or field.store)) + self.assertEqual(ir_field.translate, bool(field.translate)) + if field.relational: + self.assertEqual(ir_field.relation, field.comodel_name) + if field.type == 'one2many' and field.store: + self.assertEqual(ir_field.relation_field, field.inverse_name) + if field.type == 'many2many' and field.store: + self.assertEqual(ir_field.relation_table, field.relation) + self.assertEqual(ir_field.column1, field.column1) + self.assertEqual(ir_field.column2, field.column2) + relation = self.env['ir.model.relation'].search([('name', '=', field.relation)]) + self.assertTrue(relation) + self.assertIn(relation.model.model, [field.model_name, field.comodel_name]) + + +class TestSchema(common.TransactionCase): + + def get_table_data(self, tablename): + query = ''' SELECT table_catalog, table_schema, table_name, table_type, + user_defined_type_catalog, user_defined_type_schema, + user_defined_type_name, is_insertable_into, is_typed + FROM information_schema.tables + WHERE table_name=%s ''' + self.cr.execute(query, [tablename]) + return self.cr.dictfetchone() + + def get_columns_data(self, tablename): + query = ''' SELECT table_catalog, table_schema, table_name, column_name, + column_default, data_type, is_nullable, is_updatable, + character_maximum_length, numeric_precision, + numeric_precision_radix, numeric_scale, + datetime_precision, udt_catalog, udt_schema, udt_name + FROM information_schema.columns + WHERE table_name=%s ''' + self.cr.execute(query, [tablename]) + return {row['column_name']: row for row in self.cr.dictfetchall()} + + def get_foreign_keys(self, tablename): + query = ''' SELECT a.table_name, a.column_name, + b.table_name, b.column_name, c.delete_rule + FROM information_schema.referential_constraints c, + information_schema.key_column_usage a, + information_schema.constraint_column_usage b + WHERE a.constraint_schema=c.constraint_schema + AND a.constraint_name=c.constraint_name + AND b.constraint_schema=c.constraint_schema + AND b.constraint_name=c.constraint_name + AND a.table_name=%s ''' + self.cr.execute(query, [tablename]) + return self.cr.fetchall() + + def test_00_table(self): + """ check the database schema of a model """ + model = self.env['test_new_api.foo'] + self.assertEqual(model._table, 'test_new_api_foo') + + # retrieve schema data about that table + table_data = self.get_table_data('test_new_api_foo') + self.assertEqual(table_data, { + 'is_insertable_into': u'YES', + 'is_typed': u'NO', + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_foo', + 'table_schema': u'public', + 'table_type': u'BASE TABLE', + 'user_defined_type_catalog': None, + 'user_defined_type_name': None, + 'user_defined_type_schema': None, + }) + + # retrieve schema data about the table's columns + columns_data = self.get_columns_data('test_new_api_foo') + self.assertEqual(set(columns_data), + {'id', 'create_date', 'create_uid', 'write_date', + 'write_uid', 'name', 'value1', 'value2'}) + + # retrieve schema data about the table's foreign keys + foreign_keys = self.get_foreign_keys('test_new_api_foo') + self.assertItemsEqual(foreign_keys, [ + ('test_new_api_foo', 'create_uid', 'res_users', 'id', 'SET NULL'), + ('test_new_api_foo', 'write_uid', 'res_users', 'id', 'SET NULL'), + ]) + + def test_10_boolean(self): + """ check the database representation of a boolean field """ + model = self.env['test_new_api.message'] + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['important'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'important', + 'data_type': u'boolean', + 'datetime_precision': None, + 'is_nullable': u'YES', + 'is_updatable': u'YES', + 'numeric_precision': None, + 'numeric_precision_radix': None, + 'numeric_scale': None, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_message', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'bool', + 'udt_schema': u'pg_catalog', + }) + + def test_10_integer(self): + """ check the database representation of an integer field """ + model = self.env['test_new_api.category'] + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['color'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'color', + 'data_type': u'integer', + 'datetime_precision': None, + 'is_nullable': u'YES', + 'is_updatable': u'YES', + 'numeric_precision': 32, + 'numeric_precision_radix': 2, + 'numeric_scale': 0, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_category', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'int4', + 'udt_schema': u'pg_catalog', + }) + + def test_10_float(self): + """ check the database representation of a float field """ + model = self.env['test_new_api.mixed'] + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['number'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'number', + 'data_type': u'numeric', + 'datetime_precision': None, + 'is_nullable': u'YES', + 'is_updatable': u'YES', + 'numeric_precision': None, + 'numeric_precision_radix': 10, + 'numeric_scale': None, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_mixed', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'numeric', + 'udt_schema': u'pg_catalog', + }) + + def test_10_monetary(self): + """ check the database representation of a monetary field """ + model = self.env['test_new_api.mixed'] + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['amount'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'amount', + 'data_type': u'numeric', + 'datetime_precision': None, + 'is_nullable': u'YES', + 'is_updatable': u'YES', + 'numeric_precision': None, + 'numeric_precision_radix': 10, + 'numeric_scale': None, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_mixed', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'numeric', + 'udt_schema': u'pg_catalog', + }) + + def test_10_char(self): + """ check the database representation of a char field """ + model = self.env['res.country'] + self.assertFalse(type(model).code.required) + self.assertEqual(type(model).code.size, 2) + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['code'], { + 'character_maximum_length': 2, + 'column_default': None, + 'column_name': u'code', + 'data_type': u'character varying', + 'datetime_precision': None, + 'is_nullable': u'YES', + 'is_updatable': u'YES', + 'numeric_precision': None, + 'numeric_precision_radix': None, + 'numeric_scale': None, + 'table_catalog': self.cr.dbname, + 'table_name': u'res_country', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'varchar', + 'udt_schema': u'pg_catalog', + }) + + model = self.env['test_new_api.message'] + self.assertFalse(type(model).name.required) + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['name'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'name', + 'data_type': u'character varying', + 'datetime_precision': None, + 'is_nullable': u'YES', + 'is_updatable': u'YES', + 'numeric_precision': None, + 'numeric_precision_radix': None, + 'numeric_scale': None, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_message', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'varchar', + 'udt_schema': u'pg_catalog', + }) + + model = self.env['test_new_api.category'] + self.assertTrue(type(model).name.required) + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['name'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'name', + 'data_type': u'character varying', + 'datetime_precision': None, + 'is_nullable': u'NO', + 'is_updatable': u'YES', + 'numeric_precision': None, + 'numeric_precision_radix': None, + 'numeric_scale': None, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_category', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'varchar', + 'udt_schema': u'pg_catalog', + }) + + def test_10_text(self): + """ check the database representation of a text field """ + model = self.env['test_new_api.message'] + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['body'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'body', + 'data_type': u'text', + 'datetime_precision': None, + 'is_nullable': u'YES', + 'is_updatable': u'YES', + 'numeric_precision': None, + 'numeric_precision_radix': None, + 'numeric_scale': None, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_message', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'text', + 'udt_schema': u'pg_catalog', + }) + + def test_10_html(self): + """ check the database representation of an html field """ + model = self.env['test_new_api.mixed'] + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['comment1'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'comment1', + 'data_type': u'text', + 'datetime_precision': None, + 'is_nullable': u'YES', + 'is_updatable': u'YES', + 'numeric_precision': None, + 'numeric_precision_radix': None, + 'numeric_scale': None, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_mixed', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'text', + 'udt_schema': u'pg_catalog', + }) + + def test_10_date(self): + """ check the database representation of a date field """ + model = self.env['test_new_api.mixed'] + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['date'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'date', + 'data_type': u'date', + 'datetime_precision': 0, + 'is_nullable': u'YES', + 'is_updatable': u'YES', + 'numeric_precision': None, + 'numeric_precision_radix': None, + 'numeric_scale': None, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_mixed', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'date', + 'udt_schema': u'pg_catalog', + }) + + def test_10_datetime(self): + """ check the database representation of a datetime field """ + model = self.env['res.currency.rate'] + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['name'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'name', + 'data_type': u'timestamp without time zone', + 'datetime_precision': 6, + 'is_nullable': u'NO', + 'is_updatable': u'YES', + 'numeric_precision': None, + 'numeric_precision_radix': None, + 'numeric_scale': None, + 'table_catalog': self.cr.dbname, + 'table_name': u'res_currency_rate', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'timestamp', + 'udt_schema': u'pg_catalog', + }) + + model = self.env['test_new_api.mixed'] + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['create_date'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'create_date', + 'data_type': u'timestamp without time zone', + 'datetime_precision': 6, + 'is_nullable': u'YES', + 'is_updatable': u'YES', + 'numeric_precision': None, + 'numeric_precision_radix': None, + 'numeric_scale': None, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_mixed', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'timestamp', + 'udt_schema': u'pg_catalog', + }) + + def test_10_selection(self): + """ check the database representation of a selection field """ + model = self.env['test_new_api.mixed'] + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['lang'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'lang', + 'data_type': u'character varying', + 'datetime_precision': None, + 'is_nullable': u'YES', + 'is_updatable': u'YES', + 'numeric_precision': None, + 'numeric_precision_radix': None, + 'numeric_scale': None, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_mixed', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'varchar', + 'udt_schema': u'pg_catalog', + }) + + def test_10_reference(self): + """ check the database representation of a reference field """ + model = self.env['test_new_api.mixed'] + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['reference'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'reference', + 'data_type': u'character varying', + 'datetime_precision': None, + 'is_nullable': u'YES', + 'is_updatable': u'YES', + 'numeric_precision': None, + 'numeric_precision_radix': None, + 'numeric_scale': None, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_mixed', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'varchar', + 'udt_schema': u'pg_catalog', + }) + + def test_10_many2one(self): + """ check the database representation of a many2one field """ + model = self.env['test_new_api.mixed'] + columns_data = self.get_columns_data(model._table) + self.assertEqual(columns_data['currency_id'], { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'currency_id', + 'data_type': u'integer', + 'datetime_precision': None, + 'is_nullable': u'YES', + 'is_updatable': u'YES', + 'numeric_precision': 32, + 'numeric_precision_radix': 2, + 'numeric_scale': 0, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_mixed', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'int4', + 'udt_schema': u'pg_catalog', + }) + foreign_keys = self.get_foreign_keys(model._table) + self.assertIn( + ('test_new_api_mixed', 'currency_id', 'res_currency', 'id', 'SET NULL'), + foreign_keys, + ) + + def test_10_many2many(self): + """ check the database representation of a many2many field """ + model = self.env['test_new_api.discussion'] + field = type(model).categories + comodel = self.env[field.comodel_name] + self.assertTrue(field.relation) + self.assertTrue(field.column1) + self.assertTrue(field.column2) + + columns_data = self.get_columns_data(model._table) + self.assertNotIn('categories', columns_data) + + table_data = self.get_table_data(field.relation) + self.assertEqual(table_data, { + 'is_insertable_into': u'YES', + 'is_typed': u'NO', + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_discussion_category', + 'table_schema': u'public', + 'table_type': u'BASE TABLE', + 'user_defined_type_catalog': None, + 'user_defined_type_name': None, + 'user_defined_type_schema': None, + }) + + columns_data = self.get_columns_data(field.relation) + self.assertEqual(columns_data, { + field.column1: { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'discussion', + 'data_type': u'integer', + 'datetime_precision': None, + 'is_nullable': u'NO', + 'is_updatable': u'YES', + 'numeric_precision': 32, + 'numeric_precision_radix': 2, + 'numeric_scale': 0, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_discussion_category', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'int4', + 'udt_schema': u'pg_catalog', + }, + field.column2: { + 'character_maximum_length': None, + 'column_default': None, + 'column_name': u'category', + 'data_type': u'integer', + 'datetime_precision': None, + 'is_nullable': u'NO', + 'is_updatable': u'YES', + 'numeric_precision': 32, + 'numeric_precision_radix': 2, + 'numeric_scale': 0, + 'table_catalog': self.cr.dbname, + 'table_name': u'test_new_api_discussion_category', + 'table_schema': u'public', + 'udt_catalog': self.cr.dbname, + 'udt_name': u'int4', + 'udt_schema': u'pg_catalog' + }, + }) + + foreign_keys = self.get_foreign_keys(field.relation) + self.assertItemsEqual(foreign_keys, [ + (field.relation, field.column1, model._table, 'id', 'CASCADE'), + (field.relation, field.column2, comodel._table, 'id', 'CASCADE'), + ]) From d0591c18e589187cce49a9a5e810825d8f085811 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 31 Jan 2017 10:00:12 +0100 Subject: [PATCH 04/15] [REF] base: refactor the API of model, field, constraint and relation reflection Simplify the API as much as possible, and delegate all the implementation details to the models `ir.model`, `ir.model.fields`, `ir.model.constraint` and `ir.model.relation`. --- addons/base_sparse_field/models/models.py | 12 +- addons/website_form/models/models.py | 2 +- odoo/addons/base/ir/ir_model.py | 131 +++++++++++++++++----- odoo/fields.py | 3 +- odoo/models.py | 116 +++---------------- 5 files changed, 129 insertions(+), 135 deletions(-) diff --git a/addons/base_sparse_field/models/models.py b/addons/base_sparse_field/models/models.py index b0ea825b7d3..7ad34bcd76b 100644 --- a/addons/base_sparse_field/models/models.py +++ b/addons/base_sparse_field/models/models.py @@ -27,19 +27,19 @@ class IrModelFields(models.Model): return super(IrModelFields, self).write(vals) - def _reflect_values(self, field, existing): - vals = super(IrModelFields, self)._reflect_values(field, existing) + def _reflect_field_params(self, field): + params = super(IrModelFields, self)._reflect_field_params(field) - vals['serialization_field_id'] = None + params['serialization_field_id'] = None if getattr(field, 'sparse', None): model = self.env[field.model_name] serialization_field = model._fields.get(field.sparse) if serialization_field is None: raise UserError(_("Serialization field `%s` not found for sparse field `%s`!") % (field.sparse, field.name)) - serialization_record = self._reflect(serialization_field, existing) - vals['serialization_field_id'] = serialization_record.id + serialization_record = self._reflect_field(serialization_field) + params['serialization_field_id'] = serialization_record.id - return vals + return params def _instanciate_attrs(self, field_data, partial): attrs = super(IrModelFields, self)._instanciate_attrs(field_data, partial) diff --git a/addons/website_form/models/models.py b/addons/website_form/models/models.py index f1a80abbb68..7ac2c38d715 100644 --- a/addons/website_form/models/models.py +++ b/addons/website_form/models/models.py @@ -80,7 +80,7 @@ class website_form_model_fields(models.Model): ' SET website_form_blacklisted=true' ' WHERE website_form_blacklisted IS NULL') # add an SQL-level default value on website_form_blacklisted to that - # pure-SQL ir.model.field creations (e.g. in _field_create) generate + # pure-SQL ir.model.field creations (e.g. in _reflect) generate # the right default value for a whitelist (aka fields should be # blacklisted by default) self._cr.execute('ALTER TABLE ir_model_fields ' diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index b3c35c673e5..b8ad6f51f34 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -212,21 +212,24 @@ class IrModel(models.Model): } return self.create(vals).name_get()[0] - def _reflect(self, model): - """ Reflect the given model and its fields in the models 'ir.model' and - 'ir.model.fields'. Also create entries in 'ir.model.data' if the key - 'module' is passed into the context. - """ - cr = self.env.cr - - # create/update the entries in 'ir.model' and 'ir.model.data' - params = { + def _reflect_model_params(self, model): + """ Return the values to write to the database for the given model. """ + return { 'model': model._name, 'name': model._description, 'info': next(cls.__doc__ for cls in type(model).mro() if cls.__doc__), 'state': 'manual' if model._custom else 'base', 'transient': model._transient, } + + def _reflect_model(self, model): + """ Reflect the given model and return the corresponding record. Also + create entries in 'ir.model.data'. + """ + cr = self.env.cr + + # create/update the entries in 'ir.model' and 'ir.model.data' + params = self._reflect_model_params(model) query_update(cr, self._table, params, ['model']) if not cr.rowcount: query_insert(cr, self._table, params) @@ -654,8 +657,15 @@ class IrModelFields(models.Model): res.append((field.id, '%s (%s)' % (field.field_description, field.model))) return res - def _reflect_values(self, field, existing): - """ Return the values corresponding to the given ``field``. """ + @tools.ormcache('model_name') + def _existing_field_data(self, model_name): + """ Return the given model's existing field data. """ + cr = self._cr + cr.execute("SELECT * FROM ir_model_fields WHERE model=%s", [model_name]) + return {row['name']: row for row in cr.dictfetchall()} + + def _reflect_field_params(self, field): + """ Return the values to write to the database for the given field. """ model = self.env['ir.model']._get(field.model_name) return { 'model_id': model.id, @@ -679,19 +689,18 @@ class IrModelFields(models.Model): 'column2': field.column2 if field.type == 'many2many' else None, } - def _reflect(self, field, existing): - """ Reflect the given field in the model `ir.model.fields` and return - its corresponding record. - """ - vals = self._reflect_values(field, existing) - existing_data = existing.get(field.name) + def _reflect_field(self, field): + """ Reflect the given field and return its corresponding record. """ + fields_data = self._existing_field_data(field.model_name) + field_data = fields_data.get(field.name) + params = self._reflect_field_params(field) - if existing_data is None: + if field_data is None: cr = self.env.cr # create an entry in this table - query_insert(cr, self._table, vals) + query_insert(cr, self._table, params) record = self.browse(cr.fetchone()) - self._context['todo'].append((20, record.modified, [list(vals)])) + self._context['todo'].append((20, record.modified, [list(params)])) # create a corresponding xml id module = field._module or self._context.get('module') if module: @@ -703,24 +712,29 @@ class IrModelFields(models.Model): cr.execute(""" INSERT INTO ir_model_data (module, name, model, res_id, date_init, date_update) VALUES (%s, %s, %s, %s, (now() at time zone 'UTC'), (now() at time zone 'UTC')) """, (module, xmlid, record._name, record.id)) - # update existing (for recursive calls) - existing[field.name] = dict(vals, id=record.id) + # update fields_data (for recursive calls) + fields_data[field.name] = dict(params, id=record.id) return record - diff = {key for key, val in vals.items() if existing_data[key] != val} + diff = {key for key, val in params.items() if field_data[key] != val} if diff: cr = self.env.cr # update the entry in this table - query_update(cr, self._table, vals, ['model', 'name']) + query_update(cr, self._table, params, ['model', 'name']) record = self.browse(cr.fetchone()) self._context['todo'].append((20, record.modified, [diff])) - # update existing (for recursive calls) - existing_data.update(vals) + # update fields_data (for recursive calls) + field_data.update(params) return record else: # nothing to update, simply return the corresponding record - return self.browse(existing_data['id']) + return self.browse(field_data['id']) + + def _reflect_model(self, model): + """ Reflect the given model's fields. """ + for field in model._fields.itervalues(): + self._reflect_field(field) def _instanciate_attrs(self, field_data, partial): """ Return the parameters for a field instance for ``field_data``. """ @@ -849,6 +863,53 @@ class IrModelConstraint(models.Model): default['name'] = self.name + '_copy' return super(IrModelConstraint, self).copy(default) + def _reflect_constraint(self, model, conname, type, definition, module): + """ Reflect the given constraint, to make it possible to delete it later + when the module is uninstalled. ``type`` is either 'f' or 'u' + depending on the constraint being a foreign key or not. + """ + if not module: + # no need to save constraints for custom models as they're not part + # of any module + return + assert type in ('f', 'u') + cr = self._cr + query = """ SELECT type, definition + FROM ir_model_constraint c, ir_module_module m + WHERE c.module=m.id AND c.name=%s AND m.name=%s """ + cr.execute(query, (conname, module)) + cons = cr.dictfetchone() + if not cons: + query = """ INSERT INTO ir_model_constraint + (name, date_init, date_update, module, model, type, definition) + VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC', + (SELECT id FROM ir_module_module WHERE name=%s), + (SELECT id FROM ir_model WHERE model=%s), %s, %s) """ + cr.execute(query, (conname, module, model._name, type, definition)) + elif cons['type'] != type or (definition and cons['definition'] != definition): + query = """ UPDATE ir_model_constraint + SET date_update=now() AT TIME ZONE 'UTC', type=%s, definition=%s + WHERE name=%s AND module=(SELECT id FROM ir_module_module WHERE name=%s) """ + cr.execute(query, (type, definition, conname, module)) + + def _reflect_model(self, model): + """ Reflect the _sql_constraints of the given model. """ + def cons_text(txt): + return txt.lower().replace(', ',',').replace(' (','(') + + # map each constraint on the name of the module where it is defined + constraint_module = { + constraint[0]: cls._module + for cls in reversed(type(model).mro()) + if not getattr(cls, 'pool', None) + for constraint in getattr(cls, '_local_sql_constraints', ()) + } + + for (key, definition, _) in model._sql_constraints: + conname = '%s_%s' % (model._table, key) + module = constraint_module.get(key) + self._reflect_constraint(model, conname, 'u', cons_text(definition), module) + class IrModelRelation(models.Model): """ @@ -897,6 +958,22 @@ class IrModelRelation(models.Model): self._cr.commit() + def _reflect_relation(self, model, table, module): + """ Reflect the table of a many2many field for the given model, to make + it possible to delete it later when the module is uninstalled. + """ + cr = self._cr + query = """ SELECT 1 FROM ir_model_relation r, ir_module_module m + WHERE r.module=m.id AND r.name=%s AND m.name=%s """ + cr.execute(query, (table, module)) + if not cr.rowcount: + query = """ INSERT INTO ir_model_relation (name, date_init, date_update, module, model) + VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC', + (SELECT id FROM ir_module_module WHERE name=%s), + (SELECT id FROM ir_model WHERE model=%s)) """ + cr.execute(query, (table, module, model._name)) + self.invalidate_cache() + class IrModelAccess(models.Model): _name = 'ir.model.access' diff --git a/odoo/fields.py b/odoo/fields.py index f0e3b3baf63..4722c582858 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -2261,7 +2261,8 @@ class Many2many(_RelationalMulti): # they will be automatically removed when dropping the corresponding ir.model.field # table name for custom relation all starts with x_, see __init__ if not rel.startswith('x_'): - model._save_relation_table(rel, self._module) + IMR = model.env['ir.model.relation'] + IMR._reflect_relation(model, rel, self._module) cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (rel,)) if not cr.dictfetchall(): if self.comodel_name not in model.env: diff --git a/odoo/models.py b/odoo/models.py index a2be00d2134..3cfe2088757 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -266,23 +266,14 @@ class BaseModel(object): pass @api.model_cr_context - def _field_create(self): + def _reflect(self): """ Reflect the model and its fields in the models 'ir.model' and 'ir.model.fields'. Also create entries in 'ir.model.data' if the key 'module' is passed to the context. """ - # reflect the model in 'ir.model' - self.env['ir.model']._reflect(self) - - # reflect the fields in 'ir.model.fields' - cr = self.env.cr - cr.execute("SELECT * FROM ir_model_fields WHERE model=%s", (self._name,)) - existing = {row['name']: row for row in cr.dictfetchall()} - - reflect = self.env['ir.model.fields']._reflect - for field in self._fields.itervalues(): - reflect(field, existing) - + self.env['ir.model']._reflect_model(self) + self.env['ir.model.fields']._reflect_model(self) + self.env['ir.model.constraint']._reflect_model(self) self.invalidate_cache() @api.model @@ -2064,66 +2055,10 @@ class BaseModel(object): _schema.debug("Table '%s': column '%s': dropped NOT NULL constraint", self._table, row['attname']) - @api.model_cr - def _save_constraint(self, constraint_name, type, definition, module): - """ - Record the creation of a constraint for this model, to make it possible - to delete it later when the module is uninstalled. Type can be either - 'f' or 'u' depending on the constraint being a foreign key or not. - """ - if not module: - # no need to save constraints for custom models as they're not part - # of any module - return - assert type in ('f', 'u') - cr = self._cr - cr.execute(""" - SELECT type, definition FROM ir_model_constraint, ir_module_module - WHERE ir_model_constraint.module=ir_module_module.id - AND ir_model_constraint.name=%s - AND ir_module_module.name=%s - """, (constraint_name, module)) - constraints = cr.dictfetchone() - if not constraints: - cr.execute(""" - INSERT INTO ir_model_constraint - (name, date_init, date_update, module, model, type, definition) - VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC', - (SELECT id FROM ir_module_module WHERE name=%s), - (SELECT id FROM ir_model WHERE model=%s), %s, %s)""", - (constraint_name, module, self._name, type, definition)) - elif constraints['type'] != type or (definition and constraints['definition'] != definition): - cr.execute(""" - UPDATE ir_model_constraint - SET date_update=now() AT TIME ZONE 'UTC', type=%s, definition=%s - WHERE name=%s AND module = (SELECT id FROM ir_module_module WHERE name=%s)""", - (type, definition, constraint_name, module)) - @api.model_cr def _drop_constraint(self, source_table, constraint_name): self._cr.execute("ALTER TABLE %s DROP CONSTRAINT %s" % (source_table, constraint_name)) - @api.model_cr - def _save_relation_table(self, relation_table, module): - """ - Record the creation of a many2many for this model, to make it possible - to delete it later when the module is uninstalled. - """ - cr = self._cr - cr.execute(""" - SELECT 1 FROM ir_model_relation, ir_module_module - WHERE ir_model_relation.module=ir_module_module.id - AND ir_model_relation.name=%s - AND ir_module_module.name=%s - """, (relation_table, module)) - if not cr.rowcount: - cr.execute("""INSERT INTO ir_model_relation (name, date_init, date_update, module, model) - VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC', - (SELECT id FROM ir_module_module WHERE name=%s), - (SELECT id FROM ir_model WHERE model=%s))""", - (relation_table, module, self._name)) - self.invalidate_cache() - # 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(), \ @@ -2246,7 +2181,7 @@ class BaseModel(object): stored_fields = [] # new-style stored fields with compute todo_end = self._context['todo'] update_custom_fields = self._context.get('update_custom_fields', False) - self._field_create() + self._reflect() create = not self._table_exist() if self._auto: @@ -2491,7 +2426,8 @@ class BaseModel(object): query = 'ALTER TABLE "%s" ADD FOREIGN KEY ("%s") REFERENCES "%s" ON DELETE %s' for table1, column, table2, ondelete, module in self._foreign_keys: cr.execute(query % (table1, column, table2, ondelete)) - self._save_constraint("%s_%s_fkey" % (table1, column), 'f', False, module) + conname = "%s_%s_fkey" % (table1, column) + self.env['ir.model.constraint']._reflect_constraint(self, conname, 'f', False, module) cr.commit() del type(self)._foreign_keys @@ -2561,15 +2497,15 @@ class BaseModel(object): """ cr = self._cr - def unify_cons_text(txt): + def cons_text(txt): return txt.lower().replace(', ',',').replace(' (','(') - def drop(name, definition, old_definition): + def drop(name, definition): try: cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (self._table, name)) cr.commit() - _schema.debug("Table '%s': dropped constraint '%s'. Reason: its definition changed from '%s' to '%s'", - self._table, name, old_definition, definition) + _schema.debug("Table '%s': dropped constraint '%s'. Reason: its definition changed to '%s'", + self._table, name, definition) except Exception: _schema.warning("Table '%s': unable to drop constraint '%s'!", self._table, definition) cr.rollback() @@ -2587,37 +2523,17 @@ class BaseModel(object): self._table, definition, query) cr.rollback() - # map each constraint on the name of the module where it is defined - constraint_module = { - constraint[0]: cls._module - for cls in reversed(type(self).mro()) - if not getattr(cls, 'pool', None) - for constraint in getattr(cls, '_local_sql_constraints', ()) - } - for (key, definition, _) in self._sql_constraints: conname = '%s_%s' % (self._table, key) - - # using 1 to get result if no imc but one pgc - cr.execute("""SELECT definition, 1 - FROM ir_model_constraint imc - RIGHT JOIN pg_constraint pgc - ON (pgc.conname = imc.name) - WHERE pgc.conname=%s - """, (conname, )) - existing = cr.dictfetchone() - if not existing: + cr.execute("SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname=%s", (conname,)) + if not cr.rowcount: # constraint does not exists add(conname, definition) - elif unify_cons_text(definition) != existing['definition']: - # constraint exists but its definition has changed - drop(conname, definition, existing['definition'] or '') + elif cons_text(definition) != cons_text(cr.fetchone()[0]): + # constraint exists but its definition may have changed + drop(conname, definition) add(conname, definition) - # we need to add the constraint: - module = constraint_module.get(key) - self._save_constraint(conname, 'u', unify_cons_text(definition), module) - @api.model_cr def _execute_sql(self): """ Execute the SQL code from the _sql attribute (if any).""" From a1d061080e23ebe6e175e54f3deb38d06890e659 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 25 Jan 2017 10:51:53 +0100 Subject: [PATCH 05/15] [REF] models: improve the API for post-init functions Add a method to the registry to register function calls that must be done after methods `_auto_init` and `init` have been invoked on all models. --- odoo/addons/base/ir/ir_model.py | 6 ++-- odoo/models.py | 56 +++++++++++++-------------------- odoo/modules/registry.py | 12 +++++-- 3 files changed, 34 insertions(+), 40 deletions(-) diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index b8ad6f51f34..ebf2ead262a 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -235,7 +235,7 @@ class IrModel(models.Model): query_insert(cr, self._table, params) record = self.browse(cr.fetchone()) - self._context['todo'].append((10, record.modified, [set(params) - {'model', 'state'}])) + self.pool.post_init(record.modified, set(params) - {'model', 'state'}) if model._module == self._context.get('module'): # self._module is the name of the module that last extended self @@ -700,7 +700,7 @@ class IrModelFields(models.Model): # create an entry in this table query_insert(cr, self._table, params) record = self.browse(cr.fetchone()) - self._context['todo'].append((20, record.modified, [list(params)])) + self.pool.post_init(record.modified, list(params)) # create a corresponding xml id module = field._module or self._context.get('module') if module: @@ -722,7 +722,7 @@ class IrModelFields(models.Model): # update the entry in this table query_update(cr, self._table, params, ['model', 'name']) record = self.browse(cr.fetchone()) - self._context['todo'].append((20, record.modified, [diff])) + self.pool.post_init(record.modified, diff) # update fields_data (for recursive calls) field_data.update(params) return record diff --git a/odoo/models.py b/odoo/models.py index 3cfe2088757..15b8a22f96a 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2150,24 +2150,23 @@ class BaseModel(object): @api.model_cr_context def _auto_init(self): - """ Reflect the model and initialize the database schema of ``self``: - - create the corresponding table in database, - - add the parent columns in database, - - add the '_log_access' columns if required, - - report on database columns no more existing in ``self._fields``, - - remove no more existing not null constraints, - - alter existing database columns to match ``self._fields``, - - create database tables to match ``self._fields``, - - add database indices to match ``self._fields``, - - save in self._foreign_keys a list a foreign keys to create (see - _auto_end). + """ Initialize the database schema of ``self``: + - create the corresponding table, + - create/update the necessary columns/tables for fields, + - initialize new columns on existing rows, + - add the SQL constraints given on the model, + - add the indexes on indexed fields, - Note: you should not override this method. Instead, you can modify the - model's database schema by overriding method :meth:`~.init`, which is - called right after this one. + Also reflect models, fields, relations and constraints. + + Also prepare post-init stuff to: + - add foreign key constraints, + - mark fields to recompute on existing records. + + Note: you should not override this method. Instead, you can modify + the model's database schema by overriding method :meth:`~.init`, + which is called right after this one. """ - assert 'todo' in self._context, "Context not passed correctly to method _auto_init()." - type(self)._foreign_keys = set() raise_on_invalid_object_name(self._name) @@ -2176,10 +2175,13 @@ class BaseModel(object): # has not been added in database yet! self = self.with_context(prefetch_fields=False) + def recompute(field): + _logger.info("Storing computed values of %s", field) + recs = self.with_context(active_test=False).search([]) + recs._recompute_todo(field) + cr = self._cr parent_store_compute = False - stored_fields = [] # new-style stored fields with compute - todo_end = self._context['todo'] update_custom_fields = self._context.get('update_custom_fields', False) self._reflect() create = not self._table_exist() @@ -2218,7 +2220,7 @@ class BaseModel(object): if not field.column_type: # the field is not stored as a column if field.check_schema(self) and field.compute: - stored_fields.append(field) + self.pool.post_init(recompute, field) else: res = column_data.get(name) @@ -2363,7 +2365,7 @@ class BaseModel(object): # remember new-style stored fields with compute method if field.compute: - stored_fields.append(field) + self.pool.post_init(recompute, field) # and add constraints if needed if field.type == 'many2one' and field.store: @@ -2405,20 +2407,6 @@ class BaseModel(object): self._parent_store_compute() cr.commit() - if stored_fields: - # trigger computation of new-style stored fields with a compute - def func(): - fnames = [f.name for f in stored_fields] - _logger.info("Storing computed values of %s fields %s", - self._name, ', '.join(sorted(fnames))) - recs = self.with_context(active_test=False).search([]) - if recs: - recs.invalidate_cache(fnames, recs.ids) - for f in stored_fields: - recs._recompute_todo(f) - - todo_end.append((1000, func, ())) - @api.model_cr_context def _auto_end(self): """ Create the foreign keys recorded by _auto_init. """ diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py index 7bfacb700fb..0b0773bd78e 100644 --- a/odoo/modules/registry.py +++ b/odoo/modules/registry.py @@ -6,6 +6,7 @@ """ from collections import Mapping, defaultdict, deque from contextlib import closing +from functools import partial from operator import attrgetter import logging import os @@ -106,6 +107,7 @@ class Registry(Mapping): self._init_parent = {} self._assertion_report = assertion_report.assertion_report() self._fields_by_model = None + self._post_init_queue = deque() # modules fully loaded (maintained during init phase by `loading` module) self._init_modules = set() @@ -296,6 +298,10 @@ class Registry(Mapping): for model in models: model._setup_complete() + def post_init(self, func, *args, **kwargs): + """ Register a function to call at the end of :meth:`~.init_models`. """ + self._post_init_queue.append(partial(func, *args, **kwargs)) + def init_models(self, cr, model_names, context): """ Initialize a list of models (given by their name). Call methods ``_auto_init``, ``init``, and ``_auto_end`` on each model to create @@ -308,7 +314,6 @@ class Registry(Mapping): if 'module' in context: _logger.info('module %s: creating or updating database tables', context['module']) - context = dict(context, todo=[]) env = odoo.api.Environment(cr, SUPERUSER_ID, context) models = [env[model_name] for model_name in model_names] @@ -321,8 +326,9 @@ class Registry(Mapping): model._auto_end() cr.commit() - for _, func, args in sorted(context['todo']): - func(*args) + while self._post_init_queue: + func = self._post_init_queue.popleft() + func() if models: models[0].recompute() From b6103d0b1ba670a68d89a9adf3d32870a80a6ca0 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 7 Feb 2017 14:14:49 +0100 Subject: [PATCH 06/15] [REF] models: foreign keys use Registry.post_init() instead of BaseModel._auto_end() Also use Registry.post_init() for the creation of foreign keys in `_sql_constraints`. --- odoo/fields.py | 4 ++-- odoo/models.py | 37 ++++++++++++++++++------------------- odoo/modules/registry.py | 8 ++------ 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/odoo/fields.py b/odoo/fields.py index 4722c582858..be395bb56e6 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -2272,10 +2272,10 @@ class Many2many(_RelationalMulti): # 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", (comodel._table,)) if not cr.fetchall(): - model._m2o_add_foreign_key_unchecked(rel, id2, comodel, 'cascade', self._module) + model.pool.post_init(model._m2o_add_foreign_key_unchecked, rel, id2, comodel, 'cascade', self._module) cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (model._table,)) if not cr.fetchall(): - model._m2o_add_foreign_key_unchecked(rel, id1, model, 'cascade', self._module) + model.pool.post_init(model._m2o_add_foreign_key_unchecked, rel, id1, model, 'cascade', self._module) cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id1)) cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id2)) diff --git a/odoo/models.py b/odoo/models.py index 15b8a22f96a..7a33ebc4a2f 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2073,9 +2073,14 @@ class BaseModel(object): # unchecked version: for custom cases, such as m2m relationships def _m2o_add_foreign_key_unchecked(self, source_table, source_field, dest_model, ondelete, module): - fk_def = (source_table, source_field, dest_model._table, ondelete or 'set null', module) - self._foreign_keys.add(fk_def) - _schema.debug("Table '%s': added foreign key '%s' with definition=REFERENCES \"%s\" ON DELETE %s", *fk_def[:-1]) + cr = self._cr + query = 'ALTER TABLE "%s" ADD FOREIGN KEY ("%s") REFERENCES "%s" ON DELETE %s' + cr.execute(query % (source_table, source_field, dest_model._table, ondelete or 'set null')) + _schema.debug("Table '%s': added foreign key '%s' with definition=REFERENCES \"%s\" ON DELETE %s", + source_table, source_field, dest_model._table, ondelete or 'set null') + conname = "%s_%s_fkey" % (source_table, source_field) + self.env['ir.model.constraint']._reflect_constraint(self, conname, 'f', None, module) + cr.commit() @api.model_cr def _m2o_fix_foreign_key(self, source_table, source_field, dest_model, ondelete): @@ -2167,7 +2172,6 @@ class BaseModel(object): the model's database schema by overriding method :meth:`~.init`, which is called right after this one. """ - type(self)._foreign_keys = set() raise_on_invalid_object_name(self._name) # This prevents anything called by this method (in particular default @@ -2350,7 +2354,7 @@ class BaseModel(object): if field.type == 'many2one': comodel = self.env[field.comodel_name] if comodel._auto and comodel._table != 'ir_actions': - self._m2o_fix_foreign_key(self._table, name, comodel, field.ondelete) + self.pool.post_init(self._m2o_fix_foreign_key, self._table, name, comodel, field.ondelete) else: # the column doesn't exist in database, create it @@ -2374,7 +2378,7 @@ class BaseModel(object): comodel = self.env[field.comodel_name] # ir_actions is inherited so foreign key doesn't work on it if comodel._auto and comodel._table != 'ir_actions': - self._m2o_add_foreign_key_checked(name, comodel, field.ondelete) + self.pool.post_init(self._m2o_add_foreign_key_checked, name, comodel, field.ondelete) if field.index: cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (self._table, name, self._table, name)) if field.required: @@ -2407,18 +2411,6 @@ class BaseModel(object): self._parent_store_compute() cr.commit() - @api.model_cr_context - def _auto_end(self): - """ Create the foreign keys recorded by _auto_init. """ - cr = self._cr - query = 'ALTER TABLE "%s" ADD FOREIGN KEY ("%s") REFERENCES "%s" ON DELETE %s' - for table1, column, table2, ondelete, module in self._foreign_keys: - cr.execute(query % (table1, column, table2, ondelete)) - conname = "%s_%s_fkey" % (table1, column) - self.env['ir.model.constraint']._reflect_constraint(self, conname, 'f', False, module) - cr.commit() - del type(self)._foreign_keys - @api.model_cr def init(self): """ This method is called after :meth:`~._auto_init`, and may be @@ -2484,6 +2476,7 @@ class BaseModel(object): """ cr = self._cr + foreign_key_re = re.compile(r'\s*foreign\s+key\b.*', re.I) def cons_text(txt): return txt.lower().replace(', ',',').replace(' (','(') @@ -2511,7 +2504,7 @@ class BaseModel(object): self._table, definition, query) cr.rollback() - for (key, definition, _) in self._sql_constraints: + def process(key, definition): conname = '%s_%s' % (self._table, key) cr.execute("SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname=%s", (conname,)) if not cr.rowcount: @@ -2522,6 +2515,12 @@ class BaseModel(object): drop(conname, definition) add(conname, definition) + for (key, definition, _) in self._sql_constraints: + if foreign_key_re.match(definition): + self.pool.post_init(process, key, definition) + else: + process(key, definition) + @api.model_cr def _execute_sql(self): """ Execute the SQL code from the _sql attribute (if any).""" diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py index 0b0773bd78e..db22649edf6 100644 --- a/odoo/modules/registry.py +++ b/odoo/modules/registry.py @@ -304,8 +304,8 @@ class Registry(Mapping): def init_models(self, cr, model_names, context): """ Initialize a list of models (given by their name). Call methods - ``_auto_init``, ``init``, and ``_auto_end`` on each model to create - or update the database tables supporting the models. + ``_auto_init`` and ``init`` on each model to create or update the + database tables supporting the models. The ``context`` may contain the following items: - ``module``: the name of the module being installed/updated, if any; @@ -322,10 +322,6 @@ class Registry(Mapping): model.init() cr.commit() - for model in models: - model._auto_end() - cr.commit() - while self._post_init_queue: func = self._post_init_queue.popleft() func() From 2b46025dd553b01e852ae37a90f90c240851adc1 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 25 Jan 2017 11:23:59 +0100 Subject: [PATCH 07/15] [REF] models: use Registry.post_init() for reflecting models, fields, relations and constraints --- odoo/fields.py | 2 +- odoo/models.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/odoo/fields.py b/odoo/fields.py index be395bb56e6..5b9fad9d047 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -2262,7 +2262,7 @@ class Many2many(_RelationalMulti): # table name for custom relation all starts with x_, see __init__ if not rel.startswith('x_'): IMR = model.env['ir.model.relation'] - IMR._reflect_relation(model, rel, self._module) + model.pool.post_init(IMR._reflect_relation, model, rel, self._module) cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (rel,)) if not cr.dictfetchall(): if self.comodel_name not in model.env: diff --git a/odoo/models.py b/odoo/models.py index 7a33ebc4a2f..3acf21dd204 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2162,10 +2162,9 @@ class BaseModel(object): - add the SQL constraints given on the model, - add the indexes on indexed fields, - Also reflect models, fields, relations and constraints. - Also prepare post-init stuff to: - add foreign key constraints, + - reflect models, fields, relations and constraints, - mark fields to recompute on existing records. Note: you should not override this method. Instead, you can modify @@ -2184,10 +2183,11 @@ class BaseModel(object): recs = self.with_context(active_test=False).search([]) recs._recompute_todo(field) + self.pool.post_init(self._reflect) + cr = self._cr parent_store_compute = False update_custom_fields = self._context.get('update_custom_fields', False) - self._reflect() create = not self._table_exist() if self._auto: From daa1eb1fc488d45ba9d4e782053c08cfc1a53cfe Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 25 Jan 2017 11:52:12 +0100 Subject: [PATCH 08/15] [IMP] base: remove ir_model_* tables from base.sql --- odoo/addons/base/base.sql | 71 --------------------------------- odoo/addons/base/ir/ir_model.py | 1 + odoo/models.py | 2 + odoo/modules/registry.py | 13 +++--- 4 files changed, 10 insertions(+), 77 deletions(-) diff --git a/odoo/addons/base/base.sql b/odoo/addons/base/base.sql index 31b7061cf78..9e3efde69a6 100644 --- a/odoo/addons/base/base.sql +++ b/odoo/addons/base/base.sql @@ -12,49 +12,6 @@ CREATE TABLE ir_act_url (primary key(id)) INHERITS (ir_actions); CREATE TABLE ir_act_server (primary key(id)) INHERITS (ir_actions); CREATE TABLE ir_act_client (primary key(id)) INHERITS (ir_actions); - -CREATE TABLE ir_model ( - id serial, - model varchar NOT NULL, - name varchar, - state varchar, - info text, - transient boolean, - primary key(id) -); - -CREATE TABLE ir_model_fields ( - id serial, - model varchar NOT NULL, - model_id integer references ir_model on delete cascade, - name varchar NOT NULL, - state varchar default 'base', - field_description varchar, - help varchar, - ttype varchar, - relation varchar, - relation_field varchar, - index boolean, - copy boolean, - related varchar, - readonly boolean default False, - required boolean default False, - selectable boolean default False, - translate boolean default False, - relation_table varchar, - column1 varchar, - column2 varchar, - store boolean, - primary key(id) -); - -CREATE TABLE res_lang ( - id serial, - name VARCHAR(64) NOT NULL UNIQUE, - code VARCHAR(16) NOT NULL UNIQUE, - primary key(id) -); - CREATE TABLE res_users ( id serial NOT NULL, active boolean default True, @@ -137,34 +94,6 @@ CREATE TABLE ir_model_data ( primary key(id) ); --- Records foreign keys and constraints installed by a module (so they can be --- removed when the module is uninstalled): --- - for a foreign key: type is 'f', --- - for a constraint: type is 'u' (this is the convention PostgreSQL uses). -CREATE TABLE ir_model_constraint ( - id serial NOT NULL, - date_init timestamp without time zone, - date_update timestamp without time zone, - module integer NOT NULL references ir_module_module on delete restrict, - model integer NOT NULL references ir_model on delete restrict, - type character varying(1) NOT NULL, - definition varchar, - name varchar NOT NULL, - primary key(id) -); - --- Records relation tables (i.e. implementing many2many) installed by a module --- (so they can be removed when the module is uninstalled). -CREATE TABLE ir_model_relation ( - id serial NOT NULL, - date_init timestamp without time zone, - date_update timestamp without time zone, - module integer NOT NULL references ir_module_module on delete restrict, - model integer NOT NULL references ir_model on delete restrict, - name varchar NOT NULL, - primary key(id) -); - CREATE TABLE res_currency ( id serial, name varchar NOT NULL, diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index ebf2ead262a..723b99b9a32 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -674,6 +674,7 @@ class IrModelFields(models.Model): 'field_description': field.string, 'help': field.help or None, 'ttype': field.type, + 'state': 'manual' if field.manual else 'base', 'relation': field.comodel_name or None, 'index': bool(field.index), 'store': bool(field.store), diff --git a/odoo/models.py b/odoo/models.py index 3acf21dd204..5fe192d32b8 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -544,6 +544,8 @@ class BaseModel(object): @api.model def _add_manual_fields(self, partial): + if not self.pool._init_modules: + return IrModelFields = self.env['ir.model.fields'] manual_fields = self.pool.get_manual_fields(self._cr, self._name) for name, field_data in manual_fields.iteritems(): diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py index db22649edf6..18a9731a892 100644 --- a/odoo/modules/registry.py +++ b/odoo/modules/registry.py @@ -275,12 +275,13 @@ class Registry(Mapping): lazy_property.reset_all(self) env = odoo.api.Environment(cr, SUPERUSER_ID, {}) - # load custom models - ir_model = env['ir.model'] - cr.execute('SELECT * FROM ir_model WHERE state=%s', ('manual',)) - for model_data in cr.dictfetchall(): - model_class = ir_model._instanciate(model_data) - model_class._build_model(self, cr) + # load custom models (except when loading 'base') + if self._init_modules: + ir_model = env['ir.model'] + cr.execute('SELECT * FROM ir_model WHERE state=%s', ('manual',)) + for model_data in cr.dictfetchall(): + model_class = ir_model._instanciate(model_data) + model_class._build_model(self, cr) # prepare the setup on all models models = env.values() From dfec1fa7787e1675186403a792c1ab2f246c5c07 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 26 Jan 2017 08:59:32 +0100 Subject: [PATCH 09/15] [FIX] base_sparse_field: remove pre_init_hook --- addons/base_sparse_field/__init__.py | 8 -------- addons/base_sparse_field/__manifest__.py | 1 - addons/base_sparse_field/models/models.py | 2 +- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/addons/base_sparse_field/__init__.py b/addons/base_sparse_field/__init__.py index edbaa61c052..567548c78f5 100644 --- a/addons/base_sparse_field/__init__.py +++ b/addons/base_sparse_field/__init__.py @@ -2,11 +2,3 @@ from . import models from . import tests - -def _update_database_schema(cr): - # The reflection of fields is done before updating the model's schema, - # therefore we first update the schema. - cr.execute(""" - ALTER TABLE ir_model_fields - ADD COLUMN serialization_field_id integer REFERENCES ir_model_fields ON DELETE cascade - """) diff --git a/addons/base_sparse_field/__manifest__.py b/addons/base_sparse_field/__manifest__.py index 031a6c179de..ac5a5f6cc36 100644 --- a/addons/base_sparse_field/__manifest__.py +++ b/addons/base_sparse_field/__manifest__.py @@ -14,5 +14,4 @@ 'data': [ 'views/views.xml', ], - 'pre_init_hook': '_update_database_schema', } diff --git a/addons/base_sparse_field/models/models.py b/addons/base_sparse_field/models/models.py index 7ad34bcd76b..6d4a5d03401 100644 --- a/addons/base_sparse_field/models/models.py +++ b/addons/base_sparse_field/models/models.py @@ -43,7 +43,7 @@ class IrModelFields(models.Model): def _instanciate_attrs(self, field_data, partial): attrs = super(IrModelFields, self)._instanciate_attrs(field_data, partial) - if field_data['serialization_field_id']: + if field_data.get('serialization_field_id'): serialization_record = self.browse(field_data['serialization_field_id']) attrs['sparse'] = serialization_record.name return attrs From b3c97f08bbf9c33959e060ddea42054f7f32a5c7 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 27 Jan 2017 16:16:16 +0100 Subject: [PATCH 10/15] [FIX] mail: when initializing aliases, force reflection of models --- addons/mail/models/mail_alias.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/addons/mail/models/mail_alias.py b/addons/mail/models/mail_alias.py index 07d6bfb3d58..67233efcd77 100644 --- a/addons/mail/models/mail_alias.py +++ b/addons/mail/models/mail_alias.py @@ -239,6 +239,11 @@ class AliasMixin(models.AbstractModel): if name != 'alias_id': return + # both self and the alias model must be present in 'ir.model' + IM = self.env['ir.model'] + IM._reflect_model(self) + IM._reflect_model(self.env[self.get_alias_model_name({})]) + alias_ctx = { 'alias_model_name': self.get_alias_model_name({}), 'alias_parent_model_name': self._name, From 8225f3649729eb930a2511d2d4bf7127e2f1eca9 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 3 Feb 2017 09:28:27 +0100 Subject: [PATCH 11/15] [IMP] models: speed up schema update for models Simply remove most `commit()` statements from code that updates the database schema. This improves the performance of the schema update by 30%. --- odoo/fields.py | 1 - odoo/models.py | 50 +++++++++++++--------------------------- odoo/modules/registry.py | 3 +-- 3 files changed, 17 insertions(+), 37 deletions(-) diff --git a/odoo/fields.py b/odoo/fields.py index 5b9fad9d047..1f65a6b61ff 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -2280,7 +2280,6 @@ class Many2many(_RelationalMulti): cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id1)) cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id2)) cr.execute("COMMENT ON TABLE \"%s\" IS 'RELATION BETWEEN %s AND %s'" % (rel, model._table, comodel._table)) - cr.commit() _schema.debug("Create table '%s': m2m relation between '%s' and '%s'", rel, model._table, comodel._table) return True diff --git a/odoo/models.py b/odoo/models.py index 5fe192d32b8..ecf6c4acc08 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2082,7 +2082,6 @@ class BaseModel(object): source_table, source_field, dest_model._table, ondelete or 'set null') conname = "%s_%s_fkey" % (source_table, source_field) self.env['ir.model.constraint']._reflect_constraint(self, conname, 'f', None, module) - cr.commit() @api.model_cr def _m2o_fix_foreign_key(self, source_table, source_field, dest_model, ondelete): @@ -2152,8 +2151,6 @@ class BaseModel(object): query = 'UPDATE "%s" SET "%s"=%s WHERE "%s" IS NULL' % ( self._table, column_name, field.column_format, column_name) self._cr.execute(query, (value,)) - # this is a disgrace - self._cr.commit() @api.model_cr_context def _auto_init(self): @@ -2201,7 +2198,6 @@ class BaseModel(object): cr.execute('SELECT 1 FROM "%s" LIMIT 1' % self._table) has_rows = cr.rowcount - cr.commit() if self._parent_store: if not self._parent_columns_exist(): self._create_parent_columns() @@ -2271,7 +2267,6 @@ class BaseModel(object): cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, name, column_type[1])) cr.execute('UPDATE "%s" SET "%s"=temp_change_size::%s' % (self._table, name, column_type[1])) cr.execute('ALTER TABLE "%s" DROP COLUMN temp_change_size CASCADE' % (self._table,)) - cr.commit() _schema.debug("Table '%s': column '%s' (type varchar) changed size from %s to %s", self._table, name, f_pg_size or 'unlimited', field.size or 'unlimited') for c in casts: @@ -2287,7 +2282,6 @@ class BaseModel(object): cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, name, c[2])) cr.execute('UPDATE "%s" SET "%s"= __temp_type_cast%s' % (self._table, name, c[3])) cr.execute('ALTER TABLE "%s" DROP COLUMN __temp_type_cast CASCADE' % (self._table,)) - cr.commit() _schema.debug("Table '%s': column '%s' changed type from %s to %s", self._table, name, c[0], c[1]) break @@ -2318,19 +2312,17 @@ class BaseModel(object): self._init_column(name) # add the NOT NULL constraint try: - cr.commit() - cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, name), log_exceptions=False) - _schema.debug("Table '%s': column '%s': added NOT NULL constraint", - self._table, name) + with cr.savepoint(): + cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, name), log_exceptions=False) + _schema.debug("Table '%s': column '%s': added NOT NULL constraint", + self._table, name) except Exception: msg = "Table '%s': unable to set a NOT NULL constraint on column '%s' !\n"\ "If you want to have it, you should update the records and execute manually:\n"\ "ALTER TABLE %s ALTER COLUMN %s SET NOT NULL" _schema.warning(msg, self._table, name, self._table, name) - cr.commit() elif not field.required and f_pg_notnull == 1: cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" DROP NOT NULL' % (self._table, name)) - cr.commit() _schema.debug("Table '%s': column '%s': dropped NOT NULL constraint", self._table, name) # Verify index @@ -2339,7 +2331,6 @@ class BaseModel(object): res2 = cr.dictfetchall() if not res2 and field.index: cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (self._table, name, self._table, name)) - cr.commit() if field.type == 'text': # FIXME: for fields.text columns we should try creating GIN indexes instead (seems most suitable for an ERP context) msg = "Table '%s': Adding (b-tree) index for %s column '%s'."\ @@ -2349,7 +2340,6 @@ class BaseModel(object): _schema.warning(msg, self._table, field.type, name) if res2 and not field.index: cr.execute('DROP INDEX "%s_%s_index"' % (self._table, name)) - cr.commit() msg = "Table '%s': dropping index for column '%s' of type '%s' as it is not required anymore" _schema.debug(msg, self._table, name, field.type) @@ -2385,24 +2375,21 @@ class BaseModel(object): cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (self._table, name, self._table, name)) if field.required: try: - cr.commit() - cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, name)) - _schema.debug("Table '%s': column '%s': added a NOT NULL constraint", - self._table, name) + with cr.savepoint(): + cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, name)) + _schema.debug("Table '%s': column '%s': added a NOT NULL constraint", + self._table, name) except Exception: msg = "WARNING: unable to set column %s of table %s not null !\n"\ "Try to re-run: openerp-server --update=module\n"\ "If it doesn't work, update records and execute manually:\n"\ "ALTER TABLE %s ALTER COLUMN %s SET NOT NULL" _logger.warning(msg, name, self._table, self._table, name, exc_info=True) - cr.commit() else: cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (self._table,)) create = not bool(cr.fetchone()) - cr.commit() # start a new transaction - if self._auto: self._add_sql_constraints() @@ -2411,7 +2398,6 @@ class BaseModel(object): if parent_store_compute: self._parent_store_compute() - cr.commit() @api.model_cr def init(self): @@ -2455,7 +2441,6 @@ class BaseModel(object): _logger.error("parent_right field on model %s must be indexed! Add index=True to the field definition)", self._name) if self._fields[self._parent_name].ondelete not in ('cascade', 'restrict'): _logger.error("The field %s on model %s must be set as ondelete='cascade' or 'restrict'", self._parent_name, self._name) - self._cr.commit() @api.model_cr def _select_column_data(self): @@ -2485,26 +2470,24 @@ class BaseModel(object): def drop(name, definition): try: - cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (self._table, name)) - cr.commit() - _schema.debug("Table '%s': dropped constraint '%s'. Reason: its definition changed to '%s'", - self._table, name, definition) + with cr.savepoint(): + cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (self._table, name)) + _schema.debug("Table '%s': dropped constraint '%s'. Reason: its definition changed to '%s'", + self._table, name, definition) except Exception: _schema.warning("Table '%s': unable to drop constraint '%s'!", self._table, definition) - cr.rollback() def add(name, definition): query = 'ALTER TABLE "%s" ADD CONSTRAINT "%s" %s' % (self._table, name, definition) try: - cr.execute(query) - cr.commit() - _schema.debug("Table '%s': added constraint '%s' with definition=%s", - self._table, name, definition) + with cr.savepoint(): + cr.execute(query) + _schema.debug("Table '%s': added constraint '%s' with definition=%s", + self._table, name, definition) except Exception: _schema.warning("Table '%s': unable to add constraint '%s'!\n" "If you want to have it, you should update the records and execute manually:\n%s", self._table, definition, query) - cr.rollback() def process(key, definition): conname = '%s_%s' % (self._table, key) @@ -2528,7 +2511,6 @@ class BaseModel(object): """ Execute the SQL code from the _sql attribute (if any).""" if hasattr(self, "_sql"): self._cr.execute(self._sql) - self._cr.commit() # # Update objects that uses this one to update their _inherits fields diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py index 18a9731a892..5d20ff20dab 100644 --- a/odoo/modules/registry.py +++ b/odoo/modules/registry.py @@ -321,7 +321,6 @@ class Registry(Mapping): for model in models: model._auto_init() model.init() - cr.commit() while self._post_init_queue: func = self._post_init_queue.popleft() @@ -343,7 +342,7 @@ class Registry(Mapping): if name in missing: _logger.info("Recreate table of model %s.", name) env[name].init() - cr.commit() + cr.commit() # check again, and log errors if tables are still missing for name, model in env.items(): if not model._abstract and not model._table_exist(): From d024c7602146ba293c0082303fd476c8613daedc Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 3 Feb 2017 09:42:35 +0100 Subject: [PATCH 12/15] [REF] tools: add functions for SQL schema manipulation This helps factoring out a certain number of similar queries, and removing a few methods from `BaseModel`. --- odoo/addons/base/ir/ir_actions.py | 5 +- odoo/addons/base/ir/ir_attachment.py | 6 +- odoo/addons/base/ir/ir_filters.py | 8 +- odoo/addons/base/ir/ir_model.py | 37 ++--- odoo/addons/base/ir/ir_translation.py | 14 +- odoo/addons/base/ir/ir_ui_view.py | 10 +- odoo/addons/base/ir/ir_values.py | 5 +- odoo/fields.py | 12 +- odoo/models.py | 201 +++++--------------------- odoo/modules/db.py | 3 +- odoo/modules/registry.py | 6 +- odoo/service/db.py | 3 +- odoo/tools/sql.py | 146 +++++++++++++++++++ 13 files changed, 221 insertions(+), 235 deletions(-) diff --git a/odoo/addons/base/ir/ir_actions.py b/odoo/addons/base/ir/ir_actions.py index ae5d74a7af6..472fdeafa0c 100644 --- a/odoo/addons/base/ir/ir_actions.py +++ b/odoo/addons/base/ir/ir_actions.py @@ -385,9 +385,8 @@ class IrActionsActWindowView(models.Model): @api.model_cr_context def _auto_init(self): res = super(IrActionsActWindowView, self)._auto_init() - self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'act_window_view_unique_mode_per_action\'') - if not self._cr.fetchone(): - self._cr.execute('CREATE UNIQUE INDEX act_window_view_unique_mode_per_action ON ir_act_window_view (act_window_id, view_mode)') + tools.create_unique_index(self._cr, 'act_window_view_unique_mode_per_action', + self._table, ['act_window_id', 'view_mode']) return res diff --git a/odoo/addons/base/ir/ir_attachment.py b/odoo/addons/base/ir/ir_attachment.py index 528df926e5d..0e7e9284f44 100644 --- a/odoo/addons/base/ir/ir_attachment.py +++ b/odoo/addons/base/ir/ir_attachment.py @@ -291,10 +291,8 @@ class IrAttachment(models.Model): @api.model_cr_context def _auto_init(self): res = super(IrAttachment, self)._auto_init() - self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_attachment_res_idx',)) - if not self._cr.fetchone(): - self._cr.execute('CREATE INDEX ir_attachment_res_idx ON ir_attachment (res_model, res_id)') - self._cr.commit() + tools.create_index(self._cr, 'ir_attachment_res_idx', + self._table, ['res_model', 'res_id']) return res @api.model diff --git a/odoo/addons/base/ir/ir_filters.py b/odoo/addons/base/ir/ir_filters.py index bfed5939fa6..fb6a69f398e 100644 --- a/odoo/addons/base/ir/ir_filters.py +++ b/odoo/addons/base/ir/ir_filters.py @@ -3,7 +3,7 @@ import ast -from odoo import api, fields, models, _ +from odoo import api, fields, models, tools, _ from odoo.exceptions import UserError @@ -150,8 +150,6 @@ class IrFilters(models.Model): def _auto_init(self): result = super(IrFilters, self)._auto_init() # Use unique index to implement unique constraint on the lowercase name (not possible using a constraint) - self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_filters_name_model_uid_unique_action_index'") - if not self._cr.fetchone(): - self._cr.execute("""CREATE UNIQUE INDEX "ir_filters_name_model_uid_unique_action_index" ON ir_filters - (lower(name), model_id, COALESCE(user_id,-1), COALESCE(action_id,-1))""") + tools.create_unique_index(self._cr, 'ir_filters_name_model_uid_unique_action_index', + self._table, ['lower(name)', 'model_id', 'COALESCE(user_id,-1)', 'COALESCE(action_id,-1)']) return result diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index 723b99b9a32..e5acaee9d87 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -143,12 +143,11 @@ class IrModel(models.Model): def _drop_table(self): for model in self: table = self.env[model.model]._table - self._cr.execute('select relkind from pg_class where relname=%s', (table,)) - result = self._cr.fetchone() - if result and result[0] == 'v': - self._cr.execute('DROP view %s' % table) - elif result and result[0] == 'r': - self._cr.execute('DROP TABLE %s CASCADE' % table) + kind = tools.table_kind(self._cr, table) + if kind == 'v': + self._cr.execute('DROP VIEW "%s"' % table) + elif kind == 'r': + self._cr.execute('DROP TABLE "%s" CASCADE' % table) return True @api.multi @@ -466,14 +465,9 @@ class IrModelFields(models.Model): if field.name in models.MAGIC_COLUMNS: continue model = self.env[field.model] - self._cr.execute('SELECT relkind FROM pg_class WHERE relname=%s', (model._table,)) - relkind = self._cr.fetchone() - self._cr.execute("""SELECT column_name FROM information_schema.columns - WHERE table_name=%s AND column_name=%s""", - (model._table, field.name)) - column_name = self._cr.fetchone() - if column_name and (relkind and relkind[0] == 'r'): - self._cr.execute('ALTER table "%s" DROP column "%s" cascade' % (model._table, field.name)) + if tools.column_exists(self._cr, model._table, field.name) and \ + tools.table_kind(self._cr, model._table) == 'r': + self._cr.execute('ALTER TABLE "%s" DROP COLUMN "%s" CASCADE' % (model._table, field.name)) if field.state == 'manual' and field.ttype == 'many2many': rel_name = field.relation_table or model._fields[field.name].relation tables_to_drop.add(rel_name) @@ -946,15 +940,14 @@ class IrModelRelation(models.Model): # as installed modules have defined this element we must not delete it! continue - self._cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,)) - if self._cr.fetchone(): + if tools.table_exists(self._cr, name): to_drop.add(name) self.unlink() # drop m2m relation tables for table in to_drop: - self._cr.execute('DROP TABLE %s CASCADE' % table,) + self._cr.execute('DROP TABLE "%s" CASCADE' % table,) _logger.info('Dropped table %s', table) self._cr.commit() @@ -1185,12 +1178,10 @@ class IrModelData(models.Model): @api.model_cr_context def _auto_init(self): res = super(IrModelData, self)._auto_init() - self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_model_data_module_name_uniq_index'") - if not self._cr.fetchone(): - self._cr.execute('CREATE UNIQUE INDEX ir_model_data_module_name_uniq_index ON ir_model_data (module, name)') - self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_model_data_model_res_id_index'") - if not self._cr.fetchone(): - self._cr.execute('CREATE INDEX ir_model_data_model_res_id_index ON ir_model_data (model, res_id)') + tools.create_unique_index(self._cr, 'ir_model_data_module_name_uniq_index', + self._table, ['module', 'name']) + tools.create_index(self._cr, 'ir_model_data_model_res_id_index', + self._table, ['model', 'res_id']) return res @api.multi diff --git a/odoo/addons/base/ir/ir_translation.py b/odoo/addons/base/ir/ir_translation.py index de639e10abf..4cbd41b013a 100644 --- a/odoo/addons/base/ir/ir_translation.py +++ b/odoo/addons/base/ir/ir_translation.py @@ -255,19 +255,9 @@ class IrTranslation(models.Model): @api.model_cr_context def _auto_init(self): res = super(IrTranslation, self)._auto_init() - cr = self._cr - - cr.execute("SELECT indexname FROM pg_indexes WHERE indexname LIKE 'ir_translation_%'") - indexes = [row[0] for row in cr.fetchall()] - # Add separate md5 index on src (no size limit on values, and good performance). - if 'ir_translation_src_md5' not in indexes: - cr.execute('CREATE INDEX ir_translation_src_md5 ON ir_translation (md5(src))') - cr.commit() - - if 'ir_translation_ltn' not in indexes: - cr.execute('CREATE INDEX ir_translation_ltn ON ir_translation (name, lang, type)') - cr.commit() + tools.create_index(self._cr, 'ir_translation_src_md5', self._table, ['md5(src)']) + tools.create_index(self._cr, 'ir_translation_ltn', self._table, ['name', 'lang', 'type']) return res @api.model diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py index cd3eb73f96e..9233b08f329 100644 --- a/odoo/addons/base/ir/ir_ui_view.py +++ b/odoo/addons/base/ir/ir_ui_view.py @@ -79,9 +79,8 @@ class ViewCustom(models.Model): @api.model_cr_context def _auto_init(self): res = super(ViewCustom, self)._auto_init() - self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_ui_view_custom_user_id_ref_id'") - if not self._cr.fetchone(): - self._cr.execute("CREATE INDEX ir_ui_view_custom_user_id_ref_id ON ir_ui_view_custom (user_id, ref_id)") + tools.create_index(self._cr, 'ir_ui_view_custom_user_id_ref_id', + self._table, ['user_id', 'ref_id']) return res @@ -348,9 +347,8 @@ actual arch. @api.model_cr_context def _auto_init(self): res = super(View, self)._auto_init() - self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_model_type_inherit_id\'') - if not self._cr.fetchone(): - self._cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)') + tools.create_index(self._cr, 'ir_ui_view_model_type_inherit_id', + self._table, ['model', 'inherit_id']) return res def _compute_defaults(self, values): diff --git a/odoo/addons/base/ir/ir_values.py b/odoo/addons/base/ir/ir_values.py index 8614390990f..1134a196d9a 100644 --- a/odoo/addons/base/ir/ir_values.py +++ b/odoo/addons/base/ir/ir_values.py @@ -149,9 +149,8 @@ class IrValues(models.Model): @api.model_cr_context def _auto_init(self): res = super(IrValues, self)._auto_init() - self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_values_key_model_key2_res_id_user_id_idx'") - if not self._cr.fetchone(): - self._cr.execute("CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)") + tools.create_index(self._cr, 'ir_values_key_model_key2_res_id_user_id_idx', + self._table, ['key', 'model', 'key2', 'res_id', 'user_id']) return res @api.model diff --git a/odoo/fields.py b/odoo/fields.py index 1f65a6b61ff..6aff8c14c0c 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -16,7 +16,8 @@ import psycopg2 from odoo.sql_db import LazyCursor from odoo.tools import float_precision, float_repr, float_round, frozendict, \ - html_sanitize, human_size, pg_varchar, ustr, OrderedSet + html_sanitize, human_size, pg_varchar, table_exists, \ + table_kind, ustr, OrderedSet from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT from odoo.tools.translate import html_translate, _ @@ -2263,18 +2264,15 @@ class Many2many(_RelationalMulti): if not rel.startswith('x_'): IMR = model.env['ir.model.relation'] model.pool.post_init(IMR._reflect_relation, model, rel, self._module) - cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (rel,)) - if not cr.dictfetchall(): + if not table_exists(cr, rel): if self.comodel_name not in model.env: raise UserError(_('Many2many comodel does not exist: %r') % (self.comodel_name,)) comodel = model.env[self.comodel_name] cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s"))' % (rel, id1, id2, id1, id2)) # 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", (comodel._table,)) - if not cr.fetchall(): + if table_kind(cr, comodel._table) != 'v': model.pool.post_init(model._m2o_add_foreign_key_unchecked, rel, id2, comodel, 'cascade', self._module) - cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (model._table,)) - if not cr.fetchall(): + if table_kind(cr, model._table) != 'v': model.pool.post_init(model._m2o_add_foreign_key_unchecked, rel, id1, model, 'cascade', self._module) cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id1)) diff --git a/odoo/models.py b/odoo/models.py index ecf6c4acc08..d0ebaa8a82b 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -615,12 +615,7 @@ class BaseModel(object): @api.model @ormcache() def _is_an_ordinary_table(self): - self.env.cr.execute("""\ - SELECT 1 - FROM pg_class - WHERE relname = %s - AND relkind = %s""", [self._table, 'r']) - return bool(self.env.cr.fetchone()) + return tools.table_kind(self.env.cr, self._table) == 'r' def __export_xml_id(self): """ Return a valid xml_id for the record ``self``. """ @@ -2053,13 +2048,7 @@ class BaseModel(object): _logger.debug("column %s is in the table %s but not in the corresponding object %s", row['attname'], self._table, self._name) if row['attnotnull']: - cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" DROP NOT NULL' % (self._table, row['attname'])) - _schema.debug("Table '%s': column '%s': dropped NOT NULL constraint", - self._table, row['attname']) - - @api.model_cr - def _drop_constraint(self, source_table, constraint_name): - self._cr.execute("ALTER TABLE %s DROP CONSTRAINT %s" % (source_table, constraint_name)) + tools.drop_not_null(self._table, row['attname']) # checked version: for direct m2o starting from ``self`` def _m2o_add_foreign_key_checked(self, source_field, dest_model, ondelete): @@ -2112,18 +2101,14 @@ class BaseModel(object): if cons['ondelete_rule'] != POSTGRES_CONFDELTYPES.get((ondelete or 'set null').upper(), 'a')\ or cons['foreign_table'] != dest_model._table: # Wrong FK: drop it and recreate - _schema.debug("Table '%s': dropping obsolete FK constraint: '%s'", - source_table, cons['constraint_name']) - self._drop_constraint(source_table, cons['constraint_name']) + tools.drop_constraint(self._cr, source_table, cons['constraint_name']) else: # it's all good, nothing to do! return else: # Multiple FKs found for the same field, drop them all, and re-create for cons in constraints: - _schema.debug("Table '%s': dropping duplicate FK constraints: '%s'", - source_table, cons['constraint_name']) - self._drop_constraint(source_table, cons['constraint_name']) + tools.drop_constraint(self._cr, source_table, cons['constraint_name']) # (re-)create the FK self._m2o_add_foreign_key_checked(source_field, dest_model, ondelete) @@ -2187,26 +2172,26 @@ class BaseModel(object): cr = self._cr parent_store_compute = False update_custom_fields = self._context.get('update_custom_fields', False) - create = not self._table_exist() + must_create_table = not tools.table_exists(cr, self._table) if self._auto: - if create: - self._create_table() + if must_create_table: + tools.create_model_table(cr, self._table, self._description) has_rows = False else: cr.execute('SELECT 1 FROM "%s" LIMIT 1' % self._table) has_rows = cr.rowcount if self._parent_store: - if not self._parent_columns_exist(): + if not tools.column_exists(cr, self._table, 'parent_left'): self._create_parent_columns() parent_store_compute = True self._check_removed_columns(log=False) # retrieve existing database columns - column_data = self._select_column_data() + column_data = tools.table_columns(cr, self._table) for name, field in self._fields.iteritems(): if name == 'id': @@ -2232,9 +2217,7 @@ class BaseModel(object): if not res and hasattr(field, 'oldname'): res = column_data.get(field.oldname) if res: - cr.execute('ALTER TABLE "%s" RENAME "%s" TO "%s"' % (self._table, field.oldname, name)) - res['attname'] = name - _schema.debug("Table '%s': renamed column '%s' to '%s'", self._table, field.oldname, name) + tools.rename_column(cr, self._table, field.oldname, name) # The column already exists in database. Possibly change its # type, rename it, drop it or change its constraints. @@ -2257,33 +2240,12 @@ class BaseModel(object): ('float8', 'monetary', column_type[1], '::' + column_type[1]), ] if f_pg_type == 'varchar' and field.type == 'char' and f_pg_size and (field.size is None or f_pg_size < field.size): - try: - with cr.savepoint(): - cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" TYPE %s' % (self._table, name, column_type[1]), log_exceptions=False) - except psycopg2.NotSupportedError: - # In place alter table cannot be done because a view is depending of this field. - # Do a manual copy. This will drop the view (that will be recreated later) - cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO temp_change_size' % (self._table, name)) - cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, name, column_type[1])) - cr.execute('UPDATE "%s" SET "%s"=temp_change_size::%s' % (self._table, name, column_type[1])) - cr.execute('ALTER TABLE "%s" DROP COLUMN temp_change_size CASCADE' % (self._table,)) - _schema.debug("Table '%s': column '%s' (type varchar) changed size from %s to %s", - self._table, name, f_pg_size or 'unlimited', field.size or 'unlimited') + tools.convert_column(cr, self._table, name, column_type[1]) for c in casts: if (f_pg_type == c[0]) and (field.type == c[1]): if f_pg_type != column_type[0]: converted = True - try: - with cr.savepoint(): - cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" TYPE %s' % (self._table, name, c[2]), log_exceptions=False) - except psycopg2.NotSupportedError: - # can't do inplace change -> use a casted temp column - cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO __temp_type_cast' % (self._table, name)) - cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, name, c[2])) - cr.execute('UPDATE "%s" SET "%s"= __temp_type_cast%s' % (self._table, name, c[3])) - cr.execute('ALTER TABLE "%s" DROP COLUMN __temp_type_cast CASCADE' % (self._table,)) - _schema.debug("Table '%s': column '%s' changed type from %s to %s", - self._table, name, c[0], c[1]) + tools.convert_column(cr, self._table, name, column_type[1]) break if f_pg_type != column_type[0]: @@ -2291,46 +2253,27 @@ class BaseModel(object): i = 0 while True: newname = name + '_moved' + str(i) - cr.execute("SELECT count(1) FROM pg_class c,pg_attribute a " \ - "WHERE c.relname=%s " \ - "AND a.attname=%s " \ - "AND c.oid=a.attrelid ", (self._table, newname)) - if not cr.fetchone()[0]: + if not tools.column_exists(cr, self._table, newname): break i += 1 if f_pg_notnull: - cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" DROP NOT NULL' % (self._table, name)) - cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % (self._table, name, newname)) - cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, name, column_type[1])) - cr.execute("COMMENT ON COLUMN %s.\"%s\" IS %%s" % (self._table, name), (field.string,)) - _schema.warning("Table `%s`: column `%s` has changed type (DB=%s, def=%s), data moved to column `%s`", - self._table, name, f_pg_type, field.type, newname) + tools.drop_not_null(cr, self._table, name) + tools.rename_column(cr, self._table, name, newname) + tools.create_column(cr, self._table, name, column_type[1], field.string) # if the field is required and hasn't got a NOT NULL constraint if field.required and f_pg_notnull == 0: if has_rows: self._init_column(name) # add the NOT NULL constraint - try: - with cr.savepoint(): - cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, name), log_exceptions=False) - _schema.debug("Table '%s': column '%s': added NOT NULL constraint", - self._table, name) - except Exception: - msg = "Table '%s': unable to set a NOT NULL constraint on column '%s' !\n"\ - "If you want to have it, you should update the records and execute manually:\n"\ - "ALTER TABLE %s ALTER COLUMN %s SET NOT NULL" - _schema.warning(msg, self._table, name, self._table, name) + tools.set_not_null(cr, self._table, name) elif not field.required and f_pg_notnull == 1: - cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" DROP NOT NULL' % (self._table, name)) - _schema.debug("Table '%s': column '%s': dropped NOT NULL constraint", - self._table, name) + tools.drop_not_null(cr, self._table, name) # Verify index indexname = '%s_%s_index' % (self._table, name) - cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = %s and tablename = %s", (indexname, self._table)) - res2 = cr.dictfetchall() - if not res2 and field.index: - cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (self._table, name, self._table, name)) + has_index = tools.index_exists(cr, indexname) + if not has_index and field.index: + tools.create_index(cr, indexname, self._table, [name]) if field.type == 'text': # FIXME: for fields.text columns we should try creating GIN indexes instead (seems most suitable for an ERP context) msg = "Table '%s': Adding (b-tree) index for %s column '%s'."\ @@ -2338,10 +2281,8 @@ class BaseModel(object): " because there is a length limit for indexable btree values!\n"\ "Use a search view instead if you simply want to make the field searchable." _schema.warning(msg, self._table, field.type, name) - if res2 and not field.index: - cr.execute('DROP INDEX "%s_%s_index"' % (self._table, name)) - msg = "Table '%s': dropping index for column '%s' of type '%s' as it is not required anymore" - _schema.debug(msg, self._table, name, field.type) + if has_index and not field.index: + tools.drop_index(cr, indexname, self._table) if field.type == 'many2one': comodel = self.env[field.comodel_name] @@ -2350,10 +2291,7 @@ class BaseModel(object): else: # the column doesn't exist in database, create it - cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, name, field.column_type[1])) - cr.execute("COMMENT ON COLUMN %s.\"%s\" IS %%s" % (self._table, name), (field.string,)) - _schema.debug("Table '%s': added column '%s' with definition=%s", - self._table, name, field.column_type[1]) + tools.create_column(cr, self._table, name, field.column_type[1], field.string) # initialize it if has_rows: @@ -2372,28 +2310,15 @@ class BaseModel(object): if comodel._auto and comodel._table != 'ir_actions': self.pool.post_init(self._m2o_add_foreign_key_checked, name, comodel, field.ondelete) if field.index: - cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (self._table, name, self._table, name)) + indexname = '%s_%s_index' % (self._table, name) + tools.create_index(cr, indexname, self._table, [name]) if field.required: - try: - with cr.savepoint(): - cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, name)) - _schema.debug("Table '%s': column '%s': added a NOT NULL constraint", - self._table, name) - except Exception: - msg = "WARNING: unable to set column %s of table %s not null !\n"\ - "Try to re-run: openerp-server --update=module\n"\ - "If it doesn't work, update records and execute manually:\n"\ - "ALTER TABLE %s ALTER COLUMN %s SET NOT NULL" - _logger.warning(msg, name, self._table, self._table, name, exc_info=True) - - else: - cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (self._table,)) - create = not bool(cr.fetchone()) + tools.set_not_null(cr, self._table, name) if self._auto: self._add_sql_constraints() - if create: + if must_create_table: self._execute_sql() if parent_store_compute: @@ -2406,54 +2331,21 @@ class BaseModel(object): """ pass - @api.model_cr - def _table_exist(self): - query = "SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s" - self._cr.execute(query, (self._table,)) - return self._cr.rowcount - - @api.model_cr - def _create_table(self): - self._cr.execute('CREATE TABLE "%s" (id SERIAL NOT NULL, PRIMARY KEY(id))' % (self._table,)) - self._cr.execute("COMMENT ON TABLE \"%s\" IS %%s" % self._table, (self._description,)) - _schema.debug("Table '%s': created", self._table) - - @api.model_cr - def _parent_columns_exist(self): - query = """ SELECT c.relname FROM pg_class c, pg_attribute a - WHERE c.relname=%s AND a.attname=%s AND c.oid=a.attrelid """ - self._cr.execute(query, (self._table, 'parent_left')) - return self._cr.rowcount - @api.model_cr def _create_parent_columns(self): - self._cr.execute('ALTER TABLE "%s" ADD COLUMN "parent_left" INTEGER' % (self._table,)) - self._cr.execute('ALTER TABLE "%s" ADD COLUMN "parent_right" INTEGER' % (self._table,)) + tools.create_column(self._cr, self._table, 'parent_left', 'INTEGER') + tools.create_column(self._cr, self._table, 'parent_right', 'INTEGER') if 'parent_left' not in self._fields: _logger.error("add a field parent_left on model %s: parent_left = fields.Integer('Left Parent', index=True)", self._name) - _schema.debug("Table '%s': added column '%s' with definition=%s", self._table, 'parent_left', 'INTEGER') elif not self._fields['parent_left'].index: _logger.error('parent_left field on model %s must be indexed! Add index=True to the field definition)', self._name) if 'parent_right' not in self._fields: _logger.error("add a field parent_right on model %s: parent_right = fields.Integer('Left Parent', index=True)", self._name) - _schema.debug("Table '%s': added column '%s' with definition=%s", self._table, 'parent_right', 'INTEGER') elif not self._fields['parent_right'].index: _logger.error("parent_right field on model %s must be indexed! Add index=True to the field definition)", self._name) if self._fields[self._parent_name].ondelete not in ('cascade', 'restrict'): _logger.error("The field %s on model %s must be set as ondelete='cascade' or 'restrict'", self._parent_name, self._name) - @api.model_cr - def _select_column_data(self): - # attlen is the number of bytes necessary to represent the type when - # the type has a fixed size. If the type has a varying size attlen is - # -1 and atttypmod is the size limit + 4, or -1 if there is no limit. - query = """ SELECT c.relname, a.attname, a.attlen, a.atttypmod, a.attnotnull, a.atthasdef, t.typname, - CASE WHEN a.attlen=-1 THEN (CASE WHEN a.atttypmod=-1 THEN 0 ELSE a.atttypmod-4 END) ELSE a.attlen END as size - FROM pg_class c, pg_attribute a, pg_type t - WHERE c.relname=%s AND c.oid=a.attrelid AND a.atttypid=t.oid """ - self._cr.execute(query, (self._table,)) - return {row['attname']: row for row in self._cr.dictfetchall()} - @api.model_cr def _add_sql_constraints(self): """ @@ -2468,37 +2360,16 @@ class BaseModel(object): def cons_text(txt): return txt.lower().replace(', ',',').replace(' (','(') - def drop(name, definition): - try: - with cr.savepoint(): - cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (self._table, name)) - _schema.debug("Table '%s': dropped constraint '%s'. Reason: its definition changed to '%s'", - self._table, name, definition) - except Exception: - _schema.warning("Table '%s': unable to drop constraint '%s'!", self._table, definition) - - def add(name, definition): - query = 'ALTER TABLE "%s" ADD CONSTRAINT "%s" %s' % (self._table, name, definition) - try: - with cr.savepoint(): - cr.execute(query) - _schema.debug("Table '%s': added constraint '%s' with definition=%s", - self._table, name, definition) - except Exception: - _schema.warning("Table '%s': unable to add constraint '%s'!\n" - "If you want to have it, you should update the records and execute manually:\n%s", - self._table, definition, query) - def process(key, definition): conname = '%s_%s' % (self._table, key) - cr.execute("SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname=%s", (conname,)) - if not cr.rowcount: + has_definition = tools.constraint_definition(cr, conname) + if not has_definition: # constraint does not exists - add(conname, definition) - elif cons_text(definition) != cons_text(cr.fetchone()[0]): + tools.add_constraint(cr, self._table, conname, definition) + elif cons_text(definition) != cons_text(has_definition): # constraint exists but its definition may have changed - drop(conname, definition) - add(conname, definition) + tools.drop_constraint(cr, self._table, conname) + tools.add_constraint(cr, self._table, conname, definition) for (key, definition, _) in self._sql_constraints: if foreign_key_re.match(definition): diff --git a/odoo/modules/db.py b/odoo/modules/db.py index f2592f947e2..7106ff82b69 100644 --- a/odoo/modules/db.py +++ b/odoo/modules/db.py @@ -12,8 +12,7 @@ def is_initialized(cr): The database can be initialized with the 'initialize' function below. """ - cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'") - return len(cr.fetchall()) > 0 + return odoo.tools.table_exists(cr, 'ir_module_module') def initialize(cr): """ Initialize a database with for the ORM. diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py index 5d20ff20dab..a6ab4ead1b4 100644 --- a/odoo/modules/registry.py +++ b/odoo/modules/registry.py @@ -15,7 +15,7 @@ import threading import odoo from .. import SUPERUSER_ID from odoo.tools import (assertion_report, lazy_classproperty, config, - lazy_property, topological_sort, OrderedSet) + lazy_property, table_exists, topological_sort, OrderedSet) from odoo.tools.lru import LRU _logger = logging.getLogger(__name__) @@ -333,7 +333,7 @@ class Registry(Mapping): # make sure all tables are present missing = [name for name, model in env.items() - if not model._abstract and not model._table_exist()] + if not model._abstract and not table_exists(cr, model._table)] if missing: _logger.warning("Models have no table: %s.", ", ".join(missing)) # recreate missing tables following model dependencies @@ -345,7 +345,7 @@ class Registry(Mapping): cr.commit() # check again, and log errors if tables are still missing for name, model in env.items(): - if not model._abstract and not model._table_exist(): + if not model._abstract and not table_exists(cr, model._table): _logger.error("Model %s has no table.", name) def clear_caches(self): diff --git a/odoo/service/db.py b/odoo/service/db.py index 5e74c6d38b3..a9968963fb8 100644 --- a/odoo/service/db.py +++ b/odoo/service/db.py @@ -354,8 +354,7 @@ def list_db_incompatible(databases): server_version = '.'.join(map(str, version_info[:2])) for database_name in databases: with closing(db_connect(database_name).cursor()) as cr: - cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name='ir_module_module'") - if cr.fetchone(): + if odoo.tools.table_exists(cr, 'ir_module_module'): cr.execute("SELECT latest_version FROM ir_module_module WHERE name=%s", ('base',)) base_version = cr.fetchone() if not base_version or not base_version[0]: diff --git a/odoo/tools/sql.py b/odoo/tools/sql.py index 0c67657db8d..6bdcbb05303 100644 --- a/odoo/tools/sql.py +++ b/odoo/tools/sql.py @@ -1,6 +1,152 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import logging +import psycopg2 + +_schema = logging.getLogger('odoo.schema') + +def table_exists(cr, tablename): + """ Return whether the given table exists. """ + query = "SELECT 1 FROM pg_class WHERE relkind IN ('r','v') AND relname=%s" + cr.execute(query, (tablename,)) + return cr.rowcount + +def table_kind(cr, tablename): + """ Return the kind of a table: 'r' for ordinary tables, 'v' for views. """ + cr.execute("SELECT relkind FROM pg_class WHERE relname=%s", (tablename,)) + return cr.fetchone()[0] if cr.rowcount else None + +def create_model_table(cr, tablename, comment=None): + """ Create the table for a model. """ + cr.execute('CREATE TABLE "{}" (id SERIAL NOT NULL, PRIMARY KEY(id))'.format(tablename)) + if comment: + cr.execute('COMMENT ON TABLE "{}" IS %s'.format(tablename), (comment,)) + _schema.debug("Table %r: created", tablename) + +def table_columns(cr, tablename): + """ Return a dict mapping column names to their configuration. The latter is + a dict with the following keys: `relname` (table name), `attname` + (column name), `attlen`, `atttypmod`, `attnotnull` (whether it has a NOT + NULL constraint), `atthasdef` (whether it has a default value), + `typname` (data type name), `size` (varchar size). + """ + # attlen is the number of bytes necessary to represent the type when the + # type has a fixed size. If the type has a varying size attlen is -1 and + # atttypmod is the size limit + 4, or -1 if there is no limit. + query = """ SELECT c.relname, a.attname, a.attlen, a.atttypmod, + a.attnotnull, a.atthasdef, t.typname, + CASE WHEN a.attlen=-1 THEN ( + CASE WHEN a.atttypmod=-1 THEN 0 ELSE a.atttypmod-4 END + ) ELSE a.attlen END as size + FROM pg_class c, pg_attribute a, pg_type t + WHERE c.relname=%s AND c.oid=a.attrelid AND a.atttypid=t.oid """ + cr.execute(query, (tablename,)) + return {row['attname']: row for row in cr.dictfetchall()} + +def column_exists(cr, tablename, columnname): + """ Return whether the given column exists. """ + query = """ SELECT 1 FROM pg_class c, pg_attribute a + WHERE c.relname=%s AND a.attname=%s AND c.oid=a.attrelid """ + cr.execute(query, (tablename, columnname)) + return cr.rowcount + +def create_column(cr, tablename, columnname, columntype, comment=None): + """ Create a column with the given type. """ + cr.execute('ALTER TABLE "{}" ADD COLUMN "{}" {}'.format(tablename, columnname, columntype)) + if comment: + cr.execute('COMMENT ON COLUMN "{}"."{}" IS %s'.format(tablename, columnname), (comment,)) + _schema.debug("Table %r: added column %r of type %s", tablename, columnname, columntype) + +def rename_column(cr, tablename, columnname1, columnname2): + """ Rename the given column. """ + cr.execute('ALTER TABLE "{}" RENAME COLUMN "{}" TO "{}"'.format(tablename, columnname1, columnname2)) + _schema.debug("Table %r: renamed column %r to %r", tablename, columnname1, columnname2) + +def convert_column(cr, tablename, columnname, columntype): + """ Convert the column to the given type. """ + try: + with cr.savepoint(): + cr.execute('ALTER TABLE "{}" ALTER COLUMN "{}" TYPE {}'.format(tablename, columnname, columntype), + log_exceptions=False) + except psycopg2.NotSupportedError: + # can't do inplace change -> use a casted temp column + query = 'ALTER TABLE "{0}" RENAME COLUMN "{1}" TO __temp_type_cast; ' \ + 'ALTER TABLE "{0}" ADD COLUMN "{1}" {2}; ' \ + 'UPDATE "{0}" SET "{1}"= __temp_type_cast::{2}' \ + 'ALTER TABLE "{0}" DROP COLUMN __temp_type_cast CASCADE' + cr.execute(query.format(tablename, columntype, columntype)) + _schema.debug("Table %r: column %r changed to type %s", tablename, columntype, columntype) + +def set_not_null(cr, tablename, columnname): + """ Add a NOT NULL constraint on the given column. """ + query = 'ALTER TABLE "{}" ALTER COLUMN "{}" SET NOT NULL'.format(tablename, columnname) + try: + with cr.savepoint(): + cr.execute(query) + _schema.debug("Table %r: column %r: added constraint NOT NULL", tablename, columnname) + except Exception: + msg = "Table %r: unable to set NOT NULL on column %r!\n" \ + "If you want to have it, you should update the records and execute manually:\n%s" + _schema.warning(msg, tablename, columnname, query, exc_info=True) + +def drop_not_null(cr, tablename, columnname): + """ Drop the NOT NULL constraint on the given column. """ + cr.execute('ALTER TABLE "{}" ALTER COLUMN "{}" DROP NOT NULL'.format(tablename, columnname)) + _schema.debug("Table %r: column %r: dropped constraint NOT NULL", tablename, columnname) + +def constraint_definition(cr, constraintname): + """ Return the given constraint's definition. """ + cr.execute("SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname=%s", (constraintname,)) + return cr.fetchone()[0] if cr.rowcount else None + +def add_constraint(cr, tablename, constraintname, definition): + """ Add a constraint on the given table. """ + query = 'ALTER TABLE "{}" ADD CONSTRAINT "{}" {}'.format(tablename, constraintname, definition) + try: + with cr.savepoint(): + cr.execute(query) + _schema.debug("Table %r: added constraint %r as %s", tablename, constraintname, definition) + except Exception: + msg = "Table %r: unable to add constraint %r!\n" \ + "If you want to have it, you should update the records and execute manually:\n%s" + _schema.warning(msg, tablename, constraintname, query, exc_info=True) + +def drop_constraint(cr, tablename, constraintname): + """ drop the given constraint. """ + try: + with cr.savepoint(): + cr.execute('ALTER TABLE "{}" DROP CONSTRAINT "{}"'.format(tablename, constraintname)) + _schema.debug("Table %r: dropped constraint %r", tablename, constraintname) + except Exception: + _schema.warning("Table %r: unable to drop constraint %r!", tablename, constraintname) + +def index_exists(cr, indexname): + """ Return whether the given index exists. """ + cr.execute("SELECT 1 FROM pg_indexes WHERE indexname=%s", (indexname,)) + return cr.rowcount + +def create_index(cr, indexname, tablename, expressions): + """ Create the given index unless it exists. """ + if index_exists(cr, indexname): + return + args = ', '.join(expressions) + cr.execute('CREATE INDEX "{}" ON "{}" ({})'.format(indexname, tablename, args)) + _schema.debug("Table %r: created index %r (%s)", tablename, indexname, args) + +def create_unique_index(cr, indexname, tablename, expressions): + """ Create the given index unless it exists. """ + if index_exists(cr, indexname): + return + args = ', '.join(expressions) + cr.execute('CREATE UNIQUE INDEX "{}" ON "{}" ({})'.format(indexname, tablename, args)) + _schema.debug("Table %r: created index %r (%s)", tablename, indexname, args) + +def drop_index(cr, indexname, tablename): + """ Drop the given index if it exists. """ + cr.execute('DROP INDEX IF EXISTS "{}"'.format(indexname)) + _schema.debug("Table %r: dropped index %r", tablename, indexname) + def drop_view_if_exists(cr, viewname): cr.execute("DROP view IF EXISTS %s CASCADE" % (viewname,)) cr.commit() From dc82ff59accf0733229f39b57fc030782600530e Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 7 Feb 2017 10:20:22 +0100 Subject: [PATCH 13/15] [REF] models: in `_auto_init`, delegate field schema updates to fields The fields objects are responsible for creating their column/table and indexes. The methods that update the schema are overridden to handle cases specific to each type of field. --- odoo/fields.py | 135 +++++++++++++++++++++++++++++++++++++++++---- odoo/models.py | 146 +++++++------------------------------------------ 2 files changed, 142 insertions(+), 139 deletions(-) diff --git a/odoo/fields.py b/odoo/fields.py index 6aff8c14c0c..6d8c85a3a63 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -16,11 +16,11 @@ import psycopg2 from odoo.sql_db import LazyCursor from odoo.tools import float_precision, float_repr, float_round, frozendict, \ - html_sanitize, human_size, pg_varchar, table_exists, \ - table_kind, ustr, OrderedSet + html_sanitize, human_size, pg_varchar, ustr, OrderedSet from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT from odoo.tools.translate import html_translate, _ +import odoo.tools.sql as sql DATE_LENGTH = len(date.today().strftime(DATE_FORMAT)) DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT)) @@ -285,6 +285,7 @@ class Field(object): column_type = None # database column type (ident, spec) column_format = '%s' # placeholder for value in queries + column_cast_from = () # column types that may be cast to this _slots = { 'args': EMPTY_DICT, # the parameters given to __init__() @@ -793,14 +794,91 @@ class Field(object): ############################################################################ # - # Read from/write to database + # Update database schema # - def check_schema(self, model): - """ Make sure the database contains everything for this field. Return - ``True`` if the schema was altered and the field must be recomputed. + def update_db(self, model, columns): + """ Update the database schema to implement this field. + + :param model: an instance of the field's model + :param columns: a dict mapping column names to their configuration in database + :return: ``True`` if the field must be recomputed on existing rows """ - pass + if not self.column_type: + return + + column = columns.get(self.name) + if not column and hasattr(self, 'oldname'): + # column not found; check whether it exists under its old name + column = columns.get(self.oldname) + if column: + sql.rename_column(model._cr, model._table, self.oldname, self.name) + + # create/update the column, not null constraint, indexes + self.update_db_column(model, column) + self.update_db_notnull(model, column) + self.update_db_index(model, column) + + return not column + + def update_db_column(self, model, column): + """ Create/update the column corresponding to ``self``. + + :param model: an instance of the field's model + :param column: the column's configuration (dict) if it exists, or ``None`` + """ + if not column: + # the column does not exist, create it + sql.create_column(model._cr, model._table, self.name, self.column_type[1], self.string) + return + if column['typname'] == self.column_type[0]: + return + if column['typname'] in self.column_cast_from: + sql.convert_column(model._cr, model._table, self.name, self.column_type[1]) + else: + newname = (self.name + '_moved{}').format + i = 0 + while sql.column_exists(model._cr, model._table, newname(i)): + i += 1 + if column['attnotnull']: + sql.drop_not_null(model._cr, model._table, self.name) + sql.rename_column(model._cr, model._table, self.name, newname(i)) + sql.create_column(model._cr, model._table, self.name, self.column_type[1], self.string) + + def update_db_notnull(self, model, column): + """ Add or remove the NOT NULL constraint on ``self``. + + :param model: an instance of the field's model + :param column: the column's configuration (dict) if it exists, or ``None`` + """ + has_notnull = column and column['attnotnull'] + + if not column or (self.required and not has_notnull): + # the column is new or it becomes required; initialize its values + if model._table_has_rows(): + model._init_column(self.name) + + if self.required and not has_notnull: + sql.set_not_null(model._cr, model._table, self.name) + elif not self.required and has_notnull: + sql.drop_not_null(model._cr, model._table, self.name) + + def update_db_index(self, model, column): + """ Add or remove the index corresponding to ``self``. + + :param model: an instance of the field's model + :param column: the column's configuration (dict) if it exists, or ``None`` + """ + indexname = '%s_%s_index' % (model._table, self.name) + if self.index: + sql.create_index(model._cr, indexname, model._table, ['"%s"' % self.name]) + else: + sql.drop_index(model._cr, indexname, model._table) + + ############################################################################ + # + # Read from/write to database + # def read(self, records): """ Read the value of ``self`` on ``records``, and store it in cache. """ @@ -1111,6 +1189,7 @@ class Float(Field): cursor and returning a pair (total, decimal) """ type = 'float' + column_cast_from = ('int4', 'numeric', 'float8') _slots = { '_digits': None, # digits argument passed to class initializer 'group_operator': 'sum', @@ -1171,6 +1250,7 @@ class Monetary(Field): """ type = 'monetary' column_type = ('numeric', 'numeric') + column_cast_from = ('float8',) _slots = { 'currency_field': None, 'group_operator': 'sum', @@ -1286,6 +1366,7 @@ class Char(_String): translation of terms. """ type = 'char' + column_cast_from = ('text',) _slots = { 'size': None, # maximum size of values (deprecated) } @@ -1294,6 +1375,15 @@ class Char(_String): def column_type(self): return ('varchar', pg_varchar(self.size)) + def update_db_column(self, model, column): + if ( + column and column['typname'] == 'varchar' and column['size'] and + (self.size is None or column['size'] < self.size) + ): + # the column's varchar size does not match self.size; convert it + sql.convert_column(model._cr, model._table, self.name, self.column_type[1]) + super(Char, self).update_db_column(model, column) + _related_size = property(attrgetter('size')) _description_size = property(attrgetter('size')) @@ -1331,6 +1421,7 @@ class Text(_String): """ type = 'text' column_type = ('text', 'text') + column_cast_from = ('varchar',) def convert_to_cache(self, value, record, validate=True): if value is None or value is False: @@ -1400,6 +1491,7 @@ class Html(_String): class Date(Field): type = 'date' column_type = ('date', 'date') + column_cast_from = ('timestamp',) @staticmethod def today(*args): @@ -1463,6 +1555,7 @@ class Date(Field): class Datetime(Field): type = 'datetime' column_type = ('timestamp', 'timestamp') + column_cast_from = ('date',) @staticmethod def now(*args): @@ -1842,6 +1935,21 @@ class Many2one(_Relational): if not self.delegate: self.delegate = name in model._inherits.values() + def update_db(self, model, columns): + if self.comodel_name not in model.env: + raise ValueError(_('There is no reference available for %s') % (self.comodel_name,)) + return super(Many2one, self).update_db(model, columns) + + def update_db_column(self, model, column): + super(Many2one, self).update_db_column(model, column) + comodel = model.env[self.comodel_name] + # Note: ir_actions is inherited, so foreign key doesn't work on it + if comodel._auto and comodel._table != 'ir_actions': + if not column: + model.pool.post_init(model._m2o_add_foreign_key_checked, self.name, comodel, self.ondelete) + else: + model.pool.post_init(model._m2o_fix_foreign_key, model._table, self.name, comodel, self.ondelete) + def _update(self, records, value): """ Update the cached value of ``self`` for ``records`` with ``value``. This is used to reflect the assignment ``value[name] = records``, where @@ -2100,7 +2208,7 @@ class One2many(_RelationalMulti): fnames.discard(self.inverse_name) return super(One2many, self).convert_to_onchange(value, record, fnames) - def check_schema(self, model): + def update_db(self, model, columns): if self.comodel_name in model.env: comodel = model.env[self.comodel_name] if self.inverse_name not in comodel._fields: @@ -2255,7 +2363,7 @@ class Many2many(_RelationalMulti): # add self in m2m, so that its inverse field can find it m2m[(self.relation, self.column1, self.column2)] = self - def check_schema(self, model): + def update_db(self, model, columns): cr = model._cr rel, id1, id2 = self.relation, self.column1, self.column2 # do not create relations for custom fields as they do not belong to a module @@ -2264,15 +2372,15 @@ class Many2many(_RelationalMulti): if not rel.startswith('x_'): IMR = model.env['ir.model.relation'] model.pool.post_init(IMR._reflect_relation, model, rel, self._module) - if not table_exists(cr, rel): + if not sql.table_exists(cr, rel): if self.comodel_name not in model.env: raise UserError(_('Many2many comodel does not exist: %r') % (self.comodel_name,)) comodel = model.env[self.comodel_name] cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s"))' % (rel, id1, id2, id1, id2)) # create foreign key references with ondelete=cascade, unless the targets are SQL views - if table_kind(cr, comodel._table) != 'v': + if sql.table_kind(cr, comodel._table) != 'v': model.pool.post_init(model._m2o_add_foreign_key_unchecked, rel, id2, comodel, 'cascade', self._module) - if table_kind(cr, model._table) != 'v': + if sql.table_kind(cr, model._table) != 'v': model.pool.post_init(model._m2o_add_foreign_key_unchecked, rel, id1, model, 'cascade', self._module) cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id1)) @@ -2371,6 +2479,9 @@ class Id(Field): 'readonly': True, } + def update_db(self, model, columns): + pass # this column is created with the table + def __get__(self, record, owner): if record is None: return self # the field is accessed through the class owner diff --git a/odoo/models.py b/odoo/models.py index d0ebaa8a82b..9cc5cc63509 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2137,6 +2137,14 @@ class BaseModel(object): self._table, column_name, field.column_format, column_name) self._cr.execute(query, (value,)) + @ormcache() + def _table_has_rows(self): + """ Return whether the model's table has rows. This method should only + be used when updating the database schema (:meth:`~._auto_init`). + """ + self.env.cr.execute('SELECT 1 FROM "%s" LIMIT 1' % self._table) + return self.env.cr.rowcount + @api.model_cr_context def _auto_init(self): """ Initialize the database schema of ``self``: @@ -2162,11 +2170,6 @@ class BaseModel(object): # has not been added in database yet! self = self.with_context(prefetch_fields=False) - def recompute(field): - _logger.info("Storing computed values of %s", field) - recs = self.with_context(active_test=False).search([]) - recs._recompute_todo(field) - self.pool.post_init(self._reflect) cr = self._cr @@ -2175,13 +2178,8 @@ class BaseModel(object): must_create_table = not tools.table_exists(cr, self._table) if self._auto: - if must_create_table: tools.create_model_table(cr, self._table, self._description) - has_rows = False - else: - cr.execute('SELECT 1 FROM "%s" LIMIT 1' % self._table) - has_rows = cr.rowcount if self._parent_store: if not tools.column_exists(cr, self._table, 'parent_left'): @@ -2190,130 +2188,24 @@ class BaseModel(object): self._check_removed_columns(log=False) - # retrieve existing database columns - column_data = tools.table_columns(cr, self._table) + # update the database schema for fields + columns = tools.table_columns(cr, self._table) - for name, field in self._fields.iteritems(): - if name == 'id': - continue + def recompute(field): + _logger.info("Storing computed values of %s", field) + recs = self.with_context(active_test=False).search([]) + recs._recompute_todo(field) + for field in self._fields.itervalues(): if not field.store: continue if field.manual and not update_custom_fields: - # Don't update custom (also called manual) fields - continue + continue # don't update custom fields - if not field.column_type: - # the field is not stored as a column - if field.check_schema(self) and field.compute: - self.pool.post_init(recompute, field) - - else: - res = column_data.get(name) - - # The column is not found as-is in database. Check whether - # it exists with an old name, and rename it. - if not res and hasattr(field, 'oldname'): - res = column_data.get(field.oldname) - if res: - tools.rename_column(cr, self._table, field.oldname, name) - - # The column already exists in database. Possibly change its - # type, rename it, drop it or change its constraints. - if res: - f_pg_type = res['typname'] - f_pg_size = res['size'] - f_pg_notnull = res['attnotnull'] - column_type = field.column_type - - if column_type: - converted = False - casts = [ - ('text', 'char', column_type[1], '::' + column_type[1]), - ('varchar', 'text', 'TEXT', ''), - ('int4', 'float', column_type[1], '::' + column_type[1]), - ('date', 'datetime', 'TIMESTAMP', '::TIMESTAMP'), - ('timestamp', 'date', 'date', '::date'), - ('numeric', 'float', column_type[1], '::' + column_type[1]), - ('float8', 'float', column_type[1], '::' + column_type[1]), - ('float8', 'monetary', column_type[1], '::' + column_type[1]), - ] - if f_pg_type == 'varchar' and field.type == 'char' and f_pg_size and (field.size is None or f_pg_size < field.size): - tools.convert_column(cr, self._table, name, column_type[1]) - for c in casts: - if (f_pg_type == c[0]) and (field.type == c[1]): - if f_pg_type != column_type[0]: - converted = True - tools.convert_column(cr, self._table, name, column_type[1]) - break - - if f_pg_type != column_type[0]: - if not converted: - i = 0 - while True: - newname = name + '_moved' + str(i) - if not tools.column_exists(cr, self._table, newname): - break - i += 1 - if f_pg_notnull: - tools.drop_not_null(cr, self._table, name) - tools.rename_column(cr, self._table, name, newname) - tools.create_column(cr, self._table, name, column_type[1], field.string) - - # if the field is required and hasn't got a NOT NULL constraint - if field.required and f_pg_notnull == 0: - if has_rows: - self._init_column(name) - # add the NOT NULL constraint - tools.set_not_null(cr, self._table, name) - elif not field.required and f_pg_notnull == 1: - tools.drop_not_null(cr, self._table, name) - # Verify index - indexname = '%s_%s_index' % (self._table, name) - has_index = tools.index_exists(cr, indexname) - if not has_index and field.index: - tools.create_index(cr, indexname, self._table, [name]) - if field.type == 'text': - # FIXME: for fields.text columns we should try creating GIN indexes instead (seems most suitable for an ERP context) - msg = "Table '%s': Adding (b-tree) index for %s column '%s'."\ - "This is probably useless (does not work for fulltext search) and prevents INSERTs of long texts"\ - " because there is a length limit for indexable btree values!\n"\ - "Use a search view instead if you simply want to make the field searchable." - _schema.warning(msg, self._table, field.type, name) - if has_index and not field.index: - tools.drop_index(cr, indexname, self._table) - - if field.type == 'many2one': - comodel = self.env[field.comodel_name] - if comodel._auto and comodel._table != 'ir_actions': - self.pool.post_init(self._m2o_fix_foreign_key, self._table, name, comodel, field.ondelete) - - else: - # the column doesn't exist in database, create it - tools.create_column(cr, self._table, name, field.column_type[1], field.string) - - # initialize it - if has_rows: - self._init_column(name) - - # remember new-style stored fields with compute method - if field.compute: - self.pool.post_init(recompute, field) - - # and add constraints if needed - if field.type == 'many2one' and field.store: - if field.comodel_name not in self.env: - raise ValueError(_('There is no reference available for %s') % (field.comodel_name,)) - comodel = self.env[field.comodel_name] - # ir_actions is inherited so foreign key doesn't work on it - if comodel._auto and comodel._table != 'ir_actions': - self.pool.post_init(self._m2o_add_foreign_key_checked, name, comodel, field.ondelete) - if field.index: - indexname = '%s_%s_index' % (self._table, name) - tools.create_index(cr, indexname, self._table, [name]) - if field.required: - tools.set_not_null(cr, self._table, name) + new = field.update_db(self, columns) + if new and field.compute: + self.pool.post_init(recompute, field) if self._auto: self._add_sql_constraints() From f8e573db2350b025afec8407eeb5849c7a31afa4 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 7 Feb 2017 10:21:15 +0100 Subject: [PATCH 14/15] [REF] models: refactoring of foreign keys --- odoo/fields.py | 80 ++++++++++++++++++++++++++++++----------------- odoo/models.py | 71 ----------------------------------------- odoo/tools/sql.py | 39 +++++++++++++++++++++++ 3 files changed, 90 insertions(+), 100 deletions(-) diff --git a/odoo/fields.py b/odoo/fields.py index 6d8c85a3a63..ad39822f63e 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -1936,19 +1936,31 @@ class Many2one(_Relational): self.delegate = name in model._inherits.values() def update_db(self, model, columns): - if self.comodel_name not in model.env: - raise ValueError(_('There is no reference available for %s') % (self.comodel_name,)) + comodel = model.env[self.comodel_name] + if not model.is_transient() and comodel.is_transient(): + raise ValueError('Many2one %s from Model to TransientModel is forbidden' % self) + if model.is_transient() and not comodel.is_transient(): + # Many2one relations from TransientModel Model are annoying because + # they can block deletion due to foreign keys. So unless stated + # otherwise, we default them to ondelete='cascade'. + self.ondelete = self.ondelete or 'cascade' return super(Many2one, self).update_db(model, columns) def update_db_column(self, model, column): super(Many2one, self).update_db_column(model, column) + model.pool.post_init(self.update_db_foreign_key, model, column) + + def update_db_foreign_key(self, model, column): comodel = model.env[self.comodel_name] - # Note: ir_actions is inherited, so foreign key doesn't work on it - if comodel._auto and comodel._table != 'ir_actions': - if not column: - model.pool.post_init(model._m2o_add_foreign_key_checked, self.name, comodel, self.ondelete) - else: - model.pool.post_init(model._m2o_fix_foreign_key, model._table, self.name, comodel, self.ondelete) + # ir_actions is inherited, so foreign key doesn't work on it + if not comodel._auto or comodel._table == 'ir_actions': + return + # create/update the foreign key, and reflect it in 'ir.model.constraint' + process = sql.fix_foreign_key if column else sql.add_foreign_key + new = process(model._cr, model._table, self.name, comodel._table, 'id', self.ondelete or 'set null') + if new: + conname = '%s_%s_fkey' % (model._table, self.name) + model.env['ir.model.constraint']._reflect_constraint(model, conname, 'f', None, self._module) def _update(self, records, value): """ Update the cached value of ``self`` for ``records`` with ``value``. @@ -2365,30 +2377,40 @@ class Many2many(_RelationalMulti): def update_db(self, model, columns): cr = model._cr - rel, id1, id2 = self.relation, self.column1, self.column2 - # do not create relations for custom fields as they do not belong to a module - # they will be automatically removed when dropping the corresponding ir.model.field - # table name for custom relation all starts with x_, see __init__ - if not rel.startswith('x_'): - IMR = model.env['ir.model.relation'] - model.pool.post_init(IMR._reflect_relation, model, rel, self._module) - if not sql.table_exists(cr, rel): - if self.comodel_name not in model.env: - raise UserError(_('Many2many comodel does not exist: %r') % (self.comodel_name,)) + # Do not reflect relations for custom fields, as they do not belong to a + # module. They are automatically removed when dropping the corresponding + # 'ir.model.field'. + if not self.manual: + model.pool.post_init(model.env['ir.model.relation']._reflect_relation, + model, self.relation, self._module) + if not sql.table_exists(cr, self.relation): comodel = model.env[self.comodel_name] - cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s"))' % (rel, id1, id2, id1, id2)) - # create foreign key references with ondelete=cascade, unless the targets are SQL views - if sql.table_kind(cr, comodel._table) != 'v': - model.pool.post_init(model._m2o_add_foreign_key_unchecked, rel, id2, comodel, 'cascade', self._module) - if sql.table_kind(cr, model._table) != 'v': - model.pool.post_init(model._m2o_add_foreign_key_unchecked, rel, id1, model, 'cascade', self._module) - - cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id1)) - cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id2)) - cr.execute("COMMENT ON TABLE \"%s\" IS 'RELATION BETWEEN %s AND %s'" % (rel, model._table, comodel._table)) - _schema.debug("Create table '%s': m2m relation between '%s' and '%s'", rel, model._table, comodel._table) + query = """ + CREATE TABLE "{rel}" ("{id1}" INTEGER NOT NULL, + "{id2}" INTEGER NOT NULL, + UNIQUE("{id1}","{id2}")); + COMMENT ON TABLE "{rel}" IS %s; + CREATE INDEX ON "{rel}" ("{id1}"); + CREATE INDEX ON "{rel}" ("{id2}") + """.format(rel=self.relation, id1=self.column1, id2=self.column2) + cr.execute(query, ['RELATION BETWEEN %s AND %s' % (model._table, comodel._table)]) + _schema.debug("Create table %r: m2m relation between %r and %r", self.relation, model._table, comodel._table) + model.pool.post_init(self.update_db_foreign_keys, model) return True + def update_db_foreign_keys(self, model): + """ Add the foreign keys corresponding to the field's relation table. """ + cr = model._cr + comodel = model.env[self.comodel_name] + reflect = model.env['ir.model.constraint']._reflect_constraint + # create foreign key references with ondelete=cascade, unless the targets are SQL views + if sql.table_kind(cr, model._table) != 'v': + sql.add_foreign_key(cr, self.relation, self.column1, model._table, 'id', 'cascade') + reflect(model, '%s_%s_fkey' % (self.relation, self.column1), 'f', None, self._module) + if sql.table_kind(cr, comodel._table) != 'v': + sql.add_foreign_key(cr, self.relation, self.column2, comodel._table, 'id', 'cascade') + reflect(model, '%s_%s_fkey' % (self.relation, self.column2), 'f', None, self._module) + def read(self, records): comodel = records.env[self.comodel_name] diff --git a/odoo/models.py b/odoo/models.py index 9cc5cc63509..ea5f49056fa 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -106,14 +106,6 @@ def check_method_name(name): if regex_private.match(name): raise AccessError(_('Private methods (such as %s) cannot be called remotely.') % (name,)) -POSTGRES_CONFDELTYPES = { - 'RESTRICT': 'r', - 'NO ACTION': 'a', - 'CASCADE': 'c', - 'SET NULL': 'n', - 'SET DEFAULT': 'd', -} - def intersect(la, lb): return filter(lambda x: x in lb, la) @@ -2050,69 +2042,6 @@ class BaseModel(object): if row['attnotnull']: tools.drop_not_null(self._table, row['attname']) - # 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(), \ - 'Many2One relationships from non-transient Model to TransientModel are forbidden' - if self.is_transient() and not dest_model.is_transient(): - # TransientModel relationships to regular Models are annoying - # usually because they could block deletion due to the FKs. - # So unless stated otherwise we default them to ondelete=cascade. - ondelete = ondelete or 'cascade' - field = self._fields[source_field] - self._m2o_add_foreign_key_unchecked(self._table, source_field, dest_model, ondelete, field._module) - - # unchecked version: for custom cases, such as m2m relationships - def _m2o_add_foreign_key_unchecked(self, source_table, source_field, dest_model, ondelete, module): - cr = self._cr - query = 'ALTER TABLE "%s" ADD FOREIGN KEY ("%s") REFERENCES "%s" ON DELETE %s' - cr.execute(query % (source_table, source_field, dest_model._table, ondelete or 'set null')) - _schema.debug("Table '%s': added foreign key '%s' with definition=REFERENCES \"%s\" ON DELETE %s", - source_table, source_field, dest_model._table, ondelete or 'set null') - conname = "%s_%s_fkey" % (source_table, source_field) - self.env['ir.model.constraint']._reflect_constraint(self, conname, 'f', None, module) - - @api.model_cr - def _m2o_fix_foreign_key(self, source_table, source_field, dest_model, ondelete): - # Find FK constraint(s) currently established for the m2o field, - # and see whether they are stale or not - query = """ SELECT confdeltype as ondelete_rule, conname as constraint_name, - cl2.relname as foreign_table - FROM pg_constraint as con, pg_class as cl1, pg_class as cl2, - pg_attribute as att1, pg_attribute as att2 - WHERE con.conrelid = cl1.oid - AND cl1.relname = %s - AND con.confrelid = cl2.oid - AND array_lower(con.conkey, 1) = 1 - AND con.conkey[1] = att1.attnum - AND att1.attrelid = cl1.oid - AND att1.attname = %s - AND array_lower(con.confkey, 1) = 1 - AND con.confkey[1] = att2.attnum - AND att2.attrelid = cl2.oid - AND att2.attname = %s - AND con.contype = 'f' """ - self._cr.execute(query, (source_table, source_field, 'id')) - constraints = self._cr.dictfetchall() - if constraints: - if len(constraints) == 1: - # Is it the right constraint? - cons, = constraints - if cons['ondelete_rule'] != POSTGRES_CONFDELTYPES.get((ondelete or 'set null').upper(), 'a')\ - or cons['foreign_table'] != dest_model._table: - # Wrong FK: drop it and recreate - tools.drop_constraint(self._cr, source_table, cons['constraint_name']) - else: - # it's all good, nothing to do! - return - else: - # Multiple FKs found for the same field, drop them all, and re-create - for cons in constraints: - tools.drop_constraint(self._cr, source_table, cons['constraint_name']) - - # (re-)create the FK - self._m2o_add_foreign_key_checked(source_field, dest_model, ondelete) - @api.model_cr_context def _init_column(self, column_name): """ Initialize the value of the given column for existing rows. """ diff --git a/odoo/tools/sql.py b/odoo/tools/sql.py index 6bdcbb05303..0b3851df651 100644 --- a/odoo/tools/sql.py +++ b/odoo/tools/sql.py @@ -6,6 +6,14 @@ import psycopg2 _schema = logging.getLogger('odoo.schema') +_CONFDELTYPES = { + 'RESTRICT': 'r', + 'NO ACTION': 'a', + 'CASCADE': 'c', + 'SET NULL': 'n', + 'SET DEFAULT': 'd', +} + def table_exists(cr, tablename): """ Return whether the given table exists. """ query = "SELECT 1 FROM pg_class WHERE relkind IN ('r','v') AND relname=%s" @@ -121,6 +129,37 @@ def drop_constraint(cr, tablename, constraintname): except Exception: _schema.warning("Table %r: unable to drop constraint %r!", tablename, constraintname) +def add_foreign_key(cr, tablename1, columnname1, tablename2, columnname2, ondelete): + """ Create the given foreign key, and return ``True``. """ + query = 'ALTER TABLE "{}" ADD FOREIGN KEY ("{}") REFERENCES "{}"("{}") ON DELETE {}' + cr.execute(query.format(tablename1, columnname1, tablename2, columnname2, ondelete)) + _schema.debug("Table %r: added foreign key %r references %r(%r) ON DELETE %s", + tablename1, columnname1, tablename2, columnname2, ondelete) + return True + +def fix_foreign_key(cr, tablename1, columnname1, tablename2, columnname2, ondelete): + """ Update the foreign keys between tables to match the given one, and + return ``True`` if the given foreign key has been recreated. + """ + deltype = _CONFDELTYPES.get(ondelete.upper(), 'a') + query = """ SELECT con.conname, c2.relname, a2.attnum, con.confdeltype as deltype + FROM pg_constraint as con, pg_class as c1, pg_class as c2, + pg_attribute as a1, pg_attribute as a2 + WHERE con.contype='f' AND con.conrelid=c1.oid AND con.confrelid=c2.oid + AND array_lower(con.conkey, 1)=1 AND con.conkey[1]=a1.attnum + AND array_lower(con.confkey, 1)=1 AND con.confkey[1]=a2.attnum + AND a1.attrelid=c1.oid AND a2.attrelid=c2.oid + AND c1.relname=%s AND a1.attname=%s """ + cr.execute(query, (tablename1, columnname1)) + found = False + for fk in cr.fetchall(): + if not found and fk[1:] == (tablename2, columnname2, deltype): + found = True + else: + drop_constraint(cr, tablename1, fk[0]) + if not found: + return add_foreign_key(cr, tablename1, columnname1, tablename2, columnname2, ondelete) + def index_exists(cr, indexname): """ Return whether the given index exists. """ cr.execute("SELECT 1 FROM pg_indexes WHERE indexname=%s", (indexname,)) From e78269664deb0f7710c745e46110261c6a58968e Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 15 Feb 2017 10:22:26 +0100 Subject: [PATCH 15/15] [IMP] tools: use `information_schema` instead of pg-specific tables --- odoo/fields.py | 12 ++++++------ odoo/tools/sql.py | 41 ++++++++++++++++++++--------------------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/odoo/fields.py b/odoo/fields.py index ad39822f63e..59075c1894f 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -831,16 +831,16 @@ class Field(object): # the column does not exist, create it sql.create_column(model._cr, model._table, self.name, self.column_type[1], self.string) return - if column['typname'] == self.column_type[0]: + if column['udt_name'] == self.column_type[0]: return - if column['typname'] in self.column_cast_from: + if column['udt_name'] in self.column_cast_from: sql.convert_column(model._cr, model._table, self.name, self.column_type[1]) else: newname = (self.name + '_moved{}').format i = 0 while sql.column_exists(model._cr, model._table, newname(i)): i += 1 - if column['attnotnull']: + if column['is_nullable'] == 'NO': sql.drop_not_null(model._cr, model._table, self.name) sql.rename_column(model._cr, model._table, self.name, newname(i)) sql.create_column(model._cr, model._table, self.name, self.column_type[1], self.string) @@ -851,7 +851,7 @@ class Field(object): :param model: an instance of the field's model :param column: the column's configuration (dict) if it exists, or ``None`` """ - has_notnull = column and column['attnotnull'] + has_notnull = column and column['is_nullable'] == 'NO' if not column or (self.required and not has_notnull): # the column is new or it becomes required; initialize its values @@ -1377,8 +1377,8 @@ class Char(_String): def update_db_column(self, model, column): if ( - column and column['typname'] == 'varchar' and column['size'] and - (self.size is None or column['size'] < self.size) + column and column['udt_name'] == 'varchar' and column['character_maximum_length'] and + (self.size is None or column['character_maximum_length'] < self.size) ): # the column's varchar size does not match self.size; convert it sql.convert_column(model._cr, model._table, self.name, self.column_type[1]) diff --git a/odoo/tools/sql.py b/odoo/tools/sql.py index 0b3851df651..34d1a84cd98 100644 --- a/odoo/tools/sql.py +++ b/odoo/tools/sql.py @@ -6,6 +6,13 @@ import psycopg2 _schema = logging.getLogger('odoo.schema') +_TABLE_KIND = { + 'BASE TABLE': 'r', + 'VIEW': 'v', + 'FOREIGN TABLE': 'f', + 'LOCAL TEMPORARY': 't', +} + _CONFDELTYPES = { 'RESTRICT': 'r', 'NO ACTION': 'a', @@ -16,14 +23,17 @@ _CONFDELTYPES = { def table_exists(cr, tablename): """ Return whether the given table exists. """ - query = "SELECT 1 FROM pg_class WHERE relkind IN ('r','v') AND relname=%s" + query = "SELECT 1 FROM information_schema.tables WHERE table_name=%s" cr.execute(query, (tablename,)) return cr.rowcount def table_kind(cr, tablename): - """ Return the kind of a table: 'r' for ordinary tables, 'v' for views. """ - cr.execute("SELECT relkind FROM pg_class WHERE relname=%s", (tablename,)) - return cr.fetchone()[0] if cr.rowcount else None + """ Return the kind of a table: ``'r'`` (regular table), ``'v'`` (view), + ``'f'`` (foreign table), ``'t'`` (temporary table), or ``None``. + """ + query = "SELECT table_type FROM information_schema.tables WHERE table_name=%s" + cr.execute(query, (tablename,)) + return _TABLE_KIND[cr.fetchone()[0]] if cr.rowcount else None def create_model_table(cr, tablename, comment=None): """ Create the table for a model. """ @@ -34,28 +44,16 @@ def create_model_table(cr, tablename, comment=None): def table_columns(cr, tablename): """ Return a dict mapping column names to their configuration. The latter is - a dict with the following keys: `relname` (table name), `attname` - (column name), `attlen`, `atttypmod`, `attnotnull` (whether it has a NOT - NULL constraint), `atthasdef` (whether it has a default value), - `typname` (data type name), `size` (varchar size). + a dict with the data from the table ``information_schema.columns``. """ - # attlen is the number of bytes necessary to represent the type when the - # type has a fixed size. If the type has a varying size attlen is -1 and - # atttypmod is the size limit + 4, or -1 if there is no limit. - query = """ SELECT c.relname, a.attname, a.attlen, a.atttypmod, - a.attnotnull, a.atthasdef, t.typname, - CASE WHEN a.attlen=-1 THEN ( - CASE WHEN a.atttypmod=-1 THEN 0 ELSE a.atttypmod-4 END - ) ELSE a.attlen END as size - FROM pg_class c, pg_attribute a, pg_type t - WHERE c.relname=%s AND c.oid=a.attrelid AND a.atttypid=t.oid """ + query = 'SELECT * FROM information_schema.columns WHERE table_name=%s' cr.execute(query, (tablename,)) - return {row['attname']: row for row in cr.dictfetchall()} + return {row['column_name']: row for row in cr.dictfetchall()} def column_exists(cr, tablename, columnname): """ Return whether the given column exists. """ - query = """ SELECT 1 FROM pg_class c, pg_attribute a - WHERE c.relname=%s AND a.attname=%s AND c.oid=a.attrelid """ + query = """ SELECT 1 FROM information_schema.columns + WHERE table_name=%s AND column_name=%s """ cr.execute(query, (tablename, columnname)) return cr.rowcount @@ -141,6 +139,7 @@ def fix_foreign_key(cr, tablename1, columnname1, tablename2, columnname2, ondele """ Update the foreign keys between tables to match the given one, and return ``True`` if the given foreign key has been recreated. """ + # Do not use 'information_schema' here, as those views are awfully slow! deltype = _CONFDELTYPES.get(ondelete.upper(), 'a') query = """ SELECT con.conname, c2.relname, a2.attnum, con.confdeltype as deltype FROM pg_constraint as con, pg_class as c1, pg_class as c2,