[server] *init_repository* lookup the database instead of the schema to drop tables (closes #810743) stable
authorAlain Leufroy <alain.leufroy@logilab.fr>
Tue, 18 Oct 2011 16:55:16 +0200
branchstable
changeset 8755 1f3757ef3762
parent 8754 9d5cfbde9856
child 8756 203fb56685ca
[server] *init_repository* lookup the database instead of the schema to drop tables (closes #810743) So, tables are dropped even if the schema was changed. :note: if mssql, drop views before tables. :note: because of table dependencies we try a second drop sequence
server/__init__.py
server/sqlutils.py
--- a/server/__init__.py	Thu Oct 18 15:52:05 2012 +0200
+++ b/server/__init__.py	Tue Oct 18 16:55:16 2011 +0200
@@ -30,6 +30,7 @@
 from logilab.common.modutils import LazyObject
 from logilab.common.textutils import splitstrip
 from logilab.common.registry import yes
+from logilab import database
 
 from yams import BASE_GROUPS
 
@@ -162,7 +163,8 @@
     from cubicweb.dbapi import in_memory_repo_cnx
     from cubicweb.server.repository import Repository
     from cubicweb.server.utils import manager_userpasswd
-    from cubicweb.server.sqlutils import sqlexec, sqlschema, sqldropschema
+    from cubicweb.server.sqlutils import sqlexec, sqlschema, sql_drop_all_user_tables
+    from cubicweb.server.sqlutils import _SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION as drop_filter
     # configuration to avoid db schema loading and user'state checking
     # on connection
     config.creating = True
@@ -179,13 +181,21 @@
     sqlcursor = sqlcnx.cursor()
     execute = sqlcursor.execute
     if drop:
-        _title = '-> drop tables '
-        dropsql = sqldropschema(schema, driver)
-        try:
-            sqlexec(dropsql, execute, pbtitle=_title)
-        except Exception as ex:
-            print '-> drop failed, skipped (%s).' % ex
-            sqlcnx.rollback()
+        helper = database.get_db_helper(driver)
+        dropsql = sql_drop_all_user_tables(helper, sqlcursor)
+        # We may fail dropping some tables because of table dependencies, in a first pass.
+        # So, we try a second drop sequence to drop remaining tables if needed.
+        # Note that 2 passes is an arbitrary choice as it seems enougth for our usecases.
+        # (looping may induce infinite recursion when user have no right for example)
+        # Here we try to keep code simple and backend independant. That why we don't try to
+        # distinguish remaining tables (wrong right, dependencies, ...).
+        failed = sqlexec(dropsql, execute, cnx=sqlcnx,
+                         pbtitle='-> dropping tables (first pass)')
+        if failed:
+            failed = sqlexec(failed, execute, cnx=sqlcnx,
+                             pbtitle='-> dropping tables (second pass)')
+            remainings = filter(drop_filter, helper.list_tables(sqlcursor))
+            assert not remainings, 'Remaining tables: %s' % ', '.join(remainings)
     _title = '-> creating tables '
     print _title,
     # schema entities and relations tables
--- a/server/sqlutils.py	Thu Oct 18 15:52:05 2012 +0200
+++ b/server/sqlutils.py	Tue Oct 18 16:55:16 2011 +0200
@@ -20,8 +20,10 @@
 __docformat__ = "restructuredtext en"
 
 import os
+import re
 import subprocess
 from datetime import datetime, date
+from itertools import ifilter
 
 from logilab import database as db, common as lgc
 from logilab.common.shellutils import ProgressBar
@@ -49,27 +51,53 @@
 
 
 def sqlexec(sqlstmts, cursor_or_execute, withpb=not os.environ.get('APYCOT_ROOT'),
-            pbtitle='', delimiter=';'):
+            pbtitle='', delimiter=';', cnx=None):
     """execute sql statements ignoring DROP/ CREATE GROUP or USER statements
-    error. If a cnx is given, commit at each statement
+    error.
+
+    :sqlstmts_as_string: a string or a list of sql statements.
+    :cursor_or_execute: sql cursor or a callback used to execute statements
+    :cnx: if given, commit/rollback at each statement.
+
+    :withpb: if True, display a progresse bar
+    :pbtitle: a string displayed as the progress bar title (if `withpb=True`)
+
+    :delimiter: a string used to split sqlstmts (if it is a string)
+
+    Return the failed statements (same type as sqlstmts)
     """
     if hasattr(cursor_or_execute, 'execute'):
         execute = cursor_or_execute.execute
     else:
         execute = cursor_or_execute
-    sqlstmts = sqlstmts.split(delimiter)
+    sqlstmts_as_string = False
+    if isinstance(sqlstmts, basestring):
+        sqlstmts_as_string = True
+        sqlstmts = sqlstmts.split(delimiter)
     if withpb:
         pb = ProgressBar(len(sqlstmts), title=pbtitle)
+    failed = []
     for sql in sqlstmts:
         sql = sql.strip()
         if withpb:
             pb.update()
         if not sql:
             continue
-        # some dbapi modules doesn't accept unicode for sql string
-        execute(str(sql))
+        try:
+            # some dbapi modules doesn't accept unicode for sql string
+            execute(str(sql))
+        except Exception, err:
+            if cnx:
+                cnx.rollback()
+            failed.append(sql)
+        else:
+            if cnx:
+                cnx.commit()
     if withpb:
         print
+    if sqlstmts_as_string:
+        failed = delimiter.join(failed)
+    return failed
 
 
 def sqlgrants(schema, driver, user,
@@ -137,6 +165,23 @@
     return '\n'.join(output)
 
 
+_SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION = re.compile('^(?!(sql|pg)_)').match
+def sql_drop_all_user_tables(driver_or_helper, sqlcursor):
+    """Return ths sql to drop all tables found in the database system."""
+    if not getattr(driver_or_helper, 'list_tables', None):
+        dbhelper = db.get_db_helper(driver_or_helper)
+    else:
+        dbhelper = driver_or_helper
+
+    cmds = [dbhelper.sql_drop_sequence('entities_id_seq')]
+    # for mssql, we need to drop views before tables
+    if hasattr(dbhelper, 'list_views'):
+        cmds += ['DROP VIEW %s;' % name
+                 for name in ifilter(_SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION, dbhelper.list_views(sqlcursor))]
+    cmds += ['DROP TABLE %s;' % name
+             for name in ifilter(_SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION, dbhelper.list_tables(sqlcursor))]
+    return '\n'.join(cmds)
+
 class SQLAdapterMixIn(object):
     """Mixin for SQL data sources, getting a connection from a configuration
     dictionary and handling connection locking