[MERGE] models: refactor BaseModel._auto_init
This commit is contained in:
@@ -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
|
||||
""")
|
||||
|
||||
@@ -14,5 +14,4 @@
|
||||
'data': [
|
||||
'views/views.xml',
|
||||
],
|
||||
'pre_init_hook': '_update_database_schema',
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 '
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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]:
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user