~ubuntu-branches/ubuntu/saucy/migrate/saucy-proposed

« back to all changes in this revision

Viewing changes to migrate/versioning/schema.py

  • Committer: Bazaar Package Importer
  • Author(s): Jan Dittberner
  • Date: 2010-07-12 00:24:57 UTC
  • mfrom: (1.1.5 upstream) (2.1.8 sid)
  • Revision ID: james.westby@ubuntu.com-20100712002457-4j2fdmco4u9kqzm5
Upload to unstable.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
"""
2
2
   Database schema version management.
3
3
"""
 
4
import sys
 
5
import logging
 
6
 
4
7
from sqlalchemy import (Table, Column, MetaData, String, Text, Integer,
5
8
    create_engine)
6
9
from sqlalchemy.sql import and_
7
10
from sqlalchemy import exceptions as sa_exceptions
 
11
from sqlalchemy.sql import bindparam
8
12
 
9
13
from migrate.versioning import exceptions, genmodel, schemadiff
10
14
from migrate.versioning.repository import Repository
12
16
from migrate.versioning.version import VerNum
13
17
 
14
18
 
 
19
log = logging.getLogger(__name__)
 
20
 
15
21
class ControlledSchema(object):
16
22
    """A database under version control"""
17
23
 
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)
24
 
        self._load()
 
29
        self.meta = MetaData(engine)
 
30
        self.load()
25
31
 
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)
29
36
 
30
 
    def _load(self):
 
37
    def load(self):
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:
35
 
            try:
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
 
40
        try:
 
41
            if not hasattr(self, 'table') or self.table is None:
 
42
                    self.table = Table(tname, self.meta, autoload=True)
 
43
 
 
44
            result = self.engine.execute(self.table.select(
 
45
                self.table.c.repository_id == str(self.repository.id)))
 
46
 
 
47
            data = list(result)[0]
 
48
        except:
 
49
            cls, exc, tb = sys.exc_info()
 
50
            raise exceptions.DatabaseNotControlledError, exc.__str__(), tb
 
51
 
45
52
        self.version = data['version']
46
 
 
47
 
    def _get_repository(self):
48
 
        """
49
 
        Given a database engine, try to guess the repository.
50
 
 
51
 
        :raise: :exc:`NotImplementedError`
52
 
        """
53
 
        # TODO: no guessing yet; for now, a repository must be supplied
54
 
        raise NotImplementedError()
 
53
        return data
 
54
 
 
55
    def drop(self):
 
56
        """
 
57
        Remove version control from a database.
 
58
        """
 
59
        try:
 
60
            self.table.drop()
 
61
        except (sa_exceptions.SQLError):
 
62
            raise exceptions.DatabaseNotControlledError(str(self.table))
 
63
 
 
64
    def changeset(self, version=None):
 
65
        """API to Changeset creation.
 
66
        
 
67
        Uses self.version for start version and engine.name
 
68
        to get database name.
 
69
        """
 
70
        database = self.engine.name
 
71
        start_ver = self.version
 
72
        changeset = self.repository.changeset(database, start_ver, version)
 
73
        return changeset
 
74
 
 
75
    def runchange(self, ver, change, step):
 
76
        startver = ver
 
77
        endver = ver + 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))
 
82
        # Run the change
 
83
        change.run(self.engine, step)
 
84
 
 
85
        # Update/refresh database version
 
86
        self.update_repository_table(startver, endver)
 
87
        self.load()
 
88
 
 
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))
 
94
 
 
95
    def upgrade(self, version=None):
 
96
        """
 
97
        Upgrade (or downgrade) to a specified version, or latest version.
 
98
        """
 
99
        changeset = self.changeset(version)
 
100
        for ver, change in changeset:
 
101
            self.runchange(ver, change, changeset.step)
 
102
 
 
103
    def update_db_from_model(self, model):
 
104
        """
 
105
        Modify the database to match the structure of the current Python model.
 
106
        """
 
107
        model = load_model(model)
 
