diff options
-rw-r--r-- | oslo.db/locale/oslo.db-log-warning.pot | 15 | ||||
-rw-r--r-- | oslo/db/exception.py | 3 | ||||
-rw-r--r-- | oslo/db/options.py | 48 | ||||
-rw-r--r-- | oslo/db/sqlalchemy/exc_filters.py | 21 | ||||
-rw-r--r-- | oslo/db/sqlalchemy/provision.py | 44 | ||||
-rw-r--r-- | tests/sqlalchemy/test_exc_filters.py | 26 | ||||
-rw-r--r-- | tox.ini | 4 |
7 files changed, 113 insertions, 48 deletions
diff --git a/oslo.db/locale/oslo.db-log-warning.pot b/oslo.db/locale/oslo.db-log-warning.pot index ba17943..5a4c486 100644 --- a/oslo.db/locale/oslo.db-log-warning.pot +++ b/oslo.db/locale/oslo.db-log-warning.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: oslo.db 0.3.0.1.g4796d06\n" +"Project-Id-Version: oslo.db 0.3.0.44.g8839e43\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2014-07-14 06:03+0000\n" +"POT-Creation-Date: 2014-07-28 06:03+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -17,21 +17,16 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 1.3\n" -#: oslo/db/sqlalchemy/session.py:527 -#, python-format -msgid "Database server has gone away: %s" -msgstr "" - -#: oslo/db/sqlalchemy/session.py:580 +#: oslo/db/sqlalchemy/session.py:397 msgid "Unable to detect effective SQL mode" msgstr "" -#: oslo/db/sqlalchemy/session.py:588 +#: oslo/db/sqlalchemy/session.py:405 #, python-format msgid "MySQL SQL mode is '%s', consider enabling TRADITIONAL or STRICT_ALL_TABLES" msgstr "" -#: oslo/db/sqlalchemy/session.py:696 +#: oslo/db/sqlalchemy/session.py:498 #, python-format msgid "SQL connection failed. %s attempts left." msgstr "" diff --git a/oslo/db/exception.py b/oslo/db/exception.py index 4188481..c67e4ef 100644 --- a/oslo/db/exception.py +++ b/oslo/db/exception.py @@ -30,8 +30,9 @@ class DBError(Exception): class DBDuplicateEntry(DBError): """Wraps an implementation specific exception.""" - def __init__(self, columns=None, inner_exception=None): + def __init__(self, columns=None, inner_exception=None, value=None): self.columns = columns or [] + self.value = value super(DBDuplicateEntry, self).__init__(inner_exception) diff --git a/oslo/db/options.py b/oslo/db/options.py index 72e626c..b056b1c 100644 --- a/oslo/db/options.py +++ b/oslo/db/options.py @@ -138,7 +138,53 @@ database_opts = [ def set_defaults(conf, connection=None, sqlite_db=None, max_pool_size=None, max_overflow=None, pool_timeout=None): - """Set defaults for configuration variables.""" + """Set defaults for configuration variables. + + Overrides default options values. + + :param conf: Config instance specified to set default options in it. Using + of instances instead of a global config object prevents conflicts between + options declaration. + :type conf: oslo.config.cfg.ConfigOpts instance. + + :keyword connection: SQL connection string. + Valid SQLite URL forms are: + * sqlite:///:memory: (or, sqlite://) + * sqlite:///relative/path/to/file.db + * sqlite:////absolute/path/to/file.db + :type connection: str + + :keyword sqlite_db: path to SQLite database file. + :type sqlite_db: str + + :keyword max_pool_size: maximum connections pool size. The size of the pool + to be maintained, defaults to 5, will be used if value of the parameter is + `None`. This is the largest number of connections that will be kept + persistently in the pool. Note that the pool begins with no connections; + once this number of connections is requested, that number of connections + will remain. + :type max_pool_size: int + :default max_pool_size: None + + :keyword max_overflow: The maximum overflow size of the pool. When the + number of checked-out connections reaches the size set in pool_size, + additional connections will be returned up to this limit. When those + additional connections are returned to the pool, they are disconnected and + discarded. It follows then that the total number of simultaneous + connections the pool will allow is pool_size + max_overflow, and the total + number of "sleeping" connections the pool will allow is pool_size. + max_overflow can be set to -1 to indicate no overflow limit; no limit will + be placed on the total number of concurrent connections. Defaults to 10, + will be used if value of the parameter in `None`. + :type max_overflow: int + :default max_overflow: None + + :keyword pool_timeout: The number of seconds to wait before giving up on + returning a connection. Defaults to 30, will be used if value of the + parameter is `None`. + :type pool_timeout: int + :default pool_timeout: None + """ conf.register_opts(database_opts, group='database') diff --git a/oslo/db/sqlalchemy/exc_filters.py b/oslo/db/sqlalchemy/exc_filters.py index da54ce3..d4d9438 100644 --- a/oslo/db/sqlalchemy/exc_filters.py +++ b/oslo/db/sqlalchemy/exc_filters.py @@ -89,9 +89,14 @@ def _deadlock_error(operational_error, match, engine_name, is_disconnect): @filters("mysql", sqla_exc.IntegrityError, - r"^.*\b1062\b.*Duplicate entry '[^']+' for key '([^']+)'.*$") + 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, - r"^.*duplicate\s+key.*\"([^\"]+)\"\s*\n.*$") + (r'^.*duplicate\s+key.*"(?P<columns>[^"]+)"\s*\n.*' + r'Key\s+\((?P<key>.*)\)=\((?P<value>.*)\)\s+already\s+exists.*$', + r"^.*duplicate\s+key.*\"(?P<columns>[^\"]+)\"\s*\n.*$")) def _default_dupe_key_error(integrity_error, match, engine_name, is_disconnect): """Filter for MySQL or Postgresql duplicate key error. @@ -121,7 +126,7 @@ def _default_dupe_key_error(integrity_error, match, engine_name, """ - columns = match.group(1) + columns = match.group('columns') # note(vsergeyev): UniqueConstraint name convention: "uniq_t0c10c2" # where `t` it is table name and columns `c1`, `c2` @@ -135,12 +140,14 @@ def _default_dupe_key_error(integrity_error, match, engine_name, else: columns = columns[len(uniqbase):].split("0")[1:] - raise exception.DBDuplicateEntry(columns, integrity_error) + value = match.groupdict().get('value') + + raise exception.DBDuplicateEntry(columns, integrity_error, value) @filters("sqlite", sqla_exc.IntegrityError, - (r"^.*columns?([^)]+)(is|are)\s+not\s+unique$", - r"^.*UNIQUE\s+constraint\s+failed:\s+(.+)$")) + (r"^.*columns?(?P<columns>[^)]+)(is|are)\s+not\s+unique$", + r"^.*UNIQUE\s+constraint\s+failed:\s+(?P<columns>.+)$")) def _sqlite_dupe_key_error(integrity_error, match, engine_name, is_disconnect): """Filter for SQLite duplicate key error. @@ -156,7 +163,7 @@ def _sqlite_dupe_key_error(integrity_error, match, engine_name, is_disconnect): N columns - (IntegrityError) UNIQUE constraint failed: tbl.k1, tbl.k2 """ - columns = match.group(1) + columns = match.group('columns') columns = [c.split('.')[-1] for c in columns.strip().split(", ")] raise exception.DBDuplicateEntry(columns, integrity_error) diff --git a/oslo/db/sqlalchemy/provision.py b/oslo/db/sqlalchemy/provision.py index 315d599..f1aa2cd 100644 --- a/oslo/db/sqlalchemy/provision.py +++ b/oslo/db/sqlalchemy/provision.py @@ -16,6 +16,7 @@ """Provision test environment for specific DB backends""" import argparse +import copy import logging import os import random @@ -34,9 +35,9 @@ def get_engine(uri): """Engine creation Call the function without arguments to get admin connection. Admin - connection required to create temporary user and database for each - particular test. Otherwise use existing connection to recreate connection - to the temporary database. + connection required to create temporary database for each + particular test. Otherwise use existing connection to recreate + connection to the temporary database. """ return sqlalchemy.create_engine(uri, poolclass=sqlalchemy.pool.NullPool) @@ -57,31 +58,33 @@ def _execute_sql(engine, sql, driver): def create_database(engine): - """Provide temporary user and database for each particular test.""" + """Provide temporary database for each particular test.""" driver = engine.name - auth = { - 'database': ''.join(random.choice(string.ascii_lowercase) - for i in moves.range(10)), - 'user': engine.url.username, - 'passwd': engine.url.password, - } + database = ''.join(random.choice(string.ascii_lowercase) + for i in moves.range(10)) if driver == 'sqlite': - return 'sqlite:////tmp/%s' % auth['database'] + database = '/tmp/%s' % database elif driver in ['mysql', 'postgresql']: - sql = 'create database %s;' % auth['database'] + sql = 'create database %s;' % database _execute_sql(engine, [sql], driver) else: raise ValueError('Unsupported RDBMS %s' % driver) - params = auth.copy() - params['backend'] = driver - return "%(backend)s://%(user)s:%(passwd)s@localhost/%(database)s" % params + # Both shallow and deep copies may lead to surprising behaviour + # without knowing the implementation of sqlalchemy.engine.url. + # Use a shallow copy here, since we're only overriding a single + # property, invoking __str__ and then discarding our copy. This + # is currently safe and _should_ remain safe into the future. + new_url = copy.copy(engine.url) + + new_url.database = database + return str(new_url) def drop_database(admin_engine, current_uri): - """Drop temporary database and user after each particular test.""" + """Drop temporary database after each particular test.""" engine = get_engine(current_uri) driver = engine.name @@ -101,8 +104,8 @@ def drop_database(admin_engine, current_uri): def main(): """Controller to handle commands - ::create: Create test user and database with random names. - ::drop: Drop user and database created by previous command. + ::create: Create test database with random names. + ::drop: Drop database created by previous command. """ parser = argparse.ArgumentParser( description='Controller to handle database creation and dropping' @@ -115,8 +118,7 @@ def main(): create = subparsers.add_parser( 'create', - help='Create temporary test ' - 'databases and users.') + help='Create temporary test databases.') create.set_defaults(which='create') create.add_argument( 'instances_count', @@ -125,7 +127,7 @@ def main(): drop = subparsers.add_parser( 'drop', - help='Drop temporary test databases and users.') + help='Drop temporary test databases.') drop.set_defaults(which='drop') drop.add_argument( 'instances', diff --git a/tests/sqlalchemy/test_exc_filters.py b/tests/sqlalchemy/test_exc_filters.py index 1955c8a..42e4365 100644 --- a/tests/sqlalchemy/test_exc_filters.py +++ b/tests/sqlalchemy/test_exc_filters.py @@ -267,13 +267,14 @@ class TestRaiseReferenceError(TestsExceptionFilter): class TestDuplicate(TestsExceptionFilter): def _run_dupe_constraint_test(self, dialect_name, message, - expected_columns=['a', 'b']): + expected_columns=['a', 'b'], expected_value=None): matched = self._run_test( dialect_name, "insert into table some_values", self.IntegrityError(message), exception.DBDuplicateEntry ) self.assertEqual(expected_columns, matched.columns) + self.assertEqual(expected_value, matched.value) def _not_dupe_constraint_test(self, dialect_name, statement, message, expected_cls, expected_message): @@ -294,19 +295,36 @@ class TestDuplicate(TestsExceptionFilter): def test_mysql_mysqldb(self): self._run_dupe_constraint_test("mysql", '(1062, "Duplicate entry ' - '\'2-3\' for key \'uniq_tbl0a0b\'")') + '\'2-3\' for key \'uniq_tbl0a0b\'")', expected_value='2-3') def test_mysql_mysqlconnector(self): self._run_dupe_constraint_test("mysql", '1062 (23000): Duplicate entry ' - '\'2-3\' for key \'uniq_tbl0a0b\'")') + '\'2-3\' for key \'uniq_tbl0a0b\'")', expected_value='2-3') def test_postgresql(self): self._run_dupe_constraint_test( 'postgresql', 'duplicate key value violates unique constraint' '"uniq_tbl0a0b"' - '\nDETAIL: Key (a, b)=(2, 3) already exists.\n' + '\nDETAIL: Key (a, b)=(2, 3) already exists.\n', + expected_value='2, 3' + ) + + def test_mysql_single(self): + self._run_dupe_constraint_test("mysql", + "1062 (23000): Duplicate entry '2' for key 'b'", + expected_columns=['b'], + expected_value='2' + ) + + def test_postgresql_single(self): + self._run_dupe_constraint_test( + 'postgresql', + 'duplicate key value violates unique constraint "uniq_tbl0b"\n' + 'DETAIL: Key (b)=(2) already exists.\n', + expected_columns=['b'], + expected_value='2' ) def test_unsupported_backend(self): @@ -24,10 +24,6 @@ commands = pip install SQLAlchemy>=0.9.0,!=0.9.5,<1.0.0 commands = pip install SQLAlchemy>=0.8.0,<0.9.0 python setup.py testr --slowest --testr-args='{posargs}' -[testenv:sqla_07] -commands = pip install SQLAlchemy>=0.7.7,<0.8.0 - python setup.py testr --slowest --testr-args='{posargs}' - [testenv:pep8] commands = flake8 |