[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:
+123
-12
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user