19.0 vanilla

This commit is contained in:
Ernad Husremovic 2025-10-03 18:07:25 +02:00
parent 0a7ae8db93
commit 991d2234ca
416 changed files with 646602 additions and 300844 deletions

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# pylint: disable=sql-injection
from __future__ import annotations
@ -7,6 +6,7 @@ import enum
import json
import logging
import re
import warnings
from binascii import crc32
from collections import defaultdict
from typing import TYPE_CHECKING
@ -16,14 +16,12 @@ if TYPE_CHECKING:
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",
@ -52,9 +50,10 @@ class SQL:
cr.execute(sql)
The code is given as a ``%``-format string, and supports either positional
arguments (with `%s`) or named arguments (with `%(name)s`). Escaped
characters (like ``"%%"``) are not supported, though. The arguments are
meant to be merged into the code using the `%` formatting operator.
arguments (with `%s`) or named arguments (with `%(name)s`). The arguments
are meant to be merged into the code using the `%` formatting operator.
Note that the character ``%`` must always be escaped (as ``%%``), even if
the code does not have parameters, like in ``SQL("foo LIKE 'a%%'")``.
The SQL wrapper is designed to be composable: the arguments can be either
actual parameters, or SQL objects themselves::
@ -84,10 +83,10 @@ class SQL:
__code: str
__params: tuple
__to_flush: tuple
__to_flush: tuple[Field, ...]
# pylint: disable=keyword-arg-before-vararg
def __init__(self, code: (str | SQL) = "", /, *args, to_flush: (Field | None) = None, **kwargs):
def __init__(self, code: (str | SQL) = "", /, *args, to_flush: (Field | Iterable[Field] | None) = None, **kwargs):
if isinstance(code, SQL):
if args or kwargs or to_flush:
raise TypeError("SQL() unexpected arguments when code has type SQL")
@ -106,7 +105,12 @@ class SQL:
code % () # check that code does not contain %s
self.__code = code
self.__params = ()
self.__to_flush = () if to_flush is None else (to_flush,)
if to_flush is None:
self.__to_flush = ()
elif hasattr(to_flush, '__iter__'):
self.__to_flush = tuple(to_flush)
else:
self.__to_flush = (to_flush,)
return
code_list = []
@ -121,9 +125,12 @@ class SQL:
code_list.append("%s")
params_list.append(arg)
if to_flush is not None:
to_flush_list.append(to_flush)
if hasattr(to_flush, '__iter__'):
to_flush_list.extend(to_flush)
else:
to_flush_list.append(to_flush)
self.__code = code % tuple(code_list)
self.__code = code.replace('%%', '%%%%') % tuple(code_list)
self.__params = tuple(params_list)
self.__to_flush = tuple(to_flush_list)
@ -153,6 +160,9 @@ class SQL:
def __eq__(self, other):
return isinstance(other, SQL) and self.__code == other.__code and self.__params == other.__params
def __hash__(self):
return hash((self.__code, self.__params))
def __iter__(self):
""" Yields ``self.code`` and ``self.params``. This was introduced for
backward compatibility, as it enables to access the SQL and parameters
@ -161,6 +171,7 @@ class SQL:
sql = SQL(...)
code, params = sql
"""
warnings.warn("Deprecated since 19.0, use code and params properties directly", DeprecationWarning)
yield self.code
yield self.params
@ -410,12 +421,8 @@ def set_not_null(cr, tablename, columnname):
"ALTER TABLE %s ALTER COLUMN %s SET NOT NULL",
SQL.identifier(tablename), SQL.identifier(columnname),
)
try:
with cr.savepoint(flush=False):
cr.execute(query, log_exceptions=False)
_schema.debug("Table %r: column %r: added constraint NOT NULL", tablename, columnname)
except Exception:
raise Exception("Table %r: unable to set NOT NULL on column %r", tablename, columnname)
cr.execute(query, log_exceptions=False)
_schema.debug("Table %r: column %r: added constraint NOT NULL", tablename, columnname)
def drop_not_null(cr, tablename, columnname):
@ -441,36 +448,26 @@ def constraint_definition(cr, tablename, constraintname):
def add_constraint(cr, tablename, constraintname, definition):
""" Add a constraint on the given table. """
# 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),
query1 = SQL(
"ALTER TABLE %s ADD CONSTRAINT %s %s",
SQL.identifier(tablename), SQL.identifier(constraintname), SQL(definition.replace('%', '%%')),
)
query2 = SQL(
"COMMENT ON CONSTRAINT %s ON %s IS %s",
SQL.identifier(constraintname), SQL.identifier(tablename), definition,
)
try:
with cr.savepoint(flush=False):
cr.execute(query1, log_exceptions=False)
cr.execute(query2, log_exceptions=False)
_schema.debug("Table %r: added constraint %r as %s", tablename, constraintname, definition)
except Exception:
raise Exception("Table %r: unable to add constraint %r as %s", tablename, constraintname, definition)
cr.execute(query1, log_exceptions=False)
cr.execute(query2, log_exceptions=False)
_schema.debug("Table %r: added constraint %r as %s", tablename, constraintname, definition)
def drop_constraint(cr, tablename, constraintname):
""" drop the given constraint. """
try:
with cr.savepoint(flush=False):
cr.execute(SQL(
"ALTER TABLE %s DROP CONSTRAINT %s",
SQL.identifier(tablename), SQL.identifier(constraintname),
))
_schema.debug("Table %r: dropped constraint %r", tablename, constraintname)
except Exception:
_schema.warning("Table %r: unable to drop constraint %r!", tablename, constraintname)
""" Drop the given constraint. """
cr.execute(SQL(
"ALTER TABLE %s DROP CONSTRAINT %s",
SQL.identifier(tablename), SQL.identifier(constraintname),
))
_schema.debug("Table %r: dropped constraint %r", tablename, constraintname)
def add_foreign_key(cr, tablename1, columnname1, tablename2, columnname2, ondelete):
@ -483,7 +480,6 @@ def add_foreign_key(cr, tablename1, columnname1, tablename2, columnname2, ondele
))
_schema.debug("Table %r: added foreign key %r references %r(%r) ON DELETE %s",
tablename1, columnname1, tablename2, columnname2, ondelete)
return True
def get_foreign_keys(cr, tablename1, columnname1, tablename2, columnname2, ondelete):
@ -531,8 +527,10 @@ def fix_foreign_key(cr, tablename1, columnname1, tablename2, columnname2, ondele
found = True
else:
drop_constraint(cr, tablename1, fk[0])
if not found:
return add_foreign_key(cr, tablename1, columnname1, tablename2, columnname2, ondelete)
if found:
return False
add_foreign_key(cr, tablename1, columnname1, tablename2, columnname2, ondelete)
return True
def index_exists(cr, indexname):
@ -545,32 +543,78 @@ def check_index_exist(cr, indexname):
assert index_exists(cr, indexname), f"{indexname} does not exist"
def create_index(cr, indexname, tablename, expressions, method='btree', where=''):
""" Create the given index unless it exists. """
def index_definition(cr, indexname):
""" Read the index definition from the database """
cr.execute(SQL("""
SELECT idx.indexdef, d.description
FROM pg_class c
JOIN pg_indexes idx ON c.relname = idx.indexname
LEFT JOIN pg_description d ON c.oid = d.objoid
WHERE c.relname = %s AND c.relkind = 'i'
""", indexname))
return cr.fetchone() if cr.rowcount else (None, None)
def create_index(
cr,
indexname,
tablename,
expressions,
method='btree',
where='',
*,
comment=None,
unique=False
):
""" Create the given index unless it exists.
:param cr: The cursor
:param indexname: The name of the index
:param tablename: The name of the table
:param method: The type of the index (default: btree)
:param where: WHERE clause for the index (default: '')
:param comment: The comment to set on the index
:param unique: Whether the index is unique or not (default: False)
"""
assert expressions, "Missing expressions"
if index_exists(cr, indexname):
return
cr.execute(SQL(
"CREATE INDEX %s ON %s USING %s (%s)%s",
SQL.identifier(indexname),
SQL.identifier(tablename),
definition = SQL(
"USING %s (%s)%s",
SQL(method),
SQL(", ").join(SQL(expression) for expression in expressions),
SQL(" WHERE %s", SQL(where)) if where else SQL(),
))
_schema.debug("Table %r: created index %r (%s)", tablename, indexname, ", ".join(expressions))
)
add_index(cr, indexname, tablename, definition, unique=unique, comment=comment)
def add_index(cr, indexname, tablename, definition, *, unique: bool, comment=''):
""" Create an index. """
if isinstance(definition, str):
definition = SQL(definition.replace('%', '%%'))
else:
definition = SQL(definition)
query = SQL(
"CREATE %sINDEX %s ON %s %s",
SQL("UNIQUE ") if unique else SQL(),
SQL.identifier(indexname),
SQL.identifier(tablename),
definition,
)
query_comment = SQL(
"COMMENT ON INDEX %s IS %s",
SQL.identifier(indexname), comment,
) if comment else None
cr.execute(query, log_exceptions=False)
if query_comment:
cr.execute(query_comment, log_exceptions=False)
_schema.debug("Table %r: created index %r (%s)", tablename, indexname, definition.code)
def create_unique_index(cr, indexname, tablename, expressions):
""" Create the given index unless it exists. """
if index_exists(cr, indexname):
return
cr.execute(SQL(
"CREATE UNIQUE INDEX %s ON %s (%s)",
SQL.identifier(indexname),
SQL.identifier(tablename),
SQL(", ").join(SQL(expression) for expression in expressions),
))
_schema.debug("Table %r: created index %r (%s)", tablename, indexname, ", ".join(expressions))
warnings.warn("Since 19.0, use create_index(unique=True)", DeprecationWarning)
return create_index(cr, indexname, tablename, expressions, unique=True)
def drop_index(cr, indexname, tablename):
@ -636,7 +680,7 @@ def increment_fields_skiplock(records, *fields):
for field in fields:
assert records._fields[field].type == 'integer'
cr = records._cr
cr = records.env.cr
tablename = records._table
cr.execute(SQL(
"""