12
16
from migrate.versioning.version import VerNum
19
log = logging.getLogger(__name__)
15
21
class ControlledSchema(object):
16
22
"""A database under version control"""
18
24
def __init__(self, engine, repository):
19
25
if isinstance(repository, str):
20
repository=Repository(repository)
26
repository = Repository(repository)
21
27
self.engine = engine
22
28
self.repository = repository
23
self.meta=MetaData(engine)
29
self.meta = MetaData(engine)
26
32
def __eq__(self, other):
33
"""Compare two schemas by repositories and versions"""
27
34
return (self.repository is other.repository \
28
35
and self.version == other.version)
31
38
"""Load controlled schema version info from DB"""
32
39
tname = self.repository.version_table
33
self.meta=MetaData(self.engine)
34
if not hasattr(self, 'table') or self.table is None:
36
self.table = Table(tname, self.meta, autoload=True)
37
except (exceptions.NoSuchTableError):
38
raise exceptions.DatabaseNotControlledError(tname)
39
# TODO?: verify that the table is correct (# cols, etc.)
40
result = self.engine.execute(self.table.select(
41
self.table.c.repository_id == str(self.repository.id)))
42
data = list(result)[0]
43
# TODO?: exception if row count is bad
44
# TODO: check repository id, exception if incorrect
41
if not hasattr(self, 'table') or self.table is None:
42
self.table = Table(tname, self.meta, autoload=True)
44
result = self.engine.execute(self.table.select(
45
self.table.c.repository_id == str(self.repository.id)))
47
data = list(result)[0]
49
cls, exc, tb = sys.exc_info()
50
raise exceptions.DatabaseNotControlledError, exc.__str__(), tb
45
52
self.version = data['version']
47
def _get_repository(self):
49
Given a database engine, try to guess the repository.
51
:raise: :exc:`NotImplementedError`
53
# TODO: no guessing yet; for now, a repository must be supplied
54
raise NotImplementedError()
57
Remove version control from a database.
61
except (sa_exceptions.SQLError):
62
raise exceptions.DatabaseNotControlledError(str(self.table))
64
def changeset(self, version=None):
65
"""API to Changeset creation.
67
Uses self.version for start version and engine.name
70
database = self.engine.name
71
start_ver = self.version
72
changeset = self.repository.changeset(database, start_ver, version)
75
def runchange(self, ver, change, step):
78
# Current database version must be correct! Don't run if corrupt!
79
if self.version != startver:
80
raise exceptions.InvalidVersionError("%s is not %s" % \
81
(self.version, startver))
83
change.run(self.engine, step)
85
# Update/refresh database version
86
self.update_repository_table(startver, endver)
89
def update_repository_table(self, startver, endver):
90
"""Update version_table with new information"""
91
update = self.table.update(and_(self.table.c.version == int(startver),
92
self.table.c.repository_id == str(self.repository.id)))
93
self.engine.execute(update, version=int(endver))
95
def upgrade(self, version=None):
97
Upgrade (or downgrade) to a specified version, or latest version.
99
changeset = self.changeset(version)
100
for ver, change in changeset:
101
self.runchange(ver, change, changeset.step)
103
def update_db_from_model(self, model):
105
Modify the database to match the structure of the current Python model.
107
model = load_model(model)
109
diff = schemadiff.getDiffOfModelAgainstDatabase(
110
model, self.engine, excludeTables=[self.repository.version_table])
111
genmodel.ModelGenerator(diff).applyModel()
113
self.update_repository_table(self.version, int(self.repository.latest))
57
118
def create(cls, engine, repository, version=None):
59
120
Declare a database to be under a repository's version control.
122
:raises: :exc:`DatabaseAlreadyControlledError`
123
:returns: :class:`ControlledSchema`
61
125
# Confirm that the version # is valid: positive, integer,
63
if type(repository) is str:
64
repository=Repository(repository)
127
if isinstance(repository, basestring):
128
repository = Repository(repository)
65
129
version = cls._validate_version(repository, version)
66
table=cls._create_table_version(engine, repository, version)
130
table = cls._create_table_version(engine, repository, version)
67
131
# TODO: history table
68
132
# Load repository information and return
69
133
return cls(engine, repository)
100
Column('repository_id', String(255), primary_key=True),
166
Column('repository_id', String(250), primary_key=True),
101
167
Column('repository_path', Text),
102
168
Column('version', Integer), )
170
# there can be multiple repositories/schemas in the same db
104
171
if not table.exists():
174
# test for existing repository_id
175
s = table.select(table.c.repository_id == bindparam("repository_id"))
176
result = engine.execute(s, repository_id=repository.id)
177
if result.fetchone():
178
raise exceptions.DatabaseAlreadyControlledError
109
engine.execute(table.insert(), repository_id=repository.id,
181
engine.execute(table.insert().values(
182
repository_id=repository.id,
110
183
repository_path=repository.path,
111
version=int(version))
112
except sa_exceptions.IntegrityError:
113
# An Entry for this repo already exists.
114
raise exceptions.DatabaseAlreadyControlledError()
184
version=int(version)))
132
203
Dump the current database as a Python model.
134
205
if isinstance(repository, basestring):
135
repository=Repository(repository)
206
repository = Repository(repository)
136
208
diff = schemadiff.getDiffOfModelAgainstDatabase(
137
209
MetaData(), engine, excludeTables=[repository.version_table])
138
210
return genmodel.ModelGenerator(diff, declarative).toPython()
140
def update_db_from_model(self, model):
142
Modify the database to match the structure of the current Python model.
144
if isinstance(self.repository, basestring):
145
self.repository=Repository(self.repository)
146
model = load_model(model)
147
diff = schemadiff.getDiffOfModelAgainstDatabase(
148
model, self.engine, excludeTables=[self.repository.version_table])
149
genmodel.ModelGenerator(diff).applyModel()
150
update = self.table.update(
151
self.table.c.repository_id == str(self.repository.id))
152
self.engine.execute(update, version=int(self.repository.latest))
156
Remove version control from a database.
160
except (sa_exceptions.SQLError):
161
raise exceptions.DatabaseNotControlledError(str(self.table))
163
def _engine_db(self, engine):
165
Returns the database name of an engine - ``postgres``, ``sqlite`` ...
167
# TODO: This is a bit of a hack...
168
return str(engine.dialect.__module__).split('.')[-1]
170
def changeset(self, version=None):
171
database = self._engine_db(self.engine)
172
start_ver = self.version
173
changeset = self.repository.changeset(database, start_ver, version)
176
def runchange(self, ver, change, step):
179
# Current database version must be correct! Don't run if corrupt!
180
if self.version != startver:
181
raise exceptions.InvalidVersionError("%s is not %s" % \
182
(self.version, startver))
184
change.run(self.engine, step)
185
# Update/refresh database version
186
update = self.table.update(
187
and_(self.table.c.version == int(startver),
188
self.table.c.repository_id == str(self.repository.id)))
189
self.engine.execute(update, version=int(endver))
192
def upgrade(self, version=None):
194
Upgrade (or downgrade) to a specified version, or latest version.
196
changeset = self.changeset(version)
197
for ver, change in changeset:
198
self.runchange(ver, change, changeset.step)