root/galaxy-central/eggs/sqlalchemy_migrate-0.5.4-py2.6.egg/migrate/changeset/schema.py @ 3

リビジョン 3, 13.4 KB (コミッタ: kohda, 14 年 前)

Install Unix tools  http://hannonlab.cshl.edu/galaxy_unix_tools/galaxy.html

行番号 
1"""
2   Schema module providing common schema operations.
3"""
4import re
5import sqlalchemy
6from migrate.changeset.databases.visitor import get_engine_visitor
7
8__all__ = [
9'create_column',
10'drop_column',
11'alter_column',
12'rename_table',
13'rename_index',
14]
15
16
17def create_column(column, table=None, *p, **k):
18    if table is not None:
19        return table.create_column(column, *p, **k)
20    return column.create(*p, **k)
21
22
23def drop_column(column, table=None, *p, **k):
24    if table is not None:
25        return table.drop_column(column, *p, **k)
26    return column.drop(*p, **k)
27
28
29def _to_table(table, engine=None):
30    if isinstance(table, sqlalchemy.Table):
31        return table
32    # Given: table name, maybe an engine
33    meta = sqlalchemy.MetaData()
34    if engine is not None:
35        meta.bind = engine
36    return sqlalchemy.Table(table, meta)
37
38
39def _to_index(index, table=None, engine=None):
40    if isinstance(index, sqlalchemy.Index):
41        return index
42    # Given: index name; table name required
43    table = _to_table(table, engine)
44    ret = sqlalchemy.Index(index)
45    ret.table = table
46    return ret
47
48
49def rename_table(table, name, engine=None):
50    """Rename a table, given the table's current name and the new
51    name."""
52    table = _to_table(table, engine)
53    table.rename(name)
54
55
56def rename_index(index, name, table=None, engine=None):
57    """Rename an index.
58
59    Takes an index name/object, a table name/object, and an
60    engine. Engine and table aren't required if an index object is
61    given.
62    """
63    index = _to_index(index, table, engine)
64    index.rename(name)
65
66
67def _engine_run_visitor(engine, visitorcallable, element, **kwargs):
68    conn = engine.connect()
69    try:
70        element.accept_schema_visitor(visitorcallable(engine.dialect,
71                                                      connection=conn))
72    finally:
73        conn.close()
74
75
76def alter_column(*p, **k):
77    """Alter a column.
78
79    Parameters: column name, table name, an engine, and the properties
80    of that column to change
81    """
82    if len(p) and isinstance(p[0], sqlalchemy.Column):
83        col = p[0]
84    else:
85        col = None
86    if 'table' not in k:
87        k['table'] = col.table
88    if 'engine' not in k:
89        k['engine'] = k['table'].bind
90    engine = k['engine']
91    delta = _ColumnDelta(*p, **k)
92    visitorcallable = get_engine_visitor(engine, 'schemachanger')
93    _engine_run_visitor(engine, visitorcallable, delta)
94
95    # Update column
96    if col is not None:
97        # Special case: change column key on rename, if key not
98        # explicit
99        #
100        # Used by SA : table.c.[key]
101        #
102        # This fails if the key was explit AND equal to the column
103        # name.  (It changes the key name when it shouldn't.)
104        #
105        # Not much we can do about it.
106        if 'name' in delta.keys():
107            if (col.name == col.key):
108                newname = delta['name']
109                del col.table.c[col.key]
110                setattr(col, 'key', newname)
111                col.table.c[col.key] = col
112        # Change all other attrs
113        for key, val in delta.iteritems():
114            setattr(col, key, val)
115
116
117def _normalize_table(column, table):
118    if table is not None:
119        if table is not column.table:
120            # This is a bit of a hack: we end up with dupe PK columns here
121            pk_names = map(lambda c: c.name, table.primary_key)
122            if column.primary_key and pk_names.count(column.name):
123                index = pk_names.index(column_name)
124                del table.primary_key[index]
125            table.append_column(column)
126    return column.table
127
128
129class _WrapRename(object):
130
131    def __init__(self, item, name):
132        self.item = item
133        self.name = name
134
135    def accept_schema_visitor(self, visitor):
136        if isinstance(self.item, sqlalchemy.Table):
137            suffix = 'table'
138        elif isinstance(self.item, sqlalchemy.Column):
139            suffix = 'column'
140        elif isinstance(self.item, sqlalchemy.Index):
141            suffix = 'index'
142        funcname = 'visit_%s' % suffix
143        func = getattr(visitor, funcname)
144        param = self.item, self.name
145        return func(param)
146
147
148class _ColumnDelta(dict):
149    """Extracts the differences between two columns/column-parameters"""
150
151    def __init__(self, *p, **k):
152        """Extract ALTER-able differences from two columns.
153
154        May receive parameters arranged in several different ways:
155         * old_column_object,new_column_object,*parameters Identifies
156            attributes that differ between the two columns.
157            Parameters specified outside of either column are always
158            executed and override column differences.
159         * column_object,[current_name,]*parameters Parameters
160            specified are changed; table name is extracted from column
161            object.  Name is changed to column_object.name from
162            current_name, if current_name is specified. If not
163            specified, name is unchanged.
164         * current_name,table,*parameters 'table' may be either an
165            object or a name
166        """
167        # Things are initialized differently depending on how many column
168        # parameters are given. Figure out how many and call the appropriate
169        # method.
170        if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column):
171            # At least one column specified
172            if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
173                # Two columns specified
174                func = self._init_2col
175            else:
176                # Exactly one column specified
177                func = self._init_1col
178        else:
179            # Zero columns specified
180            func = self._init_0col
181        diffs = func(*p, **k)
182        self._set_diffs(diffs)
183    # Column attributes that can be altered
184    diff_keys = ('name', 'type', 'nullable', 'default', 'server_default',
185                 'primary_key', 'foreign_key')
186
187    def _get_table_name(self):
188        if isinstance(self._table, basestring):
189            ret = self._table
190        else:
191            ret = self._table.name
192        return ret
193    table_name = property(_get_table_name)
194
195    def _get_table(self):
196        if isinstance(self._table, basestring):
197            ret = None
198        else:
199            ret = self._table
200        return ret
201    table = property(_get_table)
202
203    def _init_0col(self, current_name, *p, **k):
204        p, k = self._init_normalize_params(p, k)
205        table = k.pop('table')
206        self.current_name = current_name
207        self._table = table
208        return k
209
210    def _init_1col(self, col, *p, **k):
211        p, k = self._init_normalize_params(p, k)
212        self._table = k.pop('table', None) or col.table
213        self.result_column = col.copy()
214        if 'current_name' in k:
215            # Renamed
216            self.current_name = k.pop('current_name')
217            k.setdefault('name', col.name)
218        else:
219            self.current_name = col.name
220        return k
221
222    def _init_2col(self, start_col, end_col, *p, **k):
223        p, k = self._init_normalize_params(p, k)
224        self.result_column = start_col.copy()
225        self._table = k.pop('table', None) or start_col.table \
226            or end_col.table
227        self.current_name = start_col.name
228        for key in ('name', 'nullable', 'default', 'server_default',
229                    'primary_key', 'foreign_key'):
230            val = getattr(end_col, key, None)
231            if getattr(start_col, key, None) != val:
232                k.setdefault(key, val)
233        if not self.column_types_eq(start_col.type, end_col.type):
234            k.setdefault('type', end_col.type)
235        return k
236
237    def _init_normalize_params(self, p, k):
238        p = list(p)
239        if len(p):
240            k.setdefault('name', p.pop(0))
241        if len(p):
242            k.setdefault('type', p.pop(0))
243        # TODO: sequences? FKs?
244        return p, k
245
246    def _set_diffs(self, diffs):
247        for key in self.diff_keys:
248            if key in diffs:
249                self[key] = diffs[key]
250                if getattr(self, 'result_column', None) is not None:
251                    setattr(self.result_column, key, diffs[key])
252
253    def column_types_eq(self, this, that):
254        ret = isinstance(this, that.__class__)
255        ret = ret or isinstance(that, this.__class__)
256        # String length is a special case
257        if ret and isinstance(that, sqlalchemy.types.String):
258            ret = (getattr(this, 'length', None) == \
259                       getattr(that, 'length', None))
260        return ret
261
262    def accept_schema_visitor(self, visitor):
263        return visitor.visit_column(self)
264
265
266class ChangesetTable(object):
267    """Changeset extensions to SQLAlchemy tables."""
268
269    def create_column(self, column):
270        """Creates a column.
271
272        The column parameter may be a column definition or the name of
273        a column in this table.
274        """
275        if not isinstance(column, sqlalchemy.Column):
276            # It's a column name
277            column = getattr(self.c, str(column))
278        column.create(table=self)
279
280    def drop_column(self, column):
281        """Drop a column, given its name or definition."""
282        if not isinstance(column, sqlalchemy.Column):
283            # It's a column name
284            try:
285                column = getattr(self.c, str(column), None)
286            except AttributeError:
287                # That column isn't part of the table. We don't need
288                # its entire definition to drop the column, just its
289                # name, so create a dummy column with the same name.
290                column = sqlalchemy.Column(str(column))
291        column.drop(table=self)
292
293    def _meta_key(self):
294        return sqlalchemy.schema._get_table_key(self.name, self.schema)
295
296    def deregister(self):
297        """Remove this table from its metadata"""
298        key = self._meta_key()
299        meta = self.metadata
300        if key in meta.tables:
301            del meta.tables[key]
302
303    def rename(self, name, *args, **kwargs):
304        """Rename this table.
305
306        This changes both the database name and the name of this
307        Python object
308        """
309        engine = self.bind
310        visitorcallable = get_engine_visitor(engine, 'schemachanger')
311        param = _WrapRename(self, name)
312        _engine_run_visitor(engine, visitorcallable, param, *args, **kwargs)
313
314        # Fix metadata registration
315        meta = self.metadata
316        self.deregister()
317        self.name = name
318        self._set_parent(meta)
319
320    def _get_fullname(self):
321        """Fullname should always be up to date"""
322        # Copied from Table constructor
323        if self.schema is not None:
324            ret = "%s.%s"%(self.schema, self.name)
325        else:
326            ret = self.name
327        return ret
328
329    fullname = property(_get_fullname, (lambda self, val: None))
330
331
332class ChangesetColumn(object):
333    """Changeset extensions to SQLAlchemy columns"""
334
335    def alter(self, *p, **k):
336        """Alter a column's definition: ``ALTER TABLE ALTER COLUMN``.
337
338        May supply a new column object, or a list of properties to
339        change.
340
341        For example; the following are equivalent:
342            col.alter(Column('myint', Integer, nullable=False))
343            col.alter('myint', Integer, nullable=False)
344            col.alter(name='myint', type=Integer, nullable=False)
345
346        Column name, type, default, and nullable may be changed
347        here. Note that for column defaults, only PassiveDefaults are
348        managed by the database - changing others doesn't make sense.
349        """
350        if 'table' not in k:
351            k['table'] = self.table
352        if 'engine' not in k:
353            k['engine'] = k['table'].bind
354        return alter_column(self, *p, **k)
355
356    def create(self, table=None, *args, **kwargs):
357        """Create this column in the database.
358
359        Assumes the given table exists. ``ALTER TABLE ADD COLUMN``,
360        for most databases.
361        """
362        table = _normalize_table(self, table)
363        engine = table.bind
364        visitorcallable = get_engine_visitor(engine, 'columngenerator')
365        engine._run_visitor(visitorcallable, self, *args, **kwargs)
366
367        #add in foreign keys
368        if self.foreign_keys:
369            for fk in self.foreign_keys:
370                visitorcallable = get_engine_visitor(engine,
371                                                     'columnfkgenerator')
372                engine._run_visitor(visitorcallable, self, fk=fk)
373        return self
374
375    def drop(self, table=None, *args, **kwargs):
376        """Drop this column from the database, leaving its table intact.
377
378        ``ALTER TABLE DROP COLUMN``, for most databases.
379        """
380        table = _normalize_table(self, table)
381        engine = table.bind
382        visitorcallable = get_engine_visitor(engine, 'columndropper')
383        engine._run_visitor(lambda dialect, conn: visitorcallable(conn),
384                            self, *args, **kwargs)
385        return self
386
387
388class ChangesetIndex(object):
389    """Changeset extensions to SQLAlchemy Indexes."""
390
391    def rename(self, name, *args, **kwargs):
392        """Change the name of an index.
393
394        This changes both the Python object name and the database
395        name.
396        """
397        engine = self.table.bind
398        visitorcallable = get_engine_visitor(engine, 'schemachanger')
399        param = _WrapRename(self, name)
400        _engine_run_visitor(engine, visitorcallable, param, *args, **kwargs)
401        self.name = name
402
403
404def _patch():
405    """All the 'ugly' operations that patch SQLAlchemy's internals."""
406    sqlalchemy.schema.Table.__bases__ += (ChangesetTable, )
407    sqlalchemy.schema.Column.__bases__ += (ChangesetColumn, )
408    sqlalchemy.schema.Index.__bases__ += (ChangesetIndex, )
409_patch()
Note: リポジトリブラウザについてのヘルプは TracBrowser を参照してください。