18.0 vanilla

This commit is contained in:
Ernad Husremovic 2025-10-03 18:06:50 +02:00
parent d72e748793
commit 0a7ae8db93
337 changed files with 399651 additions and 232598 deletions

View file

@ -9,12 +9,29 @@ import logging
import re
from binascii import crc32
from collections import defaultdict
from typing import Iterable, Union
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from odoo.fields import Field
from collections.abc import Iterable
import psycopg2
import psycopg2.sql as pgsql
from .misc import named_to_positional_printf
__all__ = [
"SQL",
"create_index",
"create_unique_index",
"drop_view_if_exists",
"escape_psql",
"index_exists",
"make_identifier",
"make_index_name",
"reverse_order",
]
_schema = logging.getLogger('odoo.schema')
IDENT_RE = re.compile(r'^[a-z0-9_][a-z0-9_$\-]*$', re.I)
@ -58,64 +75,83 @@ class SQL:
if ``code`` is a string literal (not a dynamic string), then the SQL object
made with ``code`` is guaranteed to be safe, provided the SQL objects
within its parameters are themselves safe.
The wrapper may also contain some metadata ``to_flush``. If not ``None``,
its value is a field which the SQL code depends on. The metadata of a
wrapper and its parts can be accessed by the iterator ``sql.to_flush``.
"""
__slots__ = ('__code', '__args')
__slots__ = ('__code', '__params', '__to_flush')
__code: str
__params: tuple
__to_flush: tuple
# pylint: disable=keyword-arg-before-vararg
def __new__(cls, code: (str | SQL) = "", /, *args, **kwargs):
def __init__(self, code: (str | SQL) = "", /, *args, to_flush: (Field | None) = None, **kwargs):
if isinstance(code, SQL):
return code
if args or kwargs or to_flush:
raise TypeError("SQL() unexpected arguments when code has type SQL")
self.__code = code.__code
self.__params = code.__params
self.__to_flush = code.__to_flush
return
# validate the format of code and parameters
if args and kwargs:
raise TypeError("SQL() takes either positional arguments, or named arguments")
if args:
code % tuple("" for arg in args)
elif kwargs:
code, args = named_to_positional_printf(code, kwargs)
self = object.__new__(cls)
self.__code = code
self.__args = args
return self
if kwargs:
code, args = named_to_positional_printf(code, kwargs)
elif not args:
code % () # check that code does not contain %s
self.__code = code
self.__params = ()
self.__to_flush = () if to_flush is None else (to_flush,)
return
code_list = []
params_list = []
to_flush_list = []
for arg in args:
if isinstance(arg, SQL):
code_list.append(arg.__code)
params_list.extend(arg.__params)
to_flush_list.extend(arg.__to_flush)
else:
code_list.append("%s")
params_list.append(arg)
if to_flush is not None:
to_flush_list.append(to_flush)
self.__code = code % tuple(code_list)
self.__params = tuple(params_list)
self.__to_flush = tuple(to_flush_list)
@property
def code(self) -> str:
""" Return the combined SQL code string. """
stack = [] # stack of intermediate results
for node in self.__postfix():
if not isinstance(node, SQL):
stack.append("%s")
elif arity := len(node.__args):
stack[-arity:] = [node.__code % tuple(stack[-arity:])]
else:
stack.append(node.__code)
return stack[0]
return self.__code
@property
def params(self) -> list:
""" Return the combined SQL code params as a list of values. """
return [node for node in self.__postfix() if not isinstance(node, SQL)]
return list(self.__params)
def __postfix(self):
""" Return a postfix iterator for the SQL tree ``self``. """
stack = [(self, False)]
while stack:
node, ispostfix = stack.pop()
if ispostfix or not isinstance(node, SQL):
yield node
else:
stack.append((node, True))
stack.extend((arg, False) for arg in reversed(node.__args))
@property
def to_flush(self) -> Iterable[Field]:
""" Return an iterator on the fields to flush in the metadata of
``self`` and all of its parts.
"""
return self.__to_flush
def __repr__(self):
return f"SQL({', '.join(map(repr, [self.code, *self.params]))})"
return f"SQL({', '.join(map(repr, [self.__code, *self.__params]))})"
def __bool__(self):
return bool(self.__code)
def __eq__(self, other):
return self.code == other.code and self.params == other.params
return isinstance(other, SQL) and self.__code == other.__code and self.__params == other.__params
def __iter__(self):
""" Yields ``self.code`` and ``self.params``. This was introduced for
@ -134,9 +170,9 @@ class SQL:
# optimizations for special cases
if len(args) == 0:
return SQL()
if len(args) == 1:
if len(args) == 1 and isinstance(args[0], SQL):
return args[0]
if not self.__args:
if not self.__params:
return SQL(self.__code.join("%s" for arg in args), *args)
# general case: alternate args with self
items = [self] * (len(args) * 2 - 1)
@ -145,13 +181,13 @@ class SQL:
return SQL("%s" * len(items), *items)
@classmethod
def identifier(cls, name: str, subname: (str | None) = None) -> SQL:
def identifier(cls, name: str, subname: (str | None) = None, to_flush: (Field | None) = None) -> SQL:
""" Return an SQL object that represents an identifier. """
assert name.isidentifier() or IDENT_RE.match(name), f"{name!r} invalid for SQL.identifier()"
if subname is None:
return cls(f'"{name}"')
return cls(f'"{name}"', to_flush=to_flush)
assert subname.isidentifier() or IDENT_RE.match(subname), f"{subname!r} invalid for SQL.identifier()"
return cls(f'"{name}"."{subname}"')
return cls(f'"{name}"."{subname}"', to_flush=to_flush)
def existing_tables(cr, tablenames):
@ -181,7 +217,7 @@ class TableKind(enum.Enum):
Other = None
def table_kind(cr, tablename: str) -> Union[TableKind, None]:
def table_kind(cr, tablename: str) -> TableKind | None:
""" Return the kind of a table, if ``tablename`` is a regular or foreign
table, or a view (ignores indexes, sequences, toast tables, and partitioned
tables; unlogged tables are considered regular)
@ -405,9 +441,11 @@ def constraint_definition(cr, tablename, constraintname):
def add_constraint(cr, tablename, constraintname, definition):
""" Add a constraint on the given table. """
query1 = SQL(
"ALTER TABLE %s ADD CONSTRAINT %s %s",
SQL.identifier(tablename), SQL.identifier(constraintname), SQL(definition),
# There is a fundamental issue with SQL implementation that messes up with queries
# using %, for details check the PR discussion of this patch #188716. To be fixed
# in master. Here we use instead psycopg.sql
query1 = pgsql.SQL("ALTER TABLE {} ADD CONSTRAINT {} {}").format(
pgsql.Identifier(tablename), pgsql.Identifier(constraintname), pgsql.SQL(definition),
)
query2 = SQL(
"COMMENT ON CONSTRAINT %s ON %s IS %s",