Welcome, guest | Sign In | My Account | Store | Cart
# SQLSMO.py
# Created by Jorge Besada
# Last modified 3/16/2015

import os
import glob
import time
import datetime
import subprocess

# --------------Functions-----------------------------------------------------------------------------
def SqlExecute(conn, sqlquery=''):

    """
    Executes sqlquery and returns lists with column names and data
    The connection info is passed as a dictionary with these required keys:
    servername, username,password
    If username is empty will use integrated security
    These keys are optional: defdb, colseparator
    """

    if 'colseparator' not in conn.keys():
        conn['colseparator'] = chr(1)
    if conn['username'] == '':
        constr = "sqlcmd -E -S" + conn['servername'] + "  /w 8192 -W " + ' -s' + conn['colseparator'] + '  '
    else:
        constr = "sqlcmd -U" + conn['username'] + " -P" + conn['password'] + ' -S' + conn['servername'] + '  /w 8192 -W  -s' + conn['colseparator'] + '  '

    # now we execute
    try:
        data = subprocess.Popen(constr + '-Q"' + sqlquery + '"', stdout=subprocess.PIPE).communicate()
    except Exception as inst:
        print('Exception in SqlExecute:', inst)
        return -1

    records = []
    lst = data[0].splitlines()
    # lst[0] column names;  lst[1] dashed lines, (skip); lst[2:] data
    # now we decode
    for x in lst:
        try:
            #try default utf-8 decoding
            line = x.decode()
        except UnicodeDecodeError:
            #in case of weird characters this one works most of the time
            line = x.decode('ISO-8859-1')
        lst2 = line.split(conn['colseparator'])
        records.append(lst2)
    fieldnames = records[0]
    data = records[2:]

    return data, fieldnames


def GetLatestBackup(dirpath, filter='\*.*'):
    """
    Returns folder contents sorted by modified date
    Sample use:
    backupfolder = r'\\SERVERNAME\SQLBackups1\SQLBackupUser'
    This brings all files
    lst = GetLatestBackup(backupfolder)
    Here we bring a subset using filter string
    filter = '\DATABASE_*.bak'
    lst = GetLatestBackup(backupfolder, filter)
    """

    a = [s for s in glob.glob(dirpath + filter) if os.path.isfile(os.path.join(dirpath, s))]
    a.sort(key=lambda s: os.path.getmtime(os.path.join(dirpath, s)))
    return a

def DeleteOlderFiles(workfolder, days):
    """
    Used to delete older backups in a folder, days is retention days
    Sample use to delete all files in C:temp with created date older than 3 days:
    DeleteOlderFiles(r'c:\temp', 3)
    """

    # os, time already imported

    now = time.time()
    cutoff = now - (days * 86400)
    filelist = os.listdir(workfolder)
    for x in filelist:
            if os.path.isfile( workfolder + '\\' + x):
                t = os.stat( workfolder + '\\' + x )
                c = t.st_ctime

                # delete file if older than a week
                if c < cutoff:
                    print('deleting ' + x)
                    os.remove(workfolder + '\\' + x )


def KillConnections(conn, db):
    """
    Kills connections in database if database exists
    """

    s = 'SET NOCOUNT ON DECLARE @kill varchar(8000) = ' + chr(39) + chr(39) + ';'
    s += ' SELECT @kill = @kill + ' + chr(39) + 'kill ' + chr(39) + '  + CONVERT(varchar(5), spid) + ' + chr(39) + ';' + chr(39)
    s += ' FROM master..sysprocesses WHERE dbid = db_id(' + chr(39) + db + chr(39) + ')'
    s += ' select @kill; EXEC(@kill);'

    rows, fnames = [], []
    try:
        rows, fnames = SqlExecute(conn, s)
    except Exception as inst:
            print('Error killing connections: ', inst)
    return rows

def SyncLogins(conn, db):
    s = '''
    DECLARE @UserName nvarchar(255)
    DECLARE @SQLCmd nvarchar(511)
    DECLARE orphanuser_cur cursor for
    SELECT UserName = name
    FROM sysusers
    WHERE issqluser = 1 and (sid is not null and sid <> 0x0) and suser_sname(sid) is null ORDER BY name
    OPEN orphanuser_cur
    FETCH NEXT FROM orphanuser_cur INTO @UserName
    WHILE (@@fetch_status = 0)
    BEGIN
    select @UserName + ' user name being resynced'
    set @SQLCmd = 'ALTER USER '+@UserName+' WITH LOGIN = '+@UserName
    EXEC (@SQLCmd)
    FETCH NEXT FROM orphanuser_cur INTO @UserName
    END
    CLOSE orphanuser_cur
    DEALLOCATE orphanuser_cur
    '''
    # sqlcmd needs single line sql commands for the -Q option
    s = s.replace("\n", ' ')
    sqlsynclogins = 'SET NOCOUNT ON USE [' + db + '] ' + s
    rows, fnames = [], []
    try:
        rows, fnames = SqlExecute(conn, sqlsynclogins)
    except Exception as inst:
        print('Error executing fix logins: ', inst)
    return rows

def DatedString():
    """
    Returns dated string with this format
    2014_12_30_135857_4581860
    """

    from datetime import datetime
    now = str(datetime.now())
    now = now.replace('-', '_')
    now = now.replace(' ', '_')
    now = now.replace(':', '')
    now = now.replace('.', '_') + '0'

    return now


