mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 03:52:01 +02:00
18.0 vanilla
This commit is contained in:
parent
d72e748793
commit
0a7ae8db93
337 changed files with 399651 additions and 232598 deletions
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue