summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--oslo_db/exception.py25
-rw-r--r--oslo_db/sqlalchemy/exc_filters.py44
-rw-r--r--oslo_db/sqlalchemy/migration.py10
-rw-r--r--oslo_db/tests/old_import_api/sqlalchemy/test_exc_filters.py49
-rw-r--r--oslo_db/tests/old_import_api/sqlalchemy/test_migration_common.py3
-rw-r--r--oslo_db/tests/sqlalchemy/test_exc_filters.py110
-rw-r--r--oslo_db/tests/sqlalchemy/test_migration_common.py3
-rw-r--r--setup.cfg2
8 files changed, 234 insertions, 12 deletions
diff --git a/oslo_db/exception.py b/oslo_db/exception.py
index f950f6a..506006c 100644
--- a/oslo_db/exception.py
+++ b/oslo_db/exception.py
@@ -87,6 +87,23 @@ class DBDuplicateEntry(DBError):
super(DBDuplicateEntry, self).__init__(inner_exception)
+class DBConstraintError(DBError):
+ """Check constraint fails for column error.
+
+ Raised when made an attempt to write to a column a value that does not
+ satisfy a CHECK constraint.
+
+ :kwarg table: the table name for which the check fails
+ :type table: str
+ :kwarg check_name: the table of the check that failed to be satisfied
+ :type check_name: str
+ """
+ def __init__(self, table, check_name, inner_exception=None):
+ self.table = table
+ self.check_name = check_name
+ super(DBConstraintError, self).__init__(inner_exception)
+
+
class DBReferenceError(DBError):
"""Foreign key violation error.
@@ -155,6 +172,14 @@ class DBConnectionError(DBError):
pass
+class DBDataError(DBError):
+ """Raised for errors that are due to problems with the processed data.
+
+ E.g. division by zero, numeric value out of range, incorrect data type, etc
+
+ """
+
+
class InvalidSortKey(Exception):
"""A sort key destined for database query usage is invalid."""
diff --git a/oslo_db/sqlalchemy/exc_filters.py b/oslo_db/sqlalchemy/exc_filters.py
index 777fda6..af8d992 100644
--- a/oslo_db/sqlalchemy/exc_filters.py
+++ b/oslo_db/sqlalchemy/exc_filters.py
@@ -91,8 +91,12 @@ def _deadlock_error(operational_error, match, engine_name, is_disconnect):
@filters("mysql", sqla_exc.IntegrityError,
- r"^.*\b1062\b.*Duplicate entry '(?P<value>[^']+)'"
+ r"^.*\b1062\b.*Duplicate entry '(?P<value>.+)'"
r" for key '(?P<columns>[^']+)'.*$")
+# NOTE(jd) For binary types
+@filters("mysql", sqla_exc.IntegrityError,
+ r"^.*\b1062\b.*Duplicate entry \\'(?P<value>.+)\\'"
+ r" for key \\'(?P<columns>.+)\\'.*$")
# NOTE(pkholkin): the first regex is suitable only for PostgreSQL 9.x versions
# the second regex is suitable for PostgreSQL 8.x versions
@filters("postgresql", sqla_exc.IntegrityError,
@@ -187,12 +191,12 @@ def _sqlite_dupe_key_error(integrity_error, match, engine_name, is_disconnect):
r"(?i).*foreign key constraint failed")
@filters("postgresql", sqla_exc.IntegrityError,
r".*on table \"(?P<table>[^\"]+)\" violates "
- "foreign key constraint \"(?P<constraint>[^\"]+)\"\s*\n"
+ "foreign key constraint \"(?P<constraint>[^\"]+)\".*\n"
"DETAIL: Key \((?P<key>.+)\)=\(.+\) "
- "is not present in table "
+ "is (not present in|still referenced from) table "
"\"(?P<key_table>[^\"]+)\".")
@filters("mysql", sqla_exc.IntegrityError,
- r".* u?'Cannot add or update a child row: "
+ r".* u?'Cannot (add|delete) or update a (child|parent) row: "
'a foreign key constraint fails \([`"].+[`"]\.[`"](?P<table>.+)[`"], '
'CONSTRAINT [`"](?P<constraint>.+)[`"] FOREIGN KEY '
'\([`"](?P<key>.+)[`"]\) REFERENCES [`"](?P<key_table>.+)[`"] ')
@@ -220,6 +224,26 @@ def _foreign_key_error(integrity_error, match, engine_name, is_disconnect):
integrity_error)
+@filters("postgresql", sqla_exc.IntegrityError,
+ r".*new row for relation \"(?P<table>.+)\" "
+ "violates check constraint "
+ "\"(?P<check_name>.+)\"")
+def _check_constraint_error(
+ integrity_error, match, engine_name, is_disconnect):
+ """Filter for check constraint errors."""
+
+ try:
+ table = match.group("table")
+ except IndexError:
+ table = None
+ try:
+ check_name = match.group("check_name")
+ except IndexError:
+ check_name = None
+
+ raise exception.DBConstraintError(table, check_name, integrity_error)
+
+
@filters("ibm_db_sa", sqla_exc.IntegrityError, r"^.*SQL0803N.*$")
def _db2_dupe_key_error(integrity_error, match, engine_name, is_disconnect):
"""Filter for DB2 duplicate key errors.
@@ -250,6 +274,18 @@ def _raise_mysql_table_doesnt_exist_asis(
raise error
+@filters("mysql", sqla_exc.OperationalError,
+ r".*(1292|1366).*Incorrect \w+ value.*")
+@filters("mysql", sqla_exc.DataError,
+ r".*1265.*Data truncated for column.*")
+@filters("mysql", sqla_exc.DataError,
+ r".*1264.*Out of range value for column.*")
+def _raise_data_error(error, match, engine_name, is_disconnect):
+ """Raise DBDataError exception for different data errors."""
+
+ raise exception.DBDataError(error)
+
+
@filters("*", sqla_exc.OperationalError, r".*")
def _raise_operational_errors_directly_filter(operational_error,
match, engine_name,
diff --git a/oslo_db/sqlalchemy/migration.py b/oslo_db/sqlalchemy/migration.py
index 308ce60..d07ffc8 100644
--- a/oslo_db/sqlalchemy/migration.py
+++ b/oslo_db/sqlalchemy/migration.py
@@ -76,10 +76,14 @@ def db_sync(engine, abs_path, version=None, init_version=0, sanity_check=True):
if sanity_check:
_db_schema_sanity_check(engine)
if version is None or version > current_version:
- return versioning_api.upgrade(engine, repository, version)
+ migration = versioning_api.upgrade(engine, repository, version)
else:
- return versioning_api.downgrade(engine, repository,
- version)
+ migration = versioning_api.downgrade(engine, repository,
+ version)
+ if sanity_check:
+ _db_schema_sanity_check(engine)
+
+ return migration
def _db_schema_sanity_check(engine):
diff --git a/oslo_db/tests/old_import_api/sqlalchemy/test_exc_filters.py b/oslo_db/tests/old_import_api/sqlalchemy/test_exc_filters.py
index 4d4609a..77670be 100644
--- a/oslo_db/tests/old_import_api/sqlalchemy/test_exc_filters.py
+++ b/oslo_db/tests/old_import_api/sqlalchemy/test_exc_filters.py
@@ -78,6 +78,9 @@ class TestsExceptionFilter(_SQLAExceptionMatcher, oslo_test_base.BaseTestCase):
class ProgrammingError(Error):
pass
+ class DataError(Error):
+ pass
+
class TransactionRollbackError(OperationalError):
"""Special psycopg2-only error class.
@@ -223,6 +226,52 @@ class TestFallthroughsAndNonDBAPI(TestsExceptionFilter):
self.assertEqual("mysqldb has an attribute error", matched.args[0])
+class TestDataError(TestsExceptionFilter):
+
+ def test_mysql_incorrect_value(self):
+ matched = self._run_test(
+ "mysql", "insert into testtbl (id, b) values (4242, 4242)",
+ self.OperationalError(
+ "ERROR 1292 (22007): Incorrect datetime value: '4242' "
+ "for column 'b' at row 1"
+ ),
+ exception.DBDataError
+ )
+ self.assertInnerException(
+ matched,
+ "OperationalError",
+ ("ERROR 1292 (22007): Incorrect datetime value: '4242' for column "
+ "'b' at row 1"),
+ "insert into testtbl (id, b) values (4242, 4242)", ())
+
+ def test_mysql_data_truncated_for_column(self):
+ matched = self._run_test(
+ "mysql", "insert into testtbl (id, b) values (4242, '42aabbccdd')",
+ self.DataError(
+ "ERROR 1265 (01000): Data truncated for column 'b' at row 1"),
+ exception.DBDataError
+ )
+ self.assertInnerException(
+ matched,
+ "DataError",
+ "ERROR 1265 (01000): Data truncated for column 'b' at row 1",
+ "insert into testtbl (id, b) values (4242, '42aabbccdd')", ())
+
+ def test_mysql_out_of_range_value(self):
+ matched = self._run_test(
+ "mysql", "insert into testtbl (id, b) values (4242, 424242424242)",
+ self.DataError(
+ "ERROR 1264 (22003): Out of range value for column 'b' "
+ "at row 1"),
+ exception.DBDataError
+ )
+ self.assertInnerException(
+ matched,
+ "DataError",
+ "ERROR 1264 (22003): Out of range value for column 'b' at row 1",
+ "insert into testtbl (id, b) values (4242, 424242424242)", ())
+
+
class TestReferenceErrorSQLite(_SQLAExceptionMatcher, test_base.DbTestCase):
def setUp(self):
diff --git a/oslo_db/tests/old_import_api/sqlalchemy/test_migration_common.py b/oslo_db/tests/old_import_api/sqlalchemy/test_migration_common.py
index 98ae46e..c81c93a 100644
--- a/oslo_db/tests/old_import_api/sqlalchemy/test_migration_common.py
+++ b/oslo_db/tests/old_import_api/sqlalchemy/test_migration_common.py
@@ -158,7 +158,8 @@ class TestMigrationCommon(test_base.DbTestCase):
mock_find_repo.return_value = self.return_value
migration.db_sync(self.engine, self.path, self.test_version)
- mock_sanity.assert_called_once_with(self.engine)
+ self.assertEqual([mock.call(self.engine), mock.call(self.engine)],
+ mock_sanity.call_args_list)
def test_db_sync_sanity_skipped(self):
with test_utils.nested(
diff --git a/oslo_db/tests/sqlalchemy/test_exc_filters.py b/oslo_db/tests/sqlalchemy/test_exc_filters.py
index aafdcfb..dc5de84 100644
--- a/oslo_db/tests/sqlalchemy/test_exc_filters.py
+++ b/oslo_db/tests/sqlalchemy/test_exc_filters.py
@@ -233,14 +233,14 @@ class TestReferenceErrorSQLite(_SQLAExceptionMatcher, test_base.DbTestCase):
meta = sqla.MetaData(bind=self.engine)
- table_1 = sqla.Table(
+ self.table_1 = sqla.Table(
"resource_foo", meta,
sqla.Column("id", sqla.Integer, primary_key=True),
sqla.Column("foo", sqla.Integer),
mysql_engine='InnoDB',
mysql_charset='utf8',
)
- table_1.create()
+ self.table_1.create()
self.table_2 = sqla.Table(
"resource_entity", meta,
@@ -274,6 +274,30 @@ class TestReferenceErrorSQLite(_SQLAExceptionMatcher, test_base.DbTestCase):
self.assertIsNone(matched.key)
self.assertIsNone(matched.key_table)
+ def test_raise_delete(self):
+ self.engine.execute("PRAGMA foreign_keys = ON;")
+
+ with self.engine.connect() as conn:
+ conn.execute(self.table_1.insert({"id": 1234, "foo": 42}))
+ conn.execute(self.table_2.insert({"id": 4321, "foo_id": 1234}))
+ matched = self.assertRaises(
+ exception.DBReferenceError,
+ self.engine.execute,
+ self.table_1.delete()
+ )
+ self.assertInnerException(
+ matched,
+ "IntegrityError",
+ "foreign key constraint failed",
+ "DELETE FROM resource_foo",
+ (),
+ )
+
+ self.assertIsNone(matched.table)
+ self.assertIsNone(matched.constraint)
+ self.assertIsNone(matched.key)
+ self.assertIsNone(matched.key_table)
+
class TestReferenceErrorPostgreSQL(TestReferenceErrorSQLite,
test_base.PostgreSQLOpportunisticTestCase):
@@ -300,6 +324,31 @@ class TestReferenceErrorPostgreSQL(TestReferenceErrorSQLite,
self.assertEqual("foo_id", matched.key)
self.assertEqual("resource_foo", matched.key_table)
+ def test_raise_delete(self):
+ with self.engine.connect() as conn:
+ conn.execute(self.table_1.insert({"id": 1234, "foo": 42}))
+ conn.execute(self.table_2.insert({"id": 4321, "foo_id": 1234}))
+ matched = self.assertRaises(
+ exception.DBReferenceError,
+ self.engine.execute,
+ self.table_1.delete()
+ )
+ self.assertInnerException(
+ matched,
+ "IntegrityError",
+ "update or delete on table \"resource_foo\" violates foreign key "
+ "constraint \"foo_fkey\" on table \"resource_entity\"\n"
+ "DETAIL: Key (id)=(1234) is still referenced from "
+ "table \"resource_entity\".\n",
+ "DELETE FROM resource_foo",
+ {},
+ )
+
+ self.assertEqual("resource_foo", matched.table)
+ self.assertEqual("foo_fkey", matched.constraint)
+ self.assertEqual("id", matched.key)
+ self.assertEqual("resource_entity", matched.key_table)
+
class TestReferenceErrorMySQL(TestReferenceErrorSQLite,
test_base.MySQLOpportunisticTestCase):
@@ -353,6 +402,45 @@ class TestReferenceErrorMySQL(TestReferenceErrorSQLite,
self.assertEqual("foo_id", matched.key)
self.assertEqual("resource_foo", matched.key_table)
+ def test_raise_delete(self):
+ with self.engine.connect() as conn:
+ conn.execute(self.table_1.insert({"id": 1234, "foo": 42}))
+ conn.execute(self.table_2.insert({"id": 4321, "foo_id": 1234}))
+ matched = self.assertRaises(
+ exception.DBReferenceError,
+ self.engine.execute,
+ self.table_1.delete()
+ )
+ self.assertInnerException(
+ matched,
+ "IntegrityError",
+ "(1451, 'cannot delete or update a parent row: a foreign key "
+ "constraint fails (`{0}`.`resource_entity`, "
+ "constraint `foo_fkey` "
+ "foreign key (`foo_id`) references "
+ "`resource_foo` (`id`))')".format(self.engine.url.database),
+ "DELETE FROM resource_foo",
+ (),
+ )
+
+ self.assertEqual("resource_entity", matched.table)
+ self.assertEqual("foo_fkey", matched.constraint)
+ self.assertEqual("foo_id", matched.key)
+ self.assertEqual("resource_foo", matched.key_table)
+
+
+class TestConstraint(TestsExceptionFilter):
+ def test_postgresql(self):
+ matched = self._run_test(
+ "postgresql", "insert into resource some_values",
+ self.IntegrityError(
+ "new row for relation \"resource\" violates "
+ "check constraint \"ck_started_before_ended\""),
+ exception.DBConstraintError,
+ )
+ self.assertEqual("resource", matched.table)
+ self.assertEqual("ck_started_before_ended", matched.check_name)
+
class TestDuplicate(TestsExceptionFilter):
@@ -424,6 +512,24 @@ class TestDuplicate(TestsExceptionFilter):
expected_value='2'
)
+ def test_mysql_binary(self):
+ self._run_dupe_constraint_test(
+ "mysql",
+ "(1062, \'Duplicate entry "
+ "\\\'\\\\x8A$\\\\x8D\\\\xA6\"s\\\\x8E\\\' "
+ "for key \\\'PRIMARY\\\'\')",
+ expected_columns=['PRIMARY'],
+ expected_value="\\\\x8A$\\\\x8D\\\\xA6\"s\\\\x8E"
+ )
+ self._run_dupe_constraint_test(
+ "mysql",
+ "(1062, \'Duplicate entry "
+ "''\\\\x8A$\\\\x8D\\\\xA6\"s\\\\x8E!,' "
+ "for key 'PRIMARY'\')",
+ expected_columns=['PRIMARY'],
+ expected_value="'\\\\x8A$\\\\x8D\\\\xA6\"s\\\\x8E!,"
+ )
+
def test_postgresql_single(self):
self._run_dupe_constraint_test(
'postgresql',
diff --git a/oslo_db/tests/sqlalchemy/test_migration_common.py b/oslo_db/tests/sqlalchemy/test_migration_common.py
index 95efab1..ebdaaa9 100644
--- a/oslo_db/tests/sqlalchemy/test_migration_common.py
+++ b/oslo_db/tests/sqlalchemy/test_migration_common.py
@@ -174,7 +174,8 @@ class TestMigrationCommon(test_base.DbTestCase):
mock_find_repo.return_value = self.return_value
migration.db_sync(self.engine, self.path, self.test_version)
- mock_sanity.assert_called_once_with(self.engine)
+ self.assertEqual([mock.call(self.engine), mock.call(self.engine)],
+ mock_sanity.call_args_list)
def test_db_sync_sanity_skipped(self):
with test_utils.nested(
diff --git a/setup.cfg b/setup.cfg
index c4ca694..4e9172b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = oslo.db
-summary = oslo.db library
+summary = Oslo Database library
description-file =
README.rst
author = OpenStack