[MERGE] models: refactor BaseModel._auto_init

This commit is contained in:
Raphael Collet
2017-02-22 17:39:08 +01:00
22 changed files with 1122 additions and 727 deletions
-8
View File
@@ -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
""")
-1
View File
@@ -14,5 +14,4 @@
'data': [
'views/views.xml',
],
'pre_init_hook': '_update_database_schema',
}
+7 -7
View File
@@ -27,23 +27,23 @@ 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)
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
+5
View File
@@ -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,
+1 -1
View File
@@ -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 '
-71
View File
@@ -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,
+2 -3
View File
@@ -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
+2 -4
View File
@@ -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
+3 -6
View File
@@ -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,9 +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
(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
+122 -53
View File
@@ -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
@@ -212,27 +211,30 @@ 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)
record = self.browse(cr.fetchone())
self._context['todo'].append((10, record.modified, [['name', 'info', 'transient']]))
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
@@ -463,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)
@@ -654,8 +651,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,
@@ -664,6 +668,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),
@@ -679,19 +684,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.pool.post_init(record.modified, list(params))
# create a corresponding xml id
module = field._module or self._context.get('module')
if module:
@@ -703,24 +707,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
if not all(existing_data[key] == val for key, val in vals.iteritems()):
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())
modified = set(vals) - set(['model', 'name'])
self._context['todo'].append((20, record.modified, [modified]))
# update existing (for recursive calls)
existing_data.update(vals)
self.pool.post_init(record.modified, diff)
# 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 +858,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):
"""
@@ -884,19 +940,34 @@ 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()
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'
@@ -1107,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
+2 -22
View File
@@ -255,29 +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()]
# 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))')
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
+4 -6
View File
@@ -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):
+2 -3
View File
@@ -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
+1
View File
@@ -1,2 +1,3 @@
# -*- coding: utf-8 -*-
import models
import tests
@@ -6,3 +6,4 @@ from . import test_attributes
from . import test_one2many
from . import test_ui
from . import test_domain
from . import test_schema
@@ -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'),
])
+162 -31
View File
@@ -20,6 +20,7 @@ from odoo.tools import float_precision, float_repr, float_round, frozendict, \
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))
@@ -284,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__()
@@ -792,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['udt_name'] == self.column_type[0]:
return
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['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)
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['is_nullable'] == 'NO'
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. """
@@ -1110,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',
@@ -1170,6 +1250,7 @@ class Monetary(Field):
"""
type = 'monetary'
column_type = ('numeric', 'numeric')
column_cast_from = ('float8',)
_slots = {
'currency_field': None,
'group_operator': 'sum',
@@ -1285,6 +1366,7 @@ class Char(_String):
translation of terms.
"""
type = 'char'
column_cast_from = ('text',)
_slots = {
'size': None, # maximum size of values (deprecated)
}
@@ -1293,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['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])
super(Char, self).update_db_column(model, column)
_related_size = property(attrgetter('size'))
_description_size = property(attrgetter('size'))
@@ -1330,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:
@@ -1399,6 +1491,7 @@ class Html(_String):
class Date(Field):
type = 'date'
column_type = ('date', 'date')
column_cast_from = ('timestamp',)
@staticmethod
def today(*args):
@@ -1462,6 +1555,7 @@ class Date(Field):
class Datetime(Field):
type = 'datetime'
column_type = ('timestamp', 'timestamp')
column_cast_from = ('date',)
@staticmethod
def now(*args):
@@ -1841,6 +1935,33 @@ class Many2one(_Relational):
if not self.delegate:
self.delegate = name in model._inherits.values()
def update_db(self, model, columns):
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]
# 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``.
This is used to reflect the assignment ``value[name] = records``, where
@@ -2099,7 +2220,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:
@@ -2254,35 +2375,42 @@ 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
# 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)
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:
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
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)
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)
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)
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]
@@ -2373,6 +2501,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
+66 -487
View File
@@ -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)
@@ -266,23 +258,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
@@ -553,6 +536,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():
@@ -622,12 +607,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``. """
@@ -2060,132 +2040,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 _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(), \
'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):
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])
@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
_schema.debug("Table '%s': dropping obsolete FK constraint: '%s'",
source_table, cons['constraint_name'])
self._drop_constraint(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'])
# (re-)create the FK
self._m2o_add_foreign_key_checked(source_field, dest_model, ondelete)
tools.drop_not_null(self._table, row['attname'])
@api.model_cr_context
def _init_column(self, column_name):
@@ -2210,30 +2065,33 @@ 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()
@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):
""" 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 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
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)
# This prevents anything called by this method (in particular default
@@ -2241,259 +2099,51 @@ class BaseModel(object):
# has not been added in database yet!
self = self.with_context(prefetch_fields=False)
self.pool.post_init(self._reflect)
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._field_create()
create = not self._table_exist()
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)
if create:
self._create_table()
has_rows = False
else:
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():
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()
# 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:
stored_fields.append(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:
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)
# 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):
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,))
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:
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,))
cr.commit()
_schema.debug("Table '%s': column '%s' changed type from %s to %s",
self._table, name, c[0], c[1])
break
if f_pg_type != column_type[0]:
if not converted:
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]:
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)
# 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:
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)
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
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))
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'."\
"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 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)
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)
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])
# initialize it
if has_rows:
self._init_column(name)
# remember new-style stored fields with compute method
if field.compute:
stored_fields.append(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._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:
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)
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
new = field.update_db(self, columns)
if new and field.compute:
self.pool.post_init(recompute, field)
if self._auto:
self._add_sql_constraints()
if create:
if must_create_table:
self._execute_sql()
if parent_store_compute:
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. """
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))
self._save_constraint("%s_%s_fkey" % (table1, column), 'f', False, module)
cr.commit()
del type(self)._foreign_keys
@api.model_cr
def init(self):
@@ -2502,54 +2152,20 @@ 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)
self._cr.commit()
@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):
@@ -2560,70 +2176,33 @@ class BaseModel(object):
"""
cr = self._cr
foreign_key_re = re.compile(r'\s*foreign\s+key\b.*', re.I)
def unify_cons_text(txt):
def cons_text(txt):
return txt.lower().replace(', ',',').replace(' (','(')
def drop(name, definition, old_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)
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)
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()
# 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', ())
}
def process(key, definition):
conname = '%s_%s' % (self._table, key)
has_definition = tools.constraint_definition(cr, conname)
if not has_definition:
# constraint does not exists
tools.add_constraint(cr, self._table, conname, definition)
elif cons_text(definition) != cons_text(has_definition):
# constraint exists but its definition may have changed
tools.drop_constraint(cr, self._table, conname)
tools.add_constraint(cr, self._table, conname, definition)
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:
# 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 '')
add(conname, definition)
# we need to add the constraint:
module = constraint_module.get(key)
self._save_constraint(conname, 'u', unify_cons_text(definition), module)
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)."""
if hasattr(self, "_sql"):
self._cr.execute(self._sql)
self._cr.commit()
#
# Update objects that uses this one to update their _inherits fields
+1 -2
View File
@@ -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.
+22 -20
View File
@@ -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
@@ -14,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__)
@@ -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()
@@ -273,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()
@@ -296,10 +299,14 @@ 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
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;
@@ -308,21 +315,16 @@ 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]
for model in models:
model._auto_init()
model.init()
cr.commit()
for model in models:
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()
@@ -331,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
@@ -340,10 +342,10 @@ 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():
if not model._abstract and not table_exists(cr, model._table):
_logger.error("Model %s has no table.", name)
def clear_caches(self):
+1 -2
View File
@@ -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]:
+184
View File
@@ -1,6 +1,190 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
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',
'CASCADE': 'c',
'SET NULL': 'n',
'SET DEFAULT': 'd',
}
def table_exists(cr, tablename):
""" Return whether the given table exists. """
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'`` (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. """
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 data from the table ``information_schema.columns``.
"""
query = 'SELECT * FROM information_schema.columns WHERE table_name=%s'
cr.execute(query, (tablename,))
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 information_schema.columns
WHERE table_name=%s AND column_name=%s """
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 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.
"""
# 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,
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,))
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()