alembic: Allow cdr, config and voicemail to exist in the same schema
authorGeorge Joseph <gjoseph@digium.com>
Tue, 4 Oct 2016 21:59:54 +0000 (15:59 -0600)
committerGeorge Joseph <gjoseph@digium.com>
Fri, 7 Oct 2016 12:49:42 +0000 (07:49 -0500)
cdr, config and voicemail are all separate alembic trees.  Because
alembic's default is to use a table named 'alembic_version' to store
the current tree revision, the 3 trees can't exist in the same schema
without stepping on each other.

Now each tree uses 'alembic_version_<tree_name>' as the version table.
Each tree's env.py script now first checks for 'alembic_version'.  If
it finds it AND its revision is in the tree's history, the script
renames it to 'alembic_version_<tree_name>'.  Regardless, the script
then continues with the migration using 'alembic_version_<tree_name>'
and creates that table if it's not found.  The result is that if an
existing 'alembic_version' table was found but it didn't belong to this
tree, it's left alone and 'alembic_version_<tree_name>' is used or
created.

WARNING:  If multiple trees are using the same schema, they MUST NOT
CRU or D any objects with names that might exist in the other trees.
An example would be 'yesno_values' type.  If two trees perform
operations on it, one tree could pull it out from under the other.
Thankfully we currently don't share any names among cdr, config and
voicemail.

NOTE:  Since the env.py scripts in each tree were identical, a common
env.py has been placed in the ast-db-manage directory and a symlink
to it has been placed in each tree directory.

ASTERISK-24311 #close
Reported-by: Dafi Ni

Change-Id: I4d593f000350deb5d21a14fa1e9bc3896844d898

contrib/ast-db-manage/cdr/env.py [changed from file to symlink]
contrib/ast-db-manage/config/env.py [changed from file to symlink]
contrib/ast-db-manage/env.py [new file with mode: 0755]
contrib/ast-db-manage/voicemail/env.py [changed from file to symlink]