def BuildTlogSQL(dbname, sourcedb, bkfolder, recovery = 'NORECOVERY', BACKUP_MASK = '_backup_20*.BAK', LOG_MASK = '_backup_20*.TRN'):
    """
    - check for log backups
    DECLARE backupFiles CURSOR FOR
    SELECT backupFile
    FROM @fileList
    WHERE backupFile LIKE '%.TRN'
    AND backupFile LIKE @dbName + '%'
    AND backupFile > @lastFullBackup
    OPEN backupFiles
    -- Loop through all the files for the database
    FETCH NEXT FROM backupFiles INTO @backupFile
    WHILE @@FETCH_STATUS = 0
    BEGIN
       SET @cmd = 'RESTORE LOG ' + @dbName + ' FROM DISK = '''
           + @backupPath + @backupFile + ''' WITH NORECOVERY'
       PRINT @cmd
       FETCH NEXT FROM backupFiles INTO @backupFile
    END
    CLOSE backupFiles
    DEALLOCATE backupFiles
    -- 6 - put database in a useable state
    SET @cmd = 'RESTORE DATABASE ' + @dbName + ' WITH RECOVERY'
    PRINT @cmd
    """

    BKFILE = '____.BAK'
    try:
        BKFILE = GetLatestBackup(bkfolder, '\\' + sourcedb + BACKUP_MASK)[-1]   # BACKUP_MASK = '_backup_20*.BAK'
    except IndexError:
        pass

    s = ''

    filter = '\\' + sourcedb + LOG_MASK
    filelist = GetLatestBackup(bkfolder, filter )

    if len(filelist) == 0:
        print('No log files for ' + sourcedb)
        if recovery == 'RECOVERY':
            s = 'RESTORE DATABASE ' + dbname + ' WITH RECOVERY' + chr(13) + chr(10) + 'GO ' + chr(13) + chr(10)
            return s
    else:
        for x in filelist:
            if x[:-4] > BKFILE[:-4]:        # this means the log file is older than the latest full backup file
                s += 'RESTORE LOG ' + dbname + ' FROM DISK = ' + chr(39) + x + chr(39) + \
                    ' WITH NORECOVERY' + chr(13) + chr(10) + 'GO ' + chr(13) + chr(10)
        if x[:-4] > BKFILE[:-4]:
            if recovery == 'RECOVERY':
                s += 'RESTORE DATABASE ' + dbname + ' WITH RECOVERY' + chr(13) + chr(10) + 'GO ' + chr(13) + chr(10)
        return s

# ----------------End of functions----------------------------------------


# -----------------Start of class SQLSMO----------------------------------

