Changeset - 0ccdb52079e9
[Not reviewed]
default
0 1 0
timeless@gmail.com - 10 years ago 2016-05-03 14:12:24
timeless@gmail.com
spelling: referred
1 file changed with 1 insertions and 1 deletions:
0 comments (0 inline, 0 general)
kallithea/lib/dbmigrate/migrate/changeset/schema.py
Show inline comments
 
"""
 
   Schema module providing common schema operations.
 
"""
 
import warnings
 

	
 
from UserDict import DictMixin
 

	
 
import sqlalchemy
 

	
 
from sqlalchemy.schema import ForeignKeyConstraint
 
from sqlalchemy.schema import UniqueConstraint
 

	
 
from kallithea.lib.dbmigrate.migrate.exceptions import *
 
from kallithea.lib.dbmigrate.migrate.changeset import SQLA_06, SQLA_07
 
from kallithea.lib.dbmigrate.migrate.changeset.databases.visitor import (get_engine_visitor,
 
                                                                         run_single_visitor)
 

	
 

	
 
__all__ = [
 
    'create_column',
 
    'drop_column',
 
    'alter_column',
 
    'rename_table',
 
    'rename_index',
 
    'ChangesetTable',
 
    'ChangesetColumn',
 
    'ChangesetIndex',
 
    'ChangesetDefaultClause',
 
    'ColumnDelta',
 
]
 

	
 
def create_column(column, table=None, *p, **kw):
 
    """Create a column, given the table.
 

	
 
    API to :meth:`ChangesetColumn.create`.
 
    """
 
    if table is not None:
 
        return table.create_column(column, *p, **kw)
 
    return column.create(*p, **kw)
 

	
 

	
 
def drop_column(column, table=None, *p, **kw):
 
    """Drop a column, given the table.
 

	
 
    API to :meth:`ChangesetColumn.drop`.
 
    """
 
    if table is not None:
 
        return table.drop_column(column, *p, **kw)
 
    return column.drop(*p, **kw)
 

	
 

	
 
def rename_table(table, name, engine=None, **kw):
 
    """Rename a table.
 

	
 
    If Table instance is given, engine is not used.
 

	
 
    API to :meth:`ChangesetTable.rename`.
 

	
 
    :param table: Table to be renamed.
 
    :param name: New name for Table.
 
    :param engine: Engine instance.
 
    :type table: string or Table instance
 
    :type name: string
 
    :type engine: obj
 
    """
 
    table = _to_table(table, engine)
 
    table.rename(name, **kw)
 

	
 

	
 
def rename_index(index, name, table=None, engine=None, **kw):
 
    """Rename an index.
 

	
 
    If Index instance is given,
 
    table and engine are not used.
 

	
 
    API to :meth:`ChangesetIndex.rename`.
 

	
 
    :param index: Index to be renamed.
 
    :param name: New name for index.
 
    :param table: Table to which Index is reffered.
 
    :param table: Table to which Index is referred.
 
    :param engine: Engine instance.
 
    :type index: string or Index instance
 
    :type name: string
 
    :type table: string or Table instance
 
    :type engine: obj
 
    """
 
    index = _to_index(index, table, engine)
 
    index.rename(name, **kw)
 

	
 

	
 
def alter_column(*p, **k):
 
    """Alter a column.
 

	
 
    This is a helper function that creates a :class:`ColumnDelta` and
 
    runs it.
 

	
 
    :argument column:
 
      The name of the column to be altered or a
 
      :class:`ChangesetColumn` column representing it.
 

	
 
    :param table:
 
      A :class:`~sqlalchemy.schema.Table` or table name to
 
      for the table where the column will be changed.
 

	
 
    :param engine:
 
      The :class:`~sqlalchemy.engine.base.Engine` to use for table
 
      reflection and schema alterations.
 

	
 
    :returns: A :class:`ColumnDelta` instance representing the change.
 

	
 

	
 
    """
 

	
 
    if 'table' not in k and isinstance(p[0], sqlalchemy.Column):
 
        k['table'] = p[0].table
 
    if 'engine' not in k:
 
        k['engine'] = k['table'].bind
 

	
 
    # deprecation
 
    if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
 
        warnings.warn(
 
            "Passing a Column object to alter_column is deprecated."
 
            " Just pass in keyword parameters instead.",
 
            MigrateDeprecationWarning
 
            )
 
    engine = k['engine']
 

	
 
    # enough tests seem to break when metadata is always altered
 
    # that this crutch has to be left in until they can be sorted
 
    # out
 
    k['alter_metadata']=True
 

	
 
    delta = ColumnDelta(*p, **k)
 

	
 
    visitorcallable = get_engine_visitor(engine, 'schemachanger')
 
    engine._run_visitor(visitorcallable, delta)
 

	
 
    return delta
 

	
 

	
 
