<commit_before>
# Code to generate a Python model from a database or differences between a model and database.
# Some of this is borrowed heavily from the AutoCode project at:  http://code.google.com/p/sqlautocode/

import sys
import migrate, sqlalchemy


HEADER = """
## File autogenerated by genmodel.py

from sqlalchemy import *
meta = MetaData()
"""

class ModelGenerator(object):

    def __init__(self, diff):
        self.diff = diff
        dialectModule = sys.modules[self.diff.conn.dialect.__module__]  # is there an easier way to get this?
        self.colTypeMappings = dict( (v,k) for k,v in dialectModule.colspecs.items() )
    
    def column_repr(self, col):
        kwarg = []
        if col.key != col.name: kwarg.append('key')
        if col.primary_key:
            col.primary_key = True  # otherwise it dumps it as 1
            kwarg.append('primary_key')
        if not col.nullable: kwarg.append('nullable')
        if col.onupdate: kwarg.append('onupdate')
        if col.default:
            if col.primary_key:
                # I found that Postgres automatically creates a default value for the sequence, but let's not show that.
                pass
            else:
                kwarg.append('default')
        ks = ', '.join('%s=%r' % (k, getattr(col, k)) for k in kwarg )
            
        name = col.name.encode('utf8')  # crs: not sure if this is good idea, but it gets rid of extra u''
        type = self.colTypeMappings.get(col.type.__class__, None)
        if type:
            # Make the column type be an instance of this type.
            type = type()
        else:
            # We must already be a model type, no need to map from the database-specific types.
            type = col.type

        data = {'name' : name, 
                'type' : type,
                'constraints' : ', '.join([repr(cn) for cn in col.constraints]),
                'args' : ks and ks or ''
                }

        if data['constraints']:
            if data['args']: data['args'] = ',' + data['args']
        
        if data['constraints'] or data['args']:
            data['maybeComma'] = ','
        else:
            data['maybeComma'] = ''

        return """Column(%(name)r, %(type)r %(maybeComma)s %(constraints)s %(args)s)""" % data
    
    def getTableDefn(self, table):
        out = []
        tableName = table.name
        out.append("%(table)s = Table('%(table)s', meta," % {'table': tableName})
        for col in table.columns:
            out.append("  %s," % self.column_repr(col))
        out.append(")")
        return out
        
    def toPython(self):
        ''' Assume database is current and model is empty. '''
        out = []
        out.append(HEADER)
        out.append("")
        for table in self.diff.tablesMissingInModel:
            out.extend(self.getTableDefn(table))
            out.append("")
        return '\n'.join(out)
        
    def toUpgradePython(self, indent='    '):
        ''' Assume model is most current and database is out-of-date. '''
        
        decls = ['meta = MetaData(migrate_engine)']
        for table in self.diff.tablesMissingInModel + self.diff.tablesMissingInDatabase:
            decls.extend(self.getTableDefn(table))
            
        cmds = []
        for table in self.diff.tablesMissingInModel:
            tableName = table.name
            cmds.append("%(table)s.drop()" % {'table': tableName})
        for table in self.diff.tablesMissingInDatabase:
            tableName = table.name
            cmds.append("%(table)s.create()" % {'table': tableName})
            
        return '\n'.join(decls), '\n'.join(['%s%s' % (indent, line) for line in cmds])
    
    def toDowngradePython(self, indent='    '):
        return '    pass  #TODO DOWNGRADE'
    
    def applyModel(self):
        ''' Apply model to current database. '''
        
        # Yuck! We have to import from changeset to apply the monkey-patch to allow column adding/dropping.
        from migrate.changeset import schema
        
        def dbCanHandleThisChange(missingInDatabase, missingInModel, diffDecl):
            if missingInDatabase and not missingInModel and not diffDecl:
                # Even sqlite can handle this.
                return True
            else:
                return not self.diff.conn.url.drivername.startswith('sqlite')
            
        meta = sqlalchemy.MetaData(self.diff.conn.engine)
        
        for table in self.diff.tablesMissingInModel:
            table = table.tometadata(meta)
            table.drop()
        for table in self.diff.tablesMissingInDatabase:
            table = table.tometadata(meta)
            table.create()
        for modelTable in self.diff.tablesWithDiff:
            modelTable = modelTable.tometadata(meta)
            dbTable = self.diff.reflected_model.tables[modelTable.name]
            #print 'TODO DEBUG.cols1', [x.name for x in dbTable.columns]
            #dbTable = dbTable.tometadata(meta)
            #print 'TODO DEBUG.cols2', [x.name for x in dbTable.columns]
            tableName = modelTable.name
            missingInDatabase, missingInModel, diffDecl = self.diff.colDiffs[tableName]
            if dbCanHandleThisChange(missingInDatabase, missingInModel, diffDecl):
                for col in missingInDatabase:
                    modelTable.columns[col.name].create()
                for col in missingInModel:
                    dbTable.columns[col.name].drop()
                for modelCol, databaseCol, modelDecl, databaseDecl in diffDecl:
                    dbTable.columns[databaseCol.name].drop()
                    modelTable.columns[modelCol.name].create()
            else:
                # Sqlite doesn't support drop column, so you have to do more:
                #   create temp table, copy data to it, drop old table, create new table, copy data back.
                
                tempName = '_temp_%s' % modelTable.name  # I wonder if this is guaranteed to be unique?
                def getCopyStatement():
                    preparer = self.diff.conn.engine.dialect.preparer
                    commonCols = []
                    for modelCol in modelTable.columns:
                        if dbTable.columns.has_key(modelCol.name):
                            commonCols.append(modelCol.name)
                    commonColsStr = ', '.join(commonCols)
                    return 'INSERT INTO %s (%s) SELECT %s FROM %s' % (tableName, commonColsStr, commonColsStr, tempName)
                
                self.diff.conn.execute('CREATE TEMPORARY TABLE %s as SELECT * from %s' % (tempName, modelTable.name))
                modelTable.drop()
                modelTable.create()
                self.diff.conn.execute(getCopyStatement())
                self.diff.conn.execute('DROP TABLE %s' % tempName)
                
