From 12cfd5be6d9ce66b4792f8d52d1faa4e32c350ff Mon Sep 17 00:00:00 2001
From: Sean Dague <sean@dague.net>
Date: Tue, 27 Sep 2016 15:01:21 -0400
Subject: [PATCH] Unwind circular import issue with api / utils

The DeleteFromSelect function was added to sqla.utils, which is mostly
the home of 2 shadow table functions that need to import api.py to get
constants related to shadow tables. We now exclusively use that
function in api.py for archiving code. That causes some oddities
around module import.

This moves DeleteFromSelect so we can get rid of these odd late
imports.

Change-Id: I59c3444c2258f59a09a9c885bd9490055e278998
---
 nova/db/sqlalchemy/api.py   | 33 ++++++++++++++++++++++-----------
 nova/db/sqlalchemy/utils.py | 20 --------------------
 2 files changed, 22 insertions(+), 31 deletions(-)

diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index a6743a13b..2fbd9ae5f 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -38,6 +38,7 @@ from six.moves import range
 import sqlalchemy as sa
 from sqlalchemy import and_
 from sqlalchemy.exc import NoSuchTableError
+from sqlalchemy.ext.compiler import compiles
 from sqlalchemy import MetaData
 from sqlalchemy import or_
 from sqlalchemy.orm import aliased
@@ -50,6 +51,7 @@ from sqlalchemy.schema import Table
 from sqlalchemy import sql
 from sqlalchemy.sql.expression import asc
 from sqlalchemy.sql.expression import desc
+from sqlalchemy.sql.expression import UpdateBase
 from sqlalchemy.sql import false
 from sqlalchemy.sql import func
 from sqlalchemy.sql import null
@@ -416,6 +418,23 @@ class InequalityCondition(object):
         return [field != value for value in self.values]
 
 
+class DeleteFromSelect(UpdateBase):
+    def __init__(self, table, select, column):
+        self.table = table
+        self.select = select
+        self.column = column
+
+
+# NOTE(guochbo): some versions of MySQL doesn't yet support subquery with
+# 'LIMIT & IN/ALL/ANY/SOME' We need work around this with nesting select .
+@compiles(DeleteFromSelect)
+def visit_delete_from_select(element, compiler, **kw):
+    return "DELETE FROM %s WHERE %s in (SELECT T1.%s FROM (%s) as T1)" % (
+        compiler.process(element.table, asfrom=True),
+        compiler.process(element.column),
+        element.column.name,
+        compiler.process(element.select))
+
 ###################
 
 
@@ -6284,10 +6303,6 @@ def _archive_if_instance_deleted(table, shadow_table, instances, conn,
     Logic is: if I have a column called instance_uuid, and that instance
     is deleted, then I can be deleted.
     """
-    # NOTE(guochbo): There is a circular import, nova.db.sqlalchemy.utils
-    # imports nova.db.sqlalchemy.api.
-    from nova.db.sqlalchemy import utils as db_utils
-
     query_insert = shadow_table.insert(inline=True).\
         from_select(
             [c.name for c in table.c],
@@ -6302,8 +6317,8 @@ def _archive_if_instance_deleted(table, shadow_table, instances, conn,
         and_(instances.c.deleted != instances.c.deleted.default.arg,
              instances.c.uuid == table.c.instance_uuid)).\
         order_by(table.c.id).limit(max_rows)
-    delete_statement = db_utils.DeleteFromSelect(table, query_delete,
-                                                 table.c.id)
+    delete_statement = DeleteFromSelect(table, query_delete,
+                                        table.c.id)
 
     try:
         with conn.begin():
@@ -6323,10 +6338,6 @@ def _archive_deleted_rows_for_table(tablename, max_rows):
 
     :returns: number of rows archived
     """
-    # NOTE(guochbo): There is a circular import, nova.db.sqlalchemy.utils
-    # imports nova.db.sqlalchemy.api.
-    from nova.db.sqlalchemy import utils as db_utils
-
     engine = get_engine()
     conn = engine.connect()
     metadata = MetaData()
@@ -6396,7 +6407,7 @@ def _archive_deleted_rows_for_table(tablename, max_rows):
                           deleted_column != deleted_column.default.arg).\
                           order_by(column).limit(max_rows)
 
-    delete_statement = db_utils.DeleteFromSelect(table, query_delete, column)
+    delete_statement = DeleteFromSelect(table, query_delete, column)
     try:
         # Group the insert and delete in a transaction.
         with conn.begin():
diff --git a/nova/db/sqlalchemy/utils.py b/nova/db/sqlalchemy/utils.py
index 7d0770982..f408cb9bd 100644
--- a/nova/db/sqlalchemy/utils.py
+++ b/nova/db/sqlalchemy/utils.py
@@ -17,9 +17,7 @@ from oslo_db import exception as db_exc
 from oslo_db.sqlalchemy import utils as oslodbutils
 from oslo_log import log as logging
 from sqlalchemy.exc import OperationalError
-from sqlalchemy.ext.compiler import compiles
 from sqlalchemy import MetaData
-from sqlalchemy.sql.expression import UpdateBase
 from sqlalchemy import Table
 from sqlalchemy.types import NullType
 
@@ -31,24 +29,6 @@ from nova.i18n import _, _LE
 LOG = logging.getLogger(__name__)
 
 
-class DeleteFromSelect(UpdateBase):
-    def __init__(self, table, select, column):
-        self.table = table
-        self.select = select
-        self.column = column
-
-
-# NOTE(guochbo): some versions of MySQL doesn't yet support subquery with
-# 'LIMIT & IN/ALL/ANY/SOME' We need work around this with nesting select .
-@compiles(DeleteFromSelect)
-def visit_delete_from_select(element, compiler, **kw):
-    return "DELETE FROM %s WHERE %s in (SELECT T1.%s FROM (%s) as T1)" % (
-        compiler.process(element.table, asfrom=True),
-        compiler.process(element.column),
-        element.column.name,
-        compiler.process(element.select))
-
-
 def check_shadow_table(migrate_engine, table_name):
     """This method checks that table with ``table_name`` and
     corresponding shadow table have same columns.