def _to_table(table, engine=None):
 
    """Return if instance of Table, else construct new with metadata"""
 
    if isinstance(table, sqlalchemy.Table):
 
        return table
 

	
 
    # Given: table name, maybe an engine
 
    meta = sqlalchemy.MetaData()
 
    if engine is not None:
 
        meta.bind = engine
 
    return sqlalchemy.Table(table, meta)
 

	
 

	
 
def _to_index(index, table=None, engine=None):
 
    """Return if instance of Index, else construct new with metadata"""
 
    if isinstance(index, sqlalchemy.Index):
 
        return index
 

	
 
    # Given: index name; table name required
 
    table = _to_table(table, engine)
 
    ret = sqlalchemy.Index(index)
 
    ret.table = table
 
    return ret
 

	
 

	
 
class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem):
 
    """Extracts the differences between two columns/column-parameters
 

	
 
        May receive parameters arranged in several different ways:
 

	
 
        * **current_column, new_column, \*p, \*\*kw**
 
            Additional parameters can be specified to override column
 
            differences.
 

	
 
        * **current_column, \*p, \*\*kw**
 
            Additional parameters alter current_column. Table name is extracted
 
            from current_column object.
 
            Name is changed to current_column.name from current_name,
 
            if current_name is specified.
 

	
 
        * **current_col_name, \*p, \*\*kw**
 
            Table kw must specified.
 

	
 
        :param table: Table at which current Column should be bound to. \
 
        If table name is given, reflection will be used.
 
        :type table: string or Table instance
 

	
 
        :param metadata: A :class:`MetaData` instance to store
 
                         reflected table names
 

	
 
        :param engine: When reflecting tables, either engine or metadata must \
 
        be specified to acquire engine object.
 
        :type engine: :class:`Engine` instance
 
        :returns: :class:`ColumnDelta` instance provides interface for altered attributes to \
 
        `result_column` through :func:`dict` alike object.
 

	
 
        * :class:`ColumnDelta`.result_column is altered column with new attributes
 

	
 
        * :class:`ColumnDelta`.current_name is current name of column in db
 

	
 

	
 
    """
 

	
 
    # Column attributes that can be altered
 
    diff_keys = ('name', 'type', 'primary_key', 'nullable',
 
        'server_onupdate', 'server_default', 'autoincrement')
 
    diffs = dict()
 
    __visit_name__ = 'column'
 

	
 
    def __init__(self, *p, **kw):
 
        # 'alter_metadata' is not a public api. It exists purely
 
        # as a crutch until the tests that fail when 'alter_metadata'
 
        # behaviour always happens can be sorted out
 
        self.alter_metadata = kw.pop("alter_metadata", False)
 

	
 
        self.meta = kw.pop("metadata", None)
 
        self.engine = kw.pop("engine", None)
 

	
 
        # Things are initialized differently depending on how many column
 
        # parameters are given. Figure out how many and call the appropriate
 
        # method.
 
        if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column):
 
            # At least one column specified
 
            if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
 
                # Two columns specified
 
                diffs = self.compare_2_columns(*p, **kw)
 
            else:
 
                # Exactly one column specified
 
                diffs = self.compare_1_column(*p, **kw)
 
        else:
 
            # Zero columns specified
 
            if not len(p) or not isinstance(p[0], basestring):
 
                raise ValueError("First argument must be column name")
 
            diffs = self.compare_parameters(*p, **kw)
 

	
 
        self.apply_diffs(diffs)
 

	
 
    def __repr__(self):
 
        return '<ColumnDelta altermetadata=%r, %s>' % (
 
            self.alter_metadata,
 
            super(ColumnDelta, self).__repr__()
 
            )
 

	
 
    def __getitem__(self, key):
 
        if key not in self.keys():
 
            raise KeyError("No such diff key, available: %s" % self.diffs )
 
        return getattr(self.result_column, key)
 

	
 
    def __setitem__(self, key, value):
 
        if key not in self.keys():
 
            raise KeyError("No such diff key, available: %s" % self.diffs )
 
        setattr(self.result_column, key, value)
 

	
 
    def __delitem__(self, key):
 
        raise NotImplementedError
 

	
 
    def keys(self):
 
        return self.diffs.keys()
 

	
 
    def compare_parameters(self, current_name, *p, **k):
 
        """Compares Column objects with reflection"""
 
        self.table = k.pop('table')
 
        self.result_column = self._table.c.get(current_name)
 
        if len(p):
 
            k = self._extract_parameters(p, k, self.result_column)
 
        return k
 

	
 
    def compare_1_column(self, col, *p, **k):
 
        """Compares one Column object"""
 
        self.table = k.pop('table', None)
 
        if self.table is None:
 
            self.table = col.table
 
        self.result_column = col
 
        if len(p):
 
            k = self._extract_parameters(p, k, self.result_column)
 
        return k
 

	
 
    def compare_2_columns(self, old_col, new_col, *p, **k):
 
        """Compares two Column objects"""
 
        self.process_column(new_col)
 
        self.table = k.pop('table', None)
 
        # we cannot use bool() on table in SA06
 
        if self.table is None:
 
            self.table = old_col.table
 
        if self.table is None:
 
            new_col.table
 
        self.result_column = old_col
 

	
 
        # set differences
 
        # leave out some stuff for later comp
 
        for key in (set(self.diff_keys) - set(('type',))):
 
            val = getattr(new_col, key, None)
 
            if getattr(self.result_column, key, None) != val:
 
                k.setdefault(key, val)
 

	
 
        # inspect types
 
        if not self.are_column_types_eq(self.result_column.type, new_col.type):
 
            k.setdefault('type', new_col.type)
 

	
 
        if len(p):
 
            k = self._extract_parameters(p, k, self.result_column)
 
        return k
 

	
 
    def apply_diffs(self, diffs):
 
        """Populate dict and column object with new values"""
 
        self.diffs = diffs
 
        for key in self.diff_keys:
 
            if key in diffs:
 
                setattr(self.result_column, key, diffs[key])
 

	
 
        self.process_column(self.result_column)
 

	
 
        # create an instance of class type if not yet
 
        if 'type' in diffs and callable(self.result_column.type):
 
            self.result_column.type = self.result_column.type()
 

	
 
        # add column to the table
 
        if self.table is not None and self.alter_metadata:
 
            self.result_column.add_to_table(self.table)
 

	
 
    def are_column_types_eq(self, old_type, new_type):
 
        """Compares two types to be equal"""
 
        ret = old_type.__class__ == new_type.__class__
 

	
 
        # String length is a special case
 
        if ret and isinstance(new_type, sqlalchemy.types.String):
 
            ret = (getattr(old_type, 'length', None) == \
 
                       getattr(new_type, 'length', None))
 
        return ret
 

	
 
    def _extract_parameters(self, p, k, column):
 
        """Extracts data from p and modifies diffs"""
 
        p = list(p)
 
        while len(p):
 
            if isinstance(p[0], basestring):
 
                k.setdefault('name', p.pop(0))
 
            elif isinstance(p[0], sqlalchemy.types.AbstractType):
 
                k.setdefault('type', p.pop(0))
 
            elif callable(p[0]):
 
                p[0] = p[0]()
 
            else:
 
                break
 

	
 
        if len(p):
 
            new_col = column.copy_fixed()
 
            new_col._init_items(*p)
 
            k = self.compare_2_columns(column, new_col, **k)
 
        return k
 

	
 
    def process_column(self, column):
 
        """Processes default values for column"""
 
        # XXX: this is a snippet from SA processing of positional parameters
 
        toinit = list()
 

	
 
        if column.server_default is not None:
 
            if isinstance(column.server_default, sqlalchemy.FetchedValue):
 
                toinit.append(column.server_default)
 
            else:
 
                toinit.append(sqlalchemy.DefaultClause(column.server_default))
 
        if column.server_onupdate is not None:
 
            if isinstance(column.server_onupdate, FetchedValue):
 
                toinit.append(column.server_default)
 
            else:
 
                toinit.append(sqlalchemy.DefaultClause(column.server_onupdate,
 
                                            for_update=True))
 
        if toinit:
 
            column._init_items(*toinit)
 

	
 
    def _get_table(self):
 
        return getattr(self, '_table', None)
 

	
 
    def _set_table(self, table):
 
        if isinstance(table, basestring):
 
            if self.alter_metadata:
 
                if not self.meta:
 
                    raise ValueError("metadata must be specified for table"
 
                        " reflection when using alter_metadata")
 
                meta = self.meta
 
                if self.engine:
 
                    meta.bind = self.engine
 
            else:
 
                if not self.engine and not self.meta:
 
                    raise ValueError("engine or metadata must be specified"
 
                        " to reflect tables")
 
                if not self.engine:
 
                    self.engine = self.meta.bind
 
                meta = sqlalchemy.MetaData(bind=self.engine)
 
            self._table = sqlalchemy.Table(table, meta, autoload=True)
 
        elif isinstance(table, sqlalchemy.Table):
 
            self._table = table
 
            if not self.alter_metadata:
 
                self._table.meta = sqlalchemy.MetaData(bind=self._table.bind)
 
    def _get_result_column(self):
 
        return getattr(self, '_result_column', None)
 

	
 
    def _set_result_column(self, column):
 
        """Set Column to Table based on alter_metadata evaluation."""
 
        self.process_column(column)
 
        if not hasattr(self, 'current_name'):
 
            self.current_name = column.name
 
        if self.alter_metadata:
 
            self._result_column = column
 
        else:
 
            self._result_column = column.copy_fixed()
 

	
 
    table = property(_get_table, _set_table)
 
    result_column = property(_get_result_column, _set_result_column)
 

	
 

	
 
