Merge tag '0.8.10' into debian/ocata

Change-Id: I7bc453ba285514e10b38803a77634b9cf08bcfe0
This commit is contained in:
Ondřej Nový 2017-02-26 11:36:04 +01:00
commit 0d2a1dc76b
44 changed files with 702 additions and 166 deletions

View File

@ -1,6 +1,6 @@
This is the MIT license: http://www.opensource.org/licenses/mit-license.php
Copyright (C) 2009-2016 by Michael Bayer.
Copyright (C) 2009-2017 by Michael Bayer.
Alembic is a trademark of Michael Bayer.
Permission is hereby granted, free of charge, to any person obtaining a copy of this

View File

@ -1,6 +1,6 @@
from os import path
__version__ = '0.8.8'
__version__ = '0.8.10'
package_dir = path.abspath(path.dirname(__file__))

View File

@ -195,11 +195,14 @@ def _make_index(params, conn_table):
def _make_unique_constraint(params, conn_table):
# TODO: add .info such as 'duplicates_index'
return sa_schema.UniqueConstraint(
uq = sa_schema.UniqueConstraint(
*[conn_table.c[cname] for cname in params['column_names']],
name=params['name']
)
if 'duplicates_index' in params:
uq.info['duplicates_index'] = params['duplicates_index']
return uq
def _make_foreign_key(params, conn_table):
@ -364,6 +367,8 @@ def _compare_indexes_and_uniques(
supports_unique_constraints = False
unique_constraints_duplicate_unique_indexes = False
if conn_table is not None:
# 1b. ... and from connection, if the table exists
if hasattr(inspector, "get_unique_constraints"):
@ -373,6 +378,15 @@ def _compare_indexes_and_uniques(
supports_unique_constraints = True
except NotImplementedError:
pass
except TypeError:
# number of arguments is off for the base
# method in SQLAlchemy due to the cache decorator
# not being present
pass
else:
for uq in conn_uniques:
if uq.get('duplicates_index'):
unique_constraints_duplicate_unique_indexes = True
try:
conn_indexes = inspector.get_indexes(tname, schema=schema)
except NotImplementedError:
@ -384,6 +398,16 @@ def _compare_indexes_and_uniques(
for uq_def in conn_uniques)
conn_indexes = set(_make_index(ix, conn_table) for ix in conn_indexes)
# 2a. if the dialect dupes unique indexes as unique constraints
# (mysql and oracle), correct for that
if unique_constraints_duplicate_unique_indexes:
_correct_for_uq_duplicates_uix(
conn_uniques, conn_indexes,
metadata_unique_constraints,
metadata_indexes
)
# 3. give the dialect a chance to omit indexes and constraints that
# we know are either added implicitly by the DB or that the DB
# can't accurately report on
@ -581,6 +605,48 @@ def _compare_indexes_and_uniques(
obj_added(unnamed_metadata_uniques[uq_sig])
def _correct_for_uq_duplicates_uix(
conn_unique_constraints,
conn_indexes,
metadata_unique_constraints,
metadata_indexes):
# dedupe unique indexes vs. constraints, since MySQL / Oracle
# doesn't really have unique constraints as a separate construct.
# but look in the metadata and try to maintain constructs
# that already seem to be defined one way or the other
# on that side. This logic was formerly local to MySQL dialect,
# generalized to Oracle and others. See #276
metadata_uq_names = set([
cons.name for cons in metadata_unique_constraints
if cons.name is not None])
unnamed_metadata_uqs = set([
_uq_constraint_sig(cons).sig
for cons in metadata_unique_constraints
if cons.name is None
])
metadata_ix_names = set([
cons.name for cons in metadata_indexes if cons.unique])
conn_ix_names = dict(
(cons.name, cons) for cons in conn_indexes if cons.unique
)
uqs_dupe_indexes = dict(
(cons.name, cons) for cons in conn_unique_constraints
if cons.info['duplicates_index']
)
for overlap in uqs_dupe_indexes:
if overlap not in metadata_uq_names:
if _uq_constraint_sig(uqs_dupe_indexes[overlap]).sig \
not in unnamed_metadata_uqs:
conn_unique_constraints.discard(uqs_dupe_indexes[overlap])
elif overlap not in metadata_ix_names:
conn_indexes.discard(conn_ix_names[overlap])
@comparators.dispatch_for("column")
def _compare_nullable(
autogen_context, alter_column_op, schema, tname, cname, conn_col,

View File

@ -52,8 +52,7 @@ def _render_cmd_body(op_container, autogen_context):
printer = PythonPrinter(buf)
printer.writeline(
"### commands auto generated by Alembic - "
"please adjust! ###"
"# ### commands auto generated by Alembic - please adjust! ###"
)
if not op_container.ops:
@ -65,7 +64,7 @@ def _render_cmd_body(op_container, autogen_context):
for line in lines:
printer.writeline(line)
printer.writeline("### end Alembic commands ###")
printer.writeline("# ### end Alembic commands ###")
return buf.getvalue()

View File

@ -256,7 +256,7 @@ class DefaultImpl(with_metaclass(ImplMeta)):
):
comparator = _type_comparators.get(conn_type._type_affinity, None)
return comparator and comparator(metadata_type, conn_type)
return comparator and comparator(metadata_impl, conn_type)
else:
return True

View File

@ -10,7 +10,7 @@ from .base import ColumnNullable, ColumnName, ColumnDefault, \
format_server_default
from .base import alter_table
from ..autogenerate import compare
from ..util.sqla_compat import _is_type_bound
from ..util.sqla_compat import _is_type_bound, sqla_100
class MySQLImpl(DefaultImpl):
@ -132,6 +132,19 @@ class MySQLImpl(DefaultImpl):
if idx.name in removed:
metadata_indexes.remove(idx)
if not sqla_100:
self._legacy_correct_for_dupe_uq_uix(
conn_unique_constraints,
conn_indexes,
metadata_unique_constraints,
metadata_indexes
)
def _legacy_correct_for_dupe_uq_uix(self, conn_unique_constraints,
conn_indexes,
metadata_unique_constraints,
metadata_indexes):
# then dedupe unique indexes vs. constraints, since MySQL
# doesn't really have unique constraints as a separate construct.
# but look in the metadata and try to maintain constructs

View File

@ -162,7 +162,9 @@ class PostgresqlImpl(DefaultImpl):
else:
exprs = idx.columns
for expr in exprs:
if not isinstance(expr, (Column, UnaryExpression)):
while isinstance(expr, UnaryExpression):
expr = expr.element
if not isinstance(expr, Column):
util.warn(
"autogenerate skipping functional index %s; "
"not supported by SQLAlchemy reflection" % idx.name

View File

@ -1,5 +1,5 @@
from sqlalchemy import Table, MetaData, Index, select, Column, \
ForeignKeyConstraint, cast, CheckConstraint
ForeignKeyConstraint, PrimaryKeyConstraint, cast, CheckConstraint
from sqlalchemy import types as sqltypes
from sqlalchemy import schema as sql_schema
from sqlalchemy.util import OrderedDict
@ -301,6 +301,10 @@ class ApplyBatchImpl(object):
existing.type._create_events = \
existing.type.create_constraint = False
if existing.type._type_affinity is not type_._type_affinity:
existing_transfer["expr"] = cast(
existing_transfer["expr"], type_)
existing.type = type_
# we *dont* however set events for the new type, because
@ -308,7 +312,6 @@ class ApplyBatchImpl(object):
# Operations.implementation_for(alter_column) which already
# will emit an add_constraint()
existing_transfer["expr"] = cast(existing_transfer["expr"], type_)
if nullable is not None:
existing.nullable = nullable
if server_default is not False:
@ -342,7 +345,7 @@ class ApplyBatchImpl(object):
if not const.name:
raise ValueError("Constraint must have a name")
try:
del self.named_constraints[const.name]
const = self.named_constraints.pop(const.name)
except KeyError:
if _is_type_bound(const):
# type-bound constraints are only included in the new
@ -351,6 +354,10 @@ class ApplyBatchImpl(object):
# Operations.implementation_for(alter_column)
return
raise ValueError("No such constraint: '%s'" % const.name)
else:
if isinstance(const, PrimaryKeyConstraint):
for col in const.columns:
self.columns[col.name].primary_key = False
def create_index(self, idx):
self.new_indexes[idx.name] = idx

View File

@ -405,6 +405,16 @@ class EnvironmentContext(util.ModuleClsProxy):
The default is ``'alembic_version'``.
:param version_table_schema: Optional schema to place version
table within.
:param version_table_pk: boolean, whether the Alembic version table
should use a primary key constraint for the "value" column; this
only takes effect when the table is first created.
Defaults to True; setting to False should not be necessary and is
here for backwards compatibility reasons.
.. versionadded:: 0.8.10 Added the
:paramref:`.EnvironmentContext.configure.version_table_pk`
flag and additionally established that the Alembic version table
has a primary key constraint by default.
Parameters specific to the autogenerate feature, when
``alembic revision`` is run with the ``--autogenerate`` feature:

View File

@ -2,7 +2,8 @@ import logging
import sys
from contextlib import contextmanager
from sqlalchemy import MetaData, Table, Column, String, literal_column
from sqlalchemy import MetaData, Table, Column, String, literal_column,\
PrimaryKeyConstraint
from sqlalchemy.engine.strategies import MockEngineStrategy
from sqlalchemy.engine import url as sqla_url
@ -98,6 +99,12 @@ class MigrationContext(object):
version_table, MetaData(),
Column('version_num', String(32), nullable=False),
schema=version_table_schema)
if opts.get("version_table_pk", True):
self._version.append_constraint(
PrimaryKeyConstraint(
'version_num', name="%s_pkc" % version_table
)
)
self._start_from_rev = opts.get("starting_rev")
self.impl = ddl.DefaultImpl.get_by_dialect(dialect)(
@ -230,7 +237,9 @@ class MigrationContext(object):
"""
if self.as_sql:
start_from_rev = self._start_from_rev
if start_from_rev is not None and self.script:
if start_from_rev == 'base':
start_from_rev = None
elif start_from_rev is not None and self.script:
start_from_rev = \
self.script.get_revision(start_from_rev).revision

View File

@ -9,8 +9,8 @@ from ..runtime import migration
from contextlib import contextmanager
_sourceless_rev_file = re.compile(r'(?!__init__)(.*\.py)(c|o)?$')
_only_source_rev_file = re.compile(r'(?!__init__)(.*\.py)$')
_sourceless_rev_file = re.compile(r'(?!\.\#|__init__)(.*\.py)(c|o)?$')
_only_source_rev_file = re.compile(r'(?!\.\#|__init__)(.*\.py)$')
_legacy_rev = re.compile(r'([a-f0-9]+)\.py$')
_mod_def_re = re.compile(r'(upgrade|downgrade)_([a-z0-9]+)')
_slug_re = re.compile(r'\w+')
@ -82,8 +82,17 @@ class ScriptDirectory(object):
else:
paths = [self.versions]
dupes = set()
for vers in paths:
for file_ in os.listdir(vers):
path = os.path.realpath(os.path.join(vers, file_))
if path in dupes:
util.warn(
"File %s loaded twice! ignoring. Please ensure "
"version_locations is unique." % path
)
continue
dupes.add(path)
script = Script._from_filename(self, vers, file_)
if script is None:
continue

View File

@ -15,6 +15,7 @@ down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}

View File

@ -1,5 +1,5 @@
# testing/config.py
# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
# Copyright (C) 2005-2017 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under

View File

@ -1,5 +1,5 @@
# testing/engines.py
# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
# Copyright (C) 2005-2017 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under

View File

@ -115,7 +115,7 @@ datefmt = %%H:%%M:%%S
def _multi_dir_testing_config(sourceless=False):
def _multi_dir_testing_config(sourceless=False, extra_version_location=''):
dir_ = os.path.join(_get_staging_directory(), 'scripts')
url = "sqlite:///%s/foo.db" % dir_
@ -124,7 +124,7 @@ def _multi_dir_testing_config(sourceless=False):
script_location = %s
sqlalchemy.url = %s
sourceless = %s
version_locations = %%(here)s/model1/ %%(here)s/model2/ %%(here)s/model3/
version_locations = %%(here)s/model1/ %%(here)s/model2/ %%(here)s/model3/ %s
[loggers]
keys = root
@ -149,7 +149,8 @@ keys = generic
[formatter_generic]
format = %%(levelname)-5.5s [%%(name)s] %%(message)s
datefmt = %%H:%%M:%%S
""" % (dir_, url, "true" if sourceless else "false"))
""" % (dir_, url, "true" if sourceless else "false",
extra_version_location))
def _no_sql_testing_config(dialect="postgresql", directives=""):
@ -256,9 +257,11 @@ down_revision = None
from alembic import op
def upgrade():
op.execute("CREATE STEP 1")
def downgrade():
op.execute("DROP STEP 1")
@ -272,9 +275,11 @@ down_revision = '%s'
from alembic import op
def upgrade():
op.execute("CREATE STEP 2")
def downgrade():
op.execute("DROP STEP 2")
@ -288,9 +293,11 @@ down_revision = '%s'
from alembic import op
def upgrade():
op.execute("CREATE STEP 3")
def downgrade():
op.execute("DROP STEP 3")

View File

@ -1,5 +1,5 @@
# testing/exclusions.py
# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
# Copyright (C) 2005-2017 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under

View File

@ -1,5 +1,5 @@
# testing/mock.py
# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
# Copyright (C) 2005-2017 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under

View File

@ -1,5 +1,5 @@
# plugin/noseplugin.py
# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
# Copyright (C) 2005-2017 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under

View File

@ -1,5 +1,5 @@
# plugin/plugin_base.py
# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
# Copyright (C) 2005-2017 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
@ -22,7 +22,8 @@ try:
# honor it unless nose is imported too...
from nose import SkipTest
except ImportError:
from _pytest.runner import Skipped as SkipTest
from pytest import skip
SkipTest = skip.Exception
import sys
import re
@ -75,6 +76,9 @@ def setup_options(make_option):
dest="low_connections",
help="Use a low number of distinct connections - "
"i.e. for Oracle TNS")
make_option("--write-idents", type="string", dest="write_idents",
help="write out generated follower idents to <file>, "
"when -n<num> is used")
make_option("--reversetop", action="store_true",
dest="reversetop", default=False,
help="Use a random-ordering set implementation in the ORM "

View File

@ -17,7 +17,7 @@ import pytest
import argparse
import inspect
import collections
import itertools
import os
try:
import xdist # noqa
@ -50,6 +50,14 @@ def pytest_configure(config):
plugin_base.configure_follower(
config.slaveinput["follower_ident"]
)
if config.option.write_idents:
with open(config.option.write_idents, "a") as file_:
file_.write(config.slaveinput["follower_ident"] + "\n")
else:
if config.option.write_idents and \
os.path.exists(config.option.write_idents):
os.remove(config.option.write_idents)
plugin_base.pre_begin(config.option)

View File

@ -275,9 +275,14 @@ def _oracle_drop_db(cfg, eng, ident):
_ora_drop_ignore(conn, "%s_ts2" % ident)
def reap_oracle_dbs(eng):
def reap_oracle_dbs(eng, idents_file):
log.info("Reaping Oracle dbs...")
with eng.connect() as conn:
with open(idents_file) as file_:
idents = set(line.strip() for line in file_)
log.info("identifiers in file: %s", ", ".join(idents))
to_reap = conn.execute(
"select u.username from all_users u where username "
"like 'TEST_%' and not exists (select username "
@ -287,7 +292,7 @@ def reap_oracle_dbs(eng):
for name in all_names:
if name.endswith("_ts1") or name.endswith("_ts2"):
continue
else:
elif name in idents:
to_drop.add(name)
if "%s_ts1" % name in all_names:
to_drop.add("%s_ts1" % name)

View File

@ -19,10 +19,25 @@ class SuiteRequirements(Requirements):
@property
def unique_constraint_reflection(self):
def doesnt_have_check_uq_constraints(config):
if not util.sqla_084:
return True
from sqlalchemy import inspect
insp = inspect(config.db)
try:
insp.get_unique_constraints('x')
except NotImplementedError:
return True
except TypeError:
return True
except Exception:
pass
return False
return exclusions.skip_if(
lambda config: not util.sqla_084,
"SQLAlchemy 0.8.4 or greater required"
)
) + exclusions.skip_if(doesnt_have_check_uq_constraints)
@property
def foreign_key_match(self):

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python
# testing/runner.py
# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
# Copyright (C) 2005-2017 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under

View File

@ -1,5 +1,5 @@
# testing/warnings.py
# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors
# Copyright (C) 2005-2017 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under

4
debian/changelog vendored
View File

@ -1,6 +1,8 @@
alembic (0.8.8-3) UNRELEASED; urgency=medium
alembic (0.8.10-1) UNRELEASED; urgency=medium
* New upstream release
* d/control + d/gbp.conf: Change branch to debian/ocata
* d/copyright: Bump copyright years
-- Ondřej Nový <onovy@debian.org> Sun, 26 Feb 2017 11:33:46 +0100

6
debian/copyright vendored
View File

@ -4,7 +4,7 @@ Upstream-Contact: Mike Bayer
Source: https://pypi.python.org/pypi/alembic
Files: *
Copyright: 2009-2016, Michael Bayer
Copyright: 2009-2017, Michael Bayer
License: MIT
Files: alembic/testing/config.py
@ -15,7 +15,7 @@ Files: alembic/testing/config.py
alembic/testing/plugin/plugin_base.py
alembic/testing/warnings.py
alembic/testing/runner.py
Copyright: (c) 2005-2016 the SQLAlchemy authors and contributors
Copyright: (c) 2005-2017 the SQLAlchemy authors and contributors
License: MIT
Files: debian/*
@ -25,7 +25,7 @@ Copyright: (c) 2012, Matthias Kümmerer <matthias@matthias-k.org>
(c) 2014, Thomas Bechtold <toabctl@debian.org>
(c) 2014, Piotr Ożarowski <piotr@debian.org>
(c) 2013-2015, Thomas Goirand <zigo@debian.org>
(c) 2016, Ondřej Nový <onovy@debian.org>
(c) 2016-2017, Ondřej Nový <onovy@debian.org>
License: MIT
License: MIT

View File

@ -3,6 +3,112 @@
Changelog
==========
.. changelog::
:version: 0.8.10
:released: January 17, 2017
.. change:: 406
:tags: bug, versioning
:tickets: 406
The alembic_version table, when initially created, now establishes a
primary key constraint on the "version_num" column, to suit database
engines that don't support tables without primary keys. This behavior
can be controlled using the parameter
:paramref:`.EnvironmentContext.configure.version_table_pk`. Note that
this change only applies to the initial creation of the alembic_version
table; it does not impact any existing alembic_version table already
present.
.. change:: 402
:tags: bug, batch
:tickets: 402
Fixed bug where doing ``batch_op.drop_constraint()`` against the
primary key constraint would fail to remove the "primary_key" flag
from the column, resulting in the constraint being recreated.
.. change:: update_uq_dedupe
:tags: bug, autogenerate, oracle
Adjusted the logic originally added for :ticket:`276` that detects MySQL
unique constraints which are actually unique indexes to be generalized
for any dialect that has this behavior, for SQLAlchemy version 1.0 and
greater. This is to allow for upcoming SQLAlchemy support for unique
constraint reflection for Oracle, which also has no dedicated concept of
"unique constraint" and instead establishes a unique index.
.. change:: 356
:tags: bug, versioning
:tickets: 356
Added a file ignore for Python files of the form ``.#<name>.py``,
which are generated by the Emacs editor. Pull request courtesy
Markus Mattes.
.. changelog::
:version: 0.8.9
:released: November 28, 2016
.. change:: 393
:tags: bug, autogenerate
:tickets: 393
Adjustment to the "please adjust!" comment in the script.py.mako
template so that the generated comment starts with a single pound
sign, appeasing flake8.
.. change::
:tags: bug, batch
:tickets: 391
Batch mode will not use CAST() to copy data if type_ is given, however
the basic type affinity matches that of the existing type. This to
avoid SQLite's CAST of TIMESTAMP which results in truncation of the
data, in those cases where the user needs to add redundant type_ for
other reasons.
.. change::
:tags: bug, autogenerate
:tickets: 393
Continued pep8 improvements by adding appropriate whitespace in
the base template for generated migrations. Pull request courtesy
Markus Mattes.
.. change::
:tags: bug, revisioning
Added an additional check when reading in revision files to detect
if the same file is being read twice; this can occur if the same directory
or a symlink equivalent is present more than once in version_locations.
A warning is now emitted and the file is skipped. Pull request courtesy
Jiri Kuncar.
.. change::
:tags: bug, autogenerate
:tickets: 395
Fixed bug where usage of a custom TypeDecorator which returns a
per-dialect type via :meth:`.TypeDecorator.load_dialect_impl` that differs
significantly from the default "impl" for the type decorator would fail
to compare correctly during autogenerate.
.. change::
:tags: bug, autogenerate, postgresql
:tickets: 392
Fixed bug in Postgresql "functional index skip" behavior where a
functional index that ended in ASC/DESC wouldn't be detected as something
we can't compare in autogenerate, leading to duplicate definitions
in autogenerated files.
.. change::
:tags: bug, versioning
Fixed bug where the "base" specifier, as in "base:head", could not
be used explicitly when ``--sql`` mode was present.
.. changelog::
:version: 0.8.8
:released: September 12, 2016

2
docs/build/conf.py vendored
View File

@ -63,7 +63,7 @@ master_doc = 'index'
# General information about the project.
project = u'Alembic'
copyright = u'2010-2016, Mike Bayer'
copyright = u'2010-2017, Mike Bayer'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the

View File

@ -1,3 +1,3 @@
changelog>=0.3.4
changelog>=0.3.5
sphinx-paramlinks>=0.3.2
git+https://bitbucket.org/zzzeek/sqlalchemy.git

View File

@ -1,4 +1,4 @@
"""Drop Oracle databases that are left over from a
"""Drop Oracle databases that are left over from a
multiprocessing test run.
Currently the cx_Oracle driver seems to sometimes not release a
@ -6,19 +6,19 @@ TCP connection even if close() is called, which prevents the provisioning
system from dropping a database in-process.
"""
from sqlalchemy.testing.plugin import plugin_base
from sqlalchemy.testing import engines
from sqlalchemy.testing import provision
from alembic.testing.plugin import plugin_base
from alembic.testing import engines
from alembic.testing import provision
import logging
import sys
logging.basicConfig()
logging.getLogger(provision.__name__).setLevel(logging.INFO)
plugin_base.read_config()
oracle = plugin_base.file_config.get('db', 'oracle')
from sqlalchemy.testing import provision
engine = engines.testing_engine(oracle, {})
provision.reap_oracle_dbs(engine)
provision.reap_oracle_dbs(engine, sys.argv[1])

View File

@ -34,7 +34,7 @@ oracle8=oracle://scott:tiger@127.0.0.1:1521/?use_ansi=0
[alembic]
[pytest]
[tool:pytest]
addopts= --tb native -v -r fxX
python_files=tests/test_*.py

View File

@ -54,6 +54,12 @@ class DefaultRequirements(SuiteRequirements):
"""foreign key constraints always have names in the DB"""
return exclusions.fails_on('sqlite')
@property
def no_name_normalize(self):
return exclusions.skip_if(
lambda config: config.db.dialect.requires_name_normalize
)
@property
def reflects_fk_options(self):
return exclusions.only_on([
@ -71,6 +77,16 @@ class DefaultRequirements(SuiteRequirements):
"""backend supports DEFERRABLE option in foreign keys"""
return exclusions.only_on(['postgresql'])
@property
def flexible_fk_cascades(self):
"""target database must support ON UPDATE/DELETE..CASCADE with the
full range of keywords (e.g. NO ACTION, etc.)"""
return exclusions.skip_if(
['oracle'],
'target backend has poor FK cascade syntax'
)
@property
def reflects_unique_constraints_unambiguously(self):
return exclusions.fails_on("mysql")

View File

@ -26,13 +26,13 @@ class AutogenerateDiffTest(ModelOne, AutogenTest, TestBase):
autogenerate._render_migration_diffs(context, template_args)
eq_(re.sub(r"u'", "'", template_args['upgrades']),
"""### commands auto generated by Alembic - please adjust! ###
"""# ### commands auto generated by Alembic - please adjust! ###
pass
### end Alembic commands ###""")
# ### end Alembic commands ###""")
eq_(re.sub(r"u'", "'", template_args['downgrades']),
"""### commands auto generated by Alembic - please adjust! ###
"""# ### commands auto generated by Alembic - please adjust! ###
pass
### end Alembic commands ###""")
# ### end Alembic commands ###""")
def test_render_nothing_batch(self):
context = MigrationContext.configure(
@ -53,13 +53,13 @@ class AutogenerateDiffTest(ModelOne, AutogenTest, TestBase):
autogenerate._render_migration_diffs(context, template_args)
eq_(re.sub(r"u'", "'", template_args['upgrades']),
"""### commands auto generated by Alembic - please adjust! ###
"""# ### commands auto generated by Alembic - please adjust! ###
pass
### end Alembic commands ###""")
# ### end Alembic commands ###""")
eq_(re.sub(r"u'", "'", template_args['downgrades']),
"""### commands auto generated by Alembic - please adjust! ###
"""# ### commands auto generated by Alembic - please adjust! ###
pass
### end Alembic commands ###""")
# ### end Alembic commands ###""")
def test_render_diffs_standard(self):
"""test a full render including indentation"""
@ -67,7 +67,7 @@ class AutogenerateDiffTest(ModelOne, AutogenTest, TestBase):
template_args = {}
autogenerate._render_migration_diffs(self.context, template_args)
eq_(re.sub(r"u'", "'", template_args['upgrades']),
"""### commands auto generated by Alembic - please adjust! ###
"""# ### commands auto generated by Alembic - please adjust! ###
op.create_table('item',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('description', sa.String(length=100), nullable=True),
@ -96,10 +96,10 @@ nullable=True))
nullable=False)
op.drop_index('pw_idx', table_name='user')
op.drop_column('user', 'pw')
### end Alembic commands ###""")
# ### end Alembic commands ###""")
eq_(re.sub(r"u'", "'", template_args['downgrades']),
"""### commands auto generated by Alembic - please adjust! ###
"""# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('pw', sa.VARCHAR(length=50), \
nullable=True))
op.create_index('pw_idx', 'user', ['pw'], unique=False)
@ -125,7 +125,7 @@ nullable=True))
sa.ForeignKeyConstraint(['uid'], ['user.id'], )
)
op.drop_table('item')
### end Alembic commands ###""")
# ### end Alembic commands ###""")
def test_render_diffs_batch(self):
"""test a full render in batch mode including indentation"""
@ -135,7 +135,7 @@ nullable=True))
autogenerate._render_migration_diffs(self.context, template_args)
eq_(re.sub(r"u'", "'", template_args['upgrades']),
"""### commands auto generated by Alembic - please adjust! ###
"""# ### commands auto generated by Alembic - please adjust! ###
op.create_table('item',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('description', sa.String(length=100), nullable=True),
@ -169,10 +169,10 @@ nullable=True))
batch_op.drop_index('pw_idx')
batch_op.drop_column('pw')
### end Alembic commands ###""")
# ### end Alembic commands ###""")
eq_(re.sub(r"u'", "'", template_args['downgrades']),
"""### commands auto generated by Alembic - please adjust! ###
"""# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('pw', sa.VARCHAR(length=50), nullable=True))
batch_op.create_index('pw_idx', ['pw'], unique=False)
@ -203,7 +203,7 @@ nullable=True))
sa.ForeignKeyConstraint(['uid'], ['user.id'], )
)
op.drop_table('item')
### end Alembic commands ###""")
# ### end Alembic commands ###""")
def test_imports_maintined(self):
template_args = {}
@ -252,13 +252,13 @@ class AutogenerateDiffTestWSchema(ModelOne, AutogenTest, TestBase):
autogenerate._render_migration_diffs(context, template_args)
eq_(re.sub(r"u'", "'", template_args['upgrades']),
"""### commands auto generated by Alembic - please adjust! ###
"""# ### commands auto generated by Alembic - please adjust! ###
pass
### end Alembic commands ###""")
# ### end Alembic commands ###""")
eq_(re.sub(r"u'", "'", template_args['downgrades']),
"""### commands auto generated by Alembic - please adjust! ###
"""# ### commands auto generated by Alembic - please adjust! ###
pass
### end Alembic commands ###""")
# ### end Alembic commands ###""")
def test_render_diffs_extras(self):
"""test a full render including indentation (include and schema)"""
@ -271,7 +271,7 @@ class AutogenerateDiffTestWSchema(ModelOne, AutogenTest, TestBase):
autogenerate._render_migration_diffs(self.context, template_args)
eq_(re.sub(r"u'", "'", template_args['upgrades']),
"""### commands auto generated by Alembic - please adjust! ###
"""# ### commands auto generated by Alembic - please adjust! ###
op.create_table('item',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('description', sa.String(length=100), nullable=True),
@ -307,10 +307,10 @@ source_schema='%(schema)s', referent_schema='%(schema)s')
schema='%(schema)s')
op.drop_index('pw_idx', table_name='user', schema='test_schema')
op.drop_column('user', 'pw', schema='%(schema)s')
### end Alembic commands ###""" % {"schema": self.schema})
# ### end Alembic commands ###""" % {"schema": self.schema})
eq_(re.sub(r"u'", "'", template_args['downgrades']),
"""### commands auto generated by Alembic - please adjust! ###
"""# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('pw', sa.VARCHAR(length=50), \
autoincrement=False, nullable=True), schema='%(schema)s')
op.create_index('pw_idx', 'user', ['pw'], unique=False, schema='%(schema)s')
@ -341,5 +341,5 @@ name='extra_uid_fkey'),
schema='%(schema)s'
)
op.drop_table('item', schema='%(schema)s')
### end Alembic commands ###""" % {"schema": self.schema})
# ### end Alembic commands ###""" % {"schema": self.schema})

View File

@ -5,7 +5,8 @@ from sqlalchemy import MetaData, Column, Table, Integer, String, Text, \
TypeDecorator, CheckConstraint, text, PrimaryKeyConstraint, \
ForeignKeyConstraint, VARCHAR, DECIMAL, DateTime, BigInteger, BIGINT, \
SmallInteger
from sqlalchemy.types import NULLTYPE
from sqlalchemy.dialects import sqlite
from sqlalchemy.types import NULLTYPE, VARBINARY
from sqlalchemy.engine.reflection import Inspector
from alembic.operations import ops
@ -592,6 +593,26 @@ class CompareTypeSpecificityTest(TestBase):
return impl.DefaultImpl(
default.DefaultDialect(), None, False, True, None, {})
def test_typedec_to_nonstandard(self):
class PasswordType(TypeDecorator):
impl = VARBINARY
def copy(self, **kw):
return PasswordType(self.impl.length)
def load_dialect_impl(self, dialect):
if dialect.name == 'default':
impl = sqlite.NUMERIC(self.length)
else:
impl = VARBINARY(self.length)
return dialect.type_descriptor(impl)
impl = self._fixture()
impl.compare_type(
Column('x', sqlite.NUMERIC(50)),
Column('x', PasswordType(50)))
def test_string(self):
t1 = String(30)
t2 = String(40)

View File

@ -17,7 +17,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
m1 = MetaData()
m2 = MetaData()
Table('table', m1,
Table('some_table', m1,
Column('test', String(10), primary_key=True),
mysql_engine='InnoDB')
@ -26,10 +26,10 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('name', String(50), nullable=False),
Column('a1', String(10), server_default="x"),
Column('test2', String(10)),
ForeignKeyConstraint(['test2'], ['table.test']),
ForeignKeyConstraint(['test2'], ['some_table.test']),
mysql_engine='InnoDB')
Table('table', m2,
Table('some_table', m2,
Column('test', String(10), primary_key=True),
mysql_engine='InnoDB')
@ -46,7 +46,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ['test2'],
'table', ['test'],
'some_table', ['test'],
conditional_name="servergenerated"
)
@ -54,7 +54,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
m1 = MetaData()
m2 = MetaData()
Table('table', m1,
Table('some_table', m1,
Column('id', Integer, primary_key=True),
Column('test', String(10)),
mysql_engine='InnoDB')
@ -66,7 +66,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('test2', String(10)),
mysql_engine='InnoDB')
Table('table', m2,
Table('some_table', m2,
Column('id', Integer, primary_key=True),
Column('test', String(10)),
mysql_engine='InnoDB')
@ -76,7 +76,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('name', String(50), nullable=False),
Column('a1', String(10), server_default="x"),
Column('test2', String(10)),
ForeignKeyConstraint(['test2'], ['table.test']),
ForeignKeyConstraint(['test2'], ['some_table.test']),
mysql_engine='InnoDB')
diffs = self._fixture(m1, m2)
@ -84,14 +84,14 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "add_fk",
"user", ["test2"],
"table", ["test"]
"some_table", ["test"]
)
def test_no_change(self):
m1 = MetaData()
m2 = MetaData()
Table('table', m1,
Table('some_table', m1,
Column('id', Integer, primary_key=True),
Column('test', String(10)),
mysql_engine='InnoDB')
@ -101,10 +101,10 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('name', String(50), nullable=False),
Column('a1', String(10), server_default="x"),
Column('test2', Integer),
ForeignKeyConstraint(['test2'], ['table.id']),
ForeignKeyConstraint(['test2'], ['some_table.id']),
mysql_engine='InnoDB')
Table('table', m2,
Table('some_table', m2,
Column('id', Integer, primary_key=True),
Column('test', String(10)),
mysql_engine='InnoDB')
@ -114,7 +114,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('name', String(50), nullable=False),
Column('a1', String(10), server_default="x"),
Column('test2', Integer),
ForeignKeyConstraint(['test2'], ['table.id']),
ForeignKeyConstraint(['test2'], ['some_table.id']),
mysql_engine='InnoDB')
diffs = self._fixture(m1, m2)
@ -125,7 +125,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
m1 = MetaData()
m2 = MetaData()
Table('table', m1,
Table('some_table', m1,
Column('id_1', String(10), primary_key=True),
Column('id_2', String(10), primary_key=True),
mysql_engine='InnoDB')
@ -137,10 +137,10 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('other_id_1', String(10)),
Column('other_id_2', String(10)),
ForeignKeyConstraint(['other_id_1', 'other_id_2'],
['table.id_1', 'table.id_2']),
['some_table.id_1', 'some_table.id_2']),
mysql_engine='InnoDB')
Table('table', m2,
Table('some_table', m2,
Column('id_1', String(10), primary_key=True),
Column('id_2', String(10), primary_key=True),
mysql_engine='InnoDB'
@ -153,7 +153,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('other_id_1', String(10)),
Column('other_id_2', String(10)),
ForeignKeyConstraint(['other_id_1', 'other_id_2'],
['table.id_1', 'table.id_2']),
['some_table.id_1', 'some_table.id_2']),
mysql_engine='InnoDB')
diffs = self._fixture(m1, m2)
@ -164,7 +164,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
m1 = MetaData()
m2 = MetaData()
Table('table', m1,
Table('some_table', m1,
Column('id_1', String(10), primary_key=True),
Column('id_2', String(10), primary_key=True),
mysql_engine='InnoDB')
@ -177,7 +177,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('other_id_2', String(10)),
mysql_engine='InnoDB')
Table('table', m2,
Table('some_table', m2,
Column('id_1', String(10), primary_key=True),
Column('id_2', String(10), primary_key=True),
mysql_engine='InnoDB')
@ -189,7 +189,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('other_id_1', String(10)),
Column('other_id_2', String(10)),
ForeignKeyConstraint(['other_id_1', 'other_id_2'],
['table.id_1', 'table.id_2'],
['some_table.id_1', 'some_table.id_2'],
name='fk_test_name'),
mysql_engine='InnoDB')
@ -198,15 +198,16 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "add_fk",
"user", ['other_id_1', 'other_id_2'],
'table', ['id_1', 'id_2'],
'some_table', ['id_1', 'id_2'],
name="fk_test_name"
)
@config.requirements.no_name_normalize
def test_remove_composite_fk(self):
m1 = MetaData()
m2 = MetaData()
Table('table', m1,
Table('some_table', m1,
Column('id_1', String(10), primary_key=True),
Column('id_2', String(10), primary_key=True),
mysql_engine='InnoDB')
@ -218,11 +219,11 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('other_id_1', String(10)),
Column('other_id_2', String(10)),
ForeignKeyConstraint(['other_id_1', 'other_id_2'],
['table.id_1', 'table.id_2'],
['some_table.id_1', 'some_table.id_2'],
name='fk_test_name'),
mysql_engine='InnoDB')
Table('table', m2,
Table('some_table', m2,
Column('id_1', String(10), primary_key=True),
Column('id_2', String(10), primary_key=True),
mysql_engine='InnoDB')
@ -240,7 +241,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ['other_id_1', 'other_id_2'],
"table", ['id_1', 'id_2'],
"some_table", ['id_1', 'id_2'],
conditional_name="fk_test_name"
)
@ -248,7 +249,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
m1 = MetaData()
m2 = MetaData()
Table('table', m1,
Table('some_table', m1,
Column('id_1', String(10), primary_key=True),
Column('id_2', String(10), primary_key=True),
mysql_engine='InnoDB')
@ -259,7 +260,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('other_id_2', String(10)),
mysql_engine='InnoDB')
Table('table', m2,
Table('some_table', m2,
Column('id_1', String(10), key='tid1', primary_key=True),
Column('id_2', String(10), key='tid2', primary_key=True),
mysql_engine='InnoDB')
@ -269,7 +270,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('other_id_1', String(10), key='oid1'),
Column('other_id_2', String(10), key='oid2'),
ForeignKeyConstraint(['oid1', 'oid2'],
['table.tid1', 'table.tid2'],
['some_table.tid1', 'some_table.tid2'],
name='fk_test_name'),
mysql_engine='InnoDB')
@ -278,7 +279,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "add_fk",
"user", ['other_id_1', 'other_id_2'],
'table', ['id_1', 'id_2'],
'some_table', ['id_1', 'id_2'],
name="fk_test_name"
)
@ -286,7 +287,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
m1 = MetaData()
m2 = MetaData()
Table('table', m1,
Table('some_table', m1,
Column('id_1', String(10), primary_key=True),
Column('id_2', String(10), primary_key=True),
mysql_engine='InnoDB')
@ -296,10 +297,10 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('other_id_1', String(10)),
Column('other_id_2', String(10)),
ForeignKeyConstraint(['other_id_1', 'other_id_2'],
['table.id_1', 'table.id_2']),
['some_table.id_1', 'some_table.id_2']),
mysql_engine='InnoDB')
Table('table', m2,
Table('some_table', m2,
Column('id_1', String(10), key='tid1', primary_key=True),
Column('id_2', String(10), key='tid2', primary_key=True),
mysql_engine='InnoDB')
@ -309,7 +310,7 @@ class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
Column('other_id_1', String(10), key='oid1'),
Column('other_id_2', String(10), key='oid2'),
ForeignKeyConstraint(['oid1', 'oid2'],
['table.tid1', 'table.tid2']),
['some_table.tid1', 'some_table.tid2']),
mysql_engine='InnoDB')
diffs = self._fixture(m1, m2)
@ -321,6 +322,7 @@ class IncludeHooksTest(AutogenFixtureTest, TestBase):
__backend__ = True
__requires__ = 'fk_names',
@config.requirements.no_name_normalize
def test_remove_connection_fk(self):
m1 = MetaData()
m2 = MetaData()
@ -399,6 +401,7 @@ class IncludeHooksTest(AutogenFixtureTest, TestBase):
)
eq_(len(diffs), 1)
@config.requirements.no_name_normalize
def test_change_fk(self):
m1 = MetaData()
m2 = MetaData()
@ -473,13 +476,13 @@ class IncludeHooksTest(AutogenFixtureTest, TestBase):
class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
__backend__ = True
__requires__ = ('sqlalchemy_09', )
__requires__ = ('sqlalchemy_09', 'flexible_fk_cascades')
def _fk_opts_fixture(self, old_opts, new_opts):
m1 = MetaData()
m2 = MetaData()
Table('table', m1,
Table('some_table', m1,
Column('id', Integer, primary_key=True),
Column('test', String(10)),
mysql_engine='InnoDB')
@ -488,10 +491,10 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
Column('id', Integer, primary_key=True),
Column('name', String(50), nullable=False),
Column('tid', Integer),
ForeignKeyConstraint(['tid'], ['table.id'], **old_opts),
ForeignKeyConstraint(['tid'], ['some_table.id'], **old_opts),
mysql_engine='InnoDB')
Table('table', m2,
Table('some_table', m2,
Column('id', Integer, primary_key=True),
Column('test', String(10)),
mysql_engine='InnoDB')
@ -500,7 +503,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
Column('id', Integer, primary_key=True),
Column('name', String(50), nullable=False),
Column('tid', Integer),
ForeignKeyConstraint(['tid'], ['table.id'], **new_opts),
ForeignKeyConstraint(['tid'], ['some_table.id'], **new_opts),
mysql_engine='InnoDB')
return self._fixture(m1, m2)
@ -526,7 +529,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
ondelete=None,
conditional_name="servergenerated"
)
@ -534,7 +537,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[1], "add_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
ondelete="cascade"
)
else:
@ -549,7 +552,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
ondelete="CASCADE",
conditional_name="servergenerated"
)
@ -557,7 +560,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[1], "add_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
ondelete=None
)
else:
@ -579,7 +582,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
onupdate=None,
conditional_name="servergenerated"
)
@ -587,7 +590,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[1], "add_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
onupdate="cascade"
)
else:
@ -602,7 +605,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
onupdate="CASCADE",
conditional_name="servergenerated"
)
@ -610,7 +613,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[1], "add_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
onupdate=None
)
else:
@ -667,7 +670,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
onupdate=None,
ondelete=mock.ANY, # MySQL reports None, PG reports RESTRICT
conditional_name="servergenerated"
@ -676,7 +679,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[1], "add_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
onupdate=None,
ondelete="cascade"
)
@ -695,7 +698,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
onupdate=mock.ANY, # MySQL reports None, PG reports RESTRICT
ondelete=None,
conditional_name="servergenerated"
@ -704,7 +707,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[1], "add_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
onupdate="cascade",
ondelete=None
)
@ -721,7 +724,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
onupdate="CASCADE",
ondelete="SET NULL",
conditional_name="servergenerated"
@ -730,7 +733,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[1], "add_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
onupdate="RESTRICT",
ondelete="RESTRICT"
)
@ -746,7 +749,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
initially=None,
conditional_name="servergenerated"
)
@ -754,7 +757,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[1], "add_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
initially="deferred"
)
@ -767,7 +770,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
initially="DEFERRED",
deferrable=True,
conditional_name="servergenerated"
@ -776,7 +779,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[1], "add_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
initially=None
)
@ -790,7 +793,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
initially=None,
conditional_name="servergenerated"
)
@ -798,7 +801,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[1], "add_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
initially="immediate",
deferrable=True
)
@ -813,7 +816,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
initially=None, # immediate is the default
deferrable=True,
conditional_name="servergenerated"
@ -822,7 +825,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[1], "add_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
initially=None,
deferrable=None
)
@ -866,7 +869,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
deferrable=None,
conditional_name="servergenerated"
)
@ -874,7 +877,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[1], "add_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
deferrable=True
)
@ -887,7 +890,7 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[0], "remove_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
deferrable=True,
conditional_name="servergenerated"
)
@ -895,6 +898,6 @@ class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
self._assert_fk_diff(
diffs[1], "add_fk",
"user", ["tid"],
"table", ["id"],
"some_table", ["id"],
deferrable=None
)

