1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
|
# Copyright (C) 2006-2014 by the Free Software Foundation, Inc.
#
# This file is part of GNU Mailman.
#
# GNU Mailman is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'StormBaseDatabase',
]
import os
import sys
import logging
from lazr.config import as_boolean
from pkg_resources import resource_listdir, resource_string
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from zope.interface import implementer
from mailman.config import config
from mailman.interfaces.database import IDatabase
from mailman.model.version import Version
from mailman.utilities.string import expand
log = logging.getLogger('mailman.config')
NL = '\n'
@implementer(IDatabase)
class SABaseDatabase:
"""The database base class for use with SQLAlchemy.
Use this as a base class for your DB_Specific derived classes.
"""
TAG=''
def __init__(self):
self.url = None
self.store = None
def begin(self):
pass
def commit(self):
self.store.commit()
def abort(self):
self.store.rollback()
def _prepare(self, url):
pass
def _pre_reset(self, store):
"""Clean up method for testing.
This method is called during the test suite just before all the model
tables are removed. Override this to perform any database-specific
pre-removal cleanup.
"""
pass
def _post_reset(self, store):
"""Clean up method for testing.
This method is called during the test suite just after all the model
tables have been removed. Override this to perform any
database-specific post-removal cleanup.
"""
pass
def initialize(self, debug=None):
url = expand(config.database.url, config.paths)
log.debug('Database url: %s', url)
self.url = url
self._prepare(url)
engine = create_engine(url)
Session = sessionmaker(bind=engine)
store = Session()
self.store = store
store.commit()
def load_migrations(self, until=None):
"""Load schema migrations.
:param until: Load only the migrations up to the specified timestamp.
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()
def load_sql(self, store, sql):
"""Load the given SQL into the store.
:param store: The Storm store to load the schema into.
:type store: storm.locals.Store`
:param sql: The possibly multi-line SQL to load.
:type sql: string
"""
# Discard all blank and comment lines.
lines = (line for line in sql.splitlines()
if line.strip() != '' and line.strip()[:2] != '--')
sql = NL.join(lines)
for statement in sql.split(';'):
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():
raise NotImplementedError
|