deleted file mode 100755 (executable)
index 6740d5906d532e5b34fc3f9dae80641289ff5f7f..0000000000000000000000000000000000000000
+++ /dev/null
@@ -1,74 +0,0 @@
-from __future__ import with_statement
-from alembic import context
-from sqlalchemy import engine_from_config, pool
-from logging.config import fileConfig
-
-# this is the Alembic Config object, which provides
-# access to the values within the .ini file in use.
-config = context.config
-
-# Interpret the config file for Python logging.
-# This line sets up loggers basically.
-try:
-    fileConfig(config.config_file_name)
-except:
-    pass
-
-# add your model's MetaData object here
-# for 'autogenerate' support
-# from myapp import mymodel
-# target_metadata = mymodel.Base.metadata
-target_metadata = None
-
-# other values from the config, defined by the needs of env.py,
-# can be acquired:
-# my_important_option = config.get_main_option("my_important_option")
-# ... etc.
-
-def run_migrations_offline():
-    """Run migrations in 'offline' mode.
-
-    This configures the context with just a URL
-    and not an Engine, though an Engine is acceptable
-    here as well.  By skipping the Engine creation
-    we don't even need a DBAPI to be available.
-
-    Calls to context.execute() here emit the given string to the
-    script output.
-
-    """
-    url = config.get_main_option("sqlalchemy.url")
-    context.configure(url=url)
-
-    with context.begin_transaction():
-        context.run_migrations()
-
-def run_migrations_online():
-    """Run migrations in 'online' mode.
-
-    In this scenario we need to create an Engine
-    and associate a connection with the context.
-
-    """
-    engine = engine_from_config(
-                config.get_section(config.config_ini_section),
-                prefix='sqlalchemy.',
-                poolclass=pool.NullPool)
-
-    connection = engine.connect()
-    context.configure(
-                connection=connection,
-                target_metadata=target_metadata
-                )
-
-    try:
-        with context.begin_transaction():
-            context.run_migrations()
-    finally:
-        connection.close()
-
-if context.is_offline_mode():
-    run_migrations_offline()
-else:
-    run_migrations_online()
-
new file mode 120000 (symlink)
index 0000000000000000000000000000000000000000..74b15c9300802dada9b88f05bd1fbea7f50d6e87
--- /dev/null
@@ -0,0 +1 @@
+../env.py
\ No newline at end of file
deleted file mode 100755 (executable)
index 6740d5906d532e5b34fc3f9dae80641289ff5f7f..0000000000000000000000000000000000000000
+++ /dev/null
@@ -1,74 +0,0 @@
-from __future__ import with_statement
-from alembic import context
-from sqlalchemy import engine_from_config, pool
-from logging.config import fileConfig
-
-# this is the Alembic Config object, which provides
-# access to the values within the .ini file in use.
-config = context.config
-
-# Interpret the config file for Python logging.
-# This line sets up loggers basically.
-try:
-    fileConfig(config.config_file_name)
-except:
-    pass
-
-# add your model's MetaData object here
-# for 'autogenerate' support
-# from myapp import mymodel
-# target_metadata = mymodel.Base.metadata
-target_metadata = None
-
-# other values from the config, defined by the needs of env.py,
-# can be acquired:
-# my_important_option = config.get_main_option("my_important_option")
-# ... etc.
-
-def run_migrations_offline():
-    """Run migrations in 'offline' mode.
-
-    This configures the context with just a URL
-    and not an Engine, though an Engine is acceptable
-    here as well.  By skipping the Engine creation
-    we don't even need a DBAPI to be available.
-
-    Calls to context.execute() here emit the given string to the
-    script output.
-
-    """
-    url = config.get_main_option("sqlalchemy.url")
-    context.configure(url=url)
-
-    with context.begin_transaction():
-        context.run_migrations()
-
-def run_migrations_online():
-    """Run migrations in 'online' mode.
-
-    In this scenario we need to create an Engine
-    and associate a connection with the context.
-
-    """
-    engine = engine_from_config(
-                config.get_section(config.config_ini_section),
-                prefix='sqlalchemy.',
-                poolclass=pool.NullPool)
-
-    connection = engine.connect()
-    context.configure(
-                connection=connection,
-                target_metadata=target_metadata
-                )
-
-    try:
-        with context.begin_transaction():
-            context.run_migrations()
-    finally:
-        connection.close()
-
-if context.is_offline_mode():
-    run_migrations_offline()
-else:
-    run_migrations_online()
-
new file mode 120000 (symlink)
index 0000000000000000000000000000000000000000..74b15c9300802dada9b88f05bd1fbea7f50d6e87
--- /dev/null
@@ -0,0 +1 @@
+../env.py
\ No newline at end of file
diff --git a/contrib/ast-db-manage/env.py b/contrib/ast-db-manage/env.py
new file mode 100755 (executable)
index 0000000..a903451
--- /dev/null
@@ -0,0 +1,140 @@
+from __future__ import with_statement
+from alembic import context
+from alembic.script import ScriptDirectory
+from alembic.operations import Operations
+from sqlalchemy import engine_from_config, pool
+from logging.config import fileConfig
+import logging
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+try:
+    fileConfig(config.config_file_name)
+except:
+    pass
+
+logger = logging.getLogger('alembic.runtime.setup')
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = None
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+def run_migrations_offline():
+    """Run migrations in 'offline' mode.
+
+    This configures the context with just a URL
+    and not an Engine, though an Engine is acceptable
+    here as well.  By skipping the Engine creation
+    we don't even need a DBAPI to be available.
+
+    Calls to context.execute() here emit the given string to the
+    script output.
+
+    """
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(url=url)
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+def run_migrations_online():
+    """Run migrations in 'online' mode.
+
+    In this scenario we need to create an Engine
+    and associate a connection with the context.
+
+    """
+    engine = engine_from_config(
+                config.get_section(config.config_ini_section),
+                prefix='sqlalchemy.',
+                poolclass=pool.NullPool)
+
+    logger.info('Testing for an old alembic_version table.')
+
+    connection = engine.connect()
+    context.configure(
+                connection=connection,
+                target_metadata=target_metadata,
+                version_table='alembic_version'
+                )
+
+    script_location = config.get_main_option('script_location')
+    found = False
+    mc = context.get_context()
+    current_db_revision = mc.get_current_revision()
+    script = ScriptDirectory.from_config(config)
+    """ If there was an existing alembic_version table, we need to
+    check that it's current revision is in the history for the tree
+    we're working with.
+    """
+    for x in script.iterate_revisions('head', 'base'):
+        if x.revision == current_db_revision:
+            """ An alembic_versions table was found and it belongs to
+            this alembic tree
+            """
+            logger.info(
+                ('An old alembic_version table at revision %s was '
+                 'found for %s.  Renaming to alembic_version_%s.'),
+                        current_db_revision, script_location,
+                        script_location)
+            op = Operations(mc)
+            try:
+                with context.begin_transaction():
+                    op.rename_table(
+                        'alembic_version', 'alembic_version_%s'
+                        % script_location)
+                found = True
+            except:
+                logger.error(('Unable to rename alembic_version to '
+                             'alembic_version_%s.'),
+                             script_location)
+                connection.close()
+                return
+
+            break
+
+    if not found:
+        logger.info('Didn\'t find an old alembic_version table.')
+    logger.info('Trying alembic_version_%s.' % script_location)
+
+    """ We MAY have an alembic_version table that doesn't belong to
+    this tree but if we still don't have an alembic_version_<tree>
+    table, alembic will create it.
+    """
+    context.configure(
+                connection=connection,
+                target_metadata=target_metadata,
+                version_table='alembic_version_' + script_location
+                )
+    mc = context.get_context()
+    current_db_revision = mc.get_current_revision()
+    if current_db_revision:
+        logger.info(
+            'Using the alembic_version_%s table at revision %s.',
+            script_location, current_db_revision)
+    else:
+        logger.info('Creating new alembic_version_%s table.',
+                    script_location)
+
+    try:
+        with context.begin_transaction():
+            context.run_migrations()
+    finally:
+        connection.close()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()
+
deleted file mode 100755 (executable)
index 6740d5906d532e5b34fc3f9dae80641289ff5f7f..0000000000000000000000000000000000000000
+++ /dev/null
@@ -1,74 +0,0 @@
-from __future__ import with_statement
-from alembic import context
-from sqlalchemy import engine_from_config, pool
-from logging.config import fileConfig
-
-# this is the Alembic Config object, which provides
-# access to the values within the .ini file in use.
-config = context.config
-
-# Interpret the config file for Python logging.
-# This line sets up loggers basically.
-try:
-    fileConfig(config.config_file_name)
-except:
-    pass
-
-# add your model's MetaData object here
-# for 'autogenerate' support
-# from myapp import mymodel
-# target_metadata = mymodel.Base.metadata
-target_metadata = None
-
-# other values from the config, defined by the needs of env.py,
-# can be acquired:
-# my_important_option = config.get_main_option("my_important_option")
-# ... etc.
-
-def run_migrations_offline():
-    """Run migrations in 'offline' mode.
-
-    This configures the context with just a URL
-    and not an Engine, though an Engine is acceptable
-    here as well.  By skipping the Engine creation
-    we don't even need a DBAPI to be available.
-
-    Calls to context.execute() here emit the given string to the
-    script output.
-
-    """
-    url = config.get_main_option("sqlalchemy.url")
-    context.configure(url=url)
-
-    with context.begin_transaction():
-        context.run_migrations()
-
-def run_migrations_online():
-    """Run migrations in 'online' mode.
-
-    In this scenario we need to create an Engine
-    and associate a connection with the context.
-
-    """
-    engine = engine_from_config(
-                config.get_section(config.config_ini_section),
-                prefix='sqlalchemy.',
-                poolclass=pool.NullPool)
-
-    connection = engine.connect()
-    context.configure(
-                connection=connection,
-                target_metadata=target_metadata
-                )
-
-    try:
-        with context.begin_transaction():
-            context.run_migrations()
-    finally:
-        connection.close()
-
-if context.is_offline_mode():
-    run_migrations_offline()
-else:
-    run_migrations_online()
-
new file mode 120000 (symlink)
index 0000000000000000000000000000000000000000..74b15c9300802dada9b88f05bd1fbea7f50d6e87
--- /dev/null
@@ -0,0 +1 @@
+../env.py
\ No newline at end of file