View File

@ -640,7 +640,7 @@ class PGUniqueIndexTest(AutogenerateUniqueIndexTest):
eq_(diffs[0][1].name, "uq_name")
eq_(len(diffs), 1)
def test_functional_ix(self):
def test_functional_ix_one(self):
m1 = MetaData()
m2 = MetaData()
@ -665,6 +665,37 @@ class PGUniqueIndexTest(AutogenerateUniqueIndexTest):
diffs = self._fixture(m1, m2)
eq_(diffs, [])
def test_functional_ix_two(self):
m1 = MetaData()
m2 = MetaData()
t1 = Table(
'foo', m1,
Column('id', Integer, primary_key=True),
Column('email', String(50)),
Column('name', String(50))
)
Index(
"email_idx",
func.coalesce(t1.c.email, t1.c.name).desc(), unique=True)
t2 = Table(
'foo', m2,
Column('id', Integer, primary_key=True),
Column('email', String(50)),
Column('name', String(50))
)
Index(
"email_idx",
func.coalesce(t2.c.email, t2.c.name).desc(), unique=True)
with assertions.expect_warnings(
"Skipped unsupported reflection",
"autogenerate skipping functional index"
):
diffs = self._fixture(m1, m2)
eq_(diffs, [])
class MySQLUniqueIndexTest(AutogenerateUniqueIndexTest):
reports_unnamed_constraints = True
@ -681,6 +712,12 @@ class MySQLUniqueIndexTest(AutogenerateUniqueIndexTest):
assert False, "unexpected success"
class OracleUniqueIndexTest(AutogenerateUniqueIndexTest):
reports_unnamed_constraints = True
reports_unique_constraints_as_indexes = True
__only_on__ = "oracle"
class NoUqReflectionIndexTest(NoUqReflection, AutogenerateUniqueIndexTest):
reports_unique_constraints = False
__only_on__ = 'sqlite'

