summaryrefslogtreecommitdiff
path: root/src/mailman/database
diff options
context:
space:
mode:
authorBarry Warsaw2014-09-21 16:07:40 -0400
committerBarry Warsaw2014-09-21 16:07:40 -0400
commit0ad6dc0bf9f69f8245693b86ed2715effebf1fb3 (patch)
tree2dc0359dc7c3a043c87a92697c5de7ef2b0ddee3 /src/mailman/database
parentb6bc505e45a2f1f4f99d7dd2cdd868d533270ee9 (diff)
parentc339f06cca6ddf1d28cde2614a94c2a0c905957a (diff)
downloadmailman-0ad6dc0bf9f69f8245693b86ed2715effebf1fb3.tar.gz
mailman-0ad6dc0bf9f69f8245693b86ed2715effebf1fb3.tar.zst
mailman-0ad6dc0bf9f69f8245693b86ed2715effebf1fb3.zip
Merge Abilash's branch
Diffstat (limited to 'src/mailman/database')
-rw-r--r--src/mailman/database/base.py104
-rw-r--r--src/mailman/database/factory.py4
-rw-r--r--src/mailman/database/model.py46
-rw-r--r--src/mailman/database/postgresql.py4
-rw-r--r--src/mailman/database/sqlite.py6
-rw-r--r--src/mailman/database/types.py107
6 files changed, 123 insertions, 148 deletions
diff --git a/src/mailman/database/base.py b/src/mailman/database/base.py
index cbf88a4ff..f379b3124 100644
--- a/src/mailman/database/base.py
+++ b/src/mailman/database/base.py
@@ -29,8 +29,9 @@ import logging
from lazr.config import as_boolean
from pkg_resources import resource_listdir, resource_string
-from storm.cache import GenerationalCache
-from storm.locals import create_database, Store
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm.session import Session
from zope.interface import implementer
from mailman.config import config
@@ -45,23 +46,24 @@ NL = '\n'
@implementer(IDatabase)
-class StormBaseDatabase:
- """The database base class for use with the Storm ORM.
+class SABaseDatabase:
+ """The database base class for use with SQLAlchemy.
- Use this as a base class for your DB-specific derived classes.
+ Use this as a base class for your DB-Specific derived classes.
"""
-
# Tag used to distinguish the database being used. Override this in base
# classes.
+
TAG = ''
def __init__(self):
self.url = None
self.store = None
+ self.transaction = None
def begin(self):
"""See `IDatabase`."""
- # Storm takes care of this for us.
+ # SA does this for us.
pass
def commit(self):
@@ -100,18 +102,9 @@ class StormBaseDatabase:
"""
pass
- def _prepare(self, url):
- """Prepare the database for creation.
-
- Some database backends need to do so me prep work before letting Storm
- create the database. For example, we have to touch the SQLite .db
- file first so that it has the proper file modes.
- """
- pass
-
def initialize(self, debug=None):
- """See `IDatabase`."""
- # Calculate the engine url.
+ """See `IDatabase`"""
+ # Calculate the engine url
url = expand(config.database.url, config.paths)
log.debug('Database url: %s', url)
# XXX By design of SQLite, database file creation does not honor
@@ -129,13 +122,10 @@ class StormBaseDatabase:
# engines, and yes, we could have chmod'd the file after the fact, but
# half dozen and all...
self.url = url
- self._prepare(url)
- database = create_database(url)
- store = Store(database, GenerationalCache())
- database.DEBUG = (as_boolean(config.database.debug)
- if debug is None else debug)
- self.store = store
- store.commit()
+ self.engine = create_engine(url)
+ session = sessionmaker(bind=self.engine)
+ self.store = session()
+ self.store.commit()
def load_migrations(self, until=None):
"""Load schema migrations.
@@ -144,45 +134,8 @@ class StormBaseDatabase:
With default value of None, load all migrations.
:type until: string
"""
- migrations_path = config.database.migrations_path
- if '.' in migrations_path:
- parent, dot, child = migrations_path.rpartition('.')
- else:
- parent = migrations_path
- child = ''
- # If the database does not yet exist, load the base schema.
- filenames = sorted(resource_listdir(parent, child))
- # Find out which schema migrations have already been loaded.
- if self._database_exists(self.store):
- versions = set(version.version for version in
- self.store.find(Version, component='schema'))
- else:
- versions = set()
- for filename in filenames:
- module_fn, extension = os.path.splitext(filename)
- if extension != '.py':
- continue
- parts = module_fn.split('_')
- if len(parts) < 2:
- continue
- version = parts[1].strip()
- if len(version) == 0:
- # Not a schema migration file.
- continue
- if version in versions:
- log.debug('already migrated to %s', version)
- continue
- if until is not None and version > until:
- # We're done.
- break
- module_path = migrations_path + '.' + module_fn
- __import__(module_path)
- upgrade = getattr(sys.modules[module_path], 'upgrade', None)
- if upgrade is None:
- continue
- log.debug('migrating db to %s: %s', version, module_path)
- upgrade(self, self.store, version, module_path)
- self.commit()
+ from mailman.database.model import Model
+ Model.metadata.create_all(self.engine)
def load_sql(self, store, sql):
"""Load the given SQL into the store.
@@ -200,29 +153,6 @@ class StormBaseDatabase:
if statement.strip() != '':
store.execute(statement + ';')
- def load_schema(self, store, version, filename, module_path):
- """Load the schema from a file.
-
- This is a helper method for migration classes to call.
-
- :param store: The Storm store to load the schema into.
- :type store: storm.locals.Store`
- :param version: The schema version identifier of the form
- YYYYMMDDHHMMSS.
- :type version: string
- :param filename: The file name containing the schema to load. Pass
- `None` if there is no schema file to load.
- :type filename: string
- :param module_path: The fully qualified Python module path to the
- migration module being loaded. This is used to record information
- for use by the test suite.
- :type module_path: string
- """
- if filename is not None:
- contents = resource_string('mailman.database.schema', filename)
- self.load_sql(store, contents)
- # Add a marker that indicates the migration version being applied.
- store.add(Version(component='schema', version=version))
@staticmethod
def _make_temporary():
diff --git a/src/mailman/database/factory.py b/src/mailman/database/factory.py
index db453ea41..64fcc242c 100644
--- a/src/mailman/database/factory.py
+++ b/src/mailman/database/factory.py
@@ -62,10 +62,10 @@ class DatabaseFactory:
def _reset(self):
"""See `IDatabase`."""
- from mailman.database.model import ModelMeta
+ from mailman.database.model import Model
self.store.rollback()
self._pre_reset(self.store)
- ModelMeta._reset(self.store)
+ Model._reset(self)
self._post_reset(self.store)
self.store.commit()
diff --git a/src/mailman/database/model.py b/src/mailman/database/model.py
index ba2d39213..d86ebb80e 100644
--- a/src/mailman/database/model.py
+++ b/src/mailman/database/model.py
@@ -25,44 +25,24 @@ __all__ = [
]
+import contextlib
from operator import attrgetter
-from storm.properties import PropertyPublisherMeta
+from sqlalchemy.ext.declarative import declarative_base
+from mailman.config import config
-
-class ModelMeta(PropertyPublisherMeta):
+class ModelMeta(object):
"""Do more magic on table classes."""
- _class_registry = set()
-
- def __init__(self, name, bases, dict):
- # Before we let the base class do it's thing, force an __storm_table__
- # property to enforce our table naming convention.
- self.__storm_table__ = name.lower()
- super(ModelMeta, self).__init__(name, bases, dict)
- # Register the model class so that it can be more easily cleared.
- # This is required by the test framework so that the corresponding
- # table can be reset between tests.
- #
- # The PRESERVE flag indicates whether the table should be reset or
- # not. We have to handle the actual Model base class explicitly
- # because it does not correspond to a table in the database.
- if not getattr(self, 'PRESERVE', False) and name != 'Model':
- ModelMeta._class_registry.add(self)
-
@staticmethod
- def _reset(store):
- from mailman.config import config
- config.db._pre_reset(store)
- # Make sure this is deterministic, by sorting on the storm table name.
- classes = sorted(ModelMeta._class_registry,
- key=attrgetter('__storm_table__'))
- for model_class in classes:
- store.find(model_class).remove()
-
+ def _reset(db):
+ meta = Model.metadata
+ engine = config.db.engine
+ with contextlib.closing(engine.connect()) as con:
+ trans = con.begin()
+ for table in reversed(meta.sorted_tables):
+ con.execute(table.delete())
+ trans.commit()
-
-class Model:
- """Like Storm's `Storm` subclass, but with a bit extra."""
- __metaclass__ = ModelMeta
+Model = declarative_base(cls=ModelMeta)
diff --git a/src/mailman/database/postgresql.py b/src/mailman/database/postgresql.py
index 48c68a937..1ee454074 100644
--- a/src/mailman/database/postgresql.py
+++ b/src/mailman/database/postgresql.py
@@ -32,12 +32,12 @@ from functools import partial
from operator import attrgetter
from urlparse import urlsplit, urlunsplit
-from mailman.database.base import StormBaseDatabase
+from mailman.database.base import SABaseDatabase
from mailman.testing.helpers import configuration
-class PostgreSQLDatabase(StormBaseDatabase):
+class PostgreSQLDatabase(SABaseDatabase):
"""Database class for PostgreSQL."""
TAG = 'postgres'
diff --git a/src/mailman/database/sqlite.py b/src/mailman/database/sqlite.py
index 15629615f..0594d9091 100644
--- a/src/mailman/database/sqlite.py
+++ b/src/mailman/database/sqlite.py
@@ -34,12 +34,12 @@ import tempfile
from functools import partial
from urlparse import urlparse
-from mailman.database.base import StormBaseDatabase
+from mailman.database.base import SABaseDatabase
from mailman.testing.helpers import configuration
-class SQLiteDatabase(StormBaseDatabase):
+class SQLiteDatabase(SABaseDatabase):
"""Database class for SQLite."""
TAG = 'sqlite'
@@ -72,7 +72,7 @@ def _cleanup(self, tempdir):
def make_temporary(database):
"""Adapts by monkey patching an existing SQLite IDatabase."""
tempdir = tempfile.mkdtemp()
- url = 'sqlite:///' + os.path.join(tempdir, 'mailman.db')
+ url = 'sqlite:///' + os.path.join(tempdir, 'mailman.db')
with configuration('database', url=url):
database.initialize()
database._cleanup = types.MethodType(
diff --git a/src/mailman/database/types.py b/src/mailman/database/types.py
index ba3d92df4..a6f0b32ca 100644
--- a/src/mailman/database/types.py
+++ b/src/mailman/database/types.py
@@ -23,43 +23,108 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'Enum',
+ 'UUID',
]
+import uuid
-from storm.properties import SimpleProperty
-from storm.variables import Variable
+from sqlalchemy import Integer
+from sqlalchemy.types import TypeDecorator, BINARY, CHAR
+from sqlalchemy.dialects import postgresql
-class _EnumVariable(Variable):
- """Storm variable for supporting enum types.
-
- To use this, make the database column a INTEGER.
+class Enum(TypeDecorator):
+ """
+ Stores an integer-based Enum as an integer in the database, and converts it
+ on-the-fly.
"""
- def __init__(self, *args, **kws):
- self._enum = kws.pop('enum')
- super(_EnumVariable, self).__init__(*args, **kws)
+ impl = Integer
- def parse_set(self, value, from_db):
+ def __init__(self, *args, **kw):
+ self.enum = kw.pop("enum")
+ TypeDecorator.__init__(self, *args, **kw)
+
+ def process_bind_param(self, value, dialect):
if value is None:
return None
- if not from_db:
- return value
- return self._enum(value)
- def parse_get(self, value, to_db):
+ return value.value
+
+
+ def process_result_value(self, value, dialect):
if value is None:
return None
- if not to_db:
+ return self.enum(value)
+
+
+
+class UUID(TypeDecorator):
+ """
+ Stores a UUID in the database natively when it can and falls back to
+ a BINARY(16) or a CHAR(32) when it can't.
+
+ ::
+
+ from sqlalchemy_utils import UUIDType
+ import uuid
+
+ class User(Base):
+ __tablename__ = 'user'
+
+ # Pass `binary=False` to fallback to CHAR instead of BINARY
+ id = sa.Column(UUIDType(binary=False), primary_key=True)
+ """
+ impl = BINARY(16)
+
+ python_type = uuid.UUID
+
+ def __init__(self, binary=True, native=True):
+ """
+ :param binary: Whether to use a BINARY(16) or CHAR(32) fallback.
+ """
+ self.binary = binary
+ self.native = native
+
+ def load_dialect_impl(self, dialect):
+ if dialect.name == 'postgresql' and self.native:
+ # Use the native UUID type.
+ return dialect.type_descriptor(postgresql.UUID())
+
+ else:
+ # Fallback to either a BINARY or a CHAR.
+ kind = self.impl if self.binary else CHAR(32)
+ return dialect.type_descriptor(kind)
+
+ @staticmethod
+ def _coerce(value):
+ if value and not isinstance(value, uuid.UUID):
+ try:
+ value = uuid.UUID(value)
+
+ except (TypeError, ValueError):
+ value = uuid.UUID(bytes=value)
+
+ return value
+
+ def process_bind_param(self, value, dialect):
+ if value is None:
return value
- return value.value
+ if not isinstance(value, uuid.UUID):
+ value = self._coerce(value)
+
+ if self.native and dialect.name == 'postgresql':
+ return str(value)
-class Enum(SimpleProperty):
- """Custom type for Storm supporting enums."""
+ return value.bytes if self.binary else value.hex
+
+ def process_result_value(self, value, dialect):
+ if value is None:
+ return value
- variable_class = _EnumVariable
+ if self.native and dialect.name == 'postgresql':
+ return uuid.UUID(value)
- def __init__(self, enum=None):
- super(Enum, self).__init__(enum=enum)
+ return uuid.UUID(bytes=value) if self.binary else uuid.UUID(value)