<commit_msg>Execute sqlite-specific code to alter a table inside one transaction so that we can't leave the database in a nasty state<commit_after>
# Code to generate a Python model from a database or differences between a model and database.
# Some of this is borrowed heavily from the AutoCode project at:  http://code.google.com/p/sqlautocode/

import sys
import migrate, sqlalchemy


HEADER = """
## File autogenerated by genmodel.py

from sqlalchemy import *
meta = MetaData()
"""

class ModelGenerator(object):

    def __init__(self, diff):
        self.diff = diff
        dialectModule = sys.modules[self.diff.conn.dialect.__module__]  # is there an easier way to get this?
        self.colTypeMappings = dict( (v,k) for k,v in dialectModule.colspecs.items() )
    
    def column_repr(self, col):
        kwarg = []
        if col.key != col.name: kwarg.append('key')
        if col.primary_key:
            col.primary_key = True  # otherwise it dumps it as 1
            kwarg.append('primary_key')
        if not col.nullable: kwarg.append('nullable')
        if col.onupdate: kwarg.append('onupdate')
        if col.default:
            if col.primary_key:
                # I found that Postgres automatically creates a default value for the sequence, but let's not show that.
                pass
            else:
                kwarg.append('default')
        ks = ', '.join('%s=%r' % (k, getattr(col, k)) for k in kwarg )
            
        name = col.name.encode('utf8')  # crs: not sure if this is good idea, but it gets rid of extra u''
        type = self.colTypeMappings.get(col.type.__class__, None)
        if type:
            # Make the column type be an instance of this type.
            type = type()
        else:
            # We must already be a model type, no need to map from the database-specific types.
            type = col.type

        data = {'name' : name, 
                'type' : type,
                'constraints' : ', '.join([repr(cn) for cn in col.constraints]),
                'args' : ks and ks or ''
                }

        if data['constraints']:
            if data['args']: data['args'] = ',' + data['args']
        
        if data['constraints'] or data['args']:
            data['maybeComma'] = ','
        else:
            data['maybeComma'] = ''

        return """Column(%(name)r, %(type)r %(maybeComma)s %(constraints)s %(args)s)""" % data
    
    def getTableDefn(self, table):
        out = []
        tableName = table.name
        out.append("%(table)s = Table('%(table)s', meta," % {'table': tableName})
        for col in table.columns:
            out.append("  %s," % self.column_repr(col))
        out.append(")")
        return out
        
    def toPython(self):
        ''' Assume database is current and model is empty. '''
        out = []
        out.append(HEADER)
        out.append("")
        for table in self.diff.tablesMissingInModel:
            out.extend(self.getTableDefn(table))
            out.append("")
        return '\n'.join(out)
        
    def toUpgradePython(self, indent='    '):
        ''' Assume model is most current and database is out-of-date. '''
        
        decls = ['meta = MetaData(migrate_engine)']
        for table in self.diff.tablesMissingInModel + self.diff.tablesMissingInDatabase:
            decls.extend(self.getTableDefn(table))
            
        cmds = []
        for table in self.diff.tablesMissingInModel:
            tableName = table.name
            cmds.append("%(table)s.drop()" % {'table': tableName})
        for table in self.diff.tablesMissingInDatabase:
            tableName = table.name
            cmds.append("%(table)s.create()" % {'table': tableName})
            
        return '\n'.join(decls), '\n'.join(['%s%s' % (indent, line) for line in cmds])
    
    def toDowngradePython(self, indent='    '):
        return '    pass  #TODO DOWNGRADE'
    
    def applyModel(self):
        ''' Apply model to current database. '''
        
        # Yuck! We have to import from changeset to apply the monkey-patch to allow column adding/dropping.
        from migrate.changeset import schema
        
        def dbCanHandleThisChange(missingInDatabase, missingInModel, diffDecl):
            if missingInDatabase and not missingInModel and not diffDecl:
                # Even sqlite can handle this.
                return True
            else:
                return not self.diff.conn.url.drivername.startswith('sqlite')
            
        meta = sqlalchemy.MetaData(self.diff.conn.engine)
        
        for table in self.diff.tablesMissingInModel:
            table = table.tometadata(meta)
            table.drop()
        for table in self.diff.tablesMissingInDatabase:
            table = table.tometadata(meta)
            table.create()
        for modelTable in self.diff.tablesWithDiff:
            modelTable = modelTable.tometadata(meta)
            dbTable = self.diff.reflected_model.tables[modelTable.name]
            #print 'TODO DEBUG.cols1', [x.name for x in dbTable.columns]
            #dbTable = dbTable.tometadata(meta)
            #print 'TODO DEBUG.cols2', [x.name for x in dbTable.columns]
            tableName = modelTable.name
            missingInDatabase, missingInModel, diffDecl = self.diff.colDiffs[tableName]
            if dbCanHandleThisChange(missingInDatabase, missingInModel, diffDecl):
                for col in missingInDatabase:
                    modelTable.columns[col.name].create()
                for col in missingInModel:
                    dbTable.columns[col.name].drop()
                for modelCol, databaseCol, modelDecl, databaseDecl in diffDecl:
                    dbTable.columns[databaseCol.name].drop()
                    modelTable.columns[modelCol.name].create()
            else:
                # Sqlite doesn't support drop column, so you have to do more:
                #   create temp table, copy data to it, drop old table, create new table, copy data back.
                
                tempName = '_temp_%s' % modelTable.name  # I wonder if this is guaranteed to be unique?
                def getCopyStatement():
                    preparer = self.diff.conn.engine.dialect.preparer
                    commonCols = []
                    for modelCol in modelTable.columns:
                        if dbTable.columns.has_key(modelCol.name):
                            commonCols.append(modelCol.name)
                    commonColsStr = ', '.join(commonCols)
                    return 'INSERT INTO %s (%s) SELECT %s FROM %s' % (tableName, commonColsStr, commonColsStr, tempName)
                
                # Move the data in one transaction, so that we don't leave the database in a nasty state.
                connection = self.diff.conn.connect()
                trans = connection.begin()
                try:
                    connection.execute('CREATE TEMPORARY TABLE %s as SELECT * from %s' % (tempName, modelTable.name))
                    modelTable.drop(bind=connection)  # make sure the drop takes place inside our transaction with the bind parameter
                    modelTable.create(bind=connection)
                    connection.execute(getCopyStatement())
                    connection.execute('DROP TABLE %s' % tempName)
                    trans.commit()
                except:
                    trans.rollback()
                    raise
                

{'fraction_non_alphanumeric': 0.10321181162969702, 'fraction_numerical': 0.0005597928766356449, 'mean_word_length': 1.8589717943588717, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5425013', 'n_tokens_mistral': 3977, 'n_tokens_neox': 3839, 'n_words': 1093}