class SQLSMO:
    def __init__(self, conn, datafilepath='', logfilepath='', backupfilepath='' ):
        self.conn = conn
        self.datafolder = datafilepath
        self.logfolder = logfilepath
        self.logfilename = ''
        self.backupfile = backupfilepath
        self.currdb = conn['db']
        self.defdb = 'master'
        self.noexecute = 0                  #1=no execute, 0=yes to execute
        self.sqlrestore = ''
        self.sqlbackup = ''

        self.sqlfilelist =          'SET NOCOUNT ON RESTORE FILELISTONLY FROM DISK = ' + chr(39) + self.backupfile + chr(39)
        self.sqlfileheader =        'SET NOCOUNT ON RESTORE HEADERONLY FROM DISK = ' + chr(39) + self.backupfile + chr(39)
        self.sqlxp_fixeddrives =    'SET NOCOUNT ON EXEC master..xp_fixeddrives'
        self.sqlsphelpdb =          'SET NOCOUNT ON select name, physical_name, (size * 8/1000) as size, data_space_id from ['+ self.currdb + '].sys.database_files'

        self.datafiles_size_existing = 0    #info from existing database --- populated by GetDatabaseInfo
        self.logfiles_size_existing = 0     #info from existing database --- populated by GetDatabaseInfo

        self.dictdbinfo = {}                #info from existing database --- populated by GetDatabaseInfo
        self.dictfiles = {}                 #info from backup file       --- populated by GetBackupInfo
        self.dictheader = {}                #info from backup file       --- populated by GetBackupInfo
        self.dictfreespace = {}             #info on disk space          --- populated by GetFreeSpace

        self.datafiles_size = 0             #info from backup file       --- populated by GetBackupInfo
        self.logfiles_size = 0              #info from backup file       --- populated by GetBackupInfo

        self.backup_options = {'backup_type': 'DATABASE', 'compression': False} #options for backup_type: LOG, DIFFERENTIAL
        self.restore_options = {'dated_file_names': False,
                                'original_file_names': False,
                                'restore_type': 'DATABASE',                     #options: LOG
                                'recovery': 'RECOVERY',                         #options: NORECOVERY
                                'replace': True                                 #option: False
                               }

    def GetDataseInfo(self):
        """
        Returns info about existing database
        Return values are rows and column names
        It populates these variables
        self.logfiles_size_existing
        self.datafiles_size_existing
        """
        self.logfiles_size_existing  = 0.0
        self.datafiles_size_existing = 0.0

        rows, fnames = [], []
        try:
            # fnames are: name physical_name size data_space_id
            rows, fnames = SqlExecute(self.conn, self.sqlsphelpdb)
        except Exception as inst:
            print('No existing database information', inst)
            self.dictdbinfo = {}
            return rows, fnames


        for x in rows:
            self.dictdbinfo[x[0]] = x[1:]       #using as key file name

        logsize = 0.0
        datasize = 0.0
        for x in rows:
            if x[-1] == '0':                    #x[-1] is data_space_id, 0 is log, anything else is data
                gb = float(x[-2])/1000
                logsize += gb
            else:
                gb = float(x[-2])/1000
                datasize += gb

        self.logfiles_size_existing = logsize
        self.datafiles_size_existing = datasize

        return rows, fnames

    def GetBackupInfo(self):
        """
        Returns two dictionaries
        ---------------dictionary with backup file contents------------------------------------------------------------
        'LogicalName', 'PhysicalName', 'Type', 'FileGroupName', 'Size', 'MaxSize',
        'FileId', 'CreateLSN', 'DropLSN', 'UniqueId', 'ReadOnlyLSN', 'ReadWriteLSN',
        'BackupSizeInBytes', 'SourceBlockSize', 'FileGroupId', 'LogGroupGUID', 'DifferentialBaseLSN',
        'DifferentialBaseGUID', 'IsReadOnly', 'IsPresent', 'TDEThumbprint'
        The dictionary key is the LogicalName
        ---------------dictionary with contents of backup header--------------------------------------------------------
        BackupTypeDescription Database
        FirstRecoveryForkID {32840502-607C-441E-9439-D7364C4BBFFF}
        CheckpointLSN 93565000000174200025
        FamilyGUID {52405D85-E8F5-4AD8-92C9-5EC262B2A651}
        SoftwareVendorId 4608
        DatabaseBackupLSN 93487000002002400022
        BackupFinishDate 2014-08-27 02:59:05+00:00
        CodePage 0
        DatabaseVersion 661
        ExpirationDate None
        SortOrder 52
        CompatibilityLevel 100
        Collation SQL_Latin1_General_CP1_CI_AS
        BindingID {09CD467E-193E-4A29-82C9-709AE5CCA9D4}
        HasIncompleteMetaData False
        RecoveryForkID {32840502-607C-441E-9439-D7364C4BBFFF}
        SoftwareVersionMinor 50
        Position 1
        SoftwareVersionBuild 2500
        SoftwareVersionMajor 10
        Containment 0
        MachineName SQLSERVERNAME
        BackupType 1
        HasBackupChecksums False
        DifferentialBaseGUID None
        UserName DOMAIN\_sql_account
        IsSingleUser False
        ForkPointLSN None
        DifferentialBaseLSN None
        IsCopyOnly False
        RecoveryModel FULL
        BeginsLogChain False
        IsSnapshot False
        IsReadOnly False
        BackupName DatabaseName_backup_2014_08_27_020001_3668583
        FirstLSN 93565000000174200025
        BackupSetGUID {CFF54981-E420-477C-ADBB-B32FDC31EAF0}
        DeviceType 2
        IsDamaged False
        BackupSize 233650456576
        Compressed 1
        UnicodeComparisonStyle 196609
        HasBulkLoggedData False
        Flags 512
        CompressedBackupSize 56790589512
        BackupDescription None
        ServerName SQLSERVERNAME\INSTANCE
        DatabaseName DatabaseName
        UnicodeLocaleId 1033
        BackupStartDate 2014-08-27 02:18:38+00:00
        IsForceOffline False
        LastLSN 93565000000878200001
        DatabaseCreationDate 2012-06-06 15:47:24+00:00
        """

        # backup files info
        rows, fnames = [], []
        try:
            rows, fnames = SqlExecute(self.conn, self.sqlfilelist)
        except Exception as inst:
            print(inst)
            self.dictfiles = {}

        self.logfiles_size = 0.0
        self.datafiles_size = 0.0
        for x in rows:
            self.dictfiles[x[0]] = x[1:]                                    #using as key file name
            try:
                if x[2] == 'L':                                             #x[2] is Type, D or L
                    self.logfiles_size += float(float(x[4])/1000000000)     #x[4] is Size in bytes
                    self.logfilename = x[0]
            except IndexError:
                print('-Check backup file exists')
                print('-Check backup file version, maybe higher than current SQL version')
                print('-Check SQL server account has access to the backup file')
                exit()
            if x[2] == 'D':
                self.datafiles_size += float(float(x[4])/1000000000)

        # backup header info
        try:
            rows, fnames = SqlExecute(self.conn, self.sqlfileheader)
        except Exception as inst:
            print(inst)
            self.dictheader = {}

        i = 0
        for x in fnames:
            self.dictheader[x] = rows[0][i]
            if x == 'DatabaseName':
                self.defdb = self.dictheader[x]
            i += 1

        return self.dictfiles, self.dictheader

    def GetFreeSpace(self):
        """
        Returns dictionary with drive letters and free space in GB
        C	11.553        D	26.849        E	31.829        F	64.230
        """

        self.dictfreespace = {}
        rows, fnames = [], []
        try:
            rows, fnames = SqlExecute(self.conn, self.sqlxp_fixeddrives)
        except Exception as inst:
            print('Error getting disk space', inst)
            return self.dictfreespace

        i = 0

        for x in rows:
            self.dictfreespace[x[0]] = str(float(rows[i][1])/1000)   # changing to gb
            i += 1
        return self.dictfreespace

    def Ok_to_restore(self):
        """
        Check for space available in destination
        If database exists (it is an overwrite) the space used by it is considered
        """
        # Populate these two: self.datafiles_size_existing, self.logfiles_size_existing
        self.GetDataseInfo()

        # Populate these two: self.datafiles_size, self.logfiles_size
        self.GetBackupInfo()

        dict_freespace = self.GetFreeSpace()
        # print('freespace dict')
        # print(dict_freespace)
        data_disk = 0.0
        log_disk = 0.0
        for x in dict_freespace.keys():                             #x is the drive letter
            if self.datafolder[0].upper() == x.upper():                             #if letter of datafolder matches drive letter, add to data_disk variable
                data_disk += float(dict_freespace[x])
            if self.logfolder[0].upper() == x.upper():                              #if letter of logfolder matches drive letter, add to log_disk variable
                log_disk += float(dict_freespace[x])



        # test prints
        # print('Space in drives in ' + self.conn['servername'])
        # print(dict_freespace)
        # print('Total data_disk available', data_disk)
        # print('Total log_disk available', log_disk)
        # print('self.datafiles_size from backup file', self.datafiles_size)
        # print('self.logfiles_size from backup file', self.logfiles_size)
        # print('self.datafiles_size_existing', self.datafiles_size_existing)
        # print('self.logfiles_size_existing', self.logfiles_size_existing)


        # this is for the case the database does not exist:
        # compare disk space with backup file disk space
        # we decide using self.datafiles_size (0 means database does not exist)
        # but we need to check if it is a single drive for both log and data
        if self.datafolder[0] != self.logfolder[0]:     # case log and data in different drives
            # print('log and data different drives')
            if self.datafiles_size_existing == 0:       # case no existing database
                # print('database does not exist, log and data on different drives')
                if (float(data_disk) > float(self.datafiles_size)) and (float(log_disk) > float(self.logfiles_size)):  # disk space > backup space requiered
                    return True
                else:
                    return False
            else:                                       # case database exists
                # print('database exists, log and data on different drives')
                if (float(data_disk) > (float(self.datafiles_size) - float(self.datafiles_size_existing))) \
                    and (float(log_disk) > (float(self.logfiles_size) - float(self.logfiles_size_existing))):
                    return True
                else:
                    return False
        else:                                         # case log and data are in same drive
            # print('log and data same drive')
            if self.datafiles_size_existing == 0:                            # no existing database
                # print('database does not exist, log and data on same drive')
                if float(data_disk) > (float(self.datafiles_size) + float(self.logfiles_size)):
                    return True
                else:
                    return False
            else:
                # print('database exists, log and data on same drive')
                if float(data_disk > (float(self.datafiles_size) + float(self.logfiles_size)) - (float(self.datafiles_size_existing) + float(self.logfiles_size_existing))):
                    return True
                else:
                    return False

    def RestoreDatabase(self):
        """
        Restores the database from given backup file to given data and log folders
        There 2 options:
        1) rename files with a timestamp added to the name
        (This is done to avoid file name collisions)
        2) use names based on database name with _Data and _Log suffixes (this is the default)
        """

        self.GetBackupInfo()

        t = ''
        if self.restore_options['dated_file_names']:
            today = str(datetime.date.today())
            t = today.replace('-', '')
        s = 'USE MASTER RESTORE ' + self.restore_options['restore_type'] + ' [' + self.currdb + '] FROM DISK =' + chr(39) + self.backupfile + chr(39) + ' WITH '
        filecount = 0
        suffix2 = ''
        for x in self.dictfiles.keys():
            if self.dictfiles[x][1] == 'D':
                suffix = 'Data'
                d = self.datafolder
            if self.dictfiles[x][1] == 'L':
                suffix = 'Log'
                d = self.logfolder
                self.logfilename = x
            file, ext = os.path.splitext(os.path.basename(self.dictfiles[x][0]))
            if filecount > 1:           # after 2 values (0 and 1) we add one to the file name
                suffix2 = str(filecount)
            file_renamed = self.currdb + '_' + suffix + suffix2 + t + ext
            s += ' MOVE ' + chr(39) + x + chr(39) + ' TO ' + chr(39) + d + '\\' + file_renamed + chr(39) + ', '
            filecount += 1

        s = s + ' NOUNLOAD, ' + self.restore_options['recovery'] + ', STATS = 10'
        if self.restore_options['replace']:
            s += ', REPLACE'

        self.sqlrestore = s
        if self.noexecute == 1:
            return []

        # Doing the actual restore here. NEW: added code to kill connections in the same execution

        s = 'SET NOCOUNT ON DECLARE @kill varchar(8000) = ' + chr(39) + chr(39) + ';'
        s += ' SELECT @kill = @kill + ' + chr(39) + 'kill ' + chr(39) + '  + CONVERT(varchar(5), spid) + ' + chr(39) + ';' + chr(39)
        s += ' FROM master..sysprocesses WHERE dbid = db_id(' + chr(39) + self.currdb + chr(39) + ')'
        s += ' select @kill; EXEC(@kill); '
        s += self.sqlrestore
        rows, fnames = [], []
        try:
            rows, fnames = SqlExecute(self.conn, s)
        except Exception as inst:
            print('Error restoring database: ', inst)
        return rows

    def BackupDatabase(self):
        if self.backup_options['backup_type'] != 'DIFFERENTIAL':
            s = 'BACKUP ' + self.backup_options['backup_type'] + ' [' + self.currdb + '] TO DISK = ' + chr(39)
            s += self.backupfile + chr(39) + ' WITH NOFORMAT, INIT, NAME = N' + chr(39)
            s += self.currdb + ' ' + self.backup_options['backup_type'] + ' Backup' + chr(39)
        else:
            s = 'BACKUP DATABASE [' + self.currdb + '] TO DISK = ' + chr(39)
            s += self.backupfile + chr(39) + ' WITH DIFFERENTIAL, NOFORMAT, INIT, NAME = N' + chr(39)
            s += self.currdb + ' Differential Backup' + chr(39)
        if self.backup_options['compression']:
            s += ' , SKIP, NOREWIND, NOUNLOAD, COMPRESSION,  STATS = 10'
        else:
            s += ' , SKIP, NOREWIND, NOUNLOAD,  STATS = 10'
        self.sqlbackup = s
        rows, fnames = [], []
        if self.noexecute == 0:
            try:
                rows, fnames = SqlExecute(self.conn, s)
            except Exception as inst:
                print('Error restoring database: ', inst)
        return rows

    # ---------------End of class SQLSMO------------------------------------------

