updated changeset tests. whole package is finally PEP8. fixed mysql tests&bugs. updated docs where apropriate. changeset test coverage almost at 100%

This commit is contained in:
iElectric 2009-06-20 22:33:03 +00:00
parent 7eafe744c2
commit cde0f9b52d
16 changed files with 580 additions and 406 deletions

View File

@ -1,9 +1,13 @@
0.5.5 0.5.5
----- -----
- code coverage is up to 99% - server_defaults passed to column.create are now issued correctly
- constraints passed to column.create are correctly interpreted (ALTER TABLE ADD CONSTRAINT is issued after ADD COLUMN)
- column.create accepts `primary_key_name`, `unique_name` and `index_name` as string value which is used as contraint name when adding a column
- Constraint classes have cascade=True keyword argument to issue CASCADE drop where supported - Constraint classes have cascade=True keyword argument to issue CASCADE drop where supported
- added UniqueConstraint/CheckConstraint and corresponding create/drop methods - added UniqueConstraint/CheckConstraint and corresponding create/drop methods
- use SQLAlchemy quoting system to avoid name conflicts (for issue 32)
- code coverage is up to 99% with more than 100 tests
- partial refactoring of changeset package - partial refactoring of changeset package
- majoy update to documentation - majoy update to documentation
- dialect support table was added to documentation - dialect support table was added to documentation

View File

@ -31,6 +31,8 @@ Given a standard SQLAlchemy table::
) )
table.create() table.create()
.. _column-create:
Create a column:: Create a column::
col = Column('col1', String) col = Column('col1', String)
@ -39,12 +41,16 @@ Create a column::
# Column is added to table based on its name # Column is added to table based on its name
assert col is table.c.col1 assert col is table.c.col1
Drop a column (Not supported by SQLite_):: .. _column-drop:
Drop a column::
col.drop() col.drop()
Alter a column (Not supported by SQLite_):: .. _column-alter:
Alter a column::
col.alter(name='col2') col.alter(name='col2')
@ -52,17 +58,19 @@ Alter a column (Not supported by SQLite_)::
assert col is table.c.col2 assert col is table.c.col2
# Other properties can be modified as well # Other properties can be modified as well
col.alter(type=String(42), col.alter(type=String(42), default="life, the universe, and everything", nullable=False)
default="life, the universe, and everything",
nullable=False,
)
# Given another column object, col1.alter(col2), col1 will be changed to match col2 # Given another column object, col1.alter(col2), col1 will be changed to match col2
col.alter(Column('col3',String(77),nullable=True)) col.alter(Column('col3', String(77), nullable=True))
assert col.nullable assert col.nullable
assert table.c.col3 is col assert table.c.col3 is col
.. _sqlite: http://www.sqlite.org/lang_altertable.html
.. note::
Since version ``0.5.5`` you can pass primary_key_name, index_name and unique_name to column.create method to issue ALTER TABLE ADD CONSTRAINT after changing the column. Note for multi columns constraints and other advanced configuration, check :ref:`constraint tutorial <constraint-tutorial>`.
.. _table-rename:
Table Table
===== =====
@ -76,6 +84,9 @@ Rename a table::
.. _`table create/drop`: http://www.sqlalchemy.org/docs/05/metadata.html#creating-and-dropping-database-tables .. _`table create/drop`: http://www.sqlalchemy.org/docs/05/metadata.html#creating-and-dropping-database-tables
.. currentmodule:: migrate.changeset.constraint .. currentmodule:: migrate.changeset.constraint
.. _index-rename:
Index Index
===== =====
@ -87,6 +98,9 @@ Rename an index, given an SQLAlchemy ``Index`` object::
.. _`index create/drop`: http://www.sqlalchemy.org/docs/05/metadata.html#indexes .. _`index create/drop`: http://www.sqlalchemy.org/docs/05/metadata.html#indexes
.. _constraint-tutorial:
Constraint Constraint
========== ==========
@ -106,7 +120,9 @@ The following rundowns are true for all constraints classes:
# Drop the constraint # Drop the constraint
cons.drop() cons.drop()
or you can pass column objects (and table argument can be left out). or you can pass in column objects (and table argument can be left out)::
cons = PrimaryKeyConstraint(col1, col2)
3. Some dialects support CASCADE option when dropping constraints:: 3. Some dialects support CASCADE option when dropping constraints::

View File

