'======================================================================================================================= ' MIGRATION RUNNER (Standalone VBScript) '======================================================================================================================= ' Runs database migrations directly via VBScript without requiring IIS/ASP. ' ' Usage: ' cscript //nologo scripts\runMigrations.vbs [command] ' ' Commands: ' up - Apply all pending migrations (default) ' down - Rollback the last migration ' status - Show migration status ' apply - Apply a specific migration file ' rollback - Rollback a specific migration file ' ' Examples: ' cscript //nologo scripts\runMigrations.vbs ' cscript //nologo scripts\runMigrations.vbs up ' cscript //nologo scripts\runMigrations.vbs down ' cscript //nologo scripts\runMigrations.vbs status ' Option Explicit Dim fso, scriptDir, projectRoot, webConfigPath, migrationsPath Dim connectionString, command, argument Set fso = CreateObject("Scripting.FileSystemObject") ' Get paths scriptDir = fso.GetParentFolderName(WScript.ScriptFullName) projectRoot = fso.GetAbsolutePathName(scriptDir & "\..") webConfigPath = projectRoot & "\public\web.config" migrationsPath = projectRoot & "\db\migrations" ' Parse arguments command = "up" ' default command If WScript.Arguments.Count > 0 Then command = LCase(WScript.Arguments(0)) End If If WScript.Arguments.Count > 1 Then argument = WScript.Arguments(1) End If ' Validate command Select Case command Case "up", "down", "status", "apply", "rollback" ' Valid command Case Else ShowUsage WScript.Quit 1 End Select ' Load connection string from web.config connectionString = GetConnectionString(webConfigPath) If connectionString = "" Then WScript.Echo "Error: Could not read connection string from web.config" WScript.Quit 1 End If ' Execute the command On Error Resume Next Select Case command Case "up" ApplyAllPending Case "down" RollbackLast Case "status" ShowStatus Case "apply" If argument = "" Then WScript.Echo "Error: No migration file specified" WScript.Echo "Usage: cscript //nologo scripts\runMigrations.vbs apply " WScript.Quit 1 End If ApplyMigration argument Case "rollback" If argument = "" Then WScript.Echo "Error: No migration file specified" WScript.Echo "Usage: cscript //nologo scripts\runMigrations.vbs rollback " WScript.Quit 1 End If RollbackMigration argument End Select If Err.Number <> 0 Then WScript.Echo "" WScript.Echo "ERROR: " & Err.Description WScript.Echo "Number: " & Err.Number WScript.Echo "Source: " & Err.Source WScript.Quit 1 End If WScript.Quit 0 '======================================================================================================================= ' MIGRATION FUNCTIONS '======================================================================================================================= Sub ApplyAllPending() WScript.Echo "==============================================================" WScript.Echo "APPLYING PENDING MIGRATIONS" WScript.Echo "==============================================================" WScript.Echo "" EnsureSchemaMigrationsTable Dim pending, version, versions(), i, j, temp Set pending = GetPendingMigrations() If pending.Count = 0 Then WScript.Echo "No pending migrations." WScript.Echo "" WScript.Echo "==============================================================" WScript.Echo "DONE" WScript.Echo "==============================================================" Exit Sub End If ' Sort versions ReDim versions(pending.Count - 1) i = 0 For Each version In pending.Keys versions(i) = version i = i + 1 Next ' Simple bubble sort (string comparison works for YYYYMMDDHHMMSS format) For i = 0 To UBound(versions) - 1 For j = i + 1 To UBound(versions) If versions(i) > versions(j) Then temp = versions(i) versions(i) = versions(j) versions(j) = temp End If Next Next ' Apply in order For i = 0 To UBound(versions) ApplyMigration pending(versions(i)) Next WScript.Echo "" WScript.Echo "==============================================================" WScript.Echo "DONE" WScript.Echo "==============================================================" End Sub Sub RollbackLast() WScript.Echo "==============================================================" WScript.Echo "ROLLING BACK LAST MIGRATION" WScript.Echo "==============================================================" WScript.Echo "" EnsureSchemaMigrationsTable Dim applied, available, version, lastVersion, filename Set applied = GetAppliedMigrations() If applied.Count = 0 Then WScript.Echo "No migrations to rollback." WScript.Echo "" WScript.Echo "==============================================================" WScript.Echo "DONE" WScript.Echo "==============================================================" Exit Sub End If ' Find the last version (string comparison works for YYYYMMDDHHMMSS format) lastVersion = "" For Each version In applied.Keys If lastVersion = "" Or version > lastVersion Then lastVersion = version End If Next ' Find the filename Set available = GetAvailableMigrations() If available.Exists(lastVersion) Then filename = available(lastVersion) RollbackMigration filename Else WScript.Echo "Error: Migration file not found for version: " & lastVersion WScript.Quit 1 End If WScript.Echo "" WScript.Echo "==============================================================" WScript.Echo "DONE" WScript.Echo "==============================================================" End Sub Sub ShowStatus() WScript.Echo "==============================================================" WScript.Echo "MIGRATION STATUS" WScript.Echo "==============================================================" WScript.Echo "" EnsureSchemaMigrationsTable Dim applied, pending, available, version Set applied = GetAppliedMigrations() Set pending = GetPendingMigrations() Set available = GetAvailableMigrations() WScript.Echo "Applied migrations: " & applied.Count If applied.Count > 0 Then For Each version In applied.Keys If available.Exists(version) Then WScript.Echo " [X] " & available(version) Else WScript.Echo " [X] " & version & " (file not found)" End If Next End If WScript.Echo "" WScript.Echo "Pending migrations: " & pending.Count If pending.Count > 0 Then For Each version In pending.Keys WScript.Echo " [ ] " & pending(version) Next End If WScript.Echo "" WScript.Echo "==============================================================" End Sub Sub ApplyMigration(filename) Dim version version = GetVersionFromFilename(filename) If version = "" Then Err.Raise vbObjectError + 1, "Migrator", "Invalid migration filename format: " & filename End If ' Check if already applied Dim applied Set applied = GetAppliedMigrations() If applied.Exists(version) Then WScript.Echo "Migration " & version & " already applied. Skipping." Exit Sub End If WScript.Echo "Applying migration: " & filename & "..." ' Execute the migration ' NOTE: Access/Jet does NOT support DDL (CREATE TABLE, etc.) inside transactions ' So we run without transaction wrapper and rely on error checking instead Dim conn, migrationSuccess Set conn = GetConnection() On Error Resume Next migrationSuccess = ExecuteMigrationFile(filename, "Up", conn) If Err.Number <> 0 Then WScript.Echo "ERROR: Migration failed - " & Err.Description WScript.Echo "Error Number: " & Err.Number conn.Close Err.Raise vbObjectError + 2, "Migrator", "Migration failed: " & Err.Description End If On Error GoTo 0 ' Check if migration had SQL errors If Not migrationSuccess Then WScript.Echo "" WScript.Echo "ERROR: Migration failed due to SQL errors (see above)." WScript.Echo "Migration NOT recorded. Please fix the migration and try again." conn.Close WScript.Quit 1 End If ' Record the migration On Error Resume Next Dim cmd Set cmd = CreateObject("ADODB.Command") Set cmd.ActiveConnection = conn cmd.CommandText = "INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)" cmd.Execute , Array(version, Now()) If Err.Number <> 0 Then WScript.Echo "ERROR: Failed to record migration - " & Err.Description conn.Close Err.Raise vbObjectError + 3, "Migrator", "Failed to record migration: " & Err.Description End If On Error GoTo 0 conn.Close WScript.Echo "Migration " & version & " applied successfully." End Sub Sub RollbackMigration(filename) Dim version version = GetVersionFromFilename(filename) If version = "" Then Err.Raise vbObjectError + 1, "Migrator", "Invalid migration filename format: " & filename End If ' Check if applied Dim applied Set applied = GetAppliedMigrations() If Not applied.Exists(version) Then WScript.Echo "Migration " & version & " not applied. Skipping." Exit Sub End If WScript.Echo "Rolling back migration: " & filename & "..." ' Execute the migration ' NOTE: Access/Jet does NOT support DDL (DROP TABLE, etc.) inside transactions Dim conn, rollbackSuccess Set conn = GetConnection() On Error Resume Next rollbackSuccess = ExecuteMigrationFile(filename, "Down", conn) If Err.Number <> 0 Then WScript.Echo "ERROR: Rollback failed - " & Err.Description WScript.Echo "Error Number: " & Err.Number conn.Close Err.Raise vbObjectError + 4, "Migrator", "Rollback failed: " & Err.Description End If On Error GoTo 0 ' Check if rollback had SQL errors If Not rollbackSuccess Then WScript.Echo "" WScript.Echo "ERROR: Rollback failed due to SQL errors (see above)." WScript.Echo "Migration record NOT removed. Please fix the issue manually." conn.Close WScript.Quit 1 End If ' Remove migration record On Error Resume Next Dim cmd Set cmd = CreateObject("ADODB.Command") Set cmd.ActiveConnection = conn cmd.CommandText = "DELETE FROM schema_migrations WHERE version = ?" cmd.Execute , Array(version) If Err.Number <> 0 Then WScript.Echo "ERROR: Failed to remove migration record - " & Err.Description conn.Close Err.Raise vbObjectError + 5, "Migrator", "Failed to remove migration record: " & Err.Description End If On Error GoTo 0 conn.Close WScript.Echo "Migration " & version & " rolled back successfully." End Sub '======================================================================================================================= ' HELPER FUNCTIONS '======================================================================================================================= Sub ShowUsage() WScript.Echo "Usage: cscript //nologo scripts\runMigrations.vbs [command]" WScript.Echo "" WScript.Echo "Commands:" WScript.Echo " up - Apply all pending migrations (default)" WScript.Echo " down - Rollback the last migration" WScript.Echo " status - Show migration status" WScript.Echo " apply - Apply a specific migration file" WScript.Echo " rollback - Rollback a specific migration file" WScript.Echo "" WScript.Echo "Examples:" WScript.Echo " cscript //nologo scripts\runMigrations.vbs" WScript.Echo " cscript //nologo scripts\runMigrations.vbs up" WScript.Echo " cscript //nologo scripts\runMigrations.vbs down" WScript.Echo " cscript //nologo scripts\runMigrations.vbs status" End Sub Function GetConnectionString(configPath) If Not fso.FileExists(configPath) Then GetConnectionString = "" Exit Function End If Dim xmlDoc, node Set xmlDoc = CreateObject("Microsoft.XMLDOM") xmlDoc.async = False xmlDoc.load configPath Set node = xmlDoc.selectSingleNode("//appSettings/add[@key='ConnectionString']/@value") If node Is Nothing Then GetConnectionString = "" Else GetConnectionString = node.text End If End Function Function GetConnection() Dim conn Set conn = CreateObject("ADODB.Connection") conn.Open connectionString Set GetConnection = conn End Function Sub EnsureSchemaMigrationsTable() Dim conn, rs Set conn = GetConnection() On Error Resume Next Set rs = conn.Execute("SELECT TOP 1 version FROM schema_migrations") If Err.Number <> 0 Then ' Table doesn't exist, create it Err.Clear On Error GoTo 0 conn.Execute "CREATE TABLE schema_migrations (" & _ "version VARCHAR(14) PRIMARY KEY, " & _ "applied_at DATETIME NOT NULL)" Else If Not rs Is Nothing Then If Not rs.EOF Then rs.Close End If End If conn.Close On Error GoTo 0 End Sub Function GetAppliedMigrations() Dim conn, rs, versions, version Set conn = GetConnection() Set versions = CreateObject("Scripting.Dictionary") Set rs = conn.Execute("SELECT version FROM schema_migrations ORDER BY version") Do While Not rs.EOF version = Trim(rs("version")) versions.Add version, True rs.MoveNext Loop rs.Close conn.Close Set GetAppliedMigrations = versions End Function Function GetAvailableMigrations() Dim folder, files, file, migrations, version Set migrations = CreateObject("Scripting.Dictionary") If Not fso.FolderExists(migrationsPath) Then Set GetAvailableMigrations = migrations Exit Function End If Set folder = fso.GetFolder(migrationsPath) Set files = folder.Files For Each file In files If LCase(fso.GetExtensionName(file.Name)) = "asp" Then version = GetVersionFromFilename(file.Name) If version <> "" Then migrations.Add version, file.Name End If End If Next Set GetAvailableMigrations = migrations End Function Function GetPendingMigrations() Dim applied, available, pending, version Set applied = GetAppliedMigrations() Set available = GetAvailableMigrations() Set pending = CreateObject("Scripting.Dictionary") For Each version In available.Keys If Not applied.Exists(version) Then pending.Add version, available(version) End If Next Set GetPendingMigrations = pending End Function Function GetVersionFromFilename(filename) Dim parts parts = Split(filename, "_") If UBound(parts) >= 0 Then Dim version version = parts(0) ' Validate it's a 14-digit timestamp If Len(version) = 14 And IsNumeric(version) Then GetVersionFromFilename = version Exit Function End If End If GetVersionFromFilename = "" End Function ' Global variable to hold migration context for error checking Dim g_migrationContext Function ExecuteMigrationFile(filename, direction, conn) Dim migrationPath, fileContent, migration migrationPath = migrationsPath & "\" & filename If Not fso.FileExists(migrationPath) Then Err.Raise vbObjectError + 6, "Migrator", "Migration file not found: " & migrationPath End If ' Create migration context Set migration = New MigrationContext Set migration.Connection = conn Set g_migrationContext = migration ' Store globally for error checking ' Read and execute the migration file Dim stream Set stream = fso.OpenTextFile(migrationPath, 1) fileContent = stream.ReadAll stream.Close ' Remove ASP tags fileContent = Replace(fileContent, "<%", "") fileContent = Replace(fileContent, "%>", "") ' Execute the migration code ExecuteGlobal fileContent ' Call the appropriate method If direction = "Up" Then Migration_Up migration ElseIf direction = "Down" Then Migration_Down migration End If ' Return True if successful, False if error ExecuteMigrationFile = Not migration.HasError End Function '======================================================================================================================= ' MIGRATION CONTEXT CLASS '======================================================================================================================= Class MigrationContext Public Connection Public HasError Public LastErrorMessage Public LastErrorNumber Private Sub Class_Initialize() HasError = False LastErrorMessage = "" LastErrorNumber = 0 End Sub Public Sub ExecuteSQL(sql) If HasError Then Exit Sub ' Skip if previous error occurred On Error Resume Next Connection.Execute sql If Err.Number <> 0 Then HasError = True LastErrorNumber = Err.Number LastErrorMessage = "SQL Error: " & Err.Description & " (Error " & Err.Number & ")" & vbCrLf & "SQL: " & sql WScript.Echo " ERROR executing SQL: " & Err.Description WScript.Echo " Error Number: " & Err.Number WScript.Echo " SQL: " & sql Err.Clear End If On Error GoTo 0 End Sub Public Sub CreateTable(tableName, columns) If HasError Then Exit Sub WScript.Echo " Creating table: " & tableName ExecuteSQL "CREATE TABLE " & tableName & " (" & columns & ")" End Sub Public Sub DropTable(tableName) If HasError Then Exit Sub WScript.Echo " Dropping table: " & tableName ExecuteSQL "DROP TABLE " & tableName End Sub Public Sub AddColumn(tableName, columnName, columnType) If HasError Then Exit Sub WScript.Echo " Adding column: " & tableName & "." & columnName ExecuteSQL "ALTER TABLE " & tableName & " ADD COLUMN " & columnName & " " & columnType End Sub Public Sub DropColumn(tableName, columnName) If HasError Then Exit Sub WScript.Echo " Dropping column: " & tableName & "." & columnName ExecuteSQL "ALTER TABLE " & tableName & " DROP COLUMN " & columnName End Sub Public Sub CreateIndex(indexName, tableName, columns) If HasError Then Exit Sub WScript.Echo " Creating index: " & indexName & " on " & tableName ExecuteSQL "CREATE INDEX " & indexName & " ON " & tableName & " (" & columns & ")" End Sub Public Sub DropIndex(indexName, tableName) If HasError Then Exit Sub WScript.Echo " Dropping index: " & indexName & " on " & tableName ExecuteSQL "DROP INDEX " & indexName & " ON " & tableName End Sub Public Function DB() Set DB = Me End Function End Class