]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
- In conjunction with support for multiple independent bases, the
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 22 Nov 2014 19:58:09 +0000 (14:58 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 22 Nov 2014 19:58:09 +0000 (14:58 -0500)
specific version directories are now also configurable to include
multiple, user-defined directories.   When multiple directories exist,
the creation of a revision file with no down revision requires
that the starting directory is indicated; the creation of subsequent
revisions along that lineage will then automatically use that
directory for new files.
fixes #124

alembic/command.py
alembic/config.py
alembic/revision.py
alembic/script.py
alembic/templates/generic/alembic.ini.mako
alembic/templates/multidb/alembic.ini.mako
alembic/templates/pylons/alembic.ini.mako
alembic/testing/env.py
docs/build/changelog.rst
docs/build/tutorial.rst
tests/test_script_production.py

index 8b9f4d71291c46ba5d0caf84564c265611f30fb6..2bdb944224329e3af871627a5df995310e86a60d 100644 (file)
@@ -66,7 +66,8 @@ def init(config, directory, template='generic'):
 
 def revision(
         config, message=None, autogenerate=False, sql=False,
-        head="head", splice=False, branch_label=None):
+        head="head", splice=False, branch_label=None,
+        version_path=None, rev_id=None):
     """Create a new revision file."""
 
     script = ScriptDirectory.from_config(config)
@@ -103,12 +104,12 @@ def revision(
         ):
             script.run_env()
     return script.generate_revision(
-        util.rev_id(), message, refresh=True,
+        rev_id or util.rev_id(), message, refresh=True,
         head=head, splice=splice, branch_labels=branch_label,
-        **template_args)
+        version_path=version_path, **template_args)
 
 
-def merge(config, revisions, message=None, branch_label=None):
+def merge(config, revisions, message=None, branch_label=None, rev_id=None):
     """Merge two revisions together.  Creates a new migration file.
 
     .. versionadded:: 0.7.0
@@ -125,7 +126,7 @@ def merge(config, revisions, message=None, branch_label=None):
                           # e.g. multiple databases
     }
     return script.generate_revision(
-        util.rev_id(), message, refresh=True,
+        rev_id or util.rev_id(), message, refresh=True,
         head=revisions, branch_labels=branch_label,
         **template_args)
 
@@ -250,11 +251,16 @@ def history(config, rev_range=None, verbose=False):
         _display_history(config, script, base, head)
 
 
-def heads(config, verbose=False):
+def heads(config, verbose=False, resolve_dependencies=False):
     """Show current available heads in the script directory"""
 
     script = ScriptDirectory.from_config(config)
-    for rev in script.get_revisions("heads"):
+    if resolve_dependencies:
+        heads = script.get_revisions("heads")
+    else:
+        heads = script.get_revisions(script.get_heads())
+
+    for rev in heads:
         config.print_stdout(
             rev.cmd_format(
                 verbose, include_branches=True, tree_indicators=False))
index 7d1beaf71e05174538f238a50da4ad21fdfe374d..27bb31a0728dff20c5fa674c7dde6b2e765673c9 100644 (file)
@@ -249,6 +249,22 @@ class CommandLine(object):
                         "'head' to splice onto"
                     )
                 ),
+                'rev_id': (
+                    "--rev-id",
+                    dict(
+                        type=str,
+                        help="Specify a hardcoded revision id instead of "
+                        "generating one"
+                    )
+                ),
+                'version_path': (
+                    "--version-path",
+                    dict(
+                        type=str,
+                        help="Specify specific path from config for "
+                        "version file"
+                    )
+                ),
                 'branch_label': (
                     "--branch-label",
                     dict(
@@ -264,6 +280,13 @@ class CommandLine(object):
                         help="Use more verbose output"
                     )
                 ),
+                'resolve_dependencies': (
+                    '--resolve-dependencies',
+                    dict(
+                        action="store_true",
+                        help="Treat dependency versions as down revisions"
+                    )
+                ),
                 'autogenerate': (
                     "--autogenerate",
                     dict(
index 0ab52ab6c70d0ba2a1d3185851fcb1780f52c50d..237cf79ca896e26cd657f5cfbe173732c3a70650 100644 (file)
@@ -81,6 +81,16 @@ class RevisionMap(object):
         self._revision_map
         return self.bases
 
+    @util.memoized_property
+    def _real_heads(self):
+        """All "real" head revisions as strings.
+
+        :return: a tuple of string revision numbers.
+
+        """
+        self._revision_map
+        return self._real_heads
+
     @util.memoized_property
     def _real_bases(self):
         """All "real" base revisions as strings.
@@ -100,6 +110,7 @@ class RevisionMap(object):
         map_ = {}
 
         heads = sqlautil.OrderedSet()
+        _real_heads = sqlautil.OrderedSet()
         self.bases = ()
         self._real_bases = ()
 
@@ -113,6 +124,7 @@ class RevisionMap(object):
             if revision.branch_labels:
                 has_branch_labels.add(revision)
             heads.add(revision.revision)
+            _real_heads.add(revision.revision)
             if revision.is_base:
                 self.bases += (revision.revision, )
             if revision._is_real_base:
@@ -125,10 +137,13 @@ class RevisionMap(object):
                               % (downrev, rev))
                 down_revision = map_[downrev]
                 down_revision.add_nextrev(rev)
-                heads.discard(downrev)
+                if downrev in rev._versioned_down_revisions:
+                    heads.discard(downrev)
+                _real_heads.discard(downrev)
 
         map_[None] = map_[()] = None
         self.heads = tuple(heads)
+        self._real_heads = tuple(_real_heads)
 
         for revision in has_branch_labels:
             self._add_branches(revision, map_)
@@ -188,10 +203,16 @@ class RevisionMap(object):
                 )
             map_[downrev].add_nextrev(revision)
         if revision._is_real_head:
+            self._real_heads = tuple(
+                head for head in self._real_heads
+                if head not in
+                set(revision._all_down_revisions).union([revision.revision])
+            ) + (revision.revision,)
+        if revision.is_head:
             self.heads = tuple(
                 head for head in self.heads
                 if head not in
-                set(revision._all_down_revisions).union([revision.revision])
+                set(revision._versioned_down_revisions).union([revision.revision])
             ) + (revision.revision,)
 
     def get_current_head(self, branch_label=None):