class ChangesetTable(object):
 
    """Changeset extensions to SQLAlchemy tables."""
 

	
 
    def create_column(self, column, *p, **kw):
 
        """Creates a column.
 

	
 
        The column parameter may be a column definition or the name of
 
        a column in this table.
 

	
 
        API to :meth:`ChangesetColumn.create`
 

	
 
        :param column: Column to be created
 
        :type column: Column instance or string
 
        """
 
        if not isinstance(column, sqlalchemy.Column):
 
            # It's a column name
 
            column = getattr(self.c, str(column))
 
        column.create(table=self, *p, **kw)
 

	
 
    def drop_column(self, column, *p, **kw):
 
        """Drop a column, given its name or definition.
 

	
 
        API to :meth:`ChangesetColumn.drop`
 

	
 
        :param column: Column to be dropped
 
        :type column: Column instance or string
 
        """
 
        if not isinstance(column, sqlalchemy.Column):
 
            # It's a column name
 
            try:
 
                column = getattr(self.c, str(column))
 
            except AttributeError:
 
                # That column isn't part of the table. We don't need
 
                # its entire definition to drop the column, just its
 
                # name, so create a dummy column with the same name.
 
                column = sqlalchemy.Column(str(column), sqlalchemy.Integer())
 
        column.drop(table=self, *p, **kw)
 

	
 
    def rename(self, name, connection=None, **kwargs):
 
        """Rename this table.
 

	
 
        :param name: New name of the table.
 
        :type name: string
 
        :param connection: reuse connection instead of creating new one.
 
        :type connection: :class:`sqlalchemy.engine.base.Connection` instance
 
        """
 
        engine = self.bind
 
        self.new_name = name
 
        visitorcallable = get_engine_visitor(engine, 'schemachanger')
 
        run_single_visitor(engine, visitorcallable, self, connection, **kwargs)
 

	
 
        # Fix metadata registration
 
        self.name = name
 
        self.deregister()
 
        self._set_parent(self.metadata)
 

	
0 comments (0 inline, 0 general)