summaryrefslogtreecommitdiff
path: root/odoo/sql_db.py
diff options
context:
space:
mode:
authorRaphael Collet <rco@odoo.com>2019-11-26 16:04:02 +0000
committerRaphael Collet <rco@odoo.com>2020-02-05 13:50:23 +0000
commit058cf208a8e7d43ebfc2e761ec1cbfc6e7a56351 (patch)
tree198230b33ca755e3b511b6618e673b1d9e27012f /odoo/sql_db.py
parent840bd42718c940bf13d835e7f1fb298f3db370d9 (diff)
[ADD] sql_db: pre/post-commit/rollback hooks
Diffstat (limited to 'odoo/sql_db.py')
-rw-r--r--odoo/sql_db.py174
1 files changed, 98 insertions, 76 deletions
diff --git a/odoo/sql_db.py b/odoo/sql_db.py
index 922540220b7..516eb175633 100644
--- a/odoo/sql_db.py
+++ b/odoo/sql_db.py
@@ -15,6 +15,7 @@ import logging
import time
import uuid
+from decorator import decorator
import psycopg2
import psycopg2.extras
import psycopg2.extensions
@@ -80,7 +81,72 @@ re_into = re.compile('.* into "?([a-zA-Z_0-9]+)"? .*$')
sql_counter = 0
-class Cursor(object):
+
+@decorator
+def check(f, self, *args, **kwargs):
+ """ Wrap a cursor method that cannot be called when the cursor is closed. """
+ if self._closed:
+ msg = 'Unable to use a closed cursor.'
+ if self.__closer:
+ msg += ' It was closed at %s, line %s' % self.__closer
+ raise psycopg2.OperationalError(msg)
+ return f(self, *args, **kwargs)
+
+
+class BaseCursor:
+ """ Base class for cursors that manages pre/post commit/rollback hooks. """
+
+ def __init__(self):
+ self.precommit = tools.GroupCalls()
+ self.postcommit = tools.GroupCalls()
+ self.prerollback = tools.GroupCalls()
+ self.postrollback = tools.GroupCalls()
+
+ @contextmanager
+ @check
+ def savepoint(self, flush=True):
+ """context manager entering in a new savepoint"""
+ name = uuid.uuid1().hex
+ if flush:
+ flush_env(self)
+ self.precommit()
+ self.prerollback.clear()
+ self.execute('SAVEPOINT "%s"' % name)
+ try:
+ yield
+ if flush:
+ flush_env(self)
+ self.precommit()
+ self.prerollback.clear()
+ except Exception:
+ if flush:
+ clear_env(self)
+ self.precommit.clear()
+ self.prerollback()
+ self.execute('ROLLBACK TO SAVEPOINT "%s"' % name)
+ raise
+ else:
+ self.execute('RELEASE SAVEPOINT "%s"' % name)
+
+ def __enter__(self):
+ """ Using the cursor as a contextmanager automatically commits and
+ closes it::
+
+ with cr:
+ cr.execute(...)
+
+ # cr is committed if no failure occurred
+ # cr is closed in any case
+ """
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if exc_type is None:
+ self.commit()
+ self.close()
+
+
+class Cursor(BaseCursor):
"""Represents an open transaction to the PostgreSQL DB backend,
acting as a lightweight wrapper around psycopg2's
``cursor`` objects.
@@ -152,18 +218,9 @@ class Cursor(object):
"""
IN_MAX = 1000 # decent limit on size of IN queries - guideline = Oracle limit
- def check(f):
- @wraps(f)
- def wrapper(self, *args, **kwargs):
- if self._closed:
- msg = 'Unable to use a closed cursor.'
- if self.__closer:
- msg += ' It was closed at %s, line %s' % self.__closer
- raise psycopg2.OperationalError(msg)
- return f(self, *args, **kwargs)
- return wrapper
-
def __init__(self, pool, dbname, dsn, serialized=True):
+ super().__init__()
+
self.sql_from_log = {}
self.sql_into_log = {}
@@ -197,9 +254,6 @@ class Cursor(object):
self.cache = {}
- # event handlers, see method after() below
- self._event_handlers = {'commit': [], 'rollback': []}
-
def __build_dict(self, row):
return {d.name: row[i] for i, d in enumerate(self._obj.description)}
def dictfetchone(self):
@@ -373,71 +427,33 @@ class Cursor(object):
back or committed independently. You may consider the use of a
dedicated temporary cursor to do some database operation.
"""
- self._event_handlers[event].append(func)
-
- def _pop_event_handlers(self):
- # return the current handlers, and reset them on self
- result = self._event_handlers
- self._event_handlers = {'commit': [], 'rollback': []}
- return result
+ if event == 'commit':
+ self.postcommit.add(func)
+ elif event == 'rollback':
+ self.postrollback.add(func)
@check
def commit(self):
- """ Perform an SQL `COMMIT`
- """
+ """ Perform an SQL `COMMIT` """
flush_env(self)
+ self.precommit()
result = self._cnx.commit()
- for func in self._pop_event_handlers()['commit']:
- func()
+ self.prerollback.clear()
+ self.postrollback.clear()
+ self.postcommit()
return result
@check
def rollback(self):
- """ Perform an SQL `ROLLBACK`
- """
+ """ Perform an SQL `ROLLBACK` """
clear_env(self)
+ self.precommit.clear()
+ self.postcommit.clear()
+ self.prerollback()
result = self._cnx.rollback()
- for func in self._pop_event_handlers()['rollback']:
- func()
+ self.postrollback()
return result
- def __enter__(self):
- """ Using the cursor as a contextmanager automatically commits and
- closes it::
-
- with cr:
- cr.execute(...)
-
- # cr is committed if no failure occurred
- # cr is closed in any case
- """
- return self
-
- def __exit__(self, exc_type, exc_value, traceback):
- if exc_type is None:
- self.commit()
- self.close()
-
- @contextmanager
- @check
- def savepoint(self, flush=True):
- """context manager entering in a new savepoint"""
- name = uuid.uuid1().hex
- if flush:
- flush_env(self)
- self.execute('SAVEPOINT "%s"' % name)
- try:
- yield
- if flush:
- flush_env(self)
- except Exception:
- if flush:
- clear_env(self)
- self.execute('ROLLBACK TO SAVEPOINT "%s"' % name)
- raise
- else:
- self.execute('RELEASE SAVEPOINT "%s"' % name)
-
@check
def __getattr__(self, name):
return getattr(self._obj, name)
@@ -447,7 +463,7 @@ class Cursor(object):
return self._closed
-class TestCursor(object):
+class TestCursor(BaseCursor):
""" A pseudo-cursor to be used for tests, on top of a real cursor. It keeps
the transaction open across requests, and simulates committing, rolling
back, and closing:
@@ -488,21 +504,27 @@ class TestCursor(object):
def autocommit(self, on):
_logger.debug("TestCursor.autocommit(%r) does nothing", on)
+ @check
def commit(self):
+ """ Perform an SQL `COMMIT` """
flush_env(self)
+ self.precommit()
self._cursor.execute('SAVEPOINT "%s"' % self._savepoint)
+ self.prerollback.clear()
+ # ignore post-commit/rollback hooks
+ self.postcommit.clear()
+ self.postrollback.clear()
+ @check
def rollback(self):
+ """ Perform an SQL `ROLLBACK` """
clear_env(self)
+ self.precommit.clear()
+ self.prerollback()
self._cursor.execute('ROLLBACK TO SAVEPOINT "%s"' % self._savepoint)
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc_value, traceback):
- if exc_type is None:
- self.commit()
- self.close()
+ # ignore post-commit/rollback hooks
+ self.postcommit.clear()
+ self.postrollback.clear()
def __getattr__(self, name):
value = getattr(self._cursor, name)