# ===============TEST SECTION=====================================================

if __name__ == '__main__':

    print('find latest backup')
    BACKUPFOLDER = r'C:\SQL2014\BACKUPS'
    lst = GetLatestBackup(BACKUPFOLDER, '\AdventureWorks2012*.bak')

    BKFILE = lst[-1]
    print('Latest backup:')
    print(BKFILE)

    CONNECTION = {}
    SQLSERVER = r'(local)\sql2014'
    CONNECTION['servername'] = SQLSERVER
    CONNECTION['username'] = ''
    CONNECTION['password'] = ''
    CONNECTION['db'] = 'AdventureWorks2012'
    print('Connection dict:', CONNECTION)


    print('testing full backup with dated file name')
    BKFILE = r'C:\SQL2014\BACKUPS\AdventureWorks_Backup_' + DatedString() + '.BAK'
    smo = SQLSMO(CONNECTION, '', '', BKFILE)
    smo.noexecute = 0
    smo.BackupDatabase()
    print('backup script: ', smo.sqlbackup)


    print('testing log backup')
    BKFILE = r'C:\SQL2014\BACKUPS\AdventureWorks_Backup.TRN'
    smo = SQLSMO(CONNECTION, '', '', BKFILE)
    smo.noexecute = 0
    smo.backup_options['backup_type'] = 'LOG'
    smo.BackupDatabase()
    print('backup script: ', smo.sqlbackup)


    print('testing differential backup')
    BKFILE = r'C:\SQL2014\BACKUPS\AdventureWorks_Backup.DIF'
    smo = SQLSMO(CONNECTION, '', '', BKFILE)
    smo.noexecute = 0
    smo.backup_options['backup_type'] = 'DIFFERENTIAL'
    smo.BackupDatabase()
    print('backup script: ', smo.sqlbackup)


    print('testing full backup again')
    BKFILE = r'C:\SQL2014\BACKUPS\AdventureWorks_Backup.BAK'
    smo = SQLSMO(CONNECTION, '', '', BKFILE)
    smo.noexecute = 0
    smo.BackupDatabase()
    print('backup script: ', smo.sqlbackup)

    BKFILE = lst[-1]
    print('Latest backup:')
    print(BKFILE)
    DATAFOLDER = r'C:\SQL2014\DATA'
    LOGFOLDER = r'C:\SQL2014\LOG'
    SQLSERVER = r'(local)\sql2014'

    print('restoring copy of database')
    CONNECTION = {}
    CONNECTION['servername'] = SQLSERVER
    CONNECTION['username'] = ''
    CONNECTION['password'] = ''
    CONNECTION['db'] = 'AdventureWorks2012_COPY_NEW'

    print('Connection dict:', CONNECTION)

    DATAFOLDER = r'C:\SQL2014\DATA'
    LOGFOLDER = r'C:\SQL2014\LOG'

    SQLSERVER = r'(local)\sql2014'

    print('testing restores checking using Ok_to_restore')
    smo = SQLSMO(CONNECTION, DATAFOLDER, LOGFOLDER, BKFILE)

    print('-----Info on existing database----------------------------------')
    rows, fnames = smo.GetDataseInfo()
    print(smo.dictdbinfo)
    print('data size from existing db', smo.datafiles_size_existing)
    print('log size from existing db', smo.logfiles_size_existing)
    print('-----Info on existing database----------------------------------')



    go_ahead = smo.Ok_to_restore()
    if go_ahead:
        print('Ok to restore')
        print('killing connections is now part of the restore call, no need to call the function KillConnections')
        # Default is RECOVERY, put here as reminder you can change to NORECOVERY
        smo.restore_options['recovery'] = 'RECOVERY'
        print('restore options:', smo.restore_options)
        # Now we restore
        rows_restore = smo.RestoreDatabase()

        print('---------------Info on backup files from filelistonly----------------------------')
        for x in smo.dictfiles:
            print(x, smo.dictfiles[x])
        print('---------------Info on backup files from headeronly------------------------------')
        for x in smo.dictheader:
            print(x, smo.dictheader[x])

        print('data size from backup', smo.datafiles_size)
        print('log size from backup', smo.logfiles_size)
        print('rows of restore')
        print(rows_restore)
        print('Lets do some fixes: setting db to simple mode, change dbowner to sa, shrink log, back to full mode')
        s = 'USE [master] ALTER DATABASE [' + smo.currdb + '] SET RECOVERY SIMPLE WITH NO_WAIT '
        s += ' USE ' + smo.currdb + ' EXEC dbo.sp_changedbowner @loginame = ' + chr(39) + 'sa' + chr(39) + ', @map = false '
        s += ' DBCC SHRINKFILE (' + chr(39) + smo.logfilename + chr(39) + ' , 0, TRUNCATEONLY) '
        sqlfixes1 = s + 'ALTER DATABASE [' + smo.currdb + '] SET RECOVERY FULL WITH NO_WAIT '
        print('sql to apply:')
        print(sqlfixes1)
        try:
            rows, fnames = SqlExecute(smo.conn, sqlfixes1)
        except Exception as inst:
            print('Error executing database fixes: ', inst)
        for x in rows:
            print(x)
        print('resync logins')
        rows = SyncLogins(smo.conn, smo.currdb)
        for x in rows:
            print(x)
    else:
        print('Cannot restore')
    print('SQL RESTORE SCRIPT:', smo.sqlrestore)

    print('testing select query')
    conn = {}
    conn['servername'] = '(local)\sql2014'
    conn['username'] = ''
    conn['password'] = ''


    #testing select query
    s = 'set nocount on select top 10 BusinessEntityID, FirstName, MiddleName, \
        LastName, ModifiedDate from AdventureWorks2012.Person.Person'
    rows, fnames = SqlExecute(conn, s)
    print(fnames)
    for x in rows:
        print(x)

