- 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:
parent
cc82a1ad12
commit
7eafe744c2
3
TODO
3
TODO
@ -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
|
||||
|
17
docs/api.rst
17
docs/api.rst
@ -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:
|
||||
|
@ -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**:
|
||||
|
@ -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()
|
||||
|
@ -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']
|
||||
|
@ -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
|
||||
-------------
|
||||
|
||||
|
@ -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, )
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user