108
 
 
109
        diff = schemadiff.getDiffOfModelAgainstDatabase(
 
110
            model, self.engine, excludeTables=[self.repository.version_table])
 
111
        genmodel.ModelGenerator(diff).applyModel()
 
112
 
 
113
        self.update_repository_table(self.version, int(self.repository.latest))
 
114
 
 
115
        self.load()
55
116
 
56
117
    @classmethod
57
118
    def create(cls, engine, repository, version=None):
58
119
        """
59
120
        Declare a database to be under a repository's version control.
 
121
 
 
122
        :raises: :exc:`DatabaseAlreadyControlledError`
 
123
        :returns: :class:`ControlledSchema`
60
124
        """
61
125
        # Confirm that the version # is valid: positive, integer,
62
126
        # exists in repos
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)
73
137
        """
74
138
        Ensures this is a valid version number for this repository.
75
139
 
76
 
        :raises: :exc:`cls.InvalidVersionError` if invalid
 
140
        :raises: :exc:`InvalidVersionError` if invalid
77
141
        :return: valid version number
78
142
        """
79
143
        if version is None:
90
154
    def _create_table_version(cls, engine, repository, version):
91
155
        """
92
156
        Creates the versioning table in a database.
 
157
 
 
158
        :raises: :exc:`DatabaseAlreadyControlledError`
93
159
        """
94
160
        # Create tables
95
161
        tname = repository.version_table
97
163
 
98
164
        table = Table(
99
165
            tname, meta,
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), )
103
169
 
 
170
        # there can be multiple repositories/schemas in the same db
104
171
        if not table.exists():
105
172
            table.create()
106
173
 
 
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
 
179
 
107
180
        # Insert data
108
 
        try:
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)))
115
185
        return table
116
186
 
117
187
    @classmethod
120
190
        Compare the current model against the current database.
121
191
        """
122
192
        if isinstance(repository, basestring):
123
 
            repository=Repository(repository)
 
193
            repository = Repository(repository)
124
194
        model = load_model(model)
 
195
 
125
196
        diff = schemadiff.getDiffOfModelAgainstDatabase(
126
197
            model, engine, excludeTables=[repository.version_table])
127
198
        return diff
132
203
        Dump the current database as a Python model.
133
204
        """
134
205
        if isinstance(repository, basestring):
135
 
            repository=Repository(repository)
 
206
            repository = Repository(repository)
 
207
 
136
208
        diff = schemadiff.getDiffOfModelAgainstDatabase(
137
209
            MetaData(), engine, excludeTables=[repository.version_table])
138
210
        return genmodel.ModelGenerator(diff, declarative).toPython()
139
 
 
140
 
    def update_db_from_model(self, model):
141
 
        """
142
 
        Modify the database to match the structure of the current Python model.
143
 
        """
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))
153
 
 
154
 
    def drop(self):
155
 
        """
156
 
        Remove version control from a database.
157
 
        """
158
 
        try:
159
 
            self.table.drop()
160
 
        except (sa_exceptions.SQLError):
161
 
            raise exceptions.DatabaseNotControlledError(str(self.table))
162
 
 
163
 
    def _engine_db(self, engine):
164
 
        """
165
 
        Returns the database name of an engine - ``postgres``, ``sqlite`` ...
166
 
        """
167
 
        # TODO: This is a bit of a hack...
168
 
        return str(engine.dialect.__module__).split('.')[-1]
169
 
 
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)
174
 
        return changeset
175
 
 
176
 
    def runchange(self, ver, change, step):
177
 
        startver = ver
178
 
        endver = ver + 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))
183
 
        # Run the change
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))
190
 
        self._load()
191
 
 
192
 
    def upgrade(self, version=None):
193
 
        """
194
 
        Upgrade (or downgrade) to a specified version, or latest version.
195
 
        """
196
 
        changeset = self.changeset(version)
197
 
        for ver, change in changeset:
198
 
            self.runchange(ver, change, changeset.step)