@@ -351,11 +372,9 @@ class RevisionMap(object):
 
         return bool(
             set(self._get_descendant_nodes([target],
-                include_dependencies=False
-                ))
+                include_dependencies=False))
             .union(self._get_ancestor_nodes([target],
-                   include_dependencies=False
-                   ))
+                   include_dependencies=False))
             .intersection(test_against_revs)
         )
 
@@ -372,7 +391,7 @@ class RevisionMap(object):
                 return self.filter_for_lineage(
                     self.heads, branch_label), branch_label
             else:
-                return self.heads, branch_label
+                return self._real_heads, branch_label
         elif id_ == 'head':
             return (self.get_current_head(branch_label), ), branch_label
         elif id_ == 'base' or id_ is None:
index 21476521b628e46a4830a6da24b60989b724d130..aa877042c7d2a0039b1a4ec6d31a583118658dab 100644 (file)
@@ -15,6 +15,7 @@ _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+')
 _default_file_template = "%(rev)s_%(slug)s"
+_split_on_space_comma = re.compile(r',|(?: +)')
 
 
 class ScriptDirectory(object):
@@ -40,10 +41,11 @@ class ScriptDirectory(object):
 
     def __init__(self, dir, file_template=_default_file_template,
                  truncate_slug_length=40,
+                 version_locations=None,
                  sourceless=False, output_encoding="utf-8"):
         self.dir = dir
-        self.versions = os.path.join(self.dir, 'versions')
         self.file_template = file_template
+        self.version_locations = version_locations
         self.truncate_slug_length = truncate_slug_length or 40
         self.sourceless = sourceless
         self.output_encoding = output_encoding
@@ -54,12 +56,38 @@ class ScriptDirectory(object):
                                     "the 'init' command to create a new "
                                     "scripts folder." % dir)
 
+    @property
+    def versions(self):
+        loc = self._version_locations
+        if len(loc) > 1:
+            raise util.CommandError("Multiple version_locations present")
+        else:
+            return loc[0]
+
+    @util.memoized_property
+    def _version_locations(self):
+        if self.version_locations:
+            return [
+                os.path.abspath(util.coerce_resource_to_filename(location))
+                for location in self.version_locations
+            ]
+        else:
+            return (os.path.abspath(os.path.join(self.dir, 'versions')),)
+
     def _load_revisions(self):
-        for file_ in os.listdir(self.versions):
-            script = Script._from_filename(self, self.versions, file_)
-            if script is None:
-                continue
-            yield script
+        if self.version_locations:
+            paths = [
+                vers for vers in self._version_locations
+                if os.path.exists(vers)]
+        else:
+            paths = [self.versions]
+
+        for vers in paths:
+            for file_ in os.listdir(vers):
+                script = Script._from_filename(self, vers, file_)
+                if script is None:
+                    continue
+                yield script
 
     @classmethod
     def from_config(cls, config):
@@ -77,6 +105,10 @@ class ScriptDirectory(object):
         truncate_slug_length = config.get_main_option("truncate_slug_length")
         if truncate_slug_length is not None:
             truncate_slug_length = int(truncate_slug_length)
+
+        version_locations = config.get_main_option("version_locations")
+        if version_locations:
+            version_locations = _split_on_space_comma.split(version_locations)
         return ScriptDirectory(
             util.coerce_resource_to_filename(script_location),
             file_template=config.get_main_option(
@@ -84,7 +116,8 @@ class ScriptDirectory(object):
                 _default_file_template),
             truncate_slug_length=truncate_slug_length,
             sourceless=config.get_main_option("sourceless") == "true",
-            output_encoding=config.get_main_option("output_encoding", "utf-8")
+            output_encoding=config.get_main_option("output_encoding", "utf-8"),
+            version_locations=version_locations
         )
 
     @contextmanager
@@ -217,7 +250,7 @@ class ScriptDirectory(object):
             return self.revision_map.get_current_head()
 
     def get_heads(self):
-        """Return all "head" revisions as strings.
+        """Return all "versioned head" revisions as strings.
 
         This is normally a list of length one,
         unless branches are present.  The
@@ -366,9 +399,17 @@ class ScriptDirectory(object):
                     shutil.copy,
                     src, dest)
 
+    def _ensure_directory(self, path):
+        path = os.path.abspath(path)
+        if not os.path.exists(path):
+            util.status(
+                "Creating directory %s" % path,
+                os.makedirs, path)
+
     def generate_revision(
             self, revid, message, head=None,
-            refresh=False, splice=False, branch_labels=None, **kw):
+            refresh=False, splice=False, branch_labels=None,
+            version_path=None, **kw):
         """Generate a new revision file.
 
         This runs the ``script.py.mako`` template, given
@@ -384,16 +425,10 @@ class ScriptDirectory(object):
 
          .. versionadded:: 0.7.0
 
-        :param refresh: when True, the in-memory state of this
-         :class:`.ScriptDirectory` will be updated with a new
-         :class:`.Script` instance representing the new revision;
-         the :class:`.Script` instance is returned.
-         If False, the file is created but the state of the
-         :class:`.ScriptDirectory` is unmodified; ``None``
-         is returned.
         :param splice: if True, allow the "head" version to not be an
          actual head; otherwise, the selected head must be a head
          (e.g. endpoint) revision.
+        :param refresh: deprecated.
 
         """
         if head is None:
@@ -410,7 +445,33 @@ class ScriptDirectory(object):
             raise util.CommandError("Duplicate head revisions specified")
 
         create_date = datetime.datetime.now()
-        path = self._rev_path(revid, message, create_date)
+
+        if version_path is None:
+            if len(self._version_locations) > 1:
+                for head in heads:
+                    if head is not None:
+                        version_path = os.path.dirname(head.path)
+                        break
+                else:
+                    raise util.CommandError(
+                        "Multiple version locations present, "
+                        "please specify --version-path")
+            else:
+                version_path = self.versions
+
+        norm_path = os.path.normpath(os.path.abspath(version_path))
+        for vers_path in self._version_locations:
+            if os.path.normpath(vers_path) == norm_path:
+                break
+        else:
+            raise util.CommandError(
+                "Path %s is not represented in current "
+                "version locations" % version_path)
+
+        if self.version_locations:
+            self._ensure_directory(version_path)
+
+        path = self._rev_path(version_path, revid, message, create_date)
 
         if not splice:
             for head in heads:
@@ -432,23 +493,20 @@ class ScriptDirectory(object):
             message=message if message is not None else ("empty message"),
             **kw
         )