View File

@ -1674,7 +1674,7 @@ class RenderNamingConventionTest(TestBase):
eq_(
autogenerate.render_python_code(uo, render_as_batch=True),
"### commands auto generated by Alembic - please adjust! ###\n"
"# ### commands auto generated by Alembic - please adjust! ###\n"
" op.create_table('sometable',\n"
" sa.Column('x', sa.Integer(), nullable=True),\n"
" sa.Column('y', sa.Integer(), nullable=True)\n"
@ -1683,5 +1683,5 @@ class RenderNamingConventionTest(TestBase):
"as batch_op:\n"
" batch_op.create_index("
"'ix1', ['x', 'y'], unique=False)\n\n"
" ### end Alembic commands ###"
" # ### end Alembic commands ###"
)

View File

@ -13,9 +13,9 @@ from alembic.runtime.migration import MigrationContext
from sqlalchemy import Integer, Table, Column, String, MetaData, ForeignKey, \
UniqueConstraint, ForeignKeyConstraint, Index, Boolean, CheckConstraint, \
Enum
Enum, DateTime, PrimaryKeyConstraint
from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.sql import column, text
from sqlalchemy.sql import column, text, select
from sqlalchemy.schema import CreateTable, CreateIndex
from sqlalchemy import exc
@ -58,6 +58,17 @@ class BatchApplyTest(TestBase):
)
return ApplyBatchImpl(t, table_args, table_kwargs, False)
def _pk_fixture(self):
m = MetaData()
t = Table(
'tname', m,
Column('id', Integer),
Column('x', String()),
Column('y', Integer),
PrimaryKeyConstraint('id', name="mypk")
)
return ApplyBatchImpl(t, (), {}, False)
def _literal_ck_fixture(
self, copy_from=None, table_args=(), table_kwargs={}):
m = MetaData()
@ -215,6 +226,7 @@ class BatchApplyTest(TestBase):
impl.new_table.c[name].name
for name in colnames
if name in impl.table.c])
args['tname_colnames'] = ", ".join(
"CAST(%(schema)stname.%(name)s AS %(type)s) AS anon_1" % {
'schema': args['schema'],
@ -243,9 +255,9 @@ class BatchApplyTest(TestBase):
def test_change_type(self):
impl = self._simple_fixture()
impl.alter_column('tname', 'x', type_=Integer)
impl.alter_column('tname', 'x', type_=String)
new_table = self._assert_impl(impl)
assert new_table.c.x.type._type_affinity is Integer
assert new_table.c.x.type._type_affinity is String
def test_rename_col(self):
impl = self._simple_fixture()
@ -533,6 +545,14 @@ class BatchApplyTest(TestBase):
dialect='mysql'
)
def test_drop_pk(self):
impl = self._pk_fixture()
pk = self.op.schema_obj.primary_key_constraint("mypk", "tname", ["id"])
impl.drop_constraint(pk)
new_table = self._assert_impl(impl)
assert not new_table.c.id.primary_key
assert not len(new_table.primary_key)
class BatchAPITest(TestBase):
__requires__ = ('sqlalchemy_08', )
@ -751,7 +771,6 @@ class CopyFromTest(TestBase):
with self.op.batch_alter_table(
"foo", copy_from=self.table) as batch_op:
batch_op.alter_column('data', type_=Integer)
context.assert_(
'CREATE TABLE _alembic_batch_temp (id INTEGER NOT NULL, '
'data INTEGER, x INTEGER, PRIMARY KEY (id))',
@ -889,7 +908,7 @@ class CopyFromTest(TestBase):
'CREATE TABLE _alembic_batch_temp (id INTEGER NOT NULL, '
'data VARCHAR, x INTEGER, PRIMARY KEY (id))',
'INSERT INTO _alembic_batch_temp (id, data, x) SELECT foo.id, '
'CAST(foo.data AS VARCHAR) AS anon_1, foo.x FROM foo',
'foo.data, foo.x FROM foo',
'DROP TABLE foo',
'ALTER TABLE _alembic_batch_temp RENAME TO foo'
)
@ -970,6 +989,14 @@ class BatchRoundTripTest(TestBase):
)
t.create(self.conn)
def _timestamp_fixture(self):
t = Table(
'hasts', self.metadata,
Column('x', DateTime()),
)
t.create(self.conn)
return t
def _int_to_boolean_fixture(self):
t = Table(
'hasbool', self.metadata,
@ -993,6 +1020,23 @@ class BatchRoundTripTest(TestBase):
[Integer]
)
def test_no_net_change_timestamp(self):
t = self._timestamp_fixture()
import datetime
self.conn.execute(
t.insert(),
{"x": datetime.datetime(2012, 5, 18, 15, 32, 5)}
)
with self.op.batch_alter_table("hasts") as batch_op:
batch_op.alter_column("x", type_=DateTime())
eq_(
self.conn.execute(select([t.c.x])).fetchall(),
[(datetime.datetime(2012, 5, 18, 15, 32, 5),)]
)
def test_drop_col_schematype(self):
self._boolean_fixture()
with self.op.batch_alter_table(

View File

@ -5,10 +5,11 @@ from alembic.testing.fixtures import TestBase, capture_context_buffer
from alembic.testing.env import staging_env, _sqlite_testing_config, \
three_rev_fixture, clear_staging_env, _no_sql_testing_config, \
_sqlite_file_db, write_script, env_file_fixture
from alembic.testing import eq_, assert_raises_message, mock
from alembic.testing import eq_, assert_raises_message, mock, assert_raises
from alembic import util
from contextlib import contextmanager
import re
from sqlalchemy import exc as sqla_exc
class _BufMixin(object):
@ -163,7 +164,7 @@ class RevisionTest(TestBase):
def tearDown(self):
clear_staging_env()
def _env_fixture(self):
def _env_fixture(self, version_table_pk=True):
env_file_fixture("""
from sqlalchemy import MetaData, engine_from_config
@ -175,7 +176,10 @@ engine = engine_from_config(
connection = engine.connect()
context.configure(connection=connection, target_metadata=target_metadata)
context.configure(
connection=connection, target_metadata=target_metadata,
version_table_pk=%r
)
try:
with context.begin_transaction():
@ -183,7 +187,7 @@ try:
finally:
connection.close()
""")
""" % (version_table_pk, ))
def test_create_rev_plain_db_not_up_to_date(self):
self._env_fixture()
@ -249,12 +253,24 @@ finally:
command.revision, self.cfg, autogenerate=True
)
def test_err_correctly_raised_on_dupe_rows(self):
def test_pk_constraint_normally_prevents_dupe_rows(self):
self._env_fixture()
command.revision(self.cfg)
r2 = command.revision(self.cfg)
db = _sqlite_file_db()
command.upgrade(self.cfg, "head")
assert_raises(
sqla_exc.IntegrityError,
db.execute,
"insert into alembic_version values ('%s')" % r2.revision
)
def test_err_correctly_raised_on_dupe_rows_no_pk(self):
self._env_fixture(version_table_pk=False)
command.revision(self.cfg)
r2 = command.revision(self.cfg)
db = _sqlite_file_db()
command.upgrade(self.cfg, "head")
db.execute("insert into alembic_version values ('%s')" % r2.revision)
assert_raises_message(
util.CommandError,
@ -434,6 +450,24 @@ class UpgradeDowngradeStampTest(TestBase):
assert "DROP STEP 2" in buf.getvalue()
assert "DROP STEP 1" not in buf.getvalue()
def test_none_to_head_sql(self):
with capture_context_buffer() as buf:
command.upgrade(self.cfg, "head", sql=True)
assert "CREATE TABLE alembic_version" in buf.getvalue()
assert "UPDATE alembic_version" in buf.getvalue()
assert "CREATE STEP 1" in buf.getvalue()
assert "CREATE STEP 2" in buf.getvalue()
assert "CREATE STEP 3" in buf.getvalue()
def test_base_to_head_sql(self):
with capture_context_buffer() as buf:
command.upgrade(self.cfg, "base:head", sql=True)
assert "CREATE TABLE alembic_version" in buf.getvalue()
assert "UPDATE alembic_version" in buf.getvalue()
assert "CREATE STEP 1" in buf.getvalue()
assert "CREATE STEP 2" in buf.getvalue()
assert "CREATE STEP 3" in buf.getvalue()
def test_sql_stamp_from_rev(self):
with capture_context_buffer() as buf:
command.stamp(self.cfg, "%s:head" % self.a, sql=True)

View File

@ -90,11 +90,13 @@ from alembic import op
from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy import Column
def upgrade():
op.create_table("sometable",
Column("data", ENUM("one", "two", "three", name="pgenum"))
)
def downgrade():
op.drop_table("sometable")
""" % self.rid)
@ -108,6 +110,7 @@ from alembic import op
from sqlalchemy.dialects.postgresql import ENUM
from sqlalchemy import Column
def upgrade():
enum = ENUM("one", "two", "three", name="pgenum", create_type=False)
enum.create(op.get_bind(), checkfirst=False)
@ -115,6 +118,7 @@ def upgrade():
Column("data", enum)
)
def downgrade():
op.drop_table("sometable")
ENUM(name="pgenum").drop(op.get_bind(), checkfirst=False)

View File

@ -47,9 +47,11 @@ class ApplyVersionsFunctionalTest(TestBase):
from alembic import op
def upgrade():
op.execute("CREATE TABLE foo(id integer)")
def downgrade():
op.execute("DROP TABLE foo")
@ -62,9 +64,11 @@ class ApplyVersionsFunctionalTest(TestBase):
from alembic import op
def upgrade():
op.execute("CREATE TABLE bar(id integer)")
def downgrade():
op.execute("DROP TABLE bar")
@ -77,9 +81,11 @@ class ApplyVersionsFunctionalTest(TestBase):
from alembic import op
def upgrade():
op.execute("CREATE TABLE bat(id integer)")
def downgrade():
op.execute("DROP TABLE bat")
@ -221,9 +227,11 @@ class VersionNameTemplateTest(TestBase):
from alembic import op
def upgrade():
op.execute("CREATE TABLE foo(id integer)")
def downgrade():
op.execute("DROP TABLE foo")
@ -244,9 +252,11 @@ class VersionNameTemplateTest(TestBase):
from alembic import op
def upgrade():
op.execute("CREATE TABLE foo(id integer)")
def downgrade():
op.execute("DROP TABLE foo")
@ -270,11 +280,14 @@ down_revision = None
from alembic import op
def upgrade():
op.execute("CREATE TABLE foo(id integer)")
def downgrade():
op.execute("DROP TABLE foo")
""")
pyc_path = util.pyc_file_from_path(path)
if os.access(pyc_path, os.F_OK):
@ -288,7 +301,7 @@ def downgrade():
Script._from_path, script, path)
class IgnoreInitTest(TestBase):
class IgnoreFilesTest(TestBase):
sourceless = False
def setUp(self):
@ -299,12 +312,11 @@ class IgnoreInitTest(TestBase):
def tearDown(self):
clear_staging_env()
def _test_ignore_init_py(self, ext):
"""test that __init__.py is ignored."""
def _test_ignore_file_py(self, fname):
command.revision(self.cfg, message="some rev")
script = ScriptDirectory.from_config(self.cfg)
path = os.path.join(script.versions, "__init__.%s" % ext)
path = os.path.join(script.versions, fname)
with open(path, 'w') as f:
f.write(
"crap, crap -> crap"
@ -313,20 +325,42 @@ class IgnoreInitTest(TestBase):
script.get_revision('head')
def test_ignore_py(self):
def _test_ignore_init_py(self, ext):
"""test that __init__.py is ignored."""
self._test_ignore_file_py("__init__.%s" % ext)
def _test_ignore_dot_hash_py(self, ext):
"""test that .#test.py is ignored."""
self._test_ignore_file_py(".#test.%s" % ext)
def test_ignore_init_py(self):
self._test_ignore_init_py("py")
def test_ignore_pyc(self):
def test_ignore_init_pyc(self):
self._test_ignore_init_py("pyc")
def test_ignore_pyx(self):
def test_ignore_init_pyx(self):
self._test_ignore_init_py("pyx")
def test_ignore_pyo(self):
def test_ignore_init_pyo(self):
self._test_ignore_init_py("pyo")
def test_ignore_dot_hash_py(self):
self._test_ignore_dot_hash_py("py")
class SourcelessIgnoreInitTest(IgnoreInitTest):
def test_ignore_dot_hash_pyc(self):
self._test_ignore_dot_hash_py("pyc")
def test_ignore_dot_hash_pyx(self):
self._test_ignore_dot_hash_py("pyx")
def test_ignore_dot_hash_pyo(self):
self._test_ignore_dot_hash_py("pyo")
class SourcelessIgnoreFilesTest(IgnoreFilesTest):
sourceless = True
@ -350,9 +384,11 @@ class SourcelessNeedsFlagTest(TestBase):
from alembic import op
def upgrade():
op.execute("CREATE TABLE foo(id integer)")
def downgrade():
op.execute("DROP TABLE foo")

View File

@ -1,5 +1,5 @@
from alembic.testing.fixtures import TestBase
from alembic.testing import eq_, ne_, assert_raises_message, is_
from alembic.testing import eq_, ne_, assert_raises_message, is_, assertions
from alembic.testing.env import clear_staging_env, staging_env, \
_get_staging_directory, _no_sql_testing_config, env_file_fixture, \
script_file_fixture, _testing_config, _sqlite_testing_config, \
@ -201,7 +201,7 @@ class RevisionCommandTest(TestBase):
file_.write(
"<%text>#</%text> ${message}\n"
"revision = ${repr(up_revision)}\n"
"down_revision = ${repr(down_revision)}\n"
"down_revision = ${repr(down_revision)}\n\n"
"def upgrade():\n"
" ${upgrades if upgrades else 'pass'}\n\n"
"def downgrade():\n"
@ -252,9 +252,11 @@ branch_labels = ['%s']
from alembic import op
def upgrade():
pass
def downgrade():
pass
@ -708,14 +710,14 @@ class RewriterTest(TestBase):
eq_(
autogenerate.render_python_code(directives[0].upgrade_ops),
"### commands auto generated by Alembic - please adjust! ###\n"
"# ### commands auto generated by Alembic - please adjust! ###\n"
" op.add_column('t1', "
"sa.Column('x', sa.Integer(), nullable=True))\n"
" op.create_index('ixt', 't1', ['x'], unique=False)\n"
" op.alter_column('t1', 'x',\n"
" existing_type=sa.Integer(),\n"
" nullable=False)\n"
" ### end Alembic commands ###"
" # ### end Alembic commands ###"
)
@ -837,3 +839,65 @@ context.configure(dialect_name='sqlite', template_args={"somearg":"somevalue"})
contents = open(m.group(1)).read()
os.remove(m.group(1))
assert "<% z = x + y %>" in contents
class DuplicateVersionLocationsTest(TestBase):
def setUp(self):
self.env = staging_env()
self.cfg = _multi_dir_testing_config(
# this is a duplicate of one of the paths
# already present in this fixture
extra_version_location='%(here)s/model1'
)
script = ScriptDirectory.from_config(self.cfg)
self.model1 = util.rev_id()
self.model2 = util.rev_id()
self.model3 = util.rev_id()
for model, name in [
(self.model1, "model1"),
(self.model2, "model2"),
(self.model3, "model3"),
]:
script.generate_revision(
model, name, refresh=True,
version_path=os.path.join(_get_staging_directory(), name),
head="base")
write_script(script, model, """\
"%s"
revision = '%s'
down_revision = None
branch_labels = ['%s']
from alembic import op
def upgrade():
pass
def downgrade():
pass
""" % (name, model, name))
def tearDown(self):
clear_staging_env()
def test_env_emits_warning(self):
with assertions.expect_warnings(
"File %s loaded twice! ignoring. "
"Please ensure version_locations is unique" % (
os.path.realpath(os.path.join(
_get_staging_directory(),
"model1",
"%s_model1.py" % self.model1
)))
):
script = ScriptDirectory.from_config(self.cfg)
script.revision_map.heads
eq_(
[rev.revision for rev in script.walk_revisions()],
[self.model1, self.model2, self.model3]
)

View File

@ -35,8 +35,8 @@ class TestMigrationContext(TestBase):
self.transaction = self.connection.begin()
def tearDown(self):
version_table.drop(self.connection, checkfirst=True)
self.transaction.rollback()
version_table.drop(self.connection, checkfirst=True)
self.connection.close()
def make_one(self, **kwargs):
@ -58,12 +58,24 @@ class TestMigrationContext(TestBase):
context = self.make_one(dialect_name='sqlite',
opts={'version_table': 'explicit'})
eq_(context._version.name, 'explicit')
eq_(context._version.primary_key.name, 'explicit_pkc')
def test_config_explicit_version_table_schema(self):
context = self.make_one(dialect_name='sqlite',
opts={'version_table_schema': 'explicit'})
eq_(context._version.schema, 'explicit')
def test_config_explicit_no_pk(self):
context = self.make_one(dialect_name='sqlite',
opts={'version_table_pk': False})
eq_(len(context._version.primary_key), 0)
def test_config_explicit_w_pk(self):
context = self.make_one(dialect_name='sqlite',
opts={'version_table_pk': True})
eq_(len(context._version.primary_key), 1)
eq_(context._version.primary_key.name, "alembic_version_pkc")
def test_get_current_revision_doesnt_create_version_table(self):
context = self.make_one(connection=self.connection,
opts={'version_table': 'version_table'})

10
tox.ini
View File

@ -1,6 +1,8 @@
[tox]
minversion=1.8.dev1
envlist = py{27,33,34,35}-sqla{09,10,11}, py{27}-sqla{079,084}
# current mysqlclient fails with <=SQLA 0.9 on py3k due to
# old unicode statements flag
envlist = py{27,33,34,35,36}-sqla{10,11}, py{27}-sqla{079,084,09}
SQLA_REPO = {env:SQLA_REPO:git+http://git.sqlalchemy.org/sqlalchemy.git}
@ -36,7 +38,7 @@ setenv=
sqlite: SQLITE=--db sqlite
postgresql: POSTGRESQL=--db postgresql
mysql: MYSQL=--db mysql
oracle: ORACLE=--db oracle --low-connections
oracle: ORACLE=--db oracle --low-connections --write-idents oracle_idents.txt
mssql: MSSQL=--db pymssql
# tox as of 2.0 blocks all environment variables from the
@ -46,7 +48,7 @@ passenv=ORACLE_HOME NLS_LANG
commands=
{env:BASECOMMAND} {env:WORKERS} {env:SQLITE:} {env:POSTGRESQL:} {env:MYSQL:} {env:ORACLE:} {env:MSSQL:} {env:BACKENDONLY:} {env:COVERAGE:} {posargs}
{oracle}: python reap_oracle_dbs.py
{oracle}: python reap_oracle_dbs.py oracle_idents.txt
[testenv:pep8]