- refactor migrate.changeset;

- visitors are refactored to be more unified
- constraint module is refactored, CheckConstraint is added
- documentation is partialy updated, dialect support table is added (unfinished)
- test_constraint was updated
NOTE: oracle and mysql were not tested, *may be broken*
This commit is contained in:
iElectric 2009-06-16 15:17:33 +00:00
parent cc82a1ad12
commit 7eafe744c2
15 changed files with 509 additions and 287 deletions

3
TODO
View File

@ -12,3 +12,6 @@ make_update_script_for_model:
- refactor test_shell to test_api and use TestScript for cmd line testing
- controlledschema.drop() drops whole migrate table, maybe there are some other repositories bound to it!
- document sqlite hacks (unique index for pk constraint)
- document constraints usage, document all ways then can be used, document cascade,table,columns options

View File

@ -10,6 +10,7 @@ Module :mod:`ansisql <migrate.changeset.ansisql>`
.. automodule:: migrate.changeset.ansisql
:members:
:member-order: groupwise
:synopsis: Standard SQL implementation for altering database schemas
Module :mod:`constraint <migrate.changeset.constraint>`
@ -17,6 +18,8 @@ Module :mod:`constraint <migrate.changeset.constraint>`
.. automodule:: migrate.changeset.constraint
:members:
:show-inheritance:
:member-order: groupwise
:synopsis: Standalone schema constraint objects
Module :mod:`databases <migrate.changeset.databases>`
@ -26,20 +29,28 @@ Module :mod:`databases <migrate.changeset.databases>`
:members:
:synopsis: Database specific changeset implementations
.. _mysql-d:
Module :mod:`mysql <migrate.changeset.databases.mysql>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: migrate.changeset.databases.mysql
:members:
:synopsis: MySQL database specific changeset implementations
.. _oracle-d:
Module :mod:`oracle <migrate.changeset.databases.oracle>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: migrate.changeset.databases.oracle
:members:
:synopsis: Oracle database specific changeset implementations
.. _postgres-d:
Module :mod:`postgres <migrate.changeset.databases.postgres>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -47,8 +58,10 @@ Module :mod:`postgres <migrate.changeset.databases.postgres>`
:members:
:synopsis: PostgreSQL database specific changeset implementations
Module :mod:`sqlite <migrate.changeset.databases.slite>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _sqlite-d:
Module :mod:`sqlite <migrate.changeset.databases.sqlite>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: migrate.changeset.databases.sqlite
:members:

View File

@ -1,6 +1,13 @@
0.5.5
-----
- code coverage is up to 99%
- Constraint classes have cascade=True keyword argument to issue CASCADE drop where supported
- added UniqueConstraint/CheckConstraint and corresponding create/drop methods
- partial refactoring of changeset package
- majoy update to documentation
- dialect support table was added to documentation
.. _backwards-055:
**Backward incompatible changes**:

View File

@ -1,4 +1,5 @@
.. _changeset-system:
.. highlight:: python
******************
Database changeset
@ -73,6 +74,7 @@ Rename a table::
table.rename('newtablename')
.. _`table create/drop`: http://www.sqlalchemy.org/docs/05/metadata.html#creating-and-dropping-database-tables
.. currentmodule:: migrate.changeset.constraint
Index
=====
@ -88,28 +90,86 @@ Rename an index, given an SQLAlchemy ``Index`` object::
Constraint
==========
SQLAlchemy supports creating/dropping constraints at the same time a table is created/dropped. SQLAlchemy Migrate adds support for creating/dropping primary/foreign key constraints independently.
SQLAlchemy supports creating/dropping constraints at the same time a table is created/dropped. SQLAlchemy Migrate adds support for creating/dropping :class:`PrimaryKeyConstraint`/:class:`ForeignKeyConstraint`/:class:`CheckConstraint`/:class:`UniqueConstraint` constraints independently. (as ALTER TABLE statements).
The following rundowns are true for all constraints classes:
1. Make sure you do ``from migrate.changeset import *`` after SQLAlchemy imports since `migrate` does not patch SA's Constraints.
2. You can also use Constraints as in SQLAlchemy. In this case passing table argument explicitly is required::
cons = PrimaryKeyConstraint('id', 'num', table=self.table)
# Create the constraint
cons.create()
# Drop the constraint
cons.drop()
or you can pass column objects (and table argument can be left out).
3. Some dialects support CASCADE option when dropping constraints::
cons = PrimaryKeyConstraint(col1, col2)
# Create the constraint
cons.create()
# Drop the constraint
cons.drop(cascade=True)
.. note::
SQLAlchemy Migrate will try to guess the name of the constraints for databases, but if it's something other than the default, you'll need to give its name. Best practice is to always name your constraints. Note that Oracle requires that you state the name of the constraint to be created/dropped.
Examples
---------
Primary key constraints::
from migrate.changeset import *
cons = PrimaryKeyConstraint(col1, col2)
# Create the constraint
cons.create()
# Drop the constraint
cons.drop()
Note that Oracle requires that you state the name of the primary key constraint to be created/dropped. SQLAlchemy Migrate will try to guess the name of the PK constraint for other databases, but if it's something other than the default, you'll need to give its name::
PrimaryKeyConstraint(col1, col2, name='my_pk_constraint')
Foreign key constraints::
from migrate.changeset import *
cons = ForeignKeyConstraint([table.c.fkey], [othertable.c.id])
# Create the constraint
cons.create()
# Drop the constraint
cons.drop()
Names are specified just as with primary key constraints::
ForeignKeyConstraint([table.c.fkey], [othertable.c.id], name='my_fk_constraint')
Check constraints::
from migrate.changeset import *
cons = CheckConstraint('id > 3', columns=[table.c.id])
# Create the constraint
cons.create()
# Drop the constraint
cons.drop()
Unique constraints::
from migrate.changeset import *
cons = UniqueConstraint('id', 'age', table=self.table)
# Create the constraint
cons.create()
# Drop the constraint
cons.drop()

View File

@ -28,7 +28,10 @@ sys.path.append(os.path.dirname(os.path.abspath('.')))
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc']
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
# link to sqlalchemy docs
intersphinx_mapping = {'http://www.sqlalchemy.org/docs/05/': None}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

View File

@ -31,13 +31,52 @@
Version **0.5.5** breaks backward compatability, please read :ref:`changelog <backwards-055>` for more info.
Download and Development of SQLAlchemy Migrate
----------------------------------------------
Download and Development
------------------------
.. toctree::
download
Dialect support
----------------------------------
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| Operation / Dialect | :ref:`sqlite <sqlite-d>` | :ref:`postgres <postgres-d>` | :ref:`mysql <mysql-d>` | :ref:`oracle <oracle-d>` | firebird | mssql |
| | | | | | | |
+==========================+==========================+==============================+========================+===========================+==========+=======+
| ALTER TABLE | yes | yes | | | | |
| RENAME TABLE | | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| ALTER TABLE | yes | yes | | | | |
| RENAME COLUMN | (workaround) [#1]_ | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| ALTER TABLE | yes | yes | | | | |
| DROP COLUMN | (workaround) [#1]_ | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| ALTER TABLE | yes | yes | | | | |
| ADD COLUMN | (with limitations) [#2]_ | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| ALTER TABLE | no | yes | | | | |
| ADD CONSTRAINT | | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| ALTER TABLE | no | yes | | | | |
| DROP CONSTRAINT | | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| ALTER TABLE | no | yes | | | | |
| ALTER COLUMN | | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| RENAME INDEX | no | yes | | | | |
| | | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
.. [#1] Table is renamed to temporary table, new table is created followed by INSERT statements.
.. [#2] Visit http://www.sqlite.org/lang_altertable.html for more information.
Documentation
-------------

View File

@ -4,5 +4,11 @@
.. [#] SQL Data Definition Language
"""
import sqlalchemy
from migrate.changeset.schema import *
from migrate.changeset.constraint import *
sqlalchemy.schema.Table.__bases__ += (ChangesetTable, )
sqlalchemy.schema.Column.__bases__ += (ChangesetColumn, )
sqlalchemy.schema.Index.__bases__ += (ChangesetIndex, )

View File

@ -5,10 +5,15 @@
things that just happen to work with multiple databases.
"""
import sqlalchemy as sa
from sqlalchemy.engine.base import Connection, Dialect
from sqlalchemy.sql.compiler import SchemaGenerator
from sqlalchemy.schema import ForeignKeyConstraint
from migrate.changeset import constraint, exceptions
from sqlalchemy.engine.default import DefaultDialect
from sqlalchemy.sql.compiler import SchemaGenerator, SchemaDropper
from sqlalchemy.schema import (ForeignKeyConstraint,
PrimaryKeyConstraint,
CheckConstraint,
UniqueConstraint)
from migrate.changeset import exceptions, constraint
SchemaIterator = sa.engine.SchemaIterator
@ -78,6 +83,14 @@ class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator):
self.append(colspec)
self.execute()
# add in foreign keys
if column.foreign_keys:
self.visit_alter_foriegn_keys(column)
def visit_alter_foriegn_keys(self, column):
for fk in column.foreign_keys:
self.define_foreign_key(fk.constraint)
def visit_table(self, table):
"""Default table visitor, does nothing.
@ -87,7 +100,8 @@ class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator):
pass
class ANSIColumnDropper(AlterTableVisitor):
class ANSIColumnDropper(AlterTableVisitor, SchemaDropper):
"""Extends ANSI SQL dropper for column dropping (``ALTER TABLE
DROP COLUMN``).
"""
@ -118,24 +132,23 @@ class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator):
name. NONE means the name is unchanged.
"""
def visit_table(self, param):
def visit_table(self, table):
"""Rename a table. Other ops aren't supported."""
table, newname = param
self.start_alter_table(table)
self.append("RENAME TO %s" % self.preparer.quote(newname, table.quote))
self.append("RENAME TO %s" % self.preparer.quote(table.new_name, table.quote))
self.execute()
def visit_index(self, param):
def visit_index(self, index):
"""Rename an index"""
index, newname = param
self.append("ALTER INDEX %s RENAME TO %s" %
(self.preparer.quote(self._validate_identifier(index.name, True), index.quote),
self.preparer.quote(self._validate_identifier(newname, True) , index.quote)))
self.preparer.quote(self._validate_identifier(index.new_name, True) , index.quote)))
self.execute()
def visit_column(self, delta):
def visit_column(self, column):
"""Rename/change a column."""
# ALTER COLUMN is implemented as several ALTER statements
delta = column.delta
keys = delta.keys()
if 'type' in keys:
self._run_subvisit(delta, self._visit_column_type)
@ -246,99 +259,73 @@ class ANSIConstraintCommon(AlterTableVisitor):
ret = cons.name = cons.autoname()
return self.preparer.quote(ret, cons.quote)
def visit_migrate_primary_key_constraint(self, *p, **k):
self._visit_constraint(*p, **k)
class ANSIConstraintGenerator(ANSIConstraintCommon):
def visit_migrate_foreign_key_constraint(self, *p, **k):
self._visit_constraint(*p, **k)
def visit_migrate_check_constraint(self, *p, **k):
self._visit_constraint(*p, **k)
def visit_migrate_unique_constraint(self, *p, **k):
self._visit_constraint(*p, **k)
class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator):
def get_constraint_specification(self, cons, **kwargs):
if isinstance(cons, constraint.PrimaryKeyConstraint):
col_names = ', '.join([self.preparer.format_column(col) for col in cons.columns])
ret = "PRIMARY KEY (%s)" % col_names
if cons.name:
# Named constraint
ret = ("CONSTRAINT %s " % self.preparer.format_constraint(cons)) + ret
elif isinstance(cons, constraint.ForeignKeyConstraint):
params = dict(
columns = ', '.join(map(self.preparer.format_column, cons.columns)),
reftable = self.preparer.format_table(cons.reftable),
referenced = ', '.join(map(self.preparer.format_column, cons.referenced)),
name = self.get_constraint_name(cons),
)
ret = "CONSTRAINT %(name)s FOREIGN KEY (%(columns)s) "\
"REFERENCES %(reftable)s (%(referenced)s)" % params
if cons.onupdate:
ret = ret + " ON UPDATE %s" % cons.onupdate
if cons.ondelete:
ret = ret + " ON DELETE %s" % cons.ondelete
elif isinstance(cons, constraint.CheckConstraint):
ret = "CHECK (%s)" % cons.sqltext
"""Constaint SQL generators.
We cannot use SA visitors because they append comma.
"""
if isinstance(cons, PrimaryKeyConstraint):
if cons.name is not None:
self.append("CONSTRAINT %s " % self.preparer.format_constraint(cons))
self.append("PRIMARY KEY ")
self.append("(%s)" % ', '.join(self.preparer.quote(c.name, c.quote)
for c in cons))
self.define_constraint_deferrability(cons)
elif isinstance(cons, ForeignKeyConstraint):
self.define_foreign_key(cons)
elif isinstance(cons, CheckConstraint):
if cons.name is not None:
self.append("CONSTRAINT %s " %
self.preparer.format_constraint(cons))
self.append(" CHECK (%s)" % cons.sqltext)
self.define_constraint_deferrability(cons)
elif isinstance(cons, UniqueConstraint):
if cons.name is not None:
self.append("CONSTRAINT %s " %
self.preparer.format_constraint(cons))
self.append(" UNIQUE (%s)" % \
(', '.join(self.preparer.quote(c.name, c.quote) for c in cons)))
self.define_constraint_deferrability(cons)
else:
raise exceptions.InvalidConstraintError(cons)
return ret
def _visit_constraint(self, constraint):
table = self.start_alter_table(constraint)
constraint.name = self.get_constraint_name(constraint)
self.append("ADD ")
spec = self.get_constraint_specification(constraint)
self.append(spec)
self.get_constraint_specification(constraint)
self.execute()
def visit_migrate_primary_key_constraint(self, *p, **k):
return self._visit_constraint(*p, **k)
def visit_migrate_foreign_key_constraint(self, *p, **k):
return self._visit_constraint(*p, **k)
def visit_migrate_check_constraint(self, *p, **k):
return self._visit_constraint(*p, **k)
class ANSIConstraintDropper(ANSIConstraintCommon):
class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
def _visit_constraint(self, constraint):
self.start_alter_table(constraint)
self.append("DROP CONSTRAINT ")
self.append(self.get_constraint_name(constraint))
if constraint.cascade:
self.append(" CASCADE")
self.execute()
def visit_migrate_primary_key_constraint(self, *p, **k):
return self._visit_constraint(*p, **k)
def visit_migrate_foreign_key_constraint(self, *p, **k):
return self._visit_constraint(*p, **k)
def visit_migrate_check_constraint(self, *p, **k):
return self._visit_constraint(*p, **k)
class ANSIFKGenerator(AlterTableVisitor, SchemaGenerator):
"""Extends ansisql generator for column creation (alter table add col)"""
def __init__(self, *args, **kwargs):
self.fk = kwargs.pop('fk', None)
super(ANSIFKGenerator, self).__init__(*args, **kwargs)
def visit_column(self, column):
"""Create foreign keys for a column (table already exists); #32"""
if self.fk:
self.add_foreignkey(self.fk.constraint)
if self.buffer.getvalue() != '':
self.execute()
def visit_table(self, table):
pass
class ANSIDialect(object):
class ANSIDialect(DefaultDialect):
columngenerator = ANSIColumnGenerator
columndropper = ANSIColumnDropper
schemachanger = ANSISchemaChanger
columnfkgenerator = ANSIFKGenerator
@classmethod
def visitor(self, name):
return getattr(self, name)
def reflectconstraints(self, connection, table_name):
raise NotImplementedError()
constraintgenerator = ANSIConstraintGenerator
constraintdropper = ANSIConstraintDropper

View File

@ -4,6 +4,8 @@
import sqlalchemy
from sqlalchemy import schema
from migrate.changeset.exceptions import *
class ConstraintChangeset(object):
"""Base class for Constraint classes."""
@ -24,55 +26,50 @@ class ConstraintChangeset(object):
colnames.append(col)
return colnames, table
def create(self, *args, **kwargs):
def __do_imports(self, visitor_name, *a, **kw):
engine = kw.pop('engine', self.table.bind)
from migrate.changeset.databases.visitor import (get_engine_visitor,
run_single_visitor)
visitorcallable = get_engine_visitor(engine, visitor_name)
run_single_visitor(engine, visitorcallable, self, *a, **kw)
def create(self, *a, **kw):
"""Create the constraint in the database.
:param engine: the database engine to use. If this is \
:keyword:`None` the instance's engine will be used
:type engine: :class:`sqlalchemy.engine.base.Engine`
"""
from migrate.changeset.databases.visitor import get_engine_visitor
visitorcallable = get_engine_visitor(self.table.bind,
'constraintgenerator')
_engine_run_visitor(self.table.bind, visitorcallable, self)
self.__do_imports('constraintgenerator', *a, **kw)
def drop(self, *args, **kwargs):
def drop(self, *a, **kw):
"""Drop the constraint from the database.
:param engine: the database engine to use. If this is
:keyword:`None` the instance's engine will be used
:param cascade: Issue CASCADE drop if database supports it
:type engine: :class:`sqlalchemy.engine.base.Engine`
:type cascade: bool
:returns: Instance with cleared columns
"""
from migrate.changeset.databases.visitor import get_engine_visitor
visitorcallable = get_engine_visitor(self.table.bind,
'constraintdropper')
_engine_run_visitor(self.table.bind, visitorcallable, self)
self.cascade = kw.pop('cascade', False)
self.__do_imports('constraintdropper', *a, **kw)
self.columns.clear()
return self
def accept_schema_visitor(self, visitor, *p, **k):
"""Call the visitor only if it defines the given function"""
return getattr(visitor, self._func)(self)
def autoname(self):
"""Automatically generate a name for the constraint instance.
Subclasses must implement this method.
"""
def _engine_run_visitor(engine, visitorcallable, element, **kwargs):
conn = engine.connect()
try:
element.accept_schema_visitor(visitorcallable(conn))
finally:
conn.close()
class PrimaryKeyConstraint(ConstraintChangeset, schema.PrimaryKeyConstraint):
"""Primary key constraint class."""
"""Construct PrimaryKeyConstraint
Migrate's additional parameters:
_func = 'visit_migrate_primary_key_constraint'
:param cols: Columns in constraint.
:param table: If columns are passed as strings, this kw is required
:type table: Table instance
:type cols: strings or Column instances
"""
__visit_name__ = 'migrate_primary_key_constraint'
def __init__(self, *cols, **kwargs):
colnames, table = self._normalize_columns(cols)
@ -81,23 +78,34 @@ class PrimaryKeyConstraint(ConstraintChangeset, schema.PrimaryKeyConstraint):
if table is not None:
self._set_parent(table)
def autoname(self):
"""Mimic the database's automatic constraint names"""
return "%s_pkey" % self.table.name
class ForeignKeyConstraint(ConstraintChangeset, schema.ForeignKeyConstraint):
"""Foreign key constraint class."""
"""Construct ForeignKeyConstraint
Migrate's additional parameters:
_func = 'visit_migrate_foreign_key_constraint'
:param columns: Columns in constraint
:param refcolumns: Columns that this FK reffers to in another table.
:param table: If columns are passed as strings, this kw is required
:type table: Table instance
:type columns: list of strings or Column instances
:type refcolumns: list of strings or Column instances
"""
def __init__(self, columns, refcolumns, *p, **k):
__visit_name__ = 'migrate_foreign_key_constraint'
def __init__(self, columns, refcolumns, *args, **kwargs):
colnames, table = self._normalize_columns(columns)
table = k.pop('table', table)
table = kwargs.pop('table', table)
refcolnames, reftable = self._normalize_columns(refcolumns,
table_name=True)
super(ForeignKeyConstraint, self).__init__(colnames, refcolnames, *p,
**k)
super(ForeignKeyConstraint, self).__init__(colnames, refcolnames, *args,
**kwargs)
if table is not None:
self._set_parent(table)
@ -118,20 +126,60 @@ class ForeignKeyConstraint(ConstraintChangeset, schema.ForeignKeyConstraint):
class CheckConstraint(ConstraintChangeset, schema.CheckConstraint):
"""Check constraint class."""
"""Construct CheckConstraint
_func = 'visit_migrate_check_constraint'
Migrate's additional parameters:
:param sqltext: Plain SQL text to check condition
:param columns: If not name is applied, you must supply this kw\
to autoname constraint
:param table: If columns are passed as strings, this kw is required
:type table: Table instance
:type columns: list of Columns instances
:type sqltext: string
"""
__visit_name__ = 'migrate_check_constraint'
def __init__(self, sqltext, *args, **kwargs):
cols = kwargs.pop('columns')
cols = kwargs.pop('columns', False)
if not cols and not kwargs.get('name', False):
raise InvalidConstraintError('You must either set "name"'
'parameter or "columns" to autogenarate it.')
colnames, table = self._normalize_columns(cols)
table = kwargs.pop('table', table)
ConstraintChangeset.__init__(self, *args, **kwargs)
schema.CheckConstraint.__init__(self, sqltext, *args, **kwargs)
if table is not None:
self.table = table
self._set_parent(table)
self.colnames = colnames
def autoname(self):
return "%(table)s_%(cols)s_check" % \
dict(table=self.table.name, cols="_".join(self.colnames))
class UniqueConstraint(ConstraintChangeset, schema.UniqueConstraint):
"""Construct UniqueConstraint
Migrate's additional parameters:
:param cols: Columns in constraint.
:param table: If columns are passed as strings, this kw is required
:type table: Table instance
:type cols: strings or Column instances
"""
__visit_name__ = 'migrate_unique_constraint'
def __init__(self, *cols, **kwargs):
self.colnames, table = self._normalize_columns(cols)
table = kwargs.pop('table', table)
super(UniqueConstraint, self).__init__(*self.colnames, **kwargs)
if table is not None:
self._set_parent(table)
def autoname(self):
"""Mimic the database's automatic constraint names"""
return "%s_%s_key" % (self.table.name, self.colnames[0])

View File

@ -53,18 +53,11 @@ class MySQLSchemaChanger(MySQLSchemaGenerator, ansisql.ANSISchemaChanger):
# If MySQL can do this, I can't find how
raise exceptions.NotSupportedError("MySQL cannot rename indexes")
class MySQLConstraintGenerator(ansisql.ANSIConstraintGenerator):
pass
class MySQLConstraintDropper(ansisql.ANSIConstraintDropper):
#def visit_constraint(self,constraint):
# if isinstance(constraint,sqlalchemy.schema.PrimaryKeyConstraint):
# return self._visit_constraint_pk(constraint)
# elif isinstance(constraint,sqlalchemy.schema.ForeignKeyConstraint):
# return self._visit_constraint_fk(constraint)
# return super(MySQLConstraintDropper,self).visit_constraint(constraint)
def visit_migrate_primary_key_constraint(self, constraint):
self.start_alter_table(constraint)
@ -77,7 +70,6 @@ class MySQLConstraintDropper(ansisql.ANSIConstraintDropper):
self.append(self.preparer.format_constraint(constraint))
self.execute()
class MySQLDialect(ansisql.ANSIDialect):
columngenerator = MySQLColumnGenerator
columndropper = MySQLColumnDropper

View File

@ -3,27 +3,33 @@
.. _`SQLite`: http://www.sqlite.org/
"""
from migrate.changeset import ansisql, constraint, exceptions
from migrate.changeset import ansisql, exceptions, constraint
from sqlalchemy.databases import sqlite as sa_base
from sqlalchemy import Table, MetaData
#import sqlalchemy as sa
SQLiteSchemaGenerator = sa_base.SQLiteSchemaGenerator
class SQLiteCommon(object):
class SQLiteHelper(object):
def _not_supported(self, op):
raise exceptions.NotSupportedError("SQLite does not support "
"%s; see http://www.sqlite.org/lang_altertable.html" % op)
def visit_column(self, param):
class SQLiteHelper(SQLiteCommon):
def visit_column(self, column):
try:
table = self._to_table(param.table)
table = self._to_table(column.table)
except:
table = self._to_table(param)
table = self._to_table(column)
raise
table_name = self.preparer.format_table(table)
self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name)
self.execute()
insertion_string = self._modify_table(table, param)
insertion_string = self._modify_table(table, column)
table.create()
self.append(insertion_string % {'table_name': table_name})
@ -32,12 +38,17 @@ class SQLiteHelper(object):
self.execute()
class SQLiteColumnGenerator(SQLiteSchemaGenerator,
class SQLiteColumnGenerator(SQLiteSchemaGenerator, SQLiteCommon,
ansisql.ANSIColumnGenerator):
pass
"""SQLite ColumnGenerator"""
def visit_alter_foriegn_keys(self, column):
"""Does not support ALTER TABLE ADD FOREIGN KEY"""
self._not_supported("ALTER TABLE ADD CONSTRAINT")
class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper):
"""SQLite ColumnDropper"""
def _modify_table(self, table, column):
del table.columns[column.name]
@ -47,18 +58,17 @@ class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper):
class SQLiteSchemaChanger(SQLiteHelper, ansisql.ANSISchemaChanger):
"""SQLite SchemaChanger"""
def _not_supported(self, op):
raise exceptions.NotSupportedError("SQLite does not support "
"%s; see http://www.sqlite.org/lang_altertable.html" % op)
def _modify_table(self, table, delta):
def _modify_table(self, table, column):
delta = column.delta
column = table.columns[delta.current_name]
for k, v in delta.items():
setattr(column, k, v)
return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
def visit_index(self, param):
def visit_index(self, index):
"""Does not support ALTER INDEX"""
self._not_supported('ALTER INDEX')
@ -74,17 +84,6 @@ class SQLiteConstraintGenerator(ansisql.ANSIConstraintGenerator):
self.execute()
class SQLiteFKGenerator(SQLiteSchemaChanger, ansisql.ANSIFKGenerator):
def visit_column(self, column):
"""Create foreign keys for a column (table already exists); #32"""
if self.fk:
self._not_supported("ALTER TABLE ADD FOREIGN KEY")
if self.buffer.getvalue() != '':
self.execute()
class SQLiteConstraintDropper(ansisql.ANSIColumnDropper, ansisql.ANSIConstraintCommon):
def visit_migrate_primary_key_constraint(self, constraint):
@ -94,6 +93,7 @@ class SQLiteConstraintDropper(ansisql.ANSIColumnDropper, ansisql.ANSIConstraintC
self.append(msg)
self.execute()
# TODO: add not_supported tags for constraint dropper/generator
class SQLiteDialect(ansisql.ANSIDialect):
columngenerator = SQLiteColumnGenerator
@ -101,4 +101,3 @@ class SQLiteDialect(ansisql.ANSIDialect):
schemachanger = SQLiteSchemaChanger
constraintgenerator = SQLiteConstraintGenerator
constraintdropper = SQLiteConstraintDropper
columnfkgenerator = SQLiteFKGenerator

View File

@ -42,9 +42,18 @@ def get_dialect_visitor(sa_dialect, name):
# map sa dialect to migrate dialect and return visitor
sa_dialect_cls = sa_dialect.__class__
migrate_dialect_cls = dialects[sa_dialect_cls]
visitor = migrate_dialect_cls.visitor(name)
visitor = getattr(migrate_dialect_cls, name)
# bind preparer
visitor.preparer = sa_dialect.preparer(sa_dialect)
return visitor
def run_single_visitor(engine, visitorcallable, element, **kwargs):
"""Runs only one method on the visitor"""
conn = engine.contextual_connect(close_with_result=False)
try:
visitor = visitorcallable(engine.dialect, conn)
getattr(visitor, 'visit_' + element.__visit_name__)(element, **kwargs)
finally:
conn.close()

View File

@ -5,7 +5,9 @@ import re
import sqlalchemy
from migrate.changeset.databases.visitor import get_engine_visitor
from migrate.changeset.databases.visitor import (get_engine_visitor,
run_single_visitor)
from migrate.changeset.exceptions import *
__all__ = [
@ -14,6 +16,9 @@ __all__ = [
'alter_column',
'rename_table',
'rename_index',
'ChangesetTable',
'ChangesetColumn',
'ChangesetIndex',
]
@ -97,7 +102,12 @@ def alter_column(*p, **k):
engine = k['engine']
delta = _ColumnDelta(*p, **k)
visitorcallable = get_engine_visitor(engine, 'schemachanger')
_engine_run_visitor(engine, visitorcallable, delta)
column = sqlalchemy.Column(delta.current_name)
column.delta = delta
column.table = delta.table
engine._run_visitor(visitorcallable, column)
#_engine_run_visitor(engine, visitorcallable, delta)
# Update column
if col is not None:
@ -145,15 +155,6 @@ def _to_index(index, table=None, engine=None):
return ret
def _engine_run_visitor(engine, visitorcallable, element, **kwargs):
conn = engine.connect()
try:
element.accept_schema_visitor(visitorcallable(engine.dialect,
connection=conn))
finally:
conn.close()
def _normalize_table(column, table):
if table is not None:
if table is not column.table:
@ -166,22 +167,6 @@ def _normalize_table(column, table):
return column.table
class _WrapRename(object):
def __init__(self, item, name):
self.item = item
self.name = name
def accept_schema_visitor(self, visitor):
"""Map Class (Table, Index, Column) to visitor function"""
suffix = self.item.__class__.__name__.lower()
funcname = 'visit_%s' % suffix
func = getattr(visitor, funcname)
param = self.item, self.name
return func(param)
class _ColumnDelta(dict):
"""Extracts the differences between two columns/column-parameters"""
@ -330,15 +315,14 @@ class ChangesetTable(object):
Python object
"""
engine = self.bind
self.new_name = name
visitorcallable = get_engine_visitor(engine, 'schemachanger')
param = _WrapRename(self, name)
_engine_run_visitor(engine, visitorcallable, param, *args, **kwargs)
run_single_visitor(engine, visitorcallable, self, *args, **kwargs)
# Fix metadata registration
meta = self.metadata
self.deregister()
self.name = name
self._set_parent(meta)
self.deregister()
self._set_parent(self.metadata)
def _meta_key(self):
return sqlalchemy.schema._get_table_key(self.name, self.schema)
@ -368,6 +352,9 @@ class ChangesetColumn(object):
Column name, type, default, and nullable may be changed
here. Note that for column defaults, only PassiveDefaults are
managed by the database - changing others doesn't make sense.
:param table: Table to be altered
:param engine: Engine to be used
"""
if 'table' not in k:
k['table'] = self.table
@ -386,12 +373,6 @@ class ChangesetColumn(object):
visitorcallable = get_engine_visitor(engine, 'columngenerator')
engine._run_visitor(visitorcallable, self, *args, **kwargs)
# add in foreign keys
if self.foreign_keys:
for fk in self.foreign_keys:
visitorcallable = get_engine_visitor(engine,
'columnfkgenerator')
engine._run_visitor(visitorcallable, self, fk=fk)
return self
def drop(self, table=None, *args, **kwargs):
@ -402,14 +383,15 @@ class ChangesetColumn(object):
table = _normalize_table(self, table)
engine = table.bind
visitorcallable = get_engine_visitor(engine, 'columndropper')
engine._run_visitor(lambda dialect, conn: visitorcallable(conn),
self, *args, **kwargs)
engine._run_visitor(visitorcallable, self, *args, **kwargs)
return self
class ChangesetIndex(object):
"""Changeset extensions to SQLAlchemy Indexes."""
__visit_name__ = 'index'
def rename(self, name, *args, **kwargs):
"""Change the name of an index.
@ -417,15 +399,7 @@ class ChangesetIndex(object):
name.
"""
engine = self.table.bind
self.new_name = name
visitorcallable = get_engine_visitor(engine, 'schemachanger')
param = _WrapRename(self, name)
_engine_run_visitor(engine, visitorcallable, param, *args, **kwargs)
engine._run_visitor(visitorcallable, self, *args, **kwargs)
self.name = name
def _patch():
"""All the 'ugly' operations that patch SQLAlchemy's internals."""
sqlalchemy.schema.Table.__bases__ += (ChangesetTable, )
sqlalchemy.schema.Column.__bases__ += (ChangesetColumn, )
sqlalchemy.schema.Index.__bases__ += (ChangesetIndex, )
_patch()

View File

@ -13,7 +13,7 @@ from migrate.changeset.schema import _ColumnDelta
from test import fixture
# TODO: add sqlite unique constraints (indexes), test quoting
# TODO: test quoting
class TestAddDropColumn(fixture.DB):
level = fixture.DB.CONNECT
@ -25,8 +25,8 @@ class TestAddDropColumn(fixture.DB):
def _setup(self, url):
super(TestAddDropColumn, self)._setup(url)
self.meta.clear()
self.table = Table(self.table_name,self.meta,
Column('id',Integer,primary_key=True),
self.table = Table(self.table_name, self.meta,
Column('id', Integer, primary_key=True),
)
self.meta.bind = self.engine
if self.engine.has_table(self.table.name):
@ -169,7 +169,7 @@ class TestAddDropColumn(fixture.DB):
reftable.create()
def add_func(col):
self.table.append_column(col)
return create_column(col.name,self.table)
return create_column(col.name, self.table)
def drop_func(col):
ret = drop_column(col.name,self.table)
if self.engine.has_table(reftable.name):
@ -180,12 +180,12 @@ class TestAddDropColumn(fixture.DB):
self.run_, add_func, drop_func, Integer,
ForeignKey(reftable.c.id))
else:
return self.run_(add_func,drop_func,Integer,
return self.run_(add_func, drop_func, Integer,
ForeignKey(reftable.c.id))
class TestRename(fixture.DB):
level=fixture.DB.CONNECT
level = fixture.DB.CONNECT
meta = MetaData()
def _setup(self, url):
@ -195,25 +195,25 @@ class TestRename(fixture.DB):
@fixture.usedb()
def test_rename_table(self):
"""Tables can be renamed"""
#self.engine.echo=True
c_name = 'col_1'
name1 = 'name_one'
name2 = 'name_two'
xname1 = 'x'+name1
xname2 = 'x'+name2
self.column = Column(name1,Integer)
xname1 = 'x' + name1
xname2 = 'x' + name2
self.column = Column(c_name, Integer)
self.meta.clear()
self.table = Table(name1,self.meta,self.column)
self.index = Index(xname1,self.column,unique=False)
self.table = Table(name1, self.meta, self.column)
self.index = Index(xname1, self.column, unique=False)
if self.engine.has_table(self.table.name):
self.table.drop()
if self.engine.has_table(name2):
tmp = Table(name2,self.meta,autoload=True)
tmp = Table(name2, self.meta, autoload=True)
tmp.drop()
tmp.deregister()
del tmp
self.table.create()
def assert_table_name(expected,skip_object_check=False):
def assert_table_name(expected, skip_object_check=False):
"""Refresh a table via autoload
SA has changed some since this test was written; we now need to do
meta.clear() upon reloading a table - clear all rather than a
@ -245,18 +245,18 @@ class TestRename(fixture.DB):
try:
# Table renames
assert_table_name(name1)
rename_table(self.table,name2)
rename_table(self.table, name2)
assert_table_name(name2)
self.table.rename(name1)
assert_table_name(name1)
# ..by just the string
rename_table(name1,name2,engine=self.engine)
assert_table_name(name2,True) # object not updated
rename_table(name1, name2, engine=self.engine)
assert_table_name(name2, True) # object not updated
# Index renames
if self.url.startswith('sqlite') or self.url.startswith('mysql'):
self.assertRaises(changeset.exceptions.NotSupportedError,
self.index.rename,xname2)
self.index.rename, xname2)
else:
assert_index_name(xname1)
rename_index(self.index,xname2,engine=self.engine)

View File

@ -3,38 +3,50 @@
from sqlalchemy import *
from sqlalchemy.util import *
from sqlalchemy.exc import *
from migrate.changeset import *
from test import fixture
class TestConstraint(fixture.DB):
level = fixture.DB.CONNECT
class CommonTestConstraint(fixture.DB):
"""helper functions to test constraints.
we just create a fresh new table and make sure everything is
as required.
"""
def _setup(self, url):
super(TestConstraint, self)._setup(url)
super(CommonTestConstraint, self)._setup(url)
self._create_table()
def _teardown(self):
if hasattr(self, 'table') and self.engine.has_table(self.table.name):
self.table.drop()
super(TestConstraint, self)._teardown()
super(CommonTestConstraint, self)._teardown()
def _create_table(self):
self._connect(self.url)
self.meta = MetaData(self.engine)
self.table = Table('mytable', self.meta,
Column('id', Integer),
self.tablename = 'mytable'
self.table = Table(self.tablename, self.meta,
Column('id', Integer, unique=True),
Column('fkey', Integer),
mysql_engine='InnoDB')
if self.engine.has_table(self.table.name):
self.table.drop()
self.table.create()
# make sure we start at zero
self.assertEquals(len(self.table.primary_key), 0)
self.assert_(isinstance(self.table.primary_key,
schema.PrimaryKeyConstraint), self.table.primary_key.__class__)
class TestConstraint(CommonTestConstraint):
level = fixture.DB.CONNECT
def _define_pk(self, *cols):
# Add a pk by creating a PK constraint
pk = PrimaryKeyConstraint(table=self.table, *cols)
@ -46,7 +58,6 @@ class TestConstraint(fixture.DB):
self.refresh_table()
if not self.url.startswith('sqlite'):
self.assertEquals(list(self.table.primary_key), list(cols))
#self.assert_(self.table.primary_key.name is not None)
# Drop the PK constraint
if not self.url.startswith('oracle'):
@ -54,46 +65,34 @@ class TestConstraint(fixture.DB):
pk.name = self.table.primary_key.name
pk.drop()
self.refresh_table()
#self.assertEquals(list(self.table.primary_key),list())
self.assertEquals(len(self.table.primary_key), 0)
self.assert_(isinstance(self.table.primary_key,
schema.PrimaryKeyConstraint),self.table.primary_key.__class__)
self.assert_(isinstance(self.table.primary_key, schema.PrimaryKeyConstraint))
return pk
@fixture.usedb(not_supported='sqlite')
def test_define_fk(self):
"""FK constraints can be defined, created, and dropped"""
# FK target must be unique
pk = PrimaryKeyConstraint(self.table.c.id, table=self.table)
pk = PrimaryKeyConstraint(self.table.c.id, table=self.table, name="pkid")
pk.create()
# Add a FK by creating a FK constraint
self.assertEquals(self.table.c.fkey.foreign_keys._list, [])
fk = ForeignKeyConstraint([self.table.c.fkey],[self.table.c.id], table=self.table)
fk = ForeignKeyConstraint([self.table.c.fkey], [self.table.c.id], name="fk_id_fkey")
self.assert_(self.table.c.fkey.foreign_keys._list is not [])
self.assertEquals(list(fk.columns), [self.table.c.fkey])
self.assertEquals([e.column for e in fk.elements],[self.table.c.id])
self.assertEquals(list(fk.referenced),[self.table.c.id])
self.assertEquals([e.column for e in fk.elements], [self.table.c.id])
self.assertEquals(list(fk.referenced), [self.table.c.id])
if self.url.startswith('mysql'):
# MySQL FKs need an index
index = Index('index_name', self.table.c.fkey)
index.create()
if self.url.startswith('oracle'):
# Oracle constraints need a name
fk.name = 'fgsfds'
print 'drop...'
#self.engine.echo=True
fk.create()
#self.engine.echo=False
print 'dropped'
self.refresh_table()
self.assert_(self.table.c.fkey.foreign_keys._list is not [])
print 'drop...'
#self.engine.echo=True
fk.drop()
#self.engine.echo=False
print 'dropped'
self.refresh_table()
self.assertEquals(self.table.c.fkey.foreign_keys._list, [])
@ -108,40 +107,123 @@ class TestConstraint(fixture.DB):
#self.engine.echo=True
self._define_pk(self.table.c.id, self.table.c.fkey)
@fixture.usedb()
def test_drop_cascade(self):
pk = PrimaryKeyConstraint('id', table=self.table, name="id_pkey")
pk.create()
self.refresh_table()
class TestAutoname(fixture.DB):
# Drop the PK constraint forcing cascade
pk.drop(cascade=True)
class TestAutoname(CommonTestConstraint):
"""Every method tests for a type of constraint wether it can autoname
itself and if you can pass object instance and names to classes.
"""
level = fixture.DB.CONNECT
def _setup(self, url):
super(TestAutoname, self)._setup(url)
self._connect(self.url)
self.meta = MetaData(self.engine)
self.table = Table('mytable',self.meta,
Column('id', Integer),
Column('fkey', String(40)),
)
if self.engine.has_table(self.table.name):
self.table.drop()
self.table.create()
def _teardown(self):
if hasattr(self,'table') and self.engine.has_table(self.table.name):
self.table.drop()
super(TestAutoname, self)._teardown()
@fixture.usedb(not_supported='oracle')
def test_autoname(self):
"""Constraints can guess their name if none is given"""
def test_autoname_pk(self):
"""PrimaryKeyConstraints can guess their name if None is given"""
# Don't supply a name; it should create one
cons = PrimaryKeyConstraint(self.table.c.id)
cons.create()
self.refresh_table()
# TODO: test for index for sqlite
if not self.url.startswith('sqlite'):
self.assertEquals(list(cons.columns),list(self.table.primary_key))
# TODO: test for index for sqlite
self.assertEquals(list(cons.columns), list(self.table.primary_key))
# Remove the name, drop the constraint; it should succeed
cons.name = None
cons.drop()
self.refresh_table()
self.assertEquals(list(), list(self.table.primary_key))
# test string names
cons = PrimaryKeyConstraint('id', table=self.table)
cons.create()
self.refresh_table()
if not self.url.startswith('sqlite'):
# TODO: test for index for sqlite
self.assertEquals(list(cons.columns), list(self.table.primary_key))
cons.name = None
cons.drop()
@fixture.usedb(not_supported=['oracle', 'sqlite'])
def test_autoname_fk(self):
"""ForeignKeyConstraints can guess their name if None is given"""
cons = ForeignKeyConstraint([self.table.c.fkey], [self.table.c.id])
if self.url.startswith('mysql'):
# MySQL FKs need an index
index = Index('index_name', self.table.c.fkey)
index.create()
cons.create()
self.refresh_table()
self.table.c.fkey.foreign_keys[0].column is self.table.c.id
# Remove the name, drop the constraint; it should succeed
cons.name = None
cons.drop()
self.refresh_table()
self.assertEquals(self.table.c.fkey.foreign_keys._list, list())
# test string names
cons = ForeignKeyConstraint(['fkey'], ['%s.id' % self.tablename], table=self.table)
if self.url.startswith('mysql'):
# MySQL FKs need an index
index = Index('index_name', self.table.c.fkey)
index.create()
cons.create()
self.refresh_table()
self.table.c.fkey.foreign_keys[0].column is self.table.c.id
# Remove the name, drop the constraint; it should succeed
cons.name = None
cons.drop()
@fixture.usedb(not_supported=['oracle', 'sqlite'])
def test_autoname_check(self):
"""CheckConstraints can guess their name if None is given"""
cons = CheckConstraint('id > 3', columns=[self.table.c.id])
cons.create()
self.refresh_table()
self.table.insert(values={'id': 4}).execute()
try:
self.table.insert(values={'id': 1}).execute()
except IntegrityError:
pass
else:
self.fail()
# Remove the name, drop the constraint; it should succeed
cons.name = None
cons.drop()
self.refresh_table()
self.table.insert(values={'id': 2}).execute()
self.table.insert(values={'id': 5}).execute()
@fixture.usedb(not_supported=['oracle', 'sqlite'])
def test_autoname_unique(self):
"""UniqueConstraints can guess their name if None is given"""
cons = UniqueConstraint(self.table.c.fkey)
cons.create()
self.refresh_table()
self.table.insert(values={'fkey': 4}).execute()
try:
self.table.insert(values={'fkey': 4}).execute()
except IntegrityError:
pass
else:
self.fail()
# Remove the name, drop the constraint; it should succeed
cons.name = None
cons.drop()
self.refresh_table()
self.table.insert(values={'fkey': 4}).execute()
self.table.insert(values={'fkey': 4}).execute()