mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 05:32:00 +02:00
19.0 vanilla
This commit is contained in:
parent
0a7ae8db93
commit
991d2234ca
416 changed files with 646602 additions and 300844 deletions
|
|
@ -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(
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue