diff --git a/CHANGELOG b/CHANGELOG index 95ad8c1d5d..4a2bb2c614 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Manage DatabaseDataError in ModelSQL * Remove support for pysqlite2 * Do not import empty One2Many data * Deduplicate entries in domain inversion's simplify diff --git a/trytond/backend/__init__.py b/trytond/backend/__init__.py index e0573e923d..046fccfd83 100644 --- a/trytond/backend/__init__.py +++ b/trytond/backend/__init__.py @@ -11,8 +11,8 @@ except ImportError: from trytond.config import config __all__ = [ - 'name', 'Database', 'DatabaseIntegrityError', - 'DatabaseOperationalError', 'TableHandler'] + 'name', 'Database', 'TableHandler', + 'DatabaseIntegrityError', 'DatabaseDataError', 'DatabaseOperationalError'] name = urllib.parse.urlparse(config.get('database', 'uri', default='')).scheme @@ -34,5 +34,6 @@ except ImportError: Database = _module.Database DatabaseIntegrityError = _module.DatabaseIntegrityError +DatabaseDataError = _module.DatabaseDataError DatabaseOperationalError = _module.DatabaseOperationalError TableHandler = _module.TableHandler diff --git a/trytond/backend/postgresql/__init__.py b/trytond/backend/postgresql/__init__.py index e7f9780879..98083d06a7 100644 --- a/trytond/backend/postgresql/__init__.py +++ b/trytond/backend/postgresql/__init__.py @@ -2,8 +2,10 @@ # this repository contains the full copyright notices and license terms. from .database import ( - Database, DatabaseIntegrityError, DatabaseOperationalError) + Database, DatabaseDataError, DatabaseIntegrityError, + DatabaseOperationalError) from .table import TableHandler __all__ = [ - Database, DatabaseIntegrityError, DatabaseOperationalError, TableHandler] + Database, TableHandler, + DatabaseIntegrityError, DatabaseDataError, DatabaseOperationalError] diff --git a/trytond/backend/postgresql/database.py b/trytond/backend/postgresql/database.py index 703b882f43..fd0df9fded 100644 --- a/trytond/backend/postgresql/database.py +++ b/trytond/backend/postgresql/database.py @@ -27,6 +27,7 @@ try: from psycopg2.extensions import PYDATE, PYDATETIME, PYINTERVAL, PYTIME except ImportError: PYDATE, PYDATETIME, PYTIME, PYINTERVAL = None, None, None, None +from psycopg2 import DataError as DatabaseDataError from psycopg2 import IntegrityError as DatabaseIntegrityError from psycopg2 import OperationalError as DatabaseOperationalError from psycopg2 import ProgrammingError @@ -40,7 +41,9 @@ from trytond.backend.database import DatabaseInterface, SQLType from trytond.config import config, parse_uri from trytond.tools.gevent import is_gevent_monkey_patched -__all__ = ['Database', 'DatabaseIntegrityError', 'DatabaseOperationalError'] +__all__ = [ + 'Database', + 'DatabaseIntegrityError', 'DatabaseDataError', 'DatabaseOperationalError'] logger = logging.getLogger(__name__) diff --git a/trytond/backend/sqlite/__init__.py b/trytond/backend/sqlite/__init__.py index e7f9780879..98083d06a7 100644 --- a/trytond/backend/sqlite/__init__.py +++ b/trytond/backend/sqlite/__init__.py @@ -2,8 +2,10 @@ # this repository contains the full copyright notices and license terms. from .database import ( - Database, DatabaseIntegrityError, DatabaseOperationalError) + Database, DatabaseDataError, DatabaseIntegrityError, + DatabaseOperationalError) from .table import TableHandler __all__ = [ - Database, DatabaseIntegrityError, DatabaseOperationalError, TableHandler] + Database, TableHandler, + DatabaseIntegrityError, DatabaseDataError, DatabaseOperationalError] diff --git a/trytond/backend/sqlite/database.py b/trytond/backend/sqlite/database.py index 80dcbda80e..c880dc1f59 100644 --- a/trytond/backend/sqlite/database.py +++ b/trytond/backend/sqlite/database.py @@ -11,6 +11,7 @@ import time import urllib.parse import warnings from decimal import Decimal +from sqlite3 import DatabaseError from sqlite3 import IntegrityError as DatabaseIntegrityError from sqlite3 import OperationalError as DatabaseOperationalError from weakref import WeakKeyDictionary @@ -26,12 +27,18 @@ from trytond.backend.database import DatabaseInterface, SQLType from trytond.config import config, parse_uri from trytond.transaction import Transaction -__all__ = ['Database', 'DatabaseIntegrityError', 'DatabaseOperationalError'] +__all__ = [ + 'Database', + 'DatabaseIntegrityError', 'DatabaseDataError', 'DatabaseOperationalError'] logger = logging.getLogger(__name__) _default_name = config.get('database', 'default_name', default=':memory:') +class DatabaseDataError(DatabaseError): + pass + + class SQLiteExtract(Function): __slots__ = () _function = 'EXTRACT' diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py index 89e9b97ce4..e552a93405 100644 --- a/trytond/model/modelsql.py +++ b/trytond/model/modelsql.py @@ -27,8 +27,8 @@ from trytond.transaction import Transaction, record_cache_size from . import fields from .descriptors import dualmethod from .modelstorage import ( - AccessError, ModelStorage, RequiredValidationError, ValidationError, - is_leaf) + AccessError, ModelStorage, RequiredValidationError, SizeValidationError, + ValidationError, is_leaf) from .modelview import ModelView @@ -393,6 +393,29 @@ class ModelSQL(ModelStorage): gettext('ir.msg_foreign_model_missing', **error_args)) + @classmethod + def __raise_data_error( + cls, exception, values, field_names=None, transaction=None): + if field_names is None: + field_names = list(cls._fields.keys()) + if transaction is None: + transaction = Transaction() + for field_name in field_names: + if field_name not in cls._fields: + continue + field = cls._fields[field_name] + # Check field size + if (hasattr(field, 'size') + and isinstance(field.size, int) + and field.sql_type()): + size = len(values.get(field_name) or '') + if size > field.size: + error_args = cls.__names__(field_name) + error_args['size'] = size + error_args['max_size'] = field.size + raise SizeValidationError( + gettext('ir.msg_size_validation_record', **error_args)) + @classmethod def history_revisions(cls, ids): pool = Pool() @@ -640,12 +663,18 @@ class ModelSQL(ModelStorage): [insert_values])) id_new = transaction.database.lastid(cursor) new_ids.append(id_new) - except backend.DatabaseIntegrityError as exception: + except ( + backend.DatabaseIntegrityError, + backend.DatabaseDataError) as exception: transaction = Transaction() with Transaction().new_transaction(), \ Transaction().set_context(_check_access=False): - cls.__raise_integrity_error( - exception, values, transaction=transaction) + if isinstance(exception, backend.DatabaseIntegrityError): + cls.__raise_integrity_error( + exception, values, transaction=transaction) + elif isinstance(exception, backend.DatabaseDataError): + cls.__raise_data_error( + exception, values, transaction=transaction) raise transaction.create_records[cls.__name__].update(new_ids) @@ -1049,13 +1078,21 @@ class ModelSQL(ModelStorage): try: cursor.execute(*table.update(columns, update_values, where=red_sql)) - except backend.DatabaseIntegrityError as exception: + except ( + backend.DatabaseIntegrityError, + backend.DatabaseDataError) as exception: transaction = Transaction() with Transaction().new_transaction(), \ Transaction().set_context(_check_access=False): - cls.__raise_integrity_error( - exception, values, list(values.keys()), - transaction=transaction) + if isinstance( + exception, backend.DatabaseIntegrityError): + cls.__raise_integrity_error( + exception, values, list(values.keys()), + transaction=transaction) + elif isinstance(exception, backend.DatabaseDataError): + cls.__raise_data_error( + exception, values, list(values.keys()), + transaction=transaction) raise for fname, value in values.items(): diff --git a/trytond/tests/test_field_char.py b/trytond/tests/test_field_char.py index 61e77054e4..06533b1037 100644 --- a/trytond/tests/test_field_char.py +++ b/trytond/tests/test_field_char.py @@ -416,8 +416,7 @@ class FieldCharTestCase(unittest.TestCase, CommonTestCaseMixin): "Test create char with invalid size" Char = Pool().get('test.char_size') - # XXX: should be UserError but postgresql raises DataError - with self.assertRaises(Exception): + with self.assertRaises(SizeValidationError): Char.create([{ 'char': "foobar", }]) @@ -492,8 +491,7 @@ class FieldCharTestCase(unittest.TestCase, CommonTestCaseMixin): 'char': "Test", }]) - # XXX: should be UserError but postgresql raises DataError - with self.assertRaises(Exception): + with self.assertRaises(SizeValidationError): Char.write([char], { 'char': 'foobar', })