Diff to Previous Revision

--- revision 14 2015-03-05 17:12:53
+++ revision 15 2015-03-28 16:58:11
@@ -1,6 +1,6 @@
 # SQLSMO.py
 # Created by Jorge Besada
-# Last modified 3/5/2015
+# Last modified 3/16/2015
 
 import os
 import glob
@@ -75,11 +75,10 @@
     DeleteOlderFiles(r'c:\temp', 3)
     """
 
-    # import os, time
+    # os, time already imported
 
     now = time.time()
     cutoff = now - (days * 86400)
-
     filelist = os.listdir(workfolder)
     for x in filelist:
             if os.path.isfile( workfolder + '\\' + x):
@@ -98,20 +97,16 @@
     """
 
     s = 'SET NOCOUNT ON DECLARE @kill varchar(8000) = ' + chr(39) + chr(39) + ';'
-    s = s + ' SELECT @kill = @kill + ' + chr(39) + 'kill ' + chr(39) + '  + CONVERT(varchar(5), spid) + ' + chr(39) + ';' + chr(39)
-    s = s + ' FROM master..sysprocesses WHERE dbid = db_id(' + chr(39) + db + chr(39) + ')'
-    s = s +  ' select @kill; EXEC(@kill);'
-
-    rows = []
+    s += ' SELECT @kill = @kill + ' + chr(39) + 'kill ' + chr(39) + '  + CONVERT(varchar(5), spid) + ' + chr(39) + ';' + chr(39)
+    s += ' FROM master..sysprocesses WHERE dbid = db_id(' + chr(39) + db + chr(39) + ')'
+    s += ' select @kill; EXEC(@kill);'
+
+    rows, fnames = [], []
     try:
         rows, fnames = SqlExecute(conn, s)
     except Exception as inst:
             print('Error killing connections: ', inst)
     return rows