@ -41,36 +41,36 @@ Download and Development
Dialect support Dialect support
---------------------------------- ---------------
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+ +---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| Operation / Dialect | :ref:`sqlite <sqlite-d>` | :ref:`postgres <postgres-d>` | :ref:`mysql <mysql-d>` | :ref:`oracle <oracle-d>` | firebird | mssql | | Operation / Dialect | :ref:`sqlite <sqlite-d>` | :ref:`postgres <postgres-d>` | :ref:`mysql <mysql-d>` | :ref:`oracle <oracle-d>` | firebird | mssql |
| | | | | | | | | | | | | | | |
+==========================+==========================+==============================+========================+===========================+==========+=======+ +=========================================================+==========================+==============================+========================+===========================+==========+=======+
| ALTER TABLE | yes | yes | | | | | | :ref:`ALTER TABLE RENAME TABLE <table-rename>` | yes | yes | yes | | | |
| RENAME TABLE | | | | | | | | | | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+ +---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| ALTER TABLE | yes | yes | | | | | | :ref:`ALTER TABLE RENAME COLUMN <column-alter>` | yes | yes | yes | | | |
| RENAME COLUMN | (workaround) [#1]_ | | | | | | | | (workaround) [#1]_ | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+ +---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| ALTER TABLE | yes | yes | | | | | | :ref:`ALTER TABLE ADD COLUMN <column-create>` | yes | yes | yes | | | |
| DROP COLUMN | (workaround) [#1]_ | | | | | | | | (with limitations) [#2]_ | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+ +---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| ALTER TABLE | yes | yes | | | | | | :ref:`ALTER TABLE DROP COLUMN <column-drop` | yes | yes | yes | | | |
| ADD COLUMN | (with limitations) [#2]_ | | | | | | | | (workaround) [#1]_ | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+ +---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| ALTER TABLE | no | yes | | | | | | :ref:`ALTER TABLE ALTER COLUMN <column-alter>` | no | yes | yes | | | |
| ADD CONSTRAINT | | | | | | | | | | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+ +---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| ALTER TABLE | no | yes | | | | | | :ref:`ALTER TABLE ADD CONSTRAINT <constraint-tutorial>` | no | yes | yes | | | |
| DROP CONSTRAINT | | | | | | | | | | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+ +---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| ALTER TABLE | no | yes | | | | | | :ref:`ALTER TABLE DROP CONSTRAINT <constraint-tutorial>`| no | yes | yes | | | |
| ALTER COLUMN | | | | | | | | | | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+ +---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| RENAME INDEX | no | yes | | | | | | :ref:`RENAME INDEX <index-rename>` | no | yes | no | | | |
| | | | | | | | | | | | | | | |
+--------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+ +---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
.. [#1] Table is renamed to temporary table, new table is created followed by INSERT statements. .. [#1] Table is renamed to temporary table, new table is created followed by INSERT statements.

View File

@ -6,11 +6,12 @@
""" """
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.engine.default import DefaultDialect from sqlalchemy.engine.default import DefaultDialect
from sqlalchemy.sql.compiler import SchemaGenerator, SchemaDropper
from sqlalchemy.schema import (ForeignKeyConstraint, from sqlalchemy.schema import (ForeignKeyConstraint,
PrimaryKeyConstraint, PrimaryKeyConstraint,
CheckConstraint, CheckConstraint,
UniqueConstraint) UniqueConstraint,
Index)
from sqlalchemy.sql.compiler import SchemaGenerator, SchemaDropper
from migrate.changeset import exceptions, constraint from migrate.changeset import exceptions, constraint
@ -44,28 +45,29 @@ class AlterTableVisitor(SchemaIterator):
self.append('\nALTER TABLE %s ' % self.preparer.format_table(table)) self.append('\nALTER TABLE %s ' % self.preparer.format_table(table))
return table return table
def _pk_constraint(self, table, column, status): # DEPRECATED: use plain constraints instead
"""Create a primary key constraint from a table, column. #def _pk_constraint(self, table, column, status):
# """Create a primary key constraint from a table, column.
Status: true if the constraint is being added; false if being dropped # Status: true if the constraint is being added; false if being dropped
""" # """
if isinstance(column, basestring): # if isinstance(column, basestring):
column = getattr(table.c, name) # column = getattr(table.c, name)
ret = constraint.PrimaryKeyConstraint(*table.primary_key) # ret = constraint.PrimaryKeyConstraint(*table.primary_key)
if status: # if status:
# Created PK # # Created PK
ret.c.append(column) # ret.c.append(column)
else: # else:
# Dropped PK # # Dropped PK
names = [c.name for c in cons.c] # names = [c.name for c in cons.c]
index = names.index(col.name) # index = names.index(col.name)
del ret.c[index] # del ret.c[index]
# Allow explicit PK name assignment # # Allow explicit PK name assignment
if isinstance(pk, basestring): # if isinstance(pk, basestring):
ret.name = pk # ret.name = pk
return ret # return ret
class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator): class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator):
@ -77,28 +79,36 @@ class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator):
:param column: column object :param column: column object
:type column: :class:`sqlalchemy.Column` instance :type column: :class:`sqlalchemy.Column` instance
""" """
if column.default is not None:
self.traverse_single(column.default)
table = self.start_alter_table(column) table = self.start_alter_table(column)
self.append("ADD ") self.append("ADD ")
colspec = self.get_column_specification(column) self.append(self.get_column_specification(column))
self.append(colspec)
for cons in column.constraints:
self.traverse_single(cons)
self.execute() self.execute()
# add in foreign keys # ALTER TABLE STATEMENTS
if column.foreign_keys:
self.visit_alter_foriegn_keys(column)
def visit_alter_foriegn_keys(self, column): # add indexes and unique constraints
if column.index_name:
ix = Index(column.index_name,
column,
unique=bool(column.index_name or column.index))
ix.create()
elif column.unique_name:
constraint.UniqueConstraint(column, name=column.unique_name).create()
# SA bounds FK constraints to table, add manually
for fk in column.foreign_keys: for fk in column.foreign_keys:
self.define_foreign_key(fk.constraint) self.add_foreignkey(fk.constraint)
def visit_table(self, table):
"""Default table visitor, does nothing.
:param table: table object
:type table: :class:`sqlalchemy.Table` instance
"""
pass
# add primary key constraint if needed
if column.primary_key_name:
cons = constraint.PrimaryKeyConstraint(column, name=column.primary_key_name)
cons.create()
class ANSIColumnDropper(AlterTableVisitor, SchemaDropper): class ANSIColumnDropper(AlterTableVisitor, SchemaDropper):
@ -113,7 +123,7 @@ class ANSIColumnDropper(AlterTableVisitor, SchemaDropper):
:type column: :class:`sqlalchemy.Column` :type column: :class:`sqlalchemy.Column`
""" """
table = self.start_alter_table(column) table = self.start_alter_table(column)
self.append(' DROP COLUMN %s' % self.preparer.format_column(column)) self.append('DROP COLUMN %s' % self.preparer.format_column(column))
self.execute() self.execute()
@ -159,43 +169,25 @@ class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator):
# are managed by the app, not the db. # are managed by the app, not the db.
self._run_subvisit(delta, self._visit_column_default) self._run_subvisit(delta, self._visit_column_default)
if 'name' in keys: if 'name' in keys:
self._run_subvisit(delta, self._visit_column_name) self._run_subvisit(delta, self._visit_column_name, start_alter=False)
def _run_subvisit(self, delta, func): def _run_subvisit(self, delta, func, start_alter=True):
"""Runs visit method based on what needs to be changed on column""" """Runs visit method based on what needs to be changed on column"""
table = self._to_table(delta.table) table = self._to_table(delta.table)
col_name = delta.current_name col_name = delta.current_name
if start_alter:
self.start_alter_column(table, col_name)
ret = func(table, col_name, delta) ret = func(table, col_name, delta)
self.execute() self.execute()
def _visit_column_foreign_key(self, delta): def start_alter_column(self, table, col_name):
table = delta.table """Starts ALTER COLUMN"""
column = getattr(table.c, delta.current_name)
cons = constraint.ForeignKeyConstraint(column, autoload=True)
fk = delta['foreign_key']
if fk:
# For now, cons.columns is limited to one column:
# no multicolumn FKs
column.foreign_key = ForeignKey(*cons.columns)
else:
column_foreign_key = None
cons.drop()
cons.create()
def _visit_column_primary_key(self, delta):
table = delta.table
col = getattr(table.c, delta.current_name)
pk = delta['primary_key']
cons = self._pk_constraint(table, col, pk)
cons.drop()
cons.create()
def _visit_column_nullable(self, table, col_name, delta):
nullable = delta['nullable']
table = self._to_table(table)
self.start_alter_table(table) self.start_alter_table(table)
# TODO: use preparer.format_column # TODO: use preparer.format_column
self.append("ALTER COLUMN %s " % self.preparer.quote_identifier(col_name)) self.append("ALTER COLUMN %s " % self.preparer.quote_identifier(col_name))
def _visit_column_nullable(self, table, col_name, delta):
nullable = delta['nullable']
if nullable: if nullable:
self.append("DROP NOT NULL") self.append("DROP NOT NULL")
else: else:
@ -207,9 +199,6 @@ class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator):
# reason # reason
dummy = sa.Column(None, None, server_default=server_default) dummy = sa.Column(None, None, server_default=server_default)
default_text = self.get_column_default_string(dummy) default_text = self.get_column_default_string(dummy)
self.start_alter_table(table)
# TODO: use preparer.format_column
self.append("ALTER COLUMN %s " % self.preparer.quote_identifier(col_name))
if default_text is not None: if default_text is not None:
self.append("SET DEFAULT %s" % default_text) self.append("SET DEFAULT %s" % default_text)
else: else:
@ -218,15 +207,10 @@ class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator):
def _visit_column_type(self, table, col_name, delta): def _visit_column_type(self, table, col_name, delta):
type_ = delta['type'] type_ = delta['type']
if not isinstance(type_, sa.types.AbstractType): if not isinstance(type_, sa.types.AbstractType):
# It's the class itself, not an instance... make an # It's the class itself, not an instance... make an instance
# instance
type_ = type_() type_ = type_()
type_text = type_.dialect_impl(self.dialect).get_col_spec() type_text = type_.dialect_impl(self.dialect).get_col_spec()
self.start_alter_table(table) self.append("TYPE %s" % type_text)
# TODO: does type need formating?
# TODO: use preparer.format_column
self.append("ALTER COLUMN %s TYPE %s" %
(self.preparer.quote_identifier(col_name), type_text))
def _visit_column_name(self, table, col_name, delta): def _visit_column_name(self, table, col_name, delta):
new_name = delta['name'] new_name = delta['name']
@ -292,13 +276,13 @@ class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator):
if cons.name is not None: if cons.name is not None:
self.append("CONSTRAINT %s " % self.append("CONSTRAINT %s " %
self.preparer.format_constraint(cons)) self.preparer.format_constraint(cons))
self.append(" CHECK (%s)" % cons.sqltext) self.append("CHECK (%s)" % cons.sqltext)
self.define_constraint_deferrability(cons) self.define_constraint_deferrability(cons)
elif isinstance(cons, UniqueConstraint): elif isinstance(cons, UniqueConstraint):
if cons.name is not None: if cons.name is not None:
self.append("CONSTRAINT %s " % self.append("CONSTRAINT %s " %
self.preparer.format_constraint(cons)) self.preparer.format_constraint(cons))
self.append(" UNIQUE (%s)" % \ self.append("UNIQUE (%s)" % \
(', '.join(self.preparer.quote(c.name, c.quote) for c in cons))) (', '.join(self.preparer.quote(c.name, c.quote) for c in cons)))
self.define_constraint_deferrability(cons) self.define_constraint_deferrability(cons)
else: else:
@ -317,7 +301,8 @@ class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
def _visit_constraint(self, constraint): def _visit_constraint(self, constraint):
self.start_alter_table(constraint) self.start_alter_table(constraint)
self.append("DROP CONSTRAINT ") self.append("DROP CONSTRAINT ")
self.append(self.get_constraint_name(constraint)) constraint.name = self.get_constraint_name(constraint)
self.append(self.preparer.format_constraint(constraint))
if constraint.cascade: if constraint.cascade:
self.append(" CASCADE") self.append(" CASCADE")
self.execute() self.execute()

View File

@ -1,7 +1,6 @@
""" """
This module defines standalone schema constraint classes. This module defines standalone schema constraint classes.
""" """
import sqlalchemy
from sqlalchemy import schema from sqlalchemy import schema
from migrate.changeset.exceptions import * from migrate.changeset.exceptions import *
@ -142,7 +141,7 @@ class CheckConstraint(ConstraintChangeset, schema.CheckConstraint):
__visit_name__ = 'migrate_check_constraint' __visit_name__ = 'migrate_check_constraint'
def __init__(self, sqltext, *args, **kwargs): def __init__(self, sqltext, *args, **kwargs):
cols = kwargs.pop('columns', False) cols = kwargs.pop('columns', [])
if not cols and not kwargs.get('name', False): if not cols and not kwargs.get('name', False):
raise InvalidConstraintError('You must either set "name"' raise InvalidConstraintError('You must either set "name"'
'parameter or "columns" to autogenarate it.') 'parameter or "columns" to autogenarate it.')
@ -169,6 +168,8 @@ class UniqueConstraint(ConstraintChangeset, schema.UniqueConstraint):
:param table: If columns are passed as strings, this kw is required :param table: If columns are passed as strings, this kw is required
:type table: Table instance :type table: Table instance
:type cols: strings or Column instances :type cols: strings or Column instances
.. versionadded:: 0.5.5
""" """
__visit_name__ = 'migrate_unique_constraint' __visit_name__ = 'migrate_unique_constraint'

