|
- '=======================================================================================================================
- ' 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 <file> - Apply a specific migration file
- ' rollback <file> - 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 <filename>"
- 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 <filename>"
- 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 <file> - Apply a specific migration file"
- WScript.Echo " rollback <file> - 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
|