-
-
-
-
 
 def SyncLogins(conn, db):
     s = '''
@@ -136,12 +131,12 @@
     # sqlcmd needs single line sql commands for the -Q option
     s = s.replace("\n", ' ')
     sqlsynclogins = 'SET NOCOUNT ON USE [' + db + '] ' + s
+    rows, fnames = [], []
     try:
         rows, fnames = SqlExecute(conn, sqlsynclogins)
     except Exception as inst:
         print('Error executing fix logins: ', inst)
     return rows
-
 
 def DatedString():
     """
@@ -157,6 +152,59 @@
     now = now.replace('.', '_') + '0'
 
     return now
+
+
+def BuildTlogSQL(dbname, sourcedb, bkfolder, recovery = 'NORECOVERY', BACKUP_MASK = '_backup_20*.BAK', LOG_MASK = '_backup_20*.TRN'):
+    """
+    - check for log backups
+    DECLARE backupFiles CURSOR FOR
+    SELECT backupFile
+    FROM @fileList
+    WHERE backupFile LIKE '%.TRN'
+    AND backupFile LIKE @dbName + '%'
+    AND backupFile > @lastFullBackup
+    OPEN backupFiles
+    -- Loop through all the files for the database
+    FETCH NEXT FROM backupFiles INTO @backupFile
+    WHILE @@FETCH_STATUS = 0
+    BEGIN
+       SET @cmd = 'RESTORE LOG ' + @dbName + ' FROM DISK = '''
+           + @backupPath + @backupFile + ''' WITH NORECOVERY'
+       PRINT @cmd
+       FETCH NEXT FROM backupFiles INTO @backupFile
+    END
+    CLOSE backupFiles
+    DEALLOCATE backupFiles
+    -- 6 - put database in a useable state
+    SET @cmd = 'RESTORE DATABASE ' + @dbName + ' WITH RECOVERY'
+    PRINT @cmd
+    """
+
+    BKFILE = '____.BAK'
+    try:
+        BKFILE = GetLatestBackup(bkfolder, '\\' + sourcedb + BACKUP_MASK)[-1]   # BACKUP_MASK = '_backup_20*.BAK'
+    except IndexError:
+        pass
+
+    s = ''
+
+    filter = '\\' + sourcedb + LOG_MASK
+    filelist = GetLatestBackup(bkfolder, filter )
+
+    if len(filelist) == 0:
+        print('No log files for ' + sourcedb)
+        if recovery == 'RECOVERY':
+            s = 'RESTORE DATABASE ' + dbname + ' WITH RECOVERY' + chr(13) + chr(10) + 'GO ' + chr(13) + chr(10)
+            return s
+    else:
+        for x in filelist:
+            if x[:-4] > BKFILE[:-4]:        # this means the log file is older than the latest full backup file
+                s += 'RESTORE LOG ' + dbname + ' FROM DISK = ' + chr(39) + x + chr(39) + \
+                    ' WITH NORECOVERY' + chr(13) + chr(10) + 'GO ' + chr(13) + chr(10)
+        if x[:-4] > BKFILE[:-4]:
+            if recovery == 'RECOVERY':
+                s += 'RESTORE DATABASE ' + dbname + ' WITH RECOVERY' + chr(13) + chr(10) + 'GO ' + chr(13) + chr(10)
+        return s
 
 # ----------------End of functions----------------------------------------
 
@@ -172,7 +220,7 @@
         self.backupfile = backupfilepath
         self.currdb = conn['db']
         self.defdb = 'master'
-        self.noexecute = 0      #1=no execute, 0=yes to execute
+        self.noexecute = 0                  #1=no execute, 0=yes to execute
         self.sqlrestore = ''
         self.sqlbackup = ''
 
@@ -192,13 +240,13 @@
         self.datafiles_size = 0             #info from backup file       --- populated by GetBackupInfo
         self.logfiles_size = 0              #info from backup file       --- populated by GetBackupInfo
 
-        self.backup_options =   {'backup_type': 'DATABASE', 'compression': False} #options for backup_type: LOG, DIFFERENTIAL
-        self.restore_options =  {'dated_file_names': False,
+        self.backup_options = {'backup_type': 'DATABASE', 'compression': False} #options for backup_type: LOG, DIFFERENTIAL
+        self.restore_options = {'dated_file_names': False,
                                 'original_file_names': False,
                                 'restore_type': 'DATABASE',                     #options: LOG
                                 'recovery': 'RECOVERY',                         #options: NORECOVERY
                                 'replace': True                                 #option: False
-                                }
+                               }
 
     def GetDataseInfo(self):
         """
@@ -210,9 +258,8 @@
         """
         self.logfiles_size_existing  = 0.0
         self.datafiles_size_existing = 0.0
-        rows = []
-        fnames = []
-
+
+        rows, fnames = [], []
         try:
             # fnames are: name physical_name size data_space_id
             rows, fnames = SqlExecute(self.conn, self.sqlsphelpdb)
@@ -230,12 +277,12 @@
         for x in rows:
             if x[-1] == '0':                    #x[-1] is data_space_id, 0 is log, anything else is data
                 gb = float(x[-2])/1000
-                logsize = logsize + gb
+                logsize += gb
             else:
                 gb = float(x[-2])/1000
-                datasize = datasize + gb
-
-        self.logfiles_size_existing  = logsize
+                datasize += gb
+
+        self.logfiles_size_existing = logsize
         self.datafiles_size_existing = datasize
 
         return rows, fnames
@@ -275,7 +322,7 @@
         BackupType 1
         HasBackupChecksums False
         DifferentialBaseGUID None
-        UserName CARNIVAL\_sql_service
+        UserName DOMAIN\_sql_account
         IsSingleUser False
         ForkPointLSN None
         DifferentialBaseLSN None
@@ -306,19 +353,20 @@
         """
 
         # backup files info
+        rows, fnames = [], []
         try:
             rows, fnames = SqlExecute(self.conn, self.sqlfilelist)
         except Exception as inst:
             print(inst)
             self.dictfiles = {}
 
-        self.logfiles_size  = 0.0
+        self.logfiles_size = 0.0
         self.datafiles_size = 0.0
         for x in rows:
-            self.dictfiles[x[0]] = x[1:]                                                        #using as key file name
+            self.dictfiles[x[0]] = x[1:]                                    #using as key file name
             try:
-                if x[2] == 'L':                                                                 #x[2] is Type, D or L
-                    self.logfiles_size = self.logfiles_size + float(float(x[4])/1000000000)     #x[4] is Size in bytes
+                if x[2] == 'L':                                             #x[2] is Type, D or L
+                    self.logfiles_size += float(float(x[4])/1000000000)     #x[4] is Size in bytes
                     self.logfilename = x[0]
             except IndexError:
                 print('-Check backup file exists')
@@ -326,7 +374,7 @@
                 print('-Check SQL server account has access to the backup file')
                 exit()
             if x[2] == 'D':
-                self.datafiles_size = self.datafiles_size + float(float(x[4])/1000000000)
+                self.datafiles_size += float(float(x[4])/1000000000)
 
         # backup header info
         try:
@@ -338,8 +386,9 @@
         i = 0
         for x in fnames:
             self.dictheader[x] = rows[0][i]
-            if x == 'DatabaseName': self.defdb = self.dictheader[x]
-            i = i + 1
+            if x == 'DatabaseName':
+                self.defdb = self.dictheader[x]
+            i += 1
 
         return self.dictfiles, self.dictheader
 
@@ -348,8 +397,9 @@
         Returns dictionary with drive letters and free space in GB
         C	11.553        D	26.849        E	31.829        F	64.230
         """
+
         self.dictfreespace = {}
-        rows = []
+        rows, fnames = [], []
         try:
             rows, fnames = SqlExecute(self.conn, self.sqlxp_fixeddrives)
         except Exception as inst:
@@ -359,8 +409,8 @@
         i = 0
 
         for x in rows:
-             self.dictfreespace[x[0]] = str(float(rows[i][1])/1000) # changing to gb
-             i = i + 1
+            self.dictfreespace[x[0]] = str(float(rows[i][1])/1000)   # changing to gb
+            i += 1
         return self.dictfreespace
 
     def Ok_to_restore(self):
@@ -377,12 +427,15 @@
         dict_freespace = self.GetFreeSpace()
         # print('freespace dict')
         # print(dict_freespace)
-        data_disk = 0.0; log_disk = 0.0;
+        data_disk = 0.0
+        log_disk = 0.0
         for x in dict_freespace.keys():                             #x is the drive letter
-            if self.datafolder[0] == x:                             #if letter of datafolder matches drive letter, add to data_disk variable
-                data_disk = data_disk + float(dict_freespace[x])
-            if self.logfolder[0] == x:                              #if letter of logfolder matches drive letter, add to log_disk variable
-                log_disk = log_disk + float(dict_freespace[x])
+            if self.datafolder[0].upper() == x.upper():                             #if letter of datafolder matches drive letter, add to data_disk variable
+                data_disk += float(dict_freespace[x])
+            if self.logfolder[0].upper() == x.upper():                              #if letter of logfolder matches drive letter, add to log_disk variable
+                log_disk += float(dict_freespace[x])
+
+
 
         # test prints
         # print('Space in drives in ' + self.conn['servername'])
@@ -393,6 +446,8 @@
         # print('self.logfiles_size from backup file', self.logfiles_size)
         # print('self.datafiles_size_existing', self.datafiles_size_existing)
         # print('self.logfiles_size_existing', self.logfiles_size_existing)
+
+
         # this is for the case the database does not exist:
         # compare disk space with backup file disk space
         # we decide using self.datafiles_size (0 means database does not exist)
@@ -401,29 +456,28 @@
             # print('log and data different drives')
             if self.datafiles_size_existing == 0:       # case no existing database
                 # print('database does not exist, log and data on different drives')
-                if (float(data_disk) > float(self.datafiles_size)) and (float(log_disk) > float(self.logfiles_size) ): #disk space > backup space requiered
+                if (float(data_disk) > float(self.datafiles_size)) and (float(log_disk) > float(self.logfiles_size)):  # disk space > backup space requiered
                     return True
                 else:
                     return False
             else:                                       # case database exists
                 # print('database exists, log and data on different drives')
-                if ( float(data_disk) > (float(self.datafiles_size) - float(self.datafiles_size_existing))) \
-                    and (float(log_disk) > (float(self.logfiles_size) - float(self.logfiles_size_existing)) ):
+                if (float(data_disk) > (float(self.datafiles_size) - float(self.datafiles_size_existing))) \
+                    and (float(log_disk) > (float(self.logfiles_size) - float(self.logfiles_size_existing))):
                     return True
                 else:
                     return False
         else:                                         # case log and data are in same drive
             # print('log and data same drive')
             if self.datafiles_size_existing == 0:                            # no existing database
-                print('database does not exist, log and data on same drive')
-                if (float(data_disk) > (float(self.datafiles_size) + float(self.logfiles_size))):
+                # print('database does not exist, log and data on same drive')
+                if float(data_disk) > (float(self.datafiles_size) + float(self.logfiles_size)):
                     return True
                 else:
                     return False
             else:
-                print('database exists, log and data on same drive')
-                if (    float(data_disk) > (float(self.datafiles_size) + float(self.logfiles_size)) \
-                    -  (float(self.datafiles_size_existing) + float(self.logfiles_size_existing))   ):
+                # print('database exists, log and data on same drive')
+                if float(data_disk > (float(self.datafiles_size) + float(self.logfiles_size)) - (float(self.datafiles_size_existing) + float(self.logfiles_size_existing))):
                     return True
                 else:
                     return False
@@ -440,7 +494,7 @@
         self.GetBackupInfo()
 
         t = ''
-        if self.restore_options['dated_file_names'] == True:
+        if self.restore_options['dated_file_names']:
             today = str(datetime.date.today())
             t = today.replace('-', '')
         s = 'USE MASTER RESTORE ' + self.restore_options['restore_type'] + ' [' + self.currdb + '] FROM DISK =' + chr(39) + self.backupfile + chr(39) + ' WITH '
@@ -455,28 +509,28 @@
                 d = self.logfolder
                 self.logfilename = x
             file, ext = os.path.splitext(os.path.basename(self.dictfiles[x][0]))
-            if filecount > 1:           #after 2 values (0 and 1) we add one to the file name
+            if filecount > 1:           # after 2 values (0 and 1) we add one to the file name
                 suffix2 = str(filecount)
             file_renamed = self.currdb + '_' + suffix + suffix2 + t + ext
-            s = s + ' MOVE ' + chr(39) + x + chr(39) + ' TO ' + chr(39) + d + '\\' + file_renamed + chr(39) + ', '
-            filecount = filecount + 1
+            s += ' MOVE ' + chr(39) + x + chr(39) + ' TO ' + chr(39) + d + '\\' + file_renamed + chr(39) + ', '
+            filecount += 1
 
         s = s + ' NOUNLOAD, ' + self.restore_options['recovery'] + ', STATS = 10'
-        if self.restore_options['replace'] == True:
-            s = s + ', REPLACE'
+        if self.restore_options['replace']:
+            s += ', REPLACE'
 
         self.sqlrestore = s
         if self.noexecute == 1:
             return []
 
-        #Doing the actual restore here. NEW: added code to kill connections in the same execution
+        # Doing the actual restore here. NEW: added code to kill connections in the same execution
 
         s = 'SET NOCOUNT ON DECLARE @kill varchar(8000) = ' + chr(39) + chr(39) + ';'
-        s = s + ' SELECT @kill = @kill + ' + chr(39) + 'kill ' + chr(39) + '  + CONVERT(varchar(5), spid) + ' + chr(39) + ';' + chr(39)
-        s = s + ' FROM master..sysprocesses WHERE dbid = db_id(' + chr(39) + self.currdb + chr(39) + ')'
-        s = s +  ' select @kill; EXEC(@kill); '
-        s = s + self.sqlrestore
-        rows = []
+        s += ' SELECT @kill = @kill + ' + chr(39) + 'kill ' + chr(39) + '  + CONVERT(varchar(5), spid) + ' + chr(39) + ';' + chr(39)
+        s += ' FROM master..sysprocesses WHERE dbid = db_id(' + chr(39) + self.currdb + chr(39) + ')'
+        s += ' select @kill; EXEC(@kill); '
+        s += self.sqlrestore
+        rows, fnames = [], []
         try:
             rows, fnames = SqlExecute(self.conn, s)
         except Exception as inst:
@@ -486,18 +540,18 @@
     def BackupDatabase(self):
         if self.backup_options['backup_type'] != 'DIFFERENTIAL':
             s = 'BACKUP ' + self.backup_options['backup_type'] + ' [' + self.currdb + '] TO DISK = ' + chr(39)
-            s = s + self.backupfile + chr(39) + ' WITH NOFORMAT, INIT, NAME = N' + chr(39)
-            s = s + self.currdb + ' ' + self.backup_options['backup_type'] + ' Backup' + chr(39)
+            s += self.backupfile + chr(39) + ' WITH NOFORMAT, INIT, NAME = N' + chr(39)
+            s += self.currdb + ' ' + self.backup_options['backup_type'] + ' Backup' + chr(39)
         else:
             s = 'BACKUP DATABASE [' + self.currdb + '] TO DISK = ' + chr(39)
-            s = s + self.backupfile + chr(39) + ' WITH DIFFERENTIAL, NOFORMAT, INIT, NAME = N' + chr(39)
-            s = s + self.currdb + ' Differential Backup' + chr(39)
-        if self.backup_options['compression'] == True:
-            s = s + ' , SKIP, NOREWIND, NOUNLOAD, COMPRESSION,  STATS = 10'
+            s += self.backupfile + chr(39) + ' WITH DIFFERENTIAL, NOFORMAT, INIT, NAME = N' + chr(39)
+            s += self.currdb + ' Differential Backup' + chr(39)
+        if self.backup_options['compression']:
+            s += ' , SKIP, NOREWIND, NOUNLOAD, COMPRESSION,  STATS = 10'
         else:
-            s = s + ' , SKIP, NOREWIND, NOUNLOAD,  STATS = 10'
+            s += ' , SKIP, NOREWIND, NOUNLOAD,  STATS = 10'
         self.sqlbackup = s
-        rows = []
+        rows, fnames = [], []
         if self.noexecute == 0:
             try:
                 rows, fnames = SqlExecute(self.conn, s)
@@ -512,7 +566,7 @@
 if __name__ == '__main__':
 
     print('find latest backup')
-    BACKUPFOLDER = r'D:\SQL2014\BACKUPS'
+    BACKUPFOLDER = r'C:\SQL2014\BACKUPS'
     lst = GetLatestBackup(BACKUPFOLDER, '\AdventureWorks2012*.bak')
 
     BKFILE = lst[-1]
@@ -529,7 +583,7 @@
 
 
     print('testing full backup with dated file name')
-    BKFILE = r'D:\SQL2014\BACKUPS\AdventureWorks_Backup_' + DatedString() + '.BAK'
+    BKFILE = r'C:\SQL2014\BACKUPS\AdventureWorks_Backup_' + DatedString() + '.BAK'
     smo = SQLSMO(CONNECTION, '', '', BKFILE)
     smo.noexecute = 0
     smo.BackupDatabase()
@@ -537,7 +591,7 @@
 
 
     print('testing log backup')
-    BKFILE = r'D:\SQL2014\BACKUPS\AdventureWorks_Backup.TRN'
+    BKFILE = r'C:\SQL2014\BACKUPS\AdventureWorks_Backup.TRN'
     smo = SQLSMO(CONNECTION, '', '', BKFILE)
     smo.noexecute = 0
     smo.backup_options['backup_type'] = 'LOG'
@@ -546,7 +600,7 @@
 
 
     print('testing differential backup')
-    BKFILE = r'D:\SQL2014\BACKUPS\AdventureWorks_Backup.DIF'
+    BKFILE = r'C:\SQL2014\BACKUPS\AdventureWorks_Backup.DIF'
     smo = SQLSMO(CONNECTION, '', '', BKFILE)
     smo.noexecute = 0
     smo.backup_options['backup_type'] = 'DIFFERENTIAL'
@@ -555,7 +609,7 @@
 
 
     print('testing full backup again')
-    BKFILE = r'D:\SQL2014\BACKUPS\AdventureWorks_Backup.BAK'
+    BKFILE = r'C:\SQL2014\BACKUPS\AdventureWorks_Backup.BAK'
     smo = SQLSMO(CONNECTION, '', '', BKFILE)
     smo.noexecute = 0
     smo.BackupDatabase()
@@ -564,8 +618,8 @@
     BKFILE = lst[-1]
     print('Latest backup:')
     print(BKFILE)
-    DATAFOLDER = r'D:\SQL2014\DATABASES'
-    LOGFOLDER = r'D:\SQL2014\DATABASES'
+    DATAFOLDER = r'C:\SQL2014\DATA'
+    LOGFOLDER = r'C:\SQL2014\LOG'
     SQLSERVER = r'(local)\sql2014'
 
     print('restoring copy of database')
@@ -577,8 +631,8 @@
 
     print('Connection dict:', CONNECTION)
 
-    DATAFOLDER = r'D:\SQL2014\DATABASES'
-    LOGFOLDER = r'D:\SQL2014\DATABASES'
+    DATAFOLDER = r'C:\SQL2014\DATA'
+    LOGFOLDER = r'C:\SQL2014\LOG'
 
     SQLSERVER = r'(local)\sql2014'
 
@@ -595,7 +649,7 @@
 
 
     go_ahead = smo.Ok_to_restore()
-    if go_ahead == True:
+    if go_ahead:
         print('Ok to restore')
         print('killing connections is now part of the restore call, no need to call the function KillConnections')
         # Default is RECOVERY, put here as reminder you can change to NORECOVERY
@@ -616,10 +670,10 @@
         print('rows of restore')
         print(rows_restore)
         print('Lets do some fixes: setting db to simple mode, change dbowner to sa, shrink log, back to full mode')
-        s =  'USE [master] ALTER DATABASE [' + smo.currdb  + '] SET RECOVERY SIMPLE WITH NO_WAIT '
-        s = s + ' USE ' + smo.currdb  + ' EXEC dbo.sp_changedbowner @loginame = ' + chr(39) + 'sa' + chr(39) + ', @map = false '
-        s = s + ' DBCC SHRINKFILE (' + chr(39) + smo.logfilename + chr(39) +' , 0, TRUNCATEONLY) '
-        sqlfixes1 = s + 'ALTER DATABASE [' + smo.currdb  + '] SET RECOVERY FULL WITH NO_WAIT '
+        s = 'USE [master] ALTER DATABASE [' + smo.currdb + '] SET RECOVERY SIMPLE WITH NO_WAIT '
+        s += ' USE ' + smo.currdb + ' EXEC dbo.sp_changedbowner @loginame = ' + chr(39) + 'sa' + chr(39) + ', @map = false '
+        s += ' DBCC SHRINKFILE (' + chr(39) + smo.logfilename + chr(39) + ' , 0, TRUNCATEONLY) '
+        sqlfixes1 = s + 'ALTER DATABASE [' + smo.currdb + '] SET RECOVERY FULL WITH NO_WAIT '
         print('sql to apply:')
         print(sqlfixes1)
         try:
@@ -644,7 +698,8 @@
 
 
     #testing select query
-    s = 'set nocount on select top 10 BusinessEntityID, FirstName, MiddleName, LastName, ModifiedDate from AdventureWorks2012.Person.Person'
+    s = 'set nocount on select top 10 BusinessEntityID, FirstName, MiddleName, \
+        LastName, ModifiedDate from AdventureWorks2012.Person.Person'
     rows, fnames = SqlExecute(conn, s)
     print(fnames)
     for x in rows:

History