mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 04:12:02 +02:00
18.0 vanilla
This commit is contained in:
parent
d72e748793
commit
0a7ae8db93
337 changed files with 399651 additions and 232598 deletions
|
|
@ -15,7 +15,7 @@ def is_initialized(cr):
|
|||
The database can be initialized with the 'initialize' function below.
|
||||
|
||||
"""
|
||||
return odoo.tools.table_exists(cr, 'ir_module_module')
|
||||
return odoo.tools.sql.table_exists(cr, 'ir_module_module')
|
||||
|
||||
def initialize(cr):
|
||||
""" Initialize a database with for the ORM.
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ from .. import SUPERUSER_ID, api, tools
|
|||
from .module import adapt_version, initialize_sys_path, load_openerp_module
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_test_logger = logging.getLogger('odoo.tests')
|
||||
|
||||
|
||||
def load_data(env, idref, mode, kind, package):
|
||||
|
|
@ -275,14 +274,14 @@ def load_module_graph(env, graph, status=None, perform_checks=True,
|
|||
test_time = test_queries = 0
|
||||
test_results = None
|
||||
if tools.config.options['test_enable'] and (needs_update or not updating):
|
||||
loader = odoo.tests.loader
|
||||
from odoo.tests import loader # noqa: PLC0415
|
||||
suite = loader.make_suite([module_name], 'at_install')
|
||||
if suite.countTestCases():
|
||||
if not needs_update:
|
||||
registry.setup_models(env.cr)
|
||||
# Python tests
|
||||
tests_t0, tests_q0 = time.time(), odoo.sql_db.sql_counter
|
||||
test_results = loader.run_suite(suite, module_name, global_report=report)
|
||||
test_results = loader.run_suite(suite, global_report=report)
|
||||
report.update(test_results)
|
||||
test_time = time.time() - tests_t0
|
||||
test_queries = odoo.sql_db.sql_counter - tests_q0
|
||||
|
|
@ -415,11 +414,16 @@ def load_modules(registry, force_demo=False, status=None, update_module=False):
|
|||
for pyfile in tools.config['pre_upgrade_scripts'].split(','):
|
||||
odoo.modules.migration.exec_script(cr, graph['base'].installed_version, pyfile, 'base', 'pre')
|
||||
|
||||
if update_module and odoo.tools.table_exists(cr, 'ir_model_fields'):
|
||||
if update_module and odoo.tools.sql.table_exists(cr, 'ir_model_fields'):
|
||||
# determine the fields which are currently translated in the database
|
||||
cr.execute("SELECT model || '.' || name FROM ir_model_fields WHERE translate IS TRUE")
|
||||
registry._database_translated_fields = {row[0] for row in cr.fetchall()}
|
||||
|
||||
# determine the fields which are currently company dependent in the database
|
||||
if odoo.tools.sql.column_exists(cr, 'ir_model_fields', 'company_dependent'):
|
||||
cr.execute("SELECT model || '.' || name FROM ir_model_fields WHERE company_dependent IS TRUE")
|
||||
registry._database_company_dependent_fields = {row[0] for row in cr.fetchall()}
|
||||
|
||||
# processed_modules: for cleanup step after install
|
||||
# loaded_modules: to avoid double loading
|
||||
report = registry._assertion_report
|
||||
|
|
@ -435,7 +439,7 @@ def load_modules(registry, force_demo=False, status=None, update_module=False):
|
|||
|
||||
if load_lang:
|
||||
for lang in load_lang.split(','):
|
||||
tools.load_language(cr, lang)
|
||||
tools.translate.load_language(cr, lang)
|
||||
|
||||
# STEP 2: Mark other modules to be loaded/updated
|
||||
if update_module:
|
||||
|
|
@ -597,9 +601,9 @@ def load_modules(registry, force_demo=False, status=None, update_module=False):
|
|||
try:
|
||||
View._validate_custom_views(model)
|
||||
except Exception as e:
|
||||
_logger.warning('invalid custom view(s) for model %s: %s', model, tools.ustr(e))
|
||||
_logger.warning('invalid custom view(s) for model %s: %s', model, e)
|
||||
|
||||
if report.wasSuccessful():
|
||||
if not registry._assertion_report or registry._assertion_report.wasSuccessful():
|
||||
_logger.info('Modules loaded.')
|
||||
else:
|
||||
_logger.error('At least one test failed when loading the modules.')
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
""" Modules migration handling. """
|
||||
|
||||
from collections import defaultdict
|
||||
import glob
|
||||
import importlib.util
|
||||
import inspect
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from os.path import join as opj
|
||||
|
||||
import odoo.release as release
|
||||
|
|
@ -220,23 +221,39 @@ class MigrationManager(object):
|
|||
for pyfile in _get_migration_files(pkg, version, stage):
|
||||
exec_script(self.cr, installed_version, pyfile, pkg.name, stage, stageformat[stage] % version)
|
||||
|
||||
|
||||
VALID_MIGRATE_PARAMS = list(itertools.product(
|
||||
['cr', '_cr'],
|
||||
['version', '_version'],
|
||||
))
|
||||
|
||||
def exec_script(cr, installed_version, pyfile, addon, stage, version=None):
|
||||
version = version or installed_version
|
||||
name, ext = os.path.splitext(os.path.basename(pyfile))
|
||||
if ext.lower() != '.py':
|
||||
return
|
||||
mod = None
|
||||
try:
|
||||
mod = load_script(pyfile, name)
|
||||
_logger.info('module %(addon)s: Running migration %(version)s %(name)s' % dict(locals(), name=mod.__name__))
|
||||
migrate = mod.migrate
|
||||
except ImportError:
|
||||
_logger.exception('module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % dict(locals(), file=pyfile))
|
||||
raise
|
||||
except AttributeError:
|
||||
_logger.error('module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % locals())
|
||||
else:
|
||||
migrate(cr, installed_version)
|
||||
finally:
|
||||
if mod:
|
||||
del mod
|
||||
except ImportError as e:
|
||||
raise ImportError('module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % dict(locals(), file=pyfile)) from e
|
||||
|
||||
if not hasattr(mod, 'migrate'):
|
||||
raise AttributeError(
|
||||
'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function, not found in %(file)s' % dict(
|
||||
locals(),
|
||||
file=pyfile,
|
||||
))
|
||||
|
||||
try:
|
||||
sig = inspect.signature(mod.migrate)
|
||||
except TypeError as e:
|
||||
raise TypeError("module %(addon)s: `migrate` needs to be a function, got %(migrate)r" % dict(locals(), migrate=mod.migrate)) from e
|
||||
|
||||
if not (
|
||||
tuple(sig.parameters.keys()) in VALID_MIGRATE_PARAMS
|
||||
and all(p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD) for p in sig.parameters.values())
|
||||
):
|
||||
raise TypeError("module %(addon)s: `migrate`'s signature should be `(cr, version)`, %(func)s is %(sig)s" % dict(locals(), func=mod.migrate, sig=sig))
|
||||
|
||||
_logger.info('module %(addon)s: Running migration %(version)s %(name)s' % dict(locals(), name=mod.__name__)) # noqa: G002
|
||||
mod.migrate(cr, installed_version)
|
||||
|
|
|
|||
|
|
@ -6,20 +6,35 @@ import collections.abc
|
|||
import copy
|
||||
import functools
|
||||
import importlib
|
||||
import importlib.metadata
|
||||
import logging
|
||||
import os
|
||||
import pkg_resources
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
import warnings
|
||||
from os.path import join as opj, normpath
|
||||
|
||||
import odoo
|
||||
import odoo.tools as tools
|
||||
import odoo.release as release
|
||||
from odoo.tools import pycompat
|
||||
from odoo.tools.misc import file_path
|
||||
|
||||
try:
|
||||
from packaging.requirements import InvalidRequirement, Requirement
|
||||
except ImportError:
|
||||
class InvalidRequirement(Exception):
|
||||
...
|
||||
|
||||
class Requirement:
|
||||
def __init__(self, pydep):
|
||||
if not re.fullmatch(r'[\w\-]+', pydep): # check that we have no versions or marker in pydep
|
||||
msg = f"Package `packaging` is required to parse `{pydep}` external dependency and is not installed"
|
||||
raise Exception(msg)
|
||||
self.marker = None
|
||||
self.specifier = None
|
||||
self.name = pydep
|
||||
|
||||
|
||||
MANIFEST_NAMES = ('__manifest__.py', '__openerp__.py')
|
||||
README = ['README.rst', 'README.md', 'README.txt']
|
||||
|
|
@ -32,6 +47,7 @@ _DEFAULT_MANIFEST = {
|
|||
'author': 'Odoo S.A.',
|
||||
'auto_install': False,
|
||||
'category': 'Uncategorized',
|
||||
'cloc_exclude': [],
|
||||
'configurator_snippets': {}, # website themes
|
||||
'countries': [],
|
||||
'data': [],
|
||||
|
|
@ -62,6 +78,17 @@ _DEFAULT_MANIFEST = {
|
|||
'website': '',
|
||||
}
|
||||
|
||||
# matches field definitions like
|
||||
# partner_id: base.ResPartner = fields.Many2one
|
||||
# partner_id = fields.Many2one[base.ResPartner]
|
||||
TYPED_FIELD_DEFINITION_RE = re.compile(r'''
|
||||
\b (?P<field_name>\w+) \s*
|
||||
(:\s*(?P<field_type>[^ ]*))? \s*
|
||||
= \s*
|
||||
fields\.(?P<field_class>Many2one|One2many|Many2many)
|
||||
(\[(?P<type_param>[^\]]+)\])?
|
||||
''', re.VERBOSE)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -103,7 +130,7 @@ def initialize_sys_path():
|
|||
|
||||
# hook odoo.addons on addons paths
|
||||
for ad in tools.config['addons_path'].split(','):
|
||||
ad = os.path.normcase(os.path.abspath(tools.ustr(ad.strip())))
|
||||
ad = os.path.normcase(os.path.abspath(ad.strip()))
|
||||
if ad not in odoo.addons.__path__:
|
||||
odoo.addons.__path__.append(ad)
|
||||
|
||||
|
|
@ -116,7 +143,7 @@ def initialize_sys_path():
|
|||
from odoo import upgrade
|
||||
legacy_upgrade_path = os.path.join(base_path, 'base', 'maintenance', 'migrations')
|
||||
for up in (tools.config['upgrade_path'] or legacy_upgrade_path).split(','):
|
||||
up = os.path.normcase(os.path.abspath(tools.ustr(up.strip())))
|
||||
up = os.path.normcase(os.path.abspath(up.strip()))
|
||||
if os.path.isdir(up) and up not in upgrade.__path__:
|
||||
upgrade.__path__.append(up)
|
||||
|
||||
|
|
@ -155,39 +182,6 @@ def get_module_path(module, downloaded=False, display_warning=True):
|
|||
_logger.warning('module %s: module not found', module)
|
||||
return False
|
||||
|
||||
def get_module_filetree(module, dir='.'):
|
||||
warnings.warn(
|
||||
"Since 16.0: use os.walk or a recursive glob or something",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
path = get_module_path(module)
|
||||
if not path:
|
||||
return False
|
||||
|
||||
dir = os.path.normpath(dir)
|
||||
if dir == '.':
|
||||
dir = ''
|
||||
if dir.startswith('..') or (dir and dir[0] == '/'):
|
||||
raise Exception('Cannot access file outside the module')
|
||||
|
||||
files = odoo.tools.osutil.listdir(path, True)
|
||||
|
||||
tree = {}
|
||||
for f in files:
|
||||
if not f.startswith(dir):
|
||||
continue
|
||||
|
||||
if dir:
|
||||
f = f[len(dir)+int(not dir.endswith('/')):]
|
||||
lst = f.split(os.sep)
|
||||
current = tree
|
||||
while len(lst) != 1:
|
||||
current = current.setdefault(lst.pop(0), {})
|
||||
current[lst.pop(0)] = None
|
||||
|
||||
return tree
|
||||
|
||||
def get_resource_path(module, *args):
|
||||
"""Return the full path of a resource of the given module.
|
||||
|
||||
|
|
@ -372,11 +366,6 @@ def get_manifest(module, mod_path=None):
|
|||
def _get_manifest_cached(module, mod_path=None):
|
||||
return load_manifest(module, mod_path)
|
||||
|
||||
def load_information_from_description_file(module, mod_path=None):
|
||||
warnings.warn(
|
||||
'load_information_from_description_file() is a deprecated '
|
||||
'alias to get_manifest()', DeprecationWarning, stacklevel=2)
|
||||
return get_manifest(module, mod_path)
|
||||
|
||||
def load_openerp_module(module_name):
|
||||
""" Load an OpenERP module, if not already loaded.
|
||||
|
|
@ -401,6 +390,24 @@ def load_openerp_module(module_name):
|
|||
if info['post_load']:
|
||||
getattr(sys.modules[qualname], info['post_load'])()
|
||||
|
||||
except AttributeError as err:
|
||||
_logger.critical("Couldn't load module %s", module_name)
|
||||
trace = traceback.format_exc()
|
||||
match = TYPED_FIELD_DEFINITION_RE.search(trace)
|
||||
if match and "most likely due to a circular import" in trace:
|
||||
field_name = match['field_name']
|
||||
field_class = match['field_class']
|
||||
field_type = match['field_type'] or match['type_param']
|
||||
if "." not in field_type:
|
||||
field_type = f"{module_name}.{field_type}"
|
||||
raise AttributeError(
|
||||
f"{err}\n"
|
||||
"To avoid circular import for the the comodel use the annotation syntax:\n"
|
||||
f" {field_name}: {field_type} = fields.{field_class}(...)\n"
|
||||
"and add at the beggining of the file:\n"
|
||||
" from __future__ import annotations"
|
||||
).with_traceback(err.__traceback__) from None
|
||||
raise
|
||||
except Exception:
|
||||
_logger.critical("Couldn't load module %s", module_name)
|
||||
raise
|
||||
|
|
@ -464,21 +471,31 @@ current_test = False
|
|||
|
||||
def check_python_external_dependency(pydep):
|
||||
try:
|
||||
pkg_resources.get_distribution(pydep)
|
||||
except pkg_resources.DistributionNotFound as e:
|
||||
requirement = Requirement(pydep)
|
||||
except InvalidRequirement as e:
|
||||
msg = f"{pydep} is an invalid external dependency specification: {e}"
|
||||
raise Exception(msg) from e
|
||||
if requirement.marker and not requirement.marker.evaluate():
|
||||
_logger.debug(
|
||||
"Ignored external dependency %s because environment markers do not match",
|
||||
pydep
|
||||
)
|
||||
return
|
||||
try:
|
||||
version = importlib.metadata.version(requirement.name)
|
||||
except importlib.metadata.PackageNotFoundError as e:
|
||||
try:
|
||||
# keep compatibility with module name but log a warning instead of info
|
||||
importlib.import_module(pydep)
|
||||
_logger.info("python external dependency on '%s' does not appear to be a valid PyPI package. Using a PyPI package name is recommended.", pydep)
|
||||
_logger.warning("python external dependency on '%s' does not appear o be a valid PyPI package. Using a PyPI package name is recommended.", pydep)
|
||||
return
|
||||
except ImportError:
|
||||
# backward compatibility attempt failed
|
||||
_logger.warning("DistributionNotFound: %s", e)
|
||||
raise Exception('Python library not installed: %s' % (pydep,))
|
||||
except pkg_resources.VersionConflict as e:
|
||||
_logger.warning("VersionConflict: %s", e)
|
||||
raise Exception('Python library version conflict: %s' % (pydep,))
|
||||
except Exception as e:
|
||||
_logger.warning("get_distribution(%s) failed: %s", pydep, e)
|
||||
raise Exception('Error finding python library %s' % (pydep,))
|
||||
pass
|
||||
msg = f"External dependency {pydep} not installed: {e}"
|
||||
raise Exception(msg) from e
|
||||
if requirement.specifier and not requirement.specifier.contains(version):
|
||||
msg = f"External dependency version mismatch: {pydep} (installed: {version})"
|
||||
raise Exception(msg)
|
||||
|
||||
|
||||
def check_manifest_dependencies(manifest):
|
||||
|
|
|
|||
|
|
@ -4,33 +4,39 @@
|
|||
""" Models registries.
|
||||
|
||||
"""
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import Mapping
|
||||
from contextlib import closing, contextmanager
|
||||
from functools import partial
|
||||
from operator import attrgetter
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import typing
|
||||
import warnings
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import Mapping
|
||||
from contextlib import closing, contextmanager, nullcontext
|
||||
from functools import partial
|
||||
from operator import attrgetter
|
||||
|
||||
import psycopg2
|
||||
|
||||
import odoo
|
||||
from odoo.modules.db import FunctionStatus
|
||||
from odoo.osv.expression import get_unaccent_wrapper
|
||||
from .. import SUPERUSER_ID
|
||||
from odoo.sql_db import TestCursor
|
||||
from odoo.tools import (
|
||||
config, existing_tables, lazy_classproperty,
|
||||
lazy_property, sql, Collector, OrderedSet, SQL,
|
||||
format_frame
|
||||
config, lazy_classproperty,
|
||||
lazy_property, sql, OrderedSet, SQL,
|
||||
remove_accents,
|
||||
)
|
||||
from odoo.tools.func import locked
|
||||
from odoo.tools.lru import LRU
|
||||
from odoo.tools.misc import Collector, format_frame
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from odoo.models import BaseModel
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_schema = logging.getLogger('odoo.schema')
|
||||
|
|
@ -43,6 +49,7 @@ _REGISTRY_CACHES = {
|
|||
'routing': 1024, # 2 entries per website
|
||||
'routing.rewrites': 8192, # url_rewrite entries
|
||||
'templates.cached_values': 2048, # arbitrary
|
||||
'groups': 1, # contains all res.groups
|
||||
}
|
||||
|
||||
# cache invalidation dependencies, as follows:
|
||||
|
|
@ -52,8 +59,19 @@ _CACHES_BY_KEY = {
|
|||
'assets': ('assets', 'templates.cached_values'),
|
||||
'templates': ('templates', 'templates.cached_values'),
|
||||
'routing': ('routing', 'routing.rewrites', 'templates.cached_values'),
|
||||
'groups': ('groups', 'templates', 'templates.cached_values'), # The processing of groups is saved in the view
|
||||
}
|
||||
|
||||
_REPLICA_RETRY_TIME = 20 * 60 # 20 minutes
|
||||
|
||||
|
||||
def _unaccent(x):
|
||||
if isinstance(x, SQL):
|
||||
return SQL("unaccent(%s)", x)
|
||||
if isinstance(x, psycopg2.sql.Composable):
|
||||
return psycopg2.sql.SQL('unaccent({})').format(x)
|
||||
return f'unaccent({x})'
|
||||
|
||||
class Registry(Mapping):
|
||||
""" Model registry for a particular database.
|
||||
|
||||
|
|
@ -82,6 +100,7 @@ class Registry(Mapping):
|
|||
|
||||
def __new__(cls, db_name):
|
||||
""" Return the registry for the given database name."""
|
||||
assert db_name, "Missing database name"
|
||||
with cls._lock:
|
||||
try:
|
||||
return cls.registries[db_name]
|
||||
|
|
@ -112,7 +131,7 @@ class Registry(Mapping):
|
|||
odoo.modules.reset_modules_state(db_name)
|
||||
raise
|
||||
except Exception:
|
||||
_logger.exception('Failed to load registry')
|
||||
_logger.error('Failed to load registry')
|
||||
del cls.registries[db_name] # pylint: disable=unsupported-delete-operation
|
||||
raise
|
||||
|
||||
|
|
@ -124,16 +143,22 @@ class Registry(Mapping):
|
|||
registry._init = False
|
||||
registry.ready = True
|
||||
registry.registry_invalidated = bool(update_module)
|
||||
registry.signal_changes()
|
||||
|
||||
_logger.info("Registry loaded in %.3fs", time.time() - t0)
|
||||
return registry
|
||||
|
||||
def init(self, db_name):
|
||||
self.models = {} # model name/model instance mapping
|
||||
self.models: dict[str, type[BaseModel]] = {} # model name/model instance mapping
|
||||
self._sql_constraints = set()
|
||||
self._init = True
|
||||
self._database_translated_fields = () # names of translated fields in database
|
||||
self._assertion_report = odoo.tests.result.OdooTestResult()
|
||||
self._database_company_dependent_fields = () # names of company dependent fields in database
|
||||
if config['test_enable'] or config['test_file']:
|
||||
from odoo.tests.result import OdooTestResult # noqa: PLC0415
|
||||
self._assertion_report = OdooTestResult()
|
||||
else:
|
||||
self._assertion_report = None
|
||||
self._fields_by_model = None
|
||||
self._ordinary_tables = None
|
||||
self._constraint_queue = deque()
|
||||
|
|
@ -145,7 +170,11 @@ class Registry(Mapping):
|
|||
self.loaded_xmlids = set()
|
||||
|
||||
self.db_name = db_name
|
||||
self._db = odoo.sql_db.db_connect(db_name)
|
||||
self._db = odoo.sql_db.db_connect(db_name, readonly=False)
|
||||
self._db_readonly = None
|
||||
self._db_readonly_failed_time = None
|
||||
if config['db_replica_host'] is not False or config['test_enable']: # by default, only use readonly pool if we have a db_replica_host defined. Allows to have an empty replica host for testing
|
||||
self._db_readonly = odoo.sql_db.db_connect(db_name, readonly=True)
|
||||
|
||||
# cursor for test mode; None means "normal" mode
|
||||
self.test_cr = None
|
||||
|
|
@ -160,6 +189,9 @@ class Registry(Mapping):
|
|||
self.field_depends_context = Collector()
|
||||
self.field_inverses = Collector()
|
||||
|
||||
# company dependent
|
||||
self.many2one_company_dependents = Collector() # {model_name: (field1, field2, ...)}
|
||||
|
||||
# cache of methods get_field_trigger_tree() and is_modifying_relations()
|
||||
self._field_trigger_trees = {}
|
||||
self._is_modifying_relations = {}
|
||||
|
|
@ -179,6 +211,9 @@ class Registry(Mapping):
|
|||
self.has_unaccent = odoo.modules.db.has_unaccent(cr)
|
||||
self.has_trigram = odoo.modules.db.has_trigram(cr)
|
||||
|
||||
self.unaccent = _unaccent if self.has_unaccent else lambda x: x
|
||||
self.unaccent_python = remove_accents if self.has_unaccent else lambda x: x
|
||||
|
||||
@classmethod
|
||||
@locked
|
||||
def delete(cls, db_name):
|
||||
|
|
@ -204,7 +239,7 @@ class Registry(Mapping):
|
|||
""" Return an iterator over all model names. """
|
||||
return iter(self.models)
|
||||
|
||||
def __getitem__(self, model_name):
|
||||
def __getitem__(self, model_name: str) -> type[BaseModel]:
|
||||
""" Return the model with the given name or raise KeyError if it doesn't exist."""
|
||||
return self.models[model_name]
|
||||
|
||||
|
|
@ -309,6 +344,7 @@ class Registry(Mapping):
|
|||
self.field_depends.clear()
|
||||
self.field_depends_context.clear()
|
||||
self.field_inverses.clear()
|
||||
self.many2one_company_dependents.clear()
|
||||
|
||||
# do the actual setup
|
||||
for model in models:
|
||||
|
|
@ -598,7 +634,7 @@ class Registry(Mapping):
|
|||
""" Create or drop column indexes for the given models. """
|
||||
|
||||
expected = [
|
||||
(sql.make_index_name(Model._table, field.name), Model._table, field, getattr(field, 'unaccent', False))
|
||||
(sql.make_index_name(Model._table, field.name), Model._table, field)
|
||||
for model_name in model_names
|
||||
for Model in [self.models[model_name]]
|
||||
if Model._auto and not Model._abstract
|
||||
|
|
@ -613,7 +649,7 @@ class Registry(Mapping):
|
|||
[tuple(row[0] for row in expected)])
|
||||
existing = dict(cr.fetchall())
|
||||
|
||||
for indexname, tablename, field, unaccent in expected:
|
||||
for indexname, tablename, field in expected:
|
||||
index = field.index
|
||||
assert index in ('btree', 'btree_not_null', 'trigram', True, False, None)
|
||||
if index and indexname not in existing and \
|
||||
|
|
@ -623,19 +659,24 @@ class Registry(Mapping):
|
|||
if field.translate:
|
||||
column_expression = f'''(jsonb_path_query_array({column_expression}, '$.*')::text)'''
|
||||
# add `unaccent` to the trigram index only because the
|
||||
# trigram indexes are mainly used for (i/=)like search and
|
||||
# trigram indexes are mainly used for (=)ilike search and
|
||||
# unaccent is added only in these cases when searching
|
||||
if unaccent and self.has_unaccent:
|
||||
if self.has_unaccent == FunctionStatus.INDEXABLE:
|
||||
column_expression = get_unaccent_wrapper(cr)(column_expression)
|
||||
else:
|
||||
warnings.warn(
|
||||
"PostgreSQL function 'unaccent' is present but not immutable, "
|
||||
"therefore trigram indexes may not be effective.",
|
||||
)
|
||||
if self.has_unaccent == FunctionStatus.INDEXABLE:
|
||||
column_expression = self.unaccent(column_expression)
|
||||
elif self.has_unaccent:
|
||||
warnings.warn(
|
||||
"PostgreSQL function 'unaccent' is present but not immutable, "
|
||||
"therefore trigram indexes may not be effective.",
|
||||
)
|
||||
expression = f'{column_expression} gin_trgm_ops'
|
||||
method = 'gin'
|
||||
where = ''
|
||||
elif index == 'btree_not_null' and field.company_dependent:
|
||||
# company dependent condition will use extra
|
||||
# `AND col IS NOT NULL` to use the index.
|
||||
expression = f'({column_expression} IS NOT NULL)'
|
||||
method = 'btree'
|
||||
where = f'{column_expression} IS NOT NULL'
|
||||
else: # index in ['btree', 'btree_not_null', True]
|
||||
expression = f'{column_expression}'
|
||||
method = 'btree'
|
||||
|
|
@ -706,7 +747,7 @@ class Registry(Mapping):
|
|||
for name, model in env.registry.items()
|
||||
if not model._abstract and model._table_query is None
|
||||
}
|
||||
missing_tables = set(table2model).difference(existing_tables(cr, table2model))
|
||||
missing_tables = set(table2model).difference(sql.existing_tables(cr, table2model))
|
||||
|
||||
if missing_tables:
|
||||
missing = {table2model[table] for table in missing_tables}
|
||||
|
|
@ -717,7 +758,7 @@ class Registry(Mapping):
|
|||
env[name].init()
|
||||
env.flush_all()
|
||||
# check again, and log errors if tables are still missing
|
||||
missing_tables = set(table2model).difference(existing_tables(cr, table2model))
|
||||
missing_tables = set(table2model).difference(sql.existing_tables(cr, table2model))
|
||||
for table in missing_tables:
|
||||
_logger.error("Model %s has no table.", table2model[table])
|
||||
|
||||
|
|
@ -817,6 +858,8 @@ class Registry(Mapping):
|
|||
self.registry_sequence, ' '.join('[Cache %s: %s]' % cs for cs in self.cache_sequences.items()))
|
||||
|
||||
def get_sequences(self, cr):
|
||||
assert cr.readonly is False, "can't use replica, sequence data is not replicated"
|
||||
|
||||
cache_sequences_query = ', '.join([f'base_cache_signaling_{cache_name}' for cache_name in _CACHES_BY_KEY])
|
||||
cache_sequences_values_query = ',\n'.join([f'base_cache_signaling_{cache_name}.last_value' for cache_name in _CACHES_BY_KEY])
|
||||
cr.execute(f"""
|
||||
|
|
@ -827,14 +870,14 @@ class Registry(Mapping):
|
|||
cache_sequences = dict(zip(_CACHES_BY_KEY, cache_sequences_values))
|
||||
return registry_sequence, cache_sequences
|
||||
|
||||
def check_signaling(self):
|
||||
def check_signaling(self, cr=None):
|
||||
""" Check whether the registry has changed, and performs all necessary
|
||||
operations to update the registry. Return an up-to-date registry.
|
||||
"""
|
||||
if self.in_test_mode():
|
||||
return self
|
||||
|
||||
with closing(self.cursor()) as cr:
|
||||
with nullcontext(cr) if cr is not None else closing(self.cursor()) as cr:
|
||||
db_registry_sequence, db_cache_sequences = self.get_sequences(cr)
|
||||
changes = ''
|
||||
# Check if the model registry must be reloaded
|
||||
|
|
@ -865,20 +908,19 @@ class Registry(Mapping):
|
|||
|
||||
def signal_changes(self):
|
||||
""" Notifies other processes if registry or cache has been invalidated. """
|
||||
if self.in_test_mode():
|
||||
if self.registry_invalidated:
|
||||
self.registry_sequence += 1
|
||||
for cache_name in self.cache_invalidated or ():
|
||||
self.cache_sequences[cache_name] += 1
|
||||
self.registry_invalidated = False
|
||||
self.cache_invalidated.clear()
|
||||
if not self.ready:
|
||||
_logger.warning('Calling signal_changes when registry is not ready is not suported')
|
||||
return
|
||||
|
||||
if self.registry_invalidated:
|
||||
_logger.info("Registry changed, signaling through the database")
|
||||
with closing(self.cursor()) as cr:
|
||||
cr.execute("select nextval('base_registry_signaling')")
|
||||
self.registry_sequence = cr.fetchone()[0]
|
||||
# If another process concurrently updates the registry,
|
||||
# self.registry_sequence will actually be out-of-date,
|
||||
# and the next call to check_signaling() will detect that and trigger a registry reload.
|
||||
# otherwise, self.registry_sequence should be equal to cr.fetchone()[0]
|
||||
self.registry_sequence += 1
|
||||
|
||||
# no need to notify cache invalidation in case of registry invalidation,
|
||||
# because reloading the registry implies starting with an empty cache
|
||||
|
|
@ -887,7 +929,11 @@ class Registry(Mapping):
|
|||
with closing(self.cursor()) as cr:
|
||||
for cache_name in self.cache_invalidated:
|
||||
cr.execute("select nextval(%s)", [f'base_cache_signaling_{cache_name}'])
|
||||
self.cache_sequences[cache_name] = cr.fetchone()[0]
|
||||
# If another process concurrently updates the cache,
|
||||
# self.cache_sequences[cache_name] will actually be out-of-date,
|
||||
# and the next call to check_signaling() will detect that and trigger cache invalidation.
|
||||
# otherwise, self.cache_sequences[cache_name] should be equal to cr.fetchone()[0]
|
||||
self.cache_sequences[cache_name] += 1
|
||||
|
||||
self.registry_invalidated = False
|
||||
self.cache_invalidated.clear()
|
||||
|
|
@ -918,10 +964,11 @@ class Registry(Mapping):
|
|||
""" Test whether the registry is in 'test' mode. """
|
||||
return self.test_cr is not None
|
||||
|
||||
def enter_test_mode(self, cr):
|
||||
def enter_test_mode(self, cr, test_readonly_enabled=True):
|
||||
""" Enter the 'test' mode, where one cursor serves several requests. """
|
||||
assert self.test_cr is None
|
||||
self.test_cr = cr
|
||||
self.test_readonly_enabled = test_readonly_enabled
|
||||
self.test_lock = threading.RLock()
|
||||
assert Registry._saved_lock is None
|
||||
Registry._saved_lock = Registry._lock
|
||||
|
|
@ -931,18 +978,39 @@ class Registry(Mapping):
|
|||
""" Leave the test mode. """
|
||||
assert self.test_cr is not None
|
||||
self.test_cr = None
|
||||
self.test_lock = None
|
||||
del self.test_readonly_enabled
|
||||
del self.test_lock
|
||||
assert Registry._saved_lock is not None
|
||||
Registry._lock = Registry._saved_lock
|
||||
Registry._saved_lock = None
|
||||
|
||||
def cursor(self):
|
||||
def cursor(self, /, readonly=False):
|
||||
""" Return a new cursor for the database. The cursor itself may be used
|
||||
as a context manager to commit/rollback and close automatically.
|
||||
|
||||
:param readonly: Attempt to acquire a cursor on a replica database.
|
||||
Acquire a read/write cursor on the primary database in case no
|
||||
replica exists or that no readonly cursor could be acquired.
|
||||
"""
|
||||
if self.test_cr is not None:
|
||||
# in test mode we use a proxy object that uses 'self.test_cr' underneath
|
||||
return TestCursor(self.test_cr, self.test_lock, current_test=odoo.modules.module.current_test)
|
||||
if readonly and not self.test_readonly_enabled:
|
||||
_logger.info('Explicitly ignoring readonly flag when generating a cursor')
|
||||
return TestCursor(self.test_cr, self.test_lock, readonly and self.test_readonly_enabled, current_test=odoo.modules.module.current_test)
|
||||
|
||||
if readonly and self._db_readonly is not None:
|
||||
if (
|
||||
self._db_readonly_failed_time is None
|
||||
or time.monotonic() > self._db_readonly_failed_time + _REPLICA_RETRY_TIME
|
||||
):
|
||||
try:
|
||||
cr = self._db_readonly.cursor()
|
||||
self._db_readonly_failed_time = None
|
||||
return cr
|
||||
except psycopg2.OperationalError:
|
||||
self._db_readonly_failed_time = time.monotonic()
|
||||
_logger.warning("Failed to open a readonly cursor, falling back to read-write cursor for %dmin %dsec", *divmod(_REPLICA_RETRY_TIME, 60))
|
||||
threading.current_thread().cursor_mode = 'ro->rw'
|
||||
return self._db.cursor()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue