Putting It All Together

I expanded on the original decompose.vbs script. The code below is the culmination of more than 50 individual changes and tweaks over 10+ years.

Putting It All Together

My Microsoft Access version control journey began with this StackOverflow answer.  Over the years, I expanded on the original decompose.vbs script.  I wrote about the various enhancements I added in a series of separate blog posts.  Please refer to the individual posts for more information about why I've included each additional feature in the amended decompose.vbs script below.

The code below is the culmination of more than 50 individual changes and tweaks to the original script over a more than ten-year period.  The script is relatively stable now.  I have only made a single change to it over the past four years.

Believe it or not, this script is still not the totality of my Microsoft Access version control process.  I have a separate AutoHotKey script that strips out unneeded lines in the exported text files, converts their encoding to a more diff-friendly format, and auto-reverts any files that are identical save for changes in capitalization.

The code may also include references to items that are unique to my workflow.  For example, there is a procedure in there to RemoveRelinkerLinkedTables.  The Relinker is a form that I include in most of my projects for managing automatic relinking of tables among multiple data environments (production vs. development vs. testing) and/or multiple clients.

Sample Code

Decompose.vbs

' Usage:
'  CScript decompose.vbs <input file> <path> <stubname>

' Converts all modules, classes, forms and macros from an Access Project file (.MDB) <input file> to
' text and saves the results in separate files to <path>.  Requires Microsoft Access.
'

Option Explicit

const acForm = 2
const acModule = 5
const acMacro = 4
const acReport = 3
const acQuery = 1

' BEGIN CODE
Dim fso
Set fso = CreateObject("Scripting.FileSystemObject")
const ForWriting = 2

dim sMDBFilename
If (WScript.Arguments.Count = 0) then
    MsgBox "Please pass name of .mdb file to decompose", vbExclamation, "Error"
    Wscript.Quit()
End if
sMDBFilename = fso.GetAbsolutePathName(WScript.Arguments(0))

Dim sExportpath
If (WScript.Arguments.Count = 1) then
    sExportpath = ""
else
    sExportpath = WScript.Arguments(1)
End If

Dim KeepOpenDict

If WScript.Arguments.Named.Exists("accobj") Or _
   WScript.Arguments.Named.Exists("includetables") Or _
   WScript.Arguments.Named.Exists("cleanstub") Then
    exportModulesTxt sMDBFilename, sExportpath
End If

If (Err <> 0) and (Err.Description <> NULL) Then
    MsgBox Err.Description, vbExclamation, "Error"
    Err.Clear
End If

Function exportModulesTxt(sMDBFilename, sExportpath)
    Dim myComponent
    Dim sModuleType
    Dim sTempname
    Dim sOutstring

    dim myType, myName, myPath
    myType = fso.GetExtensionName(sMDBFilename)
    myName = fso.GetBaseName(sMDBFilename)
    myPath = fso.GetParentFolderName(sMDBFilename)

    If (sExportpath = "") then
        sExportpath = myPath & "\Source\"
    End If
    
    Dim sStubMDBFilename
    If (WScript.Arguments.Count < 3) then
        sStubMDBFilename = sExportpath & myName & "_stub." & myType
    Else
        sStubMDBFileName = WScript.Arguments(2)
    End If

    WScript.Echo "copy stub to " & sStubMDBFilename & "..."
    On Error Resume Next
        fso.CreateFolder(sExportpath)
    On Error Goto 0
    fso.CopyFile sMDBFilename, sStubMDBFilename

    WScript.Echo "starting Access..."
    Dim oApplication
    Set oApplication = CreateObject("Access.Application")
    WScript.Echo "opening " & sStubMDBFilename & " ..."
    oApplication.OpenCurrentDatabase sStubMDBFilename
    Dim db
    Set db = oApplication.CurrentDB
    oApplication.Visible = false

    KeepOpen db
    
    If WScript.Arguments.Named.Exists("accobj") Then

        dim dctDelete
        Set dctDelete = CreateObject("Scripting.Dictionary")
        WScript.Echo "exporting..."
        Dim myObj
        On Error Resume Next
        WScript.Echo oApplication.CurrentProject.AllForms.Count
        For Each myObj In oApplication.CurrentProject.AllForms        
            WScript.Echo "Forms:    " & myObj.name
            Dim IsTempFrm
            IsTempFrm = False
            IsTempFrm = (db.Containers("Forms").Documents(myObj.Name).Properties("Description") = "temp form created by clsSQL") 
            Err.Clear
            If Not IsTempFrm Then oApplication.SaveAsText acForm, myObj.Name, sExportpath & "\" & myObj.Name & ".form"
            If Err Then         
                LogError sExportPath, "Form: " & myObj.Name
                Err.Clear
            Else
                oApplication.DoCmd.Close acForm, myObj.Name
                dctDelete.Add "FO" & myObj.Name, acForm     
            End If
        Next
        WScript.Echo db.QueryDefs.Count
        Dim f
        For Each myObj in db.QueryDefs
            If Not Left(myObj.Name,4) = "~sq_" Then         
                WScript.Echo "Queries:  " & myObj.Name
                Dim IsTempQry
                IsTempQry = False
                IsTempQry = (db.QueryDefs(myObj.Name).Properties("Description") = "temp query created by clsSQL") Or _
                            (db.QueryDefs(myObj.Name).Properties("Description") = "temp query created by clsExcel")                        
                Err.Clear
                If Not IsTempQry Then
                    'save SQL to text file
                    set f = fso.CreateTextFile(sExportpath & "\" & myObj.Name & ".sql", True)
                    Dim sqlTxt
                    If Len(myObj.Connect)>0 Then
                        sqlTxt = myObj.SQL
                    Else
                        sqlTxt = AddLineBreaks(myObj.SQL)
                    End If
                    f.WriteLine(sqlTxt)
                    f.Close
                    'save query object
                    oApplication.SaveAsText acQuery, myObj.Name, sExportpath & "\" & myObj.Name & ".qry"
                End If
                If Err Then         
                    LogError sExportPath, "Query: " & myObj.Name
                    Err.Clear
                Else
                    oApplication.DoCmd.Close acQuery, myObj.Name        
                    dctDelete.Add "QU" & myObj.Name, acQuery
                End If            
            End If
        Next
        set f = Nothing
        For Each myObj In oApplication.CurrentProject.AllModules
            WScript.Echo "Modules:  " & myObj.Name
            oApplication.SaveAsText acModule, myObj.Name, sExportpath & "\" & myObj.Name & ".bas"
            dctDelete.Add "MO" & myObj.Name, acModule
        Next
        For Each myObj In oApplication.CurrentProject.AllMacros
            WScript.Echo "Macros:   " & myObj.Name
            oApplication.SaveAsText acMacro, myObj.Name, sExportpath & "\" & myObj.Name & ".mac"
            dctDelete.Add "MA" & myObj.Name, acMacro
        Next
        For Each myObj In oApplication.CurrentProject.AllReports
            Err.Clear
            WScript.Echo "Reports:  " & myObj.Name
            oApplication.SaveAsText acReport, myObj.Name, sExportpath & "\" & myObj.Name & ".report"
            If Err Then
                LogError sExportPath, "Report: " & myObj.Name
                Err.Clear
            Else
                dctDelete.Add "RE" & myObj.Name, acReport
            End If
        Next
        For Each myObj in oApplication.VBE.ActiveVBProject.VBComponents
            If myObj.Type = 3 Then  ' UserForm
                WScript.Echo "User forms:  " & myObj.Name
                myObj.Export sExportPath & "\" & myObj.Name & ".userform"
            End If
        Next
        
        ExportCSVs db, sExportPath, oApplication
        ExportRefs sExportPath, oApplication
        ExportProps db, sExportPath
    End If
    
    If WScript.Arguments.Named.Exists("includetables") Then
        ExportTables db, sExportPath, oApplication
        ExportRelations db, sExportPath
        ExportIcons sExportPath, oApplication
    End If

    If WScript.Arguments.Named.Exists("cleanstub") Then
        WScript.Echo "deleting..."
        dim sObjectname
        On Error Resume Next
        For Each sObjectname In dctDelete
            WScript.Echo "  " & Mid(sObjectname, 3)
            oApplication.DoCmd.DeleteObject dctDelete(sObjectname), Mid(sObjectname, 3)
            If Err Then
                LogError sExportPath, "Error deleting: " & Mid(sObjectname, 3)
                Err.Clear
            End If
        Next
        
        WScript.Echo "clearing temporary tables..."
        ClearTempTables db
        RemoveRelinkerLinkedTables db, sExportPath

        On Error Goto 0
    End If
    
    Dim rsKeepOpen
    For Each rsKeepOpen In KeepOpenDict.Items
        rsKeepOpen.Close
    Next
    oApplication.CloseCurrentDatabase
    oApplication.Quit
    
End Function

Public Sub ClearTempTables(db)
Dim td, recs
    For Each td In db.TableDefs
        'Get local, non-system tables...
        If Not Left(td.Name, 4) = "MSys" And td.Connect = "" Then
            '...excluding these ones...
            If td.Name <> "YrMos" Then
                '...with lots of records
                recs = db.OpenRecordset("SELECT Count(*) AS Num FROM [" & td.Name & "]")("Num")
                If recs > 500 Then
                    WScript.Echo "  " & td.Name & "  " & recs
                    'And clear them out
                    db.Execute "DELETE * FROM [" & td.Name & "]"
                End If
            End If
        End If
    Next
End Sub

Public Sub RemoveRelinkerLinkedTables(db, sExportPath)
    On Error Resume Next
    Dim rsLocs, rsTbls
    Set rsLocs = db.OpenRecordset("SELECT LocID FROM eLocations")
    Set rsTbls = db.OpenRecordset("SELECT LocalTblName FROM eSyncTables")
    If Err Then 
        'MsgBox Err.Number & ": " & Err.Description
        'LogError sExportPath, "rsLocs/rsTbls"
        Exit Sub
    End If
    
    Dim TblName
    Do Until rsTbls.EOF
        TblName = rsTbls.Fields("LocalTblName").Value
        db.TableDefs.Delete TblName
        If Err Then
            'LogError sExportPath, TblName
            Err.Clear
        End If
        Do Until rsLocs.EOF
            TblName = "_" & rsLocs.Fields("LocID").Value & "_" & rsTbls.Fields("LocalTblName").Value
            db.TableDefs.Delete TblName
            rsLocs.MoveNext
            If Err Then 
                'LogError sExportPath, TblName
                Err.Clear
            End If
        Loop
        rsLocs.MoveFirst
        rsTbls.MoveNext
    Loop
End Sub

Public Sub ExportTables(db, ExportPath, oApp)
Dim td, objFSO, f, prop, fld, ix, x, TableList
    'On Error Resume Next
    Set objFSO = CreateObject("Scripting.FileSystemObject")
    Set TableList = objFSO.OpenTextFile(ExportPath & "\_TableList.table", 8, True)
    For Each td In db.TableDefs
        Err.Clear
        'Exclude system tables and hidden tables (i.e., Relinker tables)
        Dim acTable
        acTable = 0        
        If Not Left(td.Name, 4) = "MSys" And Not oApp.GetHiddenAttribute(acTable, td.Name) Then
            On Error Resume Next
            WScript.Echo "Tables:   " & td.Name & " (" & td.Fields.Count & " fields, " & td.Indexes.Count & " indexes)"
            If Err Then
                'LogError ExportPath, "Error exporting table: " & td.Name
                Err.Clear
            End If
            On Error Goto 0
            If UCase(td.Name) = "AUX" Then
                Set f = objFSO.OpenTextFile(ExportPath & "\" & td.Name & "_.table", 8, True)
            Else
                Set f = objFSO.OpenTextFile(ExportPath & "\" & td.Name & ".table", 8, True)
            End If

            'Write connect strings to a separate file to minimize the number of modified text files when switching db's
            TableList.WriteLine td.Name & "    Connect: " & td.Connect
            f.WriteLine "Table Properties"
            f.WriteLine "----------------"
            On Error Resume Next
            f.WriteLine "    Description: " & td.Properties("Description").Value
            On Error Goto 0
            f.WriteLine "    Attributes: " & td.Attributes
            f.WriteLine "    SourceTableName: " & td.SourceTableName
            
            f.WriteLine ""
            f.WriteLine "Fields"
            f.WriteLine "------"
            For Each fld In td.Fields
                f.WriteLine fld.Name & " [" & DbTypeToString(fld.Type) & "] (" & fld.Size & ")"
                On Error Resume Next
                f.WriteLine "    Description: " & fld.Properties("Description").Value
                On Error Goto 0
                f.WriteLine "    Attributes: " & fld.Attributes
                f.WriteLine "    Ordinal Position: "  & fld.OrdinalPosition
                f.WriteLine "    Default Value: "  & fld.DefaultValue
                f.WriteLine "    Required: "  & fld.Required
                f.WriteLine "    AllowZeroLength: "  & fld.AllowZeroLength
            Next

            f.WriteLine ""
            f.WriteLine "Indexes"
            f.WriteLine "-------"
            On Error Resume Next
            For Each ix In td.Indexes
                f.WriteLine ix.Name
                For Each fld In ix.Fields
                    f.WriteLine "  " & fld.Name & " {Attributes: " & fld.Attributes & "}"
                Next
                For Each prop In ix.Properties
                    If prop.Name <> "DistinctCount" Then
                        f.WriteLine "    " & prop.Name & ": " & prop.Value
                    End If
                Next
                f.WriteLine ""
            Next
            On Error Goto 0
            f.Close
            Set f = Nothing
        End If
        If Err Then 
            LogError ExportPath, "Error exporting: " & td.Name
            Err.Clear
        End If 
    Next
    Set objFSO = Nothing
End Sub

Public Sub ExportRelations(db, ExportPath)
Dim objFSO, f, fld
Dim rel, tbl, ftbl

    Set objFSO = CreateObject("Scripting.FileSystemObject")
    Set f = objFSO.OpenTextFile(ExportPath & "\_RelationList.table", 8, True)
    For Each rel In db.Relations
        WScript.Echo "Relations: " & rel.Name
        f.WriteLine rel.Name
        f.WriteLine "-------------------"
        tbl = rel.Table
        ftbl = rel.ForeignTable
        For Each fld In rel.Fields
            f.WriteLine tbl & "." & fld.Name & " --> " & ftbl & "." & fld.ForeignName            
        Next 'fld
        f.WriteLine "---== Attributes ==---"
        if (rel.Attributes And 1) > 0 then f.WriteLine "dbRelationUnique: The relationship is one-to-one."
        if (rel.Attributes And 2) > 0 then f.WriteLine "dbRelationDontEnforce: The relationship isn't enforced (no referential integrity)."
        if (rel.Attributes And 4) > 0 then f.WriteLine "dbRelationInherited: The relationship exists in a non-current database that contains the two linked tables."
        if (rel.Attributes And 256) > 0 then f.WriteLine "dbRelationUpdateCascade: Updates will cascade."
        if (rel.Attributes And 4096) > 0 then f.WriteLine "dbRelationDeleteCascade: Deletions will cascade."
        if (rel.Attributes And 16777216) > 0 then f.WriteLine "dbRelationLeft: Microsoft Access only. In Design view, display a LEFT JOIN as the default join type."
        if (rel.Attributes And 33554432) > 0 then f.WriteLine "dbRelationRight: Microsoft Access only. In Design view, display a RIGHT JOIN as the default join type."
        f.WriteLine ""
    Next 'rel
    f.Close
    Set f = Nothing
    Set objFSO = Nothing
End Sub

Public Sub ExportCSVs(db, ExportPath, oApp)
Const acExportDelim = 2
Dim Tbls, TblName, QDef, QDefName
    On Error Resume Next
    
    QDefName = "___TempCsvExportQDef___"
    Tbls = Split(db.Properties("hg_csv"), ",")
    If Err = 0 Then
        For Each TblName In Tbls
            WScript.Echo "CSV:   " & Trim(TblName)
            Dim ColumnNames
            ColumnNames = ColNames(db, Trim(TblName), True)
            Set QDef = db.CreateQueryDef(QDefName, "SELECT * FROM " & Trim(TblName) & " ORDER BY " & ColumnNames)
            oApp.DoCmd.TransferText acExportDelim, , QDef.Name, ExportPath & "\" & Trim(TblName) & ".csv", True
            db.QueryDefs.Delete QDef.Name
            Set QDef = Nothing
            If Err <> 0 Then
                WScript.Echo "  error " & Err & ": " & Err.Description & " {" & ColumnNames & "}"
                Err.Clear
            End If
        Next
    Else
        Err.Clear
    End If
    For Each TblName In Array("MSysIMEXSpecs", "MSysIMEXColumns")
        WScript.Echo "CSV:   " & Trim(TblName)
        Set QDef = db.CreateQueryDef(QDefName, "SELECT * FROM " & Trim(TblName) & " ORDER BY " & ColNames(db, Trim(TblName), True))
        oApp.DoCmd.TransferText acExportDelim, , QDef.Name, ExportPath & "\" & Trim(TblName) & ".csv", True
        db.QueryDefs.Delete QDef.Name
        Set QDef = Nothing
    Next
End Sub

Public Function ColNames(db, TblName, SortableOnly)
    Dim i, td
    On Error Resume Next
    Set td = db.TableDefs(TblName)
    If td Is Nothing Then Set td = db.QueryDefs(TblName)
    ColNames = td.Fields(0).Name
    For i = 1 To td.Fields.Count - 1
        Dim isSortable
        Select Case td.Fields(i).Type
        Case 11, 12: isSortable = False  ' Memo, OLE, and Hyperlink fields are not sortable
        Case Else: isSortable = True
        End Select
        
        If isSortable Or Not SortableOnly Then
            ColNames = ColNames & ", " & td.Fields(i).Name
        End If
    Next
End Function

Public Function AddLineBreaks(Txt) 'As String
    Dim s
    s = Txt
    s = Replace(s, " INNER JOIN ",  vbCrLf & " INNER JOIN ")
    s = Replace(s, " LEFT JOIN ",  vbCrLf & " LEFT JOIN ")
    s = Replace(s, " ON ", vbCrLf & " ON ")
    s = Replace(s, " AND ", vbCrLf & "  AND ")
    s = Replace(s, " OR ", vbCrLf & " OR ")
    s = Replace(s, ", ", "," & vbCrLf & " ")
    AddLineBreaks = s
End Function

Public Sub ExportRefs(ExportPath, oApp)
Dim ref, Refs, objFSO, i
    Set objFSO = CreateObject("Scripting.FileSystemObject")
    
    Set Refs = objFSO.CreateTextFile(ExportPath & "\References.txt", True)
	'Refs.WriteLine oApp.VBE.ActiveVBProject.References.Count & " references found"
    Refs.WriteLine Join(Array("Name", "Major", "Minor", "Type", "Builtin", "Broken", "GUID", "Description"), vbTab)
	On Error Resume Next
    For Each ref In oApp.VBE.ActiveVBProject.References
		i = i + 1
		'Refs.WriteLine "Reference " & i
        Refs.WriteLine Join(Array(ref.Name, ref.Major, ref.Minor, ref.Type, ref.Builtin, ref.IsBroken, ref.GUID, ref.Description), vbTab)
		If err.number <> 0 then
			Refs.WriteLine err.Description
			err.Clear
		End If
	Next
    Refs.Close
    Set Refs=Nothing
End Sub

Public Sub ExportProps(db, ExportPath)
Dim i, Props, objFSO, Prop
    Set objFSO = CreateObject("Scripting.FileSystemObject")
    
    Set Props = objFSO.CreateTextFile(ExportPath & "\DbProperties.txt", True)
    Props.WriteLine Join(Array("Index", "Name                           ", "Value"), vbTab)
    On Error Resume Next
    For i = 0 To db.Properties.Count - 1
        Set Prop = db.Properties(i)
        Props.WriteLine Join(Array(i, Left(Prop.Name & "                               ", 31), Prop.Value), vbTab)
    Next
    On Error Goto 0
    Props.Close
    Set Props=Nothing
End Sub

Public Sub ExportIcons(ExportPath, oApp)
    On Error Resume Next
    oApp.Run "ExportIcons", ExportPath & "eIcons"    
End Sub

Public Sub LogError(sExportPath, sNote)
Dim objFso, objFile, filLog, PATH
    Set objFso = CreateObject("Scripting.FileSystemObject")
    Set objFile = objFso.OpenTextFile(sExportPath & "_Errors.Log", 8, True)
    objFile.WriteLine Date() & " {" & sNote & "} " & Err.Number & ": " & Err.Description
    objFile.Close
    Set objFso = Nothing
    Set objFile = Nothing
End Sub

Private Function DbTypeToString(DbType)
    Select Case DbType
    Case 16: DbTypeToString = "Big Int"
    Case  9: DbTypeToString = "Binary"
    Case  1: DbTypeToString = "Boolean"
    Case  2: DbTypeToString = "Byte"
    Case 18: DbTypeToString = "Char"
    Case  5: DbTypeToString = "Currency"
    Case  8: DbTypeToString = "Date"
    Case 20: DbTypeToString = "Decimal"
    Case  7: DbTypeToString = "Double"
    Case 21: DbTypeToString = "Float"
    Case 15: DbTypeToString = "GUID"
    Case  3: DbTypeToString = "Integer"
    Case  4: DbTypeToString = "Long"
    Case 11: DbTypeToString = "Long Binary"
    Case 12: DbTypeToString = "Memo"
    Case 19: DbTypeToString = "Numeric"
    Case  6: DbTypeToString = "Single"
    Case 10: DbTypeToString = "Text"
    Case 22: DbTypeToString = "Time"
    Case 23: DbTypeToString = "TimeStamp"
    Case 17: DbTypeToString = "VarBinary"
    End Select
End Function

Private Function KeepOpen(db) 'As Collection
Const dbOpenDynaset = 2, dbOpenForwardOnly = 8
Dim dbNames, rsKeepOpen, rsDBList, DbName
    Set KeepOpenDict = CreateObject("Scripting.Dictionary")
    Set rsDBList = db.OpenRecordset("SELECT DISTINCT Database FROM MSysObjects WHERE Database Like '*.*db'", dbOpenForwardOnly)
    With rsDBList
        Do Until .EOF
            DbName = .Fields("Database").Value
            if fso.FileExists(DbName) Then 
                dbNames = dbNames & DbName & "|"
                Set rsKeepOpen = db.OpenRecordset("SELECT TOP 1 Database FROM MSysObjects", dbOpenDynaset)
                KeepOpenDict.Add DbName, rsKeepOpen
            End If
            .MoveNext
        Loop
    End With
    'MsgBox dbNames
    
End Function

All original code samples by Mike Wolfe are licensed under CC BY 4.0