adding basic support for firebird, fixes #55

This commit is contained in:
iElectric 2009-06-22 10:22:06 +00:00
parent 17cc5f36e6
commit a8c31eb25f
12 changed files with 198 additions and 96 deletions

View File

@ -39,6 +39,16 @@ Module :mod:`mysql <migrate.changeset.databases.mysql>`
:members:
:synopsis: MySQL database specific changeset implementations
.. _firebird-d:
Module :mod:`firebird <migrate.changeset.databases.firebird>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: migrate.changeset.databases.firebird
:members:
:synopsis: Firebird database specific changeset implementations
.. _oracle-d:
Module :mod:`oracle <migrate.changeset.databases.oracle>`

View File

@ -1,6 +1,7 @@
0.5.5
-----
- added support for :ref:`firebird <firebird-d>`
- 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

View File

@ -43,40 +43,40 @@ Download and Development
Dialect support
---------------
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| Operation / Dialect | :ref:`sqlite <sqlite-d>` | :ref:`postgres <postgres-d>` | :ref:`mysql <mysql-d>` | :ref:`oracle <oracle-d>` | firebird | mssql |
| | | | | | | |
+=========================================================+==========================+==============================+========================+===========================+==========+=======+
| :ref:`ALTER TABLE RENAME TABLE <table-rename>` | yes | yes | yes | yes | | |
| | | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| :ref:`ALTER TABLE RENAME COLUMN <column-alter>` | yes | yes | yes | yes | | |
| | (workaround) [#1]_ | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| :ref:`ALTER TABLE ADD COLUMN <column-create>` | yes | yes | yes | yes | | |
| | (with limitations) [#2]_ | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| :ref:`ALTER TABLE DROP COLUMN <column-drop>` | yes | yes | yes | yes | | |
| | (workaround) [#1]_ | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| :ref:`ALTER TABLE ALTER COLUMN <column-alter>` | no | yes | yes | yes | | |
| | | | | (with limitations) [#3]_ | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| :ref:`ALTER TABLE ADD CONSTRAINT <constraint-tutorial>` | no | yes | yes | yes | | |
| | | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| :ref:`ALTER TABLE DROP CONSTRAINT <constraint-tutorial>`| no | yes | yes | yes | | |
| | | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
| :ref:`RENAME INDEX <index-rename>` | no | yes | no | yes | | |
| | | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+----------+-------+
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+-------------------------------+-------+
| Operation / Dialect | :ref:`sqlite <sqlite-d>` | :ref:`postgres <postgres-d>` | :ref:`mysql <mysql-d>` | :ref:`oracle <oracle-d>` | :ref:`firebird <firebird-d>` | mssql |
| | | | | | | |
+=========================================================+==========================+==============================+========================+===========================+===============================+=======+
| :ref:`ALTER TABLE RENAME TABLE <table-rename>` | yes | yes | yes | yes | no | |
| | | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+-------------------------------+-------+
| :ref:`ALTER TABLE RENAME COLUMN <column-alter>` | yes | yes | yes | yes | yes | |
| | (workaround) [#1]_ | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+-------------------------------+-------+
| :ref:`ALTER TABLE ADD COLUMN <column-create>` | yes | yes | yes | yes | yes | |
| | (with limitations) [#2]_ | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+-------------------------------+-------+
| :ref:`ALTER TABLE DROP COLUMN <column-drop>` | yes | yes | yes | yes | yes | |
| | (workaround) [#1]_ | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+-------------------------------+-------+
| :ref:`ALTER TABLE ALTER COLUMN <column-alter>` | no | yes | yes | yes | yes [#4]_ | |
| | | | | (with limitations) [#3]_ | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+-------------------------------+-------+
| :ref:`ALTER TABLE ADD CONSTRAINT <constraint-tutorial>` | no | yes | yes | yes | yes | |
| | | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+-------------------------------+-------+
| :ref:`ALTER TABLE DROP CONSTRAINT <constraint-tutorial>`| no | yes | yes | yes | yes | |
| | | | | | | |
+---------------------------------------------------------+--------------------------+------------------------------+------------------------+---------------------------+-------------------------------+-------+
| :ref:`RENAME INDEX <index-rename>` | no | yes | no | yes | 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.
.. [#3] You can not change datatype or rename column if table has NOT NULL data, see http://blogs.x2line.com/al/archive/2005/08/30/1231.aspx for more information.
.. [#4] Changing nullable is not supported
Documentation
-------------

View File

@ -189,7 +189,7 @@ class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator):
"""Starts ALTER COLUMN"""
self.start_alter_table(table)
# TODO: use preparer.format_column
self.append("ALTER COLUMN %s " % self.preparer.quote_identifier(col_name))
self.append("ALTER COLUMN %s " % self.preparer.quote(col_name, table.quote))
def _visit_column_nullable(self, table, col_name, delta):
nullable = delta['nullable']
@ -306,9 +306,12 @@ class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
constraint.name = self.get_constraint_name(constraint)
self.append(self.preparer.format_constraint(constraint))
if constraint.cascade:
self.append(" CASCADE")
self.cascade_constraint(constraint)
self.execute()
def cascade_constraint(self, constraint):
self.append(" CASCADE")
class ANSIDialect(DefaultDialect):
columngenerator = ANSIColumnGenerator

View File

@ -39,6 +39,7 @@ class ConstraintChangeset(object):
:keyword:`None` the instance's engine will be used
:type engine: :class:`sqlalchemy.engine.base.Engine`
"""
# TODO: set the parent here instead of in __init__
self.__do_imports('constraintgenerator', *a, **kw)
def drop(self, *a, **kw):

View File

@ -0,0 +1,62 @@
"""
Firebird database specific implementations of changeset classes.
"""
from sqlalchemy.databases import firebird as sa_base
from migrate.changeset import ansisql, exceptions
FBSchemaGenerator = sa_base.FBSchemaGenerator
class FBColumnGenerator(FBSchemaGenerator, ansisql.ANSIColumnGenerator):
"""Firebird column generator implementation."""
class FBColumnDropper(ansisql.ANSIColumnDropper):
"""Firebird column dropper implementation."""
def visit_column(self, column):
table = self.start_alter_table(column)
self.append('DROP %s' % self.preparer.format_column(column))
self.execute()
class FBSchemaChanger(ansisql.ANSISchemaChanger):
"""Firebird schema changer implementation."""
def visit_table(self, table):
"""Rename table not supported"""
raise exceptions.NotSupportedError(
"Firebird does not support renaming tables.")
def _visit_column_name(self, table, col_name, delta):
new_name = delta['name']
self.start_alter_table(table)
self.append('ALTER COLUMN %s TO %s' % ((col_name), (new_name)))
def _visit_column_nullable(self, table, col_name, delta):
"""Changing NULL is not supported"""
# TODO: http://www.firebirdfaq.org/faq103/
raise exceptions.NotSupportedError(
"Firebird does not support altering NULL bevahior.")
class FBConstraintGenerator(ansisql.ANSIConstraintGenerator):
"""Firebird constraint generator implementation."""
class FBConstraintDropper(ansisql.ANSIConstraintDropper):
"""Firebird constaint dropper implementation."""
def cascade_constraint(self, constraint):
raise exceptions.NotSupportedError(
"Firebird does not support cascading constraints")
class FBDialect(ansisql.ANSIDialect):
columngenerator = FBColumnGenerator
columndropper = FBColumnDropper
schemachanger = FBSchemaChanger
constraintgenerator = FBConstraintGenerator
constraintdropper = FBConstraintDropper

View File

@ -4,7 +4,11 @@
import sqlalchemy as sa
from migrate.changeset import ansisql
from migrate.changeset.databases import sqlite, postgres, mysql, oracle
from migrate.changeset.databases import (sqlite,
postgres,
mysql,
oracle,
firebird)
# Map SA dialects to the corresponding Migrate extensions
@ -14,6 +18,7 @@ DIALECTS = {
sa.databases.postgres.PGDialect: postgres.PGDialect,
sa.databases.mysql.MySQLDialect: mysql.MySQLDialect,
sa.databases.oracle.OracleDialect: oracle.OracleDialect,
sa.databases.firebird.FBDialect: firebird.FBDialect,
}

View File

@ -3,7 +3,7 @@ from sqlalchemy.util import OrderedDict
__all__ = ['databases', 'operations']
databases = ('sqlite', 'postgres', 'mysql', 'oracle', 'mssql')
databases = ('sqlite', 'postgres', 'mysql', 'oracle', 'mssql', 'firebird')
# Map operation names to function names
operations = OrderedDict()

View File

@ -163,7 +163,7 @@ class ControlledSchema(object):
table = Table(
tname, meta,
Column('repository_id', String(255), primary_key=True),
Column('repository_id', String(250), primary_key=True),
Column('repository_path', Text),
Column('version', Integer), )

View File

@ -183,7 +183,7 @@ class TestAddDropColumn(fixture.DB):
@fixture.usedb(not_supported='sqlite')
def test_pk(self):
"""Can create columns with primary key"""
col = Column('data', Integer)
col = Column('data', Integer, nullable=False)
self.assertRaises(changeset.exceptions.InvalidConstraintError,
col.create, self.table, primary_key_name=True)
col.create(self.table, primary_key_name='data_pkey')
@ -192,7 +192,8 @@ class TestAddDropColumn(fixture.DB):
self.table.insert(values={'data': 4}).execute()
try:
self.table.insert(values={'data': 4}).execute()
except sqlalchemy.exc.IntegrityError:
except (sqlalchemy.exc.IntegrityError,
sqlalchemy.exc.ProgrammingError):
pass
else:
self.fail()
@ -211,7 +212,8 @@ class TestAddDropColumn(fixture.DB):
self.table.insert(values={'data': 5}).execute()
try:
self.table.insert(values={'data': 3}).execute()
except sqlalchemy.exc.IntegrityError:
except (sqlalchemy.exc.IntegrityError,
sqlalchemy.exc.ProgrammingError):
pass
else:
self.fail()
@ -230,7 +232,8 @@ class TestAddDropColumn(fixture.DB):
self.table.insert(values={'data': 5}).execute()
try:
self.table.insert(values={'data': 5}).execute()
except sqlalchemy.exc.IntegrityError:
except (sqlalchemy.exc.IntegrityError,
sqlalchemy.exc.ProgrammingError):
pass
else:
self.fail()
@ -249,11 +252,13 @@ class TestAddDropColumn(fixture.DB):
self.table.insert(values={'data': 5}).execute()
try:
self.table.insert(values={'data': 5}).execute()
except sqlalchemy.exc.IntegrityError:
except (sqlalchemy.exc.IntegrityError,
sqlalchemy.exc.ProgrammingError):
pass
else:
self.fail()
Index('ix_data', col).drop(bind=self.engine)
col.drop()
@fixture.usedb()
@ -272,6 +277,7 @@ class TestAddDropColumn(fixture.DB):
# TODO: test that if column is appended on creation and removed on deletion
# TODO: test column.alter with all changes at one time
# TODO: test quoting
# TODO: test drop default
class TestRename(fixture.DB):
@ -283,7 +289,7 @@ class TestRename(fixture.DB):
super(TestRename, self)._setup(url)
self.meta.bind = self.engine
@fixture.usedb()
@fixture.usedb(not_supported='firebird')
def test_rename_table(self):
"""Tables can be renamed"""
c_name = 'col_1'
@ -463,21 +469,29 @@ class TestColumnChange(fixture.DB):
self.table.c.data.alter(Column('data', String(42)))
self.refresh_table(self.table.name)
self.assert_(isinstance(self.table.c.data.type, String))
self.assertEquals(self.table.c.data.type.length, 42)
if self.engine.name == 'firebird':
self.assertEquals(self.table.c.data.type.length, 42 * 4)
else:
self.assertEquals(self.table.c.data.type.length, 42)
# Just the new type
self.table.c.data.alter(type=String(21))
self.table.c.data.alter(type=String(43))
self.refresh_table(self.table.name)
self.assert_(isinstance(self.table.c.data.type, String))
self.assertEquals(self.table.c.data.type.length, 21)
if self.engine.name == 'firebird':
self.assertEquals(self.table.c.data.type.length, 43 * 4)
else:
self.assertEquals(self.table.c.data.type.length, 43)
# Different type
self.assert_(isinstance(self.table.c.id.type, Integer))
self.assertEquals(self.table.c.id.nullable, False)
self.table.c.id.alter(type=String(20))
self.assertEquals(self.table.c.id.nullable, False)
self.refresh_table(self.table.name)
self.assert_(isinstance(self.table.c.id.type, String))
if not self.engine.name == 'firebird':
self.table.c.id.alter(type=String(20))
self.assertEquals(self.table.c.id.nullable, False)
self.refresh_table(self.table.name)
self.assert_(isinstance(self.table.c.id.type, String))
@fixture.usedb()
def test_default(self):
@ -511,14 +525,14 @@ class TestColumnChange(fixture.DB):
self.assert_(row['data'] is None, row['data'])
@fixture.usedb()
@fixture.usedb(not_supported='firebird')
def test_null(self):
"""Can change a column's null constraint"""
self.assertEquals(self.table.c.data.nullable, True)
# Column object
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.assertEquals(self.table.c.data.nullable, False)

View File

@ -6,6 +6,7 @@ from sqlalchemy.util import *
from sqlalchemy.exc import *
from migrate.changeset import *
from migrate.changeset.exceptions import *
from test import fixture
@ -31,8 +32,8 @@ class CommonTestConstraint(fixture.DB):
self.meta = MetaData(self.engine)
self.tablename = 'mytable'
self.table = Table(self.tablename, self.meta,
Column('id', Integer),
Column('fkey', Integer),
Column('id', Integer, nullable=False),
Column('fkey', Integer, nullable=False),
mysql_engine='InnoDB')
if self.engine.has_table(self.table.name):
self.table.drop()
@ -49,20 +50,21 @@ class TestConstraint(CommonTestConstraint):
def _define_pk(self, *cols):
# Add a pk by creating a PK constraint
pk = PrimaryKeyConstraint(table=self.table, *cols)
self.assertEquals(list(pk.columns), list(cols))
if self.url.startswith('oracle'):
if (self.engine.name in ('oracle', 'firebird')):
# Can't drop Oracle PKs without an explicit name
pk.name = 'fgsfds'
pk = PrimaryKeyConstraint(table=self.table, name='temp_pk_key', *cols)
else:
pk = PrimaryKeyConstraint(table=self.table, *cols)
self.assertEquals(list(pk.columns), list(cols))
pk.create()
self.refresh_table()
if not self.url.startswith('sqlite'):
self.assertEquals(list(self.table.primary_key), list(cols))
# Drop the PK constraint
if not self.url.startswith('oracle'):
# Apparently Oracle PK names aren't introspected
pk.name = self.table.primary_key.name
#if (self.engine.name in ('oracle', 'firebird')):
# # Apparently Oracle PK names aren't introspected
# pk.name = self.table.primary_key.name
pk.drop()
self.refresh_table()
self.assertEquals(len(self.table.primary_key), 0)
@ -113,18 +115,23 @@ class TestConstraint(CommonTestConstraint):
@fixture.usedb()
def test_define_pk_multi(self):
"""Multicolumn PK constraints can be defined, created, and dropped"""
#self.engine.echo=True
self._define_pk(self.table.c.id, self.table.c.fkey)
@fixture.usedb()
def test_drop_cascade(self):
"""Drop constraint cascaded"""
pk = PrimaryKeyConstraint('fkey', table=self.table, name="id_pkey")
pk.create()
self.refresh_table()
# Drop the PK constraint forcing cascade
pk.drop(cascade=True)
try:
pk.drop(cascade=True)
except NotSupportedError:
if self.engine.name == 'firebird':
pass
# TODO: add real assertion if it was added
@fixture.usedb(supported=['mysql'])
@ -149,10 +156,10 @@ class TestConstraint(CommonTestConstraint):
cons.create()
self.refresh_table()
self.table.insert(values={'id': 4}).execute()
self.table.insert(values={'id': 4, 'fkey': 1}).execute()
try:
self.table.insert(values={'id': 1}).execute()
except IntegrityError:
self.table.insert(values={'id': 1, 'fkey': 1}).execute()
except (IntegrityError, ProgrammingError):
pass
else:
self.fail()
@ -160,8 +167,8 @@ class TestConstraint(CommonTestConstraint):
# 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()
self.table.insert(values={'id': 2, 'fkey': 2}).execute()
self.table.insert(values={'id': 1, 'fkey': 2}).execute()
class TestAutoname(CommonTestConstraint):
@ -170,7 +177,7 @@ class TestAutoname(CommonTestConstraint):
"""
level = fixture.DB.CONNECT
@fixture.usedb(not_supported='oracle')
@fixture.usedb(not_supported=['oracle', 'firebird'])
def test_autoname_pk(self):
"""PrimaryKeyConstraints can guess their name if None is given"""
# Don't supply a name; it should create one
@ -197,7 +204,7 @@ class TestAutoname(CommonTestConstraint):
cons.name = None
cons.drop()
@fixture.usedb(not_supported=['oracle', 'sqlite'])
@fixture.usedb(not_supported=['oracle', 'sqlite', 'firebird'])
def test_autoname_fk(self):
"""ForeignKeyConstraints can guess their name if None is given"""
cons = PrimaryKeyConstraint(self.table.c.id)
@ -230,13 +237,12 @@ class TestAutoname(CommonTestConstraint):
cons = CheckConstraint('id > 3', columns=[self.table.c.id])
cons.create()
self.refresh_table()
if not self.engine.name == 'mysql':
self.table.insert(values={'id': 4}).execute()
self.table.insert(values={'id': 4, 'fkey': 1}).execute()
try:
self.table.insert(values={'id': 1}).execute()
except IntegrityError:
self.table.insert(values={'id': 1, 'fkey': 2}).execute()
except (IntegrityError, ProgrammingError):
pass
else:
self.fail()
@ -245,8 +251,8 @@ class TestAutoname(CommonTestConstraint):
cons.name = None
cons.drop()
self.refresh_table()
self.table.insert(values={'id': 2}).execute()
self.table.insert(values={'id': 1}).execute()
self.table.insert(values={'id': 2, 'fkey': 2}).execute()
self.table.insert(values={'id': 1, 'fkey': 3}).execute()
@fixture.usedb(not_supported=['oracle', 'sqlite'])
def test_autoname_unique(self):
@ -254,12 +260,12 @@ class TestAutoname(CommonTestConstraint):
cons = UniqueConstraint(self.table.c.fkey)
cons.create()
self.refresh_table()
self.table.insert(values={'fkey': 4}).execute()
self.table.insert(values={'fkey': 4, 'id': 1}).execute()
try:
self.table.insert(values={'fkey': 4}).execute()
except IntegrityError:
self.table.insert(values={'fkey': 4, 'id': 2}).execute()
except (sqlalchemy.exc.IntegrityError,
sqlalchemy.exc.ProgrammingError):
pass
else:
self.fail()
@ -268,5 +274,5 @@ class TestAutoname(CommonTestConstraint):
cons.name = None
cons.drop()
self.refresh_table()
self.table.insert(values={'fkey': 4}).execute()
self.table.insert(values={'fkey': 4}).execute()
self.table.insert(values={'fkey': 4, 'id': 2}).execute()
self.table.insert(values={'fkey': 4, 'id': 1}).execute()

View File

@ -139,20 +139,20 @@ class TestSchemaDiff(fixture.DB):
# Not even using sqlalchemy.PassiveDefault helps because we're doing explicit column select.
self.engine.execute(self.table.delete(), id=dataId)
# Change column nullable in model.
self.meta.remove(self.table)
self.table = Table(self.table_name,self.meta,
Column('id',Integer(),primary_key=True),
Column('name',UnicodeText(length=None)),
Column('data2',String(255),nullable=False),
)
assertDiff(True, [], [], [self.table_name]) # TODO test nullable diff
# Apply latest model changes and find no more diffs.
self._applyLatestModel()
assertDiff(False, [], [], [])
# Remove table from model.
self.meta.remove(self.table)
assertDiff(True, [], [self.table_name], [])
if not self.engine.name == 'firebird':
# Change column nullable in model.
self.meta.remove(self.table)
self.table = Table(self.table_name,self.meta,
Column('id',Integer(),primary_key=True),
Column('name',UnicodeText(length=None)),
Column('data2',String(255),nullable=False),
)
assertDiff(True, [], [], [self.table_name]) # TODO test nullable diff
# Apply latest model changes and find no more diffs.
self._applyLatestModel()
assertDiff(False, [], [], [])
# Remove table from model.
self.meta.remove(self.table)
assertDiff(True, [], [self.table_name], [])