View File

@ -20,39 +20,28 @@ class MySQLColumnDropper(ansisql.ANSIColumnDropper):
class MySQLSchemaChanger(MySQLSchemaGenerator, ansisql.ANSISchemaChanger): class MySQLSchemaChanger(MySQLSchemaGenerator, ansisql.ANSISchemaChanger):
def visit_column(self, delta): def visit_column(self, column):
keys = delta.keys() delta = column.delta
if 'type' in keys or 'nullable' in keys or 'name' in keys: table = column.table
self._run_subvisit(delta, self._visit_column_change)
if 'server_default' in keys:
# Column name might have changed above
col_name = delta.get('name', delta.current_name)
self._run_subvisit(delta, self._visit_column_default,
col_name=col_name)
def _visit_column_change(self, table_name, col_name, delta):
if not hasattr(delta, 'result_column'):
# Mysql needs the whole column definition, not just a lone
# name/type
raise exceptions.NotSupportedError(
"A column object is required to do this")
column = delta.result_column
# needed by get_column_specification
if not column.table:
column.table = delta.table
colspec = self.get_column_specification(column) colspec = self.get_column_specification(column)
# TODO: we need table formating here
self.start_alter_table(self.preparer.quote(table_name, True)) if not hasattr(delta, 'result_column'):
self.append("CHANGE COLUMN ") # Mysql needs the whole column definition, not just a lone name/type
self.append(self.preparer.quote(col_name, True)) raise exceptions.NotSupportedError(
self.append(' ') "A column object must be present in table to alter it")
self.start_alter_table(table)
old_col_name = self.preparer.quote(delta.current_name, column.quote)
self.append("CHANGE COLUMN %s " % old_col_name)
self.append(colspec) self.append(colspec)
self.execute()
def visit_index(self, param): def visit_index(self, param):
# If MySQL can do this, I can't find how # If MySQL can do this, I can't find how
raise exceptions.NotSupportedError("MySQL cannot rename indexes") raise exceptions.NotSupportedError("MySQL cannot rename indexes")
class MySQLConstraintGenerator(ansisql.ANSIConstraintGenerator): class MySQLConstraintGenerator(ansisql.ANSIConstraintGenerator):
pass pass
@ -67,9 +56,22 @@ class MySQLConstraintDropper(ansisql.ANSIConstraintDropper):
def visit_migrate_foreign_key_constraint(self, constraint): def visit_migrate_foreign_key_constraint(self, constraint):
self.start_alter_table(constraint) self.start_alter_table(constraint)
self.append("DROP FOREIGN KEY ") self.append("DROP FOREIGN KEY ")
constraint.name = self.get_constraint_name(constraint)
self.append(self.preparer.format_constraint(constraint)) self.append(self.preparer.format_constraint(constraint))
self.execute() self.execute()
def visit_migrate_check_constraint(self, *p, **k):
raise exceptions.NotSupportedError("MySQL does not support CHECK"
" constraints, use triggers instead.")
def visit_migrate_unique_constraint(self, constraint, *p, **k):
self.start_alter_table(constraint)
self.append('DROP INDEX ')
constraint.name = self.get_constraint_name(constraint)
self.append(self.preparer.format_constraint(constraint))
self.execute()
class MySQLDialect(ansisql.ANSIDialect): class MySQLDialect(ansisql.ANSIDialect):
columngenerator = MySQLColumnGenerator columngenerator = MySQLColumnGenerator
columndropper = MySQLColumnDropper columndropper = MySQLColumnDropper

View File

@ -1,10 +1,11 @@
""" """
Oracle database specific implementations of changeset classes. Oracle database specific implementations of changeset classes.
""" """
import sqlalchemy as sa
from sqlalchemy.databases import oracle as sa_base
from migrate.changeset import ansisql, exceptions from migrate.changeset import ansisql, exceptions
from sqlalchemy.databases import oracle as sa_base
import sqlalchemy as sa
OracleSchemaGenerator = sa_base.OracleSchemaGenerator OracleSchemaGenerator = sa_base.OracleSchemaGenerator

View File

@ -3,10 +3,10 @@
.. _`SQLite`: http://www.sqlite.org/ .. _`SQLite`: http://www.sqlite.org/
""" """
from migrate.changeset import ansisql, exceptions, constraint
from sqlalchemy.databases import sqlite as sa_base from sqlalchemy.databases import sqlite as sa_base
from sqlalchemy import Table, MetaData
#import sqlalchemy as sa from migrate.changeset import ansisql, exceptions
SQLiteSchemaGenerator = sa_base.SQLiteSchemaGenerator SQLiteSchemaGenerator = sa_base.SQLiteSchemaGenerator
@ -20,12 +20,13 @@ class SQLiteCommon(object):
class SQLiteHelper(SQLiteCommon): class SQLiteHelper(SQLiteCommon):
def visit_column(self, column): def visit_column(self, column):
try: table = self._to_table(column.table)
table = self._to_table(column.table)
except:
table = self._to_table(column)
raise
table_name = self.preparer.format_table(table) table_name = self.preparer.format_table(table)
# we remove all constraints, indexes so it doesnt recreate them
table.indexes = set()
table.constraints = set()
self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name) self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name)
self.execute() self.execute()
@ -42,7 +43,7 @@ class SQLiteColumnGenerator(SQLiteSchemaGenerator, SQLiteCommon,
ansisql.ANSIColumnGenerator): ansisql.ANSIColumnGenerator):
"""SQLite ColumnGenerator""" """SQLite ColumnGenerator"""
def visit_alter_foriegn_keys(self, column): def add_foreignkey(self, constraint):
"""Does not support ALTER TABLE ADD FOREIGN KEY""" """Does not support ALTER TABLE ADD FOREIGN KEY"""
self._not_supported("ALTER TABLE ADD CONSTRAINT") self._not_supported("ALTER TABLE ADD CONSTRAINT")
@ -51,7 +52,6 @@ class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper):
"""SQLite ColumnDropper""" """SQLite ColumnDropper"""
def _modify_table(self, table, column): def _modify_table(self, table, column):
del table.columns[column.name]
columns = ' ,'.join(map(self.preparer.format_column, table.columns)) columns = ' ,'.join(map(self.preparer.format_column, table.columns))
return 'INSERT INTO %(table_name)s SELECT ' + columns + \ return 'INSERT INTO %(table_name)s SELECT ' + columns + \
' from migration_tmp' ' from migration_tmp'

View File

