[REF] models: in _auto_init, delegate field schema updates to fields

The fields objects are responsible for creating their column/table and indexes.
The methods that update the schema are overridden to handle cases specific to
each type of field.
This commit is contained in:
Raphael Collet
2017-02-22 15:24:07 +01:00
parent d024c76021
commit dc82ff59ac
2 changed files with 142 additions and 139 deletions
+123 -12
View File
@@ -16,11 +16,11 @@ import psycopg2
from odoo.sql_db import LazyCursor
from odoo.tools import float_precision, float_repr, float_round, frozendict, \
html_sanitize, human_size, pg_varchar, table_exists, \
table_kind, ustr, OrderedSet
html_sanitize, human_size, pg_varchar, ustr, OrderedSet
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
from odoo.tools.translate import html_translate, _
import odoo.tools.sql as sql
DATE_LENGTH = len(date.today().strftime(DATE_FORMAT))
DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT))
@@ -285,6 +285,7 @@ class Field(object):
column_type = None # database column type (ident, spec)
column_format = '%s' # placeholder for value in queries
column_cast_from = () # column types that may be cast to this
_slots = {
'args': EMPTY_DICT, # the parameters given to __init__()
@@ -793,14 +794,91 @@ class Field(object):
############################################################################
#
# Read from/write to database
# Update database schema
#
def check_schema(self, model):
""" Make sure the database contains everything for this field. Return
``True`` if the schema was altered and the field must be recomputed.
def update_db(self, model, columns):
""" Update the database schema to implement this field.
:param model: an instance of the field's model
:param columns: a dict mapping column names to their configuration in database
:return: ``True`` if the field must be recomputed on existing rows
"""
pass
if not self.column_type:
return
column = columns.get(self.name)
if not column and hasattr(self, 'oldname'):
# column not found; check whether it exists under its old name
column = columns.get(self.oldname)
if column:
sql.rename_column(model._cr, model._table, self.oldname, self.name)
# create/update the column, not null constraint, indexes
self.update_db_column(model, column)
self.update_db_notnull(model, column)
self.update_db_index(model, column)
return not column
def update_db_column(self, model, column):
""" Create/update the column corresponding to ``self``.
:param model: an instance of the field's model
:param column: the column's configuration (dict) if it exists, or ``None``
"""
if not column:
# the column does not exist, create it
sql.create_column(model._cr, model._table, self.name, self.column_type[1], self.string)
return
if column['typname'] == self.column_type[0]:
return
if column['typname'] in self.column_cast_from:
sql.convert_column(model._cr, model._table, self.name, self.column_type[1])
else:
newname = (self.name + '_moved{}').format
i = 0
while sql.column_exists(model._cr, model._table, newname(i)):
i += 1
if column['attnotnull']:
sql.drop_not_null(model._cr, model._table, self.name)
sql.rename_column(model._cr, model._table, self.name, newname(i))
sql.create_column(model._cr, model._table, self.name, self.column_type[1], self.string)
def update_db_notnull(self, model, column):
""" Add or remove the NOT NULL constraint on ``self``.
:param model: an instance of the field's model
:param column: the column's configuration (dict) if it exists, or ``None``
"""
has_notnull = column and column['attnotnull']
if not column or (self.required and not has_notnull):
# the column is new or it becomes required; initialize its values
if model._table_has_rows():
model._init_column(self.name)
if self.required and not has_notnull:
sql.set_not_null(model._cr, model._table, self.name)
elif not self.required and has_notnull:
sql.drop_not_null(model._cr, model._table, self.name)
def update_db_index(self, model, column):
""" Add or remove the index corresponding to ``self``.
:param model: an instance of the field's model
:param column: the column's configuration (dict) if it exists, or ``None``
"""
indexname = '%s_%s_index' % (model._table, self.name)
if self.index:
sql.create_index(model._cr, indexname, model._table, ['"%s"' % self.name])
else:
sql.drop_index(model._cr, indexname, model._table)
############################################################################
#
# Read from/write to database
#
def read(self, records):
""" Read the value of ``self`` on ``records``, and store it in cache. """
@@ -1111,6 +1189,7 @@ class Float(Field):
cursor and returning a pair (total, decimal)
"""
type = 'float'
column_cast_from = ('int4', 'numeric', 'float8')
_slots = {
'_digits': None, # digits argument passed to class initializer
'group_operator': 'sum',
@@ -1171,6 +1250,7 @@ class Monetary(Field):
"""
type = 'monetary'
column_type = ('numeric', 'numeric')
column_cast_from = ('float8',)
_slots = {
'currency_field': None,
'group_operator': 'sum',
@@ -1286,6 +1366,7 @@ class Char(_String):
translation of terms.
"""
type = 'char'
column_cast_from = ('text',)
_slots = {
'size': None, # maximum size of values (deprecated)
}
@@ -1294,6 +1375,15 @@ class Char(_String):
def column_type(self):
return ('varchar', pg_varchar(self.size))
def update_db_column(self, model, column):
if (
column and column['typname'] == 'varchar' and column['size'] and
(self.size is None or column['size'] < self.size)
):
# the column's varchar size does not match self.size; convert it
sql.convert_column(model._cr, model._table, self.name, self.column_type[1])
super(Char, self).update_db_column(model, column)
_related_size = property(attrgetter('size'))
_description_size = property(attrgetter('size'))
@@ -1331,6 +1421,7 @@ class Text(_String):
"""
type = 'text'
column_type = ('text', 'text')
column_cast_from = ('varchar',)
def convert_to_cache(self, value, record, validate=True):
if value is None or value is False:
@@ -1400,6 +1491,7 @@ class Html(_String):
class Date(Field):
type = 'date'
column_type = ('date', 'date')
column_cast_from = ('timestamp',)
@staticmethod
def today(*args):
@@ -1463,6 +1555,7 @@ class Date(Field):
class Datetime(Field):
type = 'datetime'
column_type = ('timestamp', 'timestamp')
column_cast_from = ('date',)
@staticmethod
def now(*args):
@@ -1842,6 +1935,21 @@ class Many2one(_Relational):
if not self.delegate:
self.delegate = name in model._inherits.values()
def update_db(self, model, columns):
if self.comodel_name not in model.env:
raise ValueError(_('There is no reference available for %s') % (self.comodel_name,))
return super(Many2one, self).update_db(model, columns)
def update_db_column(self, model, column):
super(Many2one, self).update_db_column(model, column)
comodel = model.env[self.comodel_name]
# Note: ir_actions is inherited, so foreign key doesn't work on it
if comodel._auto and comodel._table != 'ir_actions':
if not column:
model.pool.post_init(model._m2o_add_foreign_key_checked, self.name, comodel, self.ondelete)
else:
model.pool.post_init(model._m2o_fix_foreign_key, model._table, self.name, comodel, self.ondelete)
def _update(self, records, value):
""" Update the cached value of ``self`` for ``records`` with ``value``.
This is used to reflect the assignment ``value[name] = records``, where
@@ -2100,7 +2208,7 @@ class One2many(_RelationalMulti):
fnames.discard(self.inverse_name)
return super(One2many, self).convert_to_onchange(value, record, fnames)
def check_schema(self, model):
def update_db(self, model, columns):
if self.comodel_name in model.env:
comodel = model.env[self.comodel_name]
if self.inverse_name not in comodel._fields:
@@ -2255,7 +2363,7 @@ class Many2many(_RelationalMulti):
# add self in m2m, so that its inverse field can find it
m2m[(self.relation, self.column1, self.column2)] = self
def check_schema(self, model):
def update_db(self, model, columns):
cr = model._cr
rel, id1, id2 = self.relation, self.column1, self.column2
# do not create relations for custom fields as they do not belong to a module
@@ -2264,15 +2372,15 @@ class Many2many(_RelationalMulti):
if not rel.startswith('x_'):
IMR = model.env['ir.model.relation']
model.pool.post_init(IMR._reflect_relation, model, rel, self._module)
if not table_exists(cr, rel):
if not sql.table_exists(cr, rel):
if self.comodel_name not in model.env:
raise UserError(_('Many2many comodel does not exist: %r') % (self.comodel_name,))
comodel = model.env[self.comodel_name]
cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s"))' % (rel, id1, id2, id1, id2))
# create foreign key references with ondelete=cascade, unless the targets are SQL views
if table_kind(cr, comodel._table) != 'v':
if sql.table_kind(cr, comodel._table) != 'v':
model.pool.post_init(model._m2o_add_foreign_key_unchecked, rel, id2, comodel, 'cascade', self._module)
if table_kind(cr, model._table) != 'v':
if sql.table_kind(cr, model._table) != 'v':
model.pool.post_init(model._m2o_add_foreign_key_unchecked, rel, id1, model, 'cascade', self._module)
cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id1))
@@ -2371,6 +2479,9 @@ class Id(Field):
'readonly': True,
}
def update_db(self, model, columns):
pass # this column is created with the table
def __get__(self, record, owner):
if record is None:
return self # the field is accessed through the class owner
+19 -127
View File
@@ -2137,6 +2137,14 @@ class BaseModel(object):
self._table, column_name, field.column_format, column_name)
self._cr.execute(query, (value,))
@ormcache()
def _table_has_rows(self):
""" Return whether the model's table has rows. This method should only
be used when updating the database schema (:meth:`~._auto_init`).
"""
self.env.cr.execute('SELECT 1 FROM "%s" LIMIT 1' % self._table)
return self.env.cr.rowcount
@api.model_cr_context
def _auto_init(self):
""" Initialize the database schema of ``self``:
@@ -2162,11 +2170,6 @@ class BaseModel(object):
# has not been added in database yet!
self = self.with_context(prefetch_fields=False)
def recompute(field):
_logger.info("Storing computed values of %s", field)
recs = self.with_context(active_test=False).search([])
recs._recompute_todo(field)
self.pool.post_init(self._reflect)
cr = self._cr
@@ -2175,13 +2178,8 @@ class BaseModel(object):
must_create_table = not tools.table_exists(cr, self._table)
if self._auto:
if must_create_table:
tools.create_model_table(cr, self._table, self._description)
has_rows = False
else:
cr.execute('SELECT 1 FROM "%s" LIMIT 1' % self._table)
has_rows = cr.rowcount
if self._parent_store:
if not tools.column_exists(cr, self._table, 'parent_left'):
@@ -2190,130 +2188,24 @@ class BaseModel(object):
self._check_removed_columns(log=False)
# retrieve existing database columns
column_data = tools.table_columns(cr, self._table)
# update the database schema for fields
columns = tools.table_columns(cr, self._table)
for name, field in self._fields.iteritems():
if name == 'id':
continue
def recompute(field):
_logger.info("Storing computed values of %s", field)
recs = self.with_context(active_test=False).search([])
recs._recompute_todo(field)
for field in self._fields.itervalues():
if not field.store:
continue
if field.manual and not update_custom_fields:
# Don't update custom (also called manual) fields
continue
continue # don't update custom fields
if not field.column_type:
# the field is not stored as a column
if field.check_schema(self) and field.compute:
self.pool.post_init(recompute, field)
else:
res = column_data.get(name)
# The column is not found as-is in database. Check whether
# it exists with an old name, and rename it.
if not res and hasattr(field, 'oldname'):
res = column_data.get(field.oldname)
if res:
tools.rename_column(cr, self._table, field.oldname, name)
# The column already exists in database. Possibly change its
# type, rename it, drop it or change its constraints.
if res:
f_pg_type = res['typname']
f_pg_size = res['size']
f_pg_notnull = res['attnotnull']
column_type = field.column_type
if column_type:
converted = False
casts = [
('text', 'char', column_type[1], '::' + column_type[1]),
('varchar', 'text', 'TEXT', ''),
('int4', 'float', column_type[1], '::' + column_type[1]),
('date', 'datetime', 'TIMESTAMP', '::TIMESTAMP'),
('timestamp', 'date', 'date', '::date'),
('numeric', 'float', column_type[1], '::' + column_type[1]),
('float8', 'float', column_type[1], '::' + column_type[1]),
('float8', 'monetary', column_type[1], '::' + column_type[1]),
]
if f_pg_type == 'varchar' and field.type == 'char' and f_pg_size and (field.size is None or f_pg_size < field.size):
tools.convert_column(cr, self._table, name, column_type[1])
for c in casts:
if (f_pg_type == c[0]) and (field.type == c[1]):
if f_pg_type != column_type[0]:
converted = True
tools.convert_column(cr, self._table, name, column_type[1])
break
if f_pg_type != column_type[0]:
if not converted:
i = 0
while True:
newname = name + '_moved' + str(i)
if not tools.column_exists(cr, self._table, newname):
break
i += 1
if f_pg_notnull:
tools.drop_not_null(cr, self._table, name)
tools.rename_column(cr, self._table, name, newname)
tools.create_column(cr, self._table, name, column_type[1], field.string)
# if the field is required and hasn't got a NOT NULL constraint
if field.required and f_pg_notnull == 0:
if has_rows:
self._init_column(name)
# add the NOT NULL constraint
tools.set_not_null(cr, self._table, name)
elif not field.required and f_pg_notnull == 1:
tools.drop_not_null(cr, self._table, name)
# Verify index
indexname = '%s_%s_index' % (self._table, name)
has_index = tools.index_exists(cr, indexname)
if not has_index and field.index:
tools.create_index(cr, indexname, self._table, [name])
if field.type == 'text':
# FIXME: for fields.text columns we should try creating GIN indexes instead (seems most suitable for an ERP context)
msg = "Table '%s': Adding (b-tree) index for %s column '%s'."\
"This is probably useless (does not work for fulltext search) and prevents INSERTs of long texts"\
" because there is a length limit for indexable btree values!\n"\
"Use a search view instead if you simply want to make the field searchable."
_schema.warning(msg, self._table, field.type, name)
if has_index and not field.index:
tools.drop_index(cr, indexname, self._table)
if field.type == 'many2one':
comodel = self.env[field.comodel_name]
if comodel._auto and comodel._table != 'ir_actions':
self.pool.post_init(self._m2o_fix_foreign_key, self._table, name, comodel, field.ondelete)
else:
# the column doesn't exist in database, create it
tools.create_column(cr, self._table, name, field.column_type[1], field.string)
# initialize it
if has_rows:
self._init_column(name)
# remember new-style stored fields with compute method
if field.compute:
self.pool.post_init(recompute, field)
# and add constraints if needed
if field.type == 'many2one' and field.store:
if field.comodel_name not in self.env:
raise ValueError(_('There is no reference available for %s') % (field.comodel_name,))
comodel = self.env[field.comodel_name]
# ir_actions is inherited so foreign key doesn't work on it
if comodel._auto and comodel._table != 'ir_actions':
self.pool.post_init(self._m2o_add_foreign_key_checked, name, comodel, field.ondelete)
if field.index:
indexname = '%s_%s_index' % (self._table, name)
tools.create_index(cr, indexname, self._table, [name])
if field.required:
tools.set_not_null(cr, self._table, name)
new = field.update_db(self, columns)
if new and field.compute:
self.pool.post_init(recompute, field)
if self._auto:
self._add_sql_constraints()