summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--oslo_db/sqlalchemy/exc_filters.py4
-rw-r--r--oslo_db/sqlalchemy/utils.py69
-rw-r--r--oslo_db/tests/sqlalchemy/test_exc_filters.py53
-rw-r--r--oslo_db/tests/sqlalchemy/test_utils.py119
-rw-r--r--setup.cfg2
5 files changed, 201 insertions, 46 deletions
diff --git a/oslo_db/sqlalchemy/exc_filters.py b/oslo_db/sqlalchemy/exc_filters.py
index 051d1f8..9e5a2e5 100644
--- a/oslo_db/sqlalchemy/exc_filters.py
+++ b/oslo_db/sqlalchemy/exc_filters.py
@@ -197,7 +197,7 @@ def _sqlite_dupe_key_error(integrity_error, match, engine_name, is_disconnect):
"is (not present in|still referenced from) table "
"\"(?P<key_table>[^\"]+)\".")
@filters("mysql", sqla_exc.IntegrityError,
- r".* u?'Cannot (add|delete) or update a (child|parent) row: "
+ r".*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>.+)[`"] ')
@@ -276,7 +276,7 @@ def _check_constraint_non_existing(
@filters("sqlite", sqla_exc.OperationalError,
r".* no such table: (?P<table>.+)")
@filters("mysql", sqla_exc.InternalError,
- r".*1051,.*\"Unknown table '(.+\.)?(?P<table>.+)'\"")
+ r".*1051,.*Unknown table '(.+\.)?(?P<table>.+)'\"")
@filters("postgresql", sqla_exc.ProgrammingError,
r".* table \"(?P<table>.+)\" does not exist")
def _check_table_non_existing(
diff --git a/oslo_db/sqlalchemy/utils.py b/oslo_db/sqlalchemy/utils.py
index 594a7f2..f5dda31 100644
--- a/oslo_db/sqlalchemy/utils.py
+++ b/oslo_db/sqlalchemy/utils.py
@@ -31,6 +31,7 @@ from sqlalchemy import Column
from sqlalchemy.engine import Connectable
from sqlalchemy.engine import reflection
from sqlalchemy.engine import url as sa_url
+from sqlalchemy import exc
from sqlalchemy import func
from sqlalchemy import Index
from sqlalchemy import inspect
@@ -66,6 +67,66 @@ def sanitize_db_url(url):
return url
+def get_unique_keys(model):
+ """Get a list of sets of unique model keys.
+
+ :param model: the ORM model class
+ :rtype: list of sets of strings
+ :return: unique model keys or None if unable to find them
+ """
+
+ try:
+ mapper = inspect(model)
+ except exc.NoInspectionAvailable:
+ return None
+ else:
+ table = mapper.mapped_table
+ if table is None:
+ return None
+
+ # extract result from cache if present
+ info = table.info
+ if 'oslodb_unique_keys' in info:
+ return info['oslodb_unique_keys']
+
+ res = []
+ try:
+ constraints = table.constraints
+ except AttributeError:
+ constraints = []
+ for constraint in constraints:
+ # filter out any CheckConstraints
+ if isinstance(constraint, (sqlalchemy.UniqueConstraint,
+ sqlalchemy.PrimaryKeyConstraint)):
+ res.append({c.name for c in constraint.columns})
+ try:
+ indexes = table.indexes
+ except AttributeError:
+ indexes = []
+ for index in indexes:
+ if index.unique:
+ res.append({c.name for c in index.columns})
+ # cache result for next calls with the same model
+ info['oslodb_unique_keys'] = res
+ return res
+
+
+def _stable_sorting_order(model, sort_keys):
+ """Check whetever the sorting order is stable.
+
+ :return: True if it is stable, False if it's not, None if it's impossible
+ to determine.
+ """
+ keys = get_unique_keys(model)
+ if keys is None:
+ return None
+ sort_keys_set = set(sort_keys)
+ for unique_keys in keys:
+ if unique_keys.issubset(sort_keys_set):
+ return True
+ return False
+
+
# copy from glance/db/sqlalchemy/api.py
def paginate_query(query, model, limit, sort_keys, marker=None,
sort_dir=None, sort_dirs=None):
@@ -100,11 +161,9 @@ def paginate_query(query, model, limit, sort_keys, marker=None,
:rtype: sqlalchemy.orm.query.Query
:return: The query with sorting/pagination added.
"""
-
- if 'id' not in sort_keys:
- # TODO(justinsb): If this ever gives a false-positive, check
- # the actual primary key, rather than assuming its id
- LOG.warning(_LW('Id not in sort_keys; is sort_keys unique?'))
+ if _stable_sorting_order(model, sort_keys) is False:
+ LOG.warning(_LW('Unique keys not in sort_keys. '
+ 'The sorting order may be unstable.'))
assert(not (sort_dir and sort_dirs))
diff --git a/oslo_db/tests/sqlalchemy/test_exc_filters.py b/oslo_db/tests/sqlalchemy/test_exc_filters.py
index 17153a7..e31f8dd 100644
--- a/oslo_db/tests/sqlalchemy/test_exc_filters.py
+++ b/oslo_db/tests/sqlalchemy/test_exc_filters.py
@@ -512,16 +512,11 @@ class TestReferenceErrorMySQL(TestReferenceErrorSQLite,
self.table_2.insert({'id': 1, 'foo_id': 2})
)
- self.assertInnerException(
- matched,
- "IntegrityError",
- (1452, "Cannot add or update a child 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)),
- "INSERT INTO resource_entity (id, foo_id) VALUES (%s, %s)",
- (1, 2)
- )
+ # NOTE(jd) Cannot check precisely with assertInnerException since MySQL
+ # error are not the same depending on its version…
+ self.assertIsInstance(matched.inner_exception,
+ sqlalchemy.exc.IntegrityError)
+ self.assertEqual(matched.inner_exception.orig.args[0], 1452)
self.assertEqual("resource_entity", matched.table)
self.assertEqual("foo_fkey", matched.constraint)
self.assertEqual("foo_id", matched.key)
@@ -540,19 +535,11 @@ class TestReferenceErrorMySQL(TestReferenceErrorSQLite,
self.table_2.insert({'id': 1, 'foo_id': 2})
)
- self.assertInnerException(
- matched,
- "IntegrityError",
- (
- 1452,
- 'Cannot add or update a child 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)
- ),
- "INSERT INTO resource_entity (id, foo_id) VALUES (%s, %s)",
- (1, 2)
- )
+ # NOTE(jd) Cannot check precisely with assertInnerException since MySQL
+ # error are not the same depending on its version…
+ self.assertIsInstance(matched.inner_exception,
+ sqlalchemy.exc.IntegrityError)
+ self.assertEqual(matched.inner_exception.orig.args[0], 1452)
self.assertEqual("resource_entity", matched.table)
self.assertEqual("foo_fkey", matched.constraint)
self.assertEqual("foo_id", matched.key)
@@ -567,21 +554,11 @@ class TestReferenceErrorMySQL(TestReferenceErrorSQLite,
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",
- (),
- )
-
+ # NOTE(jd) Cannot check precisely with assertInnerException since MySQL
+ # error are not the same depending on its version…
+ self.assertIsInstance(matched.inner_exception,
+ sqlalchemy.exc.IntegrityError)
+ self.assertEqual(1451, matched.inner_exception.orig.args[0])
self.assertEqual("resource_entity", matched.table)
self.assertEqual("foo_fkey", matched.constraint)
self.assertEqual("foo_id", matched.key)
diff --git a/oslo_db/tests/sqlalchemy/test_utils.py b/oslo_db/tests/sqlalchemy/test_utils.py
index 19f78ce..e5f5a6c 100644
--- a/oslo_db/tests/sqlalchemy/test_utils.py
+++ b/oslo_db/tests/sqlalchemy/test_utils.py
@@ -31,6 +31,7 @@ from sqlalchemy.engine import url as sa_url
from sqlalchemy.exc import OperationalError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.hybrid import hybrid_property
+from sqlalchemy.orm import mapper
from sqlalchemy.orm import Session
from sqlalchemy.sql import select
from sqlalchemy.types import UserDefinedType, NullType
@@ -93,6 +94,42 @@ class FakeTable(Base):
pass
+class FakeTableWithMultipleKeys(Base):
+ __tablename__ = 'fake_table_multiple_keys'
+
+ key1 = Column(String(50), primary_key=True)
+ key2 = Column(String(50), primary_key=True)
+ key3 = Column(String(50))
+
+
+class FakeTableWithIndexes(Base):
+ __tablename__ = 'fake_table_unique_index'
+
+ id = Column(String(50), primary_key=True)
+ key1 = Column(String(50))
+ key2 = Column(String(50))
+ key3 = Column(String(50))
+
+ __table_args__ = (
+ Index('idx_unique', 'key1', 'key2', unique=True),
+ Index('idx_unique', 'key1', 'key3', unique=False),
+ )
+
+
+class FakeTableClassicalyMapped(object):
+ pass
+
+
+fake_table = Table(
+ 'fake_table_classically_mapped',
+ Base.metadata,
+ Column('id', Integer, primary_key=True),
+ Column('key', String(50))
+)
+
+mapper(FakeTableClassicalyMapped, fake_table)
+
+
class FakeModel(object):
def __init__(self, values):
self.values = values
@@ -276,6 +313,88 @@ class TestPaginateQuery(test_base.BaseTestCase):
sort_dirs=['asc', 'desc'])
+class Test_UnstableSortingOrder(test_base.BaseTestCase):
+ def test_multiple_primary_keys_stable(self):
+ self.assertTrue(
+ utils._stable_sorting_order(
+ FakeTableWithMultipleKeys, ['key1', 'key2']))
+
+ def test_classically_mapped_primary_keys_stable(self):
+ self.assertTrue(
+ utils._stable_sorting_order(FakeTableClassicalyMapped, ['id']))
+
+ def test_multiple_primary_keys_unstable(self):
+ self.assertFalse(
+ utils._stable_sorting_order(
+ FakeTableWithMultipleKeys, ['key1', 'key3']))
+
+ def test_unknown_primary_keys_stable(self):
+ self.assertIsNone(
+ utils._stable_sorting_order(object, ['key1', 'key2']))
+
+ def test_unique_index_stable(self):
+ self.assertTrue(
+ utils._stable_sorting_order(
+ FakeTableWithIndexes, ['key1', 'key2']))
+
+ def test_unique_index_unstable(self):
+ self.assertFalse(
+ utils._stable_sorting_order(
+ FakeTableWithIndexes, ['key1', 'key3']))
+
+
+class TestGetUniqueKeys(test_base.BaseTestCase):
+ def test_multiple_primary_keys(self):
+ self.assertEqual(
+ [{'key1', 'key2'}],
+ utils.get_unique_keys(FakeTableWithMultipleKeys))
+
+ def test_unique_index(self):
+ self.assertEqual(
+ [{'id'}, {'key1', 'key2'}],
+ utils.get_unique_keys(FakeTableWithIndexes))
+
+ def test_unknown_primary_keys(self):
+ self.assertIsNone(utils.get_unique_keys(object))
+
+ def test_cache(self):
+
+ class CacheTable(object):
+ info = {}
+ constraints_called = 0
+ indexes_called = 0
+
+ @property
+ def constraints(self):
+ self.constraints_called += 1
+ return []
+
+ @property
+ def indexes(self):
+ self.indexes_called += 1
+ return []
+
+ class CacheModel(object):
+ pass
+
+ table = CacheTable()
+ mock_inspect = mock.Mock(return_value=mock.Mock(mapped_table=table))
+ model = CacheModel()
+ self.assertNotIn('oslodb_unique_keys', CacheTable.info)
+ with mock.patch("oslo_db.sqlalchemy.utils.inspect", mock_inspect):
+ utils.get_unique_keys(model)
+
+ self.assertIn('oslodb_unique_keys', CacheTable.info)
+ self.assertEqual(1, table.constraints_called)
+ self.assertEqual(1, table.indexes_called)
+
+ for i in range(10):
+ utils.get_unique_keys(model)
+
+ self.assertEqual(1, table.constraints_called)
+ self.assertEqual(1, table.indexes_called)
+
+
class TestPaginateQueryActualSQL(test_base.BaseTestCase):
def test_paginate_on_hybrid_assert_stmt(self):
diff --git a/setup.cfg b/setup.cfg
index ca00a01..ad2c93b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -22,7 +22,7 @@ classifier =
[extras]
# So e.g. nova can test-depend on oslo.db[mysql]
mysql =
- PyMySQL>=0.6.2 # MIT License
+ PyMySQL!=0.7.7,>=0.6.2 # MIT License
# or oslo.db[mysql-c]
mysql-c =
MySQL-python:python_version=='2.7' # GPL with FOSS exception