-        if refresh:
-            script = Script._from_path(self, path)
-            if branch_labels and not script.branch_labels:
-                raise util.CommandError(
-                    "Version %s specified branch_labels %s, however the "
-                    "migration file %s does not have them; have you upgraded "
-                    "your script.py.mako to include the "
-                    "'branch_labels' section?" % (
-                        script.revision, branch_labels, script.path
-                    ))
-
-            self.revision_map.add_revision(script)
-            return script
-        else:
-            return None
+        script = Script._from_path(self, path)
+        if branch_labels and not script.branch_labels:
+            raise util.CommandError(
+                "Version %s specified branch_labels %s, however the "
+                "migration file %s does not have them; have you upgraded "
+                "your script.py.mako to include the "
+                "'branch_labels' section?" % (
+                    script.revision, branch_labels, script.path
+                ))
 
-    def _rev_path(self, rev_id, message, create_date):
+        self.revision_map.add_revision(script)
+        return script
+
+    def _rev_path(self, path, rev_id, message, create_date):
         slug = "_".join(_slug_re.findall(message or "")).lower()
         if len(slug) > self.truncate_slug_length:
             slug = slug[:self.truncate_slug_length].rsplit('_', 1)[0] + '_'
@@ -464,7 +522,7 @@ class ScriptDirectory(object):
                 'second': create_date.second
             }
         )
-        return os.path.join(self.versions, filename)
+        return os.path.join(path, filename)
 
 
 class Script(revision.Revision):
@@ -526,7 +584,7 @@ class Script(revision.Revision):
             entry += "Parent: %s\n" % (self._format_down_revision(), )
 
         if self.dependencies:
-            entry += "Depends on: %s\n" % (
+            entry += "Also depends on: %s\n" % (
                 util.format_as_comma(self.dependencies))
 
         if self.is_branch_point:
@@ -558,7 +616,8 @@ class Script(revision.Revision):
 
     def _head_only(
             self, include_branches=False, include_doc=False,
-            include_parents=False, tree_indicators=True):
+            include_parents=False, tree_indicators=True,
+            head_indicators=True):
         text = self.revision
         if include_parents:
             if self.dependencies:
@@ -572,9 +631,14 @@ class Script(revision.Revision):
                     self._format_down_revision(), text)
         if include_branches and self.branch_labels:
             text += " (%s)" % util.format_as_comma(self.branch_labels)
+        if head_indicators or tree_indicators:
+            text += "%s%s" % (
+                " (head)" if self._is_real_head else "",
+                " (effective head)" if self.is_head and
+                    not self._is_real_head else ""
+                )
         if tree_indicators:
-            text += "%s%s%s" % (
-                " (head)" if self.is_head else "",
+            text += "%s%s" % (
                 " (branchpoint)" if self.is_branch_point else "",
                 " (mergepoint)" if self.is_merge_point else "",
             )
index 90037d77792597bf98a64229f0fcd2309ff09ec3..4d3bf6eade42b68f467d203be2bb76ec59476f83 100644 (file)
@@ -20,6 +20,11 @@ script_location = ${script_location}
 # versions/ directory
 # sourceless = false
 
+# version location specification; this defaults
+# to ${script_location}/versions.  When using multiple version
+# directories, initial revisions must be specified with --version-path
+# version_locations = %(here)s/bar %(here)s/bat ${script_location}/versions
+
 # the output encoding used when revision files
 # are written from script.py.mako
 # output_encoding = utf-8
index ced3558d955d33362ba52bacba8b58c51e1221db..929a4be67d275b02b02bb2e8a7bebac4cbc3b9e1 100644 (file)
@@ -20,6 +20,11 @@ script_location = ${script_location}
 # versions/ directory
 # sourceless = false
 
+# version location specification; this defaults
+# to ${script_location}/versions.  When using multiple version
+# directories, initial revisions must be specified with --version-path
+# version_locations = %(here)s/bar %(here)s/bat ${script_location}/versions
+
 # the output encoding used when revision files
 # are written from script.py.mako
 # output_encoding = utf-8
index 6a5f206e06ccc286770769f2e19d37b855f95dd4..62191e044158ccc1cd3b97b21f075002fe93ec23 100644 (file)
@@ -20,6 +20,11 @@ script_location = ${script_location}
 # versions/ directory
 # sourceless = false
 
+# version location specification; this defaults
+# to ${script_location}/versions.  When using multiple version
+# directories, initial revisions must be specified with --version-path
+# version_locations = %(here)s/bar %(here)s/bat ${script_location}/versions
+
 # the output encoding used when revision files
 # are written from script.py.mako
 # output_encoding = utf-8
index 24707b42ff1ab93a3a6fc7804ca115307ab18ffd..9c53d5dbccd87dca8bc522bf8ad8f293484fdcd2 100644 (file)
@@ -113,6 +113,43 @@ datefmt = %%H:%%M:%%S
     """ % (dir_, url, "true" if sourceless else "false"))
 
 
+def _multi_dir_testing_config(sourceless=False):
+    dir_ = os.path.join(_get_staging_directory(), 'scripts')
+    url = "sqlite:///%s/foo.db" % dir_
+
+    return _write_config_file("""
+[alembic]
+script_location = %s
+sqlalchemy.url = %s
+sourceless = %s
+version_locations = %%(here)s/model1/ %%(here)s/model2/ %%(here)s/model3/
+
+[loggers]
+keys = root
+
+[handlers]
+keys = console
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatters]
+keys = generic
+
+[formatter_generic]
+format = %%(levelname)-5.5s [%%(name)s] %%(message)s
+datefmt = %%H:%%M:%%S
+    """ % (dir_, url, "true" if sourceless else "false"))
+
+
 def _no_sql_testing_config(dialect="postgresql", directives=""):
     """use a postgresql url with no host so that
     connections guaranteed to fail"""
index 36a5e773a492d87ad2904d2845a818a8e91db7e9..95a690d2a55bdf867790601c9021eb2fe044f801 100644 (file)
@@ -26,6 +26,22 @@ Changelog
 
           :ref:`branches`
 
+    .. change::
+      :tags: feature, versioning
+      :tickets: 124
+
+      In conjunction with support for multiple independent bases, the
+      specific version directories are now also configurable to include
+      multiple, user-defined directories.   When multiple directories exist,
+      the creation of a revision file with no down revision requires
+      that the starting directory is indicated; the creation of subsequent
+      revisions along that lineage will then automatically use that
+      directory for new files.
+
+      .. seealso::
+
+          :ref:`multiple_version_directories`
+
     .. change::
       :tags: feature, operations, sqlite
       :tickets: 21
index 423923d656b6dac92d359487d0d0a30c8d6b6352..9b4e26f636ab029d97cf8f2bb010a39dd0b16793 100644 (file)
@@ -132,6 +132,11 @@ The file generated with the "generic" configuration looks like::
     # versions/ directory
     # sourceless = false
 
+    # version location specification; this defaults
+    # to alembic/versions.  When using multiple version
+    # directories, initial revisions must be specified with --version-path
+    # version_locations = %(here)s/bar %(here)s/bat alembic/versions
+
     # the output encoding used when revision files
     # are written from script.py.mako
     # output_encoding = utf-8
@@ -228,6 +233,12 @@ This file contains the following features:
 
   .. versionadded:: 0.6.4
 
+* ``version_locations`` - an optional list of revision file locations, to
+  allow revisions to exist in multiple directories simultaneously.
+  See :ref:`multiple_bases` for examples.
+
+  .. versionadded:: 0.7.0
+
 * ``output_encoding`` - the encoding to use when Alembic writes the
   ``script.py.mako`` file into a new migration file.  Defaults to ``'utf-8'``.
 
@@ -456,7 +467,7 @@ and ``branches``) will show us full information about each revision::
     Parent: <base>
     Path: /path/to/yourproject/alembic/versions/1975ea83b712_add_account_table.py
 
-        add account table
+        create account table
 
         Revision ID: 1975ea83b712
         Revises:
@@ -566,7 +577,7 @@ and the database did not.  We'd get output like::
 
     $ alembic revision --autogenerate -m "Added account table"
     INFO [alembic.context] Detected added table 'account'
-    Generating /Users/classic/Desktop/tmp/alembic/versions/27c6a30d7c24.py...done
+    Generating /path/to/foo/alembic/versions/27c6a30d7c24.py...done
 
 We can then view our file ``27c6a30d7c24.py`` and see that a rudimentary migration
 is already present::
@@ -1412,10 +1423,10 @@ Both it, as well as ``ae1027a6acf_add_a_column.py``, reference
 ``1975ea83b712_add_account_table.py`` as the "downgrade" revision.  To illustrate::
 
     # main source tree:
-    1975ea83b712 (add account table) -> ae1027a6acf (add a column)
+    1975ea83b712 (create account table) -> ae1027a6acf (add a column)
 
     # branched source tree
-    1975ea83b712 (add account table) -> 27c6a30d7c24 (add shopping cart table)
+    1975ea83b712 (create account table) -> 27c6a30d7c24 (add shopping cart table)
 
 Above, we can see ``1975ea83b712`` is our **branch point**; two distinct versions
 both refer to it as its parent.  The Alembic command ``branches`` illustrates
@@ -1427,7 +1438,7 @@ this fact::
   Branches into: 27c6a30d7c24, ae1027a6acf
   Path: foo/versions/1975ea83b712_add_account_table.py
 
-      add account table
+      create account table
 
       Revision ID: 1975ea83b712
       Revises:
@@ -1442,7 +1453,7 @@ as a ``branchpoint``::
     $ alembic history
     1975ea83b712 -> 27c6a30d7c24 (head), add shopping cart table
     1975ea83b712 -> ae1027a6acf (head), add a column
-    <base> -> 1975ea83b712 (branchpoint), add account table
+    <base> -> 1975ea83b712 (branchpoint), create account table
 
 We can get a view of just the current heads using ``alembic heads``::
 
@@ -1501,7 +1512,7 @@ pass to it an argument such as ``heads``, meaning we'd like to merge all
 heads.  Or, we can pass it individual revision numbers sequentally::
 
     $ alembic merge -m "merge ae1 and 27c" ae1027 27c6a
-      Generating /path/to/foo/versions/53fffde5ad5_merge_ae1_and_27c.py ... done
+      Generating /path/to/foo/alembic/versions/53fffde5ad5_merge_ae1_and_27c.py ... done
 
 Looking inside the new file, we see it as a regular migration file, with
 the only new twist is that ``down_revision`` points to both revisions::
@@ -1556,14 +1567,14 @@ History shows a similar result, as the mergepoint becomes our head::
     ae1027a6acf, 27c6a30d7c24 -> 53fffde5ad5 (head) (mergepoint), merge ae1 and 27c
     1975ea83b712 -> ae1027a6acf, add a column
     1975ea83b712 -> 27c6a30d7c24, add shopping cart table
-    <base> -> 1975ea83b712 (branchpoint), add account table
+    <base> -> 1975ea83b712 (branchpoint), create account table
 
 With a single ``head`` target, a generic ``upgrade`` can proceed::
 
     $ alembic upgrade head
     INFO  [alembic.migration] Context impl PostgresqlImpl.
     INFO  [alembic.migration] Will assume transactional DDL.
-    INFO  [alembic.migration] Running upgrade  -> 1975ea83b712, add account table
+    INFO  [alembic.migration] Running upgrade  -> 1975ea83b712, create account table
     INFO  [alembic.migration] Running upgrade 1975ea83b712 -> 27c6a30d7c24, add shopping cart table
     INFO  [alembic.migration] Running upgrade 1975ea83b712 -> ae1027a6acf, add a column
     INFO  [alembic.migration] Running upgrade ae1027a6acf, 27c6a30d7c24 -> 53fffde5ad5, merge ae1 and 27c
@@ -1584,7 +1595,7 @@ With a single ``head`` target, a generic ``upgrade`` can proceed::
 
     .. sourcecode:: sql
 
-      -- Running upgrade  -> 1975ea83b712, add account table
+      -- Running upgrade  -> 1975ea83b712, create account table
       INSERT INTO alembic_version (version_num) VALUES ('1975ea83b712')
 
       -- Running upgrade 1975ea83b712 -> 27c6a30d7c24, add shopping cart table
@@ -1667,7 +1678,7 @@ from a fresh database and ran ``upgrade heads`` we'd see::
     $ alembic upgrade heads
     INFO  [alembic.migration] Context impl PostgresqlImpl.
     INFO  [alembic.migration] Will assume transactional DDL.
-    INFO  [alembic.migration] Running upgrade  -> 1975ea83b712, add account table
+    INFO  [alembic.migration] Running upgrade  -> 1975ea83b712, create account table
     INFO  [alembic.migration] Running upgrade 1975ea83b712 -> ae1027a6acf, add a column
     INFO  [alembic.migration] Running upgrade 1975ea83b712 -> 27c6a30d7c24, add shopping cart table
 
@@ -1697,7 +1708,7 @@ continue updating the single value down to the previous versions::
     1975ea83b712 (branchpoint)
 
     $ alembic downgrade -1
-    INFO  [alembic.migration] Running downgrade 1975ea83b712 -> , add account table
+    INFO  [alembic.migration] Running downgrade 1975ea83b712 -> , create account table
 
     $ alembic current
 
@@ -1711,7 +1722,7 @@ it guarantees that ``1975ea83b712`` will have been applied, but not that
 any "sibling" versions are applied::
 
     $ alembic upgrade 27c6a
-    INFO  [alembic.migration] Running upgrade  -> 1975ea83b712, add account table
+    INFO  [alembic.migration] Running upgrade  -> 1975ea83b712, create account table
     INFO  [alembic.migration] Running upgrade 1975ea83b712 -> 27c6a30d7c24, add shopping cart table
 
 With ``1975ea83b712`` and ``27c6a30d7c24`` applied, ``ae1027a6acf`` is just
@@ -1751,7 +1762,7 @@ label applied to this revision::
     $ alembic history
     1975ea83b712 -> 27c6a30d7c24 (shoppingcart) (head), add shopping cart table
     1975ea83b712 -> ae1027a6acf (head), add a column
-    <base> -> 1975ea83b712 (branchpoint), add account table
+    <base> -> 1975ea83b712 (branchpoint), create account table
 
 With the label applied, the name ``shoppingcart`` now serves as an alias
 for the ``27c6a30d7c24`` revision specifically.  We can illustrate this
@@ -1795,7 +1806,7 @@ we use the ``--head`` argument, either with the specific revision identifier
 ``27c6a30d7c24``, or more generically using our branchname ``shoppingcart@head``::
 
     $ alembic revision -m "add a shopping cart column"  --head shoppingcart@head
-      Generating /path/to/foo/versions/d747a8a8879_add_a_shopping_cart_column.py ... done
+      Generating /path/to/foo/alembic/versions/d747a8a8879_add_a_shopping_cart_column.py ... done
 
 ``alembic history`` shows both files now part of the ``shoppingcart`` branch::
 
@@ -1803,7 +1814,7 @@ we use the ``--head`` argument, either with the specific revision identifier
     1975ea83b712 -> ae1027a6acf (head), add a column
     27c6a30d7c24 -> d747a8a8879 (shoppingcart) (head), add a shopping cart column
     1975ea83b712 -> 27c6a30d7c24 (shoppingcart), add shopping cart table
-    <base> -> 1975ea83b712 (branchpoint), add account table
+    <base> -> 1975ea83b712 (branchpoint), create account table
 
 We can limit our history operation just to this branch as well::
 
@@ -1817,7 +1828,7 @@ base, we can do that as follows::
     $ alembic history -r :shoppingcart@head
     27c6a30d7c24 -> d747a8a8879 (shoppingcart) (head), add a shopping cart column
     1975ea83b712 -> 27c6a30d7c24 (shoppingcart), add shopping cart table
-    <base> -> 1975ea83b712 (branchpoint), add account table
+    <base> -> 1975ea83b712 (branchpoint), create account table
 
 We can run this operation from the "base" side as well, but we get a different
 result::
@@ -1826,7 +1837,7 @@ result::
     1975ea83b712 -> ae1027a6acf (head), add a column
     27c6a30d7c24 -> d747a8a8879 (shoppingcart) (head), add a shopping cart column
     1975ea83b712 -> 27c6a30d7c24 (shoppingcart), add shopping cart table
-    <base> -> 1975ea83b712 (branchpoint), add account table
+    <base> -> 1975ea83b712 (branchpoint), create account table
 
 When we list from ``shoppingcart@base`` without an endpoint, it's really shorthand
 for ``-r shoppingcart@base:heads``, e.g. all heads, and since ``shoppingcart@base``
@@ -1842,7 +1853,7 @@ if this weren't a head already, we could ask for the "head of the branch
 that includes ``ae1027a6acf``" as follows::
 
     $ alembic revision -m "add another account column" --head ae10@head
-      Generating /Users/classic/dev/alembic/foo/versions/55af2cb1c267_add_another_account_column.py ... done
+      Generating /path/to/foo/alembic/versions/55af2cb1c267_add_another_account_column.py ... done
 
 More Label Syntaxes
 ^^^^^^^^^^^^^^^^^^^
@@ -1863,6 +1874,8 @@ This kind of thing works from history as well::
     $ alembic history -r current:shoppingcart@+2
 
 
+.. _multiple_bases:
+
 Working with Multiple Bases
 ---------------------------
 
@@ -1872,13 +1885,39 @@ if we have multiple heads, ``alembic revision`` allows us to tell it which
 allow us to assign names to branches that we can use in subsequent commands.
 Let's put all these together and refer to a new "base", that is, a whole
 new tree of revision files that will be semi-independent of the account/shopping
-cart revisions we've been working with.
+cart revisions we've been working with.  This new tree will deal with
+database tables involving "networking".
+
+Setting up Multiple Version Directories
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+While optional, it is often the case that when working with multiple bases,
+we'd like different sets of version files to exist within their own directories;
+typically, if an application is organized into several sub-modules, each
+one would have a version directory containing migrations pertinent to
+that module.  So to start out, we can edit ``alembic.ini`` to refer
+to multiple directories;  we'll also state the current ``versions``
+directory as one of them::
+
+  # version location specification; this defaults
+  # to foo/versions.  When using multiple version
+  # directories, initial revisions must be specified with --version-path
+  version_locations = %(here)s/model/networking %(here)s/alembic/versions
+
+The new folder ``%(here)s/model/networking`` is in terms of where
+the ``alembic.ini`` file is as we are using the symbol ``%(here)s`` which
+resolves to this.   When we create our first new revision, the directory
+``model/networking`` will be created automatically if it does not
+exist yet.  Once we've created a revision here, the path is used automatically
+when generating subsequent revision files that refer to this revision tree.
 
 Creating a Labeled Base Revision
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-We want to create a new, labeled branch in one step.  To ensure the branch can
-accommodate this label, we need to ensure our ``script.py.mako`` file, used
+We also want our new branch to have its own name, and for that we want to
+apply a branch label to the base.  In order to achieve this using the
+``alembic revision`` command without editing, we need to ensure our
+``script.py.mako`` file, used
 for generating new revision files, has the appropriate substitutions present.
 If Alembic version 0.7.0 or greater was used to generate the original
 migration environment, this is already done.  However when working with an older
@@ -1894,16 +1933,19 @@ underneath the ``down_revision`` directive::
 
 With this in place, we can create a new revision file, starting up a branch
 that will deal with database tables involving networking; we specify the
-"head" version of ``base`` as well as a ``branch_label``::
+``--head`` version of ``base``, a ``--branch-label`` of ``networking``,
+and the directory we want this first revision file to be
+placed in with ``--version-path``::
 
-    $ alembic revision -m "create networking branch" --head=base --branch-label=networking
-      Generating /Users/classic/dev/alembic/foo/versions/3782d9986ced_create_networking_branch.py ... done
+    $ alembic revision -m "create networking branch" --head=base --branch-label=networking --version-path=model/networking
+      Creating directory /path/to/foo/model/networking ... done
+      Generating /path/to/foo/model/networking/3cac04ae8714_create_networking_branch.py ... done
 
 If we ran the above command and we didn't have the newer ``script.py.mako``
 directive, we'd get this error::
 
   FAILED: Version 3cac04ae8714 specified branch_labels networking, however
-  the migration file foo/versions/3cac04ae8714_create_networking_branch.py
+  the migration file foo/model/networking/3cac04ae8714_create_networking_branch.py
   does not have them; have you upgraded your script.py.mako to include the 'branch_labels'
   section?
 
@@ -1919,23 +1961,24 @@ Once we have a new, permanent (for as long as we desire it to be)
 base in our system, we'll always have multiple heads present::
 
     $ alembic heads
-    3782d9986ced (networking)
-    ae1027a6acf
-    d747a8a8879 (shoppingcart)
+    3cac04ae8714 (networking) (head)
+    27c6a30d7c24 (shoppingcart) (head)
+    ae1027a6acf (head)
 
 When we want to add a new revision file to ``networking``, we specify
-``networking@head`` as the ``--head``::
+``networking@head`` as the ``--head``.  The appropriate version directory
+is now selected automatically based on the head we choose::
 
     $ alembic revision -m "add ip number table" --head=networking@head
-      Generating /Users/classic/dev/alembic/foo/versions/109ec7d132bf_add_ip_number_table.py ... done
+      Generating /path/to/foo/model/networking/109ec7d132bf_add_ip_number_table.py ... done
 
 It's important that we refer to the head using ``networking@head``; if we
-only refer to ``networking``, that refers to only ``3782d9986ced`` specifically;
+only refer to ``networking``, that refers to only ``3cac04ae8714`` specifically;
 if we specify this and it's not a head, ``alembic revision`` will make sure
 we didn't mean to specify the head::
 
     $ alembic revision -m "add DNS table" --head=networking
-      FAILED: Revision 3782d9986ced is not a head revision; please
+      FAILED: Revision 3cac04ae8714 is not a head revision; please
       specify --splice to create a new branch from this revision
 
 As mentioned earlier, as this base is independent, we can view its history
@@ -1943,8 +1986,8 @@ from the base using ``history -r networking@base:``::
 
     $ alembic history -r networking@base:
     109ec7d132bf -> 29f859a13ea (networking) (head), add DNS table
-    3782d9986ced -> 109ec7d132bf (networking), add ip number table
-    <base> -> 3782d9986ced (networking), create networking branch
+    3cac04ae8714 -> 109ec7d132bf (networking), add ip number table
+    <base> -> 3cac04ae8714 (networking), create networking branch
 
 Note this is the same output we'd get at this point if we used
 ``-r :networking@head``.
@@ -1953,97 +1996,147 @@ We may now run upgrades or downgrades freely, among individual branches
 (let's assume a clean database again)::
 
     $ alembic upgrade networking@head
-    INFO  [alembic.migration] Running upgrade  -> 3782d9986ced, create networking branch
-    INFO  [alembic.migration] Running upgrade 3782d9986ced -> 109ec7d132bf, add ip number table
+    INFO  [alembic.migration] Running upgrade  -> 3cac04ae8714, create networking branch
+    INFO  [alembic.migration] Running upgrade 3cac04ae8714 -> 109ec7d132bf, add ip number table
     INFO  [alembic.migration] Running upgrade 109ec7d132bf -> 29f859a13ea, add DNS table
 
 or against the whole thing using ``heads``::
 
     $ alembic upgrade heads
-    INFO  [alembic.migration] Running upgrade  -> 1975ea83b712, add account table
+    INFO  [alembic.migration] Running upgrade  -> 1975ea83b712, create account table
     INFO  [alembic.migration] Running upgrade 1975ea83b712 -> 27c6a30d7c24, add shopping cart table
     INFO  [alembic.migration] Running upgrade 27c6a30d7c24 -> d747a8a8879, add a shopping cart column
     INFO  [alembic.migration] Running upgrade 1975ea83b712 -> ae1027a6acf, add a column
     INFO  [alembic.migration] Running upgrade ae1027a6acf -> 55af2cb1c267, add another account column
 
-Branch and Merge Nuttiness
---------------------------
+Branch Dependencies
+-------------------
+
+When working with multiple roots, it is expected that these different
+revision streams will need to refer to one another.   For example, a new
+revision in ``networking`` which needs to refer to the ``account``
+table will want to establish ``55af2cb1c267, add another account column``,
+the last revision that
+works with the account table, as a dependency.   From a graph perspective,
+this means nothing more that the new file will feature both
+``55af2cb1c267`` and ``29f859a13ea , add DNS table`` as "down" revisions,
+and looks just as though we had merged these two branches together.  However,
+we don't want to consider these as "merged"; we want the two revision
+streams to *remain independent*, even though a version in ``networking``
+is going to reach over into the other stream.  To support this use case,
+Alembic provides a directive known as ``depends_on``, which allows
+a revision file to refer to another as a "dependency", very similar to
+an entry in ``down_revision`` but not quite.
+
+First we will build out our new revision on the ``networking`` branch
+in the usual way::
+
+    $ alembic revision -m "add ip account table" --head=networking@head
+      Generating /path/to/foo/model/networking/2a95102259be_add_ip_account_table.py ... done
+
+Next, we'll add an explicit dependency inside the file, by placing the
+directive ``depends_on='55af2cb1c267'`` underneath the other directives::
 
-We have quite a lot of versioning going on, history overall now shows::
+    # revision identifiers, used by Alembic.
+    revision = '2a95102259be'
+    down_revision = '29f859a13ea'
+    branch_labels = None
+    depends_on='55af2cb1c267'
 
-    $ alembic history
-    109ec7d132bf -> 29f859a13ea (networking) (head), add DNS table
-    3782d9986ced -> 109ec7d132bf (networking), add ip number table
-    <base> -> 3782d9986ced (networking), create networking branch
-    ae1027a6acf -> 55af2cb1c267 (head), add another account column
-    1975ea83b712 -> ae1027a6acf, add a column
-    27c6a30d7c24 -> d747a8a8879 (shoppingcart) (head), add a shopping cart column
-    1975ea83b712 -> 27c6a30d7c24 (shoppingcart), add shopping cart table
-    <base> -> 1975ea83b712 (branchpoint), add account table
+Currently, ``depends_on`` needs to be a real revision number, not a partial
+number or branch name.
 
+We now can see the effect this directive has, when we view the history
+of the ``networking`` branch in terms of "heads", e.g., all the revisions that
+are descendants::
 
-If you actually wanted, all three branches can be merged::
+    $ alembic history -r :networking@head
+    29f859a13ea (55af2cb1c267) -> 2a95102259be (networking) (head), add ip account table
+    109ec7d132bf -> 29f859a13ea (networking), add DNS table
+    3cac04ae8714 -> 109ec7d132bf (networking), add ip number table
+    <base> -> 3cac04ae8714 (networking), create networking branch
+    ae1027a6acf -> 55af2cb1c267 (effective head), add another account column
+    1975ea83b712 -> ae1027a6acf, Add a column
+    <base> -> 1975ea83b712 (branchpoint), create account table
+
+What we see is that the full history of the ``networking`` branch, in terms
+of an "upgrade" to the "head", will include that the tree building
+up ``55af2cb1c267 (effective head), add another account column``
+will be pulled in first.   Interstingly, we don't see this displayed
+when we display history in the other direction, e.g. from ``networking@base``::
 
-    $ alembic merge -m "merge all three branches" heads
-      Generating /Users/classic/dev/alembic/foo/versions/3180f4d6e81d_merge_all_three_branches.py ... done
+    $ alembic history -r networking@base:
+    29f859a13ea (55af2cb1c267) -> 2a95102259be (networking) (head), add ip account table
+    109ec7d132bf -> 29f859a13ea (networking), add DNS table
+    3cac04ae8714 -> 109ec7d132bf (networking), add ip number table
+    <base> -> 3cac04ae8714 (networking), create networking branch
 
-    $ alembic upgrade head
-    INFO  [alembic.migration] Running upgrade 29f859a13ea, 55af2cb1c267, d747a8a8879 -> 3180f4d6e81d, merge all three branches
+The reason for the discrepancy is that displaying history from the base
+shows us what would occur if we ran a downgrade operation, instead of an
+upgrade.  If we downgraded all the files in ``networking`` using
+``networking@base``, the dependencies aren't affected, they're left in place.
 
-at which point, we're back to one head, but note!  This head has **two** labels
-now::
+We also see something odd if we view ``heads`` at the moment::
 
     $ alembic heads
-    3180f4d6e81d (shoppingcart, networking)
+    2a95102259be (networking) (head)
+    27c6a30d7c24 (shoppingcart) (head)
+    55af2cb1c267 (effective head)
+
+The head file that we used as a "dependency", ``55af2cb1c267`` is displayed
+as an "effective" head, which we can see also in the history display earlier.
+What this means is that at the moment, if we were to upgrade all versions
+to the top, the ``55af2cb1c267`` revision number would not actually be
+present in the ``alembic_version`` table; this is because it does not have
+a branch of its own subsequent to the ``2a95102259be`` revision which depends
+on it::
 
-    $ alembic current --verbose
-    Current revision(s) for postgresql://scott:XXXXX@localhost/test:
-    Rev: 3180f4d6e81d (head) (mergepoint)
-    Merges: 29f859a13ea, 55af2cb1c267, d747a8a8879
-    Branch names: shoppingcart, networking
-    Path: foo/versions/3180f4d6e81d_merge_all_three_branches.py
+    $ alembic upgrade heads
+    INFO  [alembic.migration] Running upgrade 29f859a13ea, 55af2cb1c267 -> 2a95102259be, add ip account table
 
-        merge all three branches
+    $ alembic current
+    2a95102259be (head)
+    27c6a30d7c24 (head)
 
-        Revision ID: 3180f4d6e81d
-        Revises: 29f859a13ea, 55af2cb1c267, d747a8a8879
-        Create Date: 2014-11-20 16:27:56.395477
+If we add a new revision onto ``55af2cb1c267``, now this branch again becomes
+a "real" branch which would have its own entry in the database::
 
-When labels are combined like this, it means that ``networking@head`` and
-``shoppingcart@head`` are ultimately along the same branch, as is the
-unnamed ``ae1027a6acf`` branch since we've merged everything together.
-``alembic history`` when leading from ``networking@base:``,
-``:shoppingcart@head`` or similar will show the whole tree at this point::
+    $ alembic revision -m "more account changes" --head=55af2cb@head
+      Generating /path/to/foo/versions/34e094ad6ef1_more_account_changes.py ... done
 
-    $ alembic history -r :shoppingcart@head
-    29f859a13ea, 55af2cb1c267, d747a8a8879 -> 3180f4d6e81d (networking, shoppingcart) (head) (mergepoint), merge all three branches
+    $ alembic upgrade heads
+    INFO  [alembic.migration] Running upgrade 55af2cb1c267 -> 34e094ad6ef1, more account changes
+
+    $ alembic current
+    2a95102259be (head)
+    27c6a30d7c24 (head)
+    34e094ad6ef1 (head)
+
+
+For posterity, the revision tree now looks like::
+
+    $ alembic history
+    29f859a13ea (55af2cb1c267) -> 2a95102259be (networking) (head), add ip account table
     109ec7d132bf -> 29f859a13ea (networking), add DNS table
-    3782d9986ced -> 109ec7d132bf (networking), add ip number table
-    <base> -> 3782d9986ced (networking), create networking branch
+    3cac04ae8714 -> 109ec7d132bf (networking), add ip number table
+    <base> -> 3cac04ae8714 (networking), create networking branch
+    1975ea83b712 -> 27c6a30d7c24 (shoppingcart) (head), add shopping cart table
+    55af2cb1c267 -> 34e094ad6ef1 (head), more account changes
     ae1027a6acf -> 55af2cb1c267, add another account column
-    1975ea83b712 -> ae1027a6acf, add a column
-    27c6a30d7c24 -> d747a8a8879 (shoppingcart), add a shopping cart column
-    1975ea83b712 -> 27c6a30d7c24 (shoppingcart), add shopping cart table
-    <base> -> 1975ea83b712 (branchpoint), add account table
-
-It follows then that the "branch labels" feature is useful for branches
-that are **unmerged**.  Once branches are merged into a single stream, labels
-are not particularly useful as they tend to refer to the whole revision
-stream in any case.  They can of course be removed from revision files
-at the point at which they are no longer useful, or moved to other files.
-
-For posterity, here's the graph of the whole thing::
-
-                         --- ae10 --> 55af --->--
-                       /                         \
-    <base> --> 1975 -->                          |
-                       \                         |
-                         --- 27c6 --> d747 -->   |
-                        (shoppingcart)        \  |
-                                              +--+-----> 3180
-                                                 |    (networking,
-                                                /      shoppingcart)
-    <base> --> 3782 -----> 109e ----> 29f8 --->
+    1975ea83b712 -> ae1027a6acf, Add a column
+    <base> -> 1975ea83b712 (branchpoint), create account table
+
+
+                        --- 27c6 --> d747 --> <head>
+                       /   (shoppingcart)
+    <base> --> 1975 -->
+                       \
+                         --- ae10 --> 55af --> <head>
+                                        ^
+                                        +--------+ (dependency)
+                                                 |
+                                                 |
+    <base> --> 3782 -----> 109e ----> 29f8 ---> 2a95 --> <head>
              (networking)
 
 
index 647cf5bcda0c7b5274f82a660db385bce476e127..1f380ab4df2c345779723d2eb7640db3f476710d 100644 (file)
@@ -3,7 +3,7 @@ from alembic.testing import eq_, ne_, is_, assert_raises_message
 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, \
-    three_rev_fixture
+    three_rev_fixture, _multi_dir_testing_config
 from alembic import command
 from alembic.script import ScriptDirectory
 from alembic.environment import EnvironmentContext
@@ -30,9 +30,8 @@ class GeneralOrderedTests(TestBase):
         self._test_004_rev()
         self._test_005_nextrev()
         self._test_006_from_clean_env()
-        self._test_007_no_refresh()
-        self._test_008_long_name()
-        self._test_009_long_name_configurable()
+        self._test_007_long_name()
+        self._test_008_long_name_configurable()
 
     def _test_001_environment(self):
         assert_set = set(['env.py', 'script.py.mako', 'README'])
@@ -93,14 +92,7 @@ class GeneralOrderedTests(TestBase):
         eq_(env.get_heads(), [def_])
         eq_(env.get_base(), abc)
 
-    def _test_007_no_refresh(self):
-        rid = util.rev_id()
-        script = env.generate_revision(rid, "dont' refresh")
-        is_(script, None)
-        env2 = staging_env(create=False)
-        eq_(env2.get_current_head(), rid)
-
-    def _test_008_long_name(self):
+    def _test_007_long_name(self):
         rid = util.rev_id()
         env.generate_revision(rid,
                               "this is a really long name with "
@@ -112,7 +104,7 @@ class GeneralOrderedTests(TestBase):
                 '%s_this_is_a_really_long_name_with_lots_of_.py' % rid),
             os.F_OK)
 
-    def _test_009_long_name_configurable(self):
+    def _test_008_long_name_configurable(self):
         env.truncate_slug_length = 60
         rid = util.rev_id()
         env.generate_revision(rid,
@@ -146,9 +138,11 @@ class ScriptNamingTest(TestBase):
         )
         create_date = datetime.datetime(2012, 7, 25, 15, 8, 5)
         eq_(
-            script._rev_path("12345", "this is a message", create_date),
-            "%s/versions/12345_this_is_a_"
-            "message_2012_7_25_15_8_5.py" % _get_staging_directory()
+            script._rev_path(
+                script.versions, "12345", "this is a message", create_date),
+            os.path.abspath(
+                "%s/versions/12345_this_is_a_"
+                "message_2012_7_25_15_8_5.py" % _get_staging_directory())
         )
 
 
@@ -220,6 +214,59 @@ class RevisionCommandTest(TestBase):
         )
 
 
+class MultiDirRevisionCommandTest(TestBase):
+    def setUp(self):
+        self.env = staging_env()
+        self.cfg = _multi_dir_testing_config()
+
+    def tearDown(self):
+        clear_staging_env()
+
+    def test_multiple_dir_no_bases(self):
+        assert_raises_message(
+            util.CommandError,
+            "Multiple version locations present, please specify "
+            "--version-path",
+            command.revision, self.cfg, message="some message"
+        )
+
+    def test_multiple_dir_no_bases_invalid_version_path(self):
+        assert_raises_message(
+            util.CommandError,
+            "Path foo/bar/ is not represented in current version locations",
+            command.revision,
+            self.cfg, message="x",
+            version_path=os.path.join("foo/bar/")
+        )
+
+    def test_multiple_dir_no_bases_version_path(self):
+        script = command.revision(
+            self.cfg, message="x",
+            version_path=os.path.join(_get_staging_directory(), "model1"))
+        assert os.access(script.path, os.F_OK)
+
+    def test_multiple_dir_chooses_base(self):
+        command.revision(
+            self.cfg, message="x",
+            head="base",
+            version_path=os.path.join(_get_staging_directory(), "model1"))
+
+        script2 = command.revision(
+            self.cfg, message="y",
+            head="base",
+            version_path=os.path.join(_get_staging_directory(), "model2"))
+
+        script3 = command.revision(
+            self.cfg, message="y2",
+            head=script2.revision)
+
+        eq_(
+            os.path.dirname(script3.path),
+            os.path.abspath(os.path.join(_get_staging_directory(), "model2"))
+        )
+        assert os.access(script3.path, os.F_OK)
+
+
 class TemplateArgsTest(TestBase):
 
     def setUp(self):