summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--oslo.db/locale/oslo.db-log-warning.pot15
-rw-r--r--oslo/db/exception.py3
-rw-r--r--oslo/db/options.py48
-rw-r--r--oslo/db/sqlalchemy/exc_filters.py21
-rw-r--r--oslo/db/sqlalchemy/provision.py44
-rw-r--r--tests/sqlalchemy/test_exc_filters.py26
-rw-r--r--tox.ini4
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):
diff --git a/tox.ini b/tox.ini
index 0445c83..55189ff 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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