@ -2,8 +2,10 @@
Module for visitor class mapping. Module for visitor class mapping.
""" """
import sqlalchemy as sa import sqlalchemy as sa
from migrate.changeset.databases import sqlite, postgres, mysql, oracle
from migrate.changeset import ansisql from migrate.changeset import ansisql
from migrate.changeset.databases import sqlite, postgres, mysql, oracle
# Map SA dialects to the corresponding Migrate extensions # Map SA dialects to the corresponding Migrate extensions
dialects = { dialects = {

View File

@ -1,8 +1,6 @@
""" """
Schema module providing common schema operations. Schema module providing common schema operations.
""" """
import re
import sqlalchemy import sqlalchemy
from migrate.changeset.databases.visitor import (get_engine_visitor, from migrate.changeset.databases.visitor import (get_engine_visitor,
@ -101,13 +99,12 @@ def alter_column(*p, **k):
engine = k['engine'] engine = k['engine']
delta = _ColumnDelta(*p, **k) delta = _ColumnDelta(*p, **k)
visitorcallable = get_engine_visitor(engine, 'schemachanger')
column = sqlalchemy.Column(delta.current_name) delta.result_column.delta = delta
column.delta = delta delta.result_column.table = delta.table
column.table = delta.table
engine._run_visitor(visitorcallable, column) visitorcallable = get_engine_visitor(engine, 'schemachanger')
#_engine_run_visitor(engine, visitorcallable, delta) engine._run_visitor(visitorcallable, delta.result_column)
# Update column # Update column
if col is not None: if col is not None:
@ -155,18 +152,6 @@ def _to_index(index, table=None, engine=None):
return ret return ret
def _normalize_table(column, table):
if table is not None:
if table is not column.table:
# This is a bit of a hack: we end up with dupe PK columns here
pk_names = map(lambda c: c.name, table.primary_key)
if column.primary_key and pk_names.count(column.name):
index = pk_names.index(column_name)
del table.primary_key[index]
table.append_column(column)
return column.table
class _ColumnDelta(dict): class _ColumnDelta(dict):
"""Extracts the differences between two columns/column-parameters""" """Extracts the differences between two columns/column-parameters"""
@ -223,6 +208,7 @@ class _ColumnDelta(dict):
table = k.pop('table') table = k.pop('table')
self.current_name = current_name self.current_name = current_name
self._table = table self._table = table
self.result_column = table.c.get(current_name, None)
return k return k
def _init_1col(self, col, *p, **k): def _init_1col(self, col, *p, **k):
@ -277,9 +263,6 @@ class _ColumnDelta(dict):
getattr(that, 'length', None)) getattr(that, 'length', None))
return ret return ret
def accept_schema_visitor(self, visitor):
return visitor.visit_column(self)
class ChangesetTable(object): class ChangesetTable(object):
"""Changeset extensions to SQLAlchemy tables.""" """Changeset extensions to SQLAlchemy tables."""
@ -300,7 +283,7 @@ class ChangesetTable(object):
if not isinstance(column, sqlalchemy.Column): if not isinstance(column, sqlalchemy.Column):
# It's a column name # It's a column name
try: try:
column = getattr(self.c, str(column), None) column = getattr(self.c, str(column))
except AttributeError: except AttributeError:
# That column isn't part of the table. We don't need # That column isn't part of the table. We don't need
# its entire definition to drop the column, just its # its entire definition to drop the column, just its
@ -362,17 +345,23 @@ class ChangesetColumn(object):
k['engine'] = k['table'].bind k['engine'] = k['table'].bind
return alter_column(self, *p, **k) return alter_column(self, *p, **k)
def create(self, table=None, *args, **kwargs): def create(self, table=None, index_name=None, unique_name=None,
primary_key_name=None, *args, **kwargs):
"""Create this column in the database. """Create this column in the database.
Assumes the given table exists. ``ALTER TABLE ADD COLUMN``, Assumes the given table exists. ``ALTER TABLE ADD COLUMN``,
for most databases. for most databases.
""" """
table = _normalize_table(self, table) self.index_name = index_name
engine = table.bind self.unique_name = unique_name
self.primary_key_name = primary_key_name
for cons in ('index_name', 'unique_name', 'primary_key_name'):
self._check_sanity_constraints(cons)
self.add_to_table(table)
engine = self.table.bind
visitorcallable = get_engine_visitor(engine, 'columngenerator') visitorcallable = get_engine_visitor(engine, 'columngenerator')
engine._run_visitor(visitorcallable, self, *args, **kwargs) engine._run_visitor(visitorcallable, self, *args, **kwargs)
return self return self
def drop(self, table=None, *args, **kwargs): def drop(self, table=None, *args, **kwargs):
@ -380,12 +369,32 @@ class ChangesetColumn(object):
``ALTER TABLE DROP COLUMN``, for most databases. ``ALTER TABLE DROP COLUMN``, for most databases.
""" """
table = _normalize_table(self, table) if table is not None:
engine = table.bind self.table = table
self.remove_from_table(self.table)
engine = self.table.bind
visitorcallable = get_engine_visitor(engine, 'columndropper') visitorcallable = get_engine_visitor(engine, 'columndropper')
engine._run_visitor(visitorcallable, self, *args, **kwargs) engine._run_visitor(visitorcallable, self, *args, **kwargs)
return self return self
def add_to_table(self, table):
if table and not self.table:
self._set_parent(table)
def remove_from_table(self, table):
# TODO: remove indexes, primary keys, constraints, etc
if table.c.contains_column(self):
table.c.remove(self)
def _check_sanity_constraints(self, name):
obj = getattr(self, name)
if (getattr(self, name[:-5]) and not obj):
raise InvalidConstraintError("Column.create() accepts index_name,"
" primary_key_name and unique_name to generate constraints")
if not isinstance(obj, basestring) and obj is not None:
raise InvalidConstraintError(
"%s argument for column must be constraint name" % name)
class ChangesetIndex(object): class ChangesetIndex(object):
"""Changeset extensions to SQLAlchemy Indexes.""" """Changeset extensions to SQLAlchemy Indexes."""

View File

@ -140,7 +140,8 @@ class PythonScript(base.BaseScript):
try: try:
func(engine) func(engine)
except TypeError: except TypeError:
print "upgrade/downgrade functions must accept one parameter (migrate_engine)" print "upgrade/downgrade functions must accept engine parameter (since ver 0.5.5)"
raise
@property @property
def module(self): def module(self):

View File

@ -9,6 +9,7 @@ tag_build = .dev
[nosetests] [nosetests]
pdb = true pdb = true
pdb-failures = true pdb-failures = true
stop = true
[aliases] [aliases]
release = egg_info -RDb '' release = egg_info -RDb ''

View File

@ -1,32 +1,29 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sqlalchemy import sqlalchemy
from sqlalchemy import * from sqlalchemy import *
from sqlalchemy.databases import information_schema
import migrate
from migrate import changeset from migrate import changeset
from migrate.changeset import * from migrate.changeset import *
from migrate.changeset.schema import _ColumnDelta from migrate.changeset.schema import _ColumnDelta
from test import fixture from test import fixture
# TODO: test quoting # TODO: test quoting
# TODO: test all other constraints on create column, test defaults
class TestAddDropColumn(fixture.DB): class TestAddDropColumn(fixture.DB):
"""Test add/drop column through all possible interfaces
also test for constraints"""
level = fixture.DB.CONNECT level = fixture.DB.CONNECT
meta = MetaData()
# We'll be adding the 'data' column
table_name = 'tmp_adddropcol' table_name = 'tmp_adddropcol'
table_int = 0 table_int = 0
def _setup(self, url): def _setup(self, url):
super(TestAddDropColumn, self)._setup(url) super(TestAddDropColumn, self)._setup(url)
self.meta.clear() self.meta = MetaData()
self.table = Table(self.table_name, self.meta, self.table = Table(self.table_name, self.meta,
Column('id', Integer, primary_key=True), Column('id', Integer, unique=True),
) )
self.meta.bind = self.engine self.meta.bind = self.engine
if self.engine.has_table(self.table.name): if self.engine.has_table(self.table.name):
@ -35,72 +32,57 @@ class TestAddDropColumn(fixture.DB):
def _teardown(self): def _teardown(self):
if self.engine.has_table(self.table.name): if self.engine.has_table(self.table.name):
try: self.table.drop()
self.table.drop()
except:
pass
self.meta.clear() self.meta.clear()
super(TestAddDropColumn,self)._teardown() super(TestAddDropColumn,self)._teardown()
def run_(self,create_column_func,drop_column_func,*col_p,**col_k): def run_(self, create_column_func, drop_column_func, *col_p, **col_k):
col_name = 'data' col_name = 'data'
def _assert_numcols(expected,type_): def assert_numcols(num_of_expected_cols):
# number of cols should be correct in table object and in database
self.refresh_table(self.table_name)
result = len(self.table.c) result = len(self.table.c)
self.assertEquals(result,expected, self.assertEquals(result, num_of_expected_cols),
"# %s cols incorrect: %s != %s"%(type_,result,expected)) if col_k.get('primary_key', None):
if not col_k.get('primary_key',None): # new primary key: check its length too
return result = len(self.table.primary_key)
# new primary key: check its length too self.assertEquals(result, num_of_expected_cols)
result = len(self.table.primary_key)
self.assertEquals(result,expected,
"# %s pks incorrect: %s != %s"%(type_,result,expected))
def assert_numcols(expected):
# number of cols should be correct in table object and in database
# Changed: create/drop shouldn't mess with the objects
#_assert_numcols(expected,'object')
# Detect # database cols via autoload
#self.meta.clear()
del self.meta.tables[self.table_name]
self.table=Table(self.table_name,self.meta,autoload=True)
_assert_numcols(expected,'database')
assert_numcols(1) assert_numcols(1)
if len(col_p) == 0: if len(col_p) == 0:
col_p = [String(40)] col_p = [String(40)]
col = Column(col_name,*col_p,**col_k) col = Column(col_name, *col_p, **col_k)
create_column_func(col) create_column_func(col)
#create_column(col,self.table)
assert_numcols(2) assert_numcols(2)
self.assertEquals(getattr(self.table.c,col_name),col) col2 = getattr(self.table.c, col_name)
#drop_column(col,self.table) self.assertEquals(col2, col)
col = getattr(self.table.c,col_name) drop_column_func(col2)
drop_column_func(col)
assert_numcols(1) assert_numcols(1)
@fixture.usedb() @fixture.usedb()
def test_undefined(self): def test_undefined(self):
"""Add/drop columns not yet defined in the table""" """Add/drop columns not yet defined in the table"""
def add_func(col): def add_func(col):
return create_column(col,self.table) return create_column(col, self.table)
def drop_func(col): def drop_func(col):
return drop_column(col,self.table) return drop_column(col, self.table)
return self.run_(add_func,drop_func) return self.run_(add_func, drop_func)
@fixture.usedb() @fixture.usedb()
def test_defined(self): def test_defined(self):
"""Add/drop columns already defined in the table""" """Add/drop columns already defined in the table"""
def add_func(col): def add_func(col):
self.meta.clear() self.meta.clear()
self.table = Table(self.table_name,self.meta, self.table = Table(self.table_name, self.meta,
Column('id',Integer,primary_key=True), Column('id', Integer, primary_key=True),
col, col,
) )
return create_column(col,self.table) return create_column(col)
def drop_func(col): def drop_func(col):
return drop_column(col,self.table) return drop_column(col)
return self.run_(add_func,drop_func) return self.run_(add_func, drop_func)
@fixture.usedb() @fixture.usedb()
def test_method_bound(self): def test_method_bound(self):
@ -108,14 +90,14 @@ class TestAddDropColumn(fixture.DB):
ie. no table parameter passed to function ie. no table parameter passed to function
""" """
def add_func(col): def add_func(col):
self.assert_(col.table is None,col.table) self.assert_(col.table is None, col.table)
self.table.append_column(col) self.table.append_column(col)
return col.create() return col.create()
def drop_func(col): def drop_func(col):
#self.assert_(col.table is None,col.table) #self.assert_(col.table is None,col.table)
#self.table.append_column(col) #self.table.append_column(col)
return col.drop() return col.drop()
return self.run_(add_func,drop_func) return self.run_(add_func, drop_func)
@fixture.usedb() @fixture.usedb()
def test_method_notbound(self): def test_method_notbound(self):
@ -124,7 +106,7 @@ class TestAddDropColumn(fixture.DB):
return col.create(self.table) return col.create(self.table)
def drop_func(col): def drop_func(col):
return col.drop(self.table) return col.drop(self.table)
return self.run_(add_func,drop_func) return self.run_(add_func, drop_func)
@fixture.usedb() @fixture.usedb()
def test_tablemethod_obj(self): def test_tablemethod_obj(self):
@ -133,7 +115,7 @@ class TestAddDropColumn(fixture.DB):
return self.table.create_column(col) return self.table.create_column(col)
def drop_func(col): def drop_func(col):
return self.table.drop_column(col) return self.table.drop_column(col)
return self.run_(add_func,drop_func) return self.run_(add_func, drop_func)
@fixture.usedb() @fixture.usedb()
def test_tablemethod_name(self): def test_tablemethod_name(self):
@ -145,69 +127,181 @@ class TestAddDropColumn(fixture.DB):
def drop_func(col): def drop_func(col):
# Not necessarily bound to table # Not necessarily bound to table
return self.table.drop_column(col.name) return self.table.drop_column(col.name)
return self.run_(add_func,drop_func) return self.run_(add_func, drop_func)
@fixture.usedb() @fixture.usedb()
def test_byname(self): def test_byname(self):
"""Add/drop columns via functions; by table object and column name""" """Add/drop columns via functions; by table object and column name"""
def add_func(col): def add_func(col):
self.table.append_column(col) self.table.append_column(col)
return create_column(col.name,self.table) return create_column(col.name, self.table)
def drop_func(col): def drop_func(col):
return drop_column(col.name,self.table) return drop_column(col.name, self.table)
return self.run_(add_func,drop_func) return self.run_(add_func, drop_func)
@fixture.usedb()
def test_drop_column_not_in_table(self):
"""Drop column by name"""
def add_func(col):
return self.table.create_column(col)
def drop_func(col):
self.table.c.remove(col)
return self.table.drop_column(col.name)
self.run_(add_func, drop_func)
@fixture.usedb() @fixture.usedb()
def test_fk(self): def test_fk(self):
"""Can create columns with foreign keys""" """Can create columns with foreign keys"""
reftable = Table('tmp_ref',self.meta,
Column('id',Integer,primary_key=True),
)
# create FK's target # create FK's target
reftable = Table('tmp_ref', self.meta,
Column('id', Integer, primary_key=True),
)
if self.engine.has_table(reftable.name): if self.engine.has_table(reftable.name):
reftable.drop() reftable.drop()
reftable.create() reftable.create()
def add_func(col):
self.table.append_column(col) # create column with fk
return create_column(col.name, self.table) col = Column('data', Integer, ForeignKey(reftable.c.id))
def drop_func(col):
ret = drop_column(col.name,self.table)
if self.engine.has_table(reftable.name):
reftable.drop()
return ret
if self.url.startswith('sqlite'): if self.url.startswith('sqlite'):
self.assertRaises(changeset.exceptions.NotSupportedError, self.assertRaises(changeset.exceptions.NotSupportedError,
self.run_, add_func, drop_func, Integer, col.create, self.table)
ForeignKey(reftable.c.id))
else: else:
return self.run_(add_func, drop_func, Integer, col.create(self.table)
ForeignKey(reftable.c.id))
# check if constraint is added
for cons in self.table.constraints:
if isinstance(cons, sqlalchemy.schema.ForeignKeyConstraint):
break
else:
self.fail('No constraint found')
# TODO: test on db level if constraints work
self.assertEqual(reftable.c.id.name, col.foreign_keys[0].column.name)
col.drop(self.table)
if self.engine.has_table(reftable.name):
reftable.drop()
@fixture.usedb(not_supported='sqlite')
def test_pk(self):
"""Can create columns with primary key"""
col = Column('data', Integer)
self.assertRaises(changeset.exceptions.InvalidConstraintError,
col.create, self.table, primary_key_name=True)
col.create(self.table, primary_key_name='data_pkey')
# check if constraint was added (cannot test on objects)
self.table.insert(values={'data': 4}).execute()
try:
self.table.insert(values={'data': 4}).execute()
except sqlalchemy.exc.IntegrityError:
pass
else:
self.fail()
col.drop()
@fixture.usedb(not_supported='mysql')
def test_check(self):
"""Can create columns with check constraint"""
col = Column('data',
Integer,
sqlalchemy.schema.CheckConstraint('data > 4'))
col.create(self.table)
# check if constraint was added (cannot test on objects)
self.table.insert(values={'data': 5}).execute()
try:
self.table.insert(values={'data': 3}).execute()
except sqlalchemy.exc.IntegrityError:
pass
else:
self.fail()
col.drop()
@fixture.usedb(not_supported='sqlite')
def test_unique(self):
"""Can create columns with unique constraint"""
self.assertRaises(changeset.exceptions.InvalidConstraintError,
Column('data', Integer, unique=True).create, self.table)
col = Column('data', Integer)
col.create(self.table, unique_name='data_unique')
# check if constraint was added (cannot test on objects)
self.table.insert(values={'data': 5}).execute()
try:
self.table.insert(values={'data': 5}).execute()
except sqlalchemy.exc.IntegrityError:
pass
else:
self.fail()
col.drop(self.table)
@fixture.usedb()
def test_index(self):
"""Can create columns with indexes"""
self.assertRaises(changeset.exceptions.InvalidConstraintError,
Column('data', Integer).create, self.table, index_name=True)
col = Column('data', Integer)
col.create(self.table, index_name='ix_data')
# check if index was added
self.table.insert(values={'data': 5}).execute()
try:
self.table.insert(values={'data': 5}).execute()
except sqlalchemy.exc.IntegrityError:
pass
else:
self.fail()
col.drop()
@fixture.usedb()
def test_server_defaults(self):
"""Can create columns with server_default values"""
col = Column('data', String(244), server_default='foobar')
col.create(self.table)
self.table.insert().execute()
row = self.table.select(autocommit=True).execute().fetchone()
self.assertEqual(u'foobar', row['data'])
col.drop()
# TODO: test sequence
# TODO: test that if column is appended on creation and removed on deletion
class TestRename(fixture.DB): class TestRename(fixture.DB):
"""Tests for table and index rename methods"""
level = fixture.DB.CONNECT level = fixture.DB.CONNECT
meta = MetaData() meta = MetaData()
def _setup(self, url): def _setup(self, url):
super(TestRename, self)._setup(url) super(TestRename, self)._setup(url)
self.meta.bind = self.engine #self.meta.connect(self.engine) self.meta.bind = self.engine
@fixture.usedb() @fixture.usedb()
def test_rename_table(self): def test_rename_table(self):
"""Tables can be renamed""" """Tables can be renamed"""
c_name = 'col_1' c_name = 'col_1'
name1 = 'name_one' table_name1 = 'name_one'
name2 = 'name_two' table_name2 = 'name_two'
xname1 = 'x' + name1 index_name1 = 'x' + table_name1
xname2 = 'x' + name2 index_name2 = 'x' + table_name2
self.column = Column(c_name, Integer)
self.meta.clear() self.meta.clear()
self.table = Table(name1, self.meta, self.column) self.column = Column(c_name, Integer)
self.index = Index(xname1, self.column, unique=False) self.table = Table(table_name1, self.meta, self.column)
self.index = Index(index_name1, self.column, unique=False)
if self.engine.has_table(self.table.name): if self.engine.has_table(self.table.name):
self.table.drop() self.table.drop()
if self.engine.has_table(name2): if self.engine.has_table(table_name2):
tmp = Table(name2, self.meta, autoload=True) tmp = Table(table_name2, self.meta, autoload=True)
tmp.drop() tmp.drop()
tmp.deregister() tmp.deregister()
del tmp del tmp
@ -228,69 +322,72 @@ class TestRename(fixture.DB):
# we know the object's name isn't consistent: just assign it # we know the object's name isn't consistent: just assign it
newname = expected newname = expected
# Table DB check # Table DB check
#table = self.refresh_table(self.table,newname)
self.meta.clear() self.meta.clear()
self.table = Table(newname, self.meta, autoload=True) self.table = Table(newname, self.meta, autoload=True)
self.assertEquals(self.table.name,expected) self.assertEquals(self.table.name, expected)
def assert_index_name(expected,skip_object_check=False):
def assert_index_name(expected, skip_object_check=False):
if not skip_object_check: if not skip_object_check:
# Index object check # Index object check
self.assertEquals(self.index.name,expected) self.assertEquals(self.index.name, expected)
else: else:
# object is inconsistent # object is inconsistent
self.index.name = expected self.index.name = expected
# Index DB check # TODO: Index DB check
#TODO
try: try:
# Table renames # Table renames
assert_table_name(name1) assert_table_name(table_name1)
rename_table(self.table, name2) rename_table(self.table, table_name2)
assert_table_name(name2) assert_table_name(table_name2)
self.table.rename(name1) self.table.rename(table_name1)
assert_table_name(name1) assert_table_name(table_name1)
# ..by just the string
rename_table(name1, name2, engine=self.engine) # test by just the string
assert_table_name(name2, True) # object not updated rename_table(table_name1, table_name2, engine=self.engine)
assert_table_name(table_name2, True) # object not updated
# Index renames # Index renames
if self.url.startswith('sqlite') or self.url.startswith('mysql'): if self.url.startswith('sqlite') or self.url.startswith('mysql'):
self.assertRaises(changeset.exceptions.NotSupportedError, self.assertRaises(changeset.exceptions.NotSupportedError,
self.index.rename, xname2) self.index.rename, index_name2)
else: else:
assert_index_name(xname1) assert_index_name(index_name1)
rename_index(self.index,xname2,engine=self.engine) rename_index(self.index, index_name2, engine=self.engine)
assert_index_name(xname2) assert_index_name(index_name2)
self.index.rename(xname1) self.index.rename(index_name1)
assert_index_name(xname1) assert_index_name(index_name1)
# ..by just the string
rename_index(xname1,xname2,engine=self.engine) # test by just the string
assert_index_name(xname2,True) rename_index(index_name1, index_name2, engine=self.engine)
assert_index_name(index_name2, True)
finally: finally:
#self.index.drop()
if self.table.exists(): if self.table.exists():
self.table.drop() self.table.drop()
class TestColumnChange(fixture.DB): class TestColumnChange(fixture.DB):
level=fixture.DB.CONNECT level = fixture.DB.CONNECT
table_name = 'tmp_colchange' table_name = 'tmp_colchange'
def _setup(self, url): def _setup(self, url):
super(TestColumnChange, self)._setup(url) super(TestColumnChange, self)._setup(url)
self.meta = MetaData(self.engine) self.meta = MetaData(self.engine)
self.table = Table(self.table_name,self.meta, self.table = Table(self.table_name, self.meta,
Column('id',Integer,primary_key=True), Column('id', Integer, primary_key=True),
Column('data',String(40),server_default=DefaultClause("tluafed"),nullable=True), Column('data', String(40), server_default=DefaultClause("tluafed"),
nullable=True),
) )
if self.table.exists(): if self.table.exists():
self.table.drop() self.table.drop()
try: try:
self.table.create() self.table.create()
except sqlalchemy.exceptions.SQLError,e: except sqlalchemy.exceptions.SQLError, e:
# SQLite: database schema has changed # SQLite: database schema has changed
if not self.url.startswith('sqlite://'): if not self.url.startswith('sqlite://'):
raise raise
def _teardown(self): def _teardown(self):
if self.table.exists(): if self.table.exists():
try: try:
@ -299,90 +396,91 @@ class TestColumnChange(fixture.DB):
# SQLite: database schema has changed # SQLite: database schema has changed
if not self.url.startswith('sqlite://'): if not self.url.startswith('sqlite://'):
raise raise
#self.engine.echo=False
super(TestColumnChange, self)._teardown() super(TestColumnChange, self)._teardown()
@fixture.usedb() @fixture.usedb()
def test_rename(self): def test_rename(self):
"""Can rename a column""" """Can rename a column"""
def num_rows(col,content): def num_rows(col, content):
return len(list(self.table.select(col==content).execute())) return len(list(self.table.select(col == content).execute()))
# Table content should be preserved in changed columns # Table content should be preserved in changed columns
content = "fgsfds" content = "fgsfds"
self.engine.execute(self.table.insert(),data=content,id=42) self.engine.execute(self.table.insert(), data=content, id=42)
self.assertEquals(num_rows(self.table.c.data,content),1) self.assertEquals(num_rows(self.table.c.data, content), 1)
# ...as a function, given a column object and the new name # ...as a function, given a column object and the new name
alter_column(self.table.c.data, name='atad') alter_column('data', name='data2', table=self.table)
self.refresh_table()
alter_column(self.table.c.data2, name='atad')
self.refresh_table(self.table.name) self.refresh_table(self.table.name)
self.assert_('data' not in self.table.c.keys()) self.assert_('data' not in self.table.c.keys())
self.assert_('atad' in self.table.c.keys()) self.assert_('atad' in self.table.c.keys())
#self.assertRaises(AttributeError,getattr,self.table.c,'data') #self.assertRaises(AttributeError,getattr,self.table.c,'data')
self.table.c.atad # Should not raise exception self.table.c.atad # Should not raise exception
self.assertEquals(num_rows(self.table.c.atad,content),1) self.assertEquals(num_rows(self.table.c.atad, content), 1)
# ...as a method, given a new name # ...as a method, given a new name
self.table.c.atad.alter(name='data') self.table.c.atad.alter(name='data')
self.refresh_table(self.table.name) self.refresh_table(self.table.name)
self.assert_('atad' not in self.table.c.keys()) self.assert_('atad' not in self.table.c.keys())
self.table.c.data # Should not raise exception self.table.c.data # Should not raise exception
self.assertEquals(num_rows(self.table.c.data,content),1) self.assertEquals(num_rows(self.table.c.data, content), 1)
# ...as a function, given a new object # ...as a function, given a new object
col = Column('atad',String(40),server_default=self.table.c.data.server_default) col = Column('atad', String(40), server_default=self.table.c.data.server_default)
alter_column(self.table.c.data, col) alter_column(self.table.c.data, col)
self.refresh_table(self.table.name) self.refresh_table(self.table.name)
self.assert_('data' not in self.table.c.keys()) self.assert_('data' not in self.table.c.keys())
self.table.c.atad # Should not raise exception self.table.c.atad # Should not raise exception
self.assertEquals(num_rows(self.table.c.atad,content),1) self.assertEquals(num_rows(self.table.c.atad, content), 1)
# ...as a method, given a new object # ...as a method, given a new object
col = Column('data',String(40),server_default=self.table.c.atad.server_default) col = Column('data', String(40), server_default=self.table.c.atad.server_default)
self.table.c.atad.alter(col) self.table.c.atad.alter(col)
self.refresh_table(self.table.name) self.refresh_table(self.table.name)
self.assert_('atad' not in self.table.c.keys()) self.assert_('atad' not in self.table.c.keys())
self.table.c.data # Should not raise exception self.table.c.data # Should not raise exception
self.assertEquals(num_rows(self.table.c.data,content),1) self.assertEquals(num_rows(self.table.c.data,content), 1)
@fixture.usedb() #@fixture.usedb()
def xtest_fk(self): #def test_fk(self):
"""Can add/drop foreign key constraints to/from a column # """Can add/drop foreign key constraints to/from a column
Not supported # Not supported
""" # """
self.assert_(self.table.c.data.foreign_key is None) # self.assert_(self.table.c.data.foreign_key is None)
# add # # add
self.table.c.data.alter(foreign_key=ForeignKey(self.table.c.id)) # self.table.c.data.alter(foreign_key=ForeignKey(self.table.c.id))
self.refresh_table(self.table.name) # self.refresh_table(self.table.name)
self.assert_(self.table.c.data.foreign_key is not None) # self.assert_(self.table.c.data.foreign_key is not None)
# drop # # drop
self.table.c.data.alter(foreign_key=None) # self.table.c.data.alter(foreign_key=None)
self.refresh_table(self.table.name) # self.refresh_table(self.table.name)
self.assert_(self.table.c.data.foreign_key is None) # self.assert_(self.table.c.data.foreign_key is None)
@fixture.usedb() @fixture.usedb()
def test_type(self): def test_type(self):
"""Can change a column's type""" """Can change a column's type"""
# Entire column definition given # Entire column definition given
self.table.c.data.alter(Column('data',String(42))) self.table.c.data.alter(Column('data', String(42)))
self.refresh_table(self.table.name) self.refresh_table(self.table.name)
self.assert_(isinstance(self.table.c.data.type,String)) self.assert_(isinstance(self.table.c.data.type, String))
self.assertEquals(self.table.c.data.type.length,42) self.assertEquals(self.table.c.data.type.length, 42)
# Just the new type # Just the new type
self.table.c.data.alter(type=String(21)) self.table.c.data.alter(type=String(21))
self.refresh_table(self.table.name) self.refresh_table(self.table.name)
self.assert_(isinstance(self.table.c.data.type,String)) self.assert_(isinstance(self.table.c.data.type, String))
self.assertEquals(self.table.c.data.type.length,21) self.assertEquals(self.table.c.data.type.length, 21)
# Different type # Different type
self.assert_(isinstance(self.table.c.id.type,Integer)) self.assert_(isinstance(self.table.c.id.type, Integer))
self.assertEquals(self.table.c.id.nullable,False) self.assertEquals(self.table.c.id.nullable, False)
self.table.c.id.alter(type=String(20)) self.table.c.id.alter(type=String(20))
self.assertEquals(self.table.c.id.nullable,False) self.assertEquals(self.table.c.id.nullable, False)
self.refresh_table(self.table.name) self.refresh_table(self.table.name)
self.assert_(isinstance(self.table.c.id.type,String)) self.assert_(isinstance(self.table.c.id.type, String))
@fixture.usedb(not_supported='mysql') @fixture.usedb(not_supported='mysql')
def test_default(self): def test_default(self):
@ -391,7 +489,7 @@ class TestColumnChange(fixture.DB):
application / by SA application / by SA
""" """
#self.engine.echo=True #self.engine.echo=True
self.assertEquals(self.table.c.data.server_default.arg,'tluafed') self.assertEquals(self.table.c.data.server_default.arg, 'tluafed')
# Just the new default # Just the new default
default = 'my_default' default = 'my_default'
@ -403,7 +501,7 @@ class TestColumnChange(fixture.DB):
# Column object # Column object
default = 'your_default' default = 'your_default'
self.table.c.data.alter(Column('data',String(40),server_default=DefaultClause(default))) self.table.c.data.alter(Column('data', String(40), server_default=DefaultClause(default)))
self.refresh_table(self.table.name) self.refresh_table(self.table.name)
self.assert_(default in str(self.table.c.data.server_default.arg)) self.assert_(default in str(self.table.c.data.server_default.arg))
@ -412,90 +510,101 @@ class TestColumnChange(fixture.DB):
self.refresh_table(self.table.name) self.refresh_table(self.table.name)
# server_default isn't necessarily None for Oracle # server_default isn't necessarily None for Oracle
#self.assert_(self.table.c.data.server_default is None,self.table.c.data.server_default) #self.assert_(self.table.c.data.server_default is None,self.table.c.data.server_default)
self.engine.execute(self.table.insert(),id=11) self.engine.execute(self.table.insert(), id=11)
row = self.table.select().execute().fetchone() row = self.table.select().execute().fetchone()
self.assert_(row['data'] is None,row['data']) self.assert_(row['data'] is None, row['data'])
@fixture.usedb() @fixture.usedb()
def test_null(self): def test_null(self):
"""Can change a column's null constraint""" """Can change a column's null constraint"""
self.assertEquals(self.table.c.data.nullable,True) self.assertEquals(self.table.c.data.nullable, True)
# Column object # Column object
self.table.c.data.alter(Column('data',String(40),nullable=False)) self.table.c.data.alter(Column('data', String(40), nullable=False))
self.table.nullable=None self.table.nullable=None
self.refresh_table(self.table.name) self.refresh_table(self.table.name)
self.assertEquals(self.table.c.data.nullable,False) self.assertEquals(self.table.c.data.nullable, False)
# Just the new status # Just the new status
self.table.c.data.alter(nullable=True) self.table.c.data.alter(nullable=True)
self.refresh_table(self.table.name) self.refresh_table(self.table.name)
self.assertEquals(self.table.c.data.nullable,True) self.assertEquals(self.table.c.data.nullable, True)
@fixture.usedb() #@fixture.usedb()
def xtest_pk(self): #def test_pk(self):
"""Can add/drop a column to/from its table's primary key # """Can add/drop a column to/from its table's primary key
Not supported # Not supported
""" # """
self.engine.echo = True # self.engine.echo = True
self.assertEquals(len(self.table.primary_key),1) # self.assertEquals(len(self.table.primary_key), 1)
# Entire column definition # # Entire column definition
self.table.c.data.alter(Column('data',String,primary_key=True)) # self.table.c.data.alter(Column('data', String, primary_key=True))
self.refresh_table(self.table.name) # self.refresh_table(self.table.name)
self.assertEquals(len(self.table.primary_key),2) # self.assertEquals(len(self.table.primary_key), 2)
# # Just the new status
# self.table.c.data.alter(primary_key=False)
# self.refresh_table(self.table.name)
# self.assertEquals(len(self.table.primary_key), 1)
# Just the new status
self.table.c.data.alter(primary_key=False)
self.refresh_table(self.table.name)
self.assertEquals(len(self.table.primary_key),1)
class TestColumnDelta(fixture.Base): class TestColumnDelta(fixture.Base):
def test_deltas(self): def test_deltas(self):
def mkcol(name='id',type=String,*p,**k): def mkcol(name='id', type=String, *p, **k):
return Column(name,type,*p,**k) return Column(name, type, *p, **k)
col_orig = mkcol(primary_key=True)
def verify(expected,original,*p,**k): def verify(expected, original, *p, **k):
delta = _ColumnDelta(original,*p,**k) delta = _ColumnDelta(original, *p, **k)
result = delta.keys() result = delta.keys()
result.sort() result.sort()
self.assertEquals(expected,result) self.assertEquals(expected, result)
return delta return delta
verify([],col_orig) col_orig = mkcol(primary_key=True)
verify(['name'],col_orig,'ids')
verify([], col_orig)
verify(['name'], col_orig, 'ids')
# Parameters are always executed, even if they're 'unchanged' # Parameters are always executed, even if they're 'unchanged'
# (We can't assume given column is up-to-date) # (We can't assume given column is up-to-date)
verify(['name','primary_key','type'],col_orig,'id',Integer,primary_key=True) verify(['name', 'primary_key', 'type'],
verify(['name','primary_key','type'],col_orig,name='id',type=Integer,primary_key=True) col_orig, 'id', Integer, primary_key=True)
verify(['name', 'primary_key', 'type'],
col_orig, name='id', type=Integer, primary_key=True)
# Can compare two columns and find differences # Can compare two columns and find differences
col_new = mkcol(name='ids',primary_key=True) col_new = mkcol(name='ids', primary_key=True)
verify([],col_orig,col_orig) verify([], col_orig, col_orig)
verify(['name'],col_orig,col_orig,'ids') verify(['name'], 'ids', table=Table('test', MetaData()), name='hey')
verify(['name'],col_orig,col_orig,name='ids') verify(['name'], col_orig, col_orig, 'ids')
verify(['name'],col_orig,col_new) verify(['name'], col_orig, col_orig, name='ids')
verify(['name','type'],col_orig,col_new,type=String) verify(['name'], col_orig, col_new)
verify(['name','type'], col_orig, col_new, type=String)
# Change name, given an up-to-date definition and the current name # Change name, given an up-to-date definition and the current name
delta = verify(['name'],col_new,current_name='id') delta = verify(['name'], col_new, current_name='id')
self.assertEquals(delta.get('name'),'ids') self.assertEquals(delta.get('name'), 'ids')
# Change other params at the same time # Change other params at the same time
verify(['name','type'],col_new,current_name='id',type=String) verify(['name', 'type'], col_new, current_name='id', type=String)
# Type comparisons # Type comparisons
verify([],mkcol(type=String),mkcol(type=String)) verify([], mkcol(type=String), mkcol(type=String))
verify(['type'],mkcol(type=String),mkcol(type=Integer)) verify(['type'], mkcol(type=String), mkcol(type=Integer))
verify(['type'],mkcol(type=String),mkcol(type=String(42))) verify(['type'], mkcol(type=String), mkcol(type=String(42)))
verify([],mkcol(type=String(42)),mkcol(type=String(42))) verify([], mkcol(type=String(42)), mkcol(type=String(42)))
verify(['type'],mkcol(type=String(24)),mkcol(type=String(42))) verify(['type'], mkcol(type=String(24)), mkcol(type=String(42)))
# Other comparisons # Other comparisons
verify(['primary_key'],mkcol(nullable=False),mkcol(primary_key=True)) verify(['primary_key'], mkcol(nullable=False), mkcol(primary_key=True))
# PK implies nullable=False # PK implies nullable=False
verify(['nullable','primary_key'],mkcol(nullable=True),mkcol(primary_key=True)) verify(['nullable', 'primary_key'],
verify([],mkcol(primary_key=True),mkcol(primary_key=True)) mkcol(nullable=True), mkcol(primary_key=True))
verify(['nullable'],mkcol(nullable=True),mkcol(nullable=False)) verify([], mkcol(primary_key=True), mkcol(primary_key=True))
verify([],mkcol(nullable=True),mkcol(nullable=True)) verify(['nullable'], mkcol(nullable=True), mkcol(nullable=False))
verify(['default'],mkcol(default=None),mkcol(default='42')) verify([], mkcol(nullable=True), mkcol(nullable=True))
verify([],mkcol(default=None),mkcol(default=None)) verify(['default'], mkcol(default=None), mkcol(default='42'))
verify([],mkcol(default='42'),mkcol(default='42')) verify([], mkcol(default=None), mkcol(default=None))
verify([], mkcol(default='42'), mkcol(default='42'))

View File

@ -78,7 +78,11 @@ class TestConstraint(CommonTestConstraint):
# Add a FK by creating a FK constraint # Add a FK by creating a FK constraint
self.assertEquals(self.table.c.fkey.foreign_keys._list, []) self.assertEquals(self.table.c.fkey.foreign_keys._list, [])
fk = ForeignKeyConstraint([self.table.c.fkey], [self.table.c.id], name="fk_id_fkey") fk = ForeignKeyConstraint([self.table.c.fkey],
[self.table.c.id],
name="fk_id_fkey",
onupdate="CASCADE",
ondelete="CASCADE")
self.assert_(self.table.c.fkey.foreign_keys._list is not []) self.assert_(self.table.c.fkey.foreign_keys._list is not [])
self.assertEquals(list(fk.columns), [self.table.c.fkey]) self.assertEquals(list(fk.columns), [self.table.c.fkey])
self.assertEquals([e.column for e in fk.elements], [self.table.c.id]) self.assertEquals([e.column for e in fk.elements], [self.table.c.id])
@ -89,6 +93,13 @@ class TestConstraint(CommonTestConstraint):
index = Index('index_name', self.table.c.fkey) index = Index('index_name', self.table.c.fkey)
index.create() index.create()
fk.create() fk.create()
# test for ondelete/onupdate
fkey = self.table.c.fkey.foreign_keys._list[0]
self.assertEquals(fkey.onupdate, "CASCADE")
self.assertEquals(fkey.ondelete, "CASCADE")
# TODO: test on real db if it was set
self.refresh_table() self.refresh_table()
self.assert_(self.table.c.fkey.foreign_keys._list is not []) self.assert_(self.table.c.fkey.foreign_keys._list is not [])
@ -109,12 +120,51 @@ class TestConstraint(CommonTestConstraint):
@fixture.usedb() @fixture.usedb()
def test_drop_cascade(self): def test_drop_cascade(self):
"""Drop constraint cascaded"""
pk = PrimaryKeyConstraint('id', table=self.table, name="id_pkey") pk = PrimaryKeyConstraint('id', table=self.table, name="id_pkey")
pk.create() pk.create()
self.refresh_table() self.refresh_table()
# Drop the PK constraint forcing cascade # Drop the PK constraint forcing cascade
pk.drop(cascade=True) pk.drop(cascade=True)
# TODO: add real assertion if it was added
@fixture.usedb(supported=['mysql'])
def test_fail_mysql_check_constraints(self):
"""Check constraints raise NotSupported for mysql on drop"""
cons = CheckConstraint('id > 3', name="id_check", table=self.table)
cons.create()
self.refresh_table()
try:
cons.drop()
except NotSupportedError:
pass
else:
self.fail()
@fixture.usedb(not_supported=['sqlite', 'mysql'])
def test_named_check_constraints(self):
"""Check constraints can be defined, created, and dropped"""
self.assertRaises(InvalidConstraintError,
CheckConstraint, 'id > 3')
cons = CheckConstraint('id > 3', name="id_check", table=self.table)
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.drop()
self.refresh_table()
self.table.insert(values={'id': 2}).execute()
self.table.insert(values={'id': 1}).execute()
class TestAutoname(CommonTestConstraint): class TestAutoname(CommonTestConstraint):
@ -154,10 +204,6 @@ class TestAutoname(CommonTestConstraint):
def test_autoname_fk(self): def test_autoname_fk(self):
"""ForeignKeyConstraints can guess their name if None is given""" """ForeignKeyConstraints can guess their name if None is given"""
cons = ForeignKeyConstraint([self.table.c.fkey], [self.table.c.id]) 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() cons.create()
self.refresh_table() self.refresh_table()
self.table.c.fkey.foreign_keys[0].column is self.table.c.id self.table.c.fkey.foreign_keys[0].column is self.table.c.id
@ -170,10 +216,6 @@ class TestAutoname(CommonTestConstraint):
# test string names # test string names
cons = ForeignKeyConstraint(['fkey'], ['%s.id' % self.tablename], table=self.table) 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() cons.create()
self.refresh_table() self.refresh_table()
self.table.c.fkey.foreign_keys[0].column is self.table.c.id self.table.c.fkey.foreign_keys[0].column is self.table.c.id
@ -182,7 +224,7 @@ class TestAutoname(CommonTestConstraint):
cons.name = None cons.name = None
cons.drop() cons.drop()
@fixture.usedb(not_supported=['oracle', 'sqlite']) @fixture.usedb(not_supported=['oracle', 'sqlite', 'mysql'])
def test_autoname_check(self): def test_autoname_check(self):
"""CheckConstraints can guess their name if None is given""" """CheckConstraints can guess their name if None is given"""
cons = CheckConstraint('id > 3', columns=[self.table.c.id]) cons = CheckConstraint('id > 3', columns=[self.table.c.id])
@ -190,20 +232,21 @@ class TestAutoname(CommonTestConstraint):
self.refresh_table() self.refresh_table()
self.table.insert(values={'id': 4}).execute() if not self.engine.name == 'mysql':
try: self.table.insert(values={'id': 4}).execute()
self.table.insert(values={'id': 1}).execute() try:
except IntegrityError: self.table.insert(values={'id': 1}).execute()
pass except IntegrityError:
else: pass
self.fail() else:
self.fail()
# Remove the name, drop the constraint; it should succeed # Remove the name, drop the constraint; it should succeed
cons.name = None cons.name = None
cons.drop() cons.drop()
self.refresh_table() self.refresh_table()
self.table.insert(values={'id': 2}).execute() self.table.insert(values={'id': 2}).execute()
self.table.insert(values={'id': 5}).execute() self.table.insert(values={'id': 1}).execute()
@fixture.usedb(not_supported=['oracle', 'sqlite']) @fixture.usedb(not_supported=['oracle', 'sqlite'])
def test_autoname_unique(self): def test_autoname_unique(self):

View File

@ -130,7 +130,7 @@ class DB(Base):
def _not_supported(self, url): def _not_supported(self, url):
return not self._supported(url) return not self._supported(url)
def refresh_table(self,name=None): def refresh_table(self, name=None):
"""Reload the table from the database """Reload the table from the database
Assumes we're working with only a single table, self.table, and Assumes we're working with only a single table, self.table, and
metadata self.meta metadata self.meta

View File

@ -196,7 +196,7 @@ class TestControlledSchema(fixture.Pathed, fixture.DB):
def construct_model(self): def construct_model(self):
meta = MetaData() meta = MetaData()
user = Table('temp_model_schema', meta, Column('id', Integer), Column('user', String)) user = Table('temp_model_schema', meta, Column('id', Integer), Column('user